@copilotz/chat-adapter 0.9.44 → 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,38 +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 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
1794
  // src/threadIdentity.ts
1667
1795
  var identityValues = ({ id, externalId }) => [id, externalId].filter((value) => typeof value === "string" && value.length > 0);
1668
1796
  var isSameThreadIdentity = (owner, current) => {
@@ -1672,96 +1800,244 @@ var isSameThreadIdentity = (owner, current) => {
1672
1800
  return ownerValues.some((value) => currentValues.has(value));
1673
1801
  };
1674
1802
 
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
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)
1701
1809
  });
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
- ];
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
+ };
1719
1847
  };
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
- );
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;
1728
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
+ };
1729
1900
  }
1730
- return messagesByCorrelation;
1731
- };
1732
- var reconcileThreadMessages = (currentMessages, freshMessages) => {
1733
- if (freshMessages.length === 0) {
1734
- return { messages: currentMessages, changed: false };
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
+ };
1735
1937
  }
1736
- if (currentMessages.length === 0) {
1737
- return { messages: freshMessages, changed: true };
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: [] };
1738
1944
  }
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;
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;
1751
1967
  }
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);
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
1762
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
+ };
1763
2031
  }
1764
- return changed ? { messages: nextMessages, changed } : { messages: currentMessages, changed: false };
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;
1765
2041
  };
1766
2042
 
1767
2043
  // src/useCopilotzChat.ts
