@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 +2 -2
- package/src/define-event.ts +165 -31
- package/src/examples.ts +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jr2/agent-protocol",
|
|
3
|
-
"version": "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
|
}
|
package/src/define-event.ts
CHANGED
|
@@ -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
|
|
13
|
-
*
|
|
14
|
-
* - `
|
|
15
|
-
*
|
|
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" | "
|
|
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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
29
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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 (
|
|
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}"
|
|
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,
|
|
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
|
-
* `
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
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;
|
|
124
|
-
`
|
|
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
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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";
|