@copilotz/chat-adapter 0.9.43 → 0.9.45

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
@@ -101,6 +101,38 @@ var User = createLucideIcon("user", __iconNode);
101
101
  // src/useCopilotzChat.ts
102
102
  import { useState as useState2, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect2 } from "react";
103
103
 
104
+ // src/streamEvents.ts
105
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
106
+ var hasValues = (value) => Array.isArray(value) && value.length > 0;
107
+ var getStreamEventPayload = (event) => {
108
+ if (!isRecord(event)) return event;
109
+ return "payload" in event ? event.payload : event;
110
+ };
111
+ var getLlmAttemptId = (event) => {
112
+ if (!isRecord(event)) return null;
113
+ const metadata = isRecord(event.metadata) ? event.metadata : {};
114
+ if (typeof metadata.llmAttemptId === "string" && metadata.llmAttemptId.trim().length > 0) {
115
+ return metadata.llmAttemptId.trim();
116
+ }
117
+ if (event.subjectType === "llm_attempt" && typeof event.subjectId === "string" && event.subjectId.trim().length > 0) {
118
+ return event.subjectId.trim();
119
+ }
120
+ return null;
121
+ };
122
+ var isTerminalEmptyLlmResultEvent = (event) => {
123
+ if (!isRecord(event) || event.type !== "LLM_RESULT") return false;
124
+ const payload = getStreamEventPayload(event);
125
+ if (!isRecord(payload)) return false;
126
+ if (payload.answer !== "") return false;
127
+ if (payload.finishReason === "tool_calls") return false;
128
+ if (hasValues(payload.toolCalls)) return false;
129
+ const metadata = isRecord(event.metadata) ? event.metadata : {};
130
+ const routing = isRecord(metadata.routing) ? metadata.routing : {};
131
+ if ((routing.action === "ask" || routing.action === "handoff") && typeof routing.targetId === "string" && routing.targetId.trim().length > 0) return false;
132
+ if (hasValues(metadata.targetQueue)) return false;
133
+ return true;
134
+ };
135
+
104
136
  // src/copilotzService.ts
105
137
  var rawBaseValue = import.meta.env?.VITE_API_URL;
106
138
  var rawBase = typeof rawBaseValue === "string" && rawBaseValue.length > 0 ? rawBaseValue : "/api";
