@copilotz/chat-adapter 0.9.44 → 0.9.46

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,41 @@ 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.streamLlmAttemptId === "string" && metadata.streamLlmAttemptId.trim().length > 0) {
115
+ return metadata.streamLlmAttemptId.trim();
116
+ }
117
+ if (typeof metadata.llmAttemptId === "string" && metadata.llmAttemptId.trim().length > 0) {
118
+ return metadata.llmAttemptId.trim();
119
+ }
120
+ if (event.subjectType === "llm_attempt" && typeof event.subjectId === "string" && event.subjectId.trim().length > 0) {
121
+ return event.subjectId.trim();
122
+ }
123
+ return null;
124
+ };
125
+ var isTerminalEmptyLlmResultEvent = (event) => {
126
+ if (!isRecord(event) || event.type !== "LLM_RESULT") return false;
127
+ const payload = getStreamEventPayload(event);
128
+ if (!isRecord(payload)) return false;
129
+ if (payload.answer !== "") return false;
130
+ if (payload.finishReason === "tool_calls") return false;
131
+ if (hasValues(payload.toolCalls)) return false;
132
+ const metadata = isRecord(event.metadata) ? event.metadata : {};
133
+ const routing = isRecord(metadata.routing) ? metadata.routing : {};
134
+ if ((routing.action === "ask" || routing.action === "handoff") && typeof routing.targetId === "string" && routing.targetId.trim().length > 0) return false;
135
+ if (hasValues(metadata.targetQueue)) return false;
136
+ return true;
137
+ };
138
+
104
139
  // src/copilotzService.ts
105
140
  var rawBaseValue = import.meta.env?.VITE_API_URL;
106
141
  var rawBase = typeof rawBaseValue === "string" && rawBaseValue.length > 0 ? rawBaseValue : "/api";
