@datalayer/agent-runtimes 1.3.85 → 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.
- package/lib/components/teams/A2ATeamGraph.d.ts +113 -18
- package/lib/components/teams/A2ATeamGraph.js +217 -97
- package/lib/components/teams/index.d.ts +1 -0
- package/lib/components/teams/index.js +1 -0
- package/lib/components/teams/sceneStage.d.ts +120 -0
- package/lib/components/teams/sceneStage.js +170 -0
- package/lib/components/teams/useA2ATeam.d.ts +92 -20
- package/lib/components/teams/useA2ATeam.js +313 -101
- package/lib/examples/ScenesExample.d.ts +26 -0
- package/lib/examples/ScenesExample.js +241 -0
- package/lib/examples/example-selector.js +1 -0
- package/lib/examples/exampleGroups.js +3 -0
- package/lib/examples/index.d.ts +1 -0
- package/lib/examples/index.js +1 -0
- package/lib/examples/main.js +4 -0
- package/lib/specs/index.d.ts +1 -0
- package/lib/specs/index.js +1 -0
- package/lib/specs/mcpServers.js +8 -2
- package/lib/specs/scenes.d.ts +21 -0
- package/lib/specs/scenes.js +1191 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.js +1 -0
- package/lib/types/scenes.d.ts +207 -0
- package/lib/types/scenes.js +5 -0
- package/package.json +3 -3
- package/scripts/codegen/generate_agents.py +1 -0
- package/scripts/codegen/generate_scenes.py +301 -0
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A team
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* A team over A2A, as a graph (React Flow): each member is its character
|
|
3
|
+
* (`AssistantStage`), with its name and where it runs under it, and an edge
|
|
4
|
+
* links the entry to each peer it asks — one peer (`peer`, a team of two)
|
|
5
|
+
* or several (`peers`, a team of N; LOOP A-08). The edges are the team's
|
|
6
|
+
* `talks_to` (`links`): a peer that talks to another peer has its edge
|
|
7
|
+
* drawn too, still, since the page runs the entry's asks only.
|
|
5
8
|
*
|
|
6
|
-
*
|
|
9
|
+
* An edge is still until a message travels on it. While the entry asks, it
|
|
7
10
|
* flows from the entry to the peer; while the peer answers, from the peer to
|
|
8
|
-
* the entry ({@link A2ATeamFlow}, from the `A2APeerEvent` phases
|
|
9
|
-
* who asks for no motion sees the same thing
|
|
10
|
-
* at the end the message goes to, and the
|
|
11
|
+
* the entry ({@link A2ATeamFlow}, from the `A2APeerEvent` phases; one flow
|
|
12
|
+
* per peer, `flows`). A reader who asks for no motion sees the same thing
|
|
13
|
+
* without the movement: the arrow at the end the message goes to, and the
|
|
14
|
+
* word for it on the edge.
|
|
15
|
+
*
|
|
16
|
+
* The members stand where the layout puts them: the entry on the left and
|
|
17
|
+
* the peers to its right, one under another — or where a scene's stage
|
|
18
|
+
* directions say (`positions`, fractions of the box; LOOP A-13): the
|
|
19
|
+
* distinct `x` values are the columns, left to right, the distinct `y`
|
|
20
|
+
* values the rows, top to bottom.
|
|
11
21
|
*
|
|
12
22
|
* A member's connections — the MCP servers it reaches, Odoo for Accounting —
|
|
13
23
|
* hang under it, each a node half a member's size with the server's mark
|
|
@@ -108,17 +118,43 @@ export type A2ATeamGraphMember = {
|
|
|
108
118
|
/** Its balloon's display was chosen in its menu: the page may keep it. */
|
|
109
119
|
onBalloonDisplayChange?: (display: BalloonDisplay) => void;
|
|
110
120
|
};
|
|
121
|
+
/** Who talks to whom over A2A: an edge of the graph. */
|
|
122
|
+
export type A2ATeamGraphLink = {
|
|
123
|
+
from: string;
|
|
124
|
+
to: string;
|
|
125
|
+
};
|
|
126
|
+
/** Where a member stands: fractions of the box, as a scene's stage says. */
|
|
127
|
+
export type A2ATeamGraphPosition = {
|
|
128
|
+
x: number;
|
|
129
|
+
y: number;
|
|
130
|
+
};
|
|
111
131
|
export type A2ATeamGraphProps = {
|
|
112
132
|
/** The member that asks: drawn on the left. */
|
|
113
133
|
entry: A2ATeamGraphMember;
|
|
114
|
-
/** The member asked over A2A: drawn on the right. */
|
|
115
|
-
peer
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
/**
|
|
134
|
+
/** The member asked over A2A: drawn on the right. A team of two. */
|
|
135
|
+
peer?: A2ATeamGraphMember;
|
|
136
|
+
/** The members asked over A2A, a team of N: after `peer` when both are given. */
|
|
137
|
+
peers?: readonly A2ATeamGraphMember[];
|
|
138
|
+
/** Which way the link to `peer` carries a message now. */
|
|
139
|
+
flow?: A2ATeamFlow;
|
|
140
|
+
/** Which way each peer's link carries a message now, by the peer's id. */
|
|
141
|
+
flows?: Record<string, A2ATeamFlow>;
|
|
142
|
+
/**
|
|
143
|
+
* Whether the peers are reached: an edge is drawn faint until its peer
|
|
144
|
+
* is. One answer for all, or one per peer id.
|
|
145
|
+
*/
|
|
146
|
+
connected: boolean | Record<string, boolean>;
|
|
147
|
+
/** What an edge says at rest: `A2A · <the peer's skill>`. */
|
|
121
148
|
label?: string;
|
|
149
|
+
/** What each peer's edge says at rest, by the peer's id; `label` unsaid. */
|
|
150
|
+
labels?: Record<string, string>;
|
|
151
|
+
/**
|
|
152
|
+
* The edges: the team's `talks_to` over A2A. Unsaid, the entry to each
|
|
153
|
+
* peer. An edge between two peers is drawn still.
|
|
154
|
+
*/
|
|
155
|
+
links?: readonly A2ATeamGraphLink[];
|
|
156
|
+
/** Where each member stands, by its id (a scene's `stage.positions`). */
|
|
157
|
+
positions?: Record<string, A2ATeamGraphPosition>;
|
|
122
158
|
/** The character's size, in pixels at full scale. */
|
|
123
159
|
size?: number;
|
|
124
160
|
/** The tool calls running now, to the members' connections (`useA2ATeam`). */
|
|
@@ -130,6 +166,36 @@ export type A2ATeamGraphProps = {
|
|
|
130
166
|
*/
|
|
131
167
|
balloonRoom?: number;
|
|
132
168
|
};
|
|
169
|
+
/**
|
|
170
|
+
* The handles an edge leaves and reaches a member by, named: the middle of
|
|
171
|
+
* its character on each side, chosen by where the other member stands.
|
|
172
|
+
*/
|
|
173
|
+
export declare const MEMBER_HANDLES: {
|
|
174
|
+
readonly inLeft: "in-left";
|
|
175
|
+
readonly outRight: "out-right";
|
|
176
|
+
readonly inTop: "in-top";
|
|
177
|
+
readonly outBottom: "out-bottom";
|
|
178
|
+
readonly inRight: "in-right";
|
|
179
|
+
readonly outLeft: "out-left";
|
|
180
|
+
readonly inBottom: "in-bottom";
|
|
181
|
+
readonly outTop: "out-top";
|
|
182
|
+
readonly connections: "connections";
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* The handles for an edge from one place to another: rightward across,
|
|
186
|
+
* leftward across, or down or up when the other member is mostly below or
|
|
187
|
+
* above.
|
|
188
|
+
*/
|
|
189
|
+
export declare function handlesBetween(from: {
|
|
190
|
+
x: number;
|
|
191
|
+
y: number;
|
|
192
|
+
}, to: {
|
|
193
|
+
x: number;
|
|
194
|
+
y: number;
|
|
195
|
+
}): {
|
|
196
|
+
sourceHandle: string;
|
|
197
|
+
targetHandle: string;
|
|
198
|
+
};
|
|
133
199
|
/** How far a member was moved from its place, in the graph's coordinates. */
|
|
134
200
|
export type MemberOffset = {
|
|
135
201
|
dx: number;
|
|
@@ -152,6 +218,17 @@ export declare function connectionLabel(connection: Pick<A2ATeamConnection, 'lab
|
|
|
152
218
|
export declare function connectionDetails(connection: Pick<A2ATeamConnection, 'name' | 'tools'>, member: string | undefined, calling?: string): string;
|
|
153
219
|
/** The words on the edge for a flow. */
|
|
154
220
|
export declare function flowWords(flow: A2ATeamFlow, entry: string, peer: string): string;
|
|
221
|
+
/**
|
|
222
|
+
* An arrowhead at the end a message goes to — the target's when `forward`,
|
|
223
|
+
* the source's otherwise — pointing along the edge.
|
|
224
|
+
*/
|
|
225
|
+
export declare function arrowPath(source: {
|
|
226
|
+
x: number;
|
|
227
|
+
y: number;
|
|
228
|
+
}, target: {
|
|
229
|
+
x: number;
|
|
230
|
+
y: number;
|
|
231
|
+
}, forward: boolean): string;
|
|
155
232
|
type CallData = {
|
|
156
233
|
/** The tool being called, in words (`list invoices`); empty while none is. */
|
|
157
234
|
calling: string;
|
|
@@ -160,8 +237,26 @@ type CallData = {
|
|
|
160
237
|
export declare const CallEdge: import("react").NamedExoticComponent<EdgeProps<Edge<CallData>>>;
|
|
161
238
|
/** What a member's call says, on its edge and to a screen reader. */
|
|
162
239
|
export declare function callWords(member: string, connection: A2ATeamConnection, tool: string): string;
|
|
163
|
-
/** The viewport that shows
|
|
164
|
-
export declare function teamViewport(width: number): Viewport;
|
|
165
|
-
/**
|
|
166
|
-
export
|
|
240
|
+
/** The viewport that shows a graph `drawn` wide whole in a box `width` wide, never larger than drawn. */
|
|
241
|
+
export declare function teamViewport(width: number, drawn?: number): Viewport;
|
|
242
|
+
/** Where a member is laid out: its column and row, from the positions or the default. */
|
|
243
|
+
export type MemberPlace = {
|
|
244
|
+
column: number;
|
|
245
|
+
row: number;
|
|
246
|
+
};
|
|
247
|
+
/**
|
|
248
|
+
* Where each member stands, as columns and rows: from the positions given
|
|
249
|
+
* (the distinct `x` values are the columns left to right, the distinct `y`
|
|
250
|
+
* values the rows top to bottom), or the entry in the first column and the
|
|
251
|
+
* peers one under another in the second.
|
|
252
|
+
*/
|
|
253
|
+
export declare function placeMembers(entryId: string, peerIds: readonly string[], positions?: Record<string, A2ATeamGraphPosition>): {
|
|
254
|
+
places: Record<string, MemberPlace>;
|
|
255
|
+
columns: number;
|
|
256
|
+
rows: number;
|
|
257
|
+
};
|
|
258
|
+
/** The one flow the graph reports for all its edges: asking before answering before still. */
|
|
259
|
+
export declare function flowOfAll(flows: readonly A2ATeamFlow[]): A2ATeamFlow;
|
|
260
|
+
/** Draw a team over A2A, each link moving with what travels on it. */
|
|
261
|
+
export declare function A2ATeamGraph({ entry, peer, peers: more, flow, flows, connected, label, labels, links, positions, size, calls, balloonRoom, }: A2ATeamGraphProps): JSX.Element;
|
|
167
262
|
export default A2ATeamGraph;
|
|
@@ -21,6 +21,8 @@ const NOTEBOOK_BALLOON_ROOM = 480;
|
|
|
21
21
|
const NOTEBOOK_PREVIEW_HEIGHT = 180;
|
|
22
22
|
const GAP = 220;
|
|
23
23
|
const WIDTH = NODE_WIDTH * 2 + GAP;
|
|
24
|
+
/** Between two rows of members. */
|
|
25
|
+
const ROW_GAP = 48;
|
|
24
26
|
/** A connection's node: half a member's. */
|
|
25
27
|
const CONNECTION_WIDTH = NODE_WIDTH / 2;
|
|
26
28
|
/** Between a member and its connections. */
|
|
@@ -37,6 +39,50 @@ const HANDLE = (size) => ({
|
|
|
37
39
|
minHeight: 0,
|
|
38
40
|
border: 0,
|
|
39
41
|
});
|
|
42
|
+
/**
|
|
43
|
+
* The handles an edge leaves and reaches a member by, named: the middle of
|
|
44
|
+
* its character on each side, chosen by where the other member stands.
|
|
45
|
+
*/
|
|
46
|
+
export const MEMBER_HANDLES = {
|
|
47
|
+
inLeft: 'in-left',
|
|
48
|
+
outRight: 'out-right',
|
|
49
|
+
inTop: 'in-top',
|
|
50
|
+
outBottom: 'out-bottom',
|
|
51
|
+
inRight: 'in-right',
|
|
52
|
+
outLeft: 'out-left',
|
|
53
|
+
inBottom: 'in-bottom',
|
|
54
|
+
outTop: 'out-top',
|
|
55
|
+
connections: 'connections',
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The handles for an edge from one place to another: rightward across,
|
|
59
|
+
* leftward across, or down or up when the other member is mostly below or
|
|
60
|
+
* above.
|
|
61
|
+
*/
|
|
62
|
+
export function handlesBetween(from, to) {
|
|
63
|
+
const dx = to.x - from.x;
|
|
64
|
+
const dy = to.y - from.y;
|
|
65
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
66
|
+
return dx >= 0
|
|
67
|
+
? {
|
|
68
|
+
sourceHandle: MEMBER_HANDLES.outRight,
|
|
69
|
+
targetHandle: MEMBER_HANDLES.inLeft,
|
|
70
|
+
}
|
|
71
|
+
: {
|
|
72
|
+
sourceHandle: MEMBER_HANDLES.outLeft,
|
|
73
|
+
targetHandle: MEMBER_HANDLES.inRight,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return dy >= 0
|
|
77
|
+
? {
|
|
78
|
+
sourceHandle: MEMBER_HANDLES.outBottom,
|
|
79
|
+
targetHandle: MEMBER_HANDLES.inTop,
|
|
80
|
+
}
|
|
81
|
+
: {
|
|
82
|
+
sourceHandle: MEMBER_HANDLES.outTop,
|
|
83
|
+
targetHandle: MEMBER_HANDLES.inBottom,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
40
86
|
/**
|
|
41
87
|
* The members as they are now, by id. Not in the nodes' data: a node whose
|
|
42
88
|
* object changes is taken by React Flow for a new one, measured again, and
|
|
@@ -159,7 +205,7 @@ const MemberNode = memo(function MemberNode({ data, }) {
|
|
|
159
205
|
pointerEvents: 'all',
|
|
160
206
|
}, "data-team-member": member.id, "data-member-balloon": shown, "data-member-moved": offset.dx || offset.dy
|
|
161
207
|
? `${Math.round(offset.dx)},${Math.round(offset.dy)}`
|
|
162
|
-
: undefined, "data-member-state": persona.state, children: [_jsx(Handle, { type: "target", position: Position.Left, style: HANDLE(size), isConnectable: false }), _jsx(Box, { sx: { width: size, height: size, position: 'relative' }, children: persona.away ? (_jsxs(Button, { size: "small", sx: { position: 'absolute', top: size / 2 - 16, left: -24 }, onClick: () => member.onAway?.(false), children: ["Call ", member.name, " back"] })) : (_jsx(AssistantStage, { character: member.character, state: persona.state, size: size,
|
|
208
|
+
: undefined, "data-member-state": persona.state, children: [_jsx(Handle, { id: MEMBER_HANDLES.inLeft, type: "target", position: Position.Left, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.inRight, type: "target", position: Position.Right, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.inTop, type: "target", position: Position.Top, style: { ...HANDLE(size), top: 0 }, isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.inBottom, type: "target", position: Position.Bottom, style: { ...HANDLE(size), top: size }, isConnectable: false }), _jsx(Box, { sx: { width: size, height: size, position: 'relative' }, children: persona.away ? (_jsxs(Button, { size: "small", sx: { position: 'absolute', top: size / 2 - 16, left: -24 }, onClick: () => member.onAway?.(false), children: ["Call ", member.name, " back"] })) : (_jsx(AssistantStage, { character: member.character, state: persona.state, size: size,
|
|
163
209
|
// In the node, not over the page; the balloon opens toward the
|
|
164
210
|
// middle of the graph: rightward from the left member.
|
|
165
211
|
place: side === 'left'
|
|
@@ -236,7 +282,7 @@ const MemberNode = memo(function MemberNode({ data, }) {
|
|
|
236
282
|
stayPut: true,
|
|
237
283
|
// Named as the member, not its character: `Sales (in your
|
|
238
284
|
// browser)`; the character describes it.
|
|
239
|
-
agentName: member.name, label: memberLabel(member) })) }), _jsxs(Text, { sx: { fontWeight: 600, textAlign: 'center' }, children: [member.emoji ? `${member.emoji} ` : '', member.name] }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', textAlign: 'center' }, "data-member-where": "", children: member.where }), _jsx(Handle, { type: "source", position: Position.Right, style: HANDLE(size), isConnectable: false }), member.connections?.length ? (_jsx(Handle, { id:
|
|
285
|
+
agentName: member.name, label: memberLabel(member) })) }), _jsxs(Text, { sx: { fontWeight: 600, textAlign: 'center' }, children: [member.emoji ? `${member.emoji} ` : '', member.name] }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', textAlign: 'center' }, "data-member-where": "", children: member.where }), _jsx(Handle, { id: MEMBER_HANDLES.outRight, type: "source", position: Position.Right, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outLeft, type: "source", position: Position.Left, style: HANDLE(size), isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outBottom, type: "source", position: Position.Bottom, style: { ...HANDLE(size), top: size }, isConnectable: false }), _jsx(Handle, { id: MEMBER_HANDLES.outTop, type: "source", position: Position.Top, style: { ...HANDLE(size), top: 0 }, isConnectable: false }), member.connections?.length ? (_jsx(Handle, { id: MEMBER_HANDLES.connections, type: "source", position: Position.Bottom, style: { ...HANDLE(size), top: 'auto', bottom: 0 }, isConnectable: false })) : null] }));
|
|
240
286
|
});
|
|
241
287
|
/** A member's accessible name: its name, and where it runs. */
|
|
242
288
|
export function memberLabel(member) {
|
|
@@ -317,14 +363,29 @@ export function flowWords(flow, entry, peer) {
|
|
|
317
363
|
: '';
|
|
318
364
|
}
|
|
319
365
|
const ARROW = 9;
|
|
366
|
+
/**
|
|
367
|
+
* An arrowhead at the end a message goes to — the target's when `forward`,
|
|
368
|
+
* the source's otherwise — pointing along the edge.
|
|
369
|
+
*/
|
|
370
|
+
export function arrowPath(source, target, forward) {
|
|
371
|
+
const tip = forward ? target : source;
|
|
372
|
+
const from = forward ? source : target;
|
|
373
|
+
const length = Math.hypot(tip.x - from.x, tip.y - from.y) || 1;
|
|
374
|
+
// Along the edge, toward the tip; and across it.
|
|
375
|
+
const ax = (tip.x - from.x) / length;
|
|
376
|
+
const ay = (tip.y - from.y) / length;
|
|
377
|
+
const bx = tip.x - ax * ARROW;
|
|
378
|
+
const by = tip.y - ay * ARROW;
|
|
379
|
+
const px = -ay * (ARROW / 1.6);
|
|
380
|
+
const py = ax * (ARROW / 1.6);
|
|
381
|
+
return `M ${tip.x},${tip.y} L ${bx + px},${by + py} L ${bx - px},${by - py} Z`;
|
|
382
|
+
}
|
|
320
383
|
const LinkEdge = memo(function LinkEdge({ id, sourceX, sourceY, targetX, targetY, data, }) {
|
|
321
384
|
const flow = data?.flow ?? 'still';
|
|
322
385
|
const path = `M ${sourceX},${sourceY} L ${targetX},${targetY}`;
|
|
323
|
-
// The arrow at the end the message goes to.
|
|
386
|
+
// The arrow at the end the message goes to, along the edge.
|
|
324
387
|
const towardPeer = flow === 'asking';
|
|
325
|
-
const
|
|
326
|
-
const back = towardPeer ? -ARROW : ARROW;
|
|
327
|
-
const arrow = `M ${tipX},${targetY} L ${tipX + back},${targetY - ARROW / 1.6} L ${tipX + back},${targetY + ARROW / 1.6} Z`;
|
|
388
|
+
const arrow = arrowPath({ x: sourceX, y: sourceY }, { x: targetX, y: targetY }, towardPeer);
|
|
328
389
|
return (_jsxs(_Fragment, { children: [_jsx(BaseEdge, { id: id, path: path, style: {
|
|
329
390
|
stroke: data?.connected
|
|
330
391
|
? 'var(--borderColor-accent-emphasis, #0969da)'
|
|
@@ -368,13 +429,60 @@ function connectionsX(memberX, count) {
|
|
|
368
429
|
export function callWords(member, connection, tool) {
|
|
369
430
|
return `${member} calls ${connection.label} · ${toolWords(tool, connection)}`;
|
|
370
431
|
}
|
|
371
|
-
/** The viewport that shows
|
|
372
|
-
export function teamViewport(width) {
|
|
373
|
-
const zoom = Math.min(1, width /
|
|
374
|
-
return { zoom, x: (width -
|
|
432
|
+
/** The viewport that shows a graph `drawn` wide whole in a box `width` wide, never larger than drawn. */
|
|
433
|
+
export function teamViewport(width, drawn = WIDTH) {
|
|
434
|
+
const zoom = Math.min(1, width / drawn);
|
|
435
|
+
return { zoom, x: (width - drawn * zoom) / 2, y: 0 };
|
|
375
436
|
}
|
|
376
|
-
/**
|
|
377
|
-
|
|
437
|
+
/**
|
|
438
|
+
* Where each member stands, as columns and rows: from the positions given
|
|
439
|
+
* (the distinct `x` values are the columns left to right, the distinct `y`
|
|
440
|
+
* values the rows top to bottom), or the entry in the first column and the
|
|
441
|
+
* peers one under another in the second.
|
|
442
|
+
*/
|
|
443
|
+
export function placeMembers(entryId, peerIds, positions) {
|
|
444
|
+
const ids = [entryId, ...peerIds];
|
|
445
|
+
const placed = positions ?? {};
|
|
446
|
+
if (ids.every(id => placed[id])) {
|
|
447
|
+
const xs = [...new Set(ids.map(id => placed[id].x))].sort((a, b) => a - b);
|
|
448
|
+
const ys = [...new Set(ids.map(id => placed[id].y))].sort((a, b) => a - b);
|
|
449
|
+
return {
|
|
450
|
+
places: Object.fromEntries(ids.map(id => [
|
|
451
|
+
id,
|
|
452
|
+
{ column: xs.indexOf(placed[id].x), row: ys.indexOf(placed[id].y) },
|
|
453
|
+
])),
|
|
454
|
+
columns: xs.length,
|
|
455
|
+
rows: ys.length,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
places: {
|
|
460
|
+
[entryId]: { column: 0, row: 0 },
|
|
461
|
+
...Object.fromEntries(peerIds.map((id, at) => [id, { column: 1, row: at }])),
|
|
462
|
+
},
|
|
463
|
+
columns: peerIds.length ? 2 : 1,
|
|
464
|
+
rows: Math.max(1, peerIds.length),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
/** The one flow the graph reports for all its edges: asking before answering before still. */
|
|
468
|
+
export function flowOfAll(flows) {
|
|
469
|
+
return flows.includes('asking')
|
|
470
|
+
? 'asking'
|
|
471
|
+
: flows.includes('answering')
|
|
472
|
+
? 'answering'
|
|
473
|
+
: 'still';
|
|
474
|
+
}
|
|
475
|
+
const NO_PEERS = [];
|
|
476
|
+
/** Draw a team over A2A, each link moving with what travels on it. */
|
|
477
|
+
export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows, connected, label = 'A2A', labels, links, positions, size = 96, calls = [], balloonRoom = BALLOON_ROOM, }) {
|
|
478
|
+
const peers = useMemo(() => [...(peer ? [peer] : []), ...more], [peer, more]);
|
|
479
|
+
const members = useMemo(() => [entry, ...peers], [entry, peers]);
|
|
480
|
+
const memberOf = (id) => members.find(member => member.id === id);
|
|
481
|
+
const flowOf = (peerId) => flows?.[peerId] ??
|
|
482
|
+
(peer && peerId === peer.id ? (flow ?? 'still') : 'still');
|
|
483
|
+
const connectedOf = (peerId) => typeof connected === 'boolean' ? connected : Boolean(connected[peerId]);
|
|
484
|
+
const labelOf = (peerId) => labels?.[peerId] ?? label;
|
|
485
|
+
const edgesWanted = links ?? peers.map(one => ({ from: entry.id, to: one.id }));
|
|
378
486
|
const box = useRef(null);
|
|
379
487
|
const [width, setWidth] = useState(0);
|
|
380
488
|
useEffect(() => {
|
|
@@ -391,7 +499,11 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
391
499
|
observer.observe(element);
|
|
392
500
|
return () => observer.disconnect();
|
|
393
501
|
}, []);
|
|
394
|
-
|
|
502
|
+
// Where the members stand, as columns and rows, and the width that takes.
|
|
503
|
+
const peerIds = peers.map(one => one.id);
|
|
504
|
+
const placing = placeMembers(entry.id, peerIds, positions);
|
|
505
|
+
const drawn = Math.max(WIDTH, placing.columns * NODE_WIDTH + (placing.columns - 1) * GAP);
|
|
506
|
+
const viewport = useMemo(() => teamViewport(width || drawn, drawn), [width, drawn]);
|
|
395
507
|
// The members' height: the character, its name and where it runs.
|
|
396
508
|
const memberHeight = size + 56;
|
|
397
509
|
const connectionSize = Math.round(size / 2);
|
|
@@ -399,43 +511,47 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
399
511
|
// and room under it before the graph's edge.
|
|
400
512
|
const connectionHeight = connectionSize + 58;
|
|
401
513
|
// The connections' ids, member by member: the nodes change only when they do.
|
|
402
|
-
const placed =
|
|
514
|
+
const placed = members.map(member => ({
|
|
403
515
|
member: member.id,
|
|
404
516
|
connections: (member.connections ?? []).map(connection => connection.id),
|
|
405
517
|
}));
|
|
406
518
|
const placedKey = JSON.stringify(placed);
|
|
407
519
|
const hasConnections = placed.some(({ connections }) => connections.length);
|
|
408
520
|
// A notebook in a balloon: more room above the members.
|
|
409
|
-
const room =
|
|
521
|
+
const room = members.some(member => member.persona.notebook)
|
|
410
522
|
? Math.max(NOTEBOOK_BALLOON_ROOM, balloonRoom)
|
|
411
523
|
: balloonRoom;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
524
|
+
// Under a member: its connections, when any member has some.
|
|
525
|
+
const below = hasConnections ? CONNECTION_GAP + connectionHeight : 0;
|
|
526
|
+
const rowHeight = memberHeight + below;
|
|
527
|
+
const total = room + placing.rows * rowHeight + (placing.rows - 1) * ROW_GAP;
|
|
528
|
+
const height = Math.ceil(total * viewport.zoom);
|
|
529
|
+
// Where each member is laid out, before it is moved.
|
|
530
|
+
const layoutOf = (id) => {
|
|
531
|
+
const place = placing.places[id] ?? { column: 0, row: 0 };
|
|
532
|
+
return {
|
|
533
|
+
x: place.column * (NODE_WIDTH + GAP),
|
|
534
|
+
y: room + place.row * (rowHeight + ROW_GAP),
|
|
535
|
+
};
|
|
536
|
+
};
|
|
416
537
|
// Where the members were moved, kept inside the graph's box.
|
|
417
|
-
const total = room +
|
|
418
|
-
memberHeight +
|
|
419
|
-
(hasConnections ? CONNECTION_GAP + connectionHeight : 0);
|
|
420
538
|
const boundsOf = (id) => {
|
|
421
|
-
const
|
|
422
|
-
const
|
|
539
|
+
const layout = layoutOf(id);
|
|
540
|
+
const under = placed.find(({ member }) => member === id)?.connections.length
|
|
423
541
|
? CONNECTION_GAP + connectionHeight
|
|
424
542
|
: 0;
|
|
425
543
|
return {
|
|
426
|
-
minDx: -
|
|
427
|
-
maxDx:
|
|
428
|
-
minDy: -
|
|
429
|
-
maxDy: total -
|
|
544
|
+
minDx: -layout.x,
|
|
545
|
+
maxDx: drawn - NODE_WIDTH - layout.x,
|
|
546
|
+
minDy: -layout.y,
|
|
547
|
+
maxDy: total - layout.y - memberHeight - under,
|
|
430
548
|
};
|
|
431
549
|
};
|
|
432
|
-
const [moved, setMoved] = useState(() => (
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
550
|
+
const [moved, setMoved] = useState(() => Object.fromEntries(members.map(member => [
|
|
551
|
+
member.id,
|
|
552
|
+
MOVED.get(movedKey(member.id)) ?? AT_PLACE,
|
|
553
|
+
])));
|
|
436
554
|
const offsetOf = (id) => clampOffset(moved[id] ?? AT_PLACE, boundsOf(id));
|
|
437
|
-
const entryOffset = offsetOf(entry.id);
|
|
438
|
-
const peerOffset = offsetOf(peer.id);
|
|
439
555
|
const movingValue = {
|
|
440
556
|
zoom: viewport.zoom,
|
|
441
557
|
offsetOf,
|
|
@@ -449,66 +565,55 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
449
565
|
setMoved(current => ({ ...current, [id]: AT_PLACE }));
|
|
450
566
|
},
|
|
451
567
|
};
|
|
452
|
-
|
|
568
|
+
// Each member's place and move, and its connections: the nodes are made
|
|
569
|
+
// again only when one of them changes.
|
|
570
|
+
const layout = members.map(member => {
|
|
571
|
+
const at = layoutOf(member.id);
|
|
572
|
+
const offset = offsetOf(member.id);
|
|
573
|
+
return {
|
|
574
|
+
id: member.id,
|
|
575
|
+
x: at.x + offset.dx,
|
|
576
|
+
y: at.y + offset.dy,
|
|
577
|
+
// Its balloon opens toward the middle of the graph.
|
|
578
|
+
side: at.x + NODE_WIDTH / 2 <= drawn / 2
|
|
579
|
+
? 'left'
|
|
580
|
+
: 'right',
|
|
581
|
+
connections: (member.connections ?? []).map(connection => connection.id),
|
|
582
|
+
};
|
|
583
|
+
});
|
|
584
|
+
const layoutKey = JSON.stringify(layout);
|
|
585
|
+
const nodes = useMemo(() => JSON.parse(layoutKey).flatMap(({ id, x, y, side, connections }) => [
|
|
453
586
|
{
|
|
454
|
-
id
|
|
587
|
+
id,
|
|
455
588
|
type: 'member',
|
|
456
|
-
position: { x
|
|
589
|
+
position: { x, y },
|
|
457
590
|
width: NODE_WIDTH,
|
|
458
591
|
height: memberHeight,
|
|
459
592
|
draggable: false,
|
|
460
593
|
selectable: false,
|
|
461
594
|
// Above the edge and its label: a balloon overflows its node.
|
|
462
595
|
zIndex: 10,
|
|
463
|
-
data: { id
|
|
596
|
+
data: { id, side, size },
|
|
464
597
|
},
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
y: room + peerOffset.dy,
|
|
471
|
-
},
|
|
472
|
-
width: NODE_WIDTH,
|
|
473
|
-
height: memberHeight,
|
|
474
|
-
draggable: false,
|
|
475
|
-
selectable: false,
|
|
476
|
-
zIndex: 10,
|
|
477
|
-
data: { id: peer.id, side: 'right', size },
|
|
478
|
-
},
|
|
479
|
-
...JSON.parse(placedKey).flatMap(({ member, connections }, side) => {
|
|
480
|
-
// A member's connections move with it.
|
|
481
|
-
const moveBy = side === 0 ? entryOffset : peerOffset;
|
|
482
|
-
const xs = connectionsX((side === 0 ? 0 : NODE_WIDTH + GAP) + moveBy.dx, connections.length);
|
|
483
|
-
return connections.map((connection, at) => ({
|
|
484
|
-
id: connectionNodeId(member, connection),
|
|
598
|
+
// A member's connections move with it.
|
|
599
|
+
...connections.map((connection, at) => {
|
|
600
|
+
const xs = connectionsX(x, connections.length);
|
|
601
|
+
return {
|
|
602
|
+
id: connectionNodeId(id, connection),
|
|
485
603
|
type: 'connection',
|
|
486
604
|
position: {
|
|
487
605
|
x: xs[at],
|
|
488
|
-
y:
|
|
606
|
+
y: y + memberHeight + CONNECTION_GAP,
|
|
489
607
|
},
|
|
490
608
|
width: CONNECTION_WIDTH,
|
|
491
609
|
height: connectionHeight,
|
|
492
610
|
draggable: false,
|
|
493
611
|
selectable: false,
|
|
494
612
|
zIndex: 5,
|
|
495
|
-
data: { member, connection, size: connectionSize },
|
|
496
|
-
}
|
|
613
|
+
data: { member: id, connection, size: connectionSize },
|
|
614
|
+
};
|
|
497
615
|
}),
|
|
498
|
-
], [
|
|
499
|
-
entry.id,
|
|
500
|
-
peer.id,
|
|
501
|
-
size,
|
|
502
|
-
room,
|
|
503
|
-
memberHeight,
|
|
504
|
-
placedKey,
|
|
505
|
-
connectionSize,
|
|
506
|
-
connectionHeight,
|
|
507
|
-
entryOffset.dx,
|
|
508
|
-
entryOffset.dy,
|
|
509
|
-
peerOffset.dx,
|
|
510
|
-
peerOffset.dy,
|
|
511
|
-
]);
|
|
616
|
+
]), [layoutKey, size, memberHeight, connectionSize, connectionHeight]);
|
|
512
617
|
// The notebook open under the graph runs on a Pyodide kernel in the page:
|
|
513
618
|
// the entry's sandbox, while it is open.
|
|
514
619
|
const notebookKernel = useSignalValue(teamNotebookKernel);
|
|
@@ -526,16 +631,23 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
526
631
|
}
|
|
527
632
|
: { kind: 'browser', status: 'not running' },
|
|
528
633
|
}, [entry, notebookKernel]);
|
|
529
|
-
const
|
|
530
|
-
|
|
634
|
+
const membersById = useMemo(() => ({
|
|
635
|
+
[entry.id]: entryMember,
|
|
636
|
+
...Object.fromEntries(peers.map(one => [one.id, one])),
|
|
637
|
+
}), [entry.id, entryMember, peers]);
|
|
638
|
+
// The words for each flow, for the edges and for a screen reader.
|
|
639
|
+
const wordsOf = (link) => link.from === entry.id
|
|
640
|
+
? flowWords(flowOf(link.to), entry.name, memberOf(link.to)?.name ?? link.to)
|
|
641
|
+
: '';
|
|
642
|
+
const words = edgesWanted.map(wordsOf).filter(Boolean);
|
|
531
643
|
// The tool each connection is answering now, in words, by its node's id.
|
|
532
644
|
const busy = useMemo(() => {
|
|
533
645
|
const byNode = {};
|
|
534
646
|
const said = [];
|
|
535
647
|
for (const call of calls) {
|
|
536
|
-
const member =
|
|
537
|
-
const connection = member
|
|
538
|
-
if (!
|
|
648
|
+
const member = members.find(one => one.id === call.member);
|
|
649
|
+
const connection = member?.connections?.find(known => known.id === call.connection);
|
|
650
|
+
if (!member || !connection) {
|
|
539
651
|
continue;
|
|
540
652
|
}
|
|
541
653
|
const node = connectionNodeId(member.id, connection.id);
|
|
@@ -543,28 +655,35 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
543
655
|
said.push(callWords(member.name, connection, call.tool));
|
|
544
656
|
}
|
|
545
657
|
return { byNode, said: [...new Set(said)] };
|
|
546
|
-
}, [calls,
|
|
658
|
+
}, [calls, members]);
|
|
659
|
+
const linkKey = JSON.stringify(edgesWanted.map(link => ({
|
|
660
|
+
...link,
|
|
661
|
+
flow: link.from === entry.id ? flowOf(link.to) : 'still',
|
|
662
|
+
connected: connectedOf(link.to),
|
|
663
|
+
label: wordsOf(link)
|
|
664
|
+
? `${labelOf(link.to)} · ${wordsOf(link)}`
|
|
665
|
+
: labelOf(link.to),
|
|
666
|
+
...handlesBetween(layoutOf(link.from), layoutOf(link.to)),
|
|
667
|
+
})));
|
|
547
668
|
const edges = useMemo(() => [
|
|
548
|
-
{
|
|
549
|
-
id: `${
|
|
550
|
-
source:
|
|
551
|
-
|
|
669
|
+
...JSON.parse(linkKey).map(({ from, to, flow: linkFlow, connected: reached, label: said, sourceHandle, targetHandle, }) => ({
|
|
670
|
+
id: `${from}->${to}`,
|
|
671
|
+
source: from,
|
|
672
|
+
sourceHandle,
|
|
673
|
+
target: to,
|
|
674
|
+
targetHandle,
|
|
552
675
|
type: 'a2a',
|
|
553
676
|
selectable: false,
|
|
554
677
|
focusable: false,
|
|
555
|
-
data: {
|
|
556
|
-
|
|
557
|
-
connected,
|
|
558
|
-
label: words ? `${label} · ${words}` : label,
|
|
559
|
-
},
|
|
560
|
-
},
|
|
678
|
+
data: { flow: linkFlow, connected: reached, label: said },
|
|
679
|
+
})),
|
|
561
680
|
...JSON.parse(placedKey).flatMap(({ member, connections }) => connections.map(connection => {
|
|
562
681
|
const node = connectionNodeId(member, connection);
|
|
563
682
|
const calling = busy.byNode[node] ?? '';
|
|
564
683
|
return {
|
|
565
684
|
id: `${member}->${node}`,
|
|
566
685
|
source: member,
|
|
567
|
-
sourceHandle:
|
|
686
|
+
sourceHandle: MEMBER_HANDLES.connections,
|
|
568
687
|
target: node,
|
|
569
688
|
type: 'mcp',
|
|
570
689
|
selectable: false,
|
|
@@ -572,9 +691,10 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
572
691
|
data: { calling },
|
|
573
692
|
};
|
|
574
693
|
})),
|
|
575
|
-
], [
|
|
576
|
-
const heard = [words, ...busy.said].filter(Boolean).join('. ');
|
|
577
|
-
|
|
694
|
+
], [linkKey, placedKey, busy]);
|
|
695
|
+
const heard = [...words, ...busy.said].filter(Boolean).join('. ');
|
|
696
|
+
const flowNow = flowOfAll(peers.map(one => flowOf(one.id)));
|
|
697
|
+
return (_jsxs(Box, { ref: box, "data-a2a-team-graph": "", "data-a2a-flow": flowNow, "data-a2a-calls": calls.length, "data-a2a-members": members.length, sx: {
|
|
578
698
|
width: '100%',
|
|
579
699
|
height,
|
|
580
700
|
position: 'relative',
|
|
@@ -626,8 +746,8 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
626
746
|
overflow: 'hidden',
|
|
627
747
|
clip: 'rect(0 0 0 0)',
|
|
628
748
|
whiteSpace: 'nowrap',
|
|
629
|
-
}, children: heard }), _jsx(Moving.Provider, { value: movingValue, children: _jsx(Members.Provider, { value:
|
|
749
|
+
}, children: heard }), _jsx(Moving.Provider, { value: movingValue, children: _jsx(Members.Provider, { value: membersById, children: _jsx(Busy.Provider, { value: busy.byNode, children: _jsx(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: NODE_TYPES, edgeTypes: EDGE_TYPES, viewport: viewport, nodesDraggable: false, nodesConnectable: false, nodesFocusable: false, edgesFocusable: false, elementsSelectable: false, deleteKeyCode: null, selectionKeyCode: null, multiSelectionKeyCode: null, panOnDrag: false, panOnScroll: false, zoomOnScroll: false, zoomOnPinch: false, zoomOnDoubleClick: false, preventScrolling: false, style: { overflow: 'visible', background: 'transparent' },
|
|
630
750
|
// The library is MIT: its credit is not required on the page.
|
|
631
|
-
proOptions: { hideAttribution: true }, "aria-label": `${entry.name} and ${
|
|
751
|
+
proOptions: { hideAttribution: true }, "aria-label": `${entry.name} and ${peers.map(one => one.name).join(', ')}, over A2A` }) }) }) })] }));
|
|
632
752
|
}
|
|
633
753
|
export default A2ATeamGraph;
|