@jr2/agent-protocol 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jr2/agent-protocol",
3
- "version": "0.3.0",
3
+ "version": "0.5.1",
4
4
  "description": "The jr2 Orchestrator↔Agent wire: defineEvent and nothing else.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -43,6 +43,6 @@
43
43
  ],
44
44
  "scripts": {
45
45
  "typecheck": "tsc --noEmit",
46
- "test": "node --test"
46
+ "test": "node --test --test-global-setup=../../scripts/test-tmpdir.ts"
47
47
  }
48
48
  }
@@ -8,35 +8,69 @@
8
8
 
9
9
  import { z } from "zod";
10
10
 
11
+ // How delivering the event resolves for the caller (ADR-0006):
12
+ // - `ack` the pick is delivered and answered. A pick that moves the workflow to work inside the
13
+ // Agent's state is answered when that work is done (ADR-0024): the answer is the Machine's,
14
+ // never a schema on the pick, so no pick declares an output.
15
+ // - `poll` the caller pulls queued down-channel messages (cooperative steer checkpoint). Reserved,
16
+ // not built (ADR-0013).
11
17
  /**
12
- * How delivering the event resolves for the caller (ADR-0006):
13
- * - `ack` fire-and-acknowledge; the caller does not wait on a meaningful result.
14
- * - `deferred` the result is held open until the Machine answers (solicited down-channel).
15
- * - `poll` the caller pulls queued down-channel messages (cooperative steer checkpoint).
18
+ * How the party that sends an event gets its answer.
19
+ *
20
+ * - `"ack"`, the default: jr2 delivers the event and answers the sender. When the event starts
21
+ * work inside the Agent's state, the answer comes when that work is done.
22
+ * - `"poll"`: reserved. `createMachine` refuses it: ``machine "<id>": event "<name>" is `poll`,
23
+ * which is reserved but NOT IMPLEMENTED (ADR-0013). Only `ack` events can be delivered today;
24
+ * nothing drains a steer inbox, and degrading it to `ack` would hand the Agent a lying tool
25
+ * contract.``
26
+ *
27
+ * @see @jr2/orchestrator/docs/concepts/the-menu.md
28
+ * @see @jr2/orchestrator/docs/reference/decisions/0006-agent-control-surface.md
16
29
  */
17
- export type EventSemantics = "ack" | "deferred" | "poll";
30
+ export type EventSemantics = "ack" | "poll";
18
31
 
32
+ // Stated BY KIND (ADR-0015). Unset, each writing — each `on` key — belongs to the party that its
33
+ // state, or the nearest state above it, invokes (an Agent slot, a `gate`, or both); with none
34
+ // above, both. Set it only to give a pick to the other party (`submit: "any"` lets an in-Turn
35
+ // Gate's caller send the Agent's pick) or to hold a root pick back (`report_blocked: "agent"`;
36
+ // `approve: "external"` guarantees no Agent state can offer it).
19
37
  /**
20
- * Who may deliver the event (ADR-0015). The invoking actor kind is the primary router — an
21
- * an Agent's menu draws audience ∈ {agent, any}, a `gate` draws {external, any} — so this tag
22
- * exists to RESTRICT: tag the security-sensitive events (`approve: "external"` guarantees no
23
- * agent state can ever offer it) and leave the rest at the `"any"` default.
38
+ * Who may send an event: `"agent"` (an Agent, as a pick from its Menu), `"external"` (the caller of
39
+ * a Gate), or `"any"` (both).
40
+ *
41
+ * Leave it unset in most cases. Then the position of each `on` key decides: the event belongs to
42
+ * the party that its state, or the nearest state above it, invokes. An `on` key on a state that
43
+ * invokes no party, with no such state above it, belongs to both. Set an audience only to give an
44
+ * event to the other party too (`"any"`). Or set it to keep an event on the root from one party
45
+ * (`"agent"` or `"external"`).
46
+ *
47
+ * @see @jr2/orchestrator/docs/concepts/the-menu.md
48
+ * @see @jr2/orchestrator/docs/concepts/gates.md
49
+ * @see @jr2/orchestrator/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md
24
50
  */
25
51
  export type EventAudience = "agent" | "external" | "any";
26
52
 
