@datalayer/agent-runtimes 1.3.74 → 1.3.78

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 (143) hide show
  1. package/lib/agents/AgentDetails.d.ts +16 -2
  2. package/lib/agents/AgentDetails.js +381 -272
  3. package/lib/chat/ChatFloating.d.ts +27 -1
  4. package/lib/chat/ChatFloating.js +97 -13
  5. package/lib/chat/assistant/AgentDetailsDialog.d.ts +21 -0
  6. package/lib/chat/assistant/AgentDetailsDialog.js +19 -0
  7. package/lib/chat/assistant/AssistantContextMenu.d.ts +67 -0
  8. package/lib/chat/assistant/AssistantContextMenu.js +68 -0
  9. package/lib/chat/assistant/AssistantStage.d.ts +144 -4
  10. package/lib/chat/assistant/AssistantStage.js +280 -10
  11. package/lib/chat/assistant/BalloonParts.d.ts +33 -5
  12. package/lib/chat/assistant/BalloonParts.js +66 -44
  13. package/lib/chat/assistant/BalloonVisual.d.ts +10 -2
  14. package/lib/chat/assistant/BalloonVisual.js +4 -3
  15. package/lib/chat/assistant/SandboxDetailsDialog.d.ts +44 -0
  16. package/lib/chat/assistant/SandboxDetailsDialog.js +214 -0
  17. package/lib/chat/assistant/SpeechBalloon.d.ts +54 -6
  18. package/lib/chat/assistant/SpeechBalloon.js +73 -22
  19. package/lib/chat/assistant/assistantDetails.d.ts +84 -0
  20. package/lib/chat/assistant/assistantDetails.js +140 -0
  21. package/lib/chat/assistant/formats/files.d.ts +23 -0
  22. package/lib/chat/assistant/formats/files.js +48 -0
  23. package/lib/chat/assistant/formats/index.d.ts +2 -0
  24. package/lib/chat/assistant/formats/index.js +1 -0
  25. package/lib/chat/assistant/state.d.ts +7 -14
  26. package/lib/chat/assistant/state.js +21 -20
  27. package/lib/chat/assistant/toolLine.d.ts +16 -0
  28. package/lib/chat/assistant/toolLine.js +46 -0
  29. package/lib/chat/base/ChatBase.js +71 -1
  30. package/lib/chat/base/loopMessage.d.ts +37 -0
  31. package/lib/chat/base/loopMessage.js +37 -0
  32. package/lib/chat/indicators/TypingDots.d.ts +13 -0
  33. package/lib/chat/indicators/TypingDots.js +21 -0
  34. package/lib/chat/messages/ChatMarkdown.d.ts +16 -0
  35. package/lib/chat/messages/ChatMarkdown.js +18 -0
  36. package/lib/chat/messages/ChatMessageList.d.ts +8 -1
  37. package/lib/chat/messages/ChatMessageList.js +25 -33
  38. package/lib/chat/styles/streamdownStyles.js +3 -1
  39. package/lib/chat/tools/ToolCallDisplay.d.ts +8 -1
  40. package/lib/chat/tools/ToolCallDisplay.js +8 -7
  41. package/lib/components/code-sandboxes/CodeSandboxCellVariablesDialog.js +5 -11
  42. package/lib/components/code-sandboxes/CodeSandboxVariables.d.ts +8 -0
  43. package/lib/components/code-sandboxes/CodeSandboxVariables.js +17 -11
  44. package/lib/components/inspector/AgentInspector.d.ts +59 -0
  45. package/lib/components/inspector/AgentInspector.js +220 -0
  46. package/lib/components/inspector/AgentInspectorDialog.d.ts +19 -0
  47. package/lib/components/inspector/AgentInspectorDialog.js +9 -0
  48. package/lib/components/inspector/JsonTree.d.ts +24 -0
  49. package/lib/components/inspector/JsonTree.js +89 -0
  50. package/lib/components/inspector/a2aInspect.d.ts +56 -0
  51. package/lib/components/inspector/a2aInspect.js +478 -0
  52. package/lib/components/inspector/agentInspector.d.ts +179 -0
  53. package/lib/components/inspector/agentInspector.js +267 -0
  54. package/lib/components/inspector/chatInspect.d.ts +38 -0
  55. package/lib/components/inspector/chatInspect.js +65 -0
  56. package/lib/components/inspector/classify.d.ts +19 -0
  57. package/lib/components/inspector/classify.js +42 -0
  58. package/lib/components/inspector/index.d.ts +14 -0
  59. package/lib/components/inspector/index.js +18 -0
  60. package/lib/components/teams/A2ATeamGraph.d.ts +69 -1
  61. package/lib/components/teams/A2ATeamGraph.js +277 -37
  62. package/lib/components/teams/NotebookPreviewView.js +9 -1
  63. package/lib/components/teams/TeamNotebook.d.ts +3 -1
  64. package/lib/components/teams/TeamNotebook.js +3 -3
  65. package/lib/components/teams/TeamNotebookView.js +15 -0
  66. package/lib/components/teams/jupyterThemeVariables.d.ts +9 -0
  67. package/lib/components/teams/jupyterThemeVariables.js +441 -0
  68. package/lib/components/teams/teamConnections.d.ts +9 -1
  69. package/lib/components/teams/teamConnections.js +20 -0
  70. package/lib/components/teams/teamNotebookKernel.d.ts +3 -0
  71. package/lib/components/teams/teamNotebookKernel.js +17 -0
  72. package/lib/components/teams/useA2ATeam.d.ts +20 -3
  73. package/lib/components/teams/useA2ATeam.js +108 -23
  74. package/lib/examples/AgentA2ATeamExample.d.ts +13 -0
  75. package/lib/examples/AgentA2ATeamExample.js +67 -13
  76. package/lib/examples/AssistantExample.js +65 -26
  77. package/lib/examples/AssistantGalleryExample.js +2 -1
  78. package/lib/examples/exampleGroups.d.ts +1 -1
  79. package/lib/examples/exampleGroups.js +1 -1
  80. package/lib/examples/utils/assistantGallery.d.ts +7 -6
  81. package/lib/examples/utils/assistantGallery.js +23 -8
  82. package/lib/jupyter/exec/Python.d.ts +0 -1
  83. package/lib/jupyter/exec/Python.js +0 -32
  84. package/lib/jupyter/exec/Snippets.d.ts +4 -12
  85. package/lib/jupyter/exec/Snippets.js +0 -8
  86. package/lib/loop/apps/AppFeedback.js +3 -1
  87. package/lib/loop/apps/AppKept.d.ts +22 -0
  88. package/lib/loop/apps/AppKept.js +36 -0
  89. package/lib/loop/apps/AppRenderer.d.ts +11 -1
  90. package/lib/loop/apps/AppRenderer.js +22 -4
  91. package/lib/loop/apps/checks.d.ts +7 -0
  92. package/lib/loop/apps/checks.js +39 -0
  93. package/lib/loop/apps/index.d.ts +3 -0
  94. package/lib/loop/apps/index.js +3 -0
  95. package/lib/loop/apps/kept.d.ts +30 -0
  96. package/lib/loop/apps/kept.js +52 -0
  97. package/lib/loop/apps/records.d.ts +12 -4
  98. package/lib/loop/apps/records.js +13 -5
  99. package/lib/loop/apps/saved.d.ts +30 -0
  100. package/lib/loop/apps/saved.js +40 -0
  101. package/lib/loop/core/index.d.ts +17 -0
  102. package/lib/loop/core/index.js +1 -0
  103. package/lib/loop/embed/AppEmbed.d.ts +8 -1
  104. package/lib/loop/embed/AppEmbed.js +4 -4
  105. package/lib/loop/embed/LoopEmbed.d.ts +6 -1
  106. package/lib/loop/embed/LoopEmbed.js +4 -2
  107. package/lib/loop/embed/element.js +1 -1
  108. package/lib/loop/plugins/agents/DatalayerAgentBridge.js +12 -6
  109. package/lib/loop/plugins/agents/browserService.d.ts +7 -1
  110. package/lib/loop/plugins/agents/browserService.js +4 -0
  111. package/lib/loop/plugins/agents/plugin.d.ts +16 -0
  112. package/lib/loop/plugins/agents/switchable.d.ts +3 -0
  113. package/lib/loop/plugins/agents/switchable.js +1 -0
  114. package/lib/loop/plugins/app-activity/index.js +3 -0
  115. package/lib/loop/plugins/app-computer/index.js +3 -0
  116. package/lib/loop/plugins/app-rules/AppRulesCard.d.ts +5 -0
  117. package/lib/loop/plugins/app-rules/AppRulesCard.js +25 -6
  118. package/lib/loop/plugins/app-rules/index.js +3 -0
  119. package/lib/loop/plugins/assistant/index.d.ts +7 -2
  120. package/lib/loop/plugins/assistant/index.js +58 -4
  121. package/lib/loop/plugins/assistant-characters/index.d.ts +7 -0
  122. package/lib/loop/plugins/assistant-characters/index.js +17 -0
  123. package/lib/loop/plugins/chat/ChatView.js +4 -1
  124. package/lib/loop/plugins/page-layout/SplitLayout.d.ts +2 -1
  125. package/lib/loop/plugins/page-layout/SplitLayout.js +4 -0
  126. package/lib/loop/presets.d.ts +4 -0
  127. package/lib/loop/presets.js +3 -1
  128. package/lib/loop/shell/LoopWorkspace.d.ts +7 -0
  129. package/lib/loop/shell/LoopWorkspace.js +5 -4
  130. package/lib/loop/shell/SidebarRail.d.ts +53 -0
  131. package/lib/loop/shell/SidebarRail.js +74 -0
  132. package/lib/loop/shell/index.d.ts +2 -0
  133. package/lib/loop/shell/index.js +2 -0
  134. package/lib/loop/shell/paneMotion.d.ts +21 -0
  135. package/lib/loop/shell/paneMotion.js +37 -0
  136. package/lib/runtimes/browser/a2aPeer.d.ts +4 -1
  137. package/lib/specs/apps.js +8 -5
  138. package/lib/specs/uiPlugins.js +529 -0
  139. package/lib/types/agentspecs.d.ts +5 -3
  140. package/lib/types/chat.d.ts +12 -0
  141. package/package.json +2 -2
  142. package/scripts/codegen/generate_jupyter_theme_variables.mjs +57 -0
  143. package/scripts/codegen/generate_ui_plugins.py +112 -2
