agents 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/{agent-routing-CnkaHb-v.d.ts → agent-routing-DE5zmCQ8.d.ts} +1038 -394
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-tool-types.d.ts +26 -26
- package/dist/{agent-tools-C0XJqWzB.d.ts → agent-tools-DtXMTDGM.d.ts} +7 -7
- package/dist/agent-tools.d.ts +21 -21
- package/dist/browser/ai.js +4 -1
- package/dist/browser/ai.js.map +1 -1
- package/dist/browser/tanstack-ai.js.map +1 -1
- package/dist/callable-decorator-DP__HhBA.d.ts +72 -0
- package/dist/callable-decorator.d.ts +18 -0
- package/dist/callable-decorator.js +71 -0
- package/dist/callable-decorator.js.map +1 -0
- package/dist/capability-BjSKYpzg.js.map +1 -1
- package/dist/capability-runner-Be_-PLR1.d.ts +459 -0
- package/dist/channel-Bnm4S7T2.d.ts +491 -0
- package/dist/channels/ai-sdk.d.ts +46 -0
- package/dist/channels/ai-sdk.js +120 -0
- package/dist/channels/ai-sdk.js.map +1 -0
- package/dist/channels/email.d.ts +95 -0
- package/dist/channels/email.js +323 -0
- package/dist/channels/email.js.map +1 -0
- package/dist/channels/index.d.ts +233 -0
- package/dist/channels/index.js +608 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/slack.d.ts +140 -0
- package/dist/channels/slack.js +614 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/tanstack-ai.d.ts +39 -0
- package/dist/channels/tanstack-ai.js +17 -0
- package/dist/channels/tanstack-ai.js.map +1 -0
- package/dist/channels/telegram.d.ts +106 -0
- package/dist/channels/telegram.js +427 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/voice.d.ts +45 -0
- package/dist/channels/voice.js +122 -0
- package/dist/channels/voice.js.map +1 -0
- package/dist/chat/index.d.ts +2325 -2019
- package/dist/chat/index.js +888 -518
- package/dist/chat/index.js.map +1 -1
- package/dist/chat-sdk/index.d.ts +7 -7
- package/dist/chat-sdk/index.js +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/context/index.d.ts +216 -0
- package/dist/context/index.js +454 -0
- package/dist/context/index.js.map +1 -0
- package/dist/{current-agent-CuMErtly.d.ts → current-agent-Da_C9a3b.d.ts} +90 -107
- package/dist/current-agent-DhoDkSnH.js.map +1 -1
- package/dist/{diagnostics-CaBjfz4J.js → diagnostics-BzvaX2UT.js} +5 -1
- package/dist/diagnostics-BzvaX2UT.js.map +1 -0
- package/dist/diagnostics-C4jcz3VK.js +360 -0
- package/dist/diagnostics-C4jcz3VK.js.map +1 -0
- package/dist/index-BB0kqhIz.d.ts +101 -0
- package/dist/index-BVVgDSdq.d.ts +1 -0
- package/dist/index-XDkuQ7zm.d.ts +89 -0
- package/dist/{index-DcSAZKsB.d.ts → index-YSKgfgg9.d.ts} +3 -1
- package/dist/index.d.ts +91 -82
- package/dist/index.js +3 -2
- package/dist/ingress-BfetZbMO.js +83 -0
- package/dist/ingress-BfetZbMO.js.map +1 -0
- package/dist/internal-CYlgHl1l.js +59 -0
- package/dist/internal-CYlgHl1l.js.map +1 -0
- package/dist/internal_context.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +35 -17
- package/dist/lifecycle/index.js +1 -1
- package/dist/lifecycle-CMRGjZdw.js +1299 -0
- package/dist/lifecycle-CMRGjZdw.js.map +1 -0
- package/dist/mcp/client/index.d.ts +20 -20
- package/dist/mcp/index.d.ts +35 -35
- package/dist/mcp/index.js +1 -1
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/react.d.ts +4 -4
- package/dist/{retries-CAvxtG9d.d.ts → retries-D9Ds-1lz.d.ts} +17 -6
- package/dist/retries.d.ts +8 -6
- package/dist/retries.js +13 -1
- package/dist/retries.js.map +1 -1
- package/dist/routing/index.d.ts +137 -0
- package/dist/routing/index.js +244 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/sanitize-D9TujEK8.js +79 -0
- package/dist/sanitize-D9TujEK8.js.map +1 -0
- package/dist/scheduler-DD9NdYbF.js +665 -0
- package/dist/scheduler-DD9NdYbF.js.map +1 -0
- package/dist/{scheduler-DQoTGoAW.d.ts → scheduler-Dwh85ZGl.d.ts} +21 -22
- package/dist/schedules/index.d.ts +1 -1
- package/dist/schedules/index.js +1 -1
- package/dist/sentence-chunker-BAidJ4DA.d.ts +68 -0
- package/dist/serializable.d.ts +1 -1
- package/dist/sessions/index.d.ts +441 -0
- package/dist/sessions/index.js +2063 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/skills/index.d.ts +99 -0
- package/dist/skills/index.js +254 -5
- package/dist/skills/index.js.map +1 -1
- package/dist/{src-5W6JNKVb.js → src-DlSHshb2.js} +1460 -1110
- package/dist/src-DlSHshb2.js.map +1 -0
- package/dist/streams/index.d.ts +120 -0
- package/dist/streams/index.js +107 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams-D6tJ0NN9.d.ts +370 -0
- package/dist/streams-DZKgAj9b.js +709 -0
- package/dist/streams-DZKgAj9b.js.map +1 -0
- package/dist/sub-routing.d.ts +12 -12
- package/dist/surface-bZZJqBka.js +17 -0
- package/dist/surface-bZZJqBka.js.map +1 -0
- package/dist/tasks/index.d.ts +64 -0
- package/dist/tasks/index.js +2 -0
- package/dist/tasks-BRJ5zgya.d.ts +517 -0
- package/dist/tasks-ylZgBjhj.js +1656 -0
- package/dist/tasks-ylZgBjhj.js.map +1 -0
- package/dist/text-segment-joiner-BtAFQSA_.js +57 -0
- package/dist/text-segment-joiner-BtAFQSA_.js.map +1 -0
- package/dist/text-stream-CpdiKrJB.js +272 -0
- package/dist/text-stream-CpdiKrJB.js.map +1 -0
- package/dist/tokens-nHAKcN6M.js +52 -0
- package/dist/tokens-nHAKcN6M.js.map +1 -0
- package/dist/tool-schema-CBjGPrsQ.js +31 -0
- package/dist/tool-schema-CBjGPrsQ.js.map +1 -0
- package/dist/types-B7LojTe4.d.ts +202 -0
- package/dist/types-_Faxb570.d.ts +439 -0
- package/dist/voice/client.d.ts +226 -0
- package/dist/voice/client.js +932 -0
- package/dist/voice/client.js.map +1 -0
- package/dist/voice/errors.d.ts +43 -0
- package/dist/voice/errors.js +41 -0
- package/dist/voice/errors.js.map +1 -0
- package/dist/voice/index.d.ts +271 -0
- package/dist/voice/index.js +1812 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/react.d.ts +167 -0
- package/dist/voice/react.js +234 -0
- package/dist/voice/react.js.map +1 -0
- package/dist/voice/sfu.d.ts +71 -0
- package/dist/voice/sfu.js +157 -0
- package/dist/voice/sfu.js.map +1 -0
- package/dist/voice/text.d.ts +6 -0
- package/dist/voice/text.js +2 -0
- package/dist/voice/types.d.ts +58 -0
- package/dist/voice/types.js +18 -0
- package/dist/voice/types.js.map +1 -0
- package/dist/voice/workers-ai.d.ts +136 -0
- package/dist/voice/workers-ai.js +568 -0
- package/dist/voice/workers-ai.js.map +1 -0
- package/dist/websockets/index.d.ts +192 -0
- package/dist/websockets/index.js +2 -0
- package/dist/websockets-DUfRHPRq.js +502 -0
- package/dist/websockets-DUfRHPRq.js.map +1 -0
- package/dist/workflow-types.d.ts +25 -25
- package/dist/workflows.d.ts +21 -21
- package/dist/workflows.js +1 -1
- package/docs/agent-class.md +2 -2
- package/docs/agent-tools.md +2 -1
- package/docs/channels.md +323 -0
- package/docs/chat-agents.md +6 -13
- package/docs/context.md +131 -0
- package/docs/index.md +15 -12
- package/docs/lifecycle.md +102 -55
- package/docs/long-running-agents.md +2 -2
- package/docs/mcp-servers.md +5 -1
- package/docs/resumable-streaming.md +1 -1
- package/docs/routing.md +105 -0
- package/docs/sessions.md +237 -871
- package/docs/streams.md +213 -0
- package/docs/sub-agents.md +184 -124
- package/docs/tasks.md +246 -0
- package/docs/voice.md +745 -0
- package/package.json +115 -13
- package/dist/capability-runner-CvHGZqUu.d.ts +0 -150
- package/dist/compaction-helpers-iiKMr2TQ.js +0 -340
- package/dist/compaction-helpers-iiKMr2TQ.js.map +0 -1
- package/dist/compaction-helpers-wUz6M3us.d.ts +0 -621
- package/dist/diagnostics-CaBjfz4J.js.map +0 -1
- package/dist/durable-object-lifecycle-D6nNQJJd.js +0 -862
- package/dist/durable-object-lifecycle-D6nNQJJd.js.map +0 -1
- package/dist/experimental/memory/session/index.d.ts +0 -671
- package/dist/experimental/memory/session/index.js +0 -2379
- package/dist/experimental/memory/session/index.js.map +0 -1
- package/dist/experimental/memory/utils/index.d.ts +0 -96
- package/dist/experimental/memory/utils/index.js +0 -79
- package/dist/experimental/memory/utils/index.js.map +0 -1
- package/dist/scheduler-CR9RHGos.js +0 -857
- package/dist/scheduler-CR9RHGos.js.map +0 -1
- package/dist/src-5W6JNKVb.js.map +0 -1
- package/dist/tool-output-truncation-CNnnGZQ3.js +0 -98
- package/dist/tool-output-truncation-CNnnGZQ3.js.map +0 -1
package/dist/chat/index.d.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
+
import { o as isPlatformFailure } from "../retries-D9Ds-1lz.js";
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "../agent-routing-
|
|
3
|
+
L as AgentToolEvent,
|
|
4
|
+
R as AgentToolEventMessage,
|
|
5
|
+
Y as AgentToolRunState,
|
|
6
|
+
_ as FiberRecoveryContext$1,
|
|
7
|
+
z as AgentToolEventState
|
|
8
|
+
} from "../agent-routing-DE5zmCQ8.js";
|
|
9
|
+
import { _ as TaskStep, m as TaskRunOptions } from "../tasks-BRJ5zgya.js";
|
|
8
10
|
import {
|
|
9
11
|
n as ClientToolSchema,
|
|
10
12
|
r as createToolsFromClientSchemas,
|
|
11
13
|
t as ClientToolExecutor
|
|
12
14
|
} from "../client-tools-aIBO0Fk7.js";
|
|
15
|
+
import { n as Streams } from "../streams-D6tJ0NN9.js";
|
|
13
16
|
import {
|
|
14
17
|
a as applyAgentToolEvent,
|
|
15
18
|
i as AgentToolProgressEmitter,
|
|
@@ -18,9 +21,10 @@ import {
|
|
|
18
21
|
r as AgentToolProgressEmitResult,
|
|
19
22
|
s as interceptAgentToolBroadcast,
|
|
20
23
|
t as AgentToolBroadcastHooks
|
|
21
|
-
} from "../agent-tools-
|
|
24
|
+
} from "../agent-tools-DtXMTDGM.js";
|
|
25
|
+
import { l as SessionMessage } from "../types-B7LojTe4.js";
|
|
22
26
|
import { JSONSchema7, UIMessage } from "ai";
|
|
23
|
-
import { Connection } from "agents";
|
|
27
|
+
import { Connection, FiberRecoveryContext } from "agents";
|
|
24
28
|
|
|
25
29
|
//#region src/chat/message-builder.d.ts
|
|
26
30
|
/** The parts array type from UIMessage */
|
|
@@ -151,10 +155,16 @@ declare function isReplayChunk(
|
|
|
151
155
|
chunk: StreamChunkData
|
|
152
156
|
): boolean;
|
|
153
157
|
//#endregion
|
|
154
|
-
//#region src/
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
//#region src/sessions/sanitize.d.ts
|
|
159
|
+
/**
|
|
160
|
+
* Message hygiene shared by every Sessions write: UTF-8 measurement without a
|
|
161
|
+
* full encoded copy, and removal of ephemeral provider metadata that must not
|
|
162
|
+
* be stored or replayed.
|
|
163
|
+
*/
|
|
164
|
+
/**
|
|
165
|
+
* Measure UTF-8 byte length without allocating a complete encoded copy.
|
|
166
|
+
* Memory stays bounded by a 16 KiB buffer even for near-row-limit strings.
|
|
167
|
+
*/
|
|
158
168
|
declare function byteLength(s: string): number;
|
|
159
169
|
/**
|
|
160
170
|
* Sanitize a message for persistence by removing ephemeral provider-specific
|
|
@@ -163,7 +173,15 @@ declare function byteLength(s: string): number;
|
|
|
163
173
|
* 1. Strips OpenAI ephemeral fields (itemId, reasoningEncryptedContent)
|
|
164
174
|
* 2. Filters truly empty reasoning parts (no text, no remaining providerMetadata)
|
|
165
175
|
*/
|
|
166
|
-
declare function sanitizeMessage
|
|
176
|
+
declare function sanitizeMessage<
|
|
177
|
+
M extends {
|
|
178
|
+
parts: readonly object[];
|
|
179
|
+
}
|
|
180
|
+
>(message: M): M;
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/chat/sanitize.d.ts
|
|
183
|
+
/** Maximum serialized message size before compaction (bytes). 1.8MB with headroom below SQLite's 2MB limit. */
|
|
184
|
+
declare const ROW_MAX_BYTES = 1800000;
|
|
167
185
|
/** Optional hooks for {@link enforceRowSizeLimit}. */
|
|
168
186
|
interface EnforceRowSizeLimitOptions {
|
|
169
187
|
/**
|
|
@@ -330,7 +348,7 @@ type TextSegmentEvent =
|
|
|
330
348
|
/**
|
|
331
349
|
* @internal Converts structured stream chunks into text and boundary events
|
|
332
350
|
* without gluing words across non-text chunks. Sibling-package support for
|
|
333
|
-
*
|
|
351
|
+
* Voice and `@cloudflare/think`, not a public API.
|
|
334
352
|
*/
|
|
335
353
|
declare class TextSegmentJoiner {
|
|
336
354
|
#private;
|
|
@@ -528,7 +546,7 @@ type ChatRecoveryProgressContext = {
|
|
|
528
546
|
* Configuration for durable chat recovery, which is always enabled for chat
|
|
529
547
|
* turns. `true` uses these defaults: `maxAttempts: 10`,
|
|
530
548
|
* `stableTimeoutMs: 10_000`, `noProgressTimeoutMs: 300_000` (5 min),
|
|
531
|
-
* `maxRecoveryWork:
|
|
549
|
+
* `maxRecoveryWork: 10000`, and a generic terminal message. Assign an object to
|
|
532
550
|
* tune recovery without disabling its durable bookkeeping.
|
|
533
551
|
*
|
|
534
552
|
* **Apply this as a class field or in the constructor — never assign it in
|
|
@@ -737,21 +755,16 @@ declare function transition(
|
|
|
737
755
|
//#endregion
|
|
738
756
|
//#region src/chat/resumable-stream.d.ts
|
|
739
757
|
/**
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
* loop (see {@link cleanupStreamBuffers}) revisits any longer-lived rows — e.g.
|
|
744
|
-
* an abandoned in-flight buffer on its 1h window — by waking again each interval
|
|
745
|
-
* until they age out, then stops. Driving cleanup from an alarm (rather than
|
|
746
|
-
* only piggybacking on the next stream completion) ensures idle/one-off chat
|
|
747
|
-
* DOs still reclaim their buffers without waking forever (#1706). Shared by
|
|
748
|
-
* `AIChatAgent` and `Think`.
|
|
758
|
+
* Construct the Streams capability instance a chat host must install to back
|
|
759
|
+
* its `ResumableStream`: identical to `new Streams()` except for the raised
|
|
760
|
+
* per-chunk ceiling that chat's packed segments require.
|
|
749
761
|
*/
|
|
750
|
-
declare
|
|
762
|
+
declare function createChatStreams(): Streams;
|
|
763
|
+
/** Public status vocabulary predates the Streams state names. */
|
|
764
|
+
type PublicStreamStatus = "streaming" | "completed" | "error";
|
|
751
765
|
/**
|
|
752
|
-
* Minimal SQL interface matching Agent's this.sql tagged template.
|
|
753
|
-
*
|
|
754
|
-
* depending on the full Agent class.
|
|
766
|
+
* Minimal SQL interface matching Agent's this.sql tagged template. The
|
|
767
|
+
* adapter uses it exclusively for chat's own legacy tables during migration.
|
|
755
768
|
*/
|
|
756
769
|
type SqlTaggedTemplate = {
|
|
757
770
|
<T = Record<string, unknown>>(
|
|
@@ -759,12 +772,21 @@ type SqlTaggedTemplate = {
|
|
|
759
772
|
...values: (string | number | boolean | null)[]
|
|
760
773
|
): T[];
|
|
761
774
|
};
|
|
775
|
+
/** Host hooks for the chat stream adapter. */
|
|
776
|
+
type ResumableStreamOptions = {
|
|
777
|
+
/**
|
|
778
|
+
* Called with the durable part of the recovery progress marker (retired
|
|
779
|
+
* segments plus credits plus the seeded legacy counter) after it advances,
|
|
780
|
+
* outside any transaction. Hosts mirror it to the pre-derivation KV key so
|
|
781
|
+
* a build rolled back to the KV counter never reads a marker lower than
|
|
782
|
+
* an incident recorded under this one. Fires per stream retired and per
|
|
783
|
+
* credit, never per chunk.
|
|
784
|
+
*/
|
|
785
|
+
onProgress?: (durableSegments: number) => void;
|
|
786
|
+
};
|
|
762
787
|
declare class ResumableStream {
|
|
763
|
-
private sql;
|
|
764
788
|
private _activeStreamId;
|
|
765
789
|
private _activeRequestId;
|
|
766
|
-
/** Monotonic row-ordering index; one increment per flushed segment row. */
|
|
767
|
-
private _segmentIndex;
|
|
768
790
|
/**
|
|
769
791
|
* Whether the active stream was started in this instance (true) or
|
|
770
792
|
* restored from SQLite after hibernation/restart (false). An orphaned
|
|
@@ -774,23 +796,103 @@ declare class ResumableStream {
|
|
|
774
796
|
private _isLive;
|
|
775
797
|
/**
|
|
776
798
|
* Whether the active stream is a continuation. Mirrors the durable
|
|
777
|
-
*
|
|
778
|
-
*
|
|
799
|
+
* metadata so replay frames can carry the flag without a per-replay query;
|
|
800
|
+
* restored from SQLite after hibernation in restore().
|
|
779
801
|
*/
|
|
780
802
|
private _activeIsContinuation;
|
|
781
803
|
private _chunkBuffer;
|
|
782
804
|
private _chunkBufferBytes;
|
|
783
805
|
private _isFlushingChunks;
|
|
784
|
-
private _lastCleanupTime;
|
|
785
|
-
constructor(sql: SqlTaggedTemplate);
|
|
786
806
|
/**
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
790
|
-
|
|
791
|
-
|
|
807
|
+
* A stream whose producer finished but whose row is deliberately still
|
|
808
|
+
* `streaming`: the host will {@link cutover} it together with the message
|
|
809
|
+
* write, or {@link finalizePending} it when there is nothing to persist.
|
|
810
|
+
*/
|
|
811
|
+
private _pendingCutover;
|
|
812
|
+
private readonly ops;
|
|
813
|
+
constructor(
|
|
814
|
+
streams: Streams,
|
|
815
|
+
sql: SqlTaggedTemplate,
|
|
816
|
+
options?: ResumableStreamOptions
|
|
817
|
+
);
|
|
818
|
+
private readonly _sql;
|
|
819
|
+
private readonly _onProgress;
|
|
820
|
+
/**
|
|
821
|
+
* Tell the host the durable part of the marker moved. Called after the
|
|
822
|
+
* write that moved it has left any transaction, never inside one: the
|
|
823
|
+
* host's mirror is an async KV put, which a synchronous transaction
|
|
824
|
+
* would reject.
|
|
825
|
+
*/
|
|
826
|
+
private _notifyProgress;
|
|
827
|
+
/**
|
|
828
|
+
* One row: `retired` accumulates the segments of deleted streams and
|
|
829
|
+
* explicit credits; `legacy` holds the pre-derivation KV counter, folded
|
|
830
|
+
* in once. They are separate columns so a seed can never swallow
|
|
831
|
+
* segments retired before it landed, and a repeated seed is idempotent.
|
|
832
|
+
*/
|
|
833
|
+
private _ensureProgressTable;
|
|
834
|
+
private _retiredSegments;
|
|
835
|
+
/** Add `segments` to the retired total. One row write; a no-op for zero. */
|
|
836
|
+
private _retire;
|
|
837
|
+
/**
|
|
838
|
+
* Segments a row still accounts for: a live stream's log tail, a settled
|
|
839
|
+
* stream's final cursor (stamped exact at settlement).
|
|
840
|
+
*/
|
|
841
|
+
private _segmentsOf;
|
|
842
|
+
/**
|
|
843
|
+
* Monotonic count of durably flushed chat segments on this Durable
|
|
844
|
+
* Object, plus explicit credits (see {@link creditProgress}): the recovery
|
|
845
|
+
* engine's forward-progress marker. Advances only when a segment lands in
|
|
846
|
+
* the log — never on a reconnect replay or a recovery re-persist, which
|
|
847
|
+
* read the log without appending — and is untouched by compaction, which
|
|
848
|
+
* rewrites the transcript, not the log. Reads the stream rows plus one
|
|
849
|
+
* log-tail row per live stream: called at incident evaluation, not on the
|
|
850
|
+
* hot path.
|
|
851
|
+
*
|
|
852
|
+
* A chat row leaving the table by any path — this adapter's cutover,
|
|
853
|
+
* reclaim and clear, or the capability's own `delete()` — passes through
|
|
854
|
+
* the deletion hook, so its segments are retired before they are gone
|
|
855
|
+
* and the marker never moves on a deletion.
|
|
856
|
+
*/
|
|
857
|
+
progressMarker(): number;
|
|
858
|
+
/**
|
|
859
|
+
* Credit one unit of forward progress that the log cannot see: a parent
|
|
860
|
+
* forwarding a sub-agent's output (N9) produces no chunks of its own, yet
|
|
861
|
+
* that output is the parent turn advancing. One row write; callers
|
|
862
|
+
* throttle.
|
|
863
|
+
*/
|
|
864
|
+
creditProgress(): void;
|
|
865
|
+
/**
|
|
866
|
+
* Carry the pre-derivation KV counter forward: the marker must not read
|
|
867
|
+
* lower after the upgrade than the high-water mark an in-flight incident
|
|
868
|
+
* already recorded, or a progressing turn would look stuck until the log
|
|
869
|
+
* caught up. The counter is never written again, so its value is a
|
|
870
|
+
* constant this folds into its own column by max — idempotent across
|
|
871
|
+
* isolates, and never touching segments retired before the seed landed.
|
|
872
|
+
* A no-op for zero, so a fresh object never writes.
|
|
873
|
+
*
|
|
874
|
+
* The counter already credited a stream that was in flight at the
|
|
875
|
+
* upgrade, and that stream's live segments count again here, so the
|
|
876
|
+
* first read after the upgrade can exceed the counter by those segments.
|
|
877
|
+
* That reads as progress once, and hands an in-flight incident one extra
|
|
878
|
+
* no-progress window; it cannot recur.
|
|
879
|
+
*/
|
|
880
|
+
seedProgress(legacyTotal: number): void;
|
|
881
|
+
/**
|
|
882
|
+
* Delete chat rows. The deletion hook folds each row's segments into the
|
|
883
|
+
* retired total in the same synchronous block, retire before delete, so
|
|
884
|
+
* a partial commit could only ever count a stream twice, never lose it.
|
|
792
885
|
*/
|
|
793
|
-
private
|
|
886
|
+
private _deleteRetiring;
|
|
887
|
+
/**
|
|
888
|
+
* One-time migration of the pre-capability `cf_ai_chat_stream_*` tables
|
|
889
|
+
* into the Streams tables, preserving in-flight resumability across the
|
|
890
|
+
* upgrade (an active stream keeps its id, chunks, and last-activity), then
|
|
891
|
+
* dropping the legacy tables. Tolerates the pre-#1691/#1733 metadata
|
|
892
|
+
* schema (no `message_id` / `is_continuation` columns). The host `sql`
|
|
893
|
+
* handle touches only these chat-owned legacy tables.
|
|
894
|
+
*/
|
|
895
|
+
private _migrateLegacyTables;
|
|
794
896
|
get activeStreamId(): string | null;
|
|
795
897
|
get activeRequestId(): string | null;
|
|
796
898
|
hasActiveStream(): boolean;
|
|
@@ -801,7 +903,7 @@ declare class ResumableStream {
|
|
|
801
903
|
get isLive(): boolean;
|
|
802
904
|
/**
|
|
803
905
|
* Start tracking a new stream for resumable streaming.
|
|
804
|
-
* Creates
|
|
906
|
+
* Creates the backing stream row and sets up tracking state.
|
|
805
907
|
* @param requestId - The unique ID of the chat request
|
|
806
908
|
* @returns The generated stream ID
|
|
807
909
|
*/
|
|
@@ -816,7 +918,7 @@ declare class ResumableStream {
|
|
|
816
918
|
* The assistant message id an orphaned stream was producing — the same id the
|
|
817
919
|
* live path persists under, so recovery re-associates reconstructed chunks
|
|
818
920
|
* with the correct message (#1691). Returns null when the row is missing or
|
|
819
|
-
*
|
|
921
|
+
* predates message-id tracking.
|
|
820
922
|
*/
|
|
821
923
|
getStreamMessageId(streamId: string): string | null;
|
|
822
924
|
/**
|
|
@@ -824,15 +926,44 @@ declare class ResumableStream {
|
|
|
824
926
|
* @param streamId - The stream to mark as completed
|
|
825
927
|
*/
|
|
826
928
|
complete(streamId: string): void;
|
|
929
|
+
/**
|
|
930
|
+
* The producer finished, but leave the row `streaming` for the cutover:
|
|
931
|
+
* the host persists the message and settles the stream in one
|
|
932
|
+
* transaction with {@link cutover}. Until then a crash leaves the stream
|
|
933
|
+
* live — exactly the evidence recovery rebuilds the message from. The
|
|
934
|
+
* host MUST follow with {@link cutover} or {@link finalizePending}.
|
|
935
|
+
*/
|
|
936
|
+
finish(streamId: string): void;
|
|
937
|
+
/** The stream {@link finish}ed and awaiting its cutover, if any. */
|
|
938
|
+
get pendingCutoverId(): string | null;
|
|
939
|
+
/**
|
|
940
|
+
* The cutover: settle the stream, run `persist` (synchronous writes — the
|
|
941
|
+
* message), and delete the stream's rows in one SQLite transaction. A
|
|
942
|
+
* crash leaves either the live stream or the finished message, never
|
|
943
|
+
* neither; nothing is left to sweep. `discard: false` keeps the settled
|
|
944
|
+
* rows (an agent-tool child whose parent still tails them); they are
|
|
945
|
+
* reclaimed by the next {@link start}.
|
|
946
|
+
*/
|
|
947
|
+
cutover(
|
|
948
|
+
streamId: string,
|
|
949
|
+
persist: () => void,
|
|
950
|
+
options?: {
|
|
951
|
+
discard?: boolean;
|
|
952
|
+
}
|
|
953
|
+
): void;
|
|
954
|
+
/**
|
|
955
|
+
* Settle a {@link finish}ed stream that had nothing to persist (no parts,
|
|
956
|
+
* a persist that threw). Idempotent; a no-op when nothing is pending.
|
|
957
|
+
*/
|
|
958
|
+
finalizePending(): void;
|
|
959
|
+
private _clearActive;
|
|
827
960
|
/**
|
|
828
961
|
* Mark a stream as errored and clean up state.
|
|
829
962
|
* @param streamId - The stream to mark as errored
|
|
830
963
|
*/
|
|
831
964
|
markError(streamId: string): void;
|
|
832
|
-
/** Maximum chunk body size before skipping storage (bytes). Prevents SQLite row limit crash. */
|
|
833
|
-
private static CHUNK_MAX_BYTES;
|
|
834
965
|
/**
|
|
835
|
-
* Buffer a stream chunk for batch write to
|
|
966
|
+
* Buffer a stream chunk for batch write to storage.
|
|
836
967
|
* Chunks exceeding the row size limit are skipped to prevent crashes.
|
|
837
968
|
* The chunk is still broadcast to live clients (caller handles that),
|
|
838
969
|
* but will be missing from replay on reconnection.
|
|
@@ -841,15 +972,24 @@ declare class ResumableStream {
|
|
|
841
972
|
*/
|
|
842
973
|
storeChunk(streamId: string, body: string): void;
|
|
843
974
|
/**
|
|
844
|
-
* Flush the buffered chunks to
|
|
975
|
+
* Flush the buffered chunks to storage as a single packed segment.
|
|
845
976
|
* Uses a lock to prevent concurrent flush operations.
|
|
846
977
|
*
|
|
847
|
-
* The whole buffer becomes one
|
|
848
|
-
*
|
|
849
|
-
* inflation, while a multi-chunk segment stores a JSON
|
|
850
|
-
* collapses N chunk
|
|
978
|
+
* The whole buffer becomes one stored chunk on the backing stream: a
|
|
979
|
+
* single-chunk segment is stored unwrapped so a large chunk avoids
|
|
980
|
+
* array-escaping inflation, while a multi-chunk segment stores a JSON
|
|
981
|
+
* array of bodies. This collapses N chunk writes into one fenced append,
|
|
982
|
+
* cutting rows written / stored / scanned.
|
|
851
983
|
*/
|
|
852
984
|
flushBuffer(): void;
|
|
985
|
+
/**
|
|
986
|
+
* Stored chunk bodies for one stream, packed segments expanded, in order.
|
|
987
|
+
* A generator over paged reads, so replaying a large turn holds one page
|
|
988
|
+
* of segments in memory instead of the whole stored stream; iteration is
|
|
989
|
+
* synchronous end to end (WebSocket sends don't await), so the pages see
|
|
990
|
+
* a consistent log.
|
|
991
|
+
*/
|
|
992
|
+
private _storedBodies;
|
|
853
993
|
/**
|
|
854
994
|
* Send stored stream chunks to a connection for replay.
|
|
855
995
|
* Chunks are marked with replay: true so the client can batch-apply them.
|
|
@@ -860,11 +1000,10 @@ declare class ResumableStream {
|
|
|
860
1000
|
* - **Orphaned stream** (restored from SQLite after hibernation, no reader):
|
|
861
1001
|
* sends chunks + `done` and completes the stream. The caller should
|
|
862
1002
|
* reconstruct and persist the partial message from the stored chunks.
|
|
863
|
-
* - **Completed during replay** (defensive): sends chunks + `done`.
|
|
864
1003
|
*
|
|
865
|
-
* All sends
|
|
866
|
-
*
|
|
867
|
-
*
|
|
1004
|
+
* All sends tolerate a WebSocket closing mid-replay. If the connection
|
|
1005
|
+
* drops while iterating chunks the stream is left active so the next
|
|
1006
|
+
* reconnect can retry.
|
|
868
1007
|
*
|
|
869
1008
|
* @param connection - The WebSocket connection
|
|
870
1009
|
* @param requestId - The original request ID
|
|
@@ -872,6 +1011,13 @@ declare class ResumableStream {
|
|
|
872
1011
|
* When non-null the caller should reconstruct the message from chunks.
|
|
873
1012
|
*/
|
|
874
1013
|
replayChunks(connection: Connection, requestId: string): string | null;
|
|
1014
|
+
/**
|
|
1015
|
+
* Latest CHAT-owned row carrying a request tag. The stream table is
|
|
1016
|
+
* shared with application producers and tags are non-unique, so the
|
|
1017
|
+
* newest row by tag alone could be an unrelated stream masking chat's
|
|
1018
|
+
* recovery evidence — ownership is the `cfChat` metadata marker.
|
|
1019
|
+
*/
|
|
1020
|
+
private _latestChatRowByTag;
|
|
875
1021
|
replayCompletedChunksByRequestId(
|
|
876
1022
|
connection: Connection,
|
|
877
1023
|
requestId: string
|
|
@@ -882,9 +1028,7 @@ declare class ResumableStream {
|
|
|
882
1028
|
* frame carrying the durable terminal record's error text, mirroring what a
|
|
883
1029
|
* live client observed (content chunks, then the error). Without this, a
|
|
884
1030
|
* client that missed broadcast frames while disconnected has no other
|
|
885
|
-
* channel to the pre-error partial content
|
|
886
|
-
* messages on connect, and {@link replayCompletedChunksByRequestId} only
|
|
887
|
-
* serves `completed` streams (#1575).
|
|
1031
|
+
* channel to the pre-error partial content (#1575).
|
|
888
1032
|
*
|
|
889
1033
|
* Returns true when the caller should proceed to send its terminal frame:
|
|
890
1034
|
* either no errored stream existed (nothing to replay) or its chunks were
|
|
@@ -896,13 +1040,24 @@ declare class ResumableStream {
|
|
|
896
1040
|
connection: Connection,
|
|
897
1041
|
requestId: string
|
|
898
1042
|
): boolean;
|
|
899
|
-
/** Latest stream row for a request with the given terminal status. */
|
|
900
|
-
private _latestStreamForRequest;
|
|
901
1043
|
/**
|
|
902
|
-
*
|
|
903
|
-
*
|
|
1044
|
+
* Latest chat stream row for a request regardless of status — the recovery
|
|
1045
|
+
* engines' stream-evidence lookup.
|
|
1046
|
+
*/
|
|
1047
|
+
latestStreamInfoForRequest(requestId: string): {
|
|
1048
|
+
id: string;
|
|
1049
|
+
status: PublicStreamStatus;
|
|
1050
|
+
createdAt: number;
|
|
1051
|
+
} | null;
|
|
1052
|
+
/**
|
|
1053
|
+
* Latest in-flight chat stream for a request — recoverable-turn evidence.
|
|
904
1054
|
*/
|
|
905
|
-
|
|
1055
|
+
latestActiveStreamInfoForRequest(requestId: string): {
|
|
1056
|
+
id: string;
|
|
1057
|
+
createdAt: number;
|
|
1058
|
+
} | null;
|
|
1059
|
+
/** Every chat-owned stream row, newest first. */
|
|
1060
|
+
private _chatRows;
|
|
906
1061
|
/**
|
|
907
1062
|
* Restore active stream state if the agent was restarted during streaming.
|
|
908
1063
|
* All streams are restored regardless of age — stale cleanup happens
|
|
@@ -910,38 +1065,37 @@ declare class ResumableStream {
|
|
|
910
1065
|
*/
|
|
911
1066
|
restore(): void;
|
|
912
1067
|
/**
|
|
913
|
-
* Clear all stream data (called on chat history clear).
|
|
1068
|
+
* Clear all chat stream data (called on chat history clear). Streams other
|
|
1069
|
+
* producers opened on the same Durable Object are untouched.
|
|
914
1070
|
*/
|
|
915
1071
|
clearAll(): void;
|
|
916
1072
|
/**
|
|
917
|
-
*
|
|
1073
|
+
* Remove all chat stream data (called on destroy). The backing tables
|
|
1074
|
+
* belong to the Streams capability and are shared with other producers,
|
|
1075
|
+
* so this deletes chat's rows rather than dropping tables. Buffered
|
|
1076
|
+
* chunks are dropped (clearAll resets the buffer), not flushed: they
|
|
1077
|
+
* belong to a chat-owned stream this very call deletes, so writing them
|
|
1078
|
+
* first would only pay row writes for rows that die in the same
|
|
1079
|
+
* synchronous block.
|
|
918
1080
|
*/
|
|
919
1081
|
destroy(): void;
|
|
920
1082
|
/**
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
*
|
|
929
|
-
*
|
|
930
|
-
* sweep, so the DO can stop waking itself.
|
|
1083
|
+
* Delete every chat stream row this Durable Object no longer needs:
|
|
1084
|
+
* finished streams (their messages are persisted; the cutover normally
|
|
1085
|
+
* deletes them in the same transaction, so these are crash leftovers) and
|
|
1086
|
+
* in-flight rows abandoned past {@link ABANDONED_STREAM_RETENTION_MS} by
|
|
1087
|
+
* last chunk activity. Runs on every {@link start}, so nothing needs an
|
|
1088
|
+
* alarm to be reclaimed; a Durable Object that never starts another turn
|
|
1089
|
+
* keeps at most one turn's rows. Streams other producers opened on the
|
|
1090
|
+
* same object are untouched.
|
|
1091
|
+
* @returns How many rows were reclaimed.
|
|
931
1092
|
*/
|
|
932
|
-
|
|
933
|
-
private _maybeCleanupOldStreams;
|
|
934
|
-
/** Delete completed/errored buffers past the completion grace window, plus
|
|
935
|
-
* abandoned "streaming" rows past the stale-in-flight window. The two use
|
|
936
|
-
* different retentions: a completed buffer is redundant with the persisted
|
|
937
|
-
* message and needs only a brief replay grace, whereas an in-flight buffer
|
|
938
|
-
* must outlive resume/recovery before it is presumed dead. */
|
|
939
|
-
private _sweepOldStreams;
|
|
1093
|
+
reclaim(now?: number): number;
|
|
940
1094
|
/**
|
|
941
1095
|
* Return the stored chunks for a stream as individual chunk bodies in order,
|
|
942
|
-
* unpacking packed
|
|
943
|
-
* per-chunk sequence (0, 1, 2, …) — stable across calls because
|
|
944
|
-
* append-only — so callers can use it as a monotonic chunk sequence.
|
|
1096
|
+
* unpacking packed segments. The returned `chunk_index` is a running
|
|
1097
|
+
* per-chunk sequence (0, 1, 2, …) — stable across calls because segments
|
|
1098
|
+
* are append-only — so callers can use it as a monotonic chunk sequence.
|
|
945
1099
|
*/
|
|
946
1100
|
getStreamChunks(streamId: string): Array<{
|
|
947
1101
|
body: string;
|
|
@@ -958,2108 +1112,2216 @@ declare class ResumableStream {
|
|
|
958
1112
|
status: string;
|
|
959
1113
|
request_id: string;
|
|
960
1114
|
created_at: number;
|
|
1115
|
+
message_id: string | null;
|
|
961
1116
|
}>;
|
|
962
1117
|
/** @internal For testing only */
|
|
963
1118
|
insertStaleStream(streamId: string, requestId: string, ageMs: number): void;
|
|
964
1119
|
/**
|
|
965
|
-
* Append a chunk to a stream dated `ageMs` in the past. Used to exercise
|
|
966
|
-
*
|
|
967
|
-
* chunk must survive even when its
|
|
1120
|
+
* Append a chunk to a stream dated `ageMs` in the past. Used to exercise
|
|
1121
|
+
* reclaim's phase-2 verification: a long-running streaming row with a
|
|
1122
|
+
* *recent* chunk must survive even when its row `updated_at` (stamped at
|
|
1123
|
+
* open, not per append) is older than the coarse cutoff.
|
|
968
1124
|
* @internal For testing only
|
|
969
1125
|
*/
|
|
970
1126
|
insertChunkAt(streamId: string, body: string, ageMs: number): void;
|
|
971
1127
|
}
|
|
972
|
-
/**
|
|
973
|
-
* The buffer-cleanup alarm body: sweep aged stream buffers, then re-arm only
|
|
974
|
-
* while rows remain so a fully-swept DO stops waking itself. `rearm` schedules
|
|
975
|
-
* the next sweep — it MUST schedule a non-idempotent alarm, because this runs
|
|
976
|
-
* INSIDE the currently-executing one-shot schedule row, which `alarm()` deletes
|
|
977
|
-
* only after it returns; an idempotent reschedule would dedup onto that row and
|
|
978
|
-
* be deleted with it, so the re-arm would silently never fire and buffers that
|
|
979
|
-
* survived this sweep (e.g. a younger turn) would go uncollected. A fresh
|
|
980
|
-
* delayed row survives the deletion. Shared by `AIChatAgent` and `Think`.
|
|
981
|
-
*
|
|
982
|
-
* `@internal`
|
|
983
|
-
*/
|
|
984
|
-
declare function cleanupStreamBuffers(
|
|
985
|
-
stream: Pick<ResumableStream, "cleanup" | "hasReclaimableStreams">,
|
|
986
|
-
rearm: () => Promise<void>
|
|
987
|
-
): Promise<void>;
|
|
988
1128
|
//#endregion
|
|
989
|
-
//#region src/chat/
|
|
990
|
-
/**
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
/**
|
|
1001
|
-
|
|
1129
|
+
//#region src/chat/turn-task.d.ts
|
|
1130
|
+
/** One live turn closure, registered under its nonce while this isolate runs it. */
|
|
1131
|
+
type ChatTurnClosureEntry = {
|
|
1132
|
+
/** The snapshot persisted at claim, before the turn produces anything. */ readonly initial: unknown /** Wrap user stash data into the durable snapshot envelope. */;
|
|
1133
|
+
wrap(data: unknown): unknown /** Execute the live turn. */;
|
|
1134
|
+
run(): Promise<unknown> /** Settles the caller awaiting the turn's outcome. */;
|
|
1135
|
+
readonly settle: {
|
|
1136
|
+
resolve(value: unknown): void;
|
|
1137
|
+
reject(error: unknown): void;
|
|
1138
|
+
};
|
|
1139
|
+
};
|
|
1140
|
+
/** The host internals one chat-turn definition runs against. */
|
|
1141
|
+
type ChatTurnTaskHooks = {
|
|
1142
|
+
/** The registered definition name (the host's `CHAT_FIBER_NAME`). */ readonly definitionName: string;
|
|
1143
|
+
readonly storage: DurableObjectStorage /** The run row's creation time, for recovery staleness accounting. */;
|
|
1144
|
+
getRunCreatedAt(runId: string): Promise<number | null>;
|
|
1145
|
+
getLiveClosure(nonce: string): ChatTurnClosureEntry | undefined;
|
|
1146
|
+
keepAliveWhile<T>(
|
|
1147
|
+
fn: () => Promise<T>
|
|
1148
|
+
): Promise<T> /** The host's fiber stash context, so `this.stash()` keeps working. */;
|
|
1149
|
+
withStash<T>(
|
|
1150
|
+
context: {
|
|
1151
|
+
id: string;
|
|
1152
|
+
signal: AbortSignal;
|
|
1153
|
+
stash: (data: unknown) => void;
|
|
1154
|
+
},
|
|
1155
|
+
fn: () => Promise<T>
|
|
1156
|
+
): Promise<T> /** The unchanged recovery seam (ChatRecoveryEngine behind it). */;
|
|
1157
|
+
handleRecovery(ctx: FiberRecoveryContext): Promise<unknown>;
|
|
1158
|
+
};
|
|
1002
1159
|
/**
|
|
1003
|
-
* Build
|
|
1004
|
-
*
|
|
1005
|
-
* `${prefix}(?, ?, ...)`
|
|
1006
|
-
*
|
|
1007
|
-
* @throws if `count` is less than 1.
|
|
1160
|
+
* Build the chat-turn Task handler for one host. Registered under the
|
|
1161
|
+
* host's `CHAT_FIBER_NAME` via `Tasks#register`.
|
|
1008
1162
|
*/
|
|
1009
|
-
declare function
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
): TemplateStringsArray;
|
|
1163
|
+
declare function createChatTurnTaskDefinition(
|
|
1164
|
+
hooks: ChatTurnTaskHooks
|
|
1165
|
+
): (input: unknown, step: TaskStep) => Promise<void>;
|
|
1013
1166
|
//#endregion
|
|
1014
|
-
//#region src/chat/
|
|
1167
|
+
//#region src/chat/recovery.d.ts
|
|
1015
1168
|
/**
|
|
1016
|
-
*
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1169
|
+
* The minimal transcript-tail shape {@link createChatFiberSnapshot} reads to
|
|
1170
|
+
* derive the snapshot's `latest*Id` markers. Deliberately NOT `UIMessage`: the
|
|
1171
|
+
* snapshot only ever needs each message's `id` + `role`, so any host transcript
|
|
1172
|
+
* (AI SDK `UIMessage[]`, `Think`'s session leaves, or the pi adapter's plain
|
|
1173
|
+
* `AgentMessage[]`) satisfies it structurally. Keeping this off `UIMessage` is
|
|
1174
|
+
* the Phase-5 genericity seam — the snapshot builder must not couple to the AI
|
|
1175
|
+
* SDK message shape.
|
|
1021
1176
|
*/
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1177
|
+
interface SnapshotMessage {
|
|
1178
|
+
id?: string;
|
|
1179
|
+
role: string;
|
|
1180
|
+
}
|
|
1181
|
+
type ChatFiberSnapshot<Kind extends string = string> = {
|
|
1182
|
+
kind: Kind;
|
|
1183
|
+
version: 1;
|
|
1184
|
+
requestId: string;
|
|
1185
|
+
recoveryRootRequestId?: string;
|
|
1186
|
+
continuation: boolean;
|
|
1187
|
+
latestMessageId?: string;
|
|
1188
|
+
latestMessageRole?: string;
|
|
1189
|
+
latestUserMessageId?: string;
|
|
1190
|
+
startedAt: number;
|
|
1191
|
+
lastBody?: Record<string, unknown>;
|
|
1192
|
+
lastClientTools?: ClientToolSchema[];
|
|
1025
1193
|
};
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1194
|
+
declare function createChatFiberSnapshot<Kind extends string>({
|
|
1195
|
+
kind,
|
|
1196
|
+
requestId,
|
|
1197
|
+
recoveryRootRequestId,
|
|
1198
|
+
continuation,
|
|
1199
|
+
messages,
|
|
1200
|
+
lastBody,
|
|
1201
|
+
lastClientTools
|
|
1202
|
+
}: {
|
|
1203
|
+
kind: Kind;
|
|
1204
|
+
requestId: string;
|
|
1205
|
+
recoveryRootRequestId?: string;
|
|
1206
|
+
continuation: boolean;
|
|
1207
|
+
messages: ReadonlyArray<SnapshotMessage>;
|
|
1208
|
+
lastBody?: Record<string, unknown>;
|
|
1209
|
+
lastClientTools?: ClientToolSchema[];
|
|
1210
|
+
}): ChatFiberSnapshot<Kind>;
|
|
1211
|
+
declare function wrapChatFiberSnapshot<Kind extends string>(
|
|
1212
|
+
key: string,
|
|
1213
|
+
snapshot: ChatFiberSnapshot<Kind>,
|
|
1214
|
+
user: unknown | null
|
|
1215
|
+
): Record<string, unknown>;
|
|
1216
|
+
declare function unwrapChatFiberSnapshot<Kind extends string>(
|
|
1217
|
+
key: string,
|
|
1218
|
+
value: unknown,
|
|
1219
|
+
expectedKind?: Kind
|
|
1220
|
+
): {
|
|
1221
|
+
snapshot: ChatFiberSnapshot<Kind> | null;
|
|
1222
|
+
user: unknown | null;
|
|
1043
1223
|
};
|
|
1044
1224
|
//#endregion
|
|
1045
|
-
//#region src/chat/
|
|
1225
|
+
//#region src/chat/recovery-incident.d.ts
|
|
1046
1226
|
/**
|
|
1047
|
-
*
|
|
1227
|
+
* Whether a recovery is retrying an unanswered user turn or continuing a
|
|
1228
|
+
* partial assistant turn. Intentionally NOT part of the incident identity (see
|
|
1229
|
+
* {@link chatRecoveryIncidentId}).
|
|
1048
1230
|
*/
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
/**
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1231
|
+
type ChatRecoveryKind = "retry" | "continue";
|
|
1232
|
+
/**
|
|
1233
|
+
* Durable per-incident recovery record.
|
|
1234
|
+
*
|
|
1235
|
+
* PERSISTED CONTRACT — this shape round-trips across deploys (including the
|
|
1236
|
+
* deploy that ships the shared engine, which is itself a deploy-mid-recovery).
|
|
1237
|
+
* Fields are added as optional so older persisted incidents keep recovering.
|
|
1238
|
+
*/
|
|
1239
|
+
type ChatRecoveryIncident = {
|
|
1240
|
+
incidentId: string;
|
|
1241
|
+
requestId: string /** Stable request ID for the whole continuation chain (the recovery root). */;
|
|
1242
|
+
recoveryRootRequestId?: string;
|
|
1243
|
+
recoveryKind: ChatRecoveryKind;
|
|
1244
|
+
attempt: number;
|
|
1245
|
+
maxAttempts: number;
|
|
1246
|
+
status:
|
|
1247
|
+
| "detected"
|
|
1248
|
+
| "scheduled"
|
|
1249
|
+
| "attempting"
|
|
1250
|
+
| "completed"
|
|
1251
|
+
| "skipped"
|
|
1252
|
+
| "exhausted"
|
|
1253
|
+
| "failed";
|
|
1254
|
+
firstSeenAt: number;
|
|
1255
|
+
lastAttemptAt: number;
|
|
1063
1256
|
/**
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1257
|
+
* Epoch ms of the last attempt that observed forward progress. The recovery
|
|
1258
|
+
* budget is keyed to this (`now - lastProgressAt > noProgressTimeoutMs`), so a
|
|
1259
|
+
* turn that keeps producing content survives churn indefinitely while a
|
|
1260
|
+
* genuinely stuck turn is sealed within the window (#1637). Optional for
|
|
1261
|
+
* backward-compat — falls back to `firstSeenAt`.
|
|
1069
1262
|
*/
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
CF_AGENT_TOOL_RESULT = "cf_agent_tool_result",
|
|
1073
|
-
/** Server notifies client that a message was updated (e.g., tool result applied) */
|
|
1074
|
-
CF_AGENT_MESSAGE_UPDATED = "cf_agent_message_updated",
|
|
1075
|
-
/** Client sends tool approval response to server (for tools with needsApproval) */
|
|
1076
|
-
CF_AGENT_TOOL_APPROVAL = "cf_agent_tool_approval",
|
|
1263
|
+
lastProgressAt?: number;
|
|
1264
|
+
reason?: string;
|
|
1077
1265
|
/**
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
* Backward-compatible — clients that don't understand it ignore it. See #1620.
|
|
1266
|
+
* High-water mark of the durable, monotonic recovery-progress counter
|
|
1267
|
+
* observed for this incident. Distinguishes a turn making forward progress
|
|
1268
|
+
* but repeatedly interrupted by isolate resets (deploys) — which must NOT
|
|
1269
|
+
* exhaust the budget — from one that genuinely fails to advance. Sourced from
|
|
1270
|
+
* a persisted counter, never the compactable transcript (#1628).
|
|
1084
1271
|
*/
|
|
1085
|
-
|
|
1086
|
-
|
|
1272
|
+
progress?: number;
|
|
1273
|
+
/**
|
|
1274
|
+
* Value of the durable progress counter when this incident opened. The
|
|
1275
|
+
* runaway-loop work budget is `progress - workBaseline`, compared against
|
|
1276
|
+
* `maxRecoveryWork`. Optional for backward-compat — a missing baseline is
|
|
1277
|
+
* treated as the current marker (zero work so far), so an in-flight incident
|
|
1278
|
+
* from an older build is never falsely sealed.
|
|
1279
|
+
*/
|
|
1280
|
+
workBaseline?: number;
|
|
1281
|
+
/**
|
|
1282
|
+
* Count of recovery attempts for this incident that ended in a Durable Object
|
|
1283
|
+
* memory-limit reset (the isolate exceeded its 128 MB limit — see
|
|
1284
|
+
* `isDurableObjectMemoryLimitReset`). An OOM is a poison signal: re-running the
|
|
1285
|
+
* same memory-heavy turn deterministically re-OOMs, so unlike a deploy/eviction
|
|
1286
|
+
* it must NOT credit forward progress and must be bounded by a tight,
|
|
1287
|
+
* OOM-specific budget (`maxOomRetries`) rather than the generic attempt cap
|
|
1288
|
+
* (which resets on progress). Bumped by `ChatRecoveryEngine.recordOomAndDecide`
|
|
1289
|
+
* when a recovery callback observes an OOM; exceeding `maxOomRetries` seals the
|
|
1290
|
+
* incident with `reason="out_of_memory"` (#1825). Optional for backward-compat.
|
|
1291
|
+
*/
|
|
1292
|
+
oomAttempts?: number;
|
|
1293
|
+
};
|
|
1294
|
+
declare const CHAT_RECOVERY_INCIDENT_KEY_PREFIX = "cf:chat-recovery:incident:";
|
|
1087
1295
|
/**
|
|
1088
|
-
*
|
|
1296
|
+
* The pre-derivation forward-progress counter: a KV integer bumped per
|
|
1297
|
+
* credited chunk. The marker is now derived from the stream log
|
|
1298
|
+
* (`ResumableStream.progressMarker`), and this key is only read — once per
|
|
1299
|
+
* isolate, to seed the derived marker so it never reads lower than the
|
|
1300
|
+
* high-water mark an incident recorded before the upgrade. Current code
|
|
1301
|
+
* writes it only as a mirror of the derived marker's durable part — one put
|
|
1302
|
+
* per stream retired, none per chunk — so a build rolled back to the
|
|
1303
|
+
* counter never reads a marker lower than an incident recorded here.
|
|
1089
1304
|
*/
|
|
1090
|
-
|
|
1091
|
-
| {
|
|
1092
|
-
/** Indicates this message is a command to clear chat history */ type: MessageType.CF_AGENT_CHAT_CLEAR;
|
|
1093
|
-
}
|
|
1094
|
-
| {
|
|
1095
|
-
/** Indicates this message contains updated chat messages */ type: MessageType.CF_AGENT_CHAT_MESSAGES /** Array of chat messages */;
|
|
1096
|
-
messages: readonly ChatMessage[];
|
|
1097
|
-
}
|
|
1098
|
-
| {
|
|
1099
|
-
/** Indicates this message is a response to a chat request */ type: MessageType.CF_AGENT_USE_CHAT_RESPONSE /** Unique ID of the request this response corresponds to */;
|
|
1100
|
-
id: string /** Content body of the response */;
|
|
1101
|
-
body: string /** Whether this is the final chunk of the response */;
|
|
1102
|
-
done: boolean /** Whether this response contains an error */;
|
|
1103
|
-
error?: boolean /** Whether this is a continuation (append to last assistant message) */;
|
|
1104
|
-
continuation?: boolean /** Whether this chunk is being replayed from storage (stream resumption) */;
|
|
1105
|
-
replay?: boolean /** Signals that replay of stored chunks is complete (stream is still active) */;
|
|
1106
|
-
replayComplete?: boolean;
|
|
1107
|
-
}
|
|
1108
|
-
| {
|
|
1109
|
-
/** Indicates the server is resuming an active stream */ type: MessageType.CF_AGENT_STREAM_RESUMING /** The request ID of the stream being resumed */;
|
|
1110
|
-
id: string /** Present when this offer directly answers a client resume probe. */;
|
|
1111
|
-
probeId?: string;
|
|
1112
|
-
}
|
|
1113
|
-
| {
|
|
1114
|
-
/** Server notifies client that a message was updated (e.g., tool result applied) */ type: MessageType.CF_AGENT_MESSAGE_UPDATED /** The updated message */;
|
|
1115
|
-
message: ChatMessage;
|
|
1116
|
-
}
|
|
1117
|
-
| {
|
|
1118
|
-
/** Server responds to a resume request with no stream for this client. */ type: MessageType.CF_AGENT_STREAM_RESUME_NONE;
|
|
1119
|
-
/**
|
|
1120
|
-
* Why no stream was offered. Only `idle` proves global inactivity;
|
|
1121
|
-
* omitted by older servers and by non-authoritative delayed releases.
|
|
1122
|
-
*/
|
|
1123
|
-
reason?: StreamResumeNoneReason /** Correlates an authoritative response to its client resume probe. */;
|
|
1124
|
-
probeId?: string;
|
|
1125
|
-
}
|
|
1126
|
-
| {
|
|
1127
|
-
/**
|
|
1128
|
-
* Server signals an accepted turn whose resumable stream has not started
|
|
1129
|
-
* yet — the client should keep waiting for `STREAM_RESUMING` (or a later
|
|
1130
|
-
* `STREAM_RESUME_NONE`) rather than give up. See #1784.
|
|
1131
|
-
*/
|
|
1132
|
-
type: MessageType.CF_AGENT_STREAM_PENDING /** The accepted request id, when known. */;
|
|
1133
|
-
id?: string /** Correlates a direct keep-waiting response to its client probe. */;
|
|
1134
|
-
probeId?: string;
|
|
1135
|
-
}
|
|
1136
|
-
| {
|
|
1137
|
-
/**
|
|
1138
|
-
* Progress hint: a durable chat turn is being recovered (`recovering:
|
|
1139
|
-
* true`) or recovery has resolved (`recovering: false`). Purely advisory;
|
|
1140
|
-
* a client renders a "recovering…" indicator while true.
|
|
1141
|
-
*/
|
|
1142
|
-
type: MessageType.CF_AGENT_CHAT_RECOVERING /** Whether recovery is in progress (true) or has resolved (false). */;
|
|
1143
|
-
recovering: boolean /** The recovery-root request id of the turn being recovered, if known. */;
|
|
1144
|
-
id?: string;
|
|
1145
|
-
};
|
|
1305
|
+
declare const CHAT_RECOVERY_PROGRESS_KEY = "cf:chat-recovery:progress";
|
|
1146
1306
|
/**
|
|
1147
|
-
*
|
|
1307
|
+
* Durable record of an in-progress recovery so a "recovering…" status (#1620)
|
|
1308
|
+
* can be broadcast live and survive the set/clear happening in different
|
|
1309
|
+
* isolates (a continuation runs in a later alarm invocation).
|
|
1148
1310
|
*/
|
|
1149
|
-
|
|
1150
|
-
| {
|
|
1151
|
-
/** Indicates this message is a command to clear chat history */ type: MessageType.CF_AGENT_CHAT_CLEAR;
|
|
1152
|
-
}
|
|
1153
|
-
| {
|
|
1154
|
-
/** Indicates this message is a request to the chat API */ type: MessageType.CF_AGENT_USE_CHAT_REQUEST /** Unique ID for this request */;
|
|
1155
|
-
id: string /** Request initialization options */;
|
|
1156
|
-
init: Pick<
|
|
1157
|
-
RequestInit,
|
|
1158
|
-
| "method"
|
|
1159
|
-
| "keepalive"
|
|
1160
|
-
| "headers"
|
|
1161
|
-
| "body"
|
|
1162
|
-
| "redirect"
|
|
1163
|
-
| "integrity"
|
|
1164
|
-
| "credentials"
|
|
1165
|
-
| "mode"
|
|
1166
|
-
| "referrer"
|
|
1167
|
-
| "referrerPolicy"
|
|
1168
|
-
| "window"
|
|
1169
|
-
>;
|
|
1170
|
-
}
|
|
1171
|
-
| {
|
|
1172
|
-
/** Indicates this message contains updated chat messages */ type: MessageType.CF_AGENT_CHAT_MESSAGES /** Array of chat messages */;
|
|
1173
|
-
messages: ChatMessage[];
|
|
1174
|
-
}
|
|
1175
|
-
| {
|
|
1176
|
-
/** Indicates the user wants to stop generation of this message */ type: MessageType.CF_AGENT_CHAT_REQUEST_CANCEL;
|
|
1177
|
-
id: string;
|
|
1178
|
-
}
|
|
1179
|
-
| {
|
|
1180
|
-
/** Client acknowledges stream resuming notification and is ready to receive chunks */ type: MessageType.CF_AGENT_STREAM_RESUME_ACK /** The request ID of the stream being resumed */;
|
|
1181
|
-
id: string;
|
|
1182
|
-
}
|
|
1183
|
-
| {
|
|
1184
|
-
/** Client requests stream resume check after message handler is registered */ type: MessageType.CF_AGENT_STREAM_RESUME_REQUEST /** Opaque correlation id echoed by direct server responses. */;
|
|
1185
|
-
probeId?: string;
|
|
1186
|
-
}
|
|
1187
|
-
| {
|
|
1188
|
-
/** Client sends tool result to server (for client-side tools) */ type: MessageType.CF_AGENT_TOOL_RESULT /** The tool call ID this result is for */;
|
|
1189
|
-
toolCallId: string /** The name of the tool */;
|
|
1190
|
-
toolName: string /** The output from the tool execution */;
|
|
1191
|
-
output: unknown /** Override the tool part state (e.g. "output-error" for custom denial) */;
|
|
1192
|
-
state?:
|
|
1193
|
-
| "output-available"
|
|
1194
|
-
| "output-error" /** Error message when state is "output-error" */;
|
|
1195
|
-
errorText?: string /** Whether server should auto-continue the conversation after applying result */;
|
|
1196
|
-
autoContinue?: boolean /** Client tool schemas for continuation (client is source of truth) */;
|
|
1197
|
-
clientTools?: Array<{
|
|
1198
|
-
name: string;
|
|
1199
|
-
description?: string;
|
|
1200
|
-
parameters?: JSONSchema7;
|
|
1201
|
-
}>;
|
|
1202
|
-
}
|
|
1203
|
-
| {
|
|
1204
|
-
/** Client sends tool approval response to server (for tools with needsApproval) */ type: MessageType.CF_AGENT_TOOL_APPROVAL /** The tool call ID this approval is for */;
|
|
1205
|
-
toolCallId: string /** Whether the tool execution was approved */;
|
|
1206
|
-
approved: boolean /** Whether server should auto-continue the conversation after applying approval */;
|
|
1207
|
-
autoContinue?: boolean;
|
|
1208
|
-
};
|
|
1209
|
-
//#endregion
|
|
1210
|
-
//#region src/chat/connection.d.ts
|
|
1311
|
+
declare const CHAT_RECOVERING_KEY = "cf:chat:recovering";
|
|
1211
1312
|
/**
|
|
1212
|
-
*
|
|
1313
|
+
* Durable record of the last turn that ended in a terminal error / abandoned
|
|
1314
|
+
* recovery (#1645). Replayed on the next reconnect via the resume handshake;
|
|
1315
|
+
* cleared when a later turn supersedes it.
|
|
1316
|
+
*/
|
|
1317
|
+
declare const CHAT_LAST_TERMINAL_KEY = "cf:chat:last-terminal";
|
|
1318
|
+
/**
|
|
1319
|
+
* Secondary backstop only. The primary recovery bound is the no-progress wall
|
|
1320
|
+
* clock; with alarm debounce this cap rarely binds (it catches a pathological
|
|
1321
|
+
* tight alarm-loop). Kept high so the no-progress window seals first under
|
|
1322
|
+
* normal deploy cadence (#1637).
|
|
1323
|
+
*/
|
|
1324
|
+
declare const DEFAULT_CHAT_RECOVERY_MAX_ATTEMPTS = 10;
|
|
1325
|
+
/**
|
|
1326
|
+
* Runaway-loop guard default — the framework-imposed backstop on cumulative
|
|
1327
|
+
* recovery WORK (produced content/tool units) since an incident opened.
|
|
1213
1328
|
*
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1329
|
+
* Originally `Infinity` (rfc-chat-recovery-work-budget): the SDK shipped the
|
|
1330
|
+
* *mechanism* but no default cap, so a progressing turn was never terminated on
|
|
1331
|
+
* its own. Production issue #1825 showed that this is a footgun: an isolate that
|
|
1332
|
+
* OOMs mid-stream still credits a little progress before it dies, which resets
|
|
1333
|
+
* BOTH progress-keyed bounds (the attempt cap and the no-progress window) on
|
|
1334
|
+
* every wake — and a fast crash loop (each attempt inside the alarm-debounce
|
|
1335
|
+
* window) pins the attempt counter too. With `maxRecoveryWork = Infinity` the
|
|
1336
|
+
* ONLY instrument whose meter still climbs across such a loop is disabled, so
|
|
1337
|
+
* recovery re-runs the turn (and its LLM calls) forever.
|
|
1217
1338
|
*
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1339
|
+
* A finite default closes that loop out of the box: work climbs regardless of
|
|
1340
|
+
* debounce/progress resets, so a content-emitting runaway is always sealed with
|
|
1341
|
+
* `reason="work_budget_exceeded"`. The value is deliberately generous — it
|
|
1342
|
+
* bounds wasted re-run cost without clipping a normal interrupted turn (work
|
|
1343
|
+
* only accrues from the first interruption until the turn completes, after which
|
|
1344
|
+
* the incident is deleted). A very long agentic turn under heavy interruption
|
|
1345
|
+
* that legitimately needs more should raise `maxRecoveryWork` (or set it to
|
|
1346
|
+
* `Infinity` to restore the pre-#1825 unbounded behavior).
|
|
1347
|
+
*
|
|
1348
|
+
* The unit is one durable stream segment — about ten packed streaming chunks,
|
|
1349
|
+
* or one settled tool result, which is flushed on its own — plus one per
|
|
1350
|
+
* explicit credit for forwarded sub-agent output. The marker is derived from
|
|
1351
|
+
* the stream log (`ResumableStream.progressMarker`), so that is what it can
|
|
1352
|
+
* count. The earlier KV counter credited per milestone chunk and per five
|
|
1353
|
+
* seconds of deltas, a coarser measure of streamed text; 10 000 segments
|
|
1354
|
+
* (on the order of 100 000 chunks of re-run output) keeps the budget as
|
|
1355
|
+
* generous as 1 000 credits was for delta-heavy turns, and still finite.
|
|
1221
1356
|
*/
|
|
1357
|
+
declare const DEFAULT_CHAT_RECOVERY_MAX_WORK = 10000;
|
|
1222
1358
|
/**
|
|
1223
|
-
*
|
|
1224
|
-
* `
|
|
1225
|
-
*
|
|
1359
|
+
* Tight, OOM-specific retry budget (#1825). A Durable Object memory-limit reset
|
|
1360
|
+
* (`isDurableObjectMemoryLimitReset`) is usually deterministic — the turn's
|
|
1361
|
+
* working set no longer fits in the isolate's 128 MB — so re-running it re-OOMs.
|
|
1362
|
+
* But a single OOM CAN be a transient spike (the isolate's 128 MB is shared
|
|
1363
|
+
* across the global scope / noisy neighbors), so recovery retries a small number
|
|
1364
|
+
* of times before sealing with `reason="out_of_memory"` rather than abandoning a
|
|
1365
|
+
* turn that one more attempt might have completed. Far tighter than the generic
|
|
1366
|
+
* `maxRecoveryWork` backstop because an OOM is attributable and re-running it is
|
|
1367
|
+
* expensive (it re-runs the model). Counts attempts that ended in an OOM, not
|
|
1368
|
+
* total attempts, so a turn interrupted by deploys (no OOM) is unaffected.
|
|
1226
1369
|
*/
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
send(message: string): void;
|
|
1230
|
-
}
|
|
1370
|
+
declare const DEFAULT_CHAT_RECOVERY_MAX_OOM_RETRIES = 3;
|
|
1371
|
+
declare const DEFAULT_CHAT_RECOVERY_STABLE_TIMEOUT_MS = 10000;
|
|
1231
1372
|
/**
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
1234
|
-
* send went out, `false` if the socket was already closed. Any other error
|
|
1235
|
-
* rethrows.
|
|
1373
|
+
* Delay before retrying a recovery that timed out waiting for stable state.
|
|
1374
|
+
* Gives an actively-churning isolate (e.g. a deploy in flight) time to settle.
|
|
1236
1375
|
*/
|
|
1237
|
-
declare
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
): boolean;
|
|
1241
|
-
//#endregion
|
|
1242
|
-
//#region src/chat/continuation-state.d.ts
|
|
1376
|
+
declare const CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS = 3;
|
|
1377
|
+
declare const DEFAULT_CHAT_RECOVERY_TERMINAL_MESSAGE =
|
|
1378
|
+
"The assistant was interrupted and could not recover. Please try again.";
|
|
1243
1379
|
/**
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
* existing `ContinuationConnection` consumers.
|
|
1380
|
+
* Incidents that have not seen a new attempt within this window are assumed
|
|
1381
|
+
* abandoned and swept so durable storage does not grow without bound.
|
|
1247
1382
|
*/
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
> {
|
|
1252
|
-
connection: TConnection;
|
|
1253
|
-
connectionId: string | null;
|
|
1254
|
-
requestId: string;
|
|
1255
|
-
clientTools?: ClientToolSchema[];
|
|
1256
|
-
body?: Record<string, unknown>;
|
|
1257
|
-
errorPrefix: string | null;
|
|
1258
|
-
prerequisite: Promise<boolean> | null;
|
|
1259
|
-
pastCoalesce: boolean;
|
|
1260
|
-
}
|
|
1261
|
-
interface ContinuationDeferred<
|
|
1262
|
-
TConnection extends ContinuationConnection = ContinuationConnection
|
|
1263
|
-
> {
|
|
1264
|
-
connection: TConnection;
|
|
1265
|
-
connectionId: string | null;
|
|
1266
|
-
clientTools?: ClientToolSchema[];
|
|
1267
|
-
body?: Record<string, unknown>;
|
|
1268
|
-
errorPrefix: string;
|
|
1269
|
-
prerequisite: Promise<boolean> | null;
|
|
1270
|
-
}
|
|
1271
|
-
declare class ContinuationState<
|
|
1272
|
-
TConnection extends ContinuationConnection = ContinuationConnection
|
|
1273
|
-
> {
|
|
1274
|
-
pending: ContinuationPending<TConnection> | null;
|
|
1275
|
-
deferred: ContinuationDeferred<TConnection> | null;
|
|
1276
|
-
activeRequestId: string | null;
|
|
1277
|
-
activeConnectionId: string | null;
|
|
1278
|
-
awaitingConnections: Map<string, TConnection>;
|
|
1279
|
-
/** Clear pending state and awaiting connections (without sending RESUME_NONE). */
|
|
1280
|
-
clearPending(): void;
|
|
1281
|
-
clearDeferred(): void;
|
|
1282
|
-
clearAll(): void;
|
|
1283
|
-
/**
|
|
1284
|
-
* Mark a connection as no longer available without canceling the
|
|
1285
|
-
* continuation it initiated.
|
|
1286
|
-
*/
|
|
1287
|
-
releaseConnection(connectionId: string): void;
|
|
1288
|
-
/**
|
|
1289
|
-
* Send STREAM_RESUME_NONE to all connections waiting for a
|
|
1290
|
-
* continuation stream to start, then clear the map.
|
|
1291
|
-
*/
|
|
1292
|
-
sendResumeNone(): void;
|
|
1293
|
-
/**
|
|
1294
|
-
* Flush awaiting connections by notifying each one via the provided
|
|
1295
|
-
* callback (typically sends STREAM_RESUMING), then clear.
|
|
1296
|
-
*/
|
|
1297
|
-
flushAwaitingConnections(notify: (conn: TConnection) => void): void;
|
|
1298
|
-
/**
|
|
1299
|
-
* Transition pending → active. Called when the continuation stream
|
|
1300
|
-
* actually starts. Moves request/connection IDs to active slots,
|
|
1301
|
-
* clears pending fields.
|
|
1302
|
-
*/
|
|
1303
|
-
activatePending(): void;
|
|
1304
|
-
/**
|
|
1305
|
-
* Transition deferred → pending. Called when a continuation turn
|
|
1306
|
-
* completes and there's a deferred follow-up waiting.
|
|
1307
|
-
*
|
|
1308
|
-
* Returns the new pending state (so the host can enqueue the turn),
|
|
1309
|
-
* or null if there was nothing deferred.
|
|
1310
|
-
*/
|
|
1311
|
-
activateDeferred(
|
|
1312
|
-
generateRequestId: () => string
|
|
1313
|
-
): ContinuationPending<TConnection> | null;
|
|
1314
|
-
}
|
|
1315
|
-
//#endregion
|
|
1316
|
-
//#region src/chat/pre-stream-turns.d.ts
|
|
1317
|
-
declare class PreStreamTurns<
|
|
1318
|
-
TConnection extends ChatConnection = ChatConnection
|
|
1319
|
-
> {
|
|
1320
|
-
/**
|
|
1321
|
-
* Accepted-but-not-yet-streamed request ids. A turn enters on `begin()` and
|
|
1322
|
-
* leaves on `settle()`; the set being non-empty means "pre-stream work is in
|
|
1323
|
-
* flight", which gates parking and the eventual `resume_none` release.
|
|
1324
|
-
*/
|
|
1325
|
-
private readonly _accepted;
|
|
1326
|
-
/** Connections parked waiting for a stream to start. */
|
|
1327
|
-
readonly awaitingConnections: Map<string, TConnection>;
|
|
1328
|
-
/** The most recently accepted pre-stream request id (for the keep-waiting frame). */
|
|
1329
|
-
private _latestRequestId;
|
|
1330
|
-
/** Mark a freshly-accepted turn as in flight (pre-stream). */
|
|
1331
|
-
begin(requestId: string): void;
|
|
1332
|
-
/**
|
|
1333
|
-
* Mark an accepted turn as settled. Returns `true` when no accepted turn
|
|
1334
|
-
* remains in flight (the caller should release parked connections if no
|
|
1335
|
-
* stream is active).
|
|
1336
|
-
*/
|
|
1337
|
-
settle(requestId: string): boolean;
|
|
1338
|
-
/** Whether any accepted turn is still pre-stream. */
|
|
1339
|
-
hasInFlight(): boolean;
|
|
1340
|
-
/** The request id to advertise in the keep-waiting frame, if known. */
|
|
1341
|
-
get latestRequestId(): string | null;
|
|
1342
|
-
/**
|
|
1343
|
-
* Park a reconnecting connection and tell it to keep waiting (so its
|
|
1344
|
-
* transport does not resolve `reconnectToStream` early). No-op when nothing
|
|
1345
|
-
* is in flight. Parked connections are deliberately NOT added to the host's
|
|
1346
|
-
* `pendingResumeConnections` — they must keep receiving any live broadcast —
|
|
1347
|
-
* until the host flushes them through `notifyStreamResuming` on stream start.
|
|
1348
|
-
*/
|
|
1349
|
-
park(connection: TConnection, probeId?: string): boolean;
|
|
1350
|
-
/** Drop a single connection (e.g. on socket close) without releasing others. */
|
|
1351
|
-
release(connectionId: string): void;
|
|
1352
|
-
/**
|
|
1353
|
-
* A stream has started: hand every parked connection to `notify` (the host's
|
|
1354
|
-
* `notifyStreamResuming`, which sends `STREAM_RESUMING` and excludes the
|
|
1355
|
-
* connection from live broadcast until it ACKs), then clear the awaiting map.
|
|
1356
|
-
* The accepted set is untouched — the turn is still running.
|
|
1357
|
-
*/
|
|
1358
|
-
flushOnStreamStart(notify: (connection: TConnection) => void): void;
|
|
1359
|
-
/**
|
|
1360
|
-
* Release every parked connection with `STREAM_RESUME_NONE` (the turn settled
|
|
1361
|
-
* without ever starting a stream) and clear the awaiting map. Safe to call
|
|
1362
|
-
* when the map is empty (no-op), so the host can call it liberally from a
|
|
1363
|
-
* turn-settle path.
|
|
1364
|
-
*/
|
|
1365
|
-
releaseAwaiting(): void;
|
|
1366
|
-
/** Drop all state (chat clear / destroy). Does not send any frames. */
|
|
1367
|
-
reset(): void;
|
|
1368
|
-
}
|
|
1369
|
-
//#endregion
|
|
1370
|
-
//#region src/chat/auto-continuation-controller.d.ts
|
|
1383
|
+
declare const CHAT_RECOVERY_INCIDENT_TTL_MS: number;
|
|
1384
|
+
/** Max keys per Durable Object KV `delete([...])` call. */
|
|
1385
|
+
declare const KV_DELETE_MAX_KEYS = 128;
|
|
1371
1386
|
/**
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
* threads them per tool-result event).
|
|
1387
|
+
* PRIMARY recovery bound (#1637): seal an incident that has made no forward
|
|
1388
|
+
* progress for this long. Keyed to `lastProgressAt`, which resets on every
|
|
1389
|
+
* progress-bearing attempt — so a turn that keeps producing content survives
|
|
1390
|
+
* deploy churn indefinitely, while a genuinely stuck turn dies within 5 min.
|
|
1377
1391
|
*/
|
|
1378
|
-
|
|
1379
|
-
TConnection extends ContinuationConnection = ContinuationConnection
|
|
1380
|
-
> {
|
|
1381
|
-
connection: TConnection;
|
|
1382
|
-
clientTools: ClientToolSchema[] | undefined;
|
|
1383
|
-
body: Record<string, unknown> | undefined;
|
|
1384
|
-
errorPrefix: string;
|
|
1385
|
-
}
|
|
1392
|
+
declare const DEFAULT_CHAT_RECOVERY_NO_PROGRESS_TIMEOUT_MS: number;
|
|
1386
1393
|
/**
|
|
1387
|
-
*
|
|
1388
|
-
*
|
|
1394
|
+
* Alarm debounce: recovery alarms bunched within this window collapse into a
|
|
1395
|
+
* single attempt. A deploy rollout drops/reconnects the socket several times
|
|
1396
|
+
* over ~11–22s; without this, one logical deploy would burn several attempts.
|
|
1389
1397
|
*/
|
|
1390
|
-
|
|
1391
|
-
TConnection extends ContinuationConnection = ContinuationConnection
|
|
1392
|
-
> {
|
|
1393
|
-
/** Shared continuation state (pending/deferred/awaiting connections). */
|
|
1394
|
-
readonly continuation: ContinuationState<TConnection>;
|
|
1395
|
-
/** Generate a request id for a freshly-created continuation turn. */
|
|
1396
|
-
generateRequestId(): string;
|
|
1397
|
-
/**
|
|
1398
|
-
* `true` while an assistant turn is streaming — the parallel tool batch can
|
|
1399
|
-
* still grow with tool calls the model hasn't emitted yet, so no completeness
|
|
1400
|
-
* check is meaningful. (`_streamingAssistant !== null` in Think;
|
|
1401
|
-
* `_streamingTurnActive` in ai-chat.)
|
|
1402
|
-
*/
|
|
1403
|
-
isStreamActive(): boolean;
|
|
1404
|
-
/** `true` while a tool-result/approval apply is in flight. */
|
|
1405
|
-
hasPendingInteraction(): boolean;
|
|
1406
|
-
/**
|
|
1407
|
-
* `true` when the latest assistant message is mid-batch (a settled tool
|
|
1408
|
-
* result beside an unanswered tool call/approval — the #1649 signature).
|
|
1409
|
-
*/
|
|
1410
|
-
hasIncompleteToolBatch(): boolean;
|
|
1411
|
-
/**
|
|
1412
|
-
* Drain every in-flight tool-result/approval apply (including any enqueued
|
|
1413
|
-
* while draining) so the subsequent completeness re-check sees every result
|
|
1414
|
-
* that has already arrived. Bounded by real apply activity, never a timer.
|
|
1415
|
-
*/
|
|
1416
|
-
drainInteractionApplies(): Promise<void>;
|
|
1417
|
-
/** Hold the isolate alive for the duration of `fn` (alarm heartbeats). */
|
|
1418
|
-
keepAliveWhile<T>(fn: () => Promise<T>): Promise<T>;
|
|
1419
|
-
/**
|
|
1420
|
-
* Run the continuation turn for the current {@link ContinuationState.pending}.
|
|
1421
|
-
* Each host's inference/reply pipeline (Think: `_turnQueue.enqueue` +
|
|
1422
|
-
* `_runInferenceLoop`; ai-chat: `_runExclusiveChatTurn` + `onChatMessage`).
|
|
1423
|
-
* Reads everything it needs from `continuation.pending`, so it takes no args.
|
|
1424
|
-
*/
|
|
1425
|
-
fire(): void;
|
|
1426
|
-
}
|
|
1427
|
-
declare class AutoContinuationController<
|
|
1428
|
-
TConnection extends ContinuationConnection = ContinuationConnection
|
|
1429
|
-
> {
|
|
1430
|
-
private readonly host;
|
|
1431
|
-
/**
|
|
1432
|
-
* Small debounce window to batch adjacent client-side tool results/approvals
|
|
1433
|
-
* into a single server continuation barrier check (#1650).
|
|
1434
|
-
*/
|
|
1435
|
-
static readonly COALESCE_MS = 50;
|
|
1436
|
-
/**
|
|
1437
|
-
* Coalesce/debounce timer for the event-driven barrier (#1650). Each tool
|
|
1438
|
-
* result/approval re-arms it; on fire it runs {@link fireWhenStable}.
|
|
1439
|
-
*/
|
|
1440
|
-
private _timer;
|
|
1441
|
-
/**
|
|
1442
|
-
* Double-fire guard (#1650). Ensures only one in-flight apply-drain runs;
|
|
1443
|
-
* that drain re-checks completeness on completion before firing. A sibling
|
|
1444
|
-
* that re-arms the coalesce timer during a drain is absorbed by the
|
|
1445
|
-
* in-progress drain rather than starting its own.
|
|
1446
|
-
*/
|
|
1447
|
-
private _barrierActive;
|
|
1448
|
-
constructor(host: AutoContinuationHost<TConnection>);
|
|
1449
|
-
/**
|
|
1450
|
-
* Schedule an auto-continuation for a tool result/approval that opted in with
|
|
1451
|
-
* `autoContinue` (#1650). Coalesces rapid sibling results into a single
|
|
1452
|
-
* continuation via the debounce timer; the actual fire is gated by
|
|
1453
|
-
* {@link fireWhenStable}. If a continuation is already running
|
|
1454
|
-
* (`pastCoalesce`), the new result is stored as the deferred follow-up
|
|
1455
|
-
* instead of re-arming.
|
|
1456
|
-
*/
|
|
1457
|
-
schedule(spec: ContinuationSpec<TConnection>): void;
|
|
1458
|
-
/**
|
|
1459
|
-
* Re-arm the barrier for a result/approval that arrived WITHOUT `autoContinue`
|
|
1460
|
-
* (#1650). A standalone errored result declines to continue on its own, but in
|
|
1461
|
-
* a parallel batch a SIBLING may already have opted in — and this result can
|
|
1462
|
-
* be the one that completes the batch, so we must re-run the barrier check.
|
|
1463
|
-
* Unlike {@link schedule} this NEVER creates a pending continuation, and
|
|
1464
|
-
* no-ops once the continuation is running (`pastCoalesce`).
|
|
1465
|
-
*/
|
|
1466
|
-
rearmForBatch(): void;
|
|
1467
|
-
/** (Re)arm the coalesce timer; on fire, run {@link fireWhenStable}. */
|
|
1468
|
-
armTimer(): void;
|
|
1469
|
-
/**
|
|
1470
|
-
* Fire an auto-continuation, but only once the model's parallel tool-call
|
|
1471
|
-
* batch is fully answered (#1649) and no assistant turn is mid-stream (#1650).
|
|
1472
|
-
* The barrier is event-driven with NO orphan timeout: when the batch is still
|
|
1473
|
-
* incomplete we drain the in-flight applies, re-check, and — if still
|
|
1474
|
-
* incomplete — return WITHOUT firing and WITHOUT holding the isolate, leaving
|
|
1475
|
-
* `continuation.pending` in place. The next sibling's result re-arms the
|
|
1476
|
-
* coalesce timer and re-runs this check; the continuation fires once the final
|
|
1477
|
-
* sibling lands. A true orphan (a sibling that never arrives) simply never
|
|
1478
|
-
* auto-continues — a later user turn / chat recovery repairs the transcript.
|
|
1479
|
-
*/
|
|
1480
|
-
fireWhenStable(): void;
|
|
1481
|
-
/**
|
|
1482
|
-
* Transition the deferred follow-up (stored while a continuation was running)
|
|
1483
|
-
* to pending and re-run the barrier — its batch may still be incomplete (or a
|
|
1484
|
-
* stream active), in which case it parks and re-arms instead of firing blind.
|
|
1485
|
-
*/
|
|
1486
|
-
activateDeferredAndReschedule(): void;
|
|
1487
|
-
/**
|
|
1488
|
-
* Cancel any still-armed coalesce timer. Called on the fire path so a sibling
|
|
1489
|
-
* result that re-armed it during a barrier wait can't fire a duplicate
|
|
1490
|
-
* continuation after this one starts (#1649 / #1650).
|
|
1491
|
-
*/
|
|
1492
|
-
cancelTimer(): void;
|
|
1493
|
-
/**
|
|
1494
|
-
* `true` when the barrier is going to fire on its own — its coalesce timer is
|
|
1495
|
-
* still pending or its completeness drain is in progress. The host combines
|
|
1496
|
-
* this with its own pending/`pastCoalesce` checks to decide idle/stable.
|
|
1497
|
-
*/
|
|
1498
|
-
isArmed(): boolean;
|
|
1499
|
-
/**
|
|
1500
|
-
* Tear down the controller-owned barrier state (timer + double-fire guard).
|
|
1501
|
-
* Scoped to ONLY this controller's fields — the host clears the rest of its
|
|
1502
|
-
* turn state (stream gate, interaction tail, continuation data) separately.
|
|
1503
|
-
*/
|
|
1504
|
-
reset(): void;
|
|
1505
|
-
}
|
|
1506
|
-
//#endregion
|
|
1507
|
-
//#region src/chat/abort-registry.d.ts
|
|
1398
|
+
declare const CHAT_RECOVERY_ALARM_DEBOUNCE_MS: number;
|
|
1508
1399
|
/**
|
|
1509
|
-
*
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
* Each request gets its own AbortController keyed by request ID.
|
|
1513
|
-
* Controllers are created lazily on first signal access.
|
|
1514
|
-
*/
|
|
1515
|
-
declare class AbortRegistry {
|
|
1516
|
-
private controllers;
|
|
1517
|
-
/**
|
|
1518
|
-
* Get or create an AbortController for the given ID and return its signal.
|
|
1519
|
-
* Creates the controller lazily on first access.
|
|
1520
|
-
*/
|
|
1521
|
-
getSignal(id: string): AbortSignal | undefined;
|
|
1522
|
-
/**
|
|
1523
|
-
* Get the signal for an existing controller without creating one.
|
|
1524
|
-
* Returns undefined if no controller exists for this ID.
|
|
1525
|
-
*/
|
|
1526
|
-
getExistingSignal(id: string): AbortSignal | undefined;
|
|
1527
|
-
/**
|
|
1528
|
-
* Cancel a specific request by aborting its controller. Optionally
|
|
1529
|
-
* propagate a reason — surfaces as `signal.reason` on the registry's
|
|
1530
|
-
* controller and through any `AbortError` it produces downstream.
|
|
1531
|
-
*/
|
|
1532
|
-
cancel(id: string, reason?: unknown): void;
|
|
1533
|
-
/** Remove a controller after the request completes. */
|
|
1534
|
-
remove(id: string): void;
|
|
1535
|
-
/**
|
|
1536
|
-
* Abort all pending requests and clear the registry. Optionally propagate a
|
|
1537
|
-
* reason — surfaces as `signal.reason` on each controller and through any
|
|
1538
|
-
* `AbortError` it produces downstream, exactly like {@link cancel}.
|
|
1539
|
-
*/
|
|
1540
|
-
destroyAll(reason?: unknown): void;
|
|
1541
|
-
/** Check if a controller exists for the given ID. */
|
|
1542
|
-
has(id: string): boolean;
|
|
1543
|
-
/** Number of tracked controllers. */
|
|
1544
|
-
get size(): number;
|
|
1545
|
-
/**
|
|
1546
|
-
* Link an external `AbortSignal` to the controller for `id`. When the
|
|
1547
|
-
* external signal aborts, the registry's controller is cancelled —
|
|
1548
|
-
* propagating the abort reason — exactly the same way an internal
|
|
1549
|
-
* cancel would (e.g. via a `chat-request-cancel` WebSocket message).
|
|
1550
|
-
*
|
|
1551
|
-
* This is the integration point for callers that drive a chat turn
|
|
1552
|
-
* programmatically and want to cancel it from outside without knowing
|
|
1553
|
-
* the internally-generated request id (e.g. the helper-as-sub-agent
|
|
1554
|
-
* pattern, where a parent's `AbortSignal` from the AI SDK tool
|
|
1555
|
-
* `execute` needs to land inside a `Think.saveMessages` call running
|
|
1556
|
-
* on a child DO).
|
|
1557
|
-
*
|
|
1558
|
-
* Behavior:
|
|
1559
|
-
*
|
|
1560
|
-
* - Passing `undefined` is a no-op and returns a no-op detacher, so
|
|
1561
|
-
* callers can unconditionally call this with `options?.signal`.
|
|
1562
|
-
* - If the external signal is already aborted, the registry's
|
|
1563
|
-
* controller is created (if needed) and cancelled synchronously.
|
|
1564
|
-
* - Otherwise a one-shot `abort` listener is attached. The returned
|
|
1565
|
-
* function detaches it.
|
|
1566
|
-
*
|
|
1567
|
-
* **Always call the returned detacher in a `finally` block** — the
|
|
1568
|
-
* external signal may outlive the request (a parent chat turn that
|
|
1569
|
-
* drives many helper turns reuses one signal across all of them) and
|
|
1570
|
-
* leaving listeners attached pins closures and grows the listener
|
|
1571
|
-
* list on each turn.
|
|
1572
|
-
*
|
|
1573
|
-
* @returns A detacher function. Call it after the request finishes
|
|
1574
|
-
* (success or failure) to remove the abort listener from `signal`.
|
|
1575
|
-
*/
|
|
1576
|
-
linkExternal(id: string, signal: AbortSignal | undefined): () => void;
|
|
1577
|
-
}
|
|
1578
|
-
//#endregion
|
|
1579
|
-
//#region src/chat/async-helpers.d.ts
|
|
1580
|
-
/**
|
|
1581
|
-
* @internal Small async control-flow helpers shared by the chat hosts
|
|
1582
|
-
* (`@cloudflare/ai-chat` and `@cloudflare/think`) — not a public API. Extracted
|
|
1583
|
-
* so the host idle/stable waits and the interaction-apply completeness drain
|
|
1584
|
-
* stay byte-identical across both. See `design/chat-shared-layer.md`.
|
|
1400
|
+
* Staleness bound for the live "recovering…" flag (#1620). A flag older than
|
|
1401
|
+
* this is treated as abandoned so it can neither pin the indicator on forever
|
|
1402
|
+
* nor suppress a genuinely-new recovering signal. NOT a recovery budget.
|
|
1585
1403
|
*/
|
|
1404
|
+
declare const CHAT_RECOVERING_FLAG_TTL_MS: number;
|
|
1586
1405
|
/**
|
|
1587
|
-
*
|
|
1588
|
-
*
|
|
1589
|
-
*
|
|
1406
|
+
* Resolve a raw `chatRecovery` config field into the fully-defaulted form the
|
|
1407
|
+
* engine reasons about. Durable recovery is always enabled; a legacy runtime
|
|
1408
|
+
* `false` value from previously compiled JavaScript safely receives defaults.
|
|
1590
1409
|
*/
|
|
1591
|
-
declare
|
|
1410
|
+
declare function resolveChatRecoveryConfig(
|
|
1411
|
+
raw: ChatRecoveryConfig | undefined
|
|
1412
|
+
): ResolvedChatRecoveryConfig;
|
|
1592
1413
|
/**
|
|
1593
|
-
*
|
|
1594
|
-
*
|
|
1595
|
-
*
|
|
1596
|
-
*
|
|
1414
|
+
* Sweep recovery incidents inactive past the TTL from durable storage. Lists by
|
|
1415
|
+
* the incident key prefix, selects stale keys (`selectStaleIncidentKeys`), and
|
|
1416
|
+
* batch-deletes them — the DO KV `delete([...])` accepts up to
|
|
1417
|
+
* `KV_DELETE_MAX_KEYS` per call, collapsing N awaited round-trips into
|
|
1418
|
+
* ceil(N / 128). Shared by `AIChatAgent` and `Think` so the sweep policy lives in
|
|
1419
|
+
* one place. See `design/rfc-chat-recovery-foundation.md`.
|
|
1597
1420
|
*/
|
|
1598
|
-
declare function
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
): Promise<
|
|
1421
|
+
declare function sweepStaleChatRecoveryIncidents(
|
|
1422
|
+
storage: Pick<DurableObjectStorage, "list" | "delete">,
|
|
1423
|
+
now: number
|
|
1424
|
+
): Promise<void>;
|
|
1602
1425
|
/**
|
|
1603
|
-
*
|
|
1604
|
-
*
|
|
1605
|
-
*
|
|
1606
|
-
*
|
|
1607
|
-
*
|
|
1608
|
-
* `getTail` is re-read after every await because a sibling can extend the tail
|
|
1609
|
-
* mid-drain, and the loop stops once the tail stops advancing. Bails early when
|
|
1610
|
-
* `hasPending()` goes false (the pending continuation was cleared by a chat
|
|
1611
|
-
* clear / turn reset) so a stale drain can't hold the isolate awake.
|
|
1426
|
+
* List the persisted recovery incidents that are still live (status
|
|
1427
|
+
* `detected` / `scheduled` / `attempting`) — i.e. NOT yet terminalized
|
|
1428
|
+
* (`exhausted` / `failed`). Used by the alarm-boundary OOM circuit breaker
|
|
1429
|
+
* (#1825) to find the incident(s) it must seal when the in-DO budgets could not.
|
|
1430
|
+
* Lists by the incident key prefix so the storage layout stays encapsulated.
|
|
1612
1431
|
*/
|
|
1613
|
-
declare function
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1432
|
+
declare function listActiveChatRecoveryIncidents(
|
|
1433
|
+
storage: Pick<DurableObjectStorage, "list">
|
|
1434
|
+
): Promise<
|
|
1435
|
+
{
|
|
1436
|
+
key: string;
|
|
1437
|
+
incident: ChatRecoveryIncident;
|
|
1438
|
+
}[]
|
|
1439
|
+
>;
|
|
1619
1440
|
/**
|
|
1620
|
-
*
|
|
1621
|
-
*
|
|
1622
|
-
*
|
|
1623
|
-
*
|
|
1624
|
-
*
|
|
1441
|
+
* Summarize a child agent's persisted recovery incidents for the parent's
|
|
1442
|
+
* agent-tool reattach decision: `"in-progress"` if any incident is still live
|
|
1443
|
+
* (detected/scheduled/attempting), else `"failed"` if any terminalized
|
|
1444
|
+
* (exhausted/failed), else `"none"`. In-progress takes precedence so a parent
|
|
1445
|
+
* never gives up on a child that is still recovering. Shared by `AIChatAgent`
|
|
1446
|
+
* and `Think`. See `design/rfc-chat-recovery-foundation.md`.
|
|
1625
1447
|
*/
|
|
1448
|
+
declare function classifyAgentToolChildRecovery(
|
|
1449
|
+
storage: Pick<DurableObjectStorage, "list">
|
|
1450
|
+
): Promise<"in-progress" | "failed" | "none">;
|
|
1626
1451
|
/**
|
|
1627
|
-
*
|
|
1452
|
+
* Read the durable monotonic recovery-progress counter (0 when unset). The value
|
|
1453
|
+
* feeds the no-progress budget decision; shared by `AIChatAgent` and `Think`.
|
|
1628
1454
|
*/
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
apply: (part: Record<string, unknown>) => Record<string, unknown>;
|
|
1633
|
-
};
|
|
1455
|
+
declare function readChatRecoveryProgress(
|
|
1456
|
+
storage: Pick<DurableObjectStorage, "get">
|
|
1457
|
+
): Promise<number>;
|
|
1634
1458
|
/**
|
|
1635
|
-
*
|
|
1636
|
-
* Finds the first part matching `update.toolCallId` in one of `update.matchStates`,
|
|
1637
|
-
* applies the update immutably, and returns the new parts array with the index.
|
|
1459
|
+
* Advance the KV progress counter by one.
|
|
1638
1460
|
*
|
|
1639
|
-
*
|
|
1461
|
+
* @deprecated Hosts no longer bump a counter per credited chunk: the marker
|
|
1462
|
+
* is derived from the stream log (`ResumableStream.progressMarker`) and
|
|
1463
|
+
* explicit credits go through `ResumableStream.creditProgress`. Kept for
|
|
1464
|
+
* code that still maintains the KV counter; a value written here is folded
|
|
1465
|
+
* into the derived marker on the next seed.
|
|
1640
1466
|
*/
|
|
1641
|
-
declare function
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
): {
|
|
1645
|
-
parts: Array<Record<string, unknown>>;
|
|
1646
|
-
index: number;
|
|
1647
|
-
} | null;
|
|
1467
|
+
declare function bumpChatRecoveryProgress(
|
|
1468
|
+
storage: Pick<DurableObjectStorage, "get" | "put">
|
|
1469
|
+
): Promise<void>;
|
|
1648
1470
|
/**
|
|
1649
|
-
*
|
|
1650
|
-
*
|
|
1651
|
-
*
|
|
1652
|
-
*
|
|
1471
|
+
* Throttle window for crediting a parent turn's recovery progress from forwarded
|
|
1472
|
+
* sub-agent (agent-tool) stream chunks (N9). Forwarding a child's chunks IS
|
|
1473
|
+
* forward progress for the parent, but the credit must not write storage per
|
|
1474
|
+
* token.
|
|
1653
1475
|
*/
|
|
1654
|
-
declare
|
|
1655
|
-
toolCallId: string,
|
|
1656
|
-
output: unknown,
|
|
1657
|
-
overrideState?: "output-error",
|
|
1658
|
-
errorText?: string
|
|
1659
|
-
): ToolPartUpdate;
|
|
1476
|
+
declare const AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS = 5000;
|
|
1660
1477
|
/**
|
|
1661
|
-
*
|
|
1662
|
-
*
|
|
1663
|
-
*
|
|
1664
|
-
*
|
|
1665
|
-
*
|
|
1666
|
-
* continuation stream, but its tool part lives in the assistant message that
|
|
1667
|
-
* originally requested it. `StreamAccumulator` surfaces this as a
|
|
1668
|
-
* `cross-message-tool-update` action because the accumulator only owns the
|
|
1669
|
-
* current turn's new content and cannot mutate a part from a prior message.
|
|
1670
|
-
*
|
|
1671
|
-
* Compared to {@link toolResultUpdate} this builder is deliberately more
|
|
1672
|
-
* defensive, mirroring the equivalent fallback in `@cloudflare/ai-chat`:
|
|
1673
|
-
*
|
|
1674
|
-
* - It matches the broad set of pre-terminal **and** terminal states, so a
|
|
1675
|
-
* provider that replays the entire prior tool round-trip during a
|
|
1676
|
-
* continuation (notably the OpenAI Responses API — issue #1404) still
|
|
1677
|
-
* resolves to the same part instead of silently missing it.
|
|
1678
|
-
* - It is **first-write-wins**: a chunk arriving for a tool that already holds
|
|
1679
|
-
* a terminal result is treated as a replay and the existing output is never
|
|
1680
|
-
* overwritten. In that case `apply` returns the *same part reference*, which
|
|
1681
|
-
* callers use as an idempotent-no-op signal to skip the durable write and a
|
|
1682
|
-
* redundant `MESSAGE_UPDATED` broadcast.
|
|
1683
|
-
* - It preserves a streamed `preliminary` flag when one is present, otherwise
|
|
1684
|
-
* marks the result final (`preliminary: false`).
|
|
1478
|
+
* Per-isolate throttle gate for agent-tool stream-progress crediting (N9). The
|
|
1479
|
+
* `_lastBumpAt` clock is in-memory, so it resets per isolate and the first
|
|
1480
|
+
* forwarded chunk after a restart always credits. `shouldCredit(now)` returns
|
|
1481
|
+
* `true` at most once per `AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS` window and
|
|
1482
|
+
* records the time on each credit. Shared by `AIChatAgent` and `Think`.
|
|
1685
1483
|
*/
|
|
1686
|
-
declare
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
errorText?: string,
|
|
1691
|
-
preliminary?: boolean
|
|
1692
|
-
): ToolPartUpdate;
|
|
1484
|
+
declare class AgentToolStreamProgressThrottle {
|
|
1485
|
+
private _lastBumpAt;
|
|
1486
|
+
shouldCredit(now: number): boolean;
|
|
1487
|
+
}
|
|
1693
1488
|
/**
|
|
1694
|
-
*
|
|
1695
|
-
*
|
|
1696
|
-
*
|
|
1697
|
-
*
|
|
1698
|
-
*
|
|
1699
|
-
*
|
|
1700
|
-
* When the host later approves/rejects the execution, the new outcome
|
|
1701
|
-
* (completed / rejected / paused-again) must replace that output in place.
|
|
1702
|
-
*
|
|
1703
|
-
* Matching is deliberately narrow and idempotent:
|
|
1704
|
-
*
|
|
1705
|
-
* - only `output-available` parts are considered;
|
|
1706
|
-
* - the existing output must be a paused-execution object carrying the same
|
|
1707
|
-
* `executionId` — anything else (already replaced, different execution)
|
|
1708
|
-
* returns the *same part reference*, which callers treat as a no-op signal
|
|
1709
|
-
* (skip persist + broadcast), mirroring {@link crossMessageToolResultUpdate}.
|
|
1489
|
+
* Throttle window for crediting recovery progress from mid-segment streaming
|
|
1490
|
+
* content (text/reasoning/tool-input deltas). A milestone chunk credits
|
|
1491
|
+
* unconditionally; deltas credit at most once per window so a long single
|
|
1492
|
+
* segment registers forward progress across crashes without writing storage per
|
|
1493
|
+
* token. 5s is far finer than the 300s no-progress budget, so any crash gap
|
|
1494
|
+
* longer than this window over an actively-streaming segment still credits.
|
|
1710
1495
|
*/
|
|
1711
|
-
declare
|
|
1712
|
-
toolCallId: string,
|
|
1713
|
-
executionId: string,
|
|
1714
|
-
output: unknown
|
|
1715
|
-
): ToolPartUpdate;
|
|
1496
|
+
declare const CHAT_STREAM_PROGRESS_CREDIT_THROTTLE_MS = 5000;
|
|
1716
1497
|
/**
|
|
1717
|
-
*
|
|
1718
|
-
*
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1498
|
+
* Per-isolate throttle gate for crediting recovery progress from mid-segment
|
|
1499
|
+
* streaming-content chunks — the delta arm of {@link shouldCreditStreamProgress}.
|
|
1500
|
+
* The `_lastBumpAt` clock is in-memory, so it resets per isolate and the first
|
|
1501
|
+
* delta after a restart always credits. Shared by `AIChatAgent` and `Think`.
|
|
1721
1502
|
*/
|
|
1722
|
-
declare
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
/**
|
|
1727
|
-
type
|
|
1728
|
-
|
|
1729
|
-
|
|
1503
|
+
declare class StreamProgressCreditThrottle {
|
|
1504
|
+
private _lastBumpAt;
|
|
1505
|
+
shouldCredit(now: number): boolean;
|
|
1506
|
+
}
|
|
1507
|
+
/** Durable record of the last turn that ended in a terminal error (#1645). */
|
|
1508
|
+
type ChatTerminalRecord = {
|
|
1509
|
+
requestId: string;
|
|
1510
|
+
body: string;
|
|
1730
1511
|
};
|
|
1731
|
-
/** Extract a tool part's name from its `tool-<name>` / `dynamic-tool` shape. */
|
|
1732
1512
|
/**
|
|
1733
|
-
*
|
|
1734
|
-
*
|
|
1735
|
-
*
|
|
1736
|
-
*
|
|
1737
|
-
* its `execute()` died with the isolate.
|
|
1513
|
+
* Persist a durable record of the last terminal turn so a client that
|
|
1514
|
+
* (re)connects after the turn ended still learns its outcome (#1645). Kept
|
|
1515
|
+
* until a later turn supersedes it ({@link clearChatTerminal}); a single record
|
|
1516
|
+
* is sufficient because only the most recent terminal is relevant.
|
|
1738
1517
|
*/
|
|
1739
|
-
declare function
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
*/
|
|
1749
|
-
declare function
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
| {
|
|
1753
|
-
name?: string;
|
|
1754
|
-
}
|
|
1755
|
-
| null
|
|
1756
|
-
| undefined
|
|
1757
|
-
>
|
|
1758
|
-
| undefined
|
|
1759
|
-
): Set<string>;
|
|
1760
|
-
/**
|
|
1761
|
-
* `true` when the latest assistant message is mid-batch: it carries at least
|
|
1762
|
-
* one settled tool result AND at least one tool call/approval still awaiting a
|
|
1763
|
-
* client result. That is the #1649 signature — the model fanned out parallel
|
|
1764
|
-
* tool calls and only some have been answered. Scoped to the leaf (the step the
|
|
1765
|
-
* continuation answers) so an unrelated dangling tool in an earlier message
|
|
1766
|
-
* doesn't block a legitimate follow-up continuation.
|
|
1767
|
-
*/
|
|
1768
|
-
declare function hasIncompleteToolBatch(
|
|
1769
|
-
messages: ReadonlyArray<ToolBatchMessage>
|
|
1770
|
-
): boolean;
|
|
1771
|
-
//#endregion
|
|
1772
|
-
//#region src/chat/parse-protocol.d.ts
|
|
1518
|
+
declare function recordChatTerminal(
|
|
1519
|
+
storage: Pick<DurableObjectStorage, "put">,
|
|
1520
|
+
requestId: string,
|
|
1521
|
+
body: string
|
|
1522
|
+
): Promise<void>;
|
|
1523
|
+
/** Clear the durable terminal record once a later turn supersedes it (#1645). */
|
|
1524
|
+
declare function clearChatTerminal(
|
|
1525
|
+
storage: Pick<DurableObjectStorage, "delete">
|
|
1526
|
+
): Promise<void>;
|
|
1527
|
+
/** Read the pending terminal record, or `null` if none is stored (#1645). */
|
|
1528
|
+
declare function pendingChatTerminal(
|
|
1529
|
+
storage: Pick<DurableObjectStorage, "get">
|
|
1530
|
+
): Promise<ChatTerminalRecord | null>;
|
|
1773
1531
|
/**
|
|
1774
|
-
*
|
|
1775
|
-
*
|
|
1776
|
-
*
|
|
1777
|
-
*
|
|
1532
|
+
* Build the on-connect "recovering…" replay frame (#1620), or `null` when no
|
|
1533
|
+
* (non-stale) recovery is in progress. A client that connects between recovery
|
|
1534
|
+
* attempts (no active stream) reads the turn as working rather than frozen. A
|
|
1535
|
+
* record older than the flag TTL is treated as abandoned (its terminal-clear
|
|
1536
|
+
* never ran) and skipped, so a dead recovery can't show "recovering…" forever.
|
|
1537
|
+
* `messageType` is the package's recovering wire-type enum.
|
|
1778
1538
|
*/
|
|
1539
|
+
declare function buildChatRecoveringFrame(
|
|
1540
|
+
storage: Pick<DurableObjectStorage, "get">,
|
|
1541
|
+
messageType: string,
|
|
1542
|
+
now: number
|
|
1543
|
+
): Promise<Record<string, unknown> | null>;
|
|
1779
1544
|
/**
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
1782
|
-
*
|
|
1783
|
-
*
|
|
1545
|
+
* Set or clear the live "recovering…" status (#1620). Persists a durable record
|
|
1546
|
+
* (so set/clear stay consistent across the isolates a recovery spans) and
|
|
1547
|
+
* broadcasts a recovering frame — but only on a genuine transition, so a
|
|
1548
|
+
* deploy/reconnect storm (which re-detects recovery many times) doesn't spam
|
|
1549
|
+
* the wire. A flag older than the TTL is stale: the owning incident was
|
|
1550
|
+
* abandoned without a terminal (e.g. the DO went idle before recovery could
|
|
1551
|
+
* resolve), so it is treated as not-recovering and can neither pin the
|
|
1552
|
+
* indicator on forever nor suppress a genuinely-new recovering signal.
|
|
1553
|
+
* `messageType` is the package's recovering wire-type enum; `broadcast` is the
|
|
1554
|
+
* package's chat-broadcast wrapper.
|
|
1784
1555
|
*/
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
| {
|
|
1796
|
-
type: "clear";
|
|
1797
|
-
}
|
|
1798
|
-
| {
|
|
1799
|
-
type: "cancel";
|
|
1800
|
-
id: string;
|
|
1801
|
-
}
|
|
1802
|
-
| {
|
|
1803
|
-
type: "tool-result";
|
|
1804
|
-
toolCallId: string;
|
|
1805
|
-
toolName: string;
|
|
1806
|
-
output: unknown;
|
|
1807
|
-
state?: string;
|
|
1808
|
-
errorText?: string;
|
|
1809
|
-
autoContinue?: boolean;
|
|
1810
|
-
clientTools?: Array<{
|
|
1811
|
-
name: string;
|
|
1812
|
-
description?: string;
|
|
1813
|
-
parameters?: unknown;
|
|
1814
|
-
}>;
|
|
1815
|
-
}
|
|
1816
|
-
| {
|
|
1817
|
-
type: "tool-approval";
|
|
1818
|
-
toolCallId: string;
|
|
1819
|
-
approved: boolean;
|
|
1820
|
-
autoContinue?: boolean;
|
|
1821
|
-
}
|
|
1822
|
-
| {
|
|
1823
|
-
type: "stream-resume-request";
|
|
1824
|
-
probeId?: string;
|
|
1825
|
-
}
|
|
1826
|
-
| {
|
|
1827
|
-
type: "stream-resume-ack";
|
|
1828
|
-
id: string;
|
|
1829
|
-
}
|
|
1830
|
-
| {
|
|
1831
|
-
type: "messages";
|
|
1832
|
-
messages: unknown[];
|
|
1833
|
-
};
|
|
1556
|
+
declare function setChatRecovering(
|
|
1557
|
+
active: boolean,
|
|
1558
|
+
requestId: string | undefined,
|
|
1559
|
+
deps: {
|
|
1560
|
+
storage: Pick<DurableObjectStorage, "get" | "put" | "delete">;
|
|
1561
|
+
messageType: string;
|
|
1562
|
+
broadcast: (frame: Record<string, unknown>) => void;
|
|
1563
|
+
now: number;
|
|
1564
|
+
}
|
|
1565
|
+
): Promise<void>;
|
|
1834
1566
|
/**
|
|
1835
|
-
*
|
|
1836
|
-
*
|
|
1837
|
-
*
|
|
1838
|
-
*
|
|
1839
|
-
*
|
|
1840
|
-
*
|
|
1841
|
-
* @example
|
|
1842
|
-
* ```typescript
|
|
1843
|
-
* const event = parseProtocolMessage(rawMessage);
|
|
1844
|
-
* if (!event) return userOnMessage(connection, rawMessage);
|
|
1845
|
-
*
|
|
1846
|
-
* switch (event.type) {
|
|
1847
|
-
* case "chat-request": { ... }
|
|
1848
|
-
* case "clear": { ... }
|
|
1849
|
-
* case "tool-result": { ... }
|
|
1850
|
-
* }
|
|
1851
|
-
* ```
|
|
1567
|
+
* Observability event produced by an incident evaluation or a status
|
|
1568
|
+
* transition, emitted by the caller. The `detected`/`attempt` events come from
|
|
1569
|
+
* the budget evaluation (begin path); the `scheduled` event comes from
|
|
1570
|
+
* `ChatRecoveryEngine.scheduleRecovery`; the `completed`/`skipped`/`failed`
|
|
1571
|
+
* events come from `ChatRecoveryEngine.updateIncident`. `reason` is carried only
|
|
1572
|
+
* by the `skipped`/`failed` transitions that record a cause.
|
|
1852
1573
|
*/
|
|
1853
|
-
|
|
1574
|
+
type ChatRecoveryIncidentEvent = {
|
|
1575
|
+
type:
|
|
1576
|
+
| "chat:recovery:detected"
|
|
1577
|
+
| "chat:recovery:attempt"
|
|
1578
|
+
| "chat:recovery:scheduled"
|
|
1579
|
+
| "chat:recovery:completed"
|
|
1580
|
+
| "chat:recovery:skipped"
|
|
1581
|
+
| "chat:recovery:failed";
|
|
1582
|
+
incidentId: string;
|
|
1583
|
+
requestId: string;
|
|
1584
|
+
attempt: number;
|
|
1585
|
+
maxAttempts: number;
|
|
1586
|
+
recoveryKind: ChatRecoveryKind;
|
|
1587
|
+
reason?: string;
|
|
1588
|
+
};
|
|
1589
|
+
type EvaluateChatRecoveryIncidentInput = {
|
|
1590
|
+
/** Recovery identity for this turn. */ identity: {
|
|
1591
|
+
requestId: string;
|
|
1592
|
+
recoveryRootRequestId?: string | null;
|
|
1593
|
+
latestUserMessageId?: string | null;
|
|
1594
|
+
recoveryKind: ChatRecoveryKind;
|
|
1595
|
+
} /** Fully-resolved recovery config. */;
|
|
1596
|
+
config: ResolvedChatRecoveryConfig /** The existing incident for this identity, or `null` if this is fresh. */;
|
|
1597
|
+
existing: ChatRecoveryIncident | null /** Current value of the durable monotonic progress counter. */;
|
|
1598
|
+
currentProgress: number;
|
|
1599
|
+
/**
|
|
1600
|
+
* Whether the turn is parked on a pending CLIENT interaction (an
|
|
1601
|
+
* `input-available` client-tool part or an `approval-requested` part). Such a
|
|
1602
|
+
* turn is waiting on the human, not stuck, so it is budget-free.
|
|
1603
|
+
*/
|
|
1604
|
+
awaitingClientInteraction: boolean /** Injected clock (epoch ms) for deterministic tests. */;
|
|
1605
|
+
now: number;
|
|
1606
|
+
/**
|
|
1607
|
+
* Invoked when `config.shouldKeepRecovering` throws. Lets each package keep
|
|
1608
|
+
* its own log prefix. A throwing predicate is treated as "keep recovering".
|
|
1609
|
+
*/
|
|
1610
|
+
onShouldKeepRecoveringError?: (error: unknown) => void;
|
|
1611
|
+
};
|
|
1612
|
+
type EvaluateChatRecoveryIncidentResult = {
|
|
1613
|
+
/** The next incident record to persist. */ incident: ChatRecoveryIncident /** Whether this incident is now sealed as exhausted. */;
|
|
1614
|
+
exhausted: boolean /** Observability events to emit, in order. */;
|
|
1615
|
+
events: ChatRecoveryIncidentEvent[];
|
|
1616
|
+
};
|
|
1854
1617
|
//#endregion
|
|
1855
|
-
//#region src/chat/
|
|
1618
|
+
//#region src/chat/recovery-engine.d.ts
|
|
1619
|
+
/** The scheduled-callback entrypoints a recovery schedule can target. */
|
|
1620
|
+
type ChatRecoveryScheduleCallback =
|
|
1621
|
+
| "_chatRecoveryContinue"
|
|
1622
|
+
| "_chatRecoveryRetry";
|
|
1856
1623
|
/**
|
|
1857
|
-
*
|
|
1624
|
+
* Why a recovery callback is being scheduled. The idempotency of the
|
|
1625
|
+
* underlying Task run's `idempotencyKey` depends ONLY on this:
|
|
1858
1626
|
*
|
|
1859
|
-
*
|
|
1860
|
-
*
|
|
1861
|
-
*
|
|
1627
|
+
* - `"initial"` — the first schedule of a continuation/retry when an interrupted
|
|
1628
|
+
* turn is detected on wake. A deploy rollout drops/reconnects the socket
|
|
1629
|
+
* several times, re-triggering detection; idempotent scheduling (dedup on
|
|
1630
|
+
* callback + payload) collapses that storm into a single enqueued continuation
|
|
1631
|
+
* instead of N duplicates.
|
|
1862
1632
|
*
|
|
1863
|
-
*
|
|
1864
|
-
*
|
|
1865
|
-
*
|
|
1866
|
-
*
|
|
1867
|
-
*
|
|
1633
|
+
* - `"stable_timeout_retry"` — a reschedule issued from INSIDE the currently-
|
|
1634
|
+
* executing recovery attempt (a continuation that timed out waiting for
|
|
1635
|
+
* stable state). That attempt — a `__cf_internal_chat_recovery` Task run —
|
|
1636
|
+
* settles only AFTER the callback returns, so an idempotent reschedule would
|
|
1637
|
+
* dedup onto the doomed attempt and settle with it — the retry would never
|
|
1638
|
+
* fire. A fresh (non-idempotent) delayed attempt survives.
|
|
1868
1639
|
*/
|
|
1869
|
-
|
|
1870
|
-
incoming: UIMessage[],
|
|
1871
|
-
serverMessages: readonly UIMessage[],
|
|
1872
|
-
sanitizeForContentKey?: (message: UIMessage) => UIMessage
|
|
1873
|
-
): UIMessage[];
|
|
1640
|
+
type ChatRecoveryScheduleReason = "initial" | "stable_timeout_retry";
|
|
1874
1641
|
/**
|
|
1875
|
-
*
|
|
1876
|
-
*
|
|
1877
|
-
*
|
|
1642
|
+
* A reconstructed orphaned-stream partial. The engine seam is deliberately
|
|
1643
|
+
* **wire-vocabulary-agnostic**: `text` is the accumulated assistant text and
|
|
1644
|
+
* `parts` is OPAQUE to the engine (`unknown[]`) — each host casts it back to its
|
|
1645
|
+
* own message-part vocabulary (AI SDK `UIMessage` parts, AG-UI tool parts, …).
|
|
1646
|
+
* The single fact the engine needs about parts — does the partial carry settled
|
|
1647
|
+
* (non-idempotent) tool work that must survive a `{ persist: false }` recovery
|
|
1648
|
+
* (#1631)? — is precomputed by the {@link ChatRecoveryCodec} as
|
|
1649
|
+
* `hasSettledToolResults`. So the engine never imports a part vocabulary; the
|
|
1650
|
+
* codec owns it (see `partialHasSettledToolResults` in `recovery-codec.ts` for
|
|
1651
|
+
* the AI SDK codec's implementation of that predicate).
|
|
1878
1652
|
*/
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
* on conflicts), falling back to whichever side is present.
|
|
1901
|
-
*
|
|
1902
|
-
* The result carries the INCOMING message's id/role (the caller has already
|
|
1903
|
-
* resolved the incoming id to the existing row's id via the (b) target-id
|
|
1904
|
-
* step), so it is safe to write straight back through `updateMessage`.
|
|
1905
|
-
*
|
|
1906
|
-
* Hosts whose orphan persist only ever runs at stream finalize (no early/
|
|
1907
|
-
* mid-stream row for the same id) never hit the merge branch and don't need
|
|
1908
|
-
* this — a plain append/replace is already dedup-safe because the shared
|
|
1909
|
-
* reconstruction (`StreamAccumulator` / `applyChunkToParts`) is idempotent by
|
|
1910
|
-
* `toolCallId`.
|
|
1911
|
-
*/
|
|
1912
|
-
declare function reconcileOrphanPartial(
|
|
1913
|
-
existing: UIMessage,
|
|
1914
|
-
incoming: UIMessage
|
|
1915
|
-
): UIMessage;
|
|
1916
|
-
//#endregion
|
|
1917
|
-
//#region src/chat/repair-transcript.d.ts
|
|
1653
|
+
type RecoveryPartial = {
|
|
1654
|
+
text: string;
|
|
1655
|
+
parts: unknown[];
|
|
1656
|
+
hasSettledToolResults: boolean;
|
|
1657
|
+
};
|
|
1658
|
+
/** Lifecycle status of a recovered stream's metadata row. */
|
|
1659
|
+
type ChatStreamStatus = "streaming" | "completed" | "error";
|
|
1660
|
+
/** Identity + context for opening (or re-evaluating) a recovery incident. */
|
|
1661
|
+
interface BeginChatRecoveryIncidentInput {
|
|
1662
|
+
requestId: string;
|
|
1663
|
+
recoveryRootRequestId?: string | null;
|
|
1664
|
+
latestUserMessageId?: string | null;
|
|
1665
|
+
recoveryKind: ChatRecoveryKind;
|
|
1666
|
+
/** Test-only clock injection for deterministic debounce/window timing. */
|
|
1667
|
+
nowMs?: number;
|
|
1668
|
+
}
|
|
1669
|
+
interface BeginChatRecoveryIncidentResult {
|
|
1670
|
+
incident: ChatRecoveryIncident;
|
|
1671
|
+
config: ResolvedChatRecoveryConfig;
|
|
1672
|
+
exhausted: boolean;
|
|
1673
|
+
}
|
|
1918
1674
|
/**
|
|
1919
|
-
*
|
|
1920
|
-
*
|
|
1921
|
-
*
|
|
1922
|
-
*
|
|
1923
|
-
* terminal states: `convertToModelMessages` emits a `tool-result` for
|
|
1924
|
-
* `output-available`, `output-error`, AND `output-denied` (a user-denied
|
|
1925
|
-
* approval — its denial reason becomes the tool-result). Omitting any of these
|
|
1926
|
-
* makes repair re-flip the part every turn — clobbering a real `errorText` /
|
|
1927
|
-
* denial with the generic "interrupted" message.
|
|
1675
|
+
* Package-specific host operations the engine drives during incident
|
|
1676
|
+
* orchestration. Every method is a thin pass-through to the package's existing
|
|
1677
|
+
* storage / clock / event / interaction primitives — the engine owns only the
|
|
1678
|
+
* *sequence*, not the I/O.
|
|
1928
1679
|
*/
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
):
|
|
1932
|
-
|
|
1680
|
+
interface ChatRecoveryAdapter {
|
|
1681
|
+
/** Resolve the effective recovery config (defaults + caller overrides). */
|
|
1682
|
+
resolveConfig(): ResolvedChatRecoveryConfig;
|
|
1683
|
+
/** Wall clock; only consulted when the input carries no test `nowMs`. */
|
|
1684
|
+
now(): number;
|
|
1685
|
+
/** Evict incidents past the TTL. Runs before the existing-record read. */
|
|
1686
|
+
sweepStaleIncidents(now: number): Promise<void>;
|
|
1687
|
+
/** Read the persisted incident for `key`, or `null` if none. */
|
|
1688
|
+
getIncident(key: string): Promise<ChatRecoveryIncident | null>;
|
|
1933
1689
|
/**
|
|
1934
|
-
*
|
|
1935
|
-
*
|
|
1936
|
-
*
|
|
1937
|
-
*
|
|
1938
|
-
*
|
|
1690
|
+
* Optional: rehydrate any state the interaction predicate depends on. Invoked
|
|
1691
|
+
* after the existing-incident read and BEFORE `isAwaitingClientInteraction`.
|
|
1692
|
+
* `Think` uses this to restore client tools from durable storage on a cold
|
|
1693
|
+
* boot-recovery wake (so a HITL turn is not misread as stuck); `AIChatAgent`
|
|
1694
|
+
* has no such state and omits it.
|
|
1939
1695
|
*/
|
|
1940
|
-
|
|
1696
|
+
ensureInteractionStateLoaded?(): void;
|
|
1941
1697
|
/**
|
|
1942
|
-
*
|
|
1943
|
-
*
|
|
1698
|
+
* Optional: give the package a chance to handle a NON-chat fiber before chat
|
|
1699
|
+
* recovery inspects it. Returns `true` if the package fully consumed the
|
|
1700
|
+
* fiber, in which case the engine tells the caller to skip chat-recovery
|
|
1701
|
+
* processing for it. `Think` uses this for its messenger/workflow reply fibers
|
|
1702
|
+
* (`think:messenger-reply`); `AIChatAgent` has no non-chat fibers and omits it
|
|
1703
|
+
* (the engine then treats every recovered fiber as a chat-recovery candidate).
|
|
1704
|
+
*
|
|
1705
|
+
* Ordering invariant: the engine dispatches this FIRST, before the
|
|
1706
|
+
* chat-fiber-name gate, so a non-chat fiber is never misclassified as an
|
|
1707
|
+
* orphaned chat turn.
|
|
1944
1708
|
*/
|
|
1945
|
-
|
|
1709
|
+
tryHandleNonChatFiberRecovery?(ctx: FiberRecoveryContext$1): Promise<boolean>;
|
|
1710
|
+
/** Monotonic forward-progress marker for the no-progress budget. */
|
|
1711
|
+
readProgress(): Promise<number>;
|
|
1946
1712
|
/**
|
|
1947
|
-
*
|
|
1948
|
-
*
|
|
1713
|
+
* Whether the turn is parked on a pending CLIENT interaction (waiting on the
|
|
1714
|
+
* human, not stuck). When true the engine keeps the incident budget-free.
|
|
1715
|
+
* Optional: a host with no client-interaction/HITL substrate (e.g. the pi
|
|
1716
|
+
* fixture) omits it and the engine treats the turn as never parked (`false`).
|
|
1949
1717
|
*/
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
};
|
|
1718
|
+
isAwaitingClientInteraction?(): boolean;
|
|
1719
|
+
/** Persist the evaluated incident under `key`. */
|
|
1720
|
+
putIncident(key: string, incident: ChatRecoveryIncident): Promise<void>;
|
|
1954
1721
|
/**
|
|
1955
|
-
*
|
|
1956
|
-
* `
|
|
1957
|
-
*
|
|
1958
|
-
* `repairPart` override). A host whose default `repairPart` errors the part
|
|
1959
|
-
* (ai-chat) passes this to SKIP a part still legitimately awaiting a CLIENT
|
|
1960
|
-
* interaction (an `input-available` client tool or an `approval-requested`
|
|
1961
|
-
* part the user may still answer) so it is left verbatim rather than clobbered
|
|
1962
|
-
* with an error. Skipped parts are not counted in `removedToolCalls`.
|
|
1722
|
+
* Delete the incident record under `key`. The engine calls this on the
|
|
1723
|
+
* terminal `completed` transition (a completed recovery is never retried, so
|
|
1724
|
+
* its record is dropped rather than left in storage forever).
|
|
1963
1725
|
*/
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
/** A new messages array; unchanged messages keep their original reference. */
|
|
1968
|
-
messages: UIMessage[];
|
|
1969
|
-
/** Count of interrupted tool calls flipped to a repaired shape. */
|
|
1970
|
-
removedToolCalls: number;
|
|
1971
|
-
/** Count of tool parts whose malformed `input` was normalized. */
|
|
1972
|
-
normalizedInputs: number;
|
|
1973
|
-
/** The tool-call ids that were repaired. */
|
|
1974
|
-
toolCallIds: string[];
|
|
1975
|
-
}
|
|
1976
|
-
/**
|
|
1977
|
-
* Repair interrupted tool calls and normalize malformed tool inputs across a
|
|
1978
|
-
* transcript. Behavior mirrors `@cloudflare/think`'s original
|
|
1979
|
-
* `_repairToolTranscriptParts`:
|
|
1980
|
-
*
|
|
1981
|
-
* - a tool part with NO settled result and state `approval-responded` is kept
|
|
1982
|
-
* verbatim (an approved server tool waiting for its continuation to run
|
|
1983
|
-
* `execute()` — not abandoned);
|
|
1984
|
-
* - a tool part with NO settled result for which `shouldRepair` returns false
|
|
1985
|
-
* is kept verbatim (a part still awaiting a CLIENT interaction; see option);
|
|
1986
|
-
* - any other tool part with no settled result is normalized then handed to
|
|
1987
|
-
* `repairPart` (default: flipped to an errored result);
|
|
1988
|
-
* - a tool part WITH a settled result only has its `input` normalized.
|
|
1989
|
-
*
|
|
1990
|
-
* Messages with no changed part keep their original object reference so callers
|
|
1991
|
-
* can cheaply detect what to persist.
|
|
1992
|
-
*/
|
|
1993
|
-
declare function repairInterruptedToolParts(
|
|
1994
|
-
messages: UIMessage[],
|
|
1995
|
-
options: RepairInterruptedToolPartsOptions
|
|
1996
|
-
): RepairInterruptedToolPartsResult;
|
|
1997
|
-
//#endregion
|
|
1998
|
-
//#region src/chat/orphan-store.d.ts
|
|
1999
|
-
interface OrphanPersistStore<
|
|
2000
|
-
M extends {
|
|
2001
|
-
id: string;
|
|
2002
|
-
} = UIMessage
|
|
2003
|
-
> {
|
|
2004
|
-
/** Read the stored message with this id, or `null` if none exists. */
|
|
2005
|
-
getMessage(id: string): M | null | Promise<M | null>;
|
|
1726
|
+
deleteIncident(key: string): Promise<void>;
|
|
1727
|
+
/** Broadcast a lifecycle event produced by the evaluation or a transition. */
|
|
1728
|
+
emitRecoveryEvent(event: ChatRecoveryIncidentEvent): void;
|
|
2006
1729
|
/**
|
|
2007
|
-
*
|
|
2008
|
-
*
|
|
1730
|
+
* Enqueue a recovery callback. The engine owns the surrounding orchestration
|
|
1731
|
+
* and the package chooses its durable transport: a Task for a root chat agent,
|
|
1732
|
+
* or the temporary routed-schedule fallback for a dynamic agent. `reason`
|
|
1733
|
+
* selects deduplication and `delaySeconds` is `0` initially or the stable
|
|
1734
|
+
* retry delay for a chained attempt.
|
|
2009
1735
|
*/
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
interface PersistReconstructedOrphanOptions<
|
|
2017
|
-
TMessage extends UIMessage = UIMessage
|
|
2018
|
-
> {
|
|
2019
|
-
/** The store seam to upsert through (a `SessionProvider` write-subset). */
|
|
2020
|
-
store: OrphanPersistStore<TMessage>;
|
|
1736
|
+
scheduleRecovery(
|
|
1737
|
+
callback: ChatRecoveryScheduleCallback,
|
|
1738
|
+
data: Record<string, unknown>,
|
|
1739
|
+
reason: ChatRecoveryScheduleReason,
|
|
1740
|
+
delaySeconds: number
|
|
1741
|
+
): Promise<void>;
|
|
2021
1742
|
/**
|
|
2022
|
-
*
|
|
2023
|
-
*
|
|
2024
|
-
*
|
|
1743
|
+
* Set or clear the live "recovering…" status (#1620). The engine calls this on
|
|
1744
|
+
* the incident transitions: `scheduled` → active (keyed by the recovery-root
|
|
1745
|
+
* request id, falling back to the incident's request id), and
|
|
1746
|
+
* `completed`/`skipped`/`failed` → cleared. The package owns the underlying
|
|
1747
|
+
* staleness / idempotency / broadcast I/O.
|
|
2025
1748
|
*/
|
|
2026
|
-
|
|
1749
|
+
setRecovering(active: boolean, requestId?: string): Promise<void>;
|
|
2027
1750
|
/**
|
|
2028
|
-
*
|
|
2029
|
-
*
|
|
2030
|
-
*
|
|
1751
|
+
* Report a throw from the caller's `shouldKeepRecovering` hook. Optional: a
|
|
1752
|
+
* host that does not surface this diagnostic omits it (the engine swallows the
|
|
1753
|
+
* report).
|
|
2031
1754
|
*/
|
|
2032
|
-
|
|
1755
|
+
onShouldKeepRecoveringError?(error: unknown): void;
|
|
2033
1756
|
/**
|
|
2034
|
-
*
|
|
2035
|
-
*
|
|
1757
|
+
* Terminalize a given-up recovery turn: deliver the exhaustion notification
|
|
1758
|
+
* plus the package-owned terminal record / banner / submission writes. A thin
|
|
1759
|
+
* pass-through to the package's `_exhaustChatRecovery` (which composes
|
|
1760
|
+
* {@link runChatRecoveryExhaustion}). Driven by
|
|
1761
|
+
* {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}; the engine owns the
|
|
1762
|
+
* surrounding read → re-entry-guard → build → terminalize → seal sequence, the
|
|
1763
|
+
* package owns the terminal writes (uniformly broadcast-first; their set
|
|
1764
|
+
* differs — `Think` also writes a submission row).
|
|
2036
1765
|
*/
|
|
2037
|
-
|
|
1766
|
+
exhaustChatRecovery(
|
|
1767
|
+
incident: ChatRecoveryIncident,
|
|
1768
|
+
config: ResolvedChatRecoveryConfig,
|
|
1769
|
+
partial: RecoveryPartial,
|
|
1770
|
+
streamId: string,
|
|
1771
|
+
createdAt: number
|
|
1772
|
+
): Promise<void>;
|
|
1773
|
+
/**
|
|
1774
|
+
* Resolve the orphaned stream identity for a (recovery-root) request id —
|
|
1775
|
+
* `streamId` is `""` when no stream metadata survives. Drives BOTH the wake
|
|
1776
|
+
* path (which consumes the full {@link ResolvedRecoveryStream}) and the
|
|
1777
|
+
* give-up path (which reads only `.streamId`). A thin pass-through to the
|
|
1778
|
+
* package's stream-metadata lookup: the newest row keyed by the request id,
|
|
1779
|
+
* else the live active stream.
|
|
1780
|
+
*/
|
|
1781
|
+
resolveRecoveryStream(requestId: string): ResolvedRecoveryStream;
|
|
1782
|
+
/** Reconstruct the partial text/parts buffered for `streamId`. */
|
|
1783
|
+
getPartialStreamText(streamId: string): RecoveryPartial;
|
|
1784
|
+
/**
|
|
1785
|
+
* The in-flight recovery-root request id, consulted as a fallback in the
|
|
1786
|
+
* give-up root-id chain when the payload carries no `originalRequestId` /
|
|
1787
|
+
* `recoveredRequestId` and no incident record survives. `undefined` when no
|
|
1788
|
+
* recovery chain is active. (`AIChatAgent` and `Think` both back this with
|
|
1789
|
+
* `_activeChatRecoveryRootRequestId`.)
|
|
1790
|
+
*/
|
|
1791
|
+
activeChatRecoveryRootRequestId(): string | undefined;
|
|
1792
|
+
/**
|
|
1793
|
+
* Report a tolerated best-effort bookkeeping failure during give-up: the
|
|
1794
|
+
* incident `"read"` (before synthesizing) or the sealing `"seal"` write
|
|
1795
|
+
* (after terminalization). Neither aborts terminalization — see
|
|
1796
|
+
* {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}.
|
|
1797
|
+
*/
|
|
1798
|
+
onGiveUpBookkeepingError(phase: "read" | "seal", error: unknown): void;
|
|
2038
1799
|
}
|
|
2039
|
-
/**
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
)
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
* The minimal transcript-tail shape {@link createChatFiberSnapshot} reads to
|
|
2057
|
-
* derive the snapshot's `latest*Id` markers. Deliberately NOT `UIMessage`: the
|
|
2058
|
-
* snapshot only ever needs each message's `id` + `role`, so any host transcript
|
|
2059
|
-
* (AI SDK `UIMessage[]`, `Think`'s session leaves, or the pi adapter's plain
|
|
2060
|
-
* `AgentMessage[]`) satisfies it structurally. Keeping this off `UIMessage` is
|
|
2061
|
-
* the Phase-5 genericity seam — the snapshot builder must not couple to the AI
|
|
2062
|
-
* SDK message shape.
|
|
2063
|
-
*/
|
|
2064
|
-
interface SnapshotMessage {
|
|
2065
|
-
id?: string;
|
|
2066
|
-
role: string;
|
|
1800
|
+
/** Resolved orphaned-stream identity for a recovered chat turn. */
|
|
1801
|
+
interface ResolvedRecoveryStream {
|
|
1802
|
+
/** The orphaned stream id, or `""` when no stream metadata survives. */
|
|
1803
|
+
streamId: string;
|
|
1804
|
+
/**
|
|
1805
|
+
* Whether the orphaned stream is still the live in-flight stream (so its
|
|
1806
|
+
* partial has not already been persisted + completed by an ACK-driven
|
|
1807
|
+
* reconnect). Gates persistence and stream completion.
|
|
1808
|
+
*/
|
|
1809
|
+
streamStillActive: boolean;
|
|
1810
|
+
/**
|
|
1811
|
+
* The stream metadata row's lifecycle status, when the host tracks it
|
|
1812
|
+
* (`Think`). `undefined` for hosts that do not model terminal streams
|
|
1813
|
+
* (`AIChatAgent`) — those keep every terminal-stream branch dead, per the
|
|
1814
|
+
* "substrate capabilities are optional" decision in the RFC.
|
|
1815
|
+
*/
|
|
1816
|
+
streamStatus?: ChatStreamStatus;
|
|
2067
1817
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
1818
|
+
/** Input to {@link ChatFiberWakeHooks.classifyRecoveredTurn}. */
|
|
1819
|
+
interface ClassifyRecoveredTurnInput {
|
|
1820
|
+
snapshot: ChatFiberSnapshot | null;
|
|
2071
1821
|
requestId: string;
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
kind,
|
|
2083
|
-
requestId,
|
|
2084
|
-
recoveryRootRequestId,
|
|
2085
|
-
continuation,
|
|
2086
|
-
messages,
|
|
2087
|
-
lastBody,
|
|
2088
|
-
lastClientTools
|
|
2089
|
-
}: {
|
|
2090
|
-
kind: Kind;
|
|
1822
|
+
streamId: string;
|
|
1823
|
+
partial: RecoveryPartial;
|
|
1824
|
+
streamStillActive: boolean;
|
|
1825
|
+
streamStatus?: ChatStreamStatus;
|
|
1826
|
+
}
|
|
1827
|
+
/** Input to {@link ChatFiberWakeHooks.invokeOnChatRecovery}. */
|
|
1828
|
+
interface InvokeOnChatRecoveryInput {
|
|
1829
|
+
incident: ChatRecoveryIncident;
|
|
1830
|
+
recoveryKind: ChatRecoveryKind;
|
|
1831
|
+
recoveryRootRequestId: string;
|
|
2091
1832
|
requestId: string;
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
}
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
1833
|
+
streamId: string;
|
|
1834
|
+
partial: RecoveryPartial;
|
|
1835
|
+
snapshot: ChatFiberSnapshot | null;
|
|
1836
|
+
recoveryData: unknown;
|
|
1837
|
+
createdAt: number;
|
|
1838
|
+
}
|
|
1839
|
+
/** Input to {@link ChatFiberWakeHooks.shouldPersistOrphanedPartial}. */
|
|
1840
|
+
interface PersistOrphanedPartialInput {
|
|
1841
|
+
streamId: string;
|
|
1842
|
+
streamStillActive: boolean;
|
|
1843
|
+
streamStatus?: ChatStreamStatus;
|
|
1844
|
+
snapshot: ChatFiberSnapshot | null;
|
|
1845
|
+
}
|
|
1846
|
+
/** Input to {@link ChatFiberWakeHooks.dispatchRecoveredTurn}. */
|
|
1847
|
+
interface DispatchRecoveredTurnInput<TClassify> {
|
|
1848
|
+
incident: ChatRecoveryIncident;
|
|
1849
|
+
config: ResolvedChatRecoveryConfig;
|
|
1850
|
+
recoveryKind: ChatRecoveryKind;
|
|
1851
|
+
options: ChatRecoveryOptions;
|
|
1852
|
+
snapshot: ChatFiberSnapshot | null;
|
|
1853
|
+
requestId: string;
|
|
1854
|
+
recoveryRootRequestId: string;
|
|
1855
|
+
streamId: string;
|
|
1856
|
+
streamStatus?: ChatStreamStatus;
|
|
1857
|
+
/** The package-specific classification detail produced by `classifyRecoveredTurn`. */
|
|
1858
|
+
detail: TClassify;
|
|
1859
|
+
}
|
|
2119
1860
|
/**
|
|
2120
|
-
*
|
|
1861
|
+
* The wake-dispatch host operations the engine drives when an interrupted CHAT
|
|
1862
|
+
* fiber is detected on restart — the divergent organs the frame-collapse map
|
|
1863
|
+
* flagged. Kept SEPARATE from {@link ChatRecoveryAdapter} (and passed per call to
|
|
1864
|
+
* {@link ChatRecoveryEngine.handleChatFiberRecovery}) so the incident/give-up
|
|
1865
|
+
* adapter stays focused, and generic over `TClassify` so the
|
|
1866
|
+
* `classifyRecoveredTurn` → `dispatchRecoveredTurn` handoff is type-safe without a
|
|
1867
|
+
* class-level generic.
|
|
2121
1868
|
*
|
|
2122
|
-
*
|
|
2123
|
-
*
|
|
2124
|
-
*
|
|
1869
|
+
* The engine owns the wake LIFECYCLE (gate → parse → unwrap → stream → partial →
|
|
1870
|
+
* classify → begin-incident → exhausted-branch → onChatRecovery → persist →
|
|
1871
|
+
* complete → dispatch → catch→failed) and the shared persist clause; these hooks
|
|
1872
|
+
* own the package-specific I/O and the retry/continue/skip decision.
|
|
2125
1873
|
*/
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
| "detected"
|
|
2135
|
-
| "scheduled"
|
|
2136
|
-
| "attempting"
|
|
2137
|
-
| "completed"
|
|
2138
|
-
| "skipped"
|
|
2139
|
-
| "exhausted"
|
|
2140
|
-
| "failed";
|
|
2141
|
-
firstSeenAt: number;
|
|
2142
|
-
lastAttemptAt: number;
|
|
1874
|
+
interface ChatFiberWakeHooks<TClassify> {
|
|
1875
|
+
/** The chat-fiber name prefix (`CHAT_FIBER_NAME + ":"`) gating the wake path. */
|
|
1876
|
+
chatFiberPrefix(): string;
|
|
1877
|
+
/** Decode the fiber snapshot into the recovery snapshot + checkpointed user data. */
|
|
1878
|
+
unwrapRecoverySnapshot(ctx: FiberRecoveryContext$1): {
|
|
1879
|
+
snapshot: ChatFiberSnapshot | null;
|
|
1880
|
+
recoveryData: unknown;
|
|
1881
|
+
};
|
|
2143
1882
|
/**
|
|
2144
|
-
*
|
|
2145
|
-
*
|
|
2146
|
-
*
|
|
2147
|
-
* genuinely stuck turn is sealed within the window (#1637). Optional for
|
|
2148
|
-
* backward-compat — falls back to `firstSeenAt`.
|
|
1883
|
+
* Classify the recovered turn as a `retry` or `continue` and return any
|
|
1884
|
+
* package-specific detail the dispatch decision needs (e.g. the pre-stream
|
|
1885
|
+
* retry target id). Runs before the incident is opened.
|
|
2149
1886
|
*/
|
|
2150
|
-
|
|
2151
|
-
|
|
1887
|
+
classifyRecoveredTurn(input: ClassifyRecoveredTurnInput):
|
|
1888
|
+
| {
|
|
1889
|
+
recoveryKind: ChatRecoveryKind;
|
|
1890
|
+
detail: TClassify;
|
|
1891
|
+
}
|
|
1892
|
+
| Promise<{
|
|
1893
|
+
recoveryKind: ChatRecoveryKind;
|
|
1894
|
+
detail: TClassify;
|
|
1895
|
+
}>;
|
|
2152
1896
|
/**
|
|
2153
|
-
*
|
|
2154
|
-
*
|
|
2155
|
-
*
|
|
2156
|
-
*
|
|
2157
|
-
*
|
|
1897
|
+
* Build the package's `ChatRecoveryContext` and invoke the user `onChatRecovery`
|
|
1898
|
+
* hook, returning its (defaulted) options. The engine wraps this in the
|
|
1899
|
+
* incident `failed`-on-throw guard. Optional: a host with no user
|
|
1900
|
+
* `onChatRecovery` surface (e.g. the pi fixture) omits it and the engine
|
|
1901
|
+
* proceeds with empty options (`{}`).
|
|
2158
1902
|
*/
|
|
2159
|
-
|
|
1903
|
+
invokeOnChatRecovery?(
|
|
1904
|
+
input: InvokeOnChatRecoveryInput
|
|
1905
|
+
): Promise<ChatRecoveryOptions | void>;
|
|
2160
1906
|
/**
|
|
2161
|
-
*
|
|
2162
|
-
*
|
|
2163
|
-
*
|
|
2164
|
-
*
|
|
2165
|
-
* from an older build is never falsely sealed.
|
|
1907
|
+
* The BASE persist gate: whether the orphaned partial is eligible to be
|
|
1908
|
+
* materialized at all (live stream, or terminal-but-not-yet-persisted). The
|
|
1909
|
+
* engine ANDs this with the shared `options.persist !== false ||
|
|
1910
|
+
* partial.hasSettledToolResults` clause, so settled work is never dropped.
|
|
2166
1911
|
*/
|
|
2167
|
-
|
|
1912
|
+
shouldPersistOrphanedPartial(
|
|
1913
|
+
input: PersistOrphanedPartialInput
|
|
1914
|
+
): boolean | Promise<boolean>;
|
|
1915
|
+
/** Materialize the orphaned stream's partial into a persisted assistant message. */
|
|
1916
|
+
persistOrphanedStream(streamId: string): Promise<void>;
|
|
1917
|
+
/** Mark the (still-active) recovered stream complete and schedule cleanup. */
|
|
1918
|
+
completeRecoveredStream(streamId: string): void | Promise<void>;
|
|
2168
1919
|
/**
|
|
2169
|
-
*
|
|
2170
|
-
*
|
|
2171
|
-
*
|
|
2172
|
-
*
|
|
2173
|
-
* it must NOT credit forward progress and must be bounded by a tight,
|
|
2174
|
-
* OOM-specific budget (`maxOomRetries`) rather than the generic attempt cap
|
|
2175
|
-
* (which resets on progress). Bumped by `ChatRecoveryEngine.recordOomAndDecide`
|
|
2176
|
-
* when a recovery callback observes an OOM; exceeding `maxOomRetries` seals the
|
|
2177
|
-
* incident with `reason="out_of_memory"` (#1825). Optional for backward-compat.
|
|
1920
|
+
* The retry/continue/skip DECISION — the package-owned core. Runs after persist
|
|
1921
|
+
* + complete; owns the leaf/submission computation, the schedule calls (via
|
|
1922
|
+
* {@link ChatRecoveryEngine.scheduleRecovery}), the skip transitions, and any
|
|
1923
|
+
* package-specific terminal/broadcast writes.
|
|
2178
1924
|
*/
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
* Durable, monotonic forward-progress counter for recovery budget resets.
|
|
2184
|
-
* Bumped at production time when new content is streamed, so it reflects
|
|
2185
|
-
* genuinely new content and is immune to reconnects/re-persists; never
|
|
2186
|
-
* recomputed from the (compactable) transcript.
|
|
2187
|
-
*/
|
|
2188
|
-
declare const CHAT_RECOVERY_PROGRESS_KEY = "cf:chat-recovery:progress";
|
|
2189
|
-
/**
|
|
2190
|
-
* Durable record of an in-progress recovery so a "recovering…" status (#1620)
|
|
2191
|
-
* can be broadcast live and survive the set/clear happening in different
|
|
2192
|
-
* isolates (a continuation runs in a later alarm invocation).
|
|
2193
|
-
*/
|
|
2194
|
-
declare const CHAT_RECOVERING_KEY = "cf:chat:recovering";
|
|
2195
|
-
/**
|
|
2196
|
-
* Durable record of the last turn that ended in a terminal error / abandoned
|
|
2197
|
-
* recovery (#1645). Replayed on the next reconnect via the resume handshake;
|
|
2198
|
-
* cleared when a later turn supersedes it.
|
|
2199
|
-
*/
|
|
2200
|
-
declare const CHAT_LAST_TERMINAL_KEY = "cf:chat:last-terminal";
|
|
1925
|
+
dispatchRecoveredTurn(
|
|
1926
|
+
input: DispatchRecoveredTurnInput<TClassify>
|
|
1927
|
+
): Promise<void>;
|
|
1928
|
+
}
|
|
2201
1929
|
/**
|
|
2202
|
-
*
|
|
2203
|
-
*
|
|
2204
|
-
*
|
|
2205
|
-
* normal deploy cadence (#1637).
|
|
1930
|
+
* Drives the shared recovery orchestration over a {@link ChatRecoveryAdapter}.
|
|
1931
|
+
* The incident *budget math* lives in the pure `evaluateChatRecoveryIncident`;
|
|
1932
|
+
* this class owns the surrounding sequence and its ordering invariants.
|
|
2206
1933
|
*/
|
|
2207
|
-
declare
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
1934
|
+
declare class ChatRecoveryEngine {
|
|
1935
|
+
private readonly adapter;
|
|
1936
|
+
constructor(adapter: ChatRecoveryAdapter);
|
|
1937
|
+
/**
|
|
1938
|
+
* Open or re-evaluate the recovery incident for `input`, persist the result,
|
|
1939
|
+
* and broadcast its lifecycle events. Returns the incident, the resolved
|
|
1940
|
+
* config, and whether the budget is now exhausted.
|
|
1941
|
+
*/
|
|
1942
|
+
/**
|
|
1943
|
+
* Dispatch a recovered fiber to the package's non-chat handler (the
|
|
1944
|
+
* messenger/workflow seam) before any chat-recovery processing. Returns `true`
|
|
1945
|
+
* when the package consumed the fiber — the caller must then skip chat
|
|
1946
|
+
* recovery for it. The engine owns the *ordering* (this runs before the
|
|
1947
|
+
* chat-fiber gate); the *behavior* is adapter-owned. No-op (`false`) when the
|
|
1948
|
+
* adapter omits {@link ChatRecoveryAdapter.tryHandleNonChatFiberRecovery}.
|
|
1949
|
+
*/
|
|
1950
|
+
handleNonChatFiber(ctx: FiberRecoveryContext$1): Promise<boolean>;
|
|
1951
|
+
/**
|
|
1952
|
+
* The shared wake-recovery LIFECYCLE for an interrupted chat fiber. Both
|
|
1953
|
+
* packages drove this exact frame; the divergent organs are the
|
|
1954
|
+
* {@link ChatFiberWakeHooks}. In order:
|
|
1955
|
+
*
|
|
1956
|
+
* 1. non-chat dispatch ({@link handleNonChatFiber}) FIRST, then the chat-fiber
|
|
1957
|
+
* name gate — a non-chat fiber is never misread as an orphaned chat turn;
|
|
1958
|
+
* 2. parse the request id, unwrap the snapshot, resolve the orphaned stream +
|
|
1959
|
+
* reconstruct its partial;
|
|
1960
|
+
* 3. classify the turn (retry/continue + package detail) and open the incident;
|
|
1961
|
+
* 4. if the budget is already exhausted, persist the settled partial (so
|
|
1962
|
+
* non-idempotent tool results are not discarded — #1631) and terminalize
|
|
1963
|
+
* BEFORE consulting `onChatRecovery`;
|
|
1964
|
+
* 5. otherwise, inside a `failed`-on-throw guard: invoke `onChatRecovery`,
|
|
1965
|
+
* apply the shared persist gate (base eligibility AND `persist !== false ||
|
|
1966
|
+
* settled tool results`), complete the live stream, then hand the
|
|
1967
|
+
* retry/continue/skip DECISION to {@link ChatFiberWakeHooks.dispatchRecoveredTurn}.
|
|
1968
|
+
*
|
|
1969
|
+
* Returns `true` when the fiber was a chat (or non-chat) recovery the engine
|
|
1970
|
+
* handled, `false` when it was not a chat fiber (the caller keeps looking). Any
|
|
1971
|
+
* throw after the incident opens flips it to `failed` so it is never left
|
|
1972
|
+
* leaking in `attempting`.
|
|
1973
|
+
*/
|
|
1974
|
+
handleChatFiberRecovery<TClassify>(
|
|
1975
|
+
ctx: FiberRecoveryContext$1,
|
|
1976
|
+
wake: ChatFiberWakeHooks<TClassify>
|
|
1977
|
+
): Promise<boolean>;
|
|
1978
|
+
/**
|
|
1979
|
+
* The shared persist gate: base eligibility (the package's
|
|
1980
|
+
* {@link ChatFiberWakeHooks.shouldPersistOrphanedPartial}) AND the
|
|
1981
|
+
* never-drop-settled-work clause `options.persist !== false ||
|
|
1982
|
+
* partial.hasSettledToolResults`. `options: undefined` (the exhausted branch)
|
|
1983
|
+
* collapses the clause to the base gate. The clause lives here — not in each
|
|
1984
|
+
* package — because settled-work preservation is a cross-package invariant
|
|
1985
|
+
* (#1631), and the codec (not the engine) decides whether a partial carries
|
|
1986
|
+
* settled tool work, so the engine stays wire-vocabulary-agnostic.
|
|
1987
|
+
*/
|
|
1988
|
+
private _shouldPersistOrphanedPartial;
|
|
1989
|
+
beginIncident(
|
|
1990
|
+
input: BeginChatRecoveryIncidentInput
|
|
1991
|
+
): Promise<BeginChatRecoveryIncidentResult>;
|
|
1992
|
+
/**
|
|
1993
|
+
* Schedule a recovery continuation/retry: the transition + emit + enqueue
|
|
1994
|
+
* triplet both packages repeat at every fiber-recovery and stall-routing
|
|
1995
|
+
* decision. In order:
|
|
1996
|
+
*
|
|
1997
|
+
* 1. transition the incident to `scheduled` (persist + drive the #1620
|
|
1998
|
+
* "recovering…" status) via {@link updateIncident};
|
|
1999
|
+
* 2. emit `chat:recovery:scheduled`; and
|
|
2000
|
+
* 3. enqueue the callback through the adapter's transport — a Task run on
|
|
2001
|
+
* a root agent, an idempotent schedule on the routed fallback.
|
|
2002
|
+
*
|
|
2003
|
+
* `recoveryKind` is passed explicitly (not read off the incident) because a
|
|
2004
|
+
* caller can legitimately report a different kind than the incident was opened
|
|
2005
|
+
* with — e.g. `AIChatAgent`'s lost-partial branch opens a `continue` incident
|
|
2006
|
+
* but schedules (and reports) a `retry`. `requestId` always matches
|
|
2007
|
+
* `incident.requestId` (the evaluation rewrites it to the current attempt), so
|
|
2008
|
+
* it is read from the incident.
|
|
2009
|
+
*/
|
|
2010
|
+
scheduleRecovery(input: {
|
|
2011
|
+
incident: ChatRecoveryIncident;
|
|
2012
|
+
recoveryKind: ChatRecoveryKind;
|
|
2013
|
+
callback: ChatRecoveryScheduleCallback;
|
|
2014
|
+
data: Record<string, unknown>;
|
|
2015
|
+
reason?: ChatRecoveryScheduleReason;
|
|
2016
|
+
}): Promise<void>;
|
|
2017
|
+
/**
|
|
2018
|
+
* Reschedule a recovery continuation/retry that timed out waiting for stable
|
|
2019
|
+
* state, from INSIDE the currently-executing recovery attempt (a
|
|
2020
|
+
* `__cf_internal_chat_recovery` Task run for a root agent, the routed
|
|
2021
|
+
* one-shot schedule row for a dynamic agent). Reads the
|
|
2022
|
+
* incident; if it is still under the attempt cap, bumps `attempt`, marks it
|
|
2023
|
+
* `scheduled` with `reason:"stable_timeout_retry"`, and issues a separate
|
|
2024
|
+
* delayed attempt. It must not join the currently executing attempt: Tasks
|
|
2025
|
+
* enqueue an unkeyed chained run, while the routed fallback creates a
|
|
2026
|
+
* non-idempotent schedule.
|
|
2027
|
+
*
|
|
2028
|
+
* Returns `true` when a retry was scheduled, `false` when there is no incident
|
|
2029
|
+
* (no id / record gone) or the attempt budget is already spent — in which case
|
|
2030
|
+
* the caller falls through to the give-up path. Deliberately bypasses the
|
|
2031
|
+
* `evaluateChatRecoveryIncident` budget (this is a coarse stable-state retry,
|
|
2032
|
+
* not a fresh interruption) and {@link updateIncident} (no `scheduled` event /
|
|
2033
|
+
* recovering-flag churn on a same-turn reschedule).
|
|
2034
|
+
*/
|
|
2035
|
+
rescheduleAfterStableTimeout(input: {
|
|
2036
|
+
incidentId: string | undefined;
|
|
2037
|
+
callback: ChatRecoveryScheduleCallback;
|
|
2038
|
+
data: Record<string, unknown> | undefined;
|
|
2039
|
+
fallbackMaxAttempts: number;
|
|
2040
|
+
}): Promise<boolean>;
|
|
2041
|
+
/**
|
|
2042
|
+
* Record that a recovery callback observed a Durable Object memory-limit reset
|
|
2043
|
+
* (the isolate exceeded its 128 MB limit — `isDurableObjectMemoryLimitReset`)
|
|
2044
|
+
* and decide what to do next (#1825).
|
|
2045
|
+
*
|
|
2046
|
+
* Bumps the incident's durable `oomAttempts` counter, then:
|
|
2047
|
+
* - if it is still within `maxOomRetries`, issues a delayed, NON-idempotent
|
|
2048
|
+
* reschedule of the SAME callback (same machinery as
|
|
2049
|
+
* {@link rescheduleAfterStableTimeout}: the executing attempt settles
|
|
2050
|
+
* only after the callback returns, so an idempotent reschedule would
|
|
2051
|
+
* dedup onto that doomed attempt) and returns `"rescheduled"`. The small
|
|
2052
|
+
* delay lets a transient memory spike clear before the re-run;
|
|
2053
|
+
* - otherwise leaves the incremented count persisted (so a begin-path
|
|
2054
|
+
* re-evaluation agrees) and returns `"exhausted"` — the caller then
|
|
2055
|
+
* terminalizes via the give-up path with `reason="out_of_memory"`.
|
|
2056
|
+
*
|
|
2057
|
+
* Returns `"exhausted"` when there is no incident to track against (no id /
|
|
2058
|
+
* record gone): an OOM we cannot bound must seal rather than loop. Unlike a
|
|
2059
|
+
* stable-state retry this is gated by the OOM-specific budget, NOT the generic
|
|
2060
|
+
* attempt cap — re-running an OOM streams a little "progress" that would
|
|
2061
|
+
* otherwise reset the attempt cap forever (the #1825 loop).
|
|
2062
|
+
*/
|
|
2063
|
+
recordOomAndDecide(input: {
|
|
2064
|
+
incidentId: string | undefined;
|
|
2065
|
+
callback: ChatRecoveryScheduleCallback;
|
|
2066
|
+
data: Record<string, unknown> | undefined;
|
|
2067
|
+
maxOomRetries: number;
|
|
2068
|
+
}): Promise<"rescheduled" | "exhausted">;
|
|
2069
|
+
/**
|
|
2070
|
+
* Give up on a recovery turn whose retry budget drained, terminalizing it so
|
|
2071
|
+
* it can never become an eternal spinner (#1645). The shared spine both
|
|
2072
|
+
* packages repeated verbatim:
|
|
2073
|
+
*
|
|
2074
|
+
* 1. resolve config + the incident key from `data.incidentId`;
|
|
2075
|
+
* 2. best-effort READ the stored incident — a failed read is tolerated
|
|
2076
|
+
* (reported via `onGiveUpBookkeepingError("read", …)`) and the incident is
|
|
2077
|
+
* synthesized, because the read backs only the re-entry guard, not the
|
|
2078
|
+
* terminal UX;
|
|
2079
|
+
* 3. re-entry guard: a `stored.status === "exhausted"` record means
|
|
2080
|
+
* terminalization already fired, so a duplicate stale alarm returns without
|
|
2081
|
+
* re-broadcasting the banner;
|
|
2082
|
+
* 4. build the exhausted incident (reuse `stored`, or synthesize a minimal one
|
|
2083
|
+
* so a swept/missing record STILL terminalizes through `onExhausted`);
|
|
2084
|
+
* 5. resolve the orphaned stream id + partial;
|
|
2085
|
+
* 6. terminalize via `exhaustChatRecovery` — BEFORE sealing. The terminal
|
|
2086
|
+
* writes can reject with a platform transient in the deploy/storage window
|
|
2087
|
+
* a give-up runs in (#1730); letting that throw propagate is deliberate, so
|
|
2088
|
+
* the current recovery attempt (a Task run on a root agent, a one-shot
|
|
2089
|
+
* schedule row on the routed fallback) defers and the WHOLE give-up
|
|
2090
|
+
* re-runs on a healthy isolate. Sealing first would arm the
|
|
2091
|
+
* re-entry guard and turn that re-run into a no-op, dropping the durable
|
|
2092
|
+
* terminal record. The re-run is idempotent (terminal writes overwrite the
|
|
2093
|
+
* same key); a second banner is the documented at-least-once edge; and
|
|
2094
|
+
* 7. best-effort SEAL write so the re-entry guard sees `exhausted` on a
|
|
2095
|
+
* duplicate alarm — a failed seal (reported via
|
|
2096
|
+
* `onGiveUpBookkeepingError("seal", …)`) costs at most one re-delivered
|
|
2097
|
+
* banner.
|
|
2098
|
+
*
|
|
2099
|
+
* The two packages diverged only in parameters the caller supplies:
|
|
2100
|
+
* `reason` (`Think` passes `stable_timeout` | `recovery_error`; `AIChatAgent`
|
|
2101
|
+
* always `stable_timeout`) and the root-id chain (`Think` includes
|
|
2102
|
+
* `recoveredRequestId`; `AIChatAgent` never sets it, so the unified chain
|
|
2103
|
+
* collapses identically). Exactly-once terminalization rests on the re-entry
|
|
2104
|
+
* guard alone in `AIChatAgent`; `Think` additionally short-circuits duplicate
|
|
2105
|
+
* alarms earlier in its durable-submission layer.
|
|
2106
|
+
*/
|
|
2107
|
+
exhaustRecoveryGiveUp(input: {
|
|
2108
|
+
callback: ChatRecoveryScheduleCallback;
|
|
2109
|
+
data:
|
|
2110
|
+
| {
|
|
2111
|
+
incidentId?: string;
|
|
2112
|
+
originalRequestId?: string;
|
|
2113
|
+
recoveredRequestId?: string;
|
|
2114
|
+
}
|
|
2115
|
+
| undefined;
|
|
2116
|
+
reason: string;
|
|
2117
|
+
}): Promise<void>;
|
|
2118
|
+
/**
|
|
2119
|
+
* Apply a status transition to the recovery incident `incidentId`:
|
|
2120
|
+
*
|
|
2121
|
+
* - `completed` → drop the record (terminal, never retried);
|
|
2122
|
+
* - any other status → persist the new status (and `reason`), so the attempt
|
|
2123
|
+
* budget survives restarts until the TTL sweep reclaims it;
|
|
2124
|
+
* - emit the matching `completed`/`skipped`/`failed` lifecycle event; and
|
|
2125
|
+
* - drive the live "recovering…" status (#1620): `scheduled` marks it active
|
|
2126
|
+
* (keyed by the recovery-root request id), terminal states clear it.
|
|
2127
|
+
*
|
|
2128
|
+
* No-op when `incidentId` is undefined or the record is already gone. This is
|
|
2129
|
+
* the transition twin of {@link beginIncident}: all I/O is adapter-owned, the
|
|
2130
|
+
* engine owns only the state-machine shape.
|
|
2131
|
+
*/
|
|
2132
|
+
updateIncident(
|
|
2133
|
+
incidentId: string | undefined,
|
|
2134
|
+
status: ChatRecoveryIncident["status"],
|
|
2135
|
+
reason?: string
|
|
2136
|
+
): Promise<void>;
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* The complete give-up choreography from a single call: build the exhausted
|
|
2140
|
+
* context, fire the shared notification ({@link notifyChatRecoveryExhausted}),
|
|
2141
|
+
* then hand that context to the host's `terminalize` step. Folds the
|
|
2142
|
+
* `buildChatRecoveryExhaustedContext` → `notifyChatRecoveryExhausted` → host
|
|
2143
|
+
* terminalize sequence that every host's `_exhaustChatRecovery` repeated.
|
|
2144
|
+
*
|
|
2145
|
+
* What this OWNS (the invariant, so it cannot drift per host):
|
|
2146
|
+
* - the notification ALWAYS runs before any terminal write, and
|
|
2147
|
+
* - a throwing `onExhausted` can NEVER block terminal delivery — it is swallowed
|
|
2148
|
+
* via `onError` (a tested invariant in both published packages).
|
|
2149
|
+
*
|
|
2150
|
+
* What it deliberately does NOT own: the terminal-record / broadcast /
|
|
2151
|
+
* recovering-clear writes — their exact set diverges per host (both
|
|
2152
|
+
* `AIChatAgent` and `Think` broadcast the banner first so it survives a storage
|
|
2153
|
+
* write that rejects mid-deploy; `Think` additionally writes a submission row)
|
|
2154
|
+
* — see {@link ChatRecoveryAdapter.exhaustChatRecovery}. The host expresses
|
|
2155
|
+
* those writes inside `terminalize`. A `terminalize` that throws DOES propagate,
|
|
2156
|
+
* so the whole give-up re-runs on a healthy isolate (#1730); see
|
|
2157
|
+
* {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}.
|
|
2158
|
+
*
|
|
2159
|
+
* `partialParts` is passed explicitly (not derived from a `RecoveryPartial`) so a
|
|
2160
|
+
* foreign-vocabulary host can pass `[]` rather than fabricate AI-SDK parts — the
|
|
2161
|
+
* engine seam stays parts-vocabulary-agnostic.
|
|
2162
|
+
*/
|
|
2163
|
+
declare function runChatRecoveryExhaustion(
|
|
2164
|
+
input: {
|
|
2165
|
+
incident: ChatRecoveryIncident;
|
|
2166
|
+
config: ResolvedChatRecoveryConfig;
|
|
2167
|
+
partialText: string;
|
|
2168
|
+
partialParts: ChatRecoveryExhaustedContext["partialParts"];
|
|
2169
|
+
streamId: string;
|
|
2170
|
+
createdAt: number;
|
|
2171
|
+
},
|
|
2172
|
+
hooks: {
|
|
2173
|
+
emit: (ctx: ChatRecoveryExhaustedContext) => void;
|
|
2174
|
+
onExhausted?: (ctx: ChatRecoveryExhaustedContext) => void | Promise<void>;
|
|
2175
|
+
onError: (error: unknown) => void;
|
|
2176
|
+
terminalize: (ctx: ChatRecoveryExhaustedContext) => void | Promise<void>;
|
|
2177
|
+
}
|
|
2178
|
+
): Promise<void>;
|
|
2179
|
+
//#endregion
|
|
2180
|
+
//#region src/chat/recovery-task.d.ts
|
|
2181
|
+
/** Reserved Task definition shared by the chat hosts. */
|
|
2182
|
+
declare const CHAT_RECOVERY_TASK_NAME = "__cf_internal_chat_recovery";
|
|
2183
|
+
/** Input persisted for one recovery continuation attempt. */
|
|
2184
|
+
type ChatRecoveryTaskInput = {
|
|
2185
|
+
/** Host continuation to dispatch. */ readonly callback: ChatRecoveryScheduleCallback /** Continuation context owned by the chat host. */;
|
|
2186
|
+
readonly data: Record<
|
|
2187
|
+
string,
|
|
2188
|
+
unknown
|
|
2189
|
+
> /** Durable delay before dispatch, in seconds. */;
|
|
2190
|
+
readonly delaySeconds: number;
|
|
2191
|
+
};
|
|
2192
|
+
/** Why a recovery attempt is being enqueued. */
|
|
2193
|
+
type ChatRecoveryTaskReason = "initial" | "stable_timeout_retry" | "redefer";
|
|
2194
|
+
/** The host's bounded entry point for each continuation callback. */
|
|
2195
|
+
type ChatRecoveryTaskHooks = Record<
|
|
2196
|
+
ChatRecoveryScheduleCallback,
|
|
2197
|
+
(data: Record<string, unknown>) => Promise<void>
|
|
2198
|
+
>;
|
|
2199
|
+
/** How a bounded recovery callback hands its model turn to the alarm domain. */
|
|
2200
|
+
type ChatRecoveryHandoff = {
|
|
2201
|
+
/**
|
|
2202
|
+
* Start the detached continuation. It calls `onTurnStarted` once the model
|
|
2203
|
+
* turn begins — the point after which the bounded callback returns.
|
|
2204
|
+
*/
|
|
2205
|
+
readonly detached: (
|
|
2206
|
+
onTurnStarted: () => void
|
|
2207
|
+
) => Promise<void> /** Keep the detached turn inside the current alarm's breaker domain. */;
|
|
2208
|
+
readonly track: (turn: Promise<void>) => void;
|
|
2209
|
+
/**
|
|
2210
|
+
* Enqueue exactly one replacement attempt for a detached platform
|
|
2211
|
+
* failure. `dedupeKey` is stable across every retried call for the same
|
|
2212
|
+
* failure (see {@link dispatchChatRecoveryToHandoff}): acceptance may
|
|
2213
|
+
* throw after already durably creating the run — most likely on the
|
|
2214
|
+
* wake-mirror push, not the run insert itself — so a naive retry of an
|
|
2215
|
+
* unkeyed enqueue risks creating another one. Pass it straight through as
|
|
2216
|
+
* the run's `runId` so a retry joins that same row instead.
|
|
2217
|
+
*/
|
|
2218
|
+
readonly redefer: (
|
|
2219
|
+
dedupeKey: string
|
|
2220
|
+
) => Promise<void> /** Report a detached failure the turn's own bookkeeping already handled. */;
|
|
2221
|
+
readonly onDetachedError: (error: unknown) => void;
|
|
2222
|
+
};
|
|
2223
|
+
/**
|
|
2224
|
+
* Run a queue-driven recovery callback up to its model handoff, then return.
|
|
2225
|
+
*
|
|
2226
|
+
* The recovered turn can legitimately run for a long time, and awaiting it
|
|
2227
|
+
* would hold the Lifecycle job loop, starving every other job on the object.
|
|
2228
|
+
* A failure before the handoff rejects here, so the executing Task run (or
|
|
2229
|
+
* compatibility schedule row) keeps ownership and the driver's
|
|
2230
|
+
* platform-failure deferral applies (#1730). After the handoff the turn is
|
|
2231
|
+
* detached alarm work: a platform failure enqueues one replacement attempt,
|
|
2232
|
+
* retried a few times since the completed Task no longer owns this incident
|
|
2233
|
+
* and a failure here would otherwise abandon it silently, and any other
|
|
2234
|
+
* failure belongs to the turn's own incident bookkeeping.
|
|
2235
|
+
*/
|
|
2236
|
+
declare function dispatchChatRecoveryToHandoff(
|
|
2237
|
+
handoff: ChatRecoveryHandoff
|
|
2238
|
+
): Promise<void>;
|
|
2239
|
+
/**
|
|
2240
|
+
* Build run options for one recovery attempt.
|
|
2241
|
+
*
|
|
2242
|
+
* Initial detection joins an existing in-flight attempt for the same incident
|
|
2243
|
+
* and callback. Chained retries are otherwise unkeyed because each one is
|
|
2244
|
+
* enqueued while the preceding run still exists — a genuinely new attempt,
|
|
2245
|
+
* not a retry of this same enqueue. `dedupeKey`, when supplied, keys this
|
|
2246
|
+
* specific enqueue call by `runId` instead: every retry of one failed
|
|
2247
|
+
* `redefer` (see {@link dispatchChatRecoveryToHandoff}) reuses the same key,
|
|
2248
|
+
* so a rejected-but-already-inserted attempt is joined rather than
|
|
2249
|
+
* duplicated. Non-retention releases the initial key when the run settles.
|
|
2250
|
+
*/
|
|
2251
|
+
declare function chatRecoveryTaskRunOptions(
|
|
2252
|
+
input: ChatRecoveryTaskInput,
|
|
2253
|
+
reason: ChatRecoveryTaskReason,
|
|
2254
|
+
dedupeKey?: string
|
|
2255
|
+
): TaskRunOptions;
|
|
2256
|
+
/** Build the shared recovery Task handler for one chat host. */
|
|
2257
|
+
declare function createChatRecoveryTaskDefinition(
|
|
2258
|
+
hooks: ChatRecoveryTaskHooks
|
|
2259
|
+
): (input: unknown, step: TaskStep) => Promise<void>;
|
|
2260
|
+
//#endregion
|
|
2261
|
+
//#region src/chat/sql-batch.d.ts
|
|
2262
|
+
/**
|
|
2263
|
+
* Helpers for building batched SQLite statements that run through the Agent's
|
|
2264
|
+
* `sql` tagged template (which interleaves a `?` placeholder between every
|
|
2265
|
+
* string fragment). Used to collapse per-row INSERT/DELETE loops into a small
|
|
2266
|
+
* number of multi-row statements.
|
|
2267
|
+
*
|
|
2268
|
+
* SQLite (Durable Object / D1) caps bound parameters at 100 per query, so
|
|
2269
|
+
* callers must chunk their inputs to stay within {@link MAX_BOUND_PARAMS}.
|
|
2270
|
+
* See https://developers.cloudflare.com/d1/platform/limits/
|
|
2271
|
+
*/
|
|
2272
|
+
/** Maximum bound parameters allowed in a single SQLite (DO / D1) query. */
|
|
2273
|
+
declare const MAX_BOUND_PARAMS = 100;
|
|
2274
|
+
/**
|
|
2275
|
+
* Build a TemplateStringsArray for a single-column `IN (...)` clause. Produces
|
|
2276
|
+
* fragments for:
|
|
2277
|
+
* `${prefix}(?, ?, ...)`
|
|
2278
|
+
*
|
|
2279
|
+
* @throws if `count` is less than 1.
|
|
2280
|
+
*/
|
|
2281
|
+
declare function buildInClauseStrings(
|
|
2282
|
+
prefix: string,
|
|
2283
|
+
count: number
|
|
2284
|
+
): TemplateStringsArray;
|
|
2285
|
+
//#endregion
|
|
2286
|
+
//#region src/chat/protocol.d.ts
|
|
2287
|
+
/**
|
|
2288
|
+
* Wire protocol message type constants for the cf_agent_chat_* protocol.
|
|
2289
|
+
*
|
|
2290
|
+
* These are the string values used on the wire between agent servers and
|
|
2291
|
+
* clients. Both @cloudflare/ai-chat (via its MessageType enum) and
|
|
2292
|
+
* @cloudflare/think use these values.
|
|
2293
|
+
*/
|
|
2294
|
+
declare const STREAM_RESUME_NONE_REASONS: {
|
|
2295
|
+
/** No active, pending, or terminal stream exists for this agent. */ readonly IDLE: "idle" /** An active tool continuation is owned by another live connection. */;
|
|
2296
|
+
readonly CONTINUATION_OWNED: "continuation-owned";
|
|
2297
|
+
};
|
|
2298
|
+
type StreamResumeNoneReason =
|
|
2299
|
+
(typeof STREAM_RESUME_NONE_REASONS)[keyof typeof STREAM_RESUME_NONE_REASONS];
|
|
2300
|
+
declare const CHAT_MESSAGE_TYPES: {
|
|
2301
|
+
readonly CHAT_MESSAGES: "cf_agent_chat_messages";
|
|
2302
|
+
readonly USE_CHAT_REQUEST: "cf_agent_use_chat_request";
|
|
2303
|
+
readonly USE_CHAT_RESPONSE: "cf_agent_use_chat_response";
|
|
2304
|
+
readonly CHAT_CLEAR: "cf_agent_chat_clear";
|
|
2305
|
+
readonly CHAT_REQUEST_CANCEL: "cf_agent_chat_request_cancel";
|
|
2306
|
+
readonly STREAM_RESUMING: "cf_agent_stream_resuming";
|
|
2307
|
+
readonly STREAM_RESUME_ACK: "cf_agent_stream_resume_ack";
|
|
2308
|
+
readonly STREAM_RESUME_REQUEST: "cf_agent_stream_resume_request";
|
|
2309
|
+
readonly STREAM_RESUME_NONE: "cf_agent_stream_resume_none";
|
|
2310
|
+
readonly STREAM_PENDING: "cf_agent_stream_pending";
|
|
2311
|
+
readonly TOOL_RESULT: "cf_agent_tool_result";
|
|
2312
|
+
readonly TOOL_APPROVAL: "cf_agent_tool_approval";
|
|
2313
|
+
readonly MESSAGE_UPDATED: "cf_agent_message_updated";
|
|
2314
|
+
readonly CHAT_RECOVERING: "cf_agent_chat_recovering";
|
|
2315
|
+
};
|
|
2316
|
+
//#endregion
|
|
2317
|
+
//#region src/chat/wire-types.d.ts
|
|
2318
|
+
/**
|
|
2319
|
+
* Enum for message types to improve type safety and maintainability
|
|
2320
|
+
*/
|
|
2321
|
+
declare enum MessageType {
|
|
2322
|
+
CF_AGENT_CHAT_MESSAGES = "cf_agent_chat_messages",
|
|
2323
|
+
CF_AGENT_USE_CHAT_REQUEST = "cf_agent_use_chat_request",
|
|
2324
|
+
CF_AGENT_USE_CHAT_RESPONSE = "cf_agent_use_chat_response",
|
|
2325
|
+
CF_AGENT_CHAT_CLEAR = "cf_agent_chat_clear",
|
|
2326
|
+
CF_AGENT_CHAT_REQUEST_CANCEL = "cf_agent_chat_request_cancel",
|
|
2327
|
+
/** Sent by server when client connects and there's an active stream to resume */
|
|
2328
|
+
CF_AGENT_STREAM_RESUMING = "cf_agent_stream_resuming",
|
|
2329
|
+
/** Sent by client to acknowledge stream resuming notification and request chunks */
|
|
2330
|
+
CF_AGENT_STREAM_RESUME_ACK = "cf_agent_stream_resume_ack",
|
|
2331
|
+
/** Sent by client after message handler is ready, requesting stream resume check */
|
|
2332
|
+
CF_AGENT_STREAM_RESUME_REQUEST = "cf_agent_stream_resume_request",
|
|
2333
|
+
/** Sent by server when client requests resume but no active stream exists */
|
|
2334
|
+
CF_AGENT_STREAM_RESUME_NONE = "cf_agent_stream_resume_none",
|
|
2335
|
+
/**
|
|
2336
|
+
* Sent by server when a turn is accepted but its resumable stream has not
|
|
2337
|
+
* started yet (queued / debouncing / waiting on MCP / async setup). Tells a
|
|
2338
|
+
* reconnecting client to keep waiting rather than resolve its resume probe to
|
|
2339
|
+
* "no stream". Resolved by a later `CF_AGENT_STREAM_RESUMING` (stream started)
|
|
2340
|
+
* or `CF_AGENT_STREAM_RESUME_NONE` (settled without streaming). See #1784.
|
|
2341
|
+
*/
|
|
2342
|
+
CF_AGENT_STREAM_PENDING = "cf_agent_stream_pending",
|
|
2343
|
+
/** Client sends tool result to server (for client-side tools) */
|
|
2344
|
+
CF_AGENT_TOOL_RESULT = "cf_agent_tool_result",
|
|
2345
|
+
/** Server notifies client that a message was updated (e.g., tool result applied) */
|
|
2346
|
+
CF_AGENT_MESSAGE_UPDATED = "cf_agent_message_updated",
|
|
2347
|
+
/** Client sends tool approval response to server (for tools with needsApproval) */
|
|
2348
|
+
CF_AGENT_TOOL_APPROVAL = "cf_agent_tool_approval",
|
|
2349
|
+
/**
|
|
2350
|
+
* Server→client progress hint: a durable chat turn is being recovered
|
|
2351
|
+
* (interrupted by a deploy/eviction or a stream-stall watchdog abort and now
|
|
2352
|
+
* resuming). Sent when a recovery continuation is scheduled and cleared on
|
|
2353
|
+
* every terminal outcome. (`@cloudflare/think` also replays it on connect;
|
|
2354
|
+
* `@cloudflare/ai-chat` broadcasts the live signal only — see #1645.)
|
|
2355
|
+
* Backward-compatible — clients that don't understand it ignore it. See #1620.
|
|
2356
|
+
*/
|
|
2357
|
+
CF_AGENT_CHAT_RECOVERING = "cf_agent_chat_recovering"
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Types of messages sent from the Agent to clients
|
|
2361
|
+
*/
|
|
2362
|
+
type OutgoingMessage<ChatMessage extends UIMessage = UIMessage> =
|
|
2363
|
+
| {
|
|
2364
|
+
/** Indicates this message is a command to clear chat history */ type: MessageType.CF_AGENT_CHAT_CLEAR;
|
|
2365
|
+
}
|
|
2366
|
+
| {
|
|
2367
|
+
/** Indicates this message contains updated chat messages */ type: MessageType.CF_AGENT_CHAT_MESSAGES /** Array of chat messages */;
|
|
2368
|
+
messages: readonly ChatMessage[];
|
|
2369
|
+
}
|
|
2370
|
+
| {
|
|
2371
|
+
/** Indicates this message is a response to a chat request */ type: MessageType.CF_AGENT_USE_CHAT_RESPONSE /** Unique ID of the request this response corresponds to */;
|
|
2372
|
+
id: string /** Content body of the response */;
|
|
2373
|
+
body: string /** Whether this is the final chunk of the response */;
|
|
2374
|
+
done: boolean /** Whether this response contains an error */;
|
|
2375
|
+
error?: boolean /** Whether this is a continuation (append to last assistant message) */;
|
|
2376
|
+
continuation?: boolean /** Whether this chunk is being replayed from storage (stream resumption) */;
|
|
2377
|
+
replay?: boolean /** Signals that replay of stored chunks is complete (stream is still active) */;
|
|
2378
|
+
replayComplete?: boolean;
|
|
2379
|
+
}
|
|
2380
|
+
| {
|
|
2381
|
+
/** Indicates the server is resuming an active stream */ type: MessageType.CF_AGENT_STREAM_RESUMING /** The request ID of the stream being resumed */;
|
|
2382
|
+
id: string /** Present when this offer directly answers a client resume probe. */;
|
|
2383
|
+
probeId?: string;
|
|
2384
|
+
}
|
|
2385
|
+
| {
|
|
2386
|
+
/** Server notifies client that a message was updated (e.g., tool result applied) */ type: MessageType.CF_AGENT_MESSAGE_UPDATED /** The updated message */;
|
|
2387
|
+
message: ChatMessage;
|
|
2388
|
+
}
|
|
2389
|
+
| {
|
|
2390
|
+
/** Server responds to a resume request with no stream for this client. */ type: MessageType.CF_AGENT_STREAM_RESUME_NONE;
|
|
2391
|
+
/**
|
|
2392
|
+
* Why no stream was offered. Only `idle` proves global inactivity;
|
|
2393
|
+
* omitted by older servers and by non-authoritative delayed releases.
|
|
2394
|
+
*/
|
|
2395
|
+
reason?: StreamResumeNoneReason /** Correlates an authoritative response to its client resume probe. */;
|
|
2396
|
+
probeId?: string;
|
|
2397
|
+
}
|
|
2398
|
+
| {
|
|
2399
|
+
/**
|
|
2400
|
+
* Server signals an accepted turn whose resumable stream has not started
|
|
2401
|
+
* yet — the client should keep waiting for `STREAM_RESUMING` (or a later
|
|
2402
|
+
* `STREAM_RESUME_NONE`) rather than give up. See #1784.
|
|
2403
|
+
*/
|
|
2404
|
+
type: MessageType.CF_AGENT_STREAM_PENDING /** The accepted request id, when known. */;
|
|
2405
|
+
id?: string /** Correlates a direct keep-waiting response to its client probe. */;
|
|
2406
|
+
probeId?: string;
|
|
2407
|
+
}
|
|
2408
|
+
| {
|
|
2409
|
+
/**
|
|
2410
|
+
* Progress hint: a durable chat turn is being recovered (`recovering:
|
|
2411
|
+
* true`) or recovery has resolved (`recovering: false`). Purely advisory;
|
|
2412
|
+
* a client renders a "recovering…" indicator while true.
|
|
2413
|
+
*/
|
|
2414
|
+
type: MessageType.CF_AGENT_CHAT_RECOVERING /** Whether recovery is in progress (true) or has resolved (false). */;
|
|
2415
|
+
recovering: boolean /** The recovery-root request id of the turn being recovered, if known. */;
|
|
2416
|
+
id?: string;
|
|
2417
|
+
};
|
|
2418
|
+
/**
|
|
2419
|
+
* Types of messages sent from clients to the Agent
|
|
2420
|
+
*/
|
|
2421
|
+
type IncomingMessage<ChatMessage extends UIMessage = UIMessage> =
|
|
2422
|
+
| {
|
|
2423
|
+
/** Indicates this message is a command to clear chat history */ type: MessageType.CF_AGENT_CHAT_CLEAR;
|
|
2424
|
+
}
|
|
2425
|
+
| {
|
|
2426
|
+
/** Indicates this message is a request to the chat API */ type: MessageType.CF_AGENT_USE_CHAT_REQUEST /** Unique ID for this request */;
|
|
2427
|
+
id: string /** Request initialization options */;
|
|
2428
|
+
init: Pick<
|
|
2429
|
+
RequestInit,
|
|
2430
|
+
| "method"
|
|
2431
|
+
| "keepalive"
|
|
2432
|
+
| "headers"
|
|
2433
|
+
| "body"
|
|
2434
|
+
| "redirect"
|
|
2435
|
+
| "integrity"
|
|
2436
|
+
| "credentials"
|
|
2437
|
+
| "mode"
|
|
2438
|
+
| "referrer"
|
|
2439
|
+
| "referrerPolicy"
|
|
2440
|
+
| "window"
|
|
2441
|
+
>;
|
|
2442
|
+
}
|
|
2443
|
+
| {
|
|
2444
|
+
/** Indicates this message contains updated chat messages */ type: MessageType.CF_AGENT_CHAT_MESSAGES /** Array of chat messages */;
|
|
2445
|
+
messages: ChatMessage[];
|
|
2446
|
+
}
|
|
2447
|
+
| {
|
|
2448
|
+
/** Indicates the user wants to stop generation of this message */ type: MessageType.CF_AGENT_CHAT_REQUEST_CANCEL;
|
|
2449
|
+
id: string;
|
|
2450
|
+
}
|
|
2451
|
+
| {
|
|
2452
|
+
/** Client acknowledges stream resuming notification and is ready to receive chunks */ type: MessageType.CF_AGENT_STREAM_RESUME_ACK /** The request ID of the stream being resumed */;
|
|
2453
|
+
id: string;
|
|
2454
|
+
}
|
|
2455
|
+
| {
|
|
2456
|
+
/** Client requests stream resume check after message handler is registered */ type: MessageType.CF_AGENT_STREAM_RESUME_REQUEST /** Opaque correlation id echoed by direct server responses. */;
|
|
2457
|
+
probeId?: string;
|
|
2458
|
+
}
|
|
2459
|
+
| {
|
|
2460
|
+
/** Client sends tool result to server (for client-side tools) */ type: MessageType.CF_AGENT_TOOL_RESULT /** The tool call ID this result is for */;
|
|
2461
|
+
toolCallId: string /** The name of the tool */;
|
|
2462
|
+
toolName: string /** The output from the tool execution */;
|
|
2463
|
+
output: unknown /** Override the tool part state (e.g. "output-error" for custom denial) */;
|
|
2464
|
+
state?:
|
|
2465
|
+
| "output-available"
|
|
2466
|
+
| "output-error" /** Error message when state is "output-error" */;
|
|
2467
|
+
errorText?: string /** Whether server should auto-continue the conversation after applying result */;
|
|
2468
|
+
autoContinue?: boolean /** Client tool schemas for continuation (client is source of truth) */;
|
|
2469
|
+
clientTools?: Array<{
|
|
2470
|
+
name: string;
|
|
2471
|
+
description?: string;
|
|
2472
|
+
parameters?: JSONSchema7;
|
|
2473
|
+
}>;
|
|
2474
|
+
}
|
|
2475
|
+
| {
|
|
2476
|
+
/** Client sends tool approval response to server (for tools with needsApproval) */ type: MessageType.CF_AGENT_TOOL_APPROVAL /** The tool call ID this approval is for */;
|
|
2477
|
+
toolCallId: string /** Whether the tool execution was approved */;
|
|
2478
|
+
approved: boolean /** Whether server should auto-continue the conversation after applying approval */;
|
|
2479
|
+
autoContinue?: boolean;
|
|
2480
|
+
};
|
|
2481
|
+
//#endregion
|
|
2482
|
+
//#region src/chat/connection.d.ts
|
|
2483
|
+
/**
|
|
2484
|
+
* Connection I/O — shared WebSocket send guard for chat agents.
|
|
2211
2485
|
*
|
|
2212
|
-
*
|
|
2213
|
-
*
|
|
2214
|
-
*
|
|
2215
|
-
* OOMs mid-stream still credits a little progress before it dies, which resets
|
|
2216
|
-
* BOTH progress-keyed bounds (the attempt cap and the no-progress window) on
|
|
2217
|
-
* every wake — and a fast crash loop (each attempt inside the alarm-debounce
|
|
2218
|
-
* window) pins the attempt counter too. With `maxRecoveryWork = Infinity` the
|
|
2219
|
-
* ONLY instrument whose meter still climbs across such a loop is disabled, so
|
|
2220
|
-
* recovery re-runs the turn (and its LLM calls) forever.
|
|
2486
|
+
* `@internal` — sibling-package support for `@cloudflare/ai-chat` and
|
|
2487
|
+
* `@cloudflare/think`, not a public API. See
|
|
2488
|
+
* `design/rfc-chat-recovery-foundation.md`.
|
|
2221
2489
|
*
|
|
2222
|
-
*
|
|
2223
|
-
*
|
|
2224
|
-
*
|
|
2225
|
-
* bounds wasted re-run cost without clipping a normal interrupted turn (work
|
|
2226
|
-
* only accrues from the first interruption until the turn completes, after which
|
|
2227
|
-
* the incident is deleted). A very long agentic turn under heavy interruption
|
|
2228
|
-
* that legitimately needs more should raise `maxRecoveryWork` (or set it to
|
|
2229
|
-
* `Infinity` to restore the pre-#1825 unbounded behavior).
|
|
2230
|
-
*/
|
|
2231
|
-
declare const DEFAULT_CHAT_RECOVERY_MAX_WORK = 1000;
|
|
2232
|
-
/**
|
|
2233
|
-
* Tight, OOM-specific retry budget (#1825). A Durable Object memory-limit reset
|
|
2234
|
-
* (`isDurableObjectMemoryLimitReset`) is usually deterministic — the turn's
|
|
2235
|
-
* working set no longer fits in the isolate's 128 MB — so re-running it re-OOMs.
|
|
2236
|
-
* But a single OOM CAN be a transient spike (the isolate's 128 MB is shared
|
|
2237
|
-
* across the global scope / noisy neighbors), so recovery retries a small number
|
|
2238
|
-
* of times before sealing with `reason="out_of_memory"` rather than abandoning a
|
|
2239
|
-
* turn that one more attempt might have completed. Far tighter than the generic
|
|
2240
|
-
* `maxRecoveryWork` backstop because an OOM is attributable and re-running it is
|
|
2241
|
-
* expensive (it re-runs the model). Counts attempts that ended in an OOM, not
|
|
2242
|
-
* total attempts, so a turn interrupted by deploys (no OOM) is unaffected.
|
|
2243
|
-
*/
|
|
2244
|
-
declare const DEFAULT_CHAT_RECOVERY_MAX_OOM_RETRIES = 3;
|
|
2245
|
-
declare const DEFAULT_CHAT_RECOVERY_STABLE_TIMEOUT_MS = 10000;
|
|
2246
|
-
/**
|
|
2247
|
-
* Delay before retrying a recovery that timed out waiting for stable state.
|
|
2248
|
-
* Gives an actively-churning isolate (e.g. a deploy in flight) time to settle.
|
|
2249
|
-
*/
|
|
2250
|
-
declare const CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS = 3;
|
|
2251
|
-
declare const DEFAULT_CHAT_RECOVERY_TERMINAL_MESSAGE =
|
|
2252
|
-
"The assistant was interrupted and could not recover. Please try again.";
|
|
2253
|
-
/**
|
|
2254
|
-
* Incidents that have not seen a new attempt within this window are assumed
|
|
2255
|
-
* abandoned and swept so durable storage does not grow without bound.
|
|
2256
|
-
*/
|
|
2257
|
-
declare const CHAT_RECOVERY_INCIDENT_TTL_MS: number;
|
|
2258
|
-
/** Max keys per Durable Object KV `delete([...])` call. */
|
|
2259
|
-
declare const KV_DELETE_MAX_KEYS = 128;
|
|
2260
|
-
/**
|
|
2261
|
-
* PRIMARY recovery bound (#1637): seal an incident that has made no forward
|
|
2262
|
-
* progress for this long. Keyed to `lastProgressAt`, which resets on every
|
|
2263
|
-
* progress-bearing attempt — so a turn that keeps producing content survives
|
|
2264
|
-
* deploy churn indefinitely, while a genuinely stuck turn dies within 5 min.
|
|
2265
|
-
*/
|
|
2266
|
-
declare const DEFAULT_CHAT_RECOVERY_NO_PROGRESS_TIMEOUT_MS: number;
|
|
2267
|
-
/**
|
|
2268
|
-
* Alarm debounce: recovery alarms bunched within this window collapse into a
|
|
2269
|
-
* single attempt. A deploy rollout drops/reconnects the socket several times
|
|
2270
|
-
* over ~11–22s; without this, one logical deploy would burn several attempts.
|
|
2490
|
+
* Both packages (and `continuation-state`) hand-maintained byte-identical
|
|
2491
|
+
* copies of `sendIfOpen` / `isWebSocketClosedSendError`; this is the single
|
|
2492
|
+
* shared implementation.
|
|
2271
2493
|
*/
|
|
2272
|
-
declare const CHAT_RECOVERY_ALARM_DEBOUNCE_MS: number;
|
|
2273
2494
|
/**
|
|
2274
|
-
*
|
|
2275
|
-
*
|
|
2276
|
-
*
|
|
2495
|
+
* Minimal connection interface for sending WebSocket messages. Matches the
|
|
2496
|
+
* `Connection` type from `agents` without importing it: `Connection` extends
|
|
2497
|
+
* `WebSocket` with its own `send` overload, so it is structurally assignable.
|
|
2277
2498
|
*/
|
|
2278
|
-
|
|
2499
|
+
interface ChatConnection {
|
|
2500
|
+
readonly id: string;
|
|
2501
|
+
send(message: string): void;
|
|
2502
|
+
}
|
|
2279
2503
|
/**
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
2282
|
-
* `false`
|
|
2504
|
+
* Send a message on a connection, swallowing the specific
|
|
2505
|
+
* "send after close" error a racing disconnect produces. Returns `true` if the
|
|
2506
|
+
* send went out, `false` if the socket was already closed. Any other error
|
|
2507
|
+
* rethrows.
|
|
2283
2508
|
*/
|
|
2284
|
-
declare function
|
|
2285
|
-
|
|
2286
|
-
|
|
2509
|
+
declare function sendIfOpen(
|
|
2510
|
+
connection: ChatConnection,
|
|
2511
|
+
message: string
|
|
2512
|
+
): boolean;
|
|
2513
|
+
//#endregion
|
|
2514
|
+
//#region src/chat/continuation-state.d.ts
|
|
2287
2515
|
/**
|
|
2288
|
-
*
|
|
2289
|
-
*
|
|
2290
|
-
*
|
|
2291
|
-
* `KV_DELETE_MAX_KEYS` per call, collapsing N awaited round-trips into
|
|
2292
|
-
* ceil(N / 128). Shared by `AIChatAgent` and `Think` so the sweep policy lives in
|
|
2293
|
-
* one place. See `design/rfc-chat-recovery-foundation.md`.
|
|
2516
|
+
* Minimal connection interface for sending WebSocket messages. Alias of the
|
|
2517
|
+
* shared {@link ChatConnection} — kept as a named export for back-compat with
|
|
2518
|
+
* existing `ContinuationConnection` consumers.
|
|
2294
2519
|
*/
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2520
|
+
type ContinuationConnection = ChatConnection;
|
|
2521
|
+
interface ContinuationPending<
|
|
2522
|
+
TConnection extends ContinuationConnection = ContinuationConnection
|
|
2523
|
+
> {
|
|
2524
|
+
connection: TConnection;
|
|
2525
|
+
connectionId: string | null;
|
|
2526
|
+
requestId: string;
|
|
2527
|
+
clientTools?: ClientToolSchema[];
|
|
2528
|
+
body?: Record<string, unknown>;
|
|
2529
|
+
errorPrefix: string | null;
|
|
2530
|
+
prerequisite: Promise<boolean> | null;
|
|
2531
|
+
pastCoalesce: boolean;
|
|
2532
|
+
}
|
|
2533
|
+
interface ContinuationDeferred<
|
|
2534
|
+
TConnection extends ContinuationConnection = ContinuationConnection
|
|
2535
|
+
> {
|
|
2536
|
+
connection: TConnection;
|
|
2537
|
+
connectionId: string | null;
|
|
2538
|
+
clientTools?: ClientToolSchema[];
|
|
2539
|
+
body?: Record<string, unknown>;
|
|
2540
|
+
errorPrefix: string;
|
|
2541
|
+
prerequisite: Promise<boolean> | null;
|
|
2542
|
+
}
|
|
2543
|
+
declare class ContinuationState<
|
|
2544
|
+
TConnection extends ContinuationConnection = ContinuationConnection
|
|
2545
|
+
> {
|
|
2546
|
+
pending: ContinuationPending<TConnection> | null;
|
|
2547
|
+
deferred: ContinuationDeferred<TConnection> | null;
|
|
2548
|
+
activeRequestId: string | null;
|
|
2549
|
+
activeConnectionId: string | null;
|
|
2550
|
+
awaitingConnections: Map<string, TConnection>;
|
|
2551
|
+
/** Clear pending state and awaiting connections (without sending RESUME_NONE). */
|
|
2552
|
+
clearPending(): void;
|
|
2553
|
+
clearDeferred(): void;
|
|
2554
|
+
clearAll(): void;
|
|
2555
|
+
/**
|
|
2556
|
+
* Mark a connection as no longer available without canceling the
|
|
2557
|
+
* continuation it initiated.
|
|
2558
|
+
*/
|
|
2559
|
+
releaseConnection(connectionId: string): void;
|
|
2560
|
+
/**
|
|
2561
|
+
* Send STREAM_RESUME_NONE to all connections waiting for a
|
|
2562
|
+
* continuation stream to start, then clear the map.
|
|
2563
|
+
*/
|
|
2564
|
+
sendResumeNone(): void;
|
|
2565
|
+
/**
|
|
2566
|
+
* Flush awaiting connections by notifying each one via the provided
|
|
2567
|
+
* callback (typically sends STREAM_RESUMING), then clear.
|
|
2568
|
+
*/
|
|
2569
|
+
flushAwaitingConnections(notify: (conn: TConnection) => void): void;
|
|
2570
|
+
/**
|
|
2571
|
+
* Transition pending → active. Called when the continuation stream
|
|
2572
|
+
* actually starts. Moves request/connection IDs to active slots,
|
|
2573
|
+
* clears pending fields.
|
|
2574
|
+
*/
|
|
2575
|
+
activatePending(): void;
|
|
2576
|
+
/**
|
|
2577
|
+
* Transition deferred → pending. Called when a continuation turn
|
|
2578
|
+
* completes and there's a deferred follow-up waiting.
|
|
2579
|
+
*
|
|
2580
|
+
* Returns the new pending state (so the host can enqueue the turn),
|
|
2581
|
+
* or null if there was nothing deferred.
|
|
2582
|
+
*/
|
|
2583
|
+
activateDeferred(
|
|
2584
|
+
generateRequestId: () => string
|
|
2585
|
+
): ContinuationPending<TConnection> | null;
|
|
2586
|
+
}
|
|
2587
|
+
//#endregion
|
|
2588
|
+
//#region src/chat/pre-stream-turns.d.ts
|
|
2589
|
+
declare class PreStreamTurns<
|
|
2590
|
+
TConnection extends ChatConnection = ChatConnection
|
|
2591
|
+
> {
|
|
2592
|
+
/**
|
|
2593
|
+
* Accepted-but-not-yet-streamed request ids. A turn enters on `begin()` and
|
|
2594
|
+
* leaves on `settle()`; the set being non-empty means "pre-stream work is in
|
|
2595
|
+
* flight", which gates parking and the eventual `resume_none` release.
|
|
2596
|
+
*/
|
|
2597
|
+
private readonly _accepted;
|
|
2598
|
+
/** Connections parked waiting for a stream to start. */
|
|
2599
|
+
readonly awaitingConnections: Map<string, TConnection>;
|
|
2600
|
+
/** The most recently accepted pre-stream request id (for the keep-waiting frame). */
|
|
2601
|
+
private _latestRequestId;
|
|
2602
|
+
/** Mark a freshly-accepted turn as in flight (pre-stream). */
|
|
2603
|
+
begin(requestId: string): void;
|
|
2604
|
+
/**
|
|
2605
|
+
* Mark an accepted turn as settled. Returns `true` when no accepted turn
|
|
2606
|
+
* remains in flight (the caller should release parked connections if no
|
|
2607
|
+
* stream is active).
|
|
2608
|
+
*/
|
|
2609
|
+
settle(requestId: string): boolean;
|
|
2610
|
+
/** Whether any accepted turn is still pre-stream. */
|
|
2611
|
+
hasInFlight(): boolean;
|
|
2612
|
+
/** The request id to advertise in the keep-waiting frame, if known. */
|
|
2613
|
+
get latestRequestId(): string | null;
|
|
2614
|
+
/**
|
|
2615
|
+
* Park a reconnecting connection and tell it to keep waiting (so its
|
|
2616
|
+
* transport does not resolve `reconnectToStream` early). No-op when nothing
|
|
2617
|
+
* is in flight. Parked connections are deliberately NOT added to the host's
|
|
2618
|
+
* `pendingResumeConnections` — they must keep receiving any live broadcast —
|
|
2619
|
+
* until the host flushes them through `notifyStreamResuming` on stream start.
|
|
2620
|
+
*/
|
|
2621
|
+
park(connection: TConnection, probeId?: string): boolean;
|
|
2622
|
+
/** Drop a single connection (e.g. on socket close) without releasing others. */
|
|
2623
|
+
release(connectionId: string): void;
|
|
2624
|
+
/**
|
|
2625
|
+
* A stream has started: hand every parked connection to `notify` (the host's
|
|
2626
|
+
* `notifyStreamResuming`, which sends `STREAM_RESUMING` and excludes the
|
|
2627
|
+
* connection from live broadcast until it ACKs), then clear the awaiting map.
|
|
2628
|
+
* The accepted set is untouched — the turn is still running.
|
|
2629
|
+
*/
|
|
2630
|
+
flushOnStreamStart(notify: (connection: TConnection) => void): void;
|
|
2631
|
+
/**
|
|
2632
|
+
* Release every parked connection with `STREAM_RESUME_NONE` (the turn settled
|
|
2633
|
+
* without ever starting a stream) and clear the awaiting map. Safe to call
|
|
2634
|
+
* when the map is empty (no-op), so the host can call it liberally from a
|
|
2635
|
+
* turn-settle path.
|
|
2636
|
+
*/
|
|
2637
|
+
releaseAwaiting(): void;
|
|
2638
|
+
/** Drop all state (chat clear / destroy). Does not send any frames. */
|
|
2639
|
+
reset(): void;
|
|
2640
|
+
}
|
|
2641
|
+
//#endregion
|
|
2642
|
+
//#region src/chat/auto-continuation-controller.d.ts
|
|
2299
2643
|
/**
|
|
2300
|
-
*
|
|
2301
|
-
*
|
|
2302
|
-
*
|
|
2303
|
-
* (
|
|
2304
|
-
*
|
|
2644
|
+
* The data a host supplies to schedule (or re-target) a pending/deferred
|
|
2645
|
+
* auto-continuation. Mirrors the fields a host writes onto
|
|
2646
|
+
* {@link ContinuationState.pending} — the host owns where the values come from
|
|
2647
|
+
* (e.g. Think hardcodes a fixed `errorPrefix` and `body: undefined`; ai-chat
|
|
2648
|
+
* threads them per tool-result event).
|
|
2305
2649
|
*/
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2650
|
+
interface ContinuationSpec<
|
|
2651
|
+
TConnection extends ContinuationConnection = ContinuationConnection
|
|
2652
|
+
> {
|
|
2653
|
+
connection: TConnection;
|
|
2654
|
+
clientTools: ClientToolSchema[] | undefined;
|
|
2655
|
+
body: Record<string, unknown> | undefined;
|
|
2656
|
+
errorPrefix: string;
|
|
2657
|
+
}
|
|
2314
2658
|
/**
|
|
2315
|
-
*
|
|
2316
|
-
*
|
|
2317
|
-
* (detected/scheduled/attempting), else `"failed"` if any terminalized
|
|
2318
|
-
* (exhausted/failed), else `"none"`. In-progress takes precedence so a parent
|
|
2319
|
-
* never gives up on a child that is still recovering. Shared by `AIChatAgent`
|
|
2320
|
-
* and `Think`. See `design/rfc-chat-recovery-foundation.md`.
|
|
2659
|
+
* Host substrate the controller parameterizes over. Implemented by the agent
|
|
2660
|
+
* (typically via a small adapter object capturing `this`).
|
|
2321
2661
|
*/
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2662
|
+
interface AutoContinuationHost<
|
|
2663
|
+
TConnection extends ContinuationConnection = ContinuationConnection
|
|
2664
|
+
> {
|
|
2665
|
+
/** Shared continuation state (pending/deferred/awaiting connections). */
|
|
2666
|
+
readonly continuation: ContinuationState<TConnection>;
|
|
2667
|
+
/** Generate a request id for a freshly-created continuation turn. */
|
|
2668
|
+
generateRequestId(): string;
|
|
2669
|
+
/**
|
|
2670
|
+
* `true` while an assistant turn is streaming — the parallel tool batch can
|
|
2671
|
+
* still grow with tool calls the model hasn't emitted yet, so no completeness
|
|
2672
|
+
* check is meaningful. (`_streamingAssistant !== null` in Think;
|
|
2673
|
+
* `_streamingTurnActive` in ai-chat.)
|
|
2674
|
+
*/
|
|
2675
|
+
isStreamActive(): boolean;
|
|
2676
|
+
/** `true` while a tool-result/approval apply is in flight. */
|
|
2677
|
+
hasPendingInteraction(): boolean;
|
|
2678
|
+
/**
|
|
2679
|
+
* `true` when the latest assistant message is mid-batch (a settled tool
|
|
2680
|
+
* result beside an unanswered tool call/approval — the #1649 signature).
|
|
2681
|
+
*/
|
|
2682
|
+
hasIncompleteToolBatch(): boolean;
|
|
2683
|
+
/**
|
|
2684
|
+
* Drain every in-flight tool-result/approval apply (including any enqueued
|
|
2685
|
+
* while draining) so the subsequent completeness re-check sees every result
|
|
2686
|
+
* that has already arrived. Bounded by real apply activity, never a timer.
|
|
2687
|
+
*/
|
|
2688
|
+
drainInteractionApplies(): Promise<void>;
|
|
2689
|
+
/** Hold the isolate alive for the duration of `fn` (alarm heartbeats). */
|
|
2690
|
+
keepAliveWhile<T>(fn: () => Promise<T>): Promise<T>;
|
|
2691
|
+
/**
|
|
2692
|
+
* Run the continuation turn for the current {@link ContinuationState.pending}.
|
|
2693
|
+
* Each host's inference/reply pipeline (Think: `_turnQueue.enqueue` +
|
|
2694
|
+
* `_runInferenceLoop`; ai-chat: `_runExclusiveChatTurn` + `onChatMessage`).
|
|
2695
|
+
* Reads everything it needs from `continuation.pending`, so it takes no args.
|
|
2696
|
+
*/
|
|
2697
|
+
fire(): void;
|
|
2698
|
+
}
|
|
2699
|
+
declare class AutoContinuationController<
|
|
2700
|
+
TConnection extends ContinuationConnection = ContinuationConnection
|
|
2701
|
+
> {
|
|
2702
|
+
private readonly host;
|
|
2703
|
+
/**
|
|
2704
|
+
* Small debounce window to batch adjacent client-side tool results/approvals
|
|
2705
|
+
* into a single server continuation barrier check (#1650).
|
|
2706
|
+
*/
|
|
2707
|
+
static readonly COALESCE_MS = 50;
|
|
2708
|
+
/**
|
|
2709
|
+
* Coalesce/debounce timer for the event-driven barrier (#1650). Each tool
|
|
2710
|
+
* result/approval re-arms it; on fire it runs {@link fireWhenStable}.
|
|
2711
|
+
*/
|
|
2712
|
+
private _timer;
|
|
2713
|
+
/**
|
|
2714
|
+
* Double-fire guard (#1650). Ensures only one in-flight apply-drain runs;
|
|
2715
|
+
* that drain re-checks completeness on completion before firing. A sibling
|
|
2716
|
+
* that re-arms the coalesce timer during a drain is absorbed by the
|
|
2717
|
+
* in-progress drain rather than starting its own.
|
|
2718
|
+
*/
|
|
2719
|
+
private _barrierActive;
|
|
2720
|
+
constructor(host: AutoContinuationHost<TConnection>);
|
|
2721
|
+
/**
|
|
2722
|
+
* Schedule an auto-continuation for a tool result/approval that opted in with
|
|
2723
|
+
* `autoContinue` (#1650). Coalesces rapid sibling results into a single
|
|
2724
|
+
* continuation via the debounce timer; the actual fire is gated by
|
|
2725
|
+
* {@link fireWhenStable}. If a continuation is already running
|
|
2726
|
+
* (`pastCoalesce`), the new result is stored as the deferred follow-up
|
|
2727
|
+
* instead of re-arming.
|
|
2728
|
+
*/
|
|
2729
|
+
schedule(spec: ContinuationSpec<TConnection>): void;
|
|
2730
|
+
/**
|
|
2731
|
+
* Re-arm the barrier for a result/approval that arrived WITHOUT `autoContinue`
|
|
2732
|
+
* (#1650). A standalone errored result declines to continue on its own, but in
|
|
2733
|
+
* a parallel batch a SIBLING may already have opted in — and this result can
|
|
2734
|
+
* be the one that completes the batch, so we must re-run the barrier check.
|
|
2735
|
+
* Unlike {@link schedule} this NEVER creates a pending continuation, and
|
|
2736
|
+
* no-ops once the continuation is running (`pastCoalesce`).
|
|
2737
|
+
*/
|
|
2738
|
+
rearmForBatch(): void;
|
|
2739
|
+
/** (Re)arm the coalesce timer; on fire, run {@link fireWhenStable}. */
|
|
2740
|
+
armTimer(): void;
|
|
2741
|
+
/**
|
|
2742
|
+
* Fire an auto-continuation, but only once the model's parallel tool-call
|
|
2743
|
+
* batch is fully answered (#1649) and no assistant turn is mid-stream (#1650).
|
|
2744
|
+
* The barrier is event-driven with NO orphan timeout: when the batch is still
|
|
2745
|
+
* incomplete we drain the in-flight applies, re-check, and — if still
|
|
2746
|
+
* incomplete — return WITHOUT firing and WITHOUT holding the isolate, leaving
|
|
2747
|
+
* `continuation.pending` in place. The next sibling's result re-arms the
|
|
2748
|
+
* coalesce timer and re-runs this check; the continuation fires once the final
|
|
2749
|
+
* sibling lands. A true orphan (a sibling that never arrives) simply never
|
|
2750
|
+
* auto-continues — a later user turn / chat recovery repairs the transcript.
|
|
2751
|
+
*/
|
|
2752
|
+
fireWhenStable(): void;
|
|
2753
|
+
/**
|
|
2754
|
+
* Transition the deferred follow-up (stored while a continuation was running)
|
|
2755
|
+
* to pending and re-run the barrier — its batch may still be incomplete (or a
|
|
2756
|
+
* stream active), in which case it parks and re-arms instead of firing blind.
|
|
2757
|
+
*/
|
|
2758
|
+
activateDeferredAndReschedule(): void;
|
|
2759
|
+
/**
|
|
2760
|
+
* Cancel any still-armed coalesce timer. Called on the fire path so a sibling
|
|
2761
|
+
* result that re-armed it during a barrier wait can't fire a duplicate
|
|
2762
|
+
* continuation after this one starts (#1649 / #1650).
|
|
2763
|
+
*/
|
|
2764
|
+
cancelTimer(): void;
|
|
2765
|
+
/**
|
|
2766
|
+
* `true` when the barrier is going to fire on its own — its coalesce timer is
|
|
2767
|
+
* still pending or its completeness drain is in progress. The host combines
|
|
2768
|
+
* this with its own pending/`pastCoalesce` checks to decide idle/stable.
|
|
2769
|
+
*/
|
|
2770
|
+
isArmed(): boolean;
|
|
2771
|
+
/**
|
|
2772
|
+
* Tear down the controller-owned barrier state (timer + double-fire guard).
|
|
2773
|
+
* Scoped to ONLY this controller's fields — the host clears the rest of its
|
|
2774
|
+
* turn state (stream gate, interaction tail, continuation data) separately.
|
|
2775
|
+
*/
|
|
2776
|
+
reset(): void;
|
|
2777
|
+
}
|
|
2778
|
+
//#endregion
|
|
2779
|
+
//#region src/chat/abort-registry.d.ts
|
|
2325
2780
|
/**
|
|
2326
|
-
*
|
|
2327
|
-
*
|
|
2781
|
+
* AbortRegistry — manages per-request AbortControllers.
|
|
2782
|
+
*
|
|
2783
|
+
* Shared between AIChatAgent and Think for chat turn cancellation.
|
|
2784
|
+
* Each request gets its own AbortController keyed by request ID.
|
|
2785
|
+
* Controllers are created lazily on first signal access.
|
|
2328
2786
|
*/
|
|
2329
|
-
declare
|
|
2330
|
-
|
|
2331
|
-
|
|
2787
|
+
declare class AbortRegistry {
|
|
2788
|
+
private controllers;
|
|
2789
|
+
/**
|
|
2790
|
+
* Get or create an AbortController for the given ID and return its signal.
|
|
2791
|
+
* Creates the controller lazily on first access.
|
|
2792
|
+
*/
|
|
2793
|
+
getSignal(id: string): AbortSignal | undefined;
|
|
2794
|
+
/**
|
|
2795
|
+
* Get the signal for an existing controller without creating one.
|
|
2796
|
+
* Returns undefined if no controller exists for this ID.
|
|
2797
|
+
*/
|
|
2798
|
+
getExistingSignal(id: string): AbortSignal | undefined;
|
|
2799
|
+
/**
|
|
2800
|
+
* Cancel a specific request by aborting its controller. Optionally
|
|
2801
|
+
* propagate a reason — surfaces as `signal.reason` on the registry's
|
|
2802
|
+
* controller and through any `AbortError` it produces downstream.
|
|
2803
|
+
*/
|
|
2804
|
+
cancel(id: string, reason?: unknown): void;
|
|
2805
|
+
/** Remove a controller after the request completes. */
|
|
2806
|
+
remove(id: string): void;
|
|
2807
|
+
/**
|
|
2808
|
+
* Abort all pending requests and clear the registry. Optionally propagate a
|
|
2809
|
+
* reason — surfaces as `signal.reason` on each controller and through any
|
|
2810
|
+
* `AbortError` it produces downstream, exactly like {@link cancel}.
|
|
2811
|
+
*/
|
|
2812
|
+
destroyAll(reason?: unknown): void;
|
|
2813
|
+
/** Check if a controller exists for the given ID. */
|
|
2814
|
+
has(id: string): boolean;
|
|
2815
|
+
/** Number of tracked controllers. */
|
|
2816
|
+
get size(): number;
|
|
2817
|
+
/**
|
|
2818
|
+
* Link an external `AbortSignal` to the controller for `id`. When the
|
|
2819
|
+
* external signal aborts, the registry's controller is cancelled —
|
|
2820
|
+
* propagating the abort reason — exactly the same way an internal
|
|
2821
|
+
* cancel would (e.g. via a `chat-request-cancel` WebSocket message).
|
|
2822
|
+
*
|
|
2823
|
+
* This is the integration point for callers that drive a chat turn
|
|
2824
|
+
* programmatically and want to cancel it from outside without knowing
|
|
2825
|
+
* the internally-generated request id (e.g. the helper-as-sub-agent
|
|
2826
|
+
* pattern, where a parent's `AbortSignal` from the AI SDK tool
|
|
2827
|
+
* `execute` needs to land inside a `Think.saveMessages` call running
|
|
2828
|
+
* on a child DO).
|
|
2829
|
+
*
|
|
2830
|
+
* Behavior:
|
|
2831
|
+
*
|
|
2832
|
+
* - Passing `undefined` is a no-op and returns a no-op detacher, so
|
|
2833
|
+
* callers can unconditionally call this with `options?.signal`.
|
|
2834
|
+
* - If the external signal is already aborted, the registry's
|
|
2835
|
+
* controller is created (if needed) and cancelled synchronously.
|
|
2836
|
+
* - Otherwise a one-shot `abort` listener is attached. The returned
|
|
2837
|
+
* function detaches it.
|
|
2838
|
+
*
|
|
2839
|
+
* **Always call the returned detacher in a `finally` block** — the
|
|
2840
|
+
* external signal may outlive the request (a parent chat turn that
|
|
2841
|
+
* drives many helper turns reuses one signal across all of them) and
|
|
2842
|
+
* leaving listeners attached pins closures and grows the listener
|
|
2843
|
+
* list on each turn.
|
|
2844
|
+
*
|
|
2845
|
+
* @returns A detacher function. Call it after the request finishes
|
|
2846
|
+
* (success or failure) to remove the abort listener from `signal`.
|
|
2847
|
+
*/
|
|
2848
|
+
linkExternal(id: string, signal: AbortSignal | undefined): () => void;
|
|
2849
|
+
}
|
|
2850
|
+
//#endregion
|
|
2851
|
+
//#region src/chat/async-helpers.d.ts
|
|
2332
2852
|
/**
|
|
2333
|
-
*
|
|
2334
|
-
*
|
|
2335
|
-
*
|
|
2853
|
+
* @internal Small async control-flow helpers shared by the chat hosts
|
|
2854
|
+
* (`@cloudflare/ai-chat` and `@cloudflare/think`) — not a public API. Extracted
|
|
2855
|
+
* so the host idle/stable waits and the interaction-apply completeness drain
|
|
2856
|
+
* stay byte-identical across both. See `design/chat-shared-layer.md`.
|
|
2336
2857
|
*/
|
|
2337
|
-
declare function bumpChatRecoveryProgress(
|
|
2338
|
-
storage: Pick<DurableObjectStorage, "get" | "put">
|
|
2339
|
-
): Promise<void>;
|
|
2340
2858
|
/**
|
|
2341
|
-
*
|
|
2342
|
-
*
|
|
2343
|
-
*
|
|
2344
|
-
* token.
|
|
2859
|
+
* Sentinel returned by {@link awaitWithDeadline} when the deadline elapses
|
|
2860
|
+
* before the awaited promise settles. A single shared symbol so both hosts
|
|
2861
|
+
* compare against the same identity.
|
|
2345
2862
|
*/
|
|
2346
|
-
declare const
|
|
2863
|
+
declare const TIMED_OUT: unique symbol;
|
|
2347
2864
|
/**
|
|
2348
|
-
*
|
|
2349
|
-
* `
|
|
2350
|
-
*
|
|
2351
|
-
*
|
|
2352
|
-
* records the time on each credit. Shared by `AIChatAgent` and `Think`.
|
|
2865
|
+
* Await `promise`, but give up and resolve to {@link TIMED_OUT} once `deadline`
|
|
2866
|
+
* (an absolute `Date.now()` ms timestamp) passes. A `null` deadline waits
|
|
2867
|
+
* indefinitely (the promise is returned unchanged). The timeout timer is always
|
|
2868
|
+
* cleared so it can't pin the isolate awake past resolution.
|
|
2353
2869
|
*/
|
|
2354
|
-
declare
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2870
|
+
declare function awaitWithDeadline<T>(
|
|
2871
|
+
promise: Promise<T>,
|
|
2872
|
+
deadline: number | null
|
|
2873
|
+
): Promise<T | typeof TIMED_OUT>;
|
|
2358
2874
|
/**
|
|
2359
|
-
*
|
|
2360
|
-
*
|
|
2361
|
-
*
|
|
2362
|
-
*
|
|
2363
|
-
*
|
|
2364
|
-
*
|
|
2875
|
+
* Drain the host's interaction-apply chain so a subsequent completeness check
|
|
2876
|
+
* (e.g. `hasIncompleteToolBatch`) sees every tool result that has ALREADY
|
|
2877
|
+
* arrived.
|
|
2878
|
+
*
|
|
2879
|
+
* Bounded by real apply activity (a storage write each), never a fixed timer:
|
|
2880
|
+
* `getTail` is re-read after every await because a sibling can extend the tail
|
|
2881
|
+
* mid-drain, and the loop stops once the tail stops advancing. Bails early when
|
|
2882
|
+
* `hasPending()` goes false (the pending continuation was cleared by a chat
|
|
2883
|
+
* clear / turn reset) so a stale drain can't hold the isolate awake.
|
|
2884
|
+
*/
|
|
2885
|
+
declare function drainInteractionApplies(
|
|
2886
|
+
hasPending: () => boolean,
|
|
2887
|
+
getTail: () => Promise<unknown>
|
|
2888
|
+
): Promise<void>;
|
|
2889
|
+
//#endregion
|
|
2890
|
+
//#region src/chat/tool-state.d.ts
|
|
2891
|
+
/**
|
|
2892
|
+
* Tool State — shared update builders and applicator for tool part state changes.
|
|
2893
|
+
*
|
|
2894
|
+
* Used by both AIChatAgent and Think to apply tool results and approvals
|
|
2895
|
+
* to message parts. Each agent handles find-message, persist, and broadcast
|
|
2896
|
+
* in their own way; this module provides the state matching and update logic.
|
|
2365
2897
|
*/
|
|
2366
|
-
declare const CHAT_STREAM_PROGRESS_CREDIT_THROTTLE_MS = 5000;
|
|
2367
2898
|
/**
|
|
2368
|
-
*
|
|
2369
|
-
* streaming-content chunks — the delta arm of {@link shouldCreditStreamProgress}.
|
|
2370
|
-
* The `_lastBumpAt` clock is in-memory, so it resets per isolate and the first
|
|
2371
|
-
* delta after a restart always credits. Shared by `AIChatAgent` and `Think`.
|
|
2899
|
+
* Describes an update to apply to a tool part.
|
|
2372
2900
|
*/
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
/** Durable record of the last turn that ended in a terminal error (#1645). */
|
|
2378
|
-
type ChatTerminalRecord = {
|
|
2379
|
-
requestId: string;
|
|
2380
|
-
body: string;
|
|
2901
|
+
type ToolPartUpdate = {
|
|
2902
|
+
toolCallId: string;
|
|
2903
|
+
matchStates: string[];
|
|
2904
|
+
apply: (part: Record<string, unknown>) => Record<string, unknown>;
|
|
2381
2905
|
};
|
|
2382
2906
|
/**
|
|
2383
|
-
*
|
|
2384
|
-
*
|
|
2385
|
-
*
|
|
2386
|
-
*
|
|
2907
|
+
* Apply a tool part update to a parts array.
|
|
2908
|
+
* Finds the first part matching `update.toolCallId` in one of `update.matchStates`,
|
|
2909
|
+
* applies the update immutably, and returns the new parts array with the index.
|
|
2910
|
+
*
|
|
2911
|
+
* Returns `null` if no matching part was found.
|
|
2387
2912
|
*/
|
|
2388
|
-
declare function
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
storage: Pick<DurableObjectStorage, "delete">
|
|
2396
|
-
): Promise<void>;
|
|
2397
|
-
/** Read the pending terminal record, or `null` if none is stored (#1645). */
|
|
2398
|
-
declare function pendingChatTerminal(
|
|
2399
|
-
storage: Pick<DurableObjectStorage, "get">
|
|
2400
|
-
): Promise<ChatTerminalRecord | null>;
|
|
2913
|
+
declare function applyToolUpdate(
|
|
2914
|
+
parts: Array<Record<string, unknown>>,
|
|
2915
|
+
update: ToolPartUpdate
|
|
2916
|
+
): {
|
|
2917
|
+
parts: Array<Record<string, unknown>>;
|
|
2918
|
+
index: number;
|
|
2919
|
+
} | null;
|
|
2401
2920
|
/**
|
|
2402
|
-
* Build
|
|
2403
|
-
*
|
|
2404
|
-
*
|
|
2405
|
-
*
|
|
2406
|
-
* never ran) and skipped, so a dead recovery can't show "recovering…" forever.
|
|
2407
|
-
* `messageType` is the package's recovering wire-type enum.
|
|
2921
|
+
* Build an update descriptor for applying a tool result.
|
|
2922
|
+
*
|
|
2923
|
+
* Matches parts in `input-available`, `approval-requested`, or `approval-responded` state.
|
|
2924
|
+
* Sets state to `output-available` (with output) or `output-error` (with errorText).
|
|
2408
2925
|
*/
|
|
2409
|
-
declare function
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2926
|
+
declare function toolResultUpdate(
|
|
2927
|
+
toolCallId: string,
|
|
2928
|
+
output: unknown,
|
|
2929
|
+
overrideState?: "output-error",
|
|
2930
|
+
errorText?: string
|
|
2931
|
+
): ToolPartUpdate;
|
|
2414
2932
|
/**
|
|
2415
|
-
*
|
|
2416
|
-
*
|
|
2417
|
-
*
|
|
2418
|
-
*
|
|
2419
|
-
*
|
|
2420
|
-
*
|
|
2421
|
-
*
|
|
2422
|
-
*
|
|
2423
|
-
*
|
|
2424
|
-
*
|
|
2933
|
+
* Build an update descriptor for a terminal tool result that belongs to a
|
|
2934
|
+
* tool part in a *different* (earlier) assistant message than the one
|
|
2935
|
+
* currently being streamed.
|
|
2936
|
+
*
|
|
2937
|
+
* This is the "cross-message" case: an approved server tool executes during a
|
|
2938
|
+
* continuation stream, but its tool part lives in the assistant message that
|
|
2939
|
+
* originally requested it. `StreamAccumulator` surfaces this as a
|
|
2940
|
+
* `cross-message-tool-update` action because the accumulator only owns the
|
|
2941
|
+
* current turn's new content and cannot mutate a part from a prior message.
|
|
2942
|
+
*
|
|
2943
|
+
* Compared to {@link toolResultUpdate} this builder is deliberately more
|
|
2944
|
+
* defensive, mirroring the equivalent fallback in `@cloudflare/ai-chat`:
|
|
2945
|
+
*
|
|
2946
|
+
* - It matches the broad set of pre-terminal **and** terminal states, so a
|
|
2947
|
+
* provider that replays the entire prior tool round-trip during a
|
|
2948
|
+
* continuation (notably the OpenAI Responses API — issue #1404) still
|
|
2949
|
+
* resolves to the same part instead of silently missing it.
|
|
2950
|
+
* - It is **first-write-wins**: a chunk arriving for a tool that already holds
|
|
2951
|
+
* a terminal result is treated as a replay and the existing output is never
|
|
2952
|
+
* overwritten. In that case `apply` returns the *same part reference*, which
|
|
2953
|
+
* callers use as an idempotent-no-op signal to skip the durable write and a
|
|
2954
|
+
* redundant `MESSAGE_UPDATED` broadcast.
|
|
2955
|
+
* - It preserves a streamed `preliminary` flag when one is present, otherwise
|
|
2956
|
+
* marks the result final (`preliminary: false`).
|
|
2425
2957
|
*/
|
|
2426
|
-
declare function
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
now: number;
|
|
2434
|
-
}
|
|
2435
|
-
): Promise<void>;
|
|
2958
|
+
declare function crossMessageToolResultUpdate(
|
|
2959
|
+
toolCallId: string,
|
|
2960
|
+
updateType: "output-available" | "output-error",
|
|
2961
|
+
output?: unknown,
|
|
2962
|
+
errorText?: string,
|
|
2963
|
+
preliminary?: boolean
|
|
2964
|
+
): ToolPartUpdate;
|
|
2436
2965
|
/**
|
|
2437
|
-
*
|
|
2438
|
-
*
|
|
2439
|
-
*
|
|
2440
|
-
*
|
|
2441
|
-
*
|
|
2442
|
-
*
|
|
2966
|
+
* Build an update descriptor that replaces the output of a *paused durable
|
|
2967
|
+
* execution* tool part (e.g. a codemode runtime tool that paused for
|
|
2968
|
+
* approval).
|
|
2969
|
+
*
|
|
2970
|
+
* A paused execution completes its tool call normally — the part is already
|
|
2971
|
+
* `output-available` with an output of `{ status: "paused", executionId }`.
|
|
2972
|
+
* When the host later approves/rejects the execution, the new outcome
|
|
2973
|
+
* (completed / rejected / paused-again) must replace that output in place.
|
|
2974
|
+
*
|
|
2975
|
+
* Matching is deliberately narrow and idempotent:
|
|
2976
|
+
*
|
|
2977
|
+
* - only `output-available` parts are considered;
|
|
2978
|
+
* - the existing output must be a paused-execution object carrying the same
|
|
2979
|
+
* `executionId` — anything else (already replaced, different execution)
|
|
2980
|
+
* returns the *same part reference*, which callers treat as a no-op signal
|
|
2981
|
+
* (skip persist + broadcast), mirroring {@link crossMessageToolResultUpdate}.
|
|
2443
2982
|
*/
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
| "chat:recovery:completed"
|
|
2450
|
-
| "chat:recovery:skipped"
|
|
2451
|
-
| "chat:recovery:failed";
|
|
2452
|
-
incidentId: string;
|
|
2453
|
-
requestId: string;
|
|
2454
|
-
attempt: number;
|
|
2455
|
-
maxAttempts: number;
|
|
2456
|
-
recoveryKind: ChatRecoveryKind;
|
|
2457
|
-
reason?: string;
|
|
2458
|
-
};
|
|
2459
|
-
type EvaluateChatRecoveryIncidentInput = {
|
|
2460
|
-
/** Recovery identity for this turn. */ identity: {
|
|
2461
|
-
requestId: string;
|
|
2462
|
-
recoveryRootRequestId?: string | null;
|
|
2463
|
-
latestUserMessageId?: string | null;
|
|
2464
|
-
recoveryKind: ChatRecoveryKind;
|
|
2465
|
-
} /** Fully-resolved recovery config. */;
|
|
2466
|
-
config: ResolvedChatRecoveryConfig /** The existing incident for this identity, or `null` if this is fresh. */;
|
|
2467
|
-
existing: ChatRecoveryIncident | null /** Current value of the durable monotonic progress counter. */;
|
|
2468
|
-
currentProgress: number;
|
|
2469
|
-
/**
|
|
2470
|
-
* Whether the turn is parked on a pending CLIENT interaction (an
|
|
2471
|
-
* `input-available` client-tool part or an `approval-requested` part). Such a
|
|
2472
|
-
* turn is waiting on the human, not stuck, so it is budget-free.
|
|
2473
|
-
*/
|
|
2474
|
-
awaitingClientInteraction: boolean /** Injected clock (epoch ms) for deterministic tests. */;
|
|
2475
|
-
now: number;
|
|
2476
|
-
/**
|
|
2477
|
-
* Invoked when `config.shouldKeepRecovering` throws. Lets each package keep
|
|
2478
|
-
* its own log prefix. A throwing predicate is treated as "keep recovering".
|
|
2479
|
-
*/
|
|
2480
|
-
onShouldKeepRecoveringError?: (error: unknown) => void;
|
|
2481
|
-
};
|
|
2482
|
-
type EvaluateChatRecoveryIncidentResult = {
|
|
2483
|
-
/** The next incident record to persist. */ incident: ChatRecoveryIncident /** Whether this incident is now sealed as exhausted. */;
|
|
2484
|
-
exhausted: boolean /** Observability events to emit, in order. */;
|
|
2485
|
-
events: ChatRecoveryIncidentEvent[];
|
|
2486
|
-
};
|
|
2487
|
-
//#endregion
|
|
2488
|
-
//#region src/chat/recovery-engine.d.ts
|
|
2489
|
-
/** The scheduled-callback entrypoints a recovery schedule can target. */
|
|
2490
|
-
type ChatRecoveryScheduleCallback =
|
|
2491
|
-
| "_chatRecoveryContinue"
|
|
2492
|
-
| "_chatRecoveryRetry";
|
|
2983
|
+
declare function pausedExecutionUpdate(
|
|
2984
|
+
toolCallId: string,
|
|
2985
|
+
executionId: string,
|
|
2986
|
+
output: unknown
|
|
2987
|
+
): ToolPartUpdate;
|
|
2493
2988
|
/**
|
|
2494
|
-
*
|
|
2495
|
-
* `schedule()` call depends ONLY on this:
|
|
2496
|
-
*
|
|
2497
|
-
* - `"initial"` — the first schedule of a continuation/retry when an interrupted
|
|
2498
|
-
* turn is detected on wake. A deploy rollout drops/reconnects the socket
|
|
2499
|
-
* several times, re-triggering detection; idempotent scheduling (dedup on
|
|
2500
|
-
* callback + payload) collapses that storm into a single enqueued continuation
|
|
2501
|
-
* instead of N duplicates.
|
|
2989
|
+
* Build an update descriptor for applying a tool approval.
|
|
2502
2990
|
*
|
|
2503
|
-
*
|
|
2504
|
-
*
|
|
2505
|
-
* stable state). `alarm()` deletes that row only AFTER the callback returns,
|
|
2506
|
-
* so an idempotent reschedule would dedup onto the doomed row and be deleted
|
|
2507
|
-
* with it — the retry would never fire. A fresh (non-idempotent) delayed row
|
|
2508
|
-
* survives the deletion.
|
|
2991
|
+
* Matches parts in `input-available` or `approval-requested` state.
|
|
2992
|
+
* Sets state to `approval-responded` (if approved) or `output-denied` (if denied).
|
|
2509
2993
|
*/
|
|
2510
|
-
|
|
2994
|
+
declare function toolApprovalUpdate(
|
|
2995
|
+
toolCallId: string,
|
|
2996
|
+
approved: boolean
|
|
2997
|
+
): ToolPartUpdate;
|
|
2998
|
+
/** A minimal message shape for the leaf tool/interaction scans. */
|
|
2999
|
+
type ToolBatchMessage = {
|
|
3000
|
+
role: string;
|
|
3001
|
+
parts: ReadonlyArray<unknown>;
|
|
3002
|
+
};
|
|
3003
|
+
/** Extract a tool part's name from its `tool-<name>` / `dynamic-tool` shape. */
|
|
2511
3004
|
/**
|
|
2512
|
-
*
|
|
2513
|
-
*
|
|
2514
|
-
*
|
|
2515
|
-
*
|
|
2516
|
-
*
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
3005
|
+
* Whether a part is still awaiting a CLIENT interaction that can genuinely
|
|
3006
|
+
* arrive after a restart: an `approval-requested` part (a reconnecting client
|
|
3007
|
+
* replays the approval) or an `input-available` part for a CLIENT tool (the SPA
|
|
3008
|
+
* replays the `tool-result`). A SERVER tool's `input-available` is NOT pending —
|
|
3009
|
+
* its `execute()` died with the isolate.
|
|
3010
|
+
*/
|
|
3011
|
+
declare function partAwaitsClientInteraction(
|
|
3012
|
+
part: unknown,
|
|
3013
|
+
clientResolvable: Set<string>
|
|
3014
|
+
): boolean;
|
|
3015
|
+
/**
|
|
3016
|
+
* Names of the CLIENT-resolvable tools — the client-provided schemas from the
|
|
3017
|
+
* last request, which have no server `execute`. An interrupted `input-available`
|
|
3018
|
+
* part for one of these can still be resolved by the client replaying a
|
|
3019
|
+
* `tool-result`; a server tool's cannot.
|
|
3020
|
+
*/
|
|
3021
|
+
declare function clientResolvableToolNames(
|
|
3022
|
+
tools:
|
|
3023
|
+
| ReadonlyArray<
|
|
3024
|
+
| {
|
|
3025
|
+
name?: string;
|
|
3026
|
+
}
|
|
3027
|
+
| null
|
|
3028
|
+
| undefined
|
|
3029
|
+
>
|
|
3030
|
+
| undefined
|
|
3031
|
+
): Set<string>;
|
|
3032
|
+
/**
|
|
3033
|
+
* `true` when the latest assistant message is mid-batch: it carries at least
|
|
3034
|
+
* one settled tool result AND at least one tool call/approval still awaiting a
|
|
3035
|
+
* client result. That is the #1649 signature — the model fanned out parallel
|
|
3036
|
+
* tool calls and only some have been answered. Scoped to the leaf (the step the
|
|
3037
|
+
* continuation answers) so an unrelated dangling tool in an earlier message
|
|
3038
|
+
* doesn't block a legitimate follow-up continuation.
|
|
2522
3039
|
*/
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
/** Lifecycle status of a recovered stream's metadata row. */
|
|
2529
|
-
type ChatStreamStatus = "streaming" | "completed" | "error";
|
|
3040
|
+
declare function hasIncompleteToolBatch(
|
|
3041
|
+
messages: ReadonlyArray<ToolBatchMessage>
|
|
3042
|
+
): boolean;
|
|
3043
|
+
//#endregion
|
|
3044
|
+
//#region src/chat/parse-protocol.d.ts
|
|
2530
3045
|
/**
|
|
2531
|
-
*
|
|
2532
|
-
* source of truth for both packages; see {@link ChatRecoveryScheduleReason} for
|
|
2533
|
-
* the rationale behind each case.
|
|
3046
|
+
* Protocol Message Parser — typed parsing of cf_agent_chat_* WebSocket messages.
|
|
2534
3047
|
*
|
|
2535
|
-
*
|
|
2536
|
-
*
|
|
2537
|
-
* caught by a type error — only by the recovery suites.
|
|
3048
|
+
* Parses raw WebSocket messages into a discriminated union of protocol events.
|
|
3049
|
+
* Both AIChatAgent and Think can use this instead of manual JSON.parse + type checking.
|
|
2538
3050
|
*/
|
|
2539
|
-
declare function chatRecoverySchedulePolicy(
|
|
2540
|
-
reason: ChatRecoveryScheduleReason
|
|
2541
|
-
): {
|
|
2542
|
-
idempotent: boolean;
|
|
2543
|
-
};
|
|
2544
|
-
/** Identity + context for opening (or re-evaluating) a recovery incident. */
|
|
2545
|
-
interface BeginChatRecoveryIncidentInput {
|
|
2546
|
-
requestId: string;
|
|
2547
|
-
recoveryRootRequestId?: string | null;
|
|
2548
|
-
latestUserMessageId?: string | null;
|
|
2549
|
-
recoveryKind: ChatRecoveryKind;
|
|
2550
|
-
/** Test-only clock injection for deterministic debounce/window timing. */
|
|
2551
|
-
nowMs?: number;
|
|
2552
|
-
}
|
|
2553
|
-
interface BeginChatRecoveryIncidentResult {
|
|
2554
|
-
incident: ChatRecoveryIncident;
|
|
2555
|
-
config: ResolvedChatRecoveryConfig;
|
|
2556
|
-
exhausted: boolean;
|
|
2557
|
-
}
|
|
2558
3051
|
/**
|
|
2559
|
-
*
|
|
2560
|
-
*
|
|
2561
|
-
*
|
|
2562
|
-
*
|
|
3052
|
+
* Discriminated union of all incoming chat protocol events.
|
|
3053
|
+
*
|
|
3054
|
+
* Each agent handles the events it cares about and ignores the rest.
|
|
3055
|
+
* Returns `null` for non-JSON messages or unrecognized types.
|
|
2563
3056
|
*/
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
/**
|
|
2614
|
-
* Enqueue a recovery callback. A thin pass-through to the package's
|
|
2615
|
-
* `schedule(delaySeconds, callback, data, chatRecoverySchedulePolicy(reason))`
|
|
2616
|
-
* — the engine owns the surrounding orchestration (the transition + emit for
|
|
2617
|
-
* the initial schedule in {@link ChatRecoveryEngine.scheduleRecovery}, the
|
|
2618
|
-
* attempt bump for {@link ChatRecoveryEngine.rescheduleAfterStableTimeout});
|
|
2619
|
-
* the package owns the Durable Object alarm write and the payload shape.
|
|
2620
|
-
* `reason` selects the idempotency policy and `delaySeconds` the alarm delay
|
|
2621
|
-
* (`0` for the initial enqueue, `CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS` for
|
|
2622
|
-
* a stable-timeout reschedule).
|
|
2623
|
-
*/
|
|
2624
|
-
scheduleRecovery(
|
|
2625
|
-
callback: ChatRecoveryScheduleCallback,
|
|
2626
|
-
data: Record<string, unknown>,
|
|
2627
|
-
reason: ChatRecoveryScheduleReason,
|
|
2628
|
-
delaySeconds: number
|
|
2629
|
-
): Promise<void>;
|
|
2630
|
-
/**
|
|
2631
|
-
* Set or clear the live "recovering…" status (#1620). The engine calls this on
|
|
2632
|
-
* the incident transitions: `scheduled` → active (keyed by the recovery-root
|
|
2633
|
-
* request id, falling back to the incident's request id), and
|
|
2634
|
-
* `completed`/`skipped`/`failed` → cleared. The package owns the underlying
|
|
2635
|
-
* staleness / idempotency / broadcast I/O.
|
|
2636
|
-
*/
|
|
2637
|
-
setRecovering(active: boolean, requestId?: string): Promise<void>;
|
|
2638
|
-
/**
|
|
2639
|
-
* Report a throw from the caller's `shouldKeepRecovering` hook. Optional: a
|
|
2640
|
-
* host that does not surface this diagnostic omits it (the engine swallows the
|
|
2641
|
-
* report).
|
|
2642
|
-
*/
|
|
2643
|
-
onShouldKeepRecoveringError?(error: unknown): void;
|
|
2644
|
-
/**
|
|
2645
|
-
* Terminalize a given-up recovery turn: deliver the exhaustion notification
|
|
2646
|
-
* plus the package-owned terminal record / banner / submission writes. A thin
|
|
2647
|
-
* pass-through to the package's `_exhaustChatRecovery` (which composes
|
|
2648
|
-
* {@link runChatRecoveryExhaustion}). Driven by
|
|
2649
|
-
* {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}; the engine owns the
|
|
2650
|
-
* surrounding read → re-entry-guard → build → terminalize → seal sequence, the
|
|
2651
|
-
* package owns the terminal writes (uniformly broadcast-first; their set
|
|
2652
|
-
* differs — `Think` also writes a submission row).
|
|
2653
|
-
*/
|
|
2654
|
-
exhaustChatRecovery(
|
|
2655
|
-
incident: ChatRecoveryIncident,
|
|
2656
|
-
config: ResolvedChatRecoveryConfig,
|
|
2657
|
-
partial: RecoveryPartial,
|
|
2658
|
-
streamId: string,
|
|
2659
|
-
createdAt: number
|
|
2660
|
-
): Promise<void>;
|
|
2661
|
-
/**
|
|
2662
|
-
* Resolve the orphaned stream identity for a (recovery-root) request id —
|
|
2663
|
-
* `streamId` is `""` when no stream metadata survives. Drives BOTH the wake
|
|
2664
|
-
* path (which consumes the full {@link ResolvedRecoveryStream}) and the
|
|
2665
|
-
* give-up path (which reads only `.streamId`). A thin pass-through to the
|
|
2666
|
-
* package's stream-metadata lookup: the newest row keyed by the request id,
|
|
2667
|
-
* else the live active stream.
|
|
2668
|
-
*/
|
|
2669
|
-
resolveRecoveryStream(requestId: string): ResolvedRecoveryStream;
|
|
2670
|
-
/** Reconstruct the partial text/parts buffered for `streamId`. */
|
|
2671
|
-
getPartialStreamText(streamId: string): RecoveryPartial;
|
|
2672
|
-
/**
|
|
2673
|
-
* The in-flight recovery-root request id, consulted as a fallback in the
|
|
2674
|
-
* give-up root-id chain when the payload carries no `originalRequestId` /
|
|
2675
|
-
* `recoveredRequestId` and no incident record survives. `undefined` when no
|
|
2676
|
-
* recovery chain is active. (`AIChatAgent` and `Think` both back this with
|
|
2677
|
-
* `_activeChatRecoveryRootRequestId`.)
|
|
2678
|
-
*/
|
|
2679
|
-
activeChatRecoveryRootRequestId(): string | undefined;
|
|
2680
|
-
/**
|
|
2681
|
-
* Report a tolerated best-effort bookkeeping failure during give-up: the
|
|
2682
|
-
* incident `"read"` (before synthesizing) or the sealing `"seal"` write
|
|
2683
|
-
* (after terminalization). Neither aborts terminalization — see
|
|
2684
|
-
* {@link ChatRecoveryEngine.exhaustRecoveryGiveUp}.
|
|
2685
|
-
*/
|
|
2686
|
-
onGiveUpBookkeepingError(phase: "read" | "seal", error: unknown): void;
|
|
2687
|
-
}
|
|
2688
|
-
/** Resolved orphaned-stream identity for a recovered chat turn. */
|
|
2689
|
-
interface ResolvedRecoveryStream {
|
|
2690
|
-
/** The orphaned stream id, or `""` when no stream metadata survives. */
|
|
2691
|
-
streamId: string;
|
|
2692
|
-
/**
|
|
2693
|
-
* Whether the orphaned stream is still the live in-flight stream (so its
|
|
2694
|
-
* partial has not already been persisted + completed by an ACK-driven
|
|
2695
|
-
* reconnect). Gates persistence and stream completion.
|
|
2696
|
-
*/
|
|
2697
|
-
streamStillActive: boolean;
|
|
2698
|
-
/**
|
|
2699
|
-
* The stream metadata row's lifecycle status, when the host tracks it
|
|
2700
|
-
* (`Think`). `undefined` for hosts that do not model terminal streams
|
|
2701
|
-
* (`AIChatAgent`) — those keep every terminal-stream branch dead, per the
|
|
2702
|
-
* "substrate capabilities are optional" decision in the RFC.
|
|
2703
|
-
*/
|
|
2704
|
-
streamStatus?: ChatStreamStatus;
|
|
2705
|
-
}
|
|
2706
|
-
/** Input to {@link ChatFiberWakeHooks.classifyRecoveredTurn}. */
|
|
2707
|
-
interface ClassifyRecoveredTurnInput {
|
|
2708
|
-
snapshot: ChatFiberSnapshot | null;
|
|
2709
|
-
requestId: string;
|
|
2710
|
-
streamId: string;
|
|
2711
|
-
partial: RecoveryPartial;
|
|
2712
|
-
streamStillActive: boolean;
|
|
2713
|
-
streamStatus?: ChatStreamStatus;
|
|
2714
|
-
}
|
|
2715
|
-
/** Input to {@link ChatFiberWakeHooks.invokeOnChatRecovery}. */
|
|
2716
|
-
interface InvokeOnChatRecoveryInput {
|
|
2717
|
-
incident: ChatRecoveryIncident;
|
|
2718
|
-
recoveryKind: ChatRecoveryKind;
|
|
2719
|
-
recoveryRootRequestId: string;
|
|
2720
|
-
requestId: string;
|
|
2721
|
-
streamId: string;
|
|
2722
|
-
partial: RecoveryPartial;
|
|
2723
|
-
snapshot: ChatFiberSnapshot | null;
|
|
2724
|
-
recoveryData: unknown;
|
|
2725
|
-
createdAt: number;
|
|
2726
|
-
}
|
|
2727
|
-
/** Input to {@link ChatFiberWakeHooks.shouldPersistOrphanedPartial}. */
|
|
2728
|
-
interface PersistOrphanedPartialInput {
|
|
2729
|
-
streamId: string;
|
|
2730
|
-
streamStillActive: boolean;
|
|
2731
|
-
streamStatus?: ChatStreamStatus;
|
|
2732
|
-
snapshot: ChatFiberSnapshot | null;
|
|
2733
|
-
}
|
|
2734
|
-
/** Input to {@link ChatFiberWakeHooks.dispatchRecoveredTurn}. */
|
|
2735
|
-
interface DispatchRecoveredTurnInput<TClassify> {
|
|
2736
|
-
incident: ChatRecoveryIncident;
|
|
2737
|
-
config: ResolvedChatRecoveryConfig;
|
|
2738
|
-
recoveryKind: ChatRecoveryKind;
|
|
2739
|
-
options: ChatRecoveryOptions;
|
|
2740
|
-
snapshot: ChatFiberSnapshot | null;
|
|
2741
|
-
requestId: string;
|
|
2742
|
-
recoveryRootRequestId: string;
|
|
2743
|
-
streamId: string;
|
|
2744
|
-
streamStatus?: ChatStreamStatus;
|
|
2745
|
-
/** The package-specific classification detail produced by `classifyRecoveredTurn`. */
|
|
2746
|
-
detail: TClassify;
|
|
2747
|
-
}
|
|
3057
|
+
type ChatProtocolEvent =
|
|
3058
|
+
| {
|
|
3059
|
+
type: "chat-request";
|
|
3060
|
+
id: string;
|
|
3061
|
+
init: {
|
|
3062
|
+
method?: string;
|
|
3063
|
+
body?: string;
|
|
3064
|
+
[key: string]: unknown;
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
| {
|
|
3068
|
+
type: "clear";
|
|
3069
|
+
}
|
|
3070
|
+
| {
|
|
3071
|
+
type: "cancel";
|
|
3072
|
+
id: string;
|
|
3073
|
+
}
|
|
3074
|
+
| {
|
|
3075
|
+
type: "tool-result";
|
|
3076
|
+
toolCallId: string;
|
|
3077
|
+
toolName: string;
|
|
3078
|
+
output: unknown;
|
|
3079
|
+
state?: string;
|
|
3080
|
+
errorText?: string;
|
|
3081
|
+
autoContinue?: boolean;
|
|
3082
|
+
clientTools?: Array<{
|
|
3083
|
+
name: string;
|
|
3084
|
+
description?: string;
|
|
3085
|
+
parameters?: unknown;
|
|
3086
|
+
}>;
|
|
3087
|
+
}
|
|
3088
|
+
| {
|
|
3089
|
+
type: "tool-approval";
|
|
3090
|
+
toolCallId: string;
|
|
3091
|
+
approved: boolean;
|
|
3092
|
+
autoContinue?: boolean;
|
|
3093
|
+
}
|
|
3094
|
+
| {
|
|
3095
|
+
type: "stream-resume-request";
|
|
3096
|
+
probeId?: string;
|
|
3097
|
+
}
|
|
3098
|
+
| {
|
|
3099
|
+
type: "stream-resume-ack";
|
|
3100
|
+
id: string;
|
|
3101
|
+
}
|
|
3102
|
+
| {
|
|
3103
|
+
type: "messages";
|
|
3104
|
+
messages: unknown[];
|
|
3105
|
+
};
|
|
2748
3106
|
/**
|
|
2749
|
-
*
|
|
2750
|
-
* fiber is detected on restart — the divergent organs the frame-collapse map
|
|
2751
|
-
* flagged. Kept SEPARATE from {@link ChatRecoveryAdapter} (and passed per call to
|
|
2752
|
-
* {@link ChatRecoveryEngine.handleChatFiberRecovery}) so the incident/give-up
|
|
2753
|
-
* adapter stays focused, and generic over `TClassify` so the
|
|
2754
|
-
* `classifyRecoveredTurn` → `dispatchRecoveredTurn` handoff is type-safe without a
|
|
2755
|
-
* class-level generic.
|
|
3107
|
+
* Parse a raw WebSocket message string into a typed protocol event.
|
|
2756
3108
|
*
|
|
2757
|
-
*
|
|
2758
|
-
*
|
|
2759
|
-
*
|
|
2760
|
-
*
|
|
3109
|
+
* Returns `null` if the message is not valid JSON or not a recognized
|
|
3110
|
+
* protocol message type. Callers should fall through to the user's
|
|
3111
|
+
* `onMessage` handler when `null` is returned.
|
|
3112
|
+
*
|
|
3113
|
+
* @example
|
|
3114
|
+
* ```typescript
|
|
3115
|
+
* const event = parseProtocolMessage(rawMessage);
|
|
3116
|
+
* if (!event) return userOnMessage(connection, rawMessage);
|
|
3117
|
+
*
|
|
3118
|
+
* switch (event.type) {
|
|
3119
|
+
* case "chat-request": { ... }
|
|
3120
|
+
* case "clear": { ... }
|
|
3121
|
+
* case "tool-result": { ... }
|
|
3122
|
+
* }
|
|
3123
|
+
* ```
|
|
2761
3124
|
*/
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
/** Decode the fiber snapshot into the recovery snapshot + checkpointed user data. */
|
|
2766
|
-
unwrapRecoverySnapshot(ctx: FiberRecoveryContext): {
|
|
2767
|
-
snapshot: ChatFiberSnapshot | null;
|
|
2768
|
-
recoveryData: unknown;
|
|
2769
|
-
};
|
|
2770
|
-
/**
|
|
2771
|
-
* Classify the recovered turn as a `retry` or `continue` and return any
|
|
2772
|
-
* package-specific detail the dispatch decision needs (e.g. the pre-stream
|
|
2773
|
-
* retry target id). Runs before the incident is opened.
|
|
2774
|
-
*/
|
|
2775
|
-
classifyRecoveredTurn(input: ClassifyRecoveredTurnInput):
|
|
2776
|
-
| {
|
|
2777
|
-
recoveryKind: ChatRecoveryKind;
|
|
2778
|
-
detail: TClassify;
|
|
2779
|
-
}
|
|
2780
|
-
| Promise<{
|
|
2781
|
-
recoveryKind: ChatRecoveryKind;
|
|
2782
|
-
detail: TClassify;
|
|
2783
|
-
}>;
|
|
2784
|
-
/**
|
|
2785
|
-
* Build the package's `ChatRecoveryContext` and invoke the user `onChatRecovery`
|
|
2786
|
-
* hook, returning its (defaulted) options. The engine wraps this in the
|
|
2787
|
-
* incident `failed`-on-throw guard. Optional: a host with no user
|
|
2788
|
-
* `onChatRecovery` surface (e.g. the pi fixture) omits it and the engine
|
|
2789
|
-
* proceeds with empty options (`{}`).
|
|
2790
|
-
*/
|
|
2791
|
-
invokeOnChatRecovery?(
|
|
2792
|
-
input: InvokeOnChatRecoveryInput
|
|
2793
|
-
): Promise<ChatRecoveryOptions | void>;
|
|
2794
|
-
/**
|
|
2795
|
-
* The BASE persist gate: whether the orphaned partial is eligible to be
|
|
2796
|
-
* materialized at all (live stream, or terminal-but-not-yet-persisted). The
|
|
2797
|
-
* engine ANDs this with the shared `options.persist !== false ||
|
|
2798
|
-
* partial.hasSettledToolResults` clause, so settled work is never dropped.
|
|
2799
|
-
*/
|
|
2800
|
-
shouldPersistOrphanedPartial(
|
|
2801
|
-
input: PersistOrphanedPartialInput
|
|
2802
|
-
): boolean | Promise<boolean>;
|
|
2803
|
-
/** Materialize the orphaned stream's partial into a persisted assistant message. */
|
|
2804
|
-
persistOrphanedStream(streamId: string): Promise<void>;
|
|
2805
|
-
/** Mark the (still-active) recovered stream complete and schedule cleanup. */
|
|
2806
|
-
completeRecoveredStream(streamId: string): void | Promise<void>;
|
|
2807
|
-
/**
|
|
2808
|
-
* The retry/continue/skip DECISION — the package-owned core. Runs after persist
|
|
2809
|
-
* + complete; owns the leaf/submission computation, the schedule calls (via
|
|
2810
|
-
* {@link ChatRecoveryEngine.scheduleRecovery}), the skip transitions, and any
|
|
2811
|
-
* package-specific terminal/broadcast writes.
|
|
2812
|
-
*/
|
|
2813
|
-
dispatchRecoveredTurn(
|
|
2814
|
-
input: DispatchRecoveredTurnInput<TClassify>
|
|
2815
|
-
): Promise<void>;
|
|
2816
|
-
}
|
|
3125
|
+
declare function parseProtocolMessage(raw: string): ChatProtocolEvent | null;
|
|
3126
|
+
//#endregion
|
|
3127
|
+
//#region src/chat/message-reconciler.d.ts
|
|
2817
3128
|
/**
|
|
2818
|
-
*
|
|
2819
|
-
*
|
|
2820
|
-
*
|
|
3129
|
+
* Reconcile incoming client messages against server state.
|
|
3130
|
+
*
|
|
3131
|
+
* 1. Merges server-known tool outputs into incoming messages that still
|
|
3132
|
+
* show stale states (input-available, approval-requested, approval-responded)
|
|
3133
|
+
* 2. Reconciles assistant IDs: exact match → content-key match → toolCallId match
|
|
3134
|
+
*
|
|
3135
|
+
* @param incoming - Messages from the client
|
|
3136
|
+
* @param serverMessages - Current server-side messages (source of truth)
|
|
3137
|
+
* @param sanitizeForContentKey - Function to sanitize a message before computing
|
|
3138
|
+
* its content key (typically strips ephemeral provider metadata)
|
|
3139
|
+
* @returns Reconciled messages ready for persistence
|
|
2821
3140
|
*/
|
|
2822
|
-
declare
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
* "recovering…" status) via {@link updateIncident};
|
|
2887
|
-
* 2. emit `chat:recovery:scheduled`; and
|
|
2888
|
-
* 3. enqueue the callback through the adapter's idempotent schedule.
|
|
2889
|
-
*
|
|
2890
|
-
* `recoveryKind` is passed explicitly (not read off the incident) because a
|
|
2891
|
-
* caller can legitimately report a different kind than the incident was opened
|
|
2892
|
-
* with — e.g. `AIChatAgent`'s lost-partial branch opens a `continue` incident
|
|
2893
|
-
* but schedules (and reports) a `retry`. `requestId` always matches
|
|
2894
|
-
* `incident.requestId` (the evaluation rewrites it to the current attempt), so
|
|
2895
|
-
* it is read from the incident.
|
|
2896
|
-
*/
|
|
2897
|
-
scheduleRecovery(input: {
|
|
2898
|
-
incident: ChatRecoveryIncident;
|
|
2899
|
-
recoveryKind: ChatRecoveryKind;
|
|
2900
|
-
callback: ChatRecoveryScheduleCallback;
|
|
2901
|
-
data: Record<string, unknown>;
|
|
2902
|
-
reason?: ChatRecoveryScheduleReason;
|
|
2903
|
-
}): Promise<void>;
|
|
3141
|
+
declare function reconcileMessages(
|
|
3142
|
+
incoming: UIMessage[],
|
|
3143
|
+
serverMessages: readonly UIMessage[],
|
|
3144
|
+
sanitizeForContentKey?: (message: UIMessage) => UIMessage
|
|
3145
|
+
): UIMessage[];
|
|
3146
|
+
/**
|
|
3147
|
+
* For a single message, resolve its ID by matching toolCallId against server state.
|
|
3148
|
+
* Prevents duplicate DB rows when client IDs differ from server IDs.
|
|
3149
|
+
* Tool call IDs are unique per conversation, so matching is safe regardless of state.
|
|
3150
|
+
*/
|
|
3151
|
+
declare function resolveToolMergeId(
|
|
3152
|
+
message: UIMessage,
|
|
3153
|
+
serverMessages: readonly UIMessage[]
|
|
3154
|
+
): UIMessage;
|
|
3155
|
+
/**
|
|
3156
|
+
* Merge a freshly-reconstructed orphaned partial onto the assistant message
|
|
3157
|
+
* that already owns its target id (the orphan-persist **(c)** step).
|
|
3158
|
+
*
|
|
3159
|
+
* Used by hosts whose store can hold an assistant row for the SAME id BEFORE
|
|
3160
|
+
* the stream finalizes — e.g. an early persist at tool-approval time, or a
|
|
3161
|
+
* continuation resuming the prior assistant message. On recovery the engine
|
|
3162
|
+
* replays the same chunks, so a naive append would leave two parts per tool
|
|
3163
|
+
* call. The merge therefore:
|
|
3164
|
+
*
|
|
3165
|
+
* - keeps ALL existing parts (the persisted row is authoritative for tool
|
|
3166
|
+
* parts that had a client result applied IN PLACE — that result lives only
|
|
3167
|
+
* in storage, never in the chunk stream, so a whole-message replace would
|
|
3168
|
+
* clobber it);
|
|
3169
|
+
* - appends only the reconstructed parts whose `toolCallId` is NOT already
|
|
3170
|
+
* present (dedup by tool-call identity);
|
|
3171
|
+
* - overlays the incoming metadata onto the existing metadata (incoming wins
|
|
3172
|
+
* on conflicts), falling back to whichever side is present.
|
|
3173
|
+
*
|
|
3174
|
+
* The result carries the INCOMING message's id/role (the caller has already
|
|
3175
|
+
* resolved the incoming id to the existing row's id via the (b) target-id
|
|
3176
|
+
* step), so it is safe to write straight back through `updateMessage`.
|
|
3177
|
+
*
|
|
3178
|
+
* Hosts whose orphan persist only ever runs at stream finalize (no early/
|
|
3179
|
+
* mid-stream row for the same id) never hit the merge branch and don't need
|
|
3180
|
+
* this — a plain append/replace is already dedup-safe because the shared
|
|
3181
|
+
* reconstruction (`StreamAccumulator` / `applyChunkToParts`) is idempotent by
|
|
3182
|
+
* `toolCallId`.
|
|
3183
|
+
*/
|
|
3184
|
+
declare function reconcileOrphanPartial(
|
|
3185
|
+
existing: UIMessage,
|
|
3186
|
+
incoming: UIMessage
|
|
3187
|
+
): UIMessage;
|
|
3188
|
+
//#endregion
|
|
3189
|
+
//#region src/chat/repair-transcript.d.ts
|
|
3190
|
+
/**
|
|
3191
|
+
* Whether a tool part already has a settled result the provider accepts, so it
|
|
3192
|
+
* must NOT be re-repaired into an errored result.
|
|
3193
|
+
*
|
|
3194
|
+
* Single source of truth for the terminal tool states. Mirrors the AI SDK's
|
|
3195
|
+
* terminal states: `convertToModelMessages` emits a `tool-result` for
|
|
3196
|
+
* `output-available`, `output-error`, AND `output-denied` (a user-denied
|
|
3197
|
+
* approval — its denial reason becomes the tool-result). Omitting any of these
|
|
3198
|
+
* makes repair re-flip the part every turn — clobbering a real `errorText` /
|
|
3199
|
+
* denial with the generic "interrupted" message.
|
|
3200
|
+
*/
|
|
3201
|
+
declare function toolPartHasSettledResult(
|
|
3202
|
+
record: Record<string, unknown>
|
|
3203
|
+
): boolean;
|
|
3204
|
+
interface RepairInterruptedToolPartsOptions {
|
|
2904
3205
|
/**
|
|
2905
|
-
*
|
|
2906
|
-
*
|
|
2907
|
-
*
|
|
2908
|
-
*
|
|
2909
|
-
*
|
|
2910
|
-
* returns, so an idempotent reschedule would dedup onto that doomed row and
|
|
2911
|
-
* never fire — see {@link chatRecoverySchedulePolicy}).
|
|
2912
|
-
*
|
|
2913
|
-
* Returns `true` when a retry was scheduled, `false` when there is no incident
|
|
2914
|
-
* (no id / record gone) or the attempt budget is already spent — in which case
|
|
2915
|
-
* the caller falls through to the give-up path. Deliberately bypasses the
|
|
2916
|
-
* `evaluateChatRecoveryIncident` budget (this is a coarse stable-state retry,
|
|
2917
|
-
* not a fresh interruption) and {@link updateIncident} (no `scheduled` event /
|
|
2918
|
-
* recovering-flag churn on a same-turn reschedule).
|
|
3206
|
+
* Decide the replacement for an interrupted tool part (no settled result, not
|
|
3207
|
+
* `approval-responded`). Its `input` has already been normalized to a valid
|
|
3208
|
+
* object. The default host behavior flips it to an errored tool-result; hosts
|
|
3209
|
+
* expose this as an overridable `repairInterruptedToolPart` hook so a subclass
|
|
3210
|
+
* can, e.g., convert an interrupted client-resolved tool into a text part.
|
|
2919
3211
|
*/
|
|
2920
|
-
|
|
2921
|
-
incidentId: string | undefined;
|
|
2922
|
-
callback: ChatRecoveryScheduleCallback;
|
|
2923
|
-
data: Record<string, unknown> | undefined;
|
|
2924
|
-
fallbackMaxAttempts: number;
|
|
2925
|
-
}): Promise<boolean>;
|
|
3212
|
+
repairPart: (part: UIMessage["parts"][number]) => UIMessage["parts"][number];
|
|
2926
3213
|
/**
|
|
2927
|
-
*
|
|
2928
|
-
*
|
|
2929
|
-
* and decide what to do next (#1825).
|
|
2930
|
-
*
|
|
2931
|
-
* Bumps the incident's durable `oomAttempts` counter, then:
|
|
2932
|
-
* - if it is still within `maxOomRetries`, issues a delayed, NON-idempotent
|
|
2933
|
-
* reschedule of the SAME callback (same machinery as
|
|
2934
|
-
* {@link rescheduleAfterStableTimeout}: the executing one-shot row is
|
|
2935
|
-
* deleted only after the callback returns, so an idempotent reschedule
|
|
2936
|
-
* would dedup onto that doomed row) and returns `"rescheduled"`. The small
|
|
2937
|
-
* delay lets a transient memory spike clear before the re-run;
|
|
2938
|
-
* - otherwise leaves the incremented count persisted (so a begin-path
|
|
2939
|
-
* re-evaluation agrees) and returns `"exhausted"` — the caller then
|
|
2940
|
-
* terminalizes via the give-up path with `reason="out_of_memory"`.
|
|
2941
|
-
*
|
|
2942
|
-
* Returns `"exhausted"` when there is no incident to track against (no id /
|
|
2943
|
-
* record gone): an OOM we cannot bound must seal rather than loop. Unlike a
|
|
2944
|
-
* stable-state retry this is gated by the OOM-specific budget, NOT the generic
|
|
2945
|
-
* attempt cap — re-running an OOM streams a little "progress" that would
|
|
2946
|
-
* otherwise reset the attempt cap forever (the #1825 loop).
|
|
3214
|
+
* Whether a tool part already carries a settled result (defaults to
|
|
3215
|
+
* {@link toolPartHasSettledResult}).
|
|
2947
3216
|
*/
|
|
2948
|
-
|
|
2949
|
-
incidentId: string | undefined;
|
|
2950
|
-
callback: ChatRecoveryScheduleCallback;
|
|
2951
|
-
data: Record<string, unknown> | undefined;
|
|
2952
|
-
maxOomRetries: number;
|
|
2953
|
-
}): Promise<"rescheduled" | "exhausted">;
|
|
3217
|
+
isSettled?: (record: Record<string, unknown>) => boolean;
|
|
2954
3218
|
/**
|
|
2955
|
-
*
|
|
2956
|
-
*
|
|
2957
|
-
* packages repeated verbatim:
|
|
2958
|
-
*
|
|
2959
|
-
* 1. resolve config + the incident key from `data.incidentId`;
|
|
2960
|
-
* 2. best-effort READ the stored incident — a failed read is tolerated
|
|
2961
|
-
* (reported via `onGiveUpBookkeepingError("read", …)`) and the incident is
|
|
2962
|
-
* synthesized, because the read backs only the re-entry guard, not the
|
|
2963
|
-
* terminal UX;
|
|
2964
|
-
* 3. re-entry guard: a `stored.status === "exhausted"` record means
|
|
2965
|
-
* terminalization already fired, so a duplicate stale alarm returns without
|
|
2966
|
-
* re-broadcasting the banner;
|
|
2967
|
-
* 4. build the exhausted incident (reuse `stored`, or synthesize a minimal one
|
|
2968
|
-
* so a swept/missing record STILL terminalizes through `onExhausted`);
|
|
2969
|
-
* 5. resolve the orphaned stream id + partial;
|
|
2970
|
-
* 6. terminalize via `exhaustChatRecovery` — BEFORE sealing. The terminal
|
|
2971
|
-
* writes can reject with a platform transient in the deploy/storage window
|
|
2972
|
-
* a give-up runs in (#1730); letting that throw propagate is deliberate, so
|
|
2973
|
-
* `Agent._executeScheduleCallback` defers the one-shot row and the WHOLE
|
|
2974
|
-
* give-up re-runs on a healthy isolate. Sealing first would arm the
|
|
2975
|
-
* re-entry guard and turn that re-run into a no-op, dropping the durable
|
|
2976
|
-
* terminal record. The re-run is idempotent (terminal writes overwrite the
|
|
2977
|
-
* same key); a second banner is the documented at-least-once edge; and
|
|
2978
|
-
* 7. best-effort SEAL write so the re-entry guard sees `exhausted` on a
|
|
2979
|
-
* duplicate alarm — a failed seal (reported via
|
|
2980
|
-
* `onGiveUpBookkeepingError("seal", …)`) costs at most one re-delivered
|
|
2981
|
-
* banner.
|
|
2982
|
-
*
|
|
2983
|
-
* The two packages diverged only in parameters the caller supplies:
|
|
2984
|
-
* `reason` (`Think` passes `stable_timeout` | `recovery_error`; `AIChatAgent`
|
|
2985
|
-
* always `stable_timeout`) and the root-id chain (`Think` includes
|
|
2986
|
-
* `recoveredRequestId`; `AIChatAgent` never sets it, so the unified chain
|
|
2987
|
-
* collapses identically). Exactly-once terminalization rests on the re-entry
|
|
2988
|
-
* guard alone in `AIChatAgent`; `Think` additionally short-circuits duplicate
|
|
2989
|
-
* alarms earlier in its durable-submission layer.
|
|
3219
|
+
* Normalize a tool part's `input` (defaults to the shared
|
|
3220
|
+
* {@link normalizeToolInput}).
|
|
2990
3221
|
*/
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
incidentId?: string;
|
|
2996
|
-
originalRequestId?: string;
|
|
2997
|
-
recoveredRequestId?: string;
|
|
2998
|
-
}
|
|
2999
|
-
| undefined;
|
|
3000
|
-
reason: string;
|
|
3001
|
-
}): Promise<void>;
|
|
3222
|
+
normalizeInput?: (input: unknown) => {
|
|
3223
|
+
input: unknown;
|
|
3224
|
+
changed: boolean;
|
|
3225
|
+
};
|
|
3002
3226
|
/**
|
|
3003
|
-
*
|
|
3004
|
-
*
|
|
3005
|
-
*
|
|
3006
|
-
*
|
|
3007
|
-
*
|
|
3008
|
-
* -
|
|
3009
|
-
*
|
|
3010
|
-
*
|
|
3011
|
-
*
|
|
3012
|
-
* No-op when `incidentId` is undefined or the record is already gone. This is
|
|
3013
|
-
* the transition twin of {@link beginIncident}: all I/O is adapter-owned, the
|
|
3014
|
-
* engine owns only the state-machine shape.
|
|
3227
|
+
* Whether an interrupted tool part (no settled result, not
|
|
3228
|
+
* `approval-responded`) should be repaired at all. Defaults to `true` (repair
|
|
3229
|
+
* everything, like Think — which converts even client tools via its
|
|
3230
|
+
* `repairPart` override). A host whose default `repairPart` errors the part
|
|
3231
|
+
* (ai-chat) passes this to SKIP a part still legitimately awaiting a CLIENT
|
|
3232
|
+
* interaction (an `input-available` client tool or an `approval-requested`
|
|
3233
|
+
* part the user may still answer) so it is left verbatim rather than clobbered
|
|
3234
|
+
* with an error. Skipped parts are not counted in `removedToolCalls`.
|
|
3015
3235
|
*/
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3236
|
+
shouldRepair?: (part: UIMessage["parts"][number]) => boolean;
|
|
3237
|
+
}
|
|
3238
|
+
interface RepairInterruptedToolPartsResult {
|
|
3239
|
+
/** A new messages array; unchanged messages keep their original reference. */
|
|
3240
|
+
messages: UIMessage[];
|
|
3241
|
+
/** Count of interrupted tool calls flipped to a repaired shape. */
|
|
3242
|
+
removedToolCalls: number;
|
|
3243
|
+
/** Count of tool parts whose malformed `input` was normalized. */
|
|
3244
|
+
normalizedInputs: number;
|
|
3245
|
+
/** The tool-call ids that were repaired. */
|
|
3246
|
+
toolCallIds: string[];
|
|
3021
3247
|
}
|
|
3022
3248
|
/**
|
|
3023
|
-
*
|
|
3024
|
-
*
|
|
3025
|
-
*
|
|
3026
|
-
* `buildChatRecoveryExhaustedContext` → `notifyChatRecoveryExhausted` → host
|
|
3027
|
-
* terminalize sequence that every host's `_exhaustChatRecovery` repeated.
|
|
3028
|
-
*
|
|
3029
|
-
* What this OWNS (the invariant, so it cannot drift per host):
|
|
3030
|
-
* - the notification ALWAYS runs before any terminal write, and
|
|
3031
|
-
* - a throwing `onExhausted` can NEVER block terminal delivery — it is swallowed
|
|
3032
|
-
* via `onError` (a tested invariant in both published packages).
|
|
3249
|
+
* Repair interrupted tool calls and normalize malformed tool inputs across a
|
|
3250
|
+
* transcript. Behavior mirrors `@cloudflare/think`'s original
|
|
3251
|
+
* `_repairToolTranscriptParts`:
|
|
3033
3252
|
*
|
|
3034
|
-
*
|
|
3035
|
-
*
|
|
3036
|
-
*
|
|
3037
|
-
*
|
|
3038
|
-
*
|
|
3039
|
-
*
|
|
3040
|
-
*
|
|
3041
|
-
*
|
|
3253
|
+
* - a tool part with NO settled result and state `approval-responded` is kept
|
|
3254
|
+
* verbatim (an approved server tool waiting for its continuation to run
|
|
3255
|
+
* `execute()` — not abandoned);
|
|
3256
|
+
* - a tool part with NO settled result for which `shouldRepair` returns false
|
|
3257
|
+
* is kept verbatim (a part still awaiting a CLIENT interaction; see option);
|
|
3258
|
+
* - any other tool part with no settled result is normalized then handed to
|
|
3259
|
+
* `repairPart` (default: flipped to an errored result);
|
|
3260
|
+
* - a tool part WITH a settled result only has its `input` normalized.
|
|
3042
3261
|
*
|
|
3043
|
-
*
|
|
3044
|
-
*
|
|
3045
|
-
* engine seam stays parts-vocabulary-agnostic.
|
|
3262
|
+
* Messages with no changed part keep their original object reference so callers
|
|
3263
|
+
* can cheaply detect what to persist.
|
|
3046
3264
|
*/
|
|
3047
|
-
declare function
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
)
|
|
3265
|
+
declare function repairInterruptedToolParts(
|
|
3266
|
+
messages: UIMessage[],
|
|
3267
|
+
options: RepairInterruptedToolPartsOptions
|
|
3268
|
+
): RepairInterruptedToolPartsResult;
|
|
3269
|
+
//#endregion
|
|
3270
|
+
//#region src/chat/orphan-store.d.ts
|
|
3271
|
+
interface OrphanPersistStore<
|
|
3272
|
+
M extends {
|
|
3273
|
+
id: string;
|
|
3274
|
+
} = UIMessage
|
|
3275
|
+
> {
|
|
3276
|
+
/** Read the stored message with this id, or `null` if none exists. */
|
|
3277
|
+
getMessage(id: string): M | null | Promise<M | null>;
|
|
3278
|
+
/**
|
|
3279
|
+
* Append a new message. `parentId` is honored by tree-structured stores
|
|
3280
|
+
* (`undefined` → attach to the latest leaf); flat-array stores ignore it.
|
|
3281
|
+
*/
|
|
3282
|
+
appendMessage(message: M, parentId?: string | null): void | Promise<void>;
|
|
3283
|
+
/** Replace the stored message that owns `message.id`. */
|
|
3284
|
+
updateMessage(message: M): void | Promise<void>;
|
|
3285
|
+
}
|
|
3286
|
+
//#endregion
|
|
3287
|
+
//#region src/chat/orphan-persist.d.ts
|
|
3288
|
+
interface PersistReconstructedOrphanOptions<
|
|
3289
|
+
TMessage extends UIMessage = UIMessage
|
|
3290
|
+
> {
|
|
3291
|
+
/** The store seam to upsert through (a `SessionProvider` write-subset). */
|
|
3292
|
+
store: OrphanPersistStore<TMessage>;
|
|
3293
|
+
/**
|
|
3294
|
+
* Id for the reconstructed message when the stream carried no provider
|
|
3295
|
+
* `start.messageId` to adopt. The accumulator still adopts a provider id when
|
|
3296
|
+
* present.
|
|
3297
|
+
*/
|
|
3298
|
+
fallbackId: string;
|
|
3299
|
+
/**
|
|
3300
|
+
* Finalize the reconstructed message before upsert — e.g. strip internal
|
|
3301
|
+
* parts or resolve the persist-target id. Return `null` to skip persistence
|
|
3302
|
+
* entirely (e.g. an empty structural-only message).
|
|
3303
|
+
*/
|
|
3304
|
+
prepare: (message: TMessage) => TMessage | null;
|
|
3305
|
+
/**
|
|
3306
|
+
* Combine an existing row with the reconstructed message when a row already
|
|
3307
|
+
* owns the id (replace, or reconcile partials).
|
|
3308
|
+
*/
|
|
3309
|
+
merge: (existing: TMessage, incoming: TMessage) => TMessage;
|
|
3310
|
+
}
|
|
3311
|
+
/**
|
|
3312
|
+
* Reconstruct a message from `chunks` and upsert it via the store. Returns
|
|
3313
|
+
* `true` when a write happened (so a caller that broadcasts after — Think — can
|
|
3314
|
+
* gate its broadcast on it), `false` when there was nothing to persist (no
|
|
3315
|
+
* parts, or `prepare` returned `null`).
|
|
3316
|
+
*/
|
|
3317
|
+
declare function persistReconstructedOrphan<
|
|
3318
|
+
TMessage extends UIMessage = UIMessage
|
|
3319
|
+
>(
|
|
3320
|
+
chunks: ReadonlyArray<{
|
|
3321
|
+
body: string;
|
|
3322
|
+
}>,
|
|
3323
|
+
options: PersistReconstructedOrphanOptions<TMessage>
|
|
3324
|
+
): Promise<boolean>;
|
|
3063
3325
|
//#endregion
|
|
3064
3326
|
//#region src/chat/recovery-codec.d.ts
|
|
3065
3327
|
/**
|
|
@@ -3100,11 +3362,12 @@ interface ProgressCreditThrottle {
|
|
|
3100
3362
|
shouldCredit(now: number): boolean;
|
|
3101
3363
|
}
|
|
3102
3364
|
/**
|
|
3103
|
-
* The
|
|
3104
|
-
*
|
|
3105
|
-
*
|
|
3106
|
-
*
|
|
3107
|
-
*
|
|
3365
|
+
* The host-agnostic rule for crediting recovery forward progress from a stored
|
|
3366
|
+
* stream chunk, from when the marker was a counter bumped per chunk. Neither
|
|
3367
|
+
* host bumps a counter any more: the marker is derived from the stream log
|
|
3368
|
+
* (`ResumableStream.progressMarker`), so a chunk counts once its segment is
|
|
3369
|
+
* durably flushed. The rule stays exported for consumers that still keep a
|
|
3370
|
+
* counter of their own:
|
|
3108
3371
|
*
|
|
3109
3372
|
* - a **milestone** ({@link ChatRecoveryCodec.isProgressChunk}) always credits;
|
|
3110
3373
|
* - **streaming content** ({@link ChatRecoveryCodec.isStreamingContentChunk})
|
|
@@ -3277,6 +3540,37 @@ declare function iterateWithStallWatchdog<T>(
|
|
|
3277
3540
|
onStall: () => void
|
|
3278
3541
|
): AsyncGenerator<T>;
|
|
3279
3542
|
//#endregion
|
|
3543
|
+
//#region src/chat/truncate-older-messages.d.ts
|
|
3544
|
+
interface TruncateOptions {
|
|
3545
|
+
/** Number of recent messages to keep intact (default: 4) */
|
|
3546
|
+
keepRecent?: number;
|
|
3547
|
+
/** Max chars for tool outputs in older messages (default: 500) */
|
|
3548
|
+
maxToolOutputChars?: number;
|
|
3549
|
+
/** Max chars for text parts in older messages (default: 10000) */
|
|
3550
|
+
maxTextChars?: number;
|
|
3551
|
+
}
|
|
3552
|
+
/**
|
|
3553
|
+
* Truncate tool outputs and long text in older messages.
|
|
3554
|
+
* Returns a new array — input messages are not mutated.
|
|
3555
|
+
*
|
|
3556
|
+
* Recent messages (last `keepRecent`) are left intact.
|
|
3557
|
+
* Older messages get tool outputs and long text truncated. Structured tool
|
|
3558
|
+
* outputs are truncated in place instead of being replaced by raw strings.
|
|
3559
|
+
*
|
|
3560
|
+
* Use in assembleContext() before sending to the LLM:
|
|
3561
|
+
* ```typescript
|
|
3562
|
+
* async assembleContext() {
|
|
3563
|
+
* const history = this.sessions.getHistory(this._sessionId);
|
|
3564
|
+
* const truncated = truncateOlderMessages(history);
|
|
3565
|
+
* return convertToModelMessages(truncated);
|
|
3566
|
+
* }
|
|
3567
|
+
* ```
|
|
3568
|
+
*/
|
|
3569
|
+
declare function truncateOlderMessages(
|
|
3570
|
+
messages: SessionMessage[],
|
|
3571
|
+
options?: TruncateOptions
|
|
3572
|
+
): SessionMessage[];
|
|
3573
|
+
//#endregion
|
|
3280
3574
|
export {
|
|
3281
3575
|
AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS,
|
|
3282
3576
|
AbortRegistry,
|
|
@@ -3305,6 +3599,7 @@ export {
|
|
|
3305
3599
|
CHAT_RECOVERY_INCIDENT_TTL_MS,
|
|
3306
3600
|
CHAT_RECOVERY_PROGRESS_KEY,
|
|
3307
3601
|
CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS,
|
|
3602
|
+
CHAT_RECOVERY_TASK_NAME,
|
|
3308
3603
|
CHAT_STREAM_PROGRESS_CREDIT_THROTTLE_MS,
|
|
3309
3604
|
type ChatConnection,
|
|
3310
3605
|
type ChatFiberSnapshot,
|
|
@@ -3316,6 +3611,7 @@ export {
|
|
|
3316
3611
|
type ChatRecoveryContext,
|
|
3317
3612
|
ChatRecoveryEngine,
|
|
3318
3613
|
type ChatRecoveryExhaustedContext,
|
|
3614
|
+
type ChatRecoveryHandoff,
|
|
3319
3615
|
type ChatRecoveryIncident,
|
|
3320
3616
|
type ChatRecoveryIncidentEvent,
|
|
3321
3617
|
type ChatRecoveryKind,
|
|
@@ -3323,10 +3619,15 @@ export {
|
|
|
3323
3619
|
type ChatRecoveryProgressContext,
|
|
3324
3620
|
type ChatRecoveryScheduleCallback,
|
|
3325
3621
|
type ChatRecoveryScheduleReason,
|
|
3622
|
+
type ChatRecoveryTaskHooks,
|
|
3623
|
+
type ChatRecoveryTaskInput,
|
|
3624
|
+
type ChatRecoveryTaskReason,
|
|
3326
3625
|
type ChatResponseResult,
|
|
3327
3626
|
ChatStreamStalledError,
|
|
3328
3627
|
type ChatStreamStatus,
|
|
3329
3628
|
type ChatTerminalRecord,
|
|
3629
|
+
type ChatTurnClosureEntry,
|
|
3630
|
+
type ChatTurnTaskHooks,
|
|
3330
3631
|
type ChunkAction,
|
|
3331
3632
|
type ChunkResult,
|
|
3332
3633
|
type ClassifyRecoveredTurnInput,
|
|
@@ -3374,7 +3675,6 @@ export {
|
|
|
3374
3675
|
ResumableStream,
|
|
3375
3676
|
ResumeHandshake,
|
|
3376
3677
|
type ResumeHandshakeHost,
|
|
3377
|
-
STREAM_CLEANUP_DELAY_SECONDS,
|
|
3378
3678
|
STREAM_RESUME_NONE_REASONS,
|
|
3379
3679
|
type SaveMessagesOptions,
|
|
3380
3680
|
type SaveMessagesResult,
|
|
@@ -3392,6 +3692,7 @@ export {
|
|
|
3392
3692
|
type TextSegmentEvent,
|
|
3393
3693
|
TextSegmentJoiner,
|
|
3394
3694
|
type ToolPartUpdate,
|
|
3695
|
+
type TruncateOptions,
|
|
3395
3696
|
TurnQueue,
|
|
3396
3697
|
type TurnResult,
|
|
3397
3698
|
aiSdkRecoveryCodec,
|
|
@@ -3404,19 +3705,23 @@ export {
|
|
|
3404
3705
|
buildInClauseStrings,
|
|
3405
3706
|
bumpChatRecoveryProgress,
|
|
3406
3707
|
byteLength,
|
|
3407
|
-
|
|
3708
|
+
chatRecoveryTaskRunOptions,
|
|
3408
3709
|
classifyAgentToolChildRecovery,
|
|
3409
|
-
cleanupStreamBuffers,
|
|
3410
3710
|
clearChatTerminal,
|
|
3411
3711
|
clientResolvableToolNames,
|
|
3412
3712
|
createAgentToolEventState,
|
|
3413
3713
|
createChatFiberSnapshot,
|
|
3714
|
+
createChatRecoveryTaskDefinition,
|
|
3715
|
+
createChatStreams,
|
|
3716
|
+
createChatTurnTaskDefinition,
|
|
3414
3717
|
createToolsFromClientSchemas,
|
|
3415
3718
|
crossMessageToolResultUpdate,
|
|
3719
|
+
dispatchChatRecoveryToHandoff,
|
|
3416
3720
|
drainInteractionApplies,
|
|
3417
3721
|
enforceRowSizeLimit,
|
|
3418
3722
|
hasIncompleteToolBatch,
|
|
3419
3723
|
interceptAgentToolBroadcast,
|
|
3724
|
+
isPlatformFailure,
|
|
3420
3725
|
isReplayChunk,
|
|
3421
3726
|
iterateWithStallWatchdog,
|
|
3422
3727
|
listActiveChatRecoveryIncidents,
|
|
@@ -3442,6 +3747,7 @@ export {
|
|
|
3442
3747
|
toolApprovalUpdate,
|
|
3443
3748
|
toolPartHasSettledResult,
|
|
3444
3749
|
toolResultUpdate,
|
|
3750
|
+
truncateOlderMessages,
|
|
3445
3751
|
unwrapChatFiberSnapshot,
|
|
3446
3752
|
wrapChatFiberSnapshot
|
|
3447
3753
|
};
|