@nanobpm/agentic 0.1.0 → 0.5.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 (128) hide show
  1. package/README.md +2 -1
  2. package/dist/demand/model.d.ts +7 -4
  3. package/dist/demand/model.js +22 -4
  4. package/dist/demand/taskdef.d.ts +13 -1
  5. package/dist/demand/taskdef.js +20 -2
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +1 -0
  8. package/dist/protocol/conformance/frames.js +32 -4
  9. package/dist/protocol/index.d.ts +1 -1
  10. package/dist/protocol/payloads.d.ts +44 -0
  11. package/dist/protocol/payloads.js +61 -7
  12. package/dist/session/acp/client.d.ts +109 -0
  13. package/dist/session/acp/client.js +254 -0
  14. package/dist/session/acp/index.d.ts +27 -0
  15. package/dist/session/acp/index.js +27 -0
  16. package/dist/session/acp/jsonrpc.d.ts +25 -0
  17. package/dist/session/acp/jsonrpc.js +148 -0
  18. package/dist/session/acp/normalize.d.ts +48 -0
  19. package/dist/session/acp/normalize.js +162 -0
  20. package/dist/session/acp/protocol.d.ts +94 -0
  21. package/dist/session/acp/protocol.js +136 -0
  22. package/dist/session/acp/spawn.d.ts +36 -0
  23. package/dist/session/acp/spawn.js +68 -0
  24. package/dist/session/acp/transport.d.ts +62 -0
  25. package/dist/session/acp/transport.js +126 -0
  26. package/dist/session/adapter.d.ts +135 -0
  27. package/dist/session/adapter.js +24 -0
  28. package/dist/session/backend.d.ts +43 -0
  29. package/dist/session/backend.js +95 -0
  30. package/dist/session/events.d.ts +152 -0
  31. package/dist/session/events.js +192 -0
  32. package/dist/session/index.d.ts +31 -0
  33. package/dist/session/index.js +5 -0
  34. package/dist/session/log.d.ts +107 -0
  35. package/dist/session/log.js +351 -0
  36. package/dist/session/normalizer/claude.d.ts +23 -0
  37. package/dist/session/normalizer/claude.js +138 -0
  38. package/dist/session/normalizer/copilot.d.ts +27 -0
  39. package/dist/session/normalizer/copilot.js +105 -0
  40. package/dist/session/normalizer/deepseek.d.ts +11 -0
  41. package/dist/session/normalizer/deepseek.js +68 -0
  42. package/dist/session/normalizer/index.d.ts +36 -0
  43. package/dist/session/normalizer/index.js +29 -0
  44. package/dist/session/normalizer/kimi.d.ts +10 -0
  45. package/dist/session/normalizer/kimi.js +80 -0
  46. package/dist/session/normalizer/link.d.ts +36 -0
  47. package/dist/session/normalizer/link.js +56 -0
  48. package/dist/session/normalizer/pi.d.ts +13 -0
  49. package/dist/session/normalizer/pi.js +61 -0
  50. package/dist/session/normalizer/qwen.d.ts +11 -0
  51. package/dist/session/normalizer/qwen.js +65 -0
  52. package/dist/session/normalizer/record.d.ts +21 -0
  53. package/dist/session/normalizer/record.js +87 -0
  54. package/dist/session/normalizer/types.d.ts +139 -0
  55. package/dist/session/normalizer/types.js +31 -0
  56. package/dist/session/schema.d.ts +38 -0
  57. package/dist/session/schema.js +74 -0
  58. package/dist/transcript/index.d.ts +2 -2
  59. package/dist/transcript/index.js +1 -1
  60. package/dist/transcript/schema.d.ts +23 -1
  61. package/dist/transcript/schema.js +34 -1
  62. package/dist/transcript/store.d.ts +93 -5
  63. package/dist/transcript/store.js +287 -6
  64. package/package.json +17 -1
  65. package/src/demand/model.test.ts +82 -4
  66. package/src/demand/model.ts +30 -9
  67. package/src/demand/taskdef.test.ts +51 -6
  68. package/src/demand/taskdef.ts +31 -2
  69. package/src/index.ts +1 -0
  70. package/src/protocol/conformance/frames.ts +32 -4
  71. package/src/protocol/index.ts +4 -0
  72. package/src/protocol/payloads.test.ts +31 -1
  73. package/src/protocol/payloads.ts +110 -7
  74. package/src/session/acp/client.test.ts +222 -0
  75. package/src/session/acp/client.ts +356 -0
  76. package/src/session/acp/fake-agent.ts +71 -0
  77. package/src/session/acp/index.ts +68 -0
  78. package/src/session/acp/integration.test.ts +37 -0
  79. package/src/session/acp/jsonrpc.test.ts +75 -0
  80. package/src/session/acp/jsonrpc.ts +171 -0
  81. package/src/session/acp/normalize.test.ts +150 -0
  82. package/src/session/acp/normalize.ts +204 -0
  83. package/src/session/acp/protocol.ts +178 -0
  84. package/src/session/acp/spawn.test.ts +45 -0
  85. package/src/session/acp/spawn.ts +91 -0
  86. package/src/session/acp/transport.test.ts +82 -0
  87. package/src/session/acp/transport.ts +155 -0
  88. package/src/session/adapter.ts +159 -0
  89. package/src/session/backend.test.ts +198 -0
  90. package/src/session/backend.ts +128 -0
  91. package/src/session/events.test.ts +168 -0
  92. package/src/session/events.ts +347 -0
  93. package/src/session/index.ts +67 -0
  94. package/src/session/log.test.ts +215 -0
  95. package/src/session/log.ts +525 -0
  96. package/src/session/normalizer/backend-integration.test.ts +103 -0
  97. package/src/session/normalizer/claude.test.ts +68 -0
  98. package/src/session/normalizer/claude.ts +136 -0
  99. package/src/session/normalizer/copilot.test.ts +59 -0
  100. package/src/session/normalizer/copilot.ts +133 -0
  101. package/src/session/normalizer/deepseek.ts +80 -0
  102. package/src/session/normalizer/index.ts +61 -0
  103. package/src/session/normalizer/kimi.ts +82 -0
  104. package/src/session/normalizer/link.test.ts +24 -0
  105. package/src/session/normalizer/link.ts +81 -0
  106. package/src/session/normalizer/pi.ts +75 -0
  107. package/src/session/normalizer/probe.test.ts +49 -0
  108. package/src/session/normalizer/qwen.test.ts +20 -0
  109. package/src/session/normalizer/qwen.ts +77 -0
  110. package/src/session/normalizer/record.test.ts +68 -0
  111. package/src/session/normalizer/record.ts +88 -0
  112. package/src/session/normalizer/resume.test.ts +25 -0
  113. package/src/session/normalizer/types.ts +152 -0
  114. package/src/session/normalizer/vectors.test.ts +180 -0
  115. package/src/session/schema.test.ts +84 -0
  116. package/src/session/schema.ts +78 -0
  117. package/src/session/test-db.ts +56 -0
  118. package/src/transcript/index.ts +8 -0
  119. package/src/transcript/schema.test.ts +31 -4
  120. package/src/transcript/schema.ts +36 -1
  121. package/src/transcript/store.ts +438 -6
  122. package/src/transcript/turns.test.ts +334 -0
  123. package/dist/blackboard/test-db.d.ts +0 -5
  124. package/dist/blackboard/test-db.js +0 -42
  125. package/dist/presence/test-db.d.ts +0 -5
  126. package/dist/presence/test-db.js +0 -42
  127. package/dist/transcript/test-db.d.ts +0 -5
  128. package/dist/transcript/test-db.js +0 -41
