@datalayer/agent-runtimes 1.3.74 → 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.
Files changed (55) hide show
  1. package/lib/chat/ChatFloating.d.ts +17 -1
  2. package/lib/chat/ChatFloating.js +52 -5
  3. package/lib/chat/assistant/AssistantContextMenu.d.ts +67 -0
  4. package/lib/chat/assistant/AssistantContextMenu.js +68 -0
  5. package/lib/chat/assistant/AssistantStage.d.ts +95 -1
  6. package/lib/chat/assistant/AssistantStage.js +227 -7
  7. package/lib/chat/assistant/BalloonParts.d.ts +15 -1
  8. package/lib/chat/assistant/BalloonParts.js +26 -9
  9. package/lib/chat/assistant/BalloonVisual.d.ts +10 -2
  10. package/lib/chat/assistant/BalloonVisual.js +3 -3
  11. package/lib/chat/assistant/SpeechBalloon.d.ts +47 -1
  12. package/lib/chat/assistant/SpeechBalloon.js +60 -8
  13. package/lib/chat/base/ChatBase.js +51 -1
  14. package/lib/chat/indicators/TypingDots.d.ts +13 -0
  15. package/lib/chat/indicators/TypingDots.js +21 -0
  16. package/lib/chat/messages/ChatMessageList.js +1 -15
  17. package/lib/components/inspector/AgentInspector.d.ts +59 -0
  18. package/lib/components/inspector/AgentInspector.js +220 -0
  19. package/lib/components/inspector/AgentInspectorDialog.d.ts +19 -0
  20. package/lib/components/inspector/AgentInspectorDialog.js +9 -0
  21. package/lib/components/inspector/JsonTree.d.ts +24 -0
  22. package/lib/components/inspector/JsonTree.js +89 -0
  23. package/lib/components/inspector/a2aInspect.d.ts +56 -0
  24. package/lib/components/inspector/a2aInspect.js +478 -0
  25. package/lib/components/inspector/agentInspector.d.ts +179 -0
  26. package/lib/components/inspector/agentInspector.js +267 -0
  27. package/lib/components/inspector/chatInspect.d.ts +38 -0
  28. package/lib/components/inspector/chatInspect.js +65 -0
  29. package/lib/components/inspector/classify.d.ts +19 -0
  30. package/lib/components/inspector/classify.js +42 -0
  31. package/lib/components/inspector/index.d.ts +14 -0
  32. package/lib/components/inspector/index.js +18 -0
  33. package/lib/components/teams/A2ATeamGraph.d.ts +42 -1
  34. package/lib/components/teams/A2ATeamGraph.js +24 -7
  35. package/lib/components/teams/NotebookPreviewView.js +9 -1
  36. package/lib/components/teams/TeamNotebook.d.ts +3 -1
  37. package/lib/components/teams/TeamNotebook.js +3 -3
  38. package/lib/components/teams/jupyterThemeVariables.d.ts +9 -0
  39. package/lib/components/teams/jupyterThemeVariables.js +441 -0
  40. package/lib/components/teams/teamConnections.d.ts +9 -1
  41. package/lib/components/teams/teamConnections.js +20 -0
  42. package/lib/components/teams/useA2ATeam.d.ts +17 -4
  43. package/lib/components/teams/useA2ATeam.js +87 -23
  44. package/lib/examples/AgentA2ATeamExample.d.ts +13 -0
  45. package/lib/examples/AgentA2ATeamExample.js +61 -13
  46. package/lib/examples/AssistantExample.js +62 -2
  47. package/lib/loop/apps/AppRenderer.js +1 -1
  48. package/lib/loop/core/index.d.ts +17 -0
  49. package/lib/loop/core/index.js +1 -0
  50. package/lib/loop/plugins/assistant/index.d.ts +1 -1
  51. package/lib/loop/plugins/assistant/index.js +8 -3
  52. package/lib/runtimes/browser/a2aPeer.d.ts +4 -1
  53. package/lib/types/chat.d.ts +7 -0
  54. package/package.json +1 -1
  55. package/scripts/codegen/generate_jupyter_theme_variables.mjs +57 -0
@@ -58,6 +58,8 @@ const EphemeralDocument = React.lazy(() => import('../document/EphemeralDocument
58
58
  default: m.EphemeralDocument,
59
59
  })));
60
60
  import { useNotebookTools } from '../../tools/adapters/agent-runtimes/notebookHooks';
61
+ import { useAgentInspectorSink } from '../../components/inspector/agentInspector';
62
+ import { chatInspectorRecorder, } from '../../components/inspector/chatInspect';
61
63
  // Tracks pending prompts already auto-sent for a given conversation scope.
62
64
  // This prevents layout-driven unmount/remount cycles from re-sending prompts.
63
65
  const sentPendingPromptKeys = new Set();
@@ -513,7 +515,7 @@ useStore: useStoreMode = true, protocol: protocolRaw, onSendMessage, onSendReady
513
515
  // Extended props
514
516
  brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, welcome, trailingContent, voice, voiceSpeaking = false, onStopSpeaking, showInformation = false, onInformationClick, headerContent, children, borderRadius, backgroundColor, border, boxShadow, compact = false, placeholder, description = 'Start a conversation with the AI agent.', onStateUpdate, onNewChat, onClear, onMessagesChange, onItemsChange, onDisplayItemsChange, autoFocus = false, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, focusTrigger, frontendTools: frontendToolsProp, enableEphemeralNotebook = false, initialEphemeralNotebookOpen = true, onEphemeralNotebookOpenChange, enableEphemeralDocument = false, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar, ephemeralNotebookToolbarExtraItems, ephemeralDocumentToolbarExtraItems, ephemeralNotebookCollaborationProvider, ephemeralNotebookCollaborationDocumentId, ephemeralDocumentCollaboration, ephemeralRuntimeOverride,
