@agents24/react 0.2.15 → 0.2.17

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
@@ -802,10 +802,13 @@ function reconcileRefreshedThreads(current, incoming, activeThreadId, concurrent
802
802
  return concurrentEvents.reduce(applyThreadSummaryEvent, base);
803
803
  }
804
804
  function preferNewestThreadSummary(current, incoming) {
805
- if (!current?.updated_at || !incoming.updated_at) return incoming;
806
- const currentTime = Date.parse(current.updated_at);
807
- const incomingTime = Date.parse(incoming.updated_at);
808
- return Number.isFinite(currentTime) && Number.isFinite(incomingTime) && currentTime > incomingTime ? current : incoming;
805
+ const currentTime = Date.parse(current?.updated_at || "");
806
+ const incomingTime = Date.parse(incoming.updated_at || "");
807
+ const newest = current && Number.isFinite(currentTime) && Number.isFinite(incomingTime) && currentTime > incomingTime ? current : incoming;
808
+ if (current?.last_run_id && current.last_run_id === incoming.last_run_id && !current.active_run && ["completed", "failed", "cancelled"].includes(current.last_run_status || "") && (!incoming.active_run || incoming.active_run.run_id === current.last_run_id)) {
809
+ return { ...newest, active_run: null, last_run_status: current.last_run_status };
810
+ }
811
+ return newest;
809
812
  }
810
813
  function adoptStreamingThread(state, threadId) {
811
814
  const normalizedThreadId = threadId.trim();
@@ -846,7 +849,23 @@ function reconcileThreadTimeline(current, canonical, activeRunId, streamingMessa
846
849
  const live = current.find((item) => item.id === message.id);
847
850
  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, feedback: message.feedback, durableId: message.durableId || live.durableId } : message;
848
851
  });
849
- return [...retained, ...reconciled];
852
+ const retainedIds = new Set(retained.map((message) => message.id));
853
+ const userByRun = new Map(reconciled.filter((message) => message.role === "user").map((message) => [message.runId, message.id]));
854
+ const after = /* @__PURE__ */ new Map();
855
+ let anchor = null;
856
+ let anchorRun;
857
+ for (const message of current) {
858
+ if (canonicalIds.has(message.id)) {
859
+ anchor = message.id;
860
+ anchorRun = message.runId;
861
+ } else if (retainedIds.has(message.id)) {
862
+ const position = message.role === "assistant" && message.runId && message.runId !== anchorRun ? userByRun.get(message.runId) ?? anchor : anchor;
863
+ const group = after.get(position) || [];
864
+ group.push(message);
865
+ after.set(position, group);
866
+ }
867
+ }
868
+ return [...after.get(null) || [], ...reconciled.flatMap((message) => [message, ...after.get(message.id) || []])];
850
869
  }
851
870
  function prependThreadTimeline(current, older) {
852
871
  const mounted = preserveMountedMessageIds(older, current);
@@ -1008,11 +1027,13 @@ function applyRuntimeEvent(state, event) {
1008
1027
  ...segment ? { durableId: segment.message_id } : {}
1009
1028
  };
1010
1029
  });
