@agents24/react 0.2.15 → 0.2.16

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
@@ -1098,12 +1098,14 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onUnknownPromo
1098
1098
  const [pendingMessages, setPendingMessages] = React3.useState([]);
1099
1099
  const [messageQueue, setMessageQueue] = React3.useState(null);
1100
1100
  const pendingRef = React3.useRef(/* @__PURE__ */ new Map());
1101
+ const admissionRef = React3.useRef(null);
1102
+ const initialAdmissionRef = React3.useRef(null);
1101
1103
  const versionRef = React3.useRef(0);
1102
1104
  const cursorRef = React3.useRef(0);
1103
1105
  const promotedRef = React3.useRef(/* @__PURE__ */ new Set());
1104
1106
  const threadRef = React3.useRef(threadId);
1105
1107
  const selectionGenerationRef = React3.useRef(0);
1106
- if (threadRef.current !== threadId) selectionGenerationRef.current += 1;
1108
+ if (threadRef.current !== threadId && initialAdmissionRef.current?.threadId !== threadId) selectionGenerationRef.current += 1;
1107
1109
  const transportRef = React3.useRef(transport);
1108
1110
  const onPromotedRef = React3.useRef(onPromoted);
1109
1111
  threadRef.current = threadId;
@@ -1113,10 +1115,16 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onUnknownPromo
1113
1115
  ownSelectionRef.current = ownSelection;
1114
1116
  const callbacksRef = React3.useRef({ onOptimistic, onDiscard, onUnknownPromotion });
1115
1117
  callbacksRef.current = { onOptimistic, onDiscard, onUnknownPromotion };
1116
- const ownOperation = React3.useCallback(() => ownSelectionRef.current?.() ?? captureChatOperation(() => ({
1118
+ const ownOperation = React3.useCallback((options) => ownSelectionRef.current?.(options) ?? captureChatOperation(() => ({
1117
1119
  generation: selectionGenerationRef.current,
1118
1120
  threadId: threadRef.current
1119
- })), []);
1121
+ }), options), []);
1122
+ const confirmThreadAdmission = React3.useCallback((admittedThreadId) => {
1123
+ const initial = initialAdmissionRef.current;
1124
+ if (!initial || !initial.isCurrent()) return;
1125
+ initial.threadId = admittedThreadId;
1126
+ initial.resolve(admittedThreadId);
1127
+ }, []);
1120
1128
  const assertCurrent = (isCurrent) => {
1121
1129
  if (!isCurrent()) throw Object.assign(new Error("Thread selection changed."), { name: "AbortError" });
1122
1130
  };
@@ -1195,12 +1203,24 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onUnknownPromo
1195
1203
  updated_at: now
1196
1204
  };
1197
1205
  pendingRef.current.set(message.id, { ...message, optimisticQueued: queued });
1206
+ if (!queued && !threadRef.current && !initialAdmissionRef.current) {
1207
+ let resolve;
1208
+ const done = new Promise((callback) => {
1209
+ resolve = callback;
1210
+ });
1211
+ initialAdmissionRef.current = { id: message.id, done, resolve, isCurrent: ownOperation({ adoptThread: true }) };
1212
+ }
1198
1213
  versionRef.current += 1;
1199
1214
  if (queued) setPendingMessages((current) => [...current, message]);
1200
1215
  else callbacksRef.current.onOptimistic?.(message, Boolean(input.uploading));
1201
1216
  return message;
1202
- }, []);
1203
- const discardSubmission = React3.useCallback((message) => {
1217
+ }, [ownOperation]);
1218
+ const discardSubmission = React3.useCallback((message, failure) => {
1219
+ if (initialAdmissionRef.current?.id === message.id) {
1220
+ initialAdmissionRef.current.failure = failure;
1221
+ initialAdmissionRef.current.resolve(null);
1222
+ initialAdmissionRef.current = null;
1223
+ }
1204
1224
  if (!pendingRef.current.has(message.id)) return;
1205
1225
  pendingRef.current.delete(message.id);
1206
1226
  callbacksRef.current.onDiscard?.(message);
@@ -1208,19 +1228,31 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onUnknownPromo
1208
1228
  setPendingMessages((current) => current.filter((item) => item.client_submission_id !== message.client_submission_id));
1209
1229
  }, []);