515
517
  // Tool invocation hooks
516
- onToolCallStart, onToolCallComplete,
518
+ onToolCallStart, onToolCallComplete, inspector: inspectorGiven,
517
519
  // Identity/Authorization props
518
520
  onAuthorizationRequired: _onAuthorizationRequired, connectedIdentities,
519
521
  // Conversation persistence
@@ -1589,6 +1591,25 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
1589
1591
  onToolCallStartRef.current = onToolCallStart;
1590
1592
  const onToolCallCompleteRef = useRef(onToolCallComplete);
1591
1593
  onToolCallCompleteRef.current = onToolCallComplete;
1594
+ // The Agent Inspector: the turns and tool calls, recorded as they happen.
1595
+ const inspectorFromContext = useAgentInspectorSink();
1596
+ const inspectorSink = inspectorGiven ?? inspectorFromContext;
1597
+ const inspectorActorRef = useRef('Agent');
1598
+ inspectorActorRef.current =
1599
+ (typeof title === 'string' && title) || activeAgentId || 'Agent';
1600
+ const recorderRef = useRef(null);
1601
+ if (inspectorSink && recorderRef.current?.sink !== inspectorSink) {
1602
+ recorderRef.current = {
1603
+ sink: inspectorSink,
1604
+ recorder: chatInspectorRecorder(inspectorSink, () => inspectorActorRef.current, {
1605
+ frontendTools: () => (frontendToolsRef.current ?? []).map(tool => tool.name),
1606
+ }),
1607
+ };
1608
+ }
1609
+ else if (!inspectorSink) {
1610
+ recorderRef.current = null;
1611
+ }
1612
+ const inspectAnswerRef = useRef('');
1592
1613
  const handleRespondRef = useRef(null);
1593
1614
  // A decision made outside the chat — a host page's own approval banner —
1594
1615
  // is answered exactly as the tool card's buttons answer it: through the
@@ -2415,6 +2436,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2415
2436
  const sanitizedContent = typeof rawContent === 'string'
2416
2437
  ? sanitizeAssistantContent(rawContent)
2417
2438
  : (rawContent ?? '');
2439
+ if (typeof sanitizedContent === 'string' && sanitizedContent) {
2440
+ inspectAnswerRef.current = sanitizedContent;
2441
+ }
2418
2442
  setDisplayItems(prev => {
2419
2443
  const newItems = [...prev];
2420
2444
  const idx = newItems.findIndex(item => !isToolCallMessage(item) && item.id === currentId);
@@ -2520,6 +2544,11 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2520
2544
  toolCallId,
2521
2545
  args,
2522
2546
  });
2547
+ recorderRef.current?.recorder.toolStarted({
2548
+ id: toolCallId,
2549
+ name: toolName,
2550
+ args,
2551
+ });
2523
2552
  const frontendTool = frontendToolsRef.current?.find(t => t.name === toolName);
2524
2553
  const toolHandler = frontendTool?.handler;
2525
2554
  // Execute when the args are complete. The protocol says so
@@ -2604,6 +2633,13 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2604
2633
  setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
2605
2634
  ? updatedToolCall
2606
2635
  : item));
2636
+ recorderRef.current?.recorder.toolEnded(toolCallId, {
2637
+ result: event.toolResult.result,
2638
+ error: event.toolResult.error ??
2639
+ (updatedToolCall.status === 'error'
2640
+ ? (executionError ?? 'failed')
2641
+ : undefined),
2642
+ });
2607
2643
  // Fire post-hook for tool results
