@agents24/react 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -392,6 +392,7 @@ function threadTimelineToMessages(items, detail) {
392
392
  const feedbackByRun = new Map((detail?.turns || []).flatMap((turn) => turn.run_id && turn.feedback ? [[turn.run_id, turn.feedback]] : []));
393
393
  return items.map((item) => ({
394
394
  id: item.id,
395
+ clientSubmissionId: item.client_submission_id || void 0,
395
396
  role: item.role,
396
397
  runId: item.run_id,
397
398
  segmentIndex: item.segment_index,
@@ -602,7 +603,7 @@ function appendStableStreamingTurn(messages, userMessage, assistantMessage) {
602
603
  }
603
604
  function preserveMountedMessageIds(canonical, current) {
604
605
  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));
606
+ 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
607
  return mounted && mounted.id !== message.id ? { ...message, id: mounted.id, durableId: message.id } : message;
607
608
  });
608
609
  }
@@ -618,9 +619,9 @@ function reconcileThreadTimeline(current, canonical, activeRunId, streamingMessa
618
619
  }
619
620
  function promoteThreadMessage(current, message, runId) {
620
621
  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);
622
+ const index = current.findIndex((item) => item.id === message.id || item.durableId === message.id || Boolean(message.clientSubmissionId && item.clientSubmissionId === message.clientSubmissionId));
623
+ if (index < 0) return [...current, message.clientSubmissionId ? { ...promoted, id: message.clientSubmissionId, durableId: message.id } : promoted];
624
+ 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
625
  }
