@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.
- package/dist-embed/chunks/1f30a-DxwAt2Ba.js +1 -0
- package/dist-embed/chunks/1f310-AqUM9a8q.js +1 -0
- package/dist-embed/chunks/1f324-8MHLOhTj.js +1 -0
- package/dist-embed/chunks/1f326-YSBr3aXI.js +1 -0
- package/dist-embed/chunks/1f327-Ie-nQBNO.js +1 -0
- package/dist-embed/chunks/1f33e-Cfl2WM63.js +1 -0
- package/dist-embed/chunks/1f372-3XLDlXkE.js +1 -0
- package/dist-embed/chunks/1f373-BPUJt2bp.js +1 -0
- package/dist-embed/chunks/1f39b-CSNtbAXr.js +1 -0
- package/dist-embed/chunks/1f3ab-BUCNQHz1.js +1 -0
- package/dist-embed/chunks/1f41b-Dre1z0gP.js +1 -0
- package/dist-embed/chunks/1f441-D8yah4C4.js +1 -0
- package/dist-embed/chunks/1f465-CPle6Hjq.js +1 -0
- package/dist-embed/chunks/1f4b9-aacFyckt.js +1 -0
- package/dist-embed/chunks/1f4be-DPAcAU4R.js +1 -0
- package/dist-embed/chunks/1f4c1-Ch6aJ0cj.js +1 -0
- package/dist-embed/chunks/1f4c2-Dx7BKmLO.js +1 -0
- package/dist-embed/chunks/1f4c4-CNvjvC5k.js +1 -0
- package/dist-embed/chunks/1f4d3-GTxRgjv7.js +1 -0
- package/dist-embed/chunks/1f4d6-COOZY-Q_.js +1 -0
- package/dist-embed/chunks/1f4dc-BhoW4cjD.js +1 -0
- package/dist-embed/chunks/1f4e1-DezU8ow-.js +1 -0
- package/dist-embed/chunks/1f4e2-DsStZsI-.js +1 -0
- package/dist-embed/chunks/1f4e7-BBi7c_zx.js +1 -0
- package/dist-embed/chunks/1f500-D9xRo-IT.js +1 -0
- package/dist-embed/chunks/1f501-CzKeDPcw.js +1 -0
- package/dist-embed/chunks/1f504-BYIHMoDP.js +1 -0
- package/dist-embed/chunks/1f52d-2QxCLKhU.js +1 -0
- package/dist-embed/chunks/1f552-4d0Y0kn7.js +1 -0
- package/dist-embed/chunks/1f58b-Cp8F_HBg.js +1 -0
- package/dist-embed/chunks/1f5b2-D6BUULuh.js +1 -0
- package/dist-embed/chunks/1f5c3-DjkgJLaz.js +1 -0
- package/dist-embed/chunks/1f5d1-C4Kpe4HL.js +1 -0
- package/dist-embed/chunks/1f5dc-DyLO7aB9.js +1 -0
- package/dist-embed/chunks/1f604-CEekcaYE.js +1 -0
- package/dist-embed/chunks/1f917-CyyS9fgf.js +1 -0
- package/dist-embed/chunks/1f9d1-200d-2696-D9eSB-0q.js +1 -0
- package/dist-embed/chunks/1fa79-DHhIZbWB.js +1 -0
- package/dist-embed/chunks/1fa9d-DUI9e77m.js +1 -0
- package/dist-embed/chunks/1fa9f-10ALsMaD.js +1 -0
- package/dist-embed/chunks/23f3-ASjqFsbT.js +1 -0
- package/dist-embed/chunks/2611-BnnmLm-K.js +1 -0
- package/dist-embed/chunks/2702-CfLyp35r.js +1 -0
- package/dist-embed/chunks/270f-CLwo30mk.js +1 -0
- package/dist-embed/chunks/2728-Qg6be-6p.js +1 -0
- package/dist-embed/chunks/2b1c-Uc1aRwAf.js +1 -0
- package/dist-embed/datalayer-app-main.js +66 -66
- package/lib/chat/ChatInline.js +43 -22
- package/lib/chat/base/ChatBase.js +43 -2
- package/lib/components/agents/AgentSummary.d.ts +2 -24
- package/lib/components/agents/AgentSummary.js +3 -19
- package/lib/hooks/useChat.js +110 -144
- package/lib/portable/activity.d.ts +78 -0
- package/lib/portable/activity.js +129 -0
- package/lib/portable/fixtures.d.ts +7 -0
- package/lib/portable/fixtures.js +50 -0
- package/lib/portable/index.d.ts +4 -1
- package/lib/portable/index.js +7 -1
- package/lib/portable/lifecycle.d.ts +77 -0
- package/lib/portable/lifecycle.js +135 -0
- package/lib/portable/runtimes.d.ts +68 -0
- package/lib/portable/runtimes.js +69 -0
- package/lib/voice/VoiceMenu.d.ts +2 -1
- package/lib/voice/VoiceMenu.js +2 -2
- package/lib/voice/chatVoice.d.ts +5 -2
- package/lib/voice/chatVoice.js +10 -4
- package/lib/voice/choices.d.ts +2 -0
- package/lib/voice/choices.js +6 -0
- package/lib/voice/index.d.ts +1 -0
- package/lib/voice/index.js +1 -0
- package/lib/voice/sayAloud.d.ts +25 -0
- package/lib/voice/sayAloud.js +34 -0
- package/lib/voice/serverHearing.d.ts +34 -2
- package/lib/voice/serverHearing.js +234 -6
- package/lib/voice/settings.d.ts +12 -0
- package/lib/voice/settings.js +9 -0
- package/package.json +1 -1
package/lib/chat/ChatInline.js
CHANGED
|
@@ -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 {
|
|
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
|
-
//
|
|
83
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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).
|
package/lib/hooks/useChat.js
CHANGED
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { useCallback, useMemo } from 'react';
|
|
12
12
|
import { useChatStore } from '../stores';
|
|
13
|
-
import { createUserMessage,
|
|
14
|
-
import {
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
//
|
|
117
|
+
// Every message so far, for context.
|
|
88
118
|
const allMessages = [...messages, userMessage];
|
|
89
|
-
//
|
|
90
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
//
|
|
164
|
-
//
|
|
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:
|
|
166
|
+
id: generateMessageId(),
|
|
167
167
|
role: 'assistant',
|
|
168
|
-
content:
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
//
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
297
|
-
updateMessage,
|
|
264
|
+
setMessages,
|
|
298
265
|
setLoading,
|
|
299
266
|
setError,
|
|
300
267
|
startStreaming,
|
|
301
|
-
appendToStream,
|
|
302
268
|
stopStreaming,
|
|
303
269
|
getTools,
|
|
304
270
|
]);
|