agents 0.24.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-B2XLNMxq.d.ts → agent-routing-0DR0Tk2v.d.ts} +205 -132
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-routing.js.map +1 -1
- package/dist/agent-tool-types.d.ts +28 -22
- 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-Dj5Mzlw7.d.ts → agent-tools-YrATZJu2.d.ts} +34 -20
- package/dist/agent-tools.d.ts +20 -18
- 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-B4WbF81e.js.map +1 -1
- package/dist/{capability-runner-BUBa6Ake.d.ts → capability-runner-DBAnGr6L.d.ts} +7 -2
- package/dist/chat/index.d.ts +340 -234
- package/dist/chat/index.js +592 -66
- 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-C9DAjZIY.js → client-4OlMIJx-.js} +102 -29
- package/dist/client-4OlMIJx-.js.map +1 -0
- package/dist/{client-BxdkCuVy.d.ts → client-je3koD9Z.d.ts} +45 -10
- package/dist/client.d.ts +7 -5
- package/dist/client.js +2 -2
- 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-C4w86kcN.d.ts → current-agent-vAk_5JMt.d.ts} +4 -4
- package/dist/{durable-object-lifecycle-ei7M7lqB.d.ts → durable-object-lifecycle-HhgOMWq9.d.ts} +15 -7
- 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-Dlgtd3Mj.d.ts → index-BreL0YrQ.d.ts} +2 -2
- package/dist/index-kbSUIer3.d.ts +343 -0
- package/dist/index.d.ts +46 -44
- package/dist/index.js +1 -1
- package/dist/internal_context.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +3 -3
- package/dist/lifecycle/index.js +1 -1
- package/dist/{lifecycle-Mm_jQh7r.js → lifecycle-CFu6OV03.js} +39 -10
- package/dist/{lifecycle-Mm_jQh7r.js.map → lifecycle-CFu6OV03.js.map} +1 -1
- package/dist/mcp/client/index.d.ts +20 -20
- 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 +1 -1
- 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 +3 -3
- package/dist/queue/index.js +1 -1
- package/dist/{queue-BC9F5cUo.js → queue-BB-zfIYF.js} +2 -2
- package/dist/{queue-BC9F5cUo.js.map → queue-BB-zfIYF.js.map} +1 -1
- package/dist/react.d.ts +6 -6
- package/dist/react.js +160 -64
- package/dist/react.js.map +1 -1
- package/dist/{retries-D9B2UCq3.d.ts → retries-CV6AkXmQ.d.ts} +18 -9
- package/dist/retries.d.ts +11 -9
- package/dist/retries.js +11 -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-D_KHqxBa.js → scheduler-DZ3xQfeK.js} +2 -2
- package/dist/{scheduler-D_KHqxBa.js.map → scheduler-DZ3xQfeK.js.map} +1 -1
- package/dist/{scheduler-BGq6M5Kd.d.ts → scheduler-RdACNXDP.d.ts} +3 -3
- 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-BNU3ZiJM.js → src-BZeu-SCD.js} +423 -115
- package/dist/src-BZeu-SCD.js.map +1 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/state/index.js +1 -1
- package/dist/streams/index.d.ts +1 -1
- package/dist/streams/index.js +1 -1
- package/dist/{streams-BcE8JP2X.d.ts → streams-Cmkj9QfW.d.ts} +4 -2
- package/dist/{streams-CCPRV6dt.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-D4nLqVSI.js → tasks-68DESxf0.js} +2 -2
- package/dist/{tasks-D4nLqVSI.js.map → tasks-68DESxf0.js.map} +1 -1
- package/dist/{tasks-V2lCT8ZV.d.ts → tasks-9YxYtByh.d.ts} +2 -2
- package/dist/transport-PfFnt-Ld.d.ts +525 -0
- package/dist/{types-DCD6BNZH.d.ts → types-BA4anrjI.d.ts} +2 -2
- package/dist/voice/index.d.ts +2 -2
- package/dist/websockets/index.d.ts +23 -6
- package/dist/websockets/index.js +1 -1
- package/dist/{websockets-D7IzWbZb.js → websockets-Ci-LZFHH.js} +45 -20
- package/dist/{websockets-D7IzWbZb.js.map → websockets-Ci-LZFHH.js.map} +1 -1
- 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-tools.md +29 -4
- package/docs/browse-the-web.md +87 -5
- package/docs/chat-agents.md +124 -16
- 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 +3 -0
- package/docs/lifecycle.md +20 -4
- package/docs/models-pi-ai.md +325 -0
- package/docs/models.md +504 -0
- package/docs/resumable-streaming.md +2 -0
- package/docs/routing.md +13 -13
- package/docs/sessions.md +36 -1
- package/docs/state.md +1 -0
- package/docs/sub-agents.md +9 -0
- package/package.json +40 -2
- package/dist/agent-tools-y7zLfw4Q.js.map +0 -1
- package/dist/browser/ai.js.map +0 -1
- package/dist/client-C9DAjZIY.js.map +0 -1
- package/dist/client-tools-aIBO0Fk7.d.ts +0 -53
- package/dist/connector-CptFKzRh.js.map +0 -1
- package/dist/message-builder-BymO4N_D.js.map +0 -1
- package/dist/src-BNU3ZiJM.js.map +0 -1
- package/dist/streams-CCPRV6dt.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
|
|
@@ -496,16 +531,14 @@ const REPLAY_PAGE_SEGMENTS = 10;
|
|
|
496
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. */
|
|
@@ -856,6 +965,7 @@ var ResumableStream = class {
|
|
|
856
965
|
this.flushBuffer();
|
|
857
966
|
this.ops.settle(streamId, "errored", null);
|
|
858
967
|
if (this._pendingCutover === streamId) this._pendingCutover = null;
|
|
968
|
+
this._lastClosedStreamId = streamId;
|
|
859
969
|
this._clearActive();
|
|
860
970
|
}
|
|
861
971
|
/**
|
|
@@ -865,13 +975,17 @@ var ResumableStream = class {
|
|
|
865
975
|
* but will be missing from replay on reconnection.
|
|
866
976
|
* @param streamId - The stream this chunk belongs to
|
|
867
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.
|
|
868
981
|
*/
|
|
869
982
|
storeChunk(streamId, body) {
|
|
870
|
-
const bodyBytes =
|
|
871
|
-
if (bodyBytes >
|
|
983
|
+
const bodyBytes = storedChunkBytes(body);
|
|
984
|
+
if (bodyBytes > 18e5) {
|
|
872
985
|
console.warn(`[ResumableStream] Skipping oversized chunk (${bodyBytes} bytes) to prevent SQLite row limit crash. Live clients still receive it.`);
|
|
873
986
|
return;
|
|
874
987
|
}
|
|
988
|
+
const seq = streamId === this._activeStreamId && this._nextChunkSeq !== null ? this._nextChunkSeq++ : void 0;
|
|
875
989
|
if (this._chunkBuffer.length >= CHUNK_BUFFER_MAX_SIZE) this.flushBuffer();
|
|
876
990
|
if (this._chunkBuffer.length > 0 && this._chunkBufferBytes + bodyBytes > SEGMENT_MAX_BYTES) this.flushBuffer();
|
|
877
991
|
this._chunkBuffer.push({
|
|
@@ -880,6 +994,7 @@ var ResumableStream = class {
|
|
|
880
994
|
});
|
|
881
995
|
this._chunkBufferBytes += bodyBytes;
|
|
882
996
|
if (this._chunkBuffer.length >= CHUNK_BUFFER_SIZE) this.flushBuffer();
|
|
997
|
+
return seq;
|
|
883
998
|
}
|
|
884
999
|
/**
|
|
885
1000
|
* Flush the buffered chunks to storage as a single packed segment.
|
|
@@ -950,13 +1065,16 @@ var ResumableStream = class {
|
|
|
950
1065
|
if (!streamId) return null;
|
|
951
1066
|
this.flushBuffer();
|
|
952
1067
|
const continuation = this._activeIsContinuation;
|
|
953
|
-
if (!sendReplayBodies(connection, requestId, this._storedBodies(streamId), continuation)) return null;
|
|
1068
|
+
if (!sendReplayBodies(connection, requestId, this._storedBodies(streamId), continuation, this._seqBase(streamId))) return null;
|
|
954
1069
|
if (!this._isLive) {
|
|
1070
|
+
const row = this.ops.getStream(streamId);
|
|
955
1071
|
sendReplayControl(connection, requestId, {
|
|
956
1072
|
done: true,
|
|
957
|
-
continuation
|
|
1073
|
+
continuation,
|
|
1074
|
+
messageIds: row ? parseChatMetadata(row)?.originMessageIds : void 0,
|
|
1075
|
+
outcome: "aborted"
|
|
958
1076
|
});
|
|
959
|
-
this.complete(streamId);
|
|
1077
|
+
this.complete(streamId, "aborted");
|
|
960
1078
|
return streamId;
|
|
961
1079
|
}
|
|
962
1080
|
sendReplayControl(connection, requestId, {
|
|
@@ -979,10 +1097,33 @@ var ResumableStream = class {
|
|
|
979
1097
|
this.flushBuffer();
|
|
980
1098
|
const row = this._latestChatRowByTag(requestId, "completed");
|
|
981
1099
|
if (!row) return false;
|
|
982
|
-
const
|
|
983
|
-
|
|
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;
|
|
984
1103
|
return sendReplayControl(connection, requestId, {
|
|
985
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,
|
|
986
1127
|
continuation
|
|
987
1128
|
});
|
|
988
1129
|
}
|
|
@@ -1004,7 +1145,8 @@ var ResumableStream = class {
|
|
|
1004
1145
|
this.flushBuffer();
|
|
1005
1146
|
const row = this._latestChatRowByTag(requestId, "errored");
|
|
1006
1147
|
if (!row) return true;
|
|
1007
|
-
|
|
1148
|
+
const chat = parseChatMetadata(row);
|
|
1149
|
+
return sendReplayBodies(connection, requestId, this._storedBodies(row.stream_id), chat?.isContinuation === 1, chat?.seqBase);
|
|
1008
1150
|
}
|
|
1009
1151
|
/**
|
|
1010
1152
|
* Latest chat stream row for a request regardless of status — the recovery
|
|
@@ -1053,6 +1195,7 @@ var ResumableStream = class {
|
|
|
1053
1195
|
this._activeStreamId = row.stream_id;
|
|
1054
1196
|
this._activeRequestId = row.tag;
|
|
1055
1197
|
this._activeIsContinuation = row.chat.isContinuation === 1;
|
|
1198
|
+
this._nextChunkSeq = null;
|
|
1056
1199
|
}
|
|
1057
1200
|
}
|
|
1058
1201
|
/**
|
|
@@ -1063,6 +1206,8 @@ var ResumableStream = class {
|
|
|
1063
1206
|
this._chunkBuffer = [];
|
|
1064
1207
|
this._chunkBufferBytes = 0;
|
|
1065
1208
|
this._deleteRetiring(this._chatRows());
|
|
1209
|
+
this._deletedTerminals.clear();
|
|
1210
|
+
this._lastClosedStreamId = null;
|
|
1066
1211
|
this._activeStreamId = null;
|
|
1067
1212
|
this._activeRequestId = null;
|
|
1068
1213
|
this._activeIsContinuation = false;
|
|
@@ -2211,23 +2356,83 @@ function parseProtocolMessage(raw) {
|
|
|
2211
2356
|
/**
|
|
2212
2357
|
* Reconcile incoming client messages against server state.
|
|
2213
2358
|
*
|
|
2214
|
-
* 1.
|
|
2215
|
-
*
|
|
2216
|
-
*
|
|
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}).
|
|
2217
2368
|
*
|
|
2218
2369
|
* @param incoming - Messages from the client
|
|
2219
2370
|
* @param serverMessages - Current server-side messages (source of truth)
|
|
2220
2371
|
* @param sanitizeForContentKey - Function to sanitize a message before computing
|
|
2221
|
-
* 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)
|
|
2222
2375
|
* @returns Reconciled messages ready for persistence
|
|
2223
2376
|
*/
|
|
2224
2377
|
function reconcileMessages(incoming, serverMessages, sanitizeForContentKey) {
|
|
2225
|
-
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;
|
|
2226
2425
|
}
|
|
2227
2426
|
/**
|
|
2228
2427
|
* For a single message, resolve its ID by matching toolCallId against server state.
|
|
2229
2428
|
* Prevents duplicate DB rows when client IDs differ from server IDs.
|
|
2230
|
-
*
|
|
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`.
|
|
2231
2436
|
*/
|
|
2232
2437
|
function resolveToolMergeId(message, serverMessages) {
|
|
2233
2438
|
if (message.role !== "assistant") return message;
|
|
@@ -2293,23 +2498,28 @@ function assistantContentKey(message, sanitize) {
|
|
|
2293
2498
|
return JSON.stringify(sanitized.parts);
|
|
2294
2499
|
}
|
|
2295
2500
|
function mergeServerToolOutputs(incoming, serverMessages) {
|
|
2296
|
-
const
|
|
2501
|
+
const serverResolvedPartsByMessage = /* @__PURE__ */ new Map();
|
|
2297
2502
|
for (const msg of serverMessages) {
|
|
2298
2503
|
if (msg.role !== "assistant") continue;
|
|
2504
|
+
const resolvedParts = /* @__PURE__ */ new Map();
|
|
2299
2505
|
for (const part of msg.parts) {
|
|
2300
2506
|
const record = part;
|
|
2301
|
-
if (
|
|
2507
|
+
if (isResolvedToolPart(record)) resolvedParts.set(record.toolCallId, record);
|
|
2302
2508
|
}
|
|
2509
|
+
if (resolvedParts.size > 0) serverResolvedPartsByMessage.set(msg.id, resolvedParts);
|
|
2303
2510
|
}
|
|
2304
|
-
if (
|
|
2511
|
+
if (serverResolvedPartsByMessage.size === 0) return incoming;
|
|
2305
2512
|
return incoming.map((msg) => {
|
|
2306
2513
|
if (msg.role !== "assistant") return msg;
|
|
2514
|
+
const ownResolvedParts = serverResolvedPartsByMessage.get(msg.id);
|
|
2515
|
+
if (!ownResolvedParts) return msg;
|
|
2307
2516
|
let hasChanges = false;
|
|
2308
2517
|
const updatedParts = msg.parts.map((part) => {
|
|
2309
2518
|
const record = part;
|
|
2310
|
-
if (
|
|
2519
|
+
if (!isPendingToolPart(record)) return part;
|
|
2520
|
+
const server = ownResolvedParts.get(record.toolCallId);
|
|
2521
|
+
if (server) {
|
|
2311
2522
|
hasChanges = true;
|
|
2312
|
-
const server = serverResolvedParts.get(record.toolCallId);
|
|
2313
2523
|
const merged = {
|
|
2314
2524
|
...part,
|
|
2315
2525
|
state: server.state
|
|
@@ -2344,7 +2554,22 @@ function reconcileAssistantIds(incoming, serverMessages, sanitize) {
|
|
|
2344
2554
|
}
|
|
2345
2555
|
return incoming.map((incomingMessage, incomingIdx) => {
|
|
2346
2556
|
if (exactMatchMap.has(incomingIdx)) return incomingMessage;
|
|
2347
|
-
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
|
+
}
|
|
2348
2573
|
const incomingKey = assistantContentKey(incomingMessage, sanitize);
|
|
2349
2574
|
if (!incomingKey) return incomingMessage;
|
|
2350
2575
|
for (let i = 0; i < serverMessages.length; i++) {
|
|
@@ -2365,6 +2590,53 @@ function reconcileAssistantIds(incoming, serverMessages, sanitize) {
|
|
|
2365
2590
|
function hasToolCallPart(message) {
|
|
2366
2591
|
return message.parts.some((part) => "toolCallId" in part);
|
|
2367
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
|
+
}
|
|
2368
2640
|
function findMessageByToolCallId(messages, toolCallId) {
|
|
2369
2641
|
for (const msg of messages) {
|
|
2370
2642
|
if (msg.role !== "assistant") continue;
|
|
@@ -2396,7 +2668,7 @@ function toolPartHasSettledResult(record) {
|
|
|
2396
2668
|
*
|
|
2397
2669
|
* - a tool part with NO settled result and state `approval-responded` is kept
|
|
2398
2670
|
* verbatim (an approved server tool waiting for its continuation to run
|
|
2399
|
-
* `execute()` — not abandoned);
|
|
2671
|
+
* `execute()` — not abandoned), unless `repairApprovalResponded` is set;
|
|
2400
2672
|
* - a tool part with NO settled result for which `shouldRepair` returns false
|
|
2401
2673
|
* is kept verbatim (a part still awaiting a CLIENT interaction; see option);
|
|
2402
2674
|
* - any other tool part with no settled result is normalized then handed to
|
|
@@ -2415,7 +2687,8 @@ function repairInterruptedToolParts(messages, options) {
|
|
|
2415
2687
|
let normalizedInputs = 0;
|
|
2416
2688
|
const toolCallIds = [];
|
|
2417
2689
|
const repaired = [];
|
|
2418
|
-
for (const message of messages) {
|
|
2690
|
+
for (const [messageIndex, message] of messages.entries()) {
|
|
2691
|
+
const isLastMessage = messageIndex === messages.length - 1;
|
|
2419
2692
|
const parts = [];
|
|
2420
2693
|
let messageChanged = false;
|
|
2421
2694
|
for (const part of message.parts) {
|
|
@@ -2427,8 +2700,23 @@ function repairInterruptedToolParts(messages, options) {
|
|
|
2427
2700
|
}
|
|
2428
2701
|
if (!isSettled(record)) {
|
|
2429
2702
|
if ((typeof record.state === "string" ? record.state : "") === "approval-responded") {
|
|
2430
|
-
|
|
2431
|
-
|
|
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
|
+
}
|
|
2432
2720
|
}
|
|
2433
2721
|
if (!shouldRepair(part)) {
|
|
2434
2722
|
parts.push(part);
|
|
@@ -2493,7 +2781,7 @@ async function persistReconstructedOrphan(chunks, options) {
|
|
|
2493
2781
|
}
|
|
2494
2782
|
//#endregion
|
|
2495
2783
|
//#region src/chat/recovery.ts
|
|
2496
|
-
function createChatFiberSnapshot({ kind, requestId, recoveryRootRequestId, continuation, messages, lastBody, lastClientTools }) {
|
|
2784
|
+
function createChatFiberSnapshot({ kind, requestId, recoveryRootRequestId, continuation, messages, lastBody, lastClientTools, originMessageIds }) {
|
|
2497
2785
|
const latestMessage = messages.length > 0 ? messages[messages.length - 1] : void 0;
|
|
2498
2786
|
let latestUser;
|
|
2499
2787
|
for (let index = messages.length - 1; index >= 0; index--) if (messages[index].role === "user") {
|
|
@@ -2511,7 +2799,8 @@ function createChatFiberSnapshot({ kind, requestId, recoveryRootRequestId, conti
|
|
|
2511
2799
|
latestUserMessageId: latestUser?.id,
|
|
2512
2800
|
startedAt: Date.now(),
|
|
2513
2801
|
lastBody,
|
|
2514
|
-
lastClientTools
|
|
2802
|
+
lastClientTools,
|
|
2803
|
+
...originMessageIds ? { originMessageIds } : {}
|
|
2515
2804
|
};
|
|
2516
2805
|
}
|
|
2517
2806
|
function wrapChatFiberSnapshot(key, snapshot, user) {
|
|
@@ -2703,13 +2992,20 @@ var ResumeHandshake = class {
|
|
|
2703
2992
|
if (resumableStream.hasActiveStream() && resumableStream.activeRequestId === requestId) {
|
|
2704
2993
|
const orphanedStreamId = resumableStream.replayChunks(connection, resumableStream.activeRequestId);
|
|
2705
2994
|
if (orphanedStreamId) await this.host.persistOrphanedStream(orphanedStreamId);
|
|
2706
|
-
} else if (resumableStream.hasActiveStream()) {} else if (
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
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
|
+
}
|
|
2713
3009
|
}
|
|
2714
3010
|
/**
|
|
2715
3011
|
* Replay a pending terminal outcome (#1645) over the resume handshake so a
|
|
@@ -2741,12 +3037,14 @@ var ResumeHandshake = class {
|
|
|
2741
3037
|
const pending = await this.host.pendingChatTerminal();
|
|
2742
3038
|
if (!pending || pending.requestId !== requestId) return false;
|
|
2743
3039
|
if (!resumableStream.replayErroredChunksByRequestId(connection, pending.requestId)) return true;
|
|
3040
|
+
const messageIds = pending.messageIds ?? resumableStream.getOriginMessageIds(pending.requestId);
|
|
2744
3041
|
sendIfOpen(connection, JSON.stringify({
|
|
2745
3042
|
body: pending.body,
|
|
2746
3043
|
done: true,
|
|
2747
3044
|
error: true,
|
|
2748
3045
|
id: pending.requestId,
|
|
2749
|
-
type: responseMessageType
|
|
3046
|
+
type: responseMessageType,
|
|
3047
|
+
...messageIds && { messageIds }
|
|
2750
3048
|
}));
|
|
2751
3049
|
return true;
|
|
2752
3050
|
}
|
|
@@ -2786,7 +3084,7 @@ const CHAT_LAST_TERMINAL_KEY = "cf:chat:last-terminal";
|
|
|
2786
3084
|
const DEFAULT_CHAT_RECOVERY_MAX_ATTEMPTS = 10;
|
|
2787
3085
|
/**
|
|
2788
3086
|
* Runaway-loop guard default — the framework-imposed backstop on cumulative
|
|
2789
|
-
* recovery WORK (
|
|
3087
|
+
* recovery WORK (durable stream segments, see below) since an incident opened.
|
|
2790
3088
|
*
|
|
2791
3089
|
* Originally `Infinity` (rfc-chat-recovery-work-budget): the SDK shipped the
|
|
2792
3090
|
* *mechanism* but no default cap, so a progressing turn was never terminated on
|
|
@@ -3026,11 +3324,13 @@ var StreamProgressCreditThrottle = class {
|
|
|
3026
3324
|
* until a later turn supersedes it ({@link clearChatTerminal}); a single record
|
|
3027
3325
|
* is sufficient because only the most recent terminal is relevant.
|
|
3028
3326
|
*/
|
|
3029
|
-
async function recordChatTerminal(storage, requestId, body) {
|
|
3030
|
-
|
|
3327
|
+
async function recordChatTerminal(storage, requestId, body, messageIds) {
|
|
3328
|
+
const record = {
|
|
3031
3329
|
requestId,
|
|
3032
3330
|
body
|
|
3033
|
-
}
|
|
3331
|
+
};
|
|
3332
|
+
if (messageIds?.length) record.messageIds = messageIds;
|
|
3333
|
+
await storage.put(CHAT_LAST_TERMINAL_KEY, record);
|
|
3034
3334
|
}
|
|
3035
3335
|
/** Clear the durable terminal record once a later turn supersedes it (#1645). */
|
|
3036
3336
|
async function clearChatTerminal(storage) {
|
|
@@ -3127,10 +3427,12 @@ async function evaluateChatRecoveryIncident(input) {
|
|
|
3127
3427
|
const workBudgetExceeded = existing != null && Number.isFinite(config.maxRecoveryWork) && work > config.maxRecoveryWork;
|
|
3128
3428
|
const oomAttempts = existing?.oomAttempts ?? 0;
|
|
3129
3429
|
const oomBudgetExceeded = existing != null && !awaitingClientInteraction && oomAttempts > config.maxOomRetries;
|
|
3430
|
+
const transientRetries = existing?.transientRetries ?? 0;
|
|
3431
|
+
const transientBudgetExceeded = existing != null && !awaitingClientInteraction && transientRetries >= config.maxAttempts;
|
|
3130
3432
|
const debounced = existing != null && !madeProgress && now - existing.lastAttemptAt < 3e4;
|
|
3131
3433
|
const attempt = madeProgress ? 1 : debounced ? existing?.attempt ?? 1 : (existing?.attempt ?? 0) + 1;
|
|
3132
3434
|
let abortedByCaller = false;
|
|
3133
|
-
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 {
|
|
3134
3436
|
const ctx = {
|
|
3135
3437
|
incidentId,
|
|
3136
3438
|
requestId: identity.requestId,
|
|
@@ -3145,7 +3447,7 @@ async function evaluateChatRecoveryIncident(input) {
|
|
|
3145
3447
|
} catch (error) {
|
|
3146
3448
|
input.onShouldKeepRecoveringError?.(error);
|
|
3147
3449
|
}
|
|
3148
|
-
const exhausted = !awaitingClientInteraction && (oomBudgetExceeded || noProgressExceeded || workBudgetExceeded || abortedByCaller || attempt > config.maxAttempts);
|
|
3450
|
+
const exhausted = !awaitingClientInteraction && (oomBudgetExceeded || noProgressExceeded || workBudgetExceeded || abortedByCaller || transientBudgetExceeded || attempt > config.maxAttempts);
|
|
3149
3451
|
const incident = {
|
|
3150
3452
|
incidentId,
|
|
3151
3453
|
requestId: identity.requestId,
|
|
@@ -3160,6 +3462,7 @@ async function evaluateChatRecoveryIncident(input) {
|
|
|
3160
3462
|
progress,
|
|
3161
3463
|
workBaseline,
|
|
3162
3464
|
...oomAttempts > 0 ? { oomAttempts } : {},
|
|
3465
|
+
...transientRetries > 0 ? { transientRetries } : {},
|
|
3163
3466
|
...exhausted ? { reason: oomBudgetExceeded ? "out_of_memory" : workBudgetExceeded ? "work_budget_exceeded" : noProgressExceeded ? "no_progress_timeout" : abortedByCaller ? "recovery_aborted" : "max_attempts_exceeded" } : {}
|
|
3164
3467
|
};
|
|
3165
3468
|
const events = [];
|
|
@@ -3272,7 +3575,7 @@ var ChatRecoveryEngine = class {
|
|
|
3272
3575
|
options: void 0,
|
|
3273
3576
|
partial
|
|
3274
3577
|
})) await wake.persistOrphanedStream(streamId);
|
|
3275
|
-
await adapter.exhaustChatRecovery(incident, config, partial, streamId, ctx.createdAt);
|
|
3578
|
+
await adapter.exhaustChatRecovery(incident, config, partial, streamId, ctx.createdAt, snapshot?.originMessageIds);
|
|
3276
3579
|
return true;
|
|
3277
3580
|
}
|
|
3278
3581
|
try {
|
|
@@ -3386,7 +3689,7 @@ var ChatRecoveryEngine = class {
|
|
|
3386
3689
|
maxAttempts: incident.maxAttempts,
|
|
3387
3690
|
recoveryKind: input.recoveryKind
|
|
3388
3691
|
});
|
|
3389
|
-
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);
|
|
3390
3693
|
}
|
|
3391
3694
|
/**
|
|
3392
3695
|
* Reschedule a recovery continuation/retry that timed out waiting for stable
|
|
@@ -3425,6 +3728,47 @@ var ChatRecoveryEngine = class {
|
|
|
3425
3728
|
return true;
|
|
3426
3729
|
}
|
|
3427
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
|
+
/**
|
|
3428
3772
|
* Record that a recovery callback observed a Durable Object memory-limit reset
|
|
3429
3773
|
* (the isolate exceeded its 128 MB limit — `isDurableObjectMemoryLimitReset`)
|
|
3430
3774
|
* and decide what to do next (#1825).
|
|
@@ -3591,6 +3935,47 @@ var ChatRecoveryEngine = class {
|
|
|
3591
3935
|
else if (status === "completed" || status === "skipped" || status === "failed") await adapter.setRecovering(false);
|
|
3592
3936
|
}
|
|
3593
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
|
+
}
|
|
3594
3979
|
/**
|
|
3595
3980
|
* Build the `ChatRecoveryExhaustedContext` delivered to `onExhausted` and the
|
|
3596
3981
|
* `chat:recovery:exhausted` event. Pure field-mapping shared by both packages;
|
|
@@ -3750,20 +4135,14 @@ async function* iterateWithStallWatchdog(source, timeoutMs, onStall) {
|
|
|
3750
4135
|
//#endregion
|
|
3751
4136
|
//#region src/chat/truncate-older-messages.ts
|
|
3752
4137
|
/**
|
|
3753
|
-
* Read-time context truncation.
|
|
3754
|
-
*
|
|
3755
|
-
* Truncates older tool outputs and long text before sending to the LLM.
|
|
3756
|
-
* Structured tool outputs keep their container shape so tool-specific
|
|
3757
|
-
* `toModelOutput` handlers can safely replay older results.
|
|
3758
|
-
* Does NOT mutate stored messages — operates on a copy.
|
|
3759
|
-
*/
|
|
3760
|
-
/**
|
|
3761
4138
|
* Truncate tool outputs and long text in older messages.
|
|
3762
4139
|
* Returns a new array — input messages are not mutated.
|
|
3763
4140
|
*
|
|
3764
4141
|
* Recent messages (last `keepRecent`) are left intact.
|
|
3765
4142
|
* Older messages get tool outputs and long text truncated. Structured tool
|
|
3766
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.
|
|
3767
4146
|
*
|
|
3768
4147
|
* Use in assembleContext() before sending to the LLM:
|
|
3769
4148
|
* ```typescript
|
|
@@ -3778,6 +4157,7 @@ function truncateOlderMessages(messages, options) {
|
|
|
3778
4157
|
const keepRecent = options?.keepRecent ?? 4;
|
|
3779
4158
|
const maxToolOutput = options?.maxToolOutputChars ?? 500;
|
|
3780
4159
|
const maxText = options?.maxTextChars ?? 1e4;
|
|
4160
|
+
const truncateToolOutputs = options?.toolOutputs ?? true;
|
|
3781
4161
|
if (messages.length <= keepRecent) return messages;
|
|
3782
4162
|
const cutoff = messages.length - keepRecent;
|
|
3783
4163
|
const result = [];
|
|
@@ -3789,7 +4169,7 @@ function truncateOlderMessages(messages, options) {
|
|
|
3789
4169
|
const msg = messages[i];
|
|
3790
4170
|
let changed = false;
|
|
3791
4171
|
const truncatedParts = msg.parts.map((part) => {
|
|
3792
|
-
if (
|
|
4172
|
+
if (truncateToolOutputs && isToolPart(part) && "output" in part && !isProviderExecuted(part)) {
|
|
3793
4173
|
const output = part.output;
|
|
3794
4174
|
if (output !== void 0) {
|
|
3795
4175
|
const truncated = truncateToolOutput(output, maxToolOutput);
|
|
@@ -3821,7 +4201,153 @@ function truncateOlderMessages(messages, options) {
|
|
|
3821
4201
|
}
|
|
3822
4202
|
return result;
|
|
3823
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
|
+
}
|
|
3824
4350
|
//#endregion
|
|
3825
|
-
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 };
|
|
3826
4352
|
|
|
3827
4353
|
//# sourceMappingURL=index.js.map
|