@datalayer/agent-runtimes 1.5.89 → 1.5.91

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 (87) hide show
  1. package/README.md +29 -297
  2. package/dist-embed/datalayer-app-main.js +40 -36
  3. package/lib/apps/embed/element.js +20 -5
  4. package/lib/apps/embed/visitorApp.d.ts +23 -45
  5. package/lib/apps/embed/visitorApp.js +65 -13
  6. package/lib/components/teams/A2ATeamGraph.d.ts +58 -152
  7. package/lib/components/teams/A2ATeamGraph.js +169 -410
  8. package/lib/components/teams/SceneTranscript.d.ts +5 -1
  9. package/lib/components/teams/SceneTranscript.js +2 -2
  10. package/lib/components/teams/SceneView.d.ts +17 -5
  11. package/lib/components/teams/SceneView.js +7 -3
  12. package/lib/components/teams/a2aTeamFlow.d.ts +16 -7
  13. package/lib/components/teams/a2aTeamFlow.js +17 -14
  14. package/lib/components/teams/backdrops/catalogue.d.ts +35 -0
  15. package/lib/components/teams/backdrops/catalogue.js +34 -0
  16. package/lib/components/teams/backdrops/edges.d.ts +20 -0
  17. package/lib/components/teams/backdrops/edges.js +29 -0
  18. package/lib/components/teams/backdrops/faces.d.ts +24 -0
  19. package/lib/components/teams/backdrops/faces.js +39 -0
  20. package/lib/components/teams/backdrops/halo.d.ts +14 -0
  21. package/lib/components/teams/backdrops/halo.js +5 -0
  22. package/lib/components/teams/backdrops/hero.d.ts +14 -0
  23. package/lib/components/teams/backdrops/hero.js +178 -0
  24. package/lib/components/teams/backdrops/index.d.ts +22 -0
  25. package/lib/components/teams/backdrops/index.js +38 -0
  26. package/lib/components/teams/backdrops/nodes.d.ts +123 -0
  27. package/lib/components/teams/backdrops/nodes.js +265 -0
  28. package/lib/components/teams/backdrops/plain.d.ts +14 -0
  29. package/lib/components/teams/backdrops/plain.js +116 -0
  30. package/lib/components/teams/backdrops/studio.d.ts +17 -0
  31. package/lib/components/teams/backdrops/studio.js +122 -0
  32. package/lib/components/teams/backdrops/theme.d.ts +14 -0
  33. package/lib/components/teams/backdrops/theme.js +19 -0
  34. package/lib/components/teams/backdrops/types.d.ts +214 -0
  35. package/lib/components/teams/backdrops/types.js +5 -0
  36. package/lib/components/teams/index.d.ts +2 -0
  37. package/lib/components/teams/index.js +2 -0
  38. package/lib/components/teams/sceneTranscriptRules.d.ts +32 -1
  39. package/lib/components/teams/sceneTranscriptRules.js +104 -2
  40. package/lib/components/teams/transcript/RecordingLabel.d.ts +17 -0
  41. package/lib/components/teams/transcript/RecordingLabel.js +7 -0
  42. package/lib/components/teams/transcript/SequenceDiagram.d.ts +39 -0
  43. package/lib/components/teams/transcript/SequenceDiagram.js +190 -0
  44. package/lib/components/teams/transcript/TranscriptDetail.d.ts +27 -0
  45. package/lib/components/teams/transcript/TranscriptDetail.js +48 -0
  46. package/lib/components/teams/transcript/TranscriptTable.d.ts +23 -0
  47. package/lib/components/teams/transcript/TranscriptTable.js +112 -0
  48. package/lib/components/teams/transcript/TranscriptViews.d.ts +66 -0
  49. package/lib/components/teams/transcript/TranscriptViews.js +135 -0
  50. package/lib/components/teams/transcript/TranscriptViewsTypes.d.ts +13 -0
  51. package/lib/components/teams/transcript/TranscriptViewsTypes.js +5 -0
  52. package/lib/components/teams/transcript/index.d.ts +23 -0
  53. package/lib/components/teams/transcript/index.js +27 -0
  54. package/lib/components/teams/transcript/recordingPlayback.d.ts +52 -0
  55. package/lib/components/teams/transcript/recordingPlayback.js +146 -0
  56. package/lib/components/teams/transcript/selection.d.ts +59 -0
  57. package/lib/components/teams/transcript/selection.js +79 -0
  58. package/lib/components/teams/transcript/sequenceDiagramRules.d.ts +35 -0
  59. package/lib/components/teams/transcript/sequenceDiagramRules.js +158 -0
  60. package/lib/components/teams/transcript/transcriptDetailRules.d.ts +80 -0
  61. package/lib/components/teams/transcript/transcriptDetailRules.js +208 -0
  62. package/lib/components/teams/transcript/transcriptExport.d.ts +31 -0
  63. package/lib/components/teams/transcript/transcriptExport.js +82 -0
  64. package/lib/components/teams/transcript/transcriptRows.d.ts +76 -0
  65. package/lib/components/teams/transcript/transcriptRows.js +187 -0
  66. package/lib/components/teams/transcript/useSceneRecording.d.ts +45 -0
  67. package/lib/components/teams/transcript/useSceneRecording.js +96 -0
  68. package/lib/examples/ScenesExample.js +16 -3
  69. package/lib/portable/hosts/agentClient.d.ts +8 -0
  70. package/lib/portable/hosts/agentClient.js +2 -0
  71. package/lib/portable/hosts/approvalRenderers.d.ts +10 -2
  72. package/lib/portable/hosts/approvalRenderers.js +81 -2
  73. package/lib/portable/hosts/contracts.d.ts +1 -0
  74. package/lib/portable/hosts/contracts.js +2 -0
  75. package/lib/portable/hosts/contracts.json +56 -2
  76. package/lib/specs/actions.js +2 -0
  77. package/lib/specs/apps.d.ts +1 -0
  78. package/lib/specs/apps.js +219 -0
  79. package/lib/specs/sceneRecordings.d.ts +14 -0
  80. package/lib/specs/sceneRecordings.js +12 -0
  81. package/lib/specs/sceneSchema.js +11 -0
  82. package/lib/specs/scenes.d.ts +1 -0
  83. package/lib/specs/scenes.js +159 -0
  84. package/lib/types/scenes.d.ts +54 -0
  85. package/package.json +2 -1
  86. package/scripts/codegen/generate_mcp_servers.py +5 -16
  87. package/scripts/codegen/generate_scenes.py +80 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * What a line's span says (SCENES S3-01): the words sent and the words
