@nanobpm/agentic 0.1.0 → 0.4.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/README.md +1 -0
- package/dist/demand/model.d.ts +7 -4
- package/dist/demand/model.js +22 -4
- package/dist/demand/taskdef.d.ts +13 -1
- package/dist/demand/taskdef.js +20 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/protocol/conformance/frames.js +32 -4
- package/dist/protocol/index.d.ts +1 -1
- package/dist/protocol/payloads.d.ts +44 -0
- package/dist/protocol/payloads.js +61 -7
- package/dist/session/acp/client.d.ts +109 -0
- package/dist/session/acp/client.js +254 -0
- package/dist/session/acp/index.d.ts +27 -0
- package/dist/session/acp/index.js +27 -0
- package/dist/session/acp/jsonrpc.d.ts +25 -0
- package/dist/session/acp/jsonrpc.js +148 -0
- package/dist/session/acp/normalize.d.ts +48 -0
- package/dist/session/acp/normalize.js +162 -0
- package/dist/session/acp/protocol.d.ts +94 -0
- package/dist/session/acp/protocol.js +136 -0
- package/dist/session/acp/spawn.d.ts +36 -0
- package/dist/session/acp/spawn.js +68 -0
- package/dist/session/acp/transport.d.ts +62 -0
- package/dist/session/acp/transport.js +126 -0
- package/dist/session/adapter.d.ts +135 -0
- package/dist/session/adapter.js +24 -0
- package/dist/session/backend.d.ts +43 -0
- package/dist/session/backend.js +95 -0
- package/dist/session/events.d.ts +152 -0
- package/dist/session/events.js +192 -0
- package/dist/session/index.d.ts +31 -0
- package/dist/session/index.js +5 -0
- package/dist/session/log.d.ts +107 -0
- package/dist/session/log.js +351 -0
- package/dist/session/normalizer/claude.d.ts +23 -0
- package/dist/session/normalizer/claude.js +138 -0
- package/dist/session/normalizer/copilot.d.ts +27 -0
- package/dist/session/normalizer/copilot.js +105 -0
- package/dist/session/normalizer/deepseek.d.ts +11 -0
- package/dist/session/normalizer/deepseek.js +68 -0
- package/dist/session/normalizer/index.d.ts +36 -0
- package/dist/session/normalizer/index.js +29 -0
- package/dist/session/normalizer/kimi.d.ts +10 -0
- package/dist/session/normalizer/kimi.js +80 -0
- package/dist/session/normalizer/link.d.ts +36 -0
- package/dist/session/normalizer/link.js +56 -0
- package/dist/session/normalizer/pi.d.ts +13 -0
- package/dist/session/normalizer/pi.js +61 -0
- package/dist/session/normalizer/qwen.d.ts +11 -0
- package/dist/session/normalizer/qwen.js +65 -0
- package/dist/session/normalizer/record.d.ts +21 -0
- package/dist/session/normalizer/record.js +87 -0
- package/dist/session/normalizer/types.d.ts +139 -0
- package/dist/session/normalizer/types.js +31 -0
- package/dist/session/schema.d.ts +38 -0
- package/dist/session/schema.js +74 -0
- package/package.json +17 -1
- package/src/demand/model.test.ts +82 -4
- package/src/demand/model.ts +30 -9
- package/src/demand/taskdef.test.ts +51 -6
- package/src/demand/taskdef.ts +31 -2
- package/src/index.ts +1 -0
- package/src/protocol/conformance/frames.ts +32 -4
- package/src/protocol/index.ts +4 -0
- package/src/protocol/payloads.test.ts +31 -1
- package/src/protocol/payloads.ts +110 -7
- package/src/session/acp/client.test.ts +222 -0
- package/src/session/acp/client.ts +356 -0
- package/src/session/acp/fake-agent.ts +71 -0
- package/src/session/acp/index.ts +68 -0
- package/src/session/acp/integration.test.ts +37 -0
- package/src/session/acp/jsonrpc.test.ts +75 -0
- package/src/session/acp/jsonrpc.ts +171 -0
- package/src/session/acp/normalize.test.ts +150 -0
- package/src/session/acp/normalize.ts +204 -0
- package/src/session/acp/protocol.ts +178 -0
- package/src/session/acp/spawn.test.ts +45 -0
- package/src/session/acp/spawn.ts +91 -0
- package/src/session/acp/transport.test.ts +82 -0
- package/src/session/acp/transport.ts +155 -0
- package/src/session/adapter.ts +159 -0
- package/src/session/backend.test.ts +198 -0
- package/src/session/backend.ts +128 -0
- package/src/session/events.test.ts +168 -0
- package/src/session/events.ts +347 -0
- package/src/session/index.ts +67 -0
- package/src/session/log.test.ts +215 -0
- package/src/session/log.ts +525 -0
- package/src/session/normalizer/backend-integration.test.ts +103 -0
- package/src/session/normalizer/claude.test.ts +68 -0
- package/src/session/normalizer/claude.ts +136 -0
- package/src/session/normalizer/copilot.test.ts +59 -0
- package/src/session/normalizer/copilot.ts +133 -0
- package/src/session/normalizer/deepseek.ts +80 -0
- package/src/session/normalizer/index.ts +61 -0
- package/src/session/normalizer/kimi.ts +82 -0
- package/src/session/normalizer/link.test.ts +24 -0
- package/src/session/normalizer/link.ts +81 -0
- package/src/session/normalizer/pi.ts +75 -0
- package/src/session/normalizer/probe.test.ts +49 -0
- package/src/session/normalizer/qwen.test.ts +20 -0
- package/src/session/normalizer/qwen.ts +77 -0
- package/src/session/normalizer/record.test.ts +68 -0
- package/src/session/normalizer/record.ts +88 -0
- package/src/session/normalizer/resume.test.ts +25 -0
- package/src/session/normalizer/types.ts +152 -0
- package/src/session/normalizer/vectors.test.ts +180 -0
- package/src/session/schema.test.ts +84 -0
- package/src/session/schema.ts +78 -0
- package/src/session/test-db.ts +56 -0
- package/dist/blackboard/test-db.d.ts +0 -5
- package/dist/blackboard/test-db.js +0 -42
- package/dist/presence/test-db.d.ts +0 -5
- package/dist/presence/test-db.js +0 -42
- package/dist/transcript/test-db.d.ts +0 -5
- package/dist/transcript/test-db.js +0 -41
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ The **Nano agentic protocol** (ADR 0056): one app-tier channel carrying agent pr
|
|
|
16
16
|
| `@nanobpm/agentic/transcript` | Transcript store, retention-by-lifecycle (S6) |
|
|
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
|
|
package/dist/demand/model.d.ts
CHANGED
|
@@ -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
|
|
38
|
-
* (non-agentic) C8 jobs the engine also runs
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
}
|
package/dist/demand/model.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
package/dist/demand/taskdef.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/demand/taskdef.js
CHANGED
|
@@ -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-
|
|
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",
|
|
84
|
+
payload: { op: "produce", stream: "job-1223", incarnation: 1, chunk: "hello world\n" },
|
|
85
85
|
},
|
|
86
|
-
hex: "
|
|
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
|
|
135
|
+
direction: "hub->worker",
|
|
108
136
|
frame: {
|
|
109
137
|
lane: "bulk",
|
|
110
138
|
family: "relay",
|
package/dist/protocol/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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;
|