53
+ // `name` doubles as the MCP tool name (agent side) and the xstate event `type` (Machine side).
27
54
  /**
28
- * A workflow-defined control event: pure data (plus zod schemas), safe to import anywhere.
29
- * `name` doubles as the MCP tool name (agent side) and the xstate event `type` (Machine side).
55
+ * A workflow event, as `defineEvent` gives it back: plain, frozen data and a zod schema, safe to
56
+ * import anywhere. Give it to `jr2Setup({ events })`. A Machine accepts only the events in that
57
+ * list.
58
+ *
59
+ * @see @jr2/orchestrator/docs/guides/write-a-workflow.md
60
+ * @see @jr2/orchestrator/docs/reference/decisions/0011-workflow-defined-events.md
30
61
  */
31
62
  export type EventDef<Name extends string = string, Input extends z.ZodObject = z.ZodObject> = {
63
+ /** The event's name: its xstate event `type`, and the name of its tool on the Agent's Menu. */
32
64
  readonly name: Name;
65
+ /** What the event means. An Agent's model reads it as the tool's description. */
33
66
  readonly description?: string;
34
- /** Input schema — a flat tagged object, never a `oneOf` (ADR-0006 encoding rule). */
67
+ // The ADR-0006 encoding rule: a flat tagged object, never a `oneOf`.
68
+ /** The schema of the event's payload: a flat `z.object`, never a union. */
35
69
  readonly input: Input;
70
+ /** How the sender gets its answer: always `"ack"` on an event that `createMachine` accepts. */
36
71
  readonly semantics: EventSemantics;
37
- readonly audience: EventAudience;
38
- /** Result schema for `deferred` events (the held tool result the Machine answers with). */
39
- readonly output?: z.ZodType;
72
+ /** Who may send the event. Absent: the state of each `on` key decides (see {@link EventAudience}). */
73
+ readonly audience?: EventAudience;
40
74
  };
41
75
 
42
76
  /**
@@ -44,6 +78,8 @@ export type EventDef<Name extends string = string, Input extends z.ZodObject = z
44
78
  * unions, so a `setup` event union derives from the defs — one source of truth, no drift:
45
79
  *
46
80
  * events: EventFrom<typeof approve | typeof requestChanges> | { type: "agent.fault"; ... }
81
+ *
82
+ * @internal
47
83
  */
48
84
  export type EventFrom<D extends EventDef> =
49
85
  D extends EventDef<infer N, infer I>
@@ -58,29 +94,129 @@ export type EventFrom<D extends EventDef> =
58
94
  // `workspace.*`).
59
95
  const NAME_RE = /^[a-zA-Z0-9_-]+$/;
60
96
 
