@agents24/react 0.2.11 → 0.2.13
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/compatibility.json +5 -5
- package/dist/index.js +247 -116
- package/dist/index.js.map +1 -1
- package/dist/model.d.ts +2 -0
- package/dist/thread-message-queue.d.ts +26 -1
- package/package.json +2 -2
package/compatibility.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractHash": "
|
|
2
|
+
"contractHash": "0394b689ce7604b444b11c8792050fae0c1da4233d6b318d22832c3b89516a4a",
|
|
3
3
|
"contractVersion": "0.3.0",
|
|
4
4
|
"corpus": "conformance/v1/corpus.json",
|
|
5
5
|
"generatedBy": "@agents24/sdk-contract",
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
"reactDom": ">=19 <20"
|
|
16
16
|
},
|
|
17
17
|
"@agents24/client": {
|
|
18
|
-
"contract": "
|
|
18
|
+
"contract": "0394b689ce7604b444b11c8792050fae0c1da4233d6b318d22832c3b89516a4a",
|
|
19
19
|
"runtime": "run-stream.v3"
|
|
20
20
|
},
|
|
21
21
|
"@agents24/node": {
|
|
22
|
-
"contract": "
|
|
22
|
+
"contract": "0394b689ce7604b444b11c8792050fae0c1da4233d6b318d22832c3b89516a4a",
|
|
23
23
|
"node": "^22.0.0 || ^24.0.0"
|
|
24
24
|
},
|
|
25
25
|
"@agents24/react": {
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"react": ">=19 <20"
|
|
28
28
|
},
|
|
29
29
|
"@talmudpedia/runtime-sdk": {
|
|
30
|
-
"contract": "
|
|
30
|
+
"contract": "0394b689ce7604b444b11c8792050fae0c1da4233d6b318d22832c3b89516a4a",
|
|
31
31
|
"protocolOwner": "@agents24/client"
|
|
32
32
|
},
|
|
33
33
|
"agents24": {
|
|
34
|
-
"contract": "
|
|
34
|
+
"contract": "0394b689ce7604b444b11c8792050fae0c1da4233d6b318d22832c3b89516a4a",
|
|
35
35
|
"python": ">=3.10"
|
|
36
36
|
}
|
|
37
37
|
},
|
package/dist/index.js
CHANGED
|
@@ -379,7 +379,8 @@ function threadDetailToMessages(detail) {
|
|
|
379
379
|
runId,
|
|
380
380
|
content: assistantText,
|
|
381
381
|
createdAt: text(turn.completed_at) || createdAt,
|
|
382
|
-
|
|
382
|
+
responseCompletion: item.response_completion,
|
|
383
|
+
isFinal: Boolean(item.response_completion) || !["queued", "running", "paused", "cancelling"].includes(String(turn.status || "").toLowerCase()),
|
|
383
384
|
feedback,
|
|
384
385
|
parts: assistantParts,
|
|
385
386
|
messageIndex: typeof turn.turn_index === "number" ? turn.turn_index : index
|
|
@@ -392,12 +393,14 @@ function threadTimelineToMessages(items, detail) {
|
|
|
392
393
|
const feedbackByRun = new Map((detail?.turns || []).flatMap((turn) => turn.run_id && turn.feedback ? [[turn.run_id, turn.feedback]] : []));
|
|
393
394
|
return items.map((item) => ({
|
|
394
395
|
id: item.id,
|
|
396
|
+
clientSubmissionId: item.client_submission_id || void 0,
|
|
395
397
|
role: item.role,
|
|
396
398
|
runId: item.run_id,
|
|
397
399
|
segmentIndex: item.segment_index,
|
|
398
400
|
content: item.text,
|
|
399
401
|
createdAt: item.created_at,
|
|
400
402
|
isFinal: item.status === "completed" || item.status === "delivered",
|
|
403
|
+
responseCompletion: item.response_completion,
|
|
401
404
|
parts: item.role === "assistant" ? partsFromResponseBlocks(item.response_blocks, item.text, `${item.id}-text`) : item.text ? [{ id: `${item.id}-text`, type: "text", kind: "text", text: item.text }] : [],
|
|
402
405
|
attachments: item.role === "user" ? item.attachment_ids.map((id) => {
|
|
403
406
|
const descriptor = item.attachments?.find((attachment) => attachment.id === id);
|
|
@@ -602,7 +605,7 @@ function appendStableStreamingTurn(messages, userMessage, assistantMessage) {
|
|
|
602
605
|
}
|
|
603
606
|
function preserveMountedMessageIds(canonical, current) {
|
|
604
607
|
return canonical.map((message) => {
|
|
605
|
-
const mounted = current.find((item) => item.role === message.role && (item.durableId === message.id || item.id === message.id || message.role === "assistant" && message.segmentIndex === 1 && item.runId && item.runId === message.runId && !item.durableId));
|
|
608
|
+
const mounted = current.find((item) => item.role === message.role && (item.durableId === message.id || item.id === message.id || Boolean(message.clientSubmissionId && item.clientSubmissionId === message.clientSubmissionId) || message.role === "assistant" && message.segmentIndex === 1 && item.runId && item.runId === message.runId && !item.durableId));
|
|
606
609
|
return mounted && mounted.id !== message.id ? { ...message, id: mounted.id, durableId: message.id } : message;
|
|
607
610
|
});
|
|
608
611
|
}
|
|
@@ -612,15 +615,15 @@ function reconcileThreadTimeline(current, canonical, activeRunId, streamingMessa
|
|
|
612
615
|
const retained = current.filter((message) => !canonicalIds.has(message.id) && (message.isFinal || activeRunId && (!message.runId || message.runId === activeRunId)));
|
|
613
616
|
const reconciled = mounted.map((message) => {
|
|
614
617
|
const live = current.find((item) => item.id === message.id);
|
|
615
|
-
return activeRunId && live?.role === "assistant" && live.runId === activeRunId && (live.id === streamingMessageId || !live.isFinal) ? { ...message, ...live, durableId: message.durableId || live.durableId } : message;
|
|
618
|
+
return activeRunId && live?.role === "assistant" && live.runId === activeRunId && (live.id === streamingMessageId || !live.isFinal || live.responseCompletion) ? message.responseCompletion && !live.responseCompletion ? { ...message, id: live.id, durableId: message.durableId || live.durableId } : { ...message, ...live, durableId: message.durableId || live.durableId } : message;
|
|
616
619
|
});
|
|
617
620
|
return [...retained, ...reconciled];
|
|
618
621
|
}
|
|
619
622
|
function promoteThreadMessage(current, message, runId) {
|
|
620
623
|
const promoted = { ...message, runId, isFinal: true };
|
|
621
|
-
const index = current.findIndex((item) => item.id === message.id || item.durableId === message.id);
|
|
622
|
-
if (index < 0) return [...current, promoted];
|
|
623
|
-
return current.map((item, itemIndex) => itemIndex === index ? { ...item, ...promoted, id: item.id, durableId: item.id === message.id ? item.durableId : message.id } : item);
|
|
624
|
+
const index = current.findIndex((item) => item.id === message.id || item.durableId === message.id || Boolean(message.clientSubmissionId && item.clientSubmissionId === message.clientSubmissionId));
|
|
625
|
+
if (index < 0) return [...current, message.clientSubmissionId ? { ...promoted, id: message.clientSubmissionId, durableId: message.id } : promoted];
|
|
626
|
+
return current.map((item, itemIndex) => itemIndex === index ? { ...item, ...promoted, id: item.id, durableId: item.id === message.id ? item.durableId : message.id } : message.clientSubmissionId && item.role === "assistant" && item.id === `${current[index].id}:assistant` ? { ...item, runId } : item);
|
|
624
627
|
}
|
|
625
628
|
function upsertStableAssistantMessage(messages, input) {
|
|
626
629
|
const next = [...messages];
|
|
@@ -659,7 +662,7 @@ function reconcileModelTurnParts(parts, event) {
|
|
|
659
662
|
const terminal = eventName === "run.completed" || eventName === "run.failed" || eventName === "run.cancelled";
|
|
660
663
|
const modelTurnId = typeof payload.model_turn_id === "string" ? payload.model_turn_id : null;
|
|
661
664
|
let next = parts;
|
|
662
|
-
if (eventName === "run.snapshot" || terminal) {
|
|
665
|
+
if (eventName === "run.snapshot" || eventName === "assistant.completed" || terminal) {
|
|
663
666
|
next = next.filter((part) => part.kind !== "model-turn");
|
|
664
667
|
} else if (eventName === "assistant.delta" && typeof payload.content === "string" && payload.content) {
|
|
665
668
|
next = next.filter((part) => part.kind !== "model-turn" || part.turnKind !== "agent");
|
|
@@ -677,17 +680,20 @@ function applyRuntimeEvent(state, event) {
|
|
|
677
680
|
let optimisticIndex = -1;
|
|
678
681
|
for (let index = state.messages.length - 1; index >= 0; index -= 1) {
|
|
679
682
|
const message = state.messages[index];
|
|
680
|
-
if (message.role === "user" && !message.runId && !message.durableId) {
|
|
683
|
+
if (message.role === "user" && (event.payload.client_submission_id ? message.clientSubmissionId === event.payload.client_submission_id : !message.runId && !message.durableId)) {
|
|
681
684
|
optimisticIndex = index;
|
|
682
685
|
break;
|
|
683
686
|
}
|
|
684
687
|
}
|
|
685
688
|
if (optimisticIndex >= 0) {
|
|
686
|
-
|
|
689
|
+
const assistantId = event.payload.client_submission_id ? `${state.messages[optimisticIndex].id}:assistant` : state.streamingMessageId;
|
|
690
|
+
state = { ...state, streamingMessageId: assistantId, messages: state.messages.map((message, index) => index === optimisticIndex ? { ...message, durableId: messageId2, runId: event.run_id } : message.role === "assistant" && message.id === assistantId ? { ...message, runId: event.run_id } : message) };
|
|
687
691
|
}
|
|
688
692
|
}
|
|
689
693
|
const projection = assistantProjectionFromEvent(event);
|
|
690
694
|
const eventName = String(event.event || "");
|
|
695
|
+
const completion = eventName === "assistant.completed" ? event.payload : event.payload?.response_completion;
|
|
696
|
+
const responseCompleted = Boolean(completion?.completed_at);
|
|
691
697
|
const runId = state.activeRunId && event.event !== "run.accepted" ? state.activeRunId : event.run_id || state.activeRunId;
|
|
692
698
|
const segmentId = typeof event.payload?.segment_id === "string" ? event.payload.segment_id : null;
|
|
693
699
|
const distinctSegment = Boolean(segmentId && segmentId !== `${runId}:0`);
|
|
@@ -697,6 +703,7 @@ function applyRuntimeEvent(state, event) {
|
|
|
697
703
|
const suspension = eventName === "run.waiting" ? "waiting" : eventName === "run.interrupted" ? "interrupted" : null;
|
|
698
704
|
const cancelling = eventName === "run.cancelling" || String(event.payload?.status || "") === "cancelling";
|
|
699
705
|
const existing = state.messages.find((message) => message.role === "assistant" && message.id === messageId) || (!distinctSegment ? state.messages.find((message) => message.role === "assistant" && Boolean(runId && message.runId === runId)) : void 0);
|
|
706
|
+
const sealedAfterTerminal = responseCompleted && !state.activeRunId && ["completed", "failed", "cancelled"].includes(state.runState) && existing?.runId === runId;
|
|
700
707
|
const payload = event.payload || {};
|
|
701
708
|
const eventThreadId = typeof payload.thread_id === "string" && payload.thread_id.trim() ? payload.thread_id.trim() : null;
|
|
702
709
|
const contextApplies = (!state.activeThreadId || !eventThreadId || eventThreadId === state.activeThreadId) && (!state.activeRunId || !runId || runId === state.activeRunId);
|
|
@@ -715,13 +722,13 @@ function applyRuntimeEvent(state, event) {
|
|
|
715
722
|
const projectedParts = hasCanonicalResponseBlocks ? projection.parts : delta ? appendAssistantDelta(
|
|
716
723
|
existing?.parts || [],
|
|
717
724
|
delta,
|
|
718
|
-
`${runId || "run"}-assistant-text-${event.seq}`
|
|
725
|
+
typeof payload.block_id === "string" ? payload.block_id : `${runId || "run"}-assistant-text-${event.seq}`
|
|
719
726
|
) : projection.parts.length ? upsertResponseBlockParts(existing?.parts || [], projection.parts) : existing?.parts || [];
|
|
720
727
|
const lifecycleParts = reconcileModelTurnParts(projectedParts, event);
|
|
721
728
|
const compressionPart = contextApplies && explicitCompression?.active ? contextCompressionPartFromEvent(event, lifecycleParts) : null;
|
|
722
729
|
const parts = compressionPart ? upsertResponseBlockParts(lifecycleParts, [compressionPart]) : lifecycleParts;
|
|
723
730
|
const activeThreadId = eventThreadId || state.activeThreadId;
|
|
724
|
-
|
|
731
|
+
let messages = upsertStableAssistantMessage(state.messages, {
|
|
725
732
|
messageId,
|
|
726
733
|
runId,
|
|
727
734
|
strictMessageId: distinctSegment,
|
|
@@ -731,7 +738,8 @@ function applyRuntimeEvent(state, event) {
|
|
|
731
738
|
runId,
|
|
732
739
|
content,
|
|
733
740
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
734
|
-
isFinal: terminal,
|
|
741
|
+
isFinal: terminal || responseCompleted,
|
|
742
|
+
responseCompletion: completion,
|
|
735
743
|
parts
|
|
736
744
|
}),
|
|
737
745
|
update: (message) => ({
|
|
@@ -739,9 +747,20 @@ function applyRuntimeEvent(state, event) {
|
|
|
739
747
|
runId: runId ?? message.runId,
|
|
740
748
|
content,
|
|
741
749
|
parts,
|
|
742
|
-
isFinal: terminal || message.isFinal
|
|
750
|
+
isFinal: terminal || responseCompleted || message.isFinal,
|
|
751
|
+
responseCompletion: completion || message.responseCompletion
|
|
743
752
|
})
|
|
744
753
|
});
|
|
754
|
+
if (responseCompleted) messages = messages.map((message) => {
|
|
755
|
+
if (message.role !== "assistant" || message.runId !== runId) return message;
|
|
756
|
+
const segment = completion?.segments.find((item) => item.message_id === message.durableId || item.message_id === message.id || message.id.endsWith(`-assistant-${item.segment_id}`)) || (!message.segmentIndex || message.segmentIndex === 1 ? completion?.segments[0] : void 0);
|
|
757
|
+
return {
|
|
758
|
+
...message,
|
|
759
|
+
isFinal: true,
|
|
760
|
+
responseCompletion: completion,
|
|
761
|
+
...segment ? { durableId: segment.message_id } : {}
|
|
762
|
+
};
|
|
763
|
+
});
|
|
745
764
|
const threads = activeThreadId ? state.threads.some((thread) => thread.id === activeThreadId) ? state.threads.map((thread) => thread.id === activeThreadId ? {
|
|
746
765
|
...thread,
|
|
747
766
|
messages,
|
|
@@ -763,15 +782,15 @@ function applyRuntimeEvent(state, event) {
|
|
|
763
782
|
return {
|
|
764
783
|
...state,
|
|
765
784
|
activeThreadId,
|
|
766
|
-
activeRunId: terminal ? null : runId,
|
|
785
|
+
activeRunId: terminal || sealedAfterTerminal ? null : runId,
|
|
767
786
|
cursor: typeof event.seq === "number" ? event.seq : state.cursor,
|
|
768
787
|
messages,
|
|
769
788
|
threads,
|
|
770
789
|
contextWindow,
|
|
771
790
|
contextCompression,
|
|
772
791
|
contextCompressionRevision: hasLiveCompressionUpdate ? state.contextCompressionRevision + 1 : state.contextCompressionRevision,
|
|
773
|
-
runState: suspension || (paused ? "paused" : cancelling ? "cancelling" : terminal ? eventName === "run.completed" ? "completed" : eventName === "run.cancelled" ? "cancelled" : "failed" : "streaming"),
|
|
774
|
-
streamingMessageId: terminal || paused || suspension ? null : messageId
|
|
792
|
+
runState: sealedAfterTerminal ? state.runState : suspension || (paused ? "paused" : cancelling ? "cancelling" : terminal ? eventName === "run.completed" ? "completed" : eventName === "run.cancelled" ? "cancelled" : "failed" : "streaming"),
|
|
793
|
+
streamingMessageId: terminal || paused || suspension || responseCompleted || existing?.responseCompletion ? null : messageId
|
|
775
794
|
};
|
|
776
795
|
}
|
|
777
796
|
function applyRuntimeEvents(state, events) {
|
|
@@ -808,7 +827,16 @@ function applyThreadSummaryEvent(threads, event) {
|
|
|
808
827
|
|
|
809
828
|
// src/thread-message-queue.ts
|
|
810
829
|
import * as React2 from "react";
|
|
811
|
-
function
|
|
830
|
+
async function retrySubmissionAdmission(admit) {
|
|
831
|
+
try {
|
|
832
|
+
return await admit();
|
|
833
|
+
} catch (error) {
|
|
834
|
+
const failure = error;
|
|
835
|
+
if (failure?.kind !== "network" && failure?.name !== "HttpRequestTimeoutError" && !(error instanceof TypeError)) throw error;
|
|
836
|
+
return admit();
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic, onDiscard }) {
|
|
812
840
|
const [pendingMessages, setPendingMessages] = React2.useState([]);
|
|
813
841
|
const [messageQueue, setMessageQueue] = React2.useState(null);
|
|
814
842
|
const pendingRef = React2.useRef(/* @__PURE__ */ new Map());
|
|
@@ -821,15 +849,19 @@ function useThreadMessageQueue({ threadId, transport, onPromoted }) {
|
|
|
821
849
|
threadRef.current = threadId;
|
|
822
850
|
transportRef.current = transport;
|
|
823
851
|
onPromotedRef.current = onPromoted;
|
|
852
|
+
const callbacksRef = React2.useRef({ onOptimistic, onDiscard });
|
|
853
|
+
callbacksRef.current = { onOptimistic, onDiscard };
|
|
854
|
+
const knownMessage = (id, submissionId) => pendingRef.current.get(id) || (submissionId ? [...pendingRef.current.values()].find((item) => item.client_submission_id === submissionId) : void 0);
|
|
824
855
|
const hydrate = React2.useCallback((page, selectedThreadId) => {
|
|
825
856
|
if (threadRef.current !== selectedThreadId) return;
|
|
826
857
|
const pending = page.pending.filter((message) => !promotedRef.current.has(message.id)).map((message) => {
|
|
827
|
-
const known =
|
|
858
|
+
const known = knownMessage(message.id, message.client_submission_id);
|
|
828
859
|
if (message.attachments?.length || !known?.attachments?.length) return message;
|
|
829
860
|
return { ...message, attachments: known.attachments.filter((attachment) => message.attachment_ids.includes(attachment.id)) };
|
|
830
861
|
});
|
|
831
862
|
for (const message of pending) pendingRef.current.set(message.id, message);
|
|
832
|
-
|
|
863
|
+
const optimistic = [...pendingRef.current.values()].filter((message) => message.thread_id === selectedThreadId && message.revision === 0 && message.optimisticQueued && !pending.some((item) => item.client_submission_id === message.client_submission_id));
|
|
864
|
+
setPendingMessages([...pending, ...optimistic]);
|
|
833
865
|
setMessageQueue(page.queue);
|
|
834
866
|
}, []);
|
|
835
867
|
const refresh = React2.useCallback(async (selectedThreadId = threadRef.current) => {
|
|
@@ -839,9 +871,10 @@ function useThreadMessageQueue({ threadId, transport, onPromoted }) {
|
|
|
839
871
|
if (version !== versionRef.current || threadRef.current !== selectedThreadId) return null;
|
|
840
872
|
for (const item of page.items) {
|
|
841
873
|
if (item.role === "user" && item.status === "delivered" && item.run_id) {
|
|
842
|
-
const pending =
|
|
874
|
+
const pending = knownMessage(item.id, item.client_submission_id);
|
|
843
875
|
if (pending && !promotedRef.current.has(item.id)) {
|
|
844
876
|
promotedRef.current.add(item.id);
|
|
877
|
+
pendingRef.current.delete(pending.id);
|
|
845
878
|
pendingRef.current.delete(item.id);
|
|
846
879
|
onPromotedRef.current({
|
|
847
880
|
...pending,
|
|
@@ -857,56 +890,98 @@ function useThreadMessageQueue({ threadId, transport, onPromoted }) {
|
|
|
857
890
|
const acceptNotice = React2.useCallback((notice) => {
|
|
858
891
|
if (notice.cursor && notice.cursor <= cursorRef.current) return false;
|
|
859
892
|
if (notice.cursor) cursorRef.current = notice.cursor;
|
|
860
|
-
const pending =
|
|
861
|
-
if (notice.status === "delivered" && notice.runId && pending && !promotedRef.current.has(
|
|
862
|
-
promotedRef.current.add(
|
|
893
|
+
const pending = knownMessage(notice.messageId, notice.clientSubmissionId);
|
|
894
|
+
if (notice.status === "delivered" && notice.runId && pending && !promotedRef.current.has(notice.messageId)) {
|
|
895
|
+
promotedRef.current.add(notice.messageId);
|
|
863
896
|
pendingRef.current.delete(pending.id);
|
|
864
897
|
versionRef.current += 1;
|
|
865
898
|
setPendingMessages((current) => current.filter((message) => message.id !== pending.id));
|
|
866
|
-
onPromotedRef.current(pending, notice.runId);
|
|
899
|
+
onPromotedRef.current({ ...pending, id: notice.messageId, run_id: notice.runId, status: "delivered" }, notice.runId);
|
|
867
900
|
}
|
|
868
901
|
if (notice.status === "removed") pendingRef.current.delete(notice.messageId);
|
|
869
902
|
void refresh().catch(() => void 0);
|
|
870
903
|
return true;
|
|
871
904
|
}, [refresh]);
|
|
905
|
+
const beginSubmission = React2.useCallback((input, queued = false) => {
|
|
906
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
907
|
+
const message = {
|
|
908
|
+
id: input.clientSubmissionId,
|
|
909
|
+
client_submission_id: input.clientSubmissionId,
|
|
910
|
+
thread_id: threadRef.current || "",
|
|
911
|
+
sequence: Number.MAX_SAFE_INTEGER,
|
|
912
|
+
run_id: null,
|
|
913
|
+
turn_id: null,
|
|
914
|
+
segment_index: 0,
|
|
915
|
+
role: "user",
|
|
916
|
+
status: "queued",
|
|
917
|
+
requested_mode: input.mode || "queue",
|
|
918
|
+
text: input.input,
|
|
919
|
+
attachment_ids: input.attachmentIds || input.attachments?.map((item) => item.id) || [],
|
|
920
|
+
attachments: input.attachments,
|
|
921
|
+
response_blocks: [],
|
|
922
|
+
revision: 0,
|
|
923
|
+
created_at: now,
|
|
924
|
+
updated_at: now
|
|
925
|
+
};
|
|
926
|
+
pendingRef.current.set(message.id, { ...message, optimisticQueued: queued });
|
|
927
|
+
versionRef.current += 1;
|
|
928
|
+
if (queued) setPendingMessages((current) => [...current, message]);
|
|
929
|
+
else callbacksRef.current.onOptimistic?.(message, Boolean(input.uploading));
|
|
930
|
+
return message;
|
|
931
|
+
}, []);
|
|
932
|
+
const discardSubmission = React2.useCallback((message) => {
|
|
933
|
+
if (!pendingRef.current.has(message.id)) return;
|
|
934
|
+
pendingRef.current.delete(message.id);
|
|
935
|
+
callbacksRef.current.onDiscard?.(message);
|
|
936
|
+
versionRef.current += 1;
|
|
937
|
+
setPendingMessages((current) => current.filter((item) => item.client_submission_id !== message.client_submission_id));
|
|
938
|
+
}, []);
|
|
872
939
|
const submit = React2.useCallback(async (input) => {
|
|
873
940
|
const selectedThreadId = threadRef.current;
|
|
874
941
|
if (!selectedThreadId) throw new Error("No active Agent thread is available.");
|
|
875
|
-
const { attachments, ...submission } = input;
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
942
|
+
const { queued, prepare, attachments, uploading, ...submission } = input;
|
|
943
|
+
const optimistic = beginSubmission({
|
|
944
|
+
...submission,
|
|
945
|
+
attachments,
|
|
946
|
+
uploading,
|
|
947
|
+
clientSubmissionId: submission.clientSubmissionId || submission.idempotencyKey
|
|
948
|
+
}, queued);
|
|
949
|
+
try {
|
|
950
|
+
const prepared = prepare ? await prepare() : {};
|
|
951
|
+
if (threadRef.current === selectedThreadId) pendingRef.current.set(optimistic.id, { ...optimistic, optimisticQueued: queued, attachment_ids: prepared.attachmentIds || optimistic.attachment_ids, attachments: prepared.attachments || attachments });
|
|
952
|
+
const request = { ...submission, ...prepared, clientSubmissionId: optimistic.client_submission_id, threadId: selectedThreadId };
|
|
953
|
+
const result = await retrySubmissionAdmission(() => transportRef.current.submit(request));
|
|
954
|
+
if (threadRef.current !== selectedThreadId) return result;
|
|
955
|
+
versionRef.current += 1;
|
|
956
|
+
const bound = {
|
|
957
|
+
...optimistic,
|
|
882
958
|
id: result.message_id,
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
segment_index: null,
|
|
888
|
-
role: "user",
|
|
959
|
+
client_submission_id: result.client_submission_id || optimistic.client_submission_id,
|
|
960
|
+
attachment_ids: request.attachmentIds || [],
|
|
961
|
+
attachments: prepared.attachments || attachments,
|
|
962
|
+
run_id: result.run_id,
|
|
889
963
|
status: result.status,
|
|
890
|
-
|
|
891
|
-
text: input.input,
|
|
892
|
-
attachment_ids: input.attachmentIds || [],
|
|
893
|
-
attachments,
|
|
894
|
-
response_blocks: [],
|
|
895
|
-
revision: 1,
|
|
896
|
-
created_at: now,
|
|
897
|
-
updated_at: now
|
|
964
|
+
revision: 1
|
|
898
965
|
};
|
|
899
|
-
pendingRef.current.
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
966
|
+
pendingRef.current.delete(optimistic.id);
|
|
967
|
+
if (result.run_id) {
|
|
968
|
+
setPendingMessages((current) => current.filter((item) => item.client_submission_id !== bound.client_submission_id));
|
|
969
|
+
if (!promotedRef.current.has(bound.id)) {
|
|
970
|
+
promotedRef.current.add(bound.id);
|
|
971
|
+
onPromotedRef.current(bound, result.run_id);
|
|
972
|
+
}
|
|
973
|
+
} else if (!promotedRef.current.has(bound.id)) {
|
|
974
|
+
if (!queued) callbacksRef.current.onDiscard?.(optimistic);
|
|
975
|
+
pendingRef.current.set(bound.id, bound);
|
|
976
|
+
setPendingMessages((current) => [...current.filter((item) => item.client_submission_id !== bound.client_submission_id), bound]);
|
|
977
|
+
}
|
|
978
|
+
void refresh().catch(() => void 0);
|
|
979
|
+
return result;
|
|
980
|
+
} catch (error) {
|
|
981
|
+
if (threadRef.current === selectedThreadId) discardSubmission(optimistic);
|
|
982
|
+
throw error;
|
|
906
983
|
}
|
|
907
|
-
|
|
908
|
-
return result;
|
|
909
|
-
}, [refresh]);
|
|
984
|
+
}, [beginSubmission, discardSubmission, refresh]);
|
|
910
985
|
const edit = React2.useCallback(async (message, input) => {
|
|
911
986
|
const updated = await transportRef.current.edit(message, input);
|
|
912
987
|
versionRef.current += 1;
|
|
@@ -949,7 +1024,51 @@ function useThreadMessageQueue({ threadId, transport, onPromoted }) {
|
|
|
949
1024
|
}, 3e3);
|
|
950
1025
|
return () => clock.clearInterval(timer);
|
|
951
1026
|
}, [threadId, pendingMessages.length, messageQueue?.paused, refresh]);
|
|
952
|
-
return { pendingMessages, messageQueue, refresh, hydrate, acceptNotice, submit, edit, remove, retractForEdit, resume };
|
|
1027
|
+
return { pendingMessages, messageQueue, refresh, hydrate, acceptNotice, beginSubmission, discardSubmission, submit, edit, remove, retractForEdit, resume };
|
|
1028
|
+
}
|
|
1029
|
+
function optimisticSubmissionTurn(message) {
|
|
1030
|
+
const user = {
|
|
1031
|
+
id: message.client_submission_id || message.id,
|
|
1032
|
+
clientSubmissionId: message.client_submission_id || void 0,
|
|
1033
|
+
role: "user",
|
|
1034
|
+
content: message.text,
|
|
1035
|
+
createdAt: message.created_at,
|
|
1036
|
+
isFinal: true,
|
|
1037
|
+
parts: message.text ? [{ id: `${message.id}:text`, type: "text", kind: "text", text: message.text }] : [],
|
|
1038
|
+
attachments: message.attachments?.map((item) => ({
|
|
1039
|
+
id: item.id,
|
|
1040
|
+
filename: item.filename,
|
|
1041
|
+
mediaType: item.mime_type,
|
|
1042
|
+
byteSize: item.byte_size
|
|
1043
|
+
}))
|
|
1044
|
+
};
|
|
1045
|
+
return [user, {
|
|
1046
|
+
id: `${user.id}:assistant`,
|
|
1047
|
+
role: "assistant",
|
|
1048
|
+
content: "",
|
|
1049
|
+
parts: [],
|
|
1050
|
+
createdAt: message.created_at,
|
|
1051
|
+
isFinal: false
|
|
1052
|
+
}];
|
|
1053
|
+
}
|
|
1054
|
+
function optimisticSubmissionState(state, message, uploading) {
|
|
1055
|
+
const [user, assistant] = optimisticSubmissionTurn(message);
|
|
1056
|
+
return {
|
|
1057
|
+
...state,
|
|
1058
|
+
messages: appendStableStreamingTurn(state.messages, user, assistant),
|
|
1059
|
+
streamingMessageId: assistant.id,
|
|
1060
|
+
runState: uploading ? "uploading" : "streaming",
|
|
1061
|
+
error: null
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
function discardOptimisticSubmission(state, message) {
|
|
1065
|
+
const activeDraft = state.streamingMessageId === `${message.id}:assistant`;
|
|
1066
|
+
return {
|
|
1067
|
+
...state,
|
|
1068
|
+
messages: state.messages.filter((item) => item.runId || item.id !== message.id && item.id !== `${message.id}:assistant`),
|
|
1069
|
+
streamingMessageId: activeDraft ? null : state.streamingMessageId,
|
|
1070
|
+
runState: activeDraft && !state.activeRunId ? "idle" : state.runState
|
|
1071
|
+
};
|
|
953
1072
|
}
|
|
954
1073
|
|
|
955
1074
|
// src/use-agent-chat.ts
|
|
@@ -1254,7 +1373,13 @@ function useAgentChat(options = {}) {
|
|
|
1254
1373
|
});
|
|
1255
1374
|
}
|
|
1256
1375
|
}, [update]);
|
|
1257
|
-
const queue = useThreadMessageQueue({
|
|
1376
|
+
const queue = useThreadMessageQueue({
|
|
1377
|
+
threadId: state.activeThreadId,
|
|
1378
|
+
transport: queueTransport,
|
|
1379
|
+
onPromoted: onQueuePromotion,
|
|
1380
|
+
onOptimistic: (message, uploading) => update((current) => optimisticSubmissionState(current, message, uploading)),
|
|
1381
|
+
onDiscard: (message) => update((current) => discardOptimisticSubmission(current, message))
|
|
1382
|
+
});
|
|
1258
1383
|
const { pendingMessages, messageQueue } = queue;
|
|
1259
1384
|
const eventBuffer = React6.useMemo(
|
|
1260
1385
|
() => createRuntimeEventFrameBuffer(
|
|
@@ -1465,38 +1590,46 @@ function useAgentChat(options = {}) {
|
|
|
1465
1590
|
clearOperationError();
|
|
1466
1591
|
setIsSubmitting(true);
|
|
1467
1592
|
try {
|
|
1468
|
-
|
|
1469
|
-
uploads: input.uploads,
|
|
1470
|
-
threadId: existingThreadId,
|
|
1471
|
-
requestedAgentId: input.requestedAgentId,
|
|
1472
|
-
idempotencyKey: runtime.createId()
|
|
1473
|
-
}) : [];
|
|
1474
|
-
const result = await queue.submit({
|
|
1593
|
+
await queue.submit({
|
|
1475
1594
|
mode: "queue",
|
|
1476
1595
|
input: input.text.trim(),
|
|
1477
|
-
attachmentIds:
|
|
1596
|
+
attachmentIds: input.attachmentIds,
|
|
1597
|
+
attachments: input.attachments?.map((item, index) => ({
|
|
1598
|
+
id: item.id || `attachment-${index}`,
|
|
1599
|
+
filename: item.filename || "Attachment",
|
|
1600
|
+
mime_type: item.mediaType || "application/octet-stream",
|
|
1601
|
+
byte_size: typeof item.byteSize === "number" ? item.byteSize : 0
|
|
1602
|
+
})) || input.uploads?.map((item, index) => ({
|
|
1603
|
+
id: `upload-${index}`,
|
|
1604
|
+
filename: item.name,
|
|
1605
|
+
mime_type: item.mediaType || "application/octet-stream",
|
|
1606
|
+
byte_size: 0
|
|
1607
|
+
})),
|
|
1608
|
+
queued: Boolean(stateRef.current.activeRunId),
|
|
1609
|
+
uploading: Boolean(input.uploads?.length),
|
|
1478
1610
|
requestedAgentId: input.requestedAgentId,
|
|
1479
1611
|
requestedModelId: input.requestedModelId,
|
|
1480
|
-
|
|
1612
|
+
clientSubmissionId: runtime.createId(),
|
|
1613
|
+
idempotencyKey: runtime.createId(),
|
|
1614
|
+
prepare: async () => {
|
|
1615
|
+
const uploaded = input.uploads?.length ? await client.attachments.uploadBatch({
|
|
1616
|
+
uploads: input.uploads,
|
|
1617
|
+
threadId: existingThreadId,
|
|
1618
|
+
requestedAgentId: input.requestedAgentId,
|
|
1619
|
+
idempotencyKey: runtime.createId()
|
|
1620
|
+
}) : [];
|
|
1621
|
+
if (input.uploads?.length && uploaded.length !== input.uploads.length) throw new Error("Attachment upload returned an incomplete batch");
|
|
1622
|
+
return {
|
|
1623
|
+
attachmentIds: uploaded.length ? uploaded.map((item) => item.id) : input.attachmentIds,
|
|
1624
|
+
attachments: uploaded.length ? uploaded.map((item) => ({
|
|
1625
|
+
id: item.id,
|
|
1626
|
+
filename: item.filename,
|
|
1627
|
+
mime_type: item.mime_type,
|
|
1628
|
+
byte_size: item.byte_size
|
|
1629
|
+
})) : void 0
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1481
1632
|
});
|
|
1482
|
-
if (stateRef.current.activeThreadId !== existingThreadId) return;
|
|
1483
|
-
if (result.run_id) {
|
|
1484
|
-
const userMessage = {
|
|
1485
|
-
id: result.message_id,
|
|
1486
|
-
role: "user",
|
|
1487
|
-
runId: result.run_id,
|
|
1488
|
-
content: input.text.trim(),
|
|
1489
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1490
|
-
isFinal: true,
|
|
1491
|
-
parts: input.text.trim() ? [{ id: `${result.message_id}-text`, type: "text", kind: "text", text: input.text.trim() }] : [],
|
|
1492
|
-
attachments: input.attachments || []
|
|
1493
|
-
};
|
|
1494
|
-
update((current) => current.activeThreadId !== existingThreadId || current.messages.some((message) => message.id === userMessage.id) ? current : { ...current, messages: [...current.messages, userMessage] });
|
|
1495
|
-
}
|
|
1496
|
-
void refreshMessages(existingThreadId).catch(() => void 0);
|
|
1497
|
-
if (result.run_id && promotedAttachRef.current !== result.run_id && stateRef.current.activeRunId !== result.run_id) {
|
|
1498
|
-
void attachRunRef.current(result.run_id).catch(() => void 0);
|
|
1499
|
-
}
|
|
1500
1633
|
} catch (error) {
|
|
1501
1634
|
failOperation("submit", error);
|
|
1502
1635
|
throw error;
|
|
@@ -1513,28 +1646,28 @@ function useAgentChat(options = {}) {
|
|
|
1513
1646
|
detach();
|
|
1514
1647
|
const controller = runtime.createAbortController();
|
|
1515
1648
|
streamControllerRef.current = controller;
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1649
|
+
const ticket = queue.beginSubmission({
|
|
1650
|
+
input: input.text.trim(),
|
|
1651
|
+
clientSubmissionId: runtime.createId(),
|
|
1652
|
+
uploading: Boolean(input.uploads?.length),
|
|
1653
|
+
attachmentIds: input.attachmentIds,
|
|
1654
|
+
attachments: input.attachments?.map((item, index) => ({
|
|
1655
|
+
id: item.id || `attachment-${index}`,
|
|
1656
|
+
filename: item.filename || "Attachment",
|
|
1657
|
+
mime_type: item.mediaType || "application/octet-stream",
|
|
1658
|
+
byte_size: typeof item.byteSize === "number" ? item.byteSize : 0
|
|
1659
|
+
})) || input.uploads?.map((upload, index) => ({
|
|
1660
|
+
id: `upload-${index}`,
|
|
1661
|
+
filename: upload.name,
|
|
1662
|
+
mime_type: upload.mediaType || "application/octet-stream",
|
|
1663
|
+
byte_size: 0
|
|
1664
|
+
}))
|
|
1665
|
+
});
|
|
1666
|
+
const userId = ticket.id;
|
|
1518
1667
|
const idempotencyKey = runtime.createId();
|
|
1519
|
-
const optimisticAttachments = input.attachments?.length ? input.attachments : input.uploads?.map((upload, index) => ({
|
|
1520
|
-
id: `${userId}-attachment-${index}`,
|
|
1521
|
-
filename: upload.name,
|
|
1522
|
-
mediaType: upload.mediaType,
|
|
1523
|
-
status: "uploading"
|
|
1524
|
-
}));
|
|
1525
|
-
const userMessage = { id: userId, role: "user", content: input.text.trim(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), parts: input.text.trim() ? [{ id: `${userId}-text`, type: "text", kind: "text", text: input.text.trim() }] : [], attachments: optimisticAttachments };
|
|
1526
|
-
const assistantMessage = { id: assistantId, role: "assistant", content: "", createdAt: (/* @__PURE__ */ new Date()).toISOString(), isFinal: false, parts: [] };
|
|
1527
1668
|
clearOperationError();
|
|
1528
1669
|
const hasPendingUploads = Boolean(input.uploads?.length);
|
|
1529
|
-
update((current) => ({
|
|
1530
|
-
...current,
|
|
1531
|
-
messages: appendStableStreamingTurn(current.messages, userMessage, assistantMessage),
|
|
1532
|
-
streamingMessageId: assistantId,
|
|
1533
|
-
runState: hasPendingUploads ? "uploading" : "streaming",
|
|
1534
|
-
error: null
|
|
1535
|
-
}));
|
|
1536
1670
|
setIsSubmitting(true);
|
|
1537
|
-
let streamStarted = false;
|
|
1538
1671
|
try {
|
|
1539
1672
|
const uploaded = hasPendingUploads ? await client.attachments.uploadBatch({
|
|
1540
1673
|
uploads: input.uploads || [],
|
|
@@ -1555,8 +1688,7 @@ function useAgentChat(options = {}) {
|
|
|
1555
1688
|
runState: "streaming"
|
|
1556
1689
|
}));
|
|
1557
1690
|
}
|
|
1558
|
-
|
|
1559
|
-
const result = await client.chat.stream({ input: input.text.trim(), threadId: stateRef.current.activeThreadId || void 0, attachmentIds, requestedModelId: input.requestedModelId, requestedAgentId: input.requestedAgentId, idempotencyKey, signal: controller.signal }, consumeEvent);
|
|
1691
|
+
const result = await client.chat.stream({ clientSubmissionId: ticket.client_submission_id, input: input.text.trim(), threadId: stateRef.current.activeThreadId || void 0, attachmentIds, requestedModelId: input.requestedModelId, requestedAgentId: input.requestedAgentId, idempotencyKey, signal: controller.signal }, consumeEvent);
|
|
1560
1692
|
eventBuffer.flush();
|
|
1561
1693
|
update((current) => {
|
|
1562
1694
|
const threadId = result.threadId || current.activeThreadId;
|
|
@@ -1572,19 +1704,13 @@ function useAgentChat(options = {}) {
|
|
|
1572
1704
|
if (result.threadId) await refreshThreads().catch(() => void 0);
|
|
1573
1705
|
if (result.threadId && !result.detached) await refreshMessages(result.threadId);
|
|
1574
1706
|
} catch (error) {
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
messages: current.messages.filter((message) => message.id !== userId && message.id !== assistantId),
|
|
1579
|
-
runState: "idle",
|
|
1580
|
-
streamingMessageId: null
|
|
1581
|
-
} : current);
|
|
1582
|
-
} else if (!isAbortLike(error)) {
|
|
1707
|
+
const admitted = Boolean(stateRef.current.messages.find((message) => message.id === userId)?.runId);
|
|
1708
|
+
if (!admitted) queue.discardSubmission(ticket);
|
|
1709
|
+
if (!isAbortLike(error)) {
|
|
1583
1710
|
update((current) => ({
|
|
1584
1711
|
...current,
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
streamingMessageId: null
|
|
1712
|
+
runState: admitted ? "reconnecting" : "failed",
|
|
1713
|
+
streamingMessageId: admitted ? current.streamingMessageId : null
|
|
1588
1714
|
}));
|
|
1589
1715
|
failOperation("submit", error);
|
|
1590
1716
|
throw error;
|
|
@@ -1744,7 +1870,8 @@ function useAgentChat(options = {}) {
|
|
|
1744
1870
|
messageId: event.message_id,
|
|
1745
1871
|
status: event.status,
|
|
1746
1872
|
runId: event.run_id,
|
|
1747
|
-
cursor: event.cursor
|
|
1873
|
+
cursor: event.cursor,
|
|
1874
|
+
clientSubmissionId: event.client_submission_id
|
|
1748
1875
|
});
|
|
1749
1876
|
if (wasQueued) void refreshMessages(event.thread_id, event.run_id || void 0).catch(() => void 0);
|
|
1750
1877
|
}
|
|
@@ -2041,11 +2168,14 @@ export {
|
|
|
2041
2168
|
contextCompressionPartFromEvent,
|
|
2042
2169
|
createPortableId,
|
|
2043
2170
|
createRuntimeEventFrameBuffer,
|
|
2171
|
+
discardOptimisticSubmission,
|
|
2044
2172
|
getThreadActivity,
|
|
2045
2173
|
initialAgentChatState,
|
|
2046
2174
|
isAbortLike,
|
|
2047
2175
|
latestThreadContext,
|
|
2048
2176
|
modelTurnPartFromEvent,
|
|
2177
|
+
optimisticSubmissionState,
|
|
2178
|
+
optimisticSubmissionTurn,
|
|
2049
2179
|
partsFromResponseBlocks,
|
|
2050
2180
|
preferNewestThreadSummary,
|
|
2051
2181
|
preserveMountedMessageIds,
|
|
@@ -2053,6 +2183,7 @@ export {
|
|
|
2053
2183
|
reconcileRefreshedThreads,
|
|
2054
2184
|
reconcileThreadTimeline,
|
|
2055
2185
|
restoreAgentChatThreadSelection,
|
|
2186
|
+
retrySubmissionAdmission,
|
|
2056
2187
|
runtimeEventFrameScheduler,
|
|
2057
2188
|
threadDetailToMessages,
|
|
2058
2189
|
threadTimelineToMessages,
|