@datalayer/agent-runtimes 1.5.43 → 1.5.44

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.
Files changed (77) hide show
  1. package/dist-embed/chunks/1f30a-DxwAt2Ba.js +1 -0
  2. package/dist-embed/chunks/1f310-AqUM9a8q.js +1 -0
  3. package/dist-embed/chunks/1f324-8MHLOhTj.js +1 -0
  4. package/dist-embed/chunks/1f326-YSBr3aXI.js +1 -0
  5. package/dist-embed/chunks/1f327-Ie-nQBNO.js +1 -0
  6. package/dist-embed/chunks/1f33e-Cfl2WM63.js +1 -0
  7. package/dist-embed/chunks/1f372-3XLDlXkE.js +1 -0
  8. package/dist-embed/chunks/1f373-BPUJt2bp.js +1 -0
  9. package/dist-embed/chunks/1f39b-CSNtbAXr.js +1 -0
  10. package/dist-embed/chunks/1f3ab-BUCNQHz1.js +1 -0
  11. package/dist-embed/chunks/1f41b-Dre1z0gP.js +1 -0
  12. package/dist-embed/chunks/1f441-D8yah4C4.js +1 -0
  13. package/dist-embed/chunks/1f465-CPle6Hjq.js +1 -0
  14. package/dist-embed/chunks/1f4b9-aacFyckt.js +1 -0
  15. package/dist-embed/chunks/1f4be-DPAcAU4R.js +1 -0
  16. package/dist-embed/chunks/1f4c1-Ch6aJ0cj.js +1 -0
  17. package/dist-embed/chunks/1f4c2-Dx7BKmLO.js +1 -0
  18. package/dist-embed/chunks/1f4c4-CNvjvC5k.js +1 -0
  19. package/dist-embed/chunks/1f4d3-GTxRgjv7.js +1 -0
  20. package/dist-embed/chunks/1f4d6-COOZY-Q_.js +1 -0
  21. package/dist-embed/chunks/1f4dc-BhoW4cjD.js +1 -0
  22. package/dist-embed/chunks/1f4e1-DezU8ow-.js +1 -0
  23. package/dist-embed/chunks/1f4e2-DsStZsI-.js +1 -0
  24. package/dist-embed/chunks/1f4e7-BBi7c_zx.js +1 -0
  25. package/dist-embed/chunks/1f500-D9xRo-IT.js +1 -0
  26. package/dist-embed/chunks/1f501-CzKeDPcw.js +1 -0
  27. package/dist-embed/chunks/1f504-BYIHMoDP.js +1 -0
  28. package/dist-embed/chunks/1f52d-2QxCLKhU.js +1 -0
  29. package/dist-embed/chunks/1f552-4d0Y0kn7.js +1 -0
  30. package/dist-embed/chunks/1f58b-Cp8F_HBg.js +1 -0
  31. package/dist-embed/chunks/1f5b2-D6BUULuh.js +1 -0
  32. package/dist-embed/chunks/1f5c3-DjkgJLaz.js +1 -0
  33. package/dist-embed/chunks/1f5d1-C4Kpe4HL.js +1 -0
  34. package/dist-embed/chunks/1f5dc-DyLO7aB9.js +1 -0
  35. package/dist-embed/chunks/1f604-CEekcaYE.js +1 -0
  36. package/dist-embed/chunks/1f917-CyyS9fgf.js +1 -0
  37. package/dist-embed/chunks/1f9d1-200d-2696-D9eSB-0q.js +1 -0
  38. package/dist-embed/chunks/1fa79-DHhIZbWB.js +1 -0
  39. package/dist-embed/chunks/1fa9d-DUI9e77m.js +1 -0
  40. package/dist-embed/chunks/1fa9f-10ALsMaD.js +1 -0
  41. package/dist-embed/chunks/23f3-ASjqFsbT.js +1 -0
  42. package/dist-embed/chunks/2611-BnnmLm-K.js +1 -0
  43. package/dist-embed/chunks/2702-CfLyp35r.js +1 -0
  44. package/dist-embed/chunks/270f-CLwo30mk.js +1 -0
  45. package/dist-embed/chunks/2728-Qg6be-6p.js +1 -0
  46. package/dist-embed/chunks/2b1c-Uc1aRwAf.js +1 -0
  47. package/dist-embed/datalayer-app-main.js +66 -66
  48. package/lib/chat/ChatInline.js +43 -22
  49. package/lib/chat/base/ChatBase.js +43 -2
  50. package/lib/components/agents/AgentSummary.d.ts +2 -24
  51. package/lib/components/agents/AgentSummary.js +3 -19
  52. package/lib/hooks/useChat.js +110 -144
  53. package/lib/portable/activity.d.ts +78 -0
  54. package/lib/portable/activity.js +129 -0
  55. package/lib/portable/fixtures.d.ts +7 -0
  56. package/lib/portable/fixtures.js +50 -0
  57. package/lib/portable/index.d.ts +4 -1
  58. package/lib/portable/index.js +7 -1
  59. package/lib/portable/lifecycle.d.ts +77 -0
  60. package/lib/portable/lifecycle.js +135 -0
  61. package/lib/portable/runtimes.d.ts +68 -0
  62. package/lib/portable/runtimes.js +69 -0
  63. package/lib/voice/VoiceMenu.d.ts +2 -1
  64. package/lib/voice/VoiceMenu.js +2 -2
  65. package/lib/voice/chatVoice.d.ts +5 -2
  66. package/lib/voice/chatVoice.js +10 -4
  67. package/lib/voice/choices.d.ts +2 -0
  68. package/lib/voice/choices.js +6 -0
  69. package/lib/voice/index.d.ts +1 -0
  70. package/lib/voice/index.js +1 -0
  71. package/lib/voice/sayAloud.d.ts +25 -0
  72. package/lib/voice/sayAloud.js +34 -0
  73. package/lib/voice/serverHearing.d.ts +34 -2
  74. package/lib/voice/serverHearing.js +234 -6
  75. package/lib/voice/settings.d.ts +12 -0
  76. package/lib/voice/settings.js +9 -0
  77. package/package.json +1 -1