package/README.md CHANGED
@@ -13,9 +13,10 @@ The **Nano agentic protocol** (ADR 0056): one app-tier channel carrying agent pr
13
13
  | `@nanobpm/agentic/vocab` | Vocab resolver + core vocabulary (S3) |
14
14
  | `@nanobpm/agentic/demand` | Demand×supply model (S4) |
15
15
  | `@nanobpm/agentic/relay` | Relay ring + QoS scheduler (S5) |
16
- | `@nanobpm/agentic/transcript` | Transcript store, retention-by-lifecycle (S6) |
16
+ | `@nanobpm/agentic/transcript` | Transcript store, retention-by-lifecycle (S6) + turn-structured view (Camunda `AgentHistoryRecordValue` parity) |
17
17
  | `@nanobpm/agentic/blackboard` | Blackboard channel family (S7) |
18
18
  | `@nanobpm/agentic/cockpit` | Operator visibility page — the cockpit (S8) |
19
+ | `@nanobpm/agentic/session` | Canonical `SessionEvent` + authoritative session log for durable agent-session resume (ADR 0062) |
19
20
 
20
21
  The barrel `@nanobpm/agentic` re-exports each family as a namespace (`protocol`, `channel`, …). The worker-side client ships separately as `@nanobpm/urban-agent-client`.