@@ -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,56 @@ 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 message = {
89
+ id: `peer-${told.current}`,
90
+ role,
91
+ content: text,
92
+ createdAt: new Date(),
93
+ };
94
+ setPeerHistory(prev => [...prev, message]);
95
+ }, []);
96
+ // A tool the peer calls, as the chat holds one: added when it starts,
97
+ // updated when it ends.
98
+ const toolCalled = useCallback((step) => {
99
+ const id = step.id ?? step.name;
100
+ const call = {
101
+ id: `peer-tool:${id}`,
102
+ type: 'tool-call',
103
+ toolCallId: id,
104
+ toolName: toolOwnName(step.name, peerConnections),
105
+ args: {},
106
+ status: step.error ? 'error' : step.ended ? 'complete' : 'executing',
107
+ ...(step.error ? { error: step.error } : {}),
108
+ };
109
+ setPeerHistory(prev => prev.some(item => item.id === call.id)
110
+ ? prev.map(item => (item.id === call.id ? call : item))
111
+ : [...prev, call]);
112
+ }, [peerConnections]);
78
113
  const [report, setReport] = useState(null);
79
114
  const [artifacts, setArtifacts] = useState([]);
80
115
  const [notebook, setNotebook] = useState(null);
81
- const [exchange, setExchange] = useState([]);
82
116
  const [flow, setFlow] = useState('still');
