@jr2/agent-protocol 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rich Snapp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # @jr2/agent-protocol
2
+
3
+ The Orchestrator↔Agent wire for jr2: `defineEvent` and nothing else — a pure leaf both `@jr2/orchestrator` and the
4
+ Adapter that serves an Agent its Menu depend on, so an event an Agent can deliver is typed once and read the same way on
5
+ both ends.
6
+
7
+ You rarely depend on this directly; a Workflow imports what it needs from `@jr2/orchestrator`. Docs, glossary, and
8
+ architecture decisions: [github.com/snapwich/jr2](https://github.com/snapwich/jr2).
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@jr2/agent-protocol",
3
+ "version": "0.1.0",
4
+ "description": "The jr2 Orchestrator↔Agent wire: defineEvent and nothing else.",
5
+ "license": "MIT",
6
+ "private": false,
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/snapwich/jr2.git",
13
+ "directory": "packages/agent-protocol"
14
+ },
15
+ "homepage": "https://github.com/snapwich/jr2#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/snapwich/jr2/issues"
18
+ },
19
+ "keywords": [
20
+ "jr2",
21
+ "agents",
22
+ "agentic",
23
+ "xstate",
24
+ "kubernetes",
25
+ "workflow"
26
+ ],
27
+ "engines": {
28
+ "node": ">=24"
29
+ },
30
+ "type": "module",
31
+ "exports": {
32
+ ".": "./src/index.ts"
33
+ },
34
+ "types": "./src/index.ts",
35
+ "dependencies": {
36
+ "zod": "^4.4.3"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^26.0.1"
40
+ },
41
+ "files": [
42
+ "src"
43
+ ],
44
+ "scripts": {
45
+ "typecheck": "tsc --noEmit",
46
+ "test": "node --test"
47
+ }
48
+ }
@@ -0,0 +1,130 @@
1
+ // `defineEvent` — the event mechanism, and zero events (ADR-0011). A workflow defines its own
2
+ // control vocabulary as pure-data defs; jr2 owns only definition, transport, validation, and
3
+ // delivery. This is a PURE factory: no import-time side effects, no global registry. Scoping is
4
+ // per-MACHINE — a Machine hands its defs to `jr2Setup({ events: [...] })` (ADR-0015), which
5
+ // attaches the vocabulary to that machine, and actors resolve event *names* (which is all a
6
+ // serializable input can carry — ADR-0007) against the Machine that INVOKED them (ADR-0049).
7
+ // Two Machines' `approve` may legitimately differ, nested one inside the other, in one run.
8
+
9
+ import { z } from "zod";
10
+
11
+ /**
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).
16
+ */
17
+ export type EventSemantics = "ack" | "deferred" | "poll";
18
+
19
+ /**
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.
24
+ */
25
+ export type EventAudience = "agent" | "external" | "any";
26
+
27
+ /**
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).
30
+ */
31
+ export type EventDef<Name extends string = string, Input extends z.ZodObject = z.ZodObject> = {
32
+ readonly name: Name;
33
+ readonly description?: string;
34
+ /** Input schema — a flat tagged object, never a `oneOf` (ADR-0006 encoding rule). */
35
+ readonly input: Input;
36
+ 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;
40
+ };
41
+
42
+ /**
43
+ * The Machine-side event type a def delivers as: `{ type: name } & input`. Distributes over
44
+ * unions, so a `setup` event union derives from the defs — one source of truth, no drift:
45
+ *
46
+ * events: EventFrom<typeof approve | typeof requestChanges> | { type: "agent.fault"; ... }
47
+ */
48
+ export type EventFrom<D extends EventDef> =
49
+ D extends EventDef<infer N, infer I>
50
+ ? // An empty input infers as `Record<string, never>`, whose intersection with `{ type }` is
51
+ // unsatisfiable — collapse it so `{ type: "approve" }` typechecks for payload-less events.
52
+ { type: N } & (z.infer<I> extends Record<string, never> ? unknown : z.infer<I>)
53
+ : never;
54
+
55
+ // The wire constrains names, not taste: a name is an MCP tool name and an xstate event type at
56
+ // once. The MCP-safe charset below also (deliberately) excludes `.`, which keeps workflow events
57
+ // mechanically out of the dotted namespaces jr2 itself delivers on (`xstate.*`, `agent.*`,
58
+ // `workspace.*`).
59
+ const NAME_RE = /^[a-zA-Z0-9_-]+$/;
60
+
61
+ /** Define one control event. Pure — returns a frozen def; registration happens nowhere. */
62
+ export function defineEvent<const Name extends string, Input extends z.ZodObject>(def: {
63
+ name: Name;
64
+ description?: string;
65
+ input: Input;
66
+ /** Default: `ack`. */
67
+ semantics?: EventSemantics;
68
+ /** Default: `any` — restrict only the security-sensitive events (ADR-0015). */
69
+ audience?: EventAudience;
70
+ output?: z.ZodType;
71
+ }): EventDef<Name, Input> {
72
+ if (!NAME_RE.test(def.name)) {
73
+ throw new Error(
74
+ `defineEvent: invalid name "${def.name}" — must match ${NAME_RE} ` +
75
+ `(it becomes an MCP tool name and an xstate event type)`,
76
+ );
77
+ }
78
+ if (def.semantics === "deferred" && !def.output) {
79
+ throw new Error(
80
+ `defineEvent: "${def.name}" is deferred but has no output schema — a deferred event's result IS its output`,
81
+ );
82
+ }
83
+ return Object.freeze({ semantics: "ack" as const, audience: "any" as const, ...def });
84
+ }
85
+
86
+ /** Duck-type guard for manifest validation (defs are plain data, not branded). */
87
+ export function isEventDef(value: unknown): value is EventDef {
88
+ const v = value as EventDef | null;
89
+ return (
90
+ !!v &&
91
+ typeof v === "object" &&
92
+ typeof v.name === "string" &&
93
+ !!v.input &&
94
+ typeof (v.input as { safeParse?: unknown }).safeParse === "function"
95
+ );
96
+ }
97
+
98
+ /**
99
+ * Resolve a defs list into a name→def map, rejecting duplicates and non-defs. `jr2Setup` calls
100
+ * this once per Machine (ADR-0015); the error names the Machine so an unlisted or double-listed
101
+ * name fails loudly at machine-build time, not at delivery. The scope is the MACHINE, not the
102
+ * Workflow: a nested Machine is reached by `import` and never carries the name `jr2 run` addresses
103
+ * (ADR-0049), so naming it a workflow here would misreport where the bad def lives.
104
+ *
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.
112
+ */
113
+ export function eventMap(machine: string, events: readonly unknown[]): Map<string, EventDef> {
114
+ const map = new Map<string, EventDef>();
115
+ for (const def of events) {
116
+ if (!isEventDef(def)) {
117
+ throw new Error(`machine "${machine}": \`events\` entry is not a defineEvent() def: ${JSON.stringify(def)}`);
118
+ }
119
+ if (map.has(def.name)) throw new Error(`machine "${machine}": duplicate event "${def.name}" in \`events\``);
120
+ if (def.semantics !== "ack") {
121
+ throw new Error(
122
+ `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.`,
125
+ );
126
+ }
127
+ map.set(def.name, def);
128
+ }
129
+ return map;
130
+ }
@@ -0,0 +1,32 @@
1
+ // An EXAMPLE event set — not the protocol (ADR-0011). jr2 ships the mechanism (`defineEvent`)
2
+ // and zero events; these defs exist for tests, fixtures, and as a starting point to copy into
3
+ // a workflow's own `jr2Setup({ events })` list (ADR-0015). ADR-0006's "standard library" framing
4
+ // is retired: nothing in jr2 consumes these by name.
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.
9
+
10
+ import { z } from "zod";
11
+ import { defineEvent } from "./define-event.ts";
12
+
13
+ export const doneEvent = defineEvent({
14
+ name: "done",
15
+ description: "Signal the Agent has finished its turn of work.",
16
+ input: z.object({ summary: z.string().optional() }),
17
+ });
18
+
19
+ export const requestReviewEvent = defineEvent({
20
+ name: "request_review",
21
+ description: "Hand the current work off for review.",
22
+ input: z.object({ summary: z.string() }),
23
+ });
24
+
25
+ export const reportBlockedEvent = defineEvent({
26
+ name: "report_blocked",
27
+ description: "Report that the Agent cannot proceed and why.",
28
+ input: z.object({ reason: z.string() }),
29
+ });
30
+
31
+ /** The example set (drop into `jr2Setup({ events: [...exampleEvents] })` — ADR-0015). */
32
+ export const exampleEvents = [doneEvent, requestReviewEvent, reportBlockedEvent] as const;
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ // @jr2/agent-protocol — the Orchestrator↔Agent wire.
2
+ //
3
+ // The minimal shared vocabulary both sides must agree on to talk: the `defineEvent` mechanism
4
+ // (workflows declare their own control events — jr2 ships none, ADR-0011) and an example event
5
+ // set. A pure leaf — no MCP server, no flue, no xstate. (The old `mcpPath` addressing rule died
6
+ // with ADR-0013: the MCP surface lives in the Sandbox's Adapter, which builds its own paths.)
7
+
8
+ export * from "./define-event.ts";
9
+ export * from "./examples.ts";