@tanstack/ai-sandbox 0.2.3 → 0.3.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/dist/esm/agents-file.js +53 -34
- package/dist/esm/agents-file.js.map +1 -1
- package/dist/esm/align.d.ts +121 -0
- package/dist/esm/align.js +197 -0
- package/dist/esm/align.js.map +1 -0
- package/dist/esm/approvals.js +63 -29
- package/dist/esm/approvals.js.map +1 -1
- package/dist/esm/attach-preflight.d.ts +85 -0
- package/dist/esm/attach-preflight.js +189 -0
- package/dist/esm/attach-preflight.js.map +1 -0
- package/dist/esm/bootstrap.js +103 -117
- package/dist/esm/bootstrap.js.map +1 -1
- package/dist/esm/bridge-events.js +96 -71
- package/dist/esm/bridge-events.js.map +1 -1
- package/dist/esm/capabilities.d.ts +0 -5
- package/dist/esm/capabilities.js +32 -28
- package/dist/esm/capabilities.js.map +1 -1
- package/dist/esm/chunk-identity.d.ts +52 -0
- package/dist/esm/chunk-identity.js +102 -0
- package/dist/esm/chunk-identity.js.map +1 -0
- package/dist/esm/claim.d.ts +187 -0
- package/dist/esm/claim.js +349 -0
- package/dist/esm/claim.js.map +1 -0
- package/dist/esm/contracts.d.ts +13 -0
- package/dist/esm/driver.d.ts +83 -0
- package/dist/esm/driver.js +138 -0
- package/dist/esm/driver.js.map +1 -0
- package/dist/esm/durability.d.ts +263 -0
- package/dist/esm/durability.js +230 -0
- package/dist/esm/durability.js.map +1 -0
- package/dist/esm/errors.js +28 -24
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/file-diff.js +151 -135
- package/dist/esm/file-diff.js.map +1 -1
- package/dist/esm/git-exec.js +51 -62
- package/dist/esm/git-exec.js.map +1 -1
- package/dist/esm/harness-cwd.js +24 -19
- package/dist/esm/harness-cwd.js.map +1 -1
- package/dist/esm/index.d.ts +30 -8
- package/dist/esm/index.js +23 -91
- package/dist/esm/instance-store.d.ts +88 -0
- package/dist/esm/instance-store.js +67 -0
- package/dist/esm/instance-store.js.map +1 -0
- package/dist/esm/journal-bytes.d.ts +67 -0
- package/dist/esm/journal-bytes.js +110 -0
- package/dist/esm/journal-bytes.js.map +1 -0
- package/dist/esm/journal-reader.d.ts +66 -0
- package/dist/esm/journal-reader.js +228 -0
- package/dist/esm/journal-reader.js.map +1 -0
- package/dist/esm/journal-sweep.d.ts +113 -0
- package/dist/esm/journal-sweep.js +309 -0
- package/dist/esm/journal-sweep.js.map +1 -0
- package/dist/esm/journal.d.ts +542 -0
- package/dist/esm/journal.js +679 -0
- package/dist/esm/journal.js.map +1 -0
- package/dist/esm/key.js +36 -33
- package/dist/esm/key.js.map +1 -1
- package/dist/esm/middleware.d.ts +50 -2
- package/dist/esm/middleware.js +335 -208
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/ngrok.js +75 -49
- package/dist/esm/ngrok.js.map +1 -1
- package/dist/esm/policy.js +43 -34
- package/dist/esm/policy.js.map +1 -1
- package/dist/esm/projection.js +16 -8
- package/dist/esm/projection.js.map +1 -1
- package/dist/esm/reap.d.ts +238 -0
- package/dist/esm/reap.js +355 -0
- package/dist/esm/reap.js.map +1 -0
- package/dist/esm/reclaim.d.ts +84 -0
- package/dist/esm/reclaim.js +106 -0
- package/dist/esm/reclaim.js.map +1 -0
- package/dist/esm/remote-tools.js +73 -62
- package/dist/esm/remote-tools.js.map +1 -1
- package/dist/esm/run.d.ts +93 -25
- package/dist/esm/run.js +274 -79
- package/dist/esm/run.js.map +1 -1
- package/dist/esm/runner.d.ts +119 -2
- package/dist/esm/runner.js +270 -51
- package/dist/esm/runner.js.map +1 -1
- package/dist/esm/sandbox.d.ts +3 -2
- package/dist/esm/sandbox.js +139 -123
- package/dist/esm/sandbox.js.map +1 -1
- package/dist/esm/secrets.js +39 -47
- package/dist/esm/secrets.js.map +1 -1
- package/dist/esm/setup-plan.js +22 -14
- package/dist/esm/setup-plan.js.map +1 -1
- package/dist/esm/shell.d.ts +8 -0
- package/dist/esm/shell.js +197 -158
- package/dist/esm/shell.js.map +1 -1
- package/dist/esm/testkit/conformance.d.ts +16 -0
- package/dist/esm/testkit/conformance.js +97 -0
- package/dist/esm/testkit/conformance.js.map +1 -0
- package/dist/esm/testkit/durable-run-fields-conformance.d.ts +4 -0
- package/dist/esm/testkit/durable-run-fields-conformance.js +95 -0
- package/dist/esm/testkit/durable-run-fields-conformance.js.map +1 -0
- package/dist/esm/testkit/journal-conformance.d.ts +51 -0
- package/dist/esm/testkit/journal-conformance.js +378 -0
- package/dist/esm/testkit/journal-conformance.js.map +1 -0
- package/dist/esm/testkit/reaper-conformance.d.ts +37 -0
- package/dist/esm/testkit/reaper-conformance.js +847 -0
- package/dist/esm/testkit/reaper-conformance.js.map +1 -0
- package/dist/esm/testkit/shell-spawn.d.ts +2 -0
- package/dist/esm/testkit/shell-spawn.js +60 -0
- package/dist/esm/testkit/shell-spawn.js.map +1 -0
- package/dist/esm/testkit/takeover-conformance.d.ts +24 -0
- package/dist/esm/testkit/takeover-conformance.js +685 -0
- package/dist/esm/testkit/takeover-conformance.js.map +1 -0
- package/dist/esm/tool-bridge.js +227 -180
- package/dist/esm/tool-bridge.js.map +1 -1
- package/dist/esm/tool-history.d.ts +62 -0
- package/dist/esm/tool-history.js +171 -0
- package/dist/esm/tool-history.js.map +1 -0
- package/dist/esm/watch.js +310 -236
- package/dist/esm/watch.js.map +1 -1
- package/dist/esm/workspace.d.ts +1 -1
- package/dist/esm/workspace.js +49 -28
- package/dist/esm/workspace.js.map +1 -1
- package/package.json +16 -6
- package/skills/ai-sandbox/SKILL.md +658 -20
- package/src/align.ts +297 -0
- package/src/attach-preflight.ts +292 -0
- package/src/capabilities.ts +4 -13
- package/src/chunk-identity.ts +154 -0
- package/src/claim.ts +479 -0
- package/src/contracts.ts +13 -0
- package/src/driver.ts +205 -0
- package/src/durability.ts +380 -0
- package/src/index.ts +212 -27
- package/src/instance-store.ts +122 -0
- package/src/journal-bytes.ts +136 -0
- package/src/journal-reader.ts +359 -0
- package/src/journal-sweep.ts +406 -0
- package/src/journal.ts +875 -0
- package/src/middleware.ts +470 -30
- package/src/reap.ts +723 -0
- package/src/reclaim.ts +191 -0
- package/src/run.ts +365 -75
- package/src/runner.ts +347 -3
- package/src/sandbox.ts +38 -8
- package/src/shell.ts +106 -38
- package/src/testkit/conformance.ts +117 -0
- package/src/testkit/durable-run-fields-conformance.ts +147 -0
- package/src/testkit/journal-conformance.ts +676 -0
- package/src/testkit/reaper-conformance.ts +1201 -0
- package/src/testkit/shell-spawn.ts +67 -0
- package/src/testkit/takeover-conformance.ts +1040 -0
- package/src/tool-history.ts +245 -0
- package/src/workspace.ts +1 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/run-log.d.ts +0 -81
- package/dist/esm/run-log.js +0 -107
- package/dist/esm/run-log.js.map +0 -1
- package/dist/esm/store.d.ts +0 -53
- package/dist/esm/store.js +0 -34
- package/dist/esm/store.js.map +0 -1
- package/src/run-log.ts +0 -224
- package/src/store.ts +0 -83
|
@@ -1,76 +1,101 @@
|
|
|
1
1
|
import { EventType } from "@tanstack/ai";
|
|
2
|
+
//#region src/bridge-events.ts
|
|
3
|
+
/**
|
|
4
|
+
* Helpers that let a harness adapter surface custom events emitted by BRIDGED
|
|
5
|
+
* tools (via {@link ToolBridgeCoreOptions.emitCustomEvent}) on its live output
|
|
6
|
+
* stream.
|
|
7
|
+
*
|
|
8
|
+
* The bridge runs out-of-band from the harness's own event stream, so a bridged
|
|
9
|
+
* tool's progress/console events have no path to the client on their own. An
|
|
10
|
+
* adapter creates a {@link BridgeEventChannel}, hands its `emitCustomEvent` to
|
|
11
|
+
* the bridge provisioner, and {@link mergeChunkStreams | merges} the channel's
|
|
12
|
+
* stream into its translated output — so events interleave live while the agent
|
|
13
|
+
* runs (e.g. code mode's `code_mode:console` logs during a long execution).
|
|
14
|
+
*/
|
|
15
|
+
/** Create a channel whose emitted events become CUSTOM {@link StreamChunk}s. */
|
|
2
16
|
function createBridgeEventChannel(meta) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
17
|
+
const buffer = [];
|
|
18
|
+
let notify = null;
|
|
19
|
+
let closed = false;
|
|
20
|
+
async function* stream() {
|
|
21
|
+
for (;;) {
|
|
22
|
+
const next = buffer.shift();
|
|
23
|
+
if (next !== void 0) {
|
|
24
|
+
yield next;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (closed) return;
|
|
28
|
+
await new Promise((resolve) => {
|
|
29
|
+
notify = resolve;
|
|
30
|
+
});
|
|
31
|
+
notify = null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
emitCustomEvent(eventName, value) {
|
|
36
|
+
if (closed) return;
|
|
37
|
+
buffer.push({
|
|
38
|
+
type: EventType.CUSTOM,
|
|
39
|
+
name: eventName,
|
|
40
|
+
value,
|
|
41
|
+
timestamp: Date.now(),
|
|
42
|
+
model: meta.model,
|
|
43
|
+
...meta.threadId !== void 0 && { threadId: meta.threadId },
|
|
44
|
+
...meta.runId !== void 0 && { runId: meta.runId }
|
|
45
|
+
});
|
|
46
|
+
notify?.();
|
|
47
|
+
},
|
|
48
|
+
close() {
|
|
49
|
+
closed = true;
|
|
50
|
+
notify?.();
|
|
51
|
+
},
|
|
52
|
+
stream: stream()
|
|
53
|
+
};
|
|
40
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Merge a `side` chunk stream into a `base` chunk stream, yielding from whichever
|
|
57
|
+
* settles first. Terminates when `base` ends (the run is over), then releases the
|
|
58
|
+
* side iterator — so a never-ending channel (until closed) doesn't hang the merge.
|
|
59
|
+
*/
|
|
41
60
|
async function* mergeChunkStreams(base, side) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
const baseIt = base[Symbol.asyncIterator]();
|
|
62
|
+
const sideIt = side[Symbol.asyncIterator]();
|
|
63
|
+
let baseNext = baseIt.next().then((r) => ({
|
|
64
|
+
from: "base",
|
|
65
|
+
r
|
|
66
|
+
}));
|
|
67
|
+
let sideNext = sideIt.next().then((r) => ({
|
|
68
|
+
from: "side",
|
|
69
|
+
r
|
|
70
|
+
}));
|
|
71
|
+
let sideLive = true;
|
|
72
|
+
try {
|
|
73
|
+
for (;;) {
|
|
74
|
+
const winner = await Promise.race(sideLive ? [baseNext, sideNext] : [baseNext]);
|
|
75
|
+
if (winner.from === "base") {
|
|
76
|
+
if (winner.r.done) return;
|
|
77
|
+
yield winner.r.value;
|
|
78
|
+
baseNext = baseIt.next().then((r) => ({
|
|
79
|
+
from: "base",
|
|
80
|
+
r
|
|
81
|
+
}));
|
|
82
|
+
} else if (winner.r.done) sideLive = false;
|
|
83
|
+
else {
|
|
84
|
+
yield winner.r.value;
|
|
85
|
+
sideNext = sideIt.next().then((r) => ({
|
|
86
|
+
from: "side",
|
|
87
|
+
r
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} finally {
|
|
92
|
+
const baseReturn = baseIt.return?.(void 0);
|
|
93
|
+
if (baseReturn) baseReturn.catch(() => {});
|
|
94
|
+
const sideReturn = sideIt.return?.(void 0);
|
|
95
|
+
if (sideReturn) sideReturn.catch(() => {});
|
|
96
|
+
}
|
|
71
97
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//# sourceMappingURL=bridge-events.js.map
|
|
98
|
+
//#endregion
|
|
99
|
+
export { createBridgeEventChannel, mergeChunkStreams };
|
|
100
|
+
|
|
101
|
+
//# sourceMappingURL=bridge-events.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-events.js","sources":["../../src/bridge-events.ts"],"sourcesContent":["/**\n * Helpers that let a harness adapter surface custom events emitted by BRIDGED\n * tools (via {@link ToolBridgeCoreOptions.emitCustomEvent}) on its live output\n * stream.\n *\n * The bridge runs out-of-band from the harness's own event stream, so a bridged\n * tool's progress/console events have no path to the client on their own. An\n * adapter creates a {@link BridgeEventChannel}, hands its `emitCustomEvent` to\n * the bridge provisioner, and {@link mergeChunkStreams | merges} the channel's\n * stream into its translated output — so events interleave live while the agent\n * runs (e.g. code mode's `code_mode:console` logs during a long execution).\n */\nimport { EventType } from '@tanstack/ai'\nimport type { StreamChunk } from '@tanstack/ai'\n\nexport interface BridgeEventChannel {\n /** Pass as the bridge's `emitCustomEvent`; buffers a CUSTOM chunk for the stream. */\n emitCustomEvent: (eventName: string, value: Record<string, unknown>) => void\n /** Live CUSTOM-chunk stream; ends after {@link close} once drained. */\n stream: AsyncIterable<StreamChunk>\n /** Stop the stream (call when the run's main output is done). */\n close: () => void\n}\n\n/** Create a channel whose emitted events become CUSTOM {@link StreamChunk}s. */\nexport function createBridgeEventChannel(meta: {\n model: string\n threadId?: string\n runId?: string\n}): BridgeEventChannel {\n const buffer: Array<StreamChunk> = []\n let notify: (() => void) | null = null\n let closed = false\n\n async function* stream(): AsyncIterable<StreamChunk> {\n for (;;) {\n const next = buffer.shift()\n if (next !== undefined) {\n yield next\n continue\n }\n if (closed) return\n await new Promise<void>((resolve) => {\n notify = resolve\n })\n notify = null\n }\n }\n\n return {\n emitCustomEvent(eventName, value) {\n if (closed) return\n buffer.push({\n type: EventType.CUSTOM,\n name: eventName,\n value,\n timestamp: Date.now(),\n model: meta.model,\n ...(meta.threadId !== undefined && { threadId: meta.threadId }),\n ...(meta.runId !== undefined && { runId: meta.runId }),\n })\n notify?.()\n },\n close() {\n closed = true\n notify?.()\n },\n stream: stream(),\n }\n}\n\n/**\n * Merge a `side` chunk stream into a `base` chunk stream, yielding from whichever\n * settles first. Terminates when `base` ends (the run is over), then releases the\n * side iterator — so a never-ending channel (until closed) doesn't hang the merge.\n */\nexport async function* mergeChunkStreams(\n base: AsyncIterable<StreamChunk>,\n side: AsyncIterable<StreamChunk>,\n): AsyncIterable<StreamChunk> {\n const baseIt = base[Symbol.asyncIterator]()\n const sideIt = side[Symbol.asyncIterator]()\n let baseNext = baseIt.next().then((r) => ({ from: 'base' as const, r }))\n let sideNext = sideIt.next().then((r) => ({ from: 'side' as const, r }))\n let sideLive = true\n try {\n for (;;) {\n const winner = await Promise.race(\n sideLive ? [baseNext, sideNext] : [baseNext],\n )\n if (winner.from === 'base') {\n if (winner.r.done) return\n yield winner.r.value\n baseNext = baseIt.next().then((r) => ({ from: 'base' as const, r }))\n } else if (winner.r.done) {\n sideLive = false\n } else {\n yield winner.r.value\n sideNext = sideIt.next().then((r) => ({ from: 'side' as const, r }))\n }\n }\n } finally {\n // Fire-and-forget: do NOT await the side return. The channel generator is\n // suspended on a promise that only `close()` resolves, and `close()` runs in\n // the adapter's `finally` AFTER this merge completes — awaiting here would\n // deadlock. The adapter's `close()` lets the generator unwind afterwards.\n const baseReturn = baseIt.return?.(undefined)\n if (baseReturn) void baseReturn.catch(() => {})\n const sideReturn = sideIt.return?.(undefined)\n if (sideReturn) void sideReturn.catch(() => {})\n }\n}\n"],"
|
|
1
|
+
{"version":3,"file":"bridge-events.js","names":[],"sources":["../../src/bridge-events.ts"],"sourcesContent":["/**\n * Helpers that let a harness adapter surface custom events emitted by BRIDGED\n * tools (via {@link ToolBridgeCoreOptions.emitCustomEvent}) on its live output\n * stream.\n *\n * The bridge runs out-of-band from the harness's own event stream, so a bridged\n * tool's progress/console events have no path to the client on their own. An\n * adapter creates a {@link BridgeEventChannel}, hands its `emitCustomEvent` to\n * the bridge provisioner, and {@link mergeChunkStreams | merges} the channel's\n * stream into its translated output — so events interleave live while the agent\n * runs (e.g. code mode's `code_mode:console` logs during a long execution).\n */\nimport { EventType } from '@tanstack/ai'\nimport type { StreamChunk } from '@tanstack/ai'\n\nexport interface BridgeEventChannel {\n /** Pass as the bridge's `emitCustomEvent`; buffers a CUSTOM chunk for the stream. */\n emitCustomEvent: (eventName: string, value: Record<string, unknown>) => void\n /** Live CUSTOM-chunk stream; ends after {@link close} once drained. */\n stream: AsyncIterable<StreamChunk>\n /** Stop the stream (call when the run's main output is done). */\n close: () => void\n}\n\n/** Create a channel whose emitted events become CUSTOM {@link StreamChunk}s. */\nexport function createBridgeEventChannel(meta: {\n model: string\n threadId?: string\n runId?: string\n}): BridgeEventChannel {\n const buffer: Array<StreamChunk> = []\n let notify: (() => void) | null = null\n let closed = false\n\n async function* stream(): AsyncIterable<StreamChunk> {\n for (;;) {\n const next = buffer.shift()\n if (next !== undefined) {\n yield next\n continue\n }\n if (closed) return\n await new Promise<void>((resolve) => {\n notify = resolve\n })\n notify = null\n }\n }\n\n return {\n emitCustomEvent(eventName, value) {\n if (closed) return\n buffer.push({\n type: EventType.CUSTOM,\n name: eventName,\n value,\n timestamp: Date.now(),\n model: meta.model,\n ...(meta.threadId !== undefined && { threadId: meta.threadId }),\n ...(meta.runId !== undefined && { runId: meta.runId }),\n })\n notify?.()\n },\n close() {\n closed = true\n notify?.()\n },\n stream: stream(),\n }\n}\n\n/**\n * Merge a `side` chunk stream into a `base` chunk stream, yielding from whichever\n * settles first. Terminates when `base` ends (the run is over), then releases the\n * side iterator — so a never-ending channel (until closed) doesn't hang the merge.\n */\nexport async function* mergeChunkStreams(\n base: AsyncIterable<StreamChunk>,\n side: AsyncIterable<StreamChunk>,\n): AsyncIterable<StreamChunk> {\n const baseIt = base[Symbol.asyncIterator]()\n const sideIt = side[Symbol.asyncIterator]()\n let baseNext = baseIt.next().then((r) => ({ from: 'base' as const, r }))\n let sideNext = sideIt.next().then((r) => ({ from: 'side' as const, r }))\n let sideLive = true\n try {\n for (;;) {\n const winner = await Promise.race(\n sideLive ? [baseNext, sideNext] : [baseNext],\n )\n if (winner.from === 'base') {\n if (winner.r.done) return\n yield winner.r.value\n baseNext = baseIt.next().then((r) => ({ from: 'base' as const, r }))\n } else if (winner.r.done) {\n sideLive = false\n } else {\n yield winner.r.value\n sideNext = sideIt.next().then((r) => ({ from: 'side' as const, r }))\n }\n }\n } finally {\n // Fire-and-forget: do NOT await the side return. The channel generator is\n // suspended on a promise that only `close()` resolves, and `close()` runs in\n // the adapter's `finally` AFTER this merge completes — awaiting here would\n // deadlock. The adapter's `close()` lets the generator unwind afterwards.\n const baseReturn = baseIt.return?.(undefined)\n if (baseReturn) void baseReturn.catch(() => {})\n const sideReturn = sideIt.return?.(undefined)\n if (sideReturn) void sideReturn.catch(() => {})\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAyBA,SAAgB,yBAAyB,MAIlB;CACrB,MAAM,SAA6B,CAAC;CACpC,IAAI,SAA8B;CAClC,IAAI,SAAS;CAEb,gBAAgB,SAAqC;EACnD,SAAS;GACP,MAAM,OAAO,OAAO,MAAM;GAC1B,IAAI,SAAS,KAAA,GAAW;IACtB,MAAM;IACN;GACF;GACA,IAAI,QAAQ;GACZ,MAAM,IAAI,SAAe,YAAY;IACnC,SAAS;GACX,CAAC;GACD,SAAS;EACX;CACF;CAEA,OAAO;EACL,gBAAgB,WAAW,OAAO;GAChC,IAAI,QAAQ;GACZ,OAAO,KAAK;IACV,MAAM,UAAU;IAChB,MAAM;IACN;IACA,WAAW,KAAK,IAAI;IACpB,OAAO,KAAK;IACZ,GAAI,KAAK,aAAa,KAAA,KAAa,EAAE,UAAU,KAAK,SAAS;IAC7D,GAAI,KAAK,UAAU,KAAA,KAAa,EAAE,OAAO,KAAK,MAAM;GACtD,CAAC;GACD,SAAS;EACX;EACA,QAAQ;GACN,SAAS;GACT,SAAS;EACX;EACA,QAAQ,OAAO;CACjB;AACF;;;;;;AAOA,gBAAuB,kBACrB,MACA,MAC4B;CAC5B,MAAM,SAAS,KAAK,OAAO,cAAc,CAAC;CAC1C,MAAM,SAAS,KAAK,OAAO,cAAc,CAAC;CAC1C,IAAI,WAAW,OAAO,KAAK,CAAC,CAAC,MAAM,OAAO;EAAE,MAAM;EAAiB;CAAE,EAAE;CACvE,IAAI,WAAW,OAAO,KAAK,CAAC,CAAC,MAAM,OAAO;EAAE,MAAM;EAAiB;CAAE,EAAE;CACvE,IAAI,WAAW;CACf,IAAI;EACF,SAAS;GACP,MAAM,SAAS,MAAM,QAAQ,KAC3B,WAAW,CAAC,UAAU,QAAQ,IAAI,CAAC,QAAQ,CAC7C;GACA,IAAI,OAAO,SAAS,QAAQ;IAC1B,IAAI,OAAO,EAAE,MAAM;IACnB,MAAM,OAAO,EAAE;IACf,WAAW,OAAO,KAAK,CAAC,CAAC,MAAM,OAAO;KAAE,MAAM;KAAiB;IAAE,EAAE;GACrE,OAAO,IAAI,OAAO,EAAE,MAClB,WAAW;QACN;IACL,MAAM,OAAO,EAAE;IACf,WAAW,OAAO,KAAK,CAAC,CAAC,MAAM,OAAO;KAAE,MAAM;KAAiB;IAAE,EAAE;GACrE;EACF;CACF,UAAU;EAKR,MAAM,aAAa,OAAO,SAAS,KAAA,CAAS;EAC5C,IAAI,YAAY,WAAgB,YAAY,CAAC,CAAC;EAC9C,MAAM,aAAa,OAAO,SAAS,KAAA,CAAS;EAC5C,IAAI,YAAY,WAAgB,YAAY,CAAC,CAAC;CAChD;AACF"}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { SandboxHandle } from './contracts.js';
|
|
2
|
-
import { LockStore, SandboxStore } from './store.js';
|
|
3
2
|
import { SandboxPolicy } from './policy.js';
|
|
4
3
|
import { ToolBridgeProvisioner } from './tool-bridge.js';
|
|
5
4
|
export declare const SandboxCapability: import('@tanstack/ai').Capability<SandboxHandle, "sandbox">;
|
|
6
|
-
export declare const SandboxStoreCapability: import('@tanstack/ai').Capability<SandboxStore, "sandbox-store">;
|
|
7
|
-
export declare const LocksCapability: import('@tanstack/ai').Capability<LockStore, "locks">;
|
|
8
5
|
/**
|
|
9
6
|
* The active sandbox policy, provided by `withSandbox` from the definition.
|
|
10
7
|
* Harness adapters read it to map allow/ask/deny rules onto their native
|
|
@@ -20,7 +17,5 @@ export declare const SandboxPolicyCapability: import('@tanstack/ai').Capability<
|
|
|
20
17
|
export declare const ToolBridgeProvisionerCapability: import('@tanstack/ai').Capability<ToolBridgeProvisioner, "tool-bridge-provisioner">;
|
|
21
18
|
/** Destructured accessors for adapters: `getSandbox(ctx)` reads the handle. */
|
|
22
19
|
export declare const getSandbox: import('@tanstack/ai').CapabilityGetter<SandboxHandle>, provideSandbox: import('@tanstack/ai').CapabilityProvider<SandboxHandle>;
|
|
23
|
-
export declare const getSandboxStore: import('@tanstack/ai').CapabilityGetter<SandboxStore>, provideSandboxStore: import('@tanstack/ai').CapabilityProvider<SandboxStore>;
|
|
24
|
-
export declare const getLocks: import('@tanstack/ai').CapabilityGetter<LockStore>, provideLocks: import('@tanstack/ai').CapabilityProvider<LockStore>;
|
|
25
20
|
export declare const getSandboxPolicy: import('@tanstack/ai').CapabilityGetter<SandboxPolicy>, provideSandboxPolicy: import('@tanstack/ai').CapabilityProvider<SandboxPolicy>;
|
|
26
21
|
export declare const getToolBridgeProvisioner: import('@tanstack/ai').CapabilityGetter<ToolBridgeProvisioner>, provideToolBridgeProvisioner: import('@tanstack/ai').CapabilityProvider<ToolBridgeProvisioner>;
|
package/dist/esm/capabilities.js
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
import { createCapability } from "@tanstack/ai";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
2
|
+
//#region src/capabilities.ts
|
|
3
|
+
/**
|
|
4
|
+
* Capability tokens the sandbox layer owns and provides.
|
|
5
|
+
*
|
|
6
|
+
* - `SandboxCapability` is PROVIDED by `withSandbox` and REQUIRED by harness
|
|
7
|
+
* adapters (`requires: [SandboxCapability]`).
|
|
8
|
+
* - `SandboxInstanceStoreCapability` lives in
|
|
9
|
+
* `./instance-store` (same package). `LocksCapability` / `withLocks` live in
|
|
10
|
+
* `@tanstack/ai/locks` and are not re-exported here.
|
|
11
|
+
*/
|
|
12
|
+
var SandboxCapability = createCapability()("sandbox");
|
|
13
|
+
/**
|
|
14
|
+
* The active sandbox policy, provided by `withSandbox` from the definition.
|
|
15
|
+
* Harness adapters read it to map allow/ask/deny rules onto their native
|
|
16
|
+
* permission system.
|
|
17
|
+
*/
|
|
18
|
+
var SandboxPolicyCapability = createCapability()("sandbox-policy");
|
|
19
|
+
/**
|
|
20
|
+
* Provisions the MCP tool-bridge endpoint for a run. OPTIONALLY provided by a
|
|
21
|
+
* serverless/edge orchestrator (e.g. a Durable Object) to override the default
|
|
22
|
+
* `node:http` host transport. Harness adapters read it via `getOptional` and
|
|
23
|
+
* fall back to `nodeHttpBridgeProvisioner` when absent.
|
|
24
|
+
*/
|
|
25
|
+
var ToolBridgeProvisionerCapability = createCapability()("tool-bridge-provisioner");
|
|
26
|
+
/** Destructured accessors for adapters: `getSandbox(ctx)` reads the handle. */
|
|
27
|
+
var [getSandbox, provideSandbox] = SandboxCapability;
|
|
28
|
+
var [getSandboxPolicy, provideSandboxPolicy] = SandboxPolicyCapability;
|
|
29
|
+
var [getToolBridgeProvisioner, provideToolBridgeProvisioner] = ToolBridgeProvisionerCapability;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { SandboxCapability, SandboxPolicyCapability, ToolBridgeProvisionerCapability, getSandbox, getSandboxPolicy, getToolBridgeProvisioner, provideSandbox, provideSandboxPolicy, provideToolBridgeProvisioner };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=capabilities.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capabilities.js","sources":["../../src/capabilities.ts"],"sourcesContent":["/**\n * Capability tokens the sandbox layer
|
|
1
|
+
{"version":3,"file":"capabilities.js","names":[],"sources":["../../src/capabilities.ts"],"sourcesContent":["/**\n * Capability tokens the sandbox layer owns and provides.\n *\n * - `SandboxCapability` is PROVIDED by `withSandbox` and REQUIRED by harness\n * adapters (`requires: [SandboxCapability]`).\n * - `SandboxInstanceStoreCapability` lives in\n * `./instance-store` (same package). `LocksCapability` / `withLocks` live in\n * `@tanstack/ai/locks` and are not re-exported here.\n */\nimport { createCapability } from '@tanstack/ai'\nimport type { SandboxHandle } from './contracts'\nimport type { SandboxPolicy } from './policy'\nimport type { ToolBridgeProvisioner } from './tool-bridge'\n\nexport const SandboxCapability = createCapability<SandboxHandle>()('sandbox')\n\n/**\n * The active sandbox policy, provided by `withSandbox` from the definition.\n * Harness adapters read it to map allow/ask/deny rules onto their native\n * permission system.\n */\nexport const SandboxPolicyCapability =\n createCapability<SandboxPolicy>()('sandbox-policy')\n\n/**\n * Provisions the MCP tool-bridge endpoint for a run. OPTIONALLY provided by a\n * serverless/edge orchestrator (e.g. a Durable Object) to override the default\n * `node:http` host transport. Harness adapters read it via `getOptional` and\n * fall back to `nodeHttpBridgeProvisioner` when absent.\n */\nexport const ToolBridgeProvisionerCapability =\n createCapability<ToolBridgeProvisioner>()('tool-bridge-provisioner')\n\n/** Destructured accessors for adapters: `getSandbox(ctx)` reads the handle. */\nexport const [getSandbox, provideSandbox] = SandboxCapability\nexport const [getSandboxPolicy, provideSandboxPolicy] = SandboxPolicyCapability\nexport const [getToolBridgeProvisioner, provideToolBridgeProvisioner] =\n ToolBridgeProvisionerCapability\n"],"mappings":";;;;;;;;;;;AAcA,IAAa,oBAAoB,iBAAgC,CAAC,CAAC,SAAS;;;;;;AAO5E,IAAa,0BACX,iBAAgC,CAAC,CAAC,gBAAgB;;;;;;;AAQpD,IAAa,kCACX,iBAAwC,CAAC,CAAC,yBAAyB;;AAGrE,IAAa,CAAC,YAAY,kBAAkB;AAC5C,IAAa,CAAC,kBAAkB,wBAAwB;AACxD,IAAa,CAAC,0BAA0B,gCACtC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { StreamChunk } from '@tanstack/ai';
|
|
2
|
+
/**
|
|
3
|
+
* A deterministic id generator scoped to one run.
|
|
4
|
+
*
|
|
5
|
+
* Passed as the harness translators' `genId`, so translating the same journal
|
|
6
|
+
* prefix twice mints the same message ids. The counter is per-generator, so a
|
|
7
|
+
* replay must create a fresh one and start from the journal's first byte —
|
|
8
|
+
* which is exactly what the alignment step (a later task) assumes.
|
|
9
|
+
*
|
|
10
|
+
* No clock, no `Math.random`, no crypto: `next` is the only state, and it is
|
|
11
|
+
* seeded fresh for every call to this factory.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createRunScopedIdGen(runId: string): () => string;
|
|
14
|
+
/**
|
|
15
|
+
* A stable, order-independent identity for a chunk, excluding wall-clock
|
|
16
|
+
* fields. Used to recognize the chunks a previous host already appended.
|
|
17
|
+
*
|
|
18
|
+
* - **Key-order independent**: object keys are sorted before stringifying, so
|
|
19
|
+
* a JSON round trip through the journal (which does not preserve key order)
|
|
20
|
+
* cannot spuriously diverge.
|
|
21
|
+
* - **Recurses into nested arrays and objects**: tool-call arguments are
|
|
22
|
+
* nested, and a shallow fingerprint would miss a changed argument.
|
|
23
|
+
* - **Excludes exactly `VOLATILE_FIELDS`** (`timestamp`) — everything else
|
|
24
|
+
* participates, including fields whose value is `undefined`.
|
|
25
|
+
* - **Distinguishes present-but-`undefined` from absent**: `undefined` is
|
|
26
|
+
* encoded as the sentinel string `"__undefined__"` rather than dropped, so
|
|
27
|
+
* `{a: undefined}` and `{}` do not collide. A translator emitting an
|
|
28
|
+
* explicit `undefined` is a different chunk shape and must fingerprint
|
|
29
|
+
* differently.
|
|
30
|
+
*/
|
|
31
|
+
export declare function chunkFingerprint(chunk: StreamChunk): string;
|
|
32
|
+
/**
|
|
33
|
+
* {@link chunkFingerprint} with the chunk's own `threadId` also excluded.
|
|
34
|
+
*
|
|
35
|
+
* NOT an alternative identity — never use it to decide that two chunks are the
|
|
36
|
+
* same. Its single purpose is DIAGNOSIS: when a replay diverges from the stored
|
|
37
|
+
* log, comparing both fingerprints answers "did the agent behave differently, or
|
|
38
|
+
* did only the conversation id move?". Two chunks that match here but not under
|
|
39
|
+
* {@link chunkFingerprint} differ in `threadId` and nothing else, which is a
|
|
40
|
+
* misconfigured attach route rather than a determinism regression (see
|
|
41
|
+
* `JournalReplayThreadIdMismatchError` in `align.ts`).
|
|
42
|
+
*/
|
|
43
|
+
export declare function chunkFingerprintIgnoringThreadId(chunk: StreamChunk): string;
|
|
44
|
+
/**
|
|
45
|
+
* A chunk's own `threadId`, or `undefined` when it carries none.
|
|
46
|
+
*
|
|
47
|
+
* Reads the field structurally rather than narrowing on `chunk.type`: nearly
|
|
48
|
+
* every member of the `StreamChunk` union declares `threadId?: string`, and an
|
|
49
|
+
* exhaustive switch would have to be revisited for each new member while adding
|
|
50
|
+
* nothing — a chunk with no `threadId` is exactly the `undefined` case.
|
|
51
|
+
*/
|
|
52
|
+
export declare function chunkThreadId(chunk: StreamChunk): string | undefined;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//#region src/chunk-identity.ts
|
|
2
|
+
/**
|
|
3
|
+
* A deterministic id generator scoped to one run.
|
|
4
|
+
*
|
|
5
|
+
* Passed as the harness translators' `genId`, so translating the same journal
|
|
6
|
+
* prefix twice mints the same message ids. The counter is per-generator, so a
|
|
7
|
+
* replay must create a fresh one and start from the journal's first byte —
|
|
8
|
+
* which is exactly what the alignment step (a later task) assumes.
|
|
9
|
+
*
|
|
10
|
+
* No clock, no `Math.random`, no crypto: `next` is the only state, and it is
|
|
11
|
+
* seeded fresh for every call to this factory.
|
|
12
|
+
*/
|
|
13
|
+
function createRunScopedIdGen(runId) {
|
|
14
|
+
let next = 0;
|
|
15
|
+
return () => {
|
|
16
|
+
const id = `${runId}-${next}`;
|
|
17
|
+
next += 1;
|
|
18
|
+
return id;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Fields excluded from a fingerprint because they are wall-clock and therefore
|
|
23
|
+
* unreproducible. Kept as an explicit set so adding one is a deliberate,
|
|
24
|
+
* reviewable act rather than a silent loosening of the comparison.
|
|
25
|
+
*/
|
|
26
|
+
var VOLATILE_FIELDS = /* @__PURE__ */ new Set(["timestamp"]);
|
|
27
|
+
/**
|
|
28
|
+
* The conversation id every chunk carries. Excluded from
|
|
29
|
+
* {@link chunkFingerprintIgnoringThreadId} — and ONLY from that variant — so
|
|
30
|
+
* alignment can tell an id-only mismatch from a real content divergence.
|
|
31
|
+
*/
|
|
32
|
+
var THREAD_ID_FIELD = "threadId";
|
|
33
|
+
var VOLATILE_AND_THREAD_ID = /* @__PURE__ */ new Set([...VOLATILE_FIELDS, THREAD_ID_FIELD]);
|
|
34
|
+
/**
|
|
35
|
+
* `dropped` applies at the TOP LEVEL only (nested calls pass `undefined`).
|
|
36
|
+
* A `threadId` nested inside, say, a tool call's arguments is real content and
|
|
37
|
+
* must keep participating in the comparison.
|
|
38
|
+
*/
|
|
39
|
+
function stableStringify(value, dropped) {
|
|
40
|
+
if (value === null) return "null";
|
|
41
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item, void 0)).join(",")}]`;
|
|
42
|
+
if (typeof value === "object") {
|
|
43
|
+
const record = value;
|
|
44
|
+
return `{${Object.keys(record).filter((key) => dropped === void 0 || !dropped.has(key)).sort().map((key) => {
|
|
45
|
+
const entry = record[key];
|
|
46
|
+
const encoded = entry === void 0 ? "\"__undefined__\"" : stableStringify(entry, void 0);
|
|
47
|
+
return `${JSON.stringify(key)}:${encoded}`;
|
|
48
|
+
}).join(",")}}`;
|
|
49
|
+
}
|
|
50
|
+
const encoded = JSON.stringify(value);
|
|
51
|
+
return encoded === void 0 ? "null" : encoded;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A stable, order-independent identity for a chunk, excluding wall-clock
|
|
55
|
+
* fields. Used to recognize the chunks a previous host already appended.
|
|
56
|
+
*
|
|
57
|
+
* - **Key-order independent**: object keys are sorted before stringifying, so
|
|
58
|
+
* a JSON round trip through the journal (which does not preserve key order)
|
|
59
|
+
* cannot spuriously diverge.
|
|
60
|
+
* - **Recurses into nested arrays and objects**: tool-call arguments are
|
|
61
|
+
* nested, and a shallow fingerprint would miss a changed argument.
|
|
62
|
+
* - **Excludes exactly `VOLATILE_FIELDS`** (`timestamp`) — everything else
|
|
63
|
+
* participates, including fields whose value is `undefined`.
|
|
64
|
+
* - **Distinguishes present-but-`undefined` from absent**: `undefined` is
|
|
65
|
+
* encoded as the sentinel string `"__undefined__"` rather than dropped, so
|
|
66
|
+
* `{a: undefined}` and `{}` do not collide. A translator emitting an
|
|
67
|
+
* explicit `undefined` is a different chunk shape and must fingerprint
|
|
68
|
+
* differently.
|
|
69
|
+
*/
|
|
70
|
+
function chunkFingerprint(chunk) {
|
|
71
|
+
return stableStringify(chunk, VOLATILE_FIELDS);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* {@link chunkFingerprint} with the chunk's own `threadId` also excluded.
|
|
75
|
+
*
|
|
76
|
+
* NOT an alternative identity — never use it to decide that two chunks are the
|
|
77
|
+
* same. Its single purpose is DIAGNOSIS: when a replay diverges from the stored
|
|
78
|
+
* log, comparing both fingerprints answers "did the agent behave differently, or
|
|
79
|
+
* did only the conversation id move?". Two chunks that match here but not under
|
|
80
|
+
* {@link chunkFingerprint} differ in `threadId` and nothing else, which is a
|
|
81
|
+
* misconfigured attach route rather than a determinism regression (see
|
|
82
|
+
* `JournalReplayThreadIdMismatchError` in `align.ts`).
|
|
83
|
+
*/
|
|
84
|
+
function chunkFingerprintIgnoringThreadId(chunk) {
|
|
85
|
+
return stableStringify(chunk, VOLATILE_AND_THREAD_ID);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A chunk's own `threadId`, or `undefined` when it carries none.
|
|
89
|
+
*
|
|
90
|
+
* Reads the field structurally rather than narrowing on `chunk.type`: nearly
|
|
91
|
+
* every member of the `StreamChunk` union declares `threadId?: string`, and an
|
|
92
|
+
* exhaustive switch would have to be revisited for each new member while adding
|
|
93
|
+
* nothing — a chunk with no `threadId` is exactly the `undefined` case.
|
|
94
|
+
*/
|
|
95
|
+
function chunkThreadId(chunk) {
|
|
96
|
+
const value = chunk[THREAD_ID_FIELD];
|
|
97
|
+
return typeof value === "string" ? value : void 0;
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
export { chunkFingerprint, chunkFingerprintIgnoringThreadId, chunkThreadId, createRunScopedIdGen };
|
|
101
|
+
|
|
102
|
+
//# sourceMappingURL=chunk-identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunk-identity.js","names":[],"sources":["../../src/chunk-identity.ts"],"sourcesContent":["/**\n * Deterministic chunk identity — the prerequisite that makes journal replay\n * exact.\n *\n * The design premise is that re-translating a journal prefix reproduces the\n * chunks a previous host already delivered, so a successor can recognize and\n * skip them (see `align.ts`, a later task). Two things in the default path\n * break that premise:\n *\n * 1. `ChatAdapter.generateId()` — `packages/ai/src/activities/chat/adapter.ts:227`,\n * read directly for this task — is:\n *\n * ```ts\n * protected generateId(): string {\n * return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(7)}`\n * }\n * ```\n *\n * and every harness translator mints message ids through it (wired as\n * `genId` in the Grok Build, Claude Code, and Codex text adapters). Both\n * `Date.now()` and `Math.random()` are non-reproducible: replaying the same\n * journal bytes through a second `generateId()` call produces a different\n * id every time, so \"same bytes ⇒ same chunks\" is false on the journaled\n * path today. {@link createRunScopedIdGen} replaces it with a run-scoped\n * counter that has neither a clock nor randomness, so two generators built\n * from the same `runId` always produce the same sequence.\n * 2. Chunks also carry `timestamp: Date.now()`, which cannot be reproduced at\n * all, deterministic id or not. {@link chunkFingerprint} therefore excludes\n * exactly that field — nothing downstream keys on a chunk's timestamp, so\n * leaving it wall-clock is safe, but every other field must participate in\n * the comparison or a real divergence would go undetected.\n */\nimport type { StreamChunk } from '@tanstack/ai'\n\n/**\n * A deterministic id generator scoped to one run.\n *\n * Passed as the harness translators' `genId`, so translating the same journal\n * prefix twice mints the same message ids. The counter is per-generator, so a\n * replay must create a fresh one and start from the journal's first byte —\n * which is exactly what the alignment step (a later task) assumes.\n *\n * No clock, no `Math.random`, no crypto: `next` is the only state, and it is\n * seeded fresh for every call to this factory.\n */\nexport function createRunScopedIdGen(runId: string): () => string {\n let next = 0\n return () => {\n const id = `${runId}-${next}`\n next += 1\n return id\n }\n}\n\n/**\n * Fields excluded from a fingerprint because they are wall-clock and therefore\n * unreproducible. Kept as an explicit set so adding one is a deliberate,\n * reviewable act rather than a silent loosening of the comparison.\n */\nconst VOLATILE_FIELDS: ReadonlySet<string> = new Set(['timestamp'])\n\n/**\n * The conversation id every chunk carries. Excluded from\n * {@link chunkFingerprintIgnoringThreadId} — and ONLY from that variant — so\n * alignment can tell an id-only mismatch from a real content divergence.\n */\nconst THREAD_ID_FIELD = 'threadId'\n\nconst VOLATILE_AND_THREAD_ID: ReadonlySet<string> = new Set([\n ...VOLATILE_FIELDS,\n THREAD_ID_FIELD,\n])\n\n/**\n * `dropped` applies at the TOP LEVEL only (nested calls pass `undefined`).\n * A `threadId` nested inside, say, a tool call's arguments is real content and\n * must keep participating in the comparison.\n */\nfunction stableStringify(\n value: unknown,\n dropped: ReadonlySet<string> | undefined,\n): string {\n if (value === null) return 'null'\n if (Array.isArray(value)) {\n return `[${value.map((item) => stableStringify(item, undefined)).join(',')}]`\n }\n if (typeof value === 'object') {\n const record: Record<string, unknown> = value as Record<string, unknown>\n const keys = Object.keys(record)\n .filter((key) => dropped === undefined || !dropped.has(key))\n .sort()\n const parts = keys.map((key) => {\n const entry = record[key]\n const encoded =\n entry === undefined\n ? '\"__undefined__\"'\n : stableStringify(entry, undefined)\n return `${JSON.stringify(key)}:${encoded}`\n })\n return `{${parts.join(',')}}`\n }\n const encoded = JSON.stringify(value)\n return encoded === undefined ? 'null' : encoded\n}\n\n/**\n * A stable, order-independent identity for a chunk, excluding wall-clock\n * fields. Used to recognize the chunks a previous host already appended.\n *\n * - **Key-order independent**: object keys are sorted before stringifying, so\n * a JSON round trip through the journal (which does not preserve key order)\n * cannot spuriously diverge.\n * - **Recurses into nested arrays and objects**: tool-call arguments are\n * nested, and a shallow fingerprint would miss a changed argument.\n * - **Excludes exactly `VOLATILE_FIELDS`** (`timestamp`) — everything else\n * participates, including fields whose value is `undefined`.\n * - **Distinguishes present-but-`undefined` from absent**: `undefined` is\n * encoded as the sentinel string `\"__undefined__\"` rather than dropped, so\n * `{a: undefined}` and `{}` do not collide. A translator emitting an\n * explicit `undefined` is a different chunk shape and must fingerprint\n * differently.\n */\nexport function chunkFingerprint(chunk: StreamChunk): string {\n return stableStringify(chunk, VOLATILE_FIELDS)\n}\n\n/**\n * {@link chunkFingerprint} with the chunk's own `threadId` also excluded.\n *\n * NOT an alternative identity — never use it to decide that two chunks are the\n * same. Its single purpose is DIAGNOSIS: when a replay diverges from the stored\n * log, comparing both fingerprints answers \"did the agent behave differently, or\n * did only the conversation id move?\". Two chunks that match here but not under\n * {@link chunkFingerprint} differ in `threadId` and nothing else, which is a\n * misconfigured attach route rather than a determinism regression (see\n * `JournalReplayThreadIdMismatchError` in `align.ts`).\n */\nexport function chunkFingerprintIgnoringThreadId(chunk: StreamChunk): string {\n return stableStringify(chunk, VOLATILE_AND_THREAD_ID)\n}\n\n/**\n * A chunk's own `threadId`, or `undefined` when it carries none.\n *\n * Reads the field structurally rather than narrowing on `chunk.type`: nearly\n * every member of the `StreamChunk` union declares `threadId?: string`, and an\n * exhaustive switch would have to be revisited for each new member while adding\n * nothing — a chunk with no `threadId` is exactly the `undefined` case.\n */\nexport function chunkThreadId(chunk: StreamChunk): string | undefined {\n const record: Record<string, unknown> = chunk as Record<string, unknown>\n const value = record[THREAD_ID_FIELD]\n return typeof value === 'string' ? value : undefined\n}\n"],"mappings":";;;;;;;;;;;;AA6CA,SAAgB,qBAAqB,OAA6B;CAChE,IAAI,OAAO;CACX,aAAa;EACX,MAAM,KAAK,GAAG,MAAM,GAAG;EACvB,QAAQ;EACR,OAAO;CACT;AACF;;;;;;AAOA,IAAM,kCAAuC,IAAI,IAAI,CAAC,WAAW,CAAC;;;;;;AAOlE,IAAM,kBAAkB;AAExB,IAAM,yCAA8C,IAAI,IAAI,CAC1D,GAAG,iBACH,eACF,CAAC;;;;;;AAOD,SAAS,gBACP,OACA,SACQ;CACR,IAAI,UAAU,MAAM,OAAO;CAC3B,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,IAAI,MAAM,KAAK,SAAS,gBAAgB,MAAM,KAAA,CAAS,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE;CAE7E,IAAI,OAAO,UAAU,UAAU;EAC7B,MAAM,SAAkC;EAYxC,OAAO,IAXM,OAAO,KAAK,MAAM,CAAC,CAC7B,QAAQ,QAAQ,YAAY,KAAA,KAAa,CAAC,QAAQ,IAAI,GAAG,CAAC,CAAC,CAC3D,KACW,CAAA,CAAK,KAAK,QAAQ;GAC9B,MAAM,QAAQ,OAAO;GACrB,MAAM,UACJ,UAAU,KAAA,IACN,sBACA,gBAAgB,OAAO,KAAA,CAAS;GACtC,OAAO,GAAG,KAAK,UAAU,GAAG,EAAE,GAAG;EACnC,CACW,CAAA,CAAM,KAAK,GAAG,EAAE;CAC7B;CACA,MAAM,UAAU,KAAK,UAAU,KAAK;CACpC,OAAO,YAAY,KAAA,IAAY,SAAS;AAC1C;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,iBAAiB,OAA4B;CAC3D,OAAO,gBAAgB,OAAO,eAAe;AAC/C;;;;;;;;;;;;AAaA,SAAgB,iCAAiC,OAA4B;CAC3E,OAAO,gBAAgB,OAAO,sBAAsB;AACtD;;;;;;;;;AAUA,SAAgB,cAAc,OAAwC;CAEpE,MAAM,QAAQ,MAAO;CACrB,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C"}
|