@datalayer/agent-runtimes 1.3.64 → 1.3.66

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.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * A team of two over A2A, as a graph (React Flow): each member is its
3
+ * character (`AssistantStage`), with its name and where it runs under it, and
4
+ * one edge links the entry to the peer.
5
+ *
6
+ * The edge is still until a message travels on it. While the entry asks, it
7
+ * flows from the entry to the peer; while the peer answers, from the peer to
8
+ * the entry ({@link A2ATeamFlow}, from the `A2APeerEvent` phases). A reader
9
+ * who asks for no motion sees the same thing without the movement: the arrow
10
+ * at the end the message goes to, and the word for it on the edge.
11
+ *
12
+ * The graph is a picture, not an editor: nothing is dragged, panned or
13
+ * zoomed, and the wheel scrolls the page. Its viewport is worked out from its
14
+ * box, so it fits a phone as it fits a desk.
15
+ *
16
+ * @module components/teams/A2ATeamGraph
17
+ */
18
+ import type { JSX } from 'react';
19
+ import { type Viewport } from '@xyflow/react';
20
+ import '@xyflow/react/dist/base.css';
21
+ import type { A2ATeamFlow } from './a2aTeamFlow';
22
+ import type { A2ATeamPersona } from './useA2ATeam';
23
+ /** One member of the team, as the graph draws it. */
24
+ export type A2ATeamGraphMember = {
25
+ id: string;
26
+ name: string;
27
+ emoji?: string;
28
+ /** Its character, as its Appspec's `interface.assistant` names it. */
29
+ character: string;
30
+ /** Where it runs, in a few words: "in your browser", "on a runtime". */
31
+ where: string;
32
+ persona: A2ATeamPersona;
33
+ /** A click on the character. */
34
+ onToggle?: () => void;
35
+ /** It was sent away, or called back. */
36
+ onAway?: (away: boolean) => void;
37
+ };
38
+ export type A2ATeamGraphProps = {
39
+ /** The member that asks: drawn on the left. */
40
+ entry: A2ATeamGraphMember;
41
+ /** The member asked over A2A: drawn on the right. */
42
+ peer: A2ATeamGraphMember;
43
+ /** Which way the link carries a message now. */
44
+ flow: A2ATeamFlow;
45
+ /** Whether the peer is reached: the edge is drawn faint until it is. */
46
+ connected: boolean;
47
+ /** What the edge says at rest: `A2A · <the peer's skill>`. */
48
+ label?: string;
49
+ /** The character's size, in pixels at full scale. */
50
+ size?: number;
51
+ };
52
+ /** The words on the edge for a flow. */
53
+ export declare function flowWords(flow: A2ATeamFlow, entry: string, peer: string): string;
54
+ /** The viewport that shows the graph's width whole in a box `width` wide, never larger than drawn. */
55
+ export declare function teamViewport(width: number): Viewport;
56
+ /** Draw a team of two over A2A, the link moving with what travels on it. */
57
+ export declare function A2ATeamGraph({ entry, peer, flow, connected, label, size, }: A2ATeamGraphProps): JSX.Element;
58
+ export default A2ATeamGraph;
@@ -0,0 +1,202 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { createContext, memo, useContext, useEffect, useMemo, useRef, useState, } from 'react';
3
+ import { BaseEdge, EdgeLabelRenderer, Handle, Position, ReactFlow, } from '@xyflow/react';
4
+ import '@xyflow/react/dist/base.css';
5
+ import { Button, Label, Text } from '@primer/react';
6
+ import { Box } from '@datalayer/primer-addons';
7
+ import { AssistantStage } from '../../chat/assistant/AssistantStage';
8
+ /** The graph's own coordinates: the members, and room above them for their balloons. */
9
+ const NODE_WIDTH = 200;
10
+ const BALLOON_ROOM = 120;
11
+ const GAP = 220;
12
+ const WIDTH = NODE_WIDTH * 2 + GAP;
13
+ /** Where the edge leaves and reaches a member: the middle of its character. */
14
+ const HANDLE = (size) => ({
15
+ opacity: 0,
16
+ pointerEvents: 'none',
17
+ top: size / 2,
18
+ width: 1,
19
+ height: 1,
20
+ minWidth: 0,
21
+ minHeight: 0,
22
+ border: 0,
23
+ });
24
+ /**
25
+ * The members as they are now, by id. Not in the nodes' data: a node whose
26
+ * object changes is taken by React Flow for a new one, measured again, and
27
+ * the edge is dropped until it is — the line vanished as the characters
28
+ * moved. The nodes stay the same objects; what they show is read from here.
29
+ */
30
+ const Members = createContext({});
31
+ const MemberNode = memo(function MemberNode({ data, }) {
32
+ const { id, side, size } = data;
33
+ const member = useContext(Members)[id];
34
+ const stageRef = useRef(null);
35
+ if (!member) {
36
+ return null;
37
+ }
38
+ const { persona } = member;
39
+ return (_jsxs(Box
40
+ // React Flow lets the pointer through a node that is neither dragged
41
+ // nor selected (`pointer-events: none` on its wrapper): the character,
42
+ // its balloon and its menu take it back, and are left to themselves —
43
+ // no drag, no pan, no wheel.
44
+ , {
45
+ // React Flow lets the pointer through a node that is neither dragged
46
+ // nor selected (`pointer-events: none` on its wrapper): the character,
47
+ // its balloon and its menu take it back, and are left to themselves —
48
+ // no drag, no pan, no wheel.
49
+ className: "nodrag nopan nowheel", sx: {
50
+ width: NODE_WIDTH,
51
+ display: 'flex',
52
+ flexDirection: 'column',
53
+ alignItems: 'center',
54
+ gap: 1,
55
+ cursor: 'default',
56
+ pointerEvents: 'all',
57
+ }, "data-team-member": member.id, "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,
58
+ // In the node, not over the page; the balloon opens toward the
59
+ // middle of the graph: rightward from the left member.
60
+ place: side === 'left'
61
+ ? { position: 'absolute', left: 'auto' }
62
+ : { position: 'absolute', right: 'auto' }, stageRef: stageRef, onDragStart: () => undefined, open: false, onToggle: member.onToggle ?? (() => undefined), balloon: persona.saying
63
+ ? { text: persona.saying, more: persona.saying.endsWith('…') }
64
+ : undefined, insist: persona.insist, onDismiss: away => member.onAway?.(away !== 'none') })) }), _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 })] }));
65
+ });
66
+ /** The words on the edge for a flow. */
67
+ export function flowWords(flow, entry, peer) {
68
+ return flow === 'asking'
69
+ ? `${entry} asks ${peer}`
70
+ : flow === 'answering'
71
+ ? `${peer} answers ${entry}`
72
+ : '';
73
+ }
74
+ const ARROW = 9;
75
+ const LinkEdge = memo(function LinkEdge({ id, sourceX, sourceY, targetX, targetY, data, }) {
76
+ const flow = data?.flow ?? 'still';
77
+ const path = `M ${sourceX},${sourceY} L ${targetX},${targetY}`;
78
+ // The arrow at the end the message goes to.
79
+ const towardPeer = flow === 'asking';
80
+ const tipX = towardPeer ? targetX : sourceX;
81
+ const back = towardPeer ? -ARROW : ARROW;
82
+ const arrow = `M ${tipX},${targetY} L ${tipX + back},${targetY - ARROW / 1.6} L ${tipX + back},${targetY + ARROW / 1.6} Z`;
83
+ return (_jsxs(_Fragment, { children: [_jsx(BaseEdge, { id: id, path: path, style: {
84
+ stroke: data?.connected
85
+ ? 'var(--borderColor-accent-emphasis, #0969da)'
86
+ : 'var(--borderColor-muted, #d0d7de)',
87
+ strokeWidth: 2,
88
+ strokeDasharray: data?.connected ? undefined : '6 6',
89
+ opacity: flow === 'still' ? 0.6 : 0.25,
90
+ } }), flow !== 'still' && (_jsxs("g", { "data-a2a-edge-flow": flow, children: [_jsx("path", { d: path, fill: "none", className: `a2a-team-flow a2a-team-flow-${flow}`, stroke: "var(--fgColor-accent, #0969da)", strokeWidth: 3, strokeLinecap: "round", strokeDasharray: "10 14" }), _jsx("path", { d: arrow, fill: "var(--fgColor-accent, #0969da)" })] })), _jsx(EdgeLabelRenderer, { children: _jsx(Box, { className: "nodrag nopan", sx: {
91
+ position: 'absolute',
92
+ transform: `translate(-50%, 12px) translate(${(sourceX + targetX) / 2}px, ${(sourceY + targetY) / 2}px)`,
93
+ pointerEvents: 'none',
94
+ textAlign: 'center',
95
+ }, children: _jsx(Label, { variant: data?.connected ? 'accent' : 'secondary', children: data?.label }) }) })] }));
96
+ });
97
+ const NODE_TYPES = { member: MemberNode };
98
+ const EDGE_TYPES = { a2a: LinkEdge };
99
+ /** The viewport that shows the graph's width whole in a box `width` wide, never larger than drawn. */
100
+ export function teamViewport(width) {
101
+ const zoom = Math.min(1, width / WIDTH);
102
+ return { zoom, x: (width - WIDTH * zoom) / 2, y: 0 };
103
+ }
104
+ /** Draw a team of two over A2A, the link moving with what travels on it. */
105
+ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size = 96, }) {
106
+ const box = useRef(null);
107
+ const [width, setWidth] = useState(0);
108
+ useEffect(() => {
109
+ const element = box.current;
110
+ if (!element) {
111
+ return undefined;
112
+ }
113
+ const measure = () => setWidth(element.clientWidth);
114
+ measure();
115
+ if (typeof ResizeObserver === 'undefined') {
116
+ return undefined;
117
+ }
118
+ const observer = new ResizeObserver(measure);
119
+ observer.observe(element);
120
+ return () => observer.disconnect();
121
+ }, []);
122
+ const viewport = useMemo(() => teamViewport(width || WIDTH), [width]);
123
+ // The members' height: the character, its name and where it runs.
124
+ const memberHeight = size + 56;
125
+ const height = Math.ceil((BALLOON_ROOM + memberHeight) * viewport.zoom);
126
+ const nodes = useMemo(() => [
127
+ {
128
+ id: entry.id,
129
+ type: 'member',
130
+ position: { x: 0, y: BALLOON_ROOM },
131
+ width: NODE_WIDTH,
132
+ height: memberHeight,
133
+ draggable: false,
134
+ selectable: false,
135
+ // Above the edge and its label: a balloon overflows its node.
136
+ zIndex: 10,
137
+ data: { id: entry.id, side: 'left', size },
138
+ },
139
+ {
140
+ id: peer.id,
141
+ type: 'member',
142
+ position: { x: NODE_WIDTH + GAP, y: BALLOON_ROOM },
143
+ width: NODE_WIDTH,
144
+ height: memberHeight,
145
+ draggable: false,
146
+ selectable: false,
147
+ zIndex: 10,
148
+ data: { id: peer.id, side: 'right', size },
149
+ },
150
+ ], [entry.id, peer.id, size, memberHeight]);
151
+ const members = useMemo(() => ({ [entry.id]: entry, [peer.id]: peer }), [entry, peer]);
152
+ const words = flowWords(flow, entry.name, peer.name);
153
+ const edges = useMemo(() => [
154
+ {
155
+ id: `${entry.id}->${peer.id}`,
156
+ source: entry.id,
157
+ target: peer.id,
158
+ type: 'a2a',
159
+ selectable: false,
160
+ focusable: false,
161
+ data: {
162
+ flow,
163
+ connected,
164
+ label: words ? `${label} · ${words}` : label,
165
+ },
166
+ },
167
+ ], [entry.id, peer.id, flow, connected, label, words]);
168
+ return (_jsxs(Box, { ref: box, "data-a2a-team-graph": "", "data-a2a-flow": flow, sx: {
169
+ width: '100%',
170
+ height,
171
+ position: 'relative',
172
+ '@keyframes a2aTeamFlow': {
173
+ from: { strokeDashoffset: 24 },
174
+ to: { strokeDashoffset: 0 },
175
+ },
176
+ '& .a2a-team-flow-asking': {
177
+ animation: 'a2aTeamFlow 0.6s linear infinite',
178
+ },
179
+ '& .a2a-team-flow-answering': {
180
+ animation: 'a2aTeamFlow 0.6s linear infinite reverse',
181
+ },
182
+ // The arrow and the words say which way; the movement is extra.
183
+ '@media (prefers-reduced-motion: reduce)': {
184
+ '& .a2a-team-flow': { animation: 'none' },
185
+ },
186
+ '& .react-flow, & .react-flow__renderer, & .react-flow__viewport': {
187
+ overflow: 'visible',
188
+ },
189
+ '& .react-flow__node': { cursor: 'default' },
190
+ // The library's credit stays, quietly (as the landing's network does).
191
+ '& .react-flow__attribution': { bg: 'transparent', color: 'fg.muted' },
192
+ '& .react-flow__attribution a': { color: 'fg.muted' },
193
+ }, children: [_jsx(Box, { as: "span", "aria-live": "polite", sx: {
194
+ position: 'absolute',
195
+ width: 1,
196
+ height: 1,
197
+ overflow: 'hidden',
198
+ clip: 'rect(0 0 0 0)',
199
+ whiteSpace: 'nowrap',
200
+ }, children: words }), _jsx(Members.Provider, { value: members, 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' }, "aria-label": `${entry.name} and ${peer.name}, over A2A` }) })] }));
201
+ }
202
+ export default A2ATeamGraph;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Which way a team's A2A link carries a message, read from what happens to
3
+ * a request (`A2APeerEvent`).
4
+ *
5
+ * The member that is asked is the peer; the one that asks is the entry (the
6
+ * team's `entry`, who talks to the person). While a request is out — asked,
7
+ * then worked on — the link carries it from the entry to the peer:
8
+ * `asking`. When the peer answers, the answer comes back the other way for a
9
+ * moment: `answering`. Otherwise, and when a request fails, it is `still`.
10
+ *
11
+ * Pure, so that a test reads it and a page drives it the same way.
12
+ *
13
+ * @module components/teams/a2aTeamFlow
14
+ */
15
+ import type { A2APeerEvent } from '../../runtimes/browser/a2aPeer';
16
+ /** Which way the link carries a message, if it carries one. */
17
+ export type A2ATeamFlow = 'still' | 'asking' | 'answering';
18
+ /** How long an answer is shown travelling back, in milliseconds. */
19
+ export declare const ANSWER_SHOWN_MS = 1800;
20
+ /**
21
+ * The flow after an event: where the link goes now, and for how long it
22
+ * stays that way before it is `still` again (`holdMs`, only for an answer:
23
+ * the request's flow lasts as long as the request).
24
+ */
25
+ export declare function flowAfter(event: A2APeerEvent): {
26
+ flow: A2ATeamFlow;
27
+ holdMs?: number;
28
+ };
29
+ /** The member a message leaves, and the one it reaches, for a flow. */
30
+ export declare function flowEnds(flow: A2ATeamFlow, entry: string, peer: string): {
31
+ from: string;
32
+ to: string;
33
+ } | undefined;
@@ -0,0 +1,32 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** How long an answer is shown travelling back, in milliseconds. */
6
+ export const ANSWER_SHOWN_MS = 1800;
7
+ /**
8
+ * The flow after an event: where the link goes now, and for how long it
9
+ * stays that way before it is `still` again (`holdMs`, only for an answer:
10
+ * the request's flow lasts as long as the request).
11
+ */
12
+ export function flowAfter(event) {
13
+ switch (event.phase) {
14
+ case 'asked':
15
+ case 'working':
16
+ return { flow: 'asking' };
17
+ case 'answered':
18
+ return { flow: 'answering', holdMs: ANSWER_SHOWN_MS };
19
+ default:
20
+ return { flow: 'still' };
21
+ }
22
+ }
23
+ /** The member a message leaves, and the one it reaches, for a flow. */
24
+ export function flowEnds(flow, entry, peer) {
25
+ if (flow === 'asking') {
26
+ return { from: entry, to: peer };
27
+ }
28
+ if (flow === 'answering') {
29
+ return { from: peer, to: entry };
30
+ }
31
+ return undefined;
32
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * A team of applications over A2A, drawn and run in a page.
3
+ *
4
+ * Not re-exported by `components/index.ts`: it brings React Flow, which a
5
+ * page that shows no team should not load. Import it from here.
6
+ *
7
+ * @module components/teams
8
+ */
9
+ export * from './a2aTeamFlow';
10
+ export * from './useA2ATeam';
11
+ export * from './A2ATeamGraph';
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * A team of applications over A2A, drawn and run in a page.
7
+ *
8
+ * Not re-exported by `components/index.ts`: it brings React Flow, which a
9
+ * page that shows no team should not load. Import it from here.
10
+ *
11
+ * @module components/teams
12
+ */
13
+ export * from './a2aTeamFlow';
14
+ export * from './useA2ATeam';
15
+ export * from './A2ATeamGraph';
@@ -0,0 +1,57 @@
1
+ import type { AssistantState } from '../../chat/assistant/state';
2
+ import { type BrowserModelOptions } from '../../runtimes/browser/model';
3
+ import { type A2APeer } from '../../runtimes/browser/a2aPeer';
4
+ import type { AppSpec } from '../../types/agentspecs';
5
+ import { type A2ATeamFlow } from './a2aTeamFlow';
6
+ /** What one member's character does: its state, its balloon, and whether it was sent away. */
7
+ export type A2ATeamPersona = {
8
+ state: AssistantState;
9
+ /** What its balloon says. */
10
+ saying?: string;
11
+ /** Whether the balloon shows without the pointer over it. */
12
+ insist: boolean;
13
+ away: boolean;
14
+ };
15
+ export declare const AT_REST: A2ATeamPersona;
16
+ /** One turn of the conversation with the entry. */
17
+ export type A2ATeamTurn = {
18
+ role: 'user' | 'assistant';
19
+ text: string;
20
+ };
21
+ /** A line short enough for a balloon. */
22
+ export declare function balloonLine(text: string, length?: number): string;
23
+ export type UseA2ATeamOptions = {
24
+ /** The member that talks to the person and asks: it runs in this page. */
25
+ entry: AppSpec;
26
+ /** The member it asks over A2A. */
27
+ peerApp: AppSpec;
28
+ /** The peer, once its card is read; `null` until then. */
29
+ peer: A2APeer | null;
30
+ /** Where the entry's model is asked, and with whose token. */
31
+ inference: Omit<BrowserModelOptions, 'model'>;
32
+ /** The name of the entry's tool, as its instructions call it: `ask_<peer id>`. */
33
+ askTool?: string;
34
+ /** The most steps of one turn. */
35
+ maxSteps?: number;
36
+ };
37
+ export type A2ATeam = {
38
+ entryPersona: A2ATeamPersona;
39
+ peerPersona: A2ATeamPersona;
40
+ setEntryAway: (away: boolean) => void;
41
+ setPeerAway: (away: boolean) => void;
42
+ turns: A2ATeamTurn[];
43
+ /** The exchange between the two, a line a step. */
44
+ exchange: string[];
45
+ /** The peer's last answer, as it gave it. */
46
+ report: string | null;
47
+ /** Which way the link carries a message now. */
48
+ flow: A2ATeamFlow;
49
+ /** Whether a turn is under way. */
50
+ busy: boolean;
51
+ /** Whether the entry can be asked: its peer is connected. */
52
+ ready: boolean;
53
+ send: (text: string) => Promise<void>;
54
+ stop: () => void;
55
+ };
56
+ /** Run a team of two over A2A in the page. */
57
+ export declare function useA2ATeam(options: UseA2ATeamOptions): A2ATeam;
@@ -0,0 +1,233 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
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.
8
+ *
9
+ * The entry (Sales) talks to the person: its instructions and its model run
10
+ * here (the Vercel AI SDK's loop), its model reached through ai-inference
11
+ * 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 — and the exchange
15
+ * between them as a log, the peer's last answer as the report, and the way
16
+ * the link carries a message as the flow ({@link flowAfter}).
17
+ *
18
+ * What `A2ATeamGraph` draws, and what a page's composer sends.
19
+ *
20
+ * @module components/teams/useA2ATeam
21
+ */
22
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
23
+ import { stepCountIs, ToolLoopAgent } from 'ai';
24
+ import { createBrowserModel, } from '../../runtimes/browser/model';
25
+ import { a2aPeerTool, } from '../../runtimes/browser/a2aPeer';
26
+ import { flowAfter } from './a2aTeamFlow';
27
+ export const AT_REST = {
28
+ state: 'idle',
29
+ insist: false,
30
+ away: false,
31
+ };
32
+ /** A line short enough for a balloon. */
33
+ export function balloonLine(text, length = 120) {
34
+ const flat = text.replace(/\s+/g, ' ').trim();
35
+ return flat.length > length ? `${flat.slice(0, length - 1)}…` : flat;
36
+ }
37
+ /** Run a team of two over A2A in the page. */
38
+ export function useA2ATeam(options) {
39
+ const { entry, peerApp, peer, inference, maxSteps = 6 } = options;
40
+ const askTool = options.askTool ?? `ask_${peerApp.id.replace(/-/g, '_')}`;
41
+ const [entryPersona, setEntryPersona] = useState(() => ({
42
+ ...AT_REST,
43
+ state: 'greeting',
44
+ saying: balloonLine(entry.interface.welcome ?? ''),
45
+ insist: Boolean(entry.interface.welcome),
46
+ }));
47
+ const [peerPersona, setPeerPersona] = useState(AT_REST);
48
+ const [turns, setTurns] = useState([]);
49
+ const [report, setReport] = useState(null);
50
+ const [exchange, setExchange] = useState([]);
51
+ const [flow, setFlow] = useState('still');
52
+ const [busy, setBusy] = useState(false);
53
+ const history = useRef([]);
54
+ const abort = useRef(null);
55
+ const flowTimer = useRef(undefined);
56
+ // What the peer does, as it does it: its own state and balloon, and the link.
57
+ const onPeerEvent = useCallback((event) => {
58
+ const next = flowAfter(event);
59
+ clearTimeout(flowTimer.current);
60
+ setFlow(next.flow);
61
+ if (next.holdMs) {
62
+ flowTimer.current = setTimeout(() => setFlow('still'), next.holdMs);
63
+ }
64
+ if (event.phase === 'asked') {
65
+ setExchange(prev => [
66
+ ...prev,
67
+ `${entry.name} → ${peerApp.name}: ${event.request}`,
68
+ ]);
69
+ setPeerPersona(prev => ({
70
+ ...prev,
71
+ state: 'greeting',
72
+ saying: 'On it. Let me read the books.',
73
+ insist: true,
74
+ }));
75
+ }
76
+ else if (event.phase === 'working') {
77
+ if (event.note) {
78
+ setExchange(prev => [...prev, `${peerApp.name}: ${event.note}`]);
79
+ }
80
+ setPeerPersona(prev => ({
81
+ ...prev,
82
+ state: 'working',
83
+ saying: event.note ? balloonLine(event.note) : prev.saying,
84
+ insist: Boolean(event.note) || prev.insist,
85
+ }));
86
+ }
87
+ else if (event.phase === 'answered') {
88
+ setExchange(prev => [
89
+ ...prev,
90
+ `${peerApp.name} → ${entry.name}: the report`,
91
+ ]);
92
+ setReport(event.answer);
93
+ setPeerPersona(prev => ({
94
+ ...prev,
95
+ state: 'speaking',
96
+ saying: balloonLine(event.answer),
97
+ insist: true,
98
+ }));
99
+ }
100
+ else {
101
+ setExchange(prev => [
102
+ ...prev,
103
+ `${peerApp.name} could not answer: ${event.error}`,
104
+ ]);
105
+ setPeerPersona(prev => ({
106
+ ...prev,
107
+ state: 'idle',
108
+ saying: balloonLine(event.error),
109
+ insist: true,
110
+ }));
111
+ }
112
+ }, [entry.name, peerApp.name]);
113
+ const agent = useMemo(() => {
114
+ if (!peer) {
115
+ return null;
116
+ }
117
+ return new ToolLoopAgent({
118
+ id: entry.id,
119
+ model: createBrowserModel({
120
+ ...inference,
121
+ model: entry.model || undefined,
122
+ }),
123
+ instructions: entry.instructions,
124
+ tools: { [askTool]: a2aPeerTool({ peer, onEvent: onPeerEvent }) },
125
+ stopWhen: stepCountIs(maxSteps),
126
+ });
127
+ }, [peer, inference, onPeerEvent, entry, askTool, maxSteps]);
128
+ const send = useCallback(async (text) => {
129
+ const asked = text.trim();
130
+ if (!asked || !agent || busy) {
131
+ return;
132
+ }
133
+ setBusy(true);
134
+ setReport(null);
135
+ setExchange([]);
136
+ setTurns(prev => [...prev, { role: 'user', text: asked }]);
137
+ setEntryPersona({
138
+ ...AT_REST,
139
+ state: 'thinking',
140
+ saying: 'Let me see…',
141
+ insist: true,
142
+ });
143
+ setPeerPersona(AT_REST);
144
+ history.current = [...history.current, { role: 'user', content: asked }];
145
+ abort.current = new AbortController();
146
+ let said = '';
147
+ try {
148
+ const result = await agent.stream({
149
+ messages: history.current,
150
+ abortSignal: abort.current.signal,
151
+ });
152
+ for await (const part of result.fullStream) {
153
+ if (part.type === 'tool-call' && part.toolName === askTool) {
154
+ const request = String(part.input?.request ?? '');
155
+ setEntryPersona(prev => ({
156
+ ...prev,
157
+ state: 'waiting',
158
+ saying: balloonLine(`${peerApp.name}, could you: ${request}`),
159
+ insist: true,
160
+ }));
161
+ }
162
+ else if (part.type === 'tool-result' && part.toolName === askTool) {
163
+ setEntryPersona(prev => ({
164
+ ...prev,
165
+ state: 'thinking',
166
+ saying: 'Thanks!',
167
+ }));
168
+ }
169
+ else if (part.type === 'text-delta') {
170
+ said += part.text;
171
+ setEntryPersona(prev => ({
172
+ ...prev,
173
+ state: 'speaking',
174
+ saying: balloonLine(said),
175
+ insist: true,
176
+ }));
177
+ }
178
+ else if (part.type === 'error') {
179
+ throw part.error;
180
+ }
181
+ }
182
+ history.current = [
183
+ ...history.current,
184
+ { role: 'assistant', content: said },
185
+ ];
186
+ setTurns(prev => [...prev, { role: 'assistant', text: said }]);
187
+ setEntryPersona(prev => ({
188
+ ...prev,
189
+ state: 'idle',
190
+ saying: balloonLine(said),
191
+ }));
192
+ }
193
+ catch (error) {
194
+ const message = error instanceof Error ? error.message : String(error);
195
+ setTurns(prev => [
196
+ ...prev,
197
+ { role: 'assistant', text: `Something went wrong: ${message}` },
198
+ ]);
199
+ setEntryPersona(prev => ({
200
+ ...prev,
201
+ state: 'idle',
202
+ saying: balloonLine(`Something went wrong: ${message}`),
203
+ insist: true,
204
+ }));
205
+ setFlow('still');
206
+ }
207
+ finally {
208
+ abort.current = null;
209
+ setBusy(false);
210
+ }
211
+ }, [agent, busy, askTool, peerApp.name]);
212
+ const stop = useCallback(() => abort.current?.abort(), []);
213
+ useEffect(() => () => {
214
+ abort.current?.abort();
215
+ clearTimeout(flowTimer.current);
216
+ }, []);
217
+ const setEntryAway = useCallback((away) => setEntryPersona(prev => ({ ...prev, away })), []);
218
+ const setPeerAway = useCallback((away) => setPeerPersona(prev => ({ ...prev, away })), []);
219
+ return {
220
+ entryPersona,
221
+ peerPersona,
222
+ setEntryAway,
223
+ setPeerAway,
224
+ turns,
225
+ exchange,
226
+ report,
227
+ flow,
228
+ busy,
229
+ ready: agent !== null,
230
+ send,
231
+ stop,
232
+ };
233
+ }
@@ -8,9 +8,12 @@
8
8
  * `odoo-accounting` toolset, and only reads them.
9
9
  *
10
10
  * Both are on screen as Office Assistant characters, Sales the paper clip and
11
- * Accounting the wizard, as their Appspecs say. The exchange is shown as it
12
- * happens: Sales asks, Accounting works and then answers, each in its own
13
- * state and balloon. Then the report appears.
11
+ * Accounting the wizard, as their Appspecs say, in a graph (`A2ATeamGraph`)
12
+ * whose one edge is the A2A link. The exchange is shown as it happens: Sales
13
+ * asks — the edge flows toward Accounting — Accounting works and then answers
14
+ * — the edge flows back — each in its own state and balloon. Then the report
15
+ * appears. The page's own state is `useA2ATeam`'s, which the landing's home
16
+ * page runs too.
14
17
  *
15
18
  * Where Accounting is: `VITE_A2A_ACCOUNTING_URL`, else the examples' local
16
19
  * server (`…:8765/api/v1/a2a/agents/accounting`), which answers this machine
@@ -1,33 +1,21 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
- import { Button, Flash, Heading, Label, Text, Textarea } from '@primer/react';
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { Button, Flash, Heading, Text, Textarea } from '@primer/react';
4
4
  import { Box } from '@datalayer/primer-addons';
5
5
  import { AnonymousKeyExpired } from '@datalayer/core/lib/components/anonymous/AnonymousKeyExpired';
6
6
  import { AnonymousKeyTimer } from '@datalayer/core/lib/components/anonymous/AnonymousKeyTimer';
7
- import { stepCountIs, ToolLoopAgent } from 'ai';
8
7
  import { Streamdown } from 'streamdown';
9
8
  import { ThemedProvider } from './utils/themedProvider';
10
9
  import { useExampleAgentRuntimesUrl } from './utils/useExampleAgentRuntimesUrl';
11
- import { AssistantStage } from '../chat/assistant/AssistantStage';
12
10
  import { useBrowserInference } from '../hooks/useBrowserInference';
13
- import { createBrowserModel } from '../runtimes/browser/model';
14
11
  import { readJwtClaims, tokenLifetimeMs, useAnonymousSessionStore, } from '../runtimes/browser/anonymousToken';
15
- import { a2aPeerTool, connectA2APeer, } from '../runtimes/browser/a2aPeer';
12
+ import { connectA2APeer } from '../runtimes/browser/a2aPeer';
13
+ import { A2ATeamGraph, useA2ATeam } from '../components/teams';
16
14
  import { ACCOUNTING_APP_0_0_1, SALES_APP_0_0_1 } from '../specs/apps';
17
15
  import { SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1 } from '../specs/teams/teams';
18
16
  const TEAM = SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1;
19
17
  const SALES = SALES_APP_0_0_1;
20
18
  const ACCOUNTING = ACCOUNTING_APP_0_0_1;
21
- /** The name Sales's tool goes by, as its instructions call it. */
22
- const ASK_TOOL = 'ask_accounting';
23
- const STAGE_SIZE = 104;
24
- const STAGE_HEIGHT = 300;
25
- const AT_REST = { state: 'idle', insist: false, away: false };
26
- /** A line short enough for a balloon. */
27
- function line(text, length = 120) {
28
- const flat = text.replace(/\s+/g, ' ').trim();
29
- return flat.length > length ? `${flat.slice(0, length - 1)}…` : flat;
30
- }
31
19
  /** The members of the team, as the team spec says them. */
32
20
  function members() {
33
21
  const sales = TEAM.agents.find(member => member.id === TEAM.entry);
@@ -38,25 +26,6 @@ function members() {
38
26
  }
39
27
  return { sales, accounting };
40
28
  }
41
- /** One persona at its desk, with its name and where it runs under it. */
42
- function Desk({ app, persona, where, left, onToggle, onAway, }) {
43
- const stageRef = useRef(null);
44
- const character = app.interface.assistant ?? 'paperclip';
45
- return (_jsxs(Box, { sx: {
46
- position: 'absolute',
47
- left,
48
- top: 0,
49
- width: 220,
50
- height: STAGE_HEIGHT,
51
- }, "data-team-member": app.id, "data-member-state": persona.state, children: [persona.away ? (_jsxs(Button, { size: "small", sx: { position: 'absolute', top: STAGE_HEIGHT - 150 }, onClick: () => onAway(false), children: ["Call ", app.name, " back"] })) : (_jsx(AssistantStage, { character: character, state: persona.state, size: STAGE_SIZE, place: { position: 'absolute', left: 56, top: STAGE_HEIGHT - 170 }, stageRef: stageRef, onDragStart: () => undefined, open: false, onToggle: onToggle, balloon: persona.saying
52
- ? { text: persona.saying, more: persona.saying.endsWith('…') }
53
- : undefined, insist: persona.insist, onDismiss: away => onAway(away !== 'none') })), _jsxs(Box, { sx: {
54
- position: 'absolute',
55
- bottom: 0,
56
- width: '100%',
57
- textAlign: 'center',
58
- }, children: [_jsxs(Text, { sx: { fontWeight: 600, display: 'block' }, children: [app.emoji, " ", app.name] }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: where })] })] }));
59
- }
60
29
  function AgentA2ATeam() {
61
30
  const { sales: salesMember } = useMemo(members, []);
62
31
  const runtimeUrl = useExampleAgentRuntimesUrl();
@@ -76,21 +45,8 @@ function AgentA2ATeam() {
76
45
  accountingKeyClock.expiresAt <= Date.now());
77
46
  const [peer, setPeer] = useState(null);
78
47
  const [peerError, setPeerError] = useState(null);
79
- const [sales, setSales] = useState({
80
- ...AT_REST,
81
- state: 'greeting',
82
- saying: line(SALES.interface.welcome ?? ''),
83
- insist: true,
84
- });
85
- const [accounting, setAccounting] = useState(AT_REST);
86
- const [turns, setTurns] = useState([]);
87
- const [report, setReport] = useState(null);
88
- const [exchange, setExchange] = useState([]);
89
48
  const [draft, setDraft] = useState('');
90
- const [busy, setBusy] = useState(false);
91
49
  const composer = useRef(null);
92
- const history = useRef([]);
93
- const abort = useRef(null);
94
50
  useEffect(() => {
95
51
  let current = true;
96
52
  setPeer(null);
@@ -103,148 +59,19 @@ function AgentA2ATeam() {
103
59
  current = false;
104
60
  };
105
61
  }, [accountingUrl, accountingKey]);
106
- // What Accounting does, as it does it: its own state and balloon.
107
- const onPeerEvent = useCallback((event) => {
108
- if (event.phase === 'asked') {
109
- setExchange(prev => [...prev, `Sales → Accounting: ${event.request}`]);
110
- setAccounting(prev => ({
111
- ...prev,
112
- state: 'greeting',
113
- saying: 'On it. Let me read the books.',
114
- insist: true,
115
- }));
116
- }
117
- else if (event.phase === 'working') {
118
- if (event.note) {
119
- setExchange(prev => [...prev, `Accounting: ${event.note}`]);
120
- }
121
- setAccounting(prev => ({
122
- ...prev,
123
- state: 'working',
124
- saying: event.note ? line(event.note) : prev.saying,
125
- insist: Boolean(event.note) || prev.insist,
126
- }));
127
- }
128
- else if (event.phase === 'answered') {
129
- setExchange(prev => [...prev, 'Accounting → Sales: the report']);
130
- setReport(event.answer);
131
- setAccounting(prev => ({
132
- ...prev,
133
- state: 'speaking',
134
- saying: line(event.answer),
135
- insist: true,
136
- }));
137
- }
138
- else {
139
- setExchange(prev => [
140
- ...prev,
141
- `Accounting could not answer: ${event.error}`,
142
- ]);
143
- setAccounting(prev => ({
144
- ...prev,
145
- state: 'idle',
146
- saying: line(event.error),
147
- insist: true,
148
- }));
149
- }
150
- }, []);
151
- const agent = useMemo(() => {
152
- if (!peer) {
153
- return null;
154
- }
155
- return new ToolLoopAgent({
156
- id: SALES.id,
157
- model: createBrowserModel({
158
- ...inference,
159
- model: SALES.model || undefined,
160
- }),
161
- instructions: SALES.instructions,
162
- tools: { [ASK_TOOL]: a2aPeerTool({ peer, onEvent: onPeerEvent }) },
163
- stopWhen: stepCountIs(6),
164
- });
165
- }, [peer, inference, onPeerEvent]);
166
- const send = useCallback(async (text) => {
167
- const asked = text.trim();
168
- if (!asked || !agent || busy) {
169
- return;
170
- }
171
- setBusy(true);
62
+ // Sales runs here and asks Accounting over A2A: the conversation, the
63
+ // exchange, the report and the way the link carries a message.
64
+ const team = useA2ATeam({
65
+ entry: SALES,
66
+ peerApp: ACCOUNTING,
67
+ peer,
68
+ inference,
69
+ askTool: 'ask_accounting',
70
+ });
71
+ const send = (text) => {
172
72
  setDraft('');
173
- setReport(null);
174
- setExchange([]);
175
- setTurns(prev => [...prev, { role: 'user', text: asked }]);
176
- setSales({
177
- ...AT_REST,
178
- state: 'thinking',
179
- saying: 'Let me see…',
180
- insist: true,
181
- });
182
- setAccounting(AT_REST);
183
- history.current = [...history.current, { role: 'user', content: asked }];
184
- abort.current = new AbortController();
185
- let said = '';
186
- try {
187
- const result = await agent.stream({
188
- messages: history.current,
189
- abortSignal: abort.current.signal,
190
- });
191
- for await (const part of result.fullStream) {
192
- if (part.type === 'tool-call' && part.toolName === ASK_TOOL) {
193
- const request = String(part.input?.request ?? '');
194
- setSales(prev => ({
195
- ...prev,
196
- state: 'waiting',
197
- saying: line(`Accounting, could you: ${request}`),
198
- insist: true,
199
- }));
200
- }
201
- else if (part.type === 'tool-result' &&
202
- part.toolName === ASK_TOOL) {
203
- setSales(prev => ({
204
- ...prev,
205
- state: 'thinking',
206
- saying: 'Thanks!',
207
- }));
208
- }
209
- else if (part.type === 'text-delta') {
210
- said += part.text;
211
- setSales(prev => ({
212
- ...prev,
213
- state: 'speaking',
214
- saying: line(said),
215
- insist: true,
216
- }));
217
- }
218
- else if (part.type === 'error') {
219
- throw part.error;
220
- }
221
- }
222
- history.current = [
223
- ...history.current,
224
- { role: 'assistant', content: said },
225
- ];
226
- setTurns(prev => [...prev, { role: 'assistant', text: said }]);
227
- setSales(prev => ({ ...prev, state: 'idle', saying: line(said) }));
228
- }
229
- catch (error) {
230
- const message = error instanceof Error ? error.message : String(error);
231
- setTurns(prev => [
232
- ...prev,
233
- { role: 'assistant', text: `Something went wrong: ${message}` },
234
- ]);
235
- setSales(prev => ({
236
- ...prev,
237
- state: 'idle',
238
- saying: line(`Something went wrong: ${message}`),
239
- insist: true,
240
- }));
241
- }
242
- finally {
243
- abort.current = null;
244
- setBusy(false);
245
- }
246
- }, [agent, busy]);
247
- useEffect(() => () => abort.current?.abort(), []);
73
+ void team.send(text);
74
+ };
248
75
  const accountingWhere = accountingKey
249
76
  ? `on a runtime, ${new URL(accountingUrl).host}, with a temporary key`
250
77
  : `on a runtime, ${new URL(accountingUrl).host}`;
@@ -252,43 +79,50 @@ function AgentA2ATeam() {
252
79
  (accountingKeyClock && !accountingKeyEnded)) && (_jsxs(Box, { sx: { display: 'flex', gap: 4, flexWrap: 'wrap', mb: 3 }, children: [anonymous.status === 'active' && anonymous.expiresAt && (_jsx(AnonymousKeyTimer, { expiresAt: anonymous.expiresAt, grantedMs: anonymous.grantedMs, label: `${SALES.name}'s trial key`, onExpire: () => useAnonymousSessionStore.getState().expire() })), accountingKeyClock && !accountingKeyEnded && (_jsx(AnonymousKeyTimer, { expiresAt: accountingKeyClock.expiresAt, grantedMs: accountingKeyClock.grantedMs, label: `${ACCOUNTING.name}'s temporary key`, onExpire: () => setAccountingKeyEnded(true) }))] })), anonymous.status === 'expired' && (_jsx(Box, { sx: { mb: 3 }, children: _jsx(AnonymousKeyExpired, { agentName: SALES.name,
253
80
  // Sales runs in this page: what ended is its model's key, and
254
81
  // signing in gives it the person's own.
255
- onSignedIn: () => useAnonymousSessionStore.getState().clear() }) })), accountingKeyEnded && (_jsxs(Flash, { variant: "danger", sx: { mb: 3 }, children: [ACCOUNTING.name, "\u2019s temporary key has ended: it no longer answers. Make a new one with", ' ', _jsx("code", { children: "examples/sales-accounting-a2a/make_temp_key.py" }), " and reload."] })), needsSignIn && (_jsx(Flash, { variant: "warning", sx: { mb: 3 }, children: "Sales asks its model through the Datalayer inference service: sign in, or wait for a visitor's trial key." })), peerError && (_jsxs(Flash, { variant: "danger", sx: { mb: 3 }, children: ["Accounting is not reachable at ", accountingUrl, ": ", peerError, " Serve it with", ' ', _jsx("code", { children: "python examples/sales-accounting-a2a/serve_accounting.py" }), ' ', "on the local server, or", ' ', _jsx("code", { children: "loop apps run accounting.yaml --cloud --a2a" }), " and set", ' ', _jsx("code", { children: "VITE_A2A_ACCOUNTING_URL" }), "."] })), _jsxs(Box, { sx: {
256
- position: 'relative',
257
- height: STAGE_HEIGHT,
82
+ onSignedIn: () => useAnonymousSessionStore.getState().clear() }) })), accountingKeyEnded && (_jsxs(Flash, { variant: "danger", sx: { mb: 3 }, children: [ACCOUNTING.name, "\u2019s temporary key has ended: it no longer answers. Make a new one with", ' ', _jsx("code", { children: "examples/sales-accounting-a2a/make_temp_key.py" }), " and reload."] })), needsSignIn && (_jsx(Flash, { variant: "warning", sx: { mb: 3 }, children: "Sales asks its model through the Datalayer inference service: sign in, or wait for a visitor's trial key." })), peerError && (_jsxs(Flash, { variant: "danger", sx: { mb: 3 }, children: ["Accounting is not reachable at ", accountingUrl, ": ", peerError, " Serve it with", ' ', _jsx("code", { children: "python examples/sales-accounting-a2a/serve_accounting.py" }), ' ', "on the local server, or", ' ', _jsx("code", { children: "loop apps run accounting.yaml --cloud --a2a" }), " and set", ' ', _jsx("code", { children: "VITE_A2A_ACCOUNTING_URL" }), "."] })), _jsx(Box, { sx: {
258
83
  border: '1px solid',
259
84
  borderColor: 'border.default',
260
85
  borderRadius: 2,
261
86
  bg: 'canvas.subtle',
262
- overflow: 'visible',
87
+ px: 2,
88
+ pt: 2,
263
89
  mb: 3,
264
- }, children: [_jsx(Desk, { app: SALES, persona: sales, where: salesMember.runsIn === 'browser'
90
+ }, children: _jsx(A2ATeamGraph, { entry: {
91
+ id: SALES.id,
92
+ name: SALES.name,
93
+ emoji: SALES.emoji,
94
+ character: SALES.interface.assistant ?? 'paperclip',
95
+ where: salesMember.runsIn === 'browser'
265
96
  ? 'in your browser'
266
- : 'on a runtime', left: "8%", onToggle: () => composer.current?.focus(), onAway: away => setSales(prev => ({ ...prev, away })) }), _jsxs(Box, { sx: {
267
- position: 'absolute',
268
- left: '38%',
269
- right: '38%',
270
- top: STAGE_HEIGHT / 2,
271
- textAlign: 'center',
272
- }, children: [_jsx(Box, { sx: {
273
- borderTop: '2px dashed',
274
- borderColor: peer ? 'accent.emphasis' : 'border.muted',
275
- } }), _jsxs(Label, { variant: peer ? 'accent' : 'secondary', sx: { mt: 2 }, children: ["A2A ", peer ? `· ${peer.skill.name}` : '· not connected'] })] }), _jsx(Desk, { app: ACCOUNTING, persona: accounting, where: accountingWhere, left: "66%", onToggle: () => undefined, onAway: away => setAccounting(prev => ({ ...prev, away })) })] }), _jsxs(Box, { sx: { display: 'flex', gap: 3, flexWrap: 'wrap' }, children: [_jsxs(Box, { sx: { flex: '1 1 420px', minWidth: 0 }, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: ["Ask ", SALES.name] }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap', mb: 2 }, children: (SALES.interface.starters ?? []).map(starter => (_jsx(Button, { size: "small", disabled: !agent || busy, onClick: () => void send(starter.message), children: starter.label }, starter.label))) }), _jsx(Textarea, { ref: composer, block: true, rows: 3, value: draft, placeholder: "Ask for a financial report\u2026", onChange: event => setDraft(event.target.value), onKeyDown: event => {
97
+ : 'on a runtime',
98
+ persona: team.entryPersona,
99
+ onToggle: () => composer.current?.focus(),
100
+ onAway: team.setEntryAway,
101
+ }, peer: {
102
+ id: ACCOUNTING.id,
103
+ name: ACCOUNTING.name,
104
+ emoji: ACCOUNTING.emoji,
105
+ character: ACCOUNTING.interface.assistant ?? 'wizard',
106
+ where: accountingWhere,
107
+ persona: team.peerPersona,
108
+ onAway: team.setPeerAway,
109
+ }, flow: team.flow, connected: peer !== null, label: peer ? `A2A · ${peer.skill.name}` : 'A2A · not connected' }) }), _jsxs(Box, { sx: { display: 'flex', gap: 3, flexWrap: 'wrap' }, children: [_jsxs(Box, { sx: { flex: '1 1 420px', minWidth: 0 }, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: ["Ask ", SALES.name] }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap', mb: 2 }, children: (SALES.interface.starters ?? []).map(starter => (_jsx(Button, { size: "small", disabled: !team.ready || team.busy, onClick: () => send(starter.message), children: starter.label }, starter.label))) }), _jsx(Textarea, { ref: composer, block: true, rows: 3, value: draft, placeholder: "Ask for a financial report\u2026", onChange: event => setDraft(event.target.value), onKeyDown: event => {
276
110
  if (event.key === 'Enter' && !event.shiftKey) {
277
111
  event.preventDefault();
278
- void send(draft);
112
+ send(draft);
279
113
  }
280
- } }), _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { variant: "primary", disabled: !agent || busy || !draft.trim(), onClick: () => void send(draft), children: "Ask" }), busy && (_jsx(Button, { onClick: () => abort.current?.abort(), children: "Stop" }))] }), _jsx(Box, { sx: { mt: 3 }, "data-team-conversation": "", children: turns.map((turn, index) => (_jsxs(Box, { sx: {
114
+ } }), _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { variant: "primary", disabled: !team.ready || team.busy || !draft.trim(), onClick: () => send(draft), children: "Ask" }), team.busy && _jsx(Button, { onClick: team.stop, children: "Stop" })] }), _jsx(Box, { sx: { mt: 3 }, "data-team-conversation": "", children: team.turns.map((turn, index) => (_jsxs(Box, { sx: {
281
115
  p: 2,
282
116
  mb: 2,
283
117
  borderRadius: 2,
284
118
  bg: turn.role === 'user' ? 'accent.subtle' : 'canvas.subtle',
285
- }, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', display: 'block' }, children: turn.role === 'user' ? 'You' : SALES.name }), _jsx(Streamdown, { children: turn.text })] }, index))) })] }), _jsxs(Box, { sx: { flex: '1 1 360px', minWidth: 0 }, children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: "The exchange" }), _jsx(Box, { as: "ol", sx: { pl: 3, color: 'fg.muted', fontSize: 1 }, "data-team-exchange": "", children: exchange.length === 0 ? (_jsx(Text, { as: "li", children: "Nothing asked yet." })) : (exchange.map((step, index) => (_jsx(Text, { as: "li", children: step }, index)))) }), _jsx(Heading, { as: "h3", sx: { fontSize: 2, mt: 3, mb: 2 }, children: "The report" }), _jsx(Box, { sx: {
119
+ }, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', display: 'block' }, children: turn.role === 'user' ? 'You' : SALES.name }), _jsx(Streamdown, { children: turn.text })] }, index))) })] }), _jsxs(Box, { sx: { flex: '1 1 360px', minWidth: 0 }, children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: "The exchange" }), _jsx(Box, { as: "ol", sx: { pl: 3, color: 'fg.muted', fontSize: 1 }, "data-team-exchange": "", children: team.exchange.length === 0 ? (_jsx(Text, { as: "li", children: "Nothing asked yet." })) : (team.exchange.map((step, index) => (_jsx(Text, { as: "li", children: step }, index)))) }), _jsx(Heading, { as: "h3", sx: { fontSize: 2, mt: 3, mb: 2 }, children: "The report" }), _jsx(Box, { sx: {
286
120
  p: 3,
287
121
  border: '1px solid',
288
122
  borderColor: 'border.default',
289
123
  borderRadius: 2,
290
124
  minHeight: 120,
291
- }, "data-team-report": "", children: report ? (_jsx(Streamdown, { children: report })) : (_jsx(Text, { sx: { color: 'fg.muted' }, children: "The report Accounting returns appears here, as it returned it." })) })] })] })] }));
125
+ }, "data-team-report": "", children: team.report ? (_jsx(Streamdown, { children: team.report })) : (_jsx(Text, { sx: { color: 'fg.muted' }, children: "The report Accounting returns appears here, as it returned it." })) })] })] })] }));
292
126
  }
293
127
  export function AgentA2ATeamExample() {
294
128
  return (_jsx(ThemedProvider, { children: _jsx(AgentA2ATeam, {}) }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/agent-runtimes",
3
- "version": "1.3.64",
3
+ "version": "1.3.66",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",