@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.
Files changed (72) hide show
  1. package/lib/chat/ChatFloating.d.ts +25 -2
  2. package/lib/chat/ChatFloating.js +80 -12
  3. package/lib/chat/assistant/AssistantContextMenu.d.ts +67 -0
  4. package/lib/chat/assistant/AssistantContextMenu.js +68 -0
  5. package/lib/chat/assistant/AssistantStage.d.ts +123 -5
  6. package/lib/chat/assistant/AssistantStage.js +245 -7
  7. package/lib/chat/assistant/BalloonParts.d.ts +15 -1
  8. package/lib/chat/assistant/BalloonParts.js +26 -9
  9. package/lib/chat/assistant/BalloonVisual.d.ts +65 -0
  10. package/lib/chat/assistant/BalloonVisual.js +58 -0
  11. package/lib/chat/assistant/SpeechBalloon.d.ts +68 -2
  12. package/lib/chat/assistant/SpeechBalloon.js +113 -7
  13. package/lib/chat/assistant/state.d.ts +19 -0
  14. package/lib/chat/assistant/state.js +44 -17
  15. package/lib/chat/base/ChatBase.js +51 -1
  16. package/lib/chat/indicators/TypingDots.d.ts +13 -0
  17. package/lib/chat/indicators/TypingDots.js +21 -0
  18. package/lib/chat/messages/ChatMessageList.js +1 -15
  19. package/lib/components/inspector/AgentInspector.d.ts +59 -0
  20. package/lib/components/inspector/AgentInspector.js +220 -0
  21. package/lib/components/inspector/AgentInspectorDialog.d.ts +19 -0
  22. package/lib/components/inspector/AgentInspectorDialog.js +9 -0
  23. package/lib/components/inspector/JsonTree.d.ts +24 -0
  24. package/lib/components/inspector/JsonTree.js +89 -0
  25. package/lib/components/inspector/a2aInspect.d.ts +56 -0
  26. package/lib/components/inspector/a2aInspect.js +478 -0
  27. package/lib/components/inspector/agentInspector.d.ts +179 -0
  28. package/lib/components/inspector/agentInspector.js +267 -0
  29. package/lib/components/inspector/chatInspect.d.ts +38 -0
  30. package/lib/components/inspector/chatInspect.js +65 -0
  31. package/lib/components/inspector/classify.d.ts +19 -0
  32. package/lib/components/inspector/classify.js +42 -0
  33. package/lib/components/inspector/index.d.ts +14 -0
  34. package/lib/components/inspector/index.js +18 -0
  35. package/lib/components/teams/A2ATeamGraph.d.ts +54 -3
  36. package/lib/components/teams/A2ATeamGraph.js +37 -11
  37. package/lib/components/teams/NotebookPreview.d.ts +11 -2
  38. package/lib/components/teams/NotebookPreview.js +6 -3
  39. package/lib/components/teams/NotebookPreviewView.js +9 -1
  40. package/lib/components/teams/TeamNotebook.d.ts +10 -1
  41. package/lib/components/teams/TeamNotebook.js +15 -3
  42. package/lib/components/teams/jupyterThemeVariables.d.ts +9 -0
  43. package/lib/components/teams/jupyterThemeVariables.js +441 -0
  44. package/lib/components/teams/teamConnections.d.ts +9 -1
  45. package/lib/components/teams/teamConnections.js +20 -0
  46. package/lib/components/teams/useA2ATeam.d.ts +17 -4
  47. package/lib/components/teams/useA2ATeam.js +87 -23
  48. package/lib/examples/AgentA2ATeamExample.d.ts +16 -2
  49. package/lib/examples/AgentA2ATeamExample.js +65 -13
  50. package/lib/examples/{ChatAssistantExample.d.ts → AssistantExample.d.ts} +2 -2
  51. package/lib/examples/{ChatAssistantExample.js → AssistantExample.js} +69 -7
  52. package/lib/examples/{ChatAssistantGalleryExample.d.ts → AssistantGalleryExample.d.ts} +2 -2
  53. package/lib/examples/{ChatAssistantGalleryExample.js → AssistantGalleryExample.js} +45 -13
  54. package/lib/examples/example-selector.js +4 -2
  55. package/lib/examples/exampleGroups.d.ts +9 -0
  56. package/lib/examples/exampleGroups.js +56 -0
  57. package/lib/examples/index.d.ts +1 -1
  58. package/lib/examples/index.js +1 -1
  59. package/lib/examples/main.js +3 -43
  60. package/lib/examples/utils/AssistantGalleryGrid.d.ts +6 -1
  61. package/lib/examples/utils/AssistantGalleryGrid.js +16 -3
  62. package/lib/examples/utils/assistantGallery.d.ts +51 -2
  63. package/lib/examples/utils/assistantGallery.js +32 -7
  64. package/lib/loop/apps/AppRenderer.js +1 -1
  65. package/lib/loop/core/index.d.ts +17 -0
  66. package/lib/loop/core/index.js +1 -0
  67. package/lib/loop/plugins/assistant/index.d.ts +1 -1
  68. package/lib/loop/plugins/assistant/index.js +8 -3
  69. package/lib/runtimes/browser/a2aPeer.d.ts +4 -1
  70. package/lib/types/chat.d.ts +7 -0
  71. package/package.json +1 -1
  72. 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 an exchange line calls what the peer gave: its words, and each of its artifacts. */
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 exchange between the two, a line a step. */
65
- exchange: string[];
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 — and the exchange
15
- * between them as a log, the peer's last answer as the report, and the way
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 an exchange line calls what the peer gave: its words, and each of its artifacts. */
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
- setExchange(prev => [
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.note) {
125
- setExchange(prev => [...prev, `${peerApp.name}: ${event.note}`]);
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
- setExchange(prev => [
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
- setExchange(prev => [
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
- }, [entry.name, peerApp.id, peerApp.name, peerConnections]);
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); a click on the one in the balloon
28
- * comes to it.
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 { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
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, TeamNotebook, teamConnectionsOf, useA2ATeam, } from '../components/teams';
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({ url: accountingUrl, key: accountingKey })
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: [_jsxs(Heading, { as: "h2", sx: { fontSize: 4, mb: 1 }, children: [TEAM.emoji, " ", TEAM.name] }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', mt: 0 }, children: TEAM.description }), ((anonymous.status === 'active' && anonymous.expiresAt) ||
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
- }, flow: team.flow, calls: team.calls, connected: peer !== null, label: peer ? `A2A · ${peer.skill.name}` : 'A2A · not connected' }) }), team.notebook && (_jsx(Box, { sx: { mb: 3 }, "data-team-notebook-placement": "under-graph", children: _jsx(TeamNotebook, { notebook: team.notebook, title: `${ACCOUNTING.name}\u2019s notebook` }) })), _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] }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap', mb: 2 }, children: (SALES.interface.starters ?? []).map(starter => (_jsx(Button, { size: "small", disabled: !team.ready || team.busy, onClick: () => send(starter.message), children: starter.label }, starter.label))) }), _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 => {
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))) })] }), _jsxs(Box, { sx: { flex: '1 1 360px', minWidth: 0 }, children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: "The exchange" }), _jsx(Box, { as: "ol", sx: { pl: 3, color: 'fg.muted', fontSize: 1 }, "data-team-exchange": "", children: team.exchange.length === 0 ? (_jsx(Text, { as: "li", children: "Nothing asked yet." })) : (team.exchange.map((step, index) => (_jsx(Text, { as: "li", children: step }, index)))) }), !team.notebook && (_jsxs(_Fragment, { children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mt: 3, mb: 2 }, children: "The report" }), _jsx(Box, { sx: {
126
- p: 3,
127
- border: '1px solid',
128
- borderColor: 'border.default',
129
- borderRadius: 2,
130
- minHeight: 120,
131
- }, "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." })) })] }))] })] })] }));
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 ChatAssistantExample: React.FC;
14
- export default ChatAssistantExample;
13
+ declare const AssistantExample: React.FC;
14
+ export default AssistantExample;