@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.
Files changed (117) hide show
  1. package/README.md +1 -0
  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/package.json +17 -1
  59. package/src/demand/model.test.ts +82 -4
  60. package/src/demand/model.ts +30 -9
  61. package/src/demand/taskdef.test.ts +51 -6
  62. package/src/demand/taskdef.ts +31 -2
  63. package/src/index.ts +1 -0
  64. package/src/protocol/conformance/frames.ts +32 -4
  65. package/src/protocol/index.ts +4 -0
  66. package/src/protocol/payloads.test.ts +31 -1
  67. package/src/protocol/payloads.ts +110 -7
  68. package/src/session/acp/client.test.ts +222 -0
  69. package/src/session/acp/client.ts +356 -0
  70. package/src/session/acp/fake-agent.ts +71 -0
  71. package/src/session/acp/index.ts +68 -0
  72. package/src/session/acp/integration.test.ts +37 -0
  73. package/src/session/acp/jsonrpc.test.ts +75 -0
  74. package/src/session/acp/jsonrpc.ts +171 -0
  75. package/src/session/acp/normalize.test.ts +150 -0
  76. package/src/session/acp/normalize.ts +204 -0
  77. package/src/session/acp/protocol.ts +178 -0
  78. package/src/session/acp/spawn.test.ts +45 -0
  79. package/src/session/acp/spawn.ts +91 -0
  80. package/src/session/acp/transport.test.ts +82 -0
  81. package/src/session/acp/transport.ts +155 -0
  82. package/src/session/adapter.ts +159 -0
  83. package/src/session/backend.test.ts +198 -0
  84. package/src/session/backend.ts +128 -0
  85. package/src/session/events.test.ts +168 -0
  86. package/src/session/events.ts +347 -0
  87. package/src/session/index.ts +67 -0
  88. package/src/session/log.test.ts +215 -0
  89. package/src/session/log.ts +525 -0
  90. package/src/session/normalizer/backend-integration.test.ts +103 -0
  91. package/src/session/normalizer/claude.test.ts +68 -0
  92. package/src/session/normalizer/claude.ts +136 -0
  93. package/src/session/normalizer/copilot.test.ts +59 -0
  94. package/src/session/normalizer/copilot.ts +133 -0
  95. package/src/session/normalizer/deepseek.ts +80 -0
  96. package/src/session/normalizer/index.ts +61 -0
  97. package/src/session/normalizer/kimi.ts +82 -0
  98. package/src/session/normalizer/link.test.ts +24 -0
  99. package/src/session/normalizer/link.ts +81 -0
  100. package/src/session/normalizer/pi.ts +75 -0
  101. package/src/session/normalizer/probe.test.ts +49 -0
  102. package/src/session/normalizer/qwen.test.ts +20 -0
  103. package/src/session/normalizer/qwen.ts +77 -0
  104. package/src/session/normalizer/record.test.ts +68 -0
  105. package/src/session/normalizer/record.ts +88 -0
  106. package/src/session/normalizer/resume.test.ts +25 -0
  107. package/src/session/normalizer/types.ts +152 -0
  108. package/src/session/normalizer/vectors.test.ts +180 -0
  109. package/src/session/schema.test.ts +84 -0
  110. package/src/session/schema.ts +78 -0
  111. package/src/session/test-db.ts +56 -0
  112. package/dist/blackboard/test-db.d.ts +0 -5
  113. package/dist/blackboard/test-db.js +0 -42
  114. package/dist/presence/test-db.d.ts +0 -5
  115. package/dist/presence/test-db.js +0 -42
  116. package/dist/transcript/test-db.d.ts +0 -5
  117. package/dist/transcript/test-db.js +0 -41
