@datalayer/agent-runtimes 1.3.84 → 1.3.86

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