@datalayer/agent-runtimes 1.3.75 → 1.3.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/agents/AgentDetails.d.ts +16 -2
- package/lib/agents/AgentDetails.js +381 -272
- package/lib/chat/ChatFloating.d.ts +12 -2
- package/lib/chat/ChatFloating.js +48 -11
- package/lib/chat/assistant/AgentDetailsDialog.d.ts +21 -0
- package/lib/chat/assistant/AgentDetailsDialog.js +19 -0
- package/lib/chat/assistant/AssistantStage.d.ts +51 -5
- package/lib/chat/assistant/AssistantStage.js +60 -10
- package/lib/chat/assistant/BalloonParts.d.ts +18 -4
- package/lib/chat/assistant/BalloonParts.js +46 -41
- package/lib/chat/assistant/BalloonVisual.js +2 -1
- package/lib/chat/assistant/SandboxDetailsDialog.d.ts +44 -0
- package/lib/chat/assistant/SandboxDetailsDialog.js +214 -0
- package/lib/chat/assistant/SpeechBalloon.d.ts +8 -6
- package/lib/chat/assistant/SpeechBalloon.js +29 -30
- package/lib/chat/assistant/assistantDetails.d.ts +84 -0
- package/lib/chat/assistant/assistantDetails.js +140 -0
- package/lib/chat/assistant/formats/files.d.ts +23 -0
- package/lib/chat/assistant/formats/files.js +48 -0
- package/lib/chat/assistant/formats/index.d.ts +2 -0
- package/lib/chat/assistant/formats/index.js +1 -0
- package/lib/chat/assistant/state.d.ts +7 -14
- package/lib/chat/assistant/state.js +21 -20
- package/lib/chat/assistant/toolLine.d.ts +16 -0
- package/lib/chat/assistant/toolLine.js +46 -0
- package/lib/chat/base/ChatBase.js +20 -0
- package/lib/chat/base/loopMessage.d.ts +37 -0
- package/lib/chat/base/loopMessage.js +37 -0
- package/lib/chat/messages/ChatMarkdown.d.ts +16 -0
- package/lib/chat/messages/ChatMarkdown.js +18 -0
- package/lib/chat/messages/ChatMessageList.d.ts +8 -1
- package/lib/chat/messages/ChatMessageList.js +24 -18
- package/lib/chat/styles/streamdownStyles.js +3 -1
- package/lib/chat/tools/ToolCallDisplay.d.ts +8 -1
- package/lib/chat/tools/ToolCallDisplay.js +8 -7
- package/lib/components/code-sandboxes/CodeSandboxCellVariablesDialog.js +5 -11
- package/lib/components/code-sandboxes/CodeSandboxVariables.d.ts +8 -0
- package/lib/components/code-sandboxes/CodeSandboxVariables.js +17 -11
- package/lib/components/teams/A2ATeamGraph.d.ts +30 -3
- package/lib/components/teams/A2ATeamGraph.js +264 -41
- package/lib/components/teams/TeamNotebookView.js +15 -0
- package/lib/components/teams/teamNotebookKernel.d.ts +3 -0
- package/lib/components/teams/teamNotebookKernel.js +17 -0
- package/lib/components/teams/useA2ATeam.d.ts +10 -6
- package/lib/components/teams/useA2ATeam.js +29 -8
- package/lib/examples/AgentA2ATeamExample.js +6 -0
- package/lib/examples/AssistantExample.js +3 -24
- package/lib/examples/AssistantGalleryExample.js +2 -1
- package/lib/examples/exampleGroups.d.ts +1 -1
- package/lib/examples/exampleGroups.js +1 -1
- package/lib/examples/utils/assistantGallery.d.ts +7 -6
- package/lib/examples/utils/assistantGallery.js +23 -8
- package/lib/jupyter/exec/Python.d.ts +0 -1
- package/lib/jupyter/exec/Python.js +0 -32
- package/lib/jupyter/exec/Snippets.d.ts +4 -12
- package/lib/jupyter/exec/Snippets.js +0 -8
- package/lib/loop/apps/AppFeedback.js +3 -1
- package/lib/loop/apps/AppKept.d.ts +22 -0
- package/lib/loop/apps/AppKept.js +36 -0
- package/lib/loop/apps/AppRenderer.d.ts +11 -1
- package/lib/loop/apps/AppRenderer.js +21 -3
- package/lib/loop/apps/checks.d.ts +7 -0
- package/lib/loop/apps/checks.js +39 -0
- package/lib/loop/apps/index.d.ts +3 -0
- package/lib/loop/apps/index.js +3 -0
- package/lib/loop/apps/kept.d.ts +30 -0
- package/lib/loop/apps/kept.js +52 -0
- package/lib/loop/apps/records.d.ts +12 -4
- package/lib/loop/apps/records.js +13 -5
- package/lib/loop/apps/saved.d.ts +30 -0
- package/lib/loop/apps/saved.js +40 -0
- package/lib/loop/embed/AppEmbed.d.ts +8 -1
- package/lib/loop/embed/AppEmbed.js +4 -4
- package/lib/loop/embed/LoopEmbed.d.ts +6 -1
- package/lib/loop/embed/LoopEmbed.js +4 -2
- package/lib/loop/embed/element.js +1 -1
- package/lib/loop/plugins/agents/DatalayerAgentBridge.js +12 -6
- package/lib/loop/plugins/agents/browserService.d.ts +7 -1
- package/lib/loop/plugins/agents/browserService.js +4 -0
- package/lib/loop/plugins/agents/plugin.d.ts +16 -0
- package/lib/loop/plugins/agents/switchable.d.ts +3 -0
- package/lib/loop/plugins/agents/switchable.js +1 -0
- package/lib/loop/plugins/app-activity/index.js +3 -0
- package/lib/loop/plugins/app-computer/index.js +3 -0
- package/lib/loop/plugins/app-rules/AppRulesCard.d.ts +5 -0
- package/lib/loop/plugins/app-rules/AppRulesCard.js +25 -6
- package/lib/loop/plugins/app-rules/index.js +3 -0
- package/lib/loop/plugins/assistant/index.d.ts +7 -2
- package/lib/loop/plugins/assistant/index.js +53 -4
- package/lib/loop/plugins/assistant-characters/index.d.ts +7 -0
- package/lib/loop/plugins/assistant-characters/index.js +17 -0
- package/lib/loop/plugins/chat/ChatView.js +4 -1
- package/lib/loop/plugins/page-layout/SplitLayout.d.ts +2 -1
- package/lib/loop/plugins/page-layout/SplitLayout.js +4 -0
- package/lib/loop/presets.d.ts +4 -0
- package/lib/loop/presets.js +3 -1
- package/lib/loop/shell/LoopWorkspace.d.ts +7 -0
- package/lib/loop/shell/LoopWorkspace.js +5 -4
- package/lib/loop/shell/SidebarRail.d.ts +53 -0
- package/lib/loop/shell/SidebarRail.js +74 -0
- package/lib/loop/shell/index.d.ts +2 -0
- package/lib/loop/shell/index.js +2 -0
- package/lib/loop/shell/paneMotion.d.ts +21 -0
- package/lib/loop/shell/paneMotion.js +37 -0
- package/lib/specs/apps.js +8 -5
- package/lib/specs/uiPlugins.js +529 -0
- package/lib/types/agentspecs.d.ts +5 -3
- package/lib/types/chat.d.ts +5 -0
- package/package.json +2 -2
- package/scripts/codegen/generate_ui_plugins.py +112 -2
|
@@ -1,10 +1,12 @@
|
|
|
1
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';
|
|
2
|
+
import { createContext, memo, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { BaseEdge, EdgeLabelRenderer, Handle, Position, ReactFlow, } from '@xyflow/react';
|
|
4
4
|
import '@xyflow/react/dist/base.css';
|
|
5
5
|
import { Button, Label, Text } from '@primer/react';
|
|
6
6
|
import { Box } from '@datalayer/primer-addons';
|
|
7
7
|
import { AssistantStage, } from '../../chat/assistant/AssistantStage';
|
|
8
|
+
import { useSignalValue } from '@datalayer/reactor/react';
|
|
9
|
+
import { teamNotebookKernel } from './teamNotebookKernel';
|
|
8
10
|
import { SpecMark } from '../../chat/marks/SpecMark';
|
|
9
11
|
import { toolWords, } from './a2aTeamFlow';
|
|
10
12
|
import { NotebookPreview } from './NotebookPreview';
|
|
@@ -44,18 +46,62 @@ const HANDLE = (size) => ({
|
|
|
44
46
|
const Members = createContext({});
|
|
45
47
|
/** The connections being called now, by node id: read by their nodes, as the members are. */
|
|
46
48
|
const Busy = createContext({});
|
|
49
|
+
const AT_PLACE = { dx: 0, dy: 0 };
|
|
50
|
+
/** An offset kept inside its bounds. */
|
|
51
|
+
export function clampOffset(offset, bounds) {
|
|
52
|
+
return {
|
|
53
|
+
dx: Math.min(bounds.maxDx, Math.max(bounds.minDx, offset.dx)),
|
|
54
|
+
dy: Math.min(bounds.maxDy, Math.max(bounds.minDy, offset.dy)),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Where the members were moved, by page and member: kept for the session,
|
|
59
|
+
* in memory, so a graph drawn again on the page keeps them where they were.
|
|
60
|
+
*/
|
|
61
|
+
const MOVED = new Map();
|
|
62
|
+
const movedKey = (id) => `${typeof location === 'undefined' ? '' : location.pathname}#${id}`;
|
|
63
|
+
/** How far a press moves before it is a drag, not a click, in pixels. */
|
|
64
|
+
const DRAG_THRESHOLD = 4;
|
|
65
|
+
/** How far an arrow key moves a member; with Shift, more. */
|
|
66
|
+
const NUDGE = 10;
|
|
67
|
+
const NUDGE_FAR = 40;
|
|
68
|
+
/** The members' moves, read by their nodes. */
|
|
69
|
+
const Moving = createContext({
|
|
70
|
+
zoom: 1,
|
|
71
|
+
offsetOf: () => AT_PLACE,
|
|
72
|
+
move: () => undefined,
|
|
73
|
+
reset: () => undefined,
|
|
74
|
+
});
|
|
47
75
|
/** A connection's node id: under its member. */
|
|
48
76
|
const connectionNodeId = (member, connection) => `${member}/${connection}`;
|
|
49
77
|
const MemberNode = memo(function MemberNode({ data, }) {
|
|
50
78
|
const { id, side, size } = data;
|
|
51
79
|
const member = useContext(Members)[id];
|
|
80
|
+
const moving = useContext(Moving);
|
|
81
|
+
const offset = moving.offsetOf(id);
|
|
52
82
|
const stageRef = useRef(null);
|
|
53
83
|
// Its balloon's display, as its menu changes it.
|
|
54
84
|
const [display, setDisplay] = useState();
|
|
55
|
-
|
|
85
|
+
// Its balloon as a click on it left it: shown, hidden, or as the team
|
|
86
|
+
// says (`auto`: what it is doing, and on hover).
|
|
87
|
+
const [shown, setShown] = useState('auto');
|
|
88
|
+
// What it says or does now: something new reopens a hidden balloon.
|
|
89
|
+
const persona = member?.persona;
|
|
90
|
+
const news = [
|
|
91
|
+
persona?.saying,
|
|
92
|
+
persona?.tool ? toolLineText(persona.tool) : '',
|
|
93
|
+
persona?.notebook?.name,
|
|
94
|
+
].join('\u0000');
|
|
95
|
+
const seen = useRef(news);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (news !== seen.current) {
|
|
98
|
+
seen.current = news;
|
|
99
|
+
setShown(current => (current === 'hidden' ? 'auto' : current));
|
|
100
|
+
}
|
|
101
|
+
}, [news]);
|
|
102
|
+
if (!member || !persona) {
|
|
56
103
|
return null;
|
|
57
104
|
}
|
|
58
|
-
const { persona } = member;
|
|
59
105
|
const shownDisplay = display ?? member.balloonDisplay ?? 'current';
|
|
60
106
|
// History: what it said and did, listed.
|
|
61
107
|
const listed = shownDisplay === 'history' && member.history?.length
|
|
@@ -70,6 +116,29 @@ const MemberNode = memo(function MemberNode({ data, }) {
|
|
|
70
116
|
visual: notebookBalloonVisual(persona.notebook, notebookTitle),
|
|
71
117
|
}
|
|
72
118
|
: {};
|
|
119
|
+
const balloonNow = persona.tool
|
|
120
|
+
? {
|
|
121
|
+
text: toolLineText(persona.tool),
|
|
122
|
+
tool: persona.tool,
|
|
123
|
+
busy: persona.state !== 'idle',
|
|
124
|
+
...given,
|
|
125
|
+
...listed,
|
|
126
|
+
}
|
|
127
|
+
: persona.saying
|
|
128
|
+
? {
|
|
129
|
+
text: persona.saying,
|
|
130
|
+
...(persona.full ? { fullText: persona.full } : {}),
|
|
131
|
+
more: persona.saying.endsWith('…'),
|
|
132
|
+
speaking: persona.state === 'speaking',
|
|
133
|
+
busy: persona.state === 'thinking' ||
|
|
134
|
+
persona.state === 'working' ||
|
|
135
|
+
persona.state === 'speaking',
|
|
136
|
+
...given,
|
|
137
|
+
...listed,
|
|
138
|
+
}
|
|
139
|
+
: 'history' in listed
|
|
140
|
+
? { text: '', ...listed }
|
|
141
|
+
: undefined;
|
|
73
142
|
return (_jsxs(Box
|
|
74
143
|
// React Flow lets the pointer through a node that is neither dragged
|
|
75
144
|
// nor selected (`pointer-events: none` on its wrapper): the character,
|
|
@@ -88,47 +157,106 @@ const MemberNode = memo(function MemberNode({ data, }) {
|
|
|
88
157
|
gap: 1,
|
|
89
158
|
cursor: 'default',
|
|
90
159
|
pointerEvents: 'all',
|
|
91
|
-
}, "data-team-member": member.id, "data-member-
|
|
160
|
+
}, "data-team-member": member.id, "data-member-balloon": shown, "data-member-moved": offset.dx || offset.dy
|
|
161
|
+
? `${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,
|
|
92
163
|
// In the node, not over the page; the balloon opens toward the
|
|
93
164
|
// middle of the graph: rightward from the left member.
|
|
94
165
|
place: side === 'left'
|
|
95
166
|
? { position: 'absolute', left: 'auto' }
|
|
96
|
-
: { position: 'absolute', right: 'auto' }, stageRef: stageRef,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
busy: persona.state !== 'idle',
|
|
104
|
-
...given,
|
|
105
|
-
...listed,
|
|
167
|
+
: { position: 'absolute', right: 'auto' }, stageRef: stageRef,
|
|
168
|
+
// Dragged within the graph's box: a press that moves more than
|
|
169
|
+
// a few pixels is a drag; one that does not is a click, and the
|
|
170
|
+
// stage ignores the click that ends a drag.
|
|
171
|
+
onDragStart: event => {
|
|
172
|
+
if (event.button !== 0) {
|
|
173
|
+
return;
|
|
106
174
|
}
|
|
107
|
-
:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
persona.state === 'speaking',
|
|
116
|
-
...given,
|
|
117
|
-
...listed,
|
|
175
|
+
const start = { x: event.clientX, y: event.clientY };
|
|
176
|
+
const from = offset;
|
|
177
|
+
let dragging = false;
|
|
178
|
+
const onMove = (next) => {
|
|
179
|
+
const dx = next.clientX - start.x;
|
|
180
|
+
const dy = next.clientY - start.y;
|
|
181
|
+
if (!dragging && Math.hypot(dx, dy) <= DRAG_THRESHOLD) {
|
|
182
|
+
return;
|
|
118
183
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
:
|
|
184
|
+
dragging = true;
|
|
185
|
+
moving.move(id, {
|
|
186
|
+
dx: from.dx + dx / moving.zoom,
|
|
187
|
+
dy: from.dy + dy / moving.zoom,
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
const onUp = () => {
|
|
191
|
+
window.removeEventListener('pointermove', onMove);
|
|
192
|
+
window.removeEventListener('pointerup', onUp);
|
|
193
|
+
window.removeEventListener('pointercancel', onUp);
|
|
194
|
+
};
|
|
195
|
+
window.addEventListener('pointermove', onMove);
|
|
196
|
+
window.addEventListener('pointerup', onUp);
|
|
197
|
+
window.addEventListener('pointercancel', onUp);
|
|
198
|
+
}, onCharacterKeyDown: event => {
|
|
199
|
+
const step = event.shiftKey ? NUDGE_FAR : NUDGE;
|
|
200
|
+
const by = {
|
|
201
|
+
ArrowLeft: { dx: -step, dy: 0 },
|
|
202
|
+
ArrowRight: { dx: step, dy: 0 },
|
|
203
|
+
ArrowUp: { dx: 0, dy: -step },
|
|
204
|
+
ArrowDown: { dx: 0, dy: step },
|
|
205
|
+
};
|
|
206
|
+
const nudge = by[event.key];
|
|
207
|
+
if (nudge) {
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
moving.move(id, {
|
|
210
|
+
dx: offset.dx + nudge.dx,
|
|
211
|
+
dy: offset.dy + nudge.dy,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}, onResetPosition: offset.dx || offset.dy ? () => moving.reset(id) : undefined, open: false, onToggle: member.onToggle ?? (() => undefined),
|
|
215
|
+
// A click shows and hides its balloon; the menu's *Ask Sales*
|
|
216
|
+
// still takes the reader to the composer.
|
|
217
|
+
onCharacterClick: () => setShown(current => current === 'shown' ||
|
|
218
|
+
(current === 'auto' &&
|
|
219
|
+
(persona.insist || display === 'history'))
|
|
220
|
+
? 'hidden'
|
|
221
|
+
: 'shown'), balloonDisplay: shownDisplay, onBalloonDisplayChange: next => {
|
|
222
|
+
setDisplay(next);
|
|
223
|
+
member.onBalloonDisplayChange?.(next);
|
|
224
|
+
}, conversationLabel: member.onToggle ? (member.conversationLabel ?? undefined) : false, balloon: shown === 'hidden'
|
|
225
|
+
? undefined
|
|
226
|
+
: (balloonNow ??
|
|
227
|
+
(shown === 'shown'
|
|
228
|
+
? {
|
|
229
|
+
text: `${member.name}, ${member.where ?? ''}`.replace(/, $/, ''),
|
|
230
|
+
}
|
|
231
|
+
: undefined)), expandTarget: member.expandTarget, expandOnArrival: !!member.expandTarget,
|
|
122
232
|
// History, chosen in its menu: shown until Current is.
|
|
123
|
-
insist:
|
|
233
|
+
insist: shown === 'shown' ||
|
|
234
|
+
(shown === 'auto' && (persona.insist || display === 'history')), onDismiss: away => member.onAway?.(away !== 'none'), suggestions: member.suggestions, onSuggestion: member.onSuggestion, inspector: member.inspector, inspectAgent: member.name, onStop: member.onStop, about: member.about, sandbox: member.sandbox, contextMenu: member.contextMenu,
|
|
124
235
|
// A member of the graph: what is clicked around it is the graph.
|
|
125
|
-
stayPut: true
|
|
236
|
+
stayPut: true,
|
|
237
|
+
// Named as the member, not its character: `Sales (in your
|
|
238
|
+
// 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: "connections", type: "source", position: Position.Bottom, style: { ...HANDLE(size), top: 'auto', bottom: 0 }, isConnectable: false })) : null] }));
|
|
126
240
|
});
|
|
241
|
+
/** A member's accessible name: its name, and where it runs. */
|
|
242
|
+
export function memberLabel(member) {
|
|
243
|
+
return member.where ? `${member.name} (${member.where})` : member.name;
|
|
244
|
+
}
|
|
245
|
+
/** A connection's accessible name: `Odoo, Accounting's MCP connection`. */
|
|
246
|
+
export function connectionLabel(connection, member) {
|
|
247
|
+
return member
|
|
248
|
+
? `${connection.label}, ${member}\u2019s MCP connection`
|
|
249
|
+
: `${connection.label}, an MCP connection`;
|
|
250
|
+
}
|
|
127
251
|
/** A connection of a member's: the server's mark, what it reaches, and how. */
|
|
128
252
|
const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
|
|
129
253
|
const { member, connection: connectionId, size } = data;
|
|
130
|
-
const
|
|
254
|
+
const owner = useContext(Members)[member];
|
|
255
|
+
const connection = owner?.connections?.find(known => known.id === connectionId);
|
|
131
256
|
const tool = useContext(Busy)[id];
|
|
257
|
+
// Its details, shown and hidden by a click or Enter on its mark.
|
|
258
|
+
const [told, setTold] = useState(false);
|
|
259
|
+
const detailsId = useId();
|
|
132
260
|
if (!connection) {
|
|
133
261
|
return null;
|
|
134
262
|
}
|
|
@@ -140,9 +268,15 @@ const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
|
|
|
140
268
|
gap: '2px',
|
|
141
269
|
cursor: 'default',
|
|
142
270
|
pointerEvents: 'all',
|
|
143
|
-
}, title: connection.name, "data-team-connection": connection.id, "data-connection-busy": tool ? 'true' : 'false', children: [_jsx(Handle, { type: "target", position: Position.Top, style: { ...HANDLE(0), top: 0 }, isConnectable: false }), _jsx(Box, { className: tool ? 'a2a-team-connection-busy' : undefined,
|
|
271
|
+
}, title: connection.name, "data-team-connection": connection.id, "data-connection-busy": tool ? 'true' : 'false', children: [_jsx(Handle, { type: "target", position: Position.Top, style: { ...HANDLE(0), top: 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
|
+
if (event.key === 'Escape' && told) {
|
|
273
|
+
event.stopPropagation();
|
|
274
|
+
setTold(false);
|
|
275
|
+
}
|
|
276
|
+
}, sx: {
|
|
144
277
|
width: size,
|
|
145
278
|
height: size,
|
|
279
|
+
p: 0,
|
|
146
280
|
display: 'flex',
|
|
147
281
|
alignItems: 'center',
|
|
148
282
|
justifyContent: 'center',
|
|
@@ -150,8 +284,30 @@ const ConnectionNode = memo(function ConnectionNode({ id, data, }) {
|
|
|
150
284
|
border: '1px solid',
|
|
151
285
|
borderColor: tool ? 'accent.emphasis' : 'border.default',
|
|
152
286
|
bg: 'canvas.default',
|
|
153
|
-
|
|
287
|
+
cursor: 'pointer',
|
|
288
|
+
'&:focus-visible': {
|
|
289
|
+
outline: '2px solid',
|
|
290
|
+
outlineColor: 'var(--focus-outlineColor, var(--fgColor-accent))',
|
|
291
|
+
outlineOffset: 2,
|
|
292
|
+
},
|
|
293
|
+
}, children: _jsx(SpecMark, { icon: connection.icon, emoji: connection.emoji, size: Math.round(size * 0.66) }) }), _jsx(Text, { sx: { fontWeight: 600, fontSize: 1, textAlign: 'center' }, children: connection.label }), connection.via && (_jsx(Text, { sx: { fontSize: '10px', color: 'fg.muted', textAlign: 'center' }, "data-connection-via": "", children: connection.via })), _jsx(Box, { id: detailsId, hidden: !told, "data-connection-details": "", sx: {
|
|
294
|
+
fontSize: '10px',
|
|
295
|
+
color: 'fg.muted',
|
|
296
|
+
textAlign: 'center',
|
|
297
|
+
maxWidth: CONNECTION_WIDTH + 40,
|
|
298
|
+
}, children: connectionDetails(connection, owner?.name, tool) })] }));
|
|
154
299
|
});
|
|
300
|
+
/** What a connection's details say: the server, who reaches it, and the call running now. */
|
|
301
|
+
export function connectionDetails(connection, member, calling) {
|
|
302
|
+
const tools = connection.tools.length;
|
|
303
|
+
return [
|
|
304
|
+
`${connection.name}, an MCP server${member ? ` ${member} reaches` : ''}.`,
|
|
305
|
+
tools ? `${tools} tool${tools === 1 ? '' : 's'}.` : '',
|
|
306
|
+
calling ? `Now: ${calling}.` : '',
|
|
307
|
+
]
|
|
308
|
+
.filter(Boolean)
|
|
309
|
+
.join(' ');
|
|
310
|
+
}
|
|
155
311
|
/** The words on the edge for a flow. */
|
|
156
312
|
export function flowWords(flow, entry, peer) {
|
|
157
313
|
return flow === 'asking'
|
|
@@ -257,11 +413,47 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
257
413
|
memberHeight +
|
|
258
414
|
(hasConnections ? CONNECTION_GAP + connectionHeight : 0)) *
|
|
259
415
|
viewport.zoom);
|
|
416
|
+
// Where the members were moved, kept inside the graph's box.
|
|
417
|
+
const total = room +
|
|
418
|
+
memberHeight +
|
|
419
|
+
(hasConnections ? CONNECTION_GAP + connectionHeight : 0);
|
|
420
|
+
const boundsOf = (id) => {
|
|
421
|
+
const layoutX = id === entry.id ? 0 : NODE_WIDTH + GAP;
|
|
422
|
+
const below = placed.find(({ member }) => member === id)?.connections.length
|
|
423
|
+
? CONNECTION_GAP + connectionHeight
|
|
424
|
+
: 0;
|
|
425
|
+
return {
|
|
426
|
+
minDx: -layoutX,
|
|
427
|
+
maxDx: WIDTH - NODE_WIDTH - layoutX,
|
|
428
|
+
minDy: -room,
|
|
429
|
+
maxDy: total - room - memberHeight - below,
|
|
430
|
+
};
|
|
431
|
+
};
|
|
432
|
+
const [moved, setMoved] = useState(() => ({
|
|
433
|
+
[entry.id]: MOVED.get(movedKey(entry.id)) ?? AT_PLACE,
|
|
434
|
+
[peer.id]: MOVED.get(movedKey(peer.id)) ?? AT_PLACE,
|
|
435
|
+
}));
|
|
436
|
+
const offsetOf = (id) => clampOffset(moved[id] ?? AT_PLACE, boundsOf(id));
|
|
437
|
+
const entryOffset = offsetOf(entry.id);
|
|
438
|
+
const peerOffset = offsetOf(peer.id);
|
|
439
|
+
const movingValue = {
|
|
440
|
+
zoom: viewport.zoom,
|
|
441
|
+
offsetOf,
|
|
442
|
+
move: (id, offset) => {
|
|
443
|
+
const next = clampOffset(offset, boundsOf(id));
|
|
444
|
+
MOVED.set(movedKey(id), next);
|
|
445
|
+
setMoved(current => ({ ...current, [id]: next }));
|
|
446
|
+
},
|
|
447
|
+
reset: (id) => {
|
|
448
|
+
MOVED.delete(movedKey(id));
|
|
449
|
+
setMoved(current => ({ ...current, [id]: AT_PLACE }));
|
|
450
|
+
},
|
|
451
|
+
};
|
|
260
452
|
const nodes = useMemo(() => [
|
|
261
453
|
{
|
|
262
454
|
id: entry.id,
|
|
263
455
|
type: 'member',
|
|
264
|
-
position: { x:
|
|
456
|
+
position: { x: entryOffset.dx, y: room + entryOffset.dy },
|
|
265
457
|
width: NODE_WIDTH,
|
|
266
458
|
height: memberHeight,
|
|
267
459
|
draggable: false,
|
|
@@ -273,7 +465,10 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
273
465
|
{
|
|
274
466
|
id: peer.id,
|
|
275
467
|
type: 'member',
|
|
276
|
-
position: {
|
|
468
|
+
position: {
|
|
469
|
+
x: NODE_WIDTH + GAP + peerOffset.dx,
|
|
470
|
+
y: room + peerOffset.dy,
|
|
471
|
+
},
|
|
277
472
|
width: NODE_WIDTH,
|
|
278
473
|
height: memberHeight,
|
|
279
474
|
draggable: false,
|
|
@@ -282,13 +477,15 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
282
477
|
data: { id: peer.id, side: 'right', size },
|
|
283
478
|
},
|
|
284
479
|
...JSON.parse(placedKey).flatMap(({ member, connections }, side) => {
|
|
285
|
-
|
|
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);
|
|
286
483
|
return connections.map((connection, at) => ({
|
|
287
484
|
id: connectionNodeId(member, connection),
|
|
288
485
|
type: 'connection',
|
|
289
486
|
position: {
|
|
290
487
|
x: xs[at],
|
|
291
|
-
y: room + memberHeight + CONNECTION_GAP,
|
|
488
|
+
y: room + memberHeight + CONNECTION_GAP + moveBy.dy,
|
|
292
489
|
},
|
|
293
490
|
width: CONNECTION_WIDTH,
|
|
294
491
|
height: connectionHeight,
|
|
@@ -307,8 +504,29 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
307
504
|
placedKey,
|
|
308
505
|
connectionSize,
|
|
309
506
|
connectionHeight,
|
|
507
|
+
entryOffset.dx,
|
|
508
|
+
entryOffset.dy,
|
|
509
|
+
peerOffset.dx,
|
|
510
|
+
peerOffset.dy,
|
|
310
511
|
]);
|
|
311
|
-
|
|
512
|
+
// The notebook open under the graph runs on a Pyodide kernel in the page:
|
|
513
|
+
// the entry's sandbox, while it is open.
|
|
514
|
+
const notebookKernel = useSignalValue(teamNotebookKernel);
|
|
515
|
+
const entryMember = useMemo(() => entry.sandbox
|
|
516
|
+
? entry
|
|
517
|
+
: {
|
|
518
|
+
...entry,
|
|
519
|
+
// The entry runs in the page: its sandbox is the Pyodide kernel
|
|
520
|
+
// of the notebook open under the graph, and none until one is.
|
|
521
|
+
sandbox: notebookKernel
|
|
522
|
+
? {
|
|
523
|
+
kind: 'browser',
|
|
524
|
+
status: 'running',
|
|
525
|
+
connection: notebookKernel,
|
|
526
|
+
}
|
|
527
|
+
: { kind: 'browser', status: 'not running' },
|
|
528
|
+
}, [entry, notebookKernel]);
|
|
529
|
+
const members = useMemo(() => ({ [entry.id]: entryMember, [peer.id]: peer }), [entry.id, entryMember, peer]);
|
|
312
530
|
const words = flowWords(flow, entry.name, peer.name);
|
|
313
531
|
// The tool each connection is answering now, in words, by its node's id.
|
|
314
532
|
const busy = useMemo(() => {
|
|
@@ -396,6 +614,11 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
396
614
|
overflow: 'visible',
|
|
397
615
|
},
|
|
398
616
|
'& .react-flow__node': { cursor: 'default' },
|
|
617
|
+
// A member is moved by dragging its character.
|
|
618
|
+
'& [data-team-member] [data-assistant-figure]': { cursor: 'grab' },
|
|
619
|
+
'& [data-team-member] [data-assistant-figure]:active': {
|
|
620
|
+
cursor: 'grabbing',
|
|
621
|
+
},
|
|
399
622
|
}, children: [_jsx(Box, { as: "span", "aria-live": "polite", sx: {
|
|
400
623
|
position: 'absolute',
|
|
401
624
|
width: 1,
|
|
@@ -403,8 +626,8 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
403
626
|
overflow: 'hidden',
|
|
404
627
|
clip: 'rect(0 0 0 0)',
|
|
405
628
|
whiteSpace: 'nowrap',
|
|
406
|
-
}, children: heard }), _jsx(Members.Provider, { value: members, 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' },
|
|
407
|
-
|
|
408
|
-
|
|
629
|
+
}, children: heard }), _jsx(Moving.Provider, { value: movingValue, children: _jsx(Members.Provider, { value: members, 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
|
+
// The library is MIT: its credit is not required on the page.
|
|
631
|
+
proOptions: { hideAttribution: true }, "aria-label": `${entry.name} and ${peer.name}, over A2A` }) }) }) })] }));
|
|
409
632
|
}
|
|
410
633
|
export default A2ATeamGraph;
|
|
@@ -8,6 +8,7 @@ import { useSignalValue } from '@datalayer/reactor/react';
|
|
|
8
8
|
import { notebookStore } from '@datalayer/jupyter-react';
|
|
9
9
|
import { EphemeralNotebook } from '../../chat/notebook/EphemeralNotebook';
|
|
10
10
|
import { createBrowserSandboxService } from '../../loop/plugins/agents/browserService';
|
|
11
|
+
import { teamNotebookKernel } from './teamNotebookKernel';
|
|
11
12
|
/** Each notebook drawn has its own id in jupyter-react's store. */
|
|
12
13
|
let drawn = 0;
|
|
13
14
|
/** Save a notebook as an `.ipynb` file, as Jupyter writes one. */
|
|
@@ -57,6 +58,20 @@ export function TeamNotebookView({ notebook, fileName, height, }) {
|
|
|
57
58
|
const manager = service.getServiceManager() ??
|
|
58
59
|
undefined;
|
|
59
60
|
const running = snapshot.state === 'running' && manager !== undefined;
|
|
61
|
+
// Say which kernel the notebook runs on, while it does: the browser
|
|
62
|
+
// agent's *Code Sandbox Details…* lists its variables.
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const kernel = running ? service.getKernelConnection() : null;
|
|
65
|
+
if (!kernel) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
teamNotebookKernel.value = kernel;
|
|
69
|
+
return () => {
|
|
70
|
+
if (teamNotebookKernel.peek() === kernel) {
|
|
71
|
+
teamNotebookKernel.value = null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}, [running, service]);
|
|
60
75
|
return (_jsx(QueryClientProvider, { client: queries, children: _jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsxs(Box, { sx: {
|
|
61
76
|
display: 'flex',
|
|
62
77
|
alignItems: 'center',
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The kernel of the notebook a team member gave, while one is open.
|
|
7
|
+
*
|
|
8
|
+
* `TeamNotebookView` runs the notebook on a Pyodide kernel in the page and
|
|
9
|
+
* says so here; the team's graph reads it for the browser agent's *Code
|
|
10
|
+
* Sandbox Details…*, which lists that kernel's variables. Light: a signal,
|
|
11
|
+
* nothing of Jupyter loaded with it.
|
|
12
|
+
*
|
|
13
|
+
* @module components/teams/teamNotebookKernel
|
|
14
|
+
*/
|
|
15
|
+
import { signal } from '@datalayer/reactor';
|
|
16
|
+
/** The open team notebook's kernel, or `null` when none is open. */
|
|
17
|
+
export const teamNotebookKernel = signal(null);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AssistantState
|
|
1
|
+
import type { AssistantState } from '../../chat/assistant/state';
|
|
2
|
+
import type { DisplayItem } from '../../types/chat';
|
|
2
3
|
import { type BrowserModelOptions } from '../../runtimes/browser/model';
|
|
3
4
|
import { type A2APeer, type A2APeerArtifact } from '../../runtimes/browser/a2aPeer';
|
|
4
5
|
import type { AppSpec } from '../../types/agentspecs';
|
|
@@ -69,13 +70,16 @@ export type A2ATeam = {
|
|
|
69
70
|
setEntryAway: (away: boolean) => void;
|
|
70
71
|
setPeerAway: (away: boolean) => void;
|
|
71
72
|
turns: A2ATeamTurn[];
|
|
72
|
-
/** The entry's conversation, as its `history` balloon lists it. */
|
|
73
|
-
entryHistory: BalloonHistoryMessage[];
|
|
74
73
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
74
|
+
* The entry's conversation, in the chat's model (its messages): what its
|
|
75
|
+
* `history` balloon draws with the chat's components.
|
|
77
76
|
*/
|
|
78
|
-
|
|
77
|
+
entryHistory: DisplayItem[];
|
|
78
|
+
/**
|
|
79
|
+
* What the peer was asked, the tools it called and what it answered, in
|
|
80
|
+
* the chat's model (messages and tool calls).
|
|
81
|
+
*/
|
|
82
|
+
peerHistory: DisplayItem[];
|
|
79
83
|
/** The peer's last answer, as it gave it. */
|
|
80
84
|
report: string | null;
|
|
81
85
|
/** What the peer gave besides words with its last answer, by media type. */
|
|
@@ -85,9 +85,31 @@ export function useA2ATeam(options) {
|
|
|
85
85
|
const told = useRef(0);
|
|
86
86
|
const tell = useCallback((role, text) => {
|
|
87
87
|
told.current += 1;
|
|
88
|
-
const
|
|
89
|
-
|
|
88
|
+
const message = {
|
|
89
|
+
id: `peer-${told.current}`,
|
|
90
|
+
role,
|
|
91
|
+
content: text,
|
|
92
|
+
createdAt: new Date(),
|
|
93
|
+
};
|
|
94
|
+
setPeerHistory(prev => [...prev, message]);
|
|
90
95
|
}, []);
|
|
96
|
+
// A tool the peer calls, as the chat holds one: added when it starts,
|
|
97
|
+
// updated when it ends.
|
|
98
|
+
const toolCalled = useCallback((step) => {
|
|
99
|
+
const id = step.id ?? step.name;
|
|
100
|
+
const call = {
|
|
101
|
+
id: `peer-tool:${id}`,
|
|
102
|
+
type: 'tool-call',
|
|
103
|
+
toolCallId: id,
|
|
104
|
+
toolName: toolOwnName(step.name, peerConnections),
|
|
105
|
+
args: {},
|
|
106
|
+
status: step.error ? 'error' : step.ended ? 'complete' : 'executing',
|
|
107
|
+
...(step.error ? { error: step.error } : {}),
|
|
108
|
+
};
|
|
109
|
+
setPeerHistory(prev => prev.some(item => item.id === call.id)
|
|
110
|
+
? prev.map(item => (item.id === call.id ? call : item))
|
|
111
|
+
: [...prev, call]);
|
|
112
|
+
}, [peerConnections]);
|
|
91
113
|
const [report, setReport] = useState(null);
|
|
92
114
|
const [artifacts, setArtifacts] = useState([]);
|
|
93
115
|
const [notebook, setNotebook] = useState(null);
|
|
@@ -134,10 +156,8 @@ export function useA2ATeam(options) {
|
|
|
134
156
|
}));
|
|
135
157
|
}
|
|
136
158
|
else if (event.phase === 'working') {
|
|
137
|
-
if (event.tool
|
|
138
|
-
|
|
139
|
-
? `${event.tool.name} failed: ${event.tool.error}`
|
|
140
|
-
: `Used ${toolOwnName(event.tool.name, peerConnections)}`);
|
|
159
|
+
if (event.tool) {
|
|
160
|
+
toolCalled(event.tool);
|
|
141
161
|
}
|
|
142
162
|
setPeerPersona(prev => ({
|
|
143
163
|
...prev,
|
|
@@ -184,7 +204,7 @@ export function useA2ATeam(options) {
|
|
|
184
204
|
tool: undefined,
|
|
185
205
|
}));
|
|
186
206
|
}
|
|
187
|
-
}, [peerApp.id, peerConnections, inspector, tell]);
|
|
207
|
+
}, [peerApp.id, peerConnections, inspector, tell, toolCalled]);
|
|
188
208
|
const agent = useMemo(() => {
|
|
189
209
|
if (!peer) {
|
|
190
210
|
return null;
|
|
@@ -373,7 +393,8 @@ export function useA2ATeam(options) {
|
|
|
373
393
|
const entryHistory = useMemo(() => turns.map((turn, index) => ({
|
|
374
394
|
id: `turn-${index}`,
|
|
375
395
|
role: turn.role,
|
|
376
|
-
|
|
396
|
+
content: turn.text,
|
|
397
|
+
createdAt: new Date(0),
|
|
377
398
|
})), [turns]);
|
|
378
399
|
return {
|
|
379
400
|
entryHistory,
|
|
@@ -137,6 +137,7 @@ function AgentA2ATeam() {
|
|
|
137
137
|
spec: `${SALES.id}:${SALES.version}`,
|
|
138
138
|
model: SALES.model || undefined,
|
|
139
139
|
where: 'in your browser',
|
|
140
|
+
description: SALES.description || undefined,
|
|
140
141
|
},
|
|
141
142
|
}, peer: {
|
|
142
143
|
id: ACCOUNTING.id,
|
|
@@ -154,6 +155,11 @@ function AgentA2ATeam() {
|
|
|
154
155
|
spec: `${ACCOUNTING.id}:${ACCOUNTING.version}`,
|
|
155
156
|
model: ACCOUNTING.model || undefined,
|
|
156
157
|
where: accountingWhere,
|
|
158
|
+
// What its A2A card says, once it is reached.
|
|
159
|
+
description: peer?.card.description || ACCOUNTING.description || undefined,
|
|
160
|
+
skills: peer?.card.skills.map(skill => skill.name),
|
|
161
|
+
protocol: peer ? 'a2a' : undefined,
|
|
162
|
+
url: peer ? accountingUrl : undefined,
|
|
157
163
|
},
|
|
158
164
|
}, flow: team.flow, calls: team.calls,
|
|
159
165
|
// Room for the balloons: suggestions, *more* and a notebook.
|