@@ -0,0 +1,68 @@
1
+ import { asRecord, optNumber, optString, reqString } from "./record.js";
2
+ const HARNESS = "deepseek";
3
+ function toDrafts(record) {
4
+ const obj = asRecord(HARNESS, record);
5
+ switch (obj.kind) {
6
+ case "message": {
7
+ const text = optString(HARNESS, obj, "content") ?? optString(HARNESS, obj, "text");
8
+ if (text === undefined || text.length === 0)
9
+ return [];
10
+ const role = optString(HARNESS, obj, "role");
11
+ return [{ type: role === "user" ? "user" : role === "system" ? "system" : "assistant", text }];
12
+ }
13
+ case "reasoning": {
14
+ const text = optString(HARNESS, obj, "content") ?? optString(HARNESS, obj, "text");
15
+ const providerContinuation = optString(HARNESS, obj, "continuation");
16
+ return [
17
+ {
18
+ type: "reasoning",
19
+ ...(text !== undefined ? { text } : {}),
20
+ ...(providerContinuation !== undefined ? { providerContinuation } : {}),
21
+ },
22
+ ];
23
+ }
24
+ case "tool": {
25
+ const callId = reqString(HARNESS, obj, "id");
26
+ return [
27
+ {
28
+ type: "tool-call",
29
+ id: `call:${callId}`,
30
+ callId,
31
+ name: reqString(HARNESS, obj, "name"),
32
+ args: obj.arguments ?? obj.args,
33
+ },
34
+ ];
35
+ }
36
+ case "tool_result": {
37
+ const callId = reqString(HARNESS, obj, "id");
38
+ return [
39
+ {
40
+ type: "tool-result",
41
+ id: `result:${callId}`,
42
+ callId,
43
+ ok: obj.ok !== false,
44
+ result: obj.output ?? obj.result,
45
+ },
46
+ ];
47
+ }
48
+ case "usage":
49
+ return [
50
+ {
51
+ type: "usage",
52
+ inputTokens: optNumber(HARNESS, obj, "input") ?? optNumber(HARNESS, obj, "inputTokens") ?? 0,
53
+ outputTokens: optNumber(HARNESS, obj, "output") ?? optNumber(HARNESS, obj, "outputTokens") ?? 0,
54
+ ...(typeof obj.model === "string" ? { model: obj.model } : {}),
55
+ },
56
+ ];
57
+ default:
58
+ return [];
59
+ }
60
+ }
61
+ export const deepseekNormalizer = {
62
+ harness: HARNESS,
63
+ capabilities: { streaming: true, resumeById: true },
64
+ toDrafts,
65
+ resume(sessionId) {
66
+ return { transport: "sdk", sessionId, call: "restore", args: [sessionId] };
67
+ },
68
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `@nanobpm/agentic/session/normalizer` — ADR 0062 slice 3 public surface.
3
+ *
4
+ * The `stream-json`/native-transcript fallback ingestion backend: the per-harness
5
+ * {@link HarnessNormalizer} contract, the shared causal-chain linker
6
+ * ({@link normalizeSession}/{@link linkDrafts}), the capability probe
7
+ * ({@link capabilityProbe}), and the current-fleet registry that enumerates the
8
+ * harnesses this slice ships an adapter for. A caller (the ingestion boundary,
9
+ * slice 5's enrolment gate) looks a harness up by id and gets its normalizer;
10
+ * {@link probeFleet} folds the whole registry into `durable-resume`
11
+ * advertisements in one call.
12
+ *
13
+ * The registry is derived from the normalizer modules themselves (each is keyed
14
+ * by its own `harness` id), so there is no second hand-maintained list of harness
15
+ * names to drift against.
16
+ */
17
+ export type { CapabilityAdvertisement, DistributiveOmit, DraftEvent, HarnessCapabilities, HarnessNormalizer, ResumeShim, } from "./types.ts";
18
+ export { capabilityProbe, NormalizerDialectError } from "./types.ts";
19
+ export type { LinkOptions } from "./link.ts";
20
+ export { linkDrafts, normalizeSession } from "./link.ts";
21
+ import { claudeNormalizer } from "./claude.ts";
22
+ import { copilotNormalizer } from "./copilot.ts";
23
+ import { deepseekNormalizer } from "./deepseek.ts";
24
+ import { kimiNormalizer } from "./kimi.ts";
25
+ import { piNormalizer } from "./pi.ts";
26
+ import { qwenNormalizer } from "./qwen.ts";
27
+ import { type CapabilityAdvertisement, type HarnessNormalizer } from "./types.ts";
28
+ export { claudeNormalizer, copilotNormalizer, deepseekNormalizer, kimiNormalizer, piNormalizer, qwenNormalizer };
29
+ /** Every fallback normalizer in the current fleet, in reference-dialect order. */
30
+ export declare const FLEET_NORMALIZERS: readonly HarnessNormalizer[];
31
+ /** Registry keyed by `harness` id, derived from {@link FLEET_NORMALIZERS}. */
32
+ export declare const NORMALIZER_REGISTRY: ReadonlyMap<string, HarnessNormalizer>;
33
+ /** Look a harness's normalizer up by id, or `undefined` when it is not in the fleet. */
34
+ export declare function normalizerFor(harness: string): HarnessNormalizer | undefined;
35
+ /** Probe every harness in the fleet for its derived `durable-resume` advertisement. */
36
+ export declare function probeFleet(): readonly CapabilityAdvertisement[];
@@ -0,0 +1,29 @@
1
+ export { capabilityProbe, NormalizerDialectError } from "./types.js";
2
+ export { linkDrafts, normalizeSession } from "./link.js";
3
+ import { claudeNormalizer } from "./claude.js";
4
+ import { copilotNormalizer } from "./copilot.js";
5
+ import { deepseekNormalizer } from "./deepseek.js";
6
+ import { kimiNormalizer } from "./kimi.js";
7
+ import { piNormalizer } from "./pi.js";
8
+ import { qwenNormalizer } from "./qwen.js";
9
+ import { capabilityProbe } from "./types.js";
10
+ export { claudeNormalizer, copilotNormalizer, deepseekNormalizer, kimiNormalizer, piNormalizer, qwenNormalizer };
11
+ /** Every fallback normalizer in the current fleet, in reference-dialect order. */
12
+ export const FLEET_NORMALIZERS = [
13
+ copilotNormalizer,
14
+ claudeNormalizer,
15
+ qwenNormalizer,
16
+ kimiNormalizer,
17
+ piNormalizer,
18
+ deepseekNormalizer,
19
+ ];
20
+ /** Registry keyed by `harness` id, derived from {@link FLEET_NORMALIZERS}. */
21
+ export const NORMALIZER_REGISTRY = new Map(FLEET_NORMALIZERS.map((n) => [n.harness, n]));
22
+ /** Look a harness's normalizer up by id, or `undefined` when it is not in the fleet. */
23
+ export function normalizerFor(harness) {
24
+ return NORMALIZER_REGISTRY.get(harness);
25
+ }
26
+ /** Probe every harness in the fleet for its derived `durable-resume` advertisement. */
27
+ export function probeFleet() {
28
+ return FLEET_NORMALIZERS.map(capabilityProbe);
29
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Kimi normalizer — ADR 0062 slice 3.
3
+ *
4
+ * Driven with `kimi -p --output-format stream-json`. Kimi tags each frame with an
5
+ * `event` discriminator and folds a tool call and its result under one `tool`
6
+ * event distinguished by `phase`. Restore is `-S [id]` (`-c` continues the
7
+ * latest). Streaming + resume-by-id → `durable-resume`.
8
+ */
9
+ import { type HarnessNormalizer } from "./types.ts";
10
+ export declare const kimiNormalizer: HarnessNormalizer;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Kimi normalizer — ADR 0062 slice 3.
3
+ *
4
+ * Driven with `kimi -p --output-format stream-json`. Kimi tags each frame with an
5
+ * `event` discriminator and folds a tool call and its result under one `tool`
6
+ * event distinguished by `phase`. Restore is `-S [id]` (`-c` continues the
7
+ * latest). Streaming + resume-by-id → `durable-resume`.
8
+ */
9
+ import { NormalizerDialectError } from "./types.js";
10
+ import { asRecord, optNumber, optString, reqString } from "./record.js";
11
+ const HARNESS = "kimi";
12
+ function toDrafts(record) {
13
+ const obj = asRecord(HARNESS, record);
14
+ switch (obj.event) {
15
+ case "text": {
16
+ const text = optString(HARNESS, obj, "text");
17
+ if (text === undefined || text.length === 0)
18
+ return [];
19
+ const role = optString(HARNESS, obj, "role");
20
+ return [{ type: role === "user" ? "user" : role === "system" ? "system" : "assistant", text }];
21
+ }
22
+ case "reasoning": {
23
+ const text = optString(HARNESS, obj, "text");
24
+ const providerContinuation = optString(HARNESS, obj, "continuation");
25
+ return [
26
+ {
27
+ type: "reasoning",
28
+ ...(text !== undefined ? { text } : {}),
29
+ ...(providerContinuation !== undefined ? { providerContinuation } : {}),
30
+ },
31
+ ];
32
+ }
33
+ case "tool": {
34
+ const callId = reqString(HARNESS, obj, "id");
35
+ const phase = optString(HARNESS, obj, "phase");
36
+ if (phase === "result") {
37
+ return [
38
+ {
39
+ type: "tool-result",
40
+ id: `result:${callId}`,
41
+ callId,
42
+ ok: obj.ok !== false,
43
+ result: obj.output ?? obj.result,
44
+ },
45
+ ];
46
+ }
47
+ if (phase === "call" || phase === undefined) {
48
+ return [
49
+ {
50
+ type: "tool-call",
51
+ id: `call:${callId}`,
52
+ callId,
53
+ name: reqString(HARNESS, obj, "name"),
54
+ args: obj.arguments ?? obj.args,
55
+ },
56
+ ];
57
+ }
58
+ throw new NormalizerDialectError(HARNESS, `unknown tool phase ${JSON.stringify(phase)}`);
59
+ }
60
+ case "usage":
61
+ return [
62
+ {
63
+ type: "usage",
64
+ inputTokens: optNumber(HARNESS, obj, "prompt_tokens") ?? 0,
65
+ outputTokens: optNumber(HARNESS, obj, "completion_tokens") ?? 0,
66
+ ...(typeof obj.model === "string" ? { model: obj.model } : {}),
67
+ },
68
+ ];
69
+ default:
70
+ return [];
71
+ }
72
+ }
73
+ export const kimiNormalizer = {
74
+ harness: HARNESS,
75
+ capabilities: { streaming: true, resumeById: true },
76
+ toDrafts,
77
+ resume(sessionId) {
78
+ return { transport: "cli", sessionId, args: ["-S", sessionId] };
79
+ },
80
+ };
@@ -0,0 +1,36 @@
1
+ import { type SessionEvent } from "../events.ts";
2
+ import type { DraftEvent, HarnessNormalizer } from "./types.ts";
3
+ export interface LinkOptions {
4
+ /**
5
+ * Id generator for drafts that do not carry a native id. Default is
6
+ * `crypto.randomUUID`, which is unique-safe across repeated
7
+ * `normalizeSession()` calls for one session (e.g. per chunk / per resume
8
+ * leg); inject a deterministic generator in tests when stable ids are needed.
9
+ * It is only consulted when a draft omits `id`.
10
+ */
11
+ newId?: () => string;
12
+ /**
13
+ * The `parentId` the first linked event points at — the causal predecessor
14
+ * this transcript continues from. `null` (default) starts a fresh chain; on a
15
+ * resume the caller passes the last restored event's id so the new events
16
+ * continue the same chain across the resume boundary.
17
+ */
18
+ parentId?: string | null;
19
+ }
20
+ /**
21
+ * Thread a flat list of {@link DraftEvent}s into a causal chain of canonical
22
+ * {@link SessionEvent}s: each event's `id` is its native id when it supplied one
23
+ * else a freshly generated id, and its `parentId` is the id of the event before
24
+ * it (or {@link LinkOptions.parentId} for the first). Offsets are *not* assigned
25
+ * here — the authoritative log owns those on `emit` (slice 1); this only records
26
+ * causality (`parentId`), which survives compaction.
27
+ */
28
+ export declare function linkDrafts(drafts: readonly DraftEvent[], options?: LinkOptions): SessionEvent[];
29
+ /**
30
+ * Normalize a whole native transcript through a harness normalizer: map every
31
+ * record to drafts, then {@link linkDrafts} the concatenation into one canonical
32
+ * causal chain. This is the fallback backend's public ingestion call — the
33
+ * mirror of the ACP backend's `session/update` stream, producing the same
34
+ * {@link SessionEvent}s a caller feeds to a slice-1 `SessionAdapter.emit`.
35
+ */
36
+ export declare function normalizeSession(normalizer: HarnessNormalizer, records: Iterable<unknown>, options?: LinkOptions): SessionEvent[];
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The shared causal-chain linker + stream driver every dialect reuses — ADR 0062
3
+ * slice 3.
4
+ *
5
+ * A {@link HarnessNormalizer} produces {@link DraftEvent}s (semantics only); the
6
+ * *chaining* — assigning each event a stable `id` and stamping `parentId` as the
7
+ * id of its predecessor — is identical across every harness dialect, so it lives
8
+ * here exactly once (derivation over duplication, AGENTS.md). {@link normalizeSession}
9
+ * is the one entry point a caller uses: it maps a whole native transcript through
10
+ * a normalizer, links the drafts into a causal chain, and validates every result
11
+ * against the slice-1 boundary ({@link parseSessionEvent}) so a dialect can never
12
+ * emit a shape that is not a canonical {@link SessionEvent}.
13
+ */
14
+ import { randomUUID } from "node:crypto";
15
+ import { parseSessionEvent } from "../events.js";
16
+ /**
17
+ * Thread a flat list of {@link DraftEvent}s into a causal chain of canonical
18
+ * {@link SessionEvent}s: each event's `id` is its native id when it supplied one
19
+ * else a freshly generated id, and its `parentId` is the id of the event before
20
+ * it (or {@link LinkOptions.parentId} for the first). Offsets are *not* assigned
21
+ * here — the authoritative log owns those on `emit` (slice 1); this only records
22
+ * causality (`parentId`), which survives compaction.
23
+ */
24
+ export function linkDrafts(drafts, options = {}) {
25
+ const newId = options.newId ?? randomUUID;
26
+ let parentId = options.parentId ?? null;
27
+ const linked = [];
28
+ for (const draft of drafts) {
29
+ const { id: nativeId, ...rest } = draft;
30
+ const id = nativeId ?? newId();
31
+ // Re-validate the fully-formed event at the slice-1 boundary: `rest` carries
32
+ // the discriminant + payload, and we add the chain fields. parseSessionEvent
33
+ // *builds* the union member field-by-field (never an as-cast), so a dialect
34
+ // bug surfaces here as a loud SessionEventShapeError instead of a bad row.
35
+ const event = parseSessionEvent({ ...rest, id, parentId });
36
+ linked.push(event);
37
+ parentId = id;
38
+ }
39
+ return linked;
40
+ }
41
+ /**
42
+ * Normalize a whole native transcript through a harness normalizer: map every
43
+ * record to drafts, then {@link linkDrafts} the concatenation into one canonical
44
+ * causal chain. This is the fallback backend's public ingestion call — the
45
+ * mirror of the ACP backend's `session/update` stream, producing the same
46
+ * {@link SessionEvent}s a caller feeds to a slice-1 `SessionAdapter.emit`.
47
+ */
48
+ export function normalizeSession(normalizer, records, options = {}) {
49
+ const drafts = [];
50
+ for (const record of records) {
51
+ for (const draft of normalizer.toDrafts(record)) {
52
+ drafts.push(draft);
53
+ }
54
+ }
55
+ return linkDrafts(drafts, options);
56
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * pi / little-coder normalizer — ADR 0062 slice 3.
3
+ *
4
+ * Driven with `pi -p --mode json` (or `--mode rpc`): a JSON-RPC 2.0 notification
5
+ * stream. Session content arrives as `method` notifications
6
+ * (`session/message`, `session/reasoning`, `session/toolCall`,
7
+ * `session/toolResult`, `session/usage`) with a `params` payload. Restore is
8
+ * `--session-id <id>` (create-if-missing — the same flag both resumes an
9
+ * existing session and starts one under a chosen id); `-r` is the short alias and
10
+ * `--fork` branches. Streaming + resume-by-id → `durable-resume`.
11
+ */
12
+ import type { HarnessNormalizer } from "./types.ts";
13
+ export declare const piNormalizer: HarnessNormalizer;
@@ -0,0 +1,61 @@
1
+ import { asRecord, isRecord, optNumber, optString, reqString } from "./record.js";
2
+ const HARNESS = "pi";
3
+ function params(obj) {
4
+ return isRecord(obj.params) ? obj.params : {};
5
+ }
6
+ function toDrafts(record) {
7
+ const obj = asRecord(HARNESS, record);
8
+ const method = obj.method;
9
+ if (typeof method !== "string")
10
+ return []; // a JSON-RPC result/ack, not a notification
11
+ const p = params(obj);
12
+ switch (method) {
13
+ case "session/message": {
14
+ const text = optString(HARNESS, p, "text");
15
+ if (text === undefined || text.length === 0)
16
+ return [];
17
+ const role = optString(HARNESS, p, "role");
18
+ return [{ type: role === "user" ? "user" : role === "system" ? "system" : "assistant", text }];
19
+ }
20
+ case "session/reasoning": {
21
+ const text = optString(HARNESS, p, "text");
22
+ const providerContinuation = optString(HARNESS, p, "continuation");
23
+ return [
24
+ {
25
+ type: "reasoning",
26
+ ...(text !== undefined ? { text } : {}),
27
+ ...(providerContinuation !== undefined ? { providerContinuation } : {}),
28
+ },
29
+ ];
30
+ }
31
+ case "session/toolCall": {
32
+ const callId = reqString(HARNESS, p, "id");
33
+ return [{ type: "tool-call", id: `call:${callId}`, callId, name: reqString(HARNESS, p, "name"), args: p.args }];
34
+ }
35
+ case "session/toolResult": {
36
+ const callId = reqString(HARNESS, p, "id");
37
+ return [{ type: "tool-result", id: `result:${callId}`, callId, ok: p.ok !== false, result: p.result }];
38
+ }
39
+ case "session/usage":
40
+ return [
41
+ {
42
+ type: "usage",
43
+ inputTokens: optNumber(HARNESS, p, "inputTokens") ?? 0,
44
+ outputTokens: optNumber(HARNESS, p, "outputTokens") ?? 0,
45
+ ...(typeof p.model === "string" ? { model: p.model } : {}),
46
+ },
47
+ ];
48
+ default:
49
+ return [];
50
+ }
51
+ }
52
+ export const piNormalizer = {
53
+ harness: HARNESS,
54
+ capabilities: { streaming: true, resumeById: true },
55
+ toDrafts,
56
+ resume(sessionId) {
57
+ // create-if-missing: `--session-id <id>` resumes it when it exists and starts
58
+ // it under that id when it does not.
59
+ return { transport: "cli", sessionId, args: ["--session-id", sessionId] };
60
+ },
61
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Qwen Code normalizer — ADR 0062 slice 3.
3
+ *
4
+ * Driven with `qwen -p -o stream-json`. Qwen Code descends from the Gemini CLI,
5
+ * so its streaming dialect speaks that lineage: `content` frames tagged with a
6
+ * `role`, `thought` frames for reasoning, and `tool_call_request` /
7
+ * `tool_call_response` pairs. Restore is `-r <id>` (`-c` continues the latest).
8
+ * Streaming + resume-by-id → `durable-resume`.
9
+ */
10
+ import type { HarnessNormalizer } from "./types.ts";
11
+ export declare const qwenNormalizer: HarnessNormalizer;
@@ -0,0 +1,65 @@
1
+ import { asRecord, optNumber, optString, reqString } from "./record.js";
2
+ const HARNESS = "qwen-code";
3
+ function toDrafts(record) {
4
+ const obj = asRecord(HARNESS, record);
5
+ switch (obj.type) {
6
+ case "content": {
7
+ const role = optString(HARNESS, obj, "role");
8
+ const text = optString(HARNESS, obj, "text");
9
+ if (text === undefined || text.length === 0)
10
+ return [];
11
+ return [{ type: role === "user" ? "user" : role === "system" ? "system" : "assistant", text }];
12
+ }
13
+ case "thought": {
14
+ // Gemini-lineage reasoning: a bold `subject` + `description` body.
15
+ const subject = optString(HARNESS, obj, "subject");
16
+ const description = optString(HARNESS, obj, "description");
17
+ const text = [subject, description].filter((s) => s !== undefined && s.length > 0).join(": ");
18
+ const providerContinuation = optString(HARNESS, obj, "thoughtSignature");
19
+ return [
20
+ {
21
+ type: "reasoning",
22
+ ...(text.length > 0 ? { text } : {}),
23
+ ...(providerContinuation !== undefined ? { providerContinuation } : {}),
24
+ },
25
+ ];
26
+ }
27
+ case "tool_call_request": {
28
+ const callId = reqString(HARNESS, obj, "callId");
29
+ return [
30
+ { type: "tool-call", id: `call:${callId}`, callId, name: reqString(HARNESS, obj, "name"), args: obj.args },
31
+ ];
32
+ }
33
+ case "tool_call_response": {
34
+ const callId = reqString(HARNESS, obj, "callId");
35
+ return [
36
+ {
37
+ type: "tool-result",
38
+ id: `result:${callId}`,
39
+ callId,
40
+ ok: obj.error == null,
41
+ result: obj.responseParts ?? obj.error,
42
+ },
43
+ ];
44
+ }
45
+ case "usage_metadata":
46
+ return [
47
+ {
48
+ type: "usage",
49
+ inputTokens: optNumber(HARNESS, obj, "promptTokenCount") ?? 0,
50
+ outputTokens: optNumber(HARNESS, obj, "candidatesTokenCount") ?? 0,
51
+ ...(typeof obj.model === "string" ? { model: obj.model } : {}),
52
+ },
53
+ ];
54
+ default:
55
+ return [];
56
+ }
57
+ }
58
+ export const qwenNormalizer = {
59
+ harness: HARNESS,
60
+ capabilities: { streaming: true, resumeById: true },
61
+ toDrafts,
62
+ resume(sessionId) {
63
+ return { transport: "cli", sessionId, args: ["-r", sessionId] };
64
+ },
65
+ };
@@ -0,0 +1,21 @@
1
+ /** True for a plain (non-array) object; the shape every native record must have. */
2
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
3
+ /** Narrow to a record or throw a dialect error attributing the harness. */
4
+ export declare function asRecord(harness: string, value: unknown): Record<string, unknown>;
5
+ /** Read a required string field or throw a dialect error. */
6
+ export declare function reqString(harness: string, obj: Record<string, unknown>, field: string): string;
7
+ /** Read an optional string field (undefined when absent), throwing on a wrong type. */
8
+ export declare function optString(harness: string, obj: Record<string, unknown>, field: string): string | undefined;
9
+ /** Read an optional finite number field, coercing absent/null to `undefined`. */
10
+ export declare function optNumber(harness: string, obj: Record<string, unknown>, field: string): number | undefined;
11
+ /** Narrow to an array or throw a dialect error. */
12
+ export declare function asArray(harness: string, value: unknown, what: string): readonly unknown[];
13
+ /**
14
+ * Collapse a message-content value to plain text. Harnesses model an assistant
15
+ * message either as a bare string or as an array of typed content parts
16
+ * (`{ type: "text", text }`); we concatenate the text parts and ignore the rest
17
+ * (tool-use parts are lifted to their own events by the dialect). Returns
18
+ * `undefined` when there is no text to emit so the dialect can skip an empty
19
+ * message.
20
+ */
21
+ export declare function contentText(value: unknown): string | undefined;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Small untyped-record inspection helpers shared by the per-harness dialect maps
3
+ * (ADR 0062 slice 3). A native record is untyped input (a `JSON.parse`d
4
+ * `stream-json` line, an SDK event object), so every dialect narrows it the same
5
+ * way: is-it-an-object, read-a-string, read-an-optional-string. Centralising the
6
+ * narrowing keeps the dialects declarative and, crucially, keeps them free of
7
+ * `as`-casts (AGENTS.md) — they narrow through these guards instead.
8
+ */
9
+ import { NormalizerDialectError } from "./types.js";
10
+ /** True for a plain (non-array) object; the shape every native record must have. */
11
+ export function isRecord(value) {
12
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13
+ }
14
+ /** Describe a value for a dialect error: distinguishes `null` and `array` from the bare `typeof`. */
15
+ function describeType(value) {
16
+ if (value === null)
17
+ return "null";
18
+ if (Array.isArray(value))
19
+ return "array";
20
+ return typeof value;
21
+ }
22
+ /** Narrow to a record or throw a dialect error attributing the harness. */
23
+ export function asRecord(harness, value) {
24
+ if (!isRecord(value)) {
25
+ throw new NormalizerDialectError(harness, `record must be a plain object, got ${describeType(value)}`);
26
+ }
27
+ return value;
28
+ }
29
+ /** Read a required string field or throw a dialect error. */
30
+ export function reqString(harness, obj, field) {
31
+ const v = obj[field];
32
+ if (typeof v !== "string") {
33
+ throw new NormalizerDialectError(harness, `field "${field}" must be a string, got ${describeType(v)}`);
34
+ }
35
+ return v;
36
+ }
37
+ /** Read an optional string field (undefined when absent), throwing on a wrong type. */
38
+ export function optString(harness, obj, field) {
39
+ const v = obj[field];
40
+ if (v === undefined || v === null)
41
+ return undefined;
42
+ if (typeof v !== "string") {
43
+ throw new NormalizerDialectError(harness, `field "${field}" must be a string when present, got ${describeType(v)}`);
44
+ }
45
+ return v;
46
+ }
47
+ /** Read an optional finite number field, coercing absent/null to `undefined`. */
48
+ export function optNumber(harness, obj, field) {
49
+ const v = obj[field];
50
+ if (v === undefined || v === null)
51
+ return undefined;
52
+ if (typeof v !== "number" || !Number.isFinite(v)) {
53
+ throw new NormalizerDialectError(harness, `field "${field}" must be a finite number when present, got ${describeType(v)}`);
54
+ }
55
+ return v;
56
+ }
57
+ /** Narrow to an array or throw a dialect error. */
58
+ export function asArray(harness, value, what) {
59
+ if (!Array.isArray(value)) {
60
+ throw new NormalizerDialectError(harness, `${what} must be an array, got ${describeType(value)}`);
61
+ }
62
+ return value;
63
+ }
64
+ /**
65
+ * Collapse a message-content value to plain text. Harnesses model an assistant
66
+ * message either as a bare string or as an array of typed content parts
67
+ * (`{ type: "text", text }`); we concatenate the text parts and ignore the rest
68
+ * (tool-use parts are lifted to their own events by the dialect). Returns
69
+ * `undefined` when there is no text to emit so the dialect can skip an empty
70
+ * message.
71
+ */
72
+ export function contentText(value) {
73
+ if (typeof value === "string")
74
+ return value.length > 0 ? value : undefined;
75
+ if (!Array.isArray(value))
76
+ return undefined;
77
+ let text = "";
78
+ for (const part of value) {
79
+ if (typeof part === "string") {
80
+ text += part;
81
+ }
82
+ else if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
83
+ text += part.text;
84
+ }
85
+ }
86
+ return text.length > 0 ? text : undefined;
87
+ }