diff --git a/packages/persona-kit/schemas/agent.schema.json b/packages/persona-kit/schemas/agent.schema.json index eca01204..51bb0df6 100644 --- a/packages/persona-kit/schemas/agent.schema.json +++ b/packages/persona-kit/schemas/agent.schema.json @@ -48,12 +48,17 @@ }, "where": { "type": "string" + }, + "maxConcurrency": { + "type": "integer", + "description": "Optional delivery backpressure hint for this trigger. Positive integers are preserved by the parser; invalid values are treated as unset so older or malformed specs do not block delivery.", + "minimum": 1 } }, "required": [ "on" ], - "description": "A single event trigger declared by an **agent** (`agent.ts`). `on` is a Relayfile-adapter-normalized event name (e.g. `pull_request.opened`, `issue.create`, `message.created`). `match` and `where` are filter sugars the deploy CLI lints against a known registry; unknown values warn but do not fail parse, so the cloud runtime stays the source of truth.\n\nTriggers live on the agent ( {@link AgentSpec.triggers } ), not the persona — the persona only declares which providers it connects to ( {@link PersonaIntegrationConfig } ). The deploy CLI joins the two: events from `agent.triggers[provider]`, connection/scope from `persona.integrations[provider]`.\n\nExamples: { on: \"pull_request.opened\" } { on: \"pull_request_review_comment.created\", match: \"@mention\" } { on: \"check_run.completed\", where: \"conclusion=failure\" }" + "description": "A single event trigger declared by an **agent** (`agent.ts`). `on` is a Relayfile-adapter-normalized event name (e.g. `pull_request.opened`, `issue.create`, `message.created`). `match` and `where` are filter sugars the deploy CLI lints against a known registry; unknown values warn but do not fail parse, so the cloud runtime stays the source of truth.\n\nTriggers live on the agent ( {@link AgentSpec.triggers } ), not the persona — the persona only declares which providers it connects to ( {@link PersonaIntegrationConfig } ). The deploy CLI joins the two: events from `agent.triggers[provider]`, connection/scope from `persona.integrations[provider]`.\n\nExamples: { on: \"pull_request.opened\" } { on: \"pull_request_review_comment.created\", match: \"@mention\" } { on: \"check_run.completed\", where: \"conclusion=failure\" } { on: \"message.created\", maxConcurrency: 1 }" }, "PersonaSchedule": { "type": "object", diff --git a/packages/persona-kit/scripts/emit-schema.mjs b/packages/persona-kit/scripts/emit-schema.mjs index 87856146..a046f28e 100644 --- a/packages/persona-kit/scripts/emit-schema.mjs +++ b/packages/persona-kit/scripts/emit-schema.mjs @@ -134,6 +134,18 @@ const agentSchema = agentGenerator.createSchema('AgentSpec'); agentSchema.$schema = 'https://json-schema.org/draft/2020-12/schema'; agentSchema.$id = 'https://agentworkforce.dev/schemas/agent.schema.json'; +const agentTrigger = agentSchema.definitions?.PersonaIntegrationTrigger; +if ( + agentTrigger && + agentTrigger.properties?.maxConcurrency?.type === 'number' +) { + agentTrigger.properties.maxConcurrency = { + ...agentTrigger.properties.maxConcurrency, + type: 'integer', + minimum: 1 + }; +} + const agentSerialized = `${JSON.stringify(agentSchema, null, 2)}\n`; let existingAgent = ''; try { diff --git a/packages/persona-kit/src/define.test.ts b/packages/persona-kit/src/define.test.ts index 4addb54f..f883471a 100644 --- a/packages/persona-kit/src/define.test.ts +++ b/packages/persona-kit/src/define.test.ts @@ -67,10 +67,11 @@ test('TypedTriggerMap gives per-provider event autocomplete; arbitrary providers { on: 'AgentSessionEvent.prompted' }, { on: 'AppUserNotification.issueCommentMention' } ], - slack: [{ on: 'message.created' }], + slack: [{ on: 'message.created', maxConcurrency: 1 }], customProvider: [{ on: 'custom.event' }] }; assert.equal(triggers.github?.[1]?.on, 'off_registry.github_event'); + assert.equal(triggers.slack?.[0]?.maxConcurrency, 1); assert.equal(triggers.customProvider?.[0]?.on, 'custom.event'); }); diff --git a/packages/persona-kit/src/define.ts b/packages/persona-kit/src/define.ts index 038c276d..76eb2adb 100644 --- a/packages/persona-kit/src/define.ts +++ b/packages/persona-kit/src/define.ts @@ -23,6 +23,7 @@ export interface TypedTrigger
{ on: TriggerNameFor
;
match?: string;
where?: string;
+ maxConcurrency?: number;
}
/**
diff --git a/packages/persona-kit/src/emit-schema.test.ts b/packages/persona-kit/src/emit-schema.test.ts
index ff39004f..4c419ef3 100644
--- a/packages/persona-kit/src/emit-schema.test.ts
+++ b/packages/persona-kit/src/emit-schema.test.ts
@@ -138,6 +138,24 @@ test('agent schema exposes launchedBy/triggers/schedules/watch', async () => {
assert.equal(watchRule.properties?.paths && watchRule.properties.paths !== true
? watchRule.properties.paths.type
: undefined, 'array');
+ const trigger = definitions.PersonaIntegrationTrigger;
+ const maxConcurrency = trigger.properties?.maxConcurrency;
+ assert.equal(maxConcurrency && maxConcurrency !== true
+ ? maxConcurrency.type
+ : undefined, 'integer');
+ assert.equal(maxConcurrency && maxConcurrency !== true
+ ? maxConcurrency.minimum
+ : undefined, 1);
+
+ assertSchema({ triggers: { slack: [{ on: 'message.created', maxConcurrency: 1 }] } }, schema, schema, 'agent');
+ assert.throws(
+ () => assertSchema({ triggers: { slack: [{ on: 'message.created', maxConcurrency: 0 }] } }, schema, schema, 'agent'),
+ /agent\.triggers\.slack\[0\]\.maxConcurrency must be >= 1/
+ );
+ assert.throws(
+ () => assertSchema({ triggers: { slack: [{ on: 'message.created', maxConcurrency: 1.5 }] } }, schema, schema, 'agent'),
+ /agent\.triggers\.slack\[0\]\.maxConcurrency must be integer/
+ );
});
type SchemaNode = Record