@datalayer/agent-runtimes 1.3.73 → 1.3.75
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 +25 -2
- package/lib/chat/ChatFloating.js +80 -12
- package/lib/chat/assistant/AssistantContextMenu.d.ts +67 -0
- package/lib/chat/assistant/AssistantContextMenu.js +68 -0
- package/lib/chat/assistant/AssistantStage.d.ts +123 -5
- package/lib/chat/assistant/AssistantStage.js +245 -7
- package/lib/chat/assistant/BalloonParts.d.ts +15 -1
- package/lib/chat/assistant/BalloonParts.js +26 -9
- package/lib/chat/assistant/BalloonVisual.d.ts +65 -0
- package/lib/chat/assistant/BalloonVisual.js +58 -0
- package/lib/chat/assistant/SpeechBalloon.d.ts +68 -2
- package/lib/chat/assistant/SpeechBalloon.js +113 -7
- package/lib/chat/assistant/state.d.ts +19 -0
- package/lib/chat/assistant/state.js +44 -17
- package/lib/chat/base/ChatBase.js +51 -1
- package/lib/chat/indicators/TypingDots.d.ts +13 -0
- package/lib/chat/indicators/TypingDots.js +21 -0
- package/lib/chat/messages/ChatMessageList.js +1 -15
- package/lib/components/inspector/AgentInspector.d.ts +59 -0
- package/lib/components/inspector/AgentInspector.js +220 -0
- package/lib/components/inspector/AgentInspectorDialog.d.ts +19 -0
- package/lib/components/inspector/AgentInspectorDialog.js +9 -0
- package/lib/components/inspector/JsonTree.d.ts +24 -0
- package/lib/components/inspector/JsonTree.js +89 -0
- package/lib/components/inspector/a2aInspect.d.ts +56 -0
- package/lib/components/inspector/a2aInspect.js +478 -0
- package/lib/components/inspector/agentInspector.d.ts +179 -0
- package/lib/components/inspector/agentInspector.js +267 -0
- package/lib/components/inspector/chatInspect.d.ts +38 -0
- package/lib/components/inspector/chatInspect.js +65 -0
- package/lib/components/inspector/classify.d.ts +19 -0
- package/lib/components/inspector/classify.js +42 -0
- package/lib/components/inspector/index.d.ts +14 -0
- package/lib/components/inspector/index.js +18 -0
- package/lib/components/teams/A2ATeamGraph.d.ts +54 -3
- package/lib/components/teams/A2ATeamGraph.js +37 -11
- package/lib/components/teams/NotebookPreview.d.ts +11 -2
- package/lib/components/teams/NotebookPreview.js +6 -3
- package/lib/components/teams/NotebookPreviewView.js +9 -1
- package/lib/components/teams/TeamNotebook.d.ts +10 -1
- package/lib/components/teams/TeamNotebook.js +15 -3
- package/lib/components/teams/jupyterThemeVariables.d.ts +9 -0
- package/lib/components/teams/jupyterThemeVariables.js +441 -0
- package/lib/components/teams/teamConnections.d.ts +9 -1
- package/lib/components/teams/teamConnections.js +20 -0
- package/lib/components/teams/useA2ATeam.d.ts +17 -4
- package/lib/components/teams/useA2ATeam.js +87 -23
- package/lib/examples/AgentA2ATeamExample.d.ts +16 -2
- package/lib/examples/AgentA2ATeamExample.js +65 -13
- package/lib/examples/{ChatAssistantExample.d.ts → AssistantExample.d.ts} +2 -2
- package/lib/examples/{ChatAssistantExample.js → AssistantExample.js} +69 -7
- package/lib/examples/{ChatAssistantGalleryExample.d.ts → AssistantGalleryExample.d.ts} +2 -2
- package/lib/examples/{ChatAssistantGalleryExample.js → AssistantGalleryExample.js} +45 -13
- package/lib/examples/example-selector.js +4 -2
- package/lib/examples/exampleGroups.d.ts +9 -0
- package/lib/examples/exampleGroups.js +56 -0
- package/lib/examples/index.d.ts +1 -1
- package/lib/examples/index.js +1 -1
- package/lib/examples/main.js +3 -43
- package/lib/examples/utils/AssistantGalleryGrid.d.ts +6 -1
- package/lib/examples/utils/AssistantGalleryGrid.js +16 -3
- package/lib/examples/utils/assistantGallery.d.ts +51 -2
- package/lib/examples/utils/assistantGallery.js +32 -7
- package/lib/loop/apps/AppRenderer.js +1 -1
- package/lib/loop/core/index.d.ts +17 -0
- package/lib/loop/core/index.js +1 -0
- package/lib/loop/plugins/assistant/index.d.ts +1 -1
- package/lib/loop/plugins/assistant/index.js +8 -3
- package/lib/runtimes/browser/a2aPeer.d.ts +4 -1
- package/lib/types/chat.d.ts +7 -0
- package/package.json +1 -1
- package/scripts/codegen/generate_jupyter_theme_variables.mjs +57 -0
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { MCP_SERVER_LIBRARY } from '../../specs/mcpServers';
|
|
14
14
|
import { SERVER_ACTIONS } from '../../specs/actions';
|
|
15
|
+
import { connectionOfTool } from './a2aTeamFlow';
|
|
15
16
|
/** A server reference without its version: `odoo-accounting:0.0.1` is `odoo-accounting`. */
|
|
16
17
|
const idOf = (ref) => {
|
|
17
18
|
const at = ref.lastIndexOf(':');
|
|
@@ -47,3 +48,22 @@ export function teamConnectionsOf(app) {
|
|
|
47
48
|
}
|
|
48
49
|
return connections;
|
|
49
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Where a member's tool call comes from, for the Agent Inspector: a call to
|
|
53
|
+
* one of its connections is an MCP call, with the server's mark; any other
|
|
54
|
+
* is `fallback`'s (`classifyToolCall`, by the catalogues) or a tool its
|
|
55
|
+
* runtime runs (`write_notebook`).
|
|
56
|
+
*/
|
|
57
|
+
export function teamToolClassifier(connections, fallback) {
|
|
58
|
+
return (name, args) => {
|
|
59
|
+
const connection = connectionOfTool(name, connections);
|
|
60
|
+
if (connection) {
|
|
61
|
+
return {
|
|
62
|
+
source: 'mcp',
|
|
63
|
+
...(connection.icon ? { icon: connection.icon } : {}),
|
|
64
|
+
...(connection.emoji ? { emoji: connection.emoji } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return fallback?.(name, args) ?? { source: 'backend-tool' };
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { AssistantState } from '../../chat/assistant/state';
|
|
1
|
+
import type { AssistantState, BalloonHistoryMessage } from '../../chat/assistant/state';
|
|
2
2
|
import { type BrowserModelOptions } from '../../runtimes/browser/model';
|
|
3
3
|
import { type A2APeer, type A2APeerArtifact } from '../../runtimes/browser/a2aPeer';
|
|
4
4
|
import type { AppSpec } from '../../types/agentspecs';
|
|
5
|
+
import type { AgentInspectorSink } from '../inspector/agentInspector';
|
|
5
6
|
import { type A2ATeamCall, type A2ATeamConnection, type A2ATeamFlow } from './a2aTeamFlow';
|
|
6
7
|
import { type BalloonToolLine } from '../../chat/assistant/toolLine';
|
|
7
8
|
/** What one member's character does: its state, its balloon, and whether it was sent away. */
|
|
@@ -9,6 +10,8 @@ export type A2ATeamPersona = {
|
|
|
9
10
|
state: AssistantState;
|
|
10
11
|
/** What its balloon says. */
|
|
11
12
|
saying?: string;
|
|
13
|
+
/** What it says, uncut, when `saying` was cut to fit: the balloon's *more*. */
|
|
14
|
+
full?: string;
|
|
12
15
|
/** Whether the balloon shows without the pointer over it. */
|
|
13
16
|
insist: boolean;
|
|
14
17
|
away: boolean;
|
|
@@ -28,7 +31,7 @@ export type A2ATeamTurn = {
|
|
|
28
31
|
};
|
|
29
32
|
/** What a page that shows notebooks accepts: a notebook, and words in Markdown. */
|
|
30
33
|
export declare const NOTEBOOK_AND_WORDS: readonly string[];
|
|
31
|
-
/** What
|
|
34
|
+
/** What the peer gave, in words: its words, and each of its artifacts. */
|
|
32
35
|
export declare function answeredLine(artifacts: A2APeerArtifact[]): string;
|
|
33
36
|
/** The latest notebook among what a peer gave, or none. */
|
|
34
37
|
export declare function notebookAmong(artifacts: A2APeerArtifact[]): A2APeerArtifact | null;
|
|
@@ -54,6 +57,11 @@ export type UseA2ATeamOptions = {
|
|
|
54
57
|
* words, and the formats it can show, such as a notebook. Unsaid, words alone.
|
|
55
58
|
*/
|
|
56
59
|
accept?: readonly string[];
|
|
60
|
+
/**
|
|
61
|
+
* The Agent Inspector's sink: the entry's turns and its call to the peer
|
|
62
|
+
* are recorded there, and what the peer's events were made of.
|
|
63
|
+
*/
|
|
64
|
+
inspector?: AgentInspectorSink | null;
|
|
57
65
|
};
|
|
58
66
|
export type A2ATeam = {
|
|
59
67
|
entryPersona: A2ATeamPersona;
|
|
@@ -61,8 +69,13 @@ export type A2ATeam = {
|
|
|
61
69
|
setEntryAway: (away: boolean) => void;
|
|
62
70
|
setPeerAway: (away: boolean) => void;
|
|
63
71
|
turns: A2ATeamTurn[];
|
|
64
|
-
/** The
|
|
65
|
-
|
|
72
|
+
/** The entry's conversation, as its `history` balloon lists it. */
|
|
73
|
+
entryHistory: BalloonHistoryMessage[];
|
|
74
|
+
/**
|
|
75
|
+
* What the peer was asked, what it did (its tool lines) and what it
|
|
76
|
+
* answered, as its `history` balloon lists it.
|
|
77
|
+
*/
|
|
78
|
+
peerHistory: BalloonHistoryMessage[];
|
|
66
79
|
/** The peer's last answer, as it gave it. */
|
|
67
80
|
report: string | null;
|
|
68
81
|
/** What the peer gave besides words with its last answer, by media type. */
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* with whatever `inference` holds — a signed-in person's token, or a
|
|
12
12
|
* visitor's trial key. Its one tool asks the peer (Accounting) with
|
|
13
13
|
* {@link a2aPeerTool}. What each of them does is kept as a persona — the
|
|
14
|
-
* state its character acts and what its balloon says —
|
|
15
|
-
*
|
|
14
|
+
* state its character acts and what its balloon says — the peer's last
|
|
15
|
+
* answer as the report, and the way
|
|
16
16
|
* the link carries a message as the flow ({@link flowAfter}). What the peer
|
|
17
17
|
* gives besides words — the formats the page accepts (`accept`), such as a
|
|
18
18
|
* Jupyter notebook — is kept as its artifacts, and its latest notebook kept
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
* peer calls on its connections (its MCP servers), told over A2A as it calls
|
|
21
21
|
* them, are kept as the calls running now ({@link callsAfter}).
|
|
22
22
|
*
|
|
23
|
+
* Given an Agent Inspector's sink (`inspector`), the entry's turns (its
|
|
24
|
+
* model, its tokens), its call to the peer, and what the peer's events were
|
|
25
|
+
* made of are recorded there; the A2A traffic itself is recorded by the
|
|
26
|
+
* peer's fetch (`connectA2APeer`'s `inspect`).
|
|
27
|
+
*
|
|
23
28
|
* What `A2ATeamGraph` draws, and what a page's composer sends.
|
|
24
29
|
*
|
|
25
30
|
* @module components/teams/useA2ATeam
|
|
@@ -41,7 +46,7 @@ export const NOTEBOOK_AND_WORDS = [
|
|
|
41
46
|
NOTEBOOK_MEDIA_TYPE,
|
|
42
47
|
'text/markdown',
|
|
43
48
|
];
|
|
44
|
-
/** What
|
|
49
|
+
/** What the peer gave, in words: its words, and each of its artifacts. */
|
|
45
50
|
export function answeredLine(artifacts) {
|
|
46
51
|
const besides = artifacts.map(artifact => artifact.mediaType === NOTEBOOK_MEDIA_TYPE
|
|
47
52
|
? `a notebook, ${artifact.name}`
|
|
@@ -61,7 +66,7 @@ export function balloonLine(text, length = 120) {
|
|
|
61
66
|
}
|
|
62
67
|
/** Run a team of two over A2A in the page. */
|
|
63
68
|
export function useA2ATeam(options) {
|
|
64
|
-
const { entry, peerApp, peer, inference, maxSteps = 6, peerConnections = NO_CONNECTIONS, } = options;
|
|
69
|
+
const { entry, peerApp, peer, inference, maxSteps = 6, peerConnections = NO_CONNECTIONS, inspector = null, } = options;
|
|
65
70
|
// By what it names, so that a page passing a new array of the same media
|
|
66
71
|
// types does not make the agent again.
|
|
67
72
|
const acceptKey = options.accept?.join('\n');
|
|
@@ -71,24 +76,34 @@ export function useA2ATeam(options) {
|
|
|
71
76
|
...AT_REST,
|
|
72
77
|
state: 'greeting',
|
|
73
78
|
saying: balloonLine(entry.interface.welcome ?? ''),
|
|
79
|
+
full: entry.interface.welcome ?? '',
|
|
74
80
|
insist: Boolean(entry.interface.welcome),
|
|
75
81
|
}));
|
|
76
82
|
const [peerPersona, setPeerPersona] = useState(AT_REST);
|
|
77
83
|
const [turns, setTurns] = useState([]);
|
|
84
|
+
const [peerHistory, setPeerHistory] = useState([]);
|
|
85
|
+
const told = useRef(0);
|
|
86
|
+
const tell = useCallback((role, text) => {
|
|
87
|
+
told.current += 1;
|
|
88
|
+
const id = `peer-${told.current}`;
|
|
89
|
+
setPeerHistory(prev => [...prev, { id, role, text }]);
|
|
90
|
+
}, []);
|
|
78
91
|
const [report, setReport] = useState(null);
|
|
79
92
|
const [artifacts, setArtifacts] = useState([]);
|
|
80
93
|
const [notebook, setNotebook] = useState(null);
|
|
81
|
-
const [exchange, setExchange] = useState([]);
|
|
82
94
|
const [flow, setFlow] = useState('still');
|
|
83
95
|
const [calls, setCalls] = useState([]);
|
|
84
96
|
const callsNow = useRef([]);
|
|
85
97
|
const callTimer = useRef(undefined);
|
|
86
98
|
const [busy, setBusy] = useState(false);
|
|
87
99
|
const history = useRef([]);
|
|
100
|
+
const turnCount = useRef(0);
|
|
88
101
|
const abort = useRef(null);
|
|
89
102
|
const flowTimer = useRef(undefined);
|
|
90
103
|
// What the peer does, as it does it: its own state and balloon, and the link.
|
|
91
104
|
const onPeerEvent = useCallback((event) => {
|
|
105
|
+
// Linked to the A2A entry it came from (the capture's linker).
|
|
106
|
+
inspector?.note(event);
|
|
92
107
|
const next = flowAfter(event);
|
|
93
108
|
clearTimeout(flowTimer.current);
|
|
94
109
|
setFlow(next.flow);
|
|
@@ -108,26 +123,27 @@ export function useA2ATeam(options) {
|
|
|
108
123
|
}, after.holdMs);
|
|
109
124
|
}
|
|
110
125
|
if (event.phase === 'asked') {
|
|
111
|
-
|
|
112
|
-
...prev,
|
|
113
|
-
`${entry.name} → ${peerApp.name}: ${event.request}`,
|
|
114
|
-
]);
|
|
126
|
+
tell('user', event.request);
|
|
115
127
|
setPeerPersona(prev => ({
|
|
116
128
|
...prev,
|
|
117
129
|
state: 'greeting',
|
|
118
130
|
saying: 'On it. Let me read the books.',
|
|
131
|
+
full: undefined,
|
|
119
132
|
insist: true,
|
|
120
133
|
tool: undefined,
|
|
121
134
|
}));
|
|
122
135
|
}
|
|
123
136
|
else if (event.phase === 'working') {
|
|
124
|
-
if (event.
|
|
125
|
-
|
|
137
|
+
if (event.tool?.ended) {
|
|
138
|
+
tell('assistant', event.tool.error
|
|
139
|
+
? `${event.tool.name} failed: ${event.tool.error}`
|
|
140
|
+
: `Used ${toolOwnName(event.tool.name, peerConnections)}`);
|
|
126
141
|
}
|
|
127
142
|
setPeerPersona(prev => ({
|
|
128
143
|
...prev,
|
|
129
144
|
state: 'working',
|
|
130
145
|
saying: event.note ? balloonLine(event.note) : prev.saying,
|
|
146
|
+
full: event.note ?? prev.full,
|
|
131
147
|
insist: Boolean(event.note) || Boolean(event.tool) || prev.insist,
|
|
132
148
|
// The tool it calls, in plain words, from the step it told; a
|
|
133
149
|
// step without a tool (its own words) puts the tool line away.
|
|
@@ -137,10 +153,7 @@ export function useA2ATeam(options) {
|
|
|
137
153
|
}));
|
|
138
154
|
}
|
|
139
155
|
else if (event.phase === 'answered') {
|
|
140
|
-
|
|
141
|
-
...prev,
|
|
142
|
-
`${peerApp.name} → ${entry.name}: ${answeredLine(event.artifacts)}`,
|
|
143
|
-
]);
|
|
156
|
+
tell('assistant', event.answer);
|
|
144
157
|
setReport(event.answer);
|
|
145
158
|
setArtifacts(event.artifacts);
|
|
146
159
|
const given = notebookAmong(event.artifacts);
|
|
@@ -151,6 +164,7 @@ export function useA2ATeam(options) {
|
|
|
151
164
|
...prev,
|
|
152
165
|
state: 'speaking',
|
|
153
166
|
saying: balloonLine(event.answer),
|
|
167
|
+
full: event.answer,
|
|
154
168
|
insist: true,
|
|
155
169
|
tool: undefined,
|
|
156
170
|
}));
|
|
@@ -160,19 +174,17 @@ export function useA2ATeam(options) {
|
|
|
160
174
|
}
|
|
161
175
|
}
|
|
162
176
|
else {
|
|
163
|
-
|
|
164
|
-
...prev,
|
|
165
|
-
`${peerApp.name} could not answer: ${event.error}`,
|
|
166
|
-
]);
|
|
177
|
+
tell('assistant', `Could not answer: ${event.error}`);
|
|
167
178
|
setPeerPersona(prev => ({
|
|
168
179
|
...prev,
|
|
169
180
|
state: 'idle',
|
|
170
181
|
saying: balloonLine(event.error),
|
|
182
|
+
full: event.error,
|
|
171
183
|
insist: true,
|
|
172
184
|
tool: undefined,
|
|
173
185
|
}));
|
|
174
186
|
}
|
|
175
|
-
}, [
|
|
187
|
+
}, [peerApp.id, peerConnections, inspector, tell]);
|
|
176
188
|
const agent = useMemo(() => {
|
|
177
189
|
if (!peer) {
|
|
178
190
|
return null;
|
|
@@ -198,7 +210,6 @@ export function useA2ATeam(options) {
|
|
|
198
210
|
setBusy(true);
|
|
199
211
|
setReport(null);
|
|
200
212
|
setArtifacts([]);
|
|
201
|
-
setExchange([]);
|
|
202
213
|
setTurns(prev => [...prev, { role: 'user', text: asked }]);
|
|
203
214
|
setEntryPersona({
|
|
204
215
|
...AT_REST,
|
|
@@ -210,6 +221,19 @@ export function useA2ATeam(options) {
|
|
|
210
221
|
history.current = [...history.current, { role: 'user', content: asked }];
|
|
211
222
|
abort.current = new AbortController();
|
|
212
223
|
let said = '';
|
|
224
|
+
// The turn, in the inspector: the entry's model, from the question to
|
|
225
|
+
// the answer, and its tokens.
|
|
226
|
+
turnCount.current += 1;
|
|
227
|
+
const turnKey = `turn:${entry.id}:${turnCount.current}`;
|
|
228
|
+
inspector?.start({
|
|
229
|
+
key: turnKey,
|
|
230
|
+
actor: entry.name,
|
|
231
|
+
source: 'model',
|
|
232
|
+
kind: 'turn',
|
|
233
|
+
name: entry.model || undefined,
|
|
234
|
+
summary: balloonLine(asked, 160),
|
|
235
|
+
payload: { prompt: asked, model: entry.model || undefined },
|
|
236
|
+
});
|
|
213
237
|
try {
|
|
214
238
|
const result = await agent.stream({
|
|
215
239
|
messages: history.current,
|
|
@@ -218,10 +242,21 @@ export function useA2ATeam(options) {
|
|
|
218
242
|
for await (const part of result.fullStream) {
|
|
219
243
|
if (part.type === 'tool-call' && part.toolName === askTool) {
|
|
220
244
|
const request = String(part.input?.request ?? '');
|
|
245
|
+
// Its tool runs here, in the page, and asks the peer over A2A.
|
|
246
|
+
inspector?.start({
|
|
247
|
+
key: `tool:${part.toolCallId}`,
|
|
248
|
+
actor: entry.name,
|
|
249
|
+
source: 'frontend-tool',
|
|
250
|
+
kind: 'tool-call',
|
|
251
|
+
name: askTool,
|
|
252
|
+
summary: balloonLine(request, 160),
|
|
253
|
+
payload: part.input,
|
|
254
|
+
});
|
|
221
255
|
setEntryPersona(prev => ({
|
|
222
256
|
...prev,
|
|
223
257
|
state: 'waiting',
|
|
224
258
|
saying: balloonLine(`${peerApp.name}, could you: ${request}`),
|
|
259
|
+
full: `${peerApp.name}, could you: ${request}`,
|
|
225
260
|
insist: true,
|
|
226
261
|
// Its one tool, in its own words: it asks its peer.
|
|
227
262
|
tool: {
|
|
@@ -234,10 +269,16 @@ export function useA2ATeam(options) {
|
|
|
234
269
|
}));
|
|
235
270
|
}
|
|
236
271
|
else if (part.type === 'tool-result' && part.toolName === askTool) {
|
|
272
|
+
const output = part.output;
|
|
273
|
+
inspector?.end(`tool:${part.toolCallId}`, {
|
|
274
|
+
result: part.output,
|
|
275
|
+
...(output?.error ? { error: String(output.error) } : {}),
|
|
276
|
+
});
|
|
237
277
|
setEntryPersona(prev => ({
|
|
238
278
|
...prev,
|
|
239
279
|
state: 'thinking',
|
|
240
280
|
saying: 'Thanks!',
|
|
281
|
+
full: undefined,
|
|
241
282
|
tool: undefined,
|
|
242
283
|
}));
|
|
243
284
|
// Accounting has handed its answer over: back at rest, its last
|
|
@@ -250,11 +291,18 @@ export function useA2ATeam(options) {
|
|
|
250
291
|
...prev,
|
|
251
292
|
state: 'speaking',
|
|
252
293
|
saying: balloonLine(said),
|
|
294
|
+
full: said,
|
|
253
295
|
insist: true,
|
|
254
296
|
tool: undefined,
|
|
255
297
|
}));
|
|
256
298
|
}
|
|
257
299
|
else if (part.type === 'tool-error' && part.toolName === askTool) {
|
|
300
|
+
inspector?.end(`tool:${part.toolCallId}`, {
|
|
301
|
+
status: 'failed',
|
|
302
|
+
error: part.error instanceof Error
|
|
303
|
+
? part.error.message
|
|
304
|
+
: String(part.error),
|
|
305
|
+
});
|
|
258
306
|
setEntryPersona(prev => ({
|
|
259
307
|
...prev,
|
|
260
308
|
state: 'thinking',
|
|
@@ -271,15 +319,24 @@ export function useA2ATeam(options) {
|
|
|
271
319
|
...history.current,
|
|
272
320
|
{ role: 'assistant', content: said },
|
|
273
321
|
];
|
|
322
|
+
if (inspector) {
|
|
323
|
+
const usage = await Promise.resolve(result.totalUsage).catch(() => undefined);
|
|
324
|
+
inspector.end(turnKey, {
|
|
325
|
+
result: { text: said, ...(usage ? { usage } : {}) },
|
|
326
|
+
summary: `${balloonLine(asked, 60)} → ${balloonLine(said, 80)}${usage?.totalTokens ? ` · ${usage.totalTokens} tokens` : ''}`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
274
329
|
setTurns(prev => [...prev, { role: 'assistant', text: said }]);
|
|
275
330
|
setEntryPersona(prev => ({
|
|
276
331
|
...prev,
|
|
277
332
|
state: 'idle',
|
|
278
333
|
saying: balloonLine(said),
|
|
334
|
+
full: said,
|
|
279
335
|
}));
|
|
280
336
|
}
|
|
281
337
|
catch (error) {
|
|
282
338
|
const message = error instanceof Error ? error.message : String(error);
|
|
339
|
+
inspector?.end(turnKey, { status: 'failed', error: message });
|
|
283
340
|
setTurns(prev => [
|
|
284
341
|
...prev,
|
|
285
342
|
{ role: 'assistant', text: `Something went wrong: ${message}` },
|
|
@@ -288,6 +345,7 @@ export function useA2ATeam(options) {
|
|
|
288
345
|
...prev,
|
|
289
346
|
state: 'idle',
|
|
290
347
|
saying: balloonLine(`Something went wrong: ${message}`),
|
|
348
|
+
full: `Something went wrong: ${message}`,
|
|
291
349
|
insist: true,
|
|
292
350
|
tool: undefined,
|
|
293
351
|
}));
|
|
@@ -303,7 +361,7 @@ export function useA2ATeam(options) {
|
|
|
303
361
|
? prev
|
|
304
362
|
: { ...prev, state: 'idle', tool: undefined });
|
|
305
363
|
}
|
|
306
|
-
}, [agent, busy, askTool, peerApp.name]);
|
|
364
|
+
}, [agent, busy, askTool, peerApp.name, entry, inspector]);
|
|
307
365
|
const stop = useCallback(() => abort.current?.abort(), []);
|
|
308
366
|
useEffect(() => () => {
|
|
309
367
|
abort.current?.abort();
|
|
@@ -312,13 +370,19 @@ export function useA2ATeam(options) {
|
|
|
312
370
|
}, []);
|
|
313
371
|
const setEntryAway = useCallback((away) => setEntryPersona(prev => ({ ...prev, away })), []);
|
|
314
372
|
const setPeerAway = useCallback((away) => setPeerPersona(prev => ({ ...prev, away })), []);
|
|
373
|
+
const entryHistory = useMemo(() => turns.map((turn, index) => ({
|
|
374
|
+
id: `turn-${index}`,
|
|
375
|
+
role: turn.role,
|
|
376
|
+
text: turn.text,
|
|
377
|
+
})), [turns]);
|
|
315
378
|
return {
|
|
379
|
+
entryHistory,
|
|
380
|
+
peerHistory,
|
|
316
381
|
entryPersona,
|
|
317
382
|
peerPersona,
|
|
318
383
|
setEntryAway,
|
|
319
384
|
setPeerAway,
|
|
320
385
|
turns,
|
|
321
|
-
exchange,
|
|
322
386
|
report,
|
|
323
387
|
artifacts,
|
|
324
388
|
notebook,
|
|
@@ -19,13 +19,27 @@
|
|
|
19
19
|
* list_invoices…" while Accounting calls Odoo. Then the report appears. The
|
|
20
20
|
* page's own state is `useA2ATeam`'s, which the landing's home page runs too.
|
|
21
21
|
*
|
|
22
|
+
* Under the graph, each member has its own Agent Inspector: Sales' — its
|
|
23
|
+
* model turns, its `ask_accounting` call, the A2A messages it sends and
|
|
24
|
+
* receives — and Accounting's — what it received over A2A, its Odoo MCP
|
|
25
|
+
* calls, `write_notebook`, its status updates and artifacts, as the A2A
|
|
26
|
+
* stream tells them. Both read one record (`useAgentInspector`), fed by the
|
|
27
|
+
* team (`useA2ATeam`'s `inspector`) and by the fetch the A2A client sends
|
|
28
|
+
* with (`inspectA2AFetch`); an A2A message shows in both, sent in one and
|
|
29
|
+
* received in the other. Right-click a member for its menu: *Inspect the
|
|
30
|
+
* agent…* opens its inspector over the page.
|
|
31
|
+
*
|
|
32
|
+
* Sales' suggestions — its Appspec's starters — are chips in its balloon
|
|
33
|
+
* while it waits for a question: one clicked is asked, as if typed.
|
|
34
|
+
*
|
|
22
35
|
* Sales accepts a Jupyter notebook from Accounting besides its words
|
|
23
36
|
* (`acceptedOutputModes`, `NOTEBOOK_AND_WORDS`). When Accounting gives one,
|
|
24
37
|
* Sales' balloon shows it read-only (`NotebookPreview`: no kernel), and it
|
|
25
38
|
* opens right under the team, in place of the report, on the browser
|
|
26
39
|
* sandbox — a Pyodide kernel in this page — to run, change and download
|
|
27
|
-
* (`TeamNotebook`, loaded only then)
|
|
28
|
-
*
|
|
40
|
+
* (`TeamNotebook`, loaded only then): the graph draws it into the area
|
|
41
|
+
* under it (`expandTarget`), and a click on the one in the balloon, or its
|
|
42
|
+
* *Expand*, comes to it.
|
|
29
43
|
*
|
|
30
44
|
* Where Accounting is: `VITE_A2A_ACCOUNTING_URL`, else the examples' local
|
|
31
45
|
* server (`…:8765/api/v1/a2a/agents/accounting`), which answers this machine
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Button, Flash, Heading, Text, Textarea } from '@primer/react';
|
|
4
4
|
import { Box } from '@datalayer/primer-addons';
|
|
@@ -10,8 +10,9 @@ import { useExampleAgentRuntimesUrl } from './utils/useExampleAgentRuntimesUrl';
|
|
|
10
10
|
import { useBrowserInference } from '../hooks/useBrowserInference';
|
|
11
11
|
import { readJwtClaims, tokenLifetimeMs, useAnonymousSessionStore, } from '../runtimes/browser/anonymousToken';
|
|
12
12
|
import { connectA2APeer } from '../runtimes/browser/a2aPeer';
|
|
13
|
-
import { A2ATeamGraph, NOTEBOOK_AND_WORDS,
|
|
13
|
+
import { A2ATeamGraph, NOTEBOOK_AND_WORDS, teamConnectionsOf, teamToolClassifier, useA2ATeam, } from '../components/teams';
|
|
14
14
|
import { ACCOUNTING_APP_0_0_1, SALES_APP_0_0_1 } from '../specs/apps';
|
|
15
|
+
import { AgentInspector, classifyToolCall, inspectA2AFetch, useAgentInspector, } from '../components/inspector';
|
|
15
16
|
import { SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1 } from '../specs/teams/teams';
|
|
16
17
|
const TEAM = SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1;
|
|
17
18
|
const SALES = SALES_APP_0_0_1;
|
|
@@ -45,22 +46,36 @@ function AgentA2ATeam() {
|
|
|
45
46
|
}, [accountingKey]);
|
|
46
47
|
const [accountingKeyEnded, setAccountingKeyEnded] = useState(() => accountingKeyClock !== undefined &&
|
|
47
48
|
accountingKeyClock.expiresAt <= Date.now());
|
|
49
|
+
// One record of what both members do, each inspector showing its own.
|
|
50
|
+
const { sink: inspector } = useAgentInspector();
|
|
48
51
|
const [peer, setPeer] = useState(null);
|
|
49
52
|
const [peerError, setPeerError] = useState(null);
|
|
50
53
|
const [draft, setDraft] = useState('');
|
|
51
54
|
const composer = useRef(null);
|
|
55
|
+
// Under the graph: where the notebook Accounting gives runs.
|
|
56
|
+
const notebookArea = useRef(null);
|
|
52
57
|
useEffect(() => {
|
|
53
58
|
let current = true;
|
|
54
59
|
setPeer(null);
|
|
55
60
|
setPeerError(null);
|
|
56
|
-
connectA2APeer({
|
|
61
|
+
connectA2APeer({
|
|
62
|
+
url: accountingUrl,
|
|
63
|
+
key: accountingKey,
|
|
64
|
+
// The A2A traffic, recorded: Sales sends, Accounting answers.
|
|
65
|
+
fetch: inspectA2AFetch(globalThis.fetch.bind(globalThis), {
|
|
66
|
+
sink: inspector,
|
|
67
|
+
asker: SALES.name,
|
|
68
|
+
peer: ACCOUNTING.name,
|
|
69
|
+
classifyTool: teamToolClassifier(ACCOUNTING_CONNECTIONS, classifyToolCall),
|
|
70
|
+
}),
|
|
71
|
+
})
|
|
57
72
|
.then(found => current && setPeer(found))
|
|
58
73
|
.catch(error => current &&
|
|
59
74
|
setPeerError(error instanceof Error ? error.message : String(error)));
|
|
60
75
|
return () => {
|
|
61
76
|
current = false;
|
|
62
77
|
};
|
|
63
|
-
}, [accountingUrl, accountingKey]);
|
|
78
|
+
}, [accountingUrl, accountingKey, inspector]);
|
|
64
79
|
// Sales runs here and asks Accounting over A2A: the conversation, the
|
|
65
80
|
// exchange, the report and the way the link carries a message.
|
|
66
81
|
const team = useA2ATeam({
|
|
@@ -72,7 +87,13 @@ function AgentA2ATeam() {
|
|
|
72
87
|
peerConnections: ACCOUNTING_CONNECTIONS,
|
|
73
88
|
// Its words, and a notebook to run here when it gives one.
|
|
74
89
|
accept: NOTEBOOK_AND_WORDS,
|
|
90
|
+
inspector,
|
|
75
91
|
});
|
|
92
|
+
// Sales' starters, in its balloon while it waits for a question.
|
|
93
|
+
const suggestions = useMemo(() => (SALES.interface.starters ?? []).map(starter => ({
|
|
94
|
+
label: starter.label,
|
|
95
|
+
prompt: starter.message,
|
|
96
|
+
})), []);
|
|
76
97
|
const send = (text) => {
|
|
77
98
|
setDraft('');
|
|
78
99
|
void team.send(text);
|
|
@@ -80,7 +101,7 @@ function AgentA2ATeam() {
|
|
|
80
101
|
const accountingWhere = accountingKey
|
|
81
102
|
? `on a runtime, ${new URL(accountingUrl).host}, with a temporary key`
|
|
82
103
|
: `on a runtime, ${new URL(accountingUrl).host}`;
|
|
83
|
-
return (_jsxs(Box, { sx: { p: 4, maxWidth: 1080, mx: 'auto' }, children: [
|
|
104
|
+
return (_jsxs(Box, { sx: { p: 4, maxWidth: 1080, mx: 'auto' }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 4, mb: 1 }, children: "Agents A2A Team" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mt: 0 }, children: [TEAM.emoji, " ", _jsx("strong", { children: TEAM.name }), ": ", TEAM.description] }), ((anonymous.status === 'active' && anonymous.expiresAt) ||
|
|
84
105
|
(accountingKeyClock && !accountingKeyEnded)) && (_jsxs(Box, { sx: { display: 'flex', gap: 4, flexWrap: 'wrap', mb: 3 }, children: [anonymous.status === 'active' && anonymous.expiresAt && (_jsx(AnonymousKeyTimer, { expiresAt: anonymous.expiresAt, grantedMs: anonymous.grantedMs, label: `${SALES.name}'s trial key`, onExpire: () => useAnonymousSessionStore.getState().expire() })), accountingKeyClock && !accountingKeyEnded && (_jsx(AnonymousKeyTimer, { expiresAt: accountingKeyClock.expiresAt, grantedMs: accountingKeyClock.grantedMs, label: `${ACCOUNTING.name}'s temporary key`, onExpire: () => setAccountingKeyEnded(true) }))] })), anonymous.status === 'expired' && (_jsx(Box, { sx: { mb: 3 }, children: _jsx(AnonymousKeyExpired, { agentName: SALES.name,
|
|
85
106
|
// Sales runs in this page: what ended is its model's key, and
|
|
86
107
|
// signing in gives it the person's own.
|
|
@@ -102,7 +123,21 @@ function AgentA2ATeam() {
|
|
|
102
123
|
: 'on a runtime',
|
|
103
124
|
persona: team.entryPersona,
|
|
104
125
|
onToggle: () => composer.current?.focus(),
|
|
126
|
+
conversationLabel: `Ask ${SALES.name}`,
|
|
105
127
|
onAway: team.setEntryAway,
|
|
128
|
+
history: team.entryHistory,
|
|
129
|
+
expandTarget: notebookArea,
|
|
130
|
+
notebookTitle: `${ACCOUNTING.name}\u2019s notebook`,
|
|
131
|
+
suggestions: team.ready && !team.busy ? suggestions : undefined,
|
|
132
|
+
onSuggestion: suggestion => send(suggestion.prompt),
|
|
133
|
+
inspector,
|
|
134
|
+
onStop: team.busy ? team.stop : undefined,
|
|
135
|
+
about: {
|
|
136
|
+
name: SALES.name,
|
|
137
|
+
spec: `${SALES.id}:${SALES.version}`,
|
|
138
|
+
model: SALES.model || undefined,
|
|
139
|
+
where: 'in your browser',
|
|
140
|
+
},
|
|
106
141
|
}, peer: {
|
|
107
142
|
id: ACCOUNTING.id,
|
|
108
143
|
name: ACCOUNTING.name,
|
|
@@ -111,8 +146,25 @@ function AgentA2ATeam() {
|
|
|
111
146
|
where: accountingWhere,
|
|
112
147
|
persona: team.peerPersona,
|
|
113
148
|
onAway: team.setPeerAway,
|
|
149
|
+
history: team.peerHistory,
|
|
114
150
|
connections: ACCOUNTING_CONNECTIONS,
|
|
115
|
-
|
|
151
|
+
inspector,
|
|
152
|
+
about: {
|
|
153
|
+
name: ACCOUNTING.name,
|
|
154
|
+
spec: `${ACCOUNTING.id}:${ACCOUNTING.version}`,
|
|
155
|
+
model: ACCOUNTING.model || undefined,
|
|
156
|
+
where: accountingWhere,
|
|
157
|
+
},
|
|
158
|
+
}, flow: team.flow, calls: team.calls,
|
|
159
|
+
// Room for the balloons: suggestions, *more* and a notebook.
|
|
160
|
+
balloonRoom: 320, connected: peer !== null, label: peer ? `A2A · ${peer.skill.name}` : 'A2A · not connected' }) }), _jsx(Box, { ref: notebookArea, sx: { mb: team.notebook ? 3 : 0 }, "data-team-notebook-placement": "under-graph" }), _jsx(Box, { sx: {
|
|
161
|
+
display: 'grid',
|
|
162
|
+
gridTemplateColumns: ['1fr', '1fr', '1fr 1fr'],
|
|
163
|
+
gap: 3,
|
|
164
|
+
mb: 3,
|
|
165
|
+
}, "data-team-inspectors": "", children: [SALES, ACCOUNTING].map(member => (_jsxs(Box, { sx: { minWidth: 0 }, "data-team-inspector": member.id, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: [member.emoji, " ", member.name, "\u2019s Agent Inspector"] }), _jsx(AgentInspector, { sink: inspector, agent: member.name, maxHeight: 360, exportName: `agent-inspector-${member.id}`, emptyText: member === SALES
|
|
166
|
+
? 'Its model turns, its call to Accounting and the A2A messages it sends and receives show here.'
|
|
167
|
+
: 'What it receives over A2A, its Odoo MCP calls and what it sends back show here.' })] }, member.id))) }), _jsxs(Box, { sx: { display: 'flex', gap: 3, flexWrap: 'wrap' }, children: [_jsxs(Box, { sx: { flex: '1 1 420px', minWidth: 0 }, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: ["Ask ", SALES.name] }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, mt: 0 }, children: ["Or choose one of ", SALES.name, "\u2019s suggestions in its balloon."] }), _jsx(Textarea, { ref: composer, block: true, rows: 3, value: draft, placeholder: "Ask for a financial report\u2026", onChange: event => setDraft(event.target.value), onKeyDown: event => {
|
|
116
168
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
117
169
|
event.preventDefault();
|
|
118
170
|
send(draft);
|
|
@@ -122,13 +174,13 @@ function AgentA2ATeam() {
|
|
|
122
174
|
mb: 2,
|
|
123
175
|
borderRadius: 2,
|
|
124
176
|
bg: turn.role === 'user' ? 'accent.subtle' : 'canvas.subtle',
|
|
125
|
-
}, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', display: 'block' }, children: turn.role === 'user' ? 'You' : SALES.name }), _jsx(Streamdown, { children: turn.text })] }, index))) })] }),
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
177
|
+
}, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', display: 'block' }, children: turn.role === 'user' ? 'You' : SALES.name }), _jsx(Streamdown, { children: turn.text })] }, index))) })] }), _jsx(Box, { sx: { flex: '1 1 360px', minWidth: 0 }, children: !team.notebook && (_jsxs(_Fragment, { children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: "The report" }), _jsx(Box, { sx: {
|
|
178
|
+
p: 3,
|
|
179
|
+
border: '1px solid',
|
|
180
|
+
borderColor: 'border.default',
|
|
181
|
+
borderRadius: 2,
|
|
182
|
+
minHeight: 120,
|
|
183
|
+
}, "data-team-report": "", children: team.report ? (_jsx(Streamdown, { children: team.report })) : (_jsx(Text, { sx: { color: 'fg.muted' }, children: "The report Accounting returns appears here, as it returned it, or as a notebook to run when it gives one." })) })] })) })] })] }));
|
|
132
184
|
}
|
|
133
185
|
export function AgentA2ATeamExample() {
|
|
134
186
|
return (_jsx(ThemedProvider, { children: _jsx(AgentA2ATeam, {}) }));
|
|
@@ -10,5 +10,5 @@
|
|
|
10
10
|
* and so are the characters clippy.js publishes, fetched when picked.
|
|
11
11
|
*/
|
|
12
12
|
import React from 'react';
|
|
13
|
-
declare const
|
|
14
|
-
export default
|
|
13
|
+
declare const AssistantExample: React.FC;
|
|
14
|
+
export default AssistantExample;
|