@datalayer/agent-runtimes 1.3.85 → 1.3.87
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/components/teams/A2ATeamGraph.d.ts +115 -20
- package/lib/components/teams/A2ATeamGraph.js +246 -109
- package/lib/components/teams/index.d.ts +1 -0
- package/lib/components/teams/index.js +1 -0
- package/lib/components/teams/sceneStage.d.ts +120 -0
- package/lib/components/teams/sceneStage.js +170 -0
- package/lib/components/teams/useA2ATeam.d.ts +92 -20
- package/lib/components/teams/useA2ATeam.js +313 -101
- 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/specs/index.d.ts +1 -0
- package/lib/specs/index.js +1 -0
- package/lib/specs/mcpServers.js +8 -2
- package/lib/specs/sceneSchema.d.ts +8 -0
- package/lib/specs/sceneSchema.js +852 -0
- package/lib/specs/scenes.d.ts +21 -0
- package/lib/specs/scenes.js +1191 -0
- 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/package.json +3 -3
- package/scripts/codegen/generate_agents.py +1 -0
- package/scripts/codegen/generate_envvars.py +1 -1
- package/scripts/codegen/generate_scenes.py +343 -0
|
@@ -3,28 +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
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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`.
|
|
19
|
+
*
|
|
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}).
|
|
22
28
|
*
|
|
23
29
|
* Given the Agent Inspector's tracer (`inspector`), the entry's turns (an
|
|
24
|
-
* `invoke_agent` span: its model, its tokens) and its
|
|
25
|
-
* `execute_tool`
|
|
26
|
-
* the A2A traffic itself, under
|
|
27
|
-
* (`traceA2AFetch`).
|
|
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`).
|
|
28
34
|
*
|
|
29
35
|
* What `A2ATeamGraph` draws, and what a page's composer sends.
|
|
30
36
|
*
|
|
@@ -33,11 +39,12 @@
|
|
|
33
39
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
34
40
|
import { stepCountIs, ToolLoopAgent } from 'ai';
|
|
35
41
|
import { createBrowserModel, } from '../../runtimes/browser/model';
|
|
36
|
-
import { a2aPeerTool, NOTEBOOK_MEDIA_TYPE, } from '../../runtimes/browser/a2aPeer';
|
|
42
|
+
import { a2aPeerTool, askA2APeer, NOTEBOOK_MEDIA_TYPE, } from '../../runtimes/browser/a2aPeer';
|
|
37
43
|
import { endAgentTurn, endToolCall, startAgentTurn, startToolCall, } from '../inspector/agentSpans';
|
|
38
44
|
import { callsAfter, flowAfter, toolOwnName, pruneCalls, } from './a2aTeamFlow';
|
|
39
45
|
import { toolLineOfStep, } from '../../chat/assistant/toolLine';
|
|
40
46
|
const NO_CONNECTIONS = [];
|
|
47
|
+
const NO_PEERS = [];
|
|
41
48
|
export const AT_REST = {
|
|
42
49
|
state: 'idle',
|
|
43
50
|
insist: false,
|
|
@@ -66,14 +73,84 @@ export function balloonLine(text, length = 120) {
|
|
|
66
73
|
const flat = text.replace(/\s+/g, ' ').trim();
|
|
67
74
|
return flat.length > length ? `${flat.slice(0, length - 1)}…` : flat;
|
|
68
75
|
}
|
|
69
|
-
/**
|
|
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. */
|
|
70
120
|
export function useA2ATeam(options) {
|
|
71
|
-
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;
|
|
72
122
|
// By what it names, so that a page passing a new array of the same media
|
|
73
123
|
// types does not make the agent again.
|
|
74
124
|
const acceptKey = options.accept?.join('\n');
|
|
75
125
|
const accept = useMemo(() => (acceptKey ? acceptKey.split('\n') : undefined), [acceptKey]);
|
|
76
|
-
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), []);
|
|
77
154
|
const [entryPersona, setEntryPersona] = useState(() => ({
|
|
78
155
|
...AT_REST,
|
|
79
156
|
state: 'greeting',
|
|
@@ -81,11 +158,28 @@ export function useA2ATeam(options) {
|
|
|
81
158
|
full: entry.interface.welcome ?? '',
|
|
82
159
|
insist: Boolean(entry.interface.welcome),
|
|
83
160
|
}));
|
|
84
|
-
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]);
|
|
85
175
|
const [turns, setTurns] = useState([]);
|
|
86
|
-
const [
|
|
176
|
+
const [histories, setHistories] = useState({});
|
|
87
177
|
const told = useRef(0);
|
|
88
|
-
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
|
+
}
|
|
89
183
|
told.current += 1;
|
|
90
184
|
const message = {
|
|
91
185
|
id: `peer-${told.current}`,
|
|
@@ -93,62 +187,82 @@ export function useA2ATeam(options) {
|
|
|
93
187
|
content: text,
|
|
94
188
|
createdAt: new Date(),
|
|
95
189
|
};
|
|
96
|
-
|
|
97
|
-
}, []);
|
|
98
|
-
// 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,
|
|
99
193
|
// updated when it ends.
|
|
100
|
-
const toolCalled = useCallback((step) => {
|
|
101
|
-
|
|
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;
|
|
102
200
|
const call = {
|
|
103
|
-
id: `peer-tool:${
|
|
201
|
+
id: `peer-tool:${callId}`,
|
|
104
202
|
type: 'tool-call',
|
|
105
|
-
toolCallId:
|
|
106
|
-
toolName: toolOwnName(step.name,
|
|
203
|
+
toolCallId: callId,
|
|
204
|
+
toolName: toolOwnName(step.name, connections),
|
|
107
205
|
args: {},
|
|
108
206
|
status: step.error ? 'error' : step.ended ? 'complete' : 'executing',
|
|
109
207
|
...(step.error ? { error: step.error } : {}),
|
|
110
208
|
};
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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]);
|
|
115
219
|
const [report, setReport] = useState(null);
|
|
220
|
+
const [reports, setReports] = useState({});
|
|
116
221
|
const [artifacts, setArtifacts] = useState([]);
|
|
222
|
+
const [givens, setGivens] = useState({});
|
|
117
223
|
const [notebook, setNotebook] = useState(null);
|
|
118
|
-
const [
|
|
224
|
+
const [flows, setFlows] = useState({});
|
|
119
225
|
const [calls, setCalls] = useState([]);
|
|
120
226
|
const callsNow = useRef([]);
|
|
121
227
|
const callTimer = useRef(undefined);
|
|
122
228
|
const [busy, setBusy] = useState(false);
|
|
123
229
|
const history = useRef([]);
|
|
124
230
|
const abort = useRef(null);
|
|
125
|
-
const
|
|
126
|
-
// What
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
}
|
|
145
259
|
}
|
|
146
260
|
if (event.phase === 'asked') {
|
|
147
|
-
tell('user', event.request);
|
|
148
|
-
|
|
261
|
+
tell(id, 'user', event.request);
|
|
262
|
+
setPersona(id, prev => ({
|
|
149
263
|
...prev,
|
|
150
264
|
state: 'greeting',
|
|
151
|
-
saying:
|
|
265
|
+
saying: askedLine(connections),
|
|
152
266
|
full: undefined,
|
|
153
267
|
insist: true,
|
|
154
268
|
tool: undefined,
|
|
@@ -156,9 +270,9 @@ export function useA2ATeam(options) {
|
|
|
156
270
|
}
|
|
157
271
|
else if (event.phase === 'working') {
|
|
158
272
|
if (event.tool) {
|
|
159
|
-
toolCalled(event.tool);
|
|
273
|
+
toolCalled(id, event.tool);
|
|
160
274
|
}
|
|
161
|
-
|
|
275
|
+
setPersona(id, prev => ({
|
|
162
276
|
...prev,
|
|
163
277
|
state: 'working',
|
|
164
278
|
saying: event.note ? balloonLine(event.note) : prev.saying,
|
|
@@ -167,19 +281,21 @@ export function useA2ATeam(options) {
|
|
|
167
281
|
// The tool it calls, in plain words, from the step it told; a
|
|
168
282
|
// step without a tool (its own words) puts the tool line away.
|
|
169
283
|
tool: event.tool
|
|
170
|
-
? toolLineOfStep(event.tool, toolOwnName(event.tool.name,
|
|
284
|
+
? toolLineOfStep(event.tool, toolOwnName(event.tool.name, connections))
|
|
171
285
|
: undefined,
|
|
172
286
|
}));
|
|
173
287
|
}
|
|
174
288
|
else if (event.phase === 'answered') {
|
|
175
|
-
tell('assistant', event.answer);
|
|
289
|
+
tell(id, 'assistant', event.answer);
|
|
176
290
|
setReport(event.answer);
|
|
291
|
+
setReports(prev => ({ ...prev, [id]: event.answer }));
|
|
177
292
|
setArtifacts(event.artifacts);
|
|
293
|
+
setGivens(prev => ({ ...prev, [id]: event.artifacts }));
|
|
178
294
|
const given = notebookAmong(event.artifacts);
|
|
179
295
|
if (given) {
|
|
180
296
|
setNotebook(given);
|
|
181
297
|
}
|
|
182
|
-
|
|
298
|
+
setPersona(id, prev => ({
|
|
183
299
|
...prev,
|
|
184
300
|
state: 'speaking',
|
|
185
301
|
saying: balloonLine(event.answer),
|
|
@@ -188,13 +304,13 @@ export function useA2ATeam(options) {
|
|
|
188
304
|
tool: undefined,
|
|
189
305
|
}));
|
|
190
306
|
// The entry was given a notebook: in its balloon, read-only.
|
|
191
|
-
if (given) {
|
|
307
|
+
if (given && id !== entry.id) {
|
|
192
308
|
setEntryPersona(prev => ({ ...prev, notebook: given }));
|
|
193
309
|
}
|
|
194
310
|
}
|
|
195
311
|
else {
|
|
196
|
-
tell('assistant', `Could not answer: ${event.error}`);
|
|
197
|
-
|
|
312
|
+
tell(id, 'assistant', `Could not answer: ${event.error}`);
|
|
313
|
+
setPersona(id, prev => ({
|
|
198
314
|
...prev,
|
|
199
315
|
state: 'idle',
|
|
200
316
|
saying: balloonLine(event.error),
|
|
@@ -203,27 +319,81 @@ export function useA2ATeam(options) {
|
|
|
203
319
|
tool: undefined,
|
|
204
320
|
}));
|
|
205
321
|
}
|
|
206
|
-
}, [
|
|
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.
|
|
207
325
|
const agent = useMemo(() => {
|
|
208
|
-
if (!peer) {
|
|
326
|
+
if (entryPeer || !peerList.length || peerList.some(one => !one.peer)) {
|
|
209
327
|
return null;
|
|
210
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
|
+
}
|
|
211
337
|
return new ToolLoopAgent({
|
|
212
338
|
id: entry.id,
|
|
213
339
|
model: createBrowserModel({
|
|
214
340
|
...inference,
|
|
215
341
|
model: entry.model || undefined,
|
|
216
342
|
}),
|
|
217
|
-
instructions: entry
|
|
218
|
-
tools
|
|
219
|
-
[askTool]: a2aPeerTool({ peer, onEvent: onPeerEvent, accept }),
|
|
220
|
-
},
|
|
343
|
+
instructions: teamInstructions(entry, peerList),
|
|
344
|
+
tools,
|
|
221
345
|
stopWhen: stepCountIs(maxSteps),
|
|
222
346
|
});
|
|
223
|
-
}, [
|
|
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]);
|
|
224
388
|
const send = useCallback(async (text) => {
|
|
225
389
|
const asked = text.trim();
|
|
226
|
-
if (!asked ||
|
|
390
|
+
if (!asked || busy) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (entryPeer) {
|
|
394
|
+
return sendOverA2A(asked, entryPeer);
|
|
395
|
+
}
|
|
396
|
+
if (!agent) {
|
|
227
397
|
return;
|
|
228
398
|
}
|
|
229
399
|
setBusy(true);
|
|
@@ -236,12 +406,12 @@ export function useA2ATeam(options) {
|
|
|
236
406
|
saying: 'Let me see…',
|
|
237
407
|
insist: true,
|
|
238
408
|
});
|
|
239
|
-
|
|
409
|
+
peersAtRest();
|
|
240
410
|
history.current = [...history.current, { role: 'user', content: asked }];
|
|
241
411
|
abort.current = new AbortController();
|
|
242
412
|
let said = '';
|
|
243
413
|
// The turn, as a span: the entry's model, from the question to the
|
|
244
|
-
// answer, and its tokens; its calls to the
|
|
414
|
+
// answer, and its tokens; its calls to the peers under it.
|
|
245
415
|
const turnSpan = inspector
|
|
246
416
|
? startAgentTurn(inspector, {
|
|
247
417
|
agent: entry.name,
|
|
@@ -250,19 +420,26 @@ export function useA2ATeam(options) {
|
|
|
250
420
|
})
|
|
251
421
|
: undefined;
|
|
252
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);
|
|
253
425
|
try {
|
|
254
426
|
const result = await agent.stream({
|
|
255
427
|
messages: history.current,
|
|
256
428
|
abortSignal: abort.current.signal,
|
|
257
429
|
});
|
|
258
430
|
for await (const part of result.fullStream) {
|
|
259
|
-
|
|
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) {
|
|
260
437
|
const request = String(part.input?.request ?? '');
|
|
261
438
|
// Its tool runs here, in the page, and asks the peer over A2A.
|
|
262
439
|
if (inspector && turnSpan) {
|
|
263
440
|
toolSpans.set(part.toolCallId, startToolCall(inspector, {
|
|
264
441
|
agent: entry.name,
|
|
265
|
-
name: askTool,
|
|
442
|
+
name: to.askTool,
|
|
266
443
|
id: part.toolCallId,
|
|
267
444
|
args: part.input,
|
|
268
445
|
tool: { kind: 'frontend' },
|
|
@@ -272,20 +449,20 @@ export function useA2ATeam(options) {
|
|
|
272
449
|
setEntryPersona(prev => ({
|
|
273
450
|
...prev,
|
|
274
451
|
state: 'waiting',
|
|
275
|
-
saying: balloonLine(`${
|
|
276
|
-
full: `${
|
|
452
|
+
saying: balloonLine(`${to.app.name}, could you: ${request}`),
|
|
453
|
+
full: `${to.app.name}, could you: ${request}`,
|
|
277
454
|
insist: true,
|
|
278
|
-
// Its
|
|
455
|
+
// Its tool, in its own words: it asks its peer.
|
|
279
456
|
tool: {
|
|
280
457
|
id: part.toolCallId,
|
|
281
|
-
tool: askTool,
|
|
282
|
-
name: askTool,
|
|
458
|
+
tool: to.askTool,
|
|
459
|
+
name: to.askTool,
|
|
283
460
|
phase: 'running',
|
|
284
|
-
words: `Asking ${
|
|
461
|
+
words: `Asking ${to.app.name}…`,
|
|
285
462
|
},
|
|
286
463
|
}));
|
|
287
464
|
}
|
|
288
|
-
else if (part.type === 'tool-result' &&
|
|
465
|
+
else if (part.type === 'tool-result' && to) {
|
|
289
466
|
const output = part.output;
|
|
290
467
|
const toolSpan = toolSpans.get(part.toolCallId);
|
|
291
468
|
if (toolSpan) {
|
|
@@ -302,9 +479,9 @@ export function useA2ATeam(options) {
|
|
|
302
479
|
full: undefined,
|
|
303
480
|
tool: undefined,
|
|
304
481
|
}));
|
|
305
|
-
//
|
|
482
|
+
// The peer has handed its answer over: back at rest, its last
|
|
306
483
|
// words still in its balloon.
|
|
307
|
-
|
|
484
|
+
setPersona(to.id, prev => ({ ...prev, state: 'idle' }));
|
|
308
485
|
}
|
|
309
486
|
else if (part.type === 'text-delta') {
|
|
310
487
|
said += part.text;
|
|
@@ -317,7 +494,7 @@ export function useA2ATeam(options) {
|
|
|
317
494
|
tool: undefined,
|
|
318
495
|
}));
|
|
319
496
|
}
|
|
320
|
-
else if (part.type === 'tool-error' &&
|
|
497
|
+
else if (part.type === 'tool-error' && to) {
|
|
321
498
|
const toolSpan = toolSpans.get(part.toolCallId);
|
|
322
499
|
if (toolSpan) {
|
|
323
500
|
toolSpans.delete(part.toolCallId);
|
|
@@ -373,48 +550,83 @@ export function useA2ATeam(options) {
|
|
|
373
550
|
insist: true,
|
|
374
551
|
tool: undefined,
|
|
375
552
|
}));
|
|
376
|
-
|
|
553
|
+
setFlows({});
|
|
377
554
|
callsNow.current = [];
|
|
378
555
|
setCalls([]);
|
|
379
556
|
}
|
|
380
557
|
finally {
|
|
381
558
|
abort.current = null;
|
|
382
559
|
setBusy(false);
|
|
383
|
-
// However the turn ended,
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
+
])));
|
|
387
567
|
}
|
|
388
|
-
}, [
|
|
568
|
+
}, [
|
|
569
|
+
agent,
|
|
570
|
+
busy,
|
|
571
|
+
entry,
|
|
572
|
+
inspector,
|
|
573
|
+
entryPeer,
|
|
574
|
+
sendOverA2A,
|
|
575
|
+
setPersona,
|
|
576
|
+
peersAtRest,
|
|
577
|
+
]);
|
|
389
578
|
const stop = useCallback(() => abort.current?.abort(), []);
|
|
390
579
|
useEffect(() => () => {
|
|
391
580
|
abort.current?.abort();
|
|
392
|
-
clearTimeout(
|
|
581
|
+
flowTimers.current.forEach(timer => clearTimeout(timer));
|
|
393
582
|
clearTimeout(callTimer.current);
|
|
394
583
|
}, []);
|
|
395
584
|
const setEntryAway = useCallback((away) => setEntryPersona(prev => ({ ...prev, away })), []);
|
|
396
|
-
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]);
|
|
397
592
|
const entryHistory = useMemo(() => turns.map((turn, index) => ({
|
|
398
593
|
id: `turn-${index}`,
|
|
399
594
|
role: turn.role,
|
|
400
595
|
content: turn.text,
|
|
401
596
|
createdAt: new Date(0),
|
|
402
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];
|
|
403
613
|
return {
|
|
404
614
|
entryHistory,
|
|
405
|
-
peerHistory,
|
|
615
|
+
peerHistory: first?.history ?? NONE,
|
|
406
616
|
entryPersona,
|
|
407
|
-
peerPersona,
|
|
617
|
+
peerPersona: first?.persona ?? AT_REST,
|
|
408
618
|
setEntryAway,
|
|
409
619
|
setPeerAway,
|
|
410
620
|
turns,
|
|
621
|
+
peers: teamPeers,
|
|
622
|
+
flows,
|
|
411
623
|
report,
|
|
412
624
|
artifacts,
|
|
413
625
|
notebook,
|
|
414
|
-
flow,
|
|
626
|
+
flow: first?.flow ?? 'still',
|
|
415
627
|
calls,
|
|
416
628
|
busy,
|
|
417
|
-
ready: agent !== null,
|
|
629
|
+
ready: entryPeer ? true : agent !== null,
|
|
418
630
|
send,
|
|
419
631
|
stop,
|
|
420
632
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scenes of the catalogue (agentspecs `scenes`, LOOP A-15), in tabs:
|
|
3
|
+
* each scene's face and name, its `assumes` line under them, its graph and
|
|
4
|
+
* its transcript behind *Graph · Transcript* (`SceneView`), its members
|
|
5
|
+
* standing where its stage directions put them (`positions`), the entry's
|
|
6
|
+
* balloon open first (`opens_first`), and the script's cues as the entry's
|
|
7
|
+
* suggestions. A scene is tried here before it is deployed anywhere.
|
|
8
|
+
*
|
|
9
|
+
* Played against the local servers (`npm run examples`): the entry in the
|
|
10
|
+
* browser asks its model through the examples' inference — a signed-in
|
|
11
|
+
* person's token or a visitor's trial key — and the members on a runtime
|
|
12
|
+
* are served over A2A by `examples/sales-accounting-a2a/serve_scenes.py`,
|
|
13
|
+
* Accounting on :8767 as before and each other member on a runtime of its
|
|
14
|
+
* own from :8768 (`sceneRuntimePorts`), or wherever `VITE_A2A_<MEMBER>_URL`
|
|
15
|
+
* says. A scene whose entry runs on a runtime itself (Month-end close, Crop
|
|
16
|
+
* monitoring) is asked over A2A from the page, as the person
|
|
17
|
+
* (`useA2ATeam`'s `entryPeer`). A member that is not reached is drawn all
|
|
18
|
+
* the same, and the box says what its application needs (`setup`) and how
|
|
19
|
+
* to serve it, rather than failing.
|
|
20
|
+
*
|
|
21
|
+
* Right-click a member for its menu: *Inspect the agent…* opens its Agent
|
|
22
|
+
* Inspector over the page, on the one tracer every member records into.
|
|
23
|
+
*/
|
|
24
|
+
import type { JSX } from 'react';
|
|
25
|
+
export declare function ScenesExample(): JSX.Element;
|
|
26
|
+
export default ScenesExample;
|