61
- /** Define one control event. Pure — returns a frozen def; registration happens nowhere. */
97
+ /**
98
+ * The field the Orchestrator writes on every event a party delivers into a Machine: its
99
+ * {@link Sender} (ADR-0011, ADR-0015). Plain data on the event, so it rides the mailbox, a delayed
100
+ * event the run host persists, and a restore. Only the Orchestrator writes it: no def may declare
101
+ * it, and a caller that posts it is refused. The Machine's own events (`raise`, `sendTo` itself, a
102
+ * child's `sendParent`) carry none.
103
+ */
104
+ export const SENDER_KEY = "sender";
105
+
106
+ /** Who delivered an event: an Agent by its slot, or a Gate by its invoke id (ADR-0015). */
107
+ export type Sender = { readonly party: "agent" | "gate"; readonly id: string };
108
+
109
+ /**
110
+ * Define one workflow event: an event that an Agent or the caller of a Gate may send into your
111
+ * Machine. Give the result to `jr2Setup({ events })`, and write its name as an `on` key on the
112
+ * states that take it. An Agent sees each event it may send as one tool on its Menu.
113
+ *
114
+ * It gives back a frozen {@link EventDef} and registers nothing. Each Machine whose `jr2Setup`
115
+ * lists the event takes it. A Machine that does not list it does not take it.
116
+ *
117
+ * Give an event only the params that the Machine reads. When a pick enters a state that invokes an
118
+ * Effect, `createMachine` refuses each param of the pick that nothing reads: the input of the
119
+ * Effect, an `assign`, or the `fill` of `fromPick`. The compiler does not find this error.
120
+ * `jr2 check` and `jr2 up` find it when they load the module. Thus when you copy a `finish` event with a `summary`
121
+ * and nothing uses the summary, drop `summary` from the `input`, or store it with `assign`.
122
+ *
123
+ * It refuses:
124
+ * - a name with a character other than a letter, a digit, `_` or `-`:
125
+ * `defineEvent: invalid name "<name>" — must match /^[a-zA-Z0-9_-]+$/ (it becomes an MCP tool
126
+ * name and an xstate event type)`
127
+ * - an `input` with a `sender` field:
128
+ * ``defineEvent: "<name>" declares `sender` in its input — the Orchestrator writes that field
129
+ * on every delivered event (who sent it); name the payload field something else``
130
+ * - an `output` field:
131
+ * ``defineEvent: "<name>" declares an `output`, which is retired — every pick is answered when
132
+ * the work it started is done, and the answer is the Machine's, not the def's``. With
133
+ * `semantics: "deferred"`, the text says ``declares `deferred` `` in place of
134
+ * ``declares an `output` ``.
135
+ *
136
+ * @param def - The event: its `name`, its `input` schema, and optionally its `description`,
137
+ * `semantics` and `audience`.
138
+ * @returns The event, as plain data to list in `jr2Setup({ events })`.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * import { z } from "zod";
143
+ * import { agent, defineEvent, fail, jr2Setup } from "@jr2/orchestrator";
144
+ *
145
+ * const done = defineEvent({
146
+ * name: "done",
147
+ * description: "Call this when the summary is written.",
148
+ * input: z.object({ summary: z.string() }),
149
+ * });
150
+ *
151
+ * export const machine = jr2Setup({
152
+ * events: [done],
153
+ * actors: {
154
+ * writer: agent({
155
+ * model: "anthropic/claude-haiku-5-5",
156
+ * instructions: "You write short summaries.",
157
+ * workspace: "none",
158
+ * }),
159
+ * fail: fail(),
160
+ * },
161
+ * }).createMachine({
162
+ * id: "summary",
163
+ * on: { "agent.fault": ".failed" },
164
+ * initial: "writing",
165
+ * states: {
166
+ * writing: {
167
+ * invoke: { src: "writer", input: { prompt: "Summarize the release notes in one line." } },
168
+ * on: { done: "finished" },
169
+ * },
170
+ * finished: { type: "final" },
171
+ * failed: { invoke: { src: "fail", input: { reason: "the Agent's Turn could not go on" } } },
172
+ * },
173
+ * });
174
+ * ```
175
+ *
176
+ * @see @jr2/orchestrator/docs/guides/write-a-workflow.md
177
+ * @see @jr2/orchestrator/docs/concepts/the-menu.md
178
+ * @see @jr2/orchestrator/docs/reference/decisions/0011-workflow-defined-events.md
179
+ */
62
180
  export function defineEvent<const Name extends string, Input extends z.ZodObject>(def: {
181
+ /** The event's name: the `on` key that takes it, and the name of its tool on the Agent's Menu.
182
+ * Letters, digits, `_` and `-` only. */
63
183
  name: Name;
184
+ /** What the event means, and when to send it. An Agent's model reads it as the tool's
185
+ * description. Make it true at every state that takes the event. */
64
186
  description?: string;
187
+ /** The schema of the payload: a flat `z.object`. Use `z.object({})` for an event with no
188
+ * payload. Do not name a field `sender`: jr2 writes that field. */
65
189
  input: Input;
66
- /** Default: `ack`. */
190
+ /** How the sender gets its answer. Default: `"ack"`, the only value `createMachine` accepts. */
67
191
  semantics?: EventSemantics;
68
- /** Default: `any` — restrict only the security-sensitive events (ADR-0015). */
192
+ /** Who may send the event: `"agent"`, `"external"` or `"any"`. Default: unset, and the state of
193
+ * each `on` key decides (see {@link EventAudience}). */
69
194
  audience?: EventAudience;
70
- output?: z.ZodType;
71
195
  }): EventDef<Name, Input> {
196
+ // Pure: no import-time side effect and no global registry (ADR-0011). Scoping is per Machine,
197
+ // done by `jr2Setup`, never here.
72
198
  if (!NAME_RE.test(def.name)) {
73
199
  throw new Error(
74
200
  `defineEvent: invalid name "${def.name}" — must match ${NAME_RE} ` +
75
201
  `(it becomes an MCP tool name and an xstate event type)`,
76
202
  );
77
203
  }
78
- if (def.semantics === "deferred" && !def.output) {
204
+ if (SENDER_KEY in def.input.shape) {
205
+ throw new Error(
206
+ `defineEvent: "${def.name}" declares \`${SENDER_KEY}\` in its input — the Orchestrator writes that field ` +
207
+ `on every delivered event (who sent it); name the payload field something else`,
208
+ );
209
+ }
210
+ // Retired (ADR-0006): a pick's answer is the Machine's — the last reply since the pick, or what an
211
+ // `answer` action sets (ADR-0024) — so a schema on the pick would promise a shape nothing keeps.
212
+ const loose = def as { semantics?: unknown; output?: unknown };
213
+ if (loose.semantics === "deferred" || loose.output !== undefined) {
79
214
  throw new Error(
80
- `defineEvent: "${def.name}" is deferred but has no output schema — a deferred event's result IS its output`,
215
+ `defineEvent: "${def.name}" declares ${loose.output !== undefined ? "an `output`" : "`deferred`"}, which is retired — ` +
216
+ `every pick is answered when the work it started is done, and the answer is the Machine's, not the def's`,
81
217
  );
82
218
  }
83
- return Object.freeze({ semantics: "ack" as const, audience: "any" as const, ...def });
219
+ return Object.freeze({ semantics: "ack" as const, ...def });
84
220
  }
85
221
 
86
222
  /** Duck-type guard for manifest validation (defs are plain data, not branded). */
@@ -102,13 +238,11 @@ export function isEventDef(value: unknown): value is EventDef {
102
238
  * Workflow: a nested Machine is reached by `import` and never carries the name `jr2 run` addresses
103
239
  * (ADR-0049), so naming it a workflow here would misreport where the bad def lives.
104
240
  *
105
- * `deferred` and `poll` are RESERVED, not implemented (ADR-0013): the wire leaves room for them —
106
- * a surface listing ships each def's `semantics`, a delivery returns an addressable receipt — but
107
- * nothing answers a held call today, and how a Machine should is deliberately undesigned (there is
108
- * no consumer to design it against, and flue's 60s MCP timeout means the answer will be
109
- * poll-with-progress rather than a held socket). Registering one therefore FAILS here. Quietly
110
- * downgrading it to `ack` would be worse than useless: the Agent would be handed a tool whose
111
- * contract — "this call returns the Machine's answer" — is a lie.
241
+ * `poll` is RESERVED, not implemented (ADR-0013): the wire leaves room for it — a surface listing
242
+ * ships each def's `semantics` — but nothing drains a steer inbox today. Registering one therefore
243
+ * FAILS here. Quietly downgrading it to `ack` would hand the Agent a tool whose contract is a lie.
244
+ * (A pick whose answer waits on the Machine's work needs no semantics of its own: every `ack` pick
245
+ * is answered at Rest, ADR-0024.)
112
246
  */
113
247
  export function eventMap(machine: string, events: readonly unknown[]): Map<string, EventDef> {
114
248
  const map = new Map<string, EventDef>();
@@ -120,8 +254,8 @@ export function eventMap(machine: string, events: readonly unknown[]): Map<strin
120
254
  if (def.semantics !== "ack") {
121
255
  throw new Error(
122
256
  `machine "${machine}": event "${def.name}" is \`${def.semantics}\`, which is reserved but ` +
123
- `NOT IMPLEMENTED (ADR-0013). Only \`ack\` events can be delivered today; there is nothing to ` +
124
- `answer a held call with, and degrading it to \`ack\` would hand the Agent a lying tool contract.`,
257
+ `NOT IMPLEMENTED (ADR-0013). Only \`ack\` events can be delivered today; nothing drains a ` +
258
+ `steer inbox, and degrading it to \`ack\` would hand the Agent a lying tool contract.`,
125
259
  );
126
260
  }
127
261
  map.set(def.name, def);
package/src/examples.ts CHANGED
@@ -3,9 +3,9 @@
3
3
  // a workflow's own `jr2Setup({ events })` list (ADR-0015). ADR-0006's "standard library" framing
4
4
  // is retired: nothing in jr2 consumes these by name.
5
5
  //
6
- // All `ack` — outcomes an Agent reports and moves on from. The other two semantics (`deferred`,
7
- // `poll`) are reserved and NOT implemented (ADR-0013), so machine-build refuses one; an example
8
- // that could not be used in a workflow would be a trap, not a starting point.
6
+ // All `ack`, as every pick is. The other semantics (`poll`) is reserved and NOT implemented
7
+ // (ADR-0013), so machine-build refuses it; an example that could not be used in a workflow would
8
+ // be a trap, not a starting point.
9
9
 
10
10
  import { z } from "zod";
11
11
  import { defineEvent } from "./define-event.ts";