21
22
 
@@ -34,10 +34,13 @@ export interface DemandSupplyReport {
34
34
  /** The overall SLO state: worst of the missing-agent signal and diversity. */
35
35
  readonly status: SloStatus;
36
36
  /**
37
- * Deployed `taskDefinition` types that are NOT valid routing tokens — ordinary
38
- * (non-agentic) C8 jobs the engine also runs. Surfaced (not silently dropped)
39
- * so an operator can spot a mistyped agentic token, but excluded from the
40
- * agentic demand×supply accounting.
37
+ * Deployed `taskDefinition` types that carry NO `linkName="prompt"` linked
38
+ * resource — ordinary (non-agentic) in-process C8 jobs the engine also runs
39
+ * (e.g. the deterministic `pr.*` workers). Classification is by the prompt
40
+ * signal, not the token string, so a prompt-less type is `nonAgentic` even when
41
+ * it happens to parse as a routing token. Surfaced (not silently dropped) so an
42
+ * operator can spot a mis-modelled task, but excluded from the agentic
43
+ * demand×supply accounting.
41
44
  */
42
45
  readonly nonAgentic: readonly string[];
43
46
  }
@@ -31,7 +31,15 @@ const SEVERITY = { green: 0, amber: 1, red: 2 };
31
31
  function worst(a, b) {
32
32
  return SEVERITY[a] >= SEVERITY[b] ? a : b;
33
33
  }