1210
1230
  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;
1231
+ const initial = !threadRef.current ? initialAdmissionRef.current : null;
1232
+ let selectedThreadId = threadRef.current;
1233
+ if (!selectedThreadId && !initial) throw new Error("No active Agent thread is available.");
1234
+ const isCurrent = ownOperation({ adoptThread: Boolean(initial) });
1235
+ const { queued: requestedQueued, prepare, attachments, uploading, ...submission } = input;
1236
+ const priorAdmission = admissionRef.current?.threadId === (selectedThreadId || "") ? admissionRef.current.done : void 0;
1237
+ const queued = Boolean(requestedQueued || priorAdmission || initial);
1215
1238
  const optimistic = beginSubmission({
1216
1239
  ...submission,
1217
1240
  attachments,
1218
1241
  uploading,
1219
1242
  clientSubmissionId: submission.clientSubmissionId || submission.idempotencyKey
1220
1243
  }, queued);
1244
+ let releaseAdmission;
1245
+ const admission = { threadId: selectedThreadId || "", done: new Promise((resolve) => {
1246
+ releaseAdmission = resolve;
1247
+ }) };
1248
+ admissionRef.current = admission;
1221
1249
  try {
1222
1250
  const prepared = prepare ? await prepare() : {};
1251
+ if (initial) selectedThreadId = await initial.done;
1252
+ if (priorAdmission) await priorAdmission;
1223
1253
  assertCurrent(isCurrent);
1254
+ if (!selectedThreadId) throw Object.assign(new Error("Initial message admission did not complete."), { name: "AbortError" });
1255
+ admission.threadId = selectedThreadId;
1224
1256
  pendingRef.current.set(optimistic.id, { ...optimistic, optimisticQueued: queued, attachment_ids: prepared.attachmentIds || optimistic.attachment_ids, attachments: prepared.attachments || attachments });
1225
1257
  const request = { ...submission, ...prepared, clientSubmissionId: optimistic.client_submission_id, threadId: selectedThreadId };
1226
1258
  const result = await retrySubmissionAdmission(() => transportRef.current.submit(request));
@@ -1253,6 +1285,11 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onUnknownPromo
1253
1285
  } catch (error) {
1254
1286
  if (isCurrent()) discardSubmission(optimistic);
1255
1287
  throw error;
1288
+ } finally {
1289
+ void Promise.resolve(priorAdmission).then(() => {
1290
+ releaseAdmission();
1291
+ if (admissionRef.current === admission) admissionRef.current = null;
1292
+ });
1256
1293
  }
1257
1294
  }, [beginSubmission, discardSubmission, refresh, ownOperation]);
1258
1295
  const edit = React3.useCallback(async (message, input) => {
@@ -1288,16 +1325,63 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onUnknownPromo
1288
1325
  await refresh(selectedThreadId);
1289
1326
  return result;
1290
1327
  }, [refresh, ownOperation]);
