niceeval 0.5.3 → 0.6.0
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/INDEX.md +55 -0
- package/dist/agents/types.d.ts +317 -0
- package/dist/context/types.d.ts +335 -0
- package/dist/i18n/core.d.ts +11 -0
- package/dist/i18n/en.d.ts +181 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/zh-CN.d.ts +183 -0
- package/dist/o11y/execution-tree.d.ts +103 -0
- package/dist/o11y/otlp/parse.d.ts +3 -0
- package/dist/o11y/otlp/receiver.d.ts +21 -0
- package/dist/o11y/otlp/select.d.ts +22 -0
- package/dist/o11y/otlp/turn-otel.d.ts +43 -0
- package/dist/o11y/types.d.ts +190 -0
- package/dist/report/aggregate.d.ts +57 -0
- package/dist/report/aggregate.js +212 -0
- package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +1 -0
- package/dist/report/built-ins/cost-pass-rate-comparison.js +17 -0
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +2 -0
- package/dist/report/components.d.ts +178 -0
- package/dist/report/components.js +132 -0
- package/dist/report/compute.d.ts +106 -0
- package/dist/report/compute.js +618 -0
- package/dist/report/flag.d.ts +6 -0
- package/dist/report/flag.js +8 -0
- package/dist/report/format.d.ts +32 -0
- package/dist/report/format.js +126 -0
- package/dist/report/index.d.ts +19 -0
- package/dist/report/index.js +30 -0
- package/dist/report/load.d.ts +12 -0
- package/dist/report/load.js +56 -0
- package/dist/report/locale.d.ts +83 -0
- package/dist/report/locale.js +160 -0
- package/dist/report/metrics.d.ts +21 -0
- package/dist/report/metrics.js +115 -0
- package/dist/report/primitives.d.ts +64 -0
- package/dist/report/primitives.js +113 -0
- package/dist/report/react/AttemptList.d.ts +23 -0
- package/dist/report/react/AttemptList.js +20 -0
- package/dist/report/react/DeltaTable.d.ts +8 -0
- package/dist/report/react/DeltaTable.js +24 -0
- package/dist/report/react/EvalList.d.ts +10 -0
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentList.d.ts +10 -0
- package/dist/report/react/ExperimentList.js +15 -0
- package/dist/report/react/GroupSummary.d.ts +8 -0
- package/dist/report/react/GroupSummary.js +8 -0
- package/dist/report/react/MetricBars.d.ts +10 -0
- package/dist/report/react/MetricBars.js +48 -0
- package/dist/report/react/MetricLine.d.ts +9 -0
- package/dist/report/react/MetricLine.js +68 -0
- package/dist/report/react/MetricMatrix.d.ts +10 -0
- package/dist/report/react/MetricMatrix.js +23 -0
- package/dist/report/react/MetricScatter.d.ts +9 -0
- package/dist/report/react/MetricScatter.js +100 -0
- package/dist/report/react/MetricTable.d.ts +11 -0
- package/dist/report/react/MetricTable.js +26 -0
- package/dist/report/react/RunOverview.d.ts +8 -0
- package/dist/report/react/RunOverview.js +12 -0
- package/dist/report/react/Scoreboard.d.ts +8 -0
- package/dist/report/react/Scoreboard.js +26 -0
- package/dist/report/react/cell.d.ts +9 -0
- package/dist/report/react/cell.js +9 -0
- package/dist/report/react/chart-math.d.ts +14 -0
- package/dist/report/react/chart-math.js +68 -0
- package/dist/report/react/colors.d.ts +14 -0
- package/dist/report/react/colors.js +45 -0
- package/dist/report/react/fixtures.d.ts +19 -0
- package/dist/report/react/fixtures.js +393 -0
- package/dist/report/react/format.d.ts +3 -0
- package/dist/report/react/format.js +7 -0
- package/dist/report/react/index.d.ts +6 -0
- package/dist/report/react/index.js +14 -0
- package/dist/report/report.d.ts +24 -0
- package/dist/report/report.js +49 -0
- package/dist/report/text/faces.d.ts +29 -0
- package/dist/report/text/faces.js +503 -0
- package/dist/report/text/layout.d.ts +75 -0
- package/dist/report/text/layout.js +173 -0
- package/dist/report/text/plot.d.ts +37 -0
- package/dist/report/text/plot.js +81 -0
- package/dist/report/text/table.d.ts +8 -0
- package/dist/report/text/table.js +86 -0
- package/dist/report/tree.d.ts +99 -0
- package/dist/report/tree.js +228 -0
- package/dist/report/types.d.ts +420 -0
- package/dist/report/types.js +4 -0
- package/dist/report/web.d.ts +15 -0
- package/dist/report/web.js +37 -0
- package/dist/results/annotated-source.d.ts +61 -0
- package/dist/results/attempt-evidence.d.ts +69 -0
- package/dist/results/attempt-source.d.ts +15 -0
- package/dist/results/locator.d.ts +75 -0
- package/dist/results/locator.js +0 -0
- package/dist/results/select.d.ts +45 -0
- package/dist/results/select.js +230 -0
- package/dist/results/source-hash.d.ts +9 -0
- package/dist/results/types.d.ts +204 -0
- package/dist/runner/eval-source.d.ts +22 -0
- package/dist/runner/fingerprint.d.ts +23 -0
- package/dist/runner/types.d.ts +332 -0
- package/dist/sandbox/docker-stream.d.ts +24 -0
- package/dist/sandbox/docker.d.ts +84 -0
- package/dist/sandbox/e2b.d.ts +31 -0
- package/dist/sandbox/errors.d.ts +4 -0
- package/dist/sandbox/local-files.d.ts +6 -0
- package/dist/sandbox/paths.d.ts +4 -0
- package/dist/sandbox/registry.d.ts +13 -0
- package/dist/sandbox/resolve.d.ts +40 -0
- package/dist/sandbox/retry.d.ts +10 -0
- package/dist/sandbox/shell.d.ts +11 -0
- package/dist/sandbox/source-files.d.ts +13 -0
- package/dist/sandbox/types.d.ts +203 -0
- package/dist/sandbox/vercel.d.ts +37 -0
- package/dist/scoring/types.d.ts +70 -0
- package/dist/shared/aggregate.d.ts +29 -0
- package/dist/shared/aggregate.js +55 -0
- package/dist/shared/types.d.ts +27 -0
- package/dist/shared/verdict.d.ts +27 -0
- package/dist/shared/verdict.js +34 -0
- package/dist/tty-line.d.ts +6 -0
- package/dist/types.d.ts +7 -0
- package/dist/util.d.ts +22 -0
- package/docs-site/zh/concepts/adapter.mdx +9 -4
- package/docs-site/zh/concepts/assert.mdx +8 -8
- package/docs-site/zh/concepts/evals.mdx +5 -5
- package/docs-site/zh/concepts/experiment.mdx +3 -1
- package/docs-site/zh/concepts/judge.mdx +9 -9
- package/docs-site/zh/concepts/overview.mdx +2 -2
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
- package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
- package/docs-site/zh/guides/agent-feedback-loop.mdx +235 -78
- package/docs-site/zh/guides/connect-otel.mdx +3 -3
- package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
- package/docs-site/zh/guides/custom-reports.mdx +196 -36
- package/docs-site/zh/guides/experiments.mdx +7 -0
- package/docs-site/zh/guides/fixtures.mdx +1 -1
- package/docs-site/zh/guides/official-adapters.mdx +31 -9
- package/docs-site/zh/guides/publish-report.mdx +4 -4
- package/docs-site/zh/guides/report-components.mdx +194 -55
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +35 -34
- package/docs-site/zh/guides/runner.mdx +2 -2
- package/docs-site/zh/guides/sandbox-agent.mdx +10 -5
- package/docs-site/zh/guides/sandbox-providers.mdx +102 -0
- package/docs-site/zh/guides/viewing-results.mdx +199 -79
- package/docs-site/zh/guides/write-experiment.mdx +23 -1
- package/docs-site/zh/guides/write-send.mdx +2 -2
- package/docs-site/zh/index.mdx +4 -3
- package/docs-site/zh/introduction.mdx +2 -1
- package/docs-site/zh/quickstart.mdx +5 -3
- package/docs-site/zh/reference/builtin-agents.mdx +38 -13
- package/docs-site/zh/reference/capabilities.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +36 -13
- package/docs-site/zh/reference/define-agent.mdx +24 -12
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/docs-site/zh/reference/events.mdx +7 -1
- package/docs-site/zh/reference/expect.mdx +3 -3
- package/package.json +10 -6
- package/src/agents/bub.ts +134 -43
- package/src/agents/claude-code.test.ts +152 -0
- package/src/agents/claude-code.ts +127 -13
- package/src/agents/codex.test.ts +192 -0
- package/src/agents/codex.ts +151 -12
- package/src/agents/index.ts +11 -3
- package/src/agents/manifest.ts +41 -0
- package/src/agents/plugin-config.test.ts +94 -0
- package/src/agents/shared.ts +33 -2
- package/src/agents/skills.test.ts +214 -0
- package/src/agents/skills.ts +304 -0
- package/src/agents/streaming.ts +5 -5
- package/src/agents/types.ts +62 -2
- package/src/cli.ts +74 -47
- package/src/context/context.test.ts +20 -2
- package/src/context/context.ts +3 -0
- package/src/context/control-flow.ts +1 -1
- package/src/context/session.test.ts +92 -1
- package/src/context/session.ts +26 -1
- package/src/context/types.ts +4 -4
- package/src/define.test.ts +82 -0
- package/src/define.ts +72 -11
- package/src/expect/index.ts +5 -5
- package/src/i18n/en.ts +36 -15
- package/src/i18n/zh-CN.ts +39 -21
- package/src/index.ts +1 -1
- package/src/o11y/execution-tree.test.ts +452 -0
- package/src/o11y/execution-tree.ts +367 -0
- package/src/o11y/otlp/receiver.ts +1 -1
- package/src/o11y/otlp/select.ts +7 -3
- package/src/o11y/parsers/claude-code.test.ts +175 -0
- package/src/o11y/parsers/claude-code.ts +32 -0
- package/src/o11y/types.ts +6 -1
- package/src/report/aggregate.ts +26 -20
- package/src/report/built-in-user-parity.test.tsx +643 -0
- package/src/report/built-ins/cost-pass-rate-comparison.tsx +23 -0
- package/src/report/built-ins/index.ts +2 -0
- package/src/report/components.tsx +223 -50
- package/src/report/compute.ts +358 -53
- package/src/report/dual-render.test.tsx +1125 -0
- package/src/report/flag.ts +1 -1
- package/src/report/format.ts +50 -2
- package/src/report/index.ts +48 -14
- package/src/report/load.ts +1 -1
- package/src/report/locale.ts +206 -0
- package/src/report/metrics.ts +39 -15
- package/src/report/primitives.tsx +100 -2
- package/src/report/react/AttemptList.tsx +115 -0
- package/src/report/react/DeltaTable.tsx +9 -6
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentList.tsx +108 -0
- package/src/report/react/GroupSummary.tsx +66 -0
- package/src/report/react/MetricBars.tsx +24 -13
- package/src/report/react/MetricLine.tsx +21 -21
- package/src/report/react/MetricMatrix.tsx +8 -4
- package/src/report/react/MetricScatter.tsx +136 -96
- package/src/report/react/MetricTable.tsx +115 -15
- package/src/report/react/RunOverview.tsx +48 -26
- package/src/report/react/Scoreboard.tsx +15 -7
- package/src/report/react/cell.tsx +18 -9
- package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
- package/src/report/react/colors.ts +24 -15
- package/src/report/react/enhance.js +129 -0
- package/src/report/react/fixtures.ts +163 -34
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +22 -6
- package/src/report/react/render.test.tsx +164 -31
- package/src/report/react/styles.css +405 -213
- package/src/report/report.test.ts +403 -94
- package/src/report/report.ts +29 -11
- package/src/report/text/faces.ts +303 -95
- package/src/report/text/layout.ts +76 -11
- package/src/report/text/table.ts +98 -0
- package/src/report/tree.ts +103 -20
- package/src/report/types.ts +196 -25
- package/src/report/web.ts +46 -19
- package/src/results/annotated-source.test.ts +146 -0
- package/src/results/annotated-source.ts +128 -0
- package/src/results/attempt-evidence.test.ts +158 -0
- package/src/results/attempt-evidence.ts +158 -0
- package/src/results/attempt-source.ts +48 -0
- package/src/results/copy.ts +154 -139
- package/src/results/format.ts +54 -47
- package/src/results/host-equivalence.test.ts +527 -0
- package/src/results/index.ts +56 -14
- package/src/results/locator.test.ts +198 -0
- package/src/results/locator.ts +0 -0
- package/src/results/open.ts +392 -175
- package/src/results/results.test.ts +943 -494
- package/src/results/select.ts +129 -23
- package/src/results/skipped-notice.ts +0 -0
- package/src/results/source-hash.ts +28 -0
- package/src/results/types.ts +98 -62
- package/src/results/writer.ts +304 -176
- package/src/runner/attempt.test.ts +170 -0
- package/src/runner/attempt.ts +97 -15
- package/src/runner/discover.test.ts +65 -0
- package/src/runner/discover.ts +6 -2
- package/src/runner/eval-source.test.ts +121 -0
- package/src/runner/eval-source.ts +45 -0
- package/src/runner/fingerprint.ts +54 -1
- package/src/runner/remote-sandbox.ts +1 -1
- package/src/runner/report.test.ts +6 -8
- package/src/runner/report.ts +5 -4
- package/src/runner/reporters/artifacts.ts +27 -41
- package/src/runner/reporters/braintrust.test.ts +3 -3
- package/src/runner/reporters/braintrust.ts +1 -1
- package/src/runner/reporters/console.ts +6 -6
- package/src/runner/reporters/json.ts +4 -4
- package/src/runner/reporters/live.test.ts +56 -0
- package/src/runner/reporters/live.ts +36 -10
- package/src/runner/reporters/quiet.test.ts +66 -0
- package/src/runner/reporters/quiet.ts +49 -0
- package/src/runner/reporters/shared.ts +5 -5
- package/src/runner/reporters/table.ts +17 -17
- package/src/runner/run.ts +61 -102
- package/src/runner/types.ts +43 -32
- package/src/sandbox/checkpoint.ts +1 -1
- package/src/sandbox/docker.ts +15 -2
- package/src/sandbox/e2b.ts +11 -5
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +3 -3
- package/src/sandbox/local-files.ts +1 -1
- package/src/sandbox/registry.ts +6 -5
- package/src/sandbox/resolve.ts +38 -23
- package/src/sandbox/retry.test.ts +68 -0
- package/src/sandbox/retry.ts +44 -0
- package/src/sandbox/types.ts +69 -28
- package/src/sandbox/vercel.ts +17 -3
- package/src/scoring/collector.ts +3 -3
- package/src/scoring/judge.ts +2 -2
- package/src/scoring/scoped.ts +13 -1
- package/src/scoring/types.ts +7 -7
- package/src/scoring/verdict.ts +4 -4
- package/src/shared/aggregate.ts +23 -3
- package/src/shared/types.ts +2 -2
- package/src/shared/{outcome.ts → verdict.ts} +15 -15
- package/src/show/compose.ts +12 -132
- package/src/show/index.ts +125 -94
- package/src/show/render.ts +660 -160
- package/src/show/show.test.ts +472 -253
- package/src/source-loc.ts +1 -1
- package/src/tty-line.ts +32 -0
- package/src/types.ts +2 -2
- package/src/util.test.ts +21 -1
- package/src/util.ts +5 -1
- package/src/view/app/App.tsx +33 -198
- package/src/view/app/components/AttemptModal.tsx +6 -6
- package/src/view/app/components/CodeView.tsx +2 -2
- package/src/view/app/components/CopyControls.tsx +32 -36
- package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
- package/src/view/app/components/ui/badge.tsx +1 -1
- package/src/view/app/components/ui/dialog.tsx +2 -2
- package/src/view/app/i18n.ts +40 -31
- package/src/view/app/lib/artifact-url.ts +1 -1
- package/src/view/app/lib/attempt-route.test.ts +37 -47
- package/src/view/app/lib/attempt-route.ts +26 -26
- package/src/view/app/lib/rows.ts +9 -112
- package/src/view/app/lib/verdict.ts +25 -0
- package/src/view/app/main.tsx +9 -12
- package/src/view/app/pages/RunsPage.tsx +4 -4
- package/src/view/app/pages/TracesPage.tsx +3 -3
- package/src/view/app/shared.ts +0 -1
- package/src/view/app/types.ts +6 -51
- package/src/view/artifact-serving.test.ts +136 -0
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +21 -21
- package/src/view/data.test.ts +214 -143
- package/src/view/data.ts +166 -120
- package/src/view/index.ts +23 -11
- package/src/view/server.ts +52 -17
- package/src/view/shared/types.ts +22 -31
- package/src/view/styles.css +117 -327
- package/src/view/view-report.test.ts +146 -98
- package/docs-site/zh/guides/sandbox-backends.mdx +0 -75
- package/src/report/default-report.tsx +0 -222
- package/src/report/dual-face.test.tsx +0 -527
- package/src/report/react/CaseList.tsx +0 -70
- package/src/view/app/components/CostScoreChart.tsx +0 -147
- package/src/view/app/components/ExperimentTable.tsx +0 -284
- package/src/view/app/components/GroupSelector.tsx +0 -61
- package/src/view/app/components/primitives.tsx +0 -43
- package/src/view/app/lib/outcome.ts +0 -82
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
// buildExecutionTree 的单测(定稿见 docs/observability.md「OTLP traces → 统一瀑布图」、
|
|
2
|
+
// docs/concepts.md「执行树」词条)。覆盖:无 OTel 时骨架完整、有 OTel 时按 callId 精确合并、
|
|
3
|
+
// 唯一关联不上时降级成 telemetry-only 节点(不猜)、同 callId 撞多条 span 时不強行择一、
|
|
4
|
+
// call id 撞不上任何节点时同样降级、乱序/截断 transcript 的占位节点、skill.loaded 一等节点、
|
|
5
|
+
// tool 失败状态透传、telemetry-only 节点按时间排序追加在骨架之后。
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import type { StreamEvent, TraceSpan } from "../types.ts";
|
|
9
|
+
import {
|
|
10
|
+
buildExecutionTree,
|
|
11
|
+
type ExecutionActionNode,
|
|
12
|
+
type ExecutionSkillNode,
|
|
13
|
+
type ExecutionTelemetryNode,
|
|
14
|
+
} from "./execution-tree.ts";
|
|
15
|
+
import { IO_MAX, ioText } from "./otlp/select.ts";
|
|
16
|
+
import { parseClaudeCodeTranscript } from "./parsers/claude-code.ts";
|
|
17
|
+
|
|
18
|
+
function span(over: Partial<TraceSpan> & Pick<TraceSpan, "spanId">): TraceSpan {
|
|
19
|
+
return {
|
|
20
|
+
traceId: "trace-1",
|
|
21
|
+
name: "span",
|
|
22
|
+
startMs: 0,
|
|
23
|
+
endMs: 10,
|
|
24
|
+
...over,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("buildExecutionTree", () => {
|
|
29
|
+
it("builds the full skeleton from events alone, with every node's span absent and timingAvailable false", () => {
|
|
30
|
+
const events: StreamEvent[] = [
|
|
31
|
+
{ type: "message", role: "user", text: "do the thing" },
|
|
32
|
+
{ type: "thinking", text: "let me think" },
|
|
33
|
+
{ type: "skill.loaded", skill: "pdf-processing" },
|
|
34
|
+
{ type: "action.called", callId: "c1", name: "Bash", input: { cmd: "ls" }, tool: "shell" },
|
|
35
|
+
{ type: "action.result", callId: "c1", output: "a.ts\n", status: "completed" },
|
|
36
|
+
{ type: "subagent.called", callId: "s1", name: "Task" },
|
|
37
|
+
{ type: "subagent.completed", callId: "s1", output: "done", status: "completed" },
|
|
38
|
+
{ type: "input.requested", request: { prompt: "approve?" } },
|
|
39
|
+
{ type: "compaction", reason: "context full" },
|
|
40
|
+
{ type: "error", message: "boom" },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const tree = buildExecutionTree(events, []);
|
|
44
|
+
|
|
45
|
+
expect(tree.timingAvailable).toBe(false);
|
|
46
|
+
expect(tree.nodes.every((n) => n.kind === "telemetry" || n.span === undefined)).toBe(true);
|
|
47
|
+
expect(tree.nodes.map((n) => n.kind)).toEqual([
|
|
48
|
+
"message",
|
|
49
|
+
"thinking",
|
|
50
|
+
"skill.loaded",
|
|
51
|
+
"action",
|
|
52
|
+
"subagent",
|
|
53
|
+
"input.requested",
|
|
54
|
+
"compaction",
|
|
55
|
+
"error",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
const action = tree.nodes[3] as ExecutionActionNode;
|
|
59
|
+
expect(action).toMatchObject({
|
|
60
|
+
kind: "action",
|
|
61
|
+
callId: "c1",
|
|
62
|
+
name: "Bash",
|
|
63
|
+
tool: "shell",
|
|
64
|
+
input: { cmd: "ls" },
|
|
65
|
+
output: "a.ts\n",
|
|
66
|
+
status: "completed",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const subagent = tree.nodes[4];
|
|
70
|
+
expect(subagent).toMatchObject({
|
|
71
|
+
kind: "subagent",
|
|
72
|
+
callId: "s1",
|
|
73
|
+
name: "Task",
|
|
74
|
+
output: "done",
|
|
75
|
+
status: "completed",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("merges a span onto the action node it explicitly correlates to via attributes.call_id, and enriches its attributes with io.tool/io.input/io.output/io.status (same shape as otlp/select.ts's enrichTraceWithIO)", () => {
|
|
80
|
+
const events: StreamEvent[] = [
|
|
81
|
+
{ type: "action.called", callId: "c1", name: "Bash", input: { cmd: "ls" }, tool: "shell" },
|
|
82
|
+
{ type: "action.result", callId: "c1", output: "ok", status: "completed" },
|
|
83
|
+
];
|
|
84
|
+
const toolSpan = span({ spanId: "sp1", startMs: 100, endMs: 150, attributes: { call_id: "c1" } });
|
|
85
|
+
|
|
86
|
+
const tree = buildExecutionTree(events, [toolSpan]);
|
|
87
|
+
|
|
88
|
+
expect(tree.timingAvailable).toBe(true);
|
|
89
|
+
expect(tree.nodes).toHaveLength(1);
|
|
90
|
+
const action = tree.nodes[0] as ExecutionActionNode;
|
|
91
|
+
expect(action.span).toEqual({
|
|
92
|
+
...toolSpan,
|
|
93
|
+
attributes: {
|
|
94
|
+
call_id: "c1",
|
|
95
|
+
"io.tool": "Bash",
|
|
96
|
+
"io.input": ioText({ cmd: "ls" }),
|
|
97
|
+
"io.output": ioText("ok"),
|
|
98
|
+
"io.status": "completed",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
// node 自己的 input/output 仍是未截断的原始 JsonValue,不受 io.* 注入影响。
|
|
102
|
+
expect(action.input).toEqual({ cmd: "ls" });
|
|
103
|
+
expect(action.output).toBe("ok");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("also correlates via the GenAI semantic convention key gen_ai.tool.call.id", () => {
|
|
107
|
+
const events: StreamEvent[] = [
|
|
108
|
+
{ type: "action.called", callId: "c1", name: "Bash", input: {}, tool: "shell" },
|
|
109
|
+
{ type: "action.result", callId: "c1", output: "ok", status: "completed" },
|
|
110
|
+
];
|
|
111
|
+
const toolSpan = span({ spanId: "sp1", attributes: { "gen_ai.tool.call.id": "c1" } });
|
|
112
|
+
|
|
113
|
+
const tree = buildExecutionTree(events, [toolSpan]);
|
|
114
|
+
|
|
115
|
+
const action = tree.nodes[0] as ExecutionActionNode;
|
|
116
|
+
expect(action.span?.attributes).toMatchObject({ "gen_ai.tool.call.id": "c1", "io.tool": "Bash" });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("truncates long tool input/output onto the merged span's io.* attributes using the same IO_MAX budget as otlp/select.ts (not a different one)", () => {
|
|
120
|
+
const bigOutput = "x".repeat(IO_MAX + 500);
|
|
121
|
+
const events: StreamEvent[] = [
|
|
122
|
+
{ type: "action.called", callId: "c1", name: "Read", input: { path: "/big.txt" }, tool: "file_read" },
|
|
123
|
+
{ type: "action.result", callId: "c1", output: bigOutput, status: "completed" },
|
|
124
|
+
];
|
|
125
|
+
const toolSpan = span({ spanId: "sp1", attributes: { call_id: "c1" } });
|
|
126
|
+
|
|
127
|
+
const tree = buildExecutionTree(events, [toolSpan]);
|
|
128
|
+
|
|
129
|
+
const action = tree.nodes[0] as ExecutionActionNode;
|
|
130
|
+
const truncated = action.span?.attributes?.["io.output"];
|
|
131
|
+
expect(truncated).toBe(bigOutput.slice(0, IO_MAX) + `…(+${bigOutput.length - IO_MAX})`);
|
|
132
|
+
expect((truncated as string).length).toBeLessThan(bigOutput.length);
|
|
133
|
+
// 节点自身的 output 字段给程序化消费方保留完整未截断内容。
|
|
134
|
+
expect(action.output).toBe(bigOutput);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("does not enrich a subagent node's span with io.* keys (no tool-call-shaped input to join)", () => {
|
|
138
|
+
const events: StreamEvent[] = [
|
|
139
|
+
{ type: "subagent.called", callId: "s1", name: "Task" },
|
|
140
|
+
{ type: "subagent.completed", callId: "s1", output: "done", status: "completed" },
|
|
141
|
+
];
|
|
142
|
+
const agentSpan = span({ spanId: "sp1", attributes: { call_id: "s1" } });
|
|
143
|
+
|
|
144
|
+
const tree = buildExecutionTree(events, [agentSpan]);
|
|
145
|
+
|
|
146
|
+
expect(tree.nodes[0]).toMatchObject({ kind: "subagent", span: agentSpan });
|
|
147
|
+
expect(Object.keys(tree.nodes[0].span?.attributes ?? {})).toEqual(["call_id"]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("correlates a subagent node the same way as an action node", () => {
|
|
151
|
+
const events: StreamEvent[] = [
|
|
152
|
+
{ type: "subagent.called", callId: "s1", name: "Task" },
|
|
153
|
+
{ type: "subagent.completed", callId: "s1", output: "done", status: "completed" },
|
|
154
|
+
];
|
|
155
|
+
const agentSpan = span({ spanId: "sp1", attributes: { call_id: "s1" } });
|
|
156
|
+
|
|
157
|
+
const tree = buildExecutionTree(events, [agentSpan]);
|
|
158
|
+
|
|
159
|
+
expect(tree.nodes[0]).toMatchObject({ kind: "subagent", callId: "s1", span: agentSpan });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("never guesses by name/text: a span whose call_id matches no node becomes a telemetry-only node, and the skeleton node's span stays absent", () => {
|
|
163
|
+
const events: StreamEvent[] = [
|
|
164
|
+
{ type: "action.called", callId: "c1", name: "Bash", input: {}, tool: "shell" },
|
|
165
|
+
{ type: "action.result", callId: "c1", output: "ok", status: "completed" },
|
|
166
|
+
];
|
|
167
|
+
const orphanSpan = span({ spanId: "sp-orphan", attributes: { call_id: "does-not-exist" } });
|
|
168
|
+
|
|
169
|
+
const tree = buildExecutionTree(events, [orphanSpan]);
|
|
170
|
+
|
|
171
|
+
expect(tree.timingAvailable).toBe(true);
|
|
172
|
+
const action = tree.nodes.find((n) => n.kind === "action") as ExecutionActionNode;
|
|
173
|
+
expect(action.span).toBeUndefined();
|
|
174
|
+
const telemetry = tree.nodes.find((n) => n.kind === "telemetry") as ExecutionTelemetryNode;
|
|
175
|
+
expect(telemetry.span).toEqual(orphanSpan);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("a span with no call id at all (pure plumbing/model span) becomes a telemetry-only node", () => {
|
|
179
|
+
const events: StreamEvent[] = [{ type: "message", role: "assistant", text: "hi" }];
|
|
180
|
+
const modelSpan = span({ spanId: "sp-model", attributes: { "gen_ai.operation.name": "chat" } });
|
|
181
|
+
|
|
182
|
+
const tree = buildExecutionTree(events, [modelSpan]);
|
|
183
|
+
|
|
184
|
+
expect(tree.nodes).toHaveLength(2);
|
|
185
|
+
expect(tree.nodes[1]).toEqual({ id: "telemetry-sp-model", kind: "telemetry", span: modelSpan });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("two spans sharing the same call_id cannot be uniquely correlated, so neither merges — both fall back to telemetry-only", () => {
|
|
189
|
+
const events: StreamEvent[] = [
|
|
190
|
+
{ type: "action.called", callId: "c1", name: "Bash", input: {}, tool: "shell" },
|
|
191
|
+
{ type: "action.result", callId: "c1", output: "ok", status: "completed" },
|
|
192
|
+
];
|
|
193
|
+
const dup1 = span({ spanId: "sp1", attributes: { call_id: "c1" } });
|
|
194
|
+
const dup2 = span({ spanId: "sp2", attributes: { call_id: "c1" } });
|
|
195
|
+
|
|
196
|
+
const tree = buildExecutionTree(events, [dup1, dup2]);
|
|
197
|
+
|
|
198
|
+
const action = tree.nodes.find((n) => n.kind === "action") as ExecutionActionNode;
|
|
199
|
+
expect(action.span).toBeUndefined();
|
|
200
|
+
const telemetryIds = tree.nodes.filter((n) => n.kind === "telemetry").map((n) => n.id);
|
|
201
|
+
expect(telemetryIds.sort()).toEqual(["telemetry-sp1", "telemetry-sp2"]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("an action.result with no matching action.called produces a placeholder node instead of being dropped", () => {
|
|
205
|
+
const events: StreamEvent[] = [{ type: "action.result", callId: "orphan", output: "late", status: "completed" }];
|
|
206
|
+
|
|
207
|
+
const tree = buildExecutionTree(events, []);
|
|
208
|
+
|
|
209
|
+
expect(tree.nodes).toEqual([
|
|
210
|
+
{
|
|
211
|
+
id: "action-orphan",
|
|
212
|
+
kind: "action",
|
|
213
|
+
callId: "orphan",
|
|
214
|
+
name: "unknown",
|
|
215
|
+
input: null,
|
|
216
|
+
output: "late",
|
|
217
|
+
status: "completed",
|
|
218
|
+
},
|
|
219
|
+
]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("a subagent.completed with no matching subagent.called produces a placeholder node instead of being dropped", () => {
|
|
223
|
+
const events: StreamEvent[] = [{ type: "subagent.completed", callId: "orphan", output: "late", status: "failed" }];
|
|
224
|
+
|
|
225
|
+
const tree = buildExecutionTree(events, []);
|
|
226
|
+
|
|
227
|
+
expect(tree.nodes).toEqual([
|
|
228
|
+
{ id: "subagent-orphan", kind: "subagent", callId: "orphan", name: "unknown", output: "late", status: "failed" },
|
|
229
|
+
]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("an action that never receives a result stays in a distinct pending state, not silently completed", () => {
|
|
233
|
+
const events: StreamEvent[] = [{ type: "action.called", callId: "c1", name: "Bash", input: {}, tool: "shell" }];
|
|
234
|
+
|
|
235
|
+
const tree = buildExecutionTree(events, []);
|
|
236
|
+
|
|
237
|
+
expect(tree.nodes[0]).toMatchObject({ kind: "action", status: "pending" });
|
|
238
|
+
expect((tree.nodes[0] as ExecutionActionNode).output).toBeUndefined();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("propagates a failed tool result and its error-status span onto the same node", () => {
|
|
242
|
+
const events: StreamEvent[] = [
|
|
243
|
+
{ type: "action.called", callId: "c1", name: "Bash", input: { cmd: "false" }, tool: "shell" },
|
|
244
|
+
{ type: "action.result", callId: "c1", output: "exit 1", status: "failed" },
|
|
245
|
+
];
|
|
246
|
+
const failedSpan = span({ spanId: "sp1", status: "error", attributes: { call_id: "c1" } });
|
|
247
|
+
|
|
248
|
+
const tree = buildExecutionTree(events, [failedSpan]);
|
|
249
|
+
|
|
250
|
+
const action = tree.nodes[0] as ExecutionActionNode;
|
|
251
|
+
expect(action.status).toBe("failed");
|
|
252
|
+
expect(action.span?.status).toBe("error");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("skill.loaded is a first-class node carrying the skill name (and optional callId), never a generic action", () => {
|
|
256
|
+
const events: StreamEvent[] = [{ type: "skill.loaded", skill: "pdf-processing", callId: "tool_1" }];
|
|
257
|
+
|
|
258
|
+
const tree = buildExecutionTree(events, []);
|
|
259
|
+
|
|
260
|
+
expect(tree.nodes).toEqual([{ id: "skill-0", kind: "skill.loaded", skill: "pdf-processing", callId: "tool_1" }]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("a skill.loaded node passes through immediately as its own node — it has no result event to pair with, so it never gets stuck 'pending' the way action/subagent nodes do while awaiting a result", () => {
|
|
264
|
+
const events: StreamEvent[] = [
|
|
265
|
+
{ type: "action.called", callId: "c1", name: "Bash", input: {}, tool: "shell" },
|
|
266
|
+
{ type: "skill.loaded", skill: "pdf-processing", callId: "toolu_skill" },
|
|
267
|
+
{ type: "action.result", callId: "c1", output: "ok", status: "completed" },
|
|
268
|
+
{ type: "message", role: "assistant", text: "done" },
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
const tree = buildExecutionTree(events, []);
|
|
272
|
+
|
|
273
|
+
// Order preserved, skill node sits between the two halves of the unrelated action call,
|
|
274
|
+
// with no "pending"/"status" field at all (skill.loaded has none — it's not an
|
|
275
|
+
// action/subagent, so there's no "unresolved" state for it to be stuck in).
|
|
276
|
+
expect(tree.nodes.map((n) => n.kind)).toEqual(["action", "skill.loaded", "message"]);
|
|
277
|
+
const skill = tree.nodes[1] as ExecutionSkillNode;
|
|
278
|
+
expect(skill).toEqual({ id: "skill-0", kind: "skill.loaded", skill: "pdf-processing", callId: "toolu_skill" });
|
|
279
|
+
expect("status" in skill).toBe(false);
|
|
280
|
+
const action = tree.nodes[0] as ExecutionActionNode;
|
|
281
|
+
expect(action.status).toBe("completed");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("a span whose call_id explicitly correlates to a skill.loaded node's callId enriches that node with timing, same as an action/subagent node (Claude Code's Skill invocation is itself a tool_use, and the OTel mapper copies its tool_use_id onto span.attributes.call_id like any other tool span)", () => {
|
|
285
|
+
const events: StreamEvent[] = [{ type: "skill.loaded", skill: "pdf-processing", callId: "toolu_skill" }];
|
|
286
|
+
const skillSpan = span({ spanId: "sp1", startMs: 5, endMs: 20, attributes: { call_id: "toolu_skill" } });
|
|
287
|
+
|
|
288
|
+
const tree = buildExecutionTree(events, [skillSpan]);
|
|
289
|
+
|
|
290
|
+
expect(tree.nodes).toHaveLength(1);
|
|
291
|
+
const skill = tree.nodes[0] as ExecutionSkillNode;
|
|
292
|
+
expect(skill.span).toEqual(skillSpan);
|
|
293
|
+
// Not an action node, so no io.* attributes get invented onto it.
|
|
294
|
+
expect(Object.keys(skill.span?.attributes ?? {})).toEqual(["call_id"]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("a skill.loaded node with no callId at all simply never correlates — any span stays telemetry-only, no crash on the missing key", () => {
|
|
298
|
+
const events: StreamEvent[] = [{ type: "skill.loaded", skill: "pdf-processing" }];
|
|
299
|
+
const orphanSpan = span({ spanId: "sp1", attributes: { call_id: "does-not-exist" } });
|
|
300
|
+
|
|
301
|
+
const tree = buildExecutionTree(events, [orphanSpan]);
|
|
302
|
+
|
|
303
|
+
const skill = tree.nodes.find((n) => n.kind === "skill.loaded") as ExecutionSkillNode;
|
|
304
|
+
expect(skill.span).toBeUndefined();
|
|
305
|
+
expect(tree.nodes.some((n) => n.kind === "telemetry")).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("composes end to end with a real claude-code parser output: a Skill tool_use surrounded by ordinary tool calls produces the same skeleton whether piped straight from parseClaudeCodeTranscript or hand-built, and a correlated span enriches the resulting skill.loaded node", () => {
|
|
309
|
+
const line = (obj: object): string => JSON.stringify(obj);
|
|
310
|
+
const raw = [
|
|
311
|
+
line({ type: "assistant", message: { content: [{ type: "text", text: "let me check the docs" }] } }),
|
|
312
|
+
line({
|
|
313
|
+
type: "assistant",
|
|
314
|
+
message: { content: [{ type: "tool_use", id: "toolu_skill", name: "Skill", input: { skill: "pdf-processing" } }] },
|
|
315
|
+
}),
|
|
316
|
+
line({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: "toolu_skill", content: "PDF skill body" }] } }),
|
|
317
|
+
line({
|
|
318
|
+
type: "assistant",
|
|
319
|
+
message: { content: [{ type: "tool_use", id: "toolu_bash", name: "Bash", input: { command: "ls" } }] },
|
|
320
|
+
}),
|
|
321
|
+
line({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: "toolu_bash", content: "a.pdf" }] } }),
|
|
322
|
+
].join("\n");
|
|
323
|
+
|
|
324
|
+
const { events } = parseClaudeCodeTranscript(raw);
|
|
325
|
+
|
|
326
|
+
// Sanity check: the parser really did produce a first-class skill.loaded (not an
|
|
327
|
+
// action.called("Skill", ...)) with no orphan action.result for the swallowed tool_result.
|
|
328
|
+
expect(events).toEqual([
|
|
329
|
+
{ type: "message", role: "assistant", text: "let me check the docs" },
|
|
330
|
+
{ type: "skill.loaded", skill: "pdf-processing", callId: "toolu_skill" },
|
|
331
|
+
{ type: "action.called", callId: "toolu_bash", name: "Bash", input: { command: "ls" }, tool: "shell" },
|
|
332
|
+
{ type: "action.result", callId: "toolu_bash", output: "a.pdf", status: "completed" },
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
const skillSpan = span({ spanId: "sp-skill", startMs: 0, endMs: 40, attributes: { call_id: "toolu_skill" } });
|
|
336
|
+
const bashSpan = span({ spanId: "sp-bash", startMs: 41, endMs: 55, attributes: { call_id: "toolu_bash" } });
|
|
337
|
+
|
|
338
|
+
const tree = buildExecutionTree(events, [skillSpan, bashSpan]);
|
|
339
|
+
|
|
340
|
+
expect(tree.nodes.map((n) => n.kind)).toEqual(["message", "skill.loaded", "action"]);
|
|
341
|
+
const skillNode = tree.nodes[1] as ExecutionSkillNode;
|
|
342
|
+
expect(skillNode.skill).toBe("pdf-processing");
|
|
343
|
+
expect(skillNode.callId).toBe("toolu_skill");
|
|
344
|
+
expect(skillNode.span).toEqual(skillSpan);
|
|
345
|
+
const actionNode = tree.nodes[2] as ExecutionActionNode;
|
|
346
|
+
expect(actionNode.span?.attributes).toMatchObject({ call_id: "toolu_bash", "io.tool": "Bash" });
|
|
347
|
+
expect(tree.timingAvailable).toBe(true);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("appends telemetry-only nodes after the full skeleton, sorted by span.startMs regardless of arrival order", () => {
|
|
351
|
+
const events: StreamEvent[] = [{ type: "message", role: "user", text: "hi" }];
|
|
352
|
+
const late = span({ spanId: "late", startMs: 500 });
|
|
353
|
+
const early = span({ spanId: "early", startMs: 10 });
|
|
354
|
+
|
|
355
|
+
const tree = buildExecutionTree(events, [late, early]);
|
|
356
|
+
|
|
357
|
+
expect(tree.nodes.map((n) => n.id)).toEqual(["message-0", "telemetry-early", "telemetry-late"]);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("timingAvailable is true whenever spans were supplied, even if none of them correlate", () => {
|
|
361
|
+
const events: StreamEvent[] = [{ type: "message", role: "user", text: "hi" }];
|
|
362
|
+
const tree = buildExecutionTree(events, [span({ spanId: "sp1" })]);
|
|
363
|
+
expect(tree.timingAvailable).toBe(true);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("returns an empty tree for empty input", () => {
|
|
367
|
+
const tree = buildExecutionTree([], []);
|
|
368
|
+
expect(tree).toEqual({ nodes: [], timingAvailable: false });
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("skeleton invariant: with every event kind present, partially-correlating spans change ONLY the `span` field on the nodes they correlate to — node count, order, kind, and every other field are byte-identical to the no-spans run", () => {
|
|
372
|
+
const events: StreamEvent[] = [
|
|
373
|
+
{ type: "message", role: "user", text: "do the thing" },
|
|
374
|
+
{ type: "thinking", text: "let me think" },
|
|
375
|
+
{ type: "skill.loaded", skill: "pdf-processing", callId: "skillcall" },
|
|
376
|
+
{ type: "action.called", callId: "c1", name: "Bash", input: { cmd: "ls" }, tool: "shell" },
|
|
377
|
+
{ type: "action.result", callId: "c1", output: "a.ts\n", status: "completed" },
|
|
378
|
+
{ type: "subagent.called", callId: "s1", name: "Task" },
|
|
379
|
+
{ type: "subagent.completed", callId: "s1", output: "done", status: "completed" },
|
|
380
|
+
{ type: "input.requested", request: { prompt: "approve?" } },
|
|
381
|
+
{ type: "compaction", reason: "context full" },
|
|
382
|
+
{ type: "error", message: "boom" },
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
// Correlates to only 2 of the 3 callId-bearing nodes (action c1, skill skillcall);
|
|
386
|
+
// the subagent s1 deliberately gets no span, so this run mixes "correlated" and
|
|
387
|
+
// "uncorrelated-despite-spans-being-available" nodes in the same tree.
|
|
388
|
+
const actionSpan = span({ spanId: "sp-action", startMs: 10, endMs: 20, attributes: { call_id: "c1" } });
|
|
389
|
+
const skillSpan = span({ spanId: "sp-skill", startMs: 30, endMs: 40, attributes: { call_id: "skillcall" } });
|
|
390
|
+
|
|
391
|
+
const withoutSpans = buildExecutionTree(events, []);
|
|
392
|
+
const withSpans = buildExecutionTree(events, [actionSpan, skillSpan]);
|
|
393
|
+
|
|
394
|
+
expect(withoutSpans.timingAvailable).toBe(false);
|
|
395
|
+
expect(withSpans.timingAvailable).toBe(true);
|
|
396
|
+
// No telemetry-only nodes appended in either run — both supplied spans correlate.
|
|
397
|
+
expect(withoutSpans.nodes).toHaveLength(8);
|
|
398
|
+
expect(withSpans.nodes).toHaveLength(8);
|
|
399
|
+
|
|
400
|
+
const stripSpan = (n: (typeof withoutSpans.nodes)[number]): unknown => {
|
|
401
|
+
const { span: _span, ...rest } = n as unknown as { span?: unknown } & Record<string, unknown>;
|
|
402
|
+
return rest;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
for (let i = 0; i < withoutSpans.nodes.length; i++) {
|
|
406
|
+
const bare = withoutSpans.nodes[i];
|
|
407
|
+
const enriched = withSpans.nodes[i];
|
|
408
|
+
// Every field except `span` is identical — same kind, same order, same content.
|
|
409
|
+
expect(stripSpan(enriched)).toEqual(stripSpan(bare));
|
|
410
|
+
// The no-spans run never has a span, on any node.
|
|
411
|
+
expect(bare.span).toBeUndefined();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// The two nodes that DO explicitly correlate gain timing in the enriched run...
|
|
415
|
+
expect((withSpans.nodes[3] as ExecutionActionNode).span?.startMs).toBe(10);
|
|
416
|
+
expect((withSpans.nodes[2] as ExecutionSkillNode).span?.startMs).toBe(30);
|
|
417
|
+
// ...while every other node — including the subagent, which had a callId but no
|
|
418
|
+
// matching span — stays exactly as timing-unavailable as the no-spans run.
|
|
419
|
+
// (nodes: 0 message, 1 thinking, 2 skill*, 3 action*, 4 subagent, 5 input.requested,
|
|
420
|
+
// 6 compaction, 7 error — * = correlated in this run, asserted separately above.)
|
|
421
|
+
for (const i of [0, 1, 4, 5, 6, 7]) {
|
|
422
|
+
expect(withSpans.nodes[i].span).toBeUndefined();
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("correlation honesty: a span that superficially looks like it belongs to a node — same name as the tool, time window plausibly overlapping — but carries no call_id/gen_ai.tool.call.id attribute must NOT be guessed onto that node; it lands as telemetry-only and the node's span stays absent", () => {
|
|
427
|
+
const events: StreamEvent[] = [
|
|
428
|
+
{ type: "action.called", callId: "c1", name: "Bash", input: { cmd: "ls" }, tool: "shell" },
|
|
429
|
+
{ type: "action.result", callId: "c1", output: "a.ts\n", status: "completed" },
|
|
430
|
+
];
|
|
431
|
+
// Deliberately deceptive: span.name matches the tool name exactly, and the time
|
|
432
|
+
// window is exactly where you'd expect the tool call to have run — the only thing
|
|
433
|
+
// missing is an explicit correlation id. A name/time-proximity heuristic would wrongly
|
|
434
|
+
// attach this; explicit-correlation-only must not.
|
|
435
|
+
const imposter = span({
|
|
436
|
+
spanId: "sp-imposter",
|
|
437
|
+
name: "Bash",
|
|
438
|
+
startMs: 0,
|
|
439
|
+
endMs: 5,
|
|
440
|
+
attributes: { "tool.name": "Bash", "tool.input": JSON.stringify({ cmd: "ls" }) },
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const tree = buildExecutionTree(events, [imposter]);
|
|
444
|
+
|
|
445
|
+
expect(tree.timingAvailable).toBe(true);
|
|
446
|
+
const action = tree.nodes.find((n) => n.kind === "action") as ExecutionActionNode;
|
|
447
|
+
expect(action.span).toBeUndefined();
|
|
448
|
+
const telemetry = tree.nodes.filter((n) => n.kind === "telemetry") as ExecutionTelemetryNode[];
|
|
449
|
+
expect(telemetry).toHaveLength(1);
|
|
450
|
+
expect(telemetry[0].span).toEqual(imposter);
|
|
451
|
+
});
|
|
452
|
+
});
|