2608
2644
  onToolCallCompleteRef.current?.({
2609
2645
  toolName: existingToolCall.toolName,
@@ -2657,6 +2693,11 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2657
2693
  * The bar and its ring simply never appeared for a browser agent.
2658
2694
  */
2659
2695
  {
2696
+ recorderRef.current?.recorder.turnEnded({
2697
+ text: inspectAnswerRef.current || undefined,
2698
+ usage: event.usage,
2699
+ });
2700
+ inspectAnswerRef.current = '';
2660
2701
  const turnInput = event.usage?.promptTokens ?? 0;
2661
2702
  const turnOutput = event.usage?.completionTokens ?? 0;
2662
2703
  // The parts sum to the total when the harness did not send one:
@@ -2710,6 +2751,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2710
2751
  break;
2711
2752
  case 'error':
2712
2753
  console.error('[ChatBase] Protocol error:', event.error);
2754
+ recorderRef.current?.recorder.turnEnded({
2755
+ error: event.error?.message ?? 'error',
2756
+ });
2713
2757
  if (event.error?.message &&
2714
2758
  /exceeded maximum retries/i.test(event.error.message) &&
2715
2759
  hideMessagesAfterToolUIRef.current) {
@@ -2777,6 +2821,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2777
2821
  result,
2778
2822
  status: 'complete',
2779
2823
  };
2824
+ recorderRef.current?.recorder.toolEnded(toolCallId, { result });
2780
2825
  toolCallsRef.current.set(toolCallId, completedToolCall);
2781
2826
  setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
2782
2827
  ? completedToolCall
@@ -2789,6 +2834,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2789
2834
  status: 'error',
2790
2835
  error: err.message,
2791
2836
  };
2837
+ recorderRef.current?.recorder.toolEnded(toolCallId, {
2838
+ error: err.message,
2839
+ });
2792
2840
  toolCallsRef.current.set(toolCallId, errorToolCall);
2793
2841
  setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
2794
2842
  ? errorToolCall
@@ -2838,6 +2886,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2838
2886
  if (!adapterRef.current && !onSendMessage)
2839
2887
  return;
2840
2888
  stoppedRef.current = false;
2889
+ inspectAnswerRef.current = '';
2890
+ recorderRef.current?.recorder.turnStarted(messageContent, selectedModel || undefined);
2841
2891
  suppressAssistantTextForToolOnlyRef.current =
2842
2892
  isToolCallOnlyPrompt(messageContent);
2843
2893
  // Said, then sent: by the microphone at once, or from the composer it
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Three pulsing dots: a speaker is still at work and has not written yet —
3
+ * the chat's waiting indicator, and the assistant's balloon's. Still, but
4
+ * shown, for a reader who asks for reduced motion. Hidden from a screen
5
+ * reader: the live region beside it says the state.
6
+ *
7
+ * @module chat/indicators/TypingDots
8
+ */
9
+ import type { ReactElement } from 'react';
10
+ export declare function TypingDots({ size }: {
11
+ size?: number;
12
+ }): ReactElement;
13
+ export default TypingDots;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box } from '@datalayer/primer-addons';
3
+ export function TypingDots({ size = 8 }) {
4
+ return (_jsx(Box, { "aria-hidden": "true", "data-typing-dots": "", sx: { display: 'inline-flex', alignItems: 'center', gap: '4px' }, children: [0, 0.2, 0.4].map((delay, index) => (_jsx(Box, { sx: {
5
+ width: size,
6
+ height: size,
7
+ borderRadius: '50%',
8
+ bg: 'fg.muted',
9
+ animation: 'typingPulse 1.4s ease-in-out infinite',
10
+ animationDelay: `${delay}s`,
11
+ '@keyframes typingPulse': {
12
+ '0%, 60%, 100%': { transform: 'scale(0.6)', opacity: 0.4 },
13
+ '30%': { transform: 'scale(1)', opacity: 1 },
14
+ },
15
+ '@media (prefers-reduced-motion: reduce)': {
16
+ animation: 'none',
17
+ opacity: 0.7,
18
+ },
19
+ } }, index))) }));
20
+ }
21
+ export default TypingDots;
@@ -18,6 +18,7 @@ import { Streamdown } from 'streamdown';
18
18
  import { streamdownMarkdownStyles, streamdownCodeBlockStyles, } from '../styles/streamdownStyles';
19
19
  import { ToolCallDisplay } from '../tools/ToolCallDisplay';
20
20
  import { marksOfToolCall, skillIdOfCall } from '../marks';
21
+ import { TypingDots } from '../indicators/TypingDots';
21
22
  import { TurnFooter } from './TurnFooter';
22
23
  import { normalizeAssistantMarkdown } from './assistantMarkdown';
23
24
  import { parseFormSubmission } from './formSubmission';
@@ -62,21 +63,6 @@ function toneColorsOf(tone) {
62
63
  fg: `${tone}.fg`,
63
64
  };
64
65
  }
