@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
|
@@ -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
|
+
}
|