@jr2/agent-protocol 0.3.0 → 0.4.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.4.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
  }
@@ -10,17 +10,21 @@ import { z } from "zod";
10
10
 
11
11
  /**
12
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).
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).
16
18
  */
17
- export type EventSemantics = "ack" | "deferred" | "poll";
19
+ export type EventSemantics = "ack" | "poll";
18
20
 
19
21
  /**
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.
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).
24
28
  */
25
29
  export type EventAudience = "agent" | "external" | "any";
26
30
 
@@ -34,9 +38,8 @@ export type EventDef<Name extends string = string, Input extends z.ZodObject = z
34
38
  /** Input schema — a flat tagged object, never a `oneOf` (ADR-0006 encoding rule). */
35
39
  readonly input: Input;
36
40
  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;
41
+ /** Absent: each writing's state decides who may send it (see {@link EventAudience}). */
42
+ readonly audience?: EventAudience;
40
43
  };
41
44
 
42
45
  /**
@@ -58,6 +61,18 @@ export type EventFrom<D extends EventDef> =
58
61
  // `workspace.*`).
59
62
  const NAME_RE = /^[a-zA-Z0-9_-]+$/;
60
63
 
64
+ /**
65
+ * The field the Orchestrator writes on every event a party delivers into a Machine: its
66
+ * {@link Sender} (ADR-0011, ADR-0015). Plain data on the event, so it rides the mailbox, a delayed
67
+ * event the run host persists, and a restore. Only the Orchestrator writes it: no def may declare
68
+ * it, and a caller that posts it is refused. The Machine's own events (`raise`, `sendTo` itself, a
69
+ * child's `sendParent`) carry none.
70
+ */
71
+ export const SENDER_KEY = "sender";
72
+
73
+ /** Who delivered an event: an Agent by its slot, or a Gate by its invoke id (ADR-0015). */
74
+ export type Sender = { readonly party: "agent" | "gate"; readonly id: string };
75
+
61
76
  /** Define one control event. Pure — returns a frozen def; registration happens nowhere. */
62
77
  export function defineEvent<const Name extends string, Input extends z.ZodObject>(def: {
63
78
  name: Name;
@@ -65,9 +80,8 @@ export function defineEvent<const Name extends string, Input extends z.ZodObject
65
80
  input: Input;
66
81
  /** Default: `ack`. */
67
82
  semantics?: EventSemantics;
68
- /** Default: `any` — restrict only the security-sensitive events (ADR-0015). */
83
+ /** Default: unset — each writing's state decides (see {@link EventAudience}). */
69
84
  audience?: EventAudience;
70
- output?: z.ZodType;
71
85
  }): EventDef<Name, Input> {
72
86
  if (!NAME_RE.test(def.name)) {
73
87
  throw new Error(
@@ -75,12 +89,22 @@ export function defineEvent<const Name extends string, Input extends z.ZodObject
75
89
  `(it becomes an MCP tool name and an xstate event type)`,
76
90
  );
77
91
  }
78
- if (def.semantics === "deferred" && !def.output) {
92
+ if (SENDER_KEY in def.input.shape) {
93
+ throw new Error(
94
+ `defineEvent: "${def.name}" declares \`${SENDER_KEY}\` in its input — the Orchestrator writes that field ` +
95
+ `on every delivered event (who sent it); name the payload field something else`,
96
+ );
97
+ }
98
+ // Retired (ADR-0006): a pick's answer is the Machine's — the last reply since the pick, or what an
99
+ // `answer` action sets (ADR-0024) — so a schema on the pick would promise a shape nothing keeps.
100
+ const loose = def as { semantics?: unknown; output?: unknown };
101
+ if (loose.semantics === "deferred" || loose.output !== undefined) {
79
102
  throw new Error(
80
- `defineEvent: "${def.name}" is deferred but has no output schema — a deferred event's result IS its output`,
103
+ `defineEvent: "${def.name}" declares ${loose.output !== undefined ? "an `output`" : "`deferred`"}, which is retired — ` +
104
+ `every pick is answered when the work it started is done, and the answer is the Machine's, not the def's`,
81
105
  );
82
106
  }
83
- return Object.freeze({ semantics: "ack" as const, audience: "any" as const, ...def });
107
+ return Object.freeze({ semantics: "ack" as const, ...def });
84
108
  }
85
109
 
86
110
  /** Duck-type guard for manifest validation (defs are plain data, not branded). */
@@ -102,13 +126,11 @@ export function isEventDef(value: unknown): value is EventDef {
102
126
  * Workflow: a nested Machine is reached by `import` and never carries the name `jr2 run` addresses
103
127
  * (ADR-0049), so naming it a workflow here would misreport where the bad def lives.
104
128
  *
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.
129
+ * `poll` is RESERVED, not implemented (ADR-0013): the wire leaves room for it — a surface listing
130
+ * ships each def's `semantics` — but nothing drains a steer inbox today. Registering one therefore
131
+ * FAILS here. Quietly downgrading it to `ack` would hand the Agent a tool whose contract is a lie.
132
+ * (A pick whose answer waits on the Machine's work needs no semantics of its own: every `ack` pick
133
+ * is answered at Rest, ADR-0024.)
112
134
  */
113
135
  export function eventMap(machine: string, events: readonly unknown[]): Map<string, EventDef> {
114
136
  const map = new Map<string, EventDef>();
@@ -120,8 +142,8 @@ export function eventMap(machine: string, events: readonly unknown[]): Map<strin
120
142
  if (def.semantics !== "ack") {
121
143
  throw new Error(
122
144
  `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.`,
145
+ `NOT IMPLEMENTED (ADR-0013). Only \`ack\` events can be delivered today; nothing drains a ` +
146
+ `steer inbox, and degrading it to \`ack\` would hand the Agent a lying tool contract.`,
125
147
  );
126
148
  }
127
149
  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";