@jr2/agent-protocol 0.4.1 → 0.6.0

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/define-event.ts +131 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jr2/agent-protocol",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "The jr2 Orchestrator↔Agent wire: defineEvent and nothing else.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -8,37 +8,68 @@
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` the pick is delivered and answered. A pick that moves the workflow to work inside the
14
- * Agent's state is answered when that work is done (ADR-0024): the answer is the Machine's,
15
- * never a schema on the pick, so no pick declares an output.
16
- * - `poll` the caller pulls queued down-channel messages (cooperative steer checkpoint). Reserved,
17
- * not built (ADR-0013).
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
18
29
  */
19
30
  export type EventSemantics = "ack" | "poll";
20
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).
21
37
  /**
22
- * Who may deliver the event, stated BY KIND (ADR-0015): `agent` (an Agent's pick), `external` (a
23
- * Gate's caller), `any` (both). Leave it UNSET for the default: each writing of the event — each
24
- * `on` key — belongs to the party that its state, or the nearest state above it, invokes (an Agent
25
- * slot, a `gate`, or both); with none above, both. Set it only to give a pick to the other party
26
- * (`submit: "any"` lets an in-Turn Gate's caller send the Agent's pick) or to hold a root pick back
27
- * (`report_blocked: "agent"`; `approve: "external"` guarantees no Agent state can offer it).
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
28
50
  */
29
51
  export type EventAudience = "agent" | "external" | "any";
30
52
 
53
+ // `name` doubles as the MCP tool name (agent side) and the xstate event `type` (Machine side).
31
54
  /**
32
- * A workflow-defined control event: pure data (plus zod schemas), safe to import anywhere.
33
- * `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
34
61
  */
35
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. */
36
64
  readonly name: Name;
65
+ /** What the event means. An Agent's model reads it as the tool's description. */
37
66
  readonly description?: string;
38
- /** 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. */
39
69
  readonly input: Input;
70
+ /** How the sender gets its answer: always `"ack"` on an event that `createMachine` accepts. */
40
71
  readonly semantics: EventSemantics;
41
- /** Absent: each writing's state decides who may send it (see {@link EventAudience}). */
72
+ /** Who may send the event. Absent: the state of each `on` key decides (see {@link EventAudience}). */
42
73
  readonly audience?: EventAudience;
43
74
  };
44
75
 
@@ -47,6 +78,8 @@ export type EventDef<Name extends string = string, Input extends z.ZodObject = z
47
78
  * unions, so a `setup` event union derives from the defs — one source of truth, no drift:
48
79
  *
49
80
  * events: EventFrom<typeof approve | typeof requestChanges> | { type: "agent.fault"; ... }
81
+ *
82
+ * @internal
50
83
  */
51
84
  export type EventFrom<D extends EventDef> =
52
85
  D extends EventDef<infer N, infer I>
@@ -73,16 +106,95 @@ export const SENDER_KEY = "sender";
73
106
  /** Who delivered an event: an Agent by its slot, or a Gate by its invoke id (ADR-0015). */
74
107
  export type Sender = { readonly party: "agent" | "gate"; readonly id: string };
75
108
 
76
- /** Define one control event. Pure — returns a frozen def; registration happens nowhere. */
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
+ */
77
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. */
78
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. */
79
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. */
80
189
  input: Input;
81
- /** Default: `ack`. */
190
+ /** How the sender gets its answer. Default: `"ack"`, the only value `createMachine` accepts. */
82
191
  semantics?: EventSemantics;
83
- /** Default: unset — each writing's state decides (see {@link EventAudience}). */
192
+ /** Who may send the event: `"agent"`, `"external"` or `"any"`. Default: unset, and the state of
193
+ * each `on` key decides (see {@link EventAudience}). */
84
194
  audience?: EventAudience;
85
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.
86
198
  if (!NAME_RE.test(def.name)) {
87
199
  throw new Error(
88
200
  `defineEvent: invalid name "${def.name}" — must match ${NAME_RE} ` +