65
- /** Three pulsing dots: a speaker is still writing and has not settled yet. */
66
- function TypingDots({ size = 8 }) {
67
- return (_jsx(Box, { sx: { display: 'flex', alignItems: 'center', gap: '4px' }, children: [0, 0.2, 0.4].map((delay, index) => (_jsx(Box, { sx: {
68
- width: size,
69
- height: size,
70
- borderRadius: '50%',
71
- bg: 'fg.muted',
72
- animation: 'typingPulse 1.4s ease-in-out infinite',
73
- animationDelay: `${delay}s`,
74
- '@keyframes typingPulse': {
75
- '0%, 60%, 100%': { transform: 'scale(0.6)', opacity: 0.4 },
76
- '30%': { transform: 'scale(1)', opacity: 1 },
77
- },
78
- } }, index))) }));
79
- }
80
66
  const SUBAGENT_AVATAR_CONFIG = {
81
67
  userAvatar: _jsx(PersonIcon, { size: 14 }),
82
68
  assistantAvatar: _jsx(DependabotIcon, { size: 14 }),
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The Agent Inspector: what agents do, one row a thing, newest last — the
3
+ * time (to the millisecond, and since the first), who did it and toward
4
+ * whom, its source and kind, its name with its mark, its status and
5
+ * duration, and a line that says it. A row unfolds to its payload as a JSON
6
+ * tree (a call's arguments and its result), and to what was made of it.
7
+ *
8
+ * Filters by source, kind and actor; *Pause* holds the rows still while the
9
+ * record goes on, *Clear* empties it, *Export* saves the session as JSON. A
10
+ * large payload (a notebook) says its size and stays folded until asked.
11
+ *
12
+ * Fed by a sink (`useAgentInspector`) or given its entries.
13
+ *
14
+ * @module components/inspector/AgentInspector
15
+ */
16
+ import type { JSX } from 'react';
17
+ import { type AgentInspectorEntry, type AgentInspectorExport, type AgentInspectorSink, type AgentInspectorSource } from './agentInspector';
18
+ export type AgentInspectorProps = {
19
+ /** The record to show, kept current. */
20
+ sink?: AgentInspectorSink | null;
21
+ /** Or its entries, as given. */
22
+ entries?: readonly AgentInspectorEntry[];
23
+ /** How tall the rows grow before they scroll. */
24
+ maxHeight?: number | string;
25
+ /** Payloads larger than this, in bytes, stay folded until asked. */
26
+ largeBytes?: number;
27
+ /** What is said while nothing is recorded. */
28
+ emptyText?: string;
29
+ /** The file name the session is exported as, without `.json`. */
30
+ exportName?: string;
31
+ /**
32
+ * One agent's own record: what it did (`actor`), and the A2A messages it
33
+ * sent or received — one message shows in the sender's and in the
34
+ * receiver's, the same entry, as sent in one and received in the other.
35
+ */
36
+ agent?: string;
37
+ };
38
+ /** Whether an entry is one agent's: done by it, or sent to or by it. */
39
+ export declare function isAgentsEntry(entry: AgentInspectorEntry, agent: string): boolean;
40
+ /** Which way an entry went, from one agent's side: `→ Accounting`, `← Accounting`. */
41
+ export declare function directionFrom(entry: AgentInspectorEntry, agent: string | undefined): string;
42
+ /** A time of day, to the millisecond: `15:44:31.299`. */
43
+ export declare function clockTime(at: number): string;
44
+ /** Time since the first entry: `+1.204s`. */
45
+ export declare function sinceFirst(at: number, first: number): string;
46
+ /** How long a call took: `412 ms`, `3.4 s`. */
47
+ export declare function durationText(ms: number): string;
48
+ export type AgentInspectorFilter = {
49
+ sources: ReadonlySet<AgentInspectorSource>;
50
+ kinds: ReadonlySet<string>;
51
+ actors: ReadonlySet<string>;
52
+ };
53
+ /** The entries a filter keeps: an empty set keeps every value. */
54
+ export declare function filterEntries(entries: readonly AgentInspectorEntry[], filter: AgentInspectorFilter): AgentInspectorEntry[];
55
+ /** A session, as JSON. */
56
+ export declare function exportEntries(entries: readonly AgentInspectorEntry[]): AgentInspectorExport;
57
+ /** The Agent Inspector. */
58
+ export declare function AgentInspector({ sink, entries: given, maxHeight, largeBytes, emptyText, exportName, agent, }: AgentInspectorProps): JSX.Element;
59
+ export default AgentInspector;
@@ -0,0 +1,220 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo, useState } from 'react';
3
+ import { ActionList, ActionMenu, Button, Label, Text } from '@primer/react';
4
+ import { Box } from '@datalayer/primer-addons';
5
+ import { DownloadIcon, PlayIcon, TrashIcon, PauseIcon, } from '@primer/octicons-react';
6
+ import { SpecMark } from '../../chat/marks';
7
+ import { AGENT_INSPECTOR_SOURCES, AGENT_INSPECTOR_SOURCE_LABELS, formatBytes, useAgentInspectorEntries, } from './agentInspector';
8
+ import { JsonTree } from './JsonTree';
9
+ /** Whether an entry is one agent's: done by it, or sent to or by it. */
10
+ export function isAgentsEntry(entry, agent) {
11
+ return (entry.actor === agent ||
12
+ entry.direction?.from === agent ||
13
+ entry.direction?.to === agent);
14
+ }
15
+ /** Which way an entry went, from one agent's side: `→ Accounting`, `← Accounting`. */
16
+ export function directionFrom(entry, agent) {
17
+ const direction = entry.direction;
18
+ if (!direction) {
19
+ return entry.actor;
20
+ }
21
+ if (agent === direction.from) {
22
+ return `sent → ${direction.to}`;
23
+ }
24
+ if (agent === direction.to) {
25
+ return `received ← ${direction.from}`;
26
+ }
27
+ return `${direction.from} → ${direction.to}`;
28
+ }
29
+ /** A kind, as its label is coloured. */
30
+ const KIND_VARIANTS = {
31
+ request: 'accent',
32
+ 'status-update': 'attention',
33
+ 'artifact-update': 'success',
34
+ task: 'secondary',
35
+ message: 'done',
36
+ 'agent-card': 'secondary',
37
+ response: 'secondary',
38
+ 'tool-call': 'sponsors',
39
+ turn: 'primary',
40
+ error: 'danger',
41
+ approval: 'severe',
42
+ };
43
+ const pad = (value, length = 2) => String(value).padStart(length, '0');
44
+ /** A time of day, to the millisecond: `15:44:31.299`. */
45
+ export function clockTime(at) {
46
+ const date = new Date(at);
47
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}`;
48
+ }
49
+ /** Time since the first entry: `+1.204s`. */
50
+ export function sinceFirst(at, first) {
51
+ return `+${((at - first) / 1000).toFixed(3)}s`;
52
+ }
53
+ /** How long a call took: `412 ms`, `3.4 s`. */
54
+ export function durationText(ms) {
55
+ return ms < 1000 ? `${Math.round(ms)} ms` : `${(ms / 1000).toFixed(1)} s`;
56
+ }
57
+ /** The entries a filter keeps: an empty set keeps every value. */
58
+ export function filterEntries(entries, filter) {
59
+ return entries.filter(entry => (!filter.sources.size || filter.sources.has(entry.source)) &&
60
+ (!filter.kinds.size || filter.kinds.has(entry.kind)) &&
61
+ (!filter.actors.size || filter.actors.has(entry.actor)));
62
+ }
63
+ /** A session, as JSON. */
64
+ export function exportEntries(entries) {
65
+ return { exportedAt: new Date().toISOString(), entries: [...entries] };
66
+ }
67
+ function toggle(set, value) {
68
+ const next = new Set(set);
69
+ if (next.has(value)) {
70
+ next.delete(value);
71
+ }
72
+ else {
73
+ next.add(value);
74
+ }
75
+ return next;
76
+ }
77
+ function FilterMenu({ label, options, selected, onToggle, onAll, name, }) {
78
+ return (_jsxs(ActionMenu, { children: [_jsxs(ActionMenu.Button, { size: "small", "data-inspector-filter": label.toLowerCase(), children: [label, selected.size ? ` · ${selected.size}` : ''] }), _jsx(ActionMenu.Overlay, { width: "auto", children: _jsxs(ActionList, { selectionVariant: "multiple", "aria-label": label, children: [_jsx(ActionList.Item, { selected: !selected.size, onSelect: onAll, children: "All" }), _jsx(ActionList.Divider, {}), options.map(option => (_jsx(ActionList.Item, { selected: selected.has(option), onSelect: () => onToggle(option), children: name(option) }, option)))] }) })] }));
79
+ }
80
+ function Payload({ title, value, large, }) {
81
+ const bytes = useMemo(() => {
82
+ try {
83
+ return new TextEncoder().encode(JSON.stringify(value) ?? '').length;
84
+ }
85
+ catch {
86
+ return 0;
87
+ }
88
+ }, [value]);
89
+ const [shown, setShown] = useState(bytes <= large);
90
+ return (_jsxs(Box, { sx: { minWidth: 0 }, "data-inspector-payload": title, children: [_jsxs(Text, { sx: {
91
+ fontSize: 0,
92
+ color: 'fg.muted',
93
+ fontWeight: 600,
94
+ display: 'block',
95
+ }, children: [title, " \u00B7 ", formatBytes(bytes)] }), shown ? (_jsx(JsonTree, { value: value, expandDepth: bytes > large ? 1 : 2 })) : (_jsxs(Button, { size: "small", onClick: () => setShown(true), "data-inspector-large": "", children: ["Show ", formatBytes(bytes)] }))] }));
96
+ }
97
+ function Row({ entry, first, open, onToggle, large, agent, }) {
98
+ const ended = entry.endedAt !== undefined;
99
+ return (_jsxs(Box, { as: "li", "data-inspector-entry": entry.id, "data-inspector-source": entry.source, "data-inspector-kind": entry.kind, "data-inspector-status": entry.status ?? '', sx: {
100
+ borderBottom: '1px solid',
101
+ borderColor: 'border.muted',
102
+ listStyle: 'none',
103
+ }, children: [_jsxs(Box, { as: "button", type: "button", "aria-expanded": open, onClick: onToggle, sx: {
104
+ width: '100%',
105
+ display: 'grid',
106
+ gridTemplateColumns: ['auto 1fr', 'auto auto auto minmax(0, 1fr)'],
107
+ columnGap: 2,
108
+ rowGap: 1,
109
+ alignItems: 'center',
110
+ px: 2,
111
+ py: 1,
112
+ border: 0,
113
+ bg: open ? 'canvas.subtle' : 'transparent',
114
+ color: 'fg.default',
115
+ font: 'inherit',
116
+ fontSize: 0,
117
+ textAlign: 'left',
118
+ cursor: 'pointer',
119
+ '&:hover': { bg: 'canvas.subtle' },
120
+ }, children: [_jsxs(Box, { sx: { fontFamily: 'mono', color: 'fg.muted', whiteSpace: 'nowrap' }, children: [_jsx("span", { title: new Date(entry.at).toISOString(), children: clockTime(entry.at) }), ' ', _jsx(Text, { sx: { opacity: 0.7 }, children: sinceFirst(entry.at, first) })] }), _jsx(Text, { sx: { whiteSpace: 'nowrap', fontWeight: 600 }, "data-inspector-direction": "", children: directionFrom(entry, agent) }), _jsxs(Box, { sx: { display: 'flex', gap: 1, flexWrap: 'wrap' }, children: [_jsx(Label, { size: "small", variant: "secondary", children: AGENT_INSPECTOR_SOURCE_LABELS[entry.source] }), _jsx(Label, { size: "small", variant: KIND_VARIANTS[entry.kind] ?? 'secondary', children: entry.kind })] }), _jsxs(Box, { sx: { display: 'flex', gap: 1, alignItems: 'center', minWidth: 0 }, children: [(entry.icon || entry.emoji) && (_jsx(SpecMark, { icon: entry.icon, emoji: entry.emoji, size: 14 })), entry.name && entry.kind !== 'request' && (_jsx(Text, { sx: { fontFamily: 'mono', fontWeight: 600, whiteSpace: 'nowrap' }, children: entry.name })), entry.status && (_jsxs(Label, { size: "small", variant: entry.status === 'failed'
121
+ ? 'danger'
122
+ : entry.status === 'running'
123
+ ? 'attention'
124
+ : 'success', children: [entry.status, ended && entry.endedAt !== undefined
125
+ ? ` · ${durationText(entry.endedAt - entry.at)}`
126
+ : ''] })), _jsx(Text, { sx: {
127
+ color: 'fg.muted',
128
+ overflow: 'hidden',
129
+ textOverflow: 'ellipsis',
130
+ whiteSpace: 'nowrap',
131
+ minWidth: 0,
132
+ }, title: entry.summary, children: entry.summary }), entry.bytes > large && (_jsx(Text, { sx: { color: 'fg.muted', whiteSpace: 'nowrap' }, children: formatBytes(entry.bytes) }))] })] }), open && (_jsxs(Box, { "data-inspector-details": "", sx: {
133
+ px: 3,
134
+ py: 2,
135
+ display: 'flex',
136
+ flexDirection: 'column',
137
+ gap: 2,
138
+ bg: 'canvas.subtle',
139
+ }, children: [entry.a2a && (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', fontFamily: 'mono' }, children: [
140
+ entry.a2a.method && `method ${entry.a2a.method}`,
141
+ entry.a2a.rpcId !== undefined && `id ${entry.a2a.rpcId}`,
142
+ entry.a2a.taskId && `task ${entry.a2a.taskId}`,
143
+ entry.a2a.contextId && `context ${entry.a2a.contextId}`,
144
+ entry.a2a.state && entry.a2a.state,
145
+ entry.a2a.chunks &&
146
+ entry.a2a.chunks > 1 &&
147
+ `${entry.a2a.chunks} chunks`,
148
+ ]
149
+ .filter(Boolean)
150
+ .join(' · ') })), entry.error && (_jsx(Text, { sx: { color: 'danger.fg', fontSize: 0 }, children: entry.error })), entry.payload !== undefined && (_jsx(Payload, { title: entry.kind === 'tool-call' ? 'Arguments' : 'Payload', value: entry.payload, large: large })), entry.result !== undefined && (_jsx(Payload, { title: "Result", value: entry.result, large: large })), entry.events.length > 0 && (_jsx(Payload, { title: "Events", value: entry.events, large: large })), entry.links.length > 0 && (_jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["Linked: ", entry.links.join(', ')] }))] }))] }));
151
+ }
152
+ /** Save a session as a JSON file. */
153
+ function download(name, session) {
154
+ const blob = new Blob([JSON.stringify(session, null, 2)], {
155
+ type: 'application/json',
156
+ });
157
+ const url = URL.createObjectURL(blob);
158
+ const link = document.createElement('a');
159
+ link.href = url;
160
+ link.download = `${name}.json`;
161
+ link.click();
162
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
163
+ }
164
+ /** The Agent Inspector. */
165
+ export function AgentInspector({ sink, entries: given, maxHeight = 420, largeBytes = 16_000, emptyText = 'Nothing recorded yet.', exportName = 'agent-inspector', agent, }) {
166
+ const live = useAgentInspectorEntries(sink);
167
+ const everything = given ?? live;
168
+ const all = useMemo(() => agent
169
+ ? everything.filter(entry => isAgentsEntry(entry, agent))
170
+ : everything, [everything, agent]);
171
+ const [held, setHeld] = useState(null);
172
+ const [clearedAt, setClearedAt] = useState(0);
173
+ const [sources, setSources] = useState(new Set());
174
+ const [kinds, setKinds] = useState(new Set());
175
+ const [actors, setActors] = useState(new Set());
176
+ const [open, setOpen] = useState(new Set());
177
+ const recorded = useMemo(() => (clearedAt ? all.filter(entry => entry.at >= clearedAt) : all), [all, clearedAt]);
178
+ const shown = held ?? recorded;
179
+ const waiting = held ? recorded.length - held.length : 0;
180
+ const kindOptions = useMemo(() => [...new Set(recorded.map(entry => entry.kind))].sort(), [recorded]);
181
+ const actorOptions = useMemo(() => [...new Set(recorded.map(entry => entry.actor))].sort(), [recorded]);
182
+ const sourceOptions = useMemo(() => AGENT_INSPECTOR_SOURCES.filter(source => recorded.some(entry => entry.source === source)), [recorded]);
183
+ const rows = useMemo(() => filterEntries(shown, { sources, kinds, actors }), [shown, sources, kinds, actors]);
184
+ const first = shown[0]?.at ?? 0;
185
+ return (_jsxs(Box, { "data-agent-inspector": "", sx: {
186
+ border: '1px solid',
187
+ borderColor: 'border.default',
188
+ borderRadius: 2,
189
+ bg: 'canvas.default',
190
+ color: 'fg.default',
191
+ minWidth: 0,
192
+ }, children: [_jsxs(Box, { role: "toolbar", "aria-label": agent ? `${agent}'s Agent Inspector` : 'Agent Inspector', sx: {
193
+ display: 'flex',
194
+ flexWrap: 'wrap',
195
+ gap: 2,
196
+ alignItems: 'center',
197
+ p: 2,
198
+ borderBottom: '1px solid',
199
+ borderColor: 'border.default',
200
+ }, children: [_jsx(FilterMenu, { label: "Source", options: sourceOptions, selected: sources, onToggle: value => setSources(toggle(sources, value)), onAll: () => setSources(new Set()), name: value => AGENT_INSPECTOR_SOURCE_LABELS[value] }), _jsx(FilterMenu, { label: "Kind", options: kindOptions, selected: kinds, onToggle: value => setKinds(toggle(kinds, value)), onAll: () => setKinds(new Set()), name: value => value }), _jsx(FilterMenu, { label: "Actor", options: actorOptions, selected: actors, onToggle: value => setActors(toggle(actors, value)), onAll: () => setActors(new Set()), name: value => value }), _jsx(Box, { sx: { flex: 1 } }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, "data-inspector-count": "", children: [rows.length === shown.length
201
+ ? `${rows.length} ${rows.length === 1 ? 'entry' : 'entries'}`
202
+ : `${rows.length} of ${shown.length}`, waiting > 0 ? ` · ${waiting} new` : ''] }), _jsx(Button, { size: "small", leadingVisual: held ? PlayIcon : PauseIcon, "aria-pressed": held !== null, onClick: () => setHeld(held ? null : recorded), "data-inspector-pause": "", children: held ? 'Resume' : 'Pause' }), _jsx(Button, { size: "small", leadingVisual: TrashIcon, onClick: () => {
203
+ setOpen(new Set());
204
+ setHeld(null);
205
+ // One agent's view of a shared record clears only itself.
206
+ if (sink && !given && !agent) {
207
+ sink.clear();
208
+ }
209
+ else {
210
+ setClearedAt(Date.now());
211
+ }
212
+ }, "data-inspector-clear": "", children: "Clear" }), _jsx(Button, { size: "small", leadingVisual: DownloadIcon, disabled: !recorded.length, onClick: () => download(exportName, exportEntries(recorded)), "data-inspector-export": "", children: "Export" })] }), _jsx(Box, { as: "ol", "aria-label": "Recorded", sx: { m: 0, p: 0, maxHeight, overflowY: 'auto' }, children: rows.length === 0 ? (_jsx(Text, { as: "li", sx: {
213
+ display: 'block',
214
+ p: 3,
215
+ color: 'fg.muted',
216
+ fontSize: 1,
217
+ listStyle: 'none',
218
+ }, children: emptyText })) : (rows.map(entry => (_jsx(Row, { entry: entry, first: first, large: largeBytes, agent: agent, open: open.has(entry.id), onToggle: () => setOpen(toggle(open, entry.id)) }, entry.id)))) })] }));
219
+ }
220
+ export default AgentInspector;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The Agent Inspector over the page: a large dialog, the inspector loaded
3
+ * only when it opens, fed by the sink the agent's chat or team feeds. Esc
4
+ * closes it.
5
+ *
6
+ * @module components/inspector/AgentInspectorDialog
7
+ */
8
+ import type { JSX } from 'react';
9
+ import type { AgentInspectorSink } from './agentInspector';
10
+ export type AgentInspectorDialogProps = {
11
+ sink: AgentInspectorSink | null;
12
+ /** The dialog's title. */
13
+ title?: string;
14
+ /** One agent's own record (`AgentInspector`'s `agent`); all of it without. */
15
+ agent?: string;
16
+ onClose: () => void;
17
+ };
18
+ export declare function AgentInspectorDialog({ sink, title, agent, onClose, }: AgentInspectorDialogProps): JSX.Element;
19
+ export default AgentInspectorDialog;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Suspense, lazy } from 'react';
3
+ import { Dialog } from '@primer/react';
4
+ import { Text } from '@primer/react';
5
+ const AgentInspector = lazy(() => import('./AgentInspector'));
6
+ export function AgentInspectorDialog({ sink, title = 'Agent Inspector', agent, onClose, }) {
7
+ return (_jsx(Dialog, { title: title, onClose: onClose, width: "xlarge", height: "large", "data-agent-inspector-dialog": "", children: _jsx(Suspense, { fallback: _jsx(Text, { sx: { color: 'fg.muted' }, children: "Loading the Agent Inspector\u2026" }), children: _jsx(AgentInspector, { sink: sink, agent: agent, maxHeight: "calc(80vh - 140px)", emptyText: "Nothing recorded yet: what the agent does shows here as it does it." }) }) }));
8
+ }
9
+ export default AgentInspectorDialog;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A JSON value as a tree: objects and arrays fold and unfold, each value
3
+ * coloured by its type (theme colours, light and dark), long strings cut
4
+ * until asked for whole, and the whole value copied as JSON.
5
+ *
6
+ * @module components/inspector/JsonTree
7
+ */
8
+ import type { JSX } from 'react';
9
+ export type JsonTreeProps = {
10
+ value: unknown;
11
+ /** The root's name, shown as its key. */
12
+ name?: string;
13
+ /** How many levels open at first. */
14
+ expandDepth?: number;
15
+ /** Offer *Copy JSON* above the tree. */
16
+ copyable?: boolean;
17
+ /** Longer strings are cut until clicked. */
18
+ stringLimit?: number;
19
+ };
20
+ /** A value's type, as the tree colours it. */
21
+ export declare function jsonType(value: unknown): 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object' | 'undefined';
22
+ /** A JSON value, as a tree. */
23
+ export declare function JsonTree({ value, name, expandDepth, copyable, stringLimit, }: JsonTreeProps): JSX.Element;
24
+ export default JsonTree;
@@ -0,0 +1,89 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Button, Text } from '@primer/react';
4
+ import { Box } from '@datalayer/primer-addons';
5
+ import { CopyIcon, CheckIcon } from '@primer/octicons-react';
6
+ const MONO = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace';
7
+ /** A value's type, as the tree colours it. */
8
+ export function jsonType(value) {
9
+ if (value === null) {
10
+ return 'null';
11
+ }
12
+ if (Array.isArray(value)) {
13
+ return 'array';
14
+ }
15
+ const type = typeof value;
16
+ if (type === 'string' || type === 'number' || type === 'boolean') {
17
+ return type;
18
+ }
19
+ if (type === 'bigint') {
20
+ return 'number';
21
+ }
22
+ return type === 'undefined' ? 'undefined' : 'object';
23
+ }
24
+ const COLOURS = {
25
+ string: 'success.fg',
26
+ number: 'accent.fg',
27
+ boolean: 'done.fg',
28
+ null: 'fg.muted',
29
+ undefined: 'fg.muted',
30
+ };
31
+ function Leaf({ value, stringLimit, }) {
32
+ const type = jsonType(value);
33
+ const [whole, setWhole] = useState(false);
34
+ if (type === 'string') {
35
+ const text = value;
36
+ const cut = !whole && text.length > stringLimit;
37
+ return (_jsxs(Text, { "data-json-type": "string", sx: {
38
+ color: COLOURS.string,
39
+ whiteSpace: 'pre-wrap',
40
+ wordBreak: 'break-word',
41
+ }, children: [JSON.stringify(cut ? text.slice(0, stringLimit) : text), cut && (_jsxs(Box, { as: "button", type: "button", onClick: () => setWhole(true), sx: {
42
+ ml: 1,
43
+ p: 0,
44
+ border: 0,
45
+ bg: 'transparent',
46
+ color: 'accent.fg',
47
+ cursor: 'pointer',
48
+ font: 'inherit',
49
+ }, children: ["\u2026 ", text.length - stringLimit, " more"] }))] }));
50
+ }
51
+ return (_jsx(Text, { "data-json-type": type, sx: { color: COLOURS[type] ?? 'fg.default' }, children: type === 'undefined' ? 'undefined' : String(value) }));
52
+ }
53
+ function Node({ name, value, depth, expandDepth, stringLimit, }) {
54
+ const type = jsonType(value);
55
+ const branch = type === 'array' || type === 'object';
56
+ const [open, setOpen] = useState(depth < expandDepth);
57
+ const key = name !== undefined ? (_jsxs(Text, { sx: { color: 'fg.default', fontWeight: 600 }, children: [name, ": "] })) : null;
58
+ if (!branch) {
59
+ return (_jsxs(Box, { role: "treeitem", "data-json-node": "", sx: { pl: 3 }, children: [key, _jsx(Leaf, { value: value, stringLimit: stringLimit })] }));
60
+ }
61
+ const items = type === 'array'
62
+ ? value.map((item, index) => [String(index), item])
63
+ : Object.entries(value);
64
+ const [openMark, closeMark] = type === 'array' ? ['[', ']'] : ['{', '}'];
65
+ return (_jsxs(Box, { role: "treeitem", "aria-expanded": open, "data-json-node": "", sx: { pl: depth ? 3 : 0 }, children: [_jsxs(Box, { as: "button", type: "button", onClick: () => setOpen(!open), "aria-label": `${open ? 'Fold' : 'Unfold'} ${name ?? 'the value'}`, "data-json-toggle": "", sx: {
66
+ p: 0,
67
+ border: 0,
68
+ bg: 'transparent',
69
+ color: 'inherit',
70
+ font: 'inherit',
71
+ cursor: 'pointer',
72
+ textAlign: 'left',
73
+ }, children: [_jsx(Text, { sx: { color: 'fg.muted', display: 'inline-block', width: '1em' }, children: open ? '▾' : '▸' }), key, _jsx(Text, { sx: { color: 'fg.muted' }, children: open
74
+ ? openMark
75
+ : `${openMark} ${items.length} ${type === 'array' ? (items.length === 1 ? 'item' : 'items') : items.length === 1 ? 'key' : 'keys'} ${closeMark}` })] }), open && (_jsxs(Box, { role: "group", children: [items.map(([itemName, item]) => (_jsx(Node, { name: itemName, value: item, depth: depth + 1, expandDepth: expandDepth, stringLimit: stringLimit }, itemName))), _jsx(Text, { sx: { color: 'fg.muted', pl: 3 }, children: closeMark })] }))] }));
76
+ }
77
+ /** A JSON value, as a tree. */
78
+ export function JsonTree({ value, name, expandDepth = 1, copyable = true, stringLimit = 240, }) {
79
+ const [copied, setCopied] = useState(false);
80
+ const copy = () => {
81
+ const text = JSON.stringify(value, null, 2) ?? 'undefined';
82
+ void navigator.clipboard?.writeText(text).then(() => {
83
+ setCopied(true);
84
+ setTimeout(() => setCopied(false), 1500);
85
+ }, () => undefined);
86
+ };
87
+ return (_jsxs(Box, { "data-json-tree": "", sx: { fontFamily: MONO, fontSize: 0, lineHeight: 1.6, minWidth: 0 }, children: [copyable && (_jsx(Button, { size: "small", variant: "invisible", leadingVisual: copied ? CheckIcon : CopyIcon, onClick: copy, "data-json-copy": "", sx: { float: 'right' }, children: copied ? 'Copied' : 'Copy JSON' })), _jsx(Box, { role: "tree", "aria-label": name ?? 'JSON', children: _jsx(Node, { name: name, value: value, depth: 0, expandDepth: expandDepth, stringLimit: stringLimit }) })] }));
88
+ }
89
+ export default JsonTree;