@@ -1770,6 +2046,10 @@ var generateId = () => globalThis.crypto?.randomUUID?.() ?? `id-${Date.now()}-${
1770
2046
  var isAbortError = (error) => error instanceof DOMException && error.name === "AbortError" || typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
1771
2047
  var getEventPayload = (event) => getStreamEventPayload(event);
1772
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
+ };
1773
2053
  var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1774
2054
  var normalizeThreadTag = (value) => {
1775
2055
  if (!isRecord3(value)) return null;
@@ -1798,11 +2078,11 @@ var createEmptyMessagePageInfo = () => ({
1798
2078
  oldestMessageId: null,
1799
2079
  newestMessageId: null
1800
2080
  });
1801
- var createPendingAssistantActivity = () => ({
2081
+ var createPendingAssistantActivity = (messageId) => ({
1802
2082
  items: [
1803
2083
  {
1804
- id: "thinking",
1805
- kind: "thinking",
2084
+ id: `${messageId}:pending`,
2085
+ kind: "answering",
1806
2086
  status: "active",
1807
2087
  startedAt: nowTs()
1808
2088
  }
@@ -1845,7 +2125,6 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
1845
2125
  assistantName
1846
2126
  });
1847
2127
  const persistedToolUpdatesRef = useRef2([]);
1848
- const liveToolUpdatesRef = useRef2([]);
1849
2128
  const stopRequestedRef = useRef2(false);
1850
2129
  const recoveryPollGenerationRef = useRef2(0);
1851
2130
  threadsRef.current = threads;
@@ -2041,7 +2320,6 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2041
2320
  setIsLoadingOlderMessages(false);
2042
2321
  setMessagePageInfo(createEmptyMessagePageInfo());
2043
2322
  persistedToolUpdatesRef.current = [];
2044
- liveToolUpdatesRef.current = [];
2045
2323
  try {
2046
2324
  const page = await fetchThreadMessagesPage(threadId, { limit: THREAD_MESSAGES_PAGE_SIZE }, getRequestHeaders);
2047
2325
  const { viewMessages, toolResultUpdates } = await prepareThreadMessages(page.data);
@@ -2401,161 +2679,26 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2401
2679
  }, []);
2402
2680
  const sendCopilotzMessage = useCallback2(
2403
2681
  async (params) => {
2404
- let currentAssistantId = params.assistantMessageId ?? generateId();
2405
- let currentAssistantSender = params.assistantSender;
2682
+ const initialAssistantMessageId = params.assistantMessageId ?? generateId();
2683
+ let liveRunState = createLiveRunState(initialAssistantMessageId);
2406
2684
  const streamOwnerThreadId = currentThreadIdRef.current;
2407
2685
  const streamOwnerThreadExternalId = params.threadExternalId ?? currentThreadExternalIdRef.current;
2408
2686
  const streamStillOwnsVisibleThread = () => isSameThreadIdentity(
2409
2687
  { id: streamOwnerThreadId, externalId: streamOwnerThreadExternalId },
2410
2688
  { id: currentThreadIdRef.current, externalId: currentThreadExternalIdRef.current }
2411
2689
  );
2412
- params.onBeforeStart?.(currentAssistantId);
2690
+ params.onBeforeStart?.(initialAssistantMessageId);
2413
2691
  let hasStreamProgress = false;
2414
- const updateStreamingMessage = (partial, opts) => {
2415
- 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 = () => {
2692
+ const dispatchLiveRunAction = (action) => {
2477
2693
  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;
2694
+ const transition = transitionLiveRun(liveRunState, action, {
2695
+ createId: generateId
2486
2696
  });
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);
2697
+ liveRunState = transition.state;
2698
+ if (transition.operations.length > 0) {
2699
+ setMessages((prev) => applyLiveRunOperations(prev, transition.operations));
2502
2700
  }
2503
2701
  };
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
2702
  const abortController = new AbortController();
2560
2703
  if (!params.preserveActiveRun) {
2561
2704
  abortControllerRef.current?.abort();
@@ -2566,7 +2709,6 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2566
2709
  stopRequestedRef.current = false;
2567
2710
  setThreadActivityStatus("running");
2568
2711
  setIsStreaming(true);
2569
- liveToolUpdatesRef.current = [];
2570
2712
  let streamError = null;
2571
2713
  const resolveActivityThreadId = async () => {
2572
2714
  if (params.threadId) return params.threadId;
@@ -2613,10 +2755,26 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2613
2755
  participants: participantsRef.current,
2614
2756
  targetAgent: targetAgentNameRef.current,
2615
2757
  getRequestHeaders,
2616
- onToken: (token, _isComplete, raw, opts) => updateStreamingMessage(token, {
2617
- ...opts,
2618
- agent: raw?.payload?.agent ?? raw?.agent ?? null
2619
- }),
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
+ },
2620
2778
  onMessageEvent: async (event) => {
2621
2779
  if (!streamStillOwnsVisibleThread()) return;
2622
2780
  const intercepted = applyEventInterceptor(event);
@@ -2625,14 +2783,37 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2625
2783
  }
2626
2784
  const type = event?.type || "";
2627
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
+ }
2628
2798
  if (type === "TOOL_RESULT") {
2629
2799
  processToolOutput(payload ?? {});
2630
- applyLiveToolResultUpdate(extractLiveToolResultUpdate(payload ?? {}));
2800
+ dispatchLiveRunAction({
2801
+ type: "tool-result",
2802
+ update: extractLiveToolResultUpdate(payload ?? {})
2803
+ });
2631
2804
  return;
2632
2805
  }
2633
2806
  if (type === "LLM_RESULT") {
2634
2807
  const finalAnswer = typeof payload?.answer === "string" ? payload.answer : void 0;
2635
- finalizeActiveAssistantTurn(finalAnswer, getLlmAttemptId(event));
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
+ }
2636
2817
  if (isTerminalEmptyLlmResultEvent(event)) {
2637
2818
  finalizeStreamingPlaceholders();
2638
2819
  }
@@ -2644,87 +2825,22 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2644
2825
  if (type === "TOOL_CALL") {
2645
2826
  const parsedToolCall = extractLiveToolCall(payload ?? {});
2646
2827
  const eventSender = resolveLiveEventSender(event, senderOptionsRef.current);
2647
- currentAssistantSender = eventSender;
2648
- const eventAgentKey = currentAssistantSender.agentId ?? currentAssistantSender.id;
2649
2828
  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
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
+ }
2725
2842
  });
2726
- finalizeCurrentAssistantBubble();
2727
- setMessages((prev) => [...prev, viewMsg]);
2843
+ hasStreamProgress = true;
2728
2844
  return;
2729
2845
  }
2730
2846
  handleStreamMessageEvent(event);
@@ -2738,10 +2854,8 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2738
2854
  if (intercepted?.handled) {
2739
2855
  return;
2740
2856
  }
2741
- await (async () => {
2742
- if (!hasStreamProgress) return;
2743
- handleStreamAssetEvent(payload, currentAssistantId);
2744
- })();
2857
+ if (!hasStreamProgress) return;
2858
+ handleStreamAssetEvent(payload, getLatestLiveRunMessageId(liveRunState));
2745
2859
  },
2746
2860
  signal: abortController.signal
2747
2861
  });
@@ -2774,16 +2888,16 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2774
2888
  abortControllerRef.current = null;
2775
2889
  }
2776
2890
  if (!streamStillOwnsVisibleThread()) {
2777
- return currentAssistantId;
2891
+ return getLatestLiveRunMessageId(liveRunState);
2778
2892
  }
2779
2893
  if (recoveryStarted) {
2780
- return currentAssistantId;
2894
+ return getLatestLiveRunMessageId(liveRunState);
2781
2895
  }
2782
2896
  finalizeStreamingPlaceholders();
2783
2897
  if (streamError) {
2784
2898
  throw streamError;
2785
2899
  }
2786
- return currentAssistantId;
2900
+ return getLatestLiveRunMessageId(liveRunState);
2787
2901
  },
2788
2902
  [applyEventInterceptor, handleStreamMessageEvent, handleStreamAssetEvent, fetchAndSetThreadsState, finalizeStreamingPlaceholders, getRequestHeaders, refreshThreadMessages, startThreadActivityRecovery]
2789
2903
  );
@@ -2840,15 +2954,16 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2840
2954
  },
2841
2955
  senderOptionsRef.current
2842
2956
  ) : preferredAgentRef.current ? resolveAgentSender({ id: preferredAgentRef.current, name: preferredAgentRef.current }, senderOptionsRef.current) : resolveAssistantFallbackSender(senderOptionsRef.current);
2957
+ const assistantMessageId = generateId();
2843
2958
  const assistantPlaceholder = {
2844
- id: generateId(),
2959
+ id: assistantMessageId,
2845
2960
  role: "assistant",
2846
2961
  content: "",
2847
2962
  timestamp: timestamp + 1,
2848
2963
  isStreaming: true,
2849
2964
  isComplete: false,
2850
2965
  sender: assistantSender,
2851
- activity: createPendingAssistantActivity()
2966
+ activity: createPendingAssistantActivity(assistantMessageId)
2852
2967
  };
2853
2968
  setMessages((prev) => [...prev, userMessage, assistantPlaceholder]);
2854
2969
  setSpecialState(null);
@@ -2956,7 +3071,7 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2956
3071
  isStreaming: true,
2957
3072
  isComplete: false,
2958
3073
  sender: assistantSender,
2959
- activity: createPendingAssistantActivity()
3074
+ activity: createPendingAssistantActivity(assistantMessageId)
2960
3075
  }
2961
3076
  ]);
2962
3077
  setMessagePageInfo(createEmptyMessagePageInfo());