agents 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-routing-DE5zmCQ8.d.ts → agent-routing-0DR0Tk2v.d.ts} +270 -190
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-routing.js.map +1 -1
- package/dist/agent-tool-types.d.ts +13 -7
- package/dist/agent-tool-types.js +23 -1
- package/dist/agent-tool-types.js.map +1 -1
- package/dist/{agent-tools-y7zLfw4Q.js → agent-tools-CdJC-8Dc.js} +72 -9
- package/dist/agent-tools-CdJC-8Dc.js.map +1 -0
- package/dist/{agent-tools-DtXMTDGM.d.ts → agent-tools-YrATZJu2.d.ts} +29 -15
- package/dist/agent-tools.d.ts +6 -4
- package/dist/agent-tools.js.map +1 -1
- package/dist/ai-fm7xx1BK.js +312 -0
- package/dist/ai-fm7xx1BK.js.map +1 -0
- package/dist/base64-redaction-C1Uu29t5.js +59 -0
- package/dist/base64-redaction-C1Uu29t5.js.map +1 -0
- package/dist/browser/ai-sdk.d.ts +90 -0
- package/dist/browser/ai-sdk.js +51 -0
- package/dist/browser/ai-sdk.js.map +1 -0
- package/dist/browser/ai.d.ts +4 -4
- package/dist/browser/ai.js +1 -347
- package/dist/browser/index.d.ts +81 -79
- package/dist/browser/index.js +361 -2
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/tanstack-ai.d.ts +96 -1
- package/dist/browser/tanstack-ai.js +62 -2
- package/dist/browser/tanstack-ai.js.map +1 -1
- package/dist/browser-DE3d-RLk.d.ts +275 -0
- package/dist/browser-tool-CQ10e6Kj.js +480 -0
- package/dist/browser-tool-CQ10e6Kj.js.map +1 -0
- package/dist/browser-tool-DuNdlwZ9.d.ts +73 -0
- package/dist/{capability-BjSKYpzg.js → capability-B4WbF81e.js} +2 -1
- package/dist/capability-B4WbF81e.js.map +1 -0
- package/dist/{capability-runner-Be_-PLR1.d.ts → capability-runner-DBAnGr6L.d.ts} +45 -15
- package/dist/chat/index.d.ts +341 -232
- package/dist/chat/index.js +602 -72
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/react.d.ts +42 -2
- package/dist/chat/react.js +191 -35
- package/dist/chat/react.js.map +1 -1
- package/dist/chat/transport.d.ts +1 -1
- package/dist/chat/transport.js +1 -1
- package/dist/chat-sdk/index.d.ts +17 -3
- package/dist/chat-sdk/index.js +28 -2
- package/dist/chat-sdk/index.js.map +1 -1
- package/dist/client-4OlMIJx-.js +591 -0
- package/dist/client-4OlMIJx-.js.map +1 -0
- package/dist/{client-jagG8a9_.js → client-fyQrLGkD.js} +2 -2
- package/dist/{client-jagG8a9_.js.map → client-fyQrLGkD.js.map} +1 -1
- package/dist/client-je3koD9Z.d.ts +446 -0
- package/dist/client.d.ts +29 -283
- package/dist/client.js +2 -274
- package/dist/{connector-CptFKzRh.js → connector-58XDGvc3.js} +251 -120
- package/dist/connector-58XDGvc3.js.map +1 -0
- package/dist/{connector-CkQD4MK3.d.ts → connector-Cl-VqPmA.d.ts} +244 -146
- package/dist/context/index.d.ts +29 -0
- package/dist/context/index.js +64 -17
- package/dist/context/index.js.map +1 -1
- package/dist/current-agent-DhoDkSnH.js.map +1 -1
- package/dist/current-agent-vAk_5JMt.d.ts +93 -0
- package/dist/{current-agent-Da_C9a3b.d.ts → durable-object-lifecycle-HhgOMWq9.d.ts} +26 -127
- package/dist/harnesses/pi/index.d.ts +348 -0
- package/dist/harnesses/pi/index.js +754 -0
- package/dist/harnesses/pi/index.js.map +1 -0
- package/dist/images-iiTDSyRu.js +1631 -0
- package/dist/images-iiTDSyRu.js.map +1 -0
- package/dist/index-BreL0YrQ.d.ts +78 -0
- package/dist/index-kbSUIer3.d.ts +343 -0
- package/dist/index.d.ts +43 -41
- package/dist/index.js +2 -2
- package/dist/internal_context.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +19 -17
- package/dist/lifecycle/index.js +2 -2
- package/dist/{lifecycle-CMRGjZdw.js → lifecycle-CFu6OV03.js} +73 -29
- package/dist/lifecycle-CFu6OV03.js.map +1 -0
- package/dist/mcp/client/index.d.ts +20 -20
- package/dist/mcp/client/index.js +1 -1
- package/dist/mcp/client/x402.js +21 -19
- package/dist/mcp/client/x402.js.map +1 -1
- package/dist/mcp/index.d.ts +35 -35
- package/dist/mcp/index.js +2 -2
- package/dist/{message-builder-BymO4N_D.js → message-builder-DAl9AhHH.js} +89 -4
- package/dist/message-builder-DAl9AhHH.js.map +1 -0
- package/dist/models/ai-sdk/index.d.ts +353 -0
- package/dist/models/ai-sdk/index.js +2443 -0
- package/dist/models/ai-sdk/index.js.map +1 -0
- package/dist/models/pi-ai/index.d.ts +313 -0
- package/dist/models/pi-ai/index.js +1795 -0
- package/dist/models/pi-ai/index.js.map +1 -0
- package/dist/observability/ai/index.js +20 -4
- package/dist/observability/ai/index.js.map +1 -1
- package/dist/{protocol-B0nh6KNf.js → protocol-kHsr-f0X.js} +26 -12
- package/dist/{protocol-B0nh6KNf.js.map → protocol-kHsr-f0X.js.map} +1 -1
- package/dist/queue/index.d.ts +123 -0
- package/dist/queue/index.js +2 -0
- package/dist/queue-BB-zfIYF.js +406 -0
- package/dist/queue-BB-zfIYF.js.map +1 -0
- package/dist/react.d.ts +31 -13
- package/dist/react.js +190 -71
- package/dist/react.js.map +1 -1
- package/dist/{retries-D9Ds-1lz.d.ts → retries-CV6AkXmQ.d.ts} +23 -8
- package/dist/retries.d.ts +12 -8
- package/dist/retries.js +19 -1
- package/dist/retries.js.map +1 -1
- package/dist/routing/index.d.ts +2 -2
- package/dist/routing/index.js +1 -1
- package/dist/{scheduler-DD9NdYbF.js → scheduler-DZ3xQfeK.js} +4 -11
- package/dist/scheduler-DZ3xQfeK.js.map +1 -0
- package/dist/{scheduler-Dwh85ZGl.d.ts → scheduler-RdACNXDP.d.ts} +6 -6
- package/dist/schedules/index.d.ts +1 -1
- package/dist/schedules/index.js +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/sessions/index.d.ts +70 -3
- package/dist/sessions/index.js +276 -67
- package/dist/sessions/index.js.map +1 -1
- package/dist/skills/index.d.ts +29 -314
- package/dist/socket-address.d.ts +16 -0
- package/dist/socket-address.js +19 -0
- package/dist/socket-address.js.map +1 -0
- package/dist/{src-DlSHshb2.js → src-BZeu-SCD.js} +541 -500
- package/dist/src-BZeu-SCD.js.map +1 -0
- package/dist/state/index.d.ts +6 -0
- package/dist/state/index.js +137 -0
- package/dist/state/index.js.map +1 -0
- package/dist/streams/index.d.ts +1 -1
- package/dist/streams/index.js +1 -1
- package/dist/{streams-D6tJ0NN9.d.ts → streams-Cmkj9QfW.d.ts} +4 -2
- package/dist/{streams-DZKgAj9b.js → streams-DW-UCOUc.js} +9 -2
- package/dist/streams-DW-UCOUc.js.map +1 -0
- package/dist/sub-routing.d.ts +12 -12
- package/dist/tasks/index.d.ts +1 -1
- package/dist/tasks/index.js +1 -1
- package/dist/{tasks-ylZgBjhj.js → tasks-68DESxf0.js} +3 -3
- package/dist/{tasks-ylZgBjhj.js.map → tasks-68DESxf0.js.map} +1 -1
- package/dist/{tasks-BRJ5zgya.d.ts → tasks-9YxYtByh.d.ts} +5 -5
- package/dist/transport-PfFnt-Ld.d.ts +525 -0
- package/dist/transport-protocol-BxyngQ_R.js +25 -0
- package/dist/transport-protocol-BxyngQ_R.js.map +1 -0
- package/dist/transport-protocol-ZTew5Wso.d.ts +27 -0
- package/dist/types-BA4anrjI.d.ts +70 -0
- package/dist/voice/index.d.ts +2 -2
- package/dist/websockets/index.d.ts +195 -69
- package/dist/websockets/index.js +3 -2
- package/dist/websockets-Ci-LZFHH.js +1030 -0
- package/dist/websockets-Ci-LZFHH.js.map +1 -0
- package/dist/wire-types-CnMt6_HR.js.map +1 -1
- package/dist/wire-types-CrjKvDyi.d.ts +300 -0
- package/dist/workflow-types.d.ts +25 -25
- package/dist/workflows.d.ts +21 -21
- package/dist/workflows.js +1 -1
- package/dist/{ws-chat-transport-UNRIS2xl.d.ts → ws-chat-transport-BbT1A5Lc.d.ts} +51 -2
- package/dist/{ws-chat-transport-rWwta645.js → ws-chat-transport-cqMKq3IY.js} +143 -16
- package/dist/ws-chat-transport-cqMKq3IY.js.map +1 -0
- package/docs/agent-class.md +2 -2
- package/docs/agent-tools.md +29 -4
- package/docs/browse-the-web.md +87 -5
- package/docs/chat-agents.md +124 -16
- package/docs/client-sdk.md +30 -0
- package/docs/context.md +27 -0
- package/docs/durable-execution.md +2 -0
- package/docs/harnesses/pi.md +91 -0
- package/docs/human-in-the-loop.md +4 -0
- package/docs/index.md +4 -1
- package/docs/lifecycle.md +114 -23
- package/docs/models-pi-ai.md +325 -0
- package/docs/models.md +504 -0
- package/docs/queue.md +135 -241
- package/docs/resumable-streaming.md +2 -0
- package/docs/retries.md +1 -1
- package/docs/routing.md +13 -13
- package/docs/sessions.md +36 -1
- package/docs/state.md +1 -0
- package/docs/sub-agents.md +12 -3
- package/package.json +51 -3
- package/dist/agent-tools-y7zLfw4Q.js.map +0 -1
- package/dist/browser/ai.js.map +0 -1
- package/dist/capability-BjSKYpzg.js.map +0 -1
- package/dist/client-tools-aIBO0Fk7.d.ts +0 -53
- package/dist/client.js.map +0 -1
- package/dist/connector-CptFKzRh.js.map +0 -1
- package/dist/lifecycle-CMRGjZdw.js.map +0 -1
- package/dist/message-builder-BymO4N_D.js.map +0 -1
- package/dist/scheduler-DD9NdYbF.js.map +0 -1
- package/dist/src-DlSHshb2.js.map +0 -1
- package/dist/streams-DZKgAj9b.js.map +0 -1
- package/dist/websockets-DUfRHPRq.js +0 -502
- package/dist/websockets-DUfRHPRq.js.map +0 -1
- package/dist/ws-chat-transport-rWwta645.js.map +0 -1
package/dist/chat/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { i as
|
|
1
|
+
import { isPositionlessAgentToolChunk } from "../agent-tool-types.js";
|
|
2
|
+
import { isDurableObjectResetError, isPlatformFailure, tryN } from "../retries.js";
|
|
3
|
+
import { a as isReplayChunk, i as isLateToolInputChunk, n as applyLateToolInput, o as lateToolInputForwardChunks, r as getPartialStreamText, s as normalizeToolInput, t as applyChunkToParts } from "../message-builder-DAl9AhHH.js";
|
|
4
|
+
import { a as interceptAgentToolBroadcast, i as createAgentToolEventState, r as applyAgentToolEvent, s as storedChunkBytes, t as AgentToolProgressEmitter } from "../agent-tools-CdJC-8Dc.js";
|
|
4
5
|
import { t as TextSegmentJoiner } from "../text-segment-joiner-BtAFQSA_.js";
|
|
5
6
|
import { n as sanitizeMessage, t as byteLength } from "../sanitize-D9TujEK8.js";
|
|
6
|
-
import {
|
|
7
|
-
import { n as Streams } from "../streams-
|
|
7
|
+
import { a as StreamAccumulator, i as transition, n as STREAM_RESUME_NONE_REASONS, t as CHAT_MESSAGE_TYPES } from "../protocol-kHsr-f0X.js";
|
|
8
|
+
import { n as Streams } from "../streams-DW-UCOUc.js";
|
|
8
9
|
import { t as MessageType } from "../wire-types-CnMt6_HR.js";
|
|
9
10
|
import { jsonSchema, tool } from "ai";
|
|
10
11
|
import { nanoid } from "nanoid";
|
|
@@ -262,6 +263,36 @@ var TurnQueue = class {
|
|
|
262
263
|
}
|
|
263
264
|
};
|
|
264
265
|
//#endregion
|
|
266
|
+
//#region src/chat/origin-message-ids.ts
|
|
267
|
+
/**
|
|
268
|
+
* IDs of the user messages a chat request originated from: the trailing run of
|
|
269
|
+
* user messages in its `messages` (one for a plain send or a regenerate,
|
|
270
|
+
* several when queued sends arrive together). Terminal response frames echo
|
|
271
|
+
* them as `messageIds` (#2280).
|
|
272
|
+
*/
|
|
273
|
+
function originMessageIds(messages) {
|
|
274
|
+
if (!Array.isArray(messages)) return void 0;
|
|
275
|
+
const ids = [];
|
|
276
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
277
|
+
const message = messages[i];
|
|
278
|
+
if (message?.role !== "user") break;
|
|
279
|
+
if (typeof message.id === "string" && message.id) ids.unshift(message.id);
|
|
280
|
+
}
|
|
281
|
+
return ids.length > 0 ? ids : void 0;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Return `frame` with `messageIds` added when it is a terminal
|
|
285
|
+
* (`done` or `error`) frame that does not already carry them.
|
|
286
|
+
*/
|
|
287
|
+
function withOriginMessageIds(frame, messageIds) {
|
|
288
|
+
const fields = frame;
|
|
289
|
+
if (!messageIds || fields.messageIds !== void 0 || !(fields.done || fields.error)) return frame;
|
|
290
|
+
return {
|
|
291
|
+
...frame,
|
|
292
|
+
messageIds
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
//#endregion
|
|
265
296
|
//#region src/chat/submit-concurrency.ts
|
|
266
297
|
var SubmitConcurrencyController = class {
|
|
267
298
|
constructor(options) {
|
|
@@ -410,13 +441,15 @@ function isWebSocketClosedSendError(error) {
|
|
|
410
441
|
* @returns False when the connection closed mid-replay — the caller leaves
|
|
411
442
|
* its stream state untouched so the next reconnect can retry.
|
|
412
443
|
*/
|
|
413
|
-
function sendReplayBodies(connection, requestId, bodies, continuation) {
|
|
444
|
+
function sendReplayBodies(connection, requestId, bodies, continuation, firstSeq = 0) {
|
|
445
|
+
let seq = firstSeq;
|
|
414
446
|
for (const body of bodies) if (!sendIfOpen(connection, JSON.stringify({
|
|
415
447
|
body,
|
|
416
448
|
done: false,
|
|
417
449
|
id: requestId,
|
|
418
450
|
type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,
|
|
419
451
|
replay: true,
|
|
452
|
+
seq: seq++,
|
|
420
453
|
...continuation && { continuation: true }
|
|
421
454
|
}))) return false;
|
|
422
455
|
return true;
|
|
@@ -435,7 +468,9 @@ function sendReplayControl(connection, requestId, options) {
|
|
|
435
468
|
type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,
|
|
436
469
|
replay: true,
|
|
437
470
|
...options.replayComplete && { replayComplete: true },
|
|
438
|
-
...options.continuation && { continuation: true }
|
|
471
|
+
...options.continuation && { continuation: true },
|
|
472
|
+
...options.done && options.messageIds && { messageIds: options.messageIds },
|
|
473
|
+
...options.done && options.outcome && { outcome: options.outcome }
|
|
439
474
|
}));
|
|
440
475
|
}
|
|
441
476
|
//#endregion
|
|
@@ -492,20 +527,18 @@ const REPLAY_PAGE_SEGMENTS = 10;
|
|
|
492
527
|
* the newest chunk's timestamp for rows past it — so a long but still-active
|
|
493
528
|
* stream is never reclaimed mid-flight. Terminal rows carry no such window:
|
|
494
529
|
* a stream that finished is redundant with its persisted message, and the
|
|
495
|
-
* cutover deletes it in the same transaction;
|
|
496
|
-
*
|
|
530
|
+
* cutover deletes it in the same transaction; leftovers are reclaimed by
|
|
531
|
+
* the next {@link ResumableStream.start}.
|
|
497
532
|
*/
|
|
498
533
|
const ABANDONED_STREAM_RETENTION_MS = 3600 * 1e3;
|
|
499
|
-
/**
|
|
500
|
-
const
|
|
534
|
+
/** Deleted streams whose terminal details a late resume ACK can still read. */
|
|
535
|
+
const MAX_REMEMBERED_DELETED_TERMINALS = 32;
|
|
501
536
|
/**
|
|
502
537
|
* Ceiling for one stored chat segment after JSON serialization, and the
|
|
503
538
|
* `maxChunkBytes` the backing Streams capability must be constructed with.
|
|
504
539
|
* Kept under the 2 MB SQLite row limit with headroom for escaping.
|
|
505
540
|
*/
|
|
506
541
|
const CHAT_STREAM_MAX_CHUNK_BYTES = 19e5;
|
|
507
|
-
/** Maximum serialized chunk body size before skipping storage (bytes). */
|
|
508
|
-
const CHUNK_MAX_BYTES = 18e5;
|
|
509
542
|
/**
|
|
510
543
|
* Construct the Streams capability instance a chat host must install to back
|
|
511
544
|
* its `ResumableStream`: identical to `new Streams()` except for the raised
|
|
@@ -548,10 +581,13 @@ var ResumableStream = class {
|
|
|
548
581
|
this._activeRequestId = null;
|
|
549
582
|
this._isLive = false;
|
|
550
583
|
this._activeIsContinuation = false;
|
|
584
|
+
this._nextChunkSeq = null;
|
|
551
585
|
this._chunkBuffer = [];
|
|
552
586
|
this._chunkBufferBytes = 0;
|
|
553
587
|
this._isFlushingChunks = false;
|
|
554
588
|
this._pendingCutover = null;
|
|
589
|
+
this._lastClosedStreamId = null;
|
|
590
|
+
this._deletedTerminals = /* @__PURE__ */ new Map();
|
|
555
591
|
this.ops = streams.__DO_NOT_USE_WILL_BREAK__sync();
|
|
556
592
|
this.ops.ensureTables();
|
|
557
593
|
this._sql = sql;
|
|
@@ -559,7 +595,10 @@ var ResumableStream = class {
|
|
|
559
595
|
this._ensureProgressTable();
|
|
560
596
|
deletionHooks.get(streams)?.();
|
|
561
597
|
deletionHooks.set(streams, this.ops.onDelete((row, cursor) => {
|
|
562
|
-
|
|
598
|
+
const chat = parseChatMetadata(row);
|
|
599
|
+
if (!chat) return;
|
|
600
|
+
this._retire(cursor);
|
|
601
|
+
this._rememberDeleted(row, chat);
|
|
563
602
|
}));
|
|
564
603
|
this._migrateLegacyTables(sql);
|
|
565
604
|
this.restore();
|
|
@@ -759,18 +798,34 @@ var ResumableStream = class {
|
|
|
759
798
|
*/
|
|
760
799
|
start(requestId, options = {}) {
|
|
761
800
|
this.flushBuffer();
|
|
801
|
+
const seqBase = this._nextSeqForRequest(requestId);
|
|
762
802
|
this.reclaim();
|
|
763
803
|
const streamId = nanoid();
|
|
764
804
|
this._activeStreamId = streamId;
|
|
765
805
|
this._activeRequestId = requestId;
|
|
766
806
|
this._isLive = true;
|
|
767
807
|
this._activeIsContinuation = options.continuation ?? false;
|
|
808
|
+
this._nextChunkSeq = seqBase;
|
|
768
809
|
const metadata = { cfChat: 1 };
|
|
769
810
|
if (options.messageId != null) metadata.messageId = options.messageId;
|
|
811
|
+
if (options.parentMessageId != null) metadata.parentMessageId = options.parentMessageId;
|
|
770
812
|
if (this._activeIsContinuation) metadata.isContinuation = 1;
|
|
813
|
+
if (seqBase > 0) metadata.seqBase = seqBase;
|
|
814
|
+
if (options.originMessageIds?.length) metadata.originMessageIds = options.originMessageIds;
|
|
771
815
|
this.ops.insertStream(streamId, requestId, metadata);
|
|
772
816
|
return streamId;
|
|
773
817
|
}
|
|
818
|
+
_nextSeqForRequest(requestId) {
|
|
819
|
+
const prior = this._latestChatRowByTag(requestId);
|
|
820
|
+
if (!prior) return 0;
|
|
821
|
+
let count = 0;
|
|
822
|
+
for (const _body of this._storedBodies(prior.stream_id)) count++;
|
|
823
|
+
return (parseChatMetadata(prior)?.seqBase ?? 0) + count;
|
|
824
|
+
}
|
|
825
|
+
_seqBase(streamId) {
|
|
826
|
+
const row = this.ops.getStream(streamId);
|
|
827
|
+
return row && parseChatMetadata(row)?.seqBase || 0;
|
|
828
|
+
}
|
|
774
829
|
/**
|
|
775
830
|
* The assistant message id an orphaned stream was producing — the same id the
|
|
776
831
|
* live path persists under, so recovery re-associates reconstructed chunks
|
|
@@ -783,13 +838,65 @@ var ResumableStream = class {
|
|
|
783
838
|
return parseChatMetadata(row)?.messageId ?? null;
|
|
784
839
|
}
|
|
785
840
|
/**
|
|
841
|
+
* The message an orphaned stream's assistant message is a child of, or null
|
|
842
|
+
* when the stream appends to the latest leaf or predates parent tracking.
|
|
843
|
+
*/
|
|
844
|
+
getStreamParentMessageId(streamId) {
|
|
845
|
+
const row = this.ops.getStream(streamId);
|
|
846
|
+
if (!row) return null;
|
|
847
|
+
return parseChatMetadata(row)?.parentMessageId ?? null;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* The user message ids the request's latest chat stream was started for
|
|
851
|
+
* (#2280), or undefined when no stream recorded them.
|
|
852
|
+
*/
|
|
853
|
+
getOriginMessageIds(requestId) {
|
|
854
|
+
const row = this._latestChatRowByTag(requestId);
|
|
855
|
+
return (row ? parseChatMetadata(row)?.originMessageIds : void 0) ?? this._deletedTerminals.get(requestId)?.messageIds;
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* How the request's latest chat stream ended (recorded by {@link complete}
|
|
859
|
+
* or {@link finish}), or undefined for a normal completion.
|
|
860
|
+
*/
|
|
861
|
+
getOutcome(requestId) {
|
|
862
|
+
const row = this._latestChatRowByTag(requestId);
|
|
863
|
+
return (row ? parseChatMetadata(row)?.outcome : void 0) ?? this._deletedTerminals.get(requestId)?.outcome;
|
|
864
|
+
}
|
|
865
|
+
_rememberDeleted(row, chat) {
|
|
866
|
+
if (!row.tag || !chat.originMessageIds && !chat.outcome) return;
|
|
867
|
+
this._deletedTerminals.delete(row.tag);
|
|
868
|
+
this._deletedTerminals.set(row.tag, {
|
|
869
|
+
messageIds: chat.originMessageIds,
|
|
870
|
+
outcome: chat.outcome
|
|
871
|
+
});
|
|
872
|
+
if (this._deletedTerminals.size > MAX_REMEMBERED_DELETED_TERMINALS) {
|
|
873
|
+
const oldest = this._deletedTerminals.keys().next().value;
|
|
874
|
+
if (oldest !== void 0) this._deletedTerminals.delete(oldest);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
_recordOutcome(streamId, outcome) {
|
|
878
|
+
if (outcome === void 0 || outcome === "completed") return;
|
|
879
|
+
const row = this.ops.getStream(streamId);
|
|
880
|
+
const chat = row ? parseChatMetadata(row) : null;
|
|
881
|
+
if (!chat) return;
|
|
882
|
+
this.ops.setMetadata(streamId, {
|
|
883
|
+
...chat,
|
|
884
|
+
outcome
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
786
888
|
* Mark a stream as completed and flush any pending chunks.
|
|
787
889
|
* @param streamId - The stream to mark as completed
|
|
890
|
+
* @param outcome - How the request ended, when not a normal completion.
|
|
891
|
+
* Defaults to `aborted` for a stream restored without a live reader.
|
|
788
892
|
*/
|
|
789
|
-
complete(streamId) {
|
|
893
|
+
complete(streamId, outcome) {
|
|
790
894
|
this.flushBuffer();
|
|
895
|
+
const orphaned = streamId === this._activeStreamId && !this._isLive;
|
|
896
|
+
this._recordOutcome(streamId, outcome ?? (orphaned ? "aborted" : void 0));
|
|
791
897
|
this.ops.settle(streamId, "completed", null);
|
|
792
898
|
if (this._pendingCutover === streamId) this._pendingCutover = null;
|
|
899
|
+
this._lastClosedStreamId = streamId;
|
|
793
900
|
this._clearActive();
|
|
794
901
|
}
|
|
795
902
|
/**
|
|
@@ -799,9 +906,11 @@ var ResumableStream = class {
|
|
|
799
906
|
* live — exactly the evidence recovery rebuilds the message from. The
|
|
800
907
|
* host MUST follow with {@link cutover} or {@link finalizePending}.
|
|
801
908
|
*/
|
|
802
|
-
finish(streamId) {
|
|
909
|
+
finish(streamId, outcome) {
|
|
803
910
|
this.flushBuffer();
|
|
911
|
+
this._recordOutcome(streamId, outcome);
|
|
804
912
|
this._pendingCutover = streamId;
|
|
913
|
+
this._lastClosedStreamId = streamId;
|
|
805
914
|
this._clearActive();
|
|
806
915
|
}
|
|
807
916
|
/** The stream {@link finish}ed and awaiting its cutover, if any. */
|
|
@@ -814,29 +923,33 @@ var ResumableStream = class {
|
|
|
814
923
|
* crash leaves either the live stream or the finished message, never
|
|
815
924
|
* neither; nothing is left to sweep. `discard: false` keeps the settled
|
|
816
925
|
* rows (an agent-tool child whose parent still tails them); they are
|
|
817
|
-
* reclaimed by the next {@link start}.
|
|
926
|
+
* reclaimed by the next {@link start}. The settlement and `persist`
|
|
927
|
+
* writes commit or roll back together.
|
|
818
928
|
*/
|
|
819
929
|
cutover(streamId, persist, options = {}) {
|
|
820
930
|
this.flushBuffer();
|
|
821
931
|
const discard = options.discard ?? true;
|
|
932
|
+
const commit = persist;
|
|
822
933
|
const settled = this.ops.settle(streamId, "completed", null, {
|
|
823
|
-
commit
|
|
934
|
+
commit,
|
|
824
935
|
discard
|
|
825
936
|
});
|
|
826
937
|
if (settled && discard) this._notifyProgress();
|
|
827
|
-
if (!settled)
|
|
938
|
+
if (!settled) commit();
|
|
828
939
|
if (this._pendingCutover === streamId) this._pendingCutover = null;
|
|
829
940
|
this._clearActive();
|
|
830
941
|
}
|
|
831
942
|
/**
|
|
832
943
|
* Settle a {@link finish}ed stream that had nothing to persist (no parts,
|
|
833
944
|
* a persist that threw). Idempotent; a no-op when nothing is pending.
|
|
945
|
+
* The pending marker clears only once settlement succeeds, so a caller
|
|
946
|
+
* may retry after a settlement failure — matching {@link cutover}.
|
|
834
947
|
*/
|
|
835
948
|
finalizePending() {
|
|
836
949
|
const streamId = this._pendingCutover;
|
|
837
950
|
if (streamId === null) return;
|
|
838
|
-
this._pendingCutover = null;
|
|
839
951
|
this.ops.settle(streamId, "completed", null);
|
|
952
|
+
this._pendingCutover = null;
|
|
840
953
|
}
|
|
841
954
|
_clearActive() {
|
|
842
955
|
this._activeStreamId = null;
|
|
@@ -852,6 +965,7 @@ var ResumableStream = class {
|
|
|
852
965
|
this.flushBuffer();
|
|
853
966
|
this.ops.settle(streamId, "errored", null);
|
|
854
967
|
if (this._pendingCutover === streamId) this._pendingCutover = null;
|
|
968
|
+
this._lastClosedStreamId = streamId;
|
|
855
969
|
this._clearActive();
|
|
856
970
|
}
|
|
857
971
|
/**
|
|
@@ -861,13 +975,17 @@ var ResumableStream = class {
|
|
|
861
975
|
* but will be missing from replay on reconnection.
|
|
862
976
|
* @param streamId - The stream this chunk belongs to
|
|
863
977
|
* @param body - The serialized chunk body
|
|
978
|
+
* @returns The chunk's index in a replay of the stream, for the live
|
|
979
|
+
* broadcast to carry as `seq`; `undefined` when the chunk is not stored
|
|
980
|
+
* or the stream's count is not tracked.
|
|
864
981
|
*/
|
|
865
982
|
storeChunk(streamId, body) {
|
|
866
|
-
const bodyBytes =
|
|
867
|
-
if (bodyBytes >
|
|
983
|
+
const bodyBytes = storedChunkBytes(body);
|
|
984
|
+
if (bodyBytes > 18e5) {
|
|
868
985
|
console.warn(`[ResumableStream] Skipping oversized chunk (${bodyBytes} bytes) to prevent SQLite row limit crash. Live clients still receive it.`);
|
|
869
986
|
return;
|
|
870
987
|
}
|
|
988
|
+
const seq = streamId === this._activeStreamId && this._nextChunkSeq !== null ? this._nextChunkSeq++ : void 0;
|
|
871
989
|
if (this._chunkBuffer.length >= CHUNK_BUFFER_MAX_SIZE) this.flushBuffer();
|
|
872
990
|
if (this._chunkBuffer.length > 0 && this._chunkBufferBytes + bodyBytes > SEGMENT_MAX_BYTES) this.flushBuffer();
|
|
873
991
|
this._chunkBuffer.push({
|
|
@@ -876,6 +994,7 @@ var ResumableStream = class {
|
|
|
876
994
|
});
|
|
877
995
|
this._chunkBufferBytes += bodyBytes;
|
|
878
996
|
if (this._chunkBuffer.length >= CHUNK_BUFFER_SIZE) this.flushBuffer();
|
|
997
|
+
return seq;
|
|
879
998
|
}
|
|
880
999
|
/**
|
|
881
1000
|
* Flush the buffered chunks to storage as a single packed segment.
|
|
@@ -946,13 +1065,16 @@ var ResumableStream = class {
|
|
|
946
1065
|
if (!streamId) return null;
|
|
947
1066
|
this.flushBuffer();
|
|
948
1067
|
const continuation = this._activeIsContinuation;
|
|
949
|
-
if (!sendReplayBodies(connection, requestId, this._storedBodies(streamId), continuation)) return null;
|
|
1068
|
+
if (!sendReplayBodies(connection, requestId, this._storedBodies(streamId), continuation, this._seqBase(streamId))) return null;
|
|
950
1069
|
if (!this._isLive) {
|
|
1070
|
+
const row = this.ops.getStream(streamId);
|
|
951
1071
|
sendReplayControl(connection, requestId, {
|
|
952
1072
|
done: true,
|
|
953
|
-
continuation
|
|
1073
|
+
continuation,
|
|
1074
|
+
messageIds: row ? parseChatMetadata(row)?.originMessageIds : void 0,
|
|
1075
|
+
outcome: "aborted"
|
|
954
1076
|
});
|
|
955
|
-
this.complete(streamId);
|
|
1077
|
+
this.complete(streamId, "aborted");
|
|
956
1078
|
return streamId;
|
|
957
1079
|
}
|
|
958
1080
|
sendReplayControl(connection, requestId, {
|
|
@@ -975,10 +1097,33 @@ var ResumableStream = class {
|
|
|
975
1097
|
this.flushBuffer();
|
|
976
1098
|
const row = this._latestChatRowByTag(requestId, "completed");
|
|
977
1099
|
if (!row) return false;
|
|
978
|
-
const
|
|
979
|
-
|
|
1100
|
+
const chat = parseChatMetadata(row);
|
|
1101
|
+
const continuation = chat?.isContinuation === 1;
|
|
1102
|
+
if (!sendReplayBodies(connection, requestId, this._storedBodies(row.stream_id), continuation, chat?.seqBase)) return false;
|
|
980
1103
|
return sendReplayControl(connection, requestId, {
|
|
981
1104
|
done: true,
|
|
1105
|
+
continuation,
|
|
1106
|
+
messageIds: chat?.originMessageIds,
|
|
1107
|
+
outcome: chat?.outcome
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Replay the request's just-closed stream (finished and awaiting its
|
|
1112
|
+
* cutover, completed for a recovery, or errored), ending in
|
|
1113
|
+
* `replayComplete` rather than `done`: the host has not sent the request's
|
|
1114
|
+
* terminal frame yet and delivers it live once the message is persisted.
|
|
1115
|
+
* After the cutover deleted the rows, only the `replayComplete` is sent.
|
|
1116
|
+
* @returns False when the connection closed mid-replay.
|
|
1117
|
+
*/
|
|
1118
|
+
replayClosedStreamChunks(connection, requestId) {
|
|
1119
|
+
this.flushBuffer();
|
|
1120
|
+
const row = this._lastClosedStreamId !== null ? this.ops.getStream(this._lastClosedStreamId) : void 0;
|
|
1121
|
+
const chat = row?.tag === requestId ? parseChatMetadata(row) : null;
|
|
1122
|
+
const continuation = chat?.isContinuation === 1;
|
|
1123
|
+
if (row && chat && !sendReplayBodies(connection, requestId, this._storedBodies(row.stream_id), continuation, chat.seqBase)) return false;
|
|
1124
|
+
return sendReplayControl(connection, requestId, {
|
|
1125
|
+
done: false,
|
|
1126
|
+
replayComplete: true,
|
|
982
1127
|
continuation
|
|
983
1128
|
});
|
|
984
1129
|
}
|
|
@@ -1000,7 +1145,8 @@ var ResumableStream = class {
|
|
|
1000
1145
|
this.flushBuffer();
|
|
1001
1146
|
const row = this._latestChatRowByTag(requestId, "errored");
|
|
1002
1147
|
if (!row) return true;
|
|
1003
|
-
|
|
1148
|
+
const chat = parseChatMetadata(row);
|
|
1149
|
+
return sendReplayBodies(connection, requestId, this._storedBodies(row.stream_id), chat?.isContinuation === 1, chat?.seqBase);
|
|
1004
1150
|
}
|
|
1005
1151
|
/**
|
|
1006
1152
|
* Latest chat stream row for a request regardless of status — the recovery
|
|
@@ -1049,6 +1195,7 @@ var ResumableStream = class {
|
|
|
1049
1195
|
this._activeStreamId = row.stream_id;
|
|
1050
1196
|
this._activeRequestId = row.tag;
|
|
1051
1197
|
this._activeIsContinuation = row.chat.isContinuation === 1;
|
|
1198
|
+
this._nextChunkSeq = null;
|
|
1052
1199
|
}
|
|
1053
1200
|
}
|
|
1054
1201
|
/**
|
|
@@ -1059,6 +1206,8 @@ var ResumableStream = class {
|
|
|
1059
1206
|
this._chunkBuffer = [];
|
|
1060
1207
|
this._chunkBufferBytes = 0;
|
|
1061
1208
|
this._deleteRetiring(this._chatRows());
|
|
1209
|
+
this._deletedTerminals.clear();
|
|
1210
|
+
this._lastClosedStreamId = null;
|
|
1062
1211
|
this._activeStreamId = null;
|
|
1063
1212
|
this._activeRequestId = null;
|
|
1064
1213
|
this._activeIsContinuation = false;
|
|
@@ -2207,23 +2356,83 @@ function parseProtocolMessage(raw) {
|
|
|
2207
2356
|
/**
|
|
2208
2357
|
* Reconcile incoming client messages against server state.
|
|
2209
2358
|
*
|
|
2210
|
-
* 1.
|
|
2211
|
-
*
|
|
2212
|
-
*
|
|
2359
|
+
* 1. Reconciles assistant IDs: exact match → same tool call → content-key
|
|
2360
|
+
* match. Each server row is claimed at most once, and a tool-call match
|
|
2361
|
+
* requires the same toolCallId, tool and input, since providers may reuse
|
|
2362
|
+
* toolCallIds across turns.
|
|
2363
|
+
* 2. Merges server-known tool outputs into incoming messages that still
|
|
2364
|
+
* show stale states (input-available, approval-requested, approval-responded).
|
|
2365
|
+
* Outputs come only from the server row the message resolved to.
|
|
2366
|
+
* 3. Drops a stale copy of an assistant the same submit also echoes under its
|
|
2367
|
+
* stored ID (see {@link dropStaleToolCopies}).
|
|
2213
2368
|
*
|
|
2214
2369
|
* @param incoming - Messages from the client
|
|
2215
2370
|
* @param serverMessages - Current server-side messages (source of truth)
|
|
2216
2371
|
* @param sanitizeForContentKey - Function to sanitize a message before computing
|
|
2217
|
-
* its content key
|
|
2372
|
+
* its content key or comparing its tool calls against stored rows
|
|
2373
|
+
* (typically the host's persistence sanitizer, so a tool input the host
|
|
2374
|
+
* truncates on write compares equal to its stored form)
|
|
2218
2375
|
* @returns Reconciled messages ready for persistence
|
|
2219
2376
|
*/
|
|
2220
2377
|
function reconcileMessages(incoming, serverMessages, sanitizeForContentKey) {
|
|
2221
|
-
return
|
|
2378
|
+
return dropStaleToolCopies(mergeServerToolOutputs(reconcileAssistantIds(incoming, serverMessages, sanitizeForContentKey), serverMessages), serverMessages, sanitizeForContentKey);
|
|
2379
|
+
}
|
|
2380
|
+
/**
|
|
2381
|
+
* Drop a stale client copy of an assistant that the same submit also echoes
|
|
2382
|
+
* under its stored ID. Kept, the copy persists as a second row carrying the
|
|
2383
|
+
* same toolCallIds and reaches the next prompt as a duplicate tool call, which
|
|
2384
|
+
* providers that issue unique IDs reject.
|
|
2385
|
+
*
|
|
2386
|
+
* A message is dropped only when it claimed no server row, is not the last
|
|
2387
|
+
* submitted message (a new call awaiting its result sits there), and consists
|
|
2388
|
+
* solely of `step-start` parts and pending tool parts each matching the same
|
|
2389
|
+
* call already settled on a server row this submit claimed. Anything else is
|
|
2390
|
+
* kept.
|
|
2391
|
+
*/
|
|
2392
|
+
function dropStaleToolCopies(reconciled, serverMessages, sanitize) {
|
|
2393
|
+
const reconciledIds = new Set(reconciled.map((msg) => msg.id));
|
|
2394
|
+
const serverIds = /* @__PURE__ */ new Set();
|
|
2395
|
+
const settledOnClaimed = /* @__PURE__ */ new Map();
|
|
2396
|
+
for (const msg of serverMessages) {
|
|
2397
|
+
serverIds.add(msg.id);
|
|
2398
|
+
if (msg.role !== "assistant" || !reconciledIds.has(msg.id)) continue;
|
|
2399
|
+
for (const part of msg.parts) {
|
|
2400
|
+
const record = part;
|
|
2401
|
+
if (!isResolvedToolPart(record)) continue;
|
|
2402
|
+
const toolCallId = record.toolCallId;
|
|
2403
|
+
const settled = settledOnClaimed.get(toolCallId);
|
|
2404
|
+
if (settled) settled.push(record);
|
|
2405
|
+
else settledOnClaimed.set(toolCallId, [record]);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
if (settledOnClaimed.size === 0) return reconciled;
|
|
2409
|
+
const lastIndex = reconciled.length - 1;
|
|
2410
|
+
const kept = reconciled.filter((msg, index) => {
|
|
2411
|
+
if (index === lastIndex) return true;
|
|
2412
|
+
if (msg.role !== "assistant" || serverIds.has(msg.id)) return true;
|
|
2413
|
+
const comparable = sanitize ? sanitize(msg) : msg;
|
|
2414
|
+
let hasToolPart = false;
|
|
2415
|
+
for (const part of comparable.parts) {
|
|
2416
|
+
const record = part;
|
|
2417
|
+
if (record.type === "step-start") continue;
|
|
2418
|
+
if (typeof record.toolCallId !== "string" || !(isPendingToolPart(record) || record.state === "input-streaming")) return true;
|
|
2419
|
+
if (!settledOnClaimed.get(record.toolCallId)?.some((candidate) => sameToolCall(candidate, record))) return true;
|
|
2420
|
+
hasToolPart = true;
|
|
2421
|
+
}
|
|
2422
|
+
return !hasToolPart;
|
|
2423
|
+
});
|
|
2424
|
+
return kept.length === reconciled.length ? reconciled : kept;
|
|
2222
2425
|
}
|
|
2223
2426
|
/**
|
|
2224
2427
|
* For a single message, resolve its ID by matching toolCallId against server state.
|
|
2225
2428
|
* Prevents duplicate DB rows when client IDs differ from server IDs.
|
|
2226
|
-
*
|
|
2429
|
+
*
|
|
2430
|
+
* @deprecated Unsafe when a provider reuses a toolCallId across turns. This
|
|
2431
|
+
* scans the whole conversation and claims nothing, so a later assistant can
|
|
2432
|
+
* adopt an earlier row's ID and overwrite it on upsert (#1992). Use
|
|
2433
|
+
* {@link reconcileMessages}, which claims server rows one-to-one over the
|
|
2434
|
+
* whole transcript. Retained only for backwards compatibility; no longer used
|
|
2435
|
+
* by `@cloudflare/ai-chat` or `@cloudflare/think`.
|
|
2227
2436
|
*/
|
|
2228
2437
|
function resolveToolMergeId(message, serverMessages) {
|
|
2229
2438
|
if (message.role !== "assistant") return message;
|
|
@@ -2289,23 +2498,28 @@ function assistantContentKey(message, sanitize) {
|
|
|
2289
2498
|
return JSON.stringify(sanitized.parts);
|
|
2290
2499
|
}
|
|
2291
2500
|
function mergeServerToolOutputs(incoming, serverMessages) {
|
|
2292
|
-
const
|
|
2501
|
+
const serverResolvedPartsByMessage = /* @__PURE__ */ new Map();
|
|
2293
2502
|
for (const msg of serverMessages) {
|
|
2294
2503
|
if (msg.role !== "assistant") continue;
|
|
2504
|
+
const resolvedParts = /* @__PURE__ */ new Map();
|
|
2295
2505
|
for (const part of msg.parts) {
|
|
2296
2506
|
const record = part;
|
|
2297
|
-
if (
|
|
2507
|
+
if (isResolvedToolPart(record)) resolvedParts.set(record.toolCallId, record);
|
|
2298
2508
|
}
|
|
2509
|
+
if (resolvedParts.size > 0) serverResolvedPartsByMessage.set(msg.id, resolvedParts);
|
|
2299
2510
|
}
|
|
2300
|
-
if (
|
|
2511
|
+
if (serverResolvedPartsByMessage.size === 0) return incoming;
|
|
2301
2512
|
return incoming.map((msg) => {
|
|
2302
2513
|
if (msg.role !== "assistant") return msg;
|
|
2514
|
+
const ownResolvedParts = serverResolvedPartsByMessage.get(msg.id);
|
|
2515
|
+
if (!ownResolvedParts) return msg;
|
|
2303
2516
|
let hasChanges = false;
|
|
2304
2517
|
const updatedParts = msg.parts.map((part) => {
|
|
2305
2518
|
const record = part;
|
|
2306
|
-
if (
|
|
2519
|
+
if (!isPendingToolPart(record)) return part;
|
|
2520
|
+
const server = ownResolvedParts.get(record.toolCallId);
|
|
2521
|
+
if (server) {
|
|
2307
2522
|
hasChanges = true;
|
|
2308
|
-
const server = serverResolvedParts.get(record.toolCallId);
|
|
2309
2523
|
const merged = {
|
|
2310
2524
|
...part,
|
|
2311
2525
|
state: server.state
|
|
@@ -2340,7 +2554,22 @@ function reconcileAssistantIds(incoming, serverMessages, sanitize) {
|
|
|
2340
2554
|
}
|
|
2341
2555
|
return incoming.map((incomingMessage, incomingIdx) => {
|
|
2342
2556
|
if (exactMatchMap.has(incomingIdx)) return incomingMessage;
|
|
2343
|
-
if (incomingMessage.role !== "assistant"
|
|
2557
|
+
if (incomingMessage.role !== "assistant") return incomingMessage;
|
|
2558
|
+
const incomingToolParts = toolPartsByCallId(sanitize ? sanitize(incomingMessage) : incomingMessage);
|
|
2559
|
+
if (incomingToolParts.size > 0) {
|
|
2560
|
+
for (let i = 0; i < serverMessages.length; i++) {
|
|
2561
|
+
if (claimedServerIndices.has(i)) continue;
|
|
2562
|
+
const serverMessage = serverMessages[i];
|
|
2563
|
+
if (serverMessage.role === "assistant" && carriesSameToolCalls(serverMessage, incomingToolParts)) {
|
|
2564
|
+
claimedServerIndices.add(i);
|
|
2565
|
+
return {
|
|
2566
|
+
...incomingMessage,
|
|
2567
|
+
id: serverMessage.id
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
return incomingMessage;
|
|
2572
|
+
}
|
|
2344
2573
|
const incomingKey = assistantContentKey(incomingMessage, sanitize);
|
|
2345
2574
|
if (!incomingKey) return incomingMessage;
|
|
2346
2575
|
for (let i = 0; i < serverMessages.length; i++) {
|
|
@@ -2361,6 +2590,53 @@ function reconcileAssistantIds(incoming, serverMessages, sanitize) {
|
|
|
2361
2590
|
function hasToolCallPart(message) {
|
|
2362
2591
|
return message.parts.some((part) => "toolCallId" in part);
|
|
2363
2592
|
}
|
|
2593
|
+
/** A server-side tool part that has reached a terminal state. */
|
|
2594
|
+
function isResolvedToolPart(record) {
|
|
2595
|
+
return "toolCallId" in record && "state" in record && (record.state === "output-available" || record.state === "output-error" || record.state === "output-denied");
|
|
2596
|
+
}
|
|
2597
|
+
/** A client-side tool part still waiting on a result. */
|
|
2598
|
+
function isPendingToolPart(record) {
|
|
2599
|
+
return "toolCallId" in record && "state" in record && (record.state === "input-available" || record.state === "approval-requested" || record.state === "approval-responded");
|
|
2600
|
+
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Whether `serverMessage` shares at least one toolCallId with the incoming
|
|
2603
|
+
* message and every shared toolCallId is the same call on both sides. A
|
|
2604
|
+
* provider that reuses a toolCallId for a new call carries a different tool
|
|
2605
|
+
* or input, so it cannot adopt the older row's ID.
|
|
2606
|
+
*/
|
|
2607
|
+
function carriesSameToolCalls(serverMessage, incomingToolParts) {
|
|
2608
|
+
let shared = false;
|
|
2609
|
+
for (const part of serverMessage.parts) {
|
|
2610
|
+
const record = part;
|
|
2611
|
+
if (typeof record.toolCallId !== "string") continue;
|
|
2612
|
+
const incomingPart = incomingToolParts.get(record.toolCallId);
|
|
2613
|
+
if (!incomingPart) continue;
|
|
2614
|
+
if (!sameToolCall(record, incomingPart)) return false;
|
|
2615
|
+
shared = true;
|
|
2616
|
+
}
|
|
2617
|
+
return shared;
|
|
2618
|
+
}
|
|
2619
|
+
/**
|
|
2620
|
+
* Same tool and structurally equal input (object key order ignored). Static
|
|
2621
|
+
* tool parts may omit `toolName`, so it is compared only when both carry it.
|
|
2622
|
+
*/
|
|
2623
|
+
function sameToolCall(a, b) {
|
|
2624
|
+
return a.type === b.type && (a.toolName === void 0 || b.toolName === void 0 || a.toolName === b.toolName) && stableStringify(a.input) === stableStringify(b.input);
|
|
2625
|
+
}
|
|
2626
|
+
function stableStringify(value) {
|
|
2627
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
2628
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item) ?? "null").join(",")}]`;
|
|
2629
|
+
const record = value;
|
|
2630
|
+
return `{${Object.keys(record).filter((key) => record[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
2631
|
+
}
|
|
2632
|
+
function toolPartsByCallId(message) {
|
|
2633
|
+
const parts = /* @__PURE__ */ new Map();
|
|
2634
|
+
for (const part of message.parts) {
|
|
2635
|
+
const record = part;
|
|
2636
|
+
if (typeof record.toolCallId === "string" && !parts.has(record.toolCallId)) parts.set(record.toolCallId, record);
|
|
2637
|
+
}
|
|
2638
|
+
return parts;
|
|
2639
|
+
}
|
|
2364
2640
|
function findMessageByToolCallId(messages, toolCallId) {
|
|
2365
2641
|
for (const msg of messages) {
|
|
2366
2642
|
if (msg.role !== "assistant") continue;
|
|
@@ -2392,7 +2668,7 @@ function toolPartHasSettledResult(record) {
|
|
|
2392
2668
|
*
|
|
2393
2669
|
* - a tool part with NO settled result and state `approval-responded` is kept
|
|
2394
2670
|
* verbatim (an approved server tool waiting for its continuation to run
|
|
2395
|
-
* `execute()` — not abandoned);
|
|
2671
|
+
* `execute()` — not abandoned), unless `repairApprovalResponded` is set;
|
|
2396
2672
|
* - a tool part with NO settled result for which `shouldRepair` returns false
|
|
2397
2673
|
* is kept verbatim (a part still awaiting a CLIENT interaction; see option);
|
|
2398
2674
|
* - any other tool part with no settled result is normalized then handed to
|
|
@@ -2411,7 +2687,8 @@ function repairInterruptedToolParts(messages, options) {
|
|
|
2411
2687
|
let normalizedInputs = 0;
|
|
2412
2688
|
const toolCallIds = [];
|
|
2413
2689
|
const repaired = [];
|
|
2414
|
-
for (const message of messages) {
|
|
2690
|
+
for (const [messageIndex, message] of messages.entries()) {
|
|
2691
|
+
const isLastMessage = messageIndex === messages.length - 1;
|
|
2415
2692
|
const parts = [];
|
|
2416
2693
|
let messageChanged = false;
|
|
2417
2694
|
for (const part of message.parts) {
|
|
@@ -2423,8 +2700,23 @@ function repairInterruptedToolParts(messages, options) {
|
|
|
2423
2700
|
}
|
|
2424
2701
|
if (!isSettled(record)) {
|
|
2425
2702
|
if ((typeof record.state === "string" ? record.state : "") === "approval-responded") {
|
|
2426
|
-
|
|
2427
|
-
|
|
2703
|
+
if (!options.repairApprovalResponded || isLastMessage) {
|
|
2704
|
+
parts.push(part);
|
|
2705
|
+
continue;
|
|
2706
|
+
}
|
|
2707
|
+
if (record.approval?.approved === false) {
|
|
2708
|
+
const normalized = normalizeInput("input" in record ? record.input : void 0);
|
|
2709
|
+
parts.push({
|
|
2710
|
+
...part,
|
|
2711
|
+
input: normalized.input,
|
|
2712
|
+
state: "output-denied"
|
|
2713
|
+
});
|
|
2714
|
+
if (normalized.changed) normalizedInputs++;
|
|
2715
|
+
removedToolCalls++;
|
|
2716
|
+
messageChanged = true;
|
|
2717
|
+
toolCallIds.push(toolCallId);
|
|
2718
|
+
continue;
|
|
2719
|
+
}
|
|
2428
2720
|
}
|
|
2429
2721
|
if (!shouldRepair(part)) {
|
|
2430
2722
|
parts.push(part);
|
|
@@ -2489,7 +2781,7 @@ async function persistReconstructedOrphan(chunks, options) {
|
|
|
2489
2781
|
}
|
|
2490
2782
|
//#endregion
|
|
2491
2783
|
//#region src/chat/recovery.ts
|
|
2492
|
-
function createChatFiberSnapshot({ kind, requestId, recoveryRootRequestId, continuation, messages, lastBody, lastClientTools }) {
|
|
2784
|
+
function createChatFiberSnapshot({ kind, requestId, recoveryRootRequestId, continuation, messages, lastBody, lastClientTools, originMessageIds }) {
|
|
2493
2785
|
const latestMessage = messages.length > 0 ? messages[messages.length - 1] : void 0;
|
|
2494
2786
|
let latestUser;
|
|
2495
2787
|
for (let index = messages.length - 1; index >= 0; index--) if (messages[index].role === "user") {
|
|
@@ -2507,7 +2799,8 @@ function createChatFiberSnapshot({ kind, requestId, recoveryRootRequestId, conti
|
|
|
2507
2799
|
latestUserMessageId: latestUser?.id,
|
|
2508
2800
|
startedAt: Date.now(),
|
|
2509
2801
|
lastBody,
|
|
2510
|
-
lastClientTools
|
|
2802
|
+
lastClientTools,
|
|
2803
|
+
...originMessageIds ? { originMessageIds } : {}
|
|
2511
2804
|
};
|
|
2512
2805
|
}
|
|
2513
2806
|
function wrapChatFiberSnapshot(key, snapshot, user) {
|
|
@@ -2699,13 +2992,20 @@ var ResumeHandshake = class {
|
|
|
2699
2992
|
if (resumableStream.hasActiveStream() && resumableStream.activeRequestId === requestId) {
|
|
2700
2993
|
const orphanedStreamId = resumableStream.replayChunks(connection, resumableStream.activeRequestId);
|
|
2701
2994
|
if (orphanedStreamId) await this.host.persistOrphanedStream(orphanedStreamId);
|
|
2702
|
-
} else if (resumableStream.hasActiveStream()) {} else if (
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2995
|
+
} else if (resumableStream.hasActiveStream()) {} else if (this.host.holdsTerminalFrames?.(requestId)) resumableStream.replayClosedStreamChunks(connection, requestId);
|
|
2996
|
+
else if (await this._replayTerminalOnAck(connection, requestId)) {} else if (!resumableStream.replayCompletedChunksByRequestId(connection, requestId)) {
|
|
2997
|
+
const messageIds = resumableStream.getOriginMessageIds(requestId);
|
|
2998
|
+
const outcome = resumableStream.getOutcome(requestId);
|
|
2999
|
+
sendIfOpen(connection, JSON.stringify({
|
|
3000
|
+
body: "",
|
|
3001
|
+
done: true,
|
|
3002
|
+
id: requestId,
|
|
3003
|
+
type: responseMessageType,
|
|
3004
|
+
replay: true,
|
|
3005
|
+
...messageIds && { messageIds },
|
|
3006
|
+
...outcome && { outcome }
|
|
3007
|
+
}));
|
|
3008
|
+
}
|
|
2709
3009
|
}
|
|
2710
3010
|
/**
|
|
2711
3011
|
* Replay a pending terminal outcome (#1645) over the resume handshake so a
|
|
@@ -2737,12 +3037,14 @@ var ResumeHandshake = class {
|
|
|
2737
3037
|
const pending = await this.host.pendingChatTerminal();
|
|
2738
3038
|
if (!pending || pending.requestId !== requestId) return false;
|
|
2739
3039
|
if (!resumableStream.replayErroredChunksByRequestId(connection, pending.requestId)) return true;
|
|
3040
|
+
const messageIds = pending.messageIds ?? resumableStream.getOriginMessageIds(pending.requestId);
|
|
2740
3041
|
sendIfOpen(connection, JSON.stringify({
|
|
2741
3042
|
body: pending.body,
|
|
2742
3043
|
done: true,
|
|
2743
3044
|
error: true,
|
|
2744
3045
|
id: pending.requestId,
|
|
2745
|
-
type: responseMessageType
|
|
3046
|
+
type: responseMessageType,
|
|
3047
|
+
...messageIds && { messageIds }
|
|
2746
3048
|
}));
|
|
2747
3049
|
return true;
|
|
2748
3050
|
}
|
|
@@ -2782,7 +3084,7 @@ const CHAT_LAST_TERMINAL_KEY = "cf:chat:last-terminal";
|
|
|
2782
3084
|
const DEFAULT_CHAT_RECOVERY_MAX_ATTEMPTS = 10;
|
|
2783
3085
|
/**
|
|
2784
3086
|
* Runaway-loop guard default — the framework-imposed backstop on cumulative
|
|
2785
|
-
* recovery WORK (
|
|
3087
|
+
* recovery WORK (durable stream segments, see below) since an incident opened.
|
|
2786
3088
|
*
|
|
2787
3089
|
* Originally `Infinity` (rfc-chat-recovery-work-budget): the SDK shipped the
|
|
2788
3090
|
* *mechanism* but no default cap, so a progressing turn was never terminated on
|
|
@@ -3022,11 +3324,13 @@ var StreamProgressCreditThrottle = class {
|
|
|
3022
3324
|
* until a later turn supersedes it ({@link clearChatTerminal}); a single record
|
|
3023
3325
|
* is sufficient because only the most recent terminal is relevant.
|
|
3024
3326
|
*/
|
|
3025
|
-
async function recordChatTerminal(storage, requestId, body) {
|
|
3026
|
-
|
|
3327
|
+
async function recordChatTerminal(storage, requestId, body, messageIds) {
|
|
3328
|
+
const record = {
|
|
3027
3329
|
requestId,
|
|
3028
3330
|
body
|
|
3029
|
-
}
|
|
3331
|
+
};
|
|
3332
|
+
if (messageIds?.length) record.messageIds = messageIds;
|
|
3333
|
+
await storage.put(CHAT_LAST_TERMINAL_KEY, record);
|
|
3030
3334
|
}
|
|
3031
3335
|
/** Clear the durable terminal record once a later turn supersedes it (#1645). */
|
|
3032
3336
|
async function clearChatTerminal(storage) {
|
|
@@ -3123,10 +3427,12 @@ async function evaluateChatRecoveryIncident(input) {
|
|
|
3123
3427
|
const workBudgetExceeded = existing != null && Number.isFinite(config.maxRecoveryWork) && work > config.maxRecoveryWork;
|
|
3124
3428
|
const oomAttempts = existing?.oomAttempts ?? 0;
|
|
3125
3429
|
const oomBudgetExceeded = existing != null && !awaitingClientInteraction && oomAttempts > config.maxOomRetries;
|
|
3430
|
+
const transientRetries = existing?.transientRetries ?? 0;
|
|
3431
|
+
const transientBudgetExceeded = existing != null && !awaitingClientInteraction && transientRetries >= config.maxAttempts;
|
|
3126
3432
|
const debounced = existing != null && !madeProgress && now - existing.lastAttemptAt < 3e4;
|
|
3127
3433
|
const attempt = madeProgress ? 1 : debounced ? existing?.attempt ?? 1 : (existing?.attempt ?? 0) + 1;
|
|
3128
3434
|
let abortedByCaller = false;
|
|
3129
|
-
if (existing != null && !awaitingClientInteraction && config.shouldKeepRecovering && !noProgressExceeded && !workBudgetExceeded && !oomBudgetExceeded && attempt <= config.maxAttempts) try {
|
|
3435
|
+
if (existing != null && !awaitingClientInteraction && config.shouldKeepRecovering && !noProgressExceeded && !workBudgetExceeded && !oomBudgetExceeded && !transientBudgetExceeded && attempt <= config.maxAttempts) try {
|
|
3130
3436
|
const ctx = {
|
|
3131
3437
|
incidentId,
|
|
3132
3438
|
requestId: identity.requestId,
|
|
@@ -3141,7 +3447,7 @@ async function evaluateChatRecoveryIncident(input) {
|
|
|
3141
3447
|
} catch (error) {
|
|
3142
3448
|
input.onShouldKeepRecoveringError?.(error);
|
|
3143
3449
|
}
|
|
3144
|
-
const exhausted = !awaitingClientInteraction && (oomBudgetExceeded || noProgressExceeded || workBudgetExceeded || abortedByCaller || attempt > config.maxAttempts);
|
|
3450
|
+
const exhausted = !awaitingClientInteraction && (oomBudgetExceeded || noProgressExceeded || workBudgetExceeded || abortedByCaller || transientBudgetExceeded || attempt > config.maxAttempts);
|
|
3145
3451
|
const incident = {
|
|
3146
3452
|
incidentId,
|
|
3147
3453
|
requestId: identity.requestId,
|
|
@@ -3156,6 +3462,7 @@ async function evaluateChatRecoveryIncident(input) {
|
|
|
3156
3462
|
progress,
|
|
3157
3463
|
workBaseline,
|
|
3158
3464
|
...oomAttempts > 0 ? { oomAttempts } : {},
|
|
3465
|
+
...transientRetries > 0 ? { transientRetries } : {},
|
|
3159
3466
|
...exhausted ? { reason: oomBudgetExceeded ? "out_of_memory" : workBudgetExceeded ? "work_budget_exceeded" : noProgressExceeded ? "no_progress_timeout" : abortedByCaller ? "recovery_aborted" : "max_attempts_exceeded" } : {}
|
|
3160
3467
|
};
|
|
3161
3468
|
const events = [];
|
|
@@ -3268,7 +3575,7 @@ var ChatRecoveryEngine = class {
|
|
|
3268
3575
|
options: void 0,
|
|
3269
3576
|
partial
|
|
3270
3577
|
})) await wake.persistOrphanedStream(streamId);
|
|
3271
|
-
await adapter.exhaustChatRecovery(incident, config, partial, streamId, ctx.createdAt);
|
|
3578
|
+
await adapter.exhaustChatRecovery(incident, config, partial, streamId, ctx.createdAt, snapshot?.originMessageIds);
|
|
3272
3579
|
return true;
|
|
3273
3580
|
}
|
|
3274
3581
|
try {
|
|
@@ -3382,7 +3689,7 @@ var ChatRecoveryEngine = class {
|
|
|
3382
3689
|
maxAttempts: incident.maxAttempts,
|
|
3383
3690
|
recoveryKind: input.recoveryKind
|
|
3384
3691
|
});
|
|
3385
|
-
await this.adapter.scheduleRecovery(input.callback, input.data, input.reason ?? "initial", 0);
|
|
3692
|
+
await this.adapter.scheduleRecovery(input.callback, input.data, input.reason ?? "initial", input.delaySeconds ?? 0);
|
|
3386
3693
|
}
|
|
3387
3694
|
/**
|
|
3388
3695
|
* Reschedule a recovery continuation/retry that timed out waiting for stable
|
|
@@ -3421,6 +3728,47 @@ var ChatRecoveryEngine = class {
|
|
|
3421
3728
|
return true;
|
|
3422
3729
|
}
|
|
3423
3730
|
/**
|
|
3731
|
+
* Bump the incident's durable `transientRetries` counter before scheduling a
|
|
3732
|
+
* recovery for a transient or rate-limited stream error, and return the new
|
|
3733
|
+
* count so the caller can derive its backoff. Returns `1` when the incident
|
|
3734
|
+
* record is gone.
|
|
3735
|
+
*/
|
|
3736
|
+
async recordTransientRetry(incidentId) {
|
|
3737
|
+
const { adapter } = this;
|
|
3738
|
+
const key = chatRecoveryIncidentKey(incidentId);
|
|
3739
|
+
const incident = await adapter.getIncident(key);
|
|
3740
|
+
if (!incident) return 1;
|
|
3741
|
+
const transientRetries = (incident.transientRetries ?? 0) + 1;
|
|
3742
|
+
await adapter.putIncident(key, {
|
|
3743
|
+
...incident,
|
|
3744
|
+
transientRetries
|
|
3745
|
+
});
|
|
3746
|
+
return transientRetries;
|
|
3747
|
+
}
|
|
3748
|
+
/**
|
|
3749
|
+
* The user cancelled `requestId` while its recovery was scheduled but not
|
|
3750
|
+
* yet running (e.g. during a transient-error backoff). Marks every
|
|
3751
|
+
* `scheduled` incident whose recovery root or current attempt is
|
|
3752
|
+
* `requestId` as `skipped` with {@link CHAT_RECOVERY_CANCELLED_REASON}, so
|
|
3753
|
+
* the queued callback bails when it fires (see {@link isRecoveryCancelled}).
|
|
3754
|
+
* Returns whether any incident was cancelled.
|
|
3755
|
+
*/
|
|
3756
|
+
async cancelScheduledRecovery(requestId) {
|
|
3757
|
+
const incidents = await this.adapter.listActiveIncidents?.() ?? [];
|
|
3758
|
+
let cancelled = false;
|
|
3759
|
+
for (const incident of incidents) if (incident.status === "scheduled" && (incident.recoveryRootRequestId === requestId || incident.requestId === requestId)) {
|
|
3760
|
+
await this.updateIncident(incident.incidentId, "skipped", CHAT_RECOVERY_CANCELLED_REASON);
|
|
3761
|
+
cancelled = true;
|
|
3762
|
+
}
|
|
3763
|
+
return cancelled;
|
|
3764
|
+
}
|
|
3765
|
+
/** Whether the user cancelled this incident's scheduled recovery. */
|
|
3766
|
+
async isRecoveryCancelled(incidentId) {
|
|
3767
|
+
if (!incidentId) return false;
|
|
3768
|
+
const incident = await this.adapter.getIncident(chatRecoveryIncidentKey(incidentId));
|
|
3769
|
+
return incident?.status === "skipped" && incident.reason === "user_cancelled";
|
|
3770
|
+
}
|
|
3771
|
+
/**
|
|
3424
3772
|
* Record that a recovery callback observed a Durable Object memory-limit reset
|
|
3425
3773
|
* (the isolate exceeded its 128 MB limit — `isDurableObjectMemoryLimitReset`)
|
|
3426
3774
|
* and decide what to do next (#1825).
|
|
@@ -3587,6 +3935,47 @@ var ChatRecoveryEngine = class {
|
|
|
3587
3935
|
else if (status === "completed" || status === "skipped" || status === "failed") await adapter.setRecovering(false);
|
|
3588
3936
|
}
|
|
3589
3937
|
};
|
|
3938
|
+
/** `incident.reason` for a scheduled recovery the user cancelled. */
|
|
3939
|
+
const CHAT_RECOVERY_CANCELLED_REASON = "user_cancelled";
|
|
3940
|
+
/**
|
|
3941
|
+
* Delay before the `retries`-th transient-error recovery: exponential backoff
|
|
3942
|
+
* capped at {@link CHAT_RECOVERY_MAX_BACKOFF_SECONDS}. A provider
|
|
3943
|
+
* `retryAfterSeconds` (rate limits) extends it, capped at
|
|
3944
|
+
* {@link CHAT_RECOVERY_MAX_RETRY_AFTER_SECONDS}.
|
|
3945
|
+
*/
|
|
3946
|
+
function chatRecoveryBackoffSeconds(retries, retryAfterSeconds) {
|
|
3947
|
+
const backoff = Math.min(2 ** (retries - 1), 30);
|
|
3948
|
+
if (retryAfterSeconds === void 0) return backoff;
|
|
3949
|
+
return Math.min(Math.max(backoff, retryAfterSeconds), 60);
|
|
3950
|
+
}
|
|
3951
|
+
/**
|
|
3952
|
+
* The `Retry-After` an error carries, in whole seconds. Reads
|
|
3953
|
+
* `responseHeaders` (AI SDK `APICallError`) or `headers` on the error or its
|
|
3954
|
+
* `cause` chain; accepts delta-seconds or an HTTP date. `undefined` when absent
|
|
3955
|
+
* or unparseable.
|
|
3956
|
+
*/
|
|
3957
|
+
function retryAfterSeconds(error, now = Date.now()) {
|
|
3958
|
+
let current = error;
|
|
3959
|
+
for (let depth = 0; depth < 8 && current != null; depth++) {
|
|
3960
|
+
if (typeof current !== "object") return void 0;
|
|
3961
|
+
const record = current;
|
|
3962
|
+
const value = readRetryAfterHeader(record.responseHeaders) ?? readRetryAfterHeader(record.headers);
|
|
3963
|
+
if (value !== void 0) return parseRetryAfter(value, now);
|
|
3964
|
+
current = record.cause;
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
function readRetryAfterHeader(headers) {
|
|
3968
|
+
if (headers == null || typeof headers !== "object") return void 0;
|
|
3969
|
+
if (headers instanceof Headers) return headers.get("retry-after") ?? void 0;
|
|
3970
|
+
for (const [name, value] of Object.entries(headers)) if (name.toLowerCase() === "retry-after" && typeof value === "string") return value;
|
|
3971
|
+
}
|
|
3972
|
+
function parseRetryAfter(value, now) {
|
|
3973
|
+
const trimmed = value.trim();
|
|
3974
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.ceil(Number(trimmed));
|
|
3975
|
+
const date = Date.parse(trimmed);
|
|
3976
|
+
if (Number.isNaN(date)) return void 0;
|
|
3977
|
+
return Math.max(0, Math.ceil((date - now) / 1e3));
|
|
3978
|
+
}
|
|
3590
3979
|
/**
|
|
3591
3980
|
* Build the `ChatRecoveryExhaustedContext` delivered to `onExhausted` and the
|
|
3592
3981
|
* `chat:recovery:exhausted` event. Pure field-mapping shared by both packages;
|
|
@@ -3746,20 +4135,14 @@ async function* iterateWithStallWatchdog(source, timeoutMs, onStall) {
|
|
|
3746
4135
|
//#endregion
|
|
3747
4136
|
//#region src/chat/truncate-older-messages.ts
|
|
3748
4137
|
/**
|
|
3749
|
-
* Read-time context truncation.
|
|
3750
|
-
*
|
|
3751
|
-
* Truncates older tool outputs and long text before sending to the LLM.
|
|
3752
|
-
* Structured tool outputs keep their container shape so tool-specific
|
|
3753
|
-
* `toModelOutput` handlers can safely replay older results.
|
|
3754
|
-
* Does NOT mutate stored messages — operates on a copy.
|
|
3755
|
-
*/
|
|
3756
|
-
/**
|
|
3757
4138
|
* Truncate tool outputs and long text in older messages.
|
|
3758
4139
|
* Returns a new array — input messages are not mutated.
|
|
3759
4140
|
*
|
|
3760
4141
|
* Recent messages (last `keepRecent`) are left intact.
|
|
3761
4142
|
* Older messages get tool outputs and long text truncated. Structured tool
|
|
3762
4143
|
* outputs are truncated in place instead of being replaced by raw strings.
|
|
4144
|
+
* Provider-executed tool outputs are never truncated: the provider parses
|
|
4145
|
+
* them against its own schema when they are replayed.
|
|
3763
4146
|
*
|
|
3764
4147
|
* Use in assembleContext() before sending to the LLM:
|
|
3765
4148
|
* ```typescript
|
|
@@ -3774,6 +4157,7 @@ function truncateOlderMessages(messages, options) {
|
|
|
3774
4157
|
const keepRecent = options?.keepRecent ?? 4;
|
|
3775
4158
|
const maxToolOutput = options?.maxToolOutputChars ?? 500;
|
|
3776
4159
|
const maxText = options?.maxTextChars ?? 1e4;
|
|
4160
|
+
const truncateToolOutputs = options?.toolOutputs ?? true;
|
|
3777
4161
|
if (messages.length <= keepRecent) return messages;
|
|
3778
4162
|
const cutoff = messages.length - keepRecent;
|
|
3779
4163
|
const result = [];
|
|
@@ -3785,7 +4169,7 @@ function truncateOlderMessages(messages, options) {
|
|
|
3785
4169
|
const msg = messages[i];
|
|
3786
4170
|
let changed = false;
|
|
3787
4171
|
const truncatedParts = msg.parts.map((part) => {
|
|
3788
|
-
if (
|
|
4172
|
+
if (truncateToolOutputs && isToolPart(part) && "output" in part && !isProviderExecuted(part)) {
|
|
3789
4173
|
const output = part.output;
|
|
3790
4174
|
if (output !== void 0) {
|
|
3791
4175
|
const truncated = truncateToolOutput(output, maxToolOutput);
|
|
@@ -3817,7 +4201,153 @@ function truncateOlderMessages(messages, options) {
|
|
|
3817
4201
|
}
|
|
3818
4202
|
return result;
|
|
3819
4203
|
}
|
|
4204
|
+
/**
|
|
4205
|
+
* Truncate the tool results of older messages after `convertToModelMessages`.
|
|
4206
|
+
* `messages` are the UI messages that were converted, so "older" means the
|
|
4207
|
+
* same messages {@link truncateOlderMessages} treats as older.
|
|
4208
|
+
*
|
|
4209
|
+
* The converted result is what the model reads, after any `toModelOutput`, so
|
|
4210
|
+
* no tool schema applies to it. Provider-executed results are left intact.
|
|
4211
|
+
* Inline file and image bytes in an older `content` result are replaced by a
|
|
4212
|
+
* short text marker.
|
|
4213
|
+
*
|
|
4214
|
+
* Tool call ids are not unique across turns, so each converted result is
|
|
4215
|
+
* matched to the UI tool part it came from by order: the Nth result for an id
|
|
4216
|
+
* belongs to the Nth UI part with that id that converts to a tool result.
|
|
4217
|
+
*/
|
|
4218
|
+
function truncateOlderToolResults(modelMessages, messages, options) {
|
|
4219
|
+
const keepRecent = options?.keepRecent ?? 4;
|
|
4220
|
+
const maxChars = options?.maxToolOutputChars ?? 500;
|
|
4221
|
+
const cutoff = messages.length - keepRecent;
|
|
4222
|
+
if (cutoff <= 0) return modelMessages;
|
|
4223
|
+
const resultsById = /* @__PURE__ */ new Map();
|
|
4224
|
+
let hasOlderResult = false;
|
|
4225
|
+
messages.forEach((message, index) => {
|
|
4226
|
+
for (const part of message.parts) {
|
|
4227
|
+
if (!isToolPart(part) || !producesToolResult(part)) continue;
|
|
4228
|
+
const truncatable = index < cutoff && !isProviderExecuted(part);
|
|
4229
|
+
const toolCallId = part.toolCallId;
|
|
4230
|
+
const results = resultsById.get(toolCallId) ?? [];
|
|
4231
|
+
results.push(truncatable);
|
|
4232
|
+
resultsById.set(toolCallId, results);
|
|
4233
|
+
hasOlderResult ||= truncatable;
|
|
4234
|
+
}
|
|
4235
|
+
});
|
|
4236
|
+
if (!hasOlderResult) return modelMessages;
|
|
4237
|
+
return modelMessages.map((message) => {
|
|
4238
|
+
if (message.role !== "tool") return message;
|
|
4239
|
+
let changed = false;
|
|
4240
|
+
const content = message.content.map((part) => {
|
|
4241
|
+
if (part.type !== "tool-result") return part;
|
|
4242
|
+
if (!resultsById.get(part.toolCallId)?.shift()) return part;
|
|
4243
|
+
const output = truncateModelOutput(part.output, maxChars);
|
|
4244
|
+
if (output === part.output) return part;
|
|
4245
|
+
changed = true;
|
|
4246
|
+
return {
|
|
4247
|
+
...part,
|
|
4248
|
+
output
|
|
4249
|
+
};
|
|
4250
|
+
});
|
|
4251
|
+
return changed ? {
|
|
4252
|
+
...message,
|
|
4253
|
+
content
|
|
4254
|
+
} : message;
|
|
4255
|
+
});
|
|
4256
|
+
}
|
|
4257
|
+
function truncateModelOutput(output, maxChars) {
|
|
4258
|
+
switch (output.type) {
|
|
4259
|
+
case "text":
|
|
4260
|
+
case "error-text":
|
|
4261
|
+
case "json":
|
|
4262
|
+
case "error-json": {
|
|
4263
|
+
const truncated = truncateToolOutput(output.value, maxChars);
|
|
4264
|
+
return truncated.truncated ? {
|
|
4265
|
+
...output,
|
|
4266
|
+
value: truncated.output
|
|
4267
|
+
} : output;
|
|
4268
|
+
}
|
|
4269
|
+
case "content": {
|
|
4270
|
+
let changed = false;
|
|
4271
|
+
const withoutBytes = output.value.map((item) => {
|
|
4272
|
+
const mediaType = inlineBinaryMediaType(item);
|
|
4273
|
+
if (mediaType === void 0) return item;
|
|
4274
|
+
changed = true;
|
|
4275
|
+
return {
|
|
4276
|
+
type: "text",
|
|
4277
|
+
text: omittedMediaMarker(mediaType)
|
|
4278
|
+
};
|
|
4279
|
+
});
|
|
4280
|
+
const total = output.value.reduce((sum, item) => sum + (inlineText(item)?.length ?? 0), 0);
|
|
4281
|
+
if (total <= maxChars) return changed ? {
|
|
4282
|
+
...output,
|
|
4283
|
+
value: withoutBytes
|
|
4284
|
+
} : output;
|
|
4285
|
+
const suffix = truncatedSuffix(total);
|
|
4286
|
+
let remaining = Math.max(0, maxChars - suffix.length);
|
|
4287
|
+
let truncated = false;
|
|
4288
|
+
const value = output.value.flatMap((item, index) => {
|
|
4289
|
+
const text = inlineText(item);
|
|
4290
|
+
if (text === void 0) return [withoutBytes[index]];
|
|
4291
|
+
if (truncated) return [];
|
|
4292
|
+
if (text.length <= remaining) {
|
|
4293
|
+
remaining -= text.length;
|
|
4294
|
+
return [item];
|
|
4295
|
+
}
|
|
4296
|
+
truncated = true;
|
|
4297
|
+
return [withInlineText(item, maxChars <= suffix.length ? suffix.slice(0, maxChars) : `${text.slice(0, remaining)}${suffix}`)];
|
|
4298
|
+
});
|
|
4299
|
+
return {
|
|
4300
|
+
...output,
|
|
4301
|
+
value
|
|
4302
|
+
};
|
|
4303
|
+
}
|
|
4304
|
+
default: return output;
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
/** Text a content item carries inline: a text item, or a text file. */
|
|
4308
|
+
function inlineText(item) {
|
|
4309
|
+
if (item.type === "text") return item.text;
|
|
4310
|
+
if (item.type === "file" && item.data.type === "text") return item.data.text;
|
|
4311
|
+
}
|
|
4312
|
+
function withInlineText(item, text) {
|
|
4313
|
+
if (item.type === "file" && item.data.type === "text") return {
|
|
4314
|
+
...item,
|
|
4315
|
+
data: {
|
|
4316
|
+
...item.data,
|
|
4317
|
+
text
|
|
4318
|
+
}
|
|
4319
|
+
};
|
|
4320
|
+
return {
|
|
4321
|
+
...item,
|
|
4322
|
+
text
|
|
4323
|
+
};
|
|
4324
|
+
}
|
|
4325
|
+
/** Media type of a content item that carries file or image bytes inline. */
|
|
4326
|
+
function inlineBinaryMediaType(item) {
|
|
4327
|
+
if (item.type === "file-data" || item.type === "image-data") return item.mediaType;
|
|
4328
|
+
if (item.type === "file" && item.data.type === "data") return item.mediaType;
|
|
4329
|
+
}
|
|
4330
|
+
function omittedMediaMarker(mediaType) {
|
|
4331
|
+
return `[${mediaType} omitted from an older tool result]`;
|
|
4332
|
+
}
|
|
4333
|
+
/**
|
|
4334
|
+
* Whether `convertToModelMessages` turns this UI tool part into a result in a
|
|
4335
|
+
* `tool` message. Provider-executed results go in the assistant message
|
|
4336
|
+
* instead, unless the part is a denied approval.
|
|
4337
|
+
*/
|
|
4338
|
+
function producesToolResult(part) {
|
|
4339
|
+
const { state, approval } = part;
|
|
4340
|
+
if (state === "approval-responded") return approval?.approved === false;
|
|
4341
|
+
if (isProviderExecuted(part)) return false;
|
|
4342
|
+
return state === "output-available" || state === "output-error" || state === "output-denied";
|
|
4343
|
+
}
|
|
4344
|
+
function isToolPart(part) {
|
|
4345
|
+
return part.type.startsWith("tool-") || part.type === "dynamic-tool";
|
|
4346
|
+
}
|
|
4347
|
+
function isProviderExecuted(part) {
|
|
4348
|
+
return part.providerExecuted === true;
|
|
4349
|
+
}
|
|
3820
4350
|
//#endregion
|
|
3821
|
-
export { AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS, AbortRegistry, AgentToolProgressEmitter, AgentToolStreamProgressThrottle, AutoContinuationController, CHAT_LAST_TERMINAL_KEY, CHAT_MESSAGE_TYPES, CHAT_RECOVERING_FLAG_TTL_MS, CHAT_RECOVERING_KEY, CHAT_RECOVERY_ALARM_DEBOUNCE_MS, CHAT_RECOVERY_INCIDENT_KEY_PREFIX, CHAT_RECOVERY_INCIDENT_TTL_MS, CHAT_RECOVERY_PROGRESS_KEY, CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS, CHAT_RECOVERY_TASK_NAME, CHAT_STREAM_PROGRESS_CREDIT_THROTTLE_MS, ChatRecoveryEngine, ChatStreamStalledError, ContinuationState, DEFAULT_CHAT_RECOVERY_MAX_ATTEMPTS, DEFAULT_CHAT_RECOVERY_MAX_OOM_RETRIES, DEFAULT_CHAT_RECOVERY_MAX_WORK, DEFAULT_CHAT_RECOVERY_NO_PROGRESS_TIMEOUT_MS, DEFAULT_CHAT_RECOVERY_STABLE_TIMEOUT_MS, DEFAULT_CHAT_RECOVERY_TERMINAL_MESSAGE, KV_DELETE_MAX_KEYS, MAX_BOUND_PARAMS, MessageType, PreStreamTurns, ROW_MAX_BYTES, ResumableStream, ResumeHandshake, STREAM_RESUME_NONE_REASONS, StreamAccumulator, StreamProgressCreditThrottle, SubmitConcurrencyController, TIMED_OUT, TextSegmentJoiner, TurnQueue, aiSdkRecoveryCodec, applyAgentToolEvent, applyChunkToParts, applyToolUpdate, awaitWithDeadline, transition as broadcastTransition, buildChatRecoveringFrame, buildInClauseStrings, bumpChatRecoveryProgress, byteLength, chatRecoveryTaskRunOptions, classifyAgentToolChildRecovery, clearChatTerminal, clientResolvableToolNames, createAgentToolEventState, createChatFiberSnapshot, createChatRecoveryTaskDefinition, createChatStreams, createChatTurnTaskDefinition, createToolsFromClientSchemas, crossMessageToolResultUpdate, dispatchChatRecoveryToHandoff, drainInteractionApplies, enforceRowSizeLimit, hasIncompleteToolBatch, interceptAgentToolBroadcast, isPlatformFailure, isReplayChunk, iterateWithStallWatchdog, listActiveChatRecoveryIncidents, normalizeToolInput, parseProtocolMessage, partAwaitsClientInteraction, pausedExecutionUpdate, pendingChatTerminal, persistReconstructedOrphan, readChatRecoveryProgress, reconcileMessages, reconcileOrphanPartial, recordChatTerminal, repairInterruptedToolParts, resolveChatRecoveryConfig, resolveToolMergeId, runChatRecoveryExhaustion, sanitizeMessage, sendIfOpen, setChatRecovering, shouldCreditStreamProgress, sweepStaleChatRecoveryIncidents, toolApprovalUpdate, toolPartHasSettledResult, toolResultUpdate, truncateOlderMessages, unwrapChatFiberSnapshot, wrapChatFiberSnapshot };
|
|
4351
|
+
export { AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS, AbortRegistry, AgentToolProgressEmitter, AgentToolStreamProgressThrottle, AutoContinuationController, CHAT_LAST_TERMINAL_KEY, CHAT_MESSAGE_TYPES, CHAT_RECOVERING_FLAG_TTL_MS, CHAT_RECOVERING_KEY, CHAT_RECOVERY_ALARM_DEBOUNCE_MS, CHAT_RECOVERY_CANCELLED_REASON, CHAT_RECOVERY_INCIDENT_KEY_PREFIX, CHAT_RECOVERY_INCIDENT_TTL_MS, CHAT_RECOVERY_PROGRESS_KEY, CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS, CHAT_RECOVERY_TASK_NAME, CHAT_STREAM_PROGRESS_CREDIT_THROTTLE_MS, ChatRecoveryEngine, ChatStreamStalledError, ContinuationState, DEFAULT_CHAT_RECOVERY_MAX_ATTEMPTS, DEFAULT_CHAT_RECOVERY_MAX_OOM_RETRIES, DEFAULT_CHAT_RECOVERY_MAX_WORK, DEFAULT_CHAT_RECOVERY_NO_PROGRESS_TIMEOUT_MS, DEFAULT_CHAT_RECOVERY_STABLE_TIMEOUT_MS, DEFAULT_CHAT_RECOVERY_TERMINAL_MESSAGE, KV_DELETE_MAX_KEYS, MAX_BOUND_PARAMS, MessageType, PreStreamTurns, ROW_MAX_BYTES, ResumableStream, ResumeHandshake, STREAM_RESUME_NONE_REASONS, StreamAccumulator, StreamProgressCreditThrottle, SubmitConcurrencyController, TIMED_OUT, TextSegmentJoiner, TurnQueue, aiSdkRecoveryCodec, applyAgentToolEvent, applyChunkToParts, applyLateToolInput, applyToolUpdate, awaitWithDeadline, transition as broadcastTransition, buildChatRecoveringFrame, buildInClauseStrings, bumpChatRecoveryProgress, byteLength, chatRecoveryBackoffSeconds, chatRecoveryTaskRunOptions, classifyAgentToolChildRecovery, clearChatTerminal, clientResolvableToolNames, createAgentToolEventState, createChatFiberSnapshot, createChatRecoveryTaskDefinition, createChatStreams, createChatTurnTaskDefinition, createToolsFromClientSchemas, crossMessageToolResultUpdate, dispatchChatRecoveryToHandoff, drainInteractionApplies, enforceRowSizeLimit, hasIncompleteToolBatch, interceptAgentToolBroadcast, isDurableObjectResetError, isLateToolInputChunk, isPlatformFailure, isPositionlessAgentToolChunk, isReplayChunk, iterateWithStallWatchdog, lateToolInputForwardChunks, listActiveChatRecoveryIncidents, normalizeToolInput, originMessageIds, parseProtocolMessage, partAwaitsClientInteraction, partialHasSettledToolResults, pausedExecutionUpdate, pendingChatTerminal, persistReconstructedOrphan, readChatRecoveryProgress, reconcileMessages, reconcileOrphanPartial, recordChatTerminal, repairInterruptedToolParts, resolveChatRecoveryConfig, resolveToolMergeId, retryAfterSeconds, runChatRecoveryExhaustion, sanitizeMessage, sendIfOpen, setChatRecovering, shouldCreditStreamProgress, sweepStaleChatRecoveryIncidents, toolApprovalUpdate, toolPartHasSettledResult, toolResultUpdate, truncateOlderMessages, truncateOlderToolResults, unwrapChatFiberSnapshot, withOriginMessageIds, wrapChatFiberSnapshot };
|
|
3822
4352
|
|
|
3823
4353
|
//# sourceMappingURL=index.js.map
|