83
117
  const [calls, setCalls] = useState([]);
84
118
  const callsNow = useRef([]);
85
119
  const callTimer = useRef(undefined);
86
120
  const [busy, setBusy] = useState(false);
87
121
  const history = useRef([]);
122
+ const turnCount = useRef(0);
88
123
  const abort = useRef(null);
89
124
  const flowTimer = useRef(undefined);
90
125
  // What the peer does, as it does it: its own state and balloon, and the link.
91
126
  const onPeerEvent = useCallback((event) => {
127
+ // Linked to the A2A entry it came from (the capture's linker).
128
+ inspector?.note(event);
92
129
  const next = flowAfter(event);
93
130
  clearTimeout(flowTimer.current);
94
131
  setFlow(next.flow);
@@ -108,26 +145,25 @@ export function useA2ATeam(options) {
108
145
  }, after.holdMs);
109
146
  }
110
147
  if (event.phase === 'asked') {
111
- setExchange(prev => [
112
- ...prev,
113
- `${entry.name} → ${peerApp.name}: ${event.request}`,
114
- ]);
148
+ tell('user', event.request);
115
149
  setPeerPersona(prev => ({
116
150
  ...prev,
117
151
  state: 'greeting',
118
152
  saying: 'On it. Let me read the books.',
153
+ full: undefined,
119
154
  insist: true,
120
155
  tool: undefined,
121
156
  }));
122
157
  }
123
158
  else if (event.phase === 'working') {
124
- if (event.note) {
125
- setExchange(prev => [...prev, `${peerApp.name}: ${event.note}`]);
159
+ if (event.tool) {
160
+ toolCalled(event.tool);
126
161
  }
127
162
  setPeerPersona(prev => ({
128
163
  ...prev,
129
164
  state: 'working',
130
165
  saying: event.note ? balloonLine(event.note) : prev.saying,
166
+ full: event.note ?? prev.full,
131
167
  insist: Boolean(event.note) || Boolean(event.tool) || prev.insist,
132
168
  // The tool it calls, in plain words, from the step it told; a
133
169
  // step without a tool (its own words) puts the tool line away.
@@ -137,10 +173,7 @@ export function useA2ATeam(options) {
137
173
  }));
138
174
  }
139
175
  else if (event.phase === 'answered') {
140
- setExchange(prev => [
141
- ...prev,
142
- `${peerApp.name} → ${entry.name}: ${answeredLine(event.artifacts)}`,
143
- ]);
176
+ tell('assistant', event.answer);
144
177
  setReport(event.answer);
145
178
  setArtifacts(event.artifacts);
146
179
  const given = notebookAmong(event.artifacts);
@@ -151,6 +184,7 @@ export function useA2ATeam(options) {
151
184
  ...prev,
152
185
  state: 'speaking',
153
186
  saying: balloonLine(event.answer),
187
+ full: event.answer,
154
188
  insist: true,
155
189
  tool: undefined,
156
190
  }));
@@ -160,19 +194,17 @@ export function useA2ATeam(options) {
160
194
  }
161
195
  }
