@datalayer/agent-runtimes 1.3.84 → 1.3.86
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/lib/chat/ChatFloating.d.ts +5 -4
- package/lib/chat/ChatFloating.js +8 -6
- package/lib/chat/ChatLanguage.d.ts +17 -0
- package/lib/chat/ChatLanguage.js +29 -0
- package/lib/chat/assistant/AssistantStage.d.ts +4 -4
- package/lib/chat/assistant/AssistantStage.js +4 -2
- package/lib/chat/assistant/BalloonParts.d.ts +2 -1
- package/lib/chat/assistant/BalloonParts.js +12 -5
- package/lib/chat/assistant/BalloonVisual.js +3 -1
- package/lib/chat/assistant/ConversationBalloon.d.ts +1 -1
- package/lib/chat/assistant/ConversationBalloon.js +3 -2
- package/lib/chat/assistant/SpeechBalloon.d.ts +1 -1
- package/lib/chat/assistant/SpeechBalloon.js +10 -7
- package/lib/chat/assistant/toolLine.d.ts +8 -4
- package/lib/chat/assistant/toolLine.js +13 -9
- package/lib/chat/base/ChatBase.js +14 -11
- package/lib/chat/display/EmptyState.js +3 -1
- package/lib/chat/index.d.ts +2 -0
- package/lib/chat/index.js +3 -0
- package/lib/chat/messages/ChatMessages.js +3 -1
- package/lib/chat/messages/TurnFooter.js +3 -1
- package/lib/chat/prompt/InputPrompt.js +3 -1
- package/lib/chat/prompt/InputPromptLexical.js +3 -1
- package/lib/chat/prompt/footer/InPromptFooter.js +3 -1
- package/lib/chat/tools/ToolApprovalBanner.js +5 -3
- package/lib/chat/tools/ToolApprovalDialog.js +5 -3
- package/lib/chat/tools/ToolCallDisplay.js +29 -25
- package/lib/chat/words.d.ts +102 -0
- package/lib/chat/words.js +460 -0
- package/lib/components/a2ui/custom/index.d.ts +39 -0
- package/lib/components/a2ui/custom/index.js +95 -0
- package/lib/components/a2ui/custom/sandbox.d.ts +73 -0
- package/lib/components/a2ui/custom/sandbox.js +188 -0
- package/lib/components/a2ui/datalayer/index.d.ts +1 -1
- package/lib/components/a2ui/datalayer/index.js +1 -1
- package/lib/components/a2ui/datalayer/schema.d.ts +10 -0
- package/lib/components/a2ui/datalayer/schema.js +12 -1
- package/lib/components/a2ui/index.d.ts +1 -0
- package/lib/components/a2ui/index.js +1 -0
- package/lib/components/a2ui/visibleWhen.d.ts +12 -1
- package/lib/components/a2ui/visibleWhen.js +24 -3
- package/lib/components/inspector/AgentInspector.d.ts +25 -44
- package/lib/components/inspector/AgentInspector.js +27 -215
- package/lib/components/inspector/AgentInspectorDialog.d.ts +6 -6
- package/lib/components/inspector/AgentInspectorDialog.js +2 -2
- package/lib/components/inspector/a2aSpans.d.ts +51 -0
- package/lib/components/inspector/a2aSpans.js +373 -0
- package/lib/components/inspector/agentSpans.d.ts +120 -0
- package/lib/components/inspector/agentSpans.js +253 -0
- package/lib/components/inspector/chatSpans.d.ts +36 -0
- package/lib/components/inspector/chatSpans.js +50 -0
- package/lib/components/inspector/classify.d.ts +11 -8
- package/lib/components/inspector/classify.js +23 -24
- package/lib/components/inspector/index.d.ts +7 -7
- package/lib/components/inspector/index.js +7 -7
- package/lib/components/teams/A2ATeamGraph.d.ts +117 -22
- package/lib/components/teams/A2ATeamGraph.js +217 -97
- package/lib/components/teams/SceneTranscript.d.ts +29 -0
- package/lib/components/teams/SceneTranscript.js +54 -0
- package/lib/components/teams/SceneView.d.ts +41 -0
- package/lib/components/teams/SceneView.js +28 -0
- package/lib/components/teams/index.d.ts +4 -0
- package/lib/components/teams/index.js +4 -0
- package/lib/components/teams/sceneStage.d.ts +120 -0
- package/lib/components/teams/sceneStage.js +170 -0
- package/lib/components/teams/sceneTranscript.d.ts +74 -0
- package/lib/components/teams/sceneTranscript.js +294 -0
- package/lib/components/teams/teamConnections.d.ts +2 -2
- package/lib/components/teams/teamConnections.js +3 -3
- package/lib/components/teams/useA2ATeam.d.ts +94 -22
- package/lib/components/teams/useA2ATeam.js +356 -140
- package/lib/examples/AgentA2ATeamExample.d.ts +10 -9
- package/lib/examples/AgentA2ATeamExample.js +70 -60
- package/lib/examples/AssistantExample.js +2 -2
- package/lib/examples/ScenesExample.d.ts +26 -0
- package/lib/examples/ScenesExample.js +241 -0
- package/lib/examples/example-selector.js +1 -0
- package/lib/examples/exampleGroups.js +3 -0
- package/lib/examples/index.d.ts +1 -0
- package/lib/examples/index.js +1 -0
- package/lib/examples/main.js +4 -0
- package/lib/loop/apps/AppComposer.d.ts +2 -1
- package/lib/loop/apps/AppComposer.js +6 -3
- package/lib/loop/apps/AppRenderer.d.ts +2 -1
- package/lib/loop/apps/AppRenderer.js +34 -25
- package/lib/loop/apps/appspec.d.ts +11 -1
- package/lib/loop/apps/appspec.js +115 -1
- package/lib/loop/apps/checks.d.ts +8 -0
- package/lib/loop/apps/checks.js +106 -4
- package/lib/loop/apps/customComponents.d.ts +41 -0
- package/lib/loop/apps/customComponents.js +260 -0
- package/lib/loop/apps/hostTools.d.ts +8 -1
- package/lib/loop/apps/hostTools.js +9 -0
- package/lib/loop/apps/index.d.ts +1 -0
- package/lib/loop/apps/index.js +1 -0
- package/lib/loop/core/a2uiComponents.d.ts +10 -0
- package/lib/loop/core/a2uiComponents.js +15 -0
- package/lib/loop/core/index.d.ts +1 -0
- package/lib/loop/core/index.js +3 -0
- package/lib/loop/embed/AppEmbed.d.ts +11 -3
- package/lib/loop/embed/AppEmbed.js +42 -16
- package/lib/loop/embed/element.d.ts +5 -0
- package/lib/loop/embed/element.js +61 -5
- package/lib/loop/embed/embedConfig.d.ts +8 -0
- package/lib/loop/embed/embedConfig.js +18 -0
- package/lib/loop/embed/hostBridge.d.ts +28 -2
- package/lib/loop/embed/hostBridge.js +22 -2
- package/lib/loop/index.d.ts +4 -3
- package/lib/loop/index.js +4 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +7 -1
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +7 -2
- package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +4 -1
- package/lib/loop/plugins/a2ui-surface/ownComponents.d.ts +6 -0
- package/lib/loop/plugins/a2ui-surface/ownComponents.js +30 -0
- package/lib/loop/plugins/app-components/index.d.ts +26 -0
- package/lib/loop/plugins/app-components/index.js +56 -0
- package/lib/loop/plugins/app-elements/AppElements.js +4 -1
- package/lib/loop/plugins/app-page/AppPage.d.ts +9 -0
- package/lib/loop/plugins/app-page/AppPage.js +106 -8
- package/lib/loop/plugins/app-page/appPageModel.d.ts +17 -1
- package/lib/loop/plugins/app-page/appPageModel.js +120 -4
- package/lib/loop/plugins/app-page/index.d.ts +1 -0
- package/lib/loop/plugins/app-page/index.js +1 -0
- package/lib/loop/plugins/app-page/pageRun.d.ts +46 -0
- package/lib/loop/plugins/app-page/pageRun.js +111 -0
- package/lib/loop/plugins/assistant/index.js +8 -6
- package/lib/loop/plugins/chat/ChatView.js +6 -4
- package/lib/loop/plugins/chat/index.d.ts +1 -1
- package/lib/loop/plugins/chat/index.js +2 -1
- package/lib/models/PageTag.d.ts +1 -1
- package/lib/runtimes/browser/a2aPeer.d.ts +2 -1
- package/lib/specs/actions.js +78 -1
- package/lib/specs/apps.d.ts +5 -0
- package/lib/specs/apps.js +1384 -231
- package/lib/specs/appspecSchema.js +191 -1
- package/lib/specs/index.d.ts +1 -0
- package/lib/specs/index.js +1 -0
- package/lib/specs/mcpServers.js +8 -2
- package/lib/specs/scenes.d.ts +21 -0
- package/lib/specs/scenes.js +1191 -0
- package/lib/specs/teams/teams.d.ts +3 -0
- package/lib/specs/teams/teams.js +249 -0
- package/lib/types/agentspecs.d.ts +70 -0
- package/lib/types/chat.d.ts +6 -5
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.js +1 -0
- package/lib/types/scenes.d.ts +207 -0
- package/lib/types/scenes.js +5 -0
- package/lib/types/teams.d.ts +13 -2
- package/package.json +4 -4
- package/scripts/codegen/generate_agents.py +1 -0
- package/scripts/codegen/generate_apps.py +4 -2
- package/scripts/codegen/generate_scenes.py +301 -0
- package/scripts/codegen/generate_teams.py +15 -2
- package/lib/components/inspector/JsonTree.d.ts +0 -24
- package/lib/components/inspector/JsonTree.js +0 -89
- package/lib/components/inspector/a2aInspect.d.ts +0 -56
- package/lib/components/inspector/a2aInspect.js +0 -478
- package/lib/components/inspector/agentInspector.d.ts +0 -179
- package/lib/components/inspector/agentInspector.js +0 -267
- package/lib/components/inspector/chatInspect.d.ts +0 -38
- package/lib/components/inspector/chatInspect.js +0 -65
|
@@ -3,27 +3,34 @@
|
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* A team of
|
|
7
|
-
* turns in the page and asks its
|
|
6
|
+
* A team of applications over A2A, as a page runs it: the entry's loop
|
|
7
|
+
* turns in the page and asks each of its peers over A2A with a tool of its
|
|
8
|
+
* own.
|
|
8
9
|
*
|
|
9
10
|
* The entry (Sales) talks to the person: its instructions and its model run
|
|
10
11
|
* here (the Vercel AI SDK's loop), its model reached through ai-inference
|
|
11
12
|
* with whatever `inference` holds — a signed-in person's token, or a
|
|
12
|
-
* visitor's trial key. Its
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Jupyter notebook — is kept as its artifacts, and its latest notebook kept
|
|
19
|
-
* until another one comes ({@link A2ATeam.notebook}). The tools the
|
|
20
|
-
* peer calls on its connections (its MCP servers), told over A2A as it calls
|
|
21
|
-
* them, are kept as the calls running now ({@link callsAfter}).
|
|
13
|
+
* visitor's trial key. Its tools ask its peers (Accounting; Disaster
|
|
14
|
+
* assessment and Change detection), one `ask_<peer>` tool per peer, with
|
|
15
|
+
* {@link a2aPeerTool}. A team of two names its peer as `peerApp`; a team of
|
|
16
|
+
* N names them as `peers` (LOOP A-08). An entry that runs on a runtime
|
|
17
|
+
* itself (a scene of one member: Month-end close, Crop monitoring) is asked
|
|
18
|
+
* over A2A from the page, with no agent in the browser: `entryPeer`.
|
|
22
19
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* peer
|
|
20
|
+
* What each of them does is kept as a persona — the state its character
|
|
21
|
+
* acts and what its balloon says — each peer's last answer as the report,
|
|
22
|
+
* and the way each link carries a message as its flow ({@link flowAfter}).
|
|
23
|
+
* What a peer gives besides words — the formats the page accepts
|
|
24
|
+
* (`accept`), such as a Jupyter notebook — is kept as its artifacts, and
|
|
25
|
+
* the latest notebook kept until another one comes ({@link A2ATeam.notebook}).
|
|
26
|
+
* The tools a peer calls on its connections (its MCP servers), told over
|
|
27
|
+
* A2A as it calls them, are kept as the calls running now ({@link callsAfter}).
|
|
28
|
+
*
|
|
29
|
+
* Given the Agent Inspector's tracer (`inspector`), the entry's turns (an
|
|
30
|
+
* `invoke_agent` span: its model, its tokens) and its calls to its peers
|
|
31
|
+
* (`execute_tool` spans under it) are recorded there as OpenTelemetry
|
|
32
|
+
* spans; the A2A traffic itself, under those calls, is recorded by each
|
|
33
|
+
* peer's fetch (`traceA2AFetch`).
|
|
27
34
|
*
|
|
28
35
|
* What `A2ATeamGraph` draws, and what a page's composer sends.
|
|
29
36
|
*
|
|
@@ -32,10 +39,12 @@
|
|
|
32
39
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
33
40
|
import { stepCountIs, ToolLoopAgent } from 'ai';
|
|
34
41
|
import { createBrowserModel, } from '../../runtimes/browser/model';
|
|
35
|
-
import { a2aPeerTool, NOTEBOOK_MEDIA_TYPE, } from '../../runtimes/browser/a2aPeer';
|
|
42
|
+
import { a2aPeerTool, askA2APeer, NOTEBOOK_MEDIA_TYPE, } from '../../runtimes/browser/a2aPeer';
|
|
43
|
+
import { endAgentTurn, endToolCall, startAgentTurn, startToolCall, } from '../inspector/agentSpans';
|
|
36
44
|
import { callsAfter, flowAfter, toolOwnName, pruneCalls, } from './a2aTeamFlow';
|
|
37
45
|
import { toolLineOfStep, } from '../../chat/assistant/toolLine';
|
|
38
46
|
const NO_CONNECTIONS = [];
|
|
47
|
+
const NO_PEERS = [];
|
|
39
48
|
export const AT_REST = {
|
|
40
49
|
state: 'idle',
|
|
41
50
|
insist: false,
|
|
@@ -64,14 +73,84 @@ export function balloonLine(text, length = 120) {
|
|
|
64
73
|
const flat = text.replace(/\s+/g, ' ').trim();
|
|
65
74
|
return flat.length > length ? `${flat.slice(0, length - 1)}…` : flat;
|
|
66
75
|
}
|
|
67
|
-
/**
|
|
76
|
+
/** The name of the entry's tool for a peer, as its instructions call it: `ask_<peer id>`. */
|
|
77
|
+
export function askToolOf(peerId) {
|
|
78
|
+
return `ask_${peerId.replace(/-/g, '_')}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* What a member says when it is asked, before it works: the books when it
|
|
82
|
+
* reaches Odoo, the system it reaches otherwise, or just that it is on it.
|
|
83
|
+
*/
|
|
84
|
+
export function askedLine(connections) {
|
|
85
|
+
if (connections.some(connection => connection.id.startsWith('odoo'))) {
|
|
86
|
+
return 'On it. Let me read the books.';
|
|
87
|
+
}
|
|
88
|
+
const systems = [...new Set(connections.map(connection => connection.label))];
|
|
89
|
+
return systems.length
|
|
90
|
+
? `On it. Let me look in ${systems.join(' and ')}.`
|
|
91
|
+
: 'On it.';
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The entry's instructions for a team of more than one peer: its own, and
|
|
95
|
+
* which tool asks whom, so that it names each. A team of two keeps the
|
|
96
|
+
* entry's instructions as they are.
|
|
97
|
+
*/
|
|
98
|
+
export function teamInstructions(entry, peers) {
|
|
99
|
+
if (peers.length < 2) {
|
|
100
|
+
return entry.instructions;
|
|
101
|
+
}
|
|
102
|
+
const who = peers
|
|
103
|
+
.map(peer => `${peer.app.name} with \`${peer.askTool}\``)
|
|
104
|
+
.join(', ');
|
|
105
|
+
return `${entry.instructions}\n\nYour team, over A2A: ask ${who}. Each answers one request on its own.`;
|
|
106
|
+
}
|
|
107
|
+
/** A persona at rest, or greeting with a line when it has one. */
|
|
108
|
+
function restingPersona(greeting) {
|
|
109
|
+
return greeting
|
|
110
|
+
? {
|
|
111
|
+
...AT_REST,
|
|
112
|
+
state: 'greeting',
|
|
113
|
+
saying: balloonLine(greeting),
|
|
114
|
+
full: greeting,
|
|
115
|
+
insist: true,
|
|
116
|
+
}
|
|
117
|
+
: AT_REST;
|
|
118
|
+
}
|
|
119
|
+
/** Run a team over A2A in the page. */
|
|
68
120
|
export function useA2ATeam(options) {
|
|
69
|
-
const { entry, peerApp, peer,
|
|
121
|
+
const { entry, peerApp, peer = null, peerConnections = NO_CONNECTIONS, peers: given = NO_PEERS, entryPeer = null, inference, maxSteps = 6, inspector = null, } = options;
|
|
70
122
|
// By what it names, so that a page passing a new array of the same media
|
|
71
123
|
// types does not make the agent again.
|
|
72
124
|
const acceptKey = options.accept?.join('\n');
|
|
73
125
|
const accept = useMemo(() => (acceptKey ? acceptKey.split('\n') : undefined), [acceptKey]);
|
|
74
|
-
const askTool = options.askTool ??
|
|
126
|
+
const askTool = options.askTool ?? (peerApp ? askToolOf(peerApp.id) : '');
|
|
127
|
+
// The peers, resolved: the one of a team of two, then those of a team of N.
|
|
128
|
+
const peerList = useMemo(() => [
|
|
129
|
+
...(peerApp
|
|
130
|
+
? [
|
|
131
|
+
{
|
|
132
|
+
id: peerApp.id,
|
|
133
|
+
app: peerApp,
|
|
134
|
+
peer,
|
|
135
|
+
askTool,
|
|
136
|
+
connections: peerConnections,
|
|
137
|
+
},
|
|
138
|
+
]
|
|
139
|
+
: []),
|
|
140
|
+
...given.map(one => ({
|
|
141
|
+
id: one.app.id,
|
|
142
|
+
app: one.app,
|
|
143
|
+
peer: one.peer,
|
|
144
|
+
askTool: one.askTool ?? askToolOf(one.app.id),
|
|
145
|
+
connections: one.connections ?? NO_CONNECTIONS,
|
|
146
|
+
...(one.greeting ? { greeting: one.greeting } : {}),
|
|
147
|
+
})),
|
|
148
|
+
], [peerApp, peer, askTool, peerConnections, given]);
|
|
149
|
+
// Read by the event handlers, so that they need not be made again when
|
|
150
|
+
// a peer connects.
|
|
151
|
+
const peersNow = useRef(peerList);
|
|
152
|
+
peersNow.current = peerList;
|
|
153
|
+
const peerOf = useCallback((id) => peersNow.current.find(one => one.id === id), []);
|
|
75
154
|
const [entryPersona, setEntryPersona] = useState(() => ({
|
|
76
155
|
...AT_REST,
|
|
77
156
|
state: 'greeting',
|
|
@@ -79,11 +158,28 @@ export function useA2ATeam(options) {
|
|
|
79
158
|
full: entry.interface.welcome ?? '',
|
|
80
159
|
insist: Boolean(entry.interface.welcome),
|
|
81
160
|
}));
|
|
82
|
-
const [
|
|
161
|
+
const [personas, setPersonas] = useState(() => Object.fromEntries(peerList.map(one => [one.id, restingPersona(one.greeting)])));
|
|
162
|
+
const personaOf = useCallback((id) => personas[id] ?? restingPersona(peerOf(id)?.greeting), [personas, peerOf]);
|
|
163
|
+
/** Change a member's persona: the entry's, or a peer's by id. */
|
|
164
|
+
const setPersona = useCallback((id, next) => {
|
|
165
|
+
if (id === entry.id) {
|
|
166
|
+
setEntryPersona(next);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
setPersonas(prev => ({
|
|
170
|
+
...prev,
|
|
171
|
+
[id]: next(prev[id] ?? restingPersona(peerOf(id)?.greeting)),
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
}, [entry.id, peerOf]);
|
|
83
175
|
const [turns, setTurns] = useState([]);
|
|
84
|
-
const [
|
|
176
|
+
const [histories, setHistories] = useState({});
|
|
85
177
|
const told = useRef(0);
|
|
86
|
-
const tell = useCallback((role, text) => {
|
|
178
|
+
const tell = useCallback((id, role, text) => {
|
|
179
|
+
if (id === entry.id) {
|
|
180
|
+
// The entry's history is its turns.
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
87
183
|
told.current += 1;
|
|
88
184
|
const message = {
|
|
89
185
|
id: `peer-${told.current}`,
|
|
@@ -91,65 +187,82 @@ export function useA2ATeam(options) {
|
|
|
91
187
|
content: text,
|
|
92
188
|
createdAt: new Date(),
|
|
93
189
|
};
|
|
94
|
-
|
|
95
|
-
}, []);
|
|
96
|
-
// A tool
|
|
190
|
+
setHistories(prev => ({ ...prev, [id]: [...(prev[id] ?? []), message] }));
|
|
191
|
+
}, [entry.id]);
|
|
192
|
+
// A tool a peer calls, as the chat holds one: added when it starts,
|
|
97
193
|
// updated when it ends.
|
|
98
|
-
const toolCalled = useCallback((step) => {
|
|
99
|
-
|
|
194
|
+
const toolCalled = useCallback((id, step) => {
|
|
195
|
+
if (id === entry.id) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const connections = peerOf(id)?.connections ?? NO_CONNECTIONS;
|
|
199
|
+
const callId = step.id ?? step.name;
|
|
100
200
|
const call = {
|
|
101
|
-
id: `peer-tool:${
|
|
201
|
+
id: `peer-tool:${callId}`,
|
|
102
202
|
type: 'tool-call',
|
|
103
|
-
toolCallId:
|
|
104
|
-
toolName: toolOwnName(step.name,
|
|
203
|
+
toolCallId: callId,
|
|
204
|
+
toolName: toolOwnName(step.name, connections),
|
|
105
205
|
args: {},
|
|
106
206
|
status: step.error ? 'error' : step.ended ? 'complete' : 'executing',
|
|
107
207
|
...(step.error ? { error: step.error } : {}),
|
|
108
208
|
};
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
209
|
+
setHistories(prev => {
|
|
210
|
+
const history = prev[id] ?? [];
|
|
211
|
+
return {
|
|
212
|
+
...prev,
|
|
213
|
+
[id]: history.some(item => item.id === call.id)
|
|
214
|
+
? history.map(item => (item.id === call.id ? call : item))
|
|
215
|
+
: [...history, call],
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
}, [entry.id, peerOf]);
|
|
113
219
|
const [report, setReport] = useState(null);
|
|
220
|
+
const [reports, setReports] = useState({});
|
|
114
221
|
const [artifacts, setArtifacts] = useState([]);
|
|
222
|
+
const [givens, setGivens] = useState({});
|
|
115
223
|
const [notebook, setNotebook] = useState(null);
|
|
116
|
-
const [
|
|
224
|
+
const [flows, setFlows] = useState({});
|
|
117
225
|
const [calls, setCalls] = useState([]);
|
|
118
226
|
const callsNow = useRef([]);
|
|
119
227
|
const callTimer = useRef(undefined);
|
|
120
228
|
const [busy, setBusy] = useState(false);
|
|
121
229
|
const history = useRef([]);
|
|
122
|
-
const turnCount = useRef(0);
|
|
123
230
|
const abort = useRef(null);
|
|
124
|
-
const
|
|
125
|
-
// What
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
231
|
+
const flowTimers = useRef(new Map());
|
|
232
|
+
// What a member does, as it does it: its own state and balloon, and its
|
|
233
|
+
// link. A peer's when the entry asks it; the entry's own when the entry
|
|
234
|
+
// is asked over A2A.
|
|
235
|
+
const onMemberEvent = useCallback((id, event) => {
|
|
236
|
+
const connections = id === entry.id
|
|
237
|
+
? NO_CONNECTIONS
|
|
238
|
+
: (peerOf(id)?.connections ?? NO_CONNECTIONS);
|
|
239
|
+
if (id !== entry.id) {
|
|
240
|
+
const next = flowAfter(event);
|
|
241
|
+
clearTimeout(flowTimers.current.get(id));
|
|
242
|
+
setFlows(prev => ({ ...prev, [id]: next.flow }));
|
|
243
|
+
if (next.holdMs) {
|
|
244
|
+
flowTimers.current.set(id, setTimeout(() => setFlows(prev => ({ ...prev, [id]: 'still' })), next.holdMs));
|
|
245
|
+
}
|
|
246
|
+
// The calls to its connections: a call's end, when it came
|
|
247
|
+
// quickly, is shown a moment longer. Another peer's calls are its own.
|
|
248
|
+
const others = callsNow.current.filter(call => call.member !== id);
|
|
249
|
+
const after = callsAfter(callsNow.current.filter(call => call.member === id), event, id, connections, Date.now());
|
|
250
|
+
callsNow.current = [...others, ...after.calls];
|
|
251
|
+
setCalls(callsNow.current);
|
|
252
|
+
if (after.holdMs) {
|
|
253
|
+
clearTimeout(callTimer.current);
|
|
254
|
+
callTimer.current = setTimeout(() => {
|
|
255
|
+
callsNow.current = pruneCalls(callsNow.current, Date.now());
|
|
256
|
+
setCalls(callsNow.current);
|
|
257
|
+
}, after.holdMs);
|
|
258
|
+
}
|
|
146
259
|
}
|
|
147
260
|
if (event.phase === 'asked') {
|
|
148
|
-
tell('user', event.request);
|
|
149
|
-
|
|
261
|
+
tell(id, 'user', event.request);
|
|
262
|
+
setPersona(id, prev => ({
|
|
150
263
|
...prev,
|
|
151
264
|
state: 'greeting',
|
|
152
|
-
saying:
|
|
265
|
+
saying: askedLine(connections),
|
|
153
266
|
full: undefined,
|
|
154
267
|
insist: true,
|
|
155
268
|
tool: undefined,
|
|
@@ -157,9 +270,9 @@ export function useA2ATeam(options) {
|
|
|
157
270
|
}
|
|
158
271
|
else if (event.phase === 'working') {
|
|
159
272
|
if (event.tool) {
|
|
160
|
-
toolCalled(event.tool);
|
|
273
|
+
toolCalled(id, event.tool);
|
|
161
274
|
}
|
|
162
|
-
|
|
275
|
+
setPersona(id, prev => ({
|
|
163
276
|
...prev,
|
|
164
277
|
state: 'working',
|
|
165
278
|
saying: event.note ? balloonLine(event.note) : prev.saying,
|
|
@@ -168,19 +281,21 @@ export function useA2ATeam(options) {
|
|
|
168
281
|
// The tool it calls, in plain words, from the step it told; a
|
|
169
282
|
// step without a tool (its own words) puts the tool line away.
|
|
170
283
|
tool: event.tool
|
|
171
|
-
? toolLineOfStep(event.tool, toolOwnName(event.tool.name,
|
|
284
|
+
? toolLineOfStep(event.tool, toolOwnName(event.tool.name, connections))
|
|
172
285
|
: undefined,
|
|
173
286
|
}));
|
|
174
287
|
}
|
|
175
288
|
else if (event.phase === 'answered') {
|
|
176
|
-
tell('assistant', event.answer);
|
|
289
|
+
tell(id, 'assistant', event.answer);
|
|
177
290
|
setReport(event.answer);
|
|
291
|
+
setReports(prev => ({ ...prev, [id]: event.answer }));
|
|
178
292
|
setArtifacts(event.artifacts);
|
|
293
|
+
setGivens(prev => ({ ...prev, [id]: event.artifacts }));
|
|
179
294
|
const given = notebookAmong(event.artifacts);
|
|
180
295
|
if (given) {
|
|
181
296
|
setNotebook(given);
|
|
182
297
|
}
|
|
183
|
-
|
|
298
|
+
setPersona(id, prev => ({
|
|
184
299
|
...prev,
|
|
185
300
|
state: 'speaking',
|
|
186
301
|
saying: balloonLine(event.answer),
|
|
@@ -189,13 +304,13 @@ export function useA2ATeam(options) {
|
|
|
189
304
|
tool: undefined,
|
|
190
305
|
}));
|
|
191
306
|
// The entry was given a notebook: in its balloon, read-only.
|
|
192
|
-
if (given) {
|
|
307
|
+
if (given && id !== entry.id) {
|
|
193
308
|
setEntryPersona(prev => ({ ...prev, notebook: given }));
|
|
194
309
|
}
|
|
195
310
|
}
|
|
196
311
|
else {
|
|
197
|
-
tell('assistant', `Could not answer: ${event.error}`);
|
|
198
|
-
|
|
312
|
+
tell(id, 'assistant', `Could not answer: ${event.error}`);
|
|
313
|
+
setPersona(id, prev => ({
|
|
199
314
|
...prev,
|
|
200
315
|
state: 'idle',
|
|
201
316
|
saying: balloonLine(event.error),
|
|
@@ -204,27 +319,81 @@ export function useA2ATeam(options) {
|
|
|
204
319
|
tool: undefined,
|
|
205
320
|
}));
|
|
206
321
|
}
|
|
207
|
-
}, [
|
|
322
|
+
}, [entry.id, peerOf, setPersona, tell, toolCalled]);
|
|
323
|
+
// The peers' ids and whether each is reached: the agent is made again
|
|
324
|
+
// when one connects, not when a peer's state changes.
|
|
208
325
|
const agent = useMemo(() => {
|
|
209
|
-
if (!peer) {
|
|
326
|
+
if (entryPeer || !peerList.length || peerList.some(one => !one.peer)) {
|
|
210
327
|
return null;
|
|
211
328
|
}
|
|
329
|
+
const tools = {};
|
|
330
|
+
for (const one of peerList) {
|
|
331
|
+
tools[one.askTool] = a2aPeerTool({
|
|
332
|
+
peer: one.peer,
|
|
333
|
+
onEvent: event => onMemberEvent(one.id, event),
|
|
334
|
+
accept,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
212
337
|
return new ToolLoopAgent({
|
|
213
338
|
id: entry.id,
|
|
214
339
|
model: createBrowserModel({
|
|
215
340
|
...inference,
|
|
216
341
|
model: entry.model || undefined,
|
|
217
342
|
}),
|
|
218
|
-
instructions: entry
|
|
219
|
-
tools
|
|
220
|
-
[askTool]: a2aPeerTool({ peer, onEvent: onPeerEvent, accept }),
|
|
221
|
-
},
|
|
343
|
+
instructions: teamInstructions(entry, peerList),
|
|
344
|
+
tools,
|
|
222
345
|
stopWhen: stepCountIs(maxSteps),
|
|
223
346
|
});
|
|
224
|
-
}, [
|
|
347
|
+
}, [entryPeer, peerList, inference, onMemberEvent, entry, maxSteps, accept]);
|
|
348
|
+
/** Every peer at rest again, its greeting gone. */
|
|
349
|
+
const peersAtRest = useCallback(() => {
|
|
350
|
+
setPersonas(Object.fromEntries(peersNow.current.map(one => [one.id, AT_REST])));
|
|
351
|
+
}, []);
|
|
352
|
+
// The entry on a runtime: asked over A2A from the page, as the person.
|
|
353
|
+
const sendOverA2A = useCallback(async (asked, to) => {
|
|
354
|
+
setBusy(true);
|
|
355
|
+
setReport(null);
|
|
356
|
+
setArtifacts([]);
|
|
357
|
+
setTurns(prev => [...prev, { role: 'user', text: asked }]);
|
|
358
|
+
abort.current = new AbortController();
|
|
359
|
+
try {
|
|
360
|
+
const { answer } = await askA2APeer(to, asked, {
|
|
361
|
+
signal: abort.current.signal,
|
|
362
|
+
onEvent: event => onMemberEvent(entry.id, event),
|
|
363
|
+
accept,
|
|
364
|
+
});
|
|
365
|
+
setTurns(prev => [...prev, { role: 'assistant', text: answer }]);
|
|
366
|
+
setEntryPersona(prev => ({ ...prev, state: 'idle' }));
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
370
|
+
setTurns(prev => [
|
|
371
|
+
...prev,
|
|
372
|
+
{ role: 'assistant', text: `Could not answer: ${message}` },
|
|
373
|
+
]);
|
|
374
|
+
setEntryPersona(prev => ({
|
|
375
|
+
...prev,
|
|
376
|
+
state: 'idle',
|
|
377
|
+
saying: balloonLine(message),
|
|
378
|
+
full: message,
|
|
379
|
+
insist: true,
|
|
380
|
+
tool: undefined,
|
|
381
|
+
}));
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
abort.current = null;
|
|
385
|
+
setBusy(false);
|
|
386
|
+
}
|
|
387
|
+
}, [entry.id, onMemberEvent, accept]);
|
|
225
388
|
const send = useCallback(async (text) => {
|
|
226
389
|
const asked = text.trim();
|
|
227
|
-
if (!asked ||
|
|
390
|
+
if (!asked || busy) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (entryPeer) {
|
|
394
|
+
return sendOverA2A(asked, entryPeer);
|
|
395
|
+
}
|
|
396
|
+
if (!agent) {
|
|
228
397
|
return;
|
|
229
398
|
}
|
|
230
399
|
setBusy(true);
|
|
@@ -237,63 +406,72 @@ export function useA2ATeam(options) {
|
|
|
237
406
|
saying: 'Let me see…',
|
|
238
407
|
insist: true,
|
|
239
408
|
});
|
|
240
|
-
|
|
409
|
+
peersAtRest();
|
|
241
410
|
history.current = [...history.current, { role: 'user', content: asked }];
|
|
242
411
|
abort.current = new AbortController();
|
|
243
412
|
let said = '';
|
|
244
|
-
// The turn,
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
});
|
|
413
|
+
// The turn, as a span: the entry's model, from the question to the
|
|
414
|
+
// answer, and its tokens; its calls to the peers under it.
|
|
415
|
+
const turnSpan = inspector
|
|
416
|
+
? startAgentTurn(inspector, {
|
|
417
|
+
agent: entry.name,
|
|
418
|
+
prompt: asked,
|
|
419
|
+
model: entry.model || undefined,
|
|
420
|
+
})
|
|
421
|
+
: undefined;
|
|
422
|
+
const toolSpans = new Map();
|
|
423
|
+
// The peer a tool asks, by the tool's name.
|
|
424
|
+
const askedPeer = (toolName) => peersNow.current.find(one => one.askTool === toolName);
|
|
257
425
|
try {
|
|
258
426
|
const result = await agent.stream({
|
|
259
427
|
messages: history.current,
|
|
260
428
|
abortSignal: abort.current.signal,
|
|
261
429
|
});
|
|
262
430
|
for await (const part of result.fullStream) {
|
|
263
|
-
|
|
431
|
+
const to = part.type === 'tool-call' ||
|
|
432
|
+
part.type === 'tool-result' ||
|
|
433
|
+
part.type === 'tool-error'
|
|
434
|
+
? askedPeer(part.toolName)
|
|
435
|
+
: undefined;
|
|
436
|
+
if (part.type === 'tool-call' && to) {
|
|
264
437
|
const request = String(part.input?.request ?? '');
|
|
265
438
|
// Its tool runs here, in the page, and asks the peer over A2A.
|
|
266
|
-
inspector
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
439
|
+
if (inspector && turnSpan) {
|
|
440
|
+
toolSpans.set(part.toolCallId, startToolCall(inspector, {
|
|
441
|
+
agent: entry.name,
|
|
442
|
+
name: to.askTool,
|
|
443
|
+
id: part.toolCallId,
|
|
444
|
+
args: part.input,
|
|
445
|
+
tool: { kind: 'frontend' },
|
|
446
|
+
parent: turnSpan.context,
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
275
449
|
setEntryPersona(prev => ({
|
|
276
450
|
...prev,
|
|
277
451
|
state: 'waiting',
|
|
278
|
-
saying: balloonLine(`${
|
|
279
|
-
full: `${
|
|
452
|
+
saying: balloonLine(`${to.app.name}, could you: ${request}`),
|
|
453
|
+
full: `${to.app.name}, could you: ${request}`,
|
|
280
454
|
insist: true,
|
|
281
|
-
// Its
|
|
455
|
+
// Its tool, in its own words: it asks its peer.
|
|
282
456
|
tool: {
|
|
283
457
|
id: part.toolCallId,
|
|
284
|
-
tool: askTool,
|
|
285
|
-
name: askTool,
|
|
458
|
+
tool: to.askTool,
|
|
459
|
+
name: to.askTool,
|
|
286
460
|
phase: 'running',
|
|
287
|
-
words: `Asking ${
|
|
461
|
+
words: `Asking ${to.app.name}…`,
|
|
288
462
|
},
|
|
289
463
|
}));
|
|
290
464
|
}
|
|
291
|
-
else if (part.type === 'tool-result' &&
|
|
465
|
+
else if (part.type === 'tool-result' && to) {
|
|
292
466
|
const output = part.output;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
467
|
+
const toolSpan = toolSpans.get(part.toolCallId);
|
|
468
|
+
if (toolSpan) {
|
|
469
|
+
toolSpans.delete(part.toolCallId);
|
|
470
|
+
endToolCall(toolSpan, {
|
|
471
|
+
result: part.output,
|
|
472
|
+
...(output?.error ? { error: String(output.error) } : {}),
|
|
473
|
+
});
|
|
474
|
+
}
|
|
297
475
|
setEntryPersona(prev => ({
|
|
298
476
|
...prev,
|
|
299
477
|
state: 'thinking',
|
|
@@ -301,9 +479,9 @@ export function useA2ATeam(options) {
|
|
|
301
479
|
full: undefined,
|
|
302
480
|
tool: undefined,
|
|
303
481
|
}));
|
|
304
|
-
//
|
|
482
|
+
// The peer has handed its answer over: back at rest, its last
|
|
305
483
|
// words still in its balloon.
|
|
306
|
-
|
|
484
|
+
setPersona(to.id, prev => ({ ...prev, state: 'idle' }));
|
|
307
485
|
}
|
|
308
486
|
else if (part.type === 'text-delta') {
|
|
309
487
|
said += part.text;
|
|
@@ -316,13 +494,16 @@ export function useA2ATeam(options) {
|
|
|
316
494
|
tool: undefined,
|
|
317
495
|
}));
|
|
318
496
|
}
|
|
319
|
-
else if (part.type === 'tool-error' &&
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
:
|
|
325
|
-
|
|
497
|
+
else if (part.type === 'tool-error' && to) {
|
|
498
|
+
const toolSpan = toolSpans.get(part.toolCallId);
|
|
499
|
+
if (toolSpan) {
|
|
500
|
+
toolSpans.delete(part.toolCallId);
|
|
501
|
+
endToolCall(toolSpan, {
|
|
502
|
+
error: part.error instanceof Error
|
|
503
|
+
? part.error.message
|
|
504
|
+
: String(part.error),
|
|
505
|
+
});
|
|
506
|
+
}
|
|
326
507
|
setEntryPersona(prev => ({
|
|
327
508
|
...prev,
|
|
328
509
|
state: 'thinking',
|
|
@@ -339,12 +520,9 @@ export function useA2ATeam(options) {
|
|
|
339
520
|
...history.current,
|
|
340
521
|
{ role: 'assistant', content: said },
|
|
341
522
|
];
|
|
342
|
-
if (
|
|
523
|
+
if (turnSpan) {
|
|
343
524
|
const usage = await Promise.resolve(result.totalUsage).catch(() => undefined);
|
|
344
|
-
|
|
345
|
-
result: { text: said, ...(usage ? { usage } : {}) },
|
|
346
|
-
summary: `${balloonLine(asked, 60)} → ${balloonLine(said, 80)}${usage?.totalTokens ? ` · ${usage.totalTokens} tokens` : ''}`,
|
|
347
|
-
});
|
|
525
|
+
endAgentTurn(turnSpan, { text: said, usage });
|
|
348
526
|
}
|
|
349
527
|
setTurns(prev => [...prev, { role: 'assistant', text: said }]);
|
|
350
528
|
setEntryPersona(prev => ({
|
|
@@ -356,7 +534,10 @@ export function useA2ATeam(options) {
|
|
|
356
534
|
}
|
|
357
535
|
catch (error) {
|
|
358
536
|
const message = error instanceof Error ? error.message : String(error);
|
|
359
|
-
|
|
537
|
+
toolSpans.forEach(toolSpan => endToolCall(toolSpan, { error: message }));
|
|
538
|
+
if (turnSpan) {
|
|
539
|
+
endAgentTurn(turnSpan, { error: message });
|
|
540
|
+
}
|
|
360
541
|
setTurns(prev => [
|
|
361
542
|
...prev,
|
|
362
543
|
{ role: 'assistant', text: `Something went wrong: ${message}` },
|
|
@@ -369,48 +550,83 @@ export function useA2ATeam(options) {
|
|
|
369
550
|
insist: true,
|
|
370
551
|
tool: undefined,
|
|
371
552
|
}));
|
|
372
|
-
|
|
553
|
+
setFlows({});
|
|
373
554
|
callsNow.current = [];
|
|
374
555
|
setCalls([]);
|
|
375
556
|
}
|
|
376
557
|
finally {
|
|
377
558
|
abort.current = null;
|
|
378
559
|
setBusy(false);
|
|
379
|
-
// However the turn ended,
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
560
|
+
// However the turn ended, no member is still at work.
|
|
561
|
+
setPersonas(prev => Object.fromEntries(Object.entries(prev).map(([id, one]) => [
|
|
562
|
+
id,
|
|
563
|
+
one.state === 'idle' && !one.tool
|
|
564
|
+
? one
|
|
565
|
+
: { ...one, state: 'idle', tool: undefined },
|
|
566
|
+
])));
|
|
383
567
|
}
|
|
384
|
-
}, [
|
|
568
|
+
}, [
|
|
569
|
+
agent,
|
|
570
|
+
busy,
|
|
571
|
+
entry,
|
|
572
|
+
inspector,
|
|
573
|
+
entryPeer,
|
|
574
|
+
sendOverA2A,
|
|
575
|
+
setPersona,
|
|
576
|
+
peersAtRest,
|
|
577
|
+
]);
|
|
385
578
|
const stop = useCallback(() => abort.current?.abort(), []);
|
|
386
579
|
useEffect(() => () => {
|
|
387
580
|
abort.current?.abort();
|
|
388
|
-
clearTimeout(
|
|
581
|
+
flowTimers.current.forEach(timer => clearTimeout(timer));
|
|
389
582
|
clearTimeout(callTimer.current);
|
|
390
583
|
}, []);
|
|
391
584
|
const setEntryAway = useCallback((away) => setEntryPersona(prev => ({ ...prev, away })), []);
|
|
392
|
-
const
|
|
585
|
+
const setAwayOf = useCallback((id, away) => setPersona(id, prev => ({ ...prev, away })), [setPersona]);
|
|
586
|
+
const setPeerAway = useCallback((away) => {
|
|
587
|
+
const first = peersNow.current[0];
|
|
588
|
+
if (first) {
|
|
589
|
+
setAwayOf(first.id, away);
|
|
590
|
+
}
|
|
591
|
+
}, [setAwayOf]);
|
|
393
592
|
const entryHistory = useMemo(() => turns.map((turn, index) => ({
|
|
394
593
|
id: `turn-${index}`,
|
|
395
594
|
role: turn.role,
|
|
396
595
|
content: turn.text,
|
|
397
596
|
createdAt: new Date(0),
|
|
398
597
|
})), [turns]);
|
|
598
|
+
const NONE = useMemo(() => [], []);
|
|
599
|
+
const teamPeers = useMemo(() => peerList.map(one => ({
|
|
600
|
+
id: one.id,
|
|
601
|
+
app: one.app,
|
|
602
|
+
askTool: one.askTool,
|
|
603
|
+
connections: one.connections,
|
|
604
|
+
connected: one.peer !== null,
|
|
605
|
+
persona: personaOf(one.id),
|
|
606
|
+
history: histories[one.id] ?? NONE,
|
|
607
|
+
report: reports[one.id] ?? null,
|
|
608
|
+
artifacts: givens[one.id] ?? [],
|
|
609
|
+
flow: flows[one.id] ?? 'still',
|
|
610
|
+
setAway: (away) => setAwayOf(one.id, away),
|
|
611
|
+
})), [peerList, personaOf, histories, reports, givens, flows, setAwayOf, NONE]);
|
|
612
|
+
const first = teamPeers[0];
|
|
399
613
|
return {
|
|
400
614
|
entryHistory,
|
|
401
|
-
peerHistory,
|
|
615
|
+
peerHistory: first?.history ?? NONE,
|
|
402
616
|
entryPersona,
|
|
403
|
-
peerPersona,
|
|
617
|
+
peerPersona: first?.persona ?? AT_REST,
|
|
404
618
|
setEntryAway,
|
|
405
619
|
setPeerAway,
|
|
406
620
|
turns,
|
|
621
|
+
peers: teamPeers,
|
|
622
|
+
flows,
|
|
407
623
|
report,
|
|
408
624
|
artifacts,
|
|
409
625
|
notebook,
|
|
410
|
-
flow,
|
|
626
|
+
flow: first?.flow ?? 'still',
|
|
411
627
|
calls,
|
|
412
628
|
busy,
|
|
413
|
-
ready: agent !== null,
|
|
629
|
+
ready: entryPeer ? true : agent !== null,
|
|
414
630
|
send,
|
|
415
631
|
stop,
|
|
416
632
|
};
|