1030
+ const terminalThread = terminal ? { active_run: null, last_run_status: eventName.slice(4) } : {};
1011
1031
  const threads = activeThreadId ? state.threads.some((thread) => thread.id === activeThreadId) ? state.threads.map((thread) => thread.id === activeThreadId ? {
1012
1032
  ...thread,
1013
1033
  messages,
1014
1034
  isHydrated: true,
1015
- last_run_id: runId ?? thread.last_run_id,
1035
+ last_run_id: terminal && thread.active_run && thread.active_run.run_id !== runId ? thread.last_run_id : runId ?? thread.last_run_id,
1036
+ ...!thread.active_run || thread.active_run.run_id === runId ? terminalThread : {},
1016
1037
  contextWindow,
1017
1038
  contextCompression
1018
1039
  } : thread) : [{
@@ -1023,6 +1044,7 @@ function applyRuntimeEvent(state, event) {
1023
1044
  messages,
1024
1045
  isHydrated: true,
1025
1046
  last_run_id: runId,
1047
+ ...terminalThread,
1026
1048
  contextWindow,
1027
1049
  contextCompression
1028
1050
  }, ...state.threads] : state.threads;
@@ -1098,12 +1120,14 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onUnknownPromo
1098
1120
  const [pendingMessages, setPendingMessages] = React3.useState([]);
1099
1121
  const [messageQueue, setMessageQueue] = React3.useState(null);
1100
1122
  const pendingRef = React3.useRef(/* @__PURE__ */ new Map());
1123
+ const admissionRef = React3.useRef(null);
1124
+ const initialAdmissionRef = React3.useRef(null);
1101
1125
  const versionRef = React3.useRef(0);
1102
1126
  const cursorRef = React3.useRef(0);
1103
1127
  const promotedRef = React3.useRef(/* @__PURE__ */ new Set());
1104
1128
  const threadRef = React3.useRef(threadId);
1105
1129
  const selectionGenerationRef = React3.useRef(0);
1106
- if (threadRef.current !== threadId) selectionGenerationRef.current += 1;
1130
+ if (threadRef.current !== threadId && initialAdmissionRef.current?.threadId !== threadId) selectionGenerationRef.current += 1;
1107
1131
  const transportRef = React3.useRef(transport);
1108
1132
  const onPromotedRef = React3.useRef(onPromoted);
1109
1133
  threadRef.current = threadId;
@@ -1113,10 +1137,16 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onUnknownPromo
1113
1137
  ownSelectionRef.current = ownSelection;
1114
1138
  const callbacksRef = React3.useRef({ onOptimistic, onDiscard, onUnknownPromotion });
1115
1139
  callbacksRef.current = { onOptimistic, onDiscard, onUnknownPromotion };
1116
- const ownOperation = React3.useCallback(() => ownSelectionRef.current?.() ?? captureChatOperation(() => ({
1140
+ const ownOperation = React3.useCallback((options) => ownSelectionRef.current?.(options) ?? captureChatOperation(() => ({
1117
1141
  generation: selectionGenerationRef.current,
1118
1142
  threadId: threadRef.current
1119
- })), []);
1143
+ }), options), []);
1144
+ const confirmThreadAdmission = React3.useCallback((admittedThreadId) => {
1145
+ const initial = initialAdmissionRef.current;
1146
+ if (!initial || !initial.isCurrent()) return;
1147
+ initial.threadId = admittedThreadId;
1148
+ initial.resolve(admittedThreadId);
1149
+ }, []);
1120
1150
  const assertCurrent = (isCurrent) => {
1121
1151
  if (!isCurrent()) throw Object.assign(new Error("Thread selection changed."), { name: "AbortError" });
1122
1152
  };
@@ -1195,12 +1225,24 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onUnknownPromo
1195
1225
  updated_at: now
1196
1226
  };
1197
1227
  pendingRef.current.set(message.id, { ...message, optimisticQueued: queued });
1228
+ if (!queued && !threadRef.current && !initialAdmissionRef.current) {
1229
+ let resolve;
1230
+ const done = new Promise((callback) => {
1231
+ resolve = callback;
1232
+ });
1233
+ initialAdmissionRef.current = { id: message.id, done, resolve, isCurrent: ownOperation({ adoptThread: true }) };
1234
+ }
1198
1235
  versionRef.current += 1;
1199
1236
  if (queued) setPendingMessages((current) => [...current, message]);
1200
1237
  else callbacksRef.current.onOptimistic?.(message, Boolean(input.uploading));
1201
1238
  return message;
1202
- }, []);
1203
- const discardSubmission = React3.useCallback((message) => {
1239
+ }, [ownOperation]);
1240
+ const discardSubmission = React3.useCallback((message, failure) => {
1241
+ if (initialAdmissionRef.current?.id === message.id) {
1242
+ initialAdmissionRef.current.failure = failure;
1243
+ initialAdmissionRef.current.resolve(null);
1244
+ initialAdmissionRef.current = null;
1245
+ }
1204
1246
  if (!pendingRef.current.has(message.id)) return;
1205
1247
  pendingRef.current.delete(message.id);
1206
1248
  callbacksRef.current.onDiscard?.(message);
@@ -1208,19 +1250,31 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onUnknownPromo
1208
1250
  setPendingMessages((current) => current.filter((item) => item.client_submission_id !== message.client_submission_id));
1209
1251
  }, []);
