agents 0.14.2 → 0.14.4
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/LICENSE +21 -0
- package/dist/chat/index.d.ts +62 -4
- package/dist/chat/index.js +140 -28
- package/dist/chat/index.js.map +1 -1
- package/package.json +3 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License Copyright (c) 2025 Cloudflare, Inc.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of
|
|
4
|
+
charge, to any person obtaining a copy of this software and associated
|
|
5
|
+
documentation files (the "Software"), to deal in the Software without
|
|
6
|
+
restriction, including without limitation the rights to use, copy, modify, merge,
|
|
7
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice
|
|
12
|
+
(including the next paragraph) shall be included in all copies or substantial
|
|
13
|
+
portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
16
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
18
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
19
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/dist/chat/index.d.ts
CHANGED
|
@@ -663,7 +663,8 @@ declare class ResumableStream {
|
|
|
663
663
|
private sql;
|
|
664
664
|
private _activeStreamId;
|
|
665
665
|
private _activeRequestId;
|
|
666
|
-
|
|
666
|
+
/** Monotonic row-ordering index; one increment per flushed segment row. */
|
|
667
|
+
private _segmentIndex;
|
|
667
668
|
/**
|
|
668
669
|
* Whether the active stream was started in this instance (true) or
|
|
669
670
|
* restored from SQLite after hibernation/restart (false). An orphaned
|
|
@@ -672,9 +673,17 @@ declare class ResumableStream {
|
|
|
672
673
|
*/
|
|
673
674
|
private _isLive;
|
|
674
675
|
private _chunkBuffer;
|
|
676
|
+
private _chunkBufferBytes;
|
|
675
677
|
private _isFlushingChunks;
|
|
676
678
|
private _lastCleanupTime;
|
|
677
679
|
constructor(sql: SqlTaggedTemplate);
|
|
680
|
+
/**
|
|
681
|
+
* Add the #1691 recovery column to the metadata table for rows created before
|
|
682
|
+
* it existed. Inspects the current schema and only runs `alter table` when the
|
|
683
|
+
* column is absent — idempotent across Durable Object restarts, with no
|
|
684
|
+
* error-driven control flow.
|
|
685
|
+
*/
|
|
686
|
+
private _migrateMetadataColumns;
|
|
678
687
|
get activeStreamId(): string | null;
|
|
679
688
|
get activeRequestId(): string | null;
|
|
680
689
|
hasActiveStream(): boolean;
|
|
@@ -689,7 +698,19 @@ declare class ResumableStream {
|
|
|
689
698
|
* @param requestId - The unique ID of the chat request
|
|
690
699
|
* @returns The generated stream ID
|
|
691
700
|
*/
|
|
692
|
-
start(
|
|
701
|
+
start(
|
|
702
|
+
requestId: string,
|
|
703
|
+
options?: {
|
|
704
|
+
messageId?: string;
|
|
705
|
+
}
|
|
706
|
+
): string;
|
|
707
|
+
/**
|
|
708
|
+
* The assistant message id an orphaned stream was producing — the same id the
|
|
709
|
+
* live path persists under, so recovery re-associates reconstructed chunks
|
|
710
|
+
* with the correct message (#1691). Returns null when the row is missing or
|
|
711
|
+
* is a legacy row written before the `message_id` column existed.
|
|
712
|
+
*/
|
|
713
|
+
getStreamMessageId(streamId: string): string | null;
|
|
693
714
|
/**
|
|
694
715
|
* Mark a stream as completed and flush any pending chunks.
|
|
695
716
|
* @param streamId - The stream to mark as completed
|
|
@@ -712,8 +733,13 @@ declare class ResumableStream {
|
|
|
712
733
|
*/
|
|
713
734
|
storeChunk(streamId: string, body: string): void;
|
|
714
735
|
/**
|
|
715
|
-
* Flush buffered chunks to SQLite
|
|
736
|
+
* Flush the buffered chunks to SQLite as a single packed row.
|
|
716
737
|
* Uses a lock to prevent concurrent flush operations.
|
|
738
|
+
*
|
|
739
|
+
* The whole buffer becomes one row: a single-chunk segment is stored
|
|
740
|
+
* unwrapped (legacy object format) so a large chunk avoids array-escaping
|
|
741
|
+
* inflation, while a multi-chunk segment stores a JSON array of bodies. This
|
|
742
|
+
* collapses N chunk rows into one, cutting rows written / stored / scanned.
|
|
717
743
|
*/
|
|
718
744
|
flushBuffer(): void;
|
|
719
745
|
/**
|
|
@@ -757,7 +783,12 @@ declare class ResumableStream {
|
|
|
757
783
|
*/
|
|
758
784
|
destroy(): void;
|
|
759
785
|
private _maybeCleanupOldStreams;
|
|
760
|
-
/**
|
|
786
|
+
/**
|
|
787
|
+
* Return the stored chunks for a stream as individual chunk bodies in order,
|
|
788
|
+
* unpacking packed segment rows. The returned `chunk_index` is a running
|
|
789
|
+
* per-chunk sequence (0, 1, 2, …) — stable across calls because rows are
|
|
790
|
+
* append-only — so callers can use it as a monotonic chunk sequence.
|
|
791
|
+
*/
|
|
761
792
|
getStreamChunks(streamId: string): Array<{
|
|
762
793
|
body: string;
|
|
763
794
|
chunk_index: number;
|
|
@@ -778,6 +809,31 @@ declare class ResumableStream {
|
|
|
778
809
|
insertStaleStream(streamId: string, requestId: string, ageMs: number): void;
|
|
779
810
|
}
|
|
780
811
|
//#endregion
|
|
812
|
+
//#region src/chat/sql-batch.d.ts
|
|
813
|
+
/**
|
|
814
|
+
* Helpers for building batched SQLite statements that run through the Agent's
|
|
815
|
+
* `sql` tagged template (which interleaves a `?` placeholder between every
|
|
816
|
+
* string fragment). Used to collapse per-row INSERT/DELETE loops into a small
|
|
817
|
+
* number of multi-row statements.
|
|
818
|
+
*
|
|
819
|
+
* SQLite (Durable Object / D1) caps bound parameters at 100 per query, so
|
|
820
|
+
* callers must chunk their inputs to stay within {@link MAX_BOUND_PARAMS}.
|
|
821
|
+
* See https://developers.cloudflare.com/d1/platform/limits/
|
|
822
|
+
*/
|
|
823
|
+
/** Maximum bound parameters allowed in a single SQLite (DO / D1) query. */
|
|
824
|
+
declare const MAX_BOUND_PARAMS = 100;
|
|
825
|
+
/**
|
|
826
|
+
* Build a TemplateStringsArray for a single-column `IN (...)` clause. Produces
|
|
827
|
+
* fragments for:
|
|
828
|
+
* `${prefix}(?, ?, ...)`
|
|
829
|
+
*
|
|
830
|
+
* @throws if `count` is less than 1.
|
|
831
|
+
*/
|
|
832
|
+
declare function buildInClauseStrings(
|
|
833
|
+
prefix: string,
|
|
834
|
+
count: number
|
|
835
|
+
): TemplateStringsArray;
|
|
836
|
+
//#endregion
|
|
781
837
|
//#region src/chat/protocol.d.ts
|
|
782
838
|
/**
|
|
783
839
|
* Wire protocol message type constants for the cf_agent_chat_* protocol.
|
|
@@ -1228,6 +1284,7 @@ export {
|
|
|
1228
1284
|
type ContinuationPending,
|
|
1229
1285
|
ContinuationState,
|
|
1230
1286
|
type EnqueueOptions,
|
|
1287
|
+
MAX_BOUND_PARAMS,
|
|
1231
1288
|
type MessageConcurrency,
|
|
1232
1289
|
type MessagePart,
|
|
1233
1290
|
type MessageParts,
|
|
@@ -1251,6 +1308,7 @@ export {
|
|
|
1251
1308
|
applyToolUpdate,
|
|
1252
1309
|
assistantContentKey,
|
|
1253
1310
|
transition as broadcastTransition,
|
|
1311
|
+
buildInClauseStrings,
|
|
1254
1312
|
byteLength,
|
|
1255
1313
|
createAgentToolEventState,
|
|
1256
1314
|
createChatFiberSnapshot,
|
package/dist/chat/index.js
CHANGED
|
@@ -536,16 +536,39 @@ const CHAT_MESSAGE_TYPES = {
|
|
|
536
536
|
* - Stale stream cleanup
|
|
537
537
|
* - Active stream restoration after agent restart
|
|
538
538
|
*/
|
|
539
|
-
/** Number of chunks to
|
|
539
|
+
/** Number of chunks to pack into a single SQLite row before flushing */
|
|
540
540
|
const CHUNK_BUFFER_SIZE = 10;
|
|
541
541
|
/** Maximum buffer size to prevent memory issues on rapid reconnections */
|
|
542
542
|
const CHUNK_BUFFER_MAX_SIZE = 100;
|
|
543
|
+
/**
|
|
544
|
+
* Max accumulated raw chunk bytes packed into one row before forcing a flush.
|
|
545
|
+
* The SQLite row limit is 2 MB; packing serializes bodies into a JSON array,
|
|
546
|
+
* which re-escapes their contents (quotes/backslashes), so we keep the raw
|
|
547
|
+
* total well under the limit to leave generous headroom for escaping overhead.
|
|
548
|
+
* A chunk larger than this is flushed as its own (unwrapped) row.
|
|
549
|
+
*/
|
|
550
|
+
const SEGMENT_MAX_BYTES = 512e3;
|
|
543
551
|
/** Default cleanup interval for old streams (ms) - every 10 minutes */
|
|
544
552
|
const CLEANUP_INTERVAL_MS = 600 * 1e3;
|
|
545
553
|
/** Default age threshold for cleaning up completed streams (ms) - 24 hours */
|
|
546
554
|
const CLEANUP_AGE_THRESHOLD_MS = 1440 * 60 * 1e3;
|
|
547
555
|
/** Shared encoder for UTF-8 byte length measurement */
|
|
548
556
|
const textEncoder = new TextEncoder();
|
|
557
|
+
/**
|
|
558
|
+
* A stored row body is either a single chunk body (a JSON object string —
|
|
559
|
+
* legacy per-chunk rows and single-chunk segments) or a packed segment (a JSON
|
|
560
|
+
* array of chunk body strings). Unpack to the individual chunk bodies in order.
|
|
561
|
+
*
|
|
562
|
+
* Stored chunk bodies are always serialized JSON *objects*, never arrays, so
|
|
563
|
+
* `Array.isArray` reliably distinguishes a packed segment from a single body.
|
|
564
|
+
*/
|
|
565
|
+
function unpackSegmentBody(rowBody) {
|
|
566
|
+
try {
|
|
567
|
+
const parsed = JSON.parse(rowBody);
|
|
568
|
+
if (Array.isArray(parsed)) return parsed;
|
|
569
|
+
} catch {}
|
|
570
|
+
return [rowBody];
|
|
571
|
+
}
|
|
549
572
|
function sendIfOpen$1(connection, message) {
|
|
550
573
|
try {
|
|
551
574
|
connection.send(message);
|
|
@@ -563,9 +586,10 @@ var ResumableStream = class ResumableStream {
|
|
|
563
586
|
this.sql = sql;
|
|
564
587
|
this._activeStreamId = null;
|
|
565
588
|
this._activeRequestId = null;
|
|
566
|
-
this.
|
|
589
|
+
this._segmentIndex = 0;
|
|
567
590
|
this._isLive = false;
|
|
568
591
|
this._chunkBuffer = [];
|
|
592
|
+
this._chunkBufferBytes = 0;
|
|
569
593
|
this._isFlushingChunks = false;
|
|
570
594
|
this._lastCleanupTime = 0;
|
|
571
595
|
this.sql`create table if not exists cf_ai_chat_stream_chunks (
|
|
@@ -582,10 +606,22 @@ var ResumableStream = class ResumableStream {
|
|
|
582
606
|
created_at integer not null,
|
|
583
607
|
completed_at integer
|
|
584
608
|
)`;
|
|
609
|
+
this._migrateMetadataColumns();
|
|
585
610
|
this.sql`create index if not exists idx_stream_chunks_stream_id
|
|
586
611
|
on cf_ai_chat_stream_chunks(stream_id, chunk_index)`;
|
|
587
612
|
this.restore();
|
|
588
613
|
}
|
|
614
|
+
/**
|
|
615
|
+
* Add the #1691 recovery column to the metadata table for rows created before
|
|
616
|
+
* it existed. Inspects the current schema and only runs `alter table` when the
|
|
617
|
+
* column is absent — idempotent across Durable Object restarts, with no
|
|
618
|
+
* error-driven control flow.
|
|
619
|
+
*/
|
|
620
|
+
_migrateMetadataColumns() {
|
|
621
|
+
if (!(this.sql`
|
|
622
|
+
select name from pragma_table_info('cf_ai_chat_stream_metadata')
|
|
623
|
+
` ?? []).some((column) => column.name === "message_id")) this.sql`alter table cf_ai_chat_stream_metadata add column message_id text`;
|
|
624
|
+
}
|
|
589
625
|
get activeStreamId() {
|
|
590
626
|
return this._activeStreamId;
|
|
591
627
|
}
|
|
@@ -608,20 +644,35 @@ var ResumableStream = class ResumableStream {
|
|
|
608
644
|
* @param requestId - The unique ID of the chat request
|
|
609
645
|
* @returns The generated stream ID
|
|
610
646
|
*/
|
|
611
|
-
start(requestId) {
|
|
647
|
+
start(requestId, options = {}) {
|
|
612
648
|
this.flushBuffer();
|
|
613
649
|
const streamId = nanoid();
|
|
614
650
|
this._activeStreamId = streamId;
|
|
615
651
|
this._activeRequestId = requestId;
|
|
616
|
-
this.
|
|
652
|
+
this._segmentIndex = 0;
|
|
617
653
|
this._isLive = true;
|
|
654
|
+
const messageId = options.messageId ?? null;
|
|
618
655
|
this.sql`
|
|
619
|
-
insert into cf_ai_chat_stream_metadata (id, request_id, status, created_at)
|
|
620
|
-
values (${streamId}, ${requestId}, 'streaming', ${Date.now()})
|
|
656
|
+
insert into cf_ai_chat_stream_metadata (id, request_id, status, created_at, message_id)
|
|
657
|
+
values (${streamId}, ${requestId}, 'streaming', ${Date.now()}, ${messageId})
|
|
621
658
|
`;
|
|
622
659
|
return streamId;
|
|
623
660
|
}
|
|
624
661
|
/**
|
|
662
|
+
* The assistant message id an orphaned stream was producing — the same id the
|
|
663
|
+
* live path persists under, so recovery re-associates reconstructed chunks
|
|
664
|
+
* with the correct message (#1691). Returns null when the row is missing or
|
|
665
|
+
* is a legacy row written before the `message_id` column existed.
|
|
666
|
+
*/
|
|
667
|
+
getStreamMessageId(streamId) {
|
|
668
|
+
const rows = this.sql`
|
|
669
|
+
select message_id from cf_ai_chat_stream_metadata
|
|
670
|
+
where id = ${streamId}
|
|
671
|
+
`;
|
|
672
|
+
if (!rows || rows.length === 0) return null;
|
|
673
|
+
return rows[0].message_id ?? null;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
625
676
|
* Mark a stream as completed and flush any pending chunks.
|
|
626
677
|
* @param streamId - The stream to mark as completed
|
|
627
678
|
*/
|
|
@@ -634,7 +685,7 @@ var ResumableStream = class ResumableStream {
|
|
|
634
685
|
`;
|
|
635
686
|
this._activeStreamId = null;
|
|
636
687
|
this._activeRequestId = null;
|
|
637
|
-
this.
|
|
688
|
+
this._segmentIndex = 0;
|
|
638
689
|
this._isLive = false;
|
|
639
690
|
this._maybeCleanupOldStreams();
|
|
640
691
|
}
|
|
@@ -651,7 +702,7 @@ var ResumableStream = class ResumableStream {
|
|
|
651
702
|
`;
|
|
652
703
|
this._activeStreamId = null;
|
|
653
704
|
this._activeRequestId = null;
|
|
654
|
-
this.
|
|
705
|
+
this._segmentIndex = 0;
|
|
655
706
|
this._isLive = false;
|
|
656
707
|
}
|
|
657
708
|
/**
|
|
@@ -669,18 +720,22 @@ var ResumableStream = class ResumableStream {
|
|
|
669
720
|
return;
|
|
670
721
|
}
|
|
671
722
|
if (this._chunkBuffer.length >= CHUNK_BUFFER_MAX_SIZE) this.flushBuffer();
|
|
723
|
+
if (this._chunkBuffer.length > 0 && this._chunkBufferBytes + bodyBytes > SEGMENT_MAX_BYTES) this.flushBuffer();
|
|
672
724
|
this._chunkBuffer.push({
|
|
673
|
-
id: nanoid(),
|
|
674
725
|
streamId,
|
|
675
|
-
body
|
|
676
|
-
index: this._streamChunkIndex
|
|
726
|
+
body
|
|
677
727
|
});
|
|
678
|
-
this.
|
|
728
|
+
this._chunkBufferBytes += bodyBytes;
|
|
679
729
|
if (this._chunkBuffer.length >= CHUNK_BUFFER_SIZE) this.flushBuffer();
|
|
680
730
|
}
|
|
681
731
|
/**
|
|
682
|
-
* Flush buffered chunks to SQLite
|
|
732
|
+
* Flush the buffered chunks to SQLite as a single packed row.
|
|
683
733
|
* Uses a lock to prevent concurrent flush operations.
|
|
734
|
+
*
|
|
735
|
+
* The whole buffer becomes one row: a single-chunk segment is stored
|
|
736
|
+
* unwrapped (legacy object format) so a large chunk avoids array-escaping
|
|
737
|
+
* inflation, while a multi-chunk segment stores a JSON array of bodies. This
|
|
738
|
+
* collapses N chunk rows into one, cutting rows written / stored / scanned.
|
|
684
739
|
*/
|
|
685
740
|
flushBuffer() {
|
|
686
741
|
if (this._isFlushingChunks || this._chunkBuffer.length === 0) return;
|
|
@@ -688,11 +743,14 @@ var ResumableStream = class ResumableStream {
|
|
|
688
743
|
try {
|
|
689
744
|
const chunks = this._chunkBuffer;
|
|
690
745
|
this._chunkBuffer = [];
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
746
|
+
this._chunkBufferBytes = 0;
|
|
747
|
+
const streamId = chunks[0].streamId;
|
|
748
|
+
const segmentBody = chunks.length === 1 ? chunks[0].body : JSON.stringify(chunks.map((chunk) => chunk.body));
|
|
749
|
+
this.sql`
|
|
750
|
+
insert into cf_ai_chat_stream_chunks (id, stream_id, body, chunk_index, created_at)
|
|
751
|
+
values (${nanoid()}, ${streamId}, ${segmentBody}, ${this._segmentIndex}, ${Date.now()})
|
|
752
|
+
`;
|
|
753
|
+
this._segmentIndex++;
|
|
696
754
|
} finally {
|
|
697
755
|
this._isFlushingChunks = false;
|
|
698
756
|
}
|
|
@@ -727,8 +785,8 @@ var ResumableStream = class ResumableStream {
|
|
|
727
785
|
where stream_id = ${streamId}
|
|
728
786
|
order by chunk_index asc
|
|
729
787
|
`;
|
|
730
|
-
for (const chunk of chunks || []) if (!sendIfOpen$1(connection, JSON.stringify({
|
|
731
|
-
body
|
|
788
|
+
for (const chunk of chunks || []) for (const body of unpackSegmentBody(chunk.body)) if (!sendIfOpen$1(connection, JSON.stringify({
|
|
789
|
+
body,
|
|
732
790
|
done: false,
|
|
733
791
|
id: requestId,
|
|
734
792
|
type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,
|
|
@@ -780,8 +838,8 @@ var ResumableStream = class ResumableStream {
|
|
|
780
838
|
where stream_id = ${stream.id}
|
|
781
839
|
order by chunk_index asc
|
|
782
840
|
`;
|
|
783
|
-
for (const chunk of chunks || []) if (!sendIfOpen$1(connection, JSON.stringify({
|
|
784
|
-
body
|
|
841
|
+
for (const chunk of chunks || []) for (const body of unpackSegmentBody(chunk.body)) if (!sendIfOpen$1(connection, JSON.stringify({
|
|
842
|
+
body,
|
|
785
843
|
done: false,
|
|
786
844
|
id: requestId,
|
|
787
845
|
type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,
|
|
@@ -816,7 +874,7 @@ var ResumableStream = class ResumableStream {
|
|
|
816
874
|
from cf_ai_chat_stream_chunks
|
|
817
875
|
where stream_id = ${this._activeStreamId}
|
|
818
876
|
`;
|
|
819
|
-
this.
|
|
877
|
+
this._segmentIndex = lastChunk && lastChunk[0]?.max_index != null ? lastChunk[0].max_index + 1 : 0;
|
|
820
878
|
}
|
|
821
879
|
}
|
|
822
880
|
/**
|
|
@@ -824,11 +882,12 @@ var ResumableStream = class ResumableStream {
|
|
|
824
882
|
*/
|
|
825
883
|
clearAll() {
|
|
826
884
|
this._chunkBuffer = [];
|
|
885
|
+
this._chunkBufferBytes = 0;
|
|
827
886
|
this.sql`delete from cf_ai_chat_stream_chunks`;
|
|
828
887
|
this.sql`delete from cf_ai_chat_stream_metadata`;
|
|
829
888
|
this._activeStreamId = null;
|
|
830
889
|
this._activeRequestId = null;
|
|
831
|
-
this.
|
|
890
|
+
this._segmentIndex = 0;
|
|
832
891
|
}
|
|
833
892
|
/**
|
|
834
893
|
* Drop all stream tables (called on destroy).
|
|
@@ -868,13 +927,28 @@ var ResumableStream = class ResumableStream {
|
|
|
868
927
|
where status = 'streaming' and created_at < ${cutoff}
|
|
869
928
|
`;
|
|
870
929
|
}
|
|
871
|
-
/**
|
|
930
|
+
/**
|
|
931
|
+
* Return the stored chunks for a stream as individual chunk bodies in order,
|
|
932
|
+
* unpacking packed segment rows. The returned `chunk_index` is a running
|
|
933
|
+
* per-chunk sequence (0, 1, 2, …) — stable across calls because rows are
|
|
934
|
+
* append-only — so callers can use it as a monotonic chunk sequence.
|
|
935
|
+
*/
|
|
872
936
|
getStreamChunks(streamId) {
|
|
873
|
-
|
|
874
|
-
select body
|
|
937
|
+
const rows = this.sql`
|
|
938
|
+
select body from cf_ai_chat_stream_chunks
|
|
875
939
|
where stream_id = ${streamId}
|
|
876
940
|
order by chunk_index asc
|
|
877
941
|
` || [];
|
|
942
|
+
const out = [];
|
|
943
|
+
let index = 0;
|
|
944
|
+
for (const row of rows) for (const body of unpackSegmentBody(row.body)) {
|
|
945
|
+
out.push({
|
|
946
|
+
body,
|
|
947
|
+
chunk_index: index
|
|
948
|
+
});
|
|
949
|
+
index++;
|
|
950
|
+
}
|
|
951
|
+
return out;
|
|
878
952
|
}
|
|
879
953
|
/** @internal For testing only */
|
|
880
954
|
getStreamMetadata(streamId) {
|
|
@@ -899,6 +973,44 @@ var ResumableStream = class ResumableStream {
|
|
|
899
973
|
};
|
|
900
974
|
ResumableStream.CHUNK_MAX_BYTES = 18e5;
|
|
901
975
|
//#endregion
|
|
976
|
+
//#region src/chat/sql-batch.ts
|
|
977
|
+
/**
|
|
978
|
+
* Helpers for building batched SQLite statements that run through the Agent's
|
|
979
|
+
* `sql` tagged template (which interleaves a `?` placeholder between every
|
|
980
|
+
* string fragment). Used to collapse per-row INSERT/DELETE loops into a small
|
|
981
|
+
* number of multi-row statements.
|
|
982
|
+
*
|
|
983
|
+
* SQLite (Durable Object / D1) caps bound parameters at 100 per query, so
|
|
984
|
+
* callers must chunk their inputs to stay within {@link MAX_BOUND_PARAMS}.
|
|
985
|
+
* See https://developers.cloudflare.com/d1/platform/limits/
|
|
986
|
+
*/
|
|
987
|
+
/** Maximum bound parameters allowed in a single SQLite (DO / D1) query. */
|
|
988
|
+
const MAX_BOUND_PARAMS = 100;
|
|
989
|
+
/**
|
|
990
|
+
* Attach a self-referential `raw` property so a plain string[] satisfies the
|
|
991
|
+
* TemplateStringsArray shape. `sql` only reads indexed string fragments, so
|
|
992
|
+
* `raw` is never consumed — this just keeps the type system happy.
|
|
993
|
+
*/
|
|
994
|
+
function asTemplateStringsArray(parts) {
|
|
995
|
+
parts.raw = parts;
|
|
996
|
+
return parts;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Build a TemplateStringsArray for a single-column `IN (...)` clause. Produces
|
|
1000
|
+
* fragments for:
|
|
1001
|
+
* `${prefix}(?, ?, ...)`
|
|
1002
|
+
*
|
|
1003
|
+
* @throws if `count` is less than 1.
|
|
1004
|
+
*/
|
|
1005
|
+
function buildInClauseStrings(prefix, count) {
|
|
1006
|
+
if (count < 1) throw new Error(`buildInClauseStrings requires count >= 1 (got ${count})`);
|
|
1007
|
+
const parts = new Array(count + 1);
|
|
1008
|
+
parts[0] = `${prefix}(`;
|
|
1009
|
+
for (let i = 1; i < count; i++) parts[i] = ", ";
|
|
1010
|
+
parts[count] = ")";
|
|
1011
|
+
return asTemplateStringsArray(parts);
|
|
1012
|
+
}
|
|
1013
|
+
//#endregion
|
|
902
1014
|
//#region src/chat/client-tools.ts
|
|
903
1015
|
/**
|
|
904
1016
|
* Converts client tool schemas to AI SDK tool format.
|
|
@@ -1515,6 +1627,6 @@ function unwrapChatFiberSnapshot(key, value, expectedKind) {
|
|
|
1515
1627
|
};
|
|
1516
1628
|
}
|
|
1517
1629
|
//#endregion
|
|
1518
|
-
export { AbortRegistry, CHAT_MESSAGE_TYPES, ContinuationState, ROW_MAX_BYTES, ResumableStream, StreamAccumulator, SubmitConcurrencyController, TurnQueue, applyAgentToolEvent, applyChunkToParts, applyToolUpdate, assistantContentKey, transition as broadcastTransition, byteLength, createAgentToolEventState, createChatFiberSnapshot, createToolsFromClientSchemas, crossMessageToolResultUpdate, enforceRowSizeLimit, isReplayChunk, normalizeToolInput, parseProtocolMessage, reconcileMessages, resolveToolMergeId, sanitizeMessage, toolApprovalUpdate, toolResultUpdate, unwrapChatFiberSnapshot, wrapChatFiberSnapshot };
|
|
1630
|
+
export { AbortRegistry, CHAT_MESSAGE_TYPES, ContinuationState, MAX_BOUND_PARAMS, ROW_MAX_BYTES, ResumableStream, StreamAccumulator, SubmitConcurrencyController, TurnQueue, applyAgentToolEvent, applyChunkToParts, applyToolUpdate, assistantContentKey, transition as broadcastTransition, buildInClauseStrings, byteLength, createAgentToolEventState, createChatFiberSnapshot, createToolsFromClientSchemas, crossMessageToolResultUpdate, enforceRowSizeLimit, isReplayChunk, normalizeToolInput, parseProtocolMessage, reconcileMessages, resolveToolMergeId, sanitizeMessage, toolApprovalUpdate, toolResultUpdate, unwrapChatFiberSnapshot, wrapChatFiberSnapshot };
|
|
1519
1631
|
|
|
1520
1632
|
//# sourceMappingURL=index.js.map
|
package/dist/chat/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["textEncoder","sendIfOpen","isWebSocketClosedSendError"],"sources":["../../src/chat/sanitize.ts","../../src/chat/stream-accumulator.ts","../../src/chat/turn-queue.ts","../../src/chat/submit-concurrency.ts","../../src/chat/broadcast-state.ts","../../src/chat/protocol.ts","../../src/chat/resumable-stream.ts","../../src/chat/client-tools.ts","../../src/chat/continuation-state.ts","../../src/chat/abort-registry.ts","../../src/chat/tool-state.ts","../../src/chat/parse-protocol.ts","../../src/chat/message-reconciler.ts","../../src/chat/recovery.ts"],"sourcesContent":["/**\n * Message sanitization and row-size enforcement utilities.\n *\n * Shared by @cloudflare/ai-chat and @cloudflare/think to ensure persistence\n * hygiene: stripping ephemeral provider metadata and compacting\n * oversized messages before writing to SQLite.\n */\n\nimport type { ProviderMetadata, ReasoningUIPart, UIMessage } from \"ai\";\nimport { truncateToolOutput } from \"./tool-output-truncation\";\n\nconst textEncoder = new TextEncoder();\n\n/** Maximum serialized message size before compaction (bytes). 1.8MB with headroom below SQLite's 2MB limit. */\nexport const ROW_MAX_BYTES = 1_800_000;\n\n/** Measure UTF-8 byte length of a string. */\nexport function byteLength(s: string): number {\n return textEncoder.encode(s).byteLength;\n}\n\n/**\n * Sanitize a message for persistence by removing ephemeral provider-specific\n * data that should not be stored or sent back in subsequent requests.\n *\n * 1. Strips OpenAI ephemeral fields (itemId, reasoningEncryptedContent)\n * 2. Filters truly empty reasoning parts (no text, no remaining providerMetadata)\n */\nexport function sanitizeMessage(message: UIMessage): UIMessage {\n const strippedParts = message.parts.map((part) => {\n let sanitizedPart = part;\n\n if (\n \"providerMetadata\" in sanitizedPart &&\n sanitizedPart.providerMetadata &&\n typeof sanitizedPart.providerMetadata === \"object\" &&\n \"openai\" in sanitizedPart.providerMetadata\n ) {\n sanitizedPart = stripOpenAIMetadata(sanitizedPart, \"providerMetadata\");\n }\n\n if (\n \"callProviderMetadata\" in sanitizedPart &&\n sanitizedPart.callProviderMetadata &&\n typeof sanitizedPart.callProviderMetadata === \"object\" &&\n \"openai\" in sanitizedPart.callProviderMetadata\n ) {\n sanitizedPart = stripOpenAIMetadata(\n sanitizedPart,\n \"callProviderMetadata\"\n );\n }\n\n return sanitizedPart;\n }) as UIMessage[\"parts\"];\n\n const sanitizedParts = strippedParts.filter((part) => {\n if (part.type === \"reasoning\") {\n const reasoningPart = part as ReasoningUIPart;\n if (!reasoningPart.text || reasoningPart.text.trim() === \"\") {\n if (\n \"providerMetadata\" in reasoningPart &&\n reasoningPart.providerMetadata &&\n typeof reasoningPart.providerMetadata === \"object\" &&\n Object.keys(reasoningPart.providerMetadata).length > 0\n ) {\n return true;\n }\n return false;\n }\n }\n return true;\n });\n\n return { ...message, parts: sanitizedParts };\n}\n\nfunction stripOpenAIMetadata<T extends UIMessage[\"parts\"][number]>(\n part: T,\n metadataKey: \"providerMetadata\" | \"callProviderMetadata\"\n): T {\n const metadata = (part as Record<string, unknown>)[metadataKey] as {\n openai?: Record<string, unknown>;\n [key: string]: unknown;\n };\n\n if (!metadata?.openai) return part;\n\n const {\n itemId: _itemId,\n reasoningEncryptedContent: _rec,\n ...restOpenai\n } = metadata.openai;\n\n const hasOtherOpenaiFields = Object.keys(restOpenai).length > 0;\n const { openai: _openai, ...restMetadata } = metadata;\n\n let newMetadata: ProviderMetadata | undefined;\n if (hasOtherOpenaiFields) {\n newMetadata = { ...restMetadata, openai: restOpenai } as ProviderMetadata;\n } else if (Object.keys(restMetadata).length > 0) {\n newMetadata = restMetadata as ProviderMetadata;\n }\n\n const { [metadataKey]: _oldMeta, ...restPart } = part as Record<\n string,\n unknown\n >;\n\n if (newMetadata) {\n return { ...restPart, [metadataKey]: newMetadata } as T;\n }\n return restPart as T;\n}\n\n/**\n * Enforce SQLite row size limits by compacting tool outputs and text parts\n * when a serialized message exceeds the safety threshold (1.8MB).\n *\n * Compaction strategy:\n * 1. Compact tool outputs over 1KB while preserving structured output shape\n * 2. If still too big, truncate text parts from oldest to newest\n */\nexport function enforceRowSizeLimit(message: UIMessage): UIMessage {\n let json = JSON.stringify(message);\n let size = byteLength(json);\n if (size <= ROW_MAX_BYTES) return message;\n\n if (message.role !== \"assistant\") {\n return truncateTextParts(message);\n }\n\n const compactedParts = message.parts.map((part) => {\n if (\n \"output\" in part &&\n \"toolCallId\" in part &&\n \"state\" in part &&\n part.state === \"output-available\"\n ) {\n const output = (part as { output: unknown }).output;\n const truncated = truncateToolOutput(output, 1000);\n if (truncated.truncated) {\n return {\n ...part,\n output: truncated.output\n };\n }\n }\n return part;\n }) as UIMessage[\"parts\"];\n\n let result: UIMessage = { ...message, parts: compactedParts };\n\n json = JSON.stringify(result);\n size = byteLength(json);\n if (size <= ROW_MAX_BYTES) return result;\n\n return truncateTextParts(result);\n}\n\nfunction truncateTextParts(message: UIMessage): UIMessage {\n const parts = [...message.parts];\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n if (part.type === \"text\" && \"text\" in part) {\n const text = (part as { text: string }).text;\n if (text.length > 1000) {\n parts[i] = {\n ...part,\n text:\n `[Text truncated for storage (${text.length} chars). ` +\n `First 500 chars: ${text.slice(0, 500)}...]`\n } as UIMessage[\"parts\"][number];\n\n const candidate = { ...message, parts };\n if (byteLength(JSON.stringify(candidate)) <= ROW_MAX_BYTES) {\n break;\n }\n }\n }\n }\n\n return { ...message, parts };\n}\n","/**\n * StreamAccumulator — unified chunk-to-message builder.\n *\n * Used by @cloudflare/ai-chat (server + client) and @cloudflare/think\n * to incrementally build a UIMessage from stream chunks. Wraps\n * applyChunkToParts and handles the metadata chunk types (start, finish,\n * message-metadata, error) that applyChunkToParts does not cover.\n *\n * The accumulator signals domain-specific concerns (early persistence,\n * cross-message tool updates) via ChunkAction returns — callers handle\n * these according to their context.\n */\n\nimport type { UIMessage } from \"ai\";\nimport { applyChunkToParts, type StreamChunkData } from \"./message-builder\";\n\nfunction asMetadata(value: unknown): Record<string, unknown> | undefined {\n if (value != null && typeof value === \"object\" && !Array.isArray(value)) {\n return value as Record<string, unknown>;\n }\n return undefined;\n}\n\nexport interface StreamAccumulatorOptions {\n messageId: string;\n continuation?: boolean;\n existingParts?: UIMessage[\"parts\"];\n existingMetadata?: Record<string, unknown>;\n}\n\nexport type ChunkAction =\n | {\n type: \"start\";\n messageId?: string;\n metadata?: Record<string, unknown>;\n }\n | {\n type: \"finish\";\n finishReason?: string;\n metadata?: Record<string, unknown>;\n }\n | { type: \"message-metadata\"; metadata: Record<string, unknown> }\n | { type: \"tool-approval-request\"; toolCallId: string }\n | {\n type: \"cross-message-tool-update\";\n updateType: \"output-available\" | \"output-error\";\n toolCallId: string;\n output?: unknown;\n errorText?: string;\n preliminary?: boolean;\n }\n | { type: \"error\"; error: string };\n\nexport interface ChunkResult {\n handled: boolean;\n action?: ChunkAction;\n}\n\nexport class StreamAccumulator {\n messageId: string;\n readonly parts: UIMessage[\"parts\"];\n metadata?: Record<string, unknown>;\n private _isContinuation: boolean;\n\n constructor(options: StreamAccumulatorOptions) {\n this.messageId = options.messageId;\n this._isContinuation = options.continuation ?? false;\n this.parts = options.existingParts ? [...options.existingParts] : [];\n this.metadata = options.existingMetadata\n ? { ...options.existingMetadata }\n : undefined;\n }\n\n applyChunk(chunk: StreamChunkData): ChunkResult {\n const handled = applyChunkToParts(this.parts, chunk);\n\n // Detect tool-approval-request for early persistence signaling\n if (chunk.type === \"tool-approval-request\" && chunk.toolCallId) {\n return {\n handled,\n action: { type: \"tool-approval-request\", toolCallId: chunk.toolCallId }\n };\n }\n\n // Detect cross-message tool output/error: applyChunkToParts returns true\n // for recognized types but silently does nothing when the toolCallId\n // doesn't exist in the current parts array.\n if (\n (chunk.type === \"tool-output-available\" ||\n chunk.type === \"tool-output-error\") &&\n chunk.toolCallId\n ) {\n const foundInParts = this.parts.some(\n (p) => \"toolCallId\" in p && p.toolCallId === chunk.toolCallId\n );\n if (!foundInParts) {\n return {\n handled,\n action: {\n type: \"cross-message-tool-update\",\n updateType:\n chunk.type === \"tool-output-available\"\n ? \"output-available\"\n : \"output-error\",\n toolCallId: chunk.toolCallId,\n output: chunk.output,\n errorText: chunk.errorText,\n preliminary: chunk.preliminary\n }\n };\n }\n }\n\n if (!handled) {\n switch (chunk.type) {\n case \"start\": {\n if (chunk.messageId != null && !this._isContinuation) {\n this.messageId = chunk.messageId;\n }\n const startMeta = asMetadata(chunk.messageMetadata);\n if (startMeta) {\n this.metadata = this.metadata\n ? { ...this.metadata, ...startMeta }\n : { ...startMeta };\n }\n return {\n handled: true,\n action: {\n type: \"start\",\n messageId: chunk.messageId,\n metadata: startMeta\n }\n };\n }\n case \"finish\": {\n const finishMeta = asMetadata(chunk.messageMetadata);\n if (finishMeta) {\n this.metadata = this.metadata\n ? { ...this.metadata, ...finishMeta }\n : { ...finishMeta };\n }\n const finishReason =\n \"finishReason\" in chunk\n ? (chunk.finishReason as string)\n : undefined;\n return {\n handled: true,\n action: {\n type: \"finish\",\n finishReason,\n metadata: finishMeta\n }\n };\n }\n case \"message-metadata\": {\n const msgMeta = asMetadata(chunk.messageMetadata);\n if (msgMeta) {\n this.metadata = this.metadata\n ? { ...this.metadata, ...msgMeta }\n : { ...msgMeta };\n }\n return {\n handled: true,\n action: {\n type: \"message-metadata\",\n metadata: msgMeta ?? {}\n }\n };\n }\n case \"finish-step\": {\n return { handled: true };\n }\n case \"error\": {\n return {\n handled: true,\n action: {\n type: \"error\",\n error: chunk.errorText ?? JSON.stringify(chunk)\n }\n };\n }\n }\n }\n\n return { handled };\n }\n\n /** Snapshot the current state as a UIMessage. */\n toMessage(): UIMessage {\n return {\n id: this.messageId,\n role: \"assistant\",\n parts: [...this.parts],\n ...(this.metadata != null && { metadata: this.metadata })\n } as UIMessage;\n }\n\n /**\n * Merge this accumulator's message into an existing message array.\n * Handles continuation (walk backward for last assistant), replacement\n * (update existing by messageId), or append (new message).\n */\n mergeInto(messages: UIMessage[]): UIMessage[] {\n let existingIdx = messages.findIndex((m) => m.id === this.messageId);\n\n if (existingIdx < 0 && this._isContinuation) {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === \"assistant\") {\n existingIdx = i;\n break;\n }\n }\n }\n\n const messageId =\n existingIdx >= 0 ? messages[existingIdx].id : this.messageId;\n\n const partialMessage: UIMessage = {\n id: messageId,\n role: \"assistant\",\n parts: [...this.parts],\n ...(this.metadata != null && { metadata: this.metadata })\n } as UIMessage;\n\n if (existingIdx >= 0) {\n const updated = [...messages];\n updated[existingIdx] = partialMessage;\n return updated;\n }\n return [...messages, partialMessage];\n }\n}\n","/**\n * TurnQueue — serial async queue with generation-based invalidation.\n *\n * Serializes async work via a promise chain, tracks which request is\n * currently active, and lets callers invalidate all queued work by\n * advancing a generation counter.\n *\n * Used by @cloudflare/ai-chat (full concurrency policy spectrum) and\n * @cloudflare/think (simple serial queue) to prevent overlapping\n * chat turns.\n */\n\nexport type TurnResult<T> =\n | { status: \"completed\"; value: T }\n | { status: \"stale\" };\n\nexport interface EnqueueOptions {\n /**\n * Generation to bind this turn to. Defaults to the current generation\n * at the time of the `enqueue` call. If the queue's generation has\n * advanced past this value by the time the turn reaches the front,\n * `fn` is not called and `{ status: \"stale\" }` is returned.\n */\n generation?: number;\n}\n\nexport class TurnQueue {\n private _queue: Promise<void> = Promise.resolve();\n private _generation = 0;\n private _activeRequestId: string | null = null;\n private _countsByGeneration = new Map<number, number>();\n\n get generation(): number {\n return this._generation;\n }\n\n get activeRequestId(): string | null {\n return this._activeRequestId;\n }\n\n get isActive(): boolean {\n return this._activeRequestId !== null;\n }\n\n async enqueue<T>(\n requestId: string,\n fn: () => Promise<T>,\n options?: EnqueueOptions\n ): Promise<TurnResult<T>> {\n const previousTurn = this._queue;\n let releaseTurn!: () => void;\n const capturedGeneration = options?.generation ?? this._generation;\n\n this._countsByGeneration.set(\n capturedGeneration,\n (this._countsByGeneration.get(capturedGeneration) ?? 0) + 1\n );\n\n this._queue = new Promise<void>((resolve) => {\n releaseTurn = resolve;\n });\n\n await previousTurn;\n\n if (this._generation !== capturedGeneration) {\n this._decrementCount(capturedGeneration);\n releaseTurn();\n return { status: \"stale\" };\n }\n\n this._activeRequestId = requestId;\n try {\n const value = await fn();\n return { status: \"completed\", value };\n } finally {\n this._activeRequestId = null;\n this._decrementCount(capturedGeneration);\n releaseTurn();\n }\n }\n\n /**\n * Advance the generation counter. All turns enqueued under older\n * generations will be skipped when they reach the front of the queue.\n */\n reset(): void {\n this._generation++;\n }\n\n /**\n * Wait until the queue is fully drained (no pending or active turns).\n */\n async waitForIdle(): Promise<void> {\n let queue: Promise<void>;\n do {\n queue = this._queue;\n await queue;\n } while (this._queue !== queue);\n }\n\n /**\n * Number of active + queued turns for a given generation.\n * Defaults to the current generation.\n */\n queuedCount(generation?: number): number {\n return this._countsByGeneration.get(generation ?? this._generation) ?? 0;\n }\n\n private _decrementCount(generation: number): void {\n const count = (this._countsByGeneration.get(generation) ?? 1) - 1;\n if (count <= 0) {\n this._countsByGeneration.delete(generation);\n } else {\n this._countsByGeneration.set(generation, count);\n }\n }\n}\n","import type { MessageConcurrency } from \"./lifecycle\";\n\nexport type NormalizedMessageConcurrency =\n | \"queue\"\n | \"latest\"\n | \"merge\"\n | \"drop\"\n | {\n strategy: \"debounce\";\n debounceMs: number;\n };\n\nexport type SubmitConcurrencyDecision = {\n action: \"execute\" | \"drop\";\n strategy: NormalizedMessageConcurrency | null;\n submitSequence: number | null;\n debounceUntilMs: number | null;\n};\n\nexport class SubmitConcurrencyController {\n private _submitSequence = 0;\n private _latestOverlappingSubmitSequence = 0;\n private _pendingEnqueueCount = 0;\n private _resetEpoch = 0;\n private _activeDebounceTimers = new Set<ReturnType<typeof setTimeout>>();\n private _activeDebounceResolves = new Set<() => void>();\n\n constructor(private readonly options: { defaultDebounceMs: number }) {}\n\n get pendingEnqueueCount(): number {\n return this._pendingEnqueueCount;\n }\n\n get overlappingSubmitCount(): number {\n return this._latestOverlappingSubmitSequence;\n }\n\n decide(options: {\n concurrency: MessageConcurrency;\n isSubmitMessage: boolean;\n queuedTurns: number;\n }): SubmitConcurrencyDecision {\n const queuedTurnsInCurrentEpoch =\n options.queuedTurns + this._pendingEnqueueCount;\n\n if (!options.isSubmitMessage || queuedTurnsInCurrentEpoch === 0) {\n return {\n action: \"execute\",\n strategy: null,\n submitSequence: null,\n debounceUntilMs: null\n };\n }\n\n const concurrency = this.normalize(options.concurrency);\n if (concurrency === \"drop\") {\n return {\n action: \"drop\",\n strategy: concurrency,\n submitSequence: null,\n debounceUntilMs: null\n };\n }\n\n if (concurrency === \"queue\") {\n return {\n action: \"execute\",\n strategy: concurrency,\n submitSequence: null,\n debounceUntilMs: null\n };\n }\n\n const submitSequence = ++this._submitSequence;\n this._latestOverlappingSubmitSequence = submitSequence;\n\n if (concurrency === \"latest\" || concurrency === \"merge\") {\n return {\n action: \"execute\",\n strategy: concurrency,\n submitSequence,\n debounceUntilMs: null\n };\n }\n\n return {\n action: \"execute\",\n strategy: concurrency,\n submitSequence,\n debounceUntilMs: Date.now() + concurrency.debounceMs\n };\n }\n\n /**\n * Mark a submit as accepted and in-flight between admission and turn\n * queue registration. Returns an idempotent `release()` function that\n * must be called when the submit either reaches the turn queue or is\n * abandoned. The returned function is bound to the controller's reset\n * epoch — releases from before the most recent `reset()` are no-ops,\n * so post-reset submits keep an accurate count.\n */\n beginEnqueue(): () => void {\n this._pendingEnqueueCount++;\n const epoch = this._resetEpoch;\n let released = false;\n return () => {\n if (released) return;\n released = true;\n if (this._resetEpoch !== epoch) return;\n this._pendingEnqueueCount = Math.max(0, this._pendingEnqueueCount - 1);\n };\n }\n\n isSuperseded(submitSequence: number | null): boolean {\n return (\n submitSequence !== null &&\n submitSequence < this._latestOverlappingSubmitSequence\n );\n }\n\n async waitForTimestamp(timestampMs: number): Promise<void> {\n const remainingMs = timestampMs - Date.now();\n if (remainingMs <= 0) {\n return;\n }\n\n await new Promise<void>((resolve) => {\n const wrappedResolve = () => {\n this._activeDebounceResolves.delete(wrappedResolve);\n resolve();\n };\n const timer = setTimeout(() => {\n this._activeDebounceTimers.delete(timer);\n wrappedResolve();\n }, remainingMs);\n\n this._activeDebounceTimers.add(timer);\n this._activeDebounceResolves.add(wrappedResolve);\n });\n }\n\n cancelActiveDebounce(): void {\n for (const timer of this._activeDebounceTimers) {\n clearTimeout(timer);\n }\n this._activeDebounceTimers.clear();\n\n const resolves = [...this._activeDebounceResolves];\n this._activeDebounceResolves.clear();\n for (const resolve of resolves) {\n resolve();\n }\n }\n\n reset(): void {\n this._resetEpoch++;\n this._pendingEnqueueCount = 0;\n this.cancelActiveDebounce();\n }\n\n async waitForIdle(waitForQueueIdle: () => Promise<void>): Promise<void> {\n while (true) {\n await waitForQueueIdle();\n if (this._pendingEnqueueCount === 0) return;\n await new Promise<void>((resolve) => setTimeout(resolve, 5));\n }\n }\n\n private normalize(\n concurrency: MessageConcurrency\n ): NormalizedMessageConcurrency {\n if (typeof concurrency === \"string\") {\n return concurrency;\n }\n\n const debounceMs = concurrency.debounceMs;\n\n return {\n strategy: \"debounce\",\n debounceMs:\n typeof debounceMs === \"number\" &&\n Number.isFinite(debounceMs) &&\n debounceMs >= 0\n ? debounceMs\n : this.options.defaultDebounceMs\n };\n }\n}\n","/**\n * Broadcast stream state machine.\n *\n * Manages the lifecycle of a StreamAccumulator for broadcast/resume\n * streams — the path where this client is *observing* a stream owned\n * by another tab or resumed after reconnect, rather than the transport-\n * owned path that feeds directly into useChat.\n *\n * The transition function is pure (no React, no WebSocket, no side\n * effects). Callers dispatch events and apply the returned state +\n * messagesUpdate. Side effects (sending ACKs, calling onData) stay\n * in the caller.\n */\n\nimport type { UIMessage } from \"ai\";\nimport { StreamAccumulator } from \"./stream-accumulator\";\nimport type { StreamChunkData } from \"./message-builder\";\n\n// ── State ──────────────────────────────────────────────────────────\n\nexport type BroadcastStreamState =\n | { status: \"idle\" }\n | {\n status: \"observing\";\n streamId: string;\n accumulator: StreamAccumulator;\n };\n\n// ── Events ─────────────────────────────────────────────────────────\n\nexport type BroadcastStreamEvent =\n | {\n type: \"response\";\n streamId: string;\n /** Fallback message ID for a new accumulator (ignored if one exists for this stream). */\n messageId: string;\n chunkData?: unknown;\n done?: boolean;\n error?: boolean;\n replay?: boolean;\n replayComplete?: boolean;\n continuation?: boolean;\n /** Required when continuation=true so the accumulator can pick up existing parts. */\n currentMessages?: UIMessage[];\n }\n | {\n type: \"resume-fallback\";\n streamId: string;\n messageId: string;\n }\n | { type: \"clear\" };\n\n// ── Result ─────────────────────────────────────────────────────────\n\nexport interface TransitionResult {\n state: BroadcastStreamState;\n messagesUpdate?: (prev: UIMessage[]) => UIMessage[];\n isStreaming: boolean;\n}\n\n// ── Transition ─────────────────────────────────────────────────────\n\nexport function transition(\n state: BroadcastStreamState,\n event: BroadcastStreamEvent\n): TransitionResult {\n switch (event.type) {\n case \"clear\":\n return { state: { status: \"idle\" }, isStreaming: false };\n\n case \"resume-fallback\": {\n const accumulator = new StreamAccumulator({\n messageId: event.messageId\n });\n return {\n state: {\n status: \"observing\",\n streamId: event.streamId,\n accumulator\n },\n isStreaming: true\n };\n }\n\n case \"response\": {\n let accumulator: StreamAccumulator;\n\n if (state.status === \"idle\" || state.streamId !== event.streamId) {\n let messageId = event.messageId;\n let existingParts: UIMessage[\"parts\"] | undefined;\n let existingMetadata: Record<string, unknown> | undefined;\n\n if (event.continuation && event.currentMessages) {\n for (let i = event.currentMessages.length - 1; i >= 0; i--) {\n if (event.currentMessages[i].role === \"assistant\") {\n messageId = event.currentMessages[i].id;\n existingParts = [...event.currentMessages[i].parts];\n if (event.currentMessages[i].metadata != null) {\n existingMetadata = {\n ...(event.currentMessages[i].metadata as Record<\n string,\n unknown\n >)\n };\n }\n break;\n }\n }\n }\n\n accumulator = new StreamAccumulator({\n messageId,\n continuation: event.continuation,\n existingParts,\n existingMetadata\n });\n } else {\n accumulator = state.accumulator;\n }\n\n if (event.chunkData) {\n accumulator.applyChunk(event.chunkData as StreamChunkData);\n }\n\n let messagesUpdate: ((prev: UIMessage[]) => UIMessage[]) | undefined;\n\n if (event.done) {\n messagesUpdate = (prev) => accumulator.mergeInto(prev);\n return {\n state: { status: \"idle\" },\n messagesUpdate,\n isStreaming: false\n };\n }\n\n if (event.chunkData && !event.replay) {\n messagesUpdate = (prev) => accumulator.mergeInto(prev);\n } else if (event.replayComplete) {\n messagesUpdate = (prev) => accumulator.mergeInto(prev);\n }\n\n return {\n state: {\n status: \"observing\",\n streamId: event.streamId,\n accumulator\n },\n messagesUpdate,\n isStreaming: true\n };\n }\n }\n}\n","/**\n * Wire protocol message type constants for the cf_agent_chat_* protocol.\n *\n * These are the string values used on the wire between agent servers and\n * clients. Both @cloudflare/ai-chat (via its MessageType enum) and\n * @cloudflare/think use these values.\n */\nexport const CHAT_MESSAGE_TYPES = {\n CHAT_MESSAGES: \"cf_agent_chat_messages\",\n USE_CHAT_REQUEST: \"cf_agent_use_chat_request\",\n USE_CHAT_RESPONSE: \"cf_agent_use_chat_response\",\n CHAT_CLEAR: \"cf_agent_chat_clear\",\n CHAT_REQUEST_CANCEL: \"cf_agent_chat_request_cancel\",\n STREAM_RESUMING: \"cf_agent_stream_resuming\",\n STREAM_RESUME_ACK: \"cf_agent_stream_resume_ack\",\n STREAM_RESUME_REQUEST: \"cf_agent_stream_resume_request\",\n STREAM_RESUME_NONE: \"cf_agent_stream_resume_none\",\n TOOL_RESULT: \"cf_agent_tool_result\",\n TOOL_APPROVAL: \"cf_agent_tool_approval\",\n MESSAGE_UPDATED: \"cf_agent_message_updated\",\n // Server→client: a durable chat turn is being recovered (interrupted by a\n // deploy/eviction or a stream-stall watchdog abort and now resuming). Sent\n // when a recovery continuation is scheduled and cleared on every terminal\n // outcome; `@cloudflare/think` also replays it on connect so a client that\n // joins mid-recovery learns it. Purely a progress hint — backward-compatible\n // (clients that don't understand it ignore it). See issue #1620.\n CHAT_RECOVERING: \"cf_agent_chat_recovering\"\n} as const;\n","/**\n * ResumableStream: Standalone class for buffering, persisting, and replaying\n * stream chunks in SQLite. Extracted from AIChatAgent to separate concerns.\n *\n * Handles:\n * - Chunk buffering (batched writes to SQLite for performance)\n * - Stream lifecycle (start, complete, error)\n * - Chunk replay for reconnecting clients\n * - Stale stream cleanup\n * - Active stream restoration after agent restart\n */\n\nimport { nanoid } from \"nanoid\";\nimport type { Connection } from \"agents\";\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\n\n/** Number of chunks to buffer before flushing to SQLite */\nconst CHUNK_BUFFER_SIZE = 10;\n/** Maximum buffer size to prevent memory issues on rapid reconnections */\nconst CHUNK_BUFFER_MAX_SIZE = 100;\n/** Default cleanup interval for old streams (ms) - every 10 minutes */\nconst CLEANUP_INTERVAL_MS = 10 * 60 * 1000;\n/** Default age threshold for cleaning up completed streams (ms) - 24 hours */\nconst CLEANUP_AGE_THRESHOLD_MS = 24 * 60 * 60 * 1000;\n/** Shared encoder for UTF-8 byte length measurement */\nconst textEncoder = new TextEncoder();\n\nfunction sendIfOpen(connection: Connection, message: string): boolean {\n try {\n connection.send(message);\n return true;\n } catch (error) {\n if (isWebSocketClosedSendError(error)) return false;\n throw error;\n }\n}\n\nfunction isWebSocketClosedSendError(error: unknown): boolean {\n return (\n error instanceof TypeError &&\n error.message.includes(\"WebSocket send() after close\")\n );\n}\n\n/**\n * Stored stream chunk for resumable streaming\n */\ntype StreamChunk = {\n id: string;\n stream_id: string;\n body: string;\n chunk_index: number;\n created_at: number;\n};\n\n/**\n * Stream metadata for tracking active streams\n */\ntype StreamMetadata = {\n id: string;\n request_id: string;\n status: \"streaming\" | \"completed\" | \"error\";\n created_at: number;\n completed_at: number | null;\n};\n\n/**\n * Minimal SQL interface matching Agent's this.sql tagged template.\n * Allows ResumableStream to work with the Agent's SQLite without\n * depending on the full Agent class.\n */\nexport type SqlTaggedTemplate = {\n <T = Record<string, unknown>>(\n strings: TemplateStringsArray,\n ...values: (string | number | boolean | null)[]\n ): T[];\n};\n\nexport class ResumableStream {\n private _activeStreamId: string | null = null;\n private _activeRequestId: string | null = null;\n private _streamChunkIndex = 0;\n\n /**\n * Whether the active stream was started in this instance (true) or\n * restored from SQLite after hibernation/restart (false). An orphaned\n * stream has no live LLM reader — the ReadableStream was lost when the\n * DO was evicted.\n */\n private _isLive = false;\n\n private _chunkBuffer: Array<{\n id: string;\n streamId: string;\n body: string;\n index: number;\n }> = [];\n private _isFlushingChunks = false;\n private _lastCleanupTime = 0;\n\n constructor(private sql: SqlTaggedTemplate) {\n // Create tables for stream chunks and metadata\n this.sql`create table if not exists cf_ai_chat_stream_chunks (\n id text primary key,\n stream_id text not null,\n body text not null,\n chunk_index integer not null,\n created_at integer not null\n )`;\n\n this.sql`create table if not exists cf_ai_chat_stream_metadata (\n id text primary key,\n request_id text not null,\n status text not null,\n created_at integer not null,\n completed_at integer\n )`;\n\n this.sql`create index if not exists idx_stream_chunks_stream_id \n on cf_ai_chat_stream_chunks(stream_id, chunk_index)`;\n\n // Restore any active stream from a previous session\n this.restore();\n }\n\n // ── State accessors ────────────────────────────────────────────────\n\n get activeStreamId(): string | null {\n return this._activeStreamId;\n }\n\n get activeRequestId(): string | null {\n return this._activeRequestId;\n }\n\n hasActiveStream(): boolean {\n return this._activeStreamId !== null;\n }\n\n /**\n * Whether the active stream has a live LLM reader (started in this\n * instance) vs being restored from SQLite after hibernation (orphaned).\n */\n get isLive(): boolean {\n return this._isLive;\n }\n\n // ── Stream lifecycle ───────────────────────────────────────────────\n\n /**\n * Start tracking a new stream for resumable streaming.\n * Creates metadata entry in SQLite and sets up tracking state.\n * @param requestId - The unique ID of the chat request\n * @returns The generated stream ID\n */\n start(requestId: string): string {\n // Flush any pending chunks from previous streams to prevent mixing\n this.flushBuffer();\n\n const streamId = nanoid();\n this._activeStreamId = streamId;\n this._activeRequestId = requestId;\n this._streamChunkIndex = 0;\n this._isLive = true;\n\n this.sql`\n insert into cf_ai_chat_stream_metadata (id, request_id, status, created_at)\n values (${streamId}, ${requestId}, 'streaming', ${Date.now()})\n `;\n\n return streamId;\n }\n\n /**\n * Mark a stream as completed and flush any pending chunks.\n * @param streamId - The stream to mark as completed\n */\n complete(streamId: string) {\n this.flushBuffer();\n\n this.sql`\n update cf_ai_chat_stream_metadata \n set status = 'completed', completed_at = ${Date.now()} \n where id = ${streamId}\n `;\n this._activeStreamId = null;\n this._activeRequestId = null;\n this._streamChunkIndex = 0;\n this._isLive = false;\n\n // Periodically clean up old streams\n this._maybeCleanupOldStreams();\n }\n\n /**\n * Mark a stream as errored and clean up state.\n * @param streamId - The stream to mark as errored\n */\n markError(streamId: string) {\n this.flushBuffer();\n\n this.sql`\n update cf_ai_chat_stream_metadata \n set status = 'error', completed_at = ${Date.now()} \n where id = ${streamId}\n `;\n this._activeStreamId = null;\n this._activeRequestId = null;\n this._streamChunkIndex = 0;\n this._isLive = false;\n }\n\n // ── Chunk storage ──────────────────────────────────────────────────\n\n /** Maximum chunk body size before skipping storage (bytes). Prevents SQLite row limit crash. */\n private static CHUNK_MAX_BYTES = 1_800_000;\n\n /**\n * Buffer a stream chunk for batch write to SQLite.\n * Chunks exceeding the row size limit are skipped to prevent crashes.\n * The chunk is still broadcast to live clients (caller handles that),\n * but will be missing from replay on reconnection.\n * @param streamId - The stream this chunk belongs to\n * @param body - The serialized chunk body\n */\n storeChunk(streamId: string, body: string) {\n // Guard against chunks that would exceed SQLite row limit.\n // The chunk is still broadcast to live clients; only replay storage is skipped.\n const bodyBytes = textEncoder.encode(body).byteLength;\n if (bodyBytes > ResumableStream.CHUNK_MAX_BYTES) {\n console.warn(\n `[ResumableStream] Skipping oversized chunk (${bodyBytes} bytes) ` +\n `to prevent SQLite row limit crash. Live clients still receive it.`\n );\n return;\n }\n\n // Force flush if buffer is at max to prevent memory issues\n if (this._chunkBuffer.length >= CHUNK_BUFFER_MAX_SIZE) {\n this.flushBuffer();\n }\n\n this._chunkBuffer.push({\n id: nanoid(),\n streamId,\n body,\n index: this._streamChunkIndex\n });\n this._streamChunkIndex++;\n\n // Flush when buffer reaches threshold\n if (this._chunkBuffer.length >= CHUNK_BUFFER_SIZE) {\n this.flushBuffer();\n }\n }\n\n /**\n * Flush buffered chunks to SQLite in a single batch.\n * Uses a lock to prevent concurrent flush operations.\n */\n flushBuffer() {\n if (this._isFlushingChunks || this._chunkBuffer.length === 0) {\n return;\n }\n\n this._isFlushingChunks = true;\n try {\n const chunks = this._chunkBuffer;\n this._chunkBuffer = [];\n\n const now = Date.now();\n for (const chunk of chunks) {\n this.sql`\n insert into cf_ai_chat_stream_chunks (id, stream_id, body, chunk_index, created_at)\n values (${chunk.id}, ${chunk.streamId}, ${chunk.body}, ${chunk.index}, ${now})\n `;\n }\n } finally {\n this._isFlushingChunks = false;\n }\n }\n\n // ── Chunk replay ───────────────────────────────────────────────────\n\n /**\n * Send stored stream chunks to a connection for replay.\n * Chunks are marked with replay: true so the client can batch-apply them.\n *\n * Three outcomes:\n * - **Live stream**: sends chunks + `replayComplete` — client flushes and\n * continues receiving live chunks from the LLM reader.\n * - **Orphaned stream** (restored from SQLite after hibernation, no reader):\n * sends chunks + `done` and completes the stream. The caller should\n * reconstruct and persist the partial message from the stored chunks.\n * - **Completed during replay** (defensive): sends chunks + `done`.\n *\n * All sends use {@link sendIfOpen}, so a WebSocket closing mid-replay\n * does not throw. If the connection drops while iterating chunks the\n * stream is left active so the next reconnect can retry.\n *\n * @param connection - The WebSocket connection\n * @param requestId - The original request ID\n * @returns The stream ID if the stream was orphaned and finalized, null otherwise.\n * When non-null the caller should reconstruct the message from chunks.\n */\n replayChunks(connection: Connection, requestId: string): string | null {\n const streamId = this._activeStreamId;\n if (!streamId) return null;\n\n this.flushBuffer();\n\n const chunks = this.sql<StreamChunk>`\n select * from cf_ai_chat_stream_chunks \n where stream_id = ${streamId} \n order by chunk_index asc\n `;\n\n for (const chunk of chunks || []) {\n if (\n !sendIfOpen(\n connection,\n JSON.stringify({\n body: chunk.body,\n done: false,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true\n })\n )\n ) {\n // Connection closed mid-replay — leave the stream active so the\n // next reconnect can retry from the start.\n return null;\n }\n }\n\n if (this._activeStreamId !== streamId) {\n // Stream completed between our check above and now — send done.\n // In practice this cannot happen (DO is single-threaded and replay is\n // synchronous), but we guard defensively in case the flow changes.\n sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: true,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true\n })\n );\n return null;\n }\n\n if (!this._isLive) {\n // Orphaned stream — restored from SQLite after hibernation but the\n // LLM ReadableStream reader was lost. No more live chunks will ever\n // arrive, so finalize it: best-effort send done, then mark completed\n // in SQLite. The orphan-cleanup decision is committed regardless of\n // whether this particular connection received the done frame, so the\n // caller can persist the reconstructed message.\n sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: true,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true\n })\n );\n this.complete(streamId);\n return streamId;\n }\n\n // Stream is still active with a live reader — signal that replay is\n // complete so the client can flush accumulated parts to React state.\n // Without this, replayed chunks sit in activeStreamRef unflushed\n // until the next live chunk arrives.\n sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: false,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true,\n replayComplete: true\n })\n );\n return null;\n }\n\n replayCompletedChunksByRequestId(\n connection: Connection,\n requestId: string\n ): boolean {\n this.flushBuffer();\n\n const streams = this.sql<StreamMetadata>`\n select * from cf_ai_chat_stream_metadata\n where request_id = ${requestId}\n and status = 'completed'\n order by created_at desc\n limit 1\n `;\n const stream = streams[0];\n if (!stream) return false;\n\n const chunks = this.sql<StreamChunk>`\n select * from cf_ai_chat_stream_chunks\n where stream_id = ${stream.id}\n order by chunk_index asc\n `;\n\n for (const chunk of chunks || []) {\n if (\n !sendIfOpen(\n connection,\n JSON.stringify({\n body: chunk.body,\n done: false,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true\n })\n )\n ) {\n return false;\n }\n }\n\n return sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: true,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true\n })\n );\n }\n\n // ── Restore / cleanup ──────────────────────────────────────────────\n\n /**\n * Restore active stream state if the agent was restarted during streaming.\n * All streams are restored regardless of age — stale cleanup happens\n * lazily in _maybeCleanupOldStreams after recovery has had its chance.\n */\n restore() {\n const activeStreams = this.sql<StreamMetadata>`\n select * from cf_ai_chat_stream_metadata \n where status = 'streaming' \n order by created_at desc \n limit 1\n `;\n\n if (activeStreams && activeStreams.length > 0) {\n const stream = activeStreams[0];\n this._activeStreamId = stream.id;\n this._activeRequestId = stream.request_id;\n\n // Get the last chunk index\n const lastChunk = this.sql<{ max_index: number }>`\n select max(chunk_index) as max_index \n from cf_ai_chat_stream_chunks \n where stream_id = ${this._activeStreamId}\n `;\n this._streamChunkIndex =\n lastChunk && lastChunk[0]?.max_index != null\n ? lastChunk[0].max_index + 1\n : 0;\n }\n }\n\n /**\n * Clear all stream data (called on chat history clear).\n */\n clearAll() {\n this._chunkBuffer = [];\n this.sql`delete from cf_ai_chat_stream_chunks`;\n this.sql`delete from cf_ai_chat_stream_metadata`;\n this._activeStreamId = null;\n this._activeRequestId = null;\n this._streamChunkIndex = 0;\n }\n\n /**\n * Drop all stream tables (called on destroy).\n */\n destroy() {\n this.flushBuffer();\n this.sql`drop table if exists cf_ai_chat_stream_chunks`;\n this.sql`drop table if exists cf_ai_chat_stream_metadata`;\n this._activeStreamId = null;\n this._activeRequestId = null;\n }\n\n // ── Internal ───────────────────────────────────────────────────────\n\n private _maybeCleanupOldStreams() {\n const now = Date.now();\n if (now - this._lastCleanupTime < CLEANUP_INTERVAL_MS) {\n return;\n }\n this._lastCleanupTime = now;\n\n const cutoff = now - CLEANUP_AGE_THRESHOLD_MS;\n this.sql`\n delete from cf_ai_chat_stream_chunks \n where stream_id in (\n select id from cf_ai_chat_stream_metadata \n where status in ('completed', 'error') and completed_at < ${cutoff}\n )\n `;\n this.sql`\n delete from cf_ai_chat_stream_metadata \n where status in ('completed', 'error') and completed_at < ${cutoff}\n `;\n\n // Clean up abandoned \"streaming\" rows. These are orphaned streams that\n // were never completed or recovered (e.g. non-durable agents that never\n // reconnected). By this point, fiber recovery has already had its chance\n // to claim them — safe to delete.\n this.sql`\n delete from cf_ai_chat_stream_chunks\n where stream_id in (\n select id from cf_ai_chat_stream_metadata\n where status = 'streaming' and created_at < ${cutoff}\n )\n `;\n this.sql`\n delete from cf_ai_chat_stream_metadata\n where status = 'streaming' and created_at < ${cutoff}\n `;\n }\n\n // ── Test helpers (matching old AIChatAgent test API) ────────────────\n\n /** @internal For testing only */\n getStreamChunks(\n streamId: string\n ): Array<{ body: string; chunk_index: number }> {\n return (\n this.sql<{ body: string; chunk_index: number }>`\n select body, chunk_index from cf_ai_chat_stream_chunks \n where stream_id = ${streamId} \n order by chunk_index asc\n ` || []\n );\n }\n\n /** @internal For testing only */\n getStreamMetadata(\n streamId: string\n ): { status: string; request_id: string } | null {\n const result = this.sql<{ status: string; request_id: string }>`\n select status, request_id from cf_ai_chat_stream_metadata \n where id = ${streamId}\n `;\n return result && result.length > 0 ? result[0] : null;\n }\n\n /** @internal For testing only */\n getAllStreamMetadata(): Array<{\n id: string;\n status: string;\n request_id: string;\n created_at: number;\n }> {\n return (\n this.sql<{\n id: string;\n status: string;\n request_id: string;\n created_at: number;\n }>`select id, status, request_id, created_at from cf_ai_chat_stream_metadata` ||\n []\n );\n }\n\n /** @internal For testing only */\n insertStaleStream(streamId: string, requestId: string, ageMs: number): void {\n const createdAt = Date.now() - ageMs;\n this.sql`\n insert into cf_ai_chat_stream_metadata (id, request_id, status, created_at)\n values (${streamId}, ${requestId}, 'streaming', ${createdAt})\n `;\n }\n}\n","/**\n * Client tool schema handling for the cf_agent_chat protocol.\n *\n * Converts client-provided tool schemas (JSON wire format) into AI SDK\n * tool definitions. These tools have no `execute` function — when the\n * model calls them, the tool call is sent back to the client.\n *\n * Used by both @cloudflare/ai-chat and @cloudflare/think.\n */\n\nimport type { JSONSchema7, Tool, ToolSet } from \"ai\";\nimport { tool, jsonSchema } from \"ai\";\n\n/**\n * Wire-format tool schema sent from the client.\n * Uses `parameters` (JSONSchema7) rather than AI SDK's `inputSchema`\n * because Zod schemas cannot be serialized over the wire.\n */\nexport type ClientToolSchema = {\n /** Unique name for the tool */\n name: string;\n /** Human-readable description of what the tool does */\n description?: Tool[\"description\"];\n /** JSON Schema defining the tool's input parameters */\n parameters?: JSONSchema7;\n};\n\n/**\n * Converts client tool schemas to AI SDK tool format.\n *\n * These tools have no `execute` function — when the AI model calls them,\n * the tool call is sent back to the client for execution.\n *\n * @param clientTools - Array of tool schemas from the client\n * @returns Record of AI SDK tools that can be spread into your tools object\n */\nexport function createToolsFromClientSchemas(\n clientTools?: ClientToolSchema[]\n): ToolSet {\n if (!clientTools || clientTools.length === 0) {\n return {};\n }\n\n const seenNames = new Set<string>();\n for (const t of clientTools) {\n if (seenNames.has(t.name)) {\n console.warn(\n `[createToolsFromClientSchemas] Duplicate tool name \"${t.name}\" found. Later definitions will override earlier ones.`\n );\n }\n seenNames.add(t.name);\n }\n\n return Object.fromEntries(\n clientTools.map((t) => [\n t.name,\n tool({\n description: t.description ?? \"\",\n inputSchema: jsonSchema(t.parameters ?? { type: \"object\" })\n })\n ])\n );\n}\n","/**\n * ContinuationState — shared state container for auto-continuation lifecycle.\n *\n * Tracks pending, deferred, and active continuation state for the\n * tool-result → auto-continue flow. Both AIChatAgent and Think use this\n * to manage which connection/tools/body a continuation turn should use\n * and to coordinate with clients requesting stream resume.\n *\n * The scheduling algorithm (prerequisite chaining, debounce, TurnQueue\n * enrollment) stays in the host — this class only manages the data.\n */\n\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\nimport type { ClientToolSchema } from \"./client-tools\";\n\nconst MSG_STREAM_RESUME_NONE = CHAT_MESSAGE_TYPES.STREAM_RESUME_NONE;\n\nfunction sendIfOpen(\n connection: ContinuationConnection,\n message: string\n): boolean {\n try {\n connection.send(message);\n return true;\n } catch (error) {\n if (isWebSocketClosedSendError(error)) return false;\n throw error;\n }\n}\n\nfunction isWebSocketClosedSendError(error: unknown): boolean {\n return (\n error instanceof TypeError &&\n error.message.includes(\"WebSocket send() after close\")\n );\n}\n\n/**\n * Minimal connection interface for sending WebSocket messages.\n * Matches the Connection type from agents without importing it.\n * Uses a permissive send signature so Connection (which extends\n * WebSocket with its own send overload) is structurally assignable.\n */\nexport interface ContinuationConnection {\n readonly id: string;\n send(message: string): void;\n}\n\nexport interface ContinuationPending<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n connection: TConnection;\n connectionId: string | null;\n requestId: string;\n clientTools?: ClientToolSchema[];\n body?: Record<string, unknown>;\n errorPrefix: string | null;\n prerequisite: Promise<boolean> | null;\n pastCoalesce: boolean;\n}\n\nexport interface ContinuationDeferred<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n connection: TConnection;\n connectionId: string | null;\n clientTools?: ClientToolSchema[];\n body?: Record<string, unknown>;\n errorPrefix: string;\n prerequisite: Promise<boolean> | null;\n}\n\nexport class ContinuationState<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n pending: ContinuationPending<TConnection> | null = null;\n deferred: ContinuationDeferred<TConnection> | null = null;\n activeRequestId: string | null = null;\n activeConnectionId: string | null = null;\n awaitingConnections: Map<string, TConnection> = new Map();\n\n /** Clear pending state and awaiting connections (without sending RESUME_NONE). */\n clearPending(): void {\n this.pending = null;\n this.awaitingConnections.clear();\n }\n\n clearDeferred(): void {\n this.deferred = null;\n }\n\n clearAll(): void {\n this.clearPending();\n this.clearDeferred();\n this.activeRequestId = null;\n this.activeConnectionId = null;\n }\n\n /**\n * Mark a connection as no longer available without canceling the\n * continuation it initiated.\n */\n releaseConnection(connectionId: string): void {\n this.awaitingConnections.delete(connectionId);\n if (this.pending?.connectionId === connectionId) {\n this.pending = { ...this.pending, connectionId: null };\n }\n if (this.deferred?.connectionId === connectionId) {\n this.deferred = { ...this.deferred, connectionId: null };\n }\n if (this.activeConnectionId === connectionId) {\n this.activeConnectionId = null;\n }\n }\n\n /**\n * Send STREAM_RESUME_NONE to all connections waiting for a\n * continuation stream to start, then clear the map.\n */\n sendResumeNone(): void {\n const msg = JSON.stringify({ type: MSG_STREAM_RESUME_NONE });\n for (const connection of this.awaitingConnections.values()) {\n sendIfOpen(connection, msg);\n }\n this.awaitingConnections.clear();\n }\n\n /**\n * Flush awaiting connections by notifying each one via the provided\n * callback (typically sends STREAM_RESUMING), then clear.\n */\n flushAwaitingConnections(notify: (conn: TConnection) => void): void {\n for (const connection of this.awaitingConnections.values()) {\n notify(connection);\n }\n this.awaitingConnections.clear();\n }\n\n /**\n * Transition pending → active. Called when the continuation stream\n * actually starts. Moves request/connection IDs to active slots,\n * clears pending fields.\n */\n activatePending(): void {\n if (!this.pending) return;\n this.activeRequestId = this.pending.requestId;\n this.activeConnectionId = this.pending.connectionId;\n this.pending = null;\n }\n\n /**\n * Transition deferred → pending. Called when a continuation turn\n * completes and there's a deferred follow-up waiting.\n *\n * Returns the new pending state (so the host can enqueue the turn),\n * or null if there was nothing deferred.\n */\n activateDeferred(\n generateRequestId: () => string\n ): ContinuationPending<TConnection> | null {\n if (this.pending || !this.deferred) return null;\n\n const d = this.deferred;\n this.deferred = null;\n this.activeRequestId = null;\n this.activeConnectionId = null;\n\n this.pending = {\n connection: d.connection,\n connectionId: d.connectionId,\n requestId: generateRequestId(),\n clientTools: d.clientTools,\n body: d.body,\n errorPrefix: d.errorPrefix,\n prerequisite: d.prerequisite,\n pastCoalesce: false\n };\n\n if (d.connectionId !== null) {\n this.awaitingConnections.set(d.connectionId, d.connection);\n }\n return this.pending;\n }\n}\n","/**\n * AbortRegistry — manages per-request AbortControllers.\n *\n * Shared between AIChatAgent and Think for chat turn cancellation.\n * Each request gets its own AbortController keyed by request ID.\n * Controllers are created lazily on first signal access.\n */\n\nconst NOOP = () => {};\n\nexport class AbortRegistry {\n private controllers = new Map<string, AbortController>();\n\n /**\n * Get or create an AbortController for the given ID and return its signal.\n * Creates the controller lazily on first access.\n */\n getSignal(id: string): AbortSignal | undefined {\n if (typeof id !== \"string\") {\n return undefined;\n }\n\n if (!this.controllers.has(id)) {\n this.controllers.set(id, new AbortController());\n }\n\n return this.controllers.get(id)!.signal;\n }\n\n /**\n * Get the signal for an existing controller without creating one.\n * Returns undefined if no controller exists for this ID.\n */\n getExistingSignal(id: string): AbortSignal | undefined {\n return this.controllers.get(id)?.signal;\n }\n\n /**\n * Cancel a specific request by aborting its controller. Optionally\n * propagate a reason — surfaces as `signal.reason` on the registry's\n * controller and through any `AbortError` it produces downstream.\n */\n cancel(id: string, reason?: unknown): void {\n this.controllers.get(id)?.abort(reason);\n }\n\n /** Remove a controller after the request completes. */\n remove(id: string): void {\n this.controllers.delete(id);\n }\n\n /**\n * Abort all pending requests and clear the registry. Optionally propagate a\n * reason — surfaces as `signal.reason` on each controller and through any\n * `AbortError` it produces downstream, exactly like {@link cancel}.\n */\n destroyAll(reason?: unknown): void {\n for (const controller of this.controllers.values()) {\n controller.abort(reason);\n }\n this.controllers.clear();\n }\n\n /** Check if a controller exists for the given ID. */\n has(id: string): boolean {\n return this.controllers.has(id);\n }\n\n /** Number of tracked controllers. */\n get size(): number {\n return this.controllers.size;\n }\n\n /**\n * Link an external `AbortSignal` to the controller for `id`. When the\n * external signal aborts, the registry's controller is cancelled —\n * propagating the abort reason — exactly the same way an internal\n * cancel would (e.g. via a `chat-request-cancel` WebSocket message).\n *\n * This is the integration point for callers that drive a chat turn\n * programmatically and want to cancel it from outside without knowing\n * the internally-generated request id (e.g. the helper-as-sub-agent\n * pattern, where a parent's `AbortSignal` from the AI SDK tool\n * `execute` needs to land inside a `Think.saveMessages` call running\n * on a child DO).\n *\n * Behavior:\n *\n * - Passing `undefined` is a no-op and returns a no-op detacher, so\n * callers can unconditionally call this with `options?.signal`.\n * - If the external signal is already aborted, the registry's\n * controller is created (if needed) and cancelled synchronously.\n * - Otherwise a one-shot `abort` listener is attached. The returned\n * function detaches it.\n *\n * **Always call the returned detacher in a `finally` block** — the\n * external signal may outlive the request (a parent chat turn that\n * drives many helper turns reuses one signal across all of them) and\n * leaving listeners attached pins closures and grows the listener\n * list on each turn.\n *\n * @returns A detacher function. Call it after the request finishes\n * (success or failure) to remove the abort listener from `signal`.\n */\n linkExternal(id: string, signal: AbortSignal | undefined): () => void {\n if (!signal) return NOOP;\n\n if (signal.aborted) {\n // Ensure the registry controller for `id` exists, then cancel it.\n // Calling getSignal first means an early external abort still\n // produces a controller for downstream observers (`getExistingSignal`)\n // rather than a silently-empty registry.\n this.getSignal(id);\n this.cancel(id, signal.reason);\n return NOOP;\n }\n\n const listener = () => this.cancel(id, signal.reason);\n signal.addEventListener(\"abort\", listener, { once: true });\n return () => signal.removeEventListener(\"abort\", listener);\n }\n}\n","/**\n * Tool State — shared update builders and applicator for tool part state changes.\n *\n * Used by both AIChatAgent and Think to apply tool results and approvals\n * to message parts. Each agent handles find-message, persist, and broadcast\n * in their own way; this module provides the state matching and update logic.\n */\n\n/**\n * Describes an update to apply to a tool part.\n */\nexport type ToolPartUpdate = {\n toolCallId: string;\n matchStates: string[];\n apply: (part: Record<string, unknown>) => Record<string, unknown>;\n};\n\n/**\n * Apply a tool part update to a parts array.\n * Finds the first part matching `update.toolCallId` in one of `update.matchStates`,\n * applies the update immutably, and returns the new parts array with the index.\n *\n * Returns `null` if no matching part was found.\n */\nexport function applyToolUpdate(\n parts: Array<Record<string, unknown>>,\n update: ToolPartUpdate\n): { parts: Array<Record<string, unknown>>; index: number } | null {\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n if (\n \"toolCallId\" in part &&\n part.toolCallId === update.toolCallId &&\n \"state\" in part &&\n update.matchStates.includes(part.state as string)\n ) {\n const updatedParts = [...parts];\n updatedParts[i] = update.apply(part);\n return { parts: updatedParts, index: i };\n }\n }\n return null;\n}\n\n/**\n * Build an update descriptor for applying a tool result.\n *\n * Matches parts in `input-available`, `approval-requested`, or `approval-responded` state.\n * Sets state to `output-available` (with output) or `output-error` (with errorText).\n */\nexport function toolResultUpdate(\n toolCallId: string,\n output: unknown,\n overrideState?: \"output-error\",\n errorText?: string\n): ToolPartUpdate {\n return {\n toolCallId,\n matchStates: [\n \"input-available\",\n \"approval-requested\",\n \"approval-responded\"\n ],\n apply: (part) => ({\n ...part,\n ...(overrideState === \"output-error\"\n ? {\n state: \"output-error\",\n errorText: errorText ?? \"Tool execution denied by user\"\n }\n : { state: \"output-available\", output, preliminary: false })\n })\n };\n}\n\n/**\n * Build an update descriptor for a terminal tool result that belongs to a\n * tool part in a *different* (earlier) assistant message than the one\n * currently being streamed.\n *\n * This is the \"cross-message\" case: an approved server tool executes during a\n * continuation stream, but its tool part lives in the assistant message that\n * originally requested it. `StreamAccumulator` surfaces this as a\n * `cross-message-tool-update` action because the accumulator only owns the\n * current turn's new content and cannot mutate a part from a prior message.\n *\n * Compared to {@link toolResultUpdate} this builder is deliberately more\n * defensive, mirroring the equivalent fallback in `@cloudflare/ai-chat`:\n *\n * - It matches the broad set of pre-terminal **and** terminal states, so a\n * provider that replays the entire prior tool round-trip during a\n * continuation (notably the OpenAI Responses API — issue #1404) still\n * resolves to the same part instead of silently missing it.\n * - It is **first-write-wins**: a chunk arriving for a tool that already holds\n * a terminal result is treated as a replay and the existing output is never\n * overwritten. In that case `apply` returns the *same part reference*, which\n * callers use as an idempotent-no-op signal to skip the durable write and a\n * redundant `MESSAGE_UPDATED` broadcast.\n * - It preserves a streamed `preliminary` flag when one is present, otherwise\n * marks the result final (`preliminary: false`).\n */\nexport function crossMessageToolResultUpdate(\n toolCallId: string,\n updateType: \"output-available\" | \"output-error\",\n output?: unknown,\n errorText?: string,\n preliminary?: boolean\n): ToolPartUpdate {\n return {\n toolCallId,\n matchStates: [\n \"input-streaming\",\n \"input-available\",\n \"approval-requested\",\n \"approval-responded\",\n \"output-available\",\n \"output-error\",\n \"output-denied\"\n ],\n apply: (part) => {\n if (\n part.state === \"output-available\" ||\n part.state === \"output-error\" ||\n part.state === \"output-denied\"\n ) {\n return part;\n }\n if (updateType === \"output-error\") {\n return {\n ...part,\n state: \"output-error\",\n errorText: errorText ?? \"Tool execution failed\"\n };\n }\n return {\n ...part,\n state: \"output-available\",\n output,\n preliminary: preliminary ?? false\n };\n }\n };\n}\n\n/**\n * Build an update descriptor for applying a tool approval.\n *\n * Matches parts in `input-available` or `approval-requested` state.\n * Sets state to `approval-responded` (if approved) or `output-denied` (if denied).\n */\nexport function toolApprovalUpdate(\n toolCallId: string,\n approved: boolean\n): ToolPartUpdate {\n return {\n toolCallId,\n matchStates: [\"input-available\", \"approval-requested\"],\n apply: (part) => ({\n ...part,\n state: approved ? \"approval-responded\" : \"output-denied\",\n approval: {\n ...(part.approval as Record<string, unknown> | undefined),\n approved\n }\n })\n };\n}\n","/**\n * Protocol Message Parser — typed parsing of cf_agent_chat_* WebSocket messages.\n *\n * Parses raw WebSocket messages into a discriminated union of protocol events.\n * Both AIChatAgent and Think can use this instead of manual JSON.parse + type checking.\n */\n\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\n\n/**\n * Discriminated union of all incoming chat protocol events.\n *\n * Each agent handles the events it cares about and ignores the rest.\n * Returns `null` for non-JSON messages or unrecognized types.\n */\nexport type ChatProtocolEvent =\n | {\n type: \"chat-request\";\n id: string;\n init: { method?: string; body?: string; [key: string]: unknown };\n }\n | { type: \"clear\" }\n | { type: \"cancel\"; id: string }\n | {\n type: \"tool-result\";\n toolCallId: string;\n toolName: string;\n output: unknown;\n state?: string;\n errorText?: string;\n autoContinue?: boolean;\n clientTools?: Array<{\n name: string;\n description?: string;\n parameters?: unknown;\n }>;\n }\n | {\n type: \"tool-approval\";\n toolCallId: string;\n approved: boolean;\n autoContinue?: boolean;\n }\n | { type: \"stream-resume-request\" }\n | { type: \"stream-resume-ack\"; id: string }\n | { type: \"messages\"; messages: unknown[] };\n\n/**\n * Parse a raw WebSocket message string into a typed protocol event.\n *\n * Returns `null` if the message is not valid JSON or not a recognized\n * protocol message type. Callers should fall through to the user's\n * `onMessage` handler when `null` is returned.\n *\n * @example\n * ```typescript\n * const event = parseProtocolMessage(rawMessage);\n * if (!event) return userOnMessage(connection, rawMessage);\n *\n * switch (event.type) {\n * case \"chat-request\": { ... }\n * case \"clear\": { ... }\n * case \"tool-result\": { ... }\n * }\n * ```\n */\nexport function parseProtocolMessage(raw: string): ChatProtocolEvent | null {\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return null;\n }\n\n const wireType = data.type as string | undefined;\n if (!wireType) return null;\n\n switch (wireType) {\n case CHAT_MESSAGE_TYPES.USE_CHAT_REQUEST:\n return {\n type: \"chat-request\",\n id: data.id as string,\n init: (data.init as { method?: string; body?: string }) ?? {}\n };\n\n case CHAT_MESSAGE_TYPES.CHAT_CLEAR:\n return { type: \"clear\" };\n\n case CHAT_MESSAGE_TYPES.CHAT_REQUEST_CANCEL:\n return { type: \"cancel\", id: data.id as string };\n\n case CHAT_MESSAGE_TYPES.TOOL_RESULT:\n return {\n type: \"tool-result\",\n toolCallId: data.toolCallId as string,\n toolName: (data.toolName as string) ?? \"\",\n output: data.output,\n state: data.state as string | undefined,\n errorText: data.errorText as string | undefined,\n autoContinue: data.autoContinue as boolean | undefined,\n clientTools: data.clientTools as\n | Array<{\n name: string;\n description?: string;\n parameters?: unknown;\n }>\n | undefined\n };\n\n case CHAT_MESSAGE_TYPES.TOOL_APPROVAL:\n return {\n type: \"tool-approval\",\n toolCallId: data.toolCallId as string,\n approved: data.approved as boolean,\n autoContinue: data.autoContinue as boolean | undefined\n };\n\n case CHAT_MESSAGE_TYPES.STREAM_RESUME_REQUEST:\n return { type: \"stream-resume-request\" };\n\n case CHAT_MESSAGE_TYPES.STREAM_RESUME_ACK:\n return { type: \"stream-resume-ack\", id: data.id as string };\n\n case CHAT_MESSAGE_TYPES.CHAT_MESSAGES:\n return {\n type: \"messages\",\n messages: (data.messages as unknown[]) ?? []\n };\n\n default:\n return null;\n }\n}\n","/**\n * Message reconciliation — pure functions for aligning client messages\n * with server state during persistence.\n *\n * Three strategies applied in order:\n * 1. Merge server-known tool outputs into stale client messages\n * 2. Reconcile assistant IDs (exact match → content-key → toolCallId)\n * 3. Per-message toolCallId dedup for persistence\n */\n\nimport type { UIMessage } from \"ai\";\n\n/**\n * Reconcile incoming client messages against server state.\n *\n * 1. Merges server-known tool outputs into incoming messages that still\n * show stale states (input-available, approval-requested, approval-responded)\n * 2. Reconciles assistant IDs: exact match → content-key match → toolCallId match\n *\n * @param incoming - Messages from the client\n * @param serverMessages - Current server-side messages (source of truth)\n * @param sanitizeForContentKey - Function to sanitize a message before computing\n * its content key (typically strips ephemeral provider metadata)\n * @returns Reconciled messages ready for persistence\n */\nexport function reconcileMessages(\n incoming: UIMessage[],\n serverMessages: readonly UIMessage[],\n sanitizeForContentKey?: (message: UIMessage) => UIMessage\n): UIMessage[] {\n const withMergedToolOutputs = mergeServerToolOutputs(\n incoming,\n serverMessages\n );\n return reconcileAssistantIds(\n withMergedToolOutputs,\n serverMessages,\n sanitizeForContentKey\n );\n}\n\n/**\n * For a single message, resolve its ID by matching toolCallId against server state.\n * Prevents duplicate DB rows when client IDs differ from server IDs.\n * Tool call IDs are unique per conversation, so matching is safe regardless of state.\n */\nexport function resolveToolMergeId(\n message: UIMessage,\n serverMessages: readonly UIMessage[]\n): UIMessage {\n if (message.role !== \"assistant\") {\n return message;\n }\n\n for (const part of message.parts) {\n if (\"toolCallId\" in part && part.toolCallId) {\n const toolCallId = part.toolCallId as string;\n const existing = findMessageByToolCallId(serverMessages, toolCallId);\n if (existing && existing.id !== message.id) {\n return { ...message, id: existing.id };\n }\n }\n }\n\n return message;\n}\n\n/**\n * Content key for assistant messages used for dedup of identical short replies.\n * Returns JSON of sanitized parts, or undefined for non-assistant messages.\n */\nexport function assistantContentKey(\n message: UIMessage,\n sanitize?: (message: UIMessage) => UIMessage\n): string | undefined {\n if (message.role !== \"assistant\") {\n return undefined;\n }\n const sanitized = sanitize ? sanitize(message) : message;\n return JSON.stringify(sanitized.parts);\n}\n\nfunction mergeServerToolOutputs(\n incoming: UIMessage[],\n serverMessages: readonly UIMessage[]\n): UIMessage[] {\n // Index the server's RESOLVED tool parts so a stale client part (still in a\n // pre-output state) can't clobber the server's terminal state on persist.\n // All three terminal states must be protected, not just `output-available`:\n // otherwise a client that hasn't seen the server's `output-error` /\n // `output-denied` yet would persist its stale `input-available` over the\n // resolved result, losing the error/denial.\n const serverResolvedParts = new Map<string, Record<string, unknown>>();\n for (const msg of serverMessages) {\n if (msg.role !== \"assistant\") continue;\n for (const part of msg.parts) {\n const record = part as Record<string, unknown>;\n if (\n \"toolCallId\" in record &&\n \"state\" in record &&\n (record.state === \"output-available\" ||\n record.state === \"output-error\" ||\n record.state === \"output-denied\")\n ) {\n serverResolvedParts.set(record.toolCallId as string, record);\n }\n }\n }\n\n if (serverResolvedParts.size === 0) return incoming;\n\n return incoming.map((msg) => {\n if (msg.role !== \"assistant\") return msg;\n\n let hasChanges = false;\n const updatedParts = msg.parts.map((part) => {\n const record = part as Record<string, unknown>;\n if (\n \"toolCallId\" in record &&\n \"state\" in record &&\n (record.state === \"input-available\" ||\n record.state === \"approval-requested\" ||\n record.state === \"approval-responded\") &&\n serverResolvedParts.has(record.toolCallId as string)\n ) {\n hasChanges = true;\n const server = serverResolvedParts.get(record.toolCallId as string)!;\n // Overlay the server's resolved state, keeping the client part's\n // identity/input. Carry ONLY the result field that belongs to the\n // server's terminal state — so a stray `output` left on an\n // `output-error` part can't ride along and be misread as a result.\n const merged: Record<string, unknown> = {\n ...part,\n state: server.state\n };\n if (server.state === \"output-available\") {\n if (\"output\" in server) merged.output = server.output;\n } else if (server.state === \"output-error\") {\n if (\"errorText\" in server) merged.errorText = server.errorText;\n } else if (server.state === \"output-denied\") {\n if (\"approval\" in server) merged.approval = server.approval;\n }\n return merged;\n }\n return part;\n }) as UIMessage[\"parts\"];\n\n return hasChanges ? { ...msg, parts: updatedParts } : msg;\n });\n}\n\nfunction reconcileAssistantIds(\n incoming: UIMessage[],\n serverMessages: readonly UIMessage[],\n sanitize?: (message: UIMessage) => UIMessage\n): UIMessage[] {\n if (serverMessages.length === 0) return incoming;\n\n const claimedServerIndices = new Set<number>();\n const exactMatchMap = new Map<number, number>();\n\n for (let i = 0; i < incoming.length; i++) {\n const serverIdx = serverMessages.findIndex(\n (sm, si) => !claimedServerIndices.has(si) && sm.id === incoming[i].id\n );\n if (serverIdx !== -1) {\n claimedServerIndices.add(serverIdx);\n exactMatchMap.set(i, serverIdx);\n }\n }\n\n return incoming.map((incomingMessage, incomingIdx) => {\n if (exactMatchMap.has(incomingIdx)) {\n return incomingMessage;\n }\n\n if (\n incomingMessage.role !== \"assistant\" ||\n hasToolCallPart(incomingMessage)\n ) {\n return incomingMessage;\n }\n\n const incomingKey = assistantContentKey(incomingMessage, sanitize);\n if (!incomingKey) {\n return incomingMessage;\n }\n\n for (let i = 0; i < serverMessages.length; i++) {\n if (claimedServerIndices.has(i)) continue;\n\n const serverMessage = serverMessages[i];\n if (\n serverMessage.role !== \"assistant\" ||\n hasToolCallPart(serverMessage)\n ) {\n continue;\n }\n\n if (assistantContentKey(serverMessage, sanitize) === incomingKey) {\n claimedServerIndices.add(i);\n return { ...incomingMessage, id: serverMessage.id };\n }\n }\n\n return incomingMessage;\n });\n}\n\nfunction hasToolCallPart(message: UIMessage): boolean {\n return message.parts.some((part) => \"toolCallId\" in part);\n}\n\nfunction findMessageByToolCallId(\n messages: readonly UIMessage[],\n toolCallId: string\n): UIMessage | undefined {\n for (const msg of messages) {\n if (msg.role !== \"assistant\") continue;\n for (const part of msg.parts) {\n if (\"toolCallId\" in part && part.toolCallId === toolCallId) {\n return msg;\n }\n }\n }\n return undefined;\n}\n","import type { UIMessage } from \"ai\";\nimport type { ClientToolSchema } from \"./client-tools\";\n\nexport type ChatFiberSnapshot<Kind extends string = string> = {\n kind: Kind;\n version: 1;\n requestId: string;\n recoveryRootRequestId?: string;\n continuation: boolean;\n latestMessageId?: string;\n latestMessageRole?: string;\n latestUserMessageId?: string;\n startedAt: number;\n lastBody?: Record<string, unknown>;\n lastClientTools?: ClientToolSchema[];\n};\n\nexport function createChatFiberSnapshot<Kind extends string>({\n kind,\n requestId,\n recoveryRootRequestId,\n continuation,\n messages,\n lastBody,\n lastClientTools\n}: {\n kind: Kind;\n requestId: string;\n recoveryRootRequestId?: string;\n continuation: boolean;\n messages: UIMessage[];\n lastBody?: Record<string, unknown>;\n lastClientTools?: ClientToolSchema[];\n}): ChatFiberSnapshot<Kind> {\n const latestMessage =\n messages.length > 0 ? messages[messages.length - 1] : undefined;\n let latestUser: UIMessage | undefined;\n\n for (let index = messages.length - 1; index >= 0; index--) {\n if (messages[index].role === \"user\") {\n latestUser = messages[index];\n break;\n }\n }\n\n return {\n kind,\n version: 1,\n requestId,\n recoveryRootRequestId,\n continuation,\n latestMessageId: latestMessage?.id,\n latestMessageRole: latestMessage?.role,\n latestUserMessageId: latestUser?.id,\n startedAt: Date.now(),\n lastBody,\n lastClientTools\n };\n}\n\nexport function wrapChatFiberSnapshot<Kind extends string>(\n key: string,\n snapshot: ChatFiberSnapshot<Kind>,\n user: unknown | null\n): Record<string, unknown> {\n return { [key]: snapshot, user };\n}\n\nexport function unwrapChatFiberSnapshot<Kind extends string>(\n key: string,\n value: unknown,\n expectedKind?: Kind\n): {\n snapshot: ChatFiberSnapshot<Kind> | null;\n user: unknown | null;\n} {\n if (typeof value !== \"object\" || value === null || !(key in value)) {\n return { snapshot: null, user: value };\n }\n\n const envelope = value as Record<string, unknown>;\n const snapshot = envelope[key];\n if (typeof snapshot !== \"object\" || snapshot === null) {\n return { snapshot: null, user: value };\n }\n const candidate = snapshot as Record<string, unknown>;\n if (\n candidate.version !== 1 ||\n (expectedKind !== undefined && candidate.kind !== expectedKind) ||\n typeof candidate.requestId !== \"string\" ||\n typeof candidate.continuation !== \"boolean\"\n ) {\n return { snapshot: null, user: value };\n }\n\n return {\n snapshot: snapshot as ChatFiberSnapshot<Kind>,\n user: envelope.user ?? null\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["textEncoder","sendIfOpen","isWebSocketClosedSendError"],"sources":["../../src/chat/sanitize.ts","../../src/chat/stream-accumulator.ts","../../src/chat/turn-queue.ts","../../src/chat/submit-concurrency.ts","../../src/chat/broadcast-state.ts","../../src/chat/protocol.ts","../../src/chat/resumable-stream.ts","../../src/chat/sql-batch.ts","../../src/chat/client-tools.ts","../../src/chat/continuation-state.ts","../../src/chat/abort-registry.ts","../../src/chat/tool-state.ts","../../src/chat/parse-protocol.ts","../../src/chat/message-reconciler.ts","../../src/chat/recovery.ts"],"sourcesContent":["/**\n * Message sanitization and row-size enforcement utilities.\n *\n * Shared by @cloudflare/ai-chat and @cloudflare/think to ensure persistence\n * hygiene: stripping ephemeral provider metadata and compacting\n * oversized messages before writing to SQLite.\n */\n\nimport type { ProviderMetadata, ReasoningUIPart, UIMessage } from \"ai\";\nimport { truncateToolOutput } from \"./tool-output-truncation\";\n\nconst textEncoder = new TextEncoder();\n\n/** Maximum serialized message size before compaction (bytes). 1.8MB with headroom below SQLite's 2MB limit. */\nexport const ROW_MAX_BYTES = 1_800_000;\n\n/** Measure UTF-8 byte length of a string. */\nexport function byteLength(s: string): number {\n return textEncoder.encode(s).byteLength;\n}\n\n/**\n * Sanitize a message for persistence by removing ephemeral provider-specific\n * data that should not be stored or sent back in subsequent requests.\n *\n * 1. Strips OpenAI ephemeral fields (itemId, reasoningEncryptedContent)\n * 2. Filters truly empty reasoning parts (no text, no remaining providerMetadata)\n */\nexport function sanitizeMessage(message: UIMessage): UIMessage {\n const strippedParts = message.parts.map((part) => {\n let sanitizedPart = part;\n\n if (\n \"providerMetadata\" in sanitizedPart &&\n sanitizedPart.providerMetadata &&\n typeof sanitizedPart.providerMetadata === \"object\" &&\n \"openai\" in sanitizedPart.providerMetadata\n ) {\n sanitizedPart = stripOpenAIMetadata(sanitizedPart, \"providerMetadata\");\n }\n\n if (\n \"callProviderMetadata\" in sanitizedPart &&\n sanitizedPart.callProviderMetadata &&\n typeof sanitizedPart.callProviderMetadata === \"object\" &&\n \"openai\" in sanitizedPart.callProviderMetadata\n ) {\n sanitizedPart = stripOpenAIMetadata(\n sanitizedPart,\n \"callProviderMetadata\"\n );\n }\n\n return sanitizedPart;\n }) as UIMessage[\"parts\"];\n\n const sanitizedParts = strippedParts.filter((part) => {\n if (part.type === \"reasoning\") {\n const reasoningPart = part as ReasoningUIPart;\n if (!reasoningPart.text || reasoningPart.text.trim() === \"\") {\n if (\n \"providerMetadata\" in reasoningPart &&\n reasoningPart.providerMetadata &&\n typeof reasoningPart.providerMetadata === \"object\" &&\n Object.keys(reasoningPart.providerMetadata).length > 0\n ) {\n return true;\n }\n return false;\n }\n }\n return true;\n });\n\n return { ...message, parts: sanitizedParts };\n}\n\nfunction stripOpenAIMetadata<T extends UIMessage[\"parts\"][number]>(\n part: T,\n metadataKey: \"providerMetadata\" | \"callProviderMetadata\"\n): T {\n const metadata = (part as Record<string, unknown>)[metadataKey] as {\n openai?: Record<string, unknown>;\n [key: string]: unknown;\n };\n\n if (!metadata?.openai) return part;\n\n const {\n itemId: _itemId,\n reasoningEncryptedContent: _rec,\n ...restOpenai\n } = metadata.openai;\n\n const hasOtherOpenaiFields = Object.keys(restOpenai).length > 0;\n const { openai: _openai, ...restMetadata } = metadata;\n\n let newMetadata: ProviderMetadata | undefined;\n if (hasOtherOpenaiFields) {\n newMetadata = { ...restMetadata, openai: restOpenai } as ProviderMetadata;\n } else if (Object.keys(restMetadata).length > 0) {\n newMetadata = restMetadata as ProviderMetadata;\n }\n\n const { [metadataKey]: _oldMeta, ...restPart } = part as Record<\n string,\n unknown\n >;\n\n if (newMetadata) {\n return { ...restPart, [metadataKey]: newMetadata } as T;\n }\n return restPart as T;\n}\n\n/**\n * Enforce SQLite row size limits by compacting tool outputs and text parts\n * when a serialized message exceeds the safety threshold (1.8MB).\n *\n * Compaction strategy:\n * 1. Compact tool outputs over 1KB while preserving structured output shape\n * 2. If still too big, truncate text parts from oldest to newest\n */\nexport function enforceRowSizeLimit(message: UIMessage): UIMessage {\n let json = JSON.stringify(message);\n let size = byteLength(json);\n if (size <= ROW_MAX_BYTES) return message;\n\n if (message.role !== \"assistant\") {\n return truncateTextParts(message);\n }\n\n const compactedParts = message.parts.map((part) => {\n if (\n \"output\" in part &&\n \"toolCallId\" in part &&\n \"state\" in part &&\n part.state === \"output-available\"\n ) {\n const output = (part as { output: unknown }).output;\n const truncated = truncateToolOutput(output, 1000);\n if (truncated.truncated) {\n return {\n ...part,\n output: truncated.output\n };\n }\n }\n return part;\n }) as UIMessage[\"parts\"];\n\n let result: UIMessage = { ...message, parts: compactedParts };\n\n json = JSON.stringify(result);\n size = byteLength(json);\n if (size <= ROW_MAX_BYTES) return result;\n\n return truncateTextParts(result);\n}\n\nfunction truncateTextParts(message: UIMessage): UIMessage {\n const parts = [...message.parts];\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n if (part.type === \"text\" && \"text\" in part) {\n const text = (part as { text: string }).text;\n if (text.length > 1000) {\n parts[i] = {\n ...part,\n text:\n `[Text truncated for storage (${text.length} chars). ` +\n `First 500 chars: ${text.slice(0, 500)}...]`\n } as UIMessage[\"parts\"][number];\n\n const candidate = { ...message, parts };\n if (byteLength(JSON.stringify(candidate)) <= ROW_MAX_BYTES) {\n break;\n }\n }\n }\n }\n\n return { ...message, parts };\n}\n","/**\n * StreamAccumulator — unified chunk-to-message builder.\n *\n * Used by @cloudflare/ai-chat (server + client) and @cloudflare/think\n * to incrementally build a UIMessage from stream chunks. Wraps\n * applyChunkToParts and handles the metadata chunk types (start, finish,\n * message-metadata, error) that applyChunkToParts does not cover.\n *\n * The accumulator signals domain-specific concerns (early persistence,\n * cross-message tool updates) via ChunkAction returns — callers handle\n * these according to their context.\n */\n\nimport type { UIMessage } from \"ai\";\nimport { applyChunkToParts, type StreamChunkData } from \"./message-builder\";\n\nfunction asMetadata(value: unknown): Record<string, unknown> | undefined {\n if (value != null && typeof value === \"object\" && !Array.isArray(value)) {\n return value as Record<string, unknown>;\n }\n return undefined;\n}\n\nexport interface StreamAccumulatorOptions {\n messageId: string;\n continuation?: boolean;\n existingParts?: UIMessage[\"parts\"];\n existingMetadata?: Record<string, unknown>;\n}\n\nexport type ChunkAction =\n | {\n type: \"start\";\n messageId?: string;\n metadata?: Record<string, unknown>;\n }\n | {\n type: \"finish\";\n finishReason?: string;\n metadata?: Record<string, unknown>;\n }\n | { type: \"message-metadata\"; metadata: Record<string, unknown> }\n | { type: \"tool-approval-request\"; toolCallId: string }\n | {\n type: \"cross-message-tool-update\";\n updateType: \"output-available\" | \"output-error\";\n toolCallId: string;\n output?: unknown;\n errorText?: string;\n preliminary?: boolean;\n }\n | { type: \"error\"; error: string };\n\nexport interface ChunkResult {\n handled: boolean;\n action?: ChunkAction;\n}\n\nexport class StreamAccumulator {\n messageId: string;\n readonly parts: UIMessage[\"parts\"];\n metadata?: Record<string, unknown>;\n private _isContinuation: boolean;\n\n constructor(options: StreamAccumulatorOptions) {\n this.messageId = options.messageId;\n this._isContinuation = options.continuation ?? false;\n this.parts = options.existingParts ? [...options.existingParts] : [];\n this.metadata = options.existingMetadata\n ? { ...options.existingMetadata }\n : undefined;\n }\n\n applyChunk(chunk: StreamChunkData): ChunkResult {\n const handled = applyChunkToParts(this.parts, chunk);\n\n // Detect tool-approval-request for early persistence signaling\n if (chunk.type === \"tool-approval-request\" && chunk.toolCallId) {\n return {\n handled,\n action: { type: \"tool-approval-request\", toolCallId: chunk.toolCallId }\n };\n }\n\n // Detect cross-message tool output/error: applyChunkToParts returns true\n // for recognized types but silently does nothing when the toolCallId\n // doesn't exist in the current parts array.\n if (\n (chunk.type === \"tool-output-available\" ||\n chunk.type === \"tool-output-error\") &&\n chunk.toolCallId\n ) {\n const foundInParts = this.parts.some(\n (p) => \"toolCallId\" in p && p.toolCallId === chunk.toolCallId\n );\n if (!foundInParts) {\n return {\n handled,\n action: {\n type: \"cross-message-tool-update\",\n updateType:\n chunk.type === \"tool-output-available\"\n ? \"output-available\"\n : \"output-error\",\n toolCallId: chunk.toolCallId,\n output: chunk.output,\n errorText: chunk.errorText,\n preliminary: chunk.preliminary\n }\n };\n }\n }\n\n if (!handled) {\n switch (chunk.type) {\n case \"start\": {\n if (chunk.messageId != null && !this._isContinuation) {\n this.messageId = chunk.messageId;\n }\n const startMeta = asMetadata(chunk.messageMetadata);\n if (startMeta) {\n this.metadata = this.metadata\n ? { ...this.metadata, ...startMeta }\n : { ...startMeta };\n }\n return {\n handled: true,\n action: {\n type: \"start\",\n messageId: chunk.messageId,\n metadata: startMeta\n }\n };\n }\n case \"finish\": {\n const finishMeta = asMetadata(chunk.messageMetadata);\n if (finishMeta) {\n this.metadata = this.metadata\n ? { ...this.metadata, ...finishMeta }\n : { ...finishMeta };\n }\n const finishReason =\n \"finishReason\" in chunk\n ? (chunk.finishReason as string)\n : undefined;\n return {\n handled: true,\n action: {\n type: \"finish\",\n finishReason,\n metadata: finishMeta\n }\n };\n }\n case \"message-metadata\": {\n const msgMeta = asMetadata(chunk.messageMetadata);\n if (msgMeta) {\n this.metadata = this.metadata\n ? { ...this.metadata, ...msgMeta }\n : { ...msgMeta };\n }\n return {\n handled: true,\n action: {\n type: \"message-metadata\",\n metadata: msgMeta ?? {}\n }\n };\n }\n case \"finish-step\": {\n return { handled: true };\n }\n case \"error\": {\n return {\n handled: true,\n action: {\n type: \"error\",\n error: chunk.errorText ?? JSON.stringify(chunk)\n }\n };\n }\n }\n }\n\n return { handled };\n }\n\n /** Snapshot the current state as a UIMessage. */\n toMessage(): UIMessage {\n return {\n id: this.messageId,\n role: \"assistant\",\n parts: [...this.parts],\n ...(this.metadata != null && { metadata: this.metadata })\n } as UIMessage;\n }\n\n /**\n * Merge this accumulator's message into an existing message array.\n * Handles continuation (walk backward for last assistant), replacement\n * (update existing by messageId), or append (new message).\n */\n mergeInto(messages: UIMessage[]): UIMessage[] {\n let existingIdx = messages.findIndex((m) => m.id === this.messageId);\n\n if (existingIdx < 0 && this._isContinuation) {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === \"assistant\") {\n existingIdx = i;\n break;\n }\n }\n }\n\n const messageId =\n existingIdx >= 0 ? messages[existingIdx].id : this.messageId;\n\n const partialMessage: UIMessage = {\n id: messageId,\n role: \"assistant\",\n parts: [...this.parts],\n ...(this.metadata != null && { metadata: this.metadata })\n } as UIMessage;\n\n if (existingIdx >= 0) {\n const updated = [...messages];\n updated[existingIdx] = partialMessage;\n return updated;\n }\n return [...messages, partialMessage];\n }\n}\n","/**\n * TurnQueue — serial async queue with generation-based invalidation.\n *\n * Serializes async work via a promise chain, tracks which request is\n * currently active, and lets callers invalidate all queued work by\n * advancing a generation counter.\n *\n * Used by @cloudflare/ai-chat (full concurrency policy spectrum) and\n * @cloudflare/think (simple serial queue) to prevent overlapping\n * chat turns.\n */\n\nexport type TurnResult<T> =\n | { status: \"completed\"; value: T }\n | { status: \"stale\" };\n\nexport interface EnqueueOptions {\n /**\n * Generation to bind this turn to. Defaults to the current generation\n * at the time of the `enqueue` call. If the queue's generation has\n * advanced past this value by the time the turn reaches the front,\n * `fn` is not called and `{ status: \"stale\" }` is returned.\n */\n generation?: number;\n}\n\nexport class TurnQueue {\n private _queue: Promise<void> = Promise.resolve();\n private _generation = 0;\n private _activeRequestId: string | null = null;\n private _countsByGeneration = new Map<number, number>();\n\n get generation(): number {\n return this._generation;\n }\n\n get activeRequestId(): string | null {\n return this._activeRequestId;\n }\n\n get isActive(): boolean {\n return this._activeRequestId !== null;\n }\n\n async enqueue<T>(\n requestId: string,\n fn: () => Promise<T>,\n options?: EnqueueOptions\n ): Promise<TurnResult<T>> {\n const previousTurn = this._queue;\n let releaseTurn!: () => void;\n const capturedGeneration = options?.generation ?? this._generation;\n\n this._countsByGeneration.set(\n capturedGeneration,\n (this._countsByGeneration.get(capturedGeneration) ?? 0) + 1\n );\n\n this._queue = new Promise<void>((resolve) => {\n releaseTurn = resolve;\n });\n\n await previousTurn;\n\n if (this._generation !== capturedGeneration) {\n this._decrementCount(capturedGeneration);\n releaseTurn();\n return { status: \"stale\" };\n }\n\n this._activeRequestId = requestId;\n try {\n const value = await fn();\n return { status: \"completed\", value };\n } finally {\n this._activeRequestId = null;\n this._decrementCount(capturedGeneration);\n releaseTurn();\n }\n }\n\n /**\n * Advance the generation counter. All turns enqueued under older\n * generations will be skipped when they reach the front of the queue.\n */\n reset(): void {\n this._generation++;\n }\n\n /**\n * Wait until the queue is fully drained (no pending or active turns).\n */\n async waitForIdle(): Promise<void> {\n let queue: Promise<void>;\n do {\n queue = this._queue;\n await queue;\n } while (this._queue !== queue);\n }\n\n /**\n * Number of active + queued turns for a given generation.\n * Defaults to the current generation.\n */\n queuedCount(generation?: number): number {\n return this._countsByGeneration.get(generation ?? this._generation) ?? 0;\n }\n\n private _decrementCount(generation: number): void {\n const count = (this._countsByGeneration.get(generation) ?? 1) - 1;\n if (count <= 0) {\n this._countsByGeneration.delete(generation);\n } else {\n this._countsByGeneration.set(generation, count);\n }\n }\n}\n","import type { MessageConcurrency } from \"./lifecycle\";\n\nexport type NormalizedMessageConcurrency =\n | \"queue\"\n | \"latest\"\n | \"merge\"\n | \"drop\"\n | {\n strategy: \"debounce\";\n debounceMs: number;\n };\n\nexport type SubmitConcurrencyDecision = {\n action: \"execute\" | \"drop\";\n strategy: NormalizedMessageConcurrency | null;\n submitSequence: number | null;\n debounceUntilMs: number | null;\n};\n\nexport class SubmitConcurrencyController {\n private _submitSequence = 0;\n private _latestOverlappingSubmitSequence = 0;\n private _pendingEnqueueCount = 0;\n private _resetEpoch = 0;\n private _activeDebounceTimers = new Set<ReturnType<typeof setTimeout>>();\n private _activeDebounceResolves = new Set<() => void>();\n\n constructor(private readonly options: { defaultDebounceMs: number }) {}\n\n get pendingEnqueueCount(): number {\n return this._pendingEnqueueCount;\n }\n\n get overlappingSubmitCount(): number {\n return this._latestOverlappingSubmitSequence;\n }\n\n decide(options: {\n concurrency: MessageConcurrency;\n isSubmitMessage: boolean;\n queuedTurns: number;\n }): SubmitConcurrencyDecision {\n const queuedTurnsInCurrentEpoch =\n options.queuedTurns + this._pendingEnqueueCount;\n\n if (!options.isSubmitMessage || queuedTurnsInCurrentEpoch === 0) {\n return {\n action: \"execute\",\n strategy: null,\n submitSequence: null,\n debounceUntilMs: null\n };\n }\n\n const concurrency = this.normalize(options.concurrency);\n if (concurrency === \"drop\") {\n return {\n action: \"drop\",\n strategy: concurrency,\n submitSequence: null,\n debounceUntilMs: null\n };\n }\n\n if (concurrency === \"queue\") {\n return {\n action: \"execute\",\n strategy: concurrency,\n submitSequence: null,\n debounceUntilMs: null\n };\n }\n\n const submitSequence = ++this._submitSequence;\n this._latestOverlappingSubmitSequence = submitSequence;\n\n if (concurrency === \"latest\" || concurrency === \"merge\") {\n return {\n action: \"execute\",\n strategy: concurrency,\n submitSequence,\n debounceUntilMs: null\n };\n }\n\n return {\n action: \"execute\",\n strategy: concurrency,\n submitSequence,\n debounceUntilMs: Date.now() + concurrency.debounceMs\n };\n }\n\n /**\n * Mark a submit as accepted and in-flight between admission and turn\n * queue registration. Returns an idempotent `release()` function that\n * must be called when the submit either reaches the turn queue or is\n * abandoned. The returned function is bound to the controller's reset\n * epoch — releases from before the most recent `reset()` are no-ops,\n * so post-reset submits keep an accurate count.\n */\n beginEnqueue(): () => void {\n this._pendingEnqueueCount++;\n const epoch = this._resetEpoch;\n let released = false;\n return () => {\n if (released) return;\n released = true;\n if (this._resetEpoch !== epoch) return;\n this._pendingEnqueueCount = Math.max(0, this._pendingEnqueueCount - 1);\n };\n }\n\n isSuperseded(submitSequence: number | null): boolean {\n return (\n submitSequence !== null &&\n submitSequence < this._latestOverlappingSubmitSequence\n );\n }\n\n async waitForTimestamp(timestampMs: number): Promise<void> {\n const remainingMs = timestampMs - Date.now();\n if (remainingMs <= 0) {\n return;\n }\n\n await new Promise<void>((resolve) => {\n const wrappedResolve = () => {\n this._activeDebounceResolves.delete(wrappedResolve);\n resolve();\n };\n const timer = setTimeout(() => {\n this._activeDebounceTimers.delete(timer);\n wrappedResolve();\n }, remainingMs);\n\n this._activeDebounceTimers.add(timer);\n this._activeDebounceResolves.add(wrappedResolve);\n });\n }\n\n cancelActiveDebounce(): void {\n for (const timer of this._activeDebounceTimers) {\n clearTimeout(timer);\n }\n this._activeDebounceTimers.clear();\n\n const resolves = [...this._activeDebounceResolves];\n this._activeDebounceResolves.clear();\n for (const resolve of resolves) {\n resolve();\n }\n }\n\n reset(): void {\n this._resetEpoch++;\n this._pendingEnqueueCount = 0;\n this.cancelActiveDebounce();\n }\n\n async waitForIdle(waitForQueueIdle: () => Promise<void>): Promise<void> {\n while (true) {\n await waitForQueueIdle();\n if (this._pendingEnqueueCount === 0) return;\n await new Promise<void>((resolve) => setTimeout(resolve, 5));\n }\n }\n\n private normalize(\n concurrency: MessageConcurrency\n ): NormalizedMessageConcurrency {\n if (typeof concurrency === \"string\") {\n return concurrency;\n }\n\n const debounceMs = concurrency.debounceMs;\n\n return {\n strategy: \"debounce\",\n debounceMs:\n typeof debounceMs === \"number\" &&\n Number.isFinite(debounceMs) &&\n debounceMs >= 0\n ? debounceMs\n : this.options.defaultDebounceMs\n };\n }\n}\n","/**\n * Broadcast stream state machine.\n *\n * Manages the lifecycle of a StreamAccumulator for broadcast/resume\n * streams — the path where this client is *observing* a stream owned\n * by another tab or resumed after reconnect, rather than the transport-\n * owned path that feeds directly into useChat.\n *\n * The transition function is pure (no React, no WebSocket, no side\n * effects). Callers dispatch events and apply the returned state +\n * messagesUpdate. Side effects (sending ACKs, calling onData) stay\n * in the caller.\n */\n\nimport type { UIMessage } from \"ai\";\nimport { StreamAccumulator } from \"./stream-accumulator\";\nimport type { StreamChunkData } from \"./message-builder\";\n\n// ── State ──────────────────────────────────────────────────────────\n\nexport type BroadcastStreamState =\n | { status: \"idle\" }\n | {\n status: \"observing\";\n streamId: string;\n accumulator: StreamAccumulator;\n };\n\n// ── Events ─────────────────────────────────────────────────────────\n\nexport type BroadcastStreamEvent =\n | {\n type: \"response\";\n streamId: string;\n /** Fallback message ID for a new accumulator (ignored if one exists for this stream). */\n messageId: string;\n chunkData?: unknown;\n done?: boolean;\n error?: boolean;\n replay?: boolean;\n replayComplete?: boolean;\n continuation?: boolean;\n /** Required when continuation=true so the accumulator can pick up existing parts. */\n currentMessages?: UIMessage[];\n }\n | {\n type: \"resume-fallback\";\n streamId: string;\n messageId: string;\n }\n | { type: \"clear\" };\n\n// ── Result ─────────────────────────────────────────────────────────\n\nexport interface TransitionResult {\n state: BroadcastStreamState;\n messagesUpdate?: (prev: UIMessage[]) => UIMessage[];\n isStreaming: boolean;\n}\n\n// ── Transition ─────────────────────────────────────────────────────\n\nexport function transition(\n state: BroadcastStreamState,\n event: BroadcastStreamEvent\n): TransitionResult {\n switch (event.type) {\n case \"clear\":\n return { state: { status: \"idle\" }, isStreaming: false };\n\n case \"resume-fallback\": {\n const accumulator = new StreamAccumulator({\n messageId: event.messageId\n });\n return {\n state: {\n status: \"observing\",\n streamId: event.streamId,\n accumulator\n },\n isStreaming: true\n };\n }\n\n case \"response\": {\n let accumulator: StreamAccumulator;\n\n if (state.status === \"idle\" || state.streamId !== event.streamId) {\n let messageId = event.messageId;\n let existingParts: UIMessage[\"parts\"] | undefined;\n let existingMetadata: Record<string, unknown> | undefined;\n\n if (event.continuation && event.currentMessages) {\n for (let i = event.currentMessages.length - 1; i >= 0; i--) {\n if (event.currentMessages[i].role === \"assistant\") {\n messageId = event.currentMessages[i].id;\n existingParts = [...event.currentMessages[i].parts];\n if (event.currentMessages[i].metadata != null) {\n existingMetadata = {\n ...(event.currentMessages[i].metadata as Record<\n string,\n unknown\n >)\n };\n }\n break;\n }\n }\n }\n\n accumulator = new StreamAccumulator({\n messageId,\n continuation: event.continuation,\n existingParts,\n existingMetadata\n });\n } else {\n accumulator = state.accumulator;\n }\n\n if (event.chunkData) {\n accumulator.applyChunk(event.chunkData as StreamChunkData);\n }\n\n let messagesUpdate: ((prev: UIMessage[]) => UIMessage[]) | undefined;\n\n if (event.done) {\n messagesUpdate = (prev) => accumulator.mergeInto(prev);\n return {\n state: { status: \"idle\" },\n messagesUpdate,\n isStreaming: false\n };\n }\n\n if (event.chunkData && !event.replay) {\n messagesUpdate = (prev) => accumulator.mergeInto(prev);\n } else if (event.replayComplete) {\n messagesUpdate = (prev) => accumulator.mergeInto(prev);\n }\n\n return {\n state: {\n status: \"observing\",\n streamId: event.streamId,\n accumulator\n },\n messagesUpdate,\n isStreaming: true\n };\n }\n }\n}\n","/**\n * Wire protocol message type constants for the cf_agent_chat_* protocol.\n *\n * These are the string values used on the wire between agent servers and\n * clients. Both @cloudflare/ai-chat (via its MessageType enum) and\n * @cloudflare/think use these values.\n */\nexport const CHAT_MESSAGE_TYPES = {\n CHAT_MESSAGES: \"cf_agent_chat_messages\",\n USE_CHAT_REQUEST: \"cf_agent_use_chat_request\",\n USE_CHAT_RESPONSE: \"cf_agent_use_chat_response\",\n CHAT_CLEAR: \"cf_agent_chat_clear\",\n CHAT_REQUEST_CANCEL: \"cf_agent_chat_request_cancel\",\n STREAM_RESUMING: \"cf_agent_stream_resuming\",\n STREAM_RESUME_ACK: \"cf_agent_stream_resume_ack\",\n STREAM_RESUME_REQUEST: \"cf_agent_stream_resume_request\",\n STREAM_RESUME_NONE: \"cf_agent_stream_resume_none\",\n TOOL_RESULT: \"cf_agent_tool_result\",\n TOOL_APPROVAL: \"cf_agent_tool_approval\",\n MESSAGE_UPDATED: \"cf_agent_message_updated\",\n // Server→client: a durable chat turn is being recovered (interrupted by a\n // deploy/eviction or a stream-stall watchdog abort and now resuming). Sent\n // when a recovery continuation is scheduled and cleared on every terminal\n // outcome; `@cloudflare/think` also replays it on connect so a client that\n // joins mid-recovery learns it. Purely a progress hint — backward-compatible\n // (clients that don't understand it ignore it). See issue #1620.\n CHAT_RECOVERING: \"cf_agent_chat_recovering\"\n} as const;\n","/**\n * ResumableStream: Standalone class for buffering, persisting, and replaying\n * stream chunks in SQLite. Extracted from AIChatAgent to separate concerns.\n *\n * Handles:\n * - Chunk buffering (batched writes to SQLite for performance)\n * - Stream lifecycle (start, complete, error)\n * - Chunk replay for reconnecting clients\n * - Stale stream cleanup\n * - Active stream restoration after agent restart\n */\n\nimport { nanoid } from \"nanoid\";\nimport type { Connection } from \"agents\";\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\n\n/** Number of chunks to pack into a single SQLite row before flushing */\nconst CHUNK_BUFFER_SIZE = 10;\n/** Maximum buffer size to prevent memory issues on rapid reconnections */\nconst CHUNK_BUFFER_MAX_SIZE = 100;\n/**\n * Max accumulated raw chunk bytes packed into one row before forcing a flush.\n * The SQLite row limit is 2 MB; packing serializes bodies into a JSON array,\n * which re-escapes their contents (quotes/backslashes), so we keep the raw\n * total well under the limit to leave generous headroom for escaping overhead.\n * A chunk larger than this is flushed as its own (unwrapped) row.\n */\nconst SEGMENT_MAX_BYTES = 512_000;\n/** Default cleanup interval for old streams (ms) - every 10 minutes */\nconst CLEANUP_INTERVAL_MS = 10 * 60 * 1000;\n/** Default age threshold for cleaning up completed streams (ms) - 24 hours */\nconst CLEANUP_AGE_THRESHOLD_MS = 24 * 60 * 60 * 1000;\n/** Shared encoder for UTF-8 byte length measurement */\nconst textEncoder = new TextEncoder();\n\n/**\n * A stored row body is either a single chunk body (a JSON object string —\n * legacy per-chunk rows and single-chunk segments) or a packed segment (a JSON\n * array of chunk body strings). Unpack to the individual chunk bodies in order.\n *\n * Stored chunk bodies are always serialized JSON *objects*, never arrays, so\n * `Array.isArray` reliably distinguishes a packed segment from a single body.\n */\nfunction unpackSegmentBody(rowBody: string): string[] {\n try {\n const parsed = JSON.parse(rowBody);\n if (Array.isArray(parsed)) {\n return parsed as string[];\n }\n } catch {\n // Not valid JSON — treat as a single opaque body.\n }\n return [rowBody];\n}\n\nfunction sendIfOpen(connection: Connection, message: string): boolean {\n try {\n connection.send(message);\n return true;\n } catch (error) {\n if (isWebSocketClosedSendError(error)) return false;\n throw error;\n }\n}\n\nfunction isWebSocketClosedSendError(error: unknown): boolean {\n return (\n error instanceof TypeError &&\n error.message.includes(\"WebSocket send() after close\")\n );\n}\n\n/**\n * Stored stream chunk for resumable streaming\n */\ntype StreamChunk = {\n id: string;\n stream_id: string;\n body: string;\n chunk_index: number;\n created_at: number;\n};\n\n/**\n * Stream metadata for tracking active streams\n */\ntype StreamMetadata = {\n id: string;\n request_id: string;\n status: \"streaming\" | \"completed\" | \"error\";\n created_at: number;\n completed_at: number | null;\n /**\n * The assistant message id this stream is producing, captured when the\n * stream starts. This is the SAME id the live path persists under, so orphan\n * recovery (#1691) can re-associate reconstructed chunks with the correct\n * message even when the provider stream carries no `start.messageId`. Null on\n * legacy rows written before this column existed.\n */\n message_id: string | null;\n};\n\n/**\n * Minimal SQL interface matching Agent's this.sql tagged template.\n * Allows ResumableStream to work with the Agent's SQLite without\n * depending on the full Agent class.\n */\nexport type SqlTaggedTemplate = {\n <T = Record<string, unknown>>(\n strings: TemplateStringsArray,\n ...values: (string | number | boolean | null)[]\n ): T[];\n};\n\nexport class ResumableStream {\n private _activeStreamId: string | null = null;\n private _activeRequestId: string | null = null;\n /** Monotonic row-ordering index; one increment per flushed segment row. */\n private _segmentIndex = 0;\n\n /**\n * Whether the active stream was started in this instance (true) or\n * restored from SQLite after hibernation/restart (false). An orphaned\n * stream has no live LLM reader — the ReadableStream was lost when the\n * DO was evicted.\n */\n private _isLive = false;\n\n private _chunkBuffer: Array<{ streamId: string; body: string }> = [];\n private _chunkBufferBytes = 0;\n private _isFlushingChunks = false;\n private _lastCleanupTime = 0;\n\n constructor(private sql: SqlTaggedTemplate) {\n // Create tables for stream chunks and metadata\n this.sql`create table if not exists cf_ai_chat_stream_chunks (\n id text primary key,\n stream_id text not null,\n body text not null,\n chunk_index integer not null,\n created_at integer not null\n )`;\n\n this.sql`create table if not exists cf_ai_chat_stream_metadata (\n id text primary key,\n request_id text not null,\n status text not null,\n created_at integer not null,\n completed_at integer\n )`;\n\n // Backward-compatible migration (#1691): add the column that durably links a\n // stream to its assistant message. Tables created before this release lack\n // it, so `alter table add column` is idempotent — the duplicate-column\n // error (and ONLY that error) is swallowed on an already-migrated table.\n this._migrateMetadataColumns();\n\n this.sql`create index if not exists idx_stream_chunks_stream_id \n on cf_ai_chat_stream_chunks(stream_id, chunk_index)`;\n\n // Restore any active stream from a previous session\n this.restore();\n }\n\n /**\n * Add the #1691 recovery column to the metadata table for rows created before\n * it existed. Inspects the current schema and only runs `alter table` when the\n * column is absent — idempotent across Durable Object restarts, with no\n * error-driven control flow.\n */\n private _migrateMetadataColumns() {\n const columns =\n this.sql<{ name: string }>`\n select name from pragma_table_info('cf_ai_chat_stream_metadata')\n ` ?? [];\n const hasMessageId = columns.some((column) => column.name === \"message_id\");\n if (!hasMessageId) {\n this\n .sql`alter table cf_ai_chat_stream_metadata add column message_id text`;\n }\n }\n\n // ── State accessors ────────────────────────────────────────────────\n\n get activeStreamId(): string | null {\n return this._activeStreamId;\n }\n\n get activeRequestId(): string | null {\n return this._activeRequestId;\n }\n\n hasActiveStream(): boolean {\n return this._activeStreamId !== null;\n }\n\n /**\n * Whether the active stream has a live LLM reader (started in this\n * instance) vs being restored from SQLite after hibernation (orphaned).\n */\n get isLive(): boolean {\n return this._isLive;\n }\n\n // ── Stream lifecycle ───────────────────────────────────────────────\n\n /**\n * Start tracking a new stream for resumable streaming.\n * Creates metadata entry in SQLite and sets up tracking state.\n * @param requestId - The unique ID of the chat request\n * @returns The generated stream ID\n */\n start(requestId: string, options: { messageId?: string } = {}): string {\n // Flush any pending chunks from previous streams to prevent mixing\n this.flushBuffer();\n\n const streamId = nanoid();\n this._activeStreamId = streamId;\n this._activeRequestId = requestId;\n this._segmentIndex = 0;\n this._isLive = true;\n\n const messageId = options.messageId ?? null;\n\n this.sql`\n insert into cf_ai_chat_stream_metadata (id, request_id, status, created_at, message_id)\n values (${streamId}, ${requestId}, 'streaming', ${Date.now()}, ${messageId})\n `;\n\n return streamId;\n }\n\n /**\n * The assistant message id an orphaned stream was producing — the same id the\n * live path persists under, so recovery re-associates reconstructed chunks\n * with the correct message (#1691). Returns null when the row is missing or\n * is a legacy row written before the `message_id` column existed.\n */\n getStreamMessageId(streamId: string): string | null {\n const rows = this.sql<{ message_id: string | null }>`\n select message_id from cf_ai_chat_stream_metadata\n where id = ${streamId}\n `;\n if (!rows || rows.length === 0) return null;\n return rows[0].message_id ?? null;\n }\n\n /**\n * Mark a stream as completed and flush any pending chunks.\n * @param streamId - The stream to mark as completed\n */\n complete(streamId: string) {\n this.flushBuffer();\n\n this.sql`\n update cf_ai_chat_stream_metadata \n set status = 'completed', completed_at = ${Date.now()} \n where id = ${streamId}\n `;\n this._activeStreamId = null;\n this._activeRequestId = null;\n this._segmentIndex = 0;\n this._isLive = false;\n\n // Periodically clean up old streams\n this._maybeCleanupOldStreams();\n }\n\n /**\n * Mark a stream as errored and clean up state.\n * @param streamId - The stream to mark as errored\n */\n markError(streamId: string) {\n this.flushBuffer();\n\n this.sql`\n update cf_ai_chat_stream_metadata \n set status = 'error', completed_at = ${Date.now()} \n where id = ${streamId}\n `;\n this._activeStreamId = null;\n this._activeRequestId = null;\n this._segmentIndex = 0;\n this._isLive = false;\n }\n\n // ── Chunk storage ──────────────────────────────────────────────────\n\n /** Maximum chunk body size before skipping storage (bytes). Prevents SQLite row limit crash. */\n private static CHUNK_MAX_BYTES = 1_800_000;\n\n /**\n * Buffer a stream chunk for batch write to SQLite.\n * Chunks exceeding the row size limit are skipped to prevent crashes.\n * The chunk is still broadcast to live clients (caller handles that),\n * but will be missing from replay on reconnection.\n * @param streamId - The stream this chunk belongs to\n * @param body - The serialized chunk body\n */\n storeChunk(streamId: string, body: string) {\n // Guard against chunks that would exceed SQLite row limit.\n // The chunk is still broadcast to live clients; only replay storage is skipped.\n const bodyBytes = textEncoder.encode(body).byteLength;\n if (bodyBytes > ResumableStream.CHUNK_MAX_BYTES) {\n console.warn(\n `[ResumableStream] Skipping oversized chunk (${bodyBytes} bytes) ` +\n `to prevent SQLite row limit crash. Live clients still receive it.`\n );\n return;\n }\n\n // Force flush if buffer is at max to prevent memory issues\n if (this._chunkBuffer.length >= CHUNK_BUFFER_MAX_SIZE) {\n this.flushBuffer();\n }\n\n // Byte guard: keep a packed segment safely under the SQLite row limit. If\n // the buffer already holds chunks and adding this body would push the\n // segment past the threshold, flush first so this chunk starts a fresh\n // segment. A single large chunk therefore ends up alone and is written\n // unwrapped by flushBuffer (no array-escaping inflation).\n if (\n this._chunkBuffer.length > 0 &&\n this._chunkBufferBytes + bodyBytes > SEGMENT_MAX_BYTES\n ) {\n this.flushBuffer();\n }\n\n this._chunkBuffer.push({ streamId, body });\n this._chunkBufferBytes += bodyBytes;\n\n // Flush when buffer reaches the per-segment chunk threshold\n if (this._chunkBuffer.length >= CHUNK_BUFFER_SIZE) {\n this.flushBuffer();\n }\n }\n\n /**\n * Flush the buffered chunks to SQLite as a single packed row.\n * Uses a lock to prevent concurrent flush operations.\n *\n * The whole buffer becomes one row: a single-chunk segment is stored\n * unwrapped (legacy object format) so a large chunk avoids array-escaping\n * inflation, while a multi-chunk segment stores a JSON array of bodies. This\n * collapses N chunk rows into one, cutting rows written / stored / scanned.\n */\n flushBuffer() {\n if (this._isFlushingChunks || this._chunkBuffer.length === 0) {\n return;\n }\n\n this._isFlushingChunks = true;\n try {\n const chunks = this._chunkBuffer;\n this._chunkBuffer = [];\n this._chunkBufferBytes = 0;\n\n // All chunks in a buffer belong to the same stream: start() flushes\n // before switching streams, so the buffer is never cross-stream.\n const streamId = chunks[0].streamId;\n const segmentBody =\n chunks.length === 1\n ? chunks[0].body\n : JSON.stringify(chunks.map((chunk) => chunk.body));\n\n this.sql`\n insert into cf_ai_chat_stream_chunks (id, stream_id, body, chunk_index, created_at)\n values (${nanoid()}, ${streamId}, ${segmentBody}, ${this._segmentIndex}, ${Date.now()})\n `;\n this._segmentIndex++;\n } finally {\n this._isFlushingChunks = false;\n }\n }\n\n // ── Chunk replay ───────────────────────────────────────────────────\n\n /**\n * Send stored stream chunks to a connection for replay.\n * Chunks are marked with replay: true so the client can batch-apply them.\n *\n * Three outcomes:\n * - **Live stream**: sends chunks + `replayComplete` — client flushes and\n * continues receiving live chunks from the LLM reader.\n * - **Orphaned stream** (restored from SQLite after hibernation, no reader):\n * sends chunks + `done` and completes the stream. The caller should\n * reconstruct and persist the partial message from the stored chunks.\n * - **Completed during replay** (defensive): sends chunks + `done`.\n *\n * All sends use {@link sendIfOpen}, so a WebSocket closing mid-replay\n * does not throw. If the connection drops while iterating chunks the\n * stream is left active so the next reconnect can retry.\n *\n * @param connection - The WebSocket connection\n * @param requestId - The original request ID\n * @returns The stream ID if the stream was orphaned and finalized, null otherwise.\n * When non-null the caller should reconstruct the message from chunks.\n */\n replayChunks(connection: Connection, requestId: string): string | null {\n const streamId = this._activeStreamId;\n if (!streamId) return null;\n\n this.flushBuffer();\n\n const chunks = this.sql<StreamChunk>`\n select * from cf_ai_chat_stream_chunks \n where stream_id = ${streamId} \n order by chunk_index asc\n `;\n\n for (const chunk of chunks || []) {\n for (const body of unpackSegmentBody(chunk.body)) {\n if (\n !sendIfOpen(\n connection,\n JSON.stringify({\n body,\n done: false,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true\n })\n )\n ) {\n // Connection closed mid-replay — leave the stream active so the\n // next reconnect can retry from the start.\n return null;\n }\n }\n }\n\n if (this._activeStreamId !== streamId) {\n // Stream completed between our check above and now — send done.\n // In practice this cannot happen (DO is single-threaded and replay is\n // synchronous), but we guard defensively in case the flow changes.\n sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: true,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true\n })\n );\n return null;\n }\n\n if (!this._isLive) {\n // Orphaned stream — restored from SQLite after hibernation but the\n // LLM ReadableStream reader was lost. No more live chunks will ever\n // arrive, so finalize it: best-effort send done, then mark completed\n // in SQLite. The orphan-cleanup decision is committed regardless of\n // whether this particular connection received the done frame, so the\n // caller can persist the reconstructed message.\n sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: true,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true\n })\n );\n this.complete(streamId);\n return streamId;\n }\n\n // Stream is still active with a live reader — signal that replay is\n // complete so the client can flush accumulated parts to React state.\n // Without this, replayed chunks sit in activeStreamRef unflushed\n // until the next live chunk arrives.\n sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: false,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true,\n replayComplete: true\n })\n );\n return null;\n }\n\n replayCompletedChunksByRequestId(\n connection: Connection,\n requestId: string\n ): boolean {\n this.flushBuffer();\n\n const streams = this.sql<StreamMetadata>`\n select * from cf_ai_chat_stream_metadata\n where request_id = ${requestId}\n and status = 'completed'\n order by created_at desc\n limit 1\n `;\n const stream = streams[0];\n if (!stream) return false;\n\n const chunks = this.sql<StreamChunk>`\n select * from cf_ai_chat_stream_chunks\n where stream_id = ${stream.id}\n order by chunk_index asc\n `;\n\n for (const chunk of chunks || []) {\n for (const body of unpackSegmentBody(chunk.body)) {\n if (\n !sendIfOpen(\n connection,\n JSON.stringify({\n body,\n done: false,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true\n })\n )\n ) {\n return false;\n }\n }\n }\n\n return sendIfOpen(\n connection,\n JSON.stringify({\n body: \"\",\n done: true,\n id: requestId,\n type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,\n replay: true\n })\n );\n }\n\n // ── Restore / cleanup ──────────────────────────────────────────────\n\n /**\n * Restore active stream state if the agent was restarted during streaming.\n * All streams are restored regardless of age — stale cleanup happens\n * lazily in _maybeCleanupOldStreams after recovery has had its chance.\n */\n restore() {\n const activeStreams = this.sql<StreamMetadata>`\n select * from cf_ai_chat_stream_metadata \n where status = 'streaming' \n order by created_at desc \n limit 1\n `;\n\n if (activeStreams && activeStreams.length > 0) {\n const stream = activeStreams[0];\n this._activeStreamId = stream.id;\n this._activeRequestId = stream.request_id;\n\n // Resume the segment row-ordering index past the highest stored value.\n const lastChunk = this.sql<{ max_index: number }>`\n select max(chunk_index) as max_index \n from cf_ai_chat_stream_chunks \n where stream_id = ${this._activeStreamId}\n `;\n this._segmentIndex =\n lastChunk && lastChunk[0]?.max_index != null\n ? lastChunk[0].max_index + 1\n : 0;\n }\n }\n\n /**\n * Clear all stream data (called on chat history clear).\n */\n clearAll() {\n this._chunkBuffer = [];\n this._chunkBufferBytes = 0;\n this.sql`delete from cf_ai_chat_stream_chunks`;\n this.sql`delete from cf_ai_chat_stream_metadata`;\n this._activeStreamId = null;\n this._activeRequestId = null;\n this._segmentIndex = 0;\n }\n\n /**\n * Drop all stream tables (called on destroy).\n */\n destroy() {\n this.flushBuffer();\n this.sql`drop table if exists cf_ai_chat_stream_chunks`;\n this.sql`drop table if exists cf_ai_chat_stream_metadata`;\n this._activeStreamId = null;\n this._activeRequestId = null;\n }\n\n // ── Internal ───────────────────────────────────────────────────────\n\n private _maybeCleanupOldStreams() {\n const now = Date.now();\n if (now - this._lastCleanupTime < CLEANUP_INTERVAL_MS) {\n return;\n }\n this._lastCleanupTime = now;\n\n const cutoff = now - CLEANUP_AGE_THRESHOLD_MS;\n this.sql`\n delete from cf_ai_chat_stream_chunks \n where stream_id in (\n select id from cf_ai_chat_stream_metadata \n where status in ('completed', 'error') and completed_at < ${cutoff}\n )\n `;\n this.sql`\n delete from cf_ai_chat_stream_metadata \n where status in ('completed', 'error') and completed_at < ${cutoff}\n `;\n\n // Clean up abandoned \"streaming\" rows. These are orphaned streams that\n // were never completed or recovered (e.g. non-durable agents that never\n // reconnected). By this point, fiber recovery has already had its chance\n // to claim them — safe to delete.\n this.sql`\n delete from cf_ai_chat_stream_chunks\n where stream_id in (\n select id from cf_ai_chat_stream_metadata\n where status = 'streaming' and created_at < ${cutoff}\n )\n `;\n this.sql`\n delete from cf_ai_chat_stream_metadata\n where status = 'streaming' and created_at < ${cutoff}\n `;\n }\n\n // ── Test helpers (matching old AIChatAgent test API) ────────────────\n\n /**\n * Return the stored chunks for a stream as individual chunk bodies in order,\n * unpacking packed segment rows. The returned `chunk_index` is a running\n * per-chunk sequence (0, 1, 2, …) — stable across calls because rows are\n * append-only — so callers can use it as a monotonic chunk sequence.\n */\n getStreamChunks(\n streamId: string\n ): Array<{ body: string; chunk_index: number }> {\n const rows =\n this.sql<{ body: string }>`\n select body from cf_ai_chat_stream_chunks \n where stream_id = ${streamId} \n order by chunk_index asc\n ` || [];\n const out: Array<{ body: string; chunk_index: number }> = [];\n let index = 0;\n for (const row of rows) {\n for (const body of unpackSegmentBody(row.body)) {\n out.push({ body, chunk_index: index });\n index++;\n }\n }\n return out;\n }\n\n /** @internal For testing only */\n getStreamMetadata(\n streamId: string\n ): { status: string; request_id: string } | null {\n const result = this.sql<{ status: string; request_id: string }>`\n select status, request_id from cf_ai_chat_stream_metadata \n where id = ${streamId}\n `;\n return result && result.length > 0 ? result[0] : null;\n }\n\n /** @internal For testing only */\n getAllStreamMetadata(): Array<{\n id: string;\n status: string;\n request_id: string;\n created_at: number;\n }> {\n return (\n this.sql<{\n id: string;\n status: string;\n request_id: string;\n created_at: number;\n }>`select id, status, request_id, created_at from cf_ai_chat_stream_metadata` ||\n []\n );\n }\n\n /** @internal For testing only */\n insertStaleStream(streamId: string, requestId: string, ageMs: number): void {\n const createdAt = Date.now() - ageMs;\n this.sql`\n insert into cf_ai_chat_stream_metadata (id, request_id, status, created_at)\n values (${streamId}, ${requestId}, 'streaming', ${createdAt})\n `;\n }\n}\n","/**\n * Helpers for building batched SQLite statements that run through the Agent's\n * `sql` tagged template (which interleaves a `?` placeholder between every\n * string fragment). Used to collapse per-row INSERT/DELETE loops into a small\n * number of multi-row statements.\n *\n * SQLite (Durable Object / D1) caps bound parameters at 100 per query, so\n * callers must chunk their inputs to stay within {@link MAX_BOUND_PARAMS}.\n * See https://developers.cloudflare.com/d1/platform/limits/\n */\n\n/** Maximum bound parameters allowed in a single SQLite (DO / D1) query. */\nexport const MAX_BOUND_PARAMS = 100;\n\n/**\n * Attach a self-referential `raw` property so a plain string[] satisfies the\n * TemplateStringsArray shape. `sql` only reads indexed string fragments, so\n * `raw` is never consumed — this just keeps the type system happy.\n */\nfunction asTemplateStringsArray(parts: string[]): TemplateStringsArray {\n (parts as unknown as { raw: readonly string[] }).raw = parts;\n return parts as unknown as TemplateStringsArray;\n}\n\n/**\n * Build a TemplateStringsArray for a single-column `IN (...)` clause. Produces\n * fragments for:\n * `${prefix}(?, ?, ...)`\n *\n * @throws if `count` is less than 1.\n */\nexport function buildInClauseStrings(\n prefix: string,\n count: number\n): TemplateStringsArray {\n if (count < 1) {\n throw new Error(`buildInClauseStrings requires count >= 1 (got ${count})`);\n }\n const parts = new Array<string>(count + 1);\n parts[0] = `${prefix}(`;\n for (let i = 1; i < count; i++) {\n parts[i] = \", \";\n }\n parts[count] = \")\";\n return asTemplateStringsArray(parts);\n}\n","/**\n * Client tool schema handling for the cf_agent_chat protocol.\n *\n * Converts client-provided tool schemas (JSON wire format) into AI SDK\n * tool definitions. These tools have no `execute` function — when the\n * model calls them, the tool call is sent back to the client.\n *\n * Used by both @cloudflare/ai-chat and @cloudflare/think.\n */\n\nimport type { JSONSchema7, Tool, ToolSet } from \"ai\";\nimport { tool, jsonSchema } from \"ai\";\n\n/**\n * Wire-format tool schema sent from the client.\n * Uses `parameters` (JSONSchema7) rather than AI SDK's `inputSchema`\n * because Zod schemas cannot be serialized over the wire.\n */\nexport type ClientToolSchema = {\n /** Unique name for the tool */\n name: string;\n /** Human-readable description of what the tool does */\n description?: Tool[\"description\"];\n /** JSON Schema defining the tool's input parameters */\n parameters?: JSONSchema7;\n};\n\n/**\n * Converts client tool schemas to AI SDK tool format.\n *\n * These tools have no `execute` function — when the AI model calls them,\n * the tool call is sent back to the client for execution.\n *\n * @param clientTools - Array of tool schemas from the client\n * @returns Record of AI SDK tools that can be spread into your tools object\n */\nexport function createToolsFromClientSchemas(\n clientTools?: ClientToolSchema[]\n): ToolSet {\n if (!clientTools || clientTools.length === 0) {\n return {};\n }\n\n const seenNames = new Set<string>();\n for (const t of clientTools) {\n if (seenNames.has(t.name)) {\n console.warn(\n `[createToolsFromClientSchemas] Duplicate tool name \"${t.name}\" found. Later definitions will override earlier ones.`\n );\n }\n seenNames.add(t.name);\n }\n\n return Object.fromEntries(\n clientTools.map((t) => [\n t.name,\n tool({\n description: t.description ?? \"\",\n inputSchema: jsonSchema(t.parameters ?? { type: \"object\" })\n })\n ])\n );\n}\n","/**\n * ContinuationState — shared state container for auto-continuation lifecycle.\n *\n * Tracks pending, deferred, and active continuation state for the\n * tool-result → auto-continue flow. Both AIChatAgent and Think use this\n * to manage which connection/tools/body a continuation turn should use\n * and to coordinate with clients requesting stream resume.\n *\n * The scheduling algorithm (prerequisite chaining, debounce, TurnQueue\n * enrollment) stays in the host — this class only manages the data.\n */\n\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\nimport type { ClientToolSchema } from \"./client-tools\";\n\nconst MSG_STREAM_RESUME_NONE = CHAT_MESSAGE_TYPES.STREAM_RESUME_NONE;\n\nfunction sendIfOpen(\n connection: ContinuationConnection,\n message: string\n): boolean {\n try {\n connection.send(message);\n return true;\n } catch (error) {\n if (isWebSocketClosedSendError(error)) return false;\n throw error;\n }\n}\n\nfunction isWebSocketClosedSendError(error: unknown): boolean {\n return (\n error instanceof TypeError &&\n error.message.includes(\"WebSocket send() after close\")\n );\n}\n\n/**\n * Minimal connection interface for sending WebSocket messages.\n * Matches the Connection type from agents without importing it.\n * Uses a permissive send signature so Connection (which extends\n * WebSocket with its own send overload) is structurally assignable.\n */\nexport interface ContinuationConnection {\n readonly id: string;\n send(message: string): void;\n}\n\nexport interface ContinuationPending<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n connection: TConnection;\n connectionId: string | null;\n requestId: string;\n clientTools?: ClientToolSchema[];\n body?: Record<string, unknown>;\n errorPrefix: string | null;\n prerequisite: Promise<boolean> | null;\n pastCoalesce: boolean;\n}\n\nexport interface ContinuationDeferred<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n connection: TConnection;\n connectionId: string | null;\n clientTools?: ClientToolSchema[];\n body?: Record<string, unknown>;\n errorPrefix: string;\n prerequisite: Promise<boolean> | null;\n}\n\nexport class ContinuationState<\n TConnection extends ContinuationConnection = ContinuationConnection\n> {\n pending: ContinuationPending<TConnection> | null = null;\n deferred: ContinuationDeferred<TConnection> | null = null;\n activeRequestId: string | null = null;\n activeConnectionId: string | null = null;\n awaitingConnections: Map<string, TConnection> = new Map();\n\n /** Clear pending state and awaiting connections (without sending RESUME_NONE). */\n clearPending(): void {\n this.pending = null;\n this.awaitingConnections.clear();\n }\n\n clearDeferred(): void {\n this.deferred = null;\n }\n\n clearAll(): void {\n this.clearPending();\n this.clearDeferred();\n this.activeRequestId = null;\n this.activeConnectionId = null;\n }\n\n /**\n * Mark a connection as no longer available without canceling the\n * continuation it initiated.\n */\n releaseConnection(connectionId: string): void {\n this.awaitingConnections.delete(connectionId);\n if (this.pending?.connectionId === connectionId) {\n this.pending = { ...this.pending, connectionId: null };\n }\n if (this.deferred?.connectionId === connectionId) {\n this.deferred = { ...this.deferred, connectionId: null };\n }\n if (this.activeConnectionId === connectionId) {\n this.activeConnectionId = null;\n }\n }\n\n /**\n * Send STREAM_RESUME_NONE to all connections waiting for a\n * continuation stream to start, then clear the map.\n */\n sendResumeNone(): void {\n const msg = JSON.stringify({ type: MSG_STREAM_RESUME_NONE });\n for (const connection of this.awaitingConnections.values()) {\n sendIfOpen(connection, msg);\n }\n this.awaitingConnections.clear();\n }\n\n /**\n * Flush awaiting connections by notifying each one via the provided\n * callback (typically sends STREAM_RESUMING), then clear.\n */\n flushAwaitingConnections(notify: (conn: TConnection) => void): void {\n for (const connection of this.awaitingConnections.values()) {\n notify(connection);\n }\n this.awaitingConnections.clear();\n }\n\n /**\n * Transition pending → active. Called when the continuation stream\n * actually starts. Moves request/connection IDs to active slots,\n * clears pending fields.\n */\n activatePending(): void {\n if (!this.pending) return;\n this.activeRequestId = this.pending.requestId;\n this.activeConnectionId = this.pending.connectionId;\n this.pending = null;\n }\n\n /**\n * Transition deferred → pending. Called when a continuation turn\n * completes and there's a deferred follow-up waiting.\n *\n * Returns the new pending state (so the host can enqueue the turn),\n * or null if there was nothing deferred.\n */\n activateDeferred(\n generateRequestId: () => string\n ): ContinuationPending<TConnection> | null {\n if (this.pending || !this.deferred) return null;\n\n const d = this.deferred;\n this.deferred = null;\n this.activeRequestId = null;\n this.activeConnectionId = null;\n\n this.pending = {\n connection: d.connection,\n connectionId: d.connectionId,\n requestId: generateRequestId(),\n clientTools: d.clientTools,\n body: d.body,\n errorPrefix: d.errorPrefix,\n prerequisite: d.prerequisite,\n pastCoalesce: false\n };\n\n if (d.connectionId !== null) {\n this.awaitingConnections.set(d.connectionId, d.connection);\n }\n return this.pending;\n }\n}\n","/**\n * AbortRegistry — manages per-request AbortControllers.\n *\n * Shared between AIChatAgent and Think for chat turn cancellation.\n * Each request gets its own AbortController keyed by request ID.\n * Controllers are created lazily on first signal access.\n */\n\nconst NOOP = () => {};\n\nexport class AbortRegistry {\n private controllers = new Map<string, AbortController>();\n\n /**\n * Get or create an AbortController for the given ID and return its signal.\n * Creates the controller lazily on first access.\n */\n getSignal(id: string): AbortSignal | undefined {\n if (typeof id !== \"string\") {\n return undefined;\n }\n\n if (!this.controllers.has(id)) {\n this.controllers.set(id, new AbortController());\n }\n\n return this.controllers.get(id)!.signal;\n }\n\n /**\n * Get the signal for an existing controller without creating one.\n * Returns undefined if no controller exists for this ID.\n */\n getExistingSignal(id: string): AbortSignal | undefined {\n return this.controllers.get(id)?.signal;\n }\n\n /**\n * Cancel a specific request by aborting its controller. Optionally\n * propagate a reason — surfaces as `signal.reason` on the registry's\n * controller and through any `AbortError` it produces downstream.\n */\n cancel(id: string, reason?: unknown): void {\n this.controllers.get(id)?.abort(reason);\n }\n\n /** Remove a controller after the request completes. */\n remove(id: string): void {\n this.controllers.delete(id);\n }\n\n /**\n * Abort all pending requests and clear the registry. Optionally propagate a\n * reason — surfaces as `signal.reason` on each controller and through any\n * `AbortError` it produces downstream, exactly like {@link cancel}.\n */\n destroyAll(reason?: unknown): void {\n for (const controller of this.controllers.values()) {\n controller.abort(reason);\n }\n this.controllers.clear();\n }\n\n /** Check if a controller exists for the given ID. */\n has(id: string): boolean {\n return this.controllers.has(id);\n }\n\n /** Number of tracked controllers. */\n get size(): number {\n return this.controllers.size;\n }\n\n /**\n * Link an external `AbortSignal` to the controller for `id`. When the\n * external signal aborts, the registry's controller is cancelled —\n * propagating the abort reason — exactly the same way an internal\n * cancel would (e.g. via a `chat-request-cancel` WebSocket message).\n *\n * This is the integration point for callers that drive a chat turn\n * programmatically and want to cancel it from outside without knowing\n * the internally-generated request id (e.g. the helper-as-sub-agent\n * pattern, where a parent's `AbortSignal` from the AI SDK tool\n * `execute` needs to land inside a `Think.saveMessages` call running\n * on a child DO).\n *\n * Behavior:\n *\n * - Passing `undefined` is a no-op and returns a no-op detacher, so\n * callers can unconditionally call this with `options?.signal`.\n * - If the external signal is already aborted, the registry's\n * controller is created (if needed) and cancelled synchronously.\n * - Otherwise a one-shot `abort` listener is attached. The returned\n * function detaches it.\n *\n * **Always call the returned detacher in a `finally` block** — the\n * external signal may outlive the request (a parent chat turn that\n * drives many helper turns reuses one signal across all of them) and\n * leaving listeners attached pins closures and grows the listener\n * list on each turn.\n *\n * @returns A detacher function. Call it after the request finishes\n * (success or failure) to remove the abort listener from `signal`.\n */\n linkExternal(id: string, signal: AbortSignal | undefined): () => void {\n if (!signal) return NOOP;\n\n if (signal.aborted) {\n // Ensure the registry controller for `id` exists, then cancel it.\n // Calling getSignal first means an early external abort still\n // produces a controller for downstream observers (`getExistingSignal`)\n // rather than a silently-empty registry.\n this.getSignal(id);\n this.cancel(id, signal.reason);\n return NOOP;\n }\n\n const listener = () => this.cancel(id, signal.reason);\n signal.addEventListener(\"abort\", listener, { once: true });\n return () => signal.removeEventListener(\"abort\", listener);\n }\n}\n","/**\n * Tool State — shared update builders and applicator for tool part state changes.\n *\n * Used by both AIChatAgent and Think to apply tool results and approvals\n * to message parts. Each agent handles find-message, persist, and broadcast\n * in their own way; this module provides the state matching and update logic.\n */\n\n/**\n * Describes an update to apply to a tool part.\n */\nexport type ToolPartUpdate = {\n toolCallId: string;\n matchStates: string[];\n apply: (part: Record<string, unknown>) => Record<string, unknown>;\n};\n\n/**\n * Apply a tool part update to a parts array.\n * Finds the first part matching `update.toolCallId` in one of `update.matchStates`,\n * applies the update immutably, and returns the new parts array with the index.\n *\n * Returns `null` if no matching part was found.\n */\nexport function applyToolUpdate(\n parts: Array<Record<string, unknown>>,\n update: ToolPartUpdate\n): { parts: Array<Record<string, unknown>>; index: number } | null {\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n if (\n \"toolCallId\" in part &&\n part.toolCallId === update.toolCallId &&\n \"state\" in part &&\n update.matchStates.includes(part.state as string)\n ) {\n const updatedParts = [...parts];\n updatedParts[i] = update.apply(part);\n return { parts: updatedParts, index: i };\n }\n }\n return null;\n}\n\n/**\n * Build an update descriptor for applying a tool result.\n *\n * Matches parts in `input-available`, `approval-requested`, or `approval-responded` state.\n * Sets state to `output-available` (with output) or `output-error` (with errorText).\n */\nexport function toolResultUpdate(\n toolCallId: string,\n output: unknown,\n overrideState?: \"output-error\",\n errorText?: string\n): ToolPartUpdate {\n return {\n toolCallId,\n matchStates: [\n \"input-available\",\n \"approval-requested\",\n \"approval-responded\"\n ],\n apply: (part) => ({\n ...part,\n ...(overrideState === \"output-error\"\n ? {\n state: \"output-error\",\n errorText: errorText ?? \"Tool execution denied by user\"\n }\n : { state: \"output-available\", output, preliminary: false })\n })\n };\n}\n\n/**\n * Build an update descriptor for a terminal tool result that belongs to a\n * tool part in a *different* (earlier) assistant message than the one\n * currently being streamed.\n *\n * This is the \"cross-message\" case: an approved server tool executes during a\n * continuation stream, but its tool part lives in the assistant message that\n * originally requested it. `StreamAccumulator` surfaces this as a\n * `cross-message-tool-update` action because the accumulator only owns the\n * current turn's new content and cannot mutate a part from a prior message.\n *\n * Compared to {@link toolResultUpdate} this builder is deliberately more\n * defensive, mirroring the equivalent fallback in `@cloudflare/ai-chat`:\n *\n * - It matches the broad set of pre-terminal **and** terminal states, so a\n * provider that replays the entire prior tool round-trip during a\n * continuation (notably the OpenAI Responses API — issue #1404) still\n * resolves to the same part instead of silently missing it.\n * - It is **first-write-wins**: a chunk arriving for a tool that already holds\n * a terminal result is treated as a replay and the existing output is never\n * overwritten. In that case `apply` returns the *same part reference*, which\n * callers use as an idempotent-no-op signal to skip the durable write and a\n * redundant `MESSAGE_UPDATED` broadcast.\n * - It preserves a streamed `preliminary` flag when one is present, otherwise\n * marks the result final (`preliminary: false`).\n */\nexport function crossMessageToolResultUpdate(\n toolCallId: string,\n updateType: \"output-available\" | \"output-error\",\n output?: unknown,\n errorText?: string,\n preliminary?: boolean\n): ToolPartUpdate {\n return {\n toolCallId,\n matchStates: [\n \"input-streaming\",\n \"input-available\",\n \"approval-requested\",\n \"approval-responded\",\n \"output-available\",\n \"output-error\",\n \"output-denied\"\n ],\n apply: (part) => {\n if (\n part.state === \"output-available\" ||\n part.state === \"output-error\" ||\n part.state === \"output-denied\"\n ) {\n return part;\n }\n if (updateType === \"output-error\") {\n return {\n ...part,\n state: \"output-error\",\n errorText: errorText ?? \"Tool execution failed\"\n };\n }\n return {\n ...part,\n state: \"output-available\",\n output,\n preliminary: preliminary ?? false\n };\n }\n };\n}\n\n/**\n * Build an update descriptor for applying a tool approval.\n *\n * Matches parts in `input-available` or `approval-requested` state.\n * Sets state to `approval-responded` (if approved) or `output-denied` (if denied).\n */\nexport function toolApprovalUpdate(\n toolCallId: string,\n approved: boolean\n): ToolPartUpdate {\n return {\n toolCallId,\n matchStates: [\"input-available\", \"approval-requested\"],\n apply: (part) => ({\n ...part,\n state: approved ? \"approval-responded\" : \"output-denied\",\n approval: {\n ...(part.approval as Record<string, unknown> | undefined),\n approved\n }\n })\n };\n}\n","/**\n * Protocol Message Parser — typed parsing of cf_agent_chat_* WebSocket messages.\n *\n * Parses raw WebSocket messages into a discriminated union of protocol events.\n * Both AIChatAgent and Think can use this instead of manual JSON.parse + type checking.\n */\n\nimport { CHAT_MESSAGE_TYPES } from \"./protocol\";\n\n/**\n * Discriminated union of all incoming chat protocol events.\n *\n * Each agent handles the events it cares about and ignores the rest.\n * Returns `null` for non-JSON messages or unrecognized types.\n */\nexport type ChatProtocolEvent =\n | {\n type: \"chat-request\";\n id: string;\n init: { method?: string; body?: string; [key: string]: unknown };\n }\n | { type: \"clear\" }\n | { type: \"cancel\"; id: string }\n | {\n type: \"tool-result\";\n toolCallId: string;\n toolName: string;\n output: unknown;\n state?: string;\n errorText?: string;\n autoContinue?: boolean;\n clientTools?: Array<{\n name: string;\n description?: string;\n parameters?: unknown;\n }>;\n }\n | {\n type: \"tool-approval\";\n toolCallId: string;\n approved: boolean;\n autoContinue?: boolean;\n }\n | { type: \"stream-resume-request\" }\n | { type: \"stream-resume-ack\"; id: string }\n | { type: \"messages\"; messages: unknown[] };\n\n/**\n * Parse a raw WebSocket message string into a typed protocol event.\n *\n * Returns `null` if the message is not valid JSON or not a recognized\n * protocol message type. Callers should fall through to the user's\n * `onMessage` handler when `null` is returned.\n *\n * @example\n * ```typescript\n * const event = parseProtocolMessage(rawMessage);\n * if (!event) return userOnMessage(connection, rawMessage);\n *\n * switch (event.type) {\n * case \"chat-request\": { ... }\n * case \"clear\": { ... }\n * case \"tool-result\": { ... }\n * }\n * ```\n */\nexport function parseProtocolMessage(raw: string): ChatProtocolEvent | null {\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n return null;\n }\n\n const wireType = data.type as string | undefined;\n if (!wireType) return null;\n\n switch (wireType) {\n case CHAT_MESSAGE_TYPES.USE_CHAT_REQUEST:\n return {\n type: \"chat-request\",\n id: data.id as string,\n init: (data.init as { method?: string; body?: string }) ?? {}\n };\n\n case CHAT_MESSAGE_TYPES.CHAT_CLEAR:\n return { type: \"clear\" };\n\n case CHAT_MESSAGE_TYPES.CHAT_REQUEST_CANCEL:\n return { type: \"cancel\", id: data.id as string };\n\n case CHAT_MESSAGE_TYPES.TOOL_RESULT:\n return {\n type: \"tool-result\",\n toolCallId: data.toolCallId as string,\n toolName: (data.toolName as string) ?? \"\",\n output: data.output,\n state: data.state as string | undefined,\n errorText: data.errorText as string | undefined,\n autoContinue: data.autoContinue as boolean | undefined,\n clientTools: data.clientTools as\n | Array<{\n name: string;\n description?: string;\n parameters?: unknown;\n }>\n | undefined\n };\n\n case CHAT_MESSAGE_TYPES.TOOL_APPROVAL:\n return {\n type: \"tool-approval\",\n toolCallId: data.toolCallId as string,\n approved: data.approved as boolean,\n autoContinue: data.autoContinue as boolean | undefined\n };\n\n case CHAT_MESSAGE_TYPES.STREAM_RESUME_REQUEST:\n return { type: \"stream-resume-request\" };\n\n case CHAT_MESSAGE_TYPES.STREAM_RESUME_ACK:\n return { type: \"stream-resume-ack\", id: data.id as string };\n\n case CHAT_MESSAGE_TYPES.CHAT_MESSAGES:\n return {\n type: \"messages\",\n messages: (data.messages as unknown[]) ?? []\n };\n\n default:\n return null;\n }\n}\n","/**\n * Message reconciliation — pure functions for aligning client messages\n * with server state during persistence.\n *\n * Three strategies applied in order:\n * 1. Merge server-known tool outputs into stale client messages\n * 2. Reconcile assistant IDs (exact match → content-key → toolCallId)\n * 3. Per-message toolCallId dedup for persistence\n */\n\nimport type { UIMessage } from \"ai\";\n\n/**\n * Reconcile incoming client messages against server state.\n *\n * 1. Merges server-known tool outputs into incoming messages that still\n * show stale states (input-available, approval-requested, approval-responded)\n * 2. Reconciles assistant IDs: exact match → content-key match → toolCallId match\n *\n * @param incoming - Messages from the client\n * @param serverMessages - Current server-side messages (source of truth)\n * @param sanitizeForContentKey - Function to sanitize a message before computing\n * its content key (typically strips ephemeral provider metadata)\n * @returns Reconciled messages ready for persistence\n */\nexport function reconcileMessages(\n incoming: UIMessage[],\n serverMessages: readonly UIMessage[],\n sanitizeForContentKey?: (message: UIMessage) => UIMessage\n): UIMessage[] {\n const withMergedToolOutputs = mergeServerToolOutputs(\n incoming,\n serverMessages\n );\n return reconcileAssistantIds(\n withMergedToolOutputs,\n serverMessages,\n sanitizeForContentKey\n );\n}\n\n/**\n * For a single message, resolve its ID by matching toolCallId against server state.\n * Prevents duplicate DB rows when client IDs differ from server IDs.\n * Tool call IDs are unique per conversation, so matching is safe regardless of state.\n */\nexport function resolveToolMergeId(\n message: UIMessage,\n serverMessages: readonly UIMessage[]\n): UIMessage {\n if (message.role !== \"assistant\") {\n return message;\n }\n\n for (const part of message.parts) {\n if (\"toolCallId\" in part && part.toolCallId) {\n const toolCallId = part.toolCallId as string;\n const existing = findMessageByToolCallId(serverMessages, toolCallId);\n if (existing && existing.id !== message.id) {\n return { ...message, id: existing.id };\n }\n }\n }\n\n return message;\n}\n\n/**\n * Content key for assistant messages used for dedup of identical short replies.\n * Returns JSON of sanitized parts, or undefined for non-assistant messages.\n */\nexport function assistantContentKey(\n message: UIMessage,\n sanitize?: (message: UIMessage) => UIMessage\n): string | undefined {\n if (message.role !== \"assistant\") {\n return undefined;\n }\n const sanitized = sanitize ? sanitize(message) : message;\n return JSON.stringify(sanitized.parts);\n}\n\nfunction mergeServerToolOutputs(\n incoming: UIMessage[],\n serverMessages: readonly UIMessage[]\n): UIMessage[] {\n // Index the server's RESOLVED tool parts so a stale client part (still in a\n // pre-output state) can't clobber the server's terminal state on persist.\n // All three terminal states must be protected, not just `output-available`:\n // otherwise a client that hasn't seen the server's `output-error` /\n // `output-denied` yet would persist its stale `input-available` over the\n // resolved result, losing the error/denial.\n const serverResolvedParts = new Map<string, Record<string, unknown>>();\n for (const msg of serverMessages) {\n if (msg.role !== \"assistant\") continue;\n for (const part of msg.parts) {\n const record = part as Record<string, unknown>;\n if (\n \"toolCallId\" in record &&\n \"state\" in record &&\n (record.state === \"output-available\" ||\n record.state === \"output-error\" ||\n record.state === \"output-denied\")\n ) {\n serverResolvedParts.set(record.toolCallId as string, record);\n }\n }\n }\n\n if (serverResolvedParts.size === 0) return incoming;\n\n return incoming.map((msg) => {\n if (msg.role !== \"assistant\") return msg;\n\n let hasChanges = false;\n const updatedParts = msg.parts.map((part) => {\n const record = part as Record<string, unknown>;\n if (\n \"toolCallId\" in record &&\n \"state\" in record &&\n (record.state === \"input-available\" ||\n record.state === \"approval-requested\" ||\n record.state === \"approval-responded\") &&\n serverResolvedParts.has(record.toolCallId as string)\n ) {\n hasChanges = true;\n const server = serverResolvedParts.get(record.toolCallId as string)!;\n // Overlay the server's resolved state, keeping the client part's\n // identity/input. Carry ONLY the result field that belongs to the\n // server's terminal state — so a stray `output` left on an\n // `output-error` part can't ride along and be misread as a result.\n const merged: Record<string, unknown> = {\n ...part,\n state: server.state\n };\n if (server.state === \"output-available\") {\n if (\"output\" in server) merged.output = server.output;\n } else if (server.state === \"output-error\") {\n if (\"errorText\" in server) merged.errorText = server.errorText;\n } else if (server.state === \"output-denied\") {\n if (\"approval\" in server) merged.approval = server.approval;\n }\n return merged;\n }\n return part;\n }) as UIMessage[\"parts\"];\n\n return hasChanges ? { ...msg, parts: updatedParts } : msg;\n });\n}\n\nfunction reconcileAssistantIds(\n incoming: UIMessage[],\n serverMessages: readonly UIMessage[],\n sanitize?: (message: UIMessage) => UIMessage\n): UIMessage[] {\n if (serverMessages.length === 0) return incoming;\n\n const claimedServerIndices = new Set<number>();\n const exactMatchMap = new Map<number, number>();\n\n for (let i = 0; i < incoming.length; i++) {\n const serverIdx = serverMessages.findIndex(\n (sm, si) => !claimedServerIndices.has(si) && sm.id === incoming[i].id\n );\n if (serverIdx !== -1) {\n claimedServerIndices.add(serverIdx);\n exactMatchMap.set(i, serverIdx);\n }\n }\n\n return incoming.map((incomingMessage, incomingIdx) => {\n if (exactMatchMap.has(incomingIdx)) {\n return incomingMessage;\n }\n\n if (\n incomingMessage.role !== \"assistant\" ||\n hasToolCallPart(incomingMessage)\n ) {\n return incomingMessage;\n }\n\n const incomingKey = assistantContentKey(incomingMessage, sanitize);\n if (!incomingKey) {\n return incomingMessage;\n }\n\n for (let i = 0; i < serverMessages.length; i++) {\n if (claimedServerIndices.has(i)) continue;\n\n const serverMessage = serverMessages[i];\n if (\n serverMessage.role !== \"assistant\" ||\n hasToolCallPart(serverMessage)\n ) {\n continue;\n }\n\n if (assistantContentKey(serverMessage, sanitize) === incomingKey) {\n claimedServerIndices.add(i);\n return { ...incomingMessage, id: serverMessage.id };\n }\n }\n\n return incomingMessage;\n });\n}\n\nfunction hasToolCallPart(message: UIMessage): boolean {\n return message.parts.some((part) => \"toolCallId\" in part);\n}\n\nfunction findMessageByToolCallId(\n messages: readonly UIMessage[],\n toolCallId: string\n): UIMessage | undefined {\n for (const msg of messages) {\n if (msg.role !== \"assistant\") continue;\n for (const part of msg.parts) {\n if (\"toolCallId\" in part && part.toolCallId === toolCallId) {\n return msg;\n }\n }\n }\n return undefined;\n}\n","import type { UIMessage } from \"ai\";\nimport type { ClientToolSchema } from \"./client-tools\";\n\nexport type ChatFiberSnapshot<Kind extends string = string> = {\n kind: Kind;\n version: 1;\n requestId: string;\n recoveryRootRequestId?: string;\n continuation: boolean;\n latestMessageId?: string;\n latestMessageRole?: string;\n latestUserMessageId?: string;\n startedAt: number;\n lastBody?: Record<string, unknown>;\n lastClientTools?: ClientToolSchema[];\n};\n\nexport function createChatFiberSnapshot<Kind extends string>({\n kind,\n requestId,\n recoveryRootRequestId,\n continuation,\n messages,\n lastBody,\n lastClientTools\n}: {\n kind: Kind;\n requestId: string;\n recoveryRootRequestId?: string;\n continuation: boolean;\n messages: UIMessage[];\n lastBody?: Record<string, unknown>;\n lastClientTools?: ClientToolSchema[];\n}): ChatFiberSnapshot<Kind> {\n const latestMessage =\n messages.length > 0 ? messages[messages.length - 1] : undefined;\n let latestUser: UIMessage | undefined;\n\n for (let index = messages.length - 1; index >= 0; index--) {\n if (messages[index].role === \"user\") {\n latestUser = messages[index];\n break;\n }\n }\n\n return {\n kind,\n version: 1,\n requestId,\n recoveryRootRequestId,\n continuation,\n latestMessageId: latestMessage?.id,\n latestMessageRole: latestMessage?.role,\n latestUserMessageId: latestUser?.id,\n startedAt: Date.now(),\n lastBody,\n lastClientTools\n };\n}\n\nexport function wrapChatFiberSnapshot<Kind extends string>(\n key: string,\n snapshot: ChatFiberSnapshot<Kind>,\n user: unknown | null\n): Record<string, unknown> {\n return { [key]: snapshot, user };\n}\n\nexport function unwrapChatFiberSnapshot<Kind extends string>(\n key: string,\n value: unknown,\n expectedKind?: Kind\n): {\n snapshot: ChatFiberSnapshot<Kind> | null;\n user: unknown | null;\n} {\n if (typeof value !== \"object\" || value === null || !(key in value)) {\n return { snapshot: null, user: value };\n }\n\n const envelope = value as Record<string, unknown>;\n const snapshot = envelope[key];\n if (typeof snapshot !== \"object\" || snapshot === null) {\n return { snapshot: null, user: value };\n }\n const candidate = snapshot as Record<string, unknown>;\n if (\n candidate.version !== 1 ||\n (expectedKind !== undefined && candidate.kind !== expectedKind) ||\n typeof candidate.requestId !== \"string\" ||\n typeof candidate.continuation !== \"boolean\"\n ) {\n return { snapshot: null, user: value };\n }\n\n return {\n snapshot: snapshot as ChatFiberSnapshot<Kind>,\n user: envelope.user ?? null\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"durable objects"
|
|
10
10
|
],
|
|
11
11
|
"type": "module",
|
|
12
|
-
"version": "0.14.
|
|
12
|
+
"version": "0.14.4",
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"repository": {
|
|
15
15
|
"directory": "packages/agents",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"@x402/evm": "^2.14.0",
|
|
50
50
|
"ai": "^6.0.196",
|
|
51
51
|
"chat": "^4.30.0",
|
|
52
|
+
"glob": "^11.1.0",
|
|
52
53
|
"react": "^19.2.7",
|
|
53
54
|
"vitest-browser-react": "^2.2.0",
|
|
54
55
|
"zod": "^4.4.3"
|
|
@@ -259,4 +260,4 @@
|
|
|
259
260
|
"test:x402": "vitest --project x402",
|
|
260
261
|
"test:e2e": "vitest --run -c src/e2e-tests/vitest.config.ts"
|
|
261
262
|
}
|
|
262
|
-
}
|
|
263
|
+
}
|