@datalayer/agent-runtimes 1.3.84 → 1.3.85
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 +5 -4
- package/lib/chat/ChatFloating.js +8 -6
- package/lib/chat/ChatLanguage.d.ts +17 -0
- package/lib/chat/ChatLanguage.js +29 -0
- package/lib/chat/assistant/AssistantStage.d.ts +4 -4
- package/lib/chat/assistant/AssistantStage.js +4 -2
- package/lib/chat/assistant/BalloonParts.d.ts +2 -1
- package/lib/chat/assistant/BalloonParts.js +12 -5
- package/lib/chat/assistant/BalloonVisual.js +3 -1
- package/lib/chat/assistant/ConversationBalloon.d.ts +1 -1
- package/lib/chat/assistant/ConversationBalloon.js +3 -2
- package/lib/chat/assistant/SpeechBalloon.d.ts +1 -1
- package/lib/chat/assistant/SpeechBalloon.js +10 -7
- package/lib/chat/assistant/toolLine.d.ts +8 -4
- package/lib/chat/assistant/toolLine.js +13 -9
- package/lib/chat/base/ChatBase.js +14 -11
- package/lib/chat/display/EmptyState.js +3 -1
- package/lib/chat/index.d.ts +2 -0
- package/lib/chat/index.js +3 -0
- package/lib/chat/messages/ChatMessages.js +3 -1
- package/lib/chat/messages/TurnFooter.js +3 -1
- package/lib/chat/prompt/InputPrompt.js +3 -1
- package/lib/chat/prompt/InputPromptLexical.js +3 -1
- package/lib/chat/prompt/footer/InPromptFooter.js +3 -1
- package/lib/chat/tools/ToolApprovalBanner.js +5 -3
- package/lib/chat/tools/ToolApprovalDialog.js +5 -3
- package/lib/chat/tools/ToolCallDisplay.js +29 -25
- package/lib/chat/words.d.ts +102 -0
- package/lib/chat/words.js +460 -0
- package/lib/components/a2ui/custom/index.d.ts +39 -0
- package/lib/components/a2ui/custom/index.js +95 -0
- package/lib/components/a2ui/custom/sandbox.d.ts +73 -0
- package/lib/components/a2ui/custom/sandbox.js +188 -0
- package/lib/components/a2ui/datalayer/index.d.ts +1 -1
- package/lib/components/a2ui/datalayer/index.js +1 -1
- package/lib/components/a2ui/datalayer/schema.d.ts +10 -0
- package/lib/components/a2ui/datalayer/schema.js +12 -1
- package/lib/components/a2ui/index.d.ts +1 -0
- package/lib/components/a2ui/index.js +1 -0
- package/lib/components/a2ui/visibleWhen.d.ts +12 -1
- package/lib/components/a2ui/visibleWhen.js +24 -3
- package/lib/components/inspector/AgentInspector.d.ts +25 -44
- package/lib/components/inspector/AgentInspector.js +27 -215
- package/lib/components/inspector/AgentInspectorDialog.d.ts +6 -6
- package/lib/components/inspector/AgentInspectorDialog.js +2 -2
- package/lib/components/inspector/a2aSpans.d.ts +51 -0
- package/lib/components/inspector/a2aSpans.js +373 -0
- package/lib/components/inspector/agentSpans.d.ts +120 -0
- package/lib/components/inspector/agentSpans.js +253 -0
- package/lib/components/inspector/chatSpans.d.ts +36 -0
- package/lib/components/inspector/chatSpans.js +50 -0
- package/lib/components/inspector/classify.d.ts +11 -8
- package/lib/components/inspector/classify.js +23 -24
- package/lib/components/inspector/index.d.ts +7 -7
- package/lib/components/inspector/index.js +7 -7
- package/lib/components/teams/A2ATeamGraph.d.ts +4 -4
- package/lib/components/teams/SceneTranscript.d.ts +29 -0
- package/lib/components/teams/SceneTranscript.js +54 -0
- package/lib/components/teams/SceneView.d.ts +41 -0
- package/lib/components/teams/SceneView.js +28 -0
- package/lib/components/teams/index.d.ts +3 -0
- package/lib/components/teams/index.js +3 -0
- package/lib/components/teams/sceneTranscript.d.ts +74 -0
- package/lib/components/teams/sceneTranscript.js +294 -0
- package/lib/components/teams/teamConnections.d.ts +2 -2
- package/lib/components/teams/teamConnections.js +3 -3
- package/lib/components/teams/useA2ATeam.d.ts +4 -4
- package/lib/components/teams/useA2ATeam.js +50 -46
- package/lib/examples/AgentA2ATeamExample.d.ts +10 -9
- package/lib/examples/AgentA2ATeamExample.js +70 -60
- package/lib/examples/AssistantExample.js +2 -2
- package/lib/loop/apps/AppComposer.d.ts +2 -1
- package/lib/loop/apps/AppComposer.js +6 -3
- package/lib/loop/apps/AppRenderer.d.ts +2 -1
- package/lib/loop/apps/AppRenderer.js +34 -25
- package/lib/loop/apps/appspec.d.ts +11 -1
- package/lib/loop/apps/appspec.js +115 -1
- package/lib/loop/apps/checks.d.ts +8 -0
- package/lib/loop/apps/checks.js +106 -4
- package/lib/loop/apps/customComponents.d.ts +41 -0
- package/lib/loop/apps/customComponents.js +260 -0
- package/lib/loop/apps/hostTools.d.ts +8 -1
- package/lib/loop/apps/hostTools.js +9 -0
- package/lib/loop/apps/index.d.ts +1 -0
- package/lib/loop/apps/index.js +1 -0
- package/lib/loop/core/a2uiComponents.d.ts +10 -0
- package/lib/loop/core/a2uiComponents.js +15 -0
- package/lib/loop/core/index.d.ts +1 -0
- package/lib/loop/core/index.js +3 -0
- package/lib/loop/embed/AppEmbed.d.ts +11 -3
- package/lib/loop/embed/AppEmbed.js +42 -16
- package/lib/loop/embed/element.d.ts +5 -0
- package/lib/loop/embed/element.js +61 -5
- package/lib/loop/embed/embedConfig.d.ts +8 -0
- package/lib/loop/embed/embedConfig.js +18 -0
- package/lib/loop/embed/hostBridge.d.ts +28 -2
- package/lib/loop/embed/hostBridge.js +22 -2
- package/lib/loop/index.d.ts +4 -3
- package/lib/loop/index.js +4 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +7 -1
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +7 -2
- package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +4 -1
- package/lib/loop/plugins/a2ui-surface/ownComponents.d.ts +6 -0
- package/lib/loop/plugins/a2ui-surface/ownComponents.js +30 -0
- package/lib/loop/plugins/app-components/index.d.ts +26 -0
- package/lib/loop/plugins/app-components/index.js +56 -0
- package/lib/loop/plugins/app-elements/AppElements.js +4 -1
- package/lib/loop/plugins/app-page/AppPage.d.ts +9 -0
- package/lib/loop/plugins/app-page/AppPage.js +106 -8
- package/lib/loop/plugins/app-page/appPageModel.d.ts +17 -1
- package/lib/loop/plugins/app-page/appPageModel.js +120 -4
- package/lib/loop/plugins/app-page/index.d.ts +1 -0
- package/lib/loop/plugins/app-page/index.js +1 -0
- package/lib/loop/plugins/app-page/pageRun.d.ts +46 -0
- package/lib/loop/plugins/app-page/pageRun.js +111 -0
- package/lib/loop/plugins/assistant/index.js +8 -6
- package/lib/loop/plugins/chat/ChatView.js +6 -4
- package/lib/loop/plugins/chat/index.d.ts +1 -1
- package/lib/loop/plugins/chat/index.js +2 -1
- package/lib/models/PageTag.d.ts +1 -1
- package/lib/runtimes/browser/a2aPeer.d.ts +2 -1
- package/lib/specs/actions.js +78 -1
- package/lib/specs/apps.d.ts +5 -0
- package/lib/specs/apps.js +1384 -231
- package/lib/specs/appspecSchema.js +191 -1
- package/lib/specs/teams/teams.d.ts +3 -0
- package/lib/specs/teams/teams.js +249 -0
- package/lib/types/agentspecs.d.ts +70 -0
- package/lib/types/chat.d.ts +6 -5
- package/lib/types/teams.d.ts +13 -2
- package/package.json +2 -2
- package/scripts/codegen/generate_apps.py +4 -2
- package/scripts/codegen/generate_teams.py +15 -2
- package/lib/components/inspector/JsonTree.d.ts +0 -24
- package/lib/components/inspector/JsonTree.js +0 -89
- package/lib/components/inspector/a2aInspect.d.ts +0 -56
- package/lib/components/inspector/a2aInspect.js +0 -478
- package/lib/components/inspector/agentInspector.d.ts +0 -179
- package/lib/components/inspector/agentInspector.js +0 -267
- package/lib/components/inspector/chatInspect.d.ts +0 -38
- package/lib/components/inspector/chatInspect.js +0 -65
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { SegmentedControl } from '@primer/react';
|
|
4
|
+
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { SceneTranscript, useSceneTranscript } from './SceneTranscript';
|
|
6
|
+
const NO_MEMBERS = [];
|
|
7
|
+
/** The graph and the transcript of a scene, behind a *Graph · Transcript* toggle. */
|
|
8
|
+
export function SceneView({ children, members = NO_MEMBERS, tracer, lines, defaultView = 'graph', view, onViewChange, emptyText, maxHeight = 360, aside, }) {
|
|
9
|
+
const [own, setOwn] = useState(defaultView);
|
|
10
|
+
const shown = view ?? own;
|
|
11
|
+
const choose = (next) => {
|
|
12
|
+
setOwn(next);
|
|
13
|
+
onViewChange?.(next);
|
|
14
|
+
};
|
|
15
|
+
const live = useSceneTranscript(lines ? null : tracer, members);
|
|
16
|
+
const transcript = lines ?? live;
|
|
17
|
+
return (_jsxs(Box, { "data-scene-view": shown, children: [_jsxs(Box, { sx: {
|
|
18
|
+
display: 'flex',
|
|
19
|
+
alignItems: 'center',
|
|
20
|
+
justifyContent: 'space-between',
|
|
21
|
+
gap: 2,
|
|
22
|
+
flexWrap: 'wrap',
|
|
23
|
+
mb: 2,
|
|
24
|
+
}, children: [_jsxs(SegmentedControl, { "aria-label": "Scene view", size: "small", children: [_jsx(SegmentedControl.Button, { selected: shown === 'graph', onClick: () => choose('graph'), "data-scene-choice": "graph", children: "Graph" }), _jsx(SegmentedControl.Button, { selected: shown === 'transcript', onClick: () => choose('transcript'), "data-scene-choice": "transcript", children: transcript.length
|
|
25
|
+
? `Transcript (${transcript.length})`
|
|
26
|
+
: 'Transcript' })] }), aside] }), _jsx(Box, { hidden: shown !== 'graph', "data-scene-graph": "", children: children }), shown === 'transcript' && (_jsx(SceneTranscript, { lines: transcript, emptyText: emptyText, maxHeight: maxHeight }))] }));
|
|
27
|
+
}
|
|
28
|
+
export default SceneView;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transcript of a scene (LOOP A-06): what happens in it, as lines, in
|
|
3
|
+
* the order it happened — *Sales: …*, *Sales → Accounting: …*,
|
|
4
|
+
* *Accounting → Odoo: odoo_accounting_aged_balance* — each with its time.
|
|
5
|
+
*
|
|
6
|
+
* One source, read two ways. Live, the lines are read from the OpenTelemetry
|
|
7
|
+
* spans the Agent Inspector reads (core's live tracer): a turn is an
|
|
8
|
+
* `invoke_agent` span (what the person asked, what the agent answered), a
|
|
9
|
+
* tool call an `execute_tool` span (the member and, for an MCP call, the
|
|
10
|
+
* server it reaches: a connection of the member's), and a request to
|
|
11
|
+
* another member an `a2a` span (who asked whom, in what words, and what came
|
|
12
|
+
* back as its events). Once a run is over, the same lines are read from its
|
|
13
|
+
* record (`RecordEntry`: a `turn`, a `tool_call`), the session's entries as
|
|
14
|
+
* ai-agents kept them.
|
|
15
|
+
*
|
|
16
|
+
* Pure: spans or entries in, lines out, so that a test reads a scripted
|
|
17
|
+
* scene and a page draws a live one the same way ({@link SceneTranscript}).
|
|
18
|
+
*
|
|
19
|
+
* @module components/teams/sceneTranscript
|
|
20
|
+
*/
|
|
21
|
+
import type { OtelSpan } from '@datalayer/core/lib/otel/types';
|
|
22
|
+
import type { RecordEntry } from '../../loop/apps/records';
|
|
23
|
+
import { type A2ATeamConnection } from './a2aTeamFlow';
|
|
24
|
+
/** A member of the scene, as the transcript names it. */
|
|
25
|
+
export type SceneTranscriptMember = {
|
|
26
|
+
/** Its id in the team: `accounting`. */
|
|
27
|
+
id: string;
|
|
28
|
+
/** Its name, the service its spans are done by: `Accounting`. */
|
|
29
|
+
name: string;
|
|
30
|
+
/** The MCP servers it reaches: a call to one of their tools is `Member → Server: tool`. */
|
|
31
|
+
connections?: readonly A2ATeamConnection[];
|
|
32
|
+
};
|
|
33
|
+
/** What a line is: words said, a question asked, a tool called. */
|
|
34
|
+
export type SceneTranscriptKind = 'said' | 'asked' | 'called';
|
|
35
|
+
/** One line of the transcript. */
|
|
36
|
+
export type SceneTranscriptLine = {
|
|
37
|
+
/** Stable, for a list: the span's or the entry's id, and which of its lines. */
|
|
38
|
+
key: string;
|
|
39
|
+
/** When, in milliseconds since the epoch. */
|
|
40
|
+
at: number;
|
|
41
|
+
/** Who speaks or acts: a member's name, or the person ({@link PERSON}). */
|
|
42
|
+
from: string;
|
|
43
|
+
/** Who is asked or called: a member, or a server (`Odoo`); none when words are said to all. */
|
|
44
|
+
to?: string;
|
|
45
|
+
kind: SceneTranscriptKind;
|
|
46
|
+
/** The words, or the tool's name. */
|
|
47
|
+
text: string;
|
|
48
|
+
/** Still happening: the call runs, the answer has not come. */
|
|
49
|
+
open?: boolean;
|
|
50
|
+
/** It failed: the span's status, the task's state. */
|
|
51
|
+
failed?: boolean;
|
|
52
|
+
};
|
|
53
|
+
/** The person in the scene, as the transcript names them. */
|
|
54
|
+
export declare const PERSON = "You";
|
|
55
|
+
/**
|
|
56
|
+
* The transcript of a scene from the spans recorded while it works (the
|
|
57
|
+
* Agent Inspector's, one tracer for every member).
|
|
58
|
+
*/
|
|
59
|
+
export declare function transcriptOfSpans(spans: readonly OtelSpan[], members: readonly SceneTranscriptMember[]): SceneTranscriptLine[];
|
|
60
|
+
/**
|
|
61
|
+
* The transcript of one member's run once it is over, from its record: a
|
|
62
|
+
* `turn` is what the person asked and what it answered, a `tool_call` what
|
|
63
|
+
* it called in between. The calls of a turn are recorded before the turn
|
|
64
|
+
* itself (at its close), so the question is placed where the turn began.
|
|
65
|
+
*/
|
|
66
|
+
export declare function transcriptOfRecord(entries: readonly RecordEntry[], member: SceneTranscriptMember): SceneTranscriptLine[];
|
|
67
|
+
/** A line's time, as a clock reads it: `14:03:27`. */
|
|
68
|
+
export declare function timeText(at: number): string;
|
|
69
|
+
/** Who a line is from and to: `Sales`, `Sales → Accounting`, `Accounting → Odoo`. */
|
|
70
|
+
export declare function lineHeading(line: Pick<SceneTranscriptLine, 'from' | 'to' | 'kind'>): string;
|
|
71
|
+
/** A line in words: `Sales → Accounting: Which invoices are open?`. */
|
|
72
|
+
export declare function lineText(line: SceneTranscriptLine): string;
|
|
73
|
+
/** The transcript as text, one line each with its time: what *Copy as text* copies. */
|
|
74
|
+
export declare function transcriptText(lines: readonly SceneTranscriptLine[]): string;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { connectionOfTool } from './a2aTeamFlow';
|
|
6
|
+
/** The person in the scene, as the transcript names them. */
|
|
7
|
+
export const PERSON = 'You';
|
|
8
|
+
/** What a tool that is no connection's reaches, by where it comes from. */
|
|
9
|
+
const TOOL_HOMES = {
|
|
10
|
+
frontend: 'the page',
|
|
11
|
+
skill: 'a skill',
|
|
12
|
+
codemode: 'codemode',
|
|
13
|
+
runtime: 'its runtime',
|
|
14
|
+
};
|
|
15
|
+
const text = (value) => typeof value === 'string' ? value : '';
|
|
16
|
+
/** The text of the first message of a GenAI messages attribute (JSON). */
|
|
17
|
+
function messageText(value) {
|
|
18
|
+
if (typeof value !== 'string') {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(value);
|
|
23
|
+
return (parsed[0]?.parts ?? [])
|
|
24
|
+
.map(part => (typeof part.content === 'string' ? part.content : ''))
|
|
25
|
+
.join('')
|
|
26
|
+
.trim();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** A time as the span writes it, in milliseconds. */
|
|
33
|
+
function millis(iso, fallback = 0) {
|
|
34
|
+
const parsed = iso ? Date.parse(iso) : Number.NaN;
|
|
35
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
36
|
+
}
|
|
37
|
+
/** The member a service name is: by name, then by id; else the name itself. */
|
|
38
|
+
function memberNamed(service, members) {
|
|
39
|
+
return (members.find(member => member.name === service) ??
|
|
40
|
+
members.find(member => member.id === service) ?? {
|
|
41
|
+
id: service,
|
|
42
|
+
name: service,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** Where a tool call goes: the connection it is of (`Odoo`), else its home by kind. */
|
|
46
|
+
function calledOf(tool, kind, member) {
|
|
47
|
+
const connection = connectionOfTool(tool, [...(member.connections ?? [])]);
|
|
48
|
+
return connection?.label ?? TOOL_HOMES[kind] ?? TOOL_HOMES.runtime;
|
|
49
|
+
}
|
|
50
|
+
const isA2A = (span, attributes) => attributes['rpc.service'] === 'a2a' || span.span_name.startsWith('a2a ');
|
|
51
|
+
/** The lines in the order they happened: by time, a tie kept as given. */
|
|
52
|
+
function inOrder(lines) {
|
|
53
|
+
return lines
|
|
54
|
+
.map((line, index) => ({ line, index }))
|
|
55
|
+
.sort((a, b) => a.line.at - b.line.at || a.index - b.index)
|
|
56
|
+
.map(({ line }) => line);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The same thing said twice is said once: a member's answer comes back as
|
|
60
|
+
* the A2A request's events and, on a runtime whose own spans are read, as
|
|
61
|
+
* its `invoke_agent` span too. The first one stands.
|
|
62
|
+
*/
|
|
63
|
+
function onceEach(lines) {
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
return lines.filter(line => {
|
|
66
|
+
// Words said are the same words whoever they were said to.
|
|
67
|
+
const said = `${line.kind}\u0000${line.from}\u0000${line.kind === 'said' ? '' : (line.to ?? '')}\u0000${line.text}`;
|
|
68
|
+
if (seen.has(said)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
seen.add(said);
|
|
72
|
+
return true;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The transcript of a scene from the spans recorded while it works (the
|
|
77
|
+
* Agent Inspector's, one tracer for every member).
|
|
78
|
+
*/
|
|
79
|
+
export function transcriptOfSpans(spans, members) {
|
|
80
|
+
const lines = [];
|
|
81
|
+
// A tool call under which an A2A request sits is the asking itself: the
|
|
82
|
+
// request says it, with its words.
|
|
83
|
+
const asking = new Set();
|
|
84
|
+
for (const span of spans) {
|
|
85
|
+
if (span.parent_span_id && isA2A(span, span.attributes ?? {})) {
|
|
86
|
+
asking.add(span.parent_span_id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const span of spans) {
|
|
90
|
+
const attributes = span.attributes ?? {};
|
|
91
|
+
const start = millis(span.start_time);
|
|
92
|
+
const end = millis(span.end_time, start);
|
|
93
|
+
const operation = attributes['gen_ai.operation.name'];
|
|
94
|
+
const failed = span.status_code === 'ERROR';
|
|
95
|
+
if (operation === 'invoke_agent') {
|
|
96
|
+
const agent = memberNamed(text(attributes['gen_ai.agent.name']) || span.service_name, members);
|
|
97
|
+
const asked = messageText(attributes['gen_ai.input.messages']);
|
|
98
|
+
// The person's question, at the top of a turn; a turn asked by another
|
|
99
|
+
// member is told by its A2A request.
|
|
100
|
+
if (asked && !span.parent_span_id) {
|
|
101
|
+
lines.push({
|
|
102
|
+
key: `${span.span_id}:asked`,
|
|
103
|
+
at: start,
|
|
104
|
+
from: PERSON,
|
|
105
|
+
to: agent.name,
|
|
106
|
+
kind: 'asked',
|
|
107
|
+
text: asked,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const said = messageText(attributes['gen_ai.output.messages']);
|
|
111
|
+
if (said) {
|
|
112
|
+
lines.push({
|
|
113
|
+
key: `${span.span_id}:said`,
|
|
114
|
+
at: end,
|
|
115
|
+
from: agent.name,
|
|
116
|
+
kind: 'said',
|
|
117
|
+
text: said,
|
|
118
|
+
...(failed ? { failed } : {}),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
else if (failed) {
|
|
122
|
+
lines.push({
|
|
123
|
+
key: `${span.span_id}:said`,
|
|
124
|
+
at: end,
|
|
125
|
+
from: agent.name,
|
|
126
|
+
kind: 'said',
|
|
127
|
+
text: `could not answer${span.status_message ? `: ${span.status_message}` : ''}`,
|
|
128
|
+
failed,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (operation === 'execute_tool') {
|
|
134
|
+
if (asking.has(span.span_id)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const agent = memberNamed(text(attributes['gen_ai.agent.name']) || span.service_name, members);
|
|
138
|
+
const tool = text(attributes['gen_ai.tool.name']) || span.span_name;
|
|
139
|
+
lines.push({
|
|
140
|
+
key: `${span.span_id}:called`,
|
|
141
|
+
at: start,
|
|
142
|
+
from: agent.name,
|
|
143
|
+
to: calledOf(tool, text(attributes['datalayer.tool.kind']), agent),
|
|
144
|
+
kind: 'called',
|
|
145
|
+
text: tool,
|
|
146
|
+
...(span.in_progress ? { open: true } : {}),
|
|
147
|
+
...(failed ? { failed } : {}),
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (isA2A(span, attributes)) {
|
|
152
|
+
const request = text(attributes['a2a.message.text']).trim();
|
|
153
|
+
if (!request) {
|
|
154
|
+
// The agent card, a task read: nothing said.
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const asker = memberNamed(span.service_name, members);
|
|
158
|
+
const peer = memberNamed(text(attributes['peer.service']), members);
|
|
159
|
+
lines.push({
|
|
160
|
+
key: `${span.span_id}:asked`,
|
|
161
|
+
at: start,
|
|
162
|
+
from: asker.name,
|
|
163
|
+
to: peer.name,
|
|
164
|
+
kind: 'asked',
|
|
165
|
+
text: request,
|
|
166
|
+
...(span.in_progress ? { open: true } : {}),
|
|
167
|
+
});
|
|
168
|
+
// What came back: the answer, or the last words of its events.
|
|
169
|
+
let answer = text(attributes['a2a.answer.text']).trim();
|
|
170
|
+
let answeredAt = end;
|
|
171
|
+
if (!answer) {
|
|
172
|
+
for (const event of span.events ?? []) {
|
|
173
|
+
if (event.name === 'a2a.message' ||
|
|
174
|
+
event.name === 'a2a.artifact_update') {
|
|
175
|
+
const words = text(event.attributes?.['a2a.message.text']).trim();
|
|
176
|
+
if (words) {
|
|
177
|
+
answer = words;
|
|
178
|
+
answeredAt = millis(event.timestamp, end);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (answer) {
|
|
184
|
+
lines.push({
|
|
185
|
+
key: `${span.span_id}:said`,
|
|
186
|
+
at: answeredAt,
|
|
187
|
+
from: peer.name,
|
|
188
|
+
to: asker.name,
|
|
189
|
+
kind: 'said',
|
|
190
|
+
text: answer,
|
|
191
|
+
...(failed ? { failed } : {}),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
else if (failed) {
|
|
195
|
+
lines.push({
|
|
196
|
+
key: `${span.span_id}:said`,
|
|
197
|
+
at: end,
|
|
198
|
+
from: peer.name,
|
|
199
|
+
to: asker.name,
|
|
200
|
+
kind: 'said',
|
|
201
|
+
text: `could not answer${span.status_message ? `: ${span.status_message}` : ''}`,
|
|
202
|
+
failed,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return onceEach(inOrder(lines));
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The transcript of one member's run once it is over, from its record: a
|
|
211
|
+
* `turn` is what the person asked and what it answered, a `tool_call` what
|
|
212
|
+
* it called in between. The calls of a turn are recorded before the turn
|
|
213
|
+
* itself (at its close), so the question is placed where the turn began.
|
|
214
|
+
*/
|
|
215
|
+
export function transcriptOfRecord(entries, member) {
|
|
216
|
+
const ordered = [...entries].sort((a, b) => millis(a.createdAt) - millis(b.createdAt) || a.version - b.version);
|
|
217
|
+
const lines = [];
|
|
218
|
+
let pending = [];
|
|
219
|
+
let answered = false;
|
|
220
|
+
for (const entry of ordered) {
|
|
221
|
+
const at = millis(entry.createdAt);
|
|
222
|
+
if (entry.kind === 'tool_call') {
|
|
223
|
+
const tool = text(entry.payload.tool) || entry.summary;
|
|
224
|
+
pending.push({
|
|
225
|
+
key: `${entry.uid}:called`,
|
|
226
|
+
at,
|
|
227
|
+
from: member.name,
|
|
228
|
+
to: calledOf(tool, '', member),
|
|
229
|
+
kind: 'called',
|
|
230
|
+
text: tool,
|
|
231
|
+
...(entry.payload.error ? { failed: true } : {}),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else if (entry.kind === 'turn') {
|
|
235
|
+
const asked = text(entry.payload.asked) || entry.summary;
|
|
236
|
+
const said = text(entry.payload.answered);
|
|
237
|
+
lines.push({
|
|
238
|
+
key: `${entry.uid}:asked`,
|
|
239
|
+
at: pending[0]?.at ?? at,
|
|
240
|
+
from: PERSON,
|
|
241
|
+
to: member.name,
|
|
242
|
+
kind: 'asked',
|
|
243
|
+
text: asked,
|
|
244
|
+
});
|
|
245
|
+
lines.push(...pending);
|
|
246
|
+
pending = [];
|
|
247
|
+
if (said) {
|
|
248
|
+
lines.push({
|
|
249
|
+
key: `${entry.uid}:said`,
|
|
250
|
+
at,
|
|
251
|
+
from: member.name,
|
|
252
|
+
kind: 'said',
|
|
253
|
+
text: said,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
answered = true;
|
|
257
|
+
}
|
|
258
|
+
else if (entry.kind === 'output' && !answered) {
|
|
259
|
+
// A record kept without its conversations: the answer's summary.
|
|
260
|
+
pending.push({
|
|
261
|
+
key: `${entry.uid}:said`,
|
|
262
|
+
at,
|
|
263
|
+
from: member.name,
|
|
264
|
+
kind: 'said',
|
|
265
|
+
text: entry.summary,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
lines.push(...pending);
|
|
270
|
+
return lines;
|
|
271
|
+
}
|
|
272
|
+
/** A line's time, as a clock reads it: `14:03:27`. */
|
|
273
|
+
export function timeText(at) {
|
|
274
|
+
const date = new Date(at);
|
|
275
|
+
const two = (value) => String(value).padStart(2, '0');
|
|
276
|
+
return `${two(date.getHours())}:${two(date.getMinutes())}:${two(date.getSeconds())}`;
|
|
277
|
+
}
|
|
278
|
+
/** Who a line is from and to: `Sales`, `Sales → Accounting`, `Accounting → Odoo`. */
|
|
279
|
+
export function lineHeading(line) {
|
|
280
|
+
return line.to && line.kind !== 'said'
|
|
281
|
+
? `${line.from} → ${line.to}`
|
|
282
|
+
: line.from;
|
|
283
|
+
}
|
|
284
|
+
/** A line in words: `Sales → Accounting: Which invoices are open?`. */
|
|
285
|
+
export function lineText(line) {
|
|
286
|
+
const tail = line.open ? '…' : line.failed ? ' (failed)' : '';
|
|
287
|
+
return `${lineHeading(line)}: ${line.text}${tail}`;
|
|
288
|
+
}
|
|
289
|
+
/** The transcript as text, one line each with its time: what *Copy as text* copies. */
|
|
290
|
+
export function transcriptText(lines) {
|
|
291
|
+
return lines
|
|
292
|
+
.map(line => `${timeText(line.at)} ${lineText(line)}`)
|
|
293
|
+
.join('\n');
|
|
294
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AppSpec } from '../../types/agentspecs';
|
|
2
2
|
import { type A2ATeamConnection } from './a2aTeamFlow';
|
|
3
|
-
import type { ToolClass } from '../inspector/
|
|
3
|
+
import type { ToolClass } from '../inspector/agentSpans';
|
|
4
4
|
/**
|
|
5
5
|
* The connections of an application, each with its server's name and mark
|
|
6
6
|
* and its tools. A connection to a server the catalogue does not have is
|
|
@@ -8,7 +8,7 @@ import type { ToolClass } from '../inspector/agentInspector';
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function teamConnectionsOf(app: Pick<AppSpec, 'connections'>): A2ATeamConnection[];
|
|
10
10
|
/**
|
|
11
|
-
* Where a member's tool call comes from, for
|
|
11
|
+
* Where a member's tool call comes from, for its span: a call to
|
|
12
12
|
* one of its connections is an MCP call, with the server's mark; any other
|
|
13
13
|
* is `fallback`'s (`classifyToolCall`, by the catalogues) or a tool its
|
|
14
14
|
* runtime runs (`write_notebook`).
|
|
@@ -49,7 +49,7 @@ export function teamConnectionsOf(app) {
|
|
|
49
49
|
return connections;
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
* Where a member's tool call comes from, for
|
|
52
|
+
* Where a member's tool call comes from, for its span: a call to
|
|
53
53
|
* one of its connections is an MCP call, with the server's mark; any other
|
|
54
54
|
* is `fallback`'s (`classifyToolCall`, by the catalogues) or a tool its
|
|
55
55
|
* runtime runs (`write_notebook`).
|
|
@@ -59,11 +59,11 @@ export function teamToolClassifier(connections, fallback) {
|
|
|
59
59
|
const connection = connectionOfTool(name, connections);
|
|
60
60
|
if (connection) {
|
|
61
61
|
return {
|
|
62
|
-
|
|
62
|
+
kind: 'mcp',
|
|
63
63
|
...(connection.icon ? { icon: connection.icon } : {}),
|
|
64
64
|
...(connection.emoji ? { emoji: connection.emoji } : {}),
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
-
return fallback?.(name, args) ?? {
|
|
67
|
+
return fallback?.(name, args) ?? { kind: 'runtime' };
|
|
68
68
|
};
|
|
69
69
|
}
|
|
@@ -3,7 +3,7 @@ import type { DisplayItem } from '../../types/chat';
|
|
|
3
3
|
import { type BrowserModelOptions } from '../../runtimes/browser/model';
|
|
4
4
|
import { type A2APeer, type A2APeerArtifact } from '../../runtimes/browser/a2aPeer';
|
|
5
5
|
import type { AppSpec } from '../../types/agentspecs';
|
|
6
|
-
import type {
|
|
6
|
+
import type { OtelLiveTracer } from '@datalayer/core/lib/otel/live';
|
|
7
7
|
import { type A2ATeamCall, type A2ATeamConnection, type A2ATeamFlow } from './a2aTeamFlow';
|
|
8
8
|
import { type BalloonToolLine } from '../../chat/assistant/toolLine';
|
|
9
9
|
/** What one member's character does: its state, its balloon, and whether it was sent away. */
|
|
@@ -59,10 +59,10 @@ export type UseA2ATeamOptions = {
|
|
|
59
59
|
*/
|
|
60
60
|
accept?: readonly string[];
|
|
61
61
|
/**
|
|
62
|
-
* The Agent Inspector's
|
|
63
|
-
* are recorded there
|
|
62
|
+
* The Agent Inspector's tracer: the entry's turns and its call to the
|
|
63
|
+
* peer are recorded there as spans.
|
|
64
64
|
*/
|
|
65
|
-
inspector?:
|
|
65
|
+
inspector?: OtelLiveTracer | null;
|
|
66
66
|
};
|
|
67
67
|
export type A2ATeam = {
|
|
68
68
|
entryPersona: A2ATeamPersona;
|
|
@@ -20,10 +20,11 @@
|
|
|
20
20
|
* peer calls on its connections (its MCP servers), told over A2A as it calls
|
|
21
21
|
* them, are kept as the calls running now ({@link callsAfter}).
|
|
22
22
|
*
|
|
23
|
-
* Given
|
|
24
|
-
* model, its tokens)
|
|
25
|
-
*
|
|
26
|
-
* peer's fetch
|
|
23
|
+
* Given the Agent Inspector's tracer (`inspector`), the entry's turns (an
|
|
24
|
+
* `invoke_agent` span: its model, its tokens) and its call to the peer (an
|
|
25
|
+
* `execute_tool` span under it) are recorded there as OpenTelemetry spans;
|
|
26
|
+
* the A2A traffic itself, under that call, is recorded by the peer's fetch
|
|
27
|
+
* (`traceA2AFetch`).
|
|
27
28
|
*
|
|
28
29
|
* What `A2ATeamGraph` draws, and what a page's composer sends.
|
|
29
30
|
*
|
|
@@ -33,6 +34,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
33
34
|
import { stepCountIs, ToolLoopAgent } from 'ai';
|
|
34
35
|
import { createBrowserModel, } from '../../runtimes/browser/model';
|
|
35
36
|
import { a2aPeerTool, NOTEBOOK_MEDIA_TYPE, } from '../../runtimes/browser/a2aPeer';
|
|
37
|
+
import { endAgentTurn, endToolCall, startAgentTurn, startToolCall, } from '../inspector/agentSpans';
|
|
36
38
|
import { callsAfter, flowAfter, toolOwnName, pruneCalls, } from './a2aTeamFlow';
|
|
37
39
|
import { toolLineOfStep, } from '../../chat/assistant/toolLine';
|
|
38
40
|
const NO_CONNECTIONS = [];
|
|
@@ -119,13 +121,10 @@ export function useA2ATeam(options) {
|
|
|
119
121
|
const callTimer = useRef(undefined);
|
|
120
122
|
const [busy, setBusy] = useState(false);
|
|
121
123
|
const history = useRef([]);
|
|
122
|
-
const turnCount = useRef(0);
|
|
123
124
|
const abort = useRef(null);
|
|
124
125
|
const flowTimer = useRef(undefined);
|
|
125
126
|
// What the peer does, as it does it: its own state and balloon, and the link.
|
|
126
127
|
const onPeerEvent = useCallback((event) => {
|
|
127
|
-
// Linked to the A2A entry it came from (the capture's linker).
|
|
128
|
-
inspector?.note(event);
|
|
129
128
|
const next = flowAfter(event);
|
|
130
129
|
clearTimeout(flowTimer.current);
|
|
131
130
|
setFlow(next.flow);
|
|
@@ -204,7 +203,7 @@ export function useA2ATeam(options) {
|
|
|
204
203
|
tool: undefined,
|
|
205
204
|
}));
|
|
206
205
|
}
|
|
207
|
-
}, [peerApp.id, peerConnections,
|
|
206
|
+
}, [peerApp.id, peerConnections, tell, toolCalled]);
|
|
208
207
|
const agent = useMemo(() => {
|
|
209
208
|
if (!peer) {
|
|
210
209
|
return null;
|
|
@@ -241,19 +240,16 @@ export function useA2ATeam(options) {
|
|
|
241
240
|
history.current = [...history.current, { role: 'user', content: asked }];
|
|
242
241
|
abort.current = new AbortController();
|
|
243
242
|
let said = '';
|
|
244
|
-
// The turn,
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
summary: balloonLine(asked, 160),
|
|
255
|
-
payload: { prompt: asked, model: entry.model || undefined },
|
|
256
|
-
});
|
|
243
|
+
// The turn, as a span: the entry's model, from the question to the
|
|
244
|
+
// answer, and its tokens; its calls to the peer under it.
|
|
245
|
+
const turnSpan = inspector
|
|
246
|
+
? startAgentTurn(inspector, {
|
|
247
|
+
agent: entry.name,
|
|
248
|
+
prompt: asked,
|
|
249
|
+
model: entry.model || undefined,
|
|
250
|
+
})
|
|
251
|
+
: undefined;
|
|
252
|
+
const toolSpans = new Map();
|
|
257
253
|
try {
|
|
258
254
|
const result = await agent.stream({
|
|
259
255
|
messages: history.current,
|
|
@@ -263,15 +259,16 @@ export function useA2ATeam(options) {
|
|
|
263
259
|
if (part.type === 'tool-call' && part.toolName === askTool) {
|
|
264
260
|
const request = String(part.input?.request ?? '');
|
|
265
261
|
// Its tool runs here, in the page, and asks the peer over A2A.
|
|
266
|
-
inspector
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
262
|
+
if (inspector && turnSpan) {
|
|
263
|
+
toolSpans.set(part.toolCallId, startToolCall(inspector, {
|
|
264
|
+
agent: entry.name,
|
|
265
|
+
name: askTool,
|
|
266
|
+
id: part.toolCallId,
|
|
267
|
+
args: part.input,
|
|
268
|
+
tool: { kind: 'frontend' },
|
|
269
|
+
parent: turnSpan.context,
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
275
272
|
setEntryPersona(prev => ({
|
|
276
273
|
...prev,
|
|
277
274
|
state: 'waiting',
|
|
@@ -290,10 +287,14 @@ export function useA2ATeam(options) {
|
|
|
290
287
|
}
|
|
291
288
|
else if (part.type === 'tool-result' && part.toolName === askTool) {
|
|
292
289
|
const output = part.output;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
290
|
+
const toolSpan = toolSpans.get(part.toolCallId);
|
|
291
|
+
if (toolSpan) {
|
|
292
|
+
toolSpans.delete(part.toolCallId);
|
|
293
|
+
endToolCall(toolSpan, {
|
|
294
|
+
result: part.output,
|
|
295
|
+
...(output?.error ? { error: String(output.error) } : {}),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
297
298
|
setEntryPersona(prev => ({
|
|
298
299
|
...prev,
|
|
299
300
|
state: 'thinking',
|
|
@@ -317,12 +318,15 @@ export function useA2ATeam(options) {
|
|
|
317
318
|
}));
|
|
318
319
|
}
|
|
319
320
|
else if (part.type === 'tool-error' && part.toolName === askTool) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
:
|
|
325
|
-
|
|
321
|
+
const toolSpan = toolSpans.get(part.toolCallId);
|
|
322
|
+
if (toolSpan) {
|
|
323
|
+
toolSpans.delete(part.toolCallId);
|
|
324
|
+
endToolCall(toolSpan, {
|
|
325
|
+
error: part.error instanceof Error
|
|
326
|
+
? part.error.message
|
|
327
|
+
: String(part.error),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
326
330
|
setEntryPersona(prev => ({
|
|
327
331
|
...prev,
|
|
328
332
|
state: 'thinking',
|
|
@@ -339,12 +343,9 @@ export function useA2ATeam(options) {
|
|
|
339
343
|
...history.current,
|
|
340
344
|
{ role: 'assistant', content: said },
|
|
341
345
|
];
|
|
342
|
-
if (
|
|
346
|
+
if (turnSpan) {
|
|
343
347
|
const usage = await Promise.resolve(result.totalUsage).catch(() => undefined);
|
|
344
|
-
|
|
345
|
-
result: { text: said, ...(usage ? { usage } : {}) },
|
|
346
|
-
summary: `${balloonLine(asked, 60)} → ${balloonLine(said, 80)}${usage?.totalTokens ? ` · ${usage.totalTokens} tokens` : ''}`,
|
|
347
|
-
});
|
|
348
|
+
endAgentTurn(turnSpan, { text: said, usage });
|
|
348
349
|
}
|
|
349
350
|
setTurns(prev => [...prev, { role: 'assistant', text: said }]);
|
|
350
351
|
setEntryPersona(prev => ({
|
|
@@ -356,7 +357,10 @@ export function useA2ATeam(options) {
|
|
|
356
357
|
}
|
|
357
358
|
catch (error) {
|
|
358
359
|
const message = error instanceof Error ? error.message : String(error);
|
|
359
|
-
|
|
360
|
+
toolSpans.forEach(toolSpan => endToolCall(toolSpan, { error: message }));
|
|
361
|
+
if (turnSpan) {
|
|
362
|
+
endAgentTurn(turnSpan, { error: message });
|
|
363
|
+
}
|
|
360
364
|
setTurns(prev => [
|
|
361
365
|
...prev,
|
|
362
366
|
{ role: 'assistant', text: `Something went wrong: ${message}` },
|