1210
1252
  const submit = React3.useCallback(async (input) => {
1211
- const selectedThreadId = threadRef.current;
1212
- if (!selectedThreadId) throw new Error("No active Agent thread is available.");
1213
- const isCurrent = ownOperation();
1214
- const { queued, prepare, attachments, uploading, ...submission } = input;
1253
+ const initial = !threadRef.current ? initialAdmissionRef.current : null;
1254
+ let selectedThreadId = threadRef.current;
1255
+ if (!selectedThreadId && !initial) throw new Error("No active Agent thread is available.");
1256
+ const isCurrent = ownOperation({ adoptThread: Boolean(initial) });
1257
+ const { queued: requestedQueued, prepare, attachments, uploading, ...submission } = input;
1258
+ const priorAdmission = admissionRef.current?.threadId === (selectedThreadId || "") ? admissionRef.current.done : void 0;
1259
+ const queued = Boolean(requestedQueued || priorAdmission || initial);
1215
1260
  const optimistic = beginSubmission({
1216
1261
  ...submission,
1217
1262
  attachments,
1218
1263
  uploading,
1219
1264
  clientSubmissionId: submission.clientSubmissionId || submission.idempotencyKey
1220
1265
  }, queued);
1266
+ let releaseAdmission;
1267
+ const admission = { threadId: selectedThreadId || "", done: new Promise((resolve) => {
1268
+ releaseAdmission = resolve;
1269
+ }) };
1270
+ admissionRef.current = admission;
1221
1271
  try {
1222
1272
  const prepared = prepare ? await prepare() : {};
1273
+ if (initial) selectedThreadId = await initial.done;
1274
+ if (priorAdmission) await priorAdmission;
1223
1275
  assertCurrent(isCurrent);
1276
+ if (!selectedThreadId) throw Object.assign(new Error("Initial message admission did not complete."), { name: "AbortError" });
1277
+ admission.threadId = selectedThreadId;
1224
1278
  pendingRef.current.set(optimistic.id, { ...optimistic, optimisticQueued: queued, attachment_ids: prepared.attachmentIds || optimistic.attachment_ids, attachments: prepared.attachments || attachments });
1225
1279
  const request = { ...submission, ...prepared, clientSubmissionId: optimistic.client_submission_id, threadId: selectedThreadId };
1226
1280
  const result = await retrySubmissionAdmission(() => transportRef.current.submit(request));
@@ -1253,6 +1307,11 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onUnknownPromo
1253
1307
  } catch (error) {
1254
1308
  if (isCurrent()) discardSubmission(optimistic);
1255
1309
  throw error;
1310
+ } finally {
1311
+ void Promise.resolve(priorAdmission).then(() => {
1312
+ releaseAdmission();
1313
+ if (admissionRef.current === admission) admissionRef.current = null;
1314
+ });
1256
1315
  }
1257
1316
  }, [beginSubmission, discardSubmission, refresh, ownOperation]);
1258
1317
  const edit = React3.useCallback(async (message, input) => {
@@ -1288,16 +1347,63 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onUnknownPromo
1288
1347
  await refresh(selectedThreadId);
1289
1348
  return result;
1290
1349
  }, [refresh, ownOperation]);