1291
- React3.useEffect(() => {
1328
+ const reset = React3.useCallback(() => {
1329
+ initialAdmissionRef.current?.resolve(null);
1330
+ initialAdmissionRef.current = null;
1331
+ admissionRef.current = null;
1332
+ selectionGenerationRef.current += 1;
1292
1333
  versionRef.current += 1;
1293
1334
  cursorRef.current = 0;
1294
1335
  promotedRef.current.clear();
1295
1336
  pendingRef.current.clear();
1296
1337
  setPendingMessages([]);
1297
1338
  setMessageQueue(null);
1339
+ }, []);
1340
+ React3.useEffect(() => {
1341
+ const initial = initialAdmissionRef.current;
1342
+ const adopting = Boolean(initial?.threadId && initial.threadId === threadId && initial.isCurrent());
1343
+ if (adopting) {
1344
+ for (const [id, message] of pendingRef.current) pendingRef.current.set(id, { ...message, thread_id: threadId });
1345
+ initialAdmissionRef.current = null;
1346
+ } else {
1347
+ initial?.resolve(null);
1348
+ initialAdmissionRef.current = null;
1349
+ pendingRef.current.clear();
1350
+ setPendingMessages([]);
1351
+ }
1352
+ versionRef.current += 1;
1353
+ cursorRef.current = 0;
1354
+ promotedRef.current.clear();
1355
+ setMessageQueue(null);
1298
1356
  if (threadId) void refresh(threadId).catch(() => void 0);
1357
+ return () => {
1358
+ initialAdmissionRef.current?.resolve(null);
1359
+ };
1299
1360
  }, [threadId, refresh]);
1300
- return { pendingMessages, messageQueue, refresh, hydrate, acceptNotice, beginSubmission, discardSubmission, submit, edit, remove, retractForEdit, resume };
1361
+ return {
1362
+ pendingMessages,
1363
+ messageQueue,
1364
+ refresh,
1365
+ hydrate,
1366
+ acceptNotice,
1367
+ beginSubmission,
1368
+ discardSubmission,
1369
+ reset,
1370
+ confirmThreadAdmission,
1371
+ hasPendingAdmission: () => Boolean(initialAdmissionRef.current?.isCurrent()),
1372
+ waitForInitialAdmission: () => {
1373
+ const initial = initialAdmissionRef.current;
1374
+ return initial ? initial.done.then((threadId2) => {
1375
+ if (!threadId2) throw initial.failure || Object.assign(new Error("Initial message admission detached."), { name: "AbortError" });
1376
+ return threadId2;
1377
+ }) : Promise.resolve(threadRef.current);
1378
+ },
1379
+ submit,
1380
+ edit,
1381
+ remove,
1382
+ retractForEdit,
1383
+ resume
1384
+ };
1301
1385
  }
1302
1386
  function optimisticSubmissionTurn(message) {
1303
1387
  const user = {
@@ -1573,8 +1657,6 @@ function useAgentChat(options = {}) {
1573
1657
  const promotedAttachRef = React7.useRef(null);
1574
1658
  const olderRequestRef = React7.useRef(null);
1575
1659
  const submissionGenerationRef = React7.useRef(0);
1576
- const firstSubmissionRef = React7.useRef(null);
1577
- const firstSubmissionAdmittedRef = React7.useRef(null);
1578
1660
  const stateRef = React7.useRef(state);
1579
1661
  stateRef.current = state;
1580
1662
  const ownOperation = React7.useCallback((options2) => captureChatOperation(() => ({
@@ -1669,12 +1751,11 @@ function useAgentChat(options = {}) {
1669
1751
  promotedAttachRef.current = null;
1670
1752
  promotedRunHintRef.current = null;
1671
1753
  olderRequestRef.current = null;
1672
- firstSubmissionRef.current = null;
1673
- firstSubmissionAdmittedRef.current = null;
1754
+ queue.reset();
1674
1755
  setIsSubmitting(false);
1675
1756
  setIsLoadingOlder(false);
1676
1757
  setIsLoadingHistory(false);
1677
- }, []);
1758
+ }, [queue.reset]);
1678
1759
  const detach = React7.useCallback(() => {
1679
1760
  eventBuffer.flush();
1680
1761
  streamControllerRef.current?.abort();
@@ -1794,12 +1875,12 @@ function useAgentChat(options = {}) {
1794
1875
  const consumeEvent = React7.useCallback((event) => {
1795
1876
  const eventThreadId = typeof event.payload?.thread_id === "string" ? event.payload.thread_id.trim() : "";
1796
1877
  if (eventThreadId && !stateRef.current.activeThreadId) {
1878
+ queue.confirmThreadAdmission(eventThreadId);
1797
1879
  update((current) => adoptStreamingThread(current, eventThreadId));
1798
1880
  setIsSubmitting(false);
1799
- firstSubmissionAdmittedRef.current?.();
1800
1881
  }
1801
1882
  eventBuffer.push(event);
1802
- }, [eventBuffer, update]);
1883
+ }, [eventBuffer, update, queue.confirmThreadAdmission]);
1803
1884
  const attachRun = React7.useCallback(async (runId, cursor) => {
1804
1885
  eventBuffer.flush();
1805
1886
  streamControllerRef.current?.abort();
@@ -1829,12 +1910,9 @@ function useAgentChat(options = {}) {
1829
1910
  attachRunRef.current = attachRun;
1830
1911
  const submit = React7.useCallback(async (input) => {
1831
1912
  if (!input.text.trim() && !input.attachmentIds?.length && !input.uploads?.length) return;
1832
- if (firstSubmissionRef.current && !stateRef.current.activeThreadId) {
1833
- await firstSubmissionRef.current;
1834
- }
1835
1913
  const existingThreadId = stateRef.current.activeThreadId;
1836
- if (existingThreadId) {
1837
- const isCurrent = ownOperation();
1914
+ if (existingThreadId || queue.hasPendingAdmission()) {
1915
+ const isCurrent = ownOperation({ adoptThread: !existingThreadId });
1838
1916
  const submissionGeneration = ++submissionGenerationRef.current;
1839
1917
  clearOperationError();
1840
1918
  setIsSubmitting(true);
@@ -1863,7 +1941,7 @@ function useAgentChat(options = {}) {
1863
1941
  prepare: async () => {
1864
1942
  const uploaded = input.uploads?.length ? await client.attachments.uploadBatch({
1865
1943
  uploads: input.uploads,
1866
- threadId: existingThreadId,
1944
+ threadId: existingThreadId || void 0,
1867
1945
  requestedAgentId: input.requestedAgentId,
1868
1946
  idempotencyKey: runtime.createId()
1869
1947
  }) : [];
@@ -1888,9 +1966,7 @@ function useAgentChat(options = {}) {
1888
1966
  return;
1889
1967
  }
1890
1968
  invalidateThreadOpen();
1891
- const admission = new Promise((resolve) => {
1892
- firstSubmissionAdmittedRef.current = resolve;
1893
- });
1969
+ const submissionOwner = ownOperation({ adoptThread: true });
1894
1970
  const stream = (async () => {
1895
1971
  detach();
1896
1972
  const controller = runtime.createAbortController();
@@ -1971,7 +2047,7 @@ function useAgentChat(options = {}) {
1971
2047
  } catch (error) {
1972
2048
  if (!isCurrent()) return;
1973
2049
  const admitted = Boolean(stateRef.current.messages.find((message) => message.id === userId)?.runId);
1974
- if (!admitted) queue.discardSubmission(ticket);
2050
+ if (!admitted) queue.discardSubmission(ticket, error);
1975
2051
  if (!isAbortLike(error)) {
1976
2052
  update((current) => ({
1977
2053
  ...current,
@@ -1982,19 +2058,15 @@ function useAgentChat(options = {}) {
1982
2058
  throw error;
1983
2059
  }
1984
2060
  } finally {
2061
+ if (!stateRef.current.messages.some((message) => (message.id === ticket.id || message.clientSubmissionId === ticket.client_submission_id) && message.runId)) queue.discardSubmission(ticket);
1985
2062
  if (isCurrent() && submissionGeneration === submissionGenerationRef.current) setIsSubmitting(false);
1986
2063
  if (streamControllerRef.current === controller) streamControllerRef.current = null;
1987
2064
  }
1988
2065
  })();
1989
- const accepted = Promise.race([stream, admission]);
1990
- firstSubmissionRef.current = accepted;
1991
2066
  try {
1992
- await accepted;
1993
- } finally {
1994
- if (firstSubmissionRef.current === accepted) {
1995
- firstSubmissionRef.current = null;
1996
- firstSubmissionAdmittedRef.current = null;
1997
- }
2067
+ await Promise.race([stream, queue.waitForInitialAdmission()]);
2068
+ } catch (error) {
2069
+ if (submissionOwner() || !isAbortLike(error)) throw error;
1998
2070
  }
1999
2071
  }, [clearOperationError, client, consumeEvent, detach, eventBuffer, failOperation, invalidateThreadOpen, queue.submit, refreshMessages, refreshThreads, runtime, update, ownOperation]);
2000
2072
  const { steer, editPendingMessage, steerPendingMessage, removePendingMessage, resumeQueue } = useAgentChatQueueActions({