34
- /** A demanded token's network-prefix bucket, or `undefined` if not a routing token. */
34
+ /**
35
+ * A demanded token's bucket. For a valid routing token this is its network
36
+ * prefix (or a bare token's own role). For an arbitrary agentic type that is not
37
+ * a routing token (colon-form `senior:retro`, etc.) this is best-effort: the
38
+ * segment before the first `:` when present, else the whole type — a synthetic
39
+ * bucket so the leaf still surfaces as demand rather than being dropped. This is
40
+ * only ever called for prompt-bearing (agentic) leaves; non-agentic leaves are
41
+ * classified out before bucketing.
42
+ */
35
43
  function bucketOf(token) {
36
44
  try {
37
45
  const parsed = parseToken(token);
@@ -39,7 +47,8 @@ function bucketOf(token) {
39
47
  return parsed.network ?? parsed.role;
40
48
  }
41
49
  catch {
42
- return undefined;
50
+ const colon = token.indexOf(":");
51
+ return colon > 0 ? token.slice(0, colon) : token;
43
52
  }
44
53
  }
45
54
  function sortedUnique(values) {
@@ -67,14 +76,23 @@ export function computeDemandSupply(input) {
67
76
  }
68
77
  }
69
78
  const demandTokens = distinctTaskTypes(input.taskDefinitions);
79
+ // Agentic-ness is the prompt signal, OR-folded across every leaf of a type: a
80
+ // type is agentic demand iff at least one deployed leaf carries a
81
+ // `linkName="prompt"` linked resource. This is independent of the type string,
82
+ // so colon-form fleet types (`senior:retro`) are admitted and prompt-less
83
+ // deterministic `pr.*` tasks are excluded — regardless of token grammar.
84
+ const agenticByToken = new Map();
85
+ for (const leaf of input.taskDefinitions) {
86
+ agenticByToken.set(leaf.taskType, (agenticByToken.get(leaf.taskType) ?? false) || leaf.agentic);
87
+ }
70
88
  const byNetwork = new Map();
71
89
  const nonAgentic = [];
72
90
  for (const token of demandTokens) {
73
- const network = bucketOf(token);
74
- if (network === undefined) {
91
+ if (!(agenticByToken.get(token) ?? false)) {
75
92
  nonAgentic.push(token);
76
93
  continue;
77
94
  }
95
+ const network = bucketOf(token);
78
96
  const instances = sortedUnique(supplyByToken.get(token) ?? []);
79
97
  const demand = {
80
98
  token,
@@ -21,6 +21,17 @@ export interface TaskDefinitionLeaf {
21
21
  readonly process: string;
22
22
  /** The service-task element id carrying the leaf (best-effort, may be empty). */
23
23
  readonly elementId: string;
24
+ /**
25
+ * True when the service task carries a `<zeebe:linkedResource … linkName="prompt">`
26
+ * — its base-prompt side-car. This is the *structural* agentic signal (per nwf
27
+ * `SPEC.md` §"Agent job contract"): every external agent task delivers its base
28
+ * prompt through a `linkName="prompt"` linked resource, and no in-process
29
+ * (deterministic) worker task does. Agentic-ness is read from this flag, NOT
30
+ * from the `taskType` string — the deployed models use arbitrary type strings
31
+ * (colon-form `senior:retro`, dot-form, bare) that a routing-token grammar
32
+ * would mis-classify.
33
+ */
34
+ readonly agentic: boolean;
24
35
  }
25
36
  /**
26
37
  * Scan one BPMN document for its service-task `taskDefinition` leaves.
@@ -28,7 +39,8 @@ export interface TaskDefinitionLeaf {
28
39
  * Every `<bpmn:serviceTask>` carrying a `<zeebe:taskDefinition type="…">` with a
29
40
  * non-empty type yields a leaf; tasks without a task definition (or with an empty
30
41
  * type) are skipped. The scan is order-preserving and tolerant of attribute
31
- * ordering and self-closing task-definition tags.
42
+ * ordering and self-closing task-definition tags. Each leaf also records whether
43
+ * the task carries a `linkName="prompt"` linked resource (its `agentic` flag).
32
44
  */
33
45
  export declare function scanTaskDefinitions(xml: string): TaskDefinitionLeaf[];
34
46
  /**
@@ -21,13 +21,31 @@ function processId(xml) {
21
21
  const match = xml.match(/<bpmn:process\b[^>]*\bid\s*=\s*"([^"]*)"/);
22
22
  return match ? match[1] : "";
23
23
  }
24
+ /**
25
+ * True when a service-task body carries a `linkName="prompt"` linked resource —
26
+ * the base-prompt side-car that marks a task as external agentic work.
27
+ *
28
+ * Scans every `<zeebe:linkedResource …>` in the body (tolerating attribute
29
+ * ordering and self-closing tags, consistent with {@link scanTaskDefinitions})
30
+ * and returns true as soon as one carries `linkName="prompt"`.
31
+ */
32
+ function hasPromptLink(body) {
33
+ const linkRe = /<zeebe:linkedResource\b[^>]*?\/?>/g;
34
+ let link;
35
+ while ((link = linkRe.exec(body)) !== null) {
36
+ if (attr(link[0], "linkName") === "prompt")
37
+ return true;
38
+ }
39
+ return false;
40
+ }
24
41
  /**
25
42
  * Scan one BPMN document for its service-task `taskDefinition` leaves.
26
43
  *
27
44
  * Every `<bpmn:serviceTask>` carrying a `<zeebe:taskDefinition type="…">` with a
28
45
  * non-empty type yields a leaf; tasks without a task definition (or with an empty
29
46
  * type) are skipped. The scan is order-preserving and tolerant of attribute
30
- * ordering and self-closing task-definition tags.
47
+ * ordering and self-closing task-definition tags. Each leaf also records whether
48
+ * the task carries a `linkName="prompt"` linked resource (its `agentic` flag).
31
49
  */
32
50
  export function scanTaskDefinitions(xml) {
33
51
  const proc = processId(xml);
@@ -44,7 +62,7 @@ export function scanTaskDefinitions(xml) {
44
62
  const taskType = attr(tdMatch[0], "type");
45
63
  if (!taskType)
46
64
  continue;
47
- out.push({ taskType, elementId, process: proc });
65
+ out.push({ taskType, elementId, process: proc, agentic: hasPromptLink(body) });
48
66
  }
49
67
  return out;
50
68
  }
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export * as presence from "./presence/index.ts";
12
12
  export * as vocab from "./vocab/index.ts";
13
13
  export * as demand from "./demand/index.ts";
14
14
  export * as relay from "./relay/index.ts";
15
+ export * as session from "./session/index.ts";
15
16
  export * as transcript from "./transcript/index.ts";
16
17
  export * as blackboard from "./blackboard/index.ts";
17
18
  export * as cockpit from "./cockpit/index.ts";
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ export * as presence from "./presence/index.js";
12
12
  export * as vocab from "./vocab/index.js";
13
13
  export * as demand from "./demand/index.js";
14
14
  export * as relay from "./relay/index.js";
15
+ export * as session from "./session/index.js";
15
16
  export * as transcript from "./transcript/index.js";
16
17
  export * as blackboard from "./blackboard/index.js";
17
18
  export * as cockpit from "./cockpit/index.js";
@@ -75,15 +75,43 @@ export const GOLDEN_FRAMES = [
75
75
  hex: "4e4101010600000065000000187b226f70223a2272656164222c2273696e6365223a31327d",
76
76
  },
77
77
  {
78
- name: "relay-chunk-bulk",
78
+ name: "relay-produce-bulk",
79
79
  direction: "worker->hub",
80
80
  frame: {
81
81
  lane: "bulk",
82
82
  family: "relay",
83
83
  seq: 5,
84
- payload: { stream: "job-1223", offset: 2048, chunk: "hello world\n" },
84
+ payload: { op: "produce", stream: "job-1223", incarnation: 1, chunk: "hello world\n" },
85
85
  },
86
- hex: "4e41010207000000050000003b7b2273747265616d223a226a6f622d31323233222c226f6666736574223a323034382c226368756e6b223a2268656c6c6f20776f726c645c6e227d",
86
+ hex: "4e41010207000000050000004c7b226f70223a2270726f64756365222c2273747265616d223a226a6f622d31323233222c22696e6361726e6174696f6e223a312c226368756e6b223a2268656c6c6f20776f726c645c6e227d",
87
+ },
88
+ {
89
+ name: "relay-subscribe-control",
90
+ direction: "worker->hub",
91
+ frame: {
92
+ lane: "control",
93
+ family: "relay",
94
+ seq: 6,
95
+ payload: { op: "subscribe", stream: "job-1223", from: 0, credit: 1024 },
96
+ },
97
+ hex: "4e41010007000000060000003d7b226f70223a22737562736372696265222c2273747265616d223a226a6f622d31323233222c2266726f6d223a302c22637265646974223a313032347d",
98
+ },
99
+ {
100
+ name: "relay-credit-control",
101
+ direction: "worker->hub",
102
+ frame: { lane: "control", family: "relay", seq: 7, payload: { op: "credit", credit: 512 } },
103
+ hex: "4e41010007000000070000001c7b226f70223a22637265646974222c22637265646974223a3531327d",
104
+ },
105
+ {
106
+ name: "relay-subscribed-ack-control",
107
+ direction: "hub->worker",
108
+ frame: {
109
+ lane: "control",
110
+ family: "relay",
111
+ seq: 8,
112
+ payload: { op: "subscribed", stream: "job-1223", gap: false, nextOffset: 2048 },
113
+ },
114
+ hex: "4e4101000700000008000000457b226f70223a2273756273637269626564222c2273747265616d223a226a6f622d31323233222c22676170223a66616c73652c226e6578744f6666736574223a323034387d",
87
115
  },
88
116
  {
89
117
  name: "relay-seq-max-bulk",
@@ -104,7 +132,7 @@ export const GOLDEN_FRAMES = [
104
132
  },
105
133
  {
106
134
  name: "unicode-payload-relay",
107
- direction: "worker->hub",
135
+ direction: "hub->worker",
108
136
  frame: {
109
137
  lane: "bulk",
110
138
  family: "relay",
@@ -19,5 +19,5 @@ export { QOS_LANES, LANE_CODES, laneForCode, lanePriority, isQosLane, compareFra
19
19
  export { encodeFrame, decodeFrame, FrameDecodeError, FrameEncodeError, FRAME_MAGIC, FRAME_VERSION, FRAME_HEADER_BYTES, MAX_SEQ, type Frame, type FrameDecodeErrorCode, type FrameEncodeErrorCode, } from "./frame.ts";
20
20
  export { parseToken, formatToken, isValidToken, isSegmentName, isSeatLabel, TokenParseError, type RoutingToken, type TokenParseErrorCode, } from "./token.ts";
21
21
  export { validateVocabDocument, type VocabDocument, type VocabNetwork, type VocabRole, type VocabError, type VocabValidationResult, } from "./vocab/schema.ts";
22
- export { validatePayload, type Capability, type RegisterPayload, type HeartbeatPayload, type DeregisterPayload, type ServePayload, type DemandPayload, type BlackboardPayload, type BlackboardOp, type RelayPayload, type PayloadError, type PayloadValidationResult, } from "./payloads.ts";
22
+ export { validatePayload, type Capability, type RegisterPayload, type HeartbeatPayload, type DeregisterPayload, type ServePayload, type DemandPayload, type BlackboardPayload, type BlackboardOp, type RelayPayload, type RelayProducePayload, type RelaySubscribePayload, type RelayCreditPayload, type RelaySubscribedPayload, type PayloadError, type PayloadValidationResult, } from "./payloads.ts";
23
23
  export { bytesToHex, hexToBytes } from "./hex.ts";
@@ -43,11 +43,55 @@ export interface BlackboardPayload {
43
43
  readonly dedupeKey?: string;
44
44
  readonly since?: number;
45
45
  }
46
+ /**
47
+ * The `relay` family multiplexes two roles on one message family:
48
+ *
49
+ * - CONTROL frames a peer sends the hub: a producer's {@link RelayProducePayload}
50
+ * (`op: "produce"`), and a consumer's {@link RelaySubscribePayload} /
51
+ * {@link RelayCreditPayload}.
52
+ * - DELIVERY frames the hub sends a consumer: a data chunk ({@link RelayPayload},
53
+ * no `op`) and a resume ack ({@link RelaySubscribedPayload}, `op: "subscribed"`).
54
+ *
55
+ * {@link RelayPayload} is the DELIVERY data chunk specifically (`{ stream, offset,
56
+ * chunk }`, no `op`) — the hub assigns the authoritative `offset` from its ring.
57
+ * A producer must NOT send this shape; it sends {@link RelayProducePayload}.
58
+ */
46
59
  export interface RelayPayload {
47
60
  readonly stream: string;
48
61
  readonly offset: number;
49
62
  readonly chunk: string;
50
63
  }
64
+ /** A producer appends bytes to a stream. `incarnation` is the producer's
65
+ * generation, stamped so the hub can fence a stale predecessor (a retried job on
66
+ * a fresh runner takes over with a strictly higher incarnation). The hub assigns
67
+ * the offset — a producer never carries one. */
68
+ export interface RelayProducePayload {
69
+ readonly op: "produce";
70
+ readonly stream: string;
71
+ readonly incarnation: number;
72
+ readonly chunk: string;
73
+ }
74
+ /** A consumer subscribes to a stream, optionally resuming from `from` with an
75
+ * initial `credit` budget. */
76
+ export interface RelaySubscribePayload {
77
+ readonly op: "subscribe";
78
+ readonly stream: string;
79
+ readonly from?: number;
80
+ readonly credit?: number;
81
+ }
82
+ /** A consumer replenishes its flow-control budget. */
83
+ export interface RelayCreditPayload {
84
+ readonly op: "credit";
85
+ readonly credit: number;
86
+ }
87
+ /** The hub's ack to a subscribe: `gap` is true when `from` predated the retained
88
+ * ring (bytes were missed), `nextOffset` is where delivery resumes. */
89
+ export interface RelaySubscribedPayload {
90
+ readonly op: "subscribed";
91
+ readonly stream: string;
92
+ readonly gap: boolean;
93
+ readonly nextOffset: number;
94
+ }
51
95
  export interface PayloadError {
52
96
  readonly code: string;
53
97
  readonly message: string;
@@ -6,6 +6,9 @@ function isPlainObject(value) {
6
6
  function nonEmptyString(value) {
7
7
  return typeof value === "string" && value.length > 0;
8
8
  }
9
+ function nonNegInt(value) {
10
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
11
+ }
9
12
  function validateRegister(p, errors) {
10
13
  if (!nonEmptyString(p.instance)) {
11
14
  errors.push({ code: "bad-instance", message: "register.instance must be a non-empty string" });
@@ -74,14 +77,65 @@ function validateBlackboard(p, errors) {
74
77
  }
75
78
  }
76
79
  function validateRelay(p, errors) {
77
- if (!nonEmptyString(p.stream)) {
78
- errors.push({ code: "bad-stream", message: "relay.stream must be a non-empty string" });
79
- }
80
- if (typeof p.offset !== "number" || !Number.isInteger(p.offset) || p.offset < 0) {
81
- errors.push({ code: "bad-offset", message: "relay.offset must be a non-negative integer" });
80
+ const op = p.op;
81
+ // No `op`: a DELIVERY data chunk (hub -> consumer) — `{ stream, offset, chunk }`.
82
+ if (op === undefined) {
83
+ if (!nonEmptyString(p.stream)) {
84
+ errors.push({ code: "bad-stream", message: "relay.stream must be a non-empty string" });
85
+ }
86
+ if (!nonNegInt(p.offset)) {
87
+ errors.push({ code: "bad-offset", message: "relay.offset must be a non-negative integer" });
88
+ }
89
+ if (typeof p.chunk !== "string") {
90
+ errors.push({ code: "bad-chunk", message: "relay.chunk must be a string" });
91
+ }
92
+ return;
82
93
  }
83
- if (typeof p.chunk !== "string") {
84
- errors.push({ code: "bad-chunk", message: "relay.chunk must be a string" });
94
+ // Otherwise an op-tagged CONTROL/ack frame.
95
+ switch (op) {
96
+ case "produce":
97
+ if (!nonEmptyString(p.stream)) {
98
+ errors.push({ code: "bad-stream", message: "relay.produce.stream must be a non-empty string" });
99
+ }
100
+ if (!nonNegInt(p.incarnation)) {
101
+ errors.push({ code: "bad-incarnation", message: "relay.produce.incarnation must be a non-negative integer" });
102
+ }
103
+ if (typeof p.chunk !== "string") {
104
+ errors.push({ code: "bad-chunk", message: "relay.produce.chunk must be a string" });
105
+ }
106
+ return;
107
+ case "subscribe":
108
+ if (!nonEmptyString(p.stream)) {
109
+ errors.push({ code: "bad-stream", message: "relay.subscribe.stream must be a non-empty string" });
110
+ }
111
+ if ("from" in p && !nonNegInt(p.from)) {
112
+ errors.push({ code: "bad-from", message: "relay.subscribe.from must be a non-negative integer when present" });
113
+ }
114
+ if ("credit" in p && !nonNegInt(p.credit)) {
115
+ errors.push({ code: "bad-credit", message: "relay.subscribe.credit must be a non-negative integer when present" });
116
+ }
117
+ return;
118
+ case "credit":
119
+ if (!nonNegInt(p.credit)) {
120
+ errors.push({ code: "bad-credit", message: "relay.credit.credit must be a non-negative integer" });
121
+ }
122
+ return;
123
+ case "subscribed":
124
+ if (!nonEmptyString(p.stream)) {
125
+ errors.push({ code: "bad-stream", message: "relay.subscribed.stream must be a non-empty string" });
126
+ }
127
+ if (typeof p.gap !== "boolean") {
128
+ errors.push({ code: "bad-gap", message: "relay.subscribed.gap must be a boolean" });
129
+ }
130
+ if (!nonNegInt(p.nextOffset)) {
131
+ errors.push({ code: "bad-next-offset", message: "relay.subscribed.nextOffset must be a non-negative integer" });
132
+ }
133
+ return;
134
+ default:
135
+ errors.push({
136
+ code: "bad-op",
137
+ message: `relay.op must be one of produce|subscribe|credit|subscribed, got ${String(op)}`,
138
+ });
85
139
  }
86
140
  }
87
141
  /**
@@ -0,0 +1,109 @@
1
+ import type { SessionEvent } from "../events.ts";
2
+ import { AcpConnection } from "./jsonrpc.ts";
3
+ import { type AcpPromptCapabilities } from "./protocol.ts";
4
+ import type { AcpTransport } from "./transport.ts";
5
+ /**
6
+ * The narrow port the ingestion client writes canonical events to. A Slice-1
7
+ * {@link SessionAdapter} (e.g. a `SessionBackend`) satisfies it structurally, so
8
+ * the ACP stream can feed the authoritative log directly; a test can pass a
9
+ * plain collector.
10
+ */
11
+ export interface SessionEventSink {
12
+ emit(event: SessionEvent): void;
13
+ }
14
+ /** A single ACP content block sent in a prompt. `text` is the baseline shape. */
15
+ export interface AcpTextContentBlock {
16
+ readonly type: "text";
17
+ readonly text: string;
18
+ }
19
+ /** The prompt payload: a content-block array, or a bare string (wrapped as text). */
20
+ export type AcpPromptInput = string | readonly AcpTextContentBlock[];
21
+ /** The durable-resume capability probe read from the `initialize` handshake. */
22
+ export interface AcpCapabilityProbe {
23
+ /** The protocol version the agent negotiated. */
24
+ readonly protocolVersion: number;
25
+ /** The raw `agentCapabilities.loadSession` flag. */
26
+ readonly loadSession: boolean;
27
+ /**
28
+ * The nano-workforce enrolment-gate signal (slice 5): `true` iff the agent can
29
+ * durably resume via `session/load`. Equal to {@link loadSession} — named for
30
+ * the gate that consumes it, so the enrolment site reads intent, not wire detail.
31
+ */
32
+ readonly durableResume: boolean;
33
+ /** The agent's advertised prompt-content capabilities. */
34
+ readonly promptCapabilities: AcpPromptCapabilities;
35
+ /** The full, unmodified `agentCapabilities` object for consumers that need more. */
36
+ readonly agentCapabilities: unknown;
37
+ }
38
+ /** The result of a completed `session/prompt`, plus the events it produced. */
39
+ export interface AcpPromptResult {
40
+ /** The agent's stop reason (`end_turn`, `cancelled`, …) when it reports one. */
41
+ readonly stopReason: string | null;
42
+ /** The canonical events emitted while this prompt ran, in emission order. */
43
+ readonly events: readonly SessionEvent[];
44
+ }
45
+ /** Parameters shared by `session/new` and `session/load`. */
46
+ export interface AcpSessionParams {
47
+ /** The session working directory (absolute path). */
48
+ readonly cwd: string;
49
+ /** MCP servers to attach; defaults to none. */
50
+ readonly mcpServers?: readonly unknown[];
51
+ }
52
+ export interface AcpSessionClientOptions {
53
+ /** Injectable event-id generator (deterministic tests). Default `crypto.randomUUID`. */
54
+ readonly newEventId?: () => string;
55
+ /** Client name/version reported in `initialize`. */
56
+ readonly clientInfo?: {
57
+ readonly name: string;
58
+ readonly version: string;
59
+ };
60
+ /**
61
+ * How to answer an agent's `session/request_permission`. Default: select the
62
+ * first "allow"-flavoured option the agent offers (or a cancel outcome when
63
+ * none is), so a driven session is never silently blocked on approval.
64
+ */
65
+ readonly onPermissionRequest?: (params: unknown) => unknown;
66
+ }
67
+ /**
68
+ * The ACP ingestion client. Bind it to a transport and a sink, `initialize`, then
69
+ * either `newSession` + `prompt` (drive) or `restore` an existing session id.
70
+ */
71
+ export declare class AcpSessionClient {
72
+ #private;
73
+ constructor(connection: AcpConnection, sink: SessionEventSink, options?: AcpSessionClientOptions);
74
+ /** The active session id (after `newSession`/`restore`), or `undefined`. */
75
+ get sessionId(): string | undefined;
76
+ /**
77
+ * Perform the `initialize` handshake and return the durable-resume capability
78
+ * probe. Must be called before any `session/*` method.
79
+ */
80
+ initialize(): Promise<AcpCapabilityProbe>;
81
+ /** Open a fresh session (`session/new`); stores and returns its id. */
82
+ newSession(params: AcpSessionParams): Promise<string>;
83
+ /**
84
+ * **restore** — load an existing session (`session/load`). The agent replays its
85
+ * prior history as `session/update` notifications, which this client ingests
86
+ * into the sink; when the load resolves, the pending message is flushed. Returns
87
+ * the canonical events reconstructed from the replayed history, in order.
88
+ *
89
+ * Only valid against an agent whose probe reported `durableResume: true`.
90
+ */
91
+ restore(sessionId: string, params: AcpSessionParams): Promise<readonly SessionEvent[]>;
92
+ /**
93
+ * **drive** — send a prompt (`session/prompt`) and resolve when the turn ends.
94
+ * All assistant output arrives as `session/update` notifications and is emitted
95
+ * to the sink during the call; the final buffered message is flushed on
96
+ * completion.
97
+ */
98
+ prompt(input: AcpPromptInput): Promise<AcpPromptResult>;
99
+ /** **steer** — request cancellation of the running turn (`session/cancel`, a notification). */
100
+ cancel(): void;
101
+ /** Close the underlying connection and flush any pending buffered message. */
102
+ close(): void;
103
+ }
104
+ /**
105
+ * Open an ACP ingestion client over `transport`, emitting into `sink`. Wraps the
106
+ * transport in an {@link AcpConnection} and returns the ready client; call
107
+ * {@link AcpSessionClient.initialize} first.
108
+ */
109
+ export declare function openAcpSession(transport: AcpTransport, sink: SessionEventSink, options?: AcpSessionClientOptions): AcpSessionClient;