1291
- React3.useEffect(() => {
1350
+ const reset = React3.useCallback(() => {
1351
+ initialAdmissionRef.current?.resolve(null);
1352
+ initialAdmissionRef.current = null;
1353
+ admissionRef.current = null;
1354
+ selectionGenerationRef.current += 1;
1292
1355
  versionRef.current += 1;
1293
1356
  cursorRef.current = 0;
1294
1357
  promotedRef.current.clear();
1295
1358
  pendingRef.current.clear();
1296
1359
  setPendingMessages([]);
1297
1360
  setMessageQueue(null);
1361
+ }, []);
1362
+ React3.useEffect(() => {
1363
+ const initial = initialAdmissionRef.current;
1364
+ const adopting = Boolean(initial?.threadId && initial.threadId === threadId && initial.isCurrent());
1365
+ if (adopting) {
1366
+ for (const [id, message] of pendingRef.current) pendingRef.current.set(id, { ...message, thread_id: threadId });
1367
+ initialAdmissionRef.current = null;
1368
+ } else {
1369
+ initial?.resolve(null);
1370
+ initialAdmissionRef.current = null;
1371
+ pendingRef.current.clear();
1372
+ setPendingMessages([]);
1373
+ }
1374
+ versionRef.current += 1;
1375
+ cursorRef.current = 0;
1376
+ promotedRef.current.clear();
1377
+ setMessageQueue(null);
1298
1378
  if (threadId) void refresh(threadId).catch(() => void 0);
1379
+ return () => {
1380
+ initialAdmissionRef.current?.resolve(null);
1381
+ };
1299
1382
  }, [threadId, refresh]);
1300
- return { pendingMessages, messageQueue, refresh, hydrate, acceptNotice, beginSubmission, discardSubmission, submit, edit, remove, retractForEdit, resume };
1383
+ return {
1384
+ pendingMessages,
1385
+ messageQueue,
1386
+ refresh,
1387
+ hydrate,
1388
+ acceptNotice,
1389
+ beginSubmission,
1390
+ discardSubmission,
1391
+ reset,
1392
+ confirmThreadAdmission,
1393
+ hasPendingAdmission: () => Boolean(initialAdmissionRef.current?.isCurrent()),
1394
+ waitForInitialAdmission: () => {
1395
+ const initial = initialAdmissionRef.current;
1396
+ return initial ? initial.done.then((threadId2) => {
1397
+ if (!threadId2) throw initial.failure || Object.assign(new Error("Initial message admission detached."), { name: "AbortError" });
1398
+ return threadId2;
1399
+ }) : Promise.resolve(threadRef.current);
1400
+ },
1401
+ submit,
1402
+ edit,
1403
+ remove,
1404
+ retractForEdit,
1405
+ resume
1406
+ };
1301
1407
  }
1302
1408
  function optimisticSubmissionTurn(message) {
1303
1409
  const user = {
@@ -1573,8 +1679,6 @@ function useAgentChat(options = {}) {
1573
1679
  const promotedAttachRef = React7.useRef(null);
1574
1680
  const olderRequestRef = React7.useRef(null);
1575
1681
  const submissionGenerationRef = React7.useRef(0);
1576
- const firstSubmissionRef = React7.useRef(null);
1577
- const firstSubmissionAdmittedRef = React7.useRef(null);
1578
1682
  const stateRef = React7.useRef(state);
1579
1683
  stateRef.current = state;
1580
1684
  const ownOperation = React7.useCallback((options2) => captureChatOperation(() => ({
@@ -1669,12 +1773,11 @@ function useAgentChat(options = {}) {
1669
1773
  promotedAttachRef.current = null;
1670
1774
  promotedRunHintRef.current = null;
1671
1775
  olderRequestRef.current = null;
1672
- firstSubmissionRef.current = null;
1673
- firstSubmissionAdmittedRef.current = null;
1776
+ queue.reset();
1674
1777
  setIsSubmitting(false);
1675
1778
  setIsLoadingOlder(false);
1676
1779
  setIsLoadingHistory(false);
1677
- }, []);
1780
+ }, [queue.reset]);
1678
1781
  const detach = React7.useCallback(() => {
1679
1782
  eventBuffer.flush();
1680
1783
  streamControllerRef.current?.abort();
@@ -1794,12 +1897,12 @@ function useAgentChat(options = {}) {
1794
1897
  const consumeEvent = React7.useCallback((event) => {
1795
1898
  const eventThreadId = typeof event.payload?.thread_id === "string" ? event.payload.thread_id.trim() : "";
1796
1899
  if (eventThreadId && !stateRef.current.activeThreadId) {
1900
+ queue.confirmThreadAdmission(eventThreadId);
1797
1901
  update((current) => adoptStreamingThread(current, eventThreadId));
1798
1902
  setIsSubmitting(false);
1799
- firstSubmissionAdmittedRef.current?.();
1800
1903
  }
1801
1904
  eventBuffer.push(event);
1802
- }, [eventBuffer, update]);
1905
+ }, [eventBuffer, update, queue.confirmThreadAdmission]);
1803
1906
  const attachRun = React7.useCallback(async (runId, cursor) => {
1804
1907
  eventBuffer.flush();
1805
1908
  streamControllerRef.current?.abort();
@@ -1829,12 +1932,9 @@ function useAgentChat(options = {}) {
1829
1932
  attachRunRef.current = attachRun;
1830
1933
  const submit = React7.useCallback(async (input) => {
1831
1934
  if (!input.text.trim() && !input.attachmentIds?.length && !input.uploads?.length) return;
1832
- if (firstSubmissionRef.current && !stateRef.current.activeThreadId) {
1833
- await firstSubmissionRef.current;
1834
- }
1835
1935
  const existingThreadId = stateRef.current.activeThreadId;
1836
- if (existingThreadId) {
1837
- const isCurrent = ownOperation();
1936
+ if (existingThreadId || queue.hasPendingAdmission()) {
1937
+ const isCurrent = ownOperation({ adoptThread: !existingThreadId });
1838
1938
  const submissionGeneration = ++submissionGenerationRef.current;
1839
1939
  clearOperationError();
1840
1940
  setIsSubmitting(true);
@@ -1863,7 +1963,7 @@ function useAgentChat(options = {}) {
1863
1963
  prepare: async () => {
1864
1964
  const uploaded = input.uploads?.length ? await client.attachments.uploadBatch({
1865
1965
  uploads: input.uploads,
1866
- threadId: existingThreadId,
1966
+ threadId: existingThreadId || void 0,
1867
1967
  requestedAgentId: input.requestedAgentId,
1868
1968
  idempotencyKey: runtime.createId()
1869
1969
  }) : [];
@@ -1888,9 +1988,7 @@ function useAgentChat(options = {}) {
1888
1988
  return;
1889
1989
  }
1890
1990
  invalidateThreadOpen();
1891
- const admission = new Promise((resolve) => {
1892
- firstSubmissionAdmittedRef.current = resolve;
1893
- });
1991
+ const submissionOwner = ownOperation({ adoptThread: true });
1894
1992
  const stream = (async () => {
1895
1993
  detach();
1896
1994
  const controller = runtime.createAbortController();
@@ -1971,7 +2069,7 @@ function useAgentChat(options = {}) {
1971
2069
  } catch (error) {
1972
2070
  if (!isCurrent()) return;
1973
2071
  const admitted = Boolean(stateRef.current.messages.find((message) => message.id === userId)?.runId);
1974
- if (!admitted) queue.discardSubmission(ticket);
2072
+ if (!admitted) queue.discardSubmission(ticket, error);
1975
2073
  if (!isAbortLike(error)) {
1976
2074
  update((current) => ({
1977
2075
  ...current,
@@ -1982,19 +2080,15 @@ function useAgentChat(options = {}) {
1982
2080
  throw error;
1983
2081
  }
1984
2082
  } finally {
2083
+ if (!stateRef.current.messages.some((message) => (message.id === ticket.id || message.clientSubmissionId === ticket.client_submission_id) && message.runId)) queue.discardSubmission(ticket);
1985
2084
  if (isCurrent() && submissionGeneration === submissionGenerationRef.current) setIsSubmitting(false);
1986
2085
  if (streamControllerRef.current === controller) streamControllerRef.current = null;
1987
2086
  }
1988
2087
  })();
1989
- const accepted = Promise.race([stream, admission]);
1990
- firstSubmissionRef.current = accepted;
1991
2088
  try {
1992
- await accepted;
1993
- } finally {
1994
- if (firstSubmissionRef.current === accepted) {
1995
- firstSubmissionRef.current = null;
1996
- firstSubmissionAdmittedRef.current = null;
1997
- }
2089
+ await Promise.race([stream, queue.waitForInitialAdmission()]);
2090
+ } catch (error) {
2091
+ if (submissionOwner() || !isAbortLike(error)) throw error;
1998
2092
  }
1999
2093
  }, [clearOperationError, client, consumeEvent, detach, eventBuffer, failOperation, invalidateThreadOpen, queue.submit, refreshMessages, refreshThreads, runtime, update, ownOperation]);
2000
2094
  const { steer, editPendingMessage, steerPendingMessage, removePendingMessage, resumeQueue } = useAgentChatQueueActions({