@@ -428,6 +460,11 @@ async function runCopilotzStream(options) {
428
460
  let lastCompletedText = "";
429
461
  let lastTokenWasReasoning = false;
430
462
  let hadNonReasoningContent = false;
463
+ let activeLlmAttemptId = null;
464
+ let activePhaseKind = null;
465
+ let activePhaseId = null;
466
+ let phaseOrdinal = -1;
467
+ let fallbackAttemptOrdinal = 0;
431
468
  const collectedMessages = [];
432
469
  let collectedMedia = null;
433
470
  const resetTokenAggregation = () => {
@@ -436,6 +473,30 @@ async function runCopilotzStream(options) {
436
473
  lastTokenWasReasoning = false;
437
474
  hadNonReasoningContent = false;
438
475
  };
476
+ const startAttempt = (event) => {
477
+ activeLlmAttemptId = getLlmAttemptId(event) ?? `stream-attempt:${fallbackAttemptOrdinal++}`;
478
+ activePhaseKind = null;
479
+ activePhaseId = null;
480
+ phaseOrdinal = -1;
481
+ resetTokenAggregation();
482
+ };
483
+ const getTokenContext = (isReasoning) => {
484
+ if (!activeLlmAttemptId) {
485
+ startAttempt(null);
486
+ }
487
+ const kind = isReasoning ? "reasoning" : "answer";
488
+ if (activePhaseKind !== kind || !activePhaseId) {
489
+ phaseOrdinal += 1;
490
+ activePhaseKind = kind;
491
+ activePhaseId = `${activeLlmAttemptId}:${kind}:${phaseOrdinal}`;
492
+ }
493
+ return {
494
+ isReasoning,
495
+ llmAttemptId: activeLlmAttemptId,
496
+ phaseId: activePhaseId,
497
+ phaseOrdinal
498
+ };
499
+ };
439
500
  const processEvent = async (eventChunk) => {
440
501
  if (!eventChunk.trim()) return;
441
502
  const lines = eventChunk.split("\n");
@@ -461,10 +522,16 @@ async function runCopilotzStream(options) {
461
522
  return;
462
523
  }
463
524
  switch (eventType) {
525
+ case "LLM_CALL": {
526
+ startAttempt(payload2);
527
+ await onMessageEvent?.(payload2);
528
+ break;
529
+ }
464
530
  case "TOKEN": {
465
531
  const inner = payload2?.payload ?? payload2;
466
532
  const chunk = typeof inner?.token === "string" ? inner.token : "";
467
- const isReasoning = Boolean(inner?.isReasoning);
533
+ const isReasoning = typeof inner?.isReasoning === "boolean" ? inner.isReasoning : activePhaseKind === "reasoning" || lastTokenWasReasoning;
534
+ const tokenContext = getTokenContext(isReasoning);
468
535
  if (isReasoning && !lastTokenWasReasoning && hadNonReasoningContent) {
469
536
  aggregatedReasoning = "";
470
537
  aggregatedText = "";
@@ -482,12 +549,14 @@ async function runCopilotzStream(options) {
482
549
  const isComplete = Boolean(inner?.isComplete);
483
550
  if (chunk || isComplete) {
484
551
  const tokenText = isReasoning ? aggregatedReasoning : aggregatedText;
485
- onToken?.(tokenText, isComplete, payload2, { isReasoning });
552
+ onToken?.(tokenText, isComplete, payload2, tokenContext);
486
553
  if (isComplete) {
487
554
  if (!isReasoning && tokenText) {
488
555
  lastCompletedText = tokenText;
489
556
  }
490
557
  resetTokenAggregation();
558
+ activePhaseKind = null;
559
+ activePhaseId = null;
491
560
  }
492
561
  }
493
562
  break;
@@ -833,8 +902,6 @@ function useUrlState(config = {}) {
833
902
  }
834
903
 
835
904
  // src/activity.ts
836
- var thinkingId = "thinking";
837
- var answeringId = "answering";
838
905
  var getItems = (message) => Array.isArray(message.activity?.items) ? message.activity.items : [];
839
906
  var setItems = (message, items) => ({
840
907
  ...message,
@@ -860,7 +927,8 @@ var upsertItem = (message, item) => {
860
927
  };
861
928
  return setItems(message, next);
862
929
  };
863
- var completeItems = (message, shouldComplete) => setItems(message, getItems(message).map((item) => item.status === "active" && shouldComplete(item) ? { ...item, status: "complete", completedAt: Date.now() } : item));
930
+ var completeItems = (message, shouldComplete, completedAt = Date.now()) => setItems(message, getItems(message).map((item) => item.status === "active" && shouldComplete(item) ? { ...item, status: "complete", completedAt } : item));
931
+ var removeItems = (message, shouldRemove) => setItems(message, getItems(message).filter((item) => !shouldRemove(item)));
864
932
  var hasVisibleAssistantOutput = (message) => {
865
933
  if (message.role !== "assistant") return false;
866
934
  if (typeof message.content === "string" && message.content.trim().length > 0) return true;
@@ -875,38 +943,40 @@ var toPublicChatMessage = (message) => {
875
943
  var updateAssistantMessageToken = (message, params) => {
876
944
  if (message.role !== "assistant") return message;
877
945
  if (params.isReasoning) {
878
- return upsertItem({
946
+ const activityId2 = params.activityId ?? `${message.id}:thinking`;
947
+ return upsertItem(completeItems(removeItems({
879
948
  ...message,
880
949
  isStreaming: true,
881
950
  isComplete: false
882
- }, {
883
- id: thinkingId,
951
+ }, (item) => item.kind === "answering"), (item) => item.id !== activityId2 && (item.kind === "thinking" || item.kind === "answering"), params.at), {
952
+ id: activityId2,
884
953
  kind: "thinking",
885
954
  status: "active",
886
- startedAt: getItems(message).find((item) => item.id === thinkingId)?.startedAt ?? Date.now(),
955
+ startedAt: getItems(message).find((item) => item.id === activityId2)?.startedAt ?? params.at ?? Date.now(),
887
956
  details: { reasoning: params.partial }
888
957
  });
889
958
  }
890
- return upsertItem(completeItems({
959
+ const activityId = params.activityId ?? `${message.id}:answering`;
960
+ return upsertItem(completeItems(removeItems({
891
961
  ...message,
892
962
  content: params.partial,
893
963
  isStreaming: true,
894
964
  isComplete: false
895
- }, (item) => item.kind === "thinking" || item.kind === "tool"), {
896
- id: answeringId,
965
+ }, (item) => item.kind === "answering" && item.id !== activityId), (item) => item.kind === "thinking" || item.kind === "answering", params.at), {
966
+ id: activityId,
897
967
  kind: "answering",
898
968
  status: "active",
899
- startedAt: getItems(message).find((item) => item.id === answeringId)?.startedAt ?? Date.now()
969
+ startedAt: getItems(message).find((item) => item.id === activityId)?.startedAt ?? params.at ?? Date.now()
900
970
  });
901
971
  };
902
972
  var appendAssistantToolCall = (message, toolCall) => {
903
973
  if (message.role !== "assistant") return message;
904
974
  const status = toolStatusToActivityStatus(toolCall.status);
905
- return upsertItem({
975
+ return upsertItem(completeItems(removeItems({
906
976
  ...message,
907
977
  isStreaming: true,
908
978
  isComplete: false
909
- }, {
979
+ }, (item) => item.kind === "answering"), (item) => item.kind === "thinking" || item.kind === "answering"), {
910
980
  id: toolCall.id,
911
981
  kind: "tool",
912
982
  status,
@@ -955,23 +1025,23 @@ var applyAssistantToolResult = (message, update) => {
955
1025
  };
956
1026
  return setItems(message, next);
957
1027
  };
958
- var finalizeAssistantMessage = (message, finalAnswer) => {
1028
+ var finalizeAssistantMessage = (message, finalAnswer, completedAt = Date.now()) => {
959
1029
  if (message.role !== "assistant") return message;
960
1030
  const completed = completeItems({
961
1031
  ...message,
962
1032
  ...typeof finalAnswer === "string" && finalAnswer.length > 0 ? { content: finalAnswer } : {},
963
1033
  isStreaming: false,
964
1034
  isComplete: true
965
- }, (item) => item.kind === "thinking" || item.kind === "answering");
1035
+ }, (item) => item.kind === "thinking" || item.kind === "answering", completedAt);
966
1036
  return setItems(completed, getItems(completed).filter((item) => item.kind !== "answering"));
967
1037
  };
968
- var closeAssistantMessage = (message) => {
1038
+ var closeAssistantMessage = (message, completedAt = Date.now()) => {
969
1039
  if (message.role !== "assistant") return message;
970
1040
  return completeItems({
971
1041
  ...message,
972
1042
  isStreaming: false,
973
1043
  isComplete: true
974
- }, () => true);
1044
+ }, () => true, completedAt);
975
1045
  };
976
1046
 
977
1047
  // src/contract.ts
@@ -981,9 +1051,9 @@ var ContractViolation = class extends Error {
981
1051
  this.name = "ContractViolation";
982
1052
  }
983
1053
  };
984
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1054
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
985
1055
  var expectRecord = (value, path) => {
986
- if (!isRecord(value)) throw new ContractViolation(`${path} must be an object`);
1056
+ if (!isRecord2(value)) throw new ContractViolation(`${path} must be an object`);
987
1057
  return value;
988
1058
  };
989
1059
  var expectString = (value, path) => {
@@ -1104,14 +1174,14 @@ var resolveLiveEventSender = (event, options = {}) => {
1104
1174
  const raw = expectRecord(event, "stream event");
1105
1175
  const payload = raw.payload === void 0 ? raw : expectRecord(raw.payload, "stream event.payload");
1106
1176
  const agent = payload.agent ?? raw.agent;
1107
- if (isRecord(agent)) {
1177
+ if (isRecord2(agent)) {
1108
1178
  return resolveAgentSender({
1109
1179
  id: expectString(agent.id, "stream event.payload.agent.id"),
1110
1180
  name: expectString(agent.name, "stream event.payload.agent.name")
1111
1181
  }, options);
1112
1182
  }
1113
1183
  const sender = payload.sender ?? raw.sender;
1114
- if (!isRecord(sender)) {
1184
+ if (!isRecord2(sender)) {
1115
1185
  throw new ContractViolation("stream event sender contract requires payload.agent or payload.sender");
1116
1186
  }
1117
1187
  const type = expectSenderType(sender.type ?? payload.senderType, "stream event.payload.sender.type");
@@ -1241,6 +1311,98 @@ async function resolveAssetsInMessages(messages, getRequestHeaders) {
1241
1311
  }));
1242
1312
  }
1243
1313
 
1314
+ // src/messageReconciliation.ts
1315
+ var CLIENT_MESSAGE_ID_METADATA_KEY = "clientMessageId";
1316
+ var LLM_ATTEMPT_ID_METADATA_KEY = "llmAttemptId";
1317
+ var stringifyForCompare = (value) => {
1318
+ if (value === void 0) return "";
1319
+ try {
1320
+ return JSON.stringify(value) ?? "";
1321
+ } catch {
1322
+ return String(value);
1323
+ }
1324
+ };
1325
+ var getMessageSignature = (message) => JSON.stringify({
1326
+ id: message.id,
1327
+ role: message.role,
1328
+ content: message.content ?? "",
1329
+ isStreaming: message.isStreaming === true,
1330
+ isComplete: message.isComplete === true,
1331
+ attachments: stringifyForCompare(message.attachments),
1332
+ activity: stringifyForCompare(message.activity),
1333
+ metadata: stringifyForCompare(message.metadata),
1334
+ sender: message.sender ? {
1335
+ id: message.sender.id,
1336
+ type: message.sender.type,
1337
+ name: message.sender.name,
1338
+ agentId: message.sender.agentId
1339
+ } : null
1340
+ });
1341
+ var getMetadataString = (message, key) => {
1342
+ const value = message.metadata?.[key];
1343
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
1344
+ };
1345
+ var getCorrelationKeys = (message) => {
1346
+ const clientMessageId = getMetadataString(
1347
+ message,
1348
+ CLIENT_MESSAGE_ID_METADATA_KEY
1349
+ );
1350
+ const llmAttemptId = getMetadataString(
1351
+ message,
1352
+ LLM_ATTEMPT_ID_METADATA_KEY
1353
+ );
1354
+ return [
1355
+ ...clientMessageId ? [`client-message:${message.role}:${clientMessageId}`] : [],
1356
+ ...llmAttemptId ? [`llm-attempt:${message.role}:${llmAttemptId}`] : []
1357
+ ];
1358
+ };
1359
+ var indexUniqueFreshCorrelations = (freshMessages) => {
1360
+ const messagesByCorrelation = /* @__PURE__ */ new Map();
1361
+ for (const message of freshMessages) {
1362
+ for (const key of getCorrelationKeys(message)) {
1363
+ messagesByCorrelation.set(
1364
+ key,
1365
+ messagesByCorrelation.has(key) ? null : message
1366
+ );
1367
+ }
1368
+ }
1369
+ return messagesByCorrelation;
1370
+ };
1371
+ var reconcileThreadMessages = (currentMessages, freshMessages) => {
1372
+ if (freshMessages.length === 0) {
1373
+ return { messages: currentMessages, changed: false };
1374
+ }
1375
+ if (currentMessages.length === 0) {
1376
+ return { messages: freshMessages, changed: true };
1377
+ }
1378
+ const freshById = new Map(freshMessages.map((message) => [message.id, message]));
1379
+ const freshByCorrelation = indexUniqueFreshCorrelations(freshMessages);
1380
+ const seen = /* @__PURE__ */ new Set();
1381
+ let changed = false;
1382
+ const nextMessages = currentMessages.map((message) => {
1383
+ const exactMatch = freshById.get(message.id);
1384
+ const correlatedMatch = exactMatch ? null : getCorrelationKeys(message).map((key) => freshByCorrelation.get(key)).find((candidate) => candidate !== null && candidate !== void 0 && !seen.has(candidate.id));
1385
+ const fresh = exactMatch ?? correlatedMatch;
1386
+ if (!fresh) return message;
1387
+ seen.add(fresh.id);
1388
+ if (getMessageSignature(message) === getMessageSignature(fresh)) {
1389
+ return message;
1390
+ }
1391
+ changed = true;
1392
+ return fresh;
1393
+ });
1394
+ const appended = freshMessages.filter((message) => !seen.has(message.id));
1395
+ if (appended.length > 0) {
1396
+ changed = true;
1397
+ nextMessages.push(...appended);
1398
+ nextMessages.sort((a, b) => {
1399
+ const timestampDelta = (a.timestamp ?? 0) - (b.timestamp ?? 0);
1400
+ return timestampDelta !== 0 ? timestampDelta : a.id.localeCompare(b.id);
1401
+ });
1402
+ }
1403
+ return changed ? { messages: nextMessages, changed } : { messages: currentMessages, changed: false };
1404
+ };
1405
+
1244
1406
  // src/toolActivity.ts
1245
1407
  var fail = (message) => {
1246
1408
  throw new ContractViolation(message);
@@ -1263,7 +1425,7 @@ var formatToolError = (error) => {
1263
1425
  }
1264
1426
  };
1265
1427
  var expectToolArguments = (value, path) => {
1266
- if (isRecord(value)) return value;
1428
+ if (isRecord2(value)) return value;
1267
1429
  if (typeof value === "string") {
1268
1430
  try {
1269
1431
  const parsed = JSON.parse(value);
@@ -1439,9 +1601,6 @@ var canAttachToStreamingAssistant = (message, incomingAgentKey) => {
1439
1601
  const currentAgentKey = messageAgentKey(message);
1440
1602
  return !incomingAgentKey || !currentAgentKey || currentAgentKey === incomingAgentKey;
1441
1603
  };
1442
- var canAttachToCurrentStreamingAssistant = (message) => {
1443
- return !!message && message.role === "assistant" && message.isStreaming === true;
1444
- };
1445
1604
 
1446
1605
  // src/messageContract.ts
1447
1606
  var isInternalMessageMetadata = (metadata) => metadata?.visibility === "internal";
@@ -1548,9 +1707,10 @@ var convertServerMessage = (msg, options = {}) => {
1548
1707
  const isToolSender = msg.senderType === "tool";
1549
1708
  const content = isToolSender ? "" : messageContent;
1550
1709
  const reasoning = typeof msg.reasoning === "string" && msg.reasoning.length > 0 ? msg.reasoning : void 0;
1710
+ const llmAttemptId = typeof metadata?.[LLM_ATTEMPT_ID_METADATA_KEY] === "string" ? metadata[LLM_ATTEMPT_ID_METADATA_KEY].trim() : "";
1551
1711
  const activityItems = [
1552
1712
  ...reasoning ? [{
1553
- id: `${msg.id}:thinking`,
1713
+ id: `${llmAttemptId || msg.id}:reasoning:0`,
1554
1714
  kind: "thinking",
1555
1715
  status: "complete",
1556
1716
  completedAt: timestamp,
@@ -1631,27 +1791,6 @@ var prepareHydratedMessages = async (rawMessages, options = {}) => {
1631
1791
  };
1632
1792
  };
1633
1793
 
1634
- // src/streamEvents.ts
1635
- var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1636
- var hasValues = (value) => Array.isArray(value) && value.length > 0;
1637
- var getStreamEventPayload = (event) => {
1638
- if (!isRecord2(event)) return event;
1639
- return "payload" in event ? event.payload : event;
1640
- };
1641
- var isTerminalEmptyLlmResultEvent = (event) => {
1642
- if (!isRecord2(event) || event.type !== "LLM_RESULT") return false;
1643
- const payload = getStreamEventPayload(event);
1644
- if (!isRecord2(payload)) return false;
1645
- if (payload.answer !== "") return false;
1646
- if (payload.finishReason === "tool_calls") return false;
1647
- if (hasValues(payload.toolCalls)) return false;
1648
- const metadata = isRecord2(event.metadata) ? event.metadata : {};
1649
- const routing = isRecord2(metadata.routing) ? metadata.routing : {};
1650
- if ((routing.action === "ask" || routing.action === "handoff") && typeof routing.targetId === "string" && routing.targetId.trim().length > 0) return false;
1651
- if (hasValues(metadata.targetQueue)) return false;
1652
- return true;
1653
- };
1654
-
1655
1794
  // src/threadIdentity.ts
1656
1795
  var identityValues = ({ id, externalId }) => [id, externalId].filter((value) => typeof value === "string" && value.length > 0);
1657
1796
  var isSameThreadIdentity = (owner, current) => {
@@ -1661,12 +1800,256 @@ var isSameThreadIdentity = (owner, current) => {
1661
1800
  return ownerValues.some((value) => currentValues.has(value));
1662
1801
  };
1663
1802
 
1803
+ // src/liveRun.ts
1804
+ var copyState = (state) => ({
1805
+ ...state,
1806
+ attemptsById: new Map(state.attemptsById),
1807
+ toolMessageByCallId: new Map(state.toolMessageByCallId),
1808
+ pendingToolResultsByCallId: new Map(state.pendingToolResultsByCallId)
1809
+ });
1810
+ var createLiveRunState = (initialMessageId) => ({
1811
+ initialMessageId,
1812
+ initialMessageClaimed: false,
1813
+ activeAttemptId: null,
1814
+ lastAttemptId: null,
1815
+ attemptsById: /* @__PURE__ */ new Map(),
1816
+ toolMessageByCallId: /* @__PURE__ */ new Map(),
1817
+ pendingToolResultsByCallId: /* @__PURE__ */ new Map()
1818
+ });
1819
+ var ensureAttempt = (state, attemptId, sender, at, options) => {
1820
+ const existing = state.attemptsById.get(attemptId);
1821
+ if (existing) {
1822
+ const next2 = copyState(state);
1823
+ const cursor2 = sender && existing.sender !== sender ? { ...existing, sender } : existing;
1824
+ next2.attemptsById.set(attemptId, cursor2);
1825
+ next2.activeAttemptId = attemptId;
1826
+ next2.lastAttemptId = attemptId;
1827
+ return { state: next2, cursor: cursor2, operations: [] };
1828
+ }
1829
+ const next = copyState(state);
1830
+ const messageId = next.initialMessageClaimed ? options.createId() : next.initialMessageId;
1831
+ const cursor = { messageId, sender };
1832
+ next.initialMessageClaimed = true;
1833
+ next.activeAttemptId = attemptId;
1834
+ next.lastAttemptId = attemptId;
1835
+ next.attemptsById.set(attemptId, cursor);
1836
+ return {
1837
+ state: next,
1838
+ cursor,
1839
+ operations: [{
1840
+ type: "ensure-attempt",
1841
+ messageId,
1842
+ attemptId,
1843
+ sender,
1844
+ at
1845
+ }]
1846
+ };
1847
+ };
1848
+ var transitionLiveRun = (state, action, options) => {
1849
+ if (action.type === "attempt-start") {
1850
+ const ensured = ensureAttempt(
1851
+ state,
1852
+ action.attemptId,
1853
+ action.sender,
1854
+ action.at,
1855
+ options
1856
+ );
1857
+ return { state: ensured.state, operations: ensured.operations };
1858
+ }
1859
+ if (action.type === "token") {
1860
+ const ensured = ensureAttempt(
1861
+ state,
1862
+ action.attemptId,
1863
+ action.sender,
1864
+ action.at,
1865
+ options
1866
+ );
1867
+ return {
1868
+ state: ensured.state,
1869
+ operations: [
1870
+ ...ensured.operations,
1871
+ {
1872
+ type: "update-token",
1873
+ messageId: ensured.cursor.messageId,
1874
+ phaseId: action.phaseId,
1875
+ partial: action.partial,
1876
+ isReasoning: action.isReasoning,
1877
+ sender: action.sender ?? ensured.cursor.sender,
1878
+ at: action.at
1879
+ }
1880
+ ]
1881
+ };
1882
+ }
1883
+ if (action.type === "attempt-result") {
1884
+ const cursor = state.attemptsById.get(action.attemptId);
1885
+ if (!cursor) return { state, operations: [] };
1886
+ const next = copyState(state);
1887
+ if (next.activeAttemptId === action.attemptId) {
1888
+ next.activeAttemptId = null;
1889
+ }
1890
+ next.lastAttemptId = action.attemptId;
1891
+ return {
1892
+ state: next,
1893
+ operations: [{
1894
+ type: "finalize-attempt",
1895
+ messageId: cursor.messageId,
1896
+ answer: action.answer,
1897
+ at: action.at
1898
+ }]
1899
+ };
1900
+ }
1901
+ if (action.type === "tool-call") {
1902
+ if (state.toolMessageByCallId.has(action.toolCall.id)) {
1903
+ return { state, operations: [] };
1904
+ }
1905
+ const attemptId = action.attemptId ?? state.activeAttemptId ?? state.lastAttemptId ?? `tool-attempt:${action.toolCall.id}`;
1906
+ const ensured = ensureAttempt(
1907
+ state,
1908
+ attemptId,
1909
+ action.sender,
1910
+ action.at,
1911
+ options
1912
+ );
1913
+ const next = copyState(ensured.state);
1914
+ next.toolMessageByCallId.set(action.toolCall.id, ensured.cursor.messageId);
1915
+ const pendingResult = next.pendingToolResultsByCallId.get(action.toolCall.id);
1916
+ if (pendingResult) {
1917
+ next.pendingToolResultsByCallId.delete(action.toolCall.id);
1918
+ }
1919
+ return {
1920
+ state: next,
1921
+ operations: [
1922
+ ...ensured.operations,
1923
+ {
1924
+ type: "append-tool",
1925
+ messageId: ensured.cursor.messageId,
1926
+ toolCall: action.toolCall,
1927
+ sender: action.sender ?? ensured.cursor.sender,
1928
+ at: action.at
1929
+ },
1930
+ ...pendingResult ? [{
1931
+ type: "resolve-tool",
1932
+ messageId: ensured.cursor.messageId,
1933
+ update: pendingResult
1934
+ }] : []
1935
+ ]
1936
+ };
1937
+ }
1938
+ const messageId = action.update.id ? state.toolMessageByCallId.get(action.update.id) : void 0;
1939
+ if (!messageId || !action.update.id) {
1940
+ if (!action.update.id) return { state, operations: [] };
1941
+ const next = copyState(state);
1942
+ next.pendingToolResultsByCallId.set(action.update.id, action.update);
1943
+ return { state: next, operations: [] };
1944
+ }
1945
+ return {
1946
+ state,
1947
+ operations: [{ type: "resolve-tool", messageId, update: action.update }]
1948
+ };
1949
+ };
1950
+ var applyOperation = (messages, operation) => {
1951
+ const index = messages.findIndex((message) => message.id === operation.messageId);
1952
+ if (operation.type === "ensure-attempt") {
1953
+ if (index >= 0) {
1954
+ const current2 = messages[index];
1955
+ const nextMessage = {
1956
+ ...current2,
1957
+ metadata: {
1958
+ ...current2.metadata ?? {},
1959
+ [LLM_ATTEMPT_ID_METADATA_KEY]: operation.attemptId
1960
+ },
1961
+ ...operation.sender ? { sender: operation.sender } : {}
1962
+ };
1963
+ if (current2.metadata?.[LLM_ATTEMPT_ID_METADATA_KEY] === operation.attemptId && (!operation.sender || current2.sender === operation.sender)) return messages;
1964
+ const next2 = [...messages];
1965
+ next2[index] = nextMessage;
1966
+ return next2;
1967
+ }
1968
+ return [
1969
+ ...messages,
1970
+ {
1971
+ id: operation.messageId,
1972
+ role: "assistant",
1973
+ content: "",
1974
+ timestamp: operation.at,
1975
+ isStreaming: true,
1976
+ isComplete: false,
1977
+ metadata: { [LLM_ATTEMPT_ID_METADATA_KEY]: operation.attemptId },
1978
+ activity: {
1979
+ items: [{
1980
+ id: `${operation.messageId}:pending`,
1981
+ kind: "answering",
1982
+ status: "active",
1983
+ startedAt: operation.at
1984
+ }]
1985
+ },
1986
+ ...operation.sender ? { sender: operation.sender } : {}
1987
+ }
1988
+ ];
1989
+ }
1990
+ if (index < 0) return messages;
1991
+ const current = messages[index];
1992
+ let updated;
1993
+ if (operation.type === "update-token") {
1994
+ updated = {
1995
+ ...updateAssistantMessageToken(current, {
1996
+ partial: operation.partial,
1997
+ isReasoning: operation.isReasoning,
1998
+ activityId: operation.phaseId,
1999
+ at: operation.at
2000
+ }),
2001
+ ...operation.sender ? { sender: operation.sender } : {}
2002
+ };
2003
+ } else if (operation.type === "finalize-attempt") {
2004
+ updated = finalizeAssistantMessage(current, operation.answer, operation.at);
2005
+ } else if (operation.type === "append-tool") {
2006
+ updated = {
2007
+ ...appendAssistantToolCall(current, {
2008
+ ...operation.toolCall,
2009
+ startTime: operation.toolCall.startTime ?? operation.at
2010
+ }),
2011
+ ...operation.sender ? { sender: operation.sender } : {}
2012
+ };
2013
+ } else {
2014
+ const toolItem = current.activity?.items.find((item) => item.kind === "tool" && item.id === operation.update.id);
2015
+ if (!toolItem) return messages;
2016
+ updated = applyAssistantToolResult(current, {
2017
+ ...operation.update.id ? { id: operation.update.id } : {},
2018
+ ...operation.update.toolExecutionId ? { toolExecutionId: operation.update.toolExecutionId } : {},
2019
+ name: operation.update.name ?? toolItem.toolName ?? toolItem.id,
2020
+ status: operation.update.status,
2021
+ ...operation.update.result !== void 0 ? { result: operation.update.result } : {},
2022
+ ...operation.update.error !== void 0 ? { error: operation.update.error } : {},
2023
+ endTime: operation.update.endTime
2024
+ });
2025
+ const hasActiveTool = updated.activity?.items.some((item) => item.kind === "tool" && item.status === "active") ?? false;
2026
+ updated = {
2027
+ ...updated,
2028
+ isStreaming: hasActiveTool,
2029
+ isComplete: !hasActiveTool
2030
+ };
2031
+ }
2032
+ if (updated === current) return messages;
2033
+ const next = [...messages];
2034
+ next[index] = updated;
2035
+ return next;
2036
+ };
2037
+ var applyLiveRunOperations = (messages, operations) => operations.reduce(applyOperation, messages);
2038
+ var getLatestLiveRunMessageId = (state) => {
2039
+ const attemptId = state.activeAttemptId ?? state.lastAttemptId;
2040
+ return attemptId ? state.attemptsById.get(attemptId)?.messageId ?? state.initialMessageId : state.initialMessageId;
2041
+ };
2042
+
1664
2043
  // src/useCopilotzChat.ts
1665
2044
  var nowTs = () => Date.now();
1666
2045
  var generateId = () => globalThis.crypto?.randomUUID?.() ?? `id-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
1667
2046
  var isAbortError = (error) => error instanceof DOMException && error.name === "AbortError" || typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
1668
2047
  var getEventPayload = (event) => getStreamEventPayload(event);
1669
2048
  var getEventSenderType = (payload) => payload?.senderType || payload?.sender?.type;
2049
+ var getEventTimestamp = (event) => {
2050
+ const timestamp = typeof event?.createdAt === "string" ? new Date(event.createdAt).getTime() : NaN;
2051
+ return Number.isFinite(timestamp) ? timestamp : nowTs();
2052
+ };
1670
2053
  var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1671
2054
  var normalizeThreadTag = (value) => {
1672
2055
  if (!isRecord3(value)) return null;
@@ -1695,66 +2078,11 @@ var createEmptyMessagePageInfo = () => ({
1695
2078
  oldestMessageId: null,
1696
2079
  newestMessageId: null
1697
2080
  });
1698
- var stringifyForCompare = (value) => {
1699
- if (value === void 0) return "";
1700
- try {
1701
- return JSON.stringify(value) ?? "";
1702
- } catch {
1703
- return String(value);
1704
- }
1705
- };
1706
- var getMessageSignature = (message) => JSON.stringify({
1707
- id: message.id,
1708
- role: message.role,
1709
- content: message.content ?? "",
1710
- isStreaming: message.isStreaming === true,
1711
- isComplete: message.isComplete === true,
1712
- attachments: stringifyForCompare(message.attachments),
1713
- activity: stringifyForCompare(message.activity),
1714
- metadata: stringifyForCompare(message.metadata),
1715
- sender: message.sender ? {
1716
- id: message.sender.id,
1717
- type: message.sender.type,
1718
- name: message.sender.name,
1719
- agentId: message.sender.agentId
1720
- } : null
1721
- });
1722
- var reconcileThreadMessages = (currentMessages, freshMessages) => {
1723
- if (freshMessages.length === 0) {
1724
- return { messages: currentMessages, changed: false };
1725
- }
1726
- if (currentMessages.length === 0) {
1727
- return { messages: freshMessages, changed: true };
1728
- }
1729
- const freshById = new Map(freshMessages.map((message) => [message.id, message]));
1730
- const seen = /* @__PURE__ */ new Set();
1731
- let changed = false;
1732
- const nextMessages = currentMessages.map((message) => {
1733
- const fresh = freshById.get(message.id);
1734
- if (!fresh) return message;
1735
- seen.add(message.id);
1736
- if (getMessageSignature(message) === getMessageSignature(fresh)) {
1737
- return message;
1738
- }
1739
- changed = true;
1740
- return fresh;
1741
- });
1742
- const appended = freshMessages.filter((message) => !seen.has(message.id));
1743
- if (appended.length > 0) {
1744
- changed = true;
1745
- nextMessages.push(...appended);
1746
- nextMessages.sort((a, b) => {
1747
- const timestampDelta = (a.timestamp ?? 0) - (b.timestamp ?? 0);
1748
- return timestampDelta !== 0 ? timestampDelta : a.id.localeCompare(b.id);
1749
- });
1750
- }
1751
- return changed ? { messages: nextMessages, changed } : { messages: currentMessages, changed: false };
1752
- };
1753
- var createPendingAssistantActivity = () => ({
2081
+ var createPendingAssistantActivity = (messageId) => ({
1754
2082
  items: [
1755
2083
  {
1756
- id: "thinking",
1757
- kind: "thinking",
2084
+ id: `${messageId}:pending`,
2085
+ kind: "answering",
1758
2086
  status: "active",
1759
2087
  startedAt: nowTs()
1760
2088
  }
@@ -1797,7 +2125,6 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
1797
2125
  assistantName
1798
2126
  });
1799
2127
  const persistedToolUpdatesRef = useRef2([]);
1800
- const liveToolUpdatesRef = useRef2([]);
1801
2128
  const stopRequestedRef = useRef2(false);
1802
2129
  const recoveryPollGenerationRef = useRef2(0);
1803
2130
  threadsRef.current = threads;
@@ -1993,7 +2320,6 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
1993
2320
  setIsLoadingOlderMessages(false);
1994
2321
  setMessagePageInfo(createEmptyMessagePageInfo());
1995
2322
  persistedToolUpdatesRef.current = [];
1996
- liveToolUpdatesRef.current = [];
1997
2323
  try {
1998
2324
  const page = await fetchThreadMessagesPage(threadId, { limit: THREAD_MESSAGES_PAGE_SIZE }, getRequestHeaders);
1999
2325
  const { viewMessages, toolResultUpdates } = await prepareThreadMessages(page.data);
@@ -2353,154 +2679,26 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2353
2679
  }, []);
2354
2680
  const sendCopilotzMessage = useCallback2(
2355
2681
  async (params) => {
2356
- let currentAssistantId = params.assistantMessageId ?? generateId();
2357
- let currentAssistantSender = params.assistantSender;
2682
+ const initialAssistantMessageId = params.assistantMessageId ?? generateId();
2683
+ let liveRunState = createLiveRunState(initialAssistantMessageId);
2358
2684
  const streamOwnerThreadId = currentThreadIdRef.current;
2359
2685
  const streamOwnerThreadExternalId = params.threadExternalId ?? currentThreadExternalIdRef.current;
2360
2686
  const streamStillOwnsVisibleThread = () => isSameThreadIdentity(
2361
2687
  { id: streamOwnerThreadId, externalId: streamOwnerThreadExternalId },
2362
2688
  { id: currentThreadIdRef.current, externalId: currentThreadExternalIdRef.current }
2363
2689
  );
2364
- params.onBeforeStart?.(currentAssistantId);
2690
+ params.onBeforeStart?.(initialAssistantMessageId);
2365
2691
  let hasStreamProgress = false;
2366
- const updateStreamingMessage = (partial, opts) => {
2367
- if (!streamStillOwnsVisibleThread()) return;
2368
- if (partial && partial.length > 0) {
2369
- hasStreamProgress = true;
2370
- setIsRecoveringStream(false);
2371
- }
2372
- const isReasoning = opts?.isReasoning ?? false;
2373
- const nextSender = opts?.agent ? resolveAgentSender(opts.agent, senderOptionsRef.current) : currentAssistantSender;
2374
- if (nextSender) {
2375
- currentAssistantSender = nextSender;
2376
- }
2377
- const nextAgentKey = currentAssistantSender?.agentId ?? currentAssistantSender?.id ?? null;
2378
- const applyUpdate = (msg) => {
2379
- return {
2380
- ...updateAssistantMessageToken(msg, {
2381
- partial,
2382
- isReasoning
2383
- }),
2384
- ...currentAssistantSender ? { sender: currentAssistantSender } : {}
2385
- };
2386
- };
2387
- setMessages((prev) => {
2388
- const idx = prev.findIndex((m) => m.id === currentAssistantId);
2389
- if (idx >= 0 && canAttachToCurrentStreamingAssistant(prev[idx])) {
2390
- const msg = prev[idx];
2391
- const next = applyUpdate(msg);
2392
- if (msg.content === next.content && msg.activity === next.activity && msg.isStreaming === next.isStreaming && msg.isComplete === next.isComplete) {
2393
- return prev;
2394
- }
2395
- const updated = [...prev];
2396
- updated[idx] = next;
2397
- return updated;
2398
- }
2399
- const last = prev[prev.length - 1];
2400
- if (canAttachToStreamingAssistant(last, nextAgentKey)) {
2401
- currentAssistantId = last.id;
2402
- const next = applyUpdate(last);
2403
- if (last.content === next.content && last.activity === next.activity && last.isStreaming === next.isStreaming && last.isComplete === next.isComplete) {
2404
- return prev;
2405
- }
2406
- const updated = [...prev];
2407
- updated[prev.length - 1] = next;
2408
- return updated;
2409
- }
2410
- const lastStreamingBelongsToDifferentAgent = Boolean(nextAgentKey) && last?.role === "assistant" && last.isStreaming && Boolean(messageAgentKey(last)) && messageAgentKey(last) !== nextAgentKey;
2411
- if (!prev.length || prev[prev.length - 1].role !== "assistant" || !prev[prev.length - 1].isStreaming || lastStreamingBelongsToDifferentAgent) {
2412
- const newId = generateId();
2413
- currentAssistantId = newId;
2414
- const base = {
2415
- id: newId,
2416
- role: "assistant",
2417
- content: "",
2418
- timestamp: nowTs(),
2419
- isStreaming: true,
2420
- isComplete: false,
2421
- ...currentAssistantSender ? { sender: currentAssistantSender } : {}
2422
- };
2423
- return [...prev, applyUpdate(base)];
2424
- }
2425
- return prev;
2426
- });
2427
- };
2428
- const finalizeCurrentAssistantBubble = () => {
2429
- if (!streamStillOwnsVisibleThread()) return;
2430
- setMessages((prev) => {
2431
- const idx = prev.findIndex((m) => m.id === currentAssistantId);
2432
- if (idx < 0) return prev;
2433
- const msg = prev[idx];
2434
- if (!msg.isStreaming && msg.isComplete) return prev;
2435
- const updated = [...prev];
2436
- updated[idx] = closeAssistantMessage(msg);
2437
- return updated;
2438
- });
2439
- };
2440
- const curThreadId = currentThreadIdRef.current;
2441
- const applyLiveToolResultUpdate = (update) => {
2692
+ const dispatchLiveRunAction = (action) => {
2442
2693
  if (!streamStillOwnsVisibleThread()) return;
2443
- let matched = false;
2444
- setMessages((prev) => {
2445
- const next = applyToolResultUpdateToMessages(prev, update, {
2446
- isStreaming: true,
2447
- isComplete: false
2448
- });
2449
- matched = next.matched;
2450
- return next.matched ? next.messages : prev;
2694
+ const transition = transitionLiveRun(liveRunState, action, {
2695
+ createId: generateId
2451
2696
  });
2452
- if (!matched) {
2453
- liveToolUpdatesRef.current.push(update);
2697
+ liveRunState = transition.state;
2698
+ if (transition.operations.length > 0) {
2699
+ setMessages((prev) => applyLiveRunOperations(prev, transition.operations));
2454
2700
  }
2455
2701
  };
2456
- const finalizeActiveAssistantTurn = (finalAnswer) => {
2457
- if (!streamStillOwnsVisibleThread()) return;
2458
- setMessages((prev) => {
2459
- const currentIdx = prev.findIndex((message2) => message2.id === currentAssistantId && message2.role === "assistant");
2460
- const fallbackIdx = currentIdx >= 0 ? currentIdx : (() => {
2461
- for (let i = prev.length - 1; i >= 0; i--) {
2462
- if (prev[i].role === "assistant" && prev[i].isStreaming) {
2463
- return i;
2464
- }
2465
- }
2466
- return -1;
2467
- })();
2468
- if (fallbackIdx < 0) return prev;
2469
- const message = prev[fallbackIdx];
2470
- const nextMessage = finalizeAssistantMessage(message, finalAnswer);
2471
- if (message.content === nextMessage.content && message.isStreaming === nextMessage.isStreaming && message.isComplete === nextMessage.isComplete && message.activity === nextMessage.activity) {
2472
- return prev;
2473
- }
2474
- const updated = [...prev];
2475
- updated[fallbackIdx] = nextMessage;
2476
- currentAssistantId = nextMessage.id;
2477
- return updated;
2478
- });
2479
- };
2480
- const toServerMessageFromEvent = async (event) => {
2481
- if (!event) return null;
2482
- const type = event?.type || "";
2483
- const payload = event?.payload ?? event;
2484
- if (type === "TOOL_CALL") {
2485
- const parsedToolCall = extractLiveToolCall(payload);
2486
- return {
2487
- id: generateId(),
2488
- threadId: curThreadId ?? "",
2489
- senderType: "tool",
2490
- content: "",
2491
- toolCalls: [
2492
- {
2493
- id: parsedToolCall.id ?? generateId(),
2494
- name: parsedToolCall.name,
2495
- args: parsedToolCall.arguments,
2496
- ...parsedToolCall.result !== void 0 ? { output: parsedToolCall.result } : {},
2497
- status: parsedToolCall.status
2498
- }
2499
- ]
2500
- };
2501
- }
2502
- return null;
2503
- };
2504
2702
  const abortController = new AbortController();
2505
2703
  if (!params.preserveActiveRun) {
2506
2704
  abortControllerRef.current?.abort();
@@ -2511,7 +2709,6 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2511
2709
  stopRequestedRef.current = false;
2512
2710
  setThreadActivityStatus("running");
2513
2711
  setIsStreaming(true);
2514
- liveToolUpdatesRef.current = [];
2515
2712
  let streamError = null;
2516
2713
  const resolveActivityThreadId = async () => {
2517
2714
  if (params.threadId) return params.threadId;
@@ -2558,10 +2755,26 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2558
2755
  participants: participantsRef.current,
2559
2756
  targetAgent: targetAgentNameRef.current,
2560
2757
  getRequestHeaders,
2561
- onToken: (token, _isComplete, raw, opts) => updateStreamingMessage(token, {
2562
- ...opts,
2563
- agent: raw?.payload?.agent ?? raw?.agent ?? null
2564
- }),
2758
+ onToken: (token, _isComplete, raw, opts) => {
2759
+ const rawAgent = raw?.payload?.agent ?? raw?.agent ?? null;
2760
+ const sender = rawAgent ? resolveAgentSender(rawAgent, senderOptionsRef.current) : params.assistantSender;
2761
+ const attemptId = opts?.llmAttemptId ?? liveRunState.activeAttemptId ?? liveRunState.lastAttemptId ?? `stream-attempt:${initialAssistantMessageId}`;
2762
+ const isReasoning = opts?.isReasoning ?? false;
2763
+ const phaseId = opts?.phaseId ?? `${attemptId}:${isReasoning ? "reasoning" : "answer"}:0`;
2764
+ if (token.length > 0) {
2765
+ hasStreamProgress = true;
2766
+ setIsRecoveringStream(false);
2767
+ }
2768
+ dispatchLiveRunAction({
2769
+ type: "token",
2770
+ attemptId,
2771
+ phaseId,
2772
+ partial: token,
2773
+ isReasoning,
2774
+ sender,
2775
+ at: getEventTimestamp(raw)
2776
+ });
2777
+ },
2565
2778
  onMessageEvent: async (event) => {
2566
2779
  if (!streamStillOwnsVisibleThread()) return;
2567
2780
  const intercepted = applyEventInterceptor(event);
@@ -2570,14 +2783,37 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2570
2783
  }
2571
2784
  const type = event?.type || "";
2572
2785
  const payload = getEventPayload(event);
2786
+ if (type === "LLM_CALL") {
2787
+ const attemptId = getLlmAttemptId(event);
2788
+ if (attemptId) {
2789
+ dispatchLiveRunAction({
2790
+ type: "attempt-start",
2791
+ attemptId,
2792
+ sender: resolveLiveEventSender(event, senderOptionsRef.current),
2793
+ at: getEventTimestamp(event)
2794
+ });
2795
+ }
2796
+ return;
2797
+ }
2573
2798
  if (type === "TOOL_RESULT") {
2574
2799
  processToolOutput(payload ?? {});
2575
- applyLiveToolResultUpdate(extractLiveToolResultUpdate(payload ?? {}));
2800
+ dispatchLiveRunAction({
2801
+ type: "tool-result",
2802
+ update: extractLiveToolResultUpdate(payload ?? {})
2803
+ });
2576
2804
  return;
2577
2805
  }
2578
2806
  if (type === "LLM_RESULT") {
2579
2807
  const finalAnswer = typeof payload?.answer === "string" ? payload.answer : void 0;
2580
- finalizeActiveAssistantTurn(finalAnswer);
2808
+ const attemptId = getLlmAttemptId(event) ?? liveRunState.activeAttemptId ?? liveRunState.lastAttemptId;
2809
+ if (attemptId) {
2810
+ dispatchLiveRunAction({
2811
+ type: "attempt-result",
2812
+ attemptId,
2813
+ answer: finalAnswer,
2814
+ at: getEventTimestamp(event)
2815
+ });
2816
+ }
2581
2817
  if (isTerminalEmptyLlmResultEvent(event)) {
2582
2818
  finalizeStreamingPlaceholders();
2583
2819
  }
@@ -2589,87 +2825,22 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2589
2825
  if (type === "TOOL_CALL") {
2590
2826
  const parsedToolCall = extractLiveToolCall(payload ?? {});
2591
2827
  const eventSender = resolveLiveEventSender(event, senderOptionsRef.current);
2592
- currentAssistantSender = eventSender;
2593
- const eventAgentKey = currentAssistantSender.agentId ?? currentAssistantSender.id;
2594
2828
  const callId = parsedToolCall.id ?? generateId();
2595
- const toolName = parsedToolCall.name;
2596
- const bufferedUpdates = liveToolUpdatesRef.current;
2597
- const matchingUpdateIndex = bufferedUpdates.findIndex((upd) => matchesToolResultUpdate({ id: callId, name: toolName }, upd));
2598
- const bufferedUpdate = matchingUpdateIndex >= 0 ? bufferedUpdates[matchingUpdateIndex] : void 0;
2599
- if (matchingUpdateIndex >= 0) {
2600
- bufferedUpdates.splice(matchingUpdateIndex, 1);
2601
- }
2602
- const initialStatus = bufferedUpdate ? bufferedUpdate.status : parsedToolCall.status;
2603
- const initialResult = bufferedUpdate && bufferedUpdate.result !== void 0 ? bufferedUpdate.result : parsedToolCall.result;
2604
- const endTime = bufferedUpdate?.endTime;
2605
- setMessages(
2606
- (prev) => (() => {
2607
- const canHostActivity = (message) => {
2608
- if (!message) return false;
2609
- return message.role === "assistant" && message.isStreaming && message.content.trim().length === 0 && !message.attachments?.length;
2610
- };
2611
- const appendToolCall = (msg) => ({
2612
- ...appendAssistantToolCall(msg, {
2613
- id: callId,
2614
- name: toolName,
2615
- arguments: parsedToolCall.arguments,
2616
- ...initialResult !== void 0 ? { result: initialResult } : {},
2617
- status: initialStatus,
2618
- startTime: Date.now(),
2619
- ...endTime !== void 0 ? { endTime } : {}
2620
- })
2621
- });
2622
- const currentIdx = prev.findIndex((message) => message.id === currentAssistantId && message.role === "assistant" && message.isStreaming && canHostActivity(message));
2623
- if (currentIdx >= 0) {
2624
- const next = [...prev];
2625
- next[currentIdx] = appendToolCall({
2626
- ...next[currentIdx],
2627
- isStreaming: true,
2628
- isComplete: false,
2629
- ...currentAssistantSender ? { sender: currentAssistantSender } : {}
2630
- });
2631
- return next;
2632
- }
2633
- const last = prev[prev.length - 1];
2634
- if (canHostActivity(last) && canAttachToStreamingAssistant(last, eventAgentKey)) {
2635
- currentAssistantId = last.id;
2636
- const next = [...prev];
2637
- next[prev.length - 1] = appendToolCall({
2638
- ...last,
2639
- isStreaming: true,
2640
- isComplete: false,
2641
- ...currentAssistantSender ? { sender: currentAssistantSender } : {}
2642
- });
2643
- return next;
2644
- }
2645
- const newId = generateId();
2646
- currentAssistantId = newId;
2647
- return [
2648
- ...prev,
2649
- appendToolCall({
2650
- id: newId,
2651
- role: "assistant",
2652
- content: "",
2653
- timestamp: nowTs(),
2654
- isStreaming: true,
2655
- isComplete: false,
2656
- ...currentAssistantSender ? { sender: currentAssistantSender } : {}
2657
- })
2658
- ];
2659
- })()
2660
- );
2661
- hasStreamProgress = true;
2662
- return;
2663
- }
2664
- const sm = await toServerMessageFromEvent(event);
2665
- if (sm) {
2666
- const viewMsg = convertServerMessage(sm, {
2667
- senderOptions: senderOptionsRef.current,
2668
- createId: generateId,
2669
- now: nowTs
2829
+ dispatchLiveRunAction({
2830
+ type: "tool-call",
2831
+ attemptId: getLlmAttemptId(event) ?? liveRunState.activeAttemptId ?? liveRunState.lastAttemptId,
2832
+ sender: eventSender,
2833
+ at: getEventTimestamp(event),
2834
+ toolCall: {
2835
+ id: callId,
2836
+ name: parsedToolCall.name,
2837
+ arguments: parsedToolCall.arguments,
2838
+ status: parsedToolCall.status,
2839
+ ...parsedToolCall.toolExecutionId ? { toolExecutionId: parsedToolCall.toolExecutionId } : {},
2840
+ ...parsedToolCall.result !== void 0 ? { result: parsedToolCall.result } : {}
2841
+ }
2670
2842
  });
2671
- finalizeCurrentAssistantBubble();
2672
- setMessages((prev) => [...prev, viewMsg]);
2843
+ hasStreamProgress = true;
2673
2844
  return;
2674
2845
  }
2675
2846
  handleStreamMessageEvent(event);
@@ -2683,10 +2854,8 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2683
2854
  if (intercepted?.handled) {
2684
2855
  return;
2685
2856
  }
2686
- await (async () => {
2687
- if (!hasStreamProgress) return;
2688
- handleStreamAssetEvent(payload, currentAssistantId);
2689
- })();
2857
+ if (!hasStreamProgress) return;
2858
+ handleStreamAssetEvent(payload, getLatestLiveRunMessageId(liveRunState));
2690
2859
  },
2691
2860
  signal: abortController.signal
2692
2861
  });
@@ -2719,16 +2888,16 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2719
2888
  abortControllerRef.current = null;
2720
2889
  }
2721
2890
  if (!streamStillOwnsVisibleThread()) {
2722
- return currentAssistantId;
2891
+ return getLatestLiveRunMessageId(liveRunState);
2723
2892
  }
2724
2893
  if (recoveryStarted) {
2725
- return currentAssistantId;
2894
+ return getLatestLiveRunMessageId(liveRunState);
2726
2895
  }
2727
2896
  finalizeStreamingPlaceholders();
2728
2897
  if (streamError) {
2729
2898
  throw streamError;
2730
2899
  }
2731
- return currentAssistantId;
2900
+ return getLatestLiveRunMessageId(liveRunState);
2732
2901
  },
2733
2902
  [applyEventInterceptor, handleStreamMessageEvent, handleStreamAssetEvent, fetchAndSetThreadsState, finalizeStreamingPlaceholders, getRequestHeaders, refreshThreadMessages, startThreadActivityRecovery]
2734
2903
  );
@@ -2762,13 +2931,17 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2762
2931
  );
2763
2932
  const currentMetadata = threadMetadataMapRef.current[conversationKey];
2764
2933
  const pendingTitle = currentMetadata?.pendingTitle;
2934
+ const userMessageId = generateId();
2765
2935
  const userMessage = {
2766
- id: generateId(),
2936
+ id: userMessageId,
2767
2937
  role: "user",
2768
2938
  content,
2769
2939
  timestamp,
2770
2940
  attachments: attachments.length > 0 ? attachments : void 0,
2771
2941
  isComplete: true,
2942
+ metadata: {
2943
+ [CLIENT_MESSAGE_ID_METADATA_KEY]: userMessageId
2944
+ },
2772
2945
  sender: resolveUserSender({
2773
2946
  id: userId,
2774
2947
  name: getCurrentUserDisplayName(userName, userId)
@@ -2781,15 +2954,16 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2781
2954
  },
2782
2955
  senderOptionsRef.current
2783
2956
  ) : preferredAgentRef.current ? resolveAgentSender({ id: preferredAgentRef.current, name: preferredAgentRef.current }, senderOptionsRef.current) : resolveAssistantFallbackSender(senderOptionsRef.current);
2957
+ const assistantMessageId = generateId();
2784
2958
  const assistantPlaceholder = {
2785
- id: generateId(),
2959
+ id: assistantMessageId,
2786
2960
  role: "assistant",
2787
2961
  content: "",
2788
2962
  timestamp: timestamp + 1,
2789
2963
  isStreaming: true,
2790
2964
  isComplete: false,
2791
2965
  sender: assistantSender,
2792
- activity: createPendingAssistantActivity()
2966
+ activity: createPendingAssistantActivity(assistantMessageId)
2793
2967
  };
2794
2968
  setMessages((prev) => [...prev, userMessage, assistantPlaceholder]);
2795
2969
  setSpecialState(null);
@@ -2817,6 +2991,7 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2817
2991
  userId,
2818
2992
  userName: getCurrentUserDisplayName(userName, userId),
2819
2993
  agentName: preferredAgentRef.current,
2994
+ metadata: userMessage.metadata,
2820
2995
  assistantMessageId: assistantPlaceholder.id,
2821
2996
  assistantSender,
2822
2997
  preserveActiveRun,
@@ -2896,7 +3071,7 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2896
3071
  isStreaming: true,
2897
3072
  isComplete: false,
2898
3073
  sender: assistantSender,
2899
- activity: createPendingAssistantActivity()
3074
+ activity: createPendingAssistantActivity(assistantMessageId)
2900
3075
  }
2901
3076
  ]);
2902
3077
  setMessagePageInfo(createEmptyMessagePageInfo());