162
196
  else {
163
- setExchange(prev => [
164
- ...prev,
165
- `${peerApp.name} could not answer: ${event.error}`,
166
- ]);
197
+ tell('assistant', `Could not answer: ${event.error}`);
167
198
  setPeerPersona(prev => ({
168
199
  ...prev,
169
200
  state: 'idle',
170
201
  saying: balloonLine(event.error),
202
+ full: event.error,
171
203
  insist: true,
172
204
  tool: undefined,
173
205
  }));
174
206
  }
175
- }, [entry.name, peerApp.id, peerApp.name, peerConnections]);
207
+ }, [peerApp.id, peerConnections, inspector, tell, toolCalled]);
176
208
  const agent = useMemo(() => {
177
209
  if (!peer) {
178
210
  return null;
@@ -198,7 +230,6 @@ export function useA2ATeam(options) {
198
230
  setBusy(true);
199
231
  setReport(null);
200
232
  setArtifacts([]);
201
- setExchange([]);
202
233
  setTurns(prev => [...prev, { role: 'user', text: asked }]);
203
234
  setEntryPersona({
204
235
  ...AT_REST,
@@ -210,6 +241,19 @@ export function useA2ATeam(options) {
210
241
  history.current = [...history.current, { role: 'user', content: asked }];
211
242
  abort.current = new AbortController();
212
243
  let said = '';
244
+ // The turn, in the inspector: the entry's model, from the question to
245
+ // the answer, and its tokens.
246
+ turnCount.current += 1;
247
+ const turnKey = `turn:${entry.id}:${turnCount.current}`;
248
+ inspector?.start({
249
+ key: turnKey,
250
+ actor: entry.name,
251
+ source: 'model',
252
+ kind: 'turn',
253
+ name: entry.model || undefined,
254
+ summary: balloonLine(asked, 160),
255
+ payload: { prompt: asked, model: entry.model || undefined },
256
+ });
213
257
  try {
214
258
  const result = await agent.stream({
215
259
  messages: history.current,
@@ -218,10 +262,21 @@ export function useA2ATeam(options) {
218
262
  for await (const part of result.fullStream) {
219
263
  if (part.type === 'tool-call' && part.toolName === askTool) {
220
264
  const request = String(part.input?.request ?? '');
265
+ // Its tool runs here, in the page, and asks the peer over A2A.
266
+ inspector?.start({
267
+ key: `tool:${part.toolCallId}`,
268
+ actor: entry.name,
269
+ source: 'frontend-tool',
270
+ kind: 'tool-call',
271
+ name: askTool,
272
+ summary: balloonLine(request, 160),
273
+ payload: part.input,
274
+ });
221
275
  setEntryPersona(prev => ({
222
276
  ...prev,
223
277
  state: 'waiting',
224
278
  saying: balloonLine(`${peerApp.name}, could you: ${request}`),
279
+ full: `${peerApp.name}, could you: ${request}`,
225
280
  insist: true,
226
281
  // Its one tool, in its own words: it asks its peer.
227
282
  tool: {
@@ -234,10 +289,16 @@ export function useA2ATeam(options) {
234
289
  }));
235
290
  }
236
291
  else if (part.type === 'tool-result' && part.toolName === askTool) {
292
+ const output = part.output;
293
+ inspector?.end(`tool:${part.toolCallId}`, {
294
+ result: part.output,
295
+ ...(output?.error ? { error: String(output.error) } : {}),
296
+ });
237
297
  setEntryPersona(prev => ({
238
298
  ...prev,
239
299
  state: 'thinking',
240
300
  saying: 'Thanks!',
301
+ full: undefined,
241
302
  tool: undefined,
242
303
  }));
243
304
  // Accounting has handed its answer over: back at rest, its last
@@ -250,11 +311,18 @@ export function useA2ATeam(options) {
250
311
  ...prev,
251
312
  state: 'speaking',
252
313
  saying: balloonLine(said),
314
+ full: said,
253
315
  insist: true,
254
316
  tool: undefined,
255
317
  }));
256
318
  }
257
319
  else if (part.type === 'tool-error' && part.toolName === askTool) {
320
+ inspector?.end(`tool:${part.toolCallId}`, {
321
+ status: 'failed',
322
+ error: part.error instanceof Error
323
+ ? part.error.message
324
+ : String(part.error),
325
+ });
258
326
  setEntryPersona(prev => ({
259
327
  ...prev,
260
328
  state: 'thinking',
@@ -271,15 +339,24 @@ export function useA2ATeam(options) {
271
339
  ...history.current,
272
340
  { role: 'assistant', content: said },
273
341
  ];
342
+ if (inspector) {
343
+ const usage = await Promise.resolve(result.totalUsage).catch(() => undefined);
344
+ inspector.end(turnKey, {
345
+ result: { text: said, ...(usage ? { usage } : {}) },
346
+ summary: `${balloonLine(asked, 60)} → ${balloonLine(said, 80)}${usage?.totalTokens ? ` · ${usage.totalTokens} tokens` : ''}`,
347
+ });
348
+ }
274
349
  setTurns(prev => [...prev, { role: 'assistant', text: said }]);
275
350
  setEntryPersona(prev => ({
276
351
  ...prev,
277
352
  state: 'idle',
278
353
  saying: balloonLine(said),
354
+ full: said,
279
355
  }));
280
356
  }
281
357
  catch (error) {
282
358
  const message = error instanceof Error ? error.message : String(error);
359
+ inspector?.end(turnKey, { status: 'failed', error: message });
283
360
  setTurns(prev => [
284
361
  ...prev,
285
362
  { role: 'assistant', text: `Something went wrong: ${message}` },
@@ -288,6 +365,7 @@ export function useA2ATeam(options) {
288
365
  ...prev,
289
366
  state: 'idle',
290
367
  saying: balloonLine(`Something went wrong: ${message}`),
368
+ full: `Something went wrong: ${message}`,
291
369
  insist: true,
292
370
  tool: undefined,
293
371
  }));
@@ -303,7 +381,7 @@ export function useA2ATeam(options) {
303
381
  ? prev
304
382
  : { ...prev, state: 'idle', tool: undefined });
305
383
  }
306
- }, [agent, busy, askTool, peerApp.name]);
384
+ }, [agent, busy, askTool, peerApp.name, entry, inspector]);
307
385
  const stop = useCallback(() => abort.current?.abort(), []);
308
386
  useEffect(() => () => {
309
387
  abort.current?.abort();
@@ -312,13 +390,20 @@ export function useA2ATeam(options) {
312
390
  }, []);
313
391
  const setEntryAway = useCallback((away) => setEntryPersona(prev => ({ ...prev, away })), []);
314
392
  const setPeerAway = useCallback((away) => setPeerPersona(prev => ({ ...prev, away })), []);
393
+ const entryHistory = useMemo(() => turns.map((turn, index) => ({
394
+ id: `turn-${index}`,
395
+ role: turn.role,
396
+ content: turn.text,
397
+ createdAt: new Date(0),
398
+ })), [turns]);
315
399
  return {
400
+ entryHistory,
401
+ peerHistory,
316
402
  entryPersona,
317
403
  peerPersona,
318
404
  setEntryAway,
319
405
  setPeerAway,
320
406
  turns,
321
- exchange,
322
407
  report,
323
408
  artifacts,
324
409
  notebook,
@@ -19,6 +19,19 @@
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
@@ -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, 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,6 +46,8 @@ 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('');
@@ -55,14 +58,24 @@ function AgentA2ATeam() {
55
58
  let current = true;
56
59
  setPeer(null);
57
60
  setPeerError(null);
58
- 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
+ })
59
72
  .then(found => current && setPeer(found))
60
73
  .catch(error => current &&
61
74
  setPeerError(error instanceof Error ? error.message : String(error)));
62
75
  return () => {
63
76
  current = false;
64
77
  };
65
- }, [accountingUrl, accountingKey]);
78
+ }, [accountingUrl, accountingKey, inspector]);
66
79
  // Sales runs here and asks Accounting over A2A: the conversation, the
67
80
  // exchange, the report and the way the link carries a message.
68
81
  const team = useA2ATeam({
@@ -74,7 +87,13 @@ function AgentA2ATeam() {
74
87
  peerConnections: ACCOUNTING_CONNECTIONS,
75
88
  // Its words, and a notebook to run here when it gives one.
76
89
  accept: NOTEBOOK_AND_WORDS,
90
+ inspector,
77
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
+ })), []);
78
97
  const send = (text) => {
79
98
  setDraft('');
80
99
  void team.send(text);
@@ -82,7 +101,7 @@ function AgentA2ATeam() {
82
101
  const accountingWhere = accountingKey
83
102
  ? `on a runtime, ${new URL(accountingUrl).host}, with a temporary key`
84
103
  : `on a runtime, ${new URL(accountingUrl).host}`;
85
- 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) ||
86
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,
87
106
  // Sales runs in this page: what ended is its model's key, and
88
107
  // signing in gives it the person's own.
@@ -104,9 +123,22 @@ function AgentA2ATeam() {
104
123
  : 'on a runtime',
105
124
  persona: team.entryPersona,
106
125
  onToggle: () => composer.current?.focus(),
126
+ conversationLabel: `Ask ${SALES.name}`,
107
127
  onAway: team.setEntryAway,
128
+ history: team.entryHistory,
108
129
  expandTarget: notebookArea,
109
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
+ description: SALES.description || undefined,
141
+ },
110
142
  }, peer: {
111
143
  id: ACCOUNTING.id,
112
144
  name: ACCOUNTING.name,
@@ -115,8 +147,30 @@ function AgentA2ATeam() {
115
147
  where: accountingWhere,
116
148
  persona: team.peerPersona,
117
149
  onAway: team.setPeerAway,
150
+ history: team.peerHistory,
118
151
  connections: ACCOUNTING_CONNECTIONS,
119
- }, flow: team.flow, calls: team.calls, 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" }), _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 => {
152
+ inspector,
153
+ about: {
154
+ name: ACCOUNTING.name,
155
+ spec: `${ACCOUNTING.id}:${ACCOUNTING.version}`,
156
+ model: ACCOUNTING.model || undefined,
157
+ where: accountingWhere,
158
+ // What its A2A card says, once it is reached.
159
+ description: peer?.card.description || ACCOUNTING.description || undefined,
160
+ skills: peer?.card.skills.map(skill => skill.name),
161
+ protocol: peer ? 'a2a' : undefined,
162
+ url: peer ? accountingUrl : undefined,
163
+ },
164
+ }, flow: team.flow, calls: team.calls,
165
+ // Room for the balloons: suggestions, *more* and a notebook.
166
+ 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: {
167
+ display: 'grid',
168
+ gridTemplateColumns: ['1fr', '1fr', '1fr 1fr'],
169
+ gap: 3,
170
+ mb: 3,
171
+ }, "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
172
+ ? 'Its model turns, its call to Accounting and the A2A messages it sends and receives show here.'
173
+ : '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 => {
120
174
  if (event.key === 'Enter' && !event.shiftKey) {
121
175
  event.preventDefault();
122
176
  send(draft);
@@ -126,13 +180,13 @@ function AgentA2ATeam() {
126
180
  mb: 2,
127
181
  borderRadius: 2,
128
182
  bg: turn.role === 'user' ? 'accent.subtle' : 'canvas.subtle',
129
- }, 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: {
130
- p: 3,
131
- border: '1px solid',
132
- borderColor: 'border.default',
133
- borderRadius: 2,
134
- minHeight: 120,
135
- }, "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." })) })] }))] })] })] }));
183
+ }, 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: {
184
+ p: 3,
185
+ border: '1px solid',
186
+ borderColor: 'border.default',
187
+ borderRadius: 2,
188
+ minHeight: 120,
189
+ }, "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." })) })] })) })] })] }));
136
190
  }
