@datalayer/agent-runtimes 1.3.73 → 1.3.75
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/chat/ChatFloating.d.ts +25 -2
- package/lib/chat/ChatFloating.js +80 -12
- package/lib/chat/assistant/AssistantContextMenu.d.ts +67 -0
- package/lib/chat/assistant/AssistantContextMenu.js +68 -0
- package/lib/chat/assistant/AssistantStage.d.ts +123 -5
- package/lib/chat/assistant/AssistantStage.js +245 -7
- package/lib/chat/assistant/BalloonParts.d.ts +15 -1
- package/lib/chat/assistant/BalloonParts.js +26 -9
- package/lib/chat/assistant/BalloonVisual.d.ts +65 -0
- package/lib/chat/assistant/BalloonVisual.js +58 -0
- package/lib/chat/assistant/SpeechBalloon.d.ts +68 -2
- package/lib/chat/assistant/SpeechBalloon.js +113 -7
- package/lib/chat/assistant/state.d.ts +19 -0
- package/lib/chat/assistant/state.js +44 -17
- package/lib/chat/base/ChatBase.js +51 -1
- package/lib/chat/indicators/TypingDots.d.ts +13 -0
- package/lib/chat/indicators/TypingDots.js +21 -0
- package/lib/chat/messages/ChatMessageList.js +1 -15
- package/lib/components/inspector/AgentInspector.d.ts +59 -0
- package/lib/components/inspector/AgentInspector.js +220 -0
- package/lib/components/inspector/AgentInspectorDialog.d.ts +19 -0
- package/lib/components/inspector/AgentInspectorDialog.js +9 -0
- package/lib/components/inspector/JsonTree.d.ts +24 -0
- package/lib/components/inspector/JsonTree.js +89 -0
- package/lib/components/inspector/a2aInspect.d.ts +56 -0
- package/lib/components/inspector/a2aInspect.js +478 -0
- package/lib/components/inspector/agentInspector.d.ts +179 -0
- package/lib/components/inspector/agentInspector.js +267 -0
- package/lib/components/inspector/chatInspect.d.ts +38 -0
- package/lib/components/inspector/chatInspect.js +65 -0
- package/lib/components/inspector/classify.d.ts +19 -0
- package/lib/components/inspector/classify.js +42 -0
- package/lib/components/inspector/index.d.ts +14 -0
- package/lib/components/inspector/index.js +18 -0
- package/lib/components/teams/A2ATeamGraph.d.ts +54 -3
- package/lib/components/teams/A2ATeamGraph.js +37 -11
- package/lib/components/teams/NotebookPreview.d.ts +11 -2
- package/lib/components/teams/NotebookPreview.js +6 -3
- package/lib/components/teams/NotebookPreviewView.js +9 -1
- package/lib/components/teams/TeamNotebook.d.ts +10 -1
- package/lib/components/teams/TeamNotebook.js +15 -3
- package/lib/components/teams/jupyterThemeVariables.d.ts +9 -0
- package/lib/components/teams/jupyterThemeVariables.js +441 -0
- package/lib/components/teams/teamConnections.d.ts +9 -1
- package/lib/components/teams/teamConnections.js +20 -0
- package/lib/components/teams/useA2ATeam.d.ts +17 -4
- package/lib/components/teams/useA2ATeam.js +87 -23
- package/lib/examples/AgentA2ATeamExample.d.ts +16 -2
- package/lib/examples/AgentA2ATeamExample.js +65 -13
- package/lib/examples/{ChatAssistantExample.d.ts → AssistantExample.d.ts} +2 -2
- package/lib/examples/{ChatAssistantExample.js → AssistantExample.js} +69 -7
- package/lib/examples/{ChatAssistantGalleryExample.d.ts → AssistantGalleryExample.d.ts} +2 -2
- package/lib/examples/{ChatAssistantGalleryExample.js → AssistantGalleryExample.js} +45 -13
- package/lib/examples/example-selector.js +4 -2
- package/lib/examples/exampleGroups.d.ts +9 -0
- package/lib/examples/exampleGroups.js +56 -0
- package/lib/examples/index.d.ts +1 -1
- package/lib/examples/index.js +1 -1
- package/lib/examples/main.js +3 -43
- package/lib/examples/utils/AssistantGalleryGrid.d.ts +6 -1
- package/lib/examples/utils/AssistantGalleryGrid.js +16 -3
- package/lib/examples/utils/assistantGallery.d.ts +51 -2
- package/lib/examples/utils/assistantGallery.js +32 -7
- package/lib/loop/apps/AppRenderer.js +1 -1
- package/lib/loop/core/index.d.ts +17 -0
- package/lib/loop/core/index.js +1 -0
- package/lib/loop/plugins/assistant/index.d.ts +1 -1
- package/lib/loop/plugins/assistant/index.js +8 -3
- package/lib/runtimes/browser/a2aPeer.d.ts +4 -1
- package/lib/types/chat.d.ts +7 -0
- package/package.json +1 -1
- package/scripts/codegen/generate_jupyter_theme_variables.mjs +57 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The Agent Inspector's record: what an agent does, as it does it, one entry
|
|
7
|
+
* a thing — an A2A request or event, an MCP tool call, a skill, a frontend or
|
|
8
|
+
* backend tool, a model turn, an approval — each with its time, the agent
|
|
9
|
+
* that did it (`actor`), where it came from (`source`), its kind, its name,
|
|
10
|
+
* its status and, once it ends, its duration, a line that says it, and its
|
|
11
|
+
* payload whole.
|
|
12
|
+
*
|
|
13
|
+
* A call's start and its end are one entry: {@link AgentInspectorSink.start}
|
|
14
|
+
* opens it under a key (a tool call's id), {@link AgentInspectorSink.end}
|
|
15
|
+
* closes it with its result. Anything can push into a sink: the A2A capture
|
|
16
|
+
* (`inspectA2AFetch`), a team (`useA2ATeam`'s `inspector`), a chat
|
|
17
|
+
* (`ChatBase`, under {@link AgentInspectorProvider}). `AgentInspector` draws
|
|
18
|
+
* it.
|
|
19
|
+
*
|
|
20
|
+
* @module components/inspector/agentInspector
|
|
21
|
+
*/
|
|
22
|
+
import { createContext, createElement, useContext, useState, useSyncExternalStore, } from 'react';
|
|
23
|
+
/** Every source, in the order the filters list them. */
|
|
24
|
+
export const AGENT_INSPECTOR_SOURCES = [
|
|
25
|
+
'a2a',
|
|
26
|
+
'mcp',
|
|
27
|
+
'skill',
|
|
28
|
+
'frontend-tool',
|
|
29
|
+
'backend-tool',
|
|
30
|
+
'model',
|
|
31
|
+
'approval',
|
|
32
|
+
];
|
|
33
|
+
/** A source, in words. */
|
|
34
|
+
export const AGENT_INSPECTOR_SOURCE_LABELS = {
|
|
35
|
+
a2a: 'A2A',
|
|
36
|
+
mcp: 'MCP',
|
|
37
|
+
skill: 'Skill',
|
|
38
|
+
'frontend-tool': 'Frontend tool',
|
|
39
|
+
'backend-tool': 'Backend tool',
|
|
40
|
+
model: 'Model',
|
|
41
|
+
approval: 'Approval',
|
|
42
|
+
};
|
|
43
|
+
/** How large a value is, as JSON, in bytes. */
|
|
44
|
+
export function jsonBytes(value) {
|
|
45
|
+
if (value === undefined) {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const text = JSON.stringify(value);
|
|
50
|
+
return text === undefined ? 0 : new TextEncoder().encode(text).length;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** A size, in words: `812 B`, `12.3 kB`, `1.2 MB`. */
|
|
57
|
+
export function formatBytes(bytes) {
|
|
58
|
+
if (bytes < 1000) {
|
|
59
|
+
return `${bytes} B`;
|
|
60
|
+
}
|
|
61
|
+
if (bytes < 1_000_000) {
|
|
62
|
+
return `${(bytes / 1000).toFixed(1)} kB`;
|
|
63
|
+
}
|
|
64
|
+
return `${(bytes / 1_000_000).toFixed(1)} MB`;
|
|
65
|
+
}
|
|
66
|
+
/** A line short enough for a row. */
|
|
67
|
+
export function shortLine(text, length = 160) {
|
|
68
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
69
|
+
return flat.length > length ? `${flat.slice(0, length - 1)}…` : flat;
|
|
70
|
+
}
|
|
71
|
+
/** How many semantic events wait for their entry. */
|
|
72
|
+
const PENDING_LIMIT = 32;
|
|
73
|
+
/** Make a sink: the record, and who listens to it. */
|
|
74
|
+
export function createAgentInspector(options = {}) {
|
|
75
|
+
const now = options.now ?? Date.now;
|
|
76
|
+
const limit = options.limit ?? 2000;
|
|
77
|
+
let list = [];
|
|
78
|
+
let counter = 0;
|
|
79
|
+
const open = new Map();
|
|
80
|
+
const listeners = new Set();
|
|
81
|
+
let pending = [];
|
|
82
|
+
let scheduled = false;
|
|
83
|
+
const linkers = new Set();
|
|
84
|
+
const notify = () => {
|
|
85
|
+
if (scheduled) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
scheduled = true;
|
|
89
|
+
queueMicrotask(() => {
|
|
90
|
+
scheduled = false;
|
|
91
|
+
listeners.forEach(listener => listener());
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
const replace = (id, next) => {
|
|
95
|
+
list = list.map(entry => (entry.id === id ? next : entry));
|
|
96
|
+
};
|
|
97
|
+
const settlePending = (entry) => {
|
|
98
|
+
if (!pending.length) {
|
|
99
|
+
return entry;
|
|
100
|
+
}
|
|
101
|
+
const taken = pending.filter(waiting => waiting.match(entry));
|
|
102
|
+
if (!taken.length) {
|
|
103
|
+
return entry;
|
|
104
|
+
}
|
|
105
|
+
pending = pending.filter(waiting => !taken.includes(waiting));
|
|
106
|
+
return {
|
|
107
|
+
...entry,
|
|
108
|
+
events: [...entry.events, ...taken.map(waiting => waiting.event)],
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
const push = (input) => {
|
|
112
|
+
counter += 1;
|
|
113
|
+
const id = `e${counter}`;
|
|
114
|
+
const { links, at, ...rest } = input;
|
|
115
|
+
const entry = settlePending({
|
|
116
|
+
...rest,
|
|
117
|
+
id,
|
|
118
|
+
at: at ?? now(),
|
|
119
|
+
bytes: jsonBytes(input.payload) + jsonBytes(input.result),
|
|
120
|
+
links: links ?? [],
|
|
121
|
+
events: [],
|
|
122
|
+
});
|
|
123
|
+
list = [...list, entry];
|
|
124
|
+
if (list.length > limit) {
|
|
125
|
+
list = list.slice(list.length - limit);
|
|
126
|
+
}
|
|
127
|
+
notify();
|
|
128
|
+
return id;
|
|
129
|
+
};
|
|
130
|
+
const find = (id) => list.find(entry => entry.id === id);
|
|
131
|
+
const sink = {
|
|
132
|
+
push,
|
|
133
|
+
start(input) {
|
|
134
|
+
const known = open.get(input.key);
|
|
135
|
+
const entry = known ? find(known) : undefined;
|
|
136
|
+
if (entry) {
|
|
137
|
+
const payload = input.payload ?? entry.payload;
|
|
138
|
+
replace(entry.id, {
|
|
139
|
+
...entry,
|
|
140
|
+
payload,
|
|
141
|
+
summary: input.summary || entry.summary,
|
|
142
|
+
links: [...entry.links, ...(input.links ?? [])],
|
|
143
|
+
bytes: jsonBytes(payload) + jsonBytes(entry.result),
|
|
144
|
+
});
|
|
145
|
+
notify();
|
|
146
|
+
return entry.id;
|
|
147
|
+
}
|
|
148
|
+
const id = push({ ...input, status: 'running' });
|
|
149
|
+
open.set(input.key, id);
|
|
150
|
+
return id;
|
|
151
|
+
},
|
|
152
|
+
end(key, outcome = {}) {
|
|
153
|
+
const id = open.get(key);
|
|
154
|
+
const entry = id ? find(id) : undefined;
|
|
155
|
+
if (!id || !entry) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
open.delete(key);
|
|
159
|
+
const result = outcome.result ?? entry.result;
|
|
160
|
+
replace(id, {
|
|
161
|
+
...entry,
|
|
162
|
+
status: outcome.status ?? (outcome.error ? 'failed' : 'done'),
|
|
163
|
+
endedAt: outcome.at ?? now(),
|
|
164
|
+
result,
|
|
165
|
+
error: outcome.error ?? entry.error,
|
|
166
|
+
summary: outcome.summary ?? entry.summary,
|
|
167
|
+
links: [...entry.links, ...(outcome.links ?? [])],
|
|
168
|
+
bytes: jsonBytes(entry.payload) + jsonBytes(result),
|
|
169
|
+
});
|
|
170
|
+
notify();
|
|
171
|
+
return id;
|
|
172
|
+
},
|
|
173
|
+
update(id, patch) {
|
|
174
|
+
const entry = find(id);
|
|
175
|
+
if (!entry) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const next = { ...entry, ...patch(entry) };
|
|
179
|
+
replace(id, {
|
|
180
|
+
...next,
|
|
181
|
+
bytes: jsonBytes(next.payload) + jsonBytes(next.result),
|
|
182
|
+
});
|
|
183
|
+
notify();
|
|
184
|
+
},
|
|
185
|
+
attach(match, event) {
|
|
186
|
+
for (let index = list.length - 1; index >= 0; index -= 1) {
|
|
187
|
+
const entry = list[index];
|
|
188
|
+
if (match(entry) && !entry.events.includes(event)) {
|
|
189
|
+
replace(entry.id, { ...entry, events: [...entry.events, event] });
|
|
190
|
+
notify();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
pending = [...pending, { match, event }].slice(-PENDING_LIMIT);
|
|
195
|
+
},
|
|
196
|
+
running(key) {
|
|
197
|
+
return open.has(key);
|
|
198
|
+
},
|
|
199
|
+
note(event) {
|
|
200
|
+
for (const linker of linkers) {
|
|
201
|
+
const match = linker(event);
|
|
202
|
+
if (match) {
|
|
203
|
+
sink.attach(match, event);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
addLinker(linker) {
|
|
209
|
+
linkers.add(linker);
|
|
210
|
+
return () => {
|
|
211
|
+
linkers.delete(linker);
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
clear() {
|
|
215
|
+
list = [];
|
|
216
|
+
open.clear();
|
|
217
|
+
pending = [];
|
|
218
|
+
notify();
|
|
219
|
+
},
|
|
220
|
+
entries() {
|
|
221
|
+
return list;
|
|
222
|
+
},
|
|
223
|
+
subscribe(listener) {
|
|
224
|
+
listeners.add(listener);
|
|
225
|
+
return () => {
|
|
226
|
+
listeners.delete(listener);
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
exportSession() {
|
|
230
|
+
return { exportedAt: new Date(now()).toISOString(), entries: [...list] };
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
return sink;
|
|
234
|
+
}
|
|
235
|
+
const NO_ENTRIES = [];
|
|
236
|
+
const NO_SUBSCRIPTION = () => () => undefined;
|
|
237
|
+
/** A sink's entries, kept current; none without a sink. */
|
|
238
|
+
export function useAgentInspectorEntries(sink) {
|
|
239
|
+
return useSyncExternalStore(sink ? sink.subscribe : NO_SUBSCRIPTION, () => (sink ? sink.entries() : NO_ENTRIES), () => (sink ? sink.entries() : NO_ENTRIES));
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* A sink for a page, made once (or the one given), and its entries: what a
|
|
243
|
+
* page passes to its team, its chat and its `AgentInspector`.
|
|
244
|
+
*/
|
|
245
|
+
export function useAgentInspector(given) {
|
|
246
|
+
const [own] = useState(() => given ?? createAgentInspector());
|
|
247
|
+
const sink = given ?? own;
|
|
248
|
+
return { sink, entries: useAgentInspectorEntries(sink) };
|
|
249
|
+
}
|
|
250
|
+
/** The sink the chats under it push into. */
|
|
251
|
+
export const AgentInspectorContext = createContext(null);
|
|
252
|
+
/** The sink of the nearest {@link AgentInspectorProvider}, if any. */
|
|
253
|
+
export function useAgentInspectorSink() {
|
|
254
|
+
return useContext(AgentInspectorContext);
|
|
255
|
+
}
|
|
256
|
+
/** Every chat under it records what its agent does into `sink`. */
|
|
257
|
+
export function AgentInspectorProvider({ sink, children, }) {
|
|
258
|
+
return createElement(AgentInspectorContext.Provider, { value: sink }, children);
|
|
259
|
+
}
|
|
260
|
+
/** A call's arguments, in a few words: `kind=customer, limit=100`. */
|
|
261
|
+
export function argumentsLine(args, length = 100) {
|
|
262
|
+
if (!args || typeof args !== 'object') {
|
|
263
|
+
return args === undefined ? '' : shortLine(String(args), length);
|
|
264
|
+
}
|
|
265
|
+
const parts = Object.entries(args).map(([key, value]) => `${key}=${typeof value === 'string' ? value : JSON.stringify(value)}`);
|
|
266
|
+
return shortLine(parts.join(', '), length);
|
|
267
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A chat's agent, as the Agent Inspector records it: each turn of its model
|
|
3
|
+
* (the prompt, the answer, the tokens), and each tool call from its start to
|
|
4
|
+
* its end — an MCP server's, a skill, a frontend tool the page runs, a tool
|
|
5
|
+
* the runtime runs — whatever transport the chat speaks (AG-UI, Vercel AI,
|
|
6
|
+
* A2A, the in-browser harness): `ChatBase` tells it what its adapter told.
|
|
7
|
+
*
|
|
8
|
+
* @module components/inspector/chatInspect
|
|
9
|
+
*/
|
|
10
|
+
import type { MarkedMcpServer } from '../../chat/marks/toolMarks';
|
|
11
|
+
import { type AgentInspectorSink } from './agentInspector';
|
|
12
|
+
export type ChatInspectorRecorder = {
|
|
13
|
+
turnStarted: (prompt: string, model?: string) => void;
|
|
14
|
+
turnEnded: (outcome: {
|
|
15
|
+
text?: string;
|
|
16
|
+
usage?: {
|
|
17
|
+
promptTokens?: number;
|
|
18
|
+
completionTokens?: number;
|
|
19
|
+
totalTokens?: number;
|
|
20
|
+
};
|
|
21
|
+
error?: string;
|
|
22
|
+
}) => void;
|
|
23
|
+
toolStarted: (call: {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
args: Record<string, unknown>;
|
|
27
|
+
}) => void;
|
|
28
|
+
toolEnded: (id: string, outcome: {
|
|
29
|
+
result?: unknown;
|
|
30
|
+
error?: string;
|
|
31
|
+
}) => void;
|
|
32
|
+
};
|
|
33
|
+
/** A recorder for one chat's agent (`actor`) into `sink`. */
|
|
34
|
+
export declare function chatInspectorRecorder(sink: AgentInspectorSink, actor: () => string, options?: {
|
|
35
|
+
/** The page's own tools, by name: frontend tools. */
|
|
36
|
+
frontendTools?: () => readonly string[];
|
|
37
|
+
mcpServers?: () => readonly MarkedMcpServer[];
|
|
38
|
+
}): ChatInspectorRecorder;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { argumentsLine, shortLine, } from './agentInspector';
|
|
6
|
+
import { classifyToolCall } from './classify';
|
|
7
|
+
/** A recorder for one chat's agent (`actor`) into `sink`. */
|
|
8
|
+
export function chatInspectorRecorder(sink, actor, options = {}) {
|
|
9
|
+
let turns = 0;
|
|
10
|
+
let turnKey;
|
|
11
|
+
return {
|
|
12
|
+
turnStarted(prompt, model) {
|
|
13
|
+
turns += 1;
|
|
14
|
+
turnKey = `chat-turn:${turns}`;
|
|
15
|
+
sink.start({
|
|
16
|
+
key: turnKey,
|
|
17
|
+
actor: actor(),
|
|
18
|
+
source: 'model',
|
|
19
|
+
kind: 'turn',
|
|
20
|
+
...(model ? { name: model } : {}),
|
|
21
|
+
summary: shortLine(prompt),
|
|
22
|
+
payload: { prompt, ...(model ? { model } : {}) },
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
turnEnded({ text, usage, error }) {
|
|
26
|
+
if (!turnKey) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const total = usage?.totalTokens ??
|
|
30
|
+
(usage?.promptTokens ?? 0) + (usage?.completionTokens ?? 0);
|
|
31
|
+
sink.end(turnKey, {
|
|
32
|
+
...(error ? { status: 'failed', error } : {}),
|
|
33
|
+
result: {
|
|
34
|
+
...(text ? { text } : {}),
|
|
35
|
+
...(usage ? { usage } : {}),
|
|
36
|
+
},
|
|
37
|
+
...(total ? { summary: `${total} tokens` } : {}),
|
|
38
|
+
});
|
|
39
|
+
turnKey = undefined;
|
|
40
|
+
},
|
|
41
|
+
toolStarted({ id, name, args }) {
|
|
42
|
+
const kind = classifyToolCall(name, args, {
|
|
43
|
+
frontendTools: options.frontendTools?.(),
|
|
44
|
+
mcpServers: options.mcpServers?.(),
|
|
45
|
+
});
|
|
46
|
+
sink.start({
|
|
47
|
+
key: `chat-tool:${id}`,
|
|
48
|
+
actor: actor(),
|
|
49
|
+
source: kind.source,
|
|
50
|
+
...(kind.icon ? { icon: kind.icon } : {}),
|
|
51
|
+
...(kind.emoji ? { emoji: kind.emoji } : {}),
|
|
52
|
+
kind: 'tool-call',
|
|
53
|
+
name,
|
|
54
|
+
summary: argumentsLine(args),
|
|
55
|
+
payload: args,
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
toolEnded(id, { result, error }) {
|
|
59
|
+
sink.end(`chat-tool:${id}`, {
|
|
60
|
+
...(error ? { status: 'failed', error } : {}),
|
|
61
|
+
result,
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a tool call comes from, for the Agent Inspector, by what the
|
|
3
|
+
* catalogue says of its name. Apart from the record itself, which a page can
|
|
4
|
+
* keep without loading the catalogues.
|
|
5
|
+
*
|
|
6
|
+
* @module components/inspector/classify
|
|
7
|
+
*/
|
|
8
|
+
import { type MarkedMcpServer } from '../../chat/marks/toolMarks';
|
|
9
|
+
import type { AgentInspectorSource, ToolClass } from './agentInspector';
|
|
10
|
+
/**
|
|
11
|
+
* Where a tool call comes from, by who claims its name: a skill, one of
|
|
12
|
+
* `mcpServers`, a frontend tool (the catalogue's, or one of `frontendTools`),
|
|
13
|
+
* a runtime tool; otherwise `fallback` — a tool the runtime runs.
|
|
14
|
+
*/
|
|
15
|
+
export declare function classifyToolCall(name: string, args?: Record<string, unknown>, options?: {
|
|
16
|
+
mcpServers?: readonly MarkedMcpServer[];
|
|
17
|
+
frontendTools?: readonly string[];
|
|
18
|
+
fallback?: AgentInspectorSource;
|
|
19
|
+
}): ToolClass;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Where a tool call comes from, for the Agent Inspector, by what the
|
|
7
|
+
* catalogue says of its name. Apart from the record itself, which a page can
|
|
8
|
+
* keep without loading the catalogues.
|
|
9
|
+
*
|
|
10
|
+
* @module components/inspector/classify
|
|
11
|
+
*/
|
|
12
|
+
import { marksOfToolCall, } from '../../chat/marks/toolMarks';
|
|
13
|
+
/**
|
|
14
|
+
* Where a tool call comes from, by who claims its name: a skill, one of
|
|
15
|
+
* `mcpServers`, a frontend tool (the catalogue's, or one of `frontendTools`),
|
|
16
|
+
* a runtime tool; otherwise `fallback` — a tool the runtime runs.
|
|
17
|
+
*/
|
|
18
|
+
export function classifyToolCall(name, args, options = {}) {
|
|
19
|
+
if (options.frontendTools?.includes(name)) {
|
|
20
|
+
const marks = marksOfToolCall(name, args, options.mcpServers);
|
|
21
|
+
return {
|
|
22
|
+
source: 'frontend-tool',
|
|
23
|
+
...(marks?.icon ? { icon: marks.icon } : {}),
|
|
24
|
+
...(marks?.emoji ? { emoji: marks.emoji } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const marks = marksOfToolCall(name, args, options.mcpServers);
|
|
28
|
+
const source = marks?.kind === 'skill'
|
|
29
|
+
? 'skill'
|
|
30
|
+
: marks?.kind === 'mcp'
|
|
31
|
+
? 'mcp'
|
|
32
|
+
: marks?.kind === 'frontend'
|
|
33
|
+
? 'frontend-tool'
|
|
34
|
+
: marks?.kind === 'runtime'
|
|
35
|
+
? 'backend-tool'
|
|
36
|
+
: (options.fallback ?? 'backend-tool');
|
|
37
|
+
return {
|
|
38
|
+
source,
|
|
39
|
+
...(marks?.icon ? { icon: marks.icon } : {}),
|
|
40
|
+
...(marks?.emoji ? { emoji: marks.emoji } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Agent Inspector: what agents do — A2A traffic, MCP calls, skills,
|
|
3
|
+
* frontend and backend tools, model turns, approvals — recorded into a sink
|
|
4
|
+
* and drawn as rows that unfold to their payloads.
|
|
5
|
+
*
|
|
6
|
+
* @module components/inspector
|
|
7
|
+
*/
|
|
8
|
+
export * from './agentInspector';
|
|
9
|
+
export * from './classify';
|
|
10
|
+
export * from './chatInspect';
|
|
11
|
+
export * from './a2aInspect';
|
|
12
|
+
export * from './JsonTree';
|
|
13
|
+
export * from './AgentInspector';
|
|
14
|
+
export * from './AgentInspectorDialog';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The Agent Inspector: what agents do — A2A traffic, MCP calls, skills,
|
|
7
|
+
* frontend and backend tools, model turns, approvals — recorded into a sink
|
|
8
|
+
* and drawn as rows that unfold to their payloads.
|
|
9
|
+
*
|
|
10
|
+
* @module components/inspector
|
|
11
|
+
*/
|
|
12
|
+
export * from './agentInspector';
|
|
13
|
+
export * from './classify';
|
|
14
|
+
export * from './chatInspect';
|
|
15
|
+
export * from './a2aInspect';
|
|
16
|
+
export * from './JsonTree';
|
|
17
|
+
export * from './AgentInspector';
|
|
18
|
+
export * from './AgentInspectorDialog';
|
|
@@ -25,8 +25,14 @@
|
|
|
25
25
|
import type { JSX } from 'react';
|
|
26
26
|
import { type Edge, type EdgeProps, type Viewport } from '@xyflow/react';
|
|
27
27
|
import '@xyflow/react/dist/base.css';
|
|
28
|
+
import { type AssistantAbout } from '../../chat/assistant/AssistantStage';
|
|
29
|
+
import type { BalloonSuggestion } from '../../chat/assistant/SpeechBalloon';
|
|
30
|
+
import type { BalloonHistoryMessage } from '../../chat/assistant/state';
|
|
31
|
+
import type { AssistantMenuItem } from '../../chat/assistant/AssistantContextMenu';
|
|
32
|
+
import type { AgentInspectorSink } from '../inspector/agentInspector';
|
|
28
33
|
import { type A2ATeamCall, type A2ATeamConnection, type A2ATeamFlow } from './a2aTeamFlow';
|
|
29
34
|
import type { A2ATeamPersona } from './useA2ATeam';
|
|
35
|
+
import type { BalloonExpandTarget } from '../../chat/assistant/BalloonVisual';
|
|
30
36
|
import type { BalloonDisplay } from '../../chat/assistant/toolLine';
|
|
31
37
|
/** One member of the team, as the graph draws it. */
|
|
32
38
|
export type A2ATeamGraphMember = {
|
|
@@ -51,10 +57,49 @@ export type A2ATeamGraphMember = {
|
|
|
51
57
|
*/
|
|
52
58
|
balloonDisplay?: BalloonDisplay;
|
|
53
59
|
/**
|
|
54
|
-
* The notebook in its balloon was clicked: by default,
|
|
55
|
-
* `
|
|
60
|
+
* The notebook in its balloon was clicked: by default, it is expanded —
|
|
61
|
+
* into `expandTarget`, scrolled to and focused, or in a dialog.
|
|
56
62
|
*/
|
|
57
63
|
onNotebookOpen?: () => void;
|
|
64
|
+
/**
|
|
65
|
+
* Where the notebook it was given runs (`TeamNotebook`, editable, on the
|
|
66
|
+
* browser sandbox): an element of the page — the area under the graph —
|
|
67
|
+
* where it is drawn as it arrives and when it is expanded; without one,
|
|
68
|
+
* *Expand* opens it in a dialog over the page.
|
|
69
|
+
*/
|
|
70
|
+
expandTarget?: BalloonExpandTarget;
|
|
71
|
+
/** What its notebook is called: `Accounting's notebook`. */
|
|
72
|
+
notebookTitle?: string;
|
|
73
|
+
/**
|
|
74
|
+
* What the person may ask it: chips in its balloon while it waits for a
|
|
75
|
+
* question, and a group of its menu; one chosen is sent (`onSuggestion`).
|
|
76
|
+
*/
|
|
77
|
+
suggestions?: readonly BalloonSuggestion[];
|
|
78
|
+
onSuggestion?: (suggestion: BalloonSuggestion) => void;
|
|
79
|
+
/**
|
|
80
|
+
* The team's Agent Inspector: its menu offers *Inspect the agent…*, its
|
|
81
|
+
* own record (what it did, and the A2A messages it sent or received).
|
|
82
|
+
*/
|
|
83
|
+
inspector?: AgentInspectorSink | null;
|
|
84
|
+
/** Stops its turn, from its menu, while it works. */
|
|
85
|
+
onStop?: () => void;
|
|
86
|
+
/** What its menu's *About* says. */
|
|
87
|
+
about?: AssistantAbout;
|
|
88
|
+
/** The host's own entries of its menu. */
|
|
89
|
+
contextMenu?: readonly AssistantMenuItem[];
|
|
90
|
+
/**
|
|
91
|
+
* What its menu calls a click on it (`onToggle`): `Ask Sales` where it
|
|
92
|
+
* takes the person to the composer. Without `onToggle`, its menu has no
|
|
93
|
+
* such entry.
|
|
94
|
+
*/
|
|
95
|
+
conversationLabel?: string;
|
|
96
|
+
/**
|
|
97
|
+
* What it said and did, as its `history` balloon lists it (`useA2ATeam`'s
|
|
98
|
+
* `entryHistory`, `peerHistory`): shown when its menu chooses *History*.
|
|
99
|
+
*/
|
|
100
|
+
history?: readonly BalloonHistoryMessage[];
|
|
101
|
+
/** Its balloon's display was chosen in its menu: the page may keep it. */
|
|
102
|
+
onBalloonDisplayChange?: (display: BalloonDisplay) => void;
|
|
58
103
|
};
|
|
59
104
|
export type A2ATeamGraphProps = {
|
|
60
105
|
/** The member that asks: drawn on the left. */
|
|
@@ -71,6 +116,12 @@ export type A2ATeamGraphProps = {
|
|
|
71
116
|
size?: number;
|
|
72
117
|
/** The tool calls running now, to the members' connections (`useA2ATeam`). */
|
|
73
118
|
calls?: A2ATeamCall[];
|
|
119
|
+
/**
|
|
120
|
+
* The room above the members for their balloons, in pixels at full scale:
|
|
121
|
+
* a page that shows suggestions, *more* and a notebook in them gives more.
|
|
122
|
+
* A notebook in a balloon gets at least its own.
|
|
123
|
+
*/
|
|
124
|
+
balloonRoom?: number;
|
|
74
125
|
};
|
|
75
126
|
/** The words on the edge for a flow. */
|
|
76
127
|
export declare function flowWords(flow: A2ATeamFlow, entry: string, peer: string): string;
|
|
@@ -85,5 +136,5 @@ export declare function callWords(member: string, connection: A2ATeamConnection,
|
|
|
85
136
|
/** The viewport that shows the graph's width whole in a box `width` wide, never larger than drawn. */
|
|
86
137
|
export declare function teamViewport(width: number): Viewport;
|
|
87
138
|
/** Draw a team of two over A2A, the link moving with what travels on it. */
|
|
88
|
-
export declare function A2ATeamGraph({ entry, peer, flow, connected, label, size, calls, }: A2ATeamGraphProps): JSX.Element;
|
|
139
|
+
export declare function A2ATeamGraph({ entry, peer, flow, connected, label, size, calls, balloonRoom, }: A2ATeamGraphProps): JSX.Element;
|
|
89
140
|
export default A2ATeamGraph;
|
|
@@ -4,16 +4,17 @@ import { BaseEdge, EdgeLabelRenderer, Handle, Position, ReactFlow, } from '@xyfl
|
|
|
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
|
-
import { AssistantStage } from '../../chat/assistant/AssistantStage';
|
|
7
|
+
import { AssistantStage, } from '../../chat/assistant/AssistantStage';
|
|
8
8
|
import { SpecMark } from '../../chat/marks/SpecMark';
|
|
9
9
|
import { toolWords, } from './a2aTeamFlow';
|
|
10
|
-
import { NotebookPreview
|
|
10
|
+
import { NotebookPreview } from './NotebookPreview';
|
|
11
|
+
import { notebookBalloonVisual } from './TeamNotebook';
|
|
11
12
|
import { toolLineText } from '../../chat/assistant/toolLine';
|
|
12
13
|
/** The graph's own coordinates: the members, and room above them for their balloons. */
|
|
13
14
|
const NODE_WIDTH = 200;
|
|
14
15
|
const BALLOON_ROOM = 144;
|
|
15
16
|
/** The room a balloon that holds a notebook needs above its member. */
|
|
16
|
-
const NOTEBOOK_BALLOON_ROOM =
|
|
17
|
+
const NOTEBOOK_BALLOON_ROOM = 480;
|
|
17
18
|
/** How tall the notebook in a balloon grows before it scrolls. */
|
|
18
19
|
const NOTEBOOK_PREVIEW_HEIGHT = 180;
|
|
19
20
|
const GAP = 220;
|
|
@@ -49,10 +50,26 @@ const MemberNode = memo(function MemberNode({ data, }) {
|
|
|
49
50
|
const { id, side, size } = data;
|
|
50
51
|
const member = useContext(Members)[id];
|
|
51
52
|
const stageRef = useRef(null);
|
|
53
|
+
// Its balloon's display, as its menu changes it.
|
|
54
|
+
const [display, setDisplay] = useState();
|
|
52
55
|
if (!member) {
|
|
53
56
|
return null;
|
|
54
57
|
}
|
|
55
58
|
const { persona } = member;
|
|
59
|
+
const shownDisplay = display ?? member.balloonDisplay ?? 'current';
|
|
60
|
+
// History: what it said and did, listed.
|
|
61
|
+
const listed = shownDisplay === 'history' && member.history?.length
|
|
62
|
+
? { history: member.history }
|
|
63
|
+
: {};
|
|
64
|
+
// A notebook it was given: read-only in its balloon, and its large visual
|
|
65
|
+
// the one that runs, editable — under the graph, or in a dialog.
|
|
66
|
+
const notebookTitle = member.notebookTitle ?? (persona.notebook?.name || 'The notebook');
|
|
67
|
+
const given = persona.notebook
|
|
68
|
+
? {
|
|
69
|
+
attachment: (_jsx(NotebookPreview, { notebook: persona.notebook.data, title: notebookTitle, maxHeight: NOTEBOOK_PREVIEW_HEIGHT, onOpen: member.onNotebookOpen, openLabel: member.expandTarget ? 'Open it below to run it' : undefined })),
|
|
70
|
+
visual: notebookBalloonVisual(persona.notebook, notebookTitle),
|
|
71
|
+
}
|
|
72
|
+
: {};
|
|
56
73
|
return (_jsxs(Box
|
|
57
74
|
// React Flow lets the pointer through a node that is neither dragged
|
|
58
75
|
// nor selected (`pointer-events: none` on its wrapper): the character,
|
|
@@ -76,25 +93,34 @@ const MemberNode = memo(function MemberNode({ data, }) {
|
|
|
76
93
|
// middle of the graph: rightward from the left member.
|
|
77
94
|
place: side === 'left'
|
|
78
95
|
? { position: 'absolute', left: 'auto' }
|
|
79
|
-
: { position: 'absolute', right: 'auto' }, stageRef: stageRef, onDragStart: () => undefined, open: false, onToggle: member.onToggle ?? (() => undefined), balloonDisplay:
|
|
96
|
+
: { position: 'absolute', right: 'auto' }, stageRef: stageRef, onDragStart: () => undefined, open: false, onToggle: member.onToggle ?? (() => undefined), balloonDisplay: shownDisplay, onBalloonDisplayChange: next => {
|
|
97
|
+
setDisplay(next);
|
|
98
|
+
member.onBalloonDisplayChange?.(next);
|
|
99
|
+
}, conversationLabel: member.onToggle ? (member.conversationLabel ?? undefined) : false, balloon: persona.tool
|
|
80
100
|
? {
|
|
81
101
|
text: toolLineText(persona.tool),
|
|
82
102
|
tool: persona.tool,
|
|
83
103
|
busy: persona.state !== 'idle',
|
|
104
|
+
...given,
|
|
105
|
+
...listed,
|
|
84
106
|
}
|
|
85
107
|
: persona.saying
|
|
86
108
|
? {
|
|
87
109
|
text: persona.saying,
|
|
110
|
+
...(persona.full ? { fullText: persona.full } : {}),
|
|
88
111
|
more: persona.saying.endsWith('…'),
|
|
89
112
|
speaking: persona.state === 'speaking',
|
|
90
113
|
busy: persona.state === 'thinking' ||
|
|
91
114
|
persona.state === 'working' ||
|
|
92
115
|
persona.state === 'speaking',
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
attachment: persona.notebook ? (_jsx(NotebookPreview, { notebook: persona.notebook.data, title: persona.notebook.name || 'The notebook', maxHeight: NOTEBOOK_PREVIEW_HEIGHT, onOpen: member.onNotebookOpen ?? focusTeamNotebook })) : undefined,
|
|
116
|
+
...given,
|
|
117
|
+
...listed,
|
|
96
118
|
}
|
|
97
|
-
:
|
|
119
|
+
: 'history' in listed
|
|
120
|
+
? { text: '', ...listed }
|
|
121
|
+
: undefined, expandTarget: member.expandTarget, expandOnArrival: !!member.expandTarget,
|
|
122
|
+
// History, chosen in its menu: shown until Current is.
|
|
123
|
+
insist: 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, contextMenu: member.contextMenu,
|
|
98
124
|
// A member of the graph: what is clicked around it is the graph.
|
|
99
125
|
stayPut: true })) }), _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] }));
|
|
100
126
|
});
|
|
@@ -192,7 +218,7 @@ export function teamViewport(width) {
|
|
|
192
218
|
return { zoom, x: (width - WIDTH * zoom) / 2, y: 0 };
|
|
193
219
|
}
|
|
194
220
|
/** Draw a team of two over A2A, the link moving with what travels on it. */
|
|
195
|
-
export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size = 96, calls = [], }) {
|
|
221
|
+
export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size = 96, calls = [], balloonRoom = BALLOON_ROOM, }) {
|
|
196
222
|
const box = useRef(null);
|
|
197
223
|
const [width, setWidth] = useState(0);
|
|
198
224
|
useEffect(() => {
|
|
@@ -225,8 +251,8 @@ export function A2ATeamGraph({ entry, peer, flow, connected, label = 'A2A', size
|
|
|
225
251
|
const hasConnections = placed.some(({ connections }) => connections.length);
|
|
226
252
|
// A notebook in a balloon: more room above the members.
|
|
227
253
|
const room = [entry, peer].some(member => member.persona.notebook)
|
|
228
|
-
? NOTEBOOK_BALLOON_ROOM
|
|
229
|
-
:
|
|
254
|
+
? Math.max(NOTEBOOK_BALLOON_ROOM, balloonRoom)
|
|
255
|
+
: balloonRoom;
|
|
230
256
|
const height = Math.ceil((room +
|
|
231
257
|
memberHeight +
|
|
232
258
|
(hasConnections ? CONNECTION_GAP + connectionHeight : 0)) *
|