@caupulican/pi-agent-core 0.81.42 → 0.84.1
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/agent-loop.d.ts +3 -2
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +127 -33
- package/dist/agent-loop.js.map +1 -1
- package/dist/agent.d.ts +15 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +9 -38
- package/dist/agent.js.map +1 -1
- package/dist/compaction/branch-summarization.d.ts +1 -1
- package/dist/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/compaction/branch-summarization.js +3 -13
- package/dist/compaction/branch-summarization.js.map +1 -1
- package/dist/compaction/compaction.d.ts +1 -1
- package/dist/compaction/compaction.d.ts.map +1 -1
- package/dist/compaction/compaction.js +26 -57
- package/dist/compaction/compaction.js.map +1 -1
- package/dist/compaction/extraction.d.ts.map +1 -1
- package/dist/compaction/extraction.js +1 -6
- package/dist/compaction/extraction.js.map +1 -1
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/loop.d.ts.map +1 -1
- package/dist/compaction/loop.js.map +1 -1
- package/dist/compaction/token-budget.d.ts.map +1 -1
- package/dist/compaction/token-budget.js +7 -5
- package/dist/compaction/token-budget.js.map +1 -1
- package/dist/compaction/utils.d.ts +5 -0
- package/dist/compaction/utils.d.ts.map +1 -1
- package/dist/compaction/utils.js +20 -0
- package/dist/compaction/utils.js.map +1 -1
- package/dist/compaction/verification.d.ts.map +1 -1
- package/dist/compaction/verification.js +6 -5
- package/dist/compaction/verification.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js.map +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/proxy.d.ts +3 -2
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +88 -56
- package/dist/proxy.js.map +1 -1
- package/dist/reliability/classifier.d.ts.map +1 -1
- package/dist/reliability/classifier.js.map +1 -1
- package/dist/reliability/index.d.ts.map +1 -1
- package/dist/reliability/node.d.ts.map +1 -1
- package/dist/reliability/process-tree.d.ts.map +1 -1
- package/dist/reliability/process-tree.js.map +1 -1
- package/dist/reliability/provider-signatures.d.ts.map +1 -1
- package/dist/reliability/retry-controller.d.ts +1 -1
- package/dist/reliability/retry-controller.d.ts.map +1 -1
- package/dist/reliability/retry-controller.js +2 -7
- package/dist/reliability/retry-controller.js.map +1 -1
- package/dist/reliability/retry.d.ts.map +1 -1
- package/dist/reliability/retry.js.map +1 -1
- package/dist/reliability/watchdogs.d.ts.map +1 -1
- package/dist/reliability/watchdogs.js +24 -41
- package/dist/reliability/watchdogs.js.map +1 -1
- package/dist/session/message-retention.d.ts.map +1 -1
- package/dist/session/message-retention.js.map +1 -1
- package/dist/session/session-manager.d.ts +1 -1
- package/dist/session/session-manager.d.ts.map +1 -1
- package/dist/session/session-manager.js +81 -43
- package/dist/session/session-manager.js.map +1 -1
- package/dist/tool-failure-memory.d.ts.map +1 -1
- package/dist/tool-failure-memory.js +1 -1
- package/dist/tool-failure-memory.js.map +1 -1
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/usage.d.ts +3 -2
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +2 -10
- package/dist/usage.js.map +1 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/shell-output.d.ts.map +1 -1
- package/dist/utils/shell-output.js.map +1 -1
- package/dist/utils/streaming-content.d.ts +23 -0
- package/dist/utils/streaming-content.d.ts.map +1 -0
- package/dist/utils/streaming-content.js +52 -0
- package/dist/utils/streaming-content.js.map +1 -0
- package/dist/utils/truncate.d.ts.map +1 -1
- package/dist/utils/truncate.js +55 -80
- package/dist/utils/truncate.js.map +1 -1
- package/dist/uuid.d.ts +1 -1
- package/dist/uuid.d.ts.map +1 -1
- package/dist/uuid.js +1 -1
- package/dist/uuid.js.map +1 -1
- package/package.json +7 -7
- package/dist/utils/streaming-lines.d.ts +0 -20
- package/dist/utils/streaming-lines.d.ts.map +0 -1
- package/dist/utils/streaming-lines.js +0 -79
- package/dist/utils/streaming-lines.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry-controller.js","sourceRoot":"","sources":["../../src/reliability/retry-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"retry-controller.js","sourceRoot":"","sources":["../../src/reliability/retry-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Host-agnostic auto-retry driver.\n *\n * Owns the retry attempt counter and the abortable backoff for one agent run. It reads the\n * failure verdict from {@link classifyFailure} (fed a host-computed context-overflow flag), so\n * billing/auth are terminal and overflow routes to compaction — never to a pointless retry.\n * The controller only ever touches `agent.state.messages` (it drops the trailing assistant error\n * before retrying); durable history stays the host session's responsibility.\n *\n * Ported from AgentSession._prepareRetry / _isRetryableError so the exact event ordering and\n * exhaustion semantics carry over: the retry window is marked active (isRetrying) before the\n * start event fires, so prompts arriving inside start handlers queue as steering instead of\n * racing the retry continuation.\n */\n\nimport { isContextOverflow } from \"@caupulican/pi-ai/overflow\";\nimport type { AssistantMessage } from \"@caupulican/pi-ai/types\";\nimport type { AgentMessage } from \"../types.ts\";\nimport { classifyFailure } from \"./classifier.ts\";\nimport { computeRetryDelayMs, type RetryPolicy, sleepAbortable } from \"./retry.ts\";\n\n/** The slice of an Agent the retry driver reads and mutates: just the live transcript. */\nexport interface RetryAgent {\n\treadonly state: { messages: AgentMessage[] };\n}\n\nexport interface RetryStartInfo {\n\tattempt: number;\n\tmaxAttempts: number;\n\tdelayMs: number;\n\terrorMessage: string;\n}\n\nexport interface RetryEndInfo {\n\tsuccess: boolean;\n\tattempt: number;\n\tfinalError?: string;\n}\n\nexport interface RetryEvents {\n\tonRetryStart(info: RetryStartInfo): void;\n\tonRetryEnd(info: RetryEndInfo): void;\n}\n\n/** Runtime retry policy: the backoff shape plus the on/off switch the host resolves per call. */\nexport type RetryControllerPolicy = RetryPolicy & { enabled: boolean };\n\nexport class RetryController {\n\tprivate _attempt = 0;\n\tprivate _abortController: AbortController | undefined;\n\tprivate readonly agent: RetryAgent;\n\tprivate readonly getPolicy: () => RetryControllerPolicy;\n\tprivate readonly events: RetryEvents;\n\tprivate readonly getContextWindow: () => number;\n\n\tconstructor(\n\t\tagent: RetryAgent,\n\t\tgetPolicy: () => RetryControllerPolicy,\n\t\tevents: RetryEvents,\n\t\tgetContextWindow: () => number,\n\t) {\n\t\tthis.agent = agent;\n\t\tthis.getPolicy = getPolicy;\n\t\tthis.events = events;\n\t\tthis.getContextWindow = getContextWindow;\n\t}\n\n\t/** Completed retry attempts for the current run (0 when not retrying). */\n\tget attempt(): number {\n\t\treturn this._attempt;\n\t}\n\n\t/** True from the instant onRetryStart fires until the backoff sleep resolves or is aborted. */\n\tget isRetrying(): boolean {\n\t\treturn this._abortController !== undefined;\n\t}\n\n\t/** Clear the attempt counter — the host calls this after a successful turn or a final failure. */\n\treset(): void {\n\t\tthis._attempt = 0;\n\t}\n\n\t/** Cancel an in-progress backoff; the pending prepareRetry resolves false. */\n\tabort(): void {\n\t\tthis._abortController?.abort();\n\t}\n\n\t/**\n\t * Classify `message`; if it is retryable and attempts remain, drop the trailing assistant\n\t * error from agent state, emit the start event, and wait out the backoff (abortable).\n\t * @returns true if the caller should continue the agent, false otherwise.\n\t */\n\tasync prepareRetry(message: AssistantMessage): Promise<boolean> {\n\t\tconst policy = this.getPolicy();\n\t\tif (!policy.enabled) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// The classifier is the single source of the retry verdict: context overflow (host-computed\n\t\t// from the live window) routes to compaction, billing/auth are terminal, transient failures retry.\n\t\tconst classified = classifyFailure({\n\t\t\tmessage: message.errorMessage ?? \"\",\n\t\t\tcontextOverflow: isContextOverflow(message, this.getContextWindow()),\n\t\t\tprovider: message.provider,\n\t\t});\n\t\tif (!classified.retryable) {\n\t\t\treturn false;\n\t\t}\n\n\t\tthis._attempt++;\n\t\tif (this._attempt > policy.maxAttempts) {\n\t\t\t// Preserve the completed attempt count so the host can emit the final failure.\n\t\t\tthis._attempt--;\n\t\t\treturn false;\n\t\t}\n\n\t\tconst delayMs = computeRetryDelayMs(policy, this._attempt, { retryAfterMs: classified.retryAfterMs });\n\n\t\t// The retry window counts as active work from the instant listeners hear about it:\n\t\t// isRetrying must already be true inside onRetryStart handlers so prompts arriving there\n\t\t// queue as steering instead of racing the retry continuation.\n\t\tthis._abortController = new AbortController();\n\n\t\tthis.events.onRetryStart({\n\t\t\tattempt: this._attempt,\n\t\t\tmaxAttempts: policy.maxAttempts,\n\t\t\tdelayMs,\n\t\t\terrorMessage: message.errorMessage || \"Unknown error\",\n\t\t});\n\n\t\t// Remove the trailing assistant error from live agent state (the host session keeps it in history).\n\t\tconst messages = this.agent.state.messages;\n\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\tthis.agent.state.messages = messages.slice(0, -1);\n\t\t}\n\n\t\ttry {\n\t\t\tawait sleepAbortable(delayMs, this._abortController.signal);\n\t\t} catch {\n\t\t\t// Aborted mid-backoff: report the cancellation and reset so the next turn starts clean.\n\t\t\tconst attempt = this._attempt;\n\t\t\tthis._attempt = 0;\n\t\t\tthis.events.onRetryEnd({ success: false, attempt, finalError: \"Retry cancelled\" });\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tthis._abortController = undefined;\n\t\t}\n\n\t\treturn true;\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/reliability/retry.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,MAAM,WAAW,WAAW;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,oBAAoB,EAAE,WAKlC,CAAC;AAEF,MAAM,WAAW,wBAAwB;IACxC,yFAAyF;IACzF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;CACtB;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,wBAAwB,GAAG,MAAM,CAQjH;AAED,6FAA6F;AAC7F,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB9E"
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/reliability/retry.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,MAAM,WAAW,WAAW;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,oBAAoB,EAAE,WAKlC,CAAC;AAEF,MAAM,WAAW,wBAAwB;IACxC,yFAAyF;IACzF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;CACtB;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,wBAAwB,GAAG,MAAM,CAQjH;AAED,6FAA6F;AAC7F,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB9E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/reliability/retry.ts"],"names":[],"mappings":"AASA,MAAM,CAAC,MAAM,oBAAoB,GAAgB;IAChD,WAAW,EAAE,CAAC;IACd,WAAW,EAAE,IAAI;IACjB,UAAU,EAAE,OAAO;IACnB,WAAW,EAAE,CAAC;CACd,CAAC;AASF,MAAM,UAAU,mBAAmB,CAAC,MAAmB,EAAE,OAAe,EAAE,IAA+B
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/reliability/retry.ts"],"names":[],"mappings":"AASA,MAAM,CAAC,MAAM,oBAAoB,GAAgB;IAChD,WAAW,EAAE,CAAC;IACd,WAAW,EAAE,IAAI;IACjB,UAAU,EAAE,OAAO;IACnB,WAAW,EAAE,CAAC;CACd,CAAC;AASF,MAAM,UAAU,mBAAmB,CAAC,MAAmB,EAAE,OAAe,EAAE,IAA+B;IACxG,IAAI,IAAI,EAAE,YAAY,KAAK,SAAS,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IACtG,IAAI,MAAM,CAAC,WAAW,IAAI,CAAC;QAAE,OAAO,WAAW,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC;IAC3C,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,GAAG,WAAW,GAAG,MAAM,EAAE,CAAC,CAAC;AAC9E,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,cAAc,CAAC,EAAU,EAAE,MAAoB;IAC9D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACtC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACrB,MAAM,CAAC,MAAM,CAAC,MAAM,YAAY,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;YAC9E,OAAO;QACR,CAAC;QACD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC7B,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC9C,OAAO,EAAE,CAAC;QACX,CAAC,EAAE,EAAE,CAAC,CAAC;QACP,MAAM,OAAO,GAAG,GAAG,EAAE;YACpB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,CAAC,MAAM,EAAE,MAAM,YAAY,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;QAChF,CAAC,CAAC;QACF,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;AACJ,CAAC","sourcesContent":["/** Retry backoff policy. jitterRatio 0 reproduces legacy fixed 2s/4s/8s behavior. */\nexport interface RetryPolicy {\n\tmaxAttempts: number;\n\tbaseDelayMs: number;\n\tmaxDelayMs: number;\n\t/** Fraction of the computed delay added as uniform random jitter (0..1). */\n\tjitterRatio: number;\n}\n\nexport const DEFAULT_RETRY_POLICY: RetryPolicy = {\n\tmaxAttempts: 3,\n\tbaseDelayMs: 2000,\n\tmaxDelayMs: 120_000,\n\tjitterRatio: 0,\n};\n\nexport interface ComputeRetryDelayOptions {\n\t/** Provider-suggested delay (e.g. from Retry-After); wins over backoff but is capped. */\n\tretryAfterMs?: number;\n\t/** Injectable RNG for deterministic tests. Defaults to Math.random. */\n\trandom?: () => number;\n}\n\nexport function computeRetryDelayMs(policy: RetryPolicy, attempt: number, opts?: ComputeRetryDelayOptions): number {\n\tif (opts?.retryAfterMs !== undefined) {\n\t\treturn Math.min(opts.retryAfterMs, policy.maxDelayMs);\n\t}\n\tconst exponential = Math.min(policy.baseDelayMs * 2 ** (Math.max(1, attempt) - 1), policy.maxDelayMs);\n\tif (policy.jitterRatio <= 0) return exponential;\n\tconst random = opts?.random ?? Math.random;\n\treturn Math.round(exponential + policy.jitterRatio * exponential * random());\n}\n\n/** Abortable sleep: resolves after ms, rejects with the abort reason if the signal fires. */\nexport function sleepAbortable(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(signal.reason instanceof Error ? signal.reason : new Error(\"Aborted\"));\n\t\t\treturn;\n\t\t}\n\t\tconst timer = setTimeout(() => {\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\t\tconst onAbort = () => {\n\t\t\tclearTimeout(timer);\n\t\t\treject(signal?.reason instanceof Error ? signal.reason : new Error(\"Aborted\"));\n\t\t};\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watchdogs.d.ts","sourceRoot":"","sources":["../../src/reliability/watchdogs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,eAAe;IAC/B;sFACkF;IAClF,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,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,CA8BnF;AAYD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AAExD,MAAM,WAAW,iBAAiB;IACjC,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB;0FACsF;IACtF,YAAY,EAAE,MAAM,CAAC;IACrB;;;oEAGgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,UAAU,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACnE;AAED;;8FAE8F;AAC9F,eAAO,MAAM,mBAAmB,EAAE,iBAIjC,CAAC;AAEF,0FAA0F;AAC1F,MAAM,MAAM,yBAAyB,GAAG,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAStG;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,sBAAsB,CACrC,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,yBAAyB,GAC9D,QAAQ,CAyOV","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\t * Pass silenceMs to also change the bound for this and subsequent countdowns. */\n\ttouch(silenceMs?: number): 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\tlet currentSilenceMs = opts.silenceMs;\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}, currentSilenceMs);\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(silenceMs?: number): void {\n\t\t\tif (disarmed) return;\n\t\t\tif (silenceMs !== undefined) currentSilenceMs = silenceMs;\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 Api,\n\ttype AssistantMessage,\n\ttype AssistantMessageEvent,\n\tcreateAssistantMessageEventStream,\n\ttype Model,\n\ttype ProviderResponse,\n} from \"@caupulican/pi-ai\";\nimport type { StreamFn } from \"../types.ts\";\n\nexport type StallPhase = \"connect\" | \"quiet\" | \"active\";\n\nexport interface StreamIdleOptions {\n\t/** Max ms to wait for the FIRST event (connection/first-token allowance). */\n\tconnectMs: number;\n\t/** Max ms between events while content is flowing — the latest content block is\n\t * text or toolCall. A flowing stream that goes silent this long is presumed dead. */\n\tactiveIdleMs: number;\n\t/** Max ms between events while the model is quietly working — no content blocks\n\t * yet (provider queue / prompt prefill / unstreamed reasoning) or the latest block\n\t * is thinking. Deep-thinking models and huge compaction prompts legitimately sit\n\t * here for minutes, so this bound is deliberately generous. */\n\tquietIdleMs: number;\n\t/** Fired once when a stall is detected, before the inner request is aborted. */\n\tonStall?: (info: { phase: StallPhase; elapsedMs: number }) => void;\n}\n\n/** User-locked defaults: connect 120s / active 180s / quiet 600s. The quiet bound must stay\n * below the HTTP dispatcher idle timeout (see coding-agent http-dispatcher.ts, 660s) or the\n * HTTP layer would kill quiet-but-healthy streams before this watchdog ever sees the gap. */\nexport const DEFAULT_STREAM_IDLE: StreamIdleOptions = {\n\tconnectMs: 120_000,\n\tactiveIdleMs: 180_000,\n\tquietIdleMs: 600_000,\n};\n\n/** Re-resolved at the start of every request, so hosts can wire live-tunable settings. */\nexport type StreamIdleOptionsResolver = (...args: Parameters<StreamFn>) => Partial<StreamIdleOptions>;\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 * Phase-aware: `connectMs` bounds the wait for the first event; after that the\n * inter-event bound adapts to what the stream is doing — `quietIdleMs` while the\n * model is quietly working (no content blocks yet, or the latest block is thinking:\n * prefill, provider queues, unstreamed reasoning) and `activeIdleMs` once content is\n * flowing (latest block is text/toolCall). This keeps detection fast where silence is\n * anomalous without killing healthy deep-thinking or compaction-sized requests.\n * No bound ever limits total runtime (autonomy constraint).\n *\n * On stall, the inner request is aborted and the returned stream resolves immediately\n * with a synthetic `AssistantMessage` (`stopReason: \"error\"`, `errorMessage: \"stream\n * stalled: no events for <n>ms (<phase> phase)\"`) — the `stream stalled` phrasing is\n * what `classifyFailure` maps to a retryable `stream_stall`, so the host's\n * retry/failover path takes it from there.\n *\n * Options may be a resolver function; it is re-invoked at the start of every request,\n * so settings changes apply without rewrapping.\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. The inner stream's abort result is\n * forwarded untouched when it provides one; if it just ends, the wrapper synthesizes\n * an aborted terminal event so the returned stream always settles.\n */\nexport function withStreamIdleWatchdog(\n\tstreamFn: StreamFn,\n\toptions?: Partial<StreamIdleOptions> | StreamIdleOptionsResolver,\n): StreamFn {\n\treturn (model, context, streamOptions) => {\n\t\tconst resolved = typeof options === \"function\" ? options(model, context, streamOptions) : options;\n\t\tconst cleaned: Partial<StreamIdleOptions> = {};\n\t\tif (resolved) {\n\t\t\tfor (const [key, val] of Object.entries(resolved)) {\n\t\t\t\tif (val !== undefined) {\n\t\t\t\t\t(cleaned as any)[key] = val;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst opts = { ...DEFAULT_STREAM_IDLE, ...cleaned };\n\n\t\tconst controller = new AbortController();\n\t\tconst callerSignal = streamOptions?.signal;\n\t\tlet callerAborted = callerSignal?.aborted ?? false;\n\t\tconst outer = createAssistantMessageEventStream();\n\t\tlet readySettled = false;\n\t\tlet resolveReady: (stream: typeof outer) => void = () => {};\n\t\tconst ready = new Promise<typeof outer>((resolve) => {\n\t\t\tresolveReady = resolve;\n\t\t});\n\t\tconst settleReady = () => {\n\t\t\tif (readySettled) return;\n\t\t\treadySettled = true;\n\t\t\tresolveReady(outer);\n\t\t};\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 stalled = false;\n\t\tlet firstEventSeen = false;\n\t\tlet transportConfirmed = false;\n\t\tlet streamSetupSettled = false;\n\t\tlet terminalPushed = false;\n\n\t\t// The idle bound adapts per event: quiet while nothing/thinking, active while\n\t\t// text/toolCall content is flowing. Mutable so the onSilence closure always\n\t\t// reports the phase/bound that actually elapsed.\n\t\tlet currentPhase: StallPhase = \"connect\";\n\t\tlet currentBoundMs = opts.connectMs;\n\t\tconst idleBoundFor = (message: AssistantMessage): { phase: StallPhase; ms: number } => {\n\t\t\tconst lastBlock = message.content[message.content.length - 1];\n\t\t\treturn !lastBlock || lastBlock.type === \"thinking\"\n\t\t\t\t? { phase: \"quiet\", ms: opts.quietIdleMs }\n\t\t\t\t: { phase: \"active\", ms: opts.activeIdleMs };\n\t\t};\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: StallPhase, elapsedMs: number) => {\n\t\t\tif (callerAborted || stalled || terminalPushed) return;\n\t\t\tstalled = true;\n\t\t\topts.onStall?.({ phase, elapsedMs });\n\t\t\tconst description = `stream stalled: no events for ${elapsedMs}ms (${phase} phase)`;\n\t\t\tcontroller.abort(new Error(description));\n\t\t\tconst message: AssistantMessage = {\n\t\t\t\t...latest,\n\t\t\t\tstopReason: \"error\",\n\t\t\t\terrorMessage: description,\n\t\t\t};\n\t\t\tterminalPushed = true;\n\t\t\touter.push({ type: \"error\", reason: \"error\", error: message });\n\t\t\tsettleReady();\n\t\t\tcallerSignal?.removeEventListener(\"abort\", onCallerAbort);\n\t\t};\n\n\t\tlet watchdog = createSilenceWatchdog({\n\t\t\tsilenceMs: opts.connectMs,\n\t\t\tonSilence: () => stall(currentPhase, currentBoundMs),\n\t\t});\n\t\tconst markTransportConfirmed = () => {\n\t\t\tif (callerAborted || stalled || firstEventSeen || transportConfirmed) return;\n\t\t\ttransportConfirmed = true;\n\t\t\tcurrentPhase = \"quiet\";\n\t\t\tcurrentBoundMs = opts.quietIdleMs;\n\t\t\twatchdog.touch(opts.quietIdleMs);\n\t\t};\n\t\tconst originalOnResponse = streamOptions?.onResponse;\n\t\tconst pushSetupAbort = () => {\n\t\t\tif (streamSetupSettled || terminalPushed || stalled) return;\n\t\t\tterminalPushed = true;\n\t\t\twatchdog.disarm();\n\t\t\tcallerSignal?.removeEventListener(\"abort\", onCallerAbort);\n\t\t\touter.push({\n\t\t\t\ttype: \"error\",\n\t\t\t\treason: \"aborted\",\n\t\t\t\terror: {\n\t\t\t\t\t...latest,\n\t\t\t\t\tstopReason: \"aborted\",\n\t\t\t\t\terrorMessage: \"stream aborted before terminal event during stream setup\",\n\t\t\t\t},\n\t\t\t});\n\t\t\tsettleReady();\n\t\t};\n\t\tconst onCallerAbort = () => {\n\t\t\tcallerAborted = true;\n\t\t\tcontroller.abort(callerSignal?.reason);\n\t\t\tpushSetupAbort();\n\t\t};\n\t\tif (callerAborted) {\n\t\t\tonCallerAbort();\n\t\t\treturn ready;\n\t\t}\n\t\tcallerSignal?.addEventListener(\"abort\", onCallerAbort, { once: true });\n\n\t\tvoid (async () => {\n\t\t\ttry {\n\t\t\t\tlet inner: Awaited<ReturnType<StreamFn>>;\n\t\t\t\ttry {\n\t\t\t\t\tinner = await streamFn(model, context, {\n\t\t\t\t\t\t...streamOptions,\n\t\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t\t\tonResponse: async (response: ProviderResponse, responseModel: Model<Api>) => {\n\t\t\t\t\t\t\tmarkTransportConfirmed();\n\t\t\t\t\t\t\tawait originalOnResponse?.(response, responseModel);\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tstreamSetupSettled = true;\n\t\t\t\t\tsettleReady();\n\t\t\t\t} catch (error) {\n\t\t\t\t\tstreamSetupSettled = true;\n\t\t\t\t\tif (terminalPushed || stalled) return;\n\t\t\t\t\twatchdog.disarm();\n\t\t\t\t\tcallerSignal?.removeEventListener(\"abort\", onCallerAbort);\n\t\t\t\t\tconst stopReason = callerAborted ? \"aborted\" : \"error\";\n\t\t\t\t\tconst detail = error instanceof Error ? `: ${error.message}` : `: ${String(error)}`;\n\t\t\t\t\tterminalPushed = true;\n\t\t\t\t\touter.push({\n\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\treason: stopReason,\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\t...latest,\n\t\t\t\t\t\t\tstopReason,\n\t\t\t\t\t\t\terrorMessage: callerAborted\n\t\t\t\t\t\t\t\t? `stream aborted before terminal event${detail}`\n\t\t\t\t\t\t\t\t: `stream ended without terminal event${detail}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tsettleReady();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// A setup promise may resolve after a connect stall or caller abort. The outer\n\t\t\t\t// stream is already terminal in that case, so never attach a late event pump.\n\t\t\t\tif (terminalPushed || stalled || callerAborted) return;\n\t\t\t\tfor await (const event of inner) {\n\t\t\t\t\tif (stalled) break;\n\t\t\t\t\tlatest = partialFromEvent(event);\n\t\t\t\t\tconst bound = idleBoundFor(latest);\n\t\t\t\t\tcurrentPhase = bound.phase;\n\t\t\t\t\tcurrentBoundMs = bound.ms;\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: bound.ms,\n\t\t\t\t\t\t\tonSilence: () => stall(currentPhase, currentBoundMs),\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\twatchdog.touch(bound.ms);\n\t\t\t\t\t}\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\tterminalPushed = true;\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\tif (!terminalPushed && !stalled) {\n\t\t\t\t\tconst stopReason = callerAborted ? \"aborted\" : \"error\";\n\t\t\t\t\tconst description = callerAborted\n\t\t\t\t\t\t? \"stream aborted before terminal event\"\n\t\t\t\t\t\t: \"stream ended without terminal event\";\n\t\t\t\t\tterminalPushed = true;\n\t\t\t\t\touter.push({\n\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\treason: stopReason,\n\t\t\t\t\t\terror: { ...latest, stopReason, errorMessage: description },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tif (!terminalPushed && !stalled) {\n\t\t\t\t\tconst stopReason = callerAborted ? \"aborted\" : \"error\";\n\t\t\t\t\tconst detail = error instanceof Error ? `: ${error.message}` : `: ${String(error)}`;\n\t\t\t\t\tterminalPushed = true;\n\t\t\t\t\touter.push({\n\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\treason: stopReason,\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\t...latest,\n\t\t\t\t\t\t\tstopReason,\n\t\t\t\t\t\t\terrorMessage: callerAborted\n\t\t\t\t\t\t\t\t? `stream aborted before terminal event${detail}`\n\t\t\t\t\t\t\t\t: `stream ended without terminal event${detail}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tsettleReady();\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 ready;\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"watchdogs.d.ts","sourceRoot":"","sources":["../../src/reliability/watchdogs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,eAAe;IAC/B;sFACkF;IAClF,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,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,CA8BnF;AAOD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;AAExD,MAAM,WAAW,iBAAiB;IACjC,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB;0FACsF;IACtF,YAAY,EAAE,MAAM,CAAC;IACrB;;;oEAGgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,UAAU,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACnE;AAED;;8FAE8F;AAC9F,eAAO,MAAM,mBAAmB,EAAE,iBAIjC,CAAC;AAEF,0FAA0F;AAC1F,MAAM,MAAM,yBAAyB,GAAG,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAC,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAStG;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,sBAAsB,CACrC,QAAQ,EAAE,QAAQ,EAClB,OAAO,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,yBAAyB,GAC9D,QAAQ,CAsNV"}
|
|
@@ -37,7 +37,8 @@ export function createSilenceWatchdog(opts) {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
// --- Stream-idle watchdog (wraps a StreamFn) -------------------------------
|
|
40
|
-
import { createAssistantMessageEventStream
|
|
40
|
+
import { createAssistantMessageEventStream } from "@caupulican/pi-ai/event-stream";
|
|
41
|
+
import { createEmptyUsage } from "@caupulican/pi-ai/usage";
|
|
41
42
|
/** User-locked defaults: connect 120s / active 180s / quiet 600s. The quiet bound must stay
|
|
42
43
|
* below the HTTP dispatcher idle timeout (see coding-agent http-dispatcher.ts, 660s) or the
|
|
43
44
|
* HTTP layer would kill quiet-but-healthy streams before this watchdog ever sees the gap. */
|
|
@@ -114,14 +115,7 @@ export function withStreamIdleWatchdog(streamFn, options) {
|
|
|
114
115
|
api: model.api,
|
|
115
116
|
provider: model.provider,
|
|
116
117
|
model: model.id,
|
|
117
|
-
usage:
|
|
118
|
-
input: 0,
|
|
119
|
-
output: 0,
|
|
120
|
-
cacheRead: 0,
|
|
121
|
-
cacheWrite: 0,
|
|
122
|
-
totalTokens: 0,
|
|
123
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
124
|
-
},
|
|
118
|
+
usage: createEmptyUsage(),
|
|
125
119
|
stopReason: "stop",
|
|
126
120
|
timestamp: Date.now(),
|
|
127
121
|
};
|
|
@@ -130,6 +124,25 @@ export function withStreamIdleWatchdog(streamFn, options) {
|
|
|
130
124
|
let transportConfirmed = false;
|
|
131
125
|
let streamSetupSettled = false;
|
|
132
126
|
let terminalPushed = false;
|
|
127
|
+
const pushFailure = (error) => {
|
|
128
|
+
if (terminalPushed || stalled)
|
|
129
|
+
return;
|
|
130
|
+
const stopReason = callerAborted ? "aborted" : "error";
|
|
131
|
+
const detail = error instanceof Error ? `: ${error.message}` : error === undefined ? "" : `: ${String(error)}`;
|
|
132
|
+
terminalPushed = true;
|
|
133
|
+
outer.push({
|
|
134
|
+
type: "error",
|
|
135
|
+
reason: stopReason,
|
|
136
|
+
error: {
|
|
137
|
+
...latest,
|
|
138
|
+
stopReason,
|
|
139
|
+
errorMessage: callerAborted
|
|
140
|
+
? `stream aborted before terminal event${detail}`
|
|
141
|
+
: `stream ended without terminal event${detail}`,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
settleReady();
|
|
145
|
+
};
|
|
133
146
|
// The idle bound adapts per event: quiet while nothing/thinking, active while
|
|
134
147
|
// text/toolCall content is flowing. Mutable so the onSilence closure always
|
|
135
148
|
// reports the phase/bound that actually elapsed.
|
|
@@ -223,21 +236,7 @@ export function withStreamIdleWatchdog(streamFn, options) {
|
|
|
223
236
|
return;
|
|
224
237
|
watchdog.disarm();
|
|
225
238
|
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
226
|
-
|
|
227
|
-
const detail = error instanceof Error ? `: ${error.message}` : `: ${String(error)}`;
|
|
228
|
-
terminalPushed = true;
|
|
229
|
-
outer.push({
|
|
230
|
-
type: "error",
|
|
231
|
-
reason: stopReason,
|
|
232
|
-
error: {
|
|
233
|
-
...latest,
|
|
234
|
-
stopReason,
|
|
235
|
-
errorMessage: callerAborted
|
|
236
|
-
? `stream aborted before terminal event${detail}`
|
|
237
|
-
: `stream ended without terminal event${detail}`,
|
|
238
|
-
},
|
|
239
|
-
});
|
|
240
|
-
settleReady();
|
|
239
|
+
pushFailure(error);
|
|
241
240
|
return;
|
|
242
241
|
}
|
|
243
242
|
// A setup promise may resolve after a connect stall or caller abort. The outer
|
|
@@ -291,23 +290,7 @@ export function withStreamIdleWatchdog(streamFn, options) {
|
|
|
291
290
|
}
|
|
292
291
|
}
|
|
293
292
|
catch (error) {
|
|
294
|
-
|
|
295
|
-
const stopReason = callerAborted ? "aborted" : "error";
|
|
296
|
-
const detail = error instanceof Error ? `: ${error.message}` : `: ${String(error)}`;
|
|
297
|
-
terminalPushed = true;
|
|
298
|
-
outer.push({
|
|
299
|
-
type: "error",
|
|
300
|
-
reason: stopReason,
|
|
301
|
-
error: {
|
|
302
|
-
...latest,
|
|
303
|
-
stopReason,
|
|
304
|
-
errorMessage: callerAborted
|
|
305
|
-
? `stream aborted before terminal event${detail}`
|
|
306
|
-
: `stream ended without terminal event${detail}`,
|
|
307
|
-
},
|
|
308
|
-
});
|
|
309
|
-
settleReady();
|
|
310
|
-
}
|
|
293
|
+
pushFailure(error);
|
|
311
294
|
}
|
|
312
295
|
finally {
|
|
313
296
|
watchdog.disarm();
|
|
@@ -1 +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\t * Pass silenceMs to also change the bound for this and subsequent countdowns. */\n\ttouch(silenceMs?: number): 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\tlet currentSilenceMs = opts.silenceMs;\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}, currentSilenceMs);\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(silenceMs?: number): void {\n\t\t\tif (disarmed) return;\n\t\t\tif (silenceMs !== undefined) currentSilenceMs = silenceMs;\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 Api,\n\ttype AssistantMessage,\n\ttype AssistantMessageEvent,\n\tcreateAssistantMessageEventStream,\n\ttype Model,\n\ttype ProviderResponse,\n} from \"@caupulican/pi-ai\";\nimport type { StreamFn } from \"../types.ts\";\n\nexport type StallPhase = \"connect\" | \"quiet\" | \"active\";\n\nexport interface StreamIdleOptions {\n\t/** Max ms to wait for the FIRST event (connection/first-token allowance). */\n\tconnectMs: number;\n\t/** Max ms between events while content is flowing — the latest content block is\n\t * text or toolCall. A flowing stream that goes silent this long is presumed dead. */\n\tactiveIdleMs: number;\n\t/** Max ms between events while the model is quietly working — no content blocks\n\t * yet (provider queue / prompt prefill / unstreamed reasoning) or the latest block\n\t * is thinking. Deep-thinking models and huge compaction prompts legitimately sit\n\t * here for minutes, so this bound is deliberately generous. */\n\tquietIdleMs: number;\n\t/** Fired once when a stall is detected, before the inner request is aborted. */\n\tonStall?: (info: { phase: StallPhase; elapsedMs: number }) => void;\n}\n\n/** User-locked defaults: connect 120s / active 180s / quiet 600s. The quiet bound must stay\n * below the HTTP dispatcher idle timeout (see coding-agent http-dispatcher.ts, 660s) or the\n * HTTP layer would kill quiet-but-healthy streams before this watchdog ever sees the gap. */\nexport const DEFAULT_STREAM_IDLE: StreamIdleOptions = {\n\tconnectMs: 120_000,\n\tactiveIdleMs: 180_000,\n\tquietIdleMs: 600_000,\n};\n\n/** Re-resolved at the start of every request, so hosts can wire live-tunable settings. */\nexport type StreamIdleOptionsResolver = (...args: Parameters<StreamFn>) => Partial<StreamIdleOptions>;\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 * Phase-aware: `connectMs` bounds the wait for the first event; after that the\n * inter-event bound adapts to what the stream is doing — `quietIdleMs` while the\n * model is quietly working (no content blocks yet, or the latest block is thinking:\n * prefill, provider queues, unstreamed reasoning) and `activeIdleMs` once content is\n * flowing (latest block is text/toolCall). This keeps detection fast where silence is\n * anomalous without killing healthy deep-thinking or compaction-sized requests.\n * No bound ever limits total runtime (autonomy constraint).\n *\n * On stall, the inner request is aborted and the returned stream resolves immediately\n * with a synthetic `AssistantMessage` (`stopReason: \"error\"`, `errorMessage: \"stream\n * stalled: no events for <n>ms (<phase> phase)\"`) — the `stream stalled` phrasing is\n * what `classifyFailure` maps to a retryable `stream_stall`, so the host's\n * retry/failover path takes it from there.\n *\n * Options may be a resolver function; it is re-invoked at the start of every request,\n * so settings changes apply without rewrapping.\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. The inner stream's abort result is\n * forwarded untouched when it provides one; if it just ends, the wrapper synthesizes\n * an aborted terminal event so the returned stream always settles.\n */\nexport function withStreamIdleWatchdog(\n\tstreamFn: StreamFn,\n\toptions?: Partial<StreamIdleOptions> | StreamIdleOptionsResolver,\n): StreamFn {\n\treturn (model, context, streamOptions) => {\n\t\tconst resolved = typeof options === \"function\" ? options(model, context, streamOptions) : options;\n\t\tconst cleaned: Partial<StreamIdleOptions> = {};\n\t\tif (resolved) {\n\t\t\tfor (const [key, val] of Object.entries(resolved)) {\n\t\t\t\tif (val !== undefined) {\n\t\t\t\t\t(cleaned as any)[key] = val;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst opts = { ...DEFAULT_STREAM_IDLE, ...cleaned };\n\n\t\tconst controller = new AbortController();\n\t\tconst callerSignal = streamOptions?.signal;\n\t\tlet callerAborted = callerSignal?.aborted ?? false;\n\t\tconst outer = createAssistantMessageEventStream();\n\t\tlet readySettled = false;\n\t\tlet resolveReady: (stream: typeof outer) => void = () => {};\n\t\tconst ready = new Promise<typeof outer>((resolve) => {\n\t\t\tresolveReady = resolve;\n\t\t});\n\t\tconst settleReady = () => {\n\t\t\tif (readySettled) return;\n\t\t\treadySettled = true;\n\t\t\tresolveReady(outer);\n\t\t};\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 stalled = false;\n\t\tlet firstEventSeen = false;\n\t\tlet transportConfirmed = false;\n\t\tlet streamSetupSettled = false;\n\t\tlet terminalPushed = false;\n\n\t\t// The idle bound adapts per event: quiet while nothing/thinking, active while\n\t\t// text/toolCall content is flowing. Mutable so the onSilence closure always\n\t\t// reports the phase/bound that actually elapsed.\n\t\tlet currentPhase: StallPhase = \"connect\";\n\t\tlet currentBoundMs = opts.connectMs;\n\t\tconst idleBoundFor = (message: AssistantMessage): { phase: StallPhase; ms: number } => {\n\t\t\tconst lastBlock = message.content[message.content.length - 1];\n\t\t\treturn !lastBlock || lastBlock.type === \"thinking\"\n\t\t\t\t? { phase: \"quiet\", ms: opts.quietIdleMs }\n\t\t\t\t: { phase: \"active\", ms: opts.activeIdleMs };\n\t\t};\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: StallPhase, elapsedMs: number) => {\n\t\t\tif (callerAborted || stalled || terminalPushed) return;\n\t\t\tstalled = true;\n\t\t\topts.onStall?.({ phase, elapsedMs });\n\t\t\tconst description = `stream stalled: no events for ${elapsedMs}ms (${phase} phase)`;\n\t\t\tcontroller.abort(new Error(description));\n\t\t\tconst message: AssistantMessage = {\n\t\t\t\t...latest,\n\t\t\t\tstopReason: \"error\",\n\t\t\t\terrorMessage: description,\n\t\t\t};\n\t\t\tterminalPushed = true;\n\t\t\touter.push({ type: \"error\", reason: \"error\", error: message });\n\t\t\tsettleReady();\n\t\t\tcallerSignal?.removeEventListener(\"abort\", onCallerAbort);\n\t\t};\n\n\t\tlet watchdog = createSilenceWatchdog({\n\t\t\tsilenceMs: opts.connectMs,\n\t\t\tonSilence: () => stall(currentPhase, currentBoundMs),\n\t\t});\n\t\tconst markTransportConfirmed = () => {\n\t\t\tif (callerAborted || stalled || firstEventSeen || transportConfirmed) return;\n\t\t\ttransportConfirmed = true;\n\t\t\tcurrentPhase = \"quiet\";\n\t\t\tcurrentBoundMs = opts.quietIdleMs;\n\t\t\twatchdog.touch(opts.quietIdleMs);\n\t\t};\n\t\tconst originalOnResponse = streamOptions?.onResponse;\n\t\tconst pushSetupAbort = () => {\n\t\t\tif (streamSetupSettled || terminalPushed || stalled) return;\n\t\t\tterminalPushed = true;\n\t\t\twatchdog.disarm();\n\t\t\tcallerSignal?.removeEventListener(\"abort\", onCallerAbort);\n\t\t\touter.push({\n\t\t\t\ttype: \"error\",\n\t\t\t\treason: \"aborted\",\n\t\t\t\terror: {\n\t\t\t\t\t...latest,\n\t\t\t\t\tstopReason: \"aborted\",\n\t\t\t\t\terrorMessage: \"stream aborted before terminal event during stream setup\",\n\t\t\t\t},\n\t\t\t});\n\t\t\tsettleReady();\n\t\t};\n\t\tconst onCallerAbort = () => {\n\t\t\tcallerAborted = true;\n\t\t\tcontroller.abort(callerSignal?.reason);\n\t\t\tpushSetupAbort();\n\t\t};\n\t\tif (callerAborted) {\n\t\t\tonCallerAbort();\n\t\t\treturn ready;\n\t\t}\n\t\tcallerSignal?.addEventListener(\"abort\", onCallerAbort, { once: true });\n\n\t\tvoid (async () => {\n\t\t\ttry {\n\t\t\t\tlet inner: Awaited<ReturnType<StreamFn>>;\n\t\t\t\ttry {\n\t\t\t\t\tinner = await streamFn(model, context, {\n\t\t\t\t\t\t...streamOptions,\n\t\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t\t\tonResponse: async (response: ProviderResponse, responseModel: Model<Api>) => {\n\t\t\t\t\t\t\tmarkTransportConfirmed();\n\t\t\t\t\t\t\tawait originalOnResponse?.(response, responseModel);\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tstreamSetupSettled = true;\n\t\t\t\t\tsettleReady();\n\t\t\t\t} catch (error) {\n\t\t\t\t\tstreamSetupSettled = true;\n\t\t\t\t\tif (terminalPushed || stalled) return;\n\t\t\t\t\twatchdog.disarm();\n\t\t\t\t\tcallerSignal?.removeEventListener(\"abort\", onCallerAbort);\n\t\t\t\t\tconst stopReason = callerAborted ? \"aborted\" : \"error\";\n\t\t\t\t\tconst detail = error instanceof Error ? `: ${error.message}` : `: ${String(error)}`;\n\t\t\t\t\tterminalPushed = true;\n\t\t\t\t\touter.push({\n\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\treason: stopReason,\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\t...latest,\n\t\t\t\t\t\t\tstopReason,\n\t\t\t\t\t\t\terrorMessage: callerAborted\n\t\t\t\t\t\t\t\t? `stream aborted before terminal event${detail}`\n\t\t\t\t\t\t\t\t: `stream ended without terminal event${detail}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tsettleReady();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// A setup promise may resolve after a connect stall or caller abort. The outer\n\t\t\t\t// stream is already terminal in that case, so never attach a late event pump.\n\t\t\t\tif (terminalPushed || stalled || callerAborted) return;\n\t\t\t\tfor await (const event of inner) {\n\t\t\t\t\tif (stalled) break;\n\t\t\t\t\tlatest = partialFromEvent(event);\n\t\t\t\t\tconst bound = idleBoundFor(latest);\n\t\t\t\t\tcurrentPhase = bound.phase;\n\t\t\t\t\tcurrentBoundMs = bound.ms;\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: bound.ms,\n\t\t\t\t\t\t\tonSilence: () => stall(currentPhase, currentBoundMs),\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\twatchdog.touch(bound.ms);\n\t\t\t\t\t}\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\tterminalPushed = true;\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\tif (!terminalPushed && !stalled) {\n\t\t\t\t\tconst stopReason = callerAborted ? \"aborted\" : \"error\";\n\t\t\t\t\tconst description = callerAborted\n\t\t\t\t\t\t? \"stream aborted before terminal event\"\n\t\t\t\t\t\t: \"stream ended without terminal event\";\n\t\t\t\t\tterminalPushed = true;\n\t\t\t\t\touter.push({\n\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\treason: stopReason,\n\t\t\t\t\t\terror: { ...latest, stopReason, errorMessage: description },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tif (!terminalPushed && !stalled) {\n\t\t\t\t\tconst stopReason = callerAborted ? \"aborted\" : \"error\";\n\t\t\t\t\tconst detail = error instanceof Error ? `: ${error.message}` : `: ${String(error)}`;\n\t\t\t\t\tterminalPushed = true;\n\t\t\t\t\touter.push({\n\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\treason: stopReason,\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\t...latest,\n\t\t\t\t\t\t\tstopReason,\n\t\t\t\t\t\t\terrorMessage: callerAborted\n\t\t\t\t\t\t\t\t? `stream aborted before terminal event${detail}`\n\t\t\t\t\t\t\t\t: `stream ended without terminal event${detail}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tsettleReady();\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 ready;\n\t};\n}\n"]}
|
|
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\t * Pass silenceMs to also change the bound for this and subsequent countdowns. */\n\ttouch(silenceMs?: number): 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\tlet currentSilenceMs = opts.silenceMs;\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}, currentSilenceMs);\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(silenceMs?: number): void {\n\t\t\tif (disarmed) return;\n\t\t\tif (silenceMs !== undefined) currentSilenceMs = silenceMs;\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 { createAssistantMessageEventStream } from \"@caupulican/pi-ai/event-stream\";\nimport type { Api, AssistantMessage, AssistantMessageEvent, Model, ProviderResponse } from \"@caupulican/pi-ai/types\";\nimport { createEmptyUsage } from \"@caupulican/pi-ai/usage\";\nimport type { StreamFn } from \"../types.ts\";\n\nexport type StallPhase = \"connect\" | \"quiet\" | \"active\";\n\nexport interface StreamIdleOptions {\n\t/** Max ms to wait for the FIRST event (connection/first-token allowance). */\n\tconnectMs: number;\n\t/** Max ms between events while content is flowing — the latest content block is\n\t * text or toolCall. A flowing stream that goes silent this long is presumed dead. */\n\tactiveIdleMs: number;\n\t/** Max ms between events while the model is quietly working — no content blocks\n\t * yet (provider queue / prompt prefill / unstreamed reasoning) or the latest block\n\t * is thinking. Deep-thinking models and huge compaction prompts legitimately sit\n\t * here for minutes, so this bound is deliberately generous. */\n\tquietIdleMs: number;\n\t/** Fired once when a stall is detected, before the inner request is aborted. */\n\tonStall?: (info: { phase: StallPhase; elapsedMs: number }) => void;\n}\n\n/** User-locked defaults: connect 120s / active 180s / quiet 600s. The quiet bound must stay\n * below the HTTP dispatcher idle timeout (see coding-agent http-dispatcher.ts, 660s) or the\n * HTTP layer would kill quiet-but-healthy streams before this watchdog ever sees the gap. */\nexport const DEFAULT_STREAM_IDLE: StreamIdleOptions = {\n\tconnectMs: 120_000,\n\tactiveIdleMs: 180_000,\n\tquietIdleMs: 600_000,\n};\n\n/** Re-resolved at the start of every request, so hosts can wire live-tunable settings. */\nexport type StreamIdleOptionsResolver = (...args: Parameters<StreamFn>) => Partial<StreamIdleOptions>;\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 * Phase-aware: `connectMs` bounds the wait for the first event; after that the\n * inter-event bound adapts to what the stream is doing — `quietIdleMs` while the\n * model is quietly working (no content blocks yet, or the latest block is thinking:\n * prefill, provider queues, unstreamed reasoning) and `activeIdleMs` once content is\n * flowing (latest block is text/toolCall). This keeps detection fast where silence is\n * anomalous without killing healthy deep-thinking or compaction-sized requests.\n * No bound ever limits total runtime (autonomy constraint).\n *\n * On stall, the inner request is aborted and the returned stream resolves immediately\n * with a synthetic `AssistantMessage` (`stopReason: \"error\"`, `errorMessage: \"stream\n * stalled: no events for <n>ms (<phase> phase)\"`) — the `stream stalled` phrasing is\n * what `classifyFailure` maps to a retryable `stream_stall`, so the host's\n * retry/failover path takes it from there.\n *\n * Options may be a resolver function; it is re-invoked at the start of every request,\n * so settings changes apply without rewrapping.\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. The inner stream's abort result is\n * forwarded untouched when it provides one; if it just ends, the wrapper synthesizes\n * an aborted terminal event so the returned stream always settles.\n */\nexport function withStreamIdleWatchdog(\n\tstreamFn: StreamFn,\n\toptions?: Partial<StreamIdleOptions> | StreamIdleOptionsResolver,\n): StreamFn {\n\treturn (model, context, streamOptions) => {\n\t\tconst resolved = typeof options === \"function\" ? options(model, context, streamOptions) : options;\n\t\tconst cleaned: Partial<StreamIdleOptions> = {};\n\t\tif (resolved) {\n\t\t\tfor (const [key, val] of Object.entries(resolved)) {\n\t\t\t\tif (val !== undefined) {\n\t\t\t\t\t(cleaned as any)[key] = val;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst opts = { ...DEFAULT_STREAM_IDLE, ...cleaned };\n\n\t\tconst controller = new AbortController();\n\t\tconst callerSignal = streamOptions?.signal;\n\t\tlet callerAborted = callerSignal?.aborted ?? false;\n\t\tconst outer = createAssistantMessageEventStream();\n\t\tlet readySettled = false;\n\t\tlet resolveReady: (stream: typeof outer) => void = () => {};\n\t\tconst ready = new Promise<typeof outer>((resolve) => {\n\t\t\tresolveReady = resolve;\n\t\t});\n\t\tconst settleReady = () => {\n\t\t\tif (readySettled) return;\n\t\t\treadySettled = true;\n\t\t\tresolveReady(outer);\n\t\t};\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: createEmptyUsage(),\n\t\t\tstopReason: \"stop\",\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\t\tlet stalled = false;\n\t\tlet firstEventSeen = false;\n\t\tlet transportConfirmed = false;\n\t\tlet streamSetupSettled = false;\n\t\tlet terminalPushed = false;\n\t\tconst pushFailure = (error?: unknown) => {\n\t\t\tif (terminalPushed || stalled) return;\n\t\t\tconst stopReason = callerAborted ? \"aborted\" : \"error\";\n\t\t\tconst detail = error instanceof Error ? `: ${error.message}` : error === undefined ? \"\" : `: ${String(error)}`;\n\t\t\tterminalPushed = true;\n\t\t\touter.push({\n\t\t\t\ttype: \"error\",\n\t\t\t\treason: stopReason,\n\t\t\t\terror: {\n\t\t\t\t\t...latest,\n\t\t\t\t\tstopReason,\n\t\t\t\t\terrorMessage: callerAborted\n\t\t\t\t\t\t? `stream aborted before terminal event${detail}`\n\t\t\t\t\t\t: `stream ended without terminal event${detail}`,\n\t\t\t\t},\n\t\t\t});\n\t\t\tsettleReady();\n\t\t};\n\n\t\t// The idle bound adapts per event: quiet while nothing/thinking, active while\n\t\t// text/toolCall content is flowing. Mutable so the onSilence closure always\n\t\t// reports the phase/bound that actually elapsed.\n\t\tlet currentPhase: StallPhase = \"connect\";\n\t\tlet currentBoundMs = opts.connectMs;\n\t\tconst idleBoundFor = (message: AssistantMessage): { phase: StallPhase; ms: number } => {\n\t\t\tconst lastBlock = message.content[message.content.length - 1];\n\t\t\treturn !lastBlock || lastBlock.type === \"thinking\"\n\t\t\t\t? { phase: \"quiet\", ms: opts.quietIdleMs }\n\t\t\t\t: { phase: \"active\", ms: opts.activeIdleMs };\n\t\t};\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: StallPhase, elapsedMs: number) => {\n\t\t\tif (callerAborted || stalled || terminalPushed) return;\n\t\t\tstalled = true;\n\t\t\topts.onStall?.({ phase, elapsedMs });\n\t\t\tconst description = `stream stalled: no events for ${elapsedMs}ms (${phase} phase)`;\n\t\t\tcontroller.abort(new Error(description));\n\t\t\tconst message: AssistantMessage = {\n\t\t\t\t...latest,\n\t\t\t\tstopReason: \"error\",\n\t\t\t\terrorMessage: description,\n\t\t\t};\n\t\t\tterminalPushed = true;\n\t\t\touter.push({ type: \"error\", reason: \"error\", error: message });\n\t\t\tsettleReady();\n\t\t\tcallerSignal?.removeEventListener(\"abort\", onCallerAbort);\n\t\t};\n\n\t\tlet watchdog = createSilenceWatchdog({\n\t\t\tsilenceMs: opts.connectMs,\n\t\t\tonSilence: () => stall(currentPhase, currentBoundMs),\n\t\t});\n\t\tconst markTransportConfirmed = () => {\n\t\t\tif (callerAborted || stalled || firstEventSeen || transportConfirmed) return;\n\t\t\ttransportConfirmed = true;\n\t\t\tcurrentPhase = \"quiet\";\n\t\t\tcurrentBoundMs = opts.quietIdleMs;\n\t\t\twatchdog.touch(opts.quietIdleMs);\n\t\t};\n\t\tconst originalOnResponse = streamOptions?.onResponse;\n\t\tconst pushSetupAbort = () => {\n\t\t\tif (streamSetupSettled || terminalPushed || stalled) return;\n\t\t\tterminalPushed = true;\n\t\t\twatchdog.disarm();\n\t\t\tcallerSignal?.removeEventListener(\"abort\", onCallerAbort);\n\t\t\touter.push({\n\t\t\t\ttype: \"error\",\n\t\t\t\treason: \"aborted\",\n\t\t\t\terror: {\n\t\t\t\t\t...latest,\n\t\t\t\t\tstopReason: \"aborted\",\n\t\t\t\t\terrorMessage: \"stream aborted before terminal event during stream setup\",\n\t\t\t\t},\n\t\t\t});\n\t\t\tsettleReady();\n\t\t};\n\t\tconst onCallerAbort = () => {\n\t\t\tcallerAborted = true;\n\t\t\tcontroller.abort(callerSignal?.reason);\n\t\t\tpushSetupAbort();\n\t\t};\n\t\tif (callerAborted) {\n\t\t\tonCallerAbort();\n\t\t\treturn ready;\n\t\t}\n\t\tcallerSignal?.addEventListener(\"abort\", onCallerAbort, { once: true });\n\n\t\tvoid (async () => {\n\t\t\ttry {\n\t\t\t\tlet inner: Awaited<ReturnType<StreamFn>>;\n\t\t\t\ttry {\n\t\t\t\t\tinner = await streamFn(model, context, {\n\t\t\t\t\t\t...streamOptions,\n\t\t\t\t\t\tsignal: controller.signal,\n\t\t\t\t\t\tonResponse: async (response: ProviderResponse, responseModel: Model<Api>) => {\n\t\t\t\t\t\t\tmarkTransportConfirmed();\n\t\t\t\t\t\t\tawait originalOnResponse?.(response, responseModel);\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t\tstreamSetupSettled = true;\n\t\t\t\t\tsettleReady();\n\t\t\t\t} catch (error) {\n\t\t\t\t\tstreamSetupSettled = true;\n\t\t\t\t\tif (terminalPushed || stalled) return;\n\t\t\t\t\twatchdog.disarm();\n\t\t\t\t\tcallerSignal?.removeEventListener(\"abort\", onCallerAbort);\n\t\t\t\t\tpushFailure(error);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// A setup promise may resolve after a connect stall or caller abort. The outer\n\t\t\t\t// stream is already terminal in that case, so never attach a late event pump.\n\t\t\t\tif (terminalPushed || stalled || callerAborted) return;\n\t\t\t\tfor await (const event of inner) {\n\t\t\t\t\tif (stalled) break;\n\t\t\t\t\tlatest = partialFromEvent(event);\n\t\t\t\t\tconst bound = idleBoundFor(latest);\n\t\t\t\t\tcurrentPhase = bound.phase;\n\t\t\t\t\tcurrentBoundMs = bound.ms;\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: bound.ms,\n\t\t\t\t\t\t\tonSilence: () => stall(currentPhase, currentBoundMs),\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\twatchdog.touch(bound.ms);\n\t\t\t\t\t}\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\tterminalPushed = true;\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\tif (!terminalPushed && !stalled) {\n\t\t\t\t\tconst stopReason = callerAborted ? \"aborted\" : \"error\";\n\t\t\t\t\tconst description = callerAborted\n\t\t\t\t\t\t? \"stream aborted before terminal event\"\n\t\t\t\t\t\t: \"stream ended without terminal event\";\n\t\t\t\t\tterminalPushed = true;\n\t\t\t\t\touter.push({\n\t\t\t\t\t\ttype: \"error\",\n\t\t\t\t\t\treason: stopReason,\n\t\t\t\t\t\terror: { ...latest, stopReason, errorMessage: description },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tpushFailure(error);\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 ready;\n\t};\n}\n"]}
|
|
@@ -1 +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"
|
|
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"}
|
|
@@ -1 +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
|
|
1
|
+
{"version":3,"file":"message-retention.js","sourceRoot":"","sources":["../../src/session/message-retention.ts"],"names":[],"mappings":"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","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"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageContent, Message, TextContent, Usage } from "@caupulican/pi-ai";
|
|
1
|
+
import type { ImageContent, Message, TextContent, Usage } from "@caupulican/pi-ai/types";
|
|
2
2
|
import { type BashExecutionMessage, type CustomMessage } from "../messages.ts";
|
|
3
3
|
import type { AgentMessage } from "../types.ts";
|
|
4
4
|
export declare const CURRENT_SESSION_VERSION = 3;
|