137
191
  export function AgentA2ATeamExample() {
138
192
  return (_jsx(ThemedProvider, { children: _jsx(AgentA2ATeam, {}) }));
@@ -15,6 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  * and so are the characters clippy.js publishes, fetched when picked.
16
16
  */
17
17
  import { useEffect, useRef, useState } from 'react';
18
+ import { useAgentInspector } from '../components/inspector/agentInspector';
18
19
  import { buildReactorFromPlugins } from '@datalayer/reactor';
19
20
  import { Button, Heading, Text, ToggleSwitch } from '@primer/react';
20
21
  import { Box } from '@datalayer/primer-addons';
@@ -24,7 +25,7 @@ import { AssistantCharactersPlugin, assistantCharacterNamed, assistantCharacters
24
25
  import { OwlCharacterPlugin } from './utils/owlCharacterPlugin';
25
26
  import { readTestSpriteCharacter } from './utils/testSpriteCharacter';
26
27
  import { CLIPPY_JS_CHARACTERS, CLIPPY_JS_VERSION, readClippyJsCharacter, } from './utils/clippyJsCharacters';
27
- import { readAcsCharacter, readClippyCharacter, } from '../chat/assistant/formats';
28
+ import { CHARACTER_FILES_ACCEPT, readCharacterFiles, } from '../chat/assistant/formats';
28
29
  /**
29
30
  * The agent-runtimes server the assistant talks to: `?agentRuntimesUrl=`
30
31
  * when given, else this machine's on port 8765. It serves the `assistant`
@@ -34,6 +35,35 @@ const SERVER = ((typeof window === 'undefined'
34
35
  ? null
35
36
  : new URLSearchParams(window.location.search).get('agentRuntimesUrl')) ||
36
37
  'http://127.0.0.1:8765').replace(/\/+$/, '');
38
+ /** The agent the assistant talks to on that server. */
39
+ const AGENT_ID = 'assistant';
40
+ /**
41
+ * Make sure the server has the assistant's agent: `npm run examples` starts
42
+ * the server with none, so the page creates it from the
43
+ * `example-agentic-chat` spec the first time, over AG-UI, the spec's own
44
+ * protocol.
45
+ */
46
+ async function ensureAssistantAgent() {
47
+ const found = await fetch(`${SERVER}/api/v1/agents/${AGENT_ID}`);
48
+ if (found.ok) {
49
+ return;
50
+ }
51
+ if (found.status !== 404) {
52
+ throw new Error(`The server could not say whether the ${AGENT_ID} agent exists (${found.status}).`);
53
+ }
54
+ const created = await fetch(`${SERVER}/api/v1/agents`, {
55
+ method: 'POST',
56
+ headers: { 'content-type': 'application/json' },
57
+ body: JSON.stringify({
58
+ name: AGENT_ID,
59
+ agent_spec_id: 'example-agentic-chat',
60
+ transport: 'ag-ui',
61
+ }),
62
+ });
63
+ if (!created.ok && created.status !== 409) {
64
+ throw new Error(`The server could not create the ${AGENT_ID} agent (${created.status}).`);
65
+ }
66
+ }
37
67
  /** The plugins this page enables: Datalayer's characters and the owl's. */
38
68
  const reactor = buildReactorFromPlugins([
39
69
  AssistantCharactersPlugin,
@@ -45,27 +75,6 @@ const CATALOGUE = assistantCharactersOf(reactor).map(entry => ({
45
75
  id: entry.id,
46
76
  name: entry.character.name,
47
77
  }));
48
- /**
49
- * A character file a person picked, read in the page (T-26): one `.acs`, or
50
- * a clippy.js character's `agent.js` and `map.png` (and its sounds file).
51
- */
52
- async function readPicked(files) {
53
- const acs = files.find(file => file.name.toLowerCase().endsWith('.acs'));
54
- if (acs) {
55
- return readAcsCharacter(await acs.arrayBuffer());
56
- }
57
- const agentJs = files.find(file => file.name.toLowerCase() === 'agent.js');
58
- const mapPng = files.find(file => /\.(png|gif|webp|jpe?g)$/i.test(file.name));
59
- const soundsJs = files.find(file => /^sounds-.*\.js$/i.test(file.name));
60
- if (!agentJs || !mapPng) {
61
- throw new Error('Pick one .acs file, or a clippy.js character: its agent.js and its map image (and a sounds file if you have one).');
62
- }
63
- return readClippyCharacter({
64
- agentJs: await agentJs.text(),
65
- mapPng,
66
- soundsJs: soundsJs ? await soundsJs.text() : undefined,
67
- });
68
- }
69
78
  const AssistantExample = () => {
70
79
  const [character, setCharacter] = useState('paperclip');
71
80
  // A character chosen by id is the one its plugin contributes.
@@ -73,8 +82,21 @@ const AssistantExample = () => {
73
82
  ? assistantCharacterNamed(reactor, character)
74
83
  : character;
75
84
  const [loadError, setLoadError] = useState();
85
+ // The agent on the server, made if it is not there yet.
86
+ const [agentReady, setAgentReady] = useState(false);
87
+ const [agentError, setAgentError] = useState();
88
+ useEffect(() => {
89
+ let cancelled = false;
90
+ ensureAssistantAgent().then(() => !cancelled && setAgentReady(true), error => !cancelled &&
91
+ setAgentError(`${error instanceof Error ? error.message : String(error)} The assistant talks to an agent-runtimes server at ${SERVER}.`));
92
+ return () => {
93
+ cancelled = true;
94
+ };
95
+ }, []);
76
96
  // How the balloon shows the conversation (T-23): history, or current.
77
97
  const [balloon, setBalloon] = useState('history');
98
+ // What the agent does, for the menu's *Inspect the agent…*.
99
+ const { sink: inspector } = useAgentInspector();
78
100
  // Pixel, the test sprite, read once through the clippy.js reader.
79
101
  const [pixel, setPixel] = useState();
80
102
  useEffect(() => {
@@ -142,13 +164,13 @@ const AssistantExample = () => {
142
164
  onClick: () => setBalloon(balloon === 'history' ? 'current' : 'history') }), _jsx(Text, { "aria-live": "polite", "data-toggle-state": "", sx: { color: 'fg.muted' }, children: balloon === 'history' ? 'History' : 'Current' })] })] }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "The clippy.js characters" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["Fetched when picked from the", ' ', _jsxs("code", { children: ["clippyjs@", CLIPPY_JS_VERSION] }), " package on jsDelivr and read as data by the same reader. They are not in this repository: clippy.js\u2019s licence covers its code only, and the characters are Microsoft\u2019s."] }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: CLIPPY_JS_CHARACTERS.map(name => {
143
165
  const picked = clippyJs[name] !== undefined && character === clippyJs[name];
144
166
  return (_jsx(Button, { variant: picked ? 'primary' : 'default', onClick: () => void pickClippyJs(name), disabled: fetching !== undefined, "aria-pressed": picked, "data-assistant-character": `clippy-js-${name.toLowerCase()}`, children: fetching === name ? `${name}…` : name }, name));
145
- }) })] }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "A character you bring" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["The characters of Microsoft Office \u2014 Clippy, Merlin, Links and the others \u2014 are Microsoft\u2019s. Load only a character file you have the right to use: a Microsoft Agent ", _jsx("code", { children: ".acs" }), " file, or a clippy.js character\u2019s ", _jsx("code", { children: "agent.js" }), " and map image. It is read in this page and sent nowhere."] }), _jsx("input", { type: "file", multiple: true, accept: ".acs,.js,.png,.gif,.webp,.jpg,.jpeg", "aria-label": "Character files", onChange: async (event) => {
167
+ }) })] }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "A character you bring" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["The characters of Microsoft Office \u2014 Clippy, Merlin, Links and the others \u2014 are Microsoft\u2019s. Load only a character file you have the right to use: a Microsoft Agent ", _jsx("code", { children: ".acs" }), " file, or a clippy.js character\u2019s ", _jsx("code", { children: "agent.js" }), " and map image. It is read in this page and sent nowhere."] }), _jsx("input", { type: "file", multiple: true, accept: CHARACTER_FILES_ACCEPT, "aria-label": "Character files", onChange: async (event) => {
146
168
  const files = Array.from(event.target.files ?? []);
147
169
  if (!files.length) {
148
170
  return;
149
171
  }
150
172
  try {
151
- setCharacter(await readPicked(files));
173
+ setCharacter(await readCharacterFiles(files));
152
174
  setLoadError(undefined);
153
175
  }
154
176
  catch (error) {
@@ -156,8 +178,25 @@ const AssistantExample = () => {
156
178
  }
157
179
  } }), loadError && (_jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mt: 2 }, children: loadError })), typeof character !== 'string' &&
