@caupulican/pi-agent-core 0.80.103 → 0.81.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/compaction/branch-summarization.d.ts +88 -0
- package/dist/compaction/branch-summarization.d.ts.map +1 -0
- package/dist/{harness/compaction → compaction}/branch-summarization.js +86 -18
- package/dist/compaction/branch-summarization.js.map +1 -0
- package/dist/compaction/compaction.d.ts +143 -0
- package/dist/compaction/compaction.d.ts.map +1 -0
- package/dist/{harness/compaction → compaction}/compaction.js +208 -77
- package/dist/compaction/compaction.js.map +1 -0
- package/dist/compaction/index.d.ts +7 -0
- package/dist/compaction/index.d.ts.map +1 -0
- package/dist/compaction/index.js +7 -0
- package/dist/compaction/index.js.map +1 -0
- package/dist/compaction/utils.d.ts +38 -0
- package/dist/compaction/utils.d.ts.map +1 -0
- package/dist/{harness/compaction → compaction}/utils.js +36 -14
- package/dist/compaction/utils.js.map +1 -0
- package/dist/index.d.ts +5 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -16
- package/dist/index.js.map +1 -1
- package/dist/{harness/messages.d.ts → messages.d.ts} +28 -2
- package/dist/messages.d.ts.map +1 -0
- package/dist/{harness/messages.js → messages.js} +22 -1
- package/dist/messages.js.map +1 -0
- package/dist/node.d.ts +5 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +9 -1
- package/dist/node.js.map +1 -1
- package/dist/reliability/classifier.d.ts +33 -0
- package/dist/reliability/classifier.d.ts.map +1 -0
- package/dist/reliability/classifier.js +69 -0
- package/dist/reliability/classifier.js.map +1 -0
- package/dist/reliability/index.d.ts +5 -0
- package/dist/reliability/index.d.ts.map +1 -0
- package/dist/reliability/index.js +5 -0
- package/dist/reliability/index.js.map +1 -0
- package/dist/reliability/node.d.ts +2 -0
- package/dist/reliability/node.d.ts.map +1 -0
- package/dist/reliability/node.js +2 -0
- package/dist/reliability/node.js.map +1 -0
- package/dist/reliability/process-tree.d.ts +10 -0
- package/dist/reliability/process-tree.d.ts.map +1 -0
- package/dist/reliability/process-tree.js +75 -0
- package/dist/reliability/process-tree.js.map +1 -0
- package/dist/reliability/retry-controller.d.ts +66 -0
- package/dist/reliability/retry-controller.d.ts.map +1 -0
- package/dist/reliability/retry-controller.js +104 -0
- package/dist/reliability/retry-controller.js.map +1 -0
- package/dist/reliability/retry.d.ts +19 -0
- package/dist/reliability/retry.d.ts.map +1 -0
- package/dist/reliability/retry.js +35 -0
- package/dist/reliability/retry.js.map +1 -0
- package/dist/reliability/watchdogs.d.ts +47 -0
- package/dist/reliability/watchdogs.d.ts.map +1 -0
- package/dist/reliability/watchdogs.js +159 -0
- package/dist/reliability/watchdogs.js.map +1 -0
- package/dist/session/message-retention.d.ts +26 -0
- package/dist/session/message-retention.d.ts.map +1 -0
- package/dist/session/message-retention.js +95 -0
- package/dist/session/message-retention.js.map +1 -0
- package/dist/session/session-manager.d.ts +350 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +1339 -0
- package/dist/session/session-manager.js.map +1 -0
- package/dist/utils/paths.d.ts +15 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +31 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/shell-output.d.ts +10 -0
- package/dist/utils/shell-output.d.ts.map +1 -0
- package/dist/utils/shell-output.js +38 -0
- package/dist/utils/shell-output.js.map +1 -0
- package/dist/utils/truncate.d.ts.map +1 -0
- package/dist/{harness/utils → utils}/truncate.js +27 -102
- package/dist/utils/truncate.js.map +1 -0
- package/dist/uuid.d.ts.map +1 -0
- package/dist/uuid.js.map +1 -0
- package/package.json +6 -2
- package/dist/harness/agent-harness.d.ts +0 -95
- package/dist/harness/agent-harness.d.ts.map +0 -1
- package/dist/harness/agent-harness.js +0 -963
- package/dist/harness/agent-harness.js.map +0 -1
- package/dist/harness/compaction/branch-summarization.d.ts +0 -53
- package/dist/harness/compaction/branch-summarization.d.ts.map +0 -1
- package/dist/harness/compaction/branch-summarization.js.map +0 -1
- package/dist/harness/compaction/compaction.d.ts +0 -95
- package/dist/harness/compaction/compaction.d.ts.map +0 -1
- package/dist/harness/compaction/compaction.js.map +0 -1
- package/dist/harness/compaction/utils.d.ts +0 -25
- package/dist/harness/compaction/utils.d.ts.map +0 -1
- package/dist/harness/compaction/utils.js.map +0 -1
- package/dist/harness/env/nodejs.d.ts +0 -51
- package/dist/harness/env/nodejs.d.ts.map +0 -1
- package/dist/harness/env/nodejs.js +0 -486
- package/dist/harness/env/nodejs.js.map +0 -1
- package/dist/harness/messages.d.ts.map +0 -1
- package/dist/harness/messages.js.map +0 -1
- package/dist/harness/prompt-templates.d.ts +0 -48
- package/dist/harness/prompt-templates.d.ts.map +0 -1
- package/dist/harness/prompt-templates.js +0 -233
- package/dist/harness/prompt-templates.js.map +0 -1
- package/dist/harness/session/jsonl-repo.d.ts +0 -28
- package/dist/harness/session/jsonl-repo.d.ts.map +0 -1
- package/dist/harness/session/jsonl-repo.js +0 -112
- package/dist/harness/session/jsonl-repo.js.map +0 -1
- package/dist/harness/session/jsonl-storage.d.ts +0 -33
- package/dist/harness/session/jsonl-storage.d.ts.map +0 -1
- package/dist/harness/session/jsonl-storage.js +0 -231
- package/dist/harness/session/jsonl-storage.js.map +0 -1
- package/dist/harness/session/memory-repo.d.ts +0 -18
- package/dist/harness/session/memory-repo.d.ts.map +0 -1
- package/dist/harness/session/memory-repo.js +0 -42
- package/dist/harness/session/memory-repo.js.map +0 -1
- package/dist/harness/session/memory-storage.d.ts +0 -25
- package/dist/harness/session/memory-storage.d.ts.map +0 -1
- package/dist/harness/session/memory-storage.js +0 -114
- package/dist/harness/session/memory-storage.js.map +0 -1
- package/dist/harness/session/repo-utils.d.ts +0 -11
- package/dist/harness/session/repo-utils.d.ts.map +0 -1
- package/dist/harness/session/repo-utils.js +0 -39
- package/dist/harness/session/repo-utils.js.map +0 -1
- package/dist/harness/session/session.d.ts +0 -33
- package/dist/harness/session/session.d.ts.map +0 -1
- package/dist/harness/session/session.js +0 -210
- package/dist/harness/session/session.js.map +0 -1
- package/dist/harness/session/uuid.d.ts.map +0 -1
- package/dist/harness/session/uuid.js.map +0 -1
- package/dist/harness/skills.d.ts +0 -44
- package/dist/harness/skills.d.ts.map +0 -1
- package/dist/harness/skills.js +0 -311
- package/dist/harness/skills.js.map +0 -1
- package/dist/harness/system-prompt.d.ts +0 -3
- package/dist/harness/system-prompt.d.ts.map +0 -1
- package/dist/harness/system-prompt.js +0 -30
- package/dist/harness/system-prompt.js.map +0 -1
- package/dist/harness/types.d.ts +0 -615
- package/dist/harness/types.d.ts.map +0 -1
- package/dist/harness/types.js +0 -94
- package/dist/harness/types.js.map +0 -1
- package/dist/harness/utils/shell-output.d.ts +0 -14
- package/dist/harness/utils/shell-output.d.ts.map +0 -1
- package/dist/harness/utils/shell-output.js +0 -126
- package/dist/harness/utils/shell-output.js.map +0 -1
- package/dist/harness/utils/truncate.d.ts.map +0 -1
- package/dist/harness/utils/truncate.js.map +0 -1
- /package/dist/{harness/utils → utils}/truncate.d.ts +0 -0
- /package/dist/{harness/session/uuid.d.ts → uuid.d.ts} +0 -0
- /package/dist/{harness/session/uuid.js → uuid.js} +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Silence/idle watchdogs for the reliability kernel.
|
|
3
|
+
*
|
|
4
|
+
* A silence watchdog bounds "running but mute" — it never bounds total runtime,
|
|
5
|
+
* so long tasks that produce output are never killed (autonomy constraint).
|
|
6
|
+
*/
|
|
7
|
+
export interface SilenceWatchdog {
|
|
8
|
+
/** Report activity (output chunk / stream event); resets the countdown. */
|
|
9
|
+
touch(): void;
|
|
10
|
+
/** Stop permanently (normal completion). Idempotent. */
|
|
11
|
+
disarm(): void;
|
|
12
|
+
}
|
|
13
|
+
export interface SilenceWatchdogOptions {
|
|
14
|
+
silenceMs: number;
|
|
15
|
+
/** Fired at most once, after silenceMs with no touch(). The watchdog self-disarms. */
|
|
16
|
+
onSilence: () => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function createSilenceWatchdog(opts: SilenceWatchdogOptions): SilenceWatchdog;
|
|
19
|
+
import type { StreamFn } from "../types.ts";
|
|
20
|
+
export interface StreamIdleOptions {
|
|
21
|
+
/** Max ms between events once streaming has started (user-locked default 30s). */
|
|
22
|
+
idleMs: number;
|
|
23
|
+
/** Max ms to wait for the FIRST event (connection/first-token allowance). */
|
|
24
|
+
connectMs: number;
|
|
25
|
+
/** Fired once when a stall is detected, before the inner request is aborted. */
|
|
26
|
+
onStall?: (info: {
|
|
27
|
+
phase: "connect" | "stream";
|
|
28
|
+
elapsedMs: number;
|
|
29
|
+
}) => void;
|
|
30
|
+
}
|
|
31
|
+
export declare const DEFAULT_STREAM_IDLE: StreamIdleOptions;
|
|
32
|
+
/**
|
|
33
|
+
* Wrap a StreamFn so a silently dead connection cannot wedge a turn forever.
|
|
34
|
+
*
|
|
35
|
+
* `connectMs` bounds the wait for the first event (connection/first-token allowance);
|
|
36
|
+
* once streaming starts, `idleMs` bounds the gap between subsequent events. On stall,
|
|
37
|
+
* the inner request is aborted and the returned stream resolves immediately with a
|
|
38
|
+
* synthetic `AssistantMessage` (`stopReason: "error"`, `errorMessage: "stream stalled:
|
|
39
|
+
* no events for <n>ms"`) — the exact phrasing `classifyFailure` maps to a retryable
|
|
40
|
+
* `stream_stall`, so the host's retry/failover path takes it from there.
|
|
41
|
+
*
|
|
42
|
+
* A caller-initiated abort (via the options `signal`) is never treated as a stall: it
|
|
43
|
+
* is chained into the wrapper's own controller and the inner stream's own abort result
|
|
44
|
+
* is forwarded untouched.
|
|
45
|
+
*/
|
|
46
|
+
export declare function withStreamIdleWatchdog(streamFn: StreamFn, options?: Partial<StreamIdleOptions>): StreamFn;
|
|
47
|
+
//# sourceMappingURL=watchdogs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watchdogs.d.ts","sourceRoot":"","sources":["../../src/reliability/watchdogs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,eAAe;IAC/B,2EAA2E;IAC3E,KAAK,IAAI,IAAI,CAAC;IACd,wDAAwD;IACxD,MAAM,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,sBAAsB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,SAAS,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,sBAAsB,GAAG,eAAe,CA4BnF;AASD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,iBAAiB;IACjC,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC7E;AAED,eAAO,MAAM,mBAAmB,EAAE,iBAA0D,CAAC;AAS7F;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,QAAQ,CAgGzG","sourcesContent":["/**\n * Silence/idle watchdogs for the reliability kernel.\n *\n * A silence watchdog bounds \"running but mute\" — it never bounds total runtime,\n * so long tasks that produce output are never killed (autonomy constraint).\n */\n\nexport interface SilenceWatchdog {\n\t/** Report activity (output chunk / stream event); resets the countdown. */\n\ttouch(): void;\n\t/** Stop permanently (normal completion). Idempotent. */\n\tdisarm(): void;\n}\n\nexport interface SilenceWatchdogOptions {\n\tsilenceMs: number;\n\t/** Fired at most once, after silenceMs with no touch(). The watchdog self-disarms. */\n\tonSilence: () => void;\n}\n\nexport function createSilenceWatchdog(opts: SilenceWatchdogOptions): SilenceWatchdog {\n\tlet timer: NodeJS.Timeout | undefined;\n\tlet disarmed = false;\n\n\tconst arm = () => {\n\t\ttimer = setTimeout(() => {\n\t\t\tdisarmed = true;\n\t\t\ttimer = undefined;\n\t\t\topts.onSilence();\n\t\t}, opts.silenceMs);\n\t\t// Never keep the host process alive just for a watchdog.\n\t\ttimer.unref?.();\n\t};\n\n\tarm();\n\n\treturn {\n\t\ttouch(): void {\n\t\t\tif (disarmed) return;\n\t\t\tif (timer) clearTimeout(timer);\n\t\t\tarm();\n\t\t},\n\t\tdisarm(): void {\n\t\t\tdisarmed = true;\n\t\t\tif (timer) clearTimeout(timer);\n\t\t\ttimer = undefined;\n\t\t},\n\t};\n}\n\n// --- Stream-idle watchdog (wraps a StreamFn) -------------------------------\n\nimport {\n\ttype AssistantMessage,\n\ttype AssistantMessageEvent,\n\tcreateAssistantMessageEventStream,\n} from \"@caupulican/pi-ai\";\nimport type { StreamFn } from \"../types.ts\";\n\nexport interface StreamIdleOptions {\n\t/** Max ms between events once streaming has started (user-locked default 30s). */\n\tidleMs: number;\n\t/** Max ms to wait for the FIRST event (connection/first-token allowance). */\n\tconnectMs: number;\n\t/** Fired once when a stall is detected, before the inner request is aborted. */\n\tonStall?: (info: { phase: \"connect\" | \"stream\"; elapsedMs: number }) => void;\n}\n\nexport const DEFAULT_STREAM_IDLE: StreamIdleOptions = { idleMs: 30_000, connectMs: 120_000 };\n\n/** Extracts the current AssistantMessage snapshot carried by any stream event variant. */\nfunction partialFromEvent(event: AssistantMessageEvent): AssistantMessage {\n\tif (event.type === \"done\") return event.message;\n\tif (event.type === \"error\") return event.error;\n\treturn event.partial;\n}\n\n/**\n * Wrap a StreamFn so a silently dead connection cannot wedge a turn forever.\n *\n * `connectMs` bounds the wait for the first event (connection/first-token allowance);\n * once streaming starts, `idleMs` bounds the gap between subsequent events. On stall,\n * the inner request is aborted and the returned stream resolves immediately with a\n * synthetic `AssistantMessage` (`stopReason: \"error\"`, `errorMessage: \"stream stalled:\n * no events for <n>ms\"`) — the exact phrasing `classifyFailure` maps to a retryable\n * `stream_stall`, so the host's retry/failover path takes it from there.\n *\n * A caller-initiated abort (via the options `signal`) is never treated as a stall: it\n * is chained into the wrapper's own controller and the inner stream's own abort result\n * is forwarded untouched.\n */\nexport function withStreamIdleWatchdog(streamFn: StreamFn, options?: Partial<StreamIdleOptions>): StreamFn {\n\tconst opts = { ...DEFAULT_STREAM_IDLE, ...options };\n\treturn async (model, context, streamOptions) => {\n\t\tconst controller = new AbortController();\n\t\tconst callerSignal = streamOptions?.signal;\n\t\tlet callerAborted = callerSignal?.aborted ?? false;\n\t\tconst onCallerAbort = () => {\n\t\t\tcallerAborted = true;\n\t\t\tcontroller.abort(callerSignal?.reason);\n\t\t};\n\t\tif (callerAborted) controller.abort(callerSignal?.reason);\n\t\telse callerSignal?.addEventListener(\"abort\", onCallerAbort, { once: true });\n\n\t\tconst inner = await streamFn(model, context, { ...streamOptions, signal: controller.signal });\n\t\tconst outer = createAssistantMessageEventStream();\n\n\t\t// Seeded so a connect-phase stall (no event ever arrived) still has a base message\n\t\t// to report on; overwritten with the latest real snapshot once events start flowing.\n\t\tlet latest: AssistantMessage = {\n\t\t\trole: \"assistant\",\n\t\t\tcontent: [],\n\t\t\tapi: model.api,\n\t\t\tprovider: model.provider,\n\t\t\tmodel: model.id,\n\t\t\tusage: {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\ttotalTokens: 0,\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t},\n\t\t\tstopReason: \"stop\",\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\t\tlet firstEventSeen = false;\n\t\tlet stalled = false;\n\n\t\t// Emits the stall result directly (rather than after the inner loop finishes) so a\n\t\t// connection that never resolves at all still yields a result promptly — providers\n\t\t// are contractually expected to end their stream after abort, but the watchdog does\n\t\t// not depend on that to report the stall itself.\n\t\tconst stall = (phase: \"connect\" | \"stream\", elapsedMs: number) => {\n\t\t\tif (callerAborted || stalled) return;\n\t\t\tstalled = true;\n\t\t\topts.onStall?.({ phase, elapsedMs });\n\t\t\tcontroller.abort(new Error(`stream stalled: no events for ${elapsedMs}ms`));\n\t\t\tconst message: AssistantMessage = {\n\t\t\t\t...latest,\n\t\t\t\tstopReason: \"error\",\n\t\t\t\terrorMessage: `stream stalled: no events for ${elapsedMs}ms`,\n\t\t\t};\n\t\t\touter.push({ type: \"error\", reason: \"error\", error: message });\n\t\t};\n\n\t\tlet watchdog = createSilenceWatchdog({\n\t\t\tsilenceMs: opts.connectMs,\n\t\t\tonSilence: () => stall(\"connect\", opts.connectMs),\n\t\t});\n\n\t\tvoid (async () => {\n\t\t\ttry {\n\t\t\t\tfor await (const event of inner) {\n\t\t\t\t\tif (stalled) break;\n\t\t\t\t\tif (!firstEventSeen) {\n\t\t\t\t\t\tfirstEventSeen = true;\n\t\t\t\t\t\twatchdog.disarm();\n\t\t\t\t\t\twatchdog = createSilenceWatchdog({\n\t\t\t\t\t\t\tsilenceMs: opts.idleMs,\n\t\t\t\t\t\t\tonSilence: () => stall(\"stream\", opts.idleMs),\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\twatchdog.touch();\n\t\t\t\t\t}\n\t\t\t\t\tlatest = partialFromEvent(event);\n\t\t\t\t\t// A terminal event ends the turn: disarm synchronously, in the same tick as\n\t\t\t\t\t// the push below, so no watchdog can fire after the consumer's `result()`\n\t\t\t\t\t// promise resolves — a disarm that only happened once the loop later notices\n\t\t\t\t\t// `inner` is done would race with that resolution (it runs a tick or more\n\t\t\t\t\t// later) and could fire a spurious stall on an already-finished stream.\n\t\t\t\t\tconst terminal = event.type === \"done\" || event.type === \"error\";\n\t\t\t\t\tif (terminal) {\n\t\t\t\t\t\twatchdog.disarm();\n\t\t\t\t\t\tcallerSignal?.removeEventListener(\"abort\", onCallerAbort);\n\t\t\t\t\t}\n\t\t\t\t\touter.push(event);\n\t\t\t\t\tif (terminal) return;\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\twatchdog.disarm();\n\t\t\t\tcallerSignal?.removeEventListener(\"abort\", onCallerAbort);\n\t\t\t}\n\t\t})();\n\n\t\treturn outer;\n\t};\n}\n"]}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Silence/idle watchdogs for the reliability kernel.
|
|
3
|
+
*
|
|
4
|
+
* A silence watchdog bounds "running but mute" — it never bounds total runtime,
|
|
5
|
+
* so long tasks that produce output are never killed (autonomy constraint).
|
|
6
|
+
*/
|
|
7
|
+
export function createSilenceWatchdog(opts) {
|
|
8
|
+
let timer;
|
|
9
|
+
let disarmed = false;
|
|
10
|
+
const arm = () => {
|
|
11
|
+
timer = setTimeout(() => {
|
|
12
|
+
disarmed = true;
|
|
13
|
+
timer = undefined;
|
|
14
|
+
opts.onSilence();
|
|
15
|
+
}, opts.silenceMs);
|
|
16
|
+
// Never keep the host process alive just for a watchdog.
|
|
17
|
+
timer.unref?.();
|
|
18
|
+
};
|
|
19
|
+
arm();
|
|
20
|
+
return {
|
|
21
|
+
touch() {
|
|
22
|
+
if (disarmed)
|
|
23
|
+
return;
|
|
24
|
+
if (timer)
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
arm();
|
|
27
|
+
},
|
|
28
|
+
disarm() {
|
|
29
|
+
disarmed = true;
|
|
30
|
+
if (timer)
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
timer = undefined;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// --- Stream-idle watchdog (wraps a StreamFn) -------------------------------
|
|
37
|
+
import { createAssistantMessageEventStream, } from "@caupulican/pi-ai";
|
|
38
|
+
export const DEFAULT_STREAM_IDLE = { idleMs: 30_000, connectMs: 120_000 };
|
|
39
|
+
/** Extracts the current AssistantMessage snapshot carried by any stream event variant. */
|
|
40
|
+
function partialFromEvent(event) {
|
|
41
|
+
if (event.type === "done")
|
|
42
|
+
return event.message;
|
|
43
|
+
if (event.type === "error")
|
|
44
|
+
return event.error;
|
|
45
|
+
return event.partial;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Wrap a StreamFn so a silently dead connection cannot wedge a turn forever.
|
|
49
|
+
*
|
|
50
|
+
* `connectMs` bounds the wait for the first event (connection/first-token allowance);
|
|
51
|
+
* once streaming starts, `idleMs` bounds the gap between subsequent events. On stall,
|
|
52
|
+
* the inner request is aborted and the returned stream resolves immediately with a
|
|
53
|
+
* synthetic `AssistantMessage` (`stopReason: "error"`, `errorMessage: "stream stalled:
|
|
54
|
+
* no events for <n>ms"`) — the exact phrasing `classifyFailure` maps to a retryable
|
|
55
|
+
* `stream_stall`, so the host's retry/failover path takes it from there.
|
|
56
|
+
*
|
|
57
|
+
* A caller-initiated abort (via the options `signal`) is never treated as a stall: it
|
|
58
|
+
* is chained into the wrapper's own controller and the inner stream's own abort result
|
|
59
|
+
* is forwarded untouched.
|
|
60
|
+
*/
|
|
61
|
+
export function withStreamIdleWatchdog(streamFn, options) {
|
|
62
|
+
const opts = { ...DEFAULT_STREAM_IDLE, ...options };
|
|
63
|
+
return async (model, context, streamOptions) => {
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const callerSignal = streamOptions?.signal;
|
|
66
|
+
let callerAborted = callerSignal?.aborted ?? false;
|
|
67
|
+
const onCallerAbort = () => {
|
|
68
|
+
callerAborted = true;
|
|
69
|
+
controller.abort(callerSignal?.reason);
|
|
70
|
+
};
|
|
71
|
+
if (callerAborted)
|
|
72
|
+
controller.abort(callerSignal?.reason);
|
|
73
|
+
else
|
|
74
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
75
|
+
const inner = await streamFn(model, context, { ...streamOptions, signal: controller.signal });
|
|
76
|
+
const outer = createAssistantMessageEventStream();
|
|
77
|
+
// Seeded so a connect-phase stall (no event ever arrived) still has a base message
|
|
78
|
+
// to report on; overwritten with the latest real snapshot once events start flowing.
|
|
79
|
+
let latest = {
|
|
80
|
+
role: "assistant",
|
|
81
|
+
content: [],
|
|
82
|
+
api: model.api,
|
|
83
|
+
provider: model.provider,
|
|
84
|
+
model: model.id,
|
|
85
|
+
usage: {
|
|
86
|
+
input: 0,
|
|
87
|
+
output: 0,
|
|
88
|
+
cacheRead: 0,
|
|
89
|
+
cacheWrite: 0,
|
|
90
|
+
totalTokens: 0,
|
|
91
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
92
|
+
},
|
|
93
|
+
stopReason: "stop",
|
|
94
|
+
timestamp: Date.now(),
|
|
95
|
+
};
|
|
96
|
+
let firstEventSeen = false;
|
|
97
|
+
let stalled = false;
|
|
98
|
+
// Emits the stall result directly (rather than after the inner loop finishes) so a
|
|
99
|
+
// connection that never resolves at all still yields a result promptly — providers
|
|
100
|
+
// are contractually expected to end their stream after abort, but the watchdog does
|
|
101
|
+
// not depend on that to report the stall itself.
|
|
102
|
+
const stall = (phase, elapsedMs) => {
|
|
103
|
+
if (callerAborted || stalled)
|
|
104
|
+
return;
|
|
105
|
+
stalled = true;
|
|
106
|
+
opts.onStall?.({ phase, elapsedMs });
|
|
107
|
+
controller.abort(new Error(`stream stalled: no events for ${elapsedMs}ms`));
|
|
108
|
+
const message = {
|
|
109
|
+
...latest,
|
|
110
|
+
stopReason: "error",
|
|
111
|
+
errorMessage: `stream stalled: no events for ${elapsedMs}ms`,
|
|
112
|
+
};
|
|
113
|
+
outer.push({ type: "error", reason: "error", error: message });
|
|
114
|
+
};
|
|
115
|
+
let watchdog = createSilenceWatchdog({
|
|
116
|
+
silenceMs: opts.connectMs,
|
|
117
|
+
onSilence: () => stall("connect", opts.connectMs),
|
|
118
|
+
});
|
|
119
|
+
void (async () => {
|
|
120
|
+
try {
|
|
121
|
+
for await (const event of inner) {
|
|
122
|
+
if (stalled)
|
|
123
|
+
break;
|
|
124
|
+
if (!firstEventSeen) {
|
|
125
|
+
firstEventSeen = true;
|
|
126
|
+
watchdog.disarm();
|
|
127
|
+
watchdog = createSilenceWatchdog({
|
|
128
|
+
silenceMs: opts.idleMs,
|
|
129
|
+
onSilence: () => stall("stream", opts.idleMs),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
watchdog.touch();
|
|
134
|
+
}
|
|
135
|
+
latest = partialFromEvent(event);
|
|
136
|
+
// A terminal event ends the turn: disarm synchronously, in the same tick as
|
|
137
|
+
// the push below, so no watchdog can fire after the consumer's `result()`
|
|
138
|
+
// promise resolves — a disarm that only happened once the loop later notices
|
|
139
|
+
// `inner` is done would race with that resolution (it runs a tick or more
|
|
140
|
+
// later) and could fire a spurious stall on an already-finished stream.
|
|
141
|
+
const terminal = event.type === "done" || event.type === "error";
|
|
142
|
+
if (terminal) {
|
|
143
|
+
watchdog.disarm();
|
|
144
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
145
|
+
}
|
|
146
|
+
outer.push(event);
|
|
147
|
+
if (terminal)
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
watchdog.disarm();
|
|
153
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
154
|
+
}
|
|
155
|
+
})();
|
|
156
|
+
return outer;
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=watchdogs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watchdogs.js","sourceRoot":"","sources":["../../src/reliability/watchdogs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Silence/idle watchdogs for the reliability kernel.\n *\n * A silence watchdog bounds \"running but mute\" — it never bounds total runtime,\n * so long tasks that produce output are never killed (autonomy constraint).\n */\n\nexport interface SilenceWatchdog {\n\t/** Report activity (output chunk / stream event); resets the countdown. */\n\ttouch(): void;\n\t/** Stop permanently (normal completion). Idempotent. */\n\tdisarm(): void;\n}\n\nexport interface SilenceWatchdogOptions {\n\tsilenceMs: number;\n\t/** Fired at most once, after silenceMs with no touch(). The watchdog self-disarms. */\n\tonSilence: () => void;\n}\n\nexport function createSilenceWatchdog(opts: SilenceWatchdogOptions): SilenceWatchdog {\n\tlet timer: NodeJS.Timeout | undefined;\n\tlet disarmed = false;\n\n\tconst arm = () => {\n\t\ttimer = setTimeout(() => {\n\t\t\tdisarmed = true;\n\t\t\ttimer = undefined;\n\t\t\topts.onSilence();\n\t\t}, opts.silenceMs);\n\t\t// Never keep the host process alive just for a watchdog.\n\t\ttimer.unref?.();\n\t};\n\n\tarm();\n\n\treturn {\n\t\ttouch(): void {\n\t\t\tif (disarmed) return;\n\t\t\tif (timer) clearTimeout(timer);\n\t\t\tarm();\n\t\t},\n\t\tdisarm(): void {\n\t\t\tdisarmed = true;\n\t\t\tif (timer) clearTimeout(timer);\n\t\t\ttimer = undefined;\n\t\t},\n\t};\n}\n\n// --- Stream-idle watchdog (wraps a StreamFn) -------------------------------\n\nimport {\n\ttype AssistantMessage,\n\ttype AssistantMessageEvent,\n\tcreateAssistantMessageEventStream,\n} from \"@caupulican/pi-ai\";\nimport type { StreamFn } from \"../types.ts\";\n\nexport interface StreamIdleOptions {\n\t/** Max ms between events once streaming has started (user-locked default 30s). */\n\tidleMs: number;\n\t/** Max ms to wait for the FIRST event (connection/first-token allowance). */\n\tconnectMs: number;\n\t/** Fired once when a stall is detected, before the inner request is aborted. */\n\tonStall?: (info: { phase: \"connect\" | \"stream\"; elapsedMs: number }) => void;\n}\n\nexport const DEFAULT_STREAM_IDLE: StreamIdleOptions = { idleMs: 30_000, connectMs: 120_000 };\n\n/** Extracts the current AssistantMessage snapshot carried by any stream event variant. */\nfunction partialFromEvent(event: AssistantMessageEvent): AssistantMessage {\n\tif (event.type === \"done\") return event.message;\n\tif (event.type === \"error\") return event.error;\n\treturn event.partial;\n}\n\n/**\n * Wrap a StreamFn so a silently dead connection cannot wedge a turn forever.\n *\n * `connectMs` bounds the wait for the first event (connection/first-token allowance);\n * once streaming starts, `idleMs` bounds the gap between subsequent events. On stall,\n * the inner request is aborted and the returned stream resolves immediately with a\n * synthetic `AssistantMessage` (`stopReason: \"error\"`, `errorMessage: \"stream stalled:\n * no events for <n>ms\"`) — the exact phrasing `classifyFailure` maps to a retryable\n * `stream_stall`, so the host's retry/failover path takes it from there.\n *\n * A caller-initiated abort (via the options `signal`) is never treated as a stall: it\n * is chained into the wrapper's own controller and the inner stream's own abort result\n * is forwarded untouched.\n */\nexport function withStreamIdleWatchdog(streamFn: StreamFn, options?: Partial<StreamIdleOptions>): StreamFn {\n\tconst opts = { ...DEFAULT_STREAM_IDLE, ...options };\n\treturn async (model, context, streamOptions) => {\n\t\tconst controller = new AbortController();\n\t\tconst callerSignal = streamOptions?.signal;\n\t\tlet callerAborted = callerSignal?.aborted ?? false;\n\t\tconst onCallerAbort = () => {\n\t\t\tcallerAborted = true;\n\t\t\tcontroller.abort(callerSignal?.reason);\n\t\t};\n\t\tif (callerAborted) controller.abort(callerSignal?.reason);\n\t\telse callerSignal?.addEventListener(\"abort\", onCallerAbort, { once: true });\n\n\t\tconst inner = await streamFn(model, context, { ...streamOptions, signal: controller.signal });\n\t\tconst outer = createAssistantMessageEventStream();\n\n\t\t// Seeded so a connect-phase stall (no event ever arrived) still has a base message\n\t\t// to report on; overwritten with the latest real snapshot once events start flowing.\n\t\tlet latest: AssistantMessage = {\n\t\t\trole: \"assistant\",\n\t\t\tcontent: [],\n\t\t\tapi: model.api,\n\t\t\tprovider: model.provider,\n\t\t\tmodel: model.id,\n\t\t\tusage: {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\ttotalTokens: 0,\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t},\n\t\t\tstopReason: \"stop\",\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\t\tlet firstEventSeen = false;\n\t\tlet stalled = false;\n\n\t\t// Emits the stall result directly (rather than after the inner loop finishes) so a\n\t\t// connection that never resolves at all still yields a result promptly — providers\n\t\t// are contractually expected to end their stream after abort, but the watchdog does\n\t\t// not depend on that to report the stall itself.\n\t\tconst stall = (phase: \"connect\" | \"stream\", elapsedMs: number) => {\n\t\t\tif (callerAborted || stalled) return;\n\t\t\tstalled = true;\n\t\t\topts.onStall?.({ phase, elapsedMs });\n\t\t\tcontroller.abort(new Error(`stream stalled: no events for ${elapsedMs}ms`));\n\t\t\tconst message: AssistantMessage = {\n\t\t\t\t...latest,\n\t\t\t\tstopReason: \"error\",\n\t\t\t\terrorMessage: `stream stalled: no events for ${elapsedMs}ms`,\n\t\t\t};\n\t\t\touter.push({ type: \"error\", reason: \"error\", error: message });\n\t\t};\n\n\t\tlet watchdog = createSilenceWatchdog({\n\t\t\tsilenceMs: opts.connectMs,\n\t\t\tonSilence: () => stall(\"connect\", opts.connectMs),\n\t\t});\n\n\t\tvoid (async () => {\n\t\t\ttry {\n\t\t\t\tfor await (const event of inner) {\n\t\t\t\t\tif (stalled) break;\n\t\t\t\t\tif (!firstEventSeen) {\n\t\t\t\t\t\tfirstEventSeen = true;\n\t\t\t\t\t\twatchdog.disarm();\n\t\t\t\t\t\twatchdog = createSilenceWatchdog({\n\t\t\t\t\t\t\tsilenceMs: opts.idleMs,\n\t\t\t\t\t\t\tonSilence: () => stall(\"stream\", opts.idleMs),\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\twatchdog.touch();\n\t\t\t\t\t}\n\t\t\t\t\tlatest = partialFromEvent(event);\n\t\t\t\t\t// A terminal event ends the turn: disarm synchronously, in the same tick as\n\t\t\t\t\t// the push below, so no watchdog can fire after the consumer's `result()`\n\t\t\t\t\t// promise resolves — a disarm that only happened once the loop later notices\n\t\t\t\t\t// `inner` is done would race with that resolution (it runs a tick or more\n\t\t\t\t\t// later) and could fire a spurious stall on an already-finished stream.\n\t\t\t\t\tconst terminal = event.type === \"done\" || event.type === \"error\";\n\t\t\t\t\tif (terminal) {\n\t\t\t\t\t\twatchdog.disarm();\n\t\t\t\t\t\tcallerSignal?.removeEventListener(\"abort\", onCallerAbort);\n\t\t\t\t\t}\n\t\t\t\t\touter.push(event);\n\t\t\t\t\tif (terminal) return;\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\twatchdog.disarm();\n\t\t\t\tcallerSignal?.removeEventListener(\"abort\", onCallerAbort);\n\t\t\t}\n\t\t})();\n\n\t\treturn outer;\n\t};\n}\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention budget for tool result details kept in long-lived process memory.
|
|
3
|
+
*
|
|
4
|
+
* Tool result details are UI/log metadata, not provider-visible content. Large
|
|
5
|
+
* graph/search payloads can otherwise accumulate (live appends, session resume,
|
|
6
|
+
* scrollback components) until the interactive Node process hits the V8 heap limit.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MAX_RETAINED_TOOL_RESULT_DETAILS_BYTES: number;
|
|
9
|
+
/**
|
|
10
|
+
* Retention budget for tool result details kept by live TUI scrollback components.
|
|
11
|
+
*
|
|
12
|
+
* Larger than the session budget so deliberately bounded display payloads (e.g. the
|
|
13
|
+
* bash tool's 50KB/2000-line output window) keep their designed expanded view, while
|
|
14
|
+
* pathological multi-megabyte payloads still get stubbed.
|
|
15
|
+
*/
|
|
16
|
+
export declare const MAX_TUI_RETAINED_DETAILS_BYTES: number;
|
|
17
|
+
/** Replace oversized details on any retained holder with a small truncation stub. */
|
|
18
|
+
export declare function compactRetainedDetails(holder: {
|
|
19
|
+
details?: unknown;
|
|
20
|
+
}, maxBytes?: number): void;
|
|
21
|
+
/** Replace oversized tool result details with a small truncation stub. No-op for other roles. */
|
|
22
|
+
export declare function compactToolResultDetailsForRetention(message: {
|
|
23
|
+
role: string;
|
|
24
|
+
details?: unknown;
|
|
25
|
+
}): void;
|
|
26
|
+
//# sourceMappingURL=message-retention.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-retention.d.ts","sourceRoot":"","sources":["../../src/session/message-retention.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,sCAAsC,QAAY,CAAC;AA8ChE;;;;;;GAMG;AACH,eAAO,MAAM,8BAA8B,QAAa,CAAC;AAEzD,qFAAqF;AACrF,wBAAgB,sBAAsB,CACrC,MAAM,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,EAC7B,QAAQ,SAAyC,GAC/C,IAAI,CAUN;AAED,iGAAiG;AACjG,wBAAgB,oCAAoC,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAGvG","sourcesContent":["/**\n * Retention budget for tool result details kept in long-lived process memory.\n *\n * Tool result details are UI/log metadata, not provider-visible content. Large\n * graph/search payloads can otherwise accumulate (live appends, session resume,\n * scrollback components) until the interactive Node process hits the V8 heap limit.\n */\nexport const MAX_RETAINED_TOOL_RESULT_DETAILS_BYTES = 32 * 1024;\n\nfunction estimateJsonLikeBytes(value: unknown, maxBytes: number): { bytes: number; exceeded: boolean } {\n\tconst seen = new WeakSet<object>();\n\tlet bytes = 0;\n\tconst add = (amount: number): boolean => {\n\t\tbytes += amount;\n\t\treturn bytes <= maxBytes;\n\t};\n\tconst visit = (current: unknown): boolean => {\n\t\tif (bytes > maxBytes) return false;\n\t\tif (current === null) return add(4);\n\t\tif (current === undefined) return add(9);\n\t\tif (typeof current === \"string\") return add(current.length * 4 + 2);\n\t\tif (typeof current === \"number\") return add(24);\n\t\tif (typeof current === \"boolean\") return add(current ? 4 : 5);\n\t\tif (typeof current === \"bigint\") return add(current.toString().length + 2);\n\t\tif (typeof current === \"symbol\" || typeof current === \"function\") return add(12);\n\t\tif (typeof current !== \"object\") return add(8);\n\n\t\tconst objectValue = current as Record<string, unknown>;\n\t\tif (seen.has(objectValue)) return add(20);\n\t\tseen.add(objectValue);\n\t\tif (Array.isArray(objectValue)) {\n\t\t\tif (!add(2)) return false;\n\t\t\tfor (let index = 0; index < objectValue.length; index++) {\n\t\t\t\tif (!add(index === 0 ? 0 : 1)) return false;\n\t\t\t\tif (!visit(objectValue[index])) return false;\n\t\t\t}\n\t\t\treturn bytes <= maxBytes;\n\t\t}\n\n\t\tif (!add(2)) return false;\n\t\tlet first = true;\n\t\tfor (const key in objectValue) {\n\t\t\tif (!Object.hasOwn(objectValue, key)) continue;\n\t\t\tif (!add((first ? 0 : 1) + key.length * 4 + 3)) return false;\n\t\t\tfirst = false;\n\t\t\tif (!visit(objectValue[key])) return false;\n\t\t}\n\t\treturn bytes <= maxBytes;\n\t};\n\tvisit(value);\n\treturn { bytes, exceeded: bytes > maxBytes };\n}\n\n/**\n * Retention budget for tool result details kept by live TUI scrollback components.\n *\n * Larger than the session budget so deliberately bounded display payloads (e.g. the\n * bash tool's 50KB/2000-line output window) keep their designed expanded view, while\n * pathological multi-megabyte payloads still get stubbed.\n */\nexport const MAX_TUI_RETAINED_DETAILS_BYTES = 512 * 1024;\n\n/** Replace oversized details on any retained holder with a small truncation stub. */\nexport function compactRetainedDetails(\n\tholder: { details?: unknown },\n\tmaxBytes = MAX_RETAINED_TOOL_RESULT_DETAILS_BYTES,\n): void {\n\tif (holder.details === undefined) return;\n\tconst estimate = estimateJsonLikeBytes(holder.details, maxBytes);\n\tif (!estimate.exceeded) return;\n\tholder.details = {\n\t\tpiToolResultDetailsTruncated: true,\n\t\treason: \"Tool result details exceeded retention budget; model-visible content was retained.\",\n\t\tminimumBytes: estimate.bytes,\n\t\tmaxRetainedBytes: maxBytes,\n\t};\n}\n\n/** Replace oversized tool result details with a small truncation stub. No-op for other roles. */\nexport function compactToolResultDetailsForRetention(message: { role: string; details?: unknown }): void {\n\tif (message.role !== \"toolResult\") return;\n\tcompactRetainedDetails(message);\n}\n"]}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retention budget for tool result details kept in long-lived process memory.
|
|
3
|
+
*
|
|
4
|
+
* Tool result details are UI/log metadata, not provider-visible content. Large
|
|
5
|
+
* graph/search payloads can otherwise accumulate (live appends, session resume,
|
|
6
|
+
* scrollback components) until the interactive Node process hits the V8 heap limit.
|
|
7
|
+
*/
|
|
8
|
+
export const MAX_RETAINED_TOOL_RESULT_DETAILS_BYTES = 32 * 1024;
|
|
9
|
+
function estimateJsonLikeBytes(value, maxBytes) {
|
|
10
|
+
const seen = new WeakSet();
|
|
11
|
+
let bytes = 0;
|
|
12
|
+
const add = (amount) => {
|
|
13
|
+
bytes += amount;
|
|
14
|
+
return bytes <= maxBytes;
|
|
15
|
+
};
|
|
16
|
+
const visit = (current) => {
|
|
17
|
+
if (bytes > maxBytes)
|
|
18
|
+
return false;
|
|
19
|
+
if (current === null)
|
|
20
|
+
return add(4);
|
|
21
|
+
if (current === undefined)
|
|
22
|
+
return add(9);
|
|
23
|
+
if (typeof current === "string")
|
|
24
|
+
return add(current.length * 4 + 2);
|
|
25
|
+
if (typeof current === "number")
|
|
26
|
+
return add(24);
|
|
27
|
+
if (typeof current === "boolean")
|
|
28
|
+
return add(current ? 4 : 5);
|
|
29
|
+
if (typeof current === "bigint")
|
|
30
|
+
return add(current.toString().length + 2);
|
|
31
|
+
if (typeof current === "symbol" || typeof current === "function")
|
|
32
|
+
return add(12);
|
|
33
|
+
if (typeof current !== "object")
|
|
34
|
+
return add(8);
|
|
35
|
+
const objectValue = current;
|
|
36
|
+
if (seen.has(objectValue))
|
|
37
|
+
return add(20);
|
|
38
|
+
seen.add(objectValue);
|
|
39
|
+
if (Array.isArray(objectValue)) {
|
|
40
|
+
if (!add(2))
|
|
41
|
+
return false;
|
|
42
|
+
for (let index = 0; index < objectValue.length; index++) {
|
|
43
|
+
if (!add(index === 0 ? 0 : 1))
|
|
44
|
+
return false;
|
|
45
|
+
if (!visit(objectValue[index]))
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return bytes <= maxBytes;
|
|
49
|
+
}
|
|
50
|
+
if (!add(2))
|
|
51
|
+
return false;
|
|
52
|
+
let first = true;
|
|
53
|
+
for (const key in objectValue) {
|
|
54
|
+
if (!Object.hasOwn(objectValue, key))
|
|
55
|
+
continue;
|
|
56
|
+
if (!add((first ? 0 : 1) + key.length * 4 + 3))
|
|
57
|
+
return false;
|
|
58
|
+
first = false;
|
|
59
|
+
if (!visit(objectValue[key]))
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return bytes <= maxBytes;
|
|
63
|
+
};
|
|
64
|
+
visit(value);
|
|
65
|
+
return { bytes, exceeded: bytes > maxBytes };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Retention budget for tool result details kept by live TUI scrollback components.
|
|
69
|
+
*
|
|
70
|
+
* Larger than the session budget so deliberately bounded display payloads (e.g. the
|
|
71
|
+
* bash tool's 50KB/2000-line output window) keep their designed expanded view, while
|
|
72
|
+
* pathological multi-megabyte payloads still get stubbed.
|
|
73
|
+
*/
|
|
74
|
+
export const MAX_TUI_RETAINED_DETAILS_BYTES = 512 * 1024;
|
|
75
|
+
/** Replace oversized details on any retained holder with a small truncation stub. */
|
|
76
|
+
export function compactRetainedDetails(holder, maxBytes = MAX_RETAINED_TOOL_RESULT_DETAILS_BYTES) {
|
|
77
|
+
if (holder.details === undefined)
|
|
78
|
+
return;
|
|
79
|
+
const estimate = estimateJsonLikeBytes(holder.details, maxBytes);
|
|
80
|
+
if (!estimate.exceeded)
|
|
81
|
+
return;
|
|
82
|
+
holder.details = {
|
|
83
|
+
piToolResultDetailsTruncated: true,
|
|
84
|
+
reason: "Tool result details exceeded retention budget; model-visible content was retained.",
|
|
85
|
+
minimumBytes: estimate.bytes,
|
|
86
|
+
maxRetainedBytes: maxBytes,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Replace oversized tool result details with a small truncation stub. No-op for other roles. */
|
|
90
|
+
export function compactToolResultDetailsForRetention(message) {
|
|
91
|
+
if (message.role !== "toolResult")
|
|
92
|
+
return;
|
|
93
|
+
compactRetainedDetails(message);
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=message-retention.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-retention.js","sourceRoot":"","sources":["../../src/session/message-retention.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sCAAsC,GAAG,EAAE,GAAG,IAAI,CAAC;AAEhE,SAAS,qBAAqB,CAAC,KAAc,EAAE,QAAgB,EAAwC;IACtG,MAAM,IAAI,GAAG,IAAI,OAAO,EAAU,CAAC;IACnC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,MAAM,GAAG,GAAG,CAAC,MAAc,EAAW,EAAE,CAAC;QACxC,KAAK,IAAI,MAAM,CAAC;QAChB,OAAO,KAAK,IAAI,QAAQ,CAAC;IAAA,CACzB,CAAC;IACF,MAAM,KAAK,GAAG,CAAC,OAAgB,EAAW,EAAE,CAAC;QAC5C,IAAI,KAAK,GAAG,QAAQ;YAAE,OAAO,KAAK,CAAC;QACnC,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;QACpC,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;QACzC,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QACpE,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,OAAO,GAAG,CAAC,EAAE,CAAC,CAAC;QAChD,IAAI,OAAO,OAAO,KAAK,SAAS;YAAE,OAAO,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9D,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,OAAO,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC3E,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,OAAO,KAAK,UAAU;YAAE,OAAO,GAAG,CAAC,EAAE,CAAC,CAAC;QACjF,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;QAE/C,MAAM,WAAW,GAAG,OAAkC,CAAC;QACvD,IAAI,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC;YAAE,OAAO,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1C,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACtB,IAAI,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;YAChC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;gBAAE,OAAO,KAAK,CAAC;YAC1B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,WAAW,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;gBACzD,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;oBAAE,OAAO,KAAK,CAAC;gBAC5C,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;oBAAE,OAAO,KAAK,CAAC;YAC9C,CAAC;YACD,OAAO,KAAK,IAAI,QAAQ,CAAC;QAC1B,CAAC;QAED,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;QAC1B,IAAI,KAAK,GAAG,IAAI,CAAC;QACjB,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;YAC/B,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,GAAG,CAAC;gBAAE,SAAS;YAC/C,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC;gBAAE,OAAO,KAAK,CAAC;YAC7D,KAAK,GAAG,KAAK,CAAC;YACd,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;gBAAE,OAAO,KAAK,CAAC;QAC5C,CAAC;QACD,OAAO,KAAK,IAAI,QAAQ,CAAC;IAAA,CACzB,CAAC;IACF,KAAK,CAAC,KAAK,CAAC,CAAC;IACb,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,GAAG,QAAQ,EAAE,CAAC;AAAA,CAC7C;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,GAAG,GAAG,IAAI,CAAC;AAEzD,qFAAqF;AACrF,MAAM,UAAU,sBAAsB,CACrC,MAA6B,EAC7B,QAAQ,GAAG,sCAAsC,EAC1C;IACP,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO;IACzC,MAAM,QAAQ,GAAG,qBAAqB,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACjE,IAAI,CAAC,QAAQ,CAAC,QAAQ;QAAE,OAAO;IAC/B,MAAM,CAAC,OAAO,GAAG;QAChB,4BAA4B,EAAE,IAAI;QAClC,MAAM,EAAE,oFAAoF;QAC5F,YAAY,EAAE,QAAQ,CAAC,KAAK;QAC5B,gBAAgB,EAAE,QAAQ;KAC1B,CAAC;AAAA,CACF;AAED,iGAAiG;AACjG,MAAM,UAAU,oCAAoC,CAAC,OAA4C,EAAQ;IACxG,IAAI,OAAO,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO;IAC1C,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAAA,CAChC","sourcesContent":["/**\n * Retention budget for tool result details kept in long-lived process memory.\n *\n * Tool result details are UI/log metadata, not provider-visible content. Large\n * graph/search payloads can otherwise accumulate (live appends, session resume,\n * scrollback components) until the interactive Node process hits the V8 heap limit.\n */\nexport const MAX_RETAINED_TOOL_RESULT_DETAILS_BYTES = 32 * 1024;\n\nfunction estimateJsonLikeBytes(value: unknown, maxBytes: number): { bytes: number; exceeded: boolean } {\n\tconst seen = new WeakSet<object>();\n\tlet bytes = 0;\n\tconst add = (amount: number): boolean => {\n\t\tbytes += amount;\n\t\treturn bytes <= maxBytes;\n\t};\n\tconst visit = (current: unknown): boolean => {\n\t\tif (bytes > maxBytes) return false;\n\t\tif (current === null) return add(4);\n\t\tif (current === undefined) return add(9);\n\t\tif (typeof current === \"string\") return add(current.length * 4 + 2);\n\t\tif (typeof current === \"number\") return add(24);\n\t\tif (typeof current === \"boolean\") return add(current ? 4 : 5);\n\t\tif (typeof current === \"bigint\") return add(current.toString().length + 2);\n\t\tif (typeof current === \"symbol\" || typeof current === \"function\") return add(12);\n\t\tif (typeof current !== \"object\") return add(8);\n\n\t\tconst objectValue = current as Record<string, unknown>;\n\t\tif (seen.has(objectValue)) return add(20);\n\t\tseen.add(objectValue);\n\t\tif (Array.isArray(objectValue)) {\n\t\t\tif (!add(2)) return false;\n\t\t\tfor (let index = 0; index < objectValue.length; index++) {\n\t\t\t\tif (!add(index === 0 ? 0 : 1)) return false;\n\t\t\t\tif (!visit(objectValue[index])) return false;\n\t\t\t}\n\t\t\treturn bytes <= maxBytes;\n\t\t}\n\n\t\tif (!add(2)) return false;\n\t\tlet first = true;\n\t\tfor (const key in objectValue) {\n\t\t\tif (!Object.hasOwn(objectValue, key)) continue;\n\t\t\tif (!add((first ? 0 : 1) + key.length * 4 + 3)) return false;\n\t\t\tfirst = false;\n\t\t\tif (!visit(objectValue[key])) return false;\n\t\t}\n\t\treturn bytes <= maxBytes;\n\t};\n\tvisit(value);\n\treturn { bytes, exceeded: bytes > maxBytes };\n}\n\n/**\n * Retention budget for tool result details kept by live TUI scrollback components.\n *\n * Larger than the session budget so deliberately bounded display payloads (e.g. the\n * bash tool's 50KB/2000-line output window) keep their designed expanded view, while\n * pathological multi-megabyte payloads still get stubbed.\n */\nexport const MAX_TUI_RETAINED_DETAILS_BYTES = 512 * 1024;\n\n/** Replace oversized details on any retained holder with a small truncation stub. */\nexport function compactRetainedDetails(\n\tholder: { details?: unknown },\n\tmaxBytes = MAX_RETAINED_TOOL_RESULT_DETAILS_BYTES,\n): void {\n\tif (holder.details === undefined) return;\n\tconst estimate = estimateJsonLikeBytes(holder.details, maxBytes);\n\tif (!estimate.exceeded) return;\n\tholder.details = {\n\t\tpiToolResultDetailsTruncated: true,\n\t\treason: \"Tool result details exceeded retention budget; model-visible content was retained.\",\n\t\tminimumBytes: estimate.bytes,\n\t\tmaxRetainedBytes: maxBytes,\n\t};\n}\n\n/** Replace oversized tool result details with a small truncation stub. No-op for other roles. */\nexport function compactToolResultDetailsForRetention(message: { role: string; details?: unknown }): void {\n\tif (message.role !== \"toolResult\") return;\n\tcompactRetainedDetails(message);\n}\n"]}
|