@@ -428,6 +463,11 @@ async function runCopilotzStream(options) {
428
463
  let lastCompletedText = "";
429
464
  let lastTokenWasReasoning = false;
430
465
  let hadNonReasoningContent = false;
466
+ let activeLlmAttemptId = null;
467
+ let activePhaseKind = null;
468
+ let activePhaseId = null;
469
+ let phaseOrdinal = -1;
470
+ let fallbackAttemptOrdinal = 0;
431
471
  const collectedMessages = [];
432
472
  let collectedMedia = null;
433
473
  const resetTokenAggregation = () => {
@@ -436,6 +476,30 @@ async function runCopilotzStream(options) {
436
476
  lastTokenWasReasoning = false;
437
477
  hadNonReasoningContent = false;
438
478
  };
479
+ const startAttempt = (event) => {
480
+ activeLlmAttemptId = getLlmAttemptId(event) ?? `stream-attempt:${fallbackAttemptOrdinal++}`;
481
+ activePhaseKind = null;
482
+ activePhaseId = null;
483
+ phaseOrdinal = -1;
484
+ resetTokenAggregation();
485
+ };
486
+ const getTokenContext = (isReasoning) => {
487
+ if (!activeLlmAttemptId) {
488
+ startAttempt(null);
489
+ }
490
+ const kind = isReasoning ? "reasoning" : "answer";
491
+ if (activePhaseKind !== kind || !activePhaseId) {
492
+ phaseOrdinal += 1;
493
+ activePhaseKind = kind;
494
+ activePhaseId = `${activeLlmAttemptId}:${kind}:${phaseOrdinal}`;
495
+ }
496
+ return {
497
+ isReasoning,
498
+ llmAttemptId: activeLlmAttemptId,
499
+ phaseId: activePhaseId,
500
+ phaseOrdinal
501
+ };
502
+ };
439
503
  const processEvent = async (eventChunk) => {
440
504
  if (!eventChunk.trim()) return;
441
505
  const lines = eventChunk.split("\n");
@@ -461,10 +525,16 @@ async function runCopilotzStream(options) {
461
525
  return;
462
526
  }
463
527
  switch (eventType) {
528
+ case "LLM_CALL": {
529
+ startAttempt(payload2);
530
+ await onMessageEvent?.(payload2);
531
+ break;
532
+ }
464
533
  case "TOKEN": {
465
534
  const inner = payload2?.payload ?? payload2;
466
535
  const chunk = typeof inner?.token === "string" ? inner.token : "";
467
- const isReasoning = Boolean(inner?.isReasoning);
536
+ const isReasoning = typeof inner?.isReasoning === "boolean" ? inner.isReasoning : activePhaseKind === "reasoning" || lastTokenWasReasoning;
537
+ const tokenContext = getTokenContext(isReasoning);
468
538
  if (isReasoning && !lastTokenWasReasoning && hadNonReasoningContent) {
469
539
  aggregatedReasoning = "";
470
540
  aggregatedText = "";
@@ -482,12 +552,14 @@ async function runCopilotzStream(options) {
482
552
  const isComplete = Boolean(inner?.isComplete);
483
553
  if (chunk || isComplete) {
484
554
  const tokenText = isReasoning ? aggregatedReasoning : aggregatedText;
485
- onToken?.(tokenText, isComplete, payload2, { isReasoning });
555
+ onToken?.(tokenText, isComplete, payload2, tokenContext);
486
556
  if (isComplete) {
487
557
  if (!isReasoning && tokenText) {
488
558
  lastCompletedText = tokenText;
489
559
  }
490
560
  resetTokenAggregation();
561
+ activePhaseKind = null;
562
+ activePhaseId = null;
491
563
  }
492
564
  }
493
565
  break;
@@ -833,8 +905,6 @@ function useUrlState(config = {}) {
833
905
  }
834
906
 
835
907
  // src/activity.ts
836
- var thinkingId = "thinking";
837
- var answeringId = "answering";
838
908
  var getItems = (message) => Array.isArray(message.activity?.items) ? message.activity.items : [];
839
909
  var setItems = (message, items) => ({
840
910
  ...message,
@@ -860,7 +930,8 @@ var upsertItem = (message, item) => {
860
930
  };
861
931
  return setItems(message, next);
862
932
  };
863
- var completeItems = (message, shouldComplete) => setItems(message, getItems(message).map((item) => item.status === "active" && shouldComplete(item) ? { ...item, status: "complete", completedAt: Date.now() } : item));
933
+ var completeItems = (message, shouldComplete, completedAt = Date.now()) => setItems(message, getItems(message).map((item) => item.status === "active" && shouldComplete(item) ? { ...item, status: "complete", completedAt } : item));
934
+ var removeItems = (message, shouldRemove) => setItems(message, getItems(message).filter((item) => !shouldRemove(item)));
864
935
  var hasVisibleAssistantOutput = (message) => {
865
936
  if (message.role !== "assistant") return false;
866
937
  if (typeof message.content === "string" && message.content.trim().length > 0) return true;
@@ -875,38 +946,40 @@ var toPublicChatMessage = (message) => {
875
946
  var updateAssistantMessageToken = (message, params) => {
876
947
  if (message.role !== "assistant") return message;
877
948
  if (params.isReasoning) {
878
- return upsertItem({
949
+ const activityId2 = params.activityId ?? `${message.id}:thinking`;
950
+ return upsertItem(completeItems(removeItems({
879
951
  ...message,
880
952
  isStreaming: true,
881
953
  isComplete: false
882
- }, {
883
- id: thinkingId,
954
+ }, (item) => item.kind === "answering"), (item) => item.id !== activityId2 && (item.kind === "thinking" || item.kind === "answering"), params.at), {
955
+ id: activityId2,
884
956
  kind: "thinking",
885
957
  status: "active",
886
- startedAt: getItems(message).find((item) => item.id === thinkingId)?.startedAt ?? Date.now(),
958
+ startedAt: getItems(message).find((item) => item.id === activityId2)?.startedAt ?? params.at ?? Date.now(),
887
959
  details: { reasoning: params.partial }
888
960
  });
889
961
  }
890
- return upsertItem(completeItems({
962
+ const activityId = params.activityId ?? `${message.id}:answering`;
963
+ return upsertItem(completeItems(removeItems({
891
964
  ...message,
892
965
  content: params.partial,
893
966
  isStreaming: true,
894
967
  isComplete: false
895
- }, (item) => item.kind === "thinking" || item.kind === "tool"), {
896
- id: answeringId,
968
+ }, (item) => item.kind === "answering" && item.id !== activityId), (item) => item.kind === "thinking" || item.kind === "answering", params.at), {
969
+ id: activityId,
897
970
  kind: "answering",
898
971
  status: "active",
899
- startedAt: getItems(message).find((item) => item.id === answeringId)?.startedAt ?? Date.now()
972
+ startedAt: getItems(message).find((item) => item.id === activityId)?.startedAt ?? params.at ?? Date.now()
900
973
  });
901
974
  };
902
975
  var appendAssistantToolCall = (message, toolCall) => {
903
976
  if (message.role !== "assistant") return message;
904
977
  const status = toolStatusToActivityStatus(toolCall.status);
905
- return upsertItem({
978
+ return upsertItem(completeItems(removeItems({
906
979
  ...message,
907
980
  isStreaming: true,
908
981
  isComplete: false
909
- }, {
982
+ }, (item) => item.kind === "answering"), (item) => item.kind === "thinking" || item.kind === "answering"), {
910
983
  id: toolCall.id,
911
984
  kind: "tool",
912
985
  status,
@@ -955,23 +1028,23 @@ var applyAssistantToolResult = (message, update) => {
955
1028
  };
956
1029
  return setItems(message, next);
957
1030
  };
958
- var finalizeAssistantMessage = (message, finalAnswer) => {
1031
+ var finalizeAssistantMessage = (message, finalAnswer, completedAt = Date.now()) => {
959
1032
  if (message.role !== "assistant") return message;
960
1033
  const completed = completeItems({
961
1034
  ...message,
962
1035
  ...typeof finalAnswer === "string" && finalAnswer.length > 0 ? { content: finalAnswer } : {},
963
1036
  isStreaming: false,
964
1037
  isComplete: true
965
- }, (item) => item.kind === "thinking" || item.kind === "answering");
1038
+ }, (item) => item.kind === "thinking" || item.kind === "answering", completedAt);
966
1039
  return setItems(completed, getItems(completed).filter((item) => item.kind !== "answering"));
967
1040
  };
968
- var closeAssistantMessage = (message) => {
1041
+ var closeAssistantMessage = (message, completedAt = Date.now()) => {
969
1042
  if (message.role !== "assistant") return message;
970
1043
  return completeItems({
971
1044
  ...message,
972
1045
  isStreaming: false,
973
1046
  isComplete: true
974
- }, () => true);
1047
+ }, () => true, completedAt);
975
1048
  };
976
1049
 
977
1050
  // src/contract.ts
@@ -981,9 +1054,9 @@ var ContractViolation = class extends Error {
981
1054
  this.name = "ContractViolation";
982
1055
  }
983
1056
  };
984
- var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1057
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
985
1058
  var expectRecord = (value, path) => {
986
- if (!isRecord(value)) throw new ContractViolation(`${path} must be an object`);
1059
+ if (!isRecord2(value)) throw new ContractViolation(`${path} must be an object`);
987
1060
  return value;
988
1061
  };
989
1062
  var expectString = (value, path) => {
@@ -1104,14 +1177,14 @@ var resolveLiveEventSender = (event, options = {}) => {
1104
1177
  const raw = expectRecord(event, "stream event");
1105
1178
  const payload = raw.payload === void 0 ? raw : expectRecord(raw.payload, "stream event.payload");
1106
1179
  const agent = payload.agent ?? raw.agent;
1107
- if (isRecord(agent)) {
1180
+ if (isRecord2(agent)) {
1108
1181
  return resolveAgentSender({
1109
1182
  id: expectString(agent.id, "stream event.payload.agent.id"),
1110
1183
  name: expectString(agent.name, "stream event.payload.agent.name")
1111
1184
  }, options);
1112
1185
  }
1113
1186
  const sender = payload.sender ?? raw.sender;
1114
- if (!isRecord(sender)) {
1187
+ if (!isRecord2(sender)) {
1115
1188
  throw new ContractViolation("stream event sender contract requires payload.agent or payload.sender");
1116
1189
  }
1117
1190
  const type = expectSenderType(sender.type ?? payload.senderType, "stream event.payload.sender.type");
@@ -1241,6 +1314,98 @@ async function resolveAssetsInMessages(messages, getRequestHeaders) {
1241
1314
  }));
1242
1315
  }
1243
1316
 
1317
+ // src/messageReconciliation.ts
1318
+ var CLIENT_MESSAGE_ID_METADATA_KEY = "clientMessageId";
1319
+ var LLM_ATTEMPT_ID_METADATA_KEY = "llmAttemptId";
1320
+ var stringifyForCompare = (value) => {
1321
+ if (value === void 0) return "";
1322
+ try {
1323
+ return JSON.stringify(value) ?? "";
1324
+ } catch {
1325
+ return String(value);
1326
+ }
1327
+ };
1328
+ var getMessageSignature = (message) => JSON.stringify({
1329
+ id: message.id,
1330
+ role: message.role,
1331
+ content: message.content ?? "",
1332
+ isStreaming: message.isStreaming === true,
1333
+ isComplete: message.isComplete === true,
1334
+ attachments: stringifyForCompare(message.attachments),
1335
+ activity: stringifyForCompare(message.activity),
1336
+ metadata: stringifyForCompare(message.metadata),
1337
+ sender: message.sender ? {
1338
+ id: message.sender.id,
1339
+ type: message.sender.type,
1340
+ name: message.sender.name,
1341
+ agentId: message.sender.agentId
1342
+ } : null
1343
+ });
1344
+ var getMetadataString = (message, key) => {
1345
+ const value = message.metadata?.[key];
1346
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
1347
+ };
1348
+ var getCorrelationKeys = (message) => {
1349
+ const clientMessageId = getMetadataString(
1350
+ message,
1351
+ CLIENT_MESSAGE_ID_METADATA_KEY
1352
+ );
1353
+ const llmAttemptId = getMetadataString(
1354
+ message,
1355
+ LLM_ATTEMPT_ID_METADATA_KEY
1356
+ );
1357
+ return [
1358
+ ...clientMessageId ? [`client-message:${message.role}:${clientMessageId}`] : [],
1359
+ ...llmAttemptId ? [`llm-attempt:${message.role}:${llmAttemptId}`] : []
1360
+ ];
1361
+ };
1362
+ var indexUniqueFreshCorrelations = (freshMessages) => {
1363
+ const messagesByCorrelation = /* @__PURE__ */ new Map();
1364
+ for (const message of freshMessages) {
1365
+ for (const key of getCorrelationKeys(message)) {
1366
+ messagesByCorrelation.set(
1367
+ key,
1368
+ messagesByCorrelation.has(key) ? null : message
1369
+ );
1370
+ }
1371
+ }
1372
+ return messagesByCorrelation;
1373
+ };
1374
+ var reconcileThreadMessages = (currentMessages, freshMessages) => {
1375
+ if (freshMessages.length === 0) {
1376
+ return { messages: currentMessages, changed: false };
1377
+ }
1378
+ if (currentMessages.length === 0) {
1379
+ return { messages: freshMessages, changed: true };
1380
+ }
1381
+ const freshById = new Map(freshMessages.map((message) => [message.id, message]));
1382
+ const freshByCorrelation = indexUniqueFreshCorrelations(freshMessages);
1383
+ const seen = /* @__PURE__ */ new Set();
1384
+ let changed = false;
1385
+ const nextMessages = currentMessages.map((message) => {
1386
+ const exactMatch = freshById.get(message.id);
1387
+ const correlatedMatch = exactMatch ? null : getCorrelationKeys(message).map((key) => freshByCorrelation.get(key)).find((candidate) => candidate !== null && candidate !== void 0 && !seen.has(candidate.id));
1388
+ const fresh = exactMatch ?? correlatedMatch;
1389
+ if (!fresh) return message;
1390
+ seen.add(fresh.id);
1391
+ if (getMessageSignature(message) === getMessageSignature(fresh)) {
1392
+ return message;
1393
+ }
1394
+ changed = true;
1395
+ return fresh;
1396
+ });
1397
+ const appended = freshMessages.filter((message) => !seen.has(message.id));
1398
+ if (appended.length > 0) {
1399
+ changed = true;
1400
+ nextMessages.push(...appended);
1401
+ nextMessages.sort((a, b) => {
1402
+ const timestampDelta = (a.timestamp ?? 0) - (b.timestamp ?? 0);
1403
+ return timestampDelta !== 0 ? timestampDelta : a.id.localeCompare(b.id);
1404
+ });
1405
+ }
1406
+ return changed ? { messages: nextMessages, changed } : { messages: currentMessages, changed: false };
1407
+ };
1408
+
1244
1409
  // src/toolActivity.ts
1245
1410
  var fail = (message) => {
1246
1411
  throw new ContractViolation(message);
@@ -1263,7 +1428,7 @@ var formatToolError = (error) => {
1263
1428
  }
1264
1429
  };
1265
1430
  var expectToolArguments = (value, path) => {
1266
- if (isRecord(value)) return value;
1431
+ if (isRecord2(value)) return value;
1267
1432
  if (typeof value === "string") {
1268
1433
  try {
1269
1434
  const parsed = JSON.parse(value);
@@ -1439,9 +1604,6 @@ var canAttachToStreamingAssistant = (message, incomingAgentKey) => {
1439
1604
  const currentAgentKey = messageAgentKey(message);
1440
1605
  return !incomingAgentKey || !currentAgentKey || currentAgentKey === incomingAgentKey;
1441
1606
  };
1442
- var canAttachToCurrentStreamingAssistant = (message) => {
1443
- return !!message && message.role === "assistant" && message.isStreaming === true;
1444
- };
1445
1607
 
1446
1608
  // src/messageContract.ts
1447
1609
  var isInternalMessageMetadata = (metadata) => metadata?.visibility === "internal";
@@ -1548,9 +1710,10 @@ var convertServerMessage = (msg, options = {}) => {
1548
1710
  const isToolSender = msg.senderType === "tool";
1549
1711
  const content = isToolSender ? "" : messageContent;
1550
1712
  const reasoning = typeof msg.reasoning === "string" && msg.reasoning.length > 0 ? msg.reasoning : void 0;
1713
+ const llmAttemptId = typeof metadata?.[LLM_ATTEMPT_ID_METADATA_KEY] === "string" ? metadata[LLM_ATTEMPT_ID_METADATA_KEY].trim() : "";
1551
1714
  const activityItems = [
1552
1715
  ...reasoning ? [{
1553
- id: `${msg.id}:thinking`,
1716
+ id: `${llmAttemptId || msg.id}:reasoning:0`,
1554
1717
  kind: "thinking",
1555
1718
  status: "complete",
1556
1719
  completedAt: timestamp,
@@ -1631,38 +1794,6 @@ var prepareHydratedMessages = async (rawMessages, options = {}) => {
1631
1794
  };
1632
1795
  };
1633
1796
 
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 getLlmAttemptId = (event) => {
1642
- if (!isRecord2(event)) return null;
1643
- const metadata = isRecord2(event.metadata) ? event.metadata : {};
1644
- if (typeof metadata.llmAttemptId === "string" && metadata.llmAttemptId.trim().length > 0) {
1645
- return metadata.llmAttemptId.trim();
1646
- }
1647
- if (event.subjectType === "llm_attempt" && typeof event.subjectId === "string" && event.subjectId.trim().length > 0) {
1648
- return event.subjectId.trim();
1649
- }
1650
- return null;
1651
- };
1652
- var isTerminalEmptyLlmResultEvent = (event) => {
1653
- if (!isRecord2(event) || event.type !== "LLM_RESULT") return false;
1654
- const payload = getStreamEventPayload(event);
1655
- if (!isRecord2(payload)) return false;
1656
- if (payload.answer !== "") return false;
1657
- if (payload.finishReason === "tool_calls") return false;
1658
- if (hasValues(payload.toolCalls)) return false;
1659
- const metadata = isRecord2(event.metadata) ? event.metadata : {};
1660
- const routing = isRecord2(metadata.routing) ? metadata.routing : {};
1661
- if ((routing.action === "ask" || routing.action === "handoff") && typeof routing.targetId === "string" && routing.targetId.trim().length > 0) return false;
1662
- if (hasValues(metadata.targetQueue)) return false;
1663
- return true;
1664
- };
1665
-
1666
1797
  // src/threadIdentity.ts
1667
1798
  var identityValues = ({ id, externalId }) => [id, externalId].filter((value) => typeof value === "string" && value.length > 0);
1668
1799
  var isSameThreadIdentity = (owner, current) => {
@@ -1672,96 +1803,245 @@ var isSameThreadIdentity = (owner, current) => {
1672
1803
  return ownerValues.some((value) => currentValues.has(value));
1673
1804
  };
1674
1805
 
1675
- // src/messageReconciliation.ts
1676
- var CLIENT_MESSAGE_ID_METADATA_KEY = "clientMessageId";
1677
- var LLM_ATTEMPT_ID_METADATA_KEY = "llmAttemptId";
1678
- var stringifyForCompare = (value) => {
1679
- if (value === void 0) return "";
1680
- try {
1681
- return JSON.stringify(value) ?? "";
1682
- } catch {
1683
- return String(value);
1684
- }
1685
- };
1686
- var getMessageSignature = (message) => JSON.stringify({
1687
- id: message.id,
1688
- role: message.role,
1689
- content: message.content ?? "",
1690
- isStreaming: message.isStreaming === true,
1691
- isComplete: message.isComplete === true,
1692
- attachments: stringifyForCompare(message.attachments),
1693
- activity: stringifyForCompare(message.activity),
1694
- metadata: stringifyForCompare(message.metadata),
1695
- sender: message.sender ? {
1696
- id: message.sender.id,
1697
- type: message.sender.type,
1698
- name: message.sender.name,
1699
- agentId: message.sender.agentId
1700
- } : null
1806
+ // src/liveRun.ts
1807
+ var copyState = (state) => ({
1808
+ ...state,
1809
+ attemptsById: new Map(state.attemptsById),
1810
+ toolMessageByCallId: new Map(state.toolMessageByCallId),
1811
+ pendingToolResultsByCallId: new Map(state.pendingToolResultsByCallId)
1701
1812
  });
1702
- var getMetadataString = (message, key) => {
1703
- const value = message.metadata?.[key];
1704
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
1705
- };
1706
- var getCorrelationKeys = (message) => {
1707
- const clientMessageId = getMetadataString(
1708
- message,
1709
- CLIENT_MESSAGE_ID_METADATA_KEY
1710
- );
1711
- const llmAttemptId = getMetadataString(
1712
- message,
1713
- LLM_ATTEMPT_ID_METADATA_KEY
1714
- );
1715
- return [
1716
- ...clientMessageId ? [`client-message:${message.role}:${clientMessageId}`] : [],
1717
- ...llmAttemptId ? [`llm-attempt:${message.role}:${llmAttemptId}`] : []
1718
- ];
1813
+ var createLiveRunState = (initialMessageId) => ({
1814
+ initialMessageId,
1815
+ initialMessageClaimed: false,
1816
+ activeAttemptId: null,
1817
+ lastAttemptId: null,
1818
+ attemptsById: /* @__PURE__ */ new Map(),
1819
+ toolMessageByCallId: /* @__PURE__ */ new Map(),
1820
+ pendingToolResultsByCallId: /* @__PURE__ */ new Map()
1821
+ });
1822
+ var ensureAttempt = (state, attemptId, sender, at, options) => {
1823
+ const existing = state.attemptsById.get(attemptId);
1824
+ if (existing) {
1825
+ const next2 = copyState(state);
1826
+ const cursor2 = sender && existing.sender !== sender ? { ...existing, sender } : existing;
1827
+ next2.attemptsById.set(attemptId, cursor2);
1828
+ next2.activeAttemptId = attemptId;
1829
+ next2.lastAttemptId = attemptId;
1830
+ return { state: next2, cursor: cursor2, operations: [] };
1831
+ }
1832
+ const next = copyState(state);
1833
+ const messageId = next.initialMessageClaimed ? options.createId() : next.initialMessageId;
1834
+ const cursor = { messageId, sender };
1835
+ next.initialMessageClaimed = true;
1836
+ next.activeAttemptId = attemptId;
1837
+ next.lastAttemptId = attemptId;
1838
+ next.attemptsById.set(attemptId, cursor);
1839
+ return {
1840
+ state: next,
1841
+ cursor,
1842
+ operations: [{
1843
+ type: "ensure-attempt",
1844
+ messageId,
1845
+ attemptId,
1846
+ sender,
1847
+ at
1848
+ }]
1849
+ };
1719
1850
  };
1720
- var indexUniqueFreshCorrelations = (freshMessages) => {
1721
- const messagesByCorrelation = /* @__PURE__ */ new Map();
1722
- for (const message of freshMessages) {
1723
- for (const key of getCorrelationKeys(message)) {
1724
- messagesByCorrelation.set(
1725
- key,
1726
- messagesByCorrelation.has(key) ? null : message
1727
- );
1851
+ var transitionLiveRun = (state, action, options) => {
1852
+ if (action.type === "attempt-start") {
1853
+ const ensured = ensureAttempt(
1854
+ state,
1855
+ action.attemptId,
1856
+ action.sender,
1857
+ action.at,
1858
+ options
1859
+ );
1860
+ return { state: ensured.state, operations: ensured.operations };
1861
+ }
1862
+ if (action.type === "token") {
1863
+ const ensured = ensureAttempt(
1864
+ state,
1865
+ action.attemptId,
1866
+ action.sender,
1867
+ action.at,
1868
+ options
1869
+ );
1870
+ return {
1871
+ state: ensured.state,
1872
+ operations: [
1873
+ ...ensured.operations,
1874
+ {
1875
+ type: "update-token",
1876
+ messageId: ensured.cursor.messageId,
1877
+ phaseId: action.phaseId,
1878
+ partial: action.partial,
1879
+ isReasoning: action.isReasoning,
1880
+ sender: action.sender ?? ensured.cursor.sender,
1881
+ at: action.at
1882
+ }
1883
+ ]
1884
+ };
1885
+ }
1886
+ if (action.type === "attempt-result") {
1887
+ const resolvedAttemptId = state.attemptsById.has(action.attemptId) ? action.attemptId : state.activeAttemptId ?? state.lastAttemptId;
1888
+ const cursor = resolvedAttemptId ? state.attemptsById.get(resolvedAttemptId) : void 0;
1889
+ if (!cursor) return { state, operations: [] };
1890
+ const next = copyState(state);
1891
+ if (next.activeAttemptId === resolvedAttemptId) {
1892
+ next.activeAttemptId = null;
1728
1893
  }
1894
+ next.lastAttemptId = resolvedAttemptId;
1895
+ return {
1896
+ state: next,
1897
+ operations: [{
1898
+ type: "finalize-attempt",
1899
+ messageId: cursor.messageId,
1900
+ answer: action.answer,
1901
+ at: action.at
1902
+ }]
1903
+ };
1729
1904
  }
1730
- return messagesByCorrelation;
1731
- };
1732
- var reconcileThreadMessages = (currentMessages, freshMessages) => {
1733
- if (freshMessages.length === 0) {
1734
- return { messages: currentMessages, changed: false };
1905
+ if (action.type === "tool-call") {
1906
+ if (state.toolMessageByCallId.has(action.toolCall.id)) {
1907
+ return { state, operations: [] };
1908
+ }
1909
+ const attemptId = action.attemptId ?? state.activeAttemptId ?? state.lastAttemptId ?? `tool-attempt:${action.toolCall.id}`;
1910
+ const ensured = ensureAttempt(
1911
+ state,
1912
+ attemptId,
1913
+ action.sender,
1914
+ action.at,
1915
+ options
1916
+ );
1917
+ const next = copyState(ensured.state);
1918
+ next.toolMessageByCallId.set(action.toolCall.id, ensured.cursor.messageId);
1919
+ const pendingResult = next.pendingToolResultsByCallId.get(action.toolCall.id);
1920
+ if (pendingResult) {
1921
+ next.pendingToolResultsByCallId.delete(action.toolCall.id);
1922
+ }
1923
+ return {
1924
+ state: next,
1925
+ operations: [
1926
+ ...ensured.operations,
1927
+ {
1928
+ type: "append-tool",
1929
+ messageId: ensured.cursor.messageId,
1930
+ toolCall: action.toolCall,
1931
+ sender: action.sender ?? ensured.cursor.sender,
1932
+ at: action.at
1933
+ },
1934
+ ...pendingResult ? [{
1935
+ type: "resolve-tool",
1936
+ messageId: ensured.cursor.messageId,
1937
+ update: pendingResult
1938
+ }] : []
1939
+ ]
1940
+ };
1735
1941
  }
1736
- if (currentMessages.length === 0) {
1737
- return { messages: freshMessages, changed: true };
1942
+ const messageId = action.update.id ? state.toolMessageByCallId.get(action.update.id) : void 0;
1943
+ if (!messageId || !action.update.id) {
1944
+ if (!action.update.id) return { state, operations: [] };
1945
+ const next = copyState(state);
1946
+ next.pendingToolResultsByCallId.set(action.update.id, action.update);
1947
+ return { state: next, operations: [] };
1738
1948
  }
1739
- const freshById = new Map(freshMessages.map((message) => [message.id, message]));
1740
- const freshByCorrelation = indexUniqueFreshCorrelations(freshMessages);
1741
- const seen = /* @__PURE__ */ new Set();
1742
- let changed = false;
1743
- const nextMessages = currentMessages.map((message) => {
1744
- const exactMatch = freshById.get(message.id);
1745
- const correlatedMatch = exactMatch ? null : getCorrelationKeys(message).map((key) => freshByCorrelation.get(key)).find((candidate) => candidate !== null && candidate !== void 0 && !seen.has(candidate.id));
1746
- const fresh = exactMatch ?? correlatedMatch;
1747
- if (!fresh) return message;
1748
- seen.add(fresh.id);
1749
- if (getMessageSignature(message) === getMessageSignature(fresh)) {
1750
- return message;
1949
+ return {
1950
+ state,
1951
+ operations: [{ type: "resolve-tool", messageId, update: action.update }]
1952
+ };
1953
+ };
1954
+ var applyOperation = (messages, operation) => {
1955
+ const index = messages.findIndex((message) => message.id === operation.messageId);
1956
+ if (operation.type === "ensure-attempt") {
1957
+ if (index >= 0) {
1958
+ const current2 = messages[index];
1959
+ const nextMessage = {
1960
+ ...current2,
1961
+ metadata: {
1962
+ ...current2.metadata ?? {},
1963
+ [LLM_ATTEMPT_ID_METADATA_KEY]: operation.attemptId
1964
+ },
1965
+ ...operation.sender ? { sender: operation.sender } : {}
1966
+ };
1967
+ if (current2.metadata?.[LLM_ATTEMPT_ID_METADATA_KEY] === operation.attemptId && (!operation.sender || current2.sender === operation.sender)) return messages;
1968
+ const next2 = [...messages];
1969
+ next2[index] = nextMessage;
1970
+ return next2;
1751
1971
  }
1752
- changed = true;
1753
- return fresh;
1754
- });
1755
- const appended = freshMessages.filter((message) => !seen.has(message.id));
1756
- if (appended.length > 0) {
1757
- changed = true;
1758
- nextMessages.push(...appended);
1759
- nextMessages.sort((a, b) => {
1760
- const timestampDelta = (a.timestamp ?? 0) - (b.timestamp ?? 0);
1761
- return timestampDelta !== 0 ? timestampDelta : a.id.localeCompare(b.id);
1972
+ return [
1973
+ ...messages,
1974
+ {
1975
+ id: operation.messageId,
1976
+ role: "assistant",
1977
+ content: "",
1978
+ timestamp: operation.at,
1979
+ isStreaming: true,
1980
+ isComplete: false,
1981
+ metadata: { [LLM_ATTEMPT_ID_METADATA_KEY]: operation.attemptId },
1982
+ activity: {
1983
+ items: [{
1984
+ id: `${operation.messageId}:pending`,
1985
+ kind: "answering",
1986
+ status: "active",
1987
+ startedAt: operation.at
1988
+ }]
1989
+ },
1990
+ ...operation.sender ? { sender: operation.sender } : {}
1991
+ }
1992
+ ];
1993
+ }
1994
+ if (index < 0) return messages;
1995
+ const current = messages[index];
1996
+ let updated;
1997
+ if (operation.type === "update-token") {
1998
+ updated = {
1999
+ ...updateAssistantMessageToken(current, {
2000
+ partial: operation.partial,
2001
+ isReasoning: operation.isReasoning,
2002
+ activityId: operation.phaseId,
2003
+ at: operation.at
2004
+ }),
2005
+ ...operation.sender ? { sender: operation.sender } : {}
2006
+ };
2007
+ } else if (operation.type === "finalize-attempt") {
2008
+ updated = finalizeAssistantMessage(current, operation.answer, operation.at);
2009
+ } else if (operation.type === "append-tool") {
2010
+ updated = {
2011
+ ...appendAssistantToolCall(current, {
2012
+ ...operation.toolCall,
2013
+ startTime: operation.toolCall.startTime ?? operation.at
2014
+ }),
2015
+ ...operation.sender ? { sender: operation.sender } : {}
2016
+ };
2017
+ } else {
2018
+ const toolItem = current.activity?.items.find((item) => item.kind === "tool" && item.id === operation.update.id);
2019
+ if (!toolItem) return messages;
2020
+ updated = applyAssistantToolResult(current, {
2021
+ ...operation.update.id ? { id: operation.update.id } : {},
2022
+ ...operation.update.toolExecutionId ? { toolExecutionId: operation.update.toolExecutionId } : {},
2023
+ name: operation.update.name ?? toolItem.toolName ?? toolItem.id,
2024
+ status: operation.update.status,
2025
+ ...operation.update.result !== void 0 ? { result: operation.update.result } : {},
2026
+ ...operation.update.error !== void 0 ? { error: operation.update.error } : {},
2027
+ endTime: operation.update.endTime
1762
2028
  });
2029
+ const hasActiveTool = updated.activity?.items.some((item) => item.kind === "tool" && item.status === "active") ?? false;
2030
+ updated = {
2031
+ ...updated,
2032
+ isStreaming: hasActiveTool,
2033
+ isComplete: !hasActiveTool
2034
+ };
1763
2035
  }
1764
- return changed ? { messages: nextMessages, changed } : { messages: currentMessages, changed: false };
2036
+ if (updated === current) return messages;
2037
+ const next = [...messages];
2038
+ next[index] = updated;
2039
+ return next;
2040
+ };
2041
+ var applyLiveRunOperations = (messages, operations) => operations.reduce(applyOperation, messages);
2042
+ var getLatestLiveRunMessageId = (state) => {
2043
+ const attemptId = state.activeAttemptId ?? state.lastAttemptId;
2044
+ return attemptId ? state.attemptsById.get(attemptId)?.messageId ?? state.initialMessageId : state.initialMessageId;
1765
2045
  };
1766
2046
 
1767
2047
  // src/useCopilotzChat.ts
@@ -1770,6 +2050,10 @@ var generateId = () => globalThis.crypto?.randomUUID?.() ?? `id-${Date.now()}-${
1770
2050
  var isAbortError = (error) => error instanceof DOMException && error.name === "AbortError" || typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
1771
2051
  var getEventPayload = (event) => getStreamEventPayload(event);
1772
2052
  var getEventSenderType = (payload) => payload?.senderType || payload?.sender?.type;
2053
+ var getEventTimestamp = (event) => {
2054
+ const timestamp = typeof event?.createdAt === "string" ? new Date(event.createdAt).getTime() : NaN;
2055
+ return Number.isFinite(timestamp) ? timestamp : nowTs();
2056
+ };
1773
2057
  var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1774
2058
  var normalizeThreadTag = (value) => {
1775
2059
  if (!isRecord3(value)) return null;
@@ -1798,11 +2082,11 @@ var createEmptyMessagePageInfo = () => ({
1798
2082
  oldestMessageId: null,
1799
2083
  newestMessageId: null
1800
2084
  });
1801
- var createPendingAssistantActivity = () => ({
2085
+ var createPendingAssistantActivity = (messageId) => ({
1802
2086
  items: [
1803
2087
  {
1804
- id: "thinking",
1805
- kind: "thinking",
2088
+ id: `${messageId}:pending`,
2089
+ kind: "answering",
1806
2090
  status: "active",
1807
2091
  startedAt: nowTs()
1808
2092
  }
@@ -1845,7 +2129,6 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
1845
2129
  assistantName
1846
2130
  });
1847
2131
  const persistedToolUpdatesRef = useRef2([]);
1848
- const liveToolUpdatesRef = useRef2([]);
1849
2132
  const stopRequestedRef = useRef2(false);
1850
2133
  const recoveryPollGenerationRef = useRef2(0);
1851
2134
  threadsRef.current = threads;
@@ -2041,7 +2324,6 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2041
2324
  setIsLoadingOlderMessages(false);
2042
2325
  setMessagePageInfo(createEmptyMessagePageInfo());
2043
2326
  persistedToolUpdatesRef.current = [];
2044
- liveToolUpdatesRef.current = [];
2045
2327
  try {
2046
2328
  const page = await fetchThreadMessagesPage(threadId, { limit: THREAD_MESSAGES_PAGE_SIZE }, getRequestHeaders);
2047
2329
  const { viewMessages, toolResultUpdates } = await prepareThreadMessages(page.data);
@@ -2401,161 +2683,26 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2401
2683
  }, []);
2402
2684
  const sendCopilotzMessage = useCallback2(
2403
2685
  async (params) => {
2404
- let currentAssistantId = params.assistantMessageId ?? generateId();
2405
- let currentAssistantSender = params.assistantSender;
2686
+ const initialAssistantMessageId = params.assistantMessageId ?? generateId();
2687
+ let liveRunState = createLiveRunState(initialAssistantMessageId);
2406
2688
  const streamOwnerThreadId = currentThreadIdRef.current;
2407
2689
  const streamOwnerThreadExternalId = params.threadExternalId ?? currentThreadExternalIdRef.current;
2408
2690
  const streamStillOwnsVisibleThread = () => isSameThreadIdentity(
2409
2691
  { id: streamOwnerThreadId, externalId: streamOwnerThreadExternalId },
2410
2692
  { id: currentThreadIdRef.current, externalId: currentThreadExternalIdRef.current }
2411
2693
  );
2412
- params.onBeforeStart?.(currentAssistantId);
2694
+ params.onBeforeStart?.(initialAssistantMessageId);
2413
2695
  let hasStreamProgress = false;
2414
- const updateStreamingMessage = (partial, opts) => {
2696
+ const dispatchLiveRunAction = (action) => {
2415
2697
  if (!streamStillOwnsVisibleThread()) return;
2416
- if (partial && partial.length > 0) {
2417
- hasStreamProgress = true;
2418
- setIsRecoveringStream(false);
2419
- }
2420
- const isReasoning = opts?.isReasoning ?? false;
2421
- const nextSender = opts?.agent ? resolveAgentSender(opts.agent, senderOptionsRef.current) : currentAssistantSender;
2422
- if (nextSender) {
2423
- currentAssistantSender = nextSender;
2424
- }
2425
- const nextAgentKey = currentAssistantSender?.agentId ?? currentAssistantSender?.id ?? null;
2426
- const applyUpdate = (msg) => {
2427
- return {
2428
- ...updateAssistantMessageToken(msg, {
2429
- partial,
2430
- isReasoning
2431
- }),
2432
- ...currentAssistantSender ? { sender: currentAssistantSender } : {}
2433
- };
2434
- };
2435
- setMessages((prev) => {
2436
- const idx = prev.findIndex((m) => m.id === currentAssistantId);
2437
- if (idx >= 0 && canAttachToCurrentStreamingAssistant(prev[idx])) {
2438
- const msg = prev[idx];
2439
- const next = applyUpdate(msg);
2440
- if (msg.content === next.content && msg.activity === next.activity && msg.isStreaming === next.isStreaming && msg.isComplete === next.isComplete) {
2441
- return prev;
2442
- }
2443
- const updated = [...prev];
2444
- updated[idx] = next;
2445
- return updated;
2446
- }
2447
- const last = prev[prev.length - 1];
2448
- if (canAttachToStreamingAssistant(last, nextAgentKey)) {
2449
- currentAssistantId = last.id;
2450
- const next = applyUpdate(last);
2451
- if (last.content === next.content && last.activity === next.activity && last.isStreaming === next.isStreaming && last.isComplete === next.isComplete) {
2452
- return prev;
2453
- }
2454
- const updated = [...prev];
2455
- updated[prev.length - 1] = next;
2456
- return updated;
2457
- }
2458
- const lastStreamingBelongsToDifferentAgent = Boolean(nextAgentKey) && last?.role === "assistant" && last.isStreaming && Boolean(messageAgentKey(last)) && messageAgentKey(last) !== nextAgentKey;
2459
- if (!prev.length || prev[prev.length - 1].role !== "assistant" || !prev[prev.length - 1].isStreaming || lastStreamingBelongsToDifferentAgent) {
2460
- const newId = generateId();
2461
- currentAssistantId = newId;
2462
- const base = {
2463
- id: newId,
2464
- role: "assistant",
2465
- content: "",
2466
- timestamp: nowTs(),
2467
- isStreaming: true,
2468
- isComplete: false,
2469
- ...currentAssistantSender ? { sender: currentAssistantSender } : {}
2470
- };
2471
- return [...prev, applyUpdate(base)];
2472
- }
2473
- return prev;
2474
- });
2475
- };
2476
- const finalizeCurrentAssistantBubble = () => {
2477
- if (!streamStillOwnsVisibleThread()) return;
2478
- setMessages((prev) => {
2479
- const idx = prev.findIndex((m) => m.id === currentAssistantId);
2480
- if (idx < 0) return prev;
2481
- const msg = prev[idx];
2482
- if (!msg.isStreaming && msg.isComplete) return prev;
2483
- const updated = [...prev];
2484
- updated[idx] = closeAssistantMessage(msg);
2485
- return updated;
2698
+ const transition = transitionLiveRun(liveRunState, action, {
2699
+ createId: generateId
2486
2700
  });
2487
- };
2488
- const curThreadId = currentThreadIdRef.current;
2489
- const applyLiveToolResultUpdate = (update) => {
2490
- if (!streamStillOwnsVisibleThread()) return;
2491
- let matched = false;
2492
- setMessages((prev) => {
2493
- const next = applyToolResultUpdateToMessages(prev, update, {
2494
- isStreaming: true,
2495
- isComplete: false
2496
- });
2497
- matched = next.matched;
2498
- return next.matched ? next.messages : prev;
2499
- });
2500
- if (!matched) {
2501
- liveToolUpdatesRef.current.push(update);
2701
+ liveRunState = transition.state;
2702
+ if (transition.operations.length > 0) {
2703
+ setMessages((prev) => applyLiveRunOperations(prev, transition.operations));
2502
2704
  }
2503
2705
  };
2504
- const finalizeActiveAssistantTurn = (finalAnswer, llmAttemptId) => {
2505
- if (!streamStillOwnsVisibleThread()) return;
2506
- setMessages((prev) => {
2507
- const currentIdx = prev.findIndex((message2) => message2.id === currentAssistantId && message2.role === "assistant");
2508
- const fallbackIdx = currentIdx >= 0 ? currentIdx : (() => {
2509
- for (let i = prev.length - 1; i >= 0; i--) {
2510
- if (prev[i].role === "assistant" && prev[i].isStreaming) {
2511
- return i;
2512
- }
2513
- }
2514
- return -1;
2515
- })();
2516
- if (fallbackIdx < 0) return prev;
2517
- const message = prev[fallbackIdx];
2518
- const correlatedMessage = llmAttemptId ? {
2519
- ...message,
2520
- metadata: {
2521
- ...message.metadata ?? {},
2522
- [LLM_ATTEMPT_ID_METADATA_KEY]: llmAttemptId
2523
- }
2524
- } : message;
2525
- const nextMessage = finalizeAssistantMessage(correlatedMessage, finalAnswer);
2526
- if (message.content === nextMessage.content && message.isStreaming === nextMessage.isStreaming && message.isComplete === nextMessage.isComplete && message.activity === nextMessage.activity && message.metadata === nextMessage.metadata) {
2527
- return prev;
2528
- }
2529
- const updated = [...prev];
2530
- updated[fallbackIdx] = nextMessage;
2531
- currentAssistantId = nextMessage.id;
2532
- return updated;
2533
- });
2534
- };
2535
- const toServerMessageFromEvent = async (event) => {
2536
- if (!event) return null;
2537
- const type = event?.type || "";
2538
- const payload = event?.payload ?? event;
2539
- if (type === "TOOL_CALL") {
2540
- const parsedToolCall = extractLiveToolCall(payload);
2541
- return {
2542
- id: generateId(),
2543
- threadId: curThreadId ?? "",
2544
- senderType: "tool",
2545
- content: "",
2546
- toolCalls: [
2547
- {
2548
- id: parsedToolCall.id ?? generateId(),
2549
- name: parsedToolCall.name,
2550
- args: parsedToolCall.arguments,
2551
- ...parsedToolCall.result !== void 0 ? { output: parsedToolCall.result } : {},
2552
- status: parsedToolCall.status
2553
- }
2554
- ]
2555
- };
2556
- }
2557
- return null;
2558
- };
2559
2706
  const abortController = new AbortController();
2560
2707
  if (!params.preserveActiveRun) {
2561
2708
  abortControllerRef.current?.abort();
@@ -2566,7 +2713,6 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2566
2713
  stopRequestedRef.current = false;
2567
2714
  setThreadActivityStatus("running");
2568
2715
  setIsStreaming(true);
2569
- liveToolUpdatesRef.current = [];
2570
2716
  let streamError = null;
2571
2717
  const resolveActivityThreadId = async () => {
2572
2718
  if (params.threadId) return params.threadId;
@@ -2613,10 +2759,26 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2613
2759
  participants: participantsRef.current,
2614
2760
  targetAgent: targetAgentNameRef.current,
2615
2761
  getRequestHeaders,
2616
- onToken: (token, _isComplete, raw, opts) => updateStreamingMessage(token, {
2617
- ...opts,
2618
- agent: raw?.payload?.agent ?? raw?.agent ?? null
2619
- }),
2762
+ onToken: (token, _isComplete, raw, opts) => {
2763
+ const rawAgent = raw?.payload?.agent ?? raw?.agent ?? null;
2764
+ const sender = rawAgent ? resolveAgentSender(rawAgent, senderOptionsRef.current) : params.assistantSender;
2765
+ const attemptId = opts?.llmAttemptId ?? liveRunState.activeAttemptId ?? liveRunState.lastAttemptId ?? `stream-attempt:${initialAssistantMessageId}`;
2766
+ const isReasoning = opts?.isReasoning ?? false;
2767
+ const phaseId = opts?.phaseId ?? `${attemptId}:${isReasoning ? "reasoning" : "answer"}:0`;
2768
+ if (token.length > 0) {
2769
+ hasStreamProgress = true;
2770
+ setIsRecoveringStream(false);
2771
+ }
2772
+ dispatchLiveRunAction({
2773
+ type: "token",
2774
+ attemptId,
2775
+ phaseId,
2776
+ partial: token,
2777
+ isReasoning,
2778
+ sender,
2779
+ at: getEventTimestamp(raw)
2780
+ });
2781
+ },
2620
2782
  onMessageEvent: async (event) => {
2621
2783
  if (!streamStillOwnsVisibleThread()) return;
2622
2784
  const intercepted = applyEventInterceptor(event);
@@ -2625,14 +2787,37 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2625
2787
  }
2626
2788
  const type = event?.type || "";
2627
2789
  const payload = getEventPayload(event);
2790
+ if (type === "LLM_CALL") {
2791
+ const attemptId = getLlmAttemptId(event);
2792
+ if (attemptId) {
2793
+ dispatchLiveRunAction({
2794
+ type: "attempt-start",
2795
+ attemptId,
2796
+ sender: resolveLiveEventSender(event, senderOptionsRef.current),
2797
+ at: getEventTimestamp(event)
2798
+ });
2799
+ }
2800
+ return;
2801
+ }
2628
2802
  if (type === "TOOL_RESULT") {
2629
2803
  processToolOutput(payload ?? {});
2630
- applyLiveToolResultUpdate(extractLiveToolResultUpdate(payload ?? {}));
2804
+ dispatchLiveRunAction({
2805
+ type: "tool-result",
2806
+ update: extractLiveToolResultUpdate(payload ?? {})
2807
+ });
2631
2808
  return;
2632
2809
  }
2633
2810
  if (type === "LLM_RESULT") {
2634
2811
  const finalAnswer = typeof payload?.answer === "string" ? payload.answer : void 0;
2635
- finalizeActiveAssistantTurn(finalAnswer, getLlmAttemptId(event));
2812
+ const attemptId = getLlmAttemptId(event) ?? liveRunState.activeAttemptId ?? liveRunState.lastAttemptId;
2813
+ if (attemptId) {
2814
+ dispatchLiveRunAction({
2815
+ type: "attempt-result",
2816
+ attemptId,
2817
+ answer: finalAnswer,
2818
+ at: getEventTimestamp(event)
2819
+ });
2820
+ }
2636
2821
  if (isTerminalEmptyLlmResultEvent(event)) {
2637
2822
  finalizeStreamingPlaceholders();
2638
2823
  }
@@ -2644,87 +2829,22 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2644
2829
  if (type === "TOOL_CALL") {
2645
2830
  const parsedToolCall = extractLiveToolCall(payload ?? {});
2646
2831
  const eventSender = resolveLiveEventSender(event, senderOptionsRef.current);
2647
- currentAssistantSender = eventSender;
2648
- const eventAgentKey = currentAssistantSender.agentId ?? currentAssistantSender.id;
2649
2832
  const callId = parsedToolCall.id ?? generateId();
2650
- const toolName = parsedToolCall.name;
2651
- const bufferedUpdates = liveToolUpdatesRef.current;
2652
- const matchingUpdateIndex = bufferedUpdates.findIndex((upd) => matchesToolResultUpdate({ id: callId, name: toolName }, upd));
2653
- const bufferedUpdate = matchingUpdateIndex >= 0 ? bufferedUpdates[matchingUpdateIndex] : void 0;
2654
- if (matchingUpdateIndex >= 0) {
2655
- bufferedUpdates.splice(matchingUpdateIndex, 1);
2656
- }
2657
- const initialStatus = bufferedUpdate ? bufferedUpdate.status : parsedToolCall.status;
2658
- const initialResult = bufferedUpdate && bufferedUpdate.result !== void 0 ? bufferedUpdate.result : parsedToolCall.result;
2659
- const endTime = bufferedUpdate?.endTime;
2660
- setMessages(
2661
- (prev) => (() => {
2662
- const canHostActivity = (message) => {
2663
- if (!message) return false;
2664
- return message.role === "assistant" && message.isStreaming && message.content.trim().length === 0 && !message.attachments?.length;
2665
- };
2666
- const appendToolCall = (msg) => ({
2667
- ...appendAssistantToolCall(msg, {
2668
- id: callId,
2669
- name: toolName,
2670
- arguments: parsedToolCall.arguments,
2671
- ...initialResult !== void 0 ? { result: initialResult } : {},
2672
- status: initialStatus,
2673
- startTime: Date.now(),
2674
- ...endTime !== void 0 ? { endTime } : {}
2675
- })
2676
- });
2677
- const currentIdx = prev.findIndex((message) => message.id === currentAssistantId && message.role === "assistant" && message.isStreaming && canHostActivity(message));
2678
- if (currentIdx >= 0) {
2679
- const next = [...prev];
2680
- next[currentIdx] = appendToolCall({
2681
- ...next[currentIdx],
2682
- isStreaming: true,
2683
- isComplete: false,
2684
- ...currentAssistantSender ? { sender: currentAssistantSender } : {}
2685
- });
2686
- return next;
2687
- }
2688
- const last = prev[prev.length - 1];
2689
- if (canHostActivity(last) && canAttachToStreamingAssistant(last, eventAgentKey)) {
2690
- currentAssistantId = last.id;
2691
- const next = [...prev];
2692
- next[prev.length - 1] = appendToolCall({
2693
- ...last,
2694
- isStreaming: true,
2695
- isComplete: false,
2696
- ...currentAssistantSender ? { sender: currentAssistantSender } : {}
2697
- });
2698
- return next;
2699
- }
2700
- const newId = generateId();
2701
- currentAssistantId = newId;
2702
- return [
2703
- ...prev,
2704
- appendToolCall({
2705
- id: newId,
2706
- role: "assistant",
2707
- content: "",
2708
- timestamp: nowTs(),
2709
- isStreaming: true,
2710
- isComplete: false,
2711
- ...currentAssistantSender ? { sender: currentAssistantSender } : {}
2712
- })
2713
- ];
2714
- })()
2715
- );
2716
- hasStreamProgress = true;
2717
- return;
2718
- }
2719
- const sm = await toServerMessageFromEvent(event);
2720
- if (sm) {
2721
- const viewMsg = convertServerMessage(sm, {
2722
- senderOptions: senderOptionsRef.current,
2723
- createId: generateId,
2724
- now: nowTs
2833
+ dispatchLiveRunAction({
2834
+ type: "tool-call",
2835
+ attemptId: getLlmAttemptId(event) ?? liveRunState.activeAttemptId ?? liveRunState.lastAttemptId,
2836
+ sender: eventSender,
2837
+ at: getEventTimestamp(event),
2838
+ toolCall: {
2839
+ id: callId,
2840
+ name: parsedToolCall.name,
2841
+ arguments: parsedToolCall.arguments,
2842
+ status: parsedToolCall.status,
2843
+ ...parsedToolCall.toolExecutionId ? { toolExecutionId: parsedToolCall.toolExecutionId } : {},
2844
+ ...parsedToolCall.result !== void 0 ? { result: parsedToolCall.result } : {}
2845
+ }
2725
2846
  });
2726
- finalizeCurrentAssistantBubble();
2727
- setMessages((prev) => [...prev, viewMsg]);
2847
+ hasStreamProgress = true;
2728
2848
  return;
2729
2849
  }
2730
2850
  handleStreamMessageEvent(event);
@@ -2738,10 +2858,8 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2738
2858
  if (intercepted?.handled) {
2739
2859
  return;
2740
2860
  }
2741
- await (async () => {
2742
- if (!hasStreamProgress) return;
2743
- handleStreamAssetEvent(payload, currentAssistantId);
2744
- })();
2861
+ if (!hasStreamProgress) return;
2862
+ handleStreamAssetEvent(payload, getLatestLiveRunMessageId(liveRunState));
2745
2863
  },
2746
2864
  signal: abortController.signal
2747
2865
  });
@@ -2774,16 +2892,16 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2774
2892
  abortControllerRef.current = null;
2775
2893
  }
2776
2894
  if (!streamStillOwnsVisibleThread()) {
2777
- return currentAssistantId;
2895
+ return getLatestLiveRunMessageId(liveRunState);
2778
2896
  }
2779
2897
  if (recoveryStarted) {
2780
- return currentAssistantId;
2898
+ return getLatestLiveRunMessageId(liveRunState);
2781
2899
  }
2782
2900
  finalizeStreamingPlaceholders();
2783
2901
  if (streamError) {
2784
2902
  throw streamError;
2785
2903
  }
2786
- return currentAssistantId;
2904
+ return getLatestLiveRunMessageId(liveRunState);
2787
2905
  },
2788
2906
  [applyEventInterceptor, handleStreamMessageEvent, handleStreamAssetEvent, fetchAndSetThreadsState, finalizeStreamingPlaceholders, getRequestHeaders, refreshThreadMessages, startThreadActivityRecovery]
2789
2907
  );
@@ -2840,15 +2958,16 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2840
2958
  },
2841
2959
  senderOptionsRef.current
2842
2960
  ) : preferredAgentRef.current ? resolveAgentSender({ id: preferredAgentRef.current, name: preferredAgentRef.current }, senderOptionsRef.current) : resolveAssistantFallbackSender(senderOptionsRef.current);
2961
+ const assistantMessageId = generateId();
2843
2962
  const assistantPlaceholder = {
2844
- id: generateId(),
2963
+ id: assistantMessageId,
2845
2964
  role: "assistant",
2846
2965
  content: "",
2847
2966
  timestamp: timestamp + 1,
2848
2967
  isStreaming: true,
2849
2968
  isComplete: false,
2850
2969
  sender: assistantSender,
2851
- activity: createPendingAssistantActivity()
2970
+ activity: createPendingAssistantActivity(assistantMessageId)
2852
2971
  };
2853
2972
  setMessages((prev) => [...prev, userMessage, assistantPlaceholder]);
2854
2973
  setSpecialState(null);
@@ -2956,7 +3075,7 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2956
3075
  isStreaming: true,
2957
3076
  isComplete: false,
2958
3077
  sender: assistantSender,
2959
- activity: createPendingAssistantActivity()
3078
+ activity: createPendingAssistantActivity(assistantMessageId)
2960
3079
  }
2961
3080
  ]);
2962
3081
  setMessagePageInfo(createEmptyMessagePageInfo());