158
180
  character !== pixel &&
159
- !isClippyJs && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), _jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: drawn, balloonDisplay: balloon, protocol: "vercel-ai", endpoint: `${SERVER}/api/v1/vercel-ai/assistant`, title: "Assistant", description: "Hello! Ask me anything about this page.", position: "bottom-right", useStore: false,
181
+ !isClippyJs && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), agentError && (_jsx(Box, { sx: { maxWidth: 720, mx: 'auto', mt: 3 }, children: _jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg' }, children: agentError }) })), agentReady && (_jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: drawn, balloonDisplay: balloon, protocol: "ag-ui", endpoint: `${SERVER}/api/v1/ag-ui/${AGENT_ID}/`, title: "Assistant", description: "Hello! Ask me anything about this page.", position: "bottom-right", useStore: false,
160
182
  // Ask a decision beside the composer: Jev, through this server.
161
- decisions: { serverUrl: SERVER } }, typeof character === 'string' ? character : character.sprite)] }) }));
183
+ decisions: { serverUrl: SERVER },
184
+ // Right-click the character: its menu.
185
+ inspector: inspector, onBalloonDisplayChange: setBalloon, onChangeCharacter: () => document
186
+ .querySelector('[data-assistant-character]')
187
+ ?.focus(), about: {
188
+ name: 'Assistant',
189
+ spec: AGENT_ID,
190
+ where: `on a runtime, ${new URL(SERVER).host}`,
191
+ }, suggestions: [
192
+ {
193
+ title: 'What is on this page?',
194
+ message: 'What is on this page?',
195
+ },
196
+ {
197
+ title: 'Tell me a joke',
198
+ message: 'Tell me a short joke about accountants.',
199
+ },
200
+ ] }, typeof character === 'string' ? character : character.sprite))] }) }));
162
201
  };
163
202
  export default AssistantExample;
@@ -163,7 +163,8 @@ const AssistantGalleryExample = () => {
163
163
  {
164
164
  id: 'notebook',
165
165
  role: 'assistant',
166
- text: SAMPLE_NOTEBOOK_SAYING,
166
+ content: SAMPLE_NOTEBOOK_SAYING,
167
+ createdAt: new Date(0),
167
168
  },
168
169
  ]
169
170
  : []),