@jr2/agent-protocol 0.4.1 → 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 +1 -1
- package/src/define-event.ts +131 -19
package/package.json
CHANGED
package/src/define-event.ts
CHANGED
|
@@ -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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* - `poll
|
|
17
|
-
*
|
|
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
|
|
23
|
-
* Gate
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
|
33
|
-
*
|
|
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
|
-
|
|
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:
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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} ` +
|