@@ -6,7 +6,18 @@ import { CopyIcon, ArrowLeftIcon, SyncIcon, TrashIcon, CheckIcon, PlusIcon, Penc
6
6
  import { SparklesIcon } from '@datalayer/icons-react';
7
7
  import { AGUIAdapter } from '../protocols';
8
8
  import { generateMessageId } from '../types/messages';
9
- import { sanitizeAssistantContent } from '../utils';
9
+ import { BROWSER_LIMITS, emptyConversation, reduceConversation, } from '../portable/conversation';
10
+ import { conversationEventsOf } from '../portable/adapterEvents';
11
+ /** The answer so far: the newest assistant message of the conversation. */
12
+ function answerOf(state) {
13
+ for (let index = state.items.length - 1; index >= 0; index -= 1) {
14
+ const item = state.items[index];
15
+ if (item.kind === 'message' && item.role === 'assistant') {
16
+ return item.text;
17
+ }
18
+ }
19
+ return '';
20
+ }
10
21
  /**
11
22
  * Pre-defined AI action groups
12
23
  */
@@ -79,8 +90,15 @@ export function ChatInline({ selectedText, protocol, onReplaceSelection, onInser
79
90
  const [input, setInput] = useState('');
80
91
  // AI state
81
92
  const [aiState, setAiState] = useState('initial');
82
- // AI response content
83
- const [aiResponse, setAiResponse] = useState('');
93
+ // The one-shot conversation (MOBILE M-030): `reduceConversation`'s, as in
94
+ // ChatBase and on the phone; the response is its newest assistant message.
95
+ const [conversation, setConversation] = useState(() => emptyConversation(BROWSER_LIMITS));
96
+ const conversationRef = useRef(conversation);
97
+ const changeConversation = useCallback((next) => {
98
+ conversationRef.current = next;
99
+ setConversation(next);
100
+ }, []);
101
+ const aiResponse = useMemo(() => answerOf(conversation), [conversation]);
84
102
  // Previous prompt for regeneration
85
103
  const [previousPrompt, setPreviousPrompt] = useState('');
86
104
  // Current page in options menu (for sub-menus like translate)
@@ -112,20 +130,23 @@ export function ChatInline({ selectedText, protocol, onReplaceSelection, onInser
112
130
  adapterRef.current = adapter;
113
131
  // Subscribe to events
114
132
  unsubscribeRef.current = adapter.subscribe((event) => {
115
- switch (event.type) {
116
- case 'message':
117
- if (event.message?.content) {
118
- const content = typeof event.message.content === 'string'
119
- ? sanitizeAssistantContent(event.message.content)
120
- : '';
121
- setAiResponse(content);
122
- }
123
- break;
124
- case 'error':
125
- console.error('[ChatInline] Protocol error:', event.error);
126
- setError(event.error?.message || 'Unknown error');
127
- setAiState('initial');
128
- break;
133
+ if (event.type === 'error') {
134
+ console.error('[ChatInline] Protocol error:', event.error);
135
+ setError(event.error?.message || 'Unknown error');
136
+ setAiState('initial');
137
+ return;
138
+ }
139
+ // The event, as the conversation reads it: one reducer whatever the protocol.
140
+ try {
141
+ const inputs = conversationEventsOf(event, conversationRef.current);
142
+ if (inputs.length) {
143
+ changeConversation(inputs.reduce((kept, input) => reduceConversation(kept, input), conversationRef.current));
144
+ }
145
+ }
146
+ catch (failure) {
147
+ console.error('[ChatInline] Protocol event not read:', failure);
148
+ setError(failure instanceof Error ? failure.message : String(failure));
149
+ setAiState('initial');
129
150
  }
130
151
  });
131
152
  // Connect
@@ -134,7 +155,7 @@ export function ChatInline({ selectedText, protocol, onReplaceSelection, onInser
134
155
  unsubscribeRef.current?.();
135
156
  adapterRef.current?.disconnect();
136
157
  };
137
- }, [protocol]);
158
+ }, [protocol, changeConversation]);
138
159
  // Submit prompt to AI
139
160
  const submitPrompt = useCallback(async (prompt) => {
140
161
  if (!adapterRef.current) {
@@ -145,7 +166,7 @@ export function ChatInline({ selectedText, protocol, onReplaceSelection, onInser
145
166
  setInput('');
146
167
  setPreviousPrompt(prompt);
147
168
  setError(null);
148
- setAiResponse('');
169
+ changeConversation(emptyConversation(BROWSER_LIMITS));
149
170
  // Build system message with selected text context
150
171
  const systemContext = `Do not surround your answer in quote marks. Only return the answer, nothing else. The user is selecting this text:
151
172
 
@@ -178,7 +199,7 @@ ${selectedText || ''}
178
199
  setError(err.message);
179
200
  setAiState('initial');
180
201
  }
181
- }, [selectedText]);
202
+ }, [selectedText, changeConversation]);
182
203
  // Handle pending prompt from toolbar dropdown
183
204
  useEffect(() => {
184
205
  if (pendingPrompt && aiState === 'initial') {
@@ -235,8 +256,8 @@ ${selectedText || ''}
235
256
  // Handle back to options
236
257
  const handleBackToOptions = useCallback(() => {
237
258
  setAiState('initial');
238
- setAiResponse('');
239
- }, []);
259
+ changeConversation(emptyConversation(BROWSER_LIMITS));
260
+ }, [changeConversation]);
240
261
  // Handle discard
241
262
  const handleDiscard = useCallback(() => {
242
263
  onClose?.();
@@ -25,7 +25,7 @@ import { SkeletonText } from '@primer/react/experimental';
25
25
  import { SidebarExpandIcon } from '@primer/octicons-react';
26
26
  import { notebookStore, JupyterReactTheme } from '@datalayer/jupyter-react';
27
27
  import { Box, setupPrimerPortals, useThemeStore, getColorPalette, getThemeConfig, useSystemColorMode, } from '@datalayer/primer-addons';
28
- import { AlertIcon, PersonIcon } from '@primer/octicons-react';
28
+ import { AlertIcon, PersonIcon, UnmuteIcon } from '@primer/octicons-react';
29
29
  import { AiAgentIcon } from '@datalayer/icons-react';
30
30
  import { QueryClientProvider, QueryClientContext } from '@tanstack/react-query';
31
31
  import { useCoreStore } from '@datalayer/core';
@@ -57,6 +57,7 @@ import { settledVoice } from '../../voice/chatVoice';
57
57
  import { useVoiceSettings } from '../../voice/useVoiceSettings';
58
58
  import { useVoiceSpeaker } from '../../voice/useVoiceSpeaker';
59
59
  import { useSpokenAnswers } from '../../voice/useSpokenAnswers';
60
+ import { lineToSay } from '../../voice/sayAloud';
60
61
  import { isFloatingChatViewMode } from '../viewModes';
61
62
  import { ToolApprovalBanner, ToolApprovalDialog, } from '../tools';
62
63
  import { initialModelId, isOffered, offeredModels as offeredModelsFor, } from './modelChoice';
@@ -1819,6 +1820,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
1819
1820
  recorderRef.current = null;
1820
1821
  }
1821
1822
  const inspectAnswerRef = useRef('');
1823
+ // A line the agent says aloud, from a run's `voice.say` (VOICE.md VO-35).
1824
+ const voiceSayRef = useRef(undefined);
1822
1825
  const handleRespondRef = useRef(null);
1823
1826
  // A decision made outside the chat — a host page's own approval banner —
1824
1827
  // is answered exactly as the tool card's buttons answer it: through the
@@ -2801,6 +2804,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2801
2804
  // What the server says besides the conversation's text comes on
2802
2805
  // the ui socket (REACTOR_UI RU-W); the run's own — `loop.session`
2803
2806
  // among them — are the conversation's.
2807
+ if (event.activity) {
2808
+ voiceSayRef.current?.(event.activity);
2809
+ }
2804
2810
  read();
2805
2811
  break;
2806
2812
  case 'done':
@@ -2968,6 +2974,31 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2968
2974
  const ownVoice = useVoiceSpeaker(settled, !hostSpeaks);
2969
2975
  useSpokenAnswers(displayItems, isLoading, !hostSpeaks && settled?.output === 'always', ownVoice.speaker);
2970
2976
  const speakingNow = voiceSpeaking || ownVoice.state.speaking;
2977
+ /*
2978
+ * A line the agent wants heard, not written (VO-35): a caption while the
2979
+ * run goes on, said by the speaker where answers are said by themselves.
2980
+ */
2981
+ const [voiceSaid, setVoiceSaid] = useState('');
2982
+ voiceSayRef.current = activity => {
2983
+ const line = lineToSay(activity);
2984
+ if (!line) {
2985
+ return;
2986
+ }
2987
+ setVoiceSaid(line);
2988
+ if (settled?.output === 'always') {
2989
+ if (onReadAloud) {
2990
+ onReadAloud(line);
2991
+ }
2992
+ else {
2993
+ ownVoice.speaker?.say(line);
2994
+ }
2995
+ }
2996
+ };
2997
+ useEffect(() => {
2998
+ if (!isLoading) {
2999
+ setVoiceSaid('');
3000
+ }
3001
+ }, [isLoading]);
2971
3002
  // Answers spoken over, hands-free (VOICE.md VO-13): marked *interrupted*.
2972
3003
  const [interruptedIds, setInterruptedIds] = useState(() => new Set());
2973
3004
  const stopSpeaking = onStopSpeaking ?? (ownVoice.speaker ? ownVoice.stop : undefined);
@@ -3762,7 +3793,17 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3762
3793
  }, send: (text, metadata) => void handleSend(text, undefined, metadata) })) : undefined;
3763
3794
  // Why an answer could not be heard, in a sentence, by the composer.
3764
3795
  const voiceRefused = !hostSpeaks && ownVoice.state.refused ? (_jsx(Text, { role: "alert", sx: { fontSize: 0, color: 'danger.fg', maxWidth: 260 }, "data-voice-refused": "", children: ownVoice.state.refused })) : null;
3765
- const inputToolbar = showInput ? (_jsx(InputPrompt, { footerExtras: voiceRefused ? (_jsxs(_Fragment, { children: [voiceRefused, voiceControl] })) : (voiceControl), interim: voiceInterim, input: input, setInput: setInput, isLoading: isLoading, kernelStatus: liveKernelStatus, connectionConfirmed: connectionConfirmed, placeholder: placeholder,
3796
+ // The line said aloud, written as it is said: captions always (VO-65).
3797
+ const voiceSayCaption = voiceSaid ? (_jsxs(Text, { "aria-live": "polite", sx: {
3798
+ fontSize: 0,
3799
+ fontStyle: 'italic',
3800
+ color: 'fg.muted',
3801
+ maxWidth: 260,
3802
+ display: 'inline-flex',
3803
+ alignItems: 'center',
3804
+ gap: 1,
3805
+ }, "data-voice-say": "", children: [_jsx(UnmuteIcon, { size: 12, "aria-label": "Said aloud" }), voiceSaid] })) : null;
3806
+ const inputToolbar = showInput ? (_jsx(InputPrompt, { footerExtras: voiceRefused || voiceSayCaption ? (_jsxs(_Fragment, { children: [voiceSayCaption, voiceRefused, voiceControl] })) : (voiceControl), interim: voiceInterim, input: input, setInput: setInput, isLoading: isLoading, kernelStatus: liveKernelStatus, connectionConfirmed: connectionConfirmed, placeholder: placeholder,
3766
3807
  /*
3767
3808
  The same openers the empty state offers, typed into the box they would
3768
3809
  be typed into.
@@ -1,28 +1,6 @@
1
1
  import React from 'react';
2
- export interface AgentSummaryData {
3
- agentName: string;
4
- location: string;
5
- specId?: string;
6
- /**
7
- * The framework turning this agent's loop, as its spec declares it —
8
- * `pydantic-ai` server-side, `vercel-ai` in the page.
9
- */
10
- harness?: string;
11
- /**
12
- * The runtime variant the host chose: `<location>-<harness>`.
13
- *
14
- * Shown next to the harness rather than instead of it, because they answer
15
- * different questions and can disagree: the harness is what the spec asked
16
- * for, the variant is what it actually got.
17
- */
18
- variant?: string;
19
- status?: string;
20
- baseUrl?: string;
21
- sandboxBaseUrl?: string;
22
- agentId?: string;
23
- isReady?: boolean;
24
- error?: string;
25
- }
2
+ import { type AgentSummaryData } from '../../portable/runtimes';
3
+ export type { AgentSummaryData } from '../../portable/runtimes';
26
4
  export interface AgentSummaryProps {
27
5
  summary: AgentSummaryData | null;
28
6
  title?: string;
@@ -7,19 +7,7 @@ import { useEffect, useRef, useState } from 'react';
7
7
  import { Spinner } from '@primer/react';
8
8
  import { Box } from '@datalayer/primer-addons';
9
9
  import { strip } from '@datalayer/core/lib/utils';
10
- /**
11
- * Statuses that represent an agent actively being created/started. The spinner
12
- * only shows for these; app-navigation states ("selected", "switching") and
13
- * settled states ("ready", "running", ...) never spin.
14
- */
15
- const CREATING_STATUSES = new Set([
16
- 'creating',
17
- 'starting',
18
- 'launching',
19
- 'connecting',
20
- 'pending',
21
- 'resuming',
22
- ]);
10
+ import { isCreatingAgent, } from '../../portable/runtimes';
23
11
  const MAX_AGENT_NAME_LENGTH = 32;
24
12
  const toSandboxStatusUrl = (value) => {
25
13
  const raw = String(value || '').trim();
@@ -95,12 +83,8 @@ export const AgentSummary = ({ summary, title = 'Active Agent', }) => {
95
83
  if (!summary) {
96
84
  return null;
97
85
  }
98
- const normalizedStatus = (summary.status || '').toLowerCase();
99
- // Only show the spinner while the agent is genuinely being created: it is not
100
- // ready, has no id yet, and its status is an active creation/startup state.
101
- const isCreating = !summary.isReady &&
102
- !summary.agentId &&
103
- CREATING_STATUSES.has(normalizedStatus);
86
+ // Only show the spinner while the agent is genuinely being created.
87
+ const isCreating = isCreatingAgent(summary);
104
88
  const hasError = Boolean(summary.error);
105
89
  // The agent is optional: some examples run only a code sandbox. Show the
106
90
  // agent status when there is an agent (or one is being created).
@@ -10,8 +10,11 @@
10
10
  */
11
11
  import { useCallback, useMemo } from 'react';
12
12
  import { useChatStore } from '../stores';
13
- import { createUserMessage, createAssistantMessage, generateMessageId, } from '../types/messages';
14
- import { sanitizeAssistantContent } from '../utils';
13
+ import { createUserMessage, generateMessageId } from '../types/messages';
14
+ import { isToolCallMessage } from '../utils';
15
+ import { BROWSER_LIMITS, emptyConversation, reduceConversation, withPrompt, } from '../portable/conversation';
16
+ import { conversationEventsOf, snapshotOfMessages, } from '../portable/adapterEvents';
17
+ import { conversationDrawer, emptyChatLayer, } from '../chat/base/conversationItems';
15
18
  /**
16
19
  * Main hook for chat functionality
17
20
  *
@@ -56,13 +59,17 @@ export function useChat() {
56
59
  const clearMessages = useChatStore(state => state.clearMessages);
57
60
  const setLoading = useChatStore(state => state.setLoading);
58
61
  const setError = useChatStore(state => state.setError);
62
+ const setMessages = useChatStore(state => state.setMessages);
59
63
  const startStreaming = useChatStore(state => state.startStreaming);
60
- const appendToStream = useChatStore(state => state.appendToStream);
61
64
  const stopStreaming = useChatStore(state => state.stopStreaming);
62
65
  const setSuggestions = useChatStore(state => state.setSuggestions);
63
66
  const getTools = useChatStore(state => state.getTools);
64
67
  /**
65
- * Send a user message and get AI response
68
+ * Send a user message and get the AI's response. The conversation is
69
+ * `reduceConversation`'s (MOBILE M-030), as in ChatBase and on the phone:
70
+ * every adapter event read by `conversationEventsOf`, an inference
71
+ * provider's deltas as AG-UI text events, and the store's messages drawn
72
+ * from it.
66
73
  */
67
74
  const sendMessage = useCallback(async (content, options) => {
68
75
  // Check if we have a way to send messages
@@ -70,102 +77,95 @@ export function useChat() {
70
77
  setError(new Error('No inference provider or protocol adapter configured.'));
71
78
  return;
72
79
  }
73
- // Create and add user message
74
80
  const userMessage = createUserMessage(content);
75
- addMessage(userMessage);
76
- // Clear error and set loading
81
+ const draw = conversationDrawer(words => words);
82
+ let conversation = withPrompt(reduceConversation(emptyConversation(BROWSER_LIMITS), snapshotOfMessages(messages)), userMessage.id, content);
83
+ const show = (next) => {
84
+ conversation = next;
85
+ setMessages(draw(conversation, emptyChatLayer()).filter((item) => !isToolCallMessage(item)));
86
+ };
87
+ const reduce = (events) => {
88
+ if (events.length) {
89
+ show(events.reduce((kept, event) => reduceConversation(kept, event), conversation));
90
+ }
91
+ };
92
+ /** An adapter's event, as the conversation reads it; its failure is the chat's error. */
93
+ const read = (event) => {
94
+ if (event.type === 'error') {
95
+ if (event.error) {
96
+ setError(event.error);
97
+ }
98
+ return;
99
+ }
100
+ try {
101
+ reduce(conversationEventsOf(event, conversation));
102
+ }
103
+ catch (failure) {
104
+ setError(failure instanceof Error ? failure : new Error(String(failure)));
105
+ }
106
+ };
107
+ show(conversation);
77
108
  setError(null);
78
109
  setLoading(true);
110
+ startStreaming(generateMessageId());
79
111
  try {
80
- // Get registered tools
81
112
  const tools = getTools();
82
- // Merge options
83
113
  const requestOptions = {
84
114
  ...options,
85
115
  tools: tools.length > 0 ? tools : undefined,
86
116
  };
87
- // Get all messages for context
117
+ // Every message so far, for context.
88
118
  const allMessages = [...messages, userMessage];
89
- // Create placeholder for assistant message
90
- const assistantMessageId = generateMessageId();
91
- const assistantMessage = createAssistantMessage('');
92
- assistantMessage.id = assistantMessageId;
93
- addMessage(assistantMessage);
94
- // Stream the response
95
- startStreaming(assistantMessageId);
96
- // Use protocol adapter if available (for ag-ui, a2a, acp)
97
- // Otherwise fall back to inference provider (for vercel-ai, OpenAI-compatible)
119
+ // A protocol adapter (ag-ui, a2a, acp) when there is one; else the
120
+ // inference provider (vercel-ai, OpenAI-compatible).
98
121
  if (protocolAdapter) {
99
- // Track pending tool calls for this run
122
+ // The calls this run asks the page's tools for, run once it ends.
100
123
  const pendingToolCalls = new Map();
101
- let currentAssistantContent = '';
102
- // Subscribe to adapter events
103
124
  const unsubscribe = protocolAdapter.subscribe(event => {
104
- switch (event.type) {
105
- case 'message':
106
- // AG-UI sends incremental messages with accumulating content
107
- if (event.message) {
108
- const rawContent = typeof event.message.content === 'string'
109
- ? event.message.content
110
- : '';
111
- currentAssistantContent =
112
- sanitizeAssistantContent(rawContent);
113
- // Update the assistant message with the current content
114
- updateMessage(assistantMessageId, {
115
- content: currentAssistantContent,
116
- toolCalls: event.message.toolCalls,
117
- });
118
- }
119
- break;
120
- case 'tool-call':
121
- // Track tool call for execution after stream completes
122
- if (event.toolCall) {
123
- console.log('[useChat] Tool call received:', event.toolCall.toolCallId, event.toolCall.toolName);
124
- pendingToolCalls.set(event.toolCall.toolCallId, {
125
- toolName: event.toolCall.toolName,
126
- args: event.toolCall.args,
127
- });
128
- }
129
- break;
130
- case 'error':
131
- if (event.error) {
132
- setError(event.error);
133
- }
134
- break;
125
+ if (event.type === 'tool-call' && event.toolCall) {
126
+ pendingToolCalls.set(event.toolCall.toolCallId, {
127
+ toolName: event.toolCall.toolName,
128
+ args: event.toolCall.args,
129
+ });
135
130
  }
131
+ read(event);
136
132
  });
137
133
  try {
138
- // Send the initial message
139
134
  await protocolAdapter.sendMessage(userMessage, {
140
135
  tools: requestOptions.tools,
141
136
  messages: allMessages,
142
137
  });
143
- // After stream completes, execute any pending tool calls
144
138
  if (pendingToolCalls.size > 0 && toolExecutor) {
145
- console.log('[useChat] Executing', pendingToolCalls.size, 'tool calls');
146
- // Execute tool calls and collect results
147
139
  const toolResults = [];
148
140
  for (const [toolCallId, { toolName, args }] of pendingToolCalls) {
149
- console.log('[useChat] Executing tool:', toolName, 'with args:', args);
150
141
  const result = await toolExecutor.execute({
151
142
  toolCallId,
152
143
  toolName,
153
144
  args,
154
145
  });
155
- console.log('[useChat] Tool result:', result);
156
146
  toolResults.push({
157
147
  toolCallId,
158
148
  toolName,
159
149
  result: result.result,
160
150
  isError: !result.success,
161
151
  });
152
+ // The call answered, in the conversation.
153
+ reduce(conversationEventsOf({
154
+ type: 'tool-result',
155
+ toolResult: {
156
+ toolCallId,
157
+ success: result.success,
158
+ result: result.result,
159
+ ...(result.success ? {} : { error: result.error }),
160
+ },
161
+ }, conversation));
162
162
  }
163
- // Build messages array with tool results for continuation
164
- // Include the assistant message with tool calls
163
+ // The run continued with the results: the assistant's calls,
164
+ // then one `tool` message per result (AG-UI).
165
165
  const assistantWithToolCalls = {
166
- id: assistantMessageId,
166
+ id: generateMessageId(),
167
167
  role: 'assistant',
168
- content: currentAssistantContent,
168
+ content: '',
169
169
  createdAt: new Date(),
170
170
  toolCalls: Array.from(pendingToolCalls.entries()).map(([toolCallId, { toolName, args }]) => ({
171
171
  type: 'tool-call',
@@ -175,7 +175,6 @@ export function useChat() {
175
175
  status: 'completed',
176
176
  })),
177
177
  };
178
- // Create tool result messages
179
178
  const toolResultMessages = toolResults.map(tr => ({
180
179
  id: generateMessageId(),
181
180
  role: 'tool',
@@ -187,48 +186,14 @@ export function useChat() {
187
186
  isError: tr.isError,
188
187
  },
189
188
  }));
190
- // Update stored messages
191
- updateMessage(assistantMessageId, assistantWithToolCalls);
192
- toolResultMessages.forEach(msg => addMessage(msg));
193
- // Create new assistant message for the continuation
194
- const continuationAssistantId = generateMessageId();
195
- const continuationMessage = createAssistantMessage('');
196
- continuationMessage.id = continuationAssistantId;
197
- addMessage(continuationMessage);
198
- startStreaming(continuationAssistantId);
199
- // Subscribe to continuation events
200
- const unsubscribeContinuation = protocolAdapter.subscribe(event => {
201
- if (event.type === 'message' && event.message) {
202
- const rawContent = typeof event.message.content === 'string'
203
- ? event.message.content
204
- : '';
205
- const content = sanitizeAssistantContent(rawContent);
206
- updateMessage(continuationAssistantId, {
207
- content,
208
- toolCalls: event.message.toolCalls,
209
- });
210
- }
211
- else if (event.type === 'error' && event.error) {
212
- setError(event.error);
213
- }
214
- });
215
- try {
216
- // Build the full message history for continuation
217
- const continuationMessages = [
189
+ await protocolAdapter.sendMessage(toolResultMessages[toolResultMessages.length - 1], {
190
+ tools: requestOptions.tools,
191
+ messages: [
218
192
  ...allMessages,
219
193
  assistantWithToolCalls,
220
194
  ...toolResultMessages,
221
- ];
222
- // Send continuation request with tool results
223
- // AG-UI expects tool results as messages with role: "tool"
224
- await protocolAdapter.sendMessage(toolResultMessages[toolResultMessages.length - 1], {
225
- tools: requestOptions.tools,
226
- messages: continuationMessages,
227
- });
228
- }
229
- finally {
230
- unsubscribeContinuation();
231
- }
195
+ ],
196
+ });
232
197
  }
233
198
  }
234
199
  finally {
@@ -236,47 +201,50 @@ export function useChat() {
236
201
  }
237
202
  }
238
203
  else if (inferenceProvider) {
204
+ const answerId = generateMessageId();
205
+ reduce([
206
+ { type: 'RUN_STARTED' },
207
+ {
208
+ type: 'TEXT_MESSAGE_START',
209
+ messageId: answerId,
210
+ role: 'assistant',
211
+ },
212
+ ]);
239
213
  const response = await inferenceProvider.streamMessage(allMessages, requestOptions, event => {
240
- switch (event.type) {
241
- case 'content-delta':
242
- if (event.delta?.delta.content) {
243
- appendToStream(assistantMessageId, event.delta.delta.content);
244
- }
245
- break;
246
- case 'tool-call-start':
247
- // Handle tool call UI update
248
- if (event.toolCall) {
249
- // Tool calls are handled by the tool executor
250
- console.log('[useChat] Tool call started:', event.toolCall);
251
- }
252
- break;
253
- case 'error':
254
- if (event.error) {
255
- setError(event.error);
256
- }
257
- break;
214
+ if (event.type === 'content-delta' &&
215
+ event.delta?.delta.content) {
216
+ reduce([
217
+ {
218
+ type: 'TEXT_MESSAGE_CONTENT',
219
+ messageId: answerId,
220
+ delta: event.delta.delta.content,
221
+ },
222
+ ]);
223
+ }
224
+ else if (event.type === 'error' && event.error) {
225
+ setError(event.error);
258
226
  }
259
227
  });
260
- // Update final message with complete content and tool calls
261
- const finalAssistantContent = typeof response.message.content === 'string'
262
- ? sanitizeAssistantContent(response.message.content)
263
- : response.message.content;
264
- updateMessage(assistantMessageId, {
265
- content: finalAssistantContent,
266
- toolCalls: response.message.toolCalls,
267
- });
268
- // Handle tool calls if present
269
- if (response.message.toolCalls &&
270
- response.message.toolCalls.length > 0) {
271
- // Tool calls are executed by the tool executor if configured
272
- if (toolExecutor) {
273
- for (const toolCall of response.message.toolCalls) {
274
- await toolExecutor.execute({
275
- toolCallId: toolCall.toolCallId,
276
- toolName: toolCall.toolName,
277
- args: toolCall.args,
278
- });
279
- }
228
+ // The answer as the provider gave it whole, then the run's end.
229
+ const said = conversationEventsOf({
230
+ type: 'message',
231
+ message: {
232
+ id: answerId,
233
+ role: 'assistant',
234
+ content: typeof response.message.content === 'string'
235
+ ? response.message.content
236
+ : '',
237
+ },
238
+ }, conversation);
239
+ reduce([...said, { type: 'RUN_FINISHED' }]);
240
+ // The page's tools run the calls it asked for.
241
+ if (toolExecutor && response.message.toolCalls?.length) {
242
+ for (const toolCall of response.message.toolCalls) {
243
+ await toolExecutor.execute({
244
+ toolCallId: toolCall.toolCallId,
245
+ toolName: toolCall.toolName,
246
+ args: toolCall.args,
247
+ });
280
248
  }
281
249
  }
282
250
  }
@@ -293,12 +261,10 @@ export function useChat() {
293
261
  protocolAdapter,
294
262
  toolExecutor,
295
263
  messages,
296
- addMessage,
297
- updateMessage,
264
+ setMessages,
298
265
  setLoading,
299
266
  setError,
300
267
  startStreaming,
301
- appendToStream,
302
268
  stopStreaming,
303
269
  getTools,
304
270
  ]);