@datalayer/agent-runtimes 1.3.85 → 1.3.87
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 +115 -20
- package/lib/components/teams/A2ATeamGraph.js +246 -109
- 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/sceneSchema.d.ts +8 -0
- package/lib/specs/sceneSchema.js +852 -0
- 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_envvars.py +1 -1
- package/scripts/codegen/generate_scenes.py +343 -0
|
@@ -21,11 +21,19 @@ 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
|
-
/** Between a member and its connections. */
|
|
28
|
+
/** Between a member and its connections, to its right. */
|
|
27
29
|
const CONNECTION_GAP = 44;
|
|
28
30
|
const CONNECTIONS_APART = 16;
|
|
31
|
+
/** Where a member's connections start: right of it. */
|
|
32
|
+
const CONNECTION_X = NODE_WIDTH + CONNECTION_GAP;
|
|
33
|
+
/** Below the character's middle, so the A2A edge leaving it passes above. */
|
|
34
|
+
const CONNECTION_DROP = 12;
|
|
35
|
+
/** What a member's connections take beside it. */
|
|
36
|
+
const CONNECTIONS_BESIDE = CONNECTION_GAP + CONNECTION_WIDTH;
|
|
29
37
|
/** Where the edge leaves and reaches a member: the middle of its character. */
|
|
30
38
|
const HANDLE = (size) => ({
|
|
31
39
|
opacity: 0,
|
|
@@ -37,6 +45,50 @@ const HANDLE = (size) => ({
|
|
|
37
45
|
minHeight: 0,
|
|
38
46
|
border: 0,
|
|
39
47
|
});
|
|
48
|
+
/**
|
|
49
|
+
* The handles an edge leaves and reaches a member by, named: the middle of
|
|
50
|
+
* its character on each side, chosen by where the other member stands.
|
|
51
|
+
*/
|
|
52
|
+
export const MEMBER_HANDLES = {
|
|
53
|
+
inLeft: 'in-left',
|
|
54
|
+
outRight: 'out-right',
|
|
55
|
+
inTop: 'in-top',
|
|
56
|
+
outBottom: 'out-bottom',
|
|
57
|
+
inRight: 'in-right',
|
|
58
|
+
outLeft: 'out-left',
|
|
59
|
+
inBottom: 'in-bottom',
|
|
60
|
+
outTop: 'out-top',
|
|
61
|
+
connections: 'connections',
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The handles for an edge from one place to another: rightward across,
|
|
65
|
+
* leftward across, or down or up when the other member is mostly below or
|
|
66
|
+
* above.
|
|
67
|
+
*/
|
|
68
|
+
export function handlesBetween(from, to) {
|
|
69
|
+
const dx = to.x - from.x;
|
|
70
|
+
const dy = to.y - from.y;
|
|
71
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
72
|
+
return dx >= 0
|
|
73
|
+
? {
|
|
74
|
+
sourceHandle: MEMBER_HANDLES.outRight,
|
|
75
|
+
targetHandle: MEMBER_HANDLES.inLeft,
|
|
76
|
+
}
|
|
77
|
+
: {
|
|
78
|
+
sourceHandle: MEMBER_HANDLES.outLeft,
|
|
79
|
+
targetHandle: MEMBER_HANDLES.inRight,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return dy >= 0
|
|
83
|
+
? {
|
|
84
|
+
sourceHandle: MEMBER_HANDLES.outBottom,
|
|
85
|
+
targetHandle: MEMBER_HANDLES.inTop,
|
|
86
|
+
}
|
|
87
|
+
: {
|
|
88
|
+
sourceHandle: MEMBER_HANDLES.outTop,
|
|
89
|
+
targetHandle: MEMBER_HANDLES.inBottom,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
40
92
|
/**
|
|
41
93
|
* The members as they are now, by id. Not in the nodes' data: a node whose
|
|
42
94
|
* object changes is taken by React Flow for a new one, measured again, and
|
|
@@ -72,7 +124,7 @@ const Moving = createContext({
|
|
|
72
124
|
move: () => undefined,
|
|
73
125
|
reset: () => undefined,
|
|
74
126
|
});
|
|
75
|
-
/** A connection's node id:
|
|
127
|
+
/** A connection's node id: beside its member. */
|
|
76
128
|
const connectionNodeId = (member, connection) => `${member}/${connection}`;
|
|
77
129
|
const MemberNode = memo(function MemberNode({ data, }) {
|
|
78
130
|
const { id, side, size } = data;
|
|
@@ -159,7 +211,7 @@ const MemberNode = memo(function MemberNode({ data, }) {
|
|
|
159
211
|
pointerEvents: 'all',
|
|
160
212
|
}, "data-team-member": member.id, "data-member-balloon": shown, "data-member-moved": offset.dx || offset.dy
|
|
161
213
|
? `${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,
|
|
214
|
+
: 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
215
|
// In the node, not over the page; the balloon opens toward the
|
|
164
216
|
// middle of the graph: rightward from the left member.
|
|
165
217
|
place: side === 'left'
|
|
@@ -236,7 +288,7 @@ const MemberNode = memo(function MemberNode({ data, }) {
|
|
|
236
288
|
stayPut: true,
|
|
237
289
|
// Named as the member, not its character: `Sales (in your
|
|
238
290
|
// 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:
|
|
291
|
+
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.Right, style: { ...HANDLE(size), left: 'auto', right: 0 }, isConnectable: false })) : null] }));
|
|
240
292
|
});
|
|
241
293
|
/** A member's accessible name: its name, and where it runs. */
|
|
242
294
|
export function memberLabel(member) {
|
|
@@ -268,7 +320,7 @@ const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
|
|
|
268
320
|
gap: '2px',
|
|
269
321
|
cursor: 'default',
|
|
270
322
|
pointerEvents: 'all',
|
|
271
|
-
}, title: connection.name, "data-team-connection": connection.id, "data-connection-busy": tool ? 'true' : 'false', children: [_jsx(Handle, { type: "target", position: Position.
|
|
323
|
+
}, title: connection.name, "data-team-connection": connection.id, "data-connection-busy": tool ? 'true' : 'false', children: [_jsx(Handle, { type: "target", position: Position.Left, style: { ...HANDLE(size), left: 0 }, isConnectable: false }), _jsx(Box, { as: "button", type: "button", className: tool ? 'a2a-team-connection-busy' : undefined, "aria-label": connectionLabel(connection, owner?.name), "aria-expanded": told, "aria-controls": detailsId, "data-connection-figure": "", onClick: () => setTold(shown => !shown), onKeyDown: (event) => {
|
|
272
324
|
if (event.key === 'Escape' && told) {
|
|
273
325
|
event.stopPropagation();
|
|
274
326
|
setTold(false);
|
|
@@ -317,14 +369,29 @@ export function flowWords(flow, entry, peer) {
|
|
|
317
369
|
: '';
|
|
318
370
|
}
|
|
319
371
|
const ARROW = 9;
|
|
372
|
+
/**
|
|
373
|
+
* An arrowhead at the end a message goes to — the target's when `forward`,
|
|
374
|
+
* the source's otherwise — pointing along the edge.
|
|
375
|
+
*/
|
|
376
|
+
export function arrowPath(source, target, forward) {
|
|
377
|
+
const tip = forward ? target : source;
|
|
378
|
+
const from = forward ? source : target;
|
|
379
|
+
const length = Math.hypot(tip.x - from.x, tip.y - from.y) || 1;
|
|
380
|
+
// Along the edge, toward the tip; and across it.
|
|
381
|
+
const ax = (tip.x - from.x) / length;
|
|
382
|
+
const ay = (tip.y - from.y) / length;
|
|
383
|
+
const bx = tip.x - ax * ARROW;
|
|
384
|
+
const by = tip.y - ay * ARROW;
|
|
385
|
+
const px = -ay * (ARROW / 1.6);
|
|
386
|
+
const py = ax * (ARROW / 1.6);
|
|
387
|
+
return `M ${tip.x},${tip.y} L ${bx + px},${by + py} L ${bx - px},${by - py} Z`;
|
|
388
|
+
}
|
|
320
389
|
const LinkEdge = memo(function LinkEdge({ id, sourceX, sourceY, targetX, targetY, data, }) {
|
|
321
390
|
const flow = data?.flow ?? 'still';
|
|
322
391
|
const path = `M ${sourceX},${sourceY} L ${targetX},${targetY}`;
|
|
323
|
-
// The arrow at the end the message goes to.
|
|
392
|
+
// The arrow at the end the message goes to, along the edge.
|
|
324
393
|
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`;
|
|
394
|
+
const arrow = arrowPath({ x: sourceX, y: sourceY }, { x: targetX, y: targetY }, towardPeer);
|
|
328
395
|
return (_jsxs(_Fragment, { children: [_jsx(BaseEdge, { id: id, path: path, style: {
|
|
329
396
|
stroke: data?.connected
|
|
330
397
|
? 'var(--borderColor-accent-emphasis, #0969da)'
|
|
@@ -358,23 +425,78 @@ export const CallEdge = memo(function CallEdge({ id, sourceX, sourceY, targetX,
|
|
|
358
425
|
});
|
|
359
426
|
const NODE_TYPES = { member: MemberNode, connection: ConnectionNode };
|
|
360
427
|
const EDGE_TYPES = { a2a: LinkEdge, mcp: CallEdge };
|
|
361
|
-
/** Where a member's connections sit:
|
|
362
|
-
function
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
428
|
+
/** Where a member's connections sit: right of it, one under another from its character's middle. */
|
|
429
|
+
function connectionsY(memberY, size, count, connectionHeight) {
|
|
430
|
+
const top = memberY + size / 2 + CONNECTION_DROP;
|
|
431
|
+
return Array.from({ length: count }, (_, at) => top + at * (connectionHeight + CONNECTIONS_APART));
|
|
432
|
+
}
|
|
433
|
+
/** How far down a member's connections reach, from its top; none, 0. */
|
|
434
|
+
function connectionsReach(size, count, connectionHeight) {
|
|
435
|
+
return count
|
|
436
|
+
? size / 2 +
|
|
437
|
+
CONNECTION_DROP +
|
|
438
|
+
count * connectionHeight +
|
|
439
|
+
(count - 1) * CONNECTIONS_APART
|
|
440
|
+
: 0;
|
|
366
441
|
}
|
|
367
442
|
/** What a member's call says, on its edge and to a screen reader. */
|
|
368
443
|
export function callWords(member, connection, tool) {
|
|
369
444
|
return `${member} calls ${connection.label} · ${toolWords(tool, connection)}`;
|
|
370
445
|
}
|
|
371
|
-
/** The viewport that shows
|
|
372
|
-
export function teamViewport(width) {
|
|
373
|
-
const zoom = Math.min(1, width /
|
|
374
|
-
return { zoom, x: (width -
|
|
446
|
+
/** The viewport that shows a graph `drawn` wide whole in a box `width` wide, never larger than drawn. */
|
|
447
|
+
export function teamViewport(width, drawn = WIDTH) {
|
|
448
|
+
const zoom = Math.min(1, width / drawn);
|
|
449
|
+
return { zoom, x: (width - drawn * zoom) / 2, y: 0 };
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Where each member stands, as columns and rows: from the positions given
|
|
453
|
+
* (the distinct `x` values are the columns left to right, the distinct `y`
|
|
454
|
+
* values the rows top to bottom), or the entry in the first column and the
|
|
455
|
+
* peers one under another in the second.
|
|
456
|
+
*/
|
|
457
|
+
export function placeMembers(entryId, peerIds, positions) {
|
|
458
|
+
const ids = [entryId, ...peerIds];
|
|
459
|
+
const placed = positions ?? {};
|
|
460
|
+
if (ids.every(id => placed[id])) {
|
|
461
|
+
const xs = [...new Set(ids.map(id => placed[id].x))].sort((a, b) => a - b);
|
|
462
|
+
const ys = [...new Set(ids.map(id => placed[id].y))].sort((a, b) => a - b);
|
|
463
|
+
return {
|
|
464
|
+
places: Object.fromEntries(ids.map(id => [
|
|
465
|
+
id,
|
|
466
|
+
{ column: xs.indexOf(placed[id].x), row: ys.indexOf(placed[id].y) },
|
|
467
|
+
])),
|
|
468
|
+
columns: xs.length,
|
|
469
|
+
rows: ys.length,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
places: {
|
|
474
|
+
[entryId]: { column: 0, row: 0 },
|
|
475
|
+
...Object.fromEntries(peerIds.map((id, at) => [id, { column: 1, row: at }])),
|
|
476
|
+
},
|
|
477
|
+
columns: peerIds.length ? 2 : 1,
|
|
478
|
+
rows: Math.max(1, peerIds.length),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/** The one flow the graph reports for all its edges: asking before answering before still. */
|
|
482
|
+
export function flowOfAll(flows) {
|
|
483
|
+
return flows.includes('asking')
|
|
484
|
+
? 'asking'
|
|
485
|
+
: flows.includes('answering')
|
|
486
|
+
? 'answering'
|
|
487
|
+
: 'still';
|
|
375
488
|
}
|
|
376
|
-
|
|
377
|
-
|
|
489
|
+
const NO_PEERS = [];
|
|
490
|
+
/** Draw a team over A2A, each link moving with what travels on it. */
|
|
491
|
+
export function A2ATeamGraph({ entry, peer, peers: more = NO_PEERS, flow, flows, connected, label = 'A2A', labels, links, positions, size = 96, calls = [], balloonRoom = BALLOON_ROOM, }) {
|
|
492
|
+
const peers = useMemo(() => [...(peer ? [peer] : []), ...more], [peer, more]);
|
|
493
|
+
const members = useMemo(() => [entry, ...peers], [entry, peers]);
|
|
494
|
+
const memberOf = (id) => members.find(member => member.id === id);
|
|
495
|
+
const flowOf = (peerId) => flows?.[peerId] ??
|
|
496
|
+
(peer && peerId === peer.id ? (flow ?? 'still') : 'still');
|
|
497
|
+
const connectedOf = (peerId) => typeof connected === 'boolean' ? connected : Boolean(connected[peerId]);
|
|
498
|
+
const labelOf = (peerId) => labels?.[peerId] ?? label;
|
|
499
|
+
const edgesWanted = links ?? peers.map(one => ({ from: entry.id, to: one.id }));
|
|
378
500
|
const box = useRef(null);
|
|
379
501
|
const [width, setWidth] = useState(0);
|
|
380
502
|
useEffect(() => {
|
|
@@ -391,7 +513,15 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
391
513
|
observer.observe(element);
|
|
392
514
|
return () => observer.disconnect();
|
|
393
515
|
}, []);
|
|
394
|
-
|
|
516
|
+
// Where the members stand, as columns and rows, and the width that takes.
|
|
517
|
+
const peerIds = peers.map(one => one.id);
|
|
518
|
+
const placing = placeMembers(entry.id, peerIds, positions);
|
|
519
|
+
const hasConnections = members.some(member => member.connections?.length);
|
|
520
|
+
// The last column's connections sit beside it, inside the graph.
|
|
521
|
+
const drawn = Math.max(WIDTH, placing.columns * NODE_WIDTH +
|
|
522
|
+
(placing.columns - 1) * GAP +
|
|
523
|
+
(hasConnections ? CONNECTIONS_BESIDE : 0));
|
|
524
|
+
const viewport = useMemo(() => teamViewport(width || drawn, drawn), [width, drawn]);
|
|
395
525
|
// The members' height: the character, its name and where it runs.
|
|
396
526
|
const memberHeight = size + 56;
|
|
397
527
|
const connectionSize = Math.round(size / 2);
|
|
@@ -399,43 +529,46 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
399
529
|
// and room under it before the graph's edge.
|
|
400
530
|
const connectionHeight = connectionSize + 58;
|
|
401
531
|
// The connections' ids, member by member: the nodes change only when they do.
|
|
402
|
-
const placed =
|
|
532
|
+
const placed = members.map(member => ({
|
|
403
533
|
member: member.id,
|
|
404
534
|
connections: (member.connections ?? []).map(connection => connection.id),
|
|
405
535
|
}));
|
|
406
536
|
const placedKey = JSON.stringify(placed);
|
|
407
|
-
const hasConnections = placed.some(({ connections }) => connections.length);
|
|
408
537
|
// A notebook in a balloon: more room above the members.
|
|
409
|
-
const room =
|
|
538
|
+
const room = members.some(member => member.persona.notebook)
|
|
410
539
|
? Math.max(NOTEBOOK_BALLOON_ROOM, balloonRoom)
|
|
411
540
|
: balloonRoom;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
541
|
+
// Beside a member, its connections may reach below it: the row takes them.
|
|
542
|
+
const reach = Math.max(0, ...placed.map(({ connections }) => connectionsReach(size, connections.length, connectionHeight)));
|
|
543
|
+
const rowHeight = Math.max(memberHeight, reach);
|
|
544
|
+
const total = room + placing.rows * rowHeight + (placing.rows - 1) * ROW_GAP;
|
|
545
|
+
const height = Math.ceil(total * viewport.zoom);
|
|
546
|
+
// Where each member is laid out, before it is moved.
|
|
547
|
+
const layoutOf = (id) => {
|
|
548
|
+
const place = placing.places[id] ?? { column: 0, row: 0 };
|
|
549
|
+
return {
|
|
550
|
+
x: place.column * (NODE_WIDTH + GAP),
|
|
551
|
+
y: room + place.row * (rowHeight + ROW_GAP),
|
|
552
|
+
};
|
|
553
|
+
};
|
|
416
554
|
// Where the members were moved, kept inside the graph's box.
|
|
417
|
-
const total = room +
|
|
418
|
-
memberHeight +
|
|
419
|
-
(hasConnections ? CONNECTION_GAP + connectionHeight : 0);
|
|
420
555
|
const boundsOf = (id) => {
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
556
|
+
const layout = layoutOf(id);
|
|
557
|
+
const count = placed.find(({ member }) => member === id)?.connections.length ?? 0;
|
|
558
|
+
const beside = count ? CONNECTIONS_BESIDE : 0;
|
|
559
|
+
const tall = Math.max(memberHeight, connectionsReach(size, count, connectionHeight));
|
|
425
560
|
return {
|
|
426
|
-
minDx: -
|
|
427
|
-
maxDx:
|
|
428
|
-
minDy: -
|
|
429
|
-
maxDy: total -
|
|
561
|
+
minDx: -layout.x,
|
|
562
|
+
maxDx: drawn - NODE_WIDTH - beside - layout.x,
|
|
563
|
+
minDy: -layout.y,
|
|
564
|
+
maxDy: total - layout.y - tall,
|
|
430
565
|
};
|
|
431
566
|
};
|
|
432
|
-
const [moved, setMoved] = useState(() => (
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
567
|
+
const [moved, setMoved] = useState(() => Object.fromEntries(members.map(member => [
|
|
568
|
+
member.id,
|
|
569
|
+
MOVED.get(movedKey(member.id)) ?? AT_PLACE,
|
|
570
|
+
])));
|
|
436
571
|
const offsetOf = (id) => clampOffset(moved[id] ?? AT_PLACE, boundsOf(id));
|
|
437
|
-
const entryOffset = offsetOf(entry.id);
|
|
438
|
-
const peerOffset = offsetOf(peer.id);
|
|
439
572
|
const movingValue = {
|
|
440
573
|
zoom: viewport.zoom,
|
|
441
574
|
offsetOf,
|
|
@@ -449,66 +582,55 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
449
582
|
setMoved(current => ({ ...current, [id]: AT_PLACE }));
|
|
450
583
|
},
|
|
451
584
|
};
|
|
452
|
-
|
|
585
|
+
// Each member's place and move, and its connections: the nodes are made
|
|
586
|
+
// again only when one of them changes.
|
|
587
|
+
const layout = members.map(member => {
|
|
588
|
+
const at = layoutOf(member.id);
|
|
589
|
+
const offset = offsetOf(member.id);
|
|
590
|
+
return {
|
|
591
|
+
id: member.id,
|
|
592
|
+
x: at.x + offset.dx,
|
|
593
|
+
y: at.y + offset.dy,
|
|
594
|
+
// Its balloon opens toward the middle of the graph.
|
|
595
|
+
side: at.x + NODE_WIDTH / 2 <= drawn / 2
|
|
596
|
+
? 'left'
|
|
597
|
+
: 'right',
|
|
598
|
+
connections: (member.connections ?? []).map(connection => connection.id),
|
|
599
|
+
};
|
|
600
|
+
});
|
|
601
|
+
const layoutKey = JSON.stringify(layout);
|
|
602
|
+
const nodes = useMemo(() => JSON.parse(layoutKey).flatMap(({ id, x, y, side, connections }) => [
|
|
453
603
|
{
|
|
454
|
-
id
|
|
604
|
+
id,
|
|
455
605
|
type: 'member',
|
|
456
|
-
position: { x
|
|
606
|
+
position: { x, y },
|
|
457
607
|
width: NODE_WIDTH,
|
|
458
608
|
height: memberHeight,
|
|
459
609
|
draggable: false,
|
|
460
610
|
selectable: false,
|
|
461
611
|
// Above the edge and its label: a balloon overflows its node.
|
|
462
612
|
zIndex: 10,
|
|
463
|
-
data: { id
|
|
464
|
-
},
|
|
465
|
-
{
|
|
466
|
-
id: peer.id,
|
|
467
|
-
type: 'member',
|
|
468
|
-
position: {
|
|
469
|
-
x: NODE_WIDTH + GAP + peerOffset.dx,
|
|
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 },
|
|
613
|
+
data: { id, side, size },
|
|
478
614
|
},
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
id: connectionNodeId(member, connection),
|
|
615
|
+
// A member's connections move with it.
|
|
616
|
+
...connections.map((connection, at) => {
|
|
617
|
+
const ys = connectionsY(y, size, connections.length, connectionHeight);
|
|
618
|
+
return {
|
|
619
|
+
id: connectionNodeId(id, connection),
|
|
485
620
|
type: 'connection',
|
|
486
621
|
position: {
|
|
487
|
-
x:
|
|
488
|
-
y:
|
|
622
|
+
x: x + CONNECTION_X,
|
|
623
|
+
y: ys[at],
|
|
489
624
|
},
|
|
490
625
|
width: CONNECTION_WIDTH,
|
|
491
626
|
height: connectionHeight,
|
|
492
627
|
draggable: false,
|
|
493
628
|
selectable: false,
|
|
494
629
|
zIndex: 5,
|
|
495
|
-
data: { member, connection, size: connectionSize },
|
|
496
|
-
}
|
|
630
|
+
data: { member: id, connection, size: connectionSize },
|
|
631
|
+
};
|
|
497
632
|
}),
|
|
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
|
-
]);
|
|
633
|
+
]), [layoutKey, size, memberHeight, connectionSize, connectionHeight]);
|
|
512
634
|
// The notebook open under the graph runs on a Pyodide kernel in the page:
|
|
513
635
|
// the entry's sandbox, while it is open.
|
|
514
636
|
const notebookKernel = useSignalValue(teamNotebookKernel);
|
|
@@ -526,16 +648,23 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
526
648
|
}
|
|
527
649
|
: { kind: 'browser', status: 'not running' },
|
|
528
650
|
}, [entry, notebookKernel]);
|
|
529
|
-
const
|
|
530
|
-
|
|
651
|
+
const membersById = useMemo(() => ({
|
|
652
|
+
[entry.id]: entryMember,
|
|
653
|
+
...Object.fromEntries(peers.map(one => [one.id, one])),
|
|
654
|
+
}), [entry.id, entryMember, peers]);
|
|
655
|
+
// The words for each flow, for the edges and for a screen reader.
|
|
656
|
+
const wordsOf = (link) => link.from === entry.id
|
|
657
|
+
? flowWords(flowOf(link.to), entry.name, memberOf(link.to)?.name ?? link.to)
|
|
658
|
+
: '';
|
|
659
|
+
const words = edgesWanted.map(wordsOf).filter(Boolean);
|
|
531
660
|
// The tool each connection is answering now, in words, by its node's id.
|
|
532
661
|
const busy = useMemo(() => {
|
|
533
662
|
const byNode = {};
|
|
534
663
|
const said = [];
|
|
535
664
|
for (const call of calls) {
|
|
536
|
-
const member =
|
|
537
|
-
const connection = member
|
|
538
|
-
if (!
|
|
665
|
+
const member = members.find(one => one.id === call.member);
|
|
666
|
+
const connection = member?.connections?.find(known => known.id === call.connection);
|
|
667
|
+
if (!member || !connection) {
|
|
539
668
|
continue;
|
|
540
669
|
}
|
|
541
670
|
const node = connectionNodeId(member.id, connection.id);
|
|
@@ -543,28 +672,35 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
543
672
|
said.push(callWords(member.name, connection, call.tool));
|
|
544
673
|
}
|
|
545
674
|
return { byNode, said: [...new Set(said)] };
|
|
546
|
-
}, [calls,
|
|
675
|
+
}, [calls, members]);
|
|
676
|
+
const linkKey = JSON.stringify(edgesWanted.map(link => ({
|
|
677
|
+
...link,
|
|
678
|
+
flow: link.from === entry.id ? flowOf(link.to) : 'still',
|
|
679
|
+
connected: connectedOf(link.to),
|
|
680
|
+
label: wordsOf(link)
|
|
681
|
+
? `${labelOf(link.to)} · ${wordsOf(link)}`
|
|
682
|
+
: labelOf(link.to),
|
|
683
|
+
...handlesBetween(layoutOf(link.from), layoutOf(link.to)),
|
|
684
|
+
})));
|
|
547
685
|
const edges = useMemo(() => [
|
|
548
|
-
{
|
|
549
|
-
id: `${
|
|
550
|
-
source:
|
|
551
|
-
|
|
686
|
+
...JSON.parse(linkKey).map(({ from, to, flow: linkFlow, connected: reached, label: said, sourceHandle, targetHandle, }) => ({
|
|
687
|
+
id: `${from}->${to}`,
|
|
688
|
+
source: from,
|
|
689
|
+
sourceHandle,
|
|
690
|
+
target: to,
|
|
691
|
+
targetHandle,
|
|
552
692
|
type: 'a2a',
|
|
553
693
|
selectable: false,
|
|
554
694
|
focusable: false,
|
|
555
|
-
data: {
|
|
556
|
-
|
|
557
|
-
connected,
|
|
558
|
-
label: words ? `${label} · ${words}` : label,
|
|
559
|
-
},
|
|
560
|
-
},
|
|
695
|
+
data: { flow: linkFlow, connected: reached, label: said },
|
|
696
|
+
})),
|
|
561
697
|
...JSON.parse(placedKey).flatMap(({ member, connections }) => connections.map(connection => {
|
|
562
698
|
const node = connectionNodeId(member, connection);
|
|
563
699
|
const calling = busy.byNode[node] ?? '';
|
|
564
700
|
return {
|
|
565
701
|
id: `${member}->${node}`,
|
|
566
702
|
source: member,
|
|
567
|
-
sourceHandle:
|
|
703
|
+
sourceHandle: MEMBER_HANDLES.connections,
|
|
568
704
|
target: node,
|
|
569
705
|
type: 'mcp',
|
|
570
706
|
selectable: false,
|
|
@@ -572,9 +708,10 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
572
708
|
data: { calling },
|
|
573
709
|
};
|
|
574
710
|
})),
|
|
575
|
-
], [
|
|
576
|
-
const heard = [words, ...busy.said].filter(Boolean).join('. ');
|
|
577
|
-
|
|
711
|
+
], [linkKey, placedKey, busy]);
|
|
712
|
+
const heard = [...words, ...busy.said].filter(Boolean).join('. ');
|
|
713
|
+
const flowNow = flowOfAll(peers.map(one => flowOf(one.id)));
|
|
714
|
+
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
715
|
width: '100%',
|
|
579
716
|
height,
|
|
580
717
|
position: 'relative',
|
|
@@ -626,8 +763,8 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
626
763
|
overflow: 'hidden',
|
|
627
764
|
clip: 'rect(0 0 0 0)',
|
|
628
765
|
whiteSpace: 'nowrap',
|
|
629
|
-
}, children: heard }), _jsx(Moving.Provider, { value: movingValue, children: _jsx(Members.Provider, { value:
|
|
766
|
+
}, 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
767
|
// The library is MIT: its credit is not required on the page.
|
|
631
|
-
proOptions: { hideAttribution: true }, "aria-label": `${entry.name} and ${
|
|
768
|
+
proOptions: { hideAttribution: true }, "aria-label": `${entry.name} and ${peers.map(one => one.name).join(', ')}, over A2A` }) }) }) })] }));
|
|
632
769
|
}
|
|
633
770
|
export default A2ATeamGraph;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scene as a page stages it (LOOP A-13, A-15), read from the scene spec
|
|
3
|
+
* and its team: who is on stage and where each one stands, who asks whom
|
|
4
|
+
* over A2A, what the entry suggests, and where the members on a runtime are
|
|
5
|
+
* reached when the scene plays against local servers.
|
|
6
|
+
*
|
|
7
|
+
* Pure, so the examples app, the landing and the tests read a scene the
|
|
8
|
+
* same way.
|
|
9
|
+
*
|
|
10
|
+
* @module components/teams/sceneStage
|
|
11
|
+
*/
|
|
12
|
+
import type { AppSpec } from '../../types/agentspecs';
|
|
13
|
+
import type { SceneCastMemberSpec, SceneSpec } from '../../types/scenes';
|
|
14
|
+
import type { TeamSpec } from '../../types/teams';
|
|
15
|
+
/** Who talks to whom over A2A: an edge of the team's graph. */
|
|
16
|
+
export type A2ATeamLink = {
|
|
17
|
+
/** The member that asks, by its id. */
|
|
18
|
+
from: string;
|
|
19
|
+
/** The member asked. */
|
|
20
|
+
to: string;
|
|
21
|
+
};
|
|
22
|
+
/** A reference without its version: `accounting:0.0.1` is `accounting`. */
|
|
23
|
+
export declare function refId(ref: string): string;
|
|
24
|
+
/** The application a cast member is, from the catalogue, or undefined. */
|
|
25
|
+
export declare function appOfMember(member: Pick<SceneCastMemberSpec, 'app'>): AppSpec | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* The links of a team over A2A, in the order its members say them: each
|
|
28
|
+
* member's `talks_to … over: a2a`. A link to a server (over `mcp`) is a
|
|
29
|
+
* connection, drawn under the member, not an edge between members.
|
|
30
|
+
*/
|
|
31
|
+
export declare function teamLinksOf(team: Pick<TeamSpec, 'agents'> | Pick<SceneSpec, 'cast'>): A2ATeamLink[];
|
|
32
|
+
/** A suggestion the entry offers: a label on its chip, and what is sent. */
|
|
33
|
+
export type SceneSuggestion = {
|
|
34
|
+
label: string;
|
|
35
|
+
prompt: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The entry's suggestions: the script's cues, each labelled by the starter
|
|
39
|
+
* of the entry's Appspec that says the same words, or by its first words.
|
|
40
|
+
* Without a scene, the starters themselves.
|
|
41
|
+
*/
|
|
42
|
+
export declare function sceneCuesOf(scene: Pick<SceneSpec, 'script'> | undefined, starters: readonly {
|
|
43
|
+
label: string;
|
|
44
|
+
message: string;
|
|
45
|
+
}[]): SceneSuggestion[];
|
|
46
|
+
/** One member of a scene, as a page stages it. */
|
|
47
|
+
export type SceneStageMember = {
|
|
48
|
+
/** Its id in the cast. */
|
|
49
|
+
id: string;
|
|
50
|
+
/** The application it is. */
|
|
51
|
+
app: AppSpec;
|
|
52
|
+
cast: SceneCastMemberSpec;
|
|
53
|
+
/** Whether it is the member the audience talks to. */
|
|
54
|
+
entry: boolean;
|
|
55
|
+
/** Where its loop turns. */
|
|
56
|
+
runsIn: 'browser' | 'runtime';
|
|
57
|
+
/** The variable its address is read from, when it runs on a runtime. */
|
|
58
|
+
addressVariable?: string;
|
|
59
|
+
/** Where it stands: fractions of the box, from the stage directions. */
|
|
60
|
+
position?: {
|
|
61
|
+
x: number;
|
|
62
|
+
y: number;
|
|
63
|
+
};
|
|
64
|
+
/** What its application needs that is not set up, in sentences. */
|
|
65
|
+
setup: string[];
|
|
66
|
+
};
|
|
67
|
+
/** A scene as a page stages it. */
|
|
68
|
+
export type SceneStage = {
|
|
69
|
+
scene: SceneSpec;
|
|
70
|
+
entry: SceneStageMember;
|
|
71
|
+
/** The members the entry asks over A2A, in the cast's order. */
|
|
72
|
+
peers: SceneStageMember[];
|
|
73
|
+
/** Every member, the entry first. */
|
|
74
|
+
members: SceneStageMember[];
|
|
75
|
+
/** The edges between members: the team's `talks_to` over A2A. */
|
|
76
|
+
links: A2ATeamLink[];
|
|
77
|
+
/** Where each member stands, by its id. */
|
|
78
|
+
positions: Record<string, {
|
|
79
|
+
x: number;
|
|
80
|
+
y: number;
|
|
81
|
+
}>;
|
|
82
|
+
/** Whose balloon opens first. */
|
|
83
|
+
opensFirst: string;
|
|
84
|
+
/** What the audience is told: the line under the title. */
|
|
85
|
+
assumes: string;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Read a scene's stage: its members with their applications, who the entry
|
|
89
|
+
* is and whom it asks, the links, the positions. Refused when a cast member
|
|
90
|
+
* is an application the catalogue does not have, or the entry is not in the
|
|
91
|
+
* cast: a page cannot stage what it cannot draw.
|
|
92
|
+
*/
|
|
93
|
+
export declare function sceneStageOf(scene: SceneSpec): SceneStage;
|
|
94
|
+
/** The port the examples serve Accounting on, as `npm run examples` starts it. */
|
|
95
|
+
export declare const ACCOUNTING_PORT = 8767;
|
|
96
|
+
/** The first port `serve_scenes.py` starts a runtime on for the other members. */
|
|
97
|
+
export declare const SCENE_PORTS_FROM = 8768;
|
|
98
|
+
/**
|
|
99
|
+
* The members on a runtime across the catalogue's scenes, each once, in
|
|
100
|
+
* the catalogue's order (a scene's cast in its order): what
|
|
101
|
+
* `examples/sales-accounting-a2a/serve_scenes.py` serves, in that order.
|
|
102
|
+
*/
|
|
103
|
+
export declare function sceneRuntimeMembers(scenes?: readonly SceneSpec[]): string[];
|
|
104
|
+
/**
|
|
105
|
+
* The port each runtime member is served on locally: Accounting on
|
|
106
|
+
* {@link ACCOUNTING_PORT}, the others from {@link SCENE_PORTS_FROM} on, in
|
|
107
|
+
* the order of {@link sceneRuntimeMembers}. `serve_scenes.py` gives the
|
|
108
|
+
* same ports by the same rule.
|
|
109
|
+
*/
|
|
110
|
+
export declare function sceneRuntimePorts(scenes?: readonly SceneSpec[]): Record<string, number>;
|
|
111
|
+
/** The variable the examples read a member's local address from: `VITE_A2A_CHANGE_DETECTION_URL`. */
|
|
112
|
+
export declare function sceneAddressVariable(appId: string): string;
|
|
113
|
+
/** Where a member is served over A2A on a local runtime at `port`. */
|
|
114
|
+
export declare function localA2AUrl(appId: string, port: number): string;
|
|
115
|
+
/**
|
|
116
|
+
* Where a member on a runtime is reached by the examples: its variable
|
|
117
|
+
* (`VITE_A2A_<MEMBER>_URL`) when set in `env`, else the local runtime
|
|
118
|
+
* `serve_scenes.py` serves it on.
|
|
119
|
+
*/
|
|
120
|
+
export declare function sceneMemberUrl(appId: string, env: Record<string, string | undefined>, ports?: Record<string, number>): string;
|