3
+ * answered, a tool's name, arguments and result when the span recorded
4
+ * them, when it started and ended, its status and an error's message, the
5
+ * span and trace ids; for a coding harness's step, the Shield's decision on
6
+ * it and the rule it decided by. Pure: a line and its span in, the detail out
7
+ * ({@link TranscriptDetail} draws it).
8
+ *
9
+ * @module components/teams/transcript/transcriptDetailRules
10
+ */
11
+ import type { OtelSpan } from '@datalayer/core/lib/otel/types';
12
+ import { type SceneTranscriptLine } from '../sceneTranscriptRules';
13
+ /** The ids that name a span: what *Open in the Inspector* opens. */
14
+ export type TranscriptSpanIds = {
15
+ traceId: string;
16
+ spanId: string;
17
+ };
18
+ /** What the Shield decided of a coding harness's step, from its events. */
19
+ export type TranscriptShieldStep = {
20
+ /** `allow`, `ask` or `deny`: the last decision on the step. */
21
+ decision: string;
22
+ /** The decision in a person's words: *do it*, *ask me first*, *leave it to me*. */
23
+ said: string;
24
+ /** The rule it decided by: an application's Rule names it (*Change a file*). */
25
+ rule?: string;
26
+ /** The policy the rule is of: `team` (the application's Rules), `session` (the platform's default)… */
27
+ scope?: string;
28
+ /** Who decided: the policy, a person asked, the Shield. */
29
+ decidedBy?: string;
30
+ /** The harness's kind of step (`read`, `edit`, `execute`) and its class (`read`, `write`…). */
31
+ kind?: string;
32
+ actionClass?: string;
33
+ path?: string;
34
+ command?: string;
35
+ /** The harness, from the turn: `opencode`. */
36
+ harness?: string;
37
+ };
38
+ /** A decision, as a person reads it. */
39
+ export declare const SHIELD_DECISION_WORDS: Record<string, string>;
40
+ /** The detail of a line, from its span. */
41
+ export type TranscriptDetailOf = {
42
+ line: SceneTranscriptLine;
43
+ /** None for a line read from a recording: there is no span to open. */
44
+ span?: OtelSpan;
45
+ /** The words sent: what was asked, or the tool called. */
46
+ sent?: string;
47
+ /** The words answered. */
48
+ answered?: string;
49
+ tool?: {
50
+ name: string;
51
+ arguments?: string;
52
+ result?: string;
53
+ };
54
+ /** A coding harness's step: what the Shield decided, and by which rule. */
55
+ shield?: TranscriptShieldStep;
56
+ startedAt?: number;
57
+ endedAt?: number;
58
+ durationMs?: number;
59
+ /** `running`, `ok`, `failed`, or the span's own code. */
60
+ status: string;
61
+ error?: string;
62
+ ids?: TranscriptSpanIds & {
63
+ parentSpanId?: string;
64
+ };
65
+ };
66
+ /** JSON, pretty when it parses; the text as it is otherwise. */
67
+ export declare function prettyJson(value: unknown): string | undefined;
68
+ /**
69
+ * The span a line was read from: the one whose id its key begins with — a
70
+ * harness's step names its turn's span and its call, `<span>/<call>`.
71
+ */
72
+ export declare function spanOfLine(line: Pick<SceneTranscriptLine, 'key'>, spans: readonly OtelSpan[] | undefined): OtelSpan | undefined;
73
+ /** What the Shield decided of the step a line is, from its turn's span; none for any other line. */
74
+ export declare function shieldStepOf(line: Pick<SceneTranscriptLine, 'key'>, span: OtelSpan): TranscriptShieldStep | undefined;
75
+ /** The sentence for a step the Shield refused. */
76
+ export declare function shieldRefusalText(step: TranscriptShieldStep): string;
77
+ /** The detail of a line: from its span when there is one. */
78
+ export declare function transcriptDetailOf(line: SceneTranscriptLine, span?: OtelSpan): TranscriptDetailOf;
79
+ /** The sentence for a line with no span: a recording. */
80
+ export declare const NO_SPAN_TEXT = "This line comes from a recording: there is no span to open.";
@@ -0,0 +1,208 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { isShieldTurn, SHIELD_STEP_EVENTS, stepCallOf, } from '../sceneTranscriptRules.js';
6
+ import { exchangeOf } from './transcriptRows.js';
7
+ /** A decision, as a person reads it. */
8
+ export const SHIELD_DECISION_WORDS = {
9
+ allow: 'do it',
10
+ ask: 'ask me first',
11
+ deny: 'leave it to me',
12
+ };
13
+ const text = (value) => typeof value === 'string' && value.trim() ? value : undefined;
14
+ /** JSON, pretty when it parses; the text as it is otherwise. */
15
+ export function prettyJson(value) {
16
+ if (value === undefined || value === null) {
17
+ return undefined;
18
+ }
19
+ if (typeof value !== 'string') {
20
+ return JSON.stringify(value, null, 2);
21
+ }
22
+ try {
23
+ return JSON.stringify(JSON.parse(value), null, 2);
24
+ }
25
+ catch {
26
+ return value;
27
+ }
28
+ }
29
+ /** The text of a GenAI messages attribute (JSON), every message's parts. */
30
+ function messagesText(value) {
31
+ const raw = text(value);
32
+ if (!raw) {
33
+ return undefined;
34
+ }
35
+ try {
36
+ const parsed = JSON.parse(raw);
37
+ const words = parsed
38
+ .flatMap(message => message.parts ?? [])
39
+ .map(part => (typeof part.content === 'string' ? part.content : ''))
40
+ .filter(Boolean)
41
+ .join('\n\n')
42
+ .trim();
43
+ return words || undefined;
44
+ }
45
+ catch {
46
+ return raw;
47
+ }
48
+ }
49
+ const millis = (iso) => {
50
+ const parsed = iso ? Date.parse(iso) : Number.NaN;
51
+ return Number.isFinite(parsed) ? parsed : undefined;
52
+ };
53
+ /**
54
+ * The span a line was read from: the one whose id its key begins with — a
55
+ * harness's step names its turn's span and its call, `<span>/<call>`.
56
+ */
57
+ export function spanOfLine(line, spans) {
58
+ const id = exchangeOf(line.key);
59
+ return spans?.find(span => span.span_id === id || id.startsWith(`${span.span_id}/`));
60
+ }
61
+ /** What the Shield decided of the step a line is, from its turn's span; none for any other line. */
62
+ export function shieldStepOf(line, span) {
63
+ if (!isShieldTurn(span)) {
64
+ return undefined;
65
+ }
66
+ const exchange = exchangeOf(line.key);
67
+ const call = exchange.slice(span.span_id.length + 1);
68
+ let found;
69
+ for (const event of span.events ?? []) {
70
+ if (!SHIELD_STEP_EVENTS.includes(event.name) ||
71
+ stepCallOf(event) !== call) {
72
+ continue;
73
+ }
74
+ const attributes = event.attributes ?? {};
75
+ const decision = text(attributes.decision);
76
+ const step = found ?? {
77
+ decision: '',
78
+ said: '',
79
+ };
80
+ // A refusal stands: what the Shield did after it (a completion row) does not undo it.
81
+ if (decision && step.decision !== 'deny') {
82
+ step.decision = decision;
83
+ step.said = SHIELD_DECISION_WORDS[decision] ?? decision;
84
+ step.rule = text(attributes.rule) ?? step.rule;
85
+ step.scope = text(attributes.scope) ?? step.scope;
86
+ step.decidedBy =
87
+ text(attributes.decided_by) ?? text(attributes.by) ?? step.decidedBy;
88
+ }
89
+ step.kind = step.kind ?? text(attributes.kind);
90
+ step.actionClass = step.actionClass ?? text(attributes.class);
91
+ step.path = step.path ?? text(attributes.path);
92
+ step.command = step.command ?? text(attributes.command);
93
+ found = step;
94
+ }
95
+ if (!found) {
96
+ return undefined;
97
+ }
98
+ const harness = text(span.attributes?.['shield.harness']);
99
+ return Object.fromEntries(Object.entries({ ...found, harness }).filter(([, value]) => value !== undefined && value !== ''));
100
+ }
101
+ /** The sentence for a step the Shield refused. */
102
+ export function shieldRefusalText(step) {
103
+ return step.rule
104
+ ? `Refused by the Shield: ${step.said}, by the rule '${step.rule}'${step.scope ? ` (${step.scope})` : ''}.`
105
+ : `Refused by the Shield: ${step.said}.`;
106
+ }
107
+ /** The detail of a line: from its span when there is one. */
108
+ export function transcriptDetailOf(line, span) {
109
+ if (!span) {
110
+ return {
111
+ line,
112
+ status: line.open ? 'running' : line.failed ? 'failed' : 'done',
113
+ };
114
+ }
115
+ const shield = shieldStepOf(line, span);
116
+ if (shield) {
117
+ // A harness's step: its turn's span holds every step, so the times are the line's.
118
+ const refused = shield.decision === 'deny';
119
+ return {
120
+ line,
121
+ span,
122
+ sent: line.text,
123
+ shield,
124
+ startedAt: line.at,
125
+ endedAt: line.ended,
126
+ durationMs: line.ended === undefined
127
+ ? undefined
128
+ : Math.max(0, line.ended - line.at),
129
+ status: line.open ? 'running' : refused || line.failed ? 'failed' : 'ok',
130
+ error: refused ? shieldRefusalText(shield) : undefined,
131
+ ids: {
132
+ traceId: span.trace_id,
133
+ spanId: span.span_id,
134
+ ...(span.parent_span_id ? { parentSpanId: span.parent_span_id } : {}),
135
+ },
136
+ };
137
+ }
138
+ const attributes = span.attributes ?? {};
139
+ const operation = attributes['gen_ai.operation.name'];
140
+ const startedAt = millis(span.start_time);
141
+ const endedAt = span.in_progress ? undefined : millis(span.end_time);
142
+ const durationMs = span.in_progress || startedAt === undefined || endedAt === undefined
143
+ ? undefined
144
+ : Math.max(0, endedAt - startedAt);
145
+ const status = span.in_progress
146
+ ? 'running'
147
+ : span.status_code === 'ERROR'
148
+ ? 'failed'
149
+ : span.status_code === 'OK' || !span.status_code
150
+ ? 'ok'
151
+ : span.status_code.toLowerCase();
152
+ const error = span.status_code === 'ERROR'
153
+ ? (text(span.status_message) ??
154
+ text(attributes['error.type']) ??
155
+ 'failed')
156
+ : undefined;
157
+ const ids = {
158
+ traceId: span.trace_id,
159
+ spanId: span.span_id,
160
+ ...(span.parent_span_id ? { parentSpanId: span.parent_span_id } : {}),
161
+ };
162
+ const base = {
163
+ line,
164
+ span,
165
+ startedAt,
166
+ endedAt,
167
+ durationMs,
168
+ status,
169
+ error,
170
+ ids,
171
+ };
172
+ if (operation === 'execute_tool') {
173
+ const name = text(attributes['gen_ai.tool.name']) ?? line.text;
174
+ return {
175
+ ...base,
176
+ sent: name,
177
+ tool: {
178
+ name,
179
+ arguments: prettyJson(attributes['gen_ai.tool.call.arguments']),
180
+ result: prettyJson(attributes['gen_ai.tool.call.result']),
181
+ },
182
+ };
183
+ }
184
+ if (operation === 'invoke_agent') {
185
+ return {
186
+ ...base,
187
+ sent: messagesText(attributes['gen_ai.input.messages']),
188
+ answered: messagesText(attributes['gen_ai.output.messages']),
189
+ };
190
+ }
191
+ // An A2A request: its words, and what came back.
192
+ let answered = text(attributes['a2a.answer.text']);
193
+ if (!answered) {
194
+ for (const event of span.events ?? []) {
195
+ if (event.name === 'a2a.message' ||
196
+ event.name === 'a2a.artifact_update') {
197
+ answered = text(event.attributes?.['a2a.message.text']) ?? answered;
198
+ }
199
+ }
200
+ }
201
+ return {
202
+ ...base,
203
+ sent: text(attributes['a2a.message.text']),
204
+ answered,
205
+ };
206
+ }
207
+ /** The sentence for a line with no span: a recording. */
208
+ export const NO_SPAN_TEXT = 'This line comes from a recording: there is no span to open.';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The transcript as a JSON file (SCENES S3-03): the lines as they are,
3
+ * under a version, so that `agent_runtimes` reads them back in Python
4
+ * (`loop/scenes/transcript.py`, `lines_of_json`) and a problem seen on a
5
+ * stage is replayed in a test.
6
+ *
7
+ * @module components/teams/transcript/transcriptExport
8
+ */
9
+ import type { SceneTranscriptLine } from '../sceneTranscriptRules';
10
+ /** The file's version: refused when another is read. */
11
+ export declare const TRANSCRIPT_JSON_VERSION = 1;
12
+ /** What the file holds. */
13
+ export type TranscriptJson = {
14
+ version: typeof TRANSCRIPT_JSON_VERSION;
15
+ /** When it was exported, ISO. */
16
+ exported: string;
17
+ /** The scene, by id, when known. */
18
+ scene?: string;
19
+ lines: SceneTranscriptLine[];
20
+ };
21
+ /** The lines as JSON text, two spaces in. */
22
+ export declare function transcriptJson(lines: readonly SceneTranscriptLine[], options?: {
23
+ scene?: string;
24
+ now?: () => number;
25
+ }): string;
26
+ /** The lines of a file written by {@link transcriptJson}; a sentence for anything else. */
27
+ export declare function linesOfTranscriptJson(text: string): SceneTranscriptLine[];
28
+ /** The file's name: `<scene>-transcript.json`, or `transcript.json`. */
29
+ export declare function transcriptFileName(scene?: string): string;
30
+ /** Hands the browser a file to save: a link to a blob, clicked. */
31
+ export declare function downloadText(name: string, text: string, type?: string): void;
@@ -0,0 +1,82 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /** The file's version: refused when another is read. */
6
+ export const TRANSCRIPT_JSON_VERSION = 1;
7
+ /** The lines as JSON text, two spaces in. */
8
+ export function transcriptJson(lines, options = {}) {
9
+ const now = options.now ?? Date.now;
10
+ const file = {
11
+ version: TRANSCRIPT_JSON_VERSION,
12
+ exported: new Date(now()).toISOString(),
13
+ ...(options.scene ? { scene: options.scene } : {}),
14
+ lines: lines.map(line => ({ ...line })),
15
+ };
16
+ return `${JSON.stringify(file, null, 2)}\n`;
17
+ }
18
+ const KINDS = new Set(['said', 'asked', 'called']);
19
+ /** The lines of a file written by {@link transcriptJson}; a sentence for anything else. */
20
+ export function linesOfTranscriptJson(text) {
21
+ let parsed;
22
+ try {
23
+ parsed = JSON.parse(text);
24
+ }
25
+ catch {
26
+ throw new Error('The transcript file is not JSON.');
27
+ }
28
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
29
+ throw new Error('The transcript file is not an object.');
30
+ }
31
+ const file = parsed;
32
+ if (file.version !== TRANSCRIPT_JSON_VERSION) {
33
+ throw new Error(`The transcript file is version ${String(file.version)}; this reads version ${TRANSCRIPT_JSON_VERSION}.`);
34
+ }
35
+ if (!Array.isArray(file.lines)) {
36
+ throw new Error('The transcript file has no lines.');
37
+ }
38
+ return file.lines.map((line, index) => {
39
+ const at = line;
40
+ if (typeof at.key !== 'string' ||
41
+ typeof at.at !== 'number' ||
42
+ typeof at.from !== 'string' ||
43
+ typeof at.text !== 'string' ||
44
+ !KINDS.has(String(at.kind))) {
45
+ throw new Error(`Line ${index + 1} of the transcript file is not a line.`);
46
+ }
47
+ return {
48
+ key: at.key,
49
+ at: at.at,
50
+ from: at.from,
51
+ ...(typeof at.to === 'string' ? { to: at.to } : {}),
52
+ kind: at.kind,
53
+ text: at.text,
54
+ ...(at.open ? { open: true } : {}),
55
+ ...(at.failed ? { failed: true } : {}),
56
+ ...(typeof at.ended === 'number' ? { ended: at.ended } : {}),
57
+ };
58
+ });
59
+ }
60
+ /** The file's name: `<scene>-transcript.json`, or `transcript.json`. */
61
+ export function transcriptFileName(scene) {
62
+ const slug = (scene ?? '')
63
+ .replace(/[^a-z0-9-]+/gi, '-')
64
+ .replace(/^-|-$/g, '');
65
+ return slug ? `${slug}-transcript.json` : 'transcript.json';
66
+ }
67
+ /** Hands the browser a file to save: a link to a blob, clicked. */
68
+ export function downloadText(name, text, type = 'application/json') {
69
+ const document = globalThis.document;
70
+ if (!document || typeof URL.createObjectURL !== 'function') {
71
+ return;
72
+ }
73
+ const url = URL.createObjectURL(new Blob([text], { type }));
74
+ const anchor = document.createElement('a');
75
+ anchor.href = url;
76
+ anchor.download = name;
77
+ anchor.rel = 'noopener';
78
+ document.body.appendChild(anchor);
79
+ anchor.click();
80
+ anchor.remove();
81
+ setTimeout(() => URL.revokeObjectURL(url), 0);
82
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The transcript as rows (SCENES S2-01): one row per line, with what the
3
+ * lines do not say in so many words — over which protocol an exchange went,
4
+ * how long a call or an ask took, in what state it is — and the filter the
5
+ * three views share: members, kinds, *failed only*, failed first.
6
+ *
7
+ * Pure: lines in, rows out, so that a test reads a recorded transcript and
8
+ * the page draws a live one the same way ({@link TranscriptTable}).
9
+ *
10
+ * @module components/teams/transcript/transcriptRows
11
+ */
12
+ import { type SceneTranscriptKind, type SceneTranscriptLine } from '../sceneTranscriptRules';
13
+ /** Over what a line went: a member asked over A2A, a server called over MCP, a tool of the member's own. */
14
+ export type TranscriptOver = 'a2a' | 'mcp' | 'tool';
15
+ /** What a row is in: still happening, failed, or done. */
16
+ export type TranscriptState = 'open' | 'failed' | 'done';
17
+ /** One row of the table. */
18
+ export type TranscriptRow = {
19
+ /** The line's key. */
20
+ id: string;
21
+ at: number;
22
+ from: string;
23
+ to?: string;
24
+ /** None for words said to all, or asked by the person. */
25
+ over?: TranscriptOver;
26
+ kind: SceneTranscriptKind;
27
+ text: string;
28
+ /** A call's, from its `called` to its answer; an ask's, from the question to the answer. None while open. */
29
+ durationMs?: number;
30
+ state: TranscriptState;
31
+ /** The line is part of an exchange that failed: it failed, or its answer did. */
32
+ exchangeFailed: boolean;
33
+ /** Its place in the lines, from 0: what a tie in time keeps. */
34
+ order: number;
35
+ /** The line itself. */
36
+ line: SceneTranscriptLine;
37
+ };
38
+ /** The span's or the entry's id a key belongs to: what an ask and its answer share. */
39
+ export declare function exchangeOf(key: string): string;
40
+ /** The exchanges that failed: a line failed, or its answer did. */
41
+ export declare function failedExchangesOf(lines: readonly SceneTranscriptLine[]): Set<string>;
42
+ /** Over what a line went, from who it reaches. */
43
+ export declare function overOf(line: SceneTranscriptLine): TranscriptOver | undefined;
44
+ /** The rows of a transcript, in the lines' order. */
45
+ export declare function transcriptRows(lines: readonly SceneTranscriptLine[]): TranscriptRow[];
46
+ /**
47
+ * Two rows by time: an open row after a closed one at the same time (it is
48
+ * still happening: last), a tie kept in the lines' order. What the table's
49
+ * Time column sorts by.
50
+ */
51
+ export declare function compareByTime(a: TranscriptRow, b: TranscriptRow): number;
52
+ /** The rows by time ({@link compareByTime}); descending reverses it. */
53
+ export declare function byTime(rows: readonly TranscriptRow[], direction?: 'ASC' | 'DESC'): TranscriptRow[];
54
+ /** A duration in words: `12 ms`, `1.2 s`, `1 min 3 s`. */
55
+ export declare function durationText(ms: number | undefined): string;
56
+ /** What a row's state says. */
57
+ export declare const STATE_WORDS: Record<TranscriptState, string>;
58
+ /** What a row's `over` says. */
59
+ export declare const OVER_WORDS: Record<TranscriptOver, string>;
60
+ /** The rows as a Markdown table: what *Copy as Markdown* copies. */
61
+ export declare function transcriptMarkdown(rows: readonly TranscriptRow[]): string;
62
+ /** What narrows the three views: by member, by kind, to what failed; what failed first. */
63
+ export type TranscriptFilter = {
64
+ /** The members (and servers) a line is from or to; all when unsaid or empty. */
65
+ members?: readonly string[];
66
+ /** The kinds kept; all when unsaid or empty. */
67
+ kinds?: readonly SceneTranscriptKind[];
68
+ /** Only the exchanges that failed. */
69
+ failedOnly?: boolean;
70
+ /** The exchanges that failed first, each in its time order. */
71
+ failedFirst?: boolean;
72
+ };
73
+ /** The names a transcript speaks of: who is from or to, in first appearance. */
74
+ export declare function namesOf(lines: readonly SceneTranscriptLine[]): string[];
75
+ /** The lines the filter keeps, in order: the same lines for every view. */
76
+ export declare function filterLines(lines: readonly SceneTranscriptLine[], filter?: TranscriptFilter): SceneTranscriptLine[];
@@ -0,0 +1,187 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The transcript as rows (SCENES S2-01): one row per line, with what the
7
+ * lines do not say in so many words — over which protocol an exchange went,
8
+ * how long a call or an ask took, in what state it is — and the filter the
9
+ * three views share: members, kinds, *failed only*, failed first.
10
+ *
11
+ * Pure: lines in, rows out, so that a test reads a recorded transcript and
12
+ * the page draws a live one the same way ({@link TranscriptTable}).
13
+ *
14
+ * @module components/teams/transcript/transcriptRows
15
+ */
16
+ import { isToolHome, PERSON, timeText, } from '../sceneTranscriptRules.js';
17
+ /** The span's or the entry's id a key belongs to: what an ask and its answer share. */
18
+ export function exchangeOf(key) {
19
+ const at = key.lastIndexOf(':');
20
+ return at > 0 ? key.slice(0, at) : key;
21
+ }
22
+ /** Whether a line answers: words said back to the one who asked, or said to all at the end of a turn. */
23
+ const isAnswer = (line) => line.kind === 'said';
24
+ /** The answer of each exchange, by the exchange's id. */
25
+ function answersOf(lines) {
26
+ const answers = new Map();
27
+ for (const line of lines) {
28
+ if (isAnswer(line)) {
29
+ answers.set(exchangeOf(line.key), line);
30
+ }
31
+ }
32
+ return answers;
33
+ }
34
+ /** The exchanges that failed: a line failed, or its answer did. */
35
+ export function failedExchangesOf(lines) {
36
+ const failed = new Set();
37
+ for (const line of lines) {
38
+ if (line.failed) {
39
+ failed.add(exchangeOf(line.key));
40
+ }
41
+ }
42
+ return failed;
43
+ }
44
+ /** Over what a line went, from who it reaches. */
45
+ export function overOf(line) {
46
+ if (line.kind === 'called') {
47
+ return isToolHome(line.to) ? 'tool' : 'mcp';
48
+ }
49
+ if (line.kind === 'asked') {
50
+ return line.from === PERSON ? undefined : 'a2a';
51
+ }
52
+ return line.to ? 'a2a' : undefined;
53
+ }
54
+ /** The rows of a transcript, in the lines' order. */
55
+ export function transcriptRows(lines) {
56
+ const answers = answersOf(lines);
57
+ const failed = failedExchangesOf(lines);
58
+ return lines.map((line, order) => {
59
+ const exchange = exchangeOf(line.key);
60
+ const answer = line.kind === 'said' ? undefined : answers.get(exchange);
61
+ const durationMs = line.open
62
+ ? undefined
63
+ : answer
64
+ ? Math.max(0, answer.at - line.at)
65
+ : line.ended !== undefined
66
+ ? Math.max(0, line.ended - line.at)
67
+ : undefined;
68
+ return {
69
+ id: line.key,
70
+ at: line.at,
71
+ from: line.from,
72
+ ...(line.to ? { to: line.to } : {}),
73
+ ...(overOf(line) ? { over: overOf(line) } : {}),
74
+ kind: line.kind,
75
+ text: line.text,
76
+ ...(durationMs !== undefined ? { durationMs } : {}),
77
+ state: line.open ? 'open' : line.failed ? 'failed' : 'done',
78
+ exchangeFailed: failed.has(exchange),
79
+ order,
80
+ line,
81
+ };
82
+ });
83
+ }
84
+ /**
85
+ * Two rows by time: an open row after a closed one at the same time (it is
86
+ * still happening: last), a tie kept in the lines' order. What the table's
87
+ * Time column sorts by.
88
+ */
89
+ export function compareByTime(a, b) {
90
+ return (a.at - b.at ||
91
+ Number(a.state === 'open') - Number(b.state === 'open') ||
92
+ a.order - b.order);
93
+ }
94
+ /** The rows by time ({@link compareByTime}); descending reverses it. */
95
+ export function byTime(rows, direction = 'ASC') {
96
+ const ordered = [...rows].sort(compareByTime);
97
+ return direction === 'DESC' ? ordered.reverse() : ordered;
98
+ }
99
+ /** A duration in words: `12 ms`, `1.2 s`, `1 min 3 s`. */
100
+ export function durationText(ms) {
101
+ if (ms === undefined) {
102
+ return '';
103
+ }
104
+ if (ms < 1000) {
105
+ return `${Math.round(ms)} ms`;
106
+ }
107
+ if (ms < 60_000) {
108
+ return `${(ms / 1000).toFixed(1).replace(/\.0$/, '')} s`;
109
+ }
110
+ const minutes = Math.floor(ms / 60_000);
111
+ const seconds = Math.round((ms % 60_000) / 1000);
112
+ return seconds ? `${minutes} min ${seconds} s` : `${minutes} min`;
113
+ }
114
+ /** What a row's state says. */
115
+ export const STATE_WORDS = {
116
+ open: 'running',
117
+ failed: 'failed',
118
+ done: 'done',
119
+ };
120
+ /** What a row's `over` says. */
121
+ export const OVER_WORDS = {
122
+ a2a: 'A2A',
123
+ mcp: 'MCP',
124
+ tool: 'tool',
125
+ };
126
+ /** A cell's text in a Markdown table: no bar, no line break. */
127
+ const cell = (text) => text
128
+ .replace(/\|/g, '\\|')
129
+ .replace(/\s*\n\s*/g, ' ')
130
+ .trim();
131
+ /** The rows as a Markdown table: what *Copy as Markdown* copies. */
132
+ export function transcriptMarkdown(rows) {
133
+ const head = '| Time | From | To | Over | Kind | Text | Duration | State |';
134
+ const rule = '| --- | --- | --- | --- | --- | --- | --- | --- |';
135
+ const body = rows.map(row => [
136
+ timeText(row.at),
137
+ row.from,
138
+ row.to ?? '',
139
+ row.over ? OVER_WORDS[row.over] : '',
140
+ row.kind,
141
+ row.text,
142
+ durationText(row.durationMs),
143
+ STATE_WORDS[row.state],
144
+ ]
145
+ .map(cell)
146
+ .join(' | '));
147
+ return [head, rule, ...body.map(line => `| ${line} |`)].join('\n');
148
+ }
149
+ /** The names a transcript speaks of: who is from or to, in first appearance. */
150
+ export function namesOf(lines) {
151
+ const names = [];
152
+ for (const line of lines) {
153
+ for (const name of [line.from, line.to]) {
154
+ if (name && !names.includes(name)) {
155
+ names.push(name);
156
+ }
157
+ }
158
+ }
159
+ return names;
160
+ }
161
+ /** The lines the filter keeps, in order: the same lines for every view. */
162
+ export function filterLines(lines, filter = {}) {
163
+ const failed = failedExchangesOf(lines);
164
+ const members = filter.members?.length ? new Set(filter.members) : null;
165
+ const kinds = filter.kinds?.length ? new Set(filter.kinds) : null;
166
+ const kept = lines.filter(line => {
167
+ if (members &&
168
+ !members.has(line.from) &&
169
+ !(line.to && members.has(line.to))) {
170
+ return false;
171
+ }
172
+ if (kinds && !kinds.has(line.kind)) {
173
+ return false;
174
+ }
175
+ if (filter.failedOnly && !failed.has(exchangeOf(line.key))) {
176
+ return false;
177
+ }
178
+ return true;
179
+ });
180
+ if (!filter.failedFirst) {
181
+ return kept;
182
+ }
183
+ return [
184
+ ...kept.filter(line => failed.has(exchangeOf(line.key))),
185
+ ...kept.filter(line => !failed.has(exchangeOf(line.key))),
186
+ ];
187
+ }