@datalayer/agent-runtimes 1.5.26 → 1.5.28
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-embed/chunks/{ChatView-CUWE6I5M.js → ChatView-C0PWJ_wj.js} +4 -4
- package/dist-embed/datalayer-app-main.js +111 -101
- package/lib/apps/apps/AppKept.js +5 -1
- package/lib/apps/embed/element.js +10 -4
- package/lib/apps/embed/embedConfig.d.ts +10 -1
- package/lib/apps/embed/embedConfig.js +25 -1
- package/lib/apps/plugins/chat/ChatView.js +2 -2
- package/lib/chat/assistant/AssistantStage.js +17 -0
- package/lib/chat/assistant/clearance.d.ts +10 -0
- package/lib/chat/assistant/clearance.js +14 -0
- package/lib/chat/base/ChatBase.js +375 -437
- package/lib/chat/base/conversationItems.d.ts +55 -0
- package/lib/chat/base/conversationItems.js +126 -0
- package/lib/portable/adapterEvents.d.ts +107 -0
- package/lib/portable/adapterEvents.js +239 -0
- package/lib/portable/approvalCard.d.ts +56 -0
- package/lib/portable/approvalCard.js +56 -0
- package/lib/portable/conversation.d.ts +71 -18
- package/lib/portable/conversation.js +330 -74
- package/lib/portable/fixtures.js +12 -0
- package/lib/portable/index.d.ts +4 -1
- package/lib/portable/index.js +7 -1
- package/lib/portable/presence.js +4 -1
- package/lib/portable/runMeasures.d.ts +46 -0
- package/lib/portable/runMeasures.js +156 -0
- package/lib/protocols/ACPAdapter.js +13 -8
- package/lib/specs/voices.js +47 -0
- package/lib/utils/utils.d.ts +2 -8
- package/lib/utils/utils.js +3 -26
- package/lib/voice/ComposerVoice.js +25 -3
- package/lib/voice/VoiceInput.d.ts +2 -2
- package/lib/voice/VoiceInput.js +6 -4
- package/lib/voice/VoiceMenu.js +2 -1
- package/lib/voice/chatVoice.d.ts +4 -3
- package/lib/voice/chatVoice.js +6 -9
- package/lib/voice/consent.d.ts +8 -0
- package/lib/voice/consent.js +10 -0
- package/lib/voice/hearing.d.ts +23 -1
- package/lib/voice/hearing.js +18 -3
- package/lib/voice/index.d.ts +11 -6
- package/lib/voice/index.js +1 -0
- package/lib/voice/serverHearing.d.ts +58 -0
- package/lib/voice/serverHearing.js +136 -0
- package/lib/voice/settings.d.ts +5 -0
- package/lib/voice/settings.js +8 -0
- package/lib/voice/speaker.d.ts +4 -3
- package/lib/voice/speaker.js +1 -2
- package/lib/voice/useVoiceSpeaker.js +0 -3
- package/package.json +2 -2
|
@@ -34,9 +34,12 @@ import { useChatStore } from '../../stores/chatStore';
|
|
|
34
34
|
import { useConversationStore } from '../../stores/conversationStore';
|
|
35
35
|
import { askedOf } from '../../portable/records';
|
|
36
36
|
import { extractChatMessagesFromFullContext } from './fullContextMessages';
|
|
37
|
-
import { generateMessageId, createUserMessage
|
|
37
|
+
import { generateMessageId, createUserMessage } from '../../types/messages';
|
|
38
38
|
import { AI_MODEL_CATALOGUE, DEFAULT_MODEL, isChatModel, } from '../../specs/models';
|
|
39
|
-
import { internalQueryClient, isToolCallMessage, getMessageText,
|
|
39
|
+
import { internalQueryClient, isToolCallMessage, getMessageText, createProtocolAdapter, getApiBaseFromConfig, sanitizeAssistantContent, } from '../../utils';
|
|
40
|
+
import { BROWSER_LIMITS, emptyConversation, reduceConversation, withError, withoutItems, withPrompt, withTurnEnded, } from '../../portable/conversation';
|
|
41
|
+
import { conversationEventsOf, snapshotOfMessages, } from '../../portable/adapterEvents';
|
|
42
|
+
import { conversationDrawer, emptyChatLayer, rowIdOf, toolCallOf, } from './conversationItems';
|
|
40
43
|
import { useConfig, useSkills, useSkillActions, useContextSnapshot, useSandbox, } from '../../hooks';
|
|
41
44
|
import { useAgentRuntimeWebSocket } from '../../hooks/useAgentRuntimes';
|
|
42
45
|
import { agentRuntimeStore, useAgentRuntimeStore, useAgentRuntimeWsState, } from '../../stores/agentRuntimeStore';
|
|
@@ -1076,7 +1079,74 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1076
1079
|
// Check if protocol is A2A (doesn't support per-request model override)
|
|
1077
1080
|
const isA2AProtocol = protocol?.type === 'a2a';
|
|
1078
1081
|
// ---- Component state ----
|
|
1079
|
-
|
|
1082
|
+
/*
|
|
1083
|
+
* The conversation (MOBILE M-030): `reduceConversation`'s, as on the phone,
|
|
1084
|
+
* whatever protocol the agent speaks — its adapter's events normalised by
|
|
1085
|
+
* `conversationEventsOf`. Kept in a ref as each change lands, so the events
|
|
1086
|
+
* of one network chunk each read the last, and drawn on the next render
|
|
1087
|
+
* with what only the web holds beside it (`ChatLayer`).
|
|
1088
|
+
*/
|
|
1089
|
+
const [conversation, setConversation] = useState(() => emptyConversation(BROWSER_LIMITS));
|
|
1090
|
+
const conversationRef = useRef(conversation);
|
|
1091
|
+
// Whether anything has changed it yet: a store is not written over by the
|
|
1092
|
+
// empty conversation a chat mounts with.
|
|
1093
|
+
const conversationTouchedRef = useRef(false);
|
|
1094
|
+
const changeConversation = useCallback((change) => {
|
|
1095
|
+
const next = change(conversationRef.current);
|
|
1096
|
+
if (next === conversationRef.current)
|
|
1097
|
+
return;
|
|
1098
|
+
conversationRef.current = next;
|
|
1099
|
+
conversationTouchedRef.current = true;
|
|
1100
|
+
setConversation(next);
|
|
1101
|
+
}, []);
|
|
1102
|
+
const [layer, setLayer] = useState(emptyChatLayer);
|
|
1103
|
+
const layerRef = useRef(layer);
|
|
1104
|
+
const changeLayer = useCallback((change) => {
|
|
1105
|
+
const next = change(layerRef.current);
|
|
1106
|
+
if (next === layerRef.current)
|
|
1107
|
+
return;
|
|
1108
|
+
layerRef.current = next;
|
|
1109
|
+
setLayer(next);
|
|
1110
|
+
}, []);
|
|
1111
|
+
/** The conversation and what the web held beside it, gone: a new one. */
|
|
1112
|
+
const resetConversation = useCallback(() => {
|
|
1113
|
+
changeConversation(() => emptyConversation(BROWSER_LIMITS));
|
|
1114
|
+
changeLayer(() => emptyChatLayer());
|
|
1115
|
+
}, [changeConversation, changeLayer]);
|
|
1116
|
+
/** A conversation read again: its messages, its tool calls and their results. */
|
|
1117
|
+
const restoreConversation = useCallback((messages) => {
|
|
1118
|
+
let restored;
|
|
1119
|
+
try {
|
|
1120
|
+
restored = reduceConversation(emptyConversation(BROWSER_LIMITS), snapshotOfMessages(messages));
|
|
1121
|
+
}
|
|
1122
|
+
catch (failure) {
|
|
1123
|
+
console.error('[ChatBase] Conversation not read again:', failure);
|
|
1124
|
+
setError(failure instanceof Error ? failure : new Error(String(failure)));
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
const extras = new Map();
|
|
1128
|
+
for (const message of messages) {
|
|
1129
|
+
const { id, role: _role, content: _content, toolCalls: _calls, ...rest } = message;
|
|
1130
|
+
extras.set(id, rest);
|
|
1131
|
+
}
|
|
1132
|
+
changeLayer(() => ({ ...emptyChatLayer(), extras }));
|
|
1133
|
+
changeConversation(() => restored);
|
|
1134
|
+
}, [changeConversation, changeLayer]);
|
|
1135
|
+
/** One more input of the reducer. */
|
|
1136
|
+
const reduce = useCallback((event) => changeConversation(state => reduceConversation(state, event)), [changeConversation]);
|
|
1137
|
+
/** A tool call as its card draws it now, or `undefined`. */
|
|
1138
|
+
const toolCardNow = useCallback((toolCallId) => {
|
|
1139
|
+
const row = conversationRef.current.items.find(item => item.kind === 'tool' && item.id === toolCallId);
|
|
1140
|
+
return row && row.kind === 'tool' ? toolCallOf(row) : undefined;
|
|
1141
|
+
}, []);
|
|
1142
|
+
/** Every tool call as its card draws it now. */
|
|
1143
|
+
const toolCardsNow = useCallback(() => conversationRef.current.items.flatMap(item => item.kind === 'tool' ? [toolCallOf(item)] : []), []);
|
|
1144
|
+
const chatTextRef = useRef(chatText);
|
|
1145
|
+
chatTextRef.current = chatText;
|
|
1146
|
+
const drawConversation = useMemo(() => conversationDrawer(text => chatTextRef.current.error(text)), []);
|
|
1147
|
+
const displayItems = useMemo(() => drawConversation(conversation, layer), [drawConversation, conversation, layer]);
|
|
1148
|
+
const displayItemsRef = useRef(displayItems);
|
|
1149
|
+
displayItemsRef.current = displayItems;
|
|
1080
1150
|
/*
|
|
1081
1151
|
* What the harness itself reported, for an agent with no server behind it.
|
|
1082
1152
|
*
|
|
@@ -1477,9 +1547,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1477
1547
|
// ---- Refs ----
|
|
1478
1548
|
const adapterRef = useRef(null);
|
|
1479
1549
|
const unsubscribeRef = useRef(null);
|
|
1480
|
-
const toolCallsRef = useRef(new Map());
|
|
1481
1550
|
const pendingToolExecutionsRef = useRef(0);
|
|
1482
|
-
const currentAssistantMessageRef = useRef(null);
|
|
1483
1551
|
const respondedApprovalIdsRef = useRef(new Set());
|
|
1484
1552
|
const defaultSkillsBootstrapRef = useRef(new Set());
|
|
1485
1553
|
const defaultMcpToolsBootstrapRef = useRef(new Set());
|
|
@@ -1512,7 +1580,6 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1512
1580
|
const uiSocketRef = useRef(null);
|
|
1513
1581
|
const parkedUiOpsRef = useRef([]);
|
|
1514
1582
|
const historyStepsRef = useRef(new Set());
|
|
1515
|
-
const displayItemsRef = useRef([]);
|
|
1516
1583
|
const uiSocketUrl = protocol?.uiSocket;
|
|
1517
1584
|
const uiSocketTokenRef = useRef(protocol?.authToken);
|
|
1518
1585
|
uiSocketTokenRef.current = protocol?.authToken;
|
|
@@ -1532,22 +1599,56 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1532
1599
|
token: uiSocketTokenRef.current,
|
|
1533
1600
|
});
|
|
1534
1601
|
}, [uiFilesUrl]);
|
|
1602
|
+
/** The messages as a ui op reads them: their ids, what they show, who said them. */
|
|
1603
|
+
const messageStubs = useCallback(() => {
|
|
1604
|
+
const kept = layerRef.current;
|
|
1605
|
+
return conversationRef.current.items.flatMap(item => item.kind === 'message' || item.kind === 'error'
|
|
1606
|
+
? [
|
|
1607
|
+
{
|
|
1608
|
+
id: item.id,
|
|
1609
|
+
displays: kept.displays.get(item.id),
|
|
1610
|
+
speaker: kept.speakers.get(item.id),
|
|
1611
|
+
},
|
|
1612
|
+
]
|
|
1613
|
+
: []);
|
|
1614
|
+
}, []);
|
|
1615
|
+
/*
|
|
1616
|
+
* A ui op on the messages: what they show and who said them are the web's
|
|
1617
|
+
* (`ChatLayer`); a message taken back leaves the conversation.
|
|
1618
|
+
*/
|
|
1535
1619
|
const applyToMessages = useCallback((op) => {
|
|
1536
|
-
const
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
catch (error) {
|
|
1541
|
-
console.error('[ChatBase] reactor.ui op not applied:', error);
|
|
1542
|
-
return items;
|
|
1543
|
-
}
|
|
1544
|
-
};
|
|
1545
|
-
setDisplayItems(prev => apply(prev));
|
|
1546
|
-
if (useStoreMode) {
|
|
1547
|
-
const store = useChatStore.getState();
|
|
1548
|
-
store.setMessages(apply(store.messages));
|
|
1620
|
+
const stubs = messageStubs();
|
|
1621
|
+
let next;
|
|
1622
|
+
try {
|
|
1623
|
+
next = withUiOp(stubs, op);
|
|
1549
1624
|
}
|
|
1550
|
-
|
|
1625
|
+
catch (error) {
|
|
1626
|
+
console.error('[ChatBase] reactor.ui op not applied:', error);
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
const left = new Set(next.map(stub => stub.id));
|
|
1630
|
+
const removed = stubs
|
|
1631
|
+
.filter(stub => !left.has(stub.id))
|
|
1632
|
+
.map(stub => stub.id);
|
|
1633
|
+
changeLayer(kept => {
|
|
1634
|
+
const displays = new Map(kept.displays);
|
|
1635
|
+
const speakers = new Map(kept.speakers);
|
|
1636
|
+
for (const stub of next) {
|
|
1637
|
+
if (stub.displays)
|
|
1638
|
+
displays.set(stub.id, stub.displays);
|
|
1639
|
+
if (stub.speaker)
|
|
1640
|
+
speakers.set(stub.id, stub.speaker);
|
|
1641
|
+
}
|
|
1642
|
+
for (const id of removed) {
|
|
1643
|
+
displays.delete(id);
|
|
1644
|
+
speakers.delete(id);
|
|
1645
|
+
}
|
|
1646
|
+
return { ...kept, displays, speakers };
|
|
1647
|
+
});
|
|
1648
|
+
if (removed.length > 0) {
|
|
1649
|
+
changeConversation(state => withoutItems(state, removed));
|
|
1650
|
+
}
|
|
1651
|
+
}, [messageStubs, changeLayer, changeConversation]);
|
|
1551
1652
|
const applySocketOp = useCallback((op, history) => {
|
|
1552
1653
|
// The past is drawn again, never acted on: a window message is not
|
|
1553
1654
|
// raised again, and the steps of turns long over are not drawn again.
|
|
@@ -1580,32 +1681,40 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1580
1681
|
else if (routed.to === 'step') {
|
|
1581
1682
|
// A step of its code, drawn as it runs (LOOP P-16).
|
|
1582
1683
|
const step = routed.step;
|
|
1583
|
-
|
|
1684
|
+
changeLayer(kept => {
|
|
1685
|
+
const anchors = new Map(kept.steps.map(row => [row.item.id, row.after]));
|
|
1686
|
+
const last = conversationRef.current.items.at(-1)?.id ?? null;
|
|
1687
|
+
const items = withLoopStep(kept.steps.map(row => row.item), step);
|
|
1688
|
+
return {
|
|
1689
|
+
...kept,
|
|
1690
|
+
steps: items.map(item => ({
|
|
1691
|
+
item,
|
|
1692
|
+
after: anchors.has(item.id)
|
|
1693
|
+
? (anchors.get(item.id) ?? null)
|
|
1694
|
+
: last,
|
|
1695
|
+
})),
|
|
1696
|
+
};
|
|
1697
|
+
});
|
|
1584
1698
|
}
|
|
1585
1699
|
else if (routed.to === 'element') {
|
|
1586
1700
|
// In a side panel or on a page of its own (LOOP P-18).
|
|
1587
1701
|
uiElementsRef.current.add(routed.change.id);
|
|
1588
1702
|
applyLoopElement(routed.change);
|
|
1589
1703
|
}
|
|
1590
|
-
else if (!placeable(
|
|
1704
|
+
else if (!placeable(messageStubs(), routed.op)) {
|
|
1591
1705
|
const under = routed.op.op === 'show' || routed.op.op === 'ask'
|
|
1592
1706
|
? routed.op.body.data?.message_id
|
|
1593
1707
|
: undefined;
|
|
1594
1708
|
if (history && typeof under === 'string') {
|
|
1595
1709
|
// Shown in the past under a message that said nothing: the
|
|
1596
1710
|
// thread did not draw it, the chat draws it for what it showed.
|
|
1597
|
-
|
|
1598
|
-
|
|
1711
|
+
reduce({
|
|
1712
|
+
type: 'MESSAGE_SAID',
|
|
1713
|
+
messageId: under,
|
|
1599
1714
|
role: 'assistant',
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
};
|
|
1603
|
-
displayItemsRef.current = [...displayItemsRef.current, placeholder];
|
|
1604
|
-
setDisplayItems(prev => prev.some(item => item.id === under)
|
|
1605
|
-
? prev
|
|
1606
|
-
: [...prev, placeholder]);
|
|
1607
|
-
if (useStoreMode)
|
|
1608
|
-
useChatStore.getState().addMessage(placeholder);
|
|
1715
|
+
text: '',
|
|
1716
|
+
streaming: false,
|
|
1717
|
+
});
|
|
1609
1718
|
applyToMessages(routed.op);
|
|
1610
1719
|
}
|
|
1611
1720
|
else {
|
|
@@ -1615,30 +1724,22 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1615
1724
|
else {
|
|
1616
1725
|
applyToMessages(routed.op);
|
|
1617
1726
|
}
|
|
1618
|
-
}, [applyToMessages,
|
|
1727
|
+
}, [applyToMessages, messageStubs, changeLayer, reduce]);
|
|
1619
1728
|
const applySocketOpRef = useRef(applySocketOp);
|
|
1620
1729
|
applySocketOpRef.current = applySocketOp;
|
|
1621
1730
|
// What waited for its message, applied once the message is here.
|
|
1622
1731
|
useEffect(() => {
|
|
1623
|
-
displayItemsRef.current = displayItems;
|
|
1624
1732
|
if (parkedUiOpsRef.current.length === 0)
|
|
1625
1733
|
return;
|
|
1626
1734
|
const parked = parkedUiOpsRef.current.splice(0);
|
|
1627
|
-
let items = displayItems;
|
|
1628
1735
|
for (const op of parked) {
|
|
1629
|
-
if (!placeable(
|
|
1736
|
+
if (!placeable(messageStubs(), op)) {
|
|
1630
1737
|
parkedUiOpsRef.current.push(op);
|
|
1631
1738
|
continue;
|
|
1632
1739
|
}
|
|
1633
|
-
try {
|
|
1634
|
-
items = withUiOp(items, op);
|
|
1635
|
-
}
|
|
1636
|
-
catch {
|
|
1637
|
-
// Said by `applyToMessages`, which refuses it the same way.
|
|
1638
|
-
}
|
|
1639
1740
|
applyToMessages(op);
|
|
1640
1741
|
}
|
|
1641
|
-
}, [
|
|
1742
|
+
}, [conversation, applyToMessages, messageStubs]);
|
|
1642
1743
|
/** The conversation's ui socket, opened for its thread when it is not yet. */
|
|
1643
1744
|
const ensureUiSocket = useCallback(() => {
|
|
1644
1745
|
if (!uiSocketUrl)
|
|
@@ -1732,7 +1833,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1732
1833
|
if (!respond)
|
|
1733
1834
|
return;
|
|
1734
1835
|
const normalize = (name) => (name ?? '').toLowerCase().replace(/[-_]/g, '');
|
|
1735
|
-
const calls =
|
|
1836
|
+
const calls = toolCardsNow();
|
|
1736
1837
|
// By the approval id the tool result carries, else by the tool
|
|
1737
1838
|
// name of a call still waiting — the same two matches the card makes.
|
|
1738
1839
|
const call = calls.find(entry => entry.result
|
|
@@ -1758,14 +1859,14 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1758
1859
|
}
|
|
1759
1860
|
let targetToolCallId = approval.tool_call_id;
|
|
1760
1861
|
if (!targetToolCallId) {
|
|
1761
|
-
for (const
|
|
1862
|
+
for (const tc of toolCardsNow()) {
|
|
1762
1863
|
if (tc.status !== 'inProgress' && tc.status !== 'executing') {
|
|
1763
1864
|
continue;
|
|
1764
1865
|
}
|
|
1765
1866
|
const tcSig = approvalSignature(tc.toolName, tc.args ?? {});
|
|
1766
1867
|
const approvalSig = approvalSignature(approval.tool_name, approval.tool_args ?? {});
|
|
1767
1868
|
if (tcSig === approvalSig) {
|
|
1768
|
-
targetToolCallId =
|
|
1869
|
+
targetToolCallId = tc.toolCallId;
|
|
1769
1870
|
break;
|
|
1770
1871
|
}
|
|
1771
1872
|
}
|
|
@@ -1773,7 +1874,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1773
1874
|
if (!targetToolCallId) {
|
|
1774
1875
|
return false;
|
|
1775
1876
|
}
|
|
1776
|
-
const target =
|
|
1877
|
+
const target = toolCardNow(targetToolCallId);
|
|
1777
1878
|
if (!target) {
|
|
1778
1879
|
return false;
|
|
1779
1880
|
}
|
|
@@ -2236,10 +2337,10 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2236
2337
|
if (useStoreMode && !runtimeId) {
|
|
2237
2338
|
const storeMessages = useChatStore.getState().messages;
|
|
2238
2339
|
if (storeMessages.length > 0) {
|
|
2239
|
-
|
|
2340
|
+
restoreConversation(storeMessages);
|
|
2240
2341
|
}
|
|
2241
2342
|
}
|
|
2242
|
-
}, [useStoreMode, runtimeId]);
|
|
2343
|
+
}, [useStoreMode, runtimeId, restoreConversation]);
|
|
2243
2344
|
// ---- Conversation history loading ----
|
|
2244
2345
|
const prevHistoryScopeRef = useRef(undefined);
|
|
2245
2346
|
const historyVersionRef = useRef(0);
|
|
@@ -2254,8 +2355,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2254
2355
|
historyRetryAttemptsRef.current.set(historyScopeId, 0);
|
|
2255
2356
|
}
|
|
2256
2357
|
prevHistoryScopeRef.current = historyScopeId;
|
|
2257
|
-
|
|
2258
|
-
toolCallsRef.current.clear();
|
|
2358
|
+
resetConversation();
|
|
2259
2359
|
if (!historyScopeId)
|
|
2260
2360
|
return;
|
|
2261
2361
|
}
|
|
@@ -2273,7 +2373,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2273
2373
|
useConversationStore
|
|
2274
2374
|
.getState()
|
|
2275
2375
|
.setMessages(historyScopeId, given.messages);
|
|
2276
|
-
|
|
2376
|
+
restoreConversation(given.messages);
|
|
2277
2377
|
useConversationStore.getState().markFetched(historyScopeId);
|
|
2278
2378
|
setHistoryLoaded(true);
|
|
2279
2379
|
}
|
|
@@ -2285,8 +2385,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2285
2385
|
// The transcript changed on the server outside a run — a rewind to a
|
|
2286
2386
|
// checkpoint. Forget what is shown and load what the agent now has.
|
|
2287
2387
|
useConversationStore.getState().clearMessages(historyScopeId);
|
|
2288
|
-
|
|
2289
|
-
toolCallsRef.current.clear();
|
|
2388
|
+
resetConversation();
|
|
2290
2389
|
historyRetryAttemptsRef.current.set(historyScopeId, 0);
|
|
2291
2390
|
freshSnapshotRef.current = true;
|
|
2292
2391
|
}
|
|
@@ -2300,7 +2399,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2300
2399
|
// state — put over a live conversation, it erased its tool cards, a
|
|
2301
2400
|
// pending approval among them, at the socket's first retry.
|
|
2302
2401
|
if (storedMessages.length > 0) {
|
|
2303
|
-
|
|
2402
|
+
if (conversationRef.current.items.length === 0) {
|
|
2403
|
+
restoreConversation(storedMessages);
|
|
2404
|
+
}
|
|
2304
2405
|
setHistoryLoaded(true);
|
|
2305
2406
|
}
|
|
2306
2407
|
if (currentlyFetching) {
|
|
@@ -2318,7 +2419,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2318
2419
|
const fullContextToMessages = () => extractChatMessagesFromFullContext(agentRuntimeStore.getState().fullContext);
|
|
2319
2420
|
const applyMessages = (messages) => {
|
|
2320
2421
|
store.setMessages(historyScopeId, messages);
|
|
2321
|
-
|
|
2422
|
+
restoreConversation(messages);
|
|
2322
2423
|
historyRetryAttemptsRef.current.set(historyScopeId, 0);
|
|
2323
2424
|
store.markFetched(historyScopeId);
|
|
2324
2425
|
setHistoryLoaded(true);
|
|
@@ -2416,6 +2517,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2416
2517
|
activeAgentId,
|
|
2417
2518
|
historyRefreshTick,
|
|
2418
2519
|
historyVersion,
|
|
2520
|
+
resetConversation,
|
|
2521
|
+
restoreConversation,
|
|
2419
2522
|
]);
|
|
2420
2523
|
// Keep in-memory store in sync with displayItems
|
|
2421
2524
|
useEffect(() => {
|
|
@@ -2428,6 +2531,14 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2428
2531
|
}
|
|
2429
2532
|
}
|
|
2430
2533
|
}, [historyScopeId, displayItems]);
|
|
2534
|
+
// A store-mode chat keeps its messages in the shared chat store, as drawn.
|
|
2535
|
+
useEffect(() => {
|
|
2536
|
+
if (!useStoreMode || !conversationTouchedRef.current)
|
|
2537
|
+
return;
|
|
2538
|
+
useChatStore
|
|
2539
|
+
.getState()
|
|
2540
|
+
.setMessages(displayItems.filter((item) => !isToolCallMessage(item)));
|
|
2541
|
+
}, [useStoreMode, displayItems]);
|
|
2431
2542
|
// ---- Derived state ----
|
|
2432
2543
|
const messages = displayItems.filter((item) => !isToolCallMessage(item));
|
|
2433
2544
|
const ready = true;
|
|
@@ -2516,6 +2627,45 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2516
2627
|
event.type !== 'done') {
|
|
2517
2628
|
return;
|
|
2518
2629
|
}
|
|
2630
|
+
/*
|
|
2631
|
+
* The event, as the conversation reads it (`conversationEventsOf`):
|
|
2632
|
+
* one reducer whatever the protocol. What it cannot read is said.
|
|
2633
|
+
*/
|
|
2634
|
+
const read = () => {
|
|
2635
|
+
try {
|
|
2636
|
+
const inputs = conversationEventsOf(event, conversationRef.current);
|
|
2637
|
+
changeConversation(state => inputs.reduce((kept, input) => reduceConversation(kept, input), state));
|
|
2638
|
+
}
|
|
2639
|
+
catch (failure) {
|
|
2640
|
+
console.error('[ChatBase] Protocol event not read:', failure);
|
|
2641
|
+
setError(failure instanceof Error ? failure : new Error(String(failure)));
|
|
2642
|
+
}
|
|
2643
|
+
};
|
|
2644
|
+
/** The conversation's latest tool row with an answer, if any said nothing. */
|
|
2645
|
+
const toolResultFallback = () => {
|
|
2646
|
+
const items = conversationRef.current.items;
|
|
2647
|
+
const hasAssistantContent = items.some(item => item.kind === 'message' &&
|
|
2648
|
+
item.role === 'assistant' &&
|
|
2649
|
+
item.text.trim().length > 0);
|
|
2650
|
+
if (hasAssistantContent) {
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
const latestCompletedTool = [...items]
|
|
2654
|
+
.reverse()
|
|
2655
|
+
.find(item => item.kind === 'tool' &&
|
|
2656
|
+
item.status === 'complete' &&
|
|
2657
|
+
item.value !== undefined);
|
|
2658
|
+
if (!latestCompletedTool || latestCompletedTool.kind !== 'tool') {
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
reduce({
|
|
2662
|
+
type: 'MESSAGE_SAID',
|
|
2663
|
+
messageId: generateMessageId(),
|
|
2664
|
+
role: 'assistant',
|
|
2665
|
+
text: formatToolResultFallback(latestCompletedTool.value),
|
|
2666
|
+
streaming: false,
|
|
2667
|
+
});
|
|
2668
|
+
};
|
|
2519
2669
|
switch (event.type) {
|
|
2520
2670
|
case 'message':
|
|
2521
2671
|
if (event.usage) {
|
|
@@ -2545,95 +2695,21 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2545
2695
|
});
|
|
2546
2696
|
}
|
|
2547
2697
|
}
|
|
2698
|
+
// A prompt that only calls a tool shows the tool, not its words.
|
|
2548
2699
|
if (suppressAssistantTextForToolOnlyRef.current) {
|
|
2549
|
-
const suppressedMessageId = currentAssistantMessageRef.current?.id;
|
|
2550
|
-
if (suppressedMessageId) {
|
|
2551
|
-
setDisplayItems(prev => prev.filter(item => isToolCallMessage(item) || item.id !== suppressedMessageId));
|
|
2552
|
-
if (useStoreMode) {
|
|
2553
|
-
useChatStore.getState().deleteMessage(suppressedMessageId);
|
|
2554
|
-
}
|
|
2555
|
-
currentAssistantMessageRef.current = null;
|
|
2556
|
-
}
|
|
2557
2700
|
break;
|
|
2558
2701
|
}
|
|
2559
2702
|
if (event.message) {
|
|
2560
|
-
const
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
if (currentId && !isNewMessage) {
|
|
2564
|
-
/*
|
|
2565
|
-
* The message to write to is the one current now, as the event
|
|
2566
|
-
* arrives — not whichever the ref names when React gets round
|
|
2567
|
-
* to running the updater. A whole answer in one network chunk
|
|
2568
|
-
* queues every delta's updater in the same task, and the turn's
|
|
2569
|
-
* end clears the ref before any of them runs: read lazily, all
|
|
2570
|
-
* but the first found nothing to update, and the conversation
|
|
2571
|
-
* and the balloon showed only the answer's first word.
|
|
2572
|
-
*/
|
|
2573
|
-
const rawContent = event.message.content;
|
|
2574
|
-
const sanitizedContent = typeof rawContent === 'string'
|
|
2575
|
-
? sanitizeAssistantContent(rawContent)
|
|
2576
|
-
: (rawContent ?? '');
|
|
2577
|
-
if (typeof sanitizedContent === 'string' && sanitizedContent) {
|
|
2578
|
-
inspectAnswerRef.current = sanitizedContent;
|
|
2579
|
-
}
|
|
2580
|
-
setDisplayItems(prev => {
|
|
2581
|
-
const newItems = [...prev];
|
|
2582
|
-
const idx = newItems.findIndex(item => !isToolCallMessage(item) && item.id === currentId);
|
|
2583
|
-
if (idx >= 0 && !isToolCallMessage(newItems[idx])) {
|
|
2584
|
-
newItems[idx] = {
|
|
2585
|
-
...newItems[idx],
|
|
2586
|
-
content: sanitizedContent,
|
|
2587
|
-
};
|
|
2588
|
-
}
|
|
2589
|
-
return newItems;
|
|
2590
|
-
});
|
|
2591
|
-
if (useStoreMode) {
|
|
2592
|
-
useChatStore.getState().updateMessage(currentId, {
|
|
2593
|
-
content: sanitizedContent,
|
|
2594
|
-
});
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2597
|
-
else {
|
|
2598
|
-
const content = event.message.content;
|
|
2599
|
-
const contentStr = typeof content === 'string' ? content : (content ?? '');
|
|
2600
|
-
const sanitizedContent = typeof contentStr === 'string'
|
|
2601
|
-
? sanitizeAssistantContent(contentStr)
|
|
2602
|
-
: '';
|
|
2603
|
-
const newMessage = createAssistantMessage(sanitizedContent);
|
|
2604
|
-
newMessage.id = event.message.id || newMessage.id;
|
|
2605
|
-
currentAssistantMessageRef.current = newMessage;
|
|
2606
|
-
setDisplayItems(prev => {
|
|
2607
|
-
const existingIdx = prev.findIndex(item => !isToolCallMessage(item) && item.id === newMessage.id);
|
|
2608
|
-
if (existingIdx >= 0) {
|
|
2609
|
-
const newItems = [...prev];
|
|
2610
|
-
newItems[existingIdx] = {
|
|
2611
|
-
...newItems[existingIdx],
|
|
2612
|
-
content: sanitizedContent,
|
|
2613
|
-
};
|
|
2614
|
-
return newItems;
|
|
2615
|
-
}
|
|
2616
|
-
return [...prev, newMessage];
|
|
2617
|
-
});
|
|
2618
|
-
if (useStoreMode) {
|
|
2619
|
-
const existingInStore = useChatStore
|
|
2620
|
-
.getState()
|
|
2621
|
-
.messages.find(m => m.id === newMessage.id);
|
|
2622
|
-
if (existingInStore) {
|
|
2623
|
-
useChatStore.getState().updateMessage(newMessage.id, {
|
|
2624
|
-
content: sanitizedContent,
|
|
2625
|
-
});
|
|
2626
|
-
}
|
|
2627
|
-
else {
|
|
2628
|
-
useChatStore.getState().addMessage(newMessage);
|
|
2629
|
-
}
|
|
2630
|
-
}
|
|
2703
|
+
const content = event.message.content;
|
|
2704
|
+
if (typeof content === 'string' && content) {
|
|
2705
|
+
inspectAnswerRef.current = sanitizeAssistantContent(content);
|
|
2631
2706
|
}
|
|
2707
|
+
read();
|
|
2632
2708
|
}
|
|
2633
2709
|
break;
|
|
2634
2710
|
case 'tool-call':
|
|
2635
2711
|
if (event.toolCall && !stoppedRef.current) {
|
|
2636
|
-
const toolCallId = event.toolCall.toolCallId
|
|
2712
|
+
const toolCallId = event.toolCall.toolCallId;
|
|
2637
2713
|
const toolName = event.toolCall.toolName;
|
|
2638
2714
|
const args = event.toolCall.args || {};
|
|
2639
2715
|
/*
|
|
@@ -2644,38 +2720,23 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2644
2720
|
* `false`. Without the flag, the old heuristics decide.
|
|
2645
2721
|
*/
|
|
2646
2722
|
const argsComplete = event.toolCall.argsComplete;
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
existingToolCall.status === 'executing' &&
|
|
2662
|
-
(argsComplete ?? Object.keys(args).length > 0)) {
|
|
2663
|
-
pendingToolExecutionsRef.current++;
|
|
2664
|
-
executeFrontendTool(toolHandler, updatedToolCall, toolCallId);
|
|
2665
|
-
}
|
|
2723
|
+
const known = toolCardNow(toolCallId);
|
|
2724
|
+
read();
|
|
2725
|
+
const card = toolCardNow(toolCallId);
|
|
2726
|
+
if (!card) {
|
|
2727
|
+
break;
|
|
2728
|
+
}
|
|
2729
|
+
const frontendTool = frontendToolsRef.current?.find(t => t.name === toolName);
|
|
2730
|
+
const toolHandler = frontendTool?.handler;
|
|
2731
|
+
if (known) {
|
|
2732
|
+
if (toolHandler &&
|
|
2733
|
+
known.status === 'executing' &&
|
|
2734
|
+
(argsComplete ?? Object.keys(args).length > 0)) {
|
|
2735
|
+
pendingToolExecutionsRef.current++;
|
|
2736
|
+
executeFrontendTool(toolHandler, card, toolCallId);
|
|
2666
2737
|
}
|
|
2667
2738
|
}
|
|
2668
2739
|
else {
|
|
2669
|
-
const toolCallMsg = {
|
|
2670
|
-
id: `tool-${toolCallId}`,
|
|
2671
|
-
type: 'tool-call',
|
|
2672
|
-
toolCallId,
|
|
2673
|
-
toolName,
|
|
2674
|
-
args,
|
|
2675
|
-
status: 'executing',
|
|
2676
|
-
};
|
|
2677
|
-
toolCallsRef.current.set(toolCallId, toolCallMsg);
|
|
2678
|
-
setDisplayItems(prev => [...prev, toolCallMsg]);
|
|
2679
2740
|
// Fire pre-hook for new tool calls
|
|
2680
2741
|
onToolCallStartRef.current?.({
|
|
2681
2742
|
toolName,
|
|
@@ -2687,8 +2748,6 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2687
2748
|
name: toolName,
|
|
2688
2749
|
args,
|
|
2689
2750
|
});
|
|
2690
|
-
const frontendTool = frontendToolsRef.current?.find(t => t.name === toolName);
|
|
2691
|
-
const toolHandler = frontendTool?.handler;
|
|
2692
2751
|
// Execute when the args are complete. The protocol says so
|
|
2693
2752
|
// when it can (`argsComplete`); without the flag, fall back
|
|
2694
2753
|
// to the old heuristics — actual args present, or a tool
|
|
@@ -2701,129 +2760,48 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2701
2760
|
(Object.keys(args).length > 0 ||
|
|
2702
2761
|
!frontendToolExpectsArgs(frontendTool)))) {
|
|
2703
2762
|
pendingToolExecutionsRef.current++;
|
|
2704
|
-
executeFrontendTool(toolHandler,
|
|
2763
|
+
executeFrontendTool(toolHandler, card, toolCallId);
|
|
2705
2764
|
}
|
|
2706
2765
|
}
|
|
2707
2766
|
}
|
|
2708
2767
|
break;
|
|
2709
2768
|
case 'tool-result':
|
|
2710
2769
|
if (event.toolResult) {
|
|
2711
|
-
const toolCallId = event.toolResult.toolCallId;
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
hasError = true;
|
|
2733
|
-
}
|
|
2734
|
-
if (resultData.code_error &&
|
|
2735
|
-
typeof resultData.code_error === 'object') {
|
|
2736
|
-
const ce = resultData.code_error;
|
|
2737
|
-
codeError = {
|
|
2738
|
-
name: ce.name || 'Error',
|
|
2739
|
-
value: ce.value || 'Unknown error',
|
|
2740
|
-
traceback: ce.traceback,
|
|
2741
|
-
};
|
|
2742
|
-
hasError = true;
|
|
2743
|
-
}
|
|
2744
|
-
if ('exit_code' in resultData) {
|
|
2745
|
-
const ec = resultData.exit_code;
|
|
2746
|
-
exitCode = typeof ec === 'number' ? ec : null;
|
|
2747
|
-
if (exitCode != null && exitCode !== 0)
|
|
2748
|
-
hasError = true;
|
|
2749
|
-
}
|
|
2750
|
-
if ('execution_ok' in resultData &&
|
|
2751
|
-
resultData.execution_ok === false) {
|
|
2752
|
-
hasError = true;
|
|
2753
|
-
}
|
|
2754
|
-
}
|
|
2755
|
-
const updatedToolCall = {
|
|
2756
|
-
...existingToolCall,
|
|
2757
|
-
result: event.toolResult.result,
|
|
2758
|
-
status: hasError
|
|
2759
|
-
? 'error'
|
|
2760
|
-
: isPendingApproval
|
|
2761
|
-
? 'inProgress'
|
|
2762
|
-
: isHumanInTheLoop
|
|
2763
|
-
? 'executing'
|
|
2764
|
-
: 'complete',
|
|
2765
|
-
error: event.toolResult.error,
|
|
2766
|
-
executionError,
|
|
2767
|
-
codeError,
|
|
2768
|
-
exitCode,
|
|
2769
|
-
};
|
|
2770
|
-
toolCallsRef.current.set(toolCallId, updatedToolCall);
|
|
2771
|
-
setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
|
|
2772
|
-
? updatedToolCall
|
|
2773
|
-
: item));
|
|
2774
|
-
recorderRef.current?.recorder.toolEnded(toolCallId, {
|
|
2775
|
-
result: event.toolResult.result,
|
|
2776
|
-
error: event.toolResult.error ??
|
|
2777
|
-
(updatedToolCall.status === 'error'
|
|
2778
|
-
? (executionError ?? 'failed')
|
|
2779
|
-
: undefined),
|
|
2780
|
-
});
|
|
2781
|
-
// Fire post-hook for tool results
|
|
2782
|
-
onToolCallCompleteRef.current?.({
|
|
2783
|
-
toolName: existingToolCall.toolName,
|
|
2784
|
-
toolCallId,
|
|
2785
|
-
args: existingToolCall.args,
|
|
2786
|
-
result: event.toolResult.result,
|
|
2787
|
-
status: updatedToolCall.status,
|
|
2788
|
-
error: event.toolResult.error,
|
|
2789
|
-
});
|
|
2790
|
-
}
|
|
2770
|
+
const toolCallId = event.toolResult.toolCallId ?? '';
|
|
2771
|
+
const known = toolCardNow(toolCallId);
|
|
2772
|
+
read();
|
|
2773
|
+
const card = toolCardNow(toolCallId);
|
|
2774
|
+
if (known && card) {
|
|
2775
|
+
recorderRef.current?.recorder.toolEnded(toolCallId, {
|
|
2776
|
+
result: event.toolResult.result,
|
|
2777
|
+
error: event.toolResult.error ??
|
|
2778
|
+
(card.status === 'error'
|
|
2779
|
+
? (card.executionError ?? 'failed')
|
|
2780
|
+
: undefined),
|
|
2781
|
+
});
|
|
2782
|
+
// Fire post-hook for tool results
|
|
2783
|
+
onToolCallCompleteRef.current?.({
|
|
2784
|
+
toolName: known.toolName,
|
|
2785
|
+
toolCallId,
|
|
2786
|
+
args: known.args,
|
|
2787
|
+
result: event.toolResult.result,
|
|
2788
|
+
status: card.status,
|
|
2789
|
+
error: event.toolResult.error,
|
|
2790
|
+
});
|
|
2791
2791
|
}
|
|
2792
2792
|
}
|
|
2793
2793
|
break;
|
|
2794
2794
|
case 'state-update':
|
|
2795
2795
|
onStateUpdate?.(event.data);
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
const [lastToolCallId, existingToolCall] = executingToolCalls[executingToolCalls.length - 1];
|
|
2800
|
-
const isHumanInTheLoop = existingToolCall.args &&
|
|
2801
|
-
'steps' in existingToolCall.args &&
|
|
2802
|
-
Array.isArray(existingToolCall.args.steps);
|
|
2803
|
-
if (!isHumanInTheLoop) {
|
|
2804
|
-
const stateData = event.data;
|
|
2805
|
-
const result = stateData.weather ??
|
|
2806
|
-
stateData.result ??
|
|
2807
|
-
stateData.toolResult ??
|
|
2808
|
-
stateData;
|
|
2809
|
-
const updatedToolCall = {
|
|
2810
|
-
...existingToolCall,
|
|
2811
|
-
result,
|
|
2812
|
-
status: 'complete',
|
|
2813
|
-
};
|
|
2814
|
-
toolCallsRef.current.set(lastToolCallId, updatedToolCall);
|
|
2815
|
-
setDisplayItems(prev => prev.map(item => isToolCallMessage(item) &&
|
|
2816
|
-
item.toolCallId === lastToolCallId
|
|
2817
|
-
? updatedToolCall
|
|
2818
|
-
: item));
|
|
2819
|
-
}
|
|
2820
|
-
}
|
|
2821
|
-
}
|
|
2796
|
+
// A tool that answers by setting the shared state: the newest
|
|
2797
|
+
// running call is answered with it (`conversationEventsOf`).
|
|
2798
|
+
read();
|
|
2822
2799
|
break;
|
|
2823
2800
|
case 'activity':
|
|
2824
2801
|
// What the server says besides the conversation's text comes on
|
|
2825
|
-
// the ui socket (REACTOR_UI RU-W); the run's
|
|
2826
|
-
//
|
|
2802
|
+
// the ui socket (REACTOR_UI RU-W); the run's own — `loop.session`
|
|
2803
|
+
// among them — are the conversation's.
|
|
2804
|
+
read();
|
|
2827
2805
|
break;
|
|
2828
2806
|
case 'done':
|
|
2829
2807
|
/*
|
|
@@ -2863,28 +2841,11 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2863
2841
|
// (including all continuations) has finished.
|
|
2864
2842
|
if (suppressAssistantTextForToolOnlyRef.current &&
|
|
2865
2843
|
hideMessagesAfterToolUIRef.current) {
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
return prev;
|
|
2872
|
-
}
|
|
2873
|
-
const latestCompletedTool = [...prev]
|
|
2874
|
-
.reverse()
|
|
2875
|
-
.find(item => isToolCallMessage(item) &&
|
|
2876
|
-
item.status === 'complete' &&
|
|
2877
|
-
item.result !== undefined);
|
|
2878
|
-
if (!latestCompletedTool ||
|
|
2879
|
-
!isToolCallMessage(latestCompletedTool)) {
|
|
2880
|
-
return prev;
|
|
2881
|
-
}
|
|
2882
|
-
const fallbackMessage = createAssistantMessage(formatToolResultFallback(latestCompletedTool.result));
|
|
2883
|
-
if (useStoreMode) {
|
|
2884
|
-
useChatStore.getState().addMessage(fallbackMessage);
|
|
2885
|
-
}
|
|
2886
|
-
return [...prev, fallbackMessage];
|
|
2887
|
-
});
|
|
2844
|
+
toolResultFallback();
|
|
2845
|
+
}
|
|
2846
|
+
// A stopped turn was ended when it was stopped.
|
|
2847
|
+
if (!stoppedRef.current) {
|
|
2848
|
+
read();
|
|
2888
2849
|
}
|
|
2889
2850
|
suppressAssistantTextForToolOnlyRef.current = false;
|
|
2890
2851
|
pendingToolExecutionsRef.current = 0;
|
|
@@ -2900,29 +2861,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2900
2861
|
if (event.error?.message &&
|
|
2901
2862
|
/exceeded maximum retries/i.test(event.error.message) &&
|
|
2902
2863
|
hideMessagesAfterToolUIRef.current) {
|
|
2903
|
-
|
|
2904
|
-
const hasAssistantContent = prev.some(item => !isToolCallMessage(item) &&
|
|
2905
|
-
item.role === 'assistant' &&
|
|
2906
|
-
String(item.content || '').trim().length > 0);
|
|
2907
|
-
if (hasAssistantContent) {
|
|
2908
|
-
return prev;
|
|
2909
|
-
}
|
|
2910
|
-
const latestCompletedTool = [...prev]
|
|
2911
|
-
.reverse()
|
|
2912
|
-
.find(item => isToolCallMessage(item) &&
|
|
2913
|
-
item.status === 'complete' &&
|
|
2914
|
-
item.result !== undefined);
|
|
2915
|
-
if (!latestCompletedTool ||
|
|
2916
|
-
!isToolCallMessage(latestCompletedTool)) {
|
|
2917
|
-
return prev;
|
|
2918
|
-
}
|
|
2919
|
-
const fallbackMessage = createAssistantMessage(formatToolResultFallback(latestCompletedTool.result));
|
|
2920
|
-
if (useStoreMode) {
|
|
2921
|
-
useChatStore.getState().addMessage(fallbackMessage);
|
|
2922
|
-
}
|
|
2923
|
-
return [...prev, fallbackMessage];
|
|
2924
|
-
});
|
|
2864
|
+
toolResultFallback();
|
|
2925
2865
|
}
|
|
2866
|
+
read();
|
|
2926
2867
|
suppressAssistantTextForToolOnlyRef.current = false;
|
|
2927
2868
|
setError(event.error || new Error('Unknown error'));
|
|
2928
2869
|
pendingToolExecutionsRef.current = 0;
|
|
@@ -2930,6 +2871,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2930
2871
|
setIsStreaming(false);
|
|
2931
2872
|
agentRuntimeStore.getState().requestRefresh(activeAgentId);
|
|
2932
2873
|
break;
|
|
2874
|
+
default:
|
|
2875
|
+
read();
|
|
2876
|
+
break;
|
|
2933
2877
|
}
|
|
2934
2878
|
});
|
|
2935
2879
|
adapter.connect().catch(console.error);
|
|
@@ -2959,31 +2903,25 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2959
2903
|
result,
|
|
2960
2904
|
});
|
|
2961
2905
|
}
|
|
2962
|
-
const completedToolCall = {
|
|
2963
|
-
...toolCallMsg,
|
|
2964
|
-
result,
|
|
2965
|
-
status: 'complete',
|
|
2966
|
-
};
|
|
2967
2906
|
recorderRef.current?.recorder.toolEnded(toolCallId, { result });
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
:
|
|
2907
|
+
reduce({
|
|
2908
|
+
type: 'TOOL_CALL_DONE',
|
|
2909
|
+
toolCallId,
|
|
2910
|
+
success: true,
|
|
2911
|
+
result,
|
|
2912
|
+
});
|
|
2972
2913
|
}
|
|
2973
2914
|
catch (err) {
|
|
2974
2915
|
console.error('[ChatBase] Frontend tool execution error:', err);
|
|
2975
|
-
const errorToolCall = {
|
|
2976
|
-
...toolCallMsg,
|
|
2977
|
-
status: 'error',
|
|
2978
|
-
error: err.message,
|
|
2979
|
-
};
|
|
2980
2916
|
recorderRef.current?.recorder.toolEnded(toolCallId, {
|
|
2981
2917
|
error: err.message,
|
|
2982
2918
|
});
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
:
|
|
2919
|
+
reduce({
|
|
2920
|
+
type: 'TOOL_CALL_DONE',
|
|
2921
|
+
toolCallId,
|
|
2922
|
+
success: false,
|
|
2923
|
+
error: err.message,
|
|
2924
|
+
});
|
|
2987
2925
|
}
|
|
2988
2926
|
finally {
|
|
2989
2927
|
pendingToolExecutionsRef.current--;
|
|
@@ -3093,62 +3031,53 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3093
3031
|
const userMessage = createUserMessage(messageContent, spoken ? { ...spoken } : undefined);
|
|
3094
3032
|
const currentMessages = displayItems.filter((item) => !isToolCallMessage(item));
|
|
3095
3033
|
const allMessages = [...currentMessages, userMessage];
|
|
3096
|
-
|
|
3034
|
+
{
|
|
3035
|
+
const { id, role: _role, content: _content, ...extras } = userMessage;
|
|
3036
|
+
changeLayer(kept => ({
|
|
3037
|
+
...kept,
|
|
3038
|
+
extras: new Map(kept.extras).set(id, extras),
|
|
3039
|
+
}));
|
|
3040
|
+
}
|
|
3041
|
+
changeConversation(state => reduceConversation(withPrompt(state, userMessage.id, messageContent), {
|
|
3042
|
+
type: 'RUN_STARTED',
|
|
3043
|
+
}));
|
|
3097
3044
|
setInput('');
|
|
3098
3045
|
setIsLoading(true);
|
|
3099
3046
|
setIsStreaming(true);
|
|
3100
3047
|
setError(null);
|
|
3101
|
-
currentAssistantMessageRef.current = null;
|
|
3102
|
-
if (useStoreMode) {
|
|
3103
|
-
useChatStore.getState().addMessage(userMessage);
|
|
3104
|
-
}
|
|
3105
3048
|
try {
|
|
3106
3049
|
if (onSendMessage) {
|
|
3107
3050
|
if (enableStreaming) {
|
|
3108
3051
|
const assistantMessageId = generateMessageId();
|
|
3109
|
-
const
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3052
|
+
const said = (text, streaming) => reduce({
|
|
3053
|
+
type: 'MESSAGE_SAID',
|
|
3054
|
+
messageId: assistantMessageId,
|
|
3055
|
+
role: 'assistant',
|
|
3056
|
+
text,
|
|
3057
|
+
streaming,
|
|
3058
|
+
});
|
|
3059
|
+
said('', true);
|
|
3113
3060
|
if (useStoreMode) {
|
|
3114
|
-
useChatStore.getState().addMessage(assistantMessage);
|
|
3115
3061
|
useChatStore.getState().startStreaming(assistantMessageId);
|
|
3116
3062
|
}
|
|
3117
3063
|
abortControllerRef.current = new AbortController();
|
|
3118
3064
|
await onSendMessage(messageContent, allMessages, {
|
|
3119
3065
|
onChunk: (chunk) => {
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
: item));
|
|
3126
|
-
if (useStoreMode) {
|
|
3127
|
-
useChatStore
|
|
3128
|
-
.getState()
|
|
3129
|
-
.appendToStream(assistantMessageId, chunk);
|
|
3130
|
-
}
|
|
3066
|
+
reduce({
|
|
3067
|
+
type: 'TEXT_MESSAGE_CONTENT',
|
|
3068
|
+
messageId: assistantMessageId,
|
|
3069
|
+
delta: chunk,
|
|
3070
|
+
});
|
|
3131
3071
|
},
|
|
3132
3072
|
onComplete: (fullResponse) => {
|
|
3133
|
-
|
|
3134
|
-
? { ...item, content: fullResponse }
|
|
3135
|
-
: item));
|
|
3073
|
+
said(fullResponse, false);
|
|
3136
3074
|
if (useStoreMode) {
|
|
3137
|
-
useChatStore.getState().updateMessage(assistantMessageId, {
|
|
3138
|
-
content: fullResponse,
|
|
3139
|
-
});
|
|
3140
3075
|
useChatStore.getState().stopStreaming();
|
|
3141
3076
|
}
|
|
3142
3077
|
},
|
|
3143
3078
|
onError: (error) => {
|
|
3144
|
-
|
|
3145
|
-
setDisplayItems(prev => prev.map(item => item.id === assistantMessageId
|
|
3146
|
-
? { ...item, content: errorContent }
|
|
3147
|
-
: item));
|
|
3079
|
+
said(chatText.error(error.message), false);
|
|
3148
3080
|
if (useStoreMode) {
|
|
3149
|
-
useChatStore.getState().updateMessage(assistantMessageId, {
|
|
3150
|
-
content: errorContent,
|
|
3151
|
-
});
|
|
3152
3081
|
useChatStore.getState().stopStreaming();
|
|
3153
3082
|
}
|
|
3154
3083
|
setError(error);
|
|
@@ -3159,11 +3088,13 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3159
3088
|
else {
|
|
3160
3089
|
const response = await onSendMessage(messageContent, allMessages);
|
|
3161
3090
|
if (response) {
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3091
|
+
reduce({
|
|
3092
|
+
type: 'MESSAGE_SAID',
|
|
3093
|
+
messageId: generateMessageId(),
|
|
3094
|
+
role: 'assistant',
|
|
3095
|
+
text: response,
|
|
3096
|
+
streaming: false,
|
|
3097
|
+
});
|
|
3167
3098
|
}
|
|
3168
3099
|
}
|
|
3169
3100
|
}
|
|
@@ -3193,8 +3124,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3193
3124
|
catch (err) {
|
|
3194
3125
|
if (err.name !== 'AbortError') {
|
|
3195
3126
|
console.error('[ChatBase] Send error:', err);
|
|
3196
|
-
|
|
3197
|
-
|
|
3127
|
+
// Said in the conversation, as the assistant's words.
|
|
3128
|
+
changeConversation(state => withError(state, err.message));
|
|
3198
3129
|
setError(err);
|
|
3199
3130
|
}
|
|
3200
3131
|
}
|
|
@@ -3205,12 +3136,14 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3205
3136
|
// For the non-adapter path (onSendMessage), 'done' is never
|
|
3206
3137
|
// emitted so we reset when no adapter is present.
|
|
3207
3138
|
if (!adapterRef.current) {
|
|
3139
|
+
if (conversationRef.current.running) {
|
|
3140
|
+
reduce({ type: 'RUN_FINISHED' });
|
|
3141
|
+
}
|
|
3208
3142
|
setIsLoading(false);
|
|
3209
3143
|
setIsStreaming(false);
|
|
3210
3144
|
agentRuntimeStore.getState().requestRefresh(activeAgentId);
|
|
3211
3145
|
}
|
|
3212
3146
|
suppressAssistantTextForToolOnlyRef.current = false;
|
|
3213
|
-
currentAssistantMessageRef.current = null;
|
|
3214
3147
|
abortControllerRef.current = null;
|
|
3215
3148
|
}
|
|
3216
3149
|
}, [
|
|
@@ -3228,6 +3161,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3228
3161
|
noteThreadStarted,
|
|
3229
3162
|
chatText,
|
|
3230
3163
|
uploadToConversation,
|
|
3164
|
+
changeConversation,
|
|
3165
|
+
changeLayer,
|
|
3166
|
+
reduce,
|
|
3231
3167
|
]);
|
|
3232
3168
|
// Send pending prompt once history loaded and adapter/handler available
|
|
3233
3169
|
useEffect(() => {
|
|
@@ -3290,28 +3226,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3290
3226
|
void adapter.terminateRequest().catch(() => { });
|
|
3291
3227
|
}
|
|
3292
3228
|
}
|
|
3293
|
-
//
|
|
3294
|
-
|
|
3295
|
-
if (toolCall.status === 'executing' || toolCall.status === 'inProgress') {
|
|
3296
|
-
toolCallsRef.current.set(toolCallId, {
|
|
3297
|
-
...toolCall,
|
|
3298
|
-
status: 'error',
|
|
3299
|
-
error: 'Interrupted by user',
|
|
3300
|
-
});
|
|
3301
|
-
}
|
|
3302
|
-
}
|
|
3303
|
-
setDisplayItems(prev => prev.map(item => {
|
|
3304
|
-
if (!isToolCallMessage(item))
|
|
3305
|
-
return item;
|
|
3306
|
-
if (item.status !== 'executing' && item.status !== 'inProgress') {
|
|
3307
|
-
return item;
|
|
3308
|
-
}
|
|
3309
|
-
return {
|
|
3310
|
-
...item,
|
|
3311
|
-
status: 'error',
|
|
3312
|
-
error: 'Interrupted by user',
|
|
3313
|
-
};
|
|
3314
|
-
}));
|
|
3229
|
+
// The turn ends stopped: what streamed stops, a call under way says so.
|
|
3230
|
+
changeConversation(state => withTurnEnded(state, 'stopped'));
|
|
3315
3231
|
if (useStoreMode) {
|
|
3316
3232
|
useChatStore.getState().stopStreaming();
|
|
3317
3233
|
}
|
|
@@ -3324,7 +3240,6 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3324
3240
|
// own stop event arrives — if it ever does.
|
|
3325
3241
|
agentRuntimeStore.getState().stopSubagentActivity();
|
|
3326
3242
|
suppressAssistantTextForToolOnlyRef.current = false;
|
|
3327
|
-
currentAssistantMessageRef.current = null;
|
|
3328
3243
|
// Also interrupt any code running in the sandbox (best-effort).
|
|
3329
3244
|
sandboxStatusQuery.interrupt();
|
|
3330
3245
|
/*
|
|
@@ -3384,8 +3299,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3384
3299
|
}, [handleSend]);
|
|
3385
3300
|
// ---- handleNewChat ----
|
|
3386
3301
|
const handleNewChat = useCallback(() => {
|
|
3387
|
-
|
|
3388
|
-
toolCallsRef.current.clear();
|
|
3302
|
+
resetConversation();
|
|
3389
3303
|
pendingToolExecutionsRef.current = 0;
|
|
3390
3304
|
setInput('');
|
|
3391
3305
|
threadIdRef.current = generateMessageId();
|
|
@@ -3395,7 +3309,14 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3395
3309
|
useConversationStore.getState().clearMessages(historyScopeId);
|
|
3396
3310
|
onNewChat?.();
|
|
3397
3311
|
headerButtons?.onNewChat?.();
|
|
3398
|
-
}, [
|
|
3312
|
+
}, [
|
|
3313
|
+
clearStoreMessages,
|
|
3314
|
+
onNewChat,
|
|
3315
|
+
headerButtons,
|
|
3316
|
+
useStoreMode,
|
|
3317
|
+
runtimeId,
|
|
3318
|
+
resetConversation,
|
|
3319
|
+
]);
|
|
3399
3320
|
// Hand the send function to a host that owns the input box (the LOOP
|
|
3400
3321
|
// workspace). Withdrawn on unmount so nothing holds a stale sender.
|
|
3401
3322
|
useEffect(() => {
|
|
@@ -3440,8 +3361,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3440
3361
|
// ---- handleClear ----
|
|
3441
3362
|
const handleClear = useCallback(() => {
|
|
3442
3363
|
if (window.confirm('Clear all messages?')) {
|
|
3443
|
-
|
|
3444
|
-
toolCallsRef.current.clear();
|
|
3364
|
+
resetConversation();
|
|
3445
3365
|
if (useStoreMode)
|
|
3446
3366
|
clearStoreMessages();
|
|
3447
3367
|
if (historyScopeId)
|
|
@@ -3449,22 +3369,32 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3449
3369
|
onClear?.();
|
|
3450
3370
|
headerButtons?.onClear?.();
|
|
3451
3371
|
}
|
|
3452
|
-
}, [
|
|
3372
|
+
}, [
|
|
3373
|
+
clearStoreMessages,
|
|
3374
|
+
onClear,
|
|
3375
|
+
headerButtons,
|
|
3376
|
+
useStoreMode,
|
|
3377
|
+
runtimeId,
|
|
3378
|
+
resetConversation,
|
|
3379
|
+
]);
|
|
3453
3380
|
// ---- Turn removal (the TurnFooter's remove action) ----
|
|
3454
3381
|
const handleRemoveItems = useCallback((ids) => {
|
|
3455
3382
|
const drop = new Set(ids);
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3383
|
+
// The turn's rows leave the conversation, its steps the web's layer.
|
|
3384
|
+
const rows = displayItemsRef.current
|
|
3385
|
+
.filter(item => drop.has(item.id))
|
|
3386
|
+
.map(rowIdOf);
|
|
3387
|
+
changeConversation(state => withoutItems(state, rows));
|
|
3388
|
+
changeLayer(kept => kept.steps.some(step => drop.has(step.item.id))
|
|
3389
|
+
? {
|
|
3390
|
+
...kept,
|
|
3391
|
+
steps: kept.steps.filter(step => !drop.has(step.item.id)),
|
|
3462
3392
|
}
|
|
3463
|
-
|
|
3464
|
-
}, []);
|
|
3393
|
+
: kept);
|
|
3394
|
+
}, [changeConversation, changeLayer]);
|
|
3465
3395
|
// ---- HITL respond handler (passed to MessageList) ----
|
|
3466
3396
|
const handleRespond = useCallback(async (toolCallId, result) => {
|
|
3467
|
-
const existingToolCall =
|
|
3397
|
+
const existingToolCall = toolCardNow(toolCallId);
|
|
3468
3398
|
if (existingToolCall &&
|
|
3469
3399
|
(existingToolCall.status === 'executing' ||
|
|
3470
3400
|
existingToolCall.status === 'inProgress')) {
|
|
@@ -3537,16 +3467,17 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3537
3467
|
if (isMcpApprovalTool && !requiresClientContinuation) {
|
|
3538
3468
|
return;
|
|
3539
3469
|
}
|
|
3540
|
-
|
|
3541
|
-
|
|
3470
|
+
// The person's decision, said on the call; the run goes on.
|
|
3471
|
+
changeConversation(state => reduceConversation(reduceConversation(state, {
|
|
3472
|
+
type: 'TOOL_CALL_DONE',
|
|
3473
|
+
toolCallId,
|
|
3474
|
+
success: approved,
|
|
3542
3475
|
result,
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
? updatedToolCall
|
|
3549
|
-
: item));
|
|
3476
|
+
answered: true,
|
|
3477
|
+
...(approved
|
|
3478
|
+
? {}
|
|
3479
|
+
: { error: 'Tool approval rejected by user' }),
|
|
3480
|
+
}), { type: 'RUN_STARTED' }));
|
|
3550
3481
|
setIsLoading(true);
|
|
3551
3482
|
setIsStreaming(true);
|
|
3552
3483
|
await adapterRef.current.sendToolResult(toolCallId, {
|
|
@@ -3572,15 +3503,14 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3572
3503
|
}
|
|
3573
3504
|
return;
|
|
3574
3505
|
}
|
|
3575
|
-
|
|
3576
|
-
|
|
3506
|
+
// The person's answer completes the call.
|
|
3507
|
+
reduce({
|
|
3508
|
+
type: 'TOOL_CALL_DONE',
|
|
3509
|
+
toolCallId,
|
|
3510
|
+
success: true,
|
|
3577
3511
|
result,
|
|
3578
|
-
|
|
3579
|
-
};
|
|
3580
|
-
toolCallsRef.current.set(toolCallId, updatedToolCall);
|
|
3581
|
-
setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
|
|
3582
|
-
? updatedToolCall
|
|
3583
|
-
: item));
|
|
3512
|
+
answered: true,
|
|
3513
|
+
});
|
|
3584
3514
|
if (adapterRef.current) {
|
|
3585
3515
|
let responseText;
|
|
3586
3516
|
if (typeof result === 'string') {
|
|
@@ -3612,6 +3542,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3612
3542
|
};
|
|
3613
3543
|
setIsLoading(true);
|
|
3614
3544
|
setIsStreaming(true);
|
|
3545
|
+
reduce({ type: 'RUN_STARTED' });
|
|
3615
3546
|
try {
|
|
3616
3547
|
const allMessages = displayItems.filter((item) => !isToolCallMessage(item));
|
|
3617
3548
|
await adapterRef.current.sendMessage(userMessage, {
|
|
@@ -3626,7 +3557,14 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3626
3557
|
// event will handle it when the run truly completes.
|
|
3627
3558
|
}
|
|
3628
3559
|
}
|
|
3629
|
-
}, [
|
|
3560
|
+
}, [
|
|
3561
|
+
displayItems,
|
|
3562
|
+
onApproveApproval,
|
|
3563
|
+
onRejectApproval,
|
|
3564
|
+
toolCardNow,
|
|
3565
|
+
changeConversation,
|
|
3566
|
+
reduce,
|
|
3567
|
+
]);
|
|
3630
3568
|
handleRespondRef.current = handleRespond;
|
|
3631
3569
|
// ---- Suggestion handlers (for EmptyState) ----
|
|
3632
3570
|
const handleSuggestionSubmit = useCallback((suggestion) => {
|