625
626
  function upsertStableAssistantMessage(messages, input) {
626
627
  const next = [...messages];
@@ -677,13 +678,14 @@ function applyRuntimeEvent(state, event) {
677
678
  let optimisticIndex = -1;
678
679
  for (let index = state.messages.length - 1; index >= 0; index -= 1) {
679
680
  const message = state.messages[index];
680
- if (message.role === "user" && !message.runId && !message.durableId) {
681
+ if (message.role === "user" && (event.payload.client_submission_id ? message.clientSubmissionId === event.payload.client_submission_id : !message.runId && !message.durableId)) {
681
682
  optimisticIndex = index;
682
683
  break;
683
684
  }
684
685
  }
685
686
  if (optimisticIndex >= 0) {
686
- state = { ...state, messages: state.messages.map((message, index) => index === optimisticIndex ? { ...message, durableId: messageId2, runId: event.run_id } : message.role === "assistant" && !message.runId ? { ...message, runId: event.run_id } : message) };
687
+ const assistantId = event.payload.client_submission_id ? `${state.messages[optimisticIndex].id}:assistant` : state.streamingMessageId;
688
+ 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
689
  }
688
690
  }
689
691
  const projection = assistantProjectionFromEvent(event);
@@ -808,7 +810,16 @@ function applyThreadSummaryEvent(threads, event) {
808
810
 
809
811
  // src/thread-message-queue.ts
810
812
  import * as React2 from "react";
811
- function useThreadMessageQueue({ threadId, transport, onPromoted }) {
813
+ async function retrySubmissionAdmission(admit) {
814
+ try {
815
+ return await admit();
816
+ } catch (error) {
817
+ const failure = error;
818
+ if (failure?.kind !== "network" && failure?.name !== "HttpRequestTimeoutError" && !(error instanceof TypeError)) throw error;
819
+ return admit();
820
+ }
821
+ }
822
+ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic, onDiscard }) {
812
823
  const [pendingMessages, setPendingMessages] = React2.useState([]);
813
824
  const [messageQueue, setMessageQueue] = React2.useState(null);
814
825
  const pendingRef = React2.useRef(/* @__PURE__ */ new Map());
@@ -821,15 +832,19 @@ function useThreadMessageQueue({ threadId, transport, onPromoted }) {
821
832
  threadRef.current = threadId;
822
833
  transportRef.current = transport;
823
834
  onPromotedRef.current = onPromoted;
835
+ const callbacksRef = React2.useRef({ onOptimistic, onDiscard });
836
+ callbacksRef.current = { onOptimistic, onDiscard };
837
+ const knownMessage = (id, submissionId) => pendingRef.current.get(id) || (submissionId ? [...pendingRef.current.values()].find((item) => item.client_submission_id === submissionId) : void 0);
824
838
  const hydrate = React2.useCallback((page, selectedThreadId) => {
825
839
  if (threadRef.current !== selectedThreadId) return;
826
840
  const pending = page.pending.filter((message) => !promotedRef.current.has(message.id)).map((message) => {
827
- const known = pendingRef.current.get(message.id);
841
+ const known = knownMessage(message.id, message.client_submission_id);
828
842
  if (message.attachments?.length || !known?.attachments?.length) return message;
829
843
  return { ...message, attachments: known.attachments.filter((attachment) => message.attachment_ids.includes(attachment.id)) };
830
844
  });
831
845
  for (const message of pending) pendingRef.current.set(message.id, message);
832
- setPendingMessages(pending);
846
+ 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));
847
+ setPendingMessages([...pending, ...optimistic]);
833
848
  setMessageQueue(page.queue);
834
849
  }, []);
835
850
  const refresh = React2.useCallback(async (selectedThreadId = threadRef.current) => {
@@ -839,9 +854,10 @@ function useThreadMessageQueue({ threadId, transport, onPromoted }) {
839
854
  if (version !== versionRef.current || threadRef.current !== selectedThreadId) return null;
840
855
  for (const item of page.items) {
841
856
  if (item.role === "user" && item.status === "delivered" && item.run_id) {
842
- const pending = pendingRef.current.get(item.id);
857
+ const pending = knownMessage(item.id, item.client_submission_id);
843
858
  if (pending && !promotedRef.current.has(item.id)) {
844
859
  promotedRef.current.add(item.id);
860
+ pendingRef.current.delete(pending.id);
845
861
  pendingRef.current.delete(item.id);
846
862
  onPromotedRef.current({
847
863
  ...pending,
@@ -857,56 +873,98 @@ function useThreadMessageQueue({ threadId, transport, onPromoted }) {
857
873
  const acceptNotice = React2.useCallback((notice) => {
858
874
  if (notice.cursor && notice.cursor <= cursorRef.current) return false;
859
875
  if (notice.cursor) cursorRef.current = notice.cursor;
860
- const pending = pendingRef.current.get(notice.messageId);
861
- if (notice.status === "delivered" && notice.runId && pending && !promotedRef.current.has(pending.id)) {
862
- promotedRef.current.add(pending.id);
876
+ const pending = knownMessage(notice.messageId, notice.clientSubmissionId);
877
+ if (notice.status === "delivered" && notice.runId && pending && !promotedRef.current.has(notice.messageId)) {
878
+ promotedRef.current.add(notice.messageId);
863
879
  pendingRef.current.delete(pending.id);
864
880
  versionRef.current += 1;
865
881
  setPendingMessages((current) => current.filter((message) => message.id !== pending.id));
866
- onPromotedRef.current(pending, notice.runId);
882
+ onPromotedRef.current({ ...pending, id: notice.messageId, run_id: notice.runId, status: "delivered" }, notice.runId);
867
883
  }
868
884
  if (notice.status === "removed") pendingRef.current.delete(notice.messageId);
869
885
  void refresh().catch(() => void 0);
870
886
  return true;
871
887
  }, [refresh]);
888
+ const beginSubmission = React2.useCallback((input, queued = false) => {
889
+ const now = (/* @__PURE__ */ new Date()).toISOString();
890
+ const message = {
891
+ id: input.clientSubmissionId,
892
+ client_submission_id: input.clientSubmissionId,
893
+ thread_id: threadRef.current || "",
894
+ sequence: Number.MAX_SAFE_INTEGER,
895
+ run_id: null,
896
+ turn_id: null,
897
+ segment_index: 0,
898
+ role: "user",
899
+ status: "queued",
900
+ requested_mode: input.mode || "queue",
901
+ text: input.input,
902
+ attachment_ids: input.attachmentIds || input.attachments?.map((item) => item.id) || [],
903
+ attachments: input.attachments,
904
+ response_blocks: [],
905
+ revision: 0,
906
+ created_at: now,
907
+ updated_at: now
908
+ };
909
+ pendingRef.current.set(message.id, { ...message, optimisticQueued: queued });
910
+ versionRef.current += 1;
911
+ if (queued) setPendingMessages((current) => [...current, message]);
912
+ else callbacksRef.current.onOptimistic?.(message, Boolean(input.uploading));
913
+ return message;
914
+ }, []);
915
+ const discardSubmission = React2.useCallback((message) => {
916
+ if (!pendingRef.current.has(message.id)) return;
917
+ pendingRef.current.delete(message.id);
918
+ callbacksRef.current.onDiscard?.(message);
919
+ versionRef.current += 1;
920
+ setPendingMessages((current) => current.filter((item) => item.client_submission_id !== message.client_submission_id));
921
+ }, []);
872
922
  const submit = React2.useCallback(async (input) => {
873
923
  const selectedThreadId = threadRef.current;
874
924
  if (!selectedThreadId) throw new Error("No active Agent thread is available.");
875
- const { attachments, ...submission } = input;
876
- const result = await transportRef.current.submit({ ...submission, threadId: selectedThreadId });
877
- if (threadRef.current !== selectedThreadId) return result;
878
- versionRef.current += 1;
879
- if (!result.run_id && ["queued", "steer_pending", "blocked"].includes(result.status)) {
880
- const now = (/* @__PURE__ */ new Date()).toISOString();
881
- const pending = {
925
+ const { queued, prepare, attachments, uploading, ...submission } = input;
926
+ const optimistic = beginSubmission({
927
+ ...submission,
928
+ attachments,
929
+ uploading,
930
+ clientSubmissionId: submission.clientSubmissionId || submission.idempotencyKey
931
+ }, queued);
932
+ try {
933
+ const prepared = prepare ? await prepare() : {};
934
+ if (threadRef.current === selectedThreadId) pendingRef.current.set(optimistic.id, { ...optimistic, optimisticQueued: queued, attachment_ids: prepared.attachmentIds || optimistic.attachment_ids, attachments: prepared.attachments || attachments });
935
+ const request = { ...submission, ...prepared, clientSubmissionId: optimistic.client_submission_id, threadId: selectedThreadId };
936
+ const result = await retrySubmissionAdmission(() => transportRef.current.submit(request));
937
+ if (threadRef.current !== selectedThreadId) return result;
938
+ versionRef.current += 1;
939
+ const bound = {
940
+ ...optimistic,
882
941
  id: result.message_id,
883
- thread_id: selectedThreadId,
884
- sequence: Number.MAX_SAFE_INTEGER,
885
- run_id: null,
886
- turn_id: null,
887
- segment_index: null,
888
- role: "user",
942
+ client_submission_id: result.client_submission_id || optimistic.client_submission_id,
943
+ attachment_ids: request.attachmentIds || [],
944
+ attachments: prepared.attachments || attachments,
945
+ run_id: result.run_id,
889
946
  status: result.status,
890
- requested_mode: input.mode,
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
947
+ revision: 1
898
948
  };
899
- pendingRef.current.set(pending.id, pending);
900
- setPendingMessages((current) => current.some((message) => message.id === pending.id) ? current : [...current, pending]);
901
- setMessageQueue((current) => ({
902
- pending_count: Math.max(current?.pending_count || 0, result.queue_position || 1),
903
- paused: current?.paused || false,
904
- reason: current?.reason || null
905
- }));
949
+ pendingRef.current.delete(optimistic.id);
950
+ if (result.run_id) {
951
+ setPendingMessages((current) => current.filter((item) => item.client_submission_id !== bound.client_submission_id));
952
+ if (!promotedRef.current.has(bound.id)) {
953
+ promotedRef.current.add(bound.id);
954
+ onPromotedRef.current(bound, result.run_id);
955
+ }
956
+ } else if (!promotedRef.current.has(bound.id)) {
957
+ if (!queued) callbacksRef.current.onDiscard?.(optimistic);
958
+ pendingRef.current.set(bound.id, bound);
959
+ setPendingMessages((current) => [...current.filter((item) => item.client_submission_id !== bound.client_submission_id), bound]);
960
+ }
961
+ void refresh().catch(() => void 0);
962
+ return result;
963
+ } catch (error) {
964
+ if (threadRef.current === selectedThreadId) discardSubmission(optimistic);
965
+ throw error;
906
966
  }
907
- void refresh().catch(() => void 0);
908
- return result;
909
- }, [refresh]);
967
+ }, [beginSubmission, discardSubmission, refresh]);
910
968
  const edit = React2.useCallback(async (message, input) => {
911
969
  const updated = await transportRef.current.edit(message, input);
912
970
  versionRef.current += 1;
@@ -949,7 +1007,51 @@ function useThreadMessageQueue({ threadId, transport, onPromoted }) {
949
1007
  }, 3e3);
950
1008
  return () => clock.clearInterval(timer);
951
1009
  }, [threadId, pendingMessages.length, messageQueue?.paused, refresh]);
952
- return { pendingMessages, messageQueue, refresh, hydrate, acceptNotice, submit, edit, remove, retractForEdit, resume };
1010
+ return { pendingMessages, messageQueue, refresh, hydrate, acceptNotice, beginSubmission, discardSubmission, submit, edit, remove, retractForEdit, resume };
1011
+ }
1012
+ function optimisticSubmissionTurn(message) {
1013
+ const user = {
1014
+ id: message.client_submission_id || message.id,
1015
+ clientSubmissionId: message.client_submission_id || void 0,
1016
+ role: "user",
1017
+ content: message.text,
1018
+ createdAt: message.created_at,
1019
+ isFinal: true,
1020
+ parts: message.text ? [{ id: `${message.id}:text`, type: "text", kind: "text", text: message.text }] : [],
1021
+ attachments: message.attachments?.map((item) => ({
1022
+ id: item.id,
1023
+ filename: item.filename,
1024
+ mediaType: item.mime_type,
1025
+ byteSize: item.byte_size
1026
+ }))
1027
+ };
1028
+ return [user, {
1029
+ id: `${user.id}:assistant`,
1030
+ role: "assistant",
1031
+ content: "",
1032
+ parts: [],
1033
+ createdAt: message.created_at,
1034
+ isFinal: false
1035
+ }];
1036
+ }
1037
+ function optimisticSubmissionState(state, message, uploading) {
1038
+ const [user, assistant] = optimisticSubmissionTurn(message);
1039
+ return {
1040
+ ...state,
1041
+ messages: appendStableStreamingTurn(state.messages, user, assistant),
1042
+ streamingMessageId: assistant.id,
1043
+ runState: uploading ? "uploading" : "streaming",
1044
+ error: null
1045
+ };
1046
+ }
1047
+ function discardOptimisticSubmission(state, message) {
1048
+ const activeDraft = state.streamingMessageId === `${message.id}:assistant`;
1049
+ return {
1050
+ ...state,
1051
+ messages: state.messages.filter((item) => item.runId || item.id !== message.id && item.id !== `${message.id}:assistant`),
1052
+ streamingMessageId: activeDraft ? null : state.streamingMessageId,
1053
+ runState: activeDraft && !state.activeRunId ? "idle" : state.runState
1054
+ };
953
1055
  }
954
1056
 
955
1057
  // src/use-agent-chat.ts
@@ -1254,7 +1356,13 @@ function useAgentChat(options = {}) {
1254
1356
  });
1255
1357
  }
1256
1358
  }, [update]);
1257
- const queue = useThreadMessageQueue({ threadId: state.activeThreadId, transport: queueTransport, onPromoted: onQueuePromotion });
1359
+ const queue = useThreadMessageQueue({
1360
+ threadId: state.activeThreadId,
1361
+ transport: queueTransport,
1362
+ onPromoted: onQueuePromotion,
1363
+ onOptimistic: (message, uploading) => update((current) => optimisticSubmissionState(current, message, uploading)),
1364
+ onDiscard: (message) => update((current) => discardOptimisticSubmission(current, message))
1365
+ });
1258
1366
  const { pendingMessages, messageQueue } = queue;
1259
1367
  const eventBuffer = React6.useMemo(
1260
1368
  () => createRuntimeEventFrameBuffer(
@@ -1465,38 +1573,46 @@ function useAgentChat(options = {}) {
1465
1573
  clearOperationError();
1466
1574
  setIsSubmitting(true);
1467
1575
  try {
1468
- const uploaded = input.uploads?.length ? await client.attachments.uploadBatch({
1469
- uploads: input.uploads,
1470
- threadId: existingThreadId,
1471
- requestedAgentId: input.requestedAgentId,
1472
- idempotencyKey: runtime.createId()
1473
- }) : [];
1474
- const result = await queue.submit({
1576
+ await queue.submit({
1475
1577
  mode: "queue",
1476
1578
  input: input.text.trim(),
1477
- attachmentIds: uploaded.length ? uploaded.map((item) => item.id) : input.attachmentIds,
1579
+ attachmentIds: input.attachmentIds,
1580
+ attachments: input.attachments?.map((item, index) => ({
1581
+ id: item.id || `attachment-${index}`,
1582
+ filename: item.filename || "Attachment",
1583
+ mime_type: item.mediaType || "application/octet-stream",
1584
+ byte_size: typeof item.byteSize === "number" ? item.byteSize : 0
1585
+ })) || input.uploads?.map((item, index) => ({
1586
+ id: `upload-${index}`,
1587
+ filename: item.name,
1588
+ mime_type: item.mediaType || "application/octet-stream",
1589
+ byte_size: 0
1590
+ })),
1591
+ queued: Boolean(stateRef.current.activeRunId),
1592
+ uploading: Boolean(input.uploads?.length),
1478
1593
  requestedAgentId: input.requestedAgentId,
1479
1594
  requestedModelId: input.requestedModelId,
1480
- idempotencyKey: runtime.createId()
1595
+ clientSubmissionId: runtime.createId(),
1596
+ idempotencyKey: runtime.createId(),
1597
+ prepare: async () => {
1598
+ const uploaded = input.uploads?.length ? await client.attachments.uploadBatch({
1599
+ uploads: input.uploads,
1600
+ threadId: existingThreadId,
1601
+ requestedAgentId: input.requestedAgentId,
1602
+ idempotencyKey: runtime.createId()
1603
+ }) : [];
1604
+ if (input.uploads?.length && uploaded.length !== input.uploads.length) throw new Error("Attachment upload returned an incomplete batch");
1605
+ return {
1606
+ attachmentIds: uploaded.length ? uploaded.map((item) => item.id) : input.attachmentIds,
1607
+ attachments: uploaded.length ? uploaded.map((item) => ({
1608
+ id: item.id,
1609
+ filename: item.filename,
1610
+ mime_type: item.mime_type,
1611
+ byte_size: item.byte_size
1612
+ })) : void 0
1613
+ };
1614
+ }
1481
1615
  });
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
1616
  } catch (error) {
1501
1617
  failOperation("submit", error);
1502
1618
  throw error;
@@ -1513,28 +1629,28 @@ function useAgentChat(options = {}) {
1513
1629
  detach();
1514
1630
  const controller = runtime.createAbortController();
1515
1631
  streamControllerRef.current = controller;
1516
- const userId = runtime.createId();
1517
- const assistantId = runtime.createId();
1632
+ const ticket = queue.beginSubmission({
1633
+ input: input.text.trim(),
1634
+ clientSubmissionId: runtime.createId(),
1635
+ uploading: Boolean(input.uploads?.length),
1636
+ attachmentIds: input.attachmentIds,
1637
+ attachments: input.attachments?.map((item, index) => ({
1638
+ id: item.id || `attachment-${index}`,
1639
+ filename: item.filename || "Attachment",
1640
+ mime_type: item.mediaType || "application/octet-stream",
1641
+ byte_size: typeof item.byteSize === "number" ? item.byteSize : 0
1642
+ })) || input.uploads?.map((upload, index) => ({
1643
+ id: `upload-${index}`,
1644
+ filename: upload.name,
1645
+ mime_type: upload.mediaType || "application/octet-stream",
1646
+ byte_size: 0
1647
+ }))
1648
+ });
1649
+ const userId = ticket.id;
1518
1650
  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
1651
  clearOperationError();
1528
1652
  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
1653
  setIsSubmitting(true);
1537
- let streamStarted = false;
1538
1654
  try {
1539
1655
  const uploaded = hasPendingUploads ? await client.attachments.uploadBatch({
1540
1656
  uploads: input.uploads || [],
@@ -1555,8 +1671,7 @@ function useAgentChat(options = {}) {
1555
1671
  runState: "streaming"
1556
1672
  }));
1557
1673
  }
1558
- streamStarted = true;
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);
1674
+ 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
1675
  eventBuffer.flush();
1561
1676
  update((current) => {
1562
1677
  const threadId = result.threadId || current.activeThreadId;
@@ -1572,19 +1687,13 @@ function useAgentChat(options = {}) {
1572
1687
  if (result.threadId) await refreshThreads().catch(() => void 0);
1573
1688
  if (result.threadId && !result.detached) await refreshMessages(result.threadId);
1574
1689
  } catch (error) {
1575
- if (isAbortLike(error) && !streamStarted) {
1576
- update((current) => current.streamingMessageId === assistantId ? {
1577
- ...current,
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)) {
1690
+ const admitted = Boolean(stateRef.current.messages.find((message) => message.id === userId)?.runId);
1691
+ if (!admitted) queue.discardSubmission(ticket);
1692
+ if (!isAbortLike(error)) {
1583
1693
  update((current) => ({
1584
1694
  ...current,
1585
- messages: streamStarted ? current.messages : current.messages.filter((message) => message.id !== userId && message.id !== assistantId),
1586
- runState: "failed",
1587
- streamingMessageId: null
1695
+ runState: admitted ? "reconnecting" : "failed",
1696
+ streamingMessageId: admitted ? current.streamingMessageId : null
1588
1697
  }));
1589
1698
  failOperation("submit", error);
1590
1699
  throw error;
@@ -1744,7 +1853,8 @@ function useAgentChat(options = {}) {
1744
1853
  messageId: event.message_id,
1745
1854
  status: event.status,
1746
1855
  runId: event.run_id,
1747
- cursor: event.cursor
1856
+ cursor: event.cursor,
1857
+ clientSubmissionId: event.client_submission_id
1748
1858
  });
1749
1859
  if (wasQueued) void refreshMessages(event.thread_id, event.run_id || void 0).catch(() => void 0);
1750
1860
  }
@@ -2041,11 +2151,14 @@ export {
2041
2151
  contextCompressionPartFromEvent,
2042
2152
  createPortableId,
2043
2153
  createRuntimeEventFrameBuffer,
2154
+ discardOptimisticSubmission,
2044
2155
  getThreadActivity,
2045
2156
  initialAgentChatState,
2046
2157
  isAbortLike,
2047
2158
  latestThreadContext,
2048
2159
  modelTurnPartFromEvent,
2160
+ optimisticSubmissionState,
2161
+ optimisticSubmissionTurn,
2049
2162
  partsFromResponseBlocks,
2050
2163
  preferNewestThreadSummary,
2051
2164
  preserveMountedMessageIds,
@@ -2053,6 +2166,7 @@ export {
2053
2166
  reconcileRefreshedThreads,
2054
2167
  reconcileThreadTimeline,
2055
2168
  restoreAgentChatThreadSelection,
2169
+ retrySubmissionAdmission,
2056
2170
  runtimeEventFrameScheduler,
2057
2171
  threadDetailToMessages,
2058
2172
  threadTimelineToMessages,