@tangle-network/agent-eval 0.137.0 → 0.139.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/CHANGELOG.md +78 -0
- package/README.md +34 -0
- package/dist/analyst/index.d.ts +485 -104
- package/dist/analyst/index.d.ts.map +1 -1
- package/dist/analyst/index.js +10 -607
- package/dist/analyst/index.js.map +1 -1
- package/dist/{benchmark-YDrpumqB.js → benchmark-CYtcIF2V.js} +299 -159
- package/dist/benchmark-CYtcIF2V.js.map +1 -0
- package/dist/{benchmark-CHX4orG7.d.ts → benchmark-DDVdWcwA.d.ts} +67 -15
- package/dist/benchmark-DDVdWcwA.d.ts.map +1 -0
- package/dist/benchmark-command-BKfjOBJ5.js +4537 -0
- package/dist/benchmark-command-BKfjOBJ5.js.map +1 -0
- package/dist/benchmarks/index.d.ts +1 -1
- package/dist/benchmarks/index.js +1 -1
- package/dist/{benchmarks-DCLkQOmc.js → benchmarks-zxhy1QV3.js} +4 -3
- package/dist/{benchmarks-DCLkQOmc.js.map → benchmarks-zxhy1QV3.js.map} +1 -1
- package/dist/campaign/index.d.ts +5 -5
- package/dist/campaign/index.js +4 -3
- package/dist/{campaign-lgObcHFC.js → campaign-DrS6_hLd.js} +19 -11
- package/dist/campaign-DrS6_hLd.js.map +1 -0
- package/dist/canonical-D011XM8r.js +86 -0
- package/dist/canonical-D011XM8r.js.map +1 -0
- package/dist/cli.js +10 -3
- package/dist/cli.js.map +1 -1
- package/dist/{client-C8L6h6Wf.d.ts → client-BohnDFBq.d.ts} +4 -4
- package/dist/{client-C8L6h6Wf.d.ts.map → client-BohnDFBq.d.ts.map} +1 -1
- package/dist/{completion-verifier-DSyRNVzU.d.ts → completion-verifier-IPoP4fQO.d.ts} +178 -4
- package/dist/completion-verifier-IPoP4fQO.d.ts.map +1 -0
- package/dist/contract/index.d.ts +10 -10
- package/dist/contract/index.js +9 -8
- package/dist/contract/index.js.map +1 -1
- package/dist/control.d.ts +2 -2
- package/dist/{cost-ledger-D-5_-dhi.js → cost-ledger-CZ9diLxY.js} +90 -45
- package/dist/cost-ledger-CZ9diLxY.js.map +1 -0
- package/dist/{cost-ledger-D2o6JOrL.d.ts → cost-ledger-DKgyIWRj.d.ts} +6 -2
- package/dist/cost-ledger-DKgyIWRj.d.ts.map +1 -0
- package/dist/default-registry-B8vf7Rmf.d.ts +118 -0
- package/dist/default-registry-B8vf7Rmf.d.ts.map +1 -0
- package/dist/default-registry-BgJJItGr.js +2364 -0
- package/dist/default-registry-BgJJItGr.js.map +1 -0
- package/dist/dspy-rlm-engine-DTkVyDX-.js +344 -0
- package/dist/dspy-rlm-engine-DTkVyDX-.js.map +1 -0
- package/dist/{eval-campaign-CHqfLnff.js → eval-campaign-BmptJj50.js} +2 -2
- package/dist/{eval-campaign-CHqfLnff.js.map → eval-campaign-BmptJj50.js.map} +1 -1
- package/dist/exact-types-MaaFcllV.d.ts +234 -0
- package/dist/exact-types-MaaFcllV.d.ts.map +1 -0
- package/dist/external-optimizer-contracts-BrxY2Sli.d.ts +32 -0
- package/dist/external-optimizer-contracts-BrxY2Sli.d.ts.map +1 -0
- package/dist/{extract-usage-p-56bh8q.js → extract-usage-DZs601Va.js} +2 -2
- package/dist/{extract-usage-p-56bh8q.js.map → extract-usage-DZs601Va.js.map} +1 -1
- package/dist/{feedback-trajectory-N_F0PwHz.d.ts → feedback-trajectory-BJUWOkJM.d.ts} +3 -2
- package/dist/feedback-trajectory-BJUWOkJM.d.ts.map +1 -0
- package/dist/fuzz.d.ts +1 -1
- package/dist/fuzz.js +1 -1
- package/dist/{hf-dataset-DBJXXoY1.js → hf-dataset-XggBupCr.js} +2 -2
- package/dist/{hf-dataset-DBJXXoY1.js.map → hf-dataset-XggBupCr.js.map} +1 -1
- package/dist/hosted/index.d.ts +3 -3
- package/dist/{index-C-Pr4OWg.d.ts → index-BTm_P9aC.d.ts} +12 -11
- package/dist/index-BTm_P9aC.d.ts.map +1 -0
- package/dist/{index-U3RHOShi.d.ts → index-CWOPCJiw.d.ts} +2 -2
- package/dist/{index-U3RHOShi.d.ts.map → index-CWOPCJiw.d.ts.map} +1 -1
- package/dist/{index-DRNl6g_N.d.ts → index-CtR1xh4V.d.ts} +3 -3
- package/dist/{index-DRNl6g_N.d.ts.map → index-CtR1xh4V.d.ts.map} +1 -1
- package/dist/index-DEb46kc6.d.ts.map +1 -1
- package/dist/{index-BnP1QJUv.d.ts → index-_66rVpwN.d.ts} +5 -5
- package/dist/{index-BnP1QJUv.d.ts.map → index-_66rVpwN.d.ts.map} +1 -1
- package/dist/index.d.ts +35 -55
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +55 -514
- package/dist/index.js.map +1 -1
- package/dist/{insight-report-B9ooYH_g.d.ts → insight-report-Bu5Wi9tG.d.ts} +4 -4
- package/dist/{insight-report-B9ooYH_g.d.ts.map → insight-report-Bu5Wi9tG.d.ts.map} +1 -1
- package/dist/{integrity-CKxosZ5Z.d.ts → integrity-COTh3DTH.d.ts} +2 -2
- package/dist/{integrity-CKxosZ5Z.d.ts.map → integrity-COTh3DTH.d.ts.map} +1 -1
- package/dist/kind-factory-CFxA0JQX.js +2133 -0
- package/dist/kind-factory-CFxA0JQX.js.map +1 -0
- package/dist/ledger-core/index.js +2 -1
- package/dist/{ledger-core-t6sItivm.js → ledger-core-Dxz0Rkwa.js} +210 -99
- package/dist/ledger-core-Dxz0Rkwa.js.map +1 -0
- package/dist/{llm-client-DKB25jV8.js → llm-client-bkztEfIx.js} +5 -5
- package/dist/llm-client-bkztEfIx.js.map +1 -0
- package/dist/meta-eval/index.d.ts +2 -2
- package/dist/multishot/index.d.ts +2 -2
- package/dist/openapi.json +1 -1
- package/dist/{proposal-findings-DCawte-y.js → proposal-findings-2GIUo1et.js} +2 -68
- package/dist/proposal-findings-2GIUo1et.js.map +1 -0
- package/dist/{release-report-CofgVNZt.d.ts → release-report-fZarvIm-.d.ts} +3 -3
- package/dist/{release-report-CofgVNZt.d.ts.map → release-report-fZarvIm-.d.ts.map} +1 -1
- package/dist/{replay-K8FaC0CB.d.ts → replay-DjG4IG60.d.ts} +34 -143
- package/dist/replay-DjG4IG60.d.ts.map +1 -0
- package/dist/{replay-Bju0T8Ls.js → replay-SA4OB7O7.js} +48 -136
- package/dist/replay-SA4OB7O7.js.map +1 -0
- package/dist/reporting.d.ts +4 -4
- package/dist/{researcher-Da0Wj-bt.d.ts → researcher-BxhtGfKa.d.ts} +5 -5
- package/dist/{researcher-Da0Wj-bt.d.ts.map → researcher-BxhtGfKa.d.ts.map} +1 -1
- package/dist/{reward-hacking-CQ3hTCO3.d.ts → reward-hacking-CqSLiV51.d.ts} +2 -2
- package/dist/{reward-hacking-CQ3hTCO3.d.ts.map → reward-hacking-CqSLiV51.d.ts.map} +1 -1
- package/dist/rl.d.ts +5 -5
- package/dist/rl.js +1 -1
- package/dist/rollout/index.d.ts +1 -1
- package/dist/rollout/index.js +2 -2
- package/dist/{rollout-DQFl0UXA.js → rollout-8nj3mYvx.js} +2 -2
- package/dist/{rollout-DQFl0UXA.js.map → rollout-8nj3mYvx.js.map} +1 -1
- package/dist/{rubric-predictive-validity-C4sztLR3.d.ts → rubric-predictive-validity-DQBQj6uV.d.ts} +2 -2
- package/dist/{rubric-predictive-validity-C4sztLR3.d.ts.map → rubric-predictive-validity-DQBQj6uV.d.ts.map} +1 -1
- package/dist/{run-evidence-BDIircdA.d.ts → run-evidence-C4RcRQT5.d.ts} +3 -3
- package/dist/{run-evidence-BDIircdA.d.ts.map → run-evidence-C4RcRQT5.d.ts.map} +1 -1
- package/dist/{run-record-BPCa2rQ8.d.ts → run-record-CztDMXVF.d.ts} +2 -2
- package/dist/{run-record-BPCa2rQ8.d.ts.map → run-record-CztDMXVF.d.ts.map} +1 -1
- package/dist/{semantic-concept-judge-Bz64IckK.js → semantic-concept-judge-BuIJ9IfB.js} +49 -6
- package/dist/semantic-concept-judge-BuIJ9IfB.js.map +1 -0
- package/dist/{server-KjXZZUDX.js → server-DaCpLfi0.js} +3 -3
- package/dist/{server-KjXZZUDX.js.map → server-DaCpLfi0.js.map} +1 -1
- package/dist/single-run-lock-BTTtPZ9N.js +989 -0
- package/dist/single-run-lock-BTTtPZ9N.js.map +1 -0
- package/dist/{skill-usage-CFDLLlhF.d.ts → skill-usage-B-BFS8M2.d.ts} +65 -40
- package/dist/skill-usage-B-BFS8M2.d.ts.map +1 -0
- package/dist/{skillopt-optimization-method-f4o9sUT4.js → skillopt-optimization-method-BbGnCC53.js} +20 -979
- package/dist/skillopt-optimization-method-BbGnCC53.js.map +1 -0
- package/dist/{skillopt-optimization-method-BpbnlvAZ.d.ts → skillopt-optimization-method-_s0Tub7Y.d.ts} +11 -39
- package/dist/skillopt-optimization-method-_s0Tub7Y.d.ts.map +1 -0
- package/dist/{statistics-_7P642CN.d.ts → statistics-B5d0Zd-z.d.ts} +2 -2
- package/dist/{statistics-_7P642CN.d.ts.map → statistics-B5d0Zd-z.d.ts.map} +1 -1
- package/dist/store-otlp-DX4fGIcf.js +757 -0
- package/dist/store-otlp-DX4fGIcf.js.map +1 -0
- package/dist/{summary-report-DHipz9Kx.d.ts → summary-report-Cg7BifAM.d.ts} +3 -3
- package/dist/{summary-report-DHipz9Kx.d.ts.map → summary-report-Cg7BifAM.d.ts.map} +1 -1
- package/dist/tool-groups-CdYq22lX.d.ts +258 -0
- package/dist/tool-groups-CdYq22lX.d.ts.map +1 -0
- package/dist/traces.d.ts +7 -6
- package/dist/traces.js +5 -4
- package/dist/{types-CTGbIm57.d.ts → types-BBFNHxSK.d.ts} +5 -5
- package/dist/{types-CTGbIm57.d.ts.map → types-BBFNHxSK.d.ts.map} +1 -1
- package/dist/{types-CKswbJGO.d.ts → types-DoEYskCd.d.ts} +5 -5
- package/dist/{types-CKswbJGO.d.ts.map → types-DoEYskCd.d.ts.map} +1 -1
- package/dist/{types-CTvKfr5F.d.ts → types-uPrS6mD-.d.ts} +2 -2
- package/dist/{types-CTvKfr5F.d.ts.map → types-uPrS6mD-.d.ts.map} +1 -1
- package/dist/usage-receipt-CgxMEBZq.js +134 -0
- package/dist/usage-receipt-CgxMEBZq.js.map +1 -0
- package/dist/wire/index.d.ts +3 -3
- package/dist/wire/index.js +1 -1
- package/docs/trace-analysis.md +191 -385
- package/package.json +5 -4
- package/dist/analyze-runs-PVtnfjvA.d.ts +0 -72
- package/dist/analyze-runs-PVtnfjvA.d.ts.map +0 -1
- package/dist/benchmark-CHX4orG7.d.ts.map +0 -1
- package/dist/benchmark-YDrpumqB.js.map +0 -1
- package/dist/campaign-lgObcHFC.js.map +0 -1
- package/dist/completion-verifier-DSyRNVzU.d.ts.map +0 -1
- package/dist/concurrency-MUjT7VjM.js +0 -109
- package/dist/concurrency-MUjT7VjM.js.map +0 -1
- package/dist/cost-ledger-D-5_-dhi.js.map +0 -1
- package/dist/cost-ledger-D2o6JOrL.d.ts.map +0 -1
- package/dist/default-registry-CLXbRt0f.js +0 -2594
- package/dist/default-registry-CLXbRt0f.js.map +0 -1
- package/dist/default-registry-Dc5D_Loc.d.ts +0 -202
- package/dist/default-registry-Dc5D_Loc.d.ts.map +0 -1
- package/dist/feedback-trajectory-N_F0PwHz.d.ts.map +0 -1
- package/dist/index-C-Pr4OWg.d.ts.map +0 -1
- package/dist/ledger-core-t6sItivm.js.map +0 -1
- package/dist/llm-client-DKB25jV8.js.map +0 -1
- package/dist/proposal-findings-DCawte-y.js.map +0 -1
- package/dist/registry-BdM7SuTr.d.ts +0 -124
- package/dist/registry-BdM7SuTr.d.ts.map +0 -1
- package/dist/replay-Bju0T8Ls.js.map +0 -1
- package/dist/replay-K8FaC0CB.d.ts.map +0 -1
- package/dist/semantic-concept-judge-Bz64IckK.js.map +0 -1
- package/dist/skill-usage-CFDLLlhF.d.ts.map +0 -1
- package/dist/skillopt-optimization-method-BpbnlvAZ.d.ts.map +0 -1
- package/dist/skillopt-optimization-method-f4o9sUT4.js.map +0 -1
- package/dist/tools-DZk2Jn64.js +0 -1876
- package/dist/tools-DZk2Jn64.js.map +0 -1
package/dist/tools-DZk2Jn64.js
DELETED
|
@@ -1,1876 +0,0 @@
|
|
|
1
|
-
import { a as LimitExceededError, c as ValidationError, n as CaptureIntegrityError, o as NotFoundError, t as AgentEvalError } from "./errors-D-LKuDhb.js";
|
|
2
|
-
import { INPUT_VALUE, LLM_MODEL_ATTR_KEYS, OUTPUT_VALUE, SPAN_KIND_ATTR_KEYS, TOOL_ARGS_CAPTURED, TOOL_LATENCY_MS, TOOL_NAME, TOOL_NAME_ATTR_KEYS } from "./trace-attributes.js";
|
|
3
|
-
import { AxJSRuntime, agent } from "@ax-llm/ax";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { readFile, stat } from "node:fs/promises";
|
|
6
|
-
import { RE2JS } from "re2js";
|
|
7
|
-
//#region src/trace/otlp-attributes.ts
|
|
8
|
-
/** Canonical OpenInference-over-OTLP attribute vocabulary used at the trace boundary. */
|
|
9
|
-
const TOOL_SPAN_ATTRIBUTE_KEYS = [
|
|
10
|
-
TOOL_NAME,
|
|
11
|
-
TOOL_ARGS_CAPTURED,
|
|
12
|
-
TOOL_LATENCY_MS,
|
|
13
|
-
INPUT_VALUE,
|
|
14
|
-
OUTPUT_VALUE
|
|
15
|
-
];
|
|
16
|
-
const EXPLICIT_SPAN_ROLES = /* @__PURE__ */ new Set([
|
|
17
|
-
"AGENT",
|
|
18
|
-
"CHAIN",
|
|
19
|
-
"EVALUATOR",
|
|
20
|
-
"GUARDRAIL",
|
|
21
|
-
"LLM",
|
|
22
|
-
"SPAN",
|
|
23
|
-
"TOOL"
|
|
24
|
-
]);
|
|
25
|
-
/**
|
|
26
|
-
* Classify a span once for both measurement and error accounting.
|
|
27
|
-
* An explicit OpenInference kind wins; untyped spans use the same tool and
|
|
28
|
-
* model signals in online and offline intake.
|
|
29
|
-
*/
|
|
30
|
-
function classifyOtlpSpanRole(input) {
|
|
31
|
-
const explicitKind = input.kind ?? firstStringAttribute(input.attributes, SPAN_KIND_ATTR_KEYS);
|
|
32
|
-
if (explicitKind) {
|
|
33
|
-
const normalized = explicitKind.toUpperCase();
|
|
34
|
-
if (EXPLICIT_SPAN_ROLES.has(normalized)) return normalized;
|
|
35
|
-
}
|
|
36
|
-
if (firstStringAttribute(input.attributes, TOOL_NAME_ATTR_KEYS) !== void 0 || /^(?:function|tool)[.:/]/i.test(input.name)) return "TOOL";
|
|
37
|
-
const spanType = input.attributes["span.type"];
|
|
38
|
-
if (typeof spanType === "string" && spanType.toLowerCase() === "llm_request" || /(?:^|[.:/_-])(?:chat[._-]?completions?|llm)(?:$|[.:/_-])/i.test(input.name) || firstStringAttribute(input.attributes, LLM_MODEL_ATTR_KEYS) !== void 0 || typeof input.attributes["gen_ai.operation.name"] === "string") return "LLM";
|
|
39
|
-
return "UNKNOWN";
|
|
40
|
-
}
|
|
41
|
-
function isOtlpModelCall(input) {
|
|
42
|
-
return classifyOtlpSpanRole(input) === "LLM";
|
|
43
|
-
}
|
|
44
|
-
function toolSpanOtlpAttributes(span) {
|
|
45
|
-
const argsCaptured = span.argsCaptured !== false;
|
|
46
|
-
const attributes = {
|
|
47
|
-
[TOOL_NAME]: span.toolName,
|
|
48
|
-
[TOOL_ARGS_CAPTURED]: argsCaptured
|
|
49
|
-
};
|
|
50
|
-
if (span.latencyMs !== void 0) attributes[TOOL_LATENCY_MS] = span.latencyMs;
|
|
51
|
-
if (argsCaptured) attributes[INPUT_VALUE] = stringifyTraceValue(span.args);
|
|
52
|
-
if (span.result !== void 0) attributes[OUTPUT_VALUE] = stringifyTraceValue(span.result);
|
|
53
|
-
return attributes;
|
|
54
|
-
}
|
|
55
|
-
function applyToolSpanOtlpAttributes(attributes, span) {
|
|
56
|
-
for (const key of TOOL_SPAN_ATTRIBUTE_KEYS) delete attributes[key];
|
|
57
|
-
Object.assign(attributes, toolSpanOtlpAttributes(span));
|
|
58
|
-
}
|
|
59
|
-
function traceSpanKindToOpenInferenceKind(kind) {
|
|
60
|
-
switch (kind) {
|
|
61
|
-
case "llm": return "LLM";
|
|
62
|
-
case "tool": return "TOOL";
|
|
63
|
-
case "retrieval": return "CHAIN";
|
|
64
|
-
case "judge": return "EVALUATOR";
|
|
65
|
-
case "sandbox": return "CHAIN";
|
|
66
|
-
case "agent": return "AGENT";
|
|
67
|
-
default: return "SPAN";
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
function firstStringAttribute(attributes, keys) {
|
|
71
|
-
for (const key of keys) {
|
|
72
|
-
const value = attributes[key];
|
|
73
|
-
if (typeof value === "string" && value.length > 0) return value;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
function stringifyTraceValue(value) {
|
|
77
|
-
if (value === void 0) return "null";
|
|
78
|
-
if (typeof value === "string") return value;
|
|
79
|
-
try {
|
|
80
|
-
return JSON.stringify(value) ?? String(value);
|
|
81
|
-
} catch {
|
|
82
|
-
return String(value);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
//#endregion
|
|
86
|
-
//#region src/trace-analyst/otlp-span.ts
|
|
87
|
-
/**
|
|
88
|
-
* Canonical OTLP-flat-line readers shared by every consumer of the
|
|
89
|
-
* OTLP-JSONL wire shape (one OTLP span per line; the form
|
|
90
|
-
* `flattenOtlpExportToNdjson` produces and the form AppWorld / HALO
|
|
91
|
-
* emit via their OpenInference OTLP exporter).
|
|
92
|
-
*
|
|
93
|
-
* `OtlpFileTraceStore` indexes spans with these; `otlpToRunRecords`
|
|
94
|
-
* aggregates spans into `RunRecord`s with the same readers. One parser,
|
|
95
|
-
* one vocabulary — a divergence between the analyst's view of a trace and
|
|
96
|
-
* the RunRecord projected from it is a class of bug this consolidation
|
|
97
|
-
* removes by construction.
|
|
98
|
-
*
|
|
99
|
-
* Vocabulary. The readers understand BOTH dialects that appear in the
|
|
100
|
-
* wild:
|
|
101
|
-
* - the substrate's own `llm.*` / `tool.*` / `span.kind` attributes
|
|
102
|
-
* (`flattenSpanAttributes` in `trace/otel.ts`), and
|
|
103
|
-
* - the OpenInference / inference-export attributes AppWorld / HALO
|
|
104
|
-
* emit (`openinference.span.kind`, `inference.observation_kind`,
|
|
105
|
-
* `inference.llm.input_tokens`, `llm.token_count.prompt`, …).
|
|
106
|
-
*
|
|
107
|
-
* Pure, no I/O.
|
|
108
|
-
*/
|
|
109
|
-
/**
|
|
110
|
-
* Project one parsed OTLP-JSONL object to `ProjectedOtlpSpan`, or `null`
|
|
111
|
-
* when the line is missing the mandatory `trace_id` + `span_id`.
|
|
112
|
-
*/
|
|
113
|
-
function projectOtlpFlatLine(raw) {
|
|
114
|
-
const trace_id = stringField(raw, "trace_id") ?? stringField(raw, "traceId");
|
|
115
|
-
const span_id = stringField(raw, "span_id") ?? stringField(raw, "spanId");
|
|
116
|
-
if (!trace_id || !span_id) return null;
|
|
117
|
-
const parent_id = normalizeParentSpanId(trace_id, span_id, stringField(raw, "parent_span_id") ?? stringField(raw, "parentSpanId") ?? null);
|
|
118
|
-
const name = stringField(raw, "name") ?? "unknown";
|
|
119
|
-
const start_time = stringField(raw, "start_time") ?? stringField(raw, "startTime") ?? "";
|
|
120
|
-
const end_time = stringField(raw, "end_time") ?? stringField(raw, "endTime") ?? start_time;
|
|
121
|
-
const status = readOtlpStatus(raw);
|
|
122
|
-
const attributes = extractOtlpAttributes(raw);
|
|
123
|
-
const service_name = asString(attributes["service.name"]) ?? asString(attributes["resource.attributes.service.name"]) ?? null;
|
|
124
|
-
const agent_name = asString(attributes["agent.name"]) ?? asString(attributes["inference.agent.name"]) ?? asString(attributes["inference.agent_name"]) ?? null;
|
|
125
|
-
const model_name = firstStringAttr(attributes, LLM_MODEL_ATTR_KEYS);
|
|
126
|
-
const tool_name = firstStringAttr(attributes, TOOL_NAME_ATTR_KEYS);
|
|
127
|
-
const kind = inferOtlpKind(attributes);
|
|
128
|
-
let duration_ms = 0;
|
|
129
|
-
if (start_time && end_time) {
|
|
130
|
-
const a = spanEpochMillis(start_time);
|
|
131
|
-
const b = spanEpochMillis(end_time);
|
|
132
|
-
if (a !== null && b !== null) duration_ms = Math.max(0, b - a);
|
|
133
|
-
}
|
|
134
|
-
return {
|
|
135
|
-
trace_id,
|
|
136
|
-
span_id,
|
|
137
|
-
parent_span_id: parent_id && parent_id.length > 0 ? parent_id : null,
|
|
138
|
-
name,
|
|
139
|
-
kind,
|
|
140
|
-
start_time,
|
|
141
|
-
end_time,
|
|
142
|
-
duration_ms,
|
|
143
|
-
status: status.code,
|
|
144
|
-
status_message: status.message,
|
|
145
|
-
service_name,
|
|
146
|
-
agent_name,
|
|
147
|
-
model_name,
|
|
148
|
-
tool_name,
|
|
149
|
-
attributes
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
function normalizeParentSpanId(traceId, spanId, parentId) {
|
|
153
|
-
if (!parentId) return null;
|
|
154
|
-
const prefix = `${traceId}:`;
|
|
155
|
-
return spanId.startsWith(prefix) && !parentId.startsWith(prefix) ? `${prefix}${parentId}` : parentId;
|
|
156
|
-
}
|
|
157
|
-
function readOtlpStatus(raw) {
|
|
158
|
-
const status = raw.status;
|
|
159
|
-
if (status && typeof status === "object" && !Array.isArray(status)) {
|
|
160
|
-
const codeRaw = status.code;
|
|
161
|
-
const code = codeRaw === "STATUS_CODE_OK" || codeRaw === "OK" ? "OK" : codeRaw === "STATUS_CODE_ERROR" || codeRaw === "ERROR" ? "ERROR" : "UNSET";
|
|
162
|
-
const messageRaw = status.message;
|
|
163
|
-
return {
|
|
164
|
-
code,
|
|
165
|
-
message: typeof messageRaw === "string" && messageRaw.length > 0 ? messageRaw : void 0
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
return {
|
|
169
|
-
code: "UNSET",
|
|
170
|
-
message: void 0
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
function inferOtlpKind(attrs) {
|
|
174
|
-
const opik = firstStringAttr(attrs, SPAN_KIND_ATTR_KEYS);
|
|
175
|
-
if (opik) {
|
|
176
|
-
const upper = opik.toUpperCase();
|
|
177
|
-
if (upper === "AGENT" || upper === "LLM" || upper === "TOOL" || upper === "CHAIN" || upper === "EVALUATOR" || upper === "GUARDRAIL" || upper === "SPAN") return upper;
|
|
178
|
-
}
|
|
179
|
-
return "UNKNOWN";
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Flatten OTLP `attributes` + `resource.attributes` into a single
|
|
183
|
-
* dotted-key map. Span attributes override resource attributes when keys
|
|
184
|
-
* overlap. Nested objects/arrays are preserved as-is.
|
|
185
|
-
*/
|
|
186
|
-
function extractOtlpAttributes(raw) {
|
|
187
|
-
const out = {};
|
|
188
|
-
const resource = raw.resource;
|
|
189
|
-
if (resource && typeof resource === "object" && !Array.isArray(resource)) {
|
|
190
|
-
const ra = resource.attributes;
|
|
191
|
-
if (ra && typeof ra === "object" && !Array.isArray(ra)) for (const [k, v] of Object.entries(ra)) out[k] = v;
|
|
192
|
-
}
|
|
193
|
-
const spanAttrs = raw.attributes;
|
|
194
|
-
if (spanAttrs && typeof spanAttrs === "object" && !Array.isArray(spanAttrs)) for (const [k, v] of Object.entries(spanAttrs)) out[k] = v;
|
|
195
|
-
return out;
|
|
196
|
-
}
|
|
197
|
-
function stringField(raw, key) {
|
|
198
|
-
const v = raw[key];
|
|
199
|
-
return typeof v === "string" ? v : void 0;
|
|
200
|
-
}
|
|
201
|
-
function asString(v) {
|
|
202
|
-
return typeof v === "string" && v.length > 0 ? v : null;
|
|
203
|
-
}
|
|
204
|
-
/** First non-empty string value across a list of candidate attribute keys. */
|
|
205
|
-
function firstStringAttr(attrs, keys) {
|
|
206
|
-
for (const k of keys) {
|
|
207
|
-
const s = asString(attrs[k]);
|
|
208
|
-
if (s !== null) return s;
|
|
209
|
-
}
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Parse a span timestamp to epoch millis, or null when empty/unparseable. The
|
|
214
|
-
* OTLP readers accept BOTH ISO-8601 and epoch-millis-string dialects, so raw
|
|
215
|
-
* string comparison (`<`, `localeCompare`) mis-orders across dialects and
|
|
216
|
-
* `Date.parse` returns NaN for a bare epoch-millis string.
|
|
217
|
-
*/
|
|
218
|
-
function spanEpochMillis(ts) {
|
|
219
|
-
if (!ts) return null;
|
|
220
|
-
if (/^\d+$/.test(ts)) return Number(ts);
|
|
221
|
-
const n = Date.parse(ts);
|
|
222
|
-
return Number.isNaN(n) ? null : n;
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Order comparator for span timestamps across mixed ISO/epoch dialects.
|
|
226
|
-
* Unparseable timestamps sort as epoch 0 (earliest), never NaN (which would
|
|
227
|
-
* make the sort non-deterministic).
|
|
228
|
-
*/
|
|
229
|
-
function compareSpanTime(a, b) {
|
|
230
|
-
return (spanEpochMillis(a) ?? 0) - (spanEpochMillis(b) ?? 0);
|
|
231
|
-
}
|
|
232
|
-
//#endregion
|
|
233
|
-
//#region src/trace-analyst/errors.ts
|
|
234
|
-
/** Invalid trace-tool arguments, including malformed filters and regexes. */
|
|
235
|
-
var TraceAnalysisValidationError = class extends ValidationError {};
|
|
236
|
-
/** A trace read cannot fit within a documented count or byte limit. */
|
|
237
|
-
var TraceAnalysisLimitError = class extends LimitExceededError {
|
|
238
|
-
operation;
|
|
239
|
-
actual;
|
|
240
|
-
limit;
|
|
241
|
-
constructor(operation, actual, limit, message) {
|
|
242
|
-
super(message ?? `${operation} produced ${actual}, over the limit of ${limit}`);
|
|
243
|
-
this.operation = operation;
|
|
244
|
-
this.actual = actual;
|
|
245
|
-
this.limit = limit;
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
/** A supplied store returned a shape that violates the trace read contract. */
|
|
249
|
-
var TraceAnalysisStoreContractError = class extends AgentEvalError {
|
|
250
|
-
operation;
|
|
251
|
-
constructor(operation, message, options) {
|
|
252
|
-
super("backend_integrity", `${operation}: ${message}`, options);
|
|
253
|
-
this.operation = operation;
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
var TraceFileMissingError = class extends NotFoundError {
|
|
257
|
-
path;
|
|
258
|
-
constructor(path) {
|
|
259
|
-
super(`trace file not found: ${path}`);
|
|
260
|
-
this.path = path;
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
var TraceFileTooLargeError = class extends TraceAnalysisLimitError {
|
|
264
|
-
path;
|
|
265
|
-
size_bytes;
|
|
266
|
-
max_bytes;
|
|
267
|
-
constructor(path, size_bytes, max_bytes) {
|
|
268
|
-
super("OtlpFileTraceStore.readBuffer", size_bytes, max_bytes, `trace file ${path} is ${size_bytes} bytes, over the ${max_bytes}-byte limit; raise OtlpFileTraceStoreOptions.maxFileBytes or pre-split the file`);
|
|
269
|
-
this.path = path;
|
|
270
|
-
this.size_bytes = size_bytes;
|
|
271
|
-
this.max_bytes = max_bytes;
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
var TraceFileMalformedError = class extends CaptureIntegrityError {
|
|
275
|
-
path;
|
|
276
|
-
line_number;
|
|
277
|
-
byte_offset;
|
|
278
|
-
constructor(path, line_number, byte_offset, cause) {
|
|
279
|
-
super(`malformed trace row in ${path} at line ${line_number}, byte ${byte_offset}`, { cause });
|
|
280
|
-
this.path = path;
|
|
281
|
-
this.line_number = line_number;
|
|
282
|
-
this.byte_offset = byte_offset;
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
var TraceNotFoundError = class extends NotFoundError {
|
|
286
|
-
trace_id;
|
|
287
|
-
constructor(trace_id) {
|
|
288
|
-
super(`trace not found: ${trace_id}`);
|
|
289
|
-
this.trace_id = trace_id;
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
var SpanNotFoundError = class extends NotFoundError {
|
|
293
|
-
trace_id;
|
|
294
|
-
span_id;
|
|
295
|
-
constructor(trace_id, span_id) {
|
|
296
|
-
super(`span ${span_id} not found in trace ${trace_id}`);
|
|
297
|
-
this.trace_id = trace_id;
|
|
298
|
-
this.span_id = span_id;
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
//#endregion
|
|
302
|
-
//#region src/trace-analyst/shared-abortable-task.ts
|
|
303
|
-
function createSharedAbortableTask(factory) {
|
|
304
|
-
const controller = new AbortController();
|
|
305
|
-
const task = {
|
|
306
|
-
controller,
|
|
307
|
-
promise: factory(controller.signal),
|
|
308
|
-
consumers: 0,
|
|
309
|
-
settled: false
|
|
310
|
-
};
|
|
311
|
-
task.promise.then(() => {
|
|
312
|
-
task.settled = true;
|
|
313
|
-
}, () => {
|
|
314
|
-
task.settled = true;
|
|
315
|
-
});
|
|
316
|
-
return task;
|
|
317
|
-
}
|
|
318
|
-
function waitForSharedTask(task, signal) {
|
|
319
|
-
signal?.throwIfAborted();
|
|
320
|
-
task.consumers += 1;
|
|
321
|
-
return new Promise((resolve, reject) => {
|
|
322
|
-
let finished = false;
|
|
323
|
-
const finish = (callback) => {
|
|
324
|
-
if (finished) return;
|
|
325
|
-
finished = true;
|
|
326
|
-
signal?.removeEventListener("abort", onAbort);
|
|
327
|
-
task.consumers -= 1;
|
|
328
|
-
callback();
|
|
329
|
-
};
|
|
330
|
-
const onAbort = () => {
|
|
331
|
-
finish(() => {
|
|
332
|
-
reject(signal?.reason);
|
|
333
|
-
if (!task.settled && task.consumers === 0) task.controller.abort(signal?.reason);
|
|
334
|
-
});
|
|
335
|
-
};
|
|
336
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
337
|
-
task.promise.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
338
|
-
if (signal?.aborted) onAbort();
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
//#endregion
|
|
342
|
-
//#region src/trace-analyst/store-contract.ts
|
|
343
|
-
const TRACE_ANALYSIS_LIMITS = {
|
|
344
|
-
sampleTraceIds: 20,
|
|
345
|
-
queryTraces: 200,
|
|
346
|
-
viewSpans: 100,
|
|
347
|
-
searchMatches: 500,
|
|
348
|
-
filterValues: 100,
|
|
349
|
-
identifierCharacters: 256,
|
|
350
|
-
regexCharacters: 4096,
|
|
351
|
-
minimumTextBudget: 64
|
|
352
|
-
};
|
|
353
|
-
//#endregion
|
|
354
|
-
//#region src/trace-analyst/types.ts
|
|
355
|
-
const DEFAULT_TRACE_ANALYST_BUDGETS = {
|
|
356
|
-
perCallByteCeiling: 15e4,
|
|
357
|
-
perAttributeViewBudget: 4096,
|
|
358
|
-
perAttributeSpanBudget: 16384,
|
|
359
|
-
perMatchTextBudget: 1024
|
|
360
|
-
};
|
|
361
|
-
/** Marker substituted in place of truncated string payloads. Callers
|
|
362
|
-
* parsing tool output can detect it deterministically. */
|
|
363
|
-
const TRACE_ANALYST_TRUNCATION_MARKER_PREFIX = "[trace-analyst truncated:";
|
|
364
|
-
//#endregion
|
|
365
|
-
//#region src/trace-analyst/store-bounds.ts
|
|
366
|
-
function resolveTraceBudgets(overrides) {
|
|
367
|
-
const budgets = {
|
|
368
|
-
...DEFAULT_TRACE_ANALYST_BUDGETS,
|
|
369
|
-
...overrides
|
|
370
|
-
};
|
|
371
|
-
validateInteger(budgets.perCallByteCeiling, "perCallByteCeiling", 1);
|
|
372
|
-
for (const name of [
|
|
373
|
-
"perAttributeViewBudget",
|
|
374
|
-
"perAttributeSpanBudget",
|
|
375
|
-
"perMatchTextBudget"
|
|
376
|
-
]) validateInteger(budgets[name], name, TRACE_ANALYSIS_LIMITS.minimumTextBudget);
|
|
377
|
-
return budgets;
|
|
378
|
-
}
|
|
379
|
-
function boundOverview(result, byteCeiling) {
|
|
380
|
-
if (result.sample_trace_ids.length > result.total_traces) throw contractError("getOverview", "sample_trace_ids contains more entries than total_traces");
|
|
381
|
-
if (result.errors.trace_count > result.total_traces) throw contractError("getOverview", "errors.trace_count exceeds total_traces");
|
|
382
|
-
return withinByteCeiling("getOverview", result, byteCeiling);
|
|
383
|
-
}
|
|
384
|
-
function boundTracePage(result, input, byteCeiling) {
|
|
385
|
-
if (result.traces.length > input.limit) throw contractError("queryTraces", `store returned ${result.traces.length} traces for limit ${input.limit}`);
|
|
386
|
-
if (result.total < input.offset + result.traces.length) throw contractError("queryTraces", `total ${result.total} is smaller than the returned page ending at ${input.offset + result.traces.length}`);
|
|
387
|
-
if (result.has_more !== input.offset + result.traces.length < result.total) throw contractError("queryTraces", "has_more does not match the returned page position and total");
|
|
388
|
-
if (result.has_more && result.traces.length === 0) throw contractError("queryTraces", "store reported more traces without returning any progress");
|
|
389
|
-
const traces = [];
|
|
390
|
-
for (const trace of result.traces) {
|
|
391
|
-
if (encodedBytes("queryTraces", {
|
|
392
|
-
traces: [...traces, trace],
|
|
393
|
-
total: result.total,
|
|
394
|
-
has_more: true
|
|
395
|
-
}) > byteCeiling) break;
|
|
396
|
-
traces.push(trace);
|
|
397
|
-
}
|
|
398
|
-
if (result.traces.length > 0 && traces.length === 0) throw responseItemTooLarge("queryTraces", {
|
|
399
|
-
traces: [result.traces[0]],
|
|
400
|
-
total: result.total,
|
|
401
|
-
has_more: true
|
|
402
|
-
}, byteCeiling);
|
|
403
|
-
return withinByteCeiling("queryTraces", {
|
|
404
|
-
traces,
|
|
405
|
-
total: result.total,
|
|
406
|
-
has_more: result.has_more || traces.length < result.traces.length
|
|
407
|
-
}, byteCeiling);
|
|
408
|
-
}
|
|
409
|
-
function boundTraceView(result, expectedTraceId, perAttributeCap, byteCeiling) {
|
|
410
|
-
requireEqual("viewTrace", "trace_id", result.trace_id, expectedTraceId);
|
|
411
|
-
if (result.oversized) return withinByteCeiling("viewTrace", result, byteCeiling);
|
|
412
|
-
const spans = result.spans.map((span) => {
|
|
413
|
-
requireEqual("viewTrace", "span.trace_id", span.trace_id, expectedTraceId);
|
|
414
|
-
return truncateSpanAttributes("viewTrace", span, perAttributeCap).span;
|
|
415
|
-
});
|
|
416
|
-
const full = {
|
|
417
|
-
trace_id: expectedTraceId,
|
|
418
|
-
spans
|
|
419
|
-
};
|
|
420
|
-
if (encodedBytes("viewTrace", full) <= byteCeiling) return full;
|
|
421
|
-
return withinByteCeiling("viewTrace", {
|
|
422
|
-
trace_id: expectedTraceId,
|
|
423
|
-
oversized: oversizedFromSpans(spans)
|
|
424
|
-
}, byteCeiling);
|
|
425
|
-
}
|
|
426
|
-
function boundSpansView(result, expectedTraceId, requested, existingSpanIds, perAttributeCap, byteCeiling) {
|
|
427
|
-
requireEqual("viewSpans", "trace_id", result.trace_id, expectedTraceId);
|
|
428
|
-
const requestedSet = new Set(requested);
|
|
429
|
-
const missing = checkedAccountingIds("missing_span_ids", result.missing_span_ids, requestedSet);
|
|
430
|
-
const omitted = checkedAccountingIds("omitted_span_ids", result.omitted_span_ids, requestedSet);
|
|
431
|
-
const missingSet = new Set(missing);
|
|
432
|
-
const omittedSet = new Set(omitted);
|
|
433
|
-
const expectedMissing = requested.filter((id) => !existingSpanIds.has(id));
|
|
434
|
-
if (expectedMissing.length !== missing.length || expectedMissing.some((id) => !missingSet.has(id))) throw contractError("viewSpans", "missing_span_ids does not match the store existence checks");
|
|
435
|
-
if (result.has_more !== omitted.length > 0) throw contractError("viewSpans", "has_more must be true exactly when omitted_span_ids is non-empty");
|
|
436
|
-
if (result.spans.length === 0 && requested.some((id) => existingSpanIds.has(id) && omittedSet.has(id))) throw contractError("viewSpans", "store omitted every existing requested span instead of returning progress or throwing a size error");
|
|
437
|
-
const projected = /* @__PURE__ */ new Map();
|
|
438
|
-
for (const span of result.spans) {
|
|
439
|
-
requireEqual("viewSpans", "span.trace_id", span.trace_id, expectedTraceId);
|
|
440
|
-
if (!requestedSet.has(span.span_id)) throw contractError("viewSpans", `store returned unrequested span ${JSON.stringify(span.span_id)}`);
|
|
441
|
-
if (missingSet.has(span.span_id) || omittedSet.has(span.span_id)) throw contractError("viewSpans", `span ${JSON.stringify(span.span_id)} is both returned and unavailable`);
|
|
442
|
-
if (projected.has(span.span_id)) throw contractError("viewSpans", `store returned duplicate span ${JSON.stringify(span.span_id)}`);
|
|
443
|
-
projected.set(span.span_id, truncateSpanAttributes("viewSpans", span, perAttributeCap));
|
|
444
|
-
}
|
|
445
|
-
for (const id of requested) if (!missingSet.has(id) && !omittedSet.has(id) && !projected.has(id)) throw contractError("viewSpans", `store did not account for requested span ${JSON.stringify(id)}`);
|
|
446
|
-
const spans = [];
|
|
447
|
-
let addedTruncations = 0;
|
|
448
|
-
const build = () => ({
|
|
449
|
-
trace_id: expectedTraceId,
|
|
450
|
-
spans,
|
|
451
|
-
missing_span_ids: requested.filter((id) => missingSet.has(id)),
|
|
452
|
-
omitted_span_ids: requested.filter((id) => omittedSet.has(id)),
|
|
453
|
-
has_more: omittedSet.size > 0,
|
|
454
|
-
truncated_attribute_count: result.truncated_attribute_count + addedTruncations
|
|
455
|
-
});
|
|
456
|
-
withinByteCeiling("viewSpans", build(), byteCeiling);
|
|
457
|
-
for (const id of requested) {
|
|
458
|
-
const item = projected.get(id);
|
|
459
|
-
if (!item) continue;
|
|
460
|
-
spans.push(item.span);
|
|
461
|
-
omittedSet.delete(id);
|
|
462
|
-
addedTruncations += item.truncations;
|
|
463
|
-
if (encodedBytes("viewSpans", build()) <= byteCeiling) continue;
|
|
464
|
-
spans.pop();
|
|
465
|
-
omittedSet.add(id);
|
|
466
|
-
addedTruncations -= item.truncations;
|
|
467
|
-
}
|
|
468
|
-
if (projected.size > 0 && spans.length === 0) {
|
|
469
|
-
const first = projected.values().next().value;
|
|
470
|
-
if (first) throw responseItemTooLarge("viewSpans", {
|
|
471
|
-
trace_id: expectedTraceId,
|
|
472
|
-
spans: [first.span],
|
|
473
|
-
missing_span_ids: [],
|
|
474
|
-
omitted_span_ids: [],
|
|
475
|
-
has_more: false,
|
|
476
|
-
truncated_attribute_count: result.truncated_attribute_count + first.truncations
|
|
477
|
-
}, byteCeiling);
|
|
478
|
-
}
|
|
479
|
-
return withinByteCeiling("viewSpans", build(), byteCeiling);
|
|
480
|
-
}
|
|
481
|
-
function boundTraceSearch(result, expectedTraceId, maxMatches, budgets) {
|
|
482
|
-
requireEqual("searchTrace", "trace_id", result.trace_id, expectedTraceId);
|
|
483
|
-
return {
|
|
484
|
-
trace_id: expectedTraceId,
|
|
485
|
-
...boundSearchHits("searchTrace", result, maxMatches, budgets, (hit) => {
|
|
486
|
-
requireEqual("searchTrace", "hit.trace_id", hit.trace_id, expectedTraceId);
|
|
487
|
-
})
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
function boundSpanSearch(result, expectedTraceId, expectedSpanId, maxMatches, budgets) {
|
|
491
|
-
requireEqual("searchSpan", "trace_id", result.trace_id, expectedTraceId);
|
|
492
|
-
requireEqual("searchSpan", "span_id", result.span_id, expectedSpanId);
|
|
493
|
-
return {
|
|
494
|
-
trace_id: expectedTraceId,
|
|
495
|
-
span_id: expectedSpanId,
|
|
496
|
-
...boundSearchHits("searchSpan", result, maxMatches, budgets, (hit) => {
|
|
497
|
-
requireEqual("searchSpan", "hit.trace_id", hit.trace_id, expectedTraceId);
|
|
498
|
-
requireEqual("searchSpan", "hit.span_id", hit.span_id, expectedSpanId);
|
|
499
|
-
})
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
function boundSearchHits(operation, result, maxMatches, budgets, validateHit) {
|
|
503
|
-
if (result.hits.length > maxMatches) throw contractError(operation, `store returned ${result.hits.length} hits for max_matches ${maxMatches}`);
|
|
504
|
-
if (result.has_more && result.hits.length === 0) throw contractError(operation, "store reported more matches without returning any progress");
|
|
505
|
-
const hits = [];
|
|
506
|
-
for (const raw of result.hits) {
|
|
507
|
-
validateHit(raw);
|
|
508
|
-
const hit = truncateMatchRecord(raw, budgets.perMatchTextBudget);
|
|
509
|
-
if (encodedBytes(operation, {
|
|
510
|
-
hits: [...hits, hit],
|
|
511
|
-
has_more: true
|
|
512
|
-
}) > budgets.perCallByteCeiling) break;
|
|
513
|
-
hits.push(hit);
|
|
514
|
-
}
|
|
515
|
-
if (result.hits.length > 0 && hits.length === 0) throw responseItemTooLarge(operation, {
|
|
516
|
-
hits: [truncateMatchRecord(result.hits[0], budgets.perMatchTextBudget)],
|
|
517
|
-
has_more: true
|
|
518
|
-
}, budgets.perCallByteCeiling);
|
|
519
|
-
return withinByteCeiling(operation, {
|
|
520
|
-
hits,
|
|
521
|
-
has_more: result.has_more || hits.length < result.hits.length
|
|
522
|
-
}, budgets.perCallByteCeiling);
|
|
523
|
-
}
|
|
524
|
-
function checkedAccountingIds(label, ids, requested) {
|
|
525
|
-
const seen = /* @__PURE__ */ new Set();
|
|
526
|
-
for (const id of ids) {
|
|
527
|
-
if (!requested.has(id)) throw contractError("viewSpans", `${label} contains unrequested id ${JSON.stringify(id)}`);
|
|
528
|
-
if (seen.has(id)) throw contractError("viewSpans", `${label} contains duplicate id ${JSON.stringify(id)}`);
|
|
529
|
-
seen.add(id);
|
|
530
|
-
}
|
|
531
|
-
return [...ids];
|
|
532
|
-
}
|
|
533
|
-
function truncateMatchRecord(record, cap) {
|
|
534
|
-
return {
|
|
535
|
-
...record,
|
|
536
|
-
span_name: truncateForBudget(record.span_name, cap),
|
|
537
|
-
attribute_path: truncateForBudget(record.attribute_path, cap),
|
|
538
|
-
matched_text: truncateForBudget(record.matched_text, cap),
|
|
539
|
-
context_before: truncateForBudget(record.context_before, cap),
|
|
540
|
-
context_after: truncateForBudget(record.context_after, cap)
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
function truncateSpanAttributes(operation, span, cap) {
|
|
544
|
-
const attributes = {};
|
|
545
|
-
let truncations = 0;
|
|
546
|
-
for (const [key, value] of Object.entries(span.attributes)) {
|
|
547
|
-
if (typeof value === "string") {
|
|
548
|
-
const truncated = truncateForBudget(value, cap);
|
|
549
|
-
if (truncated !== value) truncations += 1;
|
|
550
|
-
attributes[key] = truncated;
|
|
551
|
-
continue;
|
|
552
|
-
}
|
|
553
|
-
if (value !== null && typeof value === "object") {
|
|
554
|
-
let json;
|
|
555
|
-
try {
|
|
556
|
-
json = JSON.stringify(value);
|
|
557
|
-
} catch (cause) {
|
|
558
|
-
throw new TraceAnalysisStoreContractError(operation, `span attribute ${JSON.stringify(key)} is not JSON-serializable`, { cause });
|
|
559
|
-
}
|
|
560
|
-
const truncated = truncateForBudget(json, cap);
|
|
561
|
-
if (truncated !== json) {
|
|
562
|
-
truncations += 1;
|
|
563
|
-
attributes[key] = truncated;
|
|
564
|
-
} else attributes[key] = value;
|
|
565
|
-
continue;
|
|
566
|
-
}
|
|
567
|
-
attributes[key] = value;
|
|
568
|
-
}
|
|
569
|
-
return {
|
|
570
|
-
span: {
|
|
571
|
-
...span,
|
|
572
|
-
attributes
|
|
573
|
-
},
|
|
574
|
-
truncations
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
function oversizedFromSpans(spans) {
|
|
578
|
-
const names = /* @__PURE__ */ new Map();
|
|
579
|
-
let maxBytes = 0;
|
|
580
|
-
let errors = 0;
|
|
581
|
-
for (const span of spans) {
|
|
582
|
-
names.set(span.name, (names.get(span.name) ?? 0) + 1);
|
|
583
|
-
maxBytes = Math.max(maxBytes, encodedBytes("viewTrace", span));
|
|
584
|
-
if (span.status === "ERROR") errors += 1;
|
|
585
|
-
}
|
|
586
|
-
return {
|
|
587
|
-
span_count: spans.length,
|
|
588
|
-
top_span_names: [...names.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 20),
|
|
589
|
-
span_response_bytes_max: maxBytes,
|
|
590
|
-
error_span_count: errors
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
function compileSearchRegex(pattern) {
|
|
594
|
-
if (typeof pattern !== "string" || pattern.length === 0) throw new TraceAnalysisValidationError("regex_pattern must be a non-empty string");
|
|
595
|
-
let source = pattern;
|
|
596
|
-
let flags = RE2JS.MULTILINE;
|
|
597
|
-
if (source.startsWith("(?i)")) {
|
|
598
|
-
source = source.slice(4);
|
|
599
|
-
flags |= RE2JS.CASE_INSENSITIVE;
|
|
600
|
-
}
|
|
601
|
-
try {
|
|
602
|
-
return RE2JS.compile(source, flags);
|
|
603
|
-
} catch (cause) {
|
|
604
|
-
throw new TraceAnalysisValidationError(`regex_pattern is invalid: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
function truncateForBudget(value, byteCap) {
|
|
608
|
-
validateInteger(byteCap, "byteCap", TRACE_ANALYSIS_LIMITS.minimumTextBudget);
|
|
609
|
-
const original = Buffer.byteLength(value, "utf8");
|
|
610
|
-
if (original <= byteCap) return value;
|
|
611
|
-
const marker = `\n[trace-analyst truncated: original ${original} bytes]`;
|
|
612
|
-
const contentCap = byteCap - Buffer.byteLength(marker, "utf8");
|
|
613
|
-
let cut = Math.max(0, Math.floor(value.length * contentCap / original));
|
|
614
|
-
while (cut > 0 && Buffer.byteLength(value.slice(0, cut), "utf8") > contentCap) cut -= 1;
|
|
615
|
-
return `${value.slice(0, cut)}${marker}`;
|
|
616
|
-
}
|
|
617
|
-
function validateInteger(value, label, minimum, maximum = Number.MAX_SAFE_INTEGER) {
|
|
618
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) throw new TraceAnalysisValidationError(`${label} must be an integer ${minimum}..${maximum}, got ${String(value)}`);
|
|
619
|
-
return value;
|
|
620
|
-
}
|
|
621
|
-
function requireEqual(operation, label, actual, expected) {
|
|
622
|
-
if (actual !== expected) throw contractError(operation, `${label} ${JSON.stringify(actual)} does not match ${JSON.stringify(expected)}`);
|
|
623
|
-
}
|
|
624
|
-
function encodedBytes(operation, value) {
|
|
625
|
-
try {
|
|
626
|
-
const encoded = JSON.stringify(value);
|
|
627
|
-
if (encoded === void 0) throw new TypeError("JSON.stringify returned undefined");
|
|
628
|
-
return Buffer.byteLength(encoded, "utf8");
|
|
629
|
-
} catch (cause) {
|
|
630
|
-
if (cause instanceof TraceAnalysisStoreContractError) throw cause;
|
|
631
|
-
throw new TraceAnalysisStoreContractError(operation, "store result is not JSON-serializable", { cause });
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
function withinByteCeiling(operation, value, ceiling) {
|
|
635
|
-
const actual = encodedBytes(operation, value);
|
|
636
|
-
if (actual > ceiling) throw new TraceAnalysisLimitError(operation, actual, ceiling, `${operation} metadata requires ${actual} bytes, over the ${ceiling}-byte response limit`);
|
|
637
|
-
return value;
|
|
638
|
-
}
|
|
639
|
-
function responseItemTooLarge(operation, value, ceiling) {
|
|
640
|
-
return new TraceAnalysisLimitError(operation, encodedBytes(operation, value), ceiling, `${operation} cannot fit one result item in the ${ceiling}-byte response limit`);
|
|
641
|
-
}
|
|
642
|
-
function contractError(operation, message) {
|
|
643
|
-
return new TraceAnalysisStoreContractError(operation, message);
|
|
644
|
-
}
|
|
645
|
-
//#endregion
|
|
646
|
-
//#region src/trace-analyst/store-schemas.ts
|
|
647
|
-
const identifier = z.string().min(1).max(TRACE_ANALYSIS_LIMITS.identifierCharacters);
|
|
648
|
-
const timestamp = z.iso.datetime({ offset: true });
|
|
649
|
-
const nonNegativeInteger = z.number().int().nonnegative();
|
|
650
|
-
const nonNegativeNumber = z.number().nonnegative();
|
|
651
|
-
const nullableString = z.string().nullable();
|
|
652
|
-
const nullableIdentifier = identifier.nullable();
|
|
653
|
-
const stringArray = z.array(z.string());
|
|
654
|
-
const filterValues = z.array(identifier).max(TRACE_ANALYSIS_LIMITS.filterValues);
|
|
655
|
-
const traceFiltersSchema = z.object({
|
|
656
|
-
has_errors: z.boolean().optional(),
|
|
657
|
-
service_names: filterValues.optional(),
|
|
658
|
-
agent_names: filterValues.optional(),
|
|
659
|
-
model_names: filterValues.optional(),
|
|
660
|
-
tool_names: filterValues.optional(),
|
|
661
|
-
start_time_after: timestamp.optional(),
|
|
662
|
-
start_time_before: timestamp.optional(),
|
|
663
|
-
regex_pattern: z.string().min(1).max(TRACE_ANALYSIS_LIMITS.regexCharacters).optional()
|
|
664
|
-
}).strict();
|
|
665
|
-
const byteCap = z.number().int().min(TRACE_ANALYSIS_LIMITS.minimumTextBudget);
|
|
666
|
-
const searchPattern = z.string().min(1).max(TRACE_ANALYSIS_LIMITS.regexCharacters);
|
|
667
|
-
const traceStoreInputSchemas = {
|
|
668
|
-
hasTrace: z.object({ trace_id: identifier }).strict(),
|
|
669
|
-
hasSpans: z.object({
|
|
670
|
-
trace_id: identifier,
|
|
671
|
-
span_ids: z.array(identifier).min(1).max(TRACE_ANALYSIS_LIMITS.viewSpans)
|
|
672
|
-
}).strict(),
|
|
673
|
-
getOverview: z.object({ filters: traceFiltersSchema.optional() }).strict(),
|
|
674
|
-
queryTraces: z.object({
|
|
675
|
-
filters: traceFiltersSchema.optional(),
|
|
676
|
-
limit: z.number().int().min(1).max(TRACE_ANALYSIS_LIMITS.queryTraces),
|
|
677
|
-
offset: z.number().int().nonnegative().optional()
|
|
678
|
-
}).strict(),
|
|
679
|
-
countTraces: z.object({ filters: traceFiltersSchema.optional() }).strict(),
|
|
680
|
-
viewTrace: z.object({
|
|
681
|
-
trace_id: identifier,
|
|
682
|
-
per_attribute_byte_cap: byteCap.optional()
|
|
683
|
-
}).strict(),
|
|
684
|
-
viewSpans: z.object({
|
|
685
|
-
trace_id: identifier,
|
|
686
|
-
span_ids: z.array(identifier).min(1).max(TRACE_ANALYSIS_LIMITS.viewSpans),
|
|
687
|
-
per_attribute_byte_cap: byteCap.optional()
|
|
688
|
-
}).strict(),
|
|
689
|
-
searchTrace: z.object({
|
|
690
|
-
trace_id: identifier,
|
|
691
|
-
regex_pattern: searchPattern,
|
|
692
|
-
max_matches: z.number().int().min(1).max(TRACE_ANALYSIS_LIMITS.searchMatches).default(50)
|
|
693
|
-
}).strict(),
|
|
694
|
-
searchSpan: z.object({
|
|
695
|
-
trace_id: identifier,
|
|
696
|
-
span_id: identifier,
|
|
697
|
-
regex_pattern: searchPattern,
|
|
698
|
-
max_matches: z.number().int().min(1).max(TRACE_ANALYSIS_LIMITS.searchMatches).default(50)
|
|
699
|
-
}).strict()
|
|
700
|
-
};
|
|
701
|
-
const spanKind = z.enum([
|
|
702
|
-
"AGENT",
|
|
703
|
-
"LLM",
|
|
704
|
-
"TOOL",
|
|
705
|
-
"CHAIN",
|
|
706
|
-
"EVALUATOR",
|
|
707
|
-
"GUARDRAIL",
|
|
708
|
-
"SPAN",
|
|
709
|
-
"UNKNOWN"
|
|
710
|
-
]);
|
|
711
|
-
const spanStatus = z.enum([
|
|
712
|
-
"OK",
|
|
713
|
-
"ERROR",
|
|
714
|
-
"UNSET"
|
|
715
|
-
]);
|
|
716
|
-
const traceSpan = z.object({
|
|
717
|
-
trace_id: identifier,
|
|
718
|
-
span_id: identifier,
|
|
719
|
-
parent_span_id: nullableIdentifier,
|
|
720
|
-
name: z.string(),
|
|
721
|
-
kind: spanKind,
|
|
722
|
-
start_time: timestamp,
|
|
723
|
-
end_time: timestamp,
|
|
724
|
-
duration_ms: nonNegativeNumber,
|
|
725
|
-
status: spanStatus,
|
|
726
|
-
status_message: z.string().optional(),
|
|
727
|
-
service_name: nullableString,
|
|
728
|
-
agent_name: nullableString,
|
|
729
|
-
model_name: nullableString,
|
|
730
|
-
tool_name: nullableString,
|
|
731
|
-
attributes: z.record(z.string(), z.json())
|
|
732
|
-
}).strict();
|
|
733
|
-
const traceSummary = z.object({
|
|
734
|
-
trace_id: identifier,
|
|
735
|
-
service_name: nullableString,
|
|
736
|
-
agent_name: nullableString,
|
|
737
|
-
span_count: nonNegativeInteger,
|
|
738
|
-
has_errors: z.boolean(),
|
|
739
|
-
start_time: timestamp,
|
|
740
|
-
end_time: timestamp,
|
|
741
|
-
duration_ms: nonNegativeNumber,
|
|
742
|
-
raw_jsonl_bytes: nonNegativeInteger,
|
|
743
|
-
models: stringArray,
|
|
744
|
-
tools: stringArray
|
|
745
|
-
}).strict();
|
|
746
|
-
const errorCluster = z.object({
|
|
747
|
-
signature: z.string(),
|
|
748
|
-
status_message_sample: z.string(),
|
|
749
|
-
span_name: nullableString,
|
|
750
|
-
tool_name: nullableString,
|
|
751
|
-
trace_count: nonNegativeInteger,
|
|
752
|
-
span_count: nonNegativeInteger,
|
|
753
|
-
prevalence: z.number().min(0).max(1),
|
|
754
|
-
exemplar_trace_ids: z.array(identifier),
|
|
755
|
-
exemplar_span_ids: z.array(identifier)
|
|
756
|
-
}).strict();
|
|
757
|
-
const overview = z.object({
|
|
758
|
-
total_traces: nonNegativeInteger,
|
|
759
|
-
raw_jsonl_bytes: nonNegativeInteger,
|
|
760
|
-
services: stringArray,
|
|
761
|
-
agents: stringArray,
|
|
762
|
-
models: stringArray,
|
|
763
|
-
tool_names: stringArray,
|
|
764
|
-
sample_trace_ids: z.array(identifier).max(TRACE_ANALYSIS_LIMITS.sampleTraceIds),
|
|
765
|
-
errors: z.object({
|
|
766
|
-
trace_count: nonNegativeInteger,
|
|
767
|
-
span_count: nonNegativeInteger
|
|
768
|
-
}).strict(),
|
|
769
|
-
error_clusters: z.array(errorCluster),
|
|
770
|
-
time_range: z.object({
|
|
771
|
-
earliest: timestamp,
|
|
772
|
-
latest: timestamp
|
|
773
|
-
}).strict().nullable()
|
|
774
|
-
}).strict();
|
|
775
|
-
const tracePage = z.object({
|
|
776
|
-
traces: z.array(traceSummary),
|
|
777
|
-
total: nonNegativeInteger,
|
|
778
|
-
has_more: z.boolean()
|
|
779
|
-
}).strict();
|
|
780
|
-
const oversizedTrace = z.object({
|
|
781
|
-
span_count: nonNegativeInteger,
|
|
782
|
-
top_span_names: z.array(z.tuple([z.string(), nonNegativeInteger])).max(20),
|
|
783
|
-
span_response_bytes_max: nonNegativeInteger,
|
|
784
|
-
error_span_count: nonNegativeInteger
|
|
785
|
-
}).strict();
|
|
786
|
-
const traceView = z.object({
|
|
787
|
-
trace_id: identifier,
|
|
788
|
-
spans: z.array(traceSpan).optional(),
|
|
789
|
-
oversized: oversizedTrace.optional()
|
|
790
|
-
}).strict().refine((value) => value.spans === void 0 !== (value.oversized === void 0), { message: "exactly one of spans or oversized is required" });
|
|
791
|
-
const spansView = z.object({
|
|
792
|
-
trace_id: identifier,
|
|
793
|
-
spans: z.array(traceSpan),
|
|
794
|
-
missing_span_ids: z.array(identifier),
|
|
795
|
-
omitted_span_ids: z.array(identifier),
|
|
796
|
-
has_more: z.boolean(),
|
|
797
|
-
truncated_attribute_count: nonNegativeInteger
|
|
798
|
-
}).strict();
|
|
799
|
-
const matchRecord = z.object({
|
|
800
|
-
trace_id: identifier,
|
|
801
|
-
span_id: identifier,
|
|
802
|
-
span_name: z.string(),
|
|
803
|
-
span_kind: spanKind,
|
|
804
|
-
attribute_path: z.string(),
|
|
805
|
-
matched_text: z.string(),
|
|
806
|
-
context_before: z.string(),
|
|
807
|
-
context_after: z.string(),
|
|
808
|
-
match_offset: nonNegativeInteger
|
|
809
|
-
}).strict();
|
|
810
|
-
const traceSearch = z.object({
|
|
811
|
-
trace_id: identifier,
|
|
812
|
-
hits: z.array(matchRecord),
|
|
813
|
-
has_more: z.boolean()
|
|
814
|
-
}).strict();
|
|
815
|
-
const spanSearch = traceSearch.extend({ span_id: identifier }).strict();
|
|
816
|
-
const traceStoreOutputSchemas = {
|
|
817
|
-
hasTrace: z.boolean(),
|
|
818
|
-
hasSpans: z.array(identifier).max(TRACE_ANALYSIS_LIMITS.viewSpans),
|
|
819
|
-
getOverview: overview,
|
|
820
|
-
queryTraces: tracePage,
|
|
821
|
-
countTraces: nonNegativeInteger,
|
|
822
|
-
viewTrace: traceView,
|
|
823
|
-
viewSpans: spansView,
|
|
824
|
-
searchTrace: traceSearch,
|
|
825
|
-
searchSpan: spanSearch
|
|
826
|
-
};
|
|
827
|
-
function parseTraceInput(operation, schema, value) {
|
|
828
|
-
try {
|
|
829
|
-
return schema.parse(value);
|
|
830
|
-
} catch (cause) {
|
|
831
|
-
throw new TraceAnalysisValidationError(`${operation}: invalid arguments: ${cause instanceof z.ZodError ? z.prettifyError(cause) : String(cause)}`, { cause });
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
function parseStoreOutput(operation, schema, value) {
|
|
835
|
-
try {
|
|
836
|
-
return schema.parse(value);
|
|
837
|
-
} catch (cause) {
|
|
838
|
-
throw new TraceAnalysisStoreContractError(operation, `invalid store result: ${cause instanceof z.ZodError ? z.prettifyError(cause) : String(cause)}`, { cause });
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
function toTraceJsonSchema(schema) {
|
|
842
|
-
return z.toJSONSchema(schema, { target: "draft-7" });
|
|
843
|
-
}
|
|
844
|
-
//#endregion
|
|
845
|
-
//#region src/trace-analyst/store-boundary.ts
|
|
846
|
-
/** Apply the public validation, cancellation, not-found, and size rules to any adapter. */
|
|
847
|
-
function createBoundedTraceAnalysisStore(source, options = {}) {
|
|
848
|
-
const budgets = resolveTraceBudgets(options.budgets);
|
|
849
|
-
return {
|
|
850
|
-
async hasTrace(traceId, context) {
|
|
851
|
-
throwIfAborted(context);
|
|
852
|
-
const { trace_id } = parseTraceInput("hasTrace", traceStoreInputSchemas.hasTrace, { trace_id: traceId });
|
|
853
|
-
const result = await source.hasTrace(trace_id, context);
|
|
854
|
-
throwIfAborted(context);
|
|
855
|
-
return parseStoreOutput("hasTrace", traceStoreOutputSchemas.hasTrace, result);
|
|
856
|
-
},
|
|
857
|
-
async hasSpans(input, context) {
|
|
858
|
-
throwIfAborted(context);
|
|
859
|
-
const parsed = parseTraceInput("hasSpans", traceStoreInputSchemas.hasSpans, input);
|
|
860
|
-
assertUniqueIds(parsed.span_ids, "hasSpans.span_ids");
|
|
861
|
-
const result = await source.hasSpans(parsed, context);
|
|
862
|
-
throwIfAborted(context);
|
|
863
|
-
return validateExistingSpanIds(parseStoreOutput("hasSpans", traceStoreOutputSchemas.hasSpans, result), parsed.span_ids);
|
|
864
|
-
},
|
|
865
|
-
async getOverview(filters, context) {
|
|
866
|
-
throwIfAborted(context);
|
|
867
|
-
const input = parseTraceInput("getOverview", traceStoreInputSchemas.getOverview, { filters });
|
|
868
|
-
const result = await source.getOverview(input.filters, context);
|
|
869
|
-
throwIfAborted(context);
|
|
870
|
-
return boundOverview(parseStoreOutput("getOverview", traceStoreOutputSchemas.getOverview, result), budgets.perCallByteCeiling);
|
|
871
|
-
},
|
|
872
|
-
async queryTraces(input, context) {
|
|
873
|
-
throwIfAborted(context);
|
|
874
|
-
const parsed = parseTraceInput("queryTraces", traceStoreInputSchemas.queryTraces, input);
|
|
875
|
-
const offset = parsed.offset ?? 0;
|
|
876
|
-
const result = await source.queryTraces({
|
|
877
|
-
...parsed,
|
|
878
|
-
offset
|
|
879
|
-
}, context);
|
|
880
|
-
throwIfAborted(context);
|
|
881
|
-
return boundTracePage(parseStoreOutput("queryTraces", traceStoreOutputSchemas.queryTraces, result), {
|
|
882
|
-
limit: parsed.limit,
|
|
883
|
-
offset
|
|
884
|
-
}, budgets.perCallByteCeiling);
|
|
885
|
-
},
|
|
886
|
-
async countTraces(filters, context) {
|
|
887
|
-
throwIfAborted(context);
|
|
888
|
-
const input = parseTraceInput("countTraces", traceStoreInputSchemas.countTraces, { filters });
|
|
889
|
-
const result = await source.countTraces(input.filters, context);
|
|
890
|
-
throwIfAborted(context);
|
|
891
|
-
return parseStoreOutput("countTraces", traceStoreOutputSchemas.countTraces, result);
|
|
892
|
-
},
|
|
893
|
-
async viewTrace(input, context) {
|
|
894
|
-
throwIfAborted(context);
|
|
895
|
-
const parsed = parseTraceInput("viewTrace", traceStoreInputSchemas.viewTrace, input);
|
|
896
|
-
await requireTrace(source, parsed.trace_id, context);
|
|
897
|
-
const perAttributeCap = parsed.per_attribute_byte_cap ?? budgets.perAttributeViewBudget;
|
|
898
|
-
const result = await source.viewTrace({
|
|
899
|
-
...parsed,
|
|
900
|
-
per_attribute_byte_cap: perAttributeCap
|
|
901
|
-
}, context);
|
|
902
|
-
throwIfAborted(context);
|
|
903
|
-
return boundTraceView(parseStoreOutput("viewTrace", traceStoreOutputSchemas.viewTrace, result), parsed.trace_id, perAttributeCap, budgets.perCallByteCeiling);
|
|
904
|
-
},
|
|
905
|
-
async viewSpans(input, context) {
|
|
906
|
-
throwIfAborted(context);
|
|
907
|
-
const parsed = parseTraceInput("viewSpans", traceStoreInputSchemas.viewSpans, input);
|
|
908
|
-
assertUniqueIds(parsed.span_ids, "viewSpans.span_ids");
|
|
909
|
-
await requireTrace(source, parsed.trace_id, context);
|
|
910
|
-
const existingSpanIds = new Set(validateExistingSpanIds(parseStoreOutput("hasSpans", traceStoreOutputSchemas.hasSpans, await source.hasSpans({
|
|
911
|
-
trace_id: parsed.trace_id,
|
|
912
|
-
span_ids: parsed.span_ids
|
|
913
|
-
}, context)), parsed.span_ids));
|
|
914
|
-
throwIfAborted(context);
|
|
915
|
-
const perAttributeCap = parsed.per_attribute_byte_cap ?? budgets.perAttributeSpanBudget;
|
|
916
|
-
const result = await source.viewSpans({
|
|
917
|
-
...parsed,
|
|
918
|
-
per_attribute_byte_cap: perAttributeCap
|
|
919
|
-
}, context);
|
|
920
|
-
throwIfAborted(context);
|
|
921
|
-
return boundSpansView(parseStoreOutput("viewSpans", traceStoreOutputSchemas.viewSpans, result), parsed.trace_id, parsed.span_ids, existingSpanIds, perAttributeCap, budgets.perCallByteCeiling);
|
|
922
|
-
},
|
|
923
|
-
async searchTrace(input, context) {
|
|
924
|
-
throwIfAborted(context);
|
|
925
|
-
const parsed = parseTraceInput("searchTrace", traceStoreInputSchemas.searchTrace, input);
|
|
926
|
-
compileSearchRegex(parsed.regex_pattern);
|
|
927
|
-
await requireTrace(source, parsed.trace_id, context);
|
|
928
|
-
const result = await source.searchTrace(parsed, context);
|
|
929
|
-
throwIfAborted(context);
|
|
930
|
-
return boundTraceSearch(parseStoreOutput("searchTrace", traceStoreOutputSchemas.searchTrace, result), parsed.trace_id, parsed.max_matches, budgets);
|
|
931
|
-
},
|
|
932
|
-
async searchSpan(input, context) {
|
|
933
|
-
throwIfAborted(context);
|
|
934
|
-
const parsed = parseTraceInput("searchSpan", traceStoreInputSchemas.searchSpan, input);
|
|
935
|
-
compileSearchRegex(parsed.regex_pattern);
|
|
936
|
-
await requireTrace(source, parsed.trace_id, context);
|
|
937
|
-
await requireSpan(source, parsed.trace_id, parsed.span_id, context);
|
|
938
|
-
const result = await source.searchSpan(parsed, context);
|
|
939
|
-
throwIfAborted(context);
|
|
940
|
-
return boundSpanSearch(parseStoreOutput("searchSpan", traceStoreOutputSchemas.searchSpan, result), parsed.trace_id, parsed.span_id, parsed.max_matches, budgets);
|
|
941
|
-
}
|
|
942
|
-
};
|
|
943
|
-
}
|
|
944
|
-
async function requireTrace(source, traceId, context) {
|
|
945
|
-
throwIfAborted(context);
|
|
946
|
-
const exists = await source.hasTrace(traceId, context);
|
|
947
|
-
throwIfAborted(context);
|
|
948
|
-
if (!parseStoreOutput("hasTrace", traceStoreOutputSchemas.hasTrace, exists)) throw new TraceNotFoundError(traceId);
|
|
949
|
-
}
|
|
950
|
-
async function requireSpan(source, traceId, spanId, context) {
|
|
951
|
-
throwIfAborted(context);
|
|
952
|
-
const existing = await source.hasSpans({
|
|
953
|
-
trace_id: traceId,
|
|
954
|
-
span_ids: [spanId]
|
|
955
|
-
}, context);
|
|
956
|
-
throwIfAborted(context);
|
|
957
|
-
if (validateExistingSpanIds(parseStoreOutput("hasSpans", traceStoreOutputSchemas.hasSpans, existing), [spanId]).length === 0) throw new SpanNotFoundError(traceId, spanId);
|
|
958
|
-
}
|
|
959
|
-
function validateExistingSpanIds(found, requested) {
|
|
960
|
-
const requestedSet = new Set(requested);
|
|
961
|
-
const seen = /* @__PURE__ */ new Set();
|
|
962
|
-
for (const id of found) {
|
|
963
|
-
if (!requestedSet.has(id)) throw new TraceAnalysisStoreContractError("hasSpans", `hasSpans returned unrequested span id ${JSON.stringify(id)}`);
|
|
964
|
-
if (seen.has(id)) throw new TraceAnalysisStoreContractError("hasSpans", `hasSpans returned duplicate span id ${JSON.stringify(id)}`);
|
|
965
|
-
seen.add(id);
|
|
966
|
-
}
|
|
967
|
-
return [...found];
|
|
968
|
-
}
|
|
969
|
-
function assertUniqueIds(ids, label) {
|
|
970
|
-
if (new Set(ids).size !== ids.length) throw new TraceAnalysisValidationError(`${label} must not contain duplicates`);
|
|
971
|
-
}
|
|
972
|
-
function throwIfAborted(context) {
|
|
973
|
-
context?.signal?.throwIfAborted();
|
|
974
|
-
}
|
|
975
|
-
//#endregion
|
|
976
|
-
//#region src/trace-analyst/store-otlp.ts
|
|
977
|
-
/**
|
|
978
|
-
* `OtlpFileTraceStore` — read-only OTLP-JSONL trace store for the
|
|
979
|
-
* trace-analyst.
|
|
980
|
-
*
|
|
981
|
-
* Wire shape. Each line of the input file is one OTLP-shaped span. The
|
|
982
|
-
* store understands flattened OTLP JSONL plus the OpenInference vocab.
|
|
983
|
-
* We project upstream's full
|
|
984
|
-
* span shape down to `TraceAnalystSpan` lazily — full materialisation
|
|
985
|
-
* only happens for the spans the agent actually requests.
|
|
986
|
-
*
|
|
987
|
-
* Indexing. On first read the store builds an in-memory index keyed
|
|
988
|
-
* by `trace_id` carrying:
|
|
989
|
-
* - byte offsets + lengths for each span line (for surgical reads
|
|
990
|
-
* without re-parsing the whole file)
|
|
991
|
-
* - a `TraceAnalystTraceSummary` rollup
|
|
992
|
-
* - sets of services / agents / models / tools / has_errors
|
|
993
|
-
* - byte size of the trace's JSONL slab
|
|
994
|
-
*
|
|
995
|
-
* Memory bound. The index keeps span metadata only — names, kinds,
|
|
996
|
-
* offsets, status. Attribute payloads stay on disk until requested.
|
|
997
|
-
* For a 50MB JSONL with 50k spans, the index is ~5MB.
|
|
998
|
-
*
|
|
999
|
-
* Concurrency. The store builds the index once on first read and
|
|
1000
|
-
* caches it. Subsequent reads reuse the index. The file is opened on
|
|
1001
|
-
* each read; we never hold a long-lived FD.
|
|
1002
|
-
*/
|
|
1003
|
-
/** Lines indexed between event-loop yields. Bounded synchronous work per
|
|
1004
|
-
* tick keeps the index build from starving other tasks on large files
|
|
1005
|
-
* while staying coarse enough that the yields are cheap. */
|
|
1006
|
-
const INDEX_YIELD_LINES = 5e3;
|
|
1007
|
-
const SCAN_YIELD_ITEMS = 500;
|
|
1008
|
-
/** Hand control back to the event loop without busy-waiting. */
|
|
1009
|
-
function yieldToEventLoop() {
|
|
1010
|
-
return new Promise((resolve) => setImmediate(resolve));
|
|
1011
|
-
}
|
|
1012
|
-
async function scanCheckpoint(signal, item) {
|
|
1013
|
-
signal?.throwIfAborted();
|
|
1014
|
-
if (item > 0 && item % SCAN_YIELD_ITEMS === 0) {
|
|
1015
|
-
await yieldToEventLoop();
|
|
1016
|
-
signal?.throwIfAborted();
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
var BufferedOtlpTraceStore = class {
|
|
1020
|
-
perAttributeViewBudget;
|
|
1021
|
-
perAttributeSpanBudget;
|
|
1022
|
-
perCallByteCeiling;
|
|
1023
|
-
perMatchTextBudget;
|
|
1024
|
-
indexValue;
|
|
1025
|
-
indexTask;
|
|
1026
|
-
bufferValue;
|
|
1027
|
-
bufferTask;
|
|
1028
|
-
constructor(opts) {
|
|
1029
|
-
this.perAttributeViewBudget = validateInteger(opts.perAttributeViewBudget ?? DEFAULT_TRACE_ANALYST_BUDGETS.perAttributeViewBudget, "perAttributeViewBudget", TRACE_ANALYSIS_LIMITS.minimumTextBudget);
|
|
1030
|
-
this.perAttributeSpanBudget = validateInteger(opts.perAttributeSpanBudget ?? DEFAULT_TRACE_ANALYST_BUDGETS.perAttributeSpanBudget, "perAttributeSpanBudget", TRACE_ANALYSIS_LIMITS.minimumTextBudget);
|
|
1031
|
-
this.perCallByteCeiling = validateInteger(opts.perCallByteCeiling ?? DEFAULT_TRACE_ANALYST_BUDGETS.perCallByteCeiling, "perCallByteCeiling", 1);
|
|
1032
|
-
this.perMatchTextBudget = validateInteger(opts.perMatchTextBudget ?? DEFAULT_TRACE_ANALYST_BUDGETS.perMatchTextBudget, "perMatchTextBudget", TRACE_ANALYSIS_LIMITS.minimumTextBudget);
|
|
1033
|
-
}
|
|
1034
|
-
async hasTrace(trace_id, context) {
|
|
1035
|
-
context?.signal?.throwIfAborted();
|
|
1036
|
-
const idx = await this.index(context);
|
|
1037
|
-
context?.signal?.throwIfAborted();
|
|
1038
|
-
return idx.byTrace.has(trace_id);
|
|
1039
|
-
}
|
|
1040
|
-
async hasSpans(opts, context) {
|
|
1041
|
-
context?.signal?.throwIfAborted();
|
|
1042
|
-
const trace = (await this.index(context)).byTrace.get(opts.trace_id);
|
|
1043
|
-
if (!trace) return [];
|
|
1044
|
-
const requested = new Set(opts.span_ids);
|
|
1045
|
-
const found = [];
|
|
1046
|
-
for (let index = 0; index < trace.spans.length; index += 1) {
|
|
1047
|
-
await scanCheckpoint(context?.signal, index);
|
|
1048
|
-
const spanId = trace.spans[index].span_id;
|
|
1049
|
-
if (requested.has(spanId)) found.push(spanId);
|
|
1050
|
-
}
|
|
1051
|
-
return found;
|
|
1052
|
-
}
|
|
1053
|
-
async getOverview(filters, context) {
|
|
1054
|
-
const idx = await this.index(context);
|
|
1055
|
-
const matched = await this.matchedTraces(idx, filters, context);
|
|
1056
|
-
const services = /* @__PURE__ */ new Set();
|
|
1057
|
-
const agents = /* @__PURE__ */ new Set();
|
|
1058
|
-
const models = /* @__PURE__ */ new Set();
|
|
1059
|
-
const tools = /* @__PURE__ */ new Set();
|
|
1060
|
-
let rawBytes = 0;
|
|
1061
|
-
let earliest = null;
|
|
1062
|
-
let latest = null;
|
|
1063
|
-
let errorTraceCount = 0;
|
|
1064
|
-
let errorSpanCount = 0;
|
|
1065
|
-
const clusters = /* @__PURE__ */ new Map();
|
|
1066
|
-
let traceIndex = 0;
|
|
1067
|
-
for (const t of matched) {
|
|
1068
|
-
await scanCheckpoint(context?.signal, traceIndex++);
|
|
1069
|
-
if (t.service_name) services.add(t.service_name);
|
|
1070
|
-
if (t.agent_name) agents.add(t.agent_name);
|
|
1071
|
-
for (const m of t.models) models.add(m);
|
|
1072
|
-
for (const tn of t.tools) tools.add(tn);
|
|
1073
|
-
rawBytes += t.raw_jsonl_bytes;
|
|
1074
|
-
if (!earliest || compareSpanTime(t.start_time, earliest) < 0) earliest = t.start_time;
|
|
1075
|
-
if (!latest || compareSpanTime(t.end_time, latest) > 0) latest = t.end_time;
|
|
1076
|
-
if (t.has_errors) {
|
|
1077
|
-
errorTraceCount += 1;
|
|
1078
|
-
let spanIndex = 0;
|
|
1079
|
-
for (const s of t.spans) {
|
|
1080
|
-
await scanCheckpoint(context?.signal, spanIndex++);
|
|
1081
|
-
if (s.status !== "ERROR") continue;
|
|
1082
|
-
errorSpanCount += 1;
|
|
1083
|
-
accumulateErrorCluster(clusters, t.trace_id, s);
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
const sample_trace_ids = matched.slice(0, 20).map((t) => t.trace_id);
|
|
1088
|
-
return {
|
|
1089
|
-
total_traces: matched.length,
|
|
1090
|
-
raw_jsonl_bytes: rawBytes,
|
|
1091
|
-
services: [...services].sort(),
|
|
1092
|
-
agents: [...agents].sort(),
|
|
1093
|
-
models: [...models].sort(),
|
|
1094
|
-
tool_names: [...tools].sort(),
|
|
1095
|
-
sample_trace_ids,
|
|
1096
|
-
errors: {
|
|
1097
|
-
trace_count: errorTraceCount,
|
|
1098
|
-
span_count: errorSpanCount
|
|
1099
|
-
},
|
|
1100
|
-
error_clusters: finalizeErrorClusters(clusters, errorTraceCount),
|
|
1101
|
-
time_range: earliest && latest ? {
|
|
1102
|
-
earliest,
|
|
1103
|
-
latest
|
|
1104
|
-
} : null
|
|
1105
|
-
};
|
|
1106
|
-
}
|
|
1107
|
-
async queryTraces(opts, context) {
|
|
1108
|
-
const limit = validateInteger(opts.limit, "queryTraces.limit", 1, TRACE_ANALYSIS_LIMITS.queryTraces);
|
|
1109
|
-
const offset = validateInteger(opts.offset ?? 0, "queryTraces.offset", 0);
|
|
1110
|
-
const idx = await this.index(context);
|
|
1111
|
-
const matched = await this.matchedTraces(idx, opts.filters, context);
|
|
1112
|
-
const slice = matched.slice(offset, offset + limit);
|
|
1113
|
-
return {
|
|
1114
|
-
traces: slice.map((t) => this.toSummary(t)),
|
|
1115
|
-
total: matched.length,
|
|
1116
|
-
has_more: offset + slice.length < matched.length
|
|
1117
|
-
};
|
|
1118
|
-
}
|
|
1119
|
-
async countTraces(filters, context) {
|
|
1120
|
-
const idx = await this.index(context);
|
|
1121
|
-
return (await this.matchedTraces(idx, filters, context)).length;
|
|
1122
|
-
}
|
|
1123
|
-
async viewTrace(opts, context) {
|
|
1124
|
-
context?.signal?.throwIfAborted();
|
|
1125
|
-
const trace = (await this.index(context)).byTrace.get(opts.trace_id);
|
|
1126
|
-
if (!trace) throw new TraceNotFoundError(opts.trace_id);
|
|
1127
|
-
const cap = validateInteger(opts.per_attribute_byte_cap ?? this.perAttributeViewBudget, "viewTrace.per_attribute_byte_cap", 1);
|
|
1128
|
-
const buf = await this.buffer(context);
|
|
1129
|
-
const spans = [];
|
|
1130
|
-
let runningBytes = 0;
|
|
1131
|
-
let span_response_bytes_max = 0;
|
|
1132
|
-
const counter = { value: 0 };
|
|
1133
|
-
let spanIndex = 0;
|
|
1134
|
-
for (const s of trace.spans) {
|
|
1135
|
-
await scanCheckpoint(context?.signal, spanIndex++);
|
|
1136
|
-
const projected = this.projectSpan(buf, trace.trace_id, s, cap, counter);
|
|
1137
|
-
const bytes = Buffer.byteLength(JSON.stringify(projected), "utf8");
|
|
1138
|
-
span_response_bytes_max = Math.max(span_response_bytes_max, bytes);
|
|
1139
|
-
runningBytes += bytes;
|
|
1140
|
-
if (runningBytes > this.perCallByteCeiling) return {
|
|
1141
|
-
trace_id: trace.trace_id,
|
|
1142
|
-
oversized: this.buildOversizedSummary(trace, span_response_bytes_max)
|
|
1143
|
-
};
|
|
1144
|
-
spans.push(projected);
|
|
1145
|
-
}
|
|
1146
|
-
return {
|
|
1147
|
-
trace_id: trace.trace_id,
|
|
1148
|
-
spans
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
|
-
async viewSpans(opts, context) {
|
|
1152
|
-
context?.signal?.throwIfAborted();
|
|
1153
|
-
const trace = (await this.index(context)).byTrace.get(opts.trace_id);
|
|
1154
|
-
if (!trace) throw new TraceNotFoundError(opts.trace_id);
|
|
1155
|
-
if (opts.span_ids.length < 1 || opts.span_ids.length > TRACE_ANALYSIS_LIMITS.viewSpans) throw new TraceAnalysisValidationError(`viewSpans.span_ids must contain 1..${TRACE_ANALYSIS_LIMITS.viewSpans} ids, got ${opts.span_ids.length}`);
|
|
1156
|
-
const cap = validateInteger(opts.per_attribute_byte_cap ?? this.perAttributeSpanBudget, "viewSpans.per_attribute_byte_cap", 1);
|
|
1157
|
-
const requested = [...new Set(opts.span_ids)];
|
|
1158
|
-
const wantSet = new Set(requested);
|
|
1159
|
-
const foundById = /* @__PURE__ */ new Map();
|
|
1160
|
-
for (let index = 0; index < trace.spans.length; index += 1) {
|
|
1161
|
-
await scanCheckpoint(context?.signal, index);
|
|
1162
|
-
const span = trace.spans[index];
|
|
1163
|
-
if (wantSet.has(span.span_id)) foundById.set(span.span_id, span);
|
|
1164
|
-
}
|
|
1165
|
-
const missing = requested.filter((id) => !foundById.has(id));
|
|
1166
|
-
const buf = await this.buffer(context);
|
|
1167
|
-
const spans = [];
|
|
1168
|
-
let truncatedAttributeCount = 0;
|
|
1169
|
-
const omitted = new Set(requested.filter((id) => foundById.has(id)));
|
|
1170
|
-
const buildResult = () => ({
|
|
1171
|
-
trace_id: trace.trace_id,
|
|
1172
|
-
spans,
|
|
1173
|
-
missing_span_ids: missing,
|
|
1174
|
-
omitted_span_ids: requested.filter((id) => omitted.has(id)),
|
|
1175
|
-
has_more: omitted.size > 0,
|
|
1176
|
-
truncated_attribute_count: truncatedAttributeCount
|
|
1177
|
-
});
|
|
1178
|
-
const metadataBytes = Buffer.byteLength(JSON.stringify(buildResult()), "utf8");
|
|
1179
|
-
if (metadataBytes > this.perCallByteCeiling) throw new TraceAnalysisLimitError("viewSpans", metadataBytes, this.perCallByteCeiling, `viewSpans accounting requires ${metadataBytes} bytes, over the ${this.perCallByteCeiling}-byte response limit`);
|
|
1180
|
-
let spanIndex = 0;
|
|
1181
|
-
let smallestRejectedBytes = Number.POSITIVE_INFINITY;
|
|
1182
|
-
for (const id of requested) {
|
|
1183
|
-
await scanCheckpoint(context?.signal, spanIndex++);
|
|
1184
|
-
const s = foundById.get(id);
|
|
1185
|
-
if (!s) continue;
|
|
1186
|
-
const counter = { value: 0 };
|
|
1187
|
-
const projected = this.projectSpan(buf, trace.trace_id, s, cap, counter);
|
|
1188
|
-
spans.push(projected);
|
|
1189
|
-
omitted.delete(id);
|
|
1190
|
-
truncatedAttributeCount += counter.value;
|
|
1191
|
-
const responseBytes = Buffer.byteLength(JSON.stringify(buildResult()), "utf8");
|
|
1192
|
-
if (responseBytes <= this.perCallByteCeiling) continue;
|
|
1193
|
-
smallestRejectedBytes = Math.min(smallestRejectedBytes, responseBytes);
|
|
1194
|
-
if (requested.length === 1) throw new TraceAnalysisLimitError("viewSpans", responseBytes, this.perCallByteCeiling, `viewSpans cannot fit span ${JSON.stringify(id)} in the ${this.perCallByteCeiling}-byte response limit`);
|
|
1195
|
-
spans.pop();
|
|
1196
|
-
omitted.add(id);
|
|
1197
|
-
truncatedAttributeCount -= counter.value;
|
|
1198
|
-
}
|
|
1199
|
-
if (foundById.size > 0 && spans.length === 0) throw new TraceAnalysisLimitError("viewSpans", smallestRejectedBytes, this.perCallByteCeiling, `viewSpans cannot fit one requested span in the ${this.perCallByteCeiling}-byte response limit`);
|
|
1200
|
-
return buildResult();
|
|
1201
|
-
}
|
|
1202
|
-
async searchTrace(opts, context) {
|
|
1203
|
-
const max_matches = validateInteger(opts.max_matches ?? 50, "searchTrace.max_matches", 1, TRACE_ANALYSIS_LIMITS.searchMatches);
|
|
1204
|
-
const trace = (await this.index(context)).byTrace.get(opts.trace_id);
|
|
1205
|
-
if (!trace) throw new TraceNotFoundError(opts.trace_id);
|
|
1206
|
-
const re = compileSearchRegex(opts.regex_pattern);
|
|
1207
|
-
const buf = await this.buffer(context);
|
|
1208
|
-
const hits = [];
|
|
1209
|
-
let hasMore = false;
|
|
1210
|
-
let spanIndex = 0;
|
|
1211
|
-
for (const s of trace.spans) {
|
|
1212
|
-
await scanCheckpoint(context?.signal, spanIndex++);
|
|
1213
|
-
const remaining = max_matches - hits.length;
|
|
1214
|
-
const localHits = await this.scanSpanForMatches(buf, trace.trace_id, s, re, this.perMatchTextBudget, remaining, context);
|
|
1215
|
-
for (const h of localHits.records) {
|
|
1216
|
-
if (hits.length >= max_matches) break;
|
|
1217
|
-
hits.push(h);
|
|
1218
|
-
}
|
|
1219
|
-
if (localHits.hasMore) {
|
|
1220
|
-
hasMore = true;
|
|
1221
|
-
break;
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
return {
|
|
1225
|
-
trace_id: trace.trace_id,
|
|
1226
|
-
hits,
|
|
1227
|
-
has_more: hasMore
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
async searchSpan(opts, context) {
|
|
1231
|
-
const max_matches = validateInteger(opts.max_matches ?? 50, "searchSpan.max_matches", 1, TRACE_ANALYSIS_LIMITS.searchMatches);
|
|
1232
|
-
const trace = (await this.index(context)).byTrace.get(opts.trace_id);
|
|
1233
|
-
if (!trace) throw new TraceNotFoundError(opts.trace_id);
|
|
1234
|
-
let span;
|
|
1235
|
-
for (let index = 0; index < trace.spans.length; index += 1) {
|
|
1236
|
-
await scanCheckpoint(context?.signal, index);
|
|
1237
|
-
const candidate = trace.spans[index];
|
|
1238
|
-
if (candidate.span_id === opts.span_id) {
|
|
1239
|
-
span = candidate;
|
|
1240
|
-
break;
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
if (!span) throw new SpanNotFoundError(opts.trace_id, opts.span_id);
|
|
1244
|
-
const re = compileSearchRegex(opts.regex_pattern);
|
|
1245
|
-
const buf = await this.buffer(context);
|
|
1246
|
-
const localHits = await this.scanSpanForMatches(buf, trace.trace_id, span, re, this.perMatchTextBudget, max_matches, context);
|
|
1247
|
-
return {
|
|
1248
|
-
trace_id: trace.trace_id,
|
|
1249
|
-
span_id: span.span_id,
|
|
1250
|
-
hits: localHits.records,
|
|
1251
|
-
has_more: localHits.hasMore
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
/** Force the index to materialise. Useful to amortise startup cost
|
|
1255
|
-
* before the first agent call. */
|
|
1256
|
-
async ensureIndexed(context) {
|
|
1257
|
-
await this.index(context);
|
|
1258
|
-
}
|
|
1259
|
-
async buffer(context) {
|
|
1260
|
-
context?.signal?.throwIfAborted();
|
|
1261
|
-
if (this.bufferValue) return this.bufferValue;
|
|
1262
|
-
if (!this.bufferTask) {
|
|
1263
|
-
const task = createSharedAbortableTask((signal) => this.readBuffer(signal));
|
|
1264
|
-
this.bufferTask = task;
|
|
1265
|
-
task.promise.then((value) => {
|
|
1266
|
-
this.bufferValue = value;
|
|
1267
|
-
if (this.bufferTask === task) this.bufferTask = void 0;
|
|
1268
|
-
}, () => {
|
|
1269
|
-
if (this.bufferTask === task) this.bufferTask = void 0;
|
|
1270
|
-
});
|
|
1271
|
-
}
|
|
1272
|
-
return waitForSharedTask(this.bufferTask, context?.signal);
|
|
1273
|
-
}
|
|
1274
|
-
async index(context) {
|
|
1275
|
-
context?.signal?.throwIfAborted();
|
|
1276
|
-
if (this.indexValue) return this.indexValue;
|
|
1277
|
-
if (!this.indexTask) {
|
|
1278
|
-
const task = createSharedAbortableTask((signal) => this.buildIndex(signal));
|
|
1279
|
-
this.indexTask = task;
|
|
1280
|
-
task.promise.then((value) => {
|
|
1281
|
-
this.indexValue = value;
|
|
1282
|
-
if (this.indexTask === task) this.indexTask = void 0;
|
|
1283
|
-
}, () => {
|
|
1284
|
-
if (this.indexTask === task) this.indexTask = void 0;
|
|
1285
|
-
});
|
|
1286
|
-
}
|
|
1287
|
-
return waitForSharedTask(this.indexTask, context?.signal);
|
|
1288
|
-
}
|
|
1289
|
-
async buildIndex(signal) {
|
|
1290
|
-
const buf = await this.buffer({ signal });
|
|
1291
|
-
signal?.throwIfAborted();
|
|
1292
|
-
const byTrace = /* @__PURE__ */ new Map();
|
|
1293
|
-
let cursor = 0;
|
|
1294
|
-
let lineNumber = 0;
|
|
1295
|
-
let sinceYield = 0;
|
|
1296
|
-
while (cursor < buf.length) {
|
|
1297
|
-
lineNumber += 1;
|
|
1298
|
-
if (++sinceYield >= INDEX_YIELD_LINES) {
|
|
1299
|
-
sinceYield = 0;
|
|
1300
|
-
await yieldToEventLoop();
|
|
1301
|
-
signal?.throwIfAborted();
|
|
1302
|
-
}
|
|
1303
|
-
const newlineIndex = buf.indexOf(10, cursor);
|
|
1304
|
-
const lineEnd = newlineIndex === -1 ? buf.length : newlineIndex;
|
|
1305
|
-
const lineLength = lineEnd - cursor;
|
|
1306
|
-
if (lineLength === 0) {
|
|
1307
|
-
cursor = lineEnd + 1;
|
|
1308
|
-
continue;
|
|
1309
|
-
}
|
|
1310
|
-
const lineSlice = buf.subarray(cursor, lineEnd).toString("utf8");
|
|
1311
|
-
const lineOffset = cursor;
|
|
1312
|
-
cursor = lineEnd + 1;
|
|
1313
|
-
let parsed;
|
|
1314
|
-
try {
|
|
1315
|
-
parsed = JSON.parse(lineSlice);
|
|
1316
|
-
} catch (cause) {
|
|
1317
|
-
throw new TraceFileMalformedError(this.sourcePath(), lineNumber, lineOffset, cause);
|
|
1318
|
-
}
|
|
1319
|
-
if (!parsed || typeof parsed !== "object") throw new TraceFileMalformedError(this.sourcePath(), lineNumber, lineOffset);
|
|
1320
|
-
const span = projectOtlpFlatLine(parsed);
|
|
1321
|
-
if (!span) throw new TraceFileMalformedError(this.sourcePath(), lineNumber, lineOffset);
|
|
1322
|
-
let entry = byTrace.get(span.trace_id);
|
|
1323
|
-
if (!entry) {
|
|
1324
|
-
entry = {
|
|
1325
|
-
trace_id: span.trace_id,
|
|
1326
|
-
service_name: span.service_name,
|
|
1327
|
-
agent_name: span.agent_name,
|
|
1328
|
-
span_count: 0,
|
|
1329
|
-
has_errors: false,
|
|
1330
|
-
start_time: span.start_time,
|
|
1331
|
-
end_time: span.end_time,
|
|
1332
|
-
duration_ms: 0,
|
|
1333
|
-
raw_jsonl_bytes: 0,
|
|
1334
|
-
models: /* @__PURE__ */ new Set(),
|
|
1335
|
-
tools: /* @__PURE__ */ new Set(),
|
|
1336
|
-
spans: []
|
|
1337
|
-
};
|
|
1338
|
-
byTrace.set(span.trace_id, entry);
|
|
1339
|
-
} else {
|
|
1340
|
-
if (!entry.service_name && span.service_name) entry.service_name = span.service_name;
|
|
1341
|
-
if (!entry.agent_name && span.agent_name) entry.agent_name = span.agent_name;
|
|
1342
|
-
}
|
|
1343
|
-
const indexEntry = {
|
|
1344
|
-
span_id: span.span_id,
|
|
1345
|
-
parent_span_id: span.parent_span_id,
|
|
1346
|
-
name: span.name,
|
|
1347
|
-
kind: span.kind,
|
|
1348
|
-
start_time: span.start_time,
|
|
1349
|
-
end_time: span.end_time,
|
|
1350
|
-
duration_ms: span.duration_ms,
|
|
1351
|
-
status: span.status,
|
|
1352
|
-
status_message: span.status_message,
|
|
1353
|
-
service_name: span.service_name,
|
|
1354
|
-
agent_name: span.agent_name,
|
|
1355
|
-
model_name: span.model_name,
|
|
1356
|
-
tool_name: span.tool_name,
|
|
1357
|
-
line_byte_offset: lineOffset,
|
|
1358
|
-
line_byte_length: lineLength
|
|
1359
|
-
};
|
|
1360
|
-
entry.spans.push(indexEntry);
|
|
1361
|
-
entry.span_count += 1;
|
|
1362
|
-
entry.raw_jsonl_bytes += lineLength + 1;
|
|
1363
|
-
if (span.status === "ERROR") entry.has_errors = true;
|
|
1364
|
-
if (compareSpanTime(span.start_time, entry.start_time) < 0) entry.start_time = span.start_time;
|
|
1365
|
-
if (compareSpanTime(span.end_time, entry.end_time) > 0) entry.end_time = span.end_time;
|
|
1366
|
-
if (span.model_name) entry.models.add(span.model_name);
|
|
1367
|
-
if (span.tool_name) entry.tools.add(span.tool_name);
|
|
1368
|
-
}
|
|
1369
|
-
let totalRawBytes = 0;
|
|
1370
|
-
for (const t of byTrace.values()) {
|
|
1371
|
-
signal?.throwIfAborted();
|
|
1372
|
-
totalRawBytes += t.raw_jsonl_bytes;
|
|
1373
|
-
t.spans.sort((a, b) => compareSpanTime(a.start_time, b.start_time) || a.line_byte_offset - b.line_byte_offset);
|
|
1374
|
-
const startMs = spanEpochMillis(t.start_time);
|
|
1375
|
-
const endMs = spanEpochMillis(t.end_time);
|
|
1376
|
-
t.duration_ms = startMs === null || endMs === null ? 0 : Math.max(0, endMs - startMs);
|
|
1377
|
-
}
|
|
1378
|
-
const sortedTraceIds = [...byTrace.keys()].sort();
|
|
1379
|
-
signal?.throwIfAborted();
|
|
1380
|
-
return {
|
|
1381
|
-
byTrace,
|
|
1382
|
-
totalRawBytes,
|
|
1383
|
-
sortedTraceIds
|
|
1384
|
-
};
|
|
1385
|
-
}
|
|
1386
|
-
async matchedTraces(idx, filters, context) {
|
|
1387
|
-
context?.signal?.throwIfAborted();
|
|
1388
|
-
const traces = [];
|
|
1389
|
-
for (let index = 0; index < idx.sortedTraceIds.length; index += 1) {
|
|
1390
|
-
await scanCheckpoint(context?.signal, index);
|
|
1391
|
-
const trace = idx.byTrace.get(idx.sortedTraceIds[index]);
|
|
1392
|
-
if (trace) traces.push(trace);
|
|
1393
|
-
}
|
|
1394
|
-
if (!filters) return traces;
|
|
1395
|
-
const indexedFiltered = [];
|
|
1396
|
-
for (let index = 0; index < traces.length; index += 1) {
|
|
1397
|
-
await scanCheckpoint(context?.signal, index);
|
|
1398
|
-
const t = traces[index];
|
|
1399
|
-
if (filters.has_errors !== void 0 && t.has_errors !== filters.has_errors) continue;
|
|
1400
|
-
if (filters.service_names && filters.service_names.length > 0) {
|
|
1401
|
-
if (!t.service_name || !filters.service_names.includes(t.service_name)) continue;
|
|
1402
|
-
}
|
|
1403
|
-
if (filters.agent_names && filters.agent_names.length > 0) {
|
|
1404
|
-
if (!t.agent_name || !filters.agent_names.includes(t.agent_name)) continue;
|
|
1405
|
-
}
|
|
1406
|
-
if (filters.model_names && filters.model_names.length > 0) {
|
|
1407
|
-
if (![...t.models].some((m) => filters.model_names.includes(m))) continue;
|
|
1408
|
-
}
|
|
1409
|
-
if (filters.tool_names && filters.tool_names.length > 0) {
|
|
1410
|
-
if (![...t.tools].some((tn) => filters.tool_names.includes(tn))) continue;
|
|
1411
|
-
}
|
|
1412
|
-
if (filters.start_time_after && t.start_time < filters.start_time_after) continue;
|
|
1413
|
-
if (filters.start_time_before && t.start_time > filters.start_time_before) continue;
|
|
1414
|
-
indexedFiltered.push(t);
|
|
1415
|
-
}
|
|
1416
|
-
if (!filters.regex_pattern) return indexedFiltered;
|
|
1417
|
-
const re = compileSearchRegex(filters.regex_pattern);
|
|
1418
|
-
const buf = await this.buffer(context);
|
|
1419
|
-
const out = [];
|
|
1420
|
-
let traceIndex = 0;
|
|
1421
|
-
for (const t of indexedFiltered) {
|
|
1422
|
-
await scanCheckpoint(context?.signal, traceIndex++);
|
|
1423
|
-
let matched = false;
|
|
1424
|
-
let spanIndex = 0;
|
|
1425
|
-
for (const s of t.spans) {
|
|
1426
|
-
await scanCheckpoint(context?.signal, spanIndex++);
|
|
1427
|
-
const slice = buf.subarray(s.line_byte_offset, s.line_byte_offset + s.line_byte_length);
|
|
1428
|
-
if (re.test(slice.toString("utf8"))) {
|
|
1429
|
-
matched = true;
|
|
1430
|
-
break;
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
if (matched) out.push(t);
|
|
1434
|
-
}
|
|
1435
|
-
return out;
|
|
1436
|
-
}
|
|
1437
|
-
toSummary(t) {
|
|
1438
|
-
return {
|
|
1439
|
-
trace_id: t.trace_id,
|
|
1440
|
-
service_name: t.service_name,
|
|
1441
|
-
agent_name: t.agent_name,
|
|
1442
|
-
span_count: t.span_count,
|
|
1443
|
-
has_errors: t.has_errors,
|
|
1444
|
-
start_time: t.start_time,
|
|
1445
|
-
end_time: t.end_time,
|
|
1446
|
-
duration_ms: t.duration_ms,
|
|
1447
|
-
raw_jsonl_bytes: t.raw_jsonl_bytes,
|
|
1448
|
-
models: [...t.models].sort(),
|
|
1449
|
-
tools: [...t.tools].sort()
|
|
1450
|
-
};
|
|
1451
|
-
}
|
|
1452
|
-
projectSpan(buf, trace_id, s, perAttrCap, counter) {
|
|
1453
|
-
const slice = buf.subarray(s.line_byte_offset, s.line_byte_offset + s.line_byte_length).toString("utf8");
|
|
1454
|
-
let raw = {};
|
|
1455
|
-
try {
|
|
1456
|
-
const parsed = JSON.parse(slice);
|
|
1457
|
-
if (parsed && typeof parsed === "object") raw = parsed;
|
|
1458
|
-
} catch {}
|
|
1459
|
-
const attrs = extractOtlpAttributes(raw);
|
|
1460
|
-
const projected = {};
|
|
1461
|
-
for (const [k, v] of Object.entries(attrs)) if (typeof v === "string") {
|
|
1462
|
-
const trunc = truncateForBudget(v, perAttrCap);
|
|
1463
|
-
if (trunc !== v) counter.value += 1;
|
|
1464
|
-
projected[k] = trunc;
|
|
1465
|
-
} else if (Array.isArray(v) || v && typeof v === "object") {
|
|
1466
|
-
const json = JSON.stringify(v);
|
|
1467
|
-
const trunc = truncateForBudget(json, perAttrCap);
|
|
1468
|
-
if (trunc !== json) {
|
|
1469
|
-
counter.value += 1;
|
|
1470
|
-
projected[k] = trunc;
|
|
1471
|
-
} else projected[k] = v;
|
|
1472
|
-
} else projected[k] = v;
|
|
1473
|
-
return {
|
|
1474
|
-
trace_id,
|
|
1475
|
-
span_id: s.span_id,
|
|
1476
|
-
parent_span_id: s.parent_span_id,
|
|
1477
|
-
name: s.name,
|
|
1478
|
-
kind: s.kind,
|
|
1479
|
-
start_time: s.start_time,
|
|
1480
|
-
end_time: s.end_time,
|
|
1481
|
-
duration_ms: s.duration_ms,
|
|
1482
|
-
status: s.status,
|
|
1483
|
-
status_message: s.status_message,
|
|
1484
|
-
service_name: s.service_name,
|
|
1485
|
-
agent_name: s.agent_name,
|
|
1486
|
-
model_name: s.model_name,
|
|
1487
|
-
tool_name: s.tool_name,
|
|
1488
|
-
attributes: projected
|
|
1489
|
-
};
|
|
1490
|
-
}
|
|
1491
|
-
buildOversizedSummary(t, span_response_bytes_max) {
|
|
1492
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1493
|
-
let errorCount = 0;
|
|
1494
|
-
for (const s of t.spans) {
|
|
1495
|
-
counts.set(s.name, (counts.get(s.name) ?? 0) + 1);
|
|
1496
|
-
if (s.status === "ERROR") errorCount += 1;
|
|
1497
|
-
}
|
|
1498
|
-
const top = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20);
|
|
1499
|
-
return {
|
|
1500
|
-
span_count: t.span_count,
|
|
1501
|
-
top_span_names: top,
|
|
1502
|
-
span_response_bytes_max,
|
|
1503
|
-
error_span_count: errorCount
|
|
1504
|
-
};
|
|
1505
|
-
}
|
|
1506
|
-
async scanSpanForMatches(buf, trace_id, s, re, textBudget, recordCap, context) {
|
|
1507
|
-
const slice = buf.subarray(s.line_byte_offset, s.line_byte_offset + s.line_byte_length).toString("utf8");
|
|
1508
|
-
const records = [];
|
|
1509
|
-
let hasMore = false;
|
|
1510
|
-
let matchIndex = 0;
|
|
1511
|
-
for (const match of re.matchAll(slice)) {
|
|
1512
|
-
await scanCheckpoint(context?.signal, matchIndex++);
|
|
1513
|
-
if (records.length >= recordCap) {
|
|
1514
|
-
hasMore = true;
|
|
1515
|
-
break;
|
|
1516
|
-
}
|
|
1517
|
-
const offset = match.index ?? 0;
|
|
1518
|
-
const before = slice.slice(Math.max(0, offset - textBudget / 2), offset);
|
|
1519
|
-
const after = slice.slice(offset + match[0].length, offset + match[0].length + Math.floor(textBudget / 2));
|
|
1520
|
-
records.push({
|
|
1521
|
-
trace_id,
|
|
1522
|
-
span_id: s.span_id,
|
|
1523
|
-
span_name: s.name,
|
|
1524
|
-
span_kind: s.kind,
|
|
1525
|
-
attribute_path: bestAttributePathForOffset(slice, offset) ?? "span.raw",
|
|
1526
|
-
matched_text: truncateForBudget(match[0], textBudget),
|
|
1527
|
-
context_before: truncateForBudget(before, textBudget),
|
|
1528
|
-
context_after: truncateForBudget(after, textBudget),
|
|
1529
|
-
match_offset: offset
|
|
1530
|
-
});
|
|
1531
|
-
}
|
|
1532
|
-
return {
|
|
1533
|
-
records,
|
|
1534
|
-
hasMore
|
|
1535
|
-
};
|
|
1536
|
-
}
|
|
1537
|
-
};
|
|
1538
|
-
var OtlpFileTraceStore = class extends BufferedOtlpTraceStore {
|
|
1539
|
-
path;
|
|
1540
|
-
maxFileBytes;
|
|
1541
|
-
constructor(opts) {
|
|
1542
|
-
super(opts);
|
|
1543
|
-
this.path = opts.path;
|
|
1544
|
-
this.maxFileBytes = validateInteger(opts.maxFileBytes ?? 268435456, "maxFileBytes", 1);
|
|
1545
|
-
}
|
|
1546
|
-
/** Stat-then-read so an oversized file fails loud before allocating the
|
|
1547
|
-
* source buffer. Missing files remain distinct from malformed traces. */
|
|
1548
|
-
async readBuffer(signal) {
|
|
1549
|
-
signal?.throwIfAborted();
|
|
1550
|
-
let stats;
|
|
1551
|
-
try {
|
|
1552
|
-
stats = await stat(this.path);
|
|
1553
|
-
} catch (err) {
|
|
1554
|
-
if (err?.code === "ENOENT") throw new TraceFileMissingError(this.path);
|
|
1555
|
-
throw err;
|
|
1556
|
-
}
|
|
1557
|
-
if (stats.size > this.maxFileBytes) throw new TraceFileTooLargeError(this.path, stats.size, this.maxFileBytes);
|
|
1558
|
-
signal?.throwIfAborted();
|
|
1559
|
-
return readFile(this.path, { signal });
|
|
1560
|
-
}
|
|
1561
|
-
sourcePath() {
|
|
1562
|
-
return this.path;
|
|
1563
|
-
}
|
|
1564
|
-
};
|
|
1565
|
-
var OtlpBufferTraceStore = class extends BufferedOtlpTraceStore {
|
|
1566
|
-
source;
|
|
1567
|
-
constructor(source, opts) {
|
|
1568
|
-
super(opts);
|
|
1569
|
-
this.source = source;
|
|
1570
|
-
}
|
|
1571
|
-
async readBuffer(signal) {
|
|
1572
|
-
signal?.throwIfAborted();
|
|
1573
|
-
return this.source;
|
|
1574
|
-
}
|
|
1575
|
-
sourcePath() {
|
|
1576
|
-
return "<memory>";
|
|
1577
|
-
}
|
|
1578
|
-
};
|
|
1579
|
-
/** Build the bounded trace store directly from OpenInference JSONL text. */
|
|
1580
|
-
function otlpTextToTraceAnalysisStore(text, opts = {}) {
|
|
1581
|
-
if (!text.trim()) throw new CaptureIntegrityError("OTLP trace text must not be empty");
|
|
1582
|
-
return new OtlpBufferTraceStore(Buffer.from(text, "utf8"), opts);
|
|
1583
|
-
}
|
|
1584
|
-
function createOtlpBufferTraceStore(source, options = {}) {
|
|
1585
|
-
return new OtlpBufferTraceStore(source, options);
|
|
1586
|
-
}
|
|
1587
|
-
/** A `"` at `idx` is a real JSON delimiter only when the run of `\`
|
|
1588
|
-
* immediately preceding it is even-length; an odd run means the quote
|
|
1589
|
-
* is escaped (`\"`) and is part of a string value, not a boundary. */
|
|
1590
|
-
function isUnescapedQuote(slice, idx) {
|
|
1591
|
-
if (slice[idx] !== "\"") return false;
|
|
1592
|
-
let backslashes = 0;
|
|
1593
|
-
let b = idx - 1;
|
|
1594
|
-
while (b >= 0 && slice[b] === "\\") {
|
|
1595
|
-
backslashes += 1;
|
|
1596
|
-
b -= 1;
|
|
1597
|
-
}
|
|
1598
|
-
return backslashes % 2 === 0;
|
|
1599
|
-
}
|
|
1600
|
-
/** Scan backwards from `from` (inclusive) for the nearest UNescaped `"`.
|
|
1601
|
-
* Returns its index, or -1 when none is found. */
|
|
1602
|
-
function prevUnescapedQuote(slice, from) {
|
|
1603
|
-
for (let i = from; i >= 0; i -= 1) if (slice[i] === "\"" && isUnescapedQuote(slice, i)) return i;
|
|
1604
|
-
return -1;
|
|
1605
|
-
}
|
|
1606
|
-
/**
|
|
1607
|
-
* Best-effort: locate the JSON path for the substring at `offset` in
|
|
1608
|
-
* a single span's JSONL slice. The slice is '...,"key":"value..."' — we
|
|
1609
|
-
* walk back from `offset` to the value-opening quote, past the `:`, to
|
|
1610
|
-
* the key's closing then opening quote, skipping `\"`-escaped quotes that
|
|
1611
|
-
* live inside string values. Returns `null` when the offset doesn't fall
|
|
1612
|
-
* inside a recognisable string field.
|
|
1613
|
-
*/
|
|
1614
|
-
function bestAttributePathForOffset(slice, offset) {
|
|
1615
|
-
const valueQuote = prevUnescapedQuote(slice, Math.min(offset, slice.length - 1));
|
|
1616
|
-
if (valueQuote < 1) return null;
|
|
1617
|
-
let j = valueQuote - 1;
|
|
1618
|
-
while (j >= 0 && slice[j] !== ":") j -= 1;
|
|
1619
|
-
if (j < 1) return null;
|
|
1620
|
-
const keyClose = prevUnescapedQuote(slice, j - 1);
|
|
1621
|
-
if (keyClose < 1) return null;
|
|
1622
|
-
const keyOpen = prevUnescapedQuote(slice, keyClose - 1);
|
|
1623
|
-
if (keyOpen < 0) return null;
|
|
1624
|
-
return slice.slice(keyOpen + 1, keyClose);
|
|
1625
|
-
}
|
|
1626
|
-
const ERROR_CLUSTER_EXEMPLARS = 5;
|
|
1627
|
-
/** Collapse volatile tokens so semantically identical failures share a key:
|
|
1628
|
-
* hex/uuid ids → <id>, numbers → #, quoted/abs paths → <path>, durations →
|
|
1629
|
-
* <dur>, whitespace collapsed. Empty/absent messages fall back to the span
|
|
1630
|
-
* name so a no-message error still forms a real cluster. */
|
|
1631
|
-
function normalizeErrorSignature(message, spanName) {
|
|
1632
|
-
const raw = (message ?? "").trim();
|
|
1633
|
-
return (raw.length > 0 ? raw : `(${spanName || "error"} — no message)`).replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "<id>").replace(/\b[0-9a-f]{12,}\b/gi, "<id>").replace(/(?:\/[\w.\-@]+){2,}/g, "<path>").replace(/\b\d+(?:\.\d+)?(ms|s|m|h|kb|mb|gb)?\b/gi, (_m, u) => u ? `#${u}` : "#").replace(/\s+/g, " ").trim();
|
|
1634
|
-
}
|
|
1635
|
-
function bump(map, key) {
|
|
1636
|
-
if (!key) return;
|
|
1637
|
-
map.set(key, (map.get(key) ?? 0) + 1);
|
|
1638
|
-
}
|
|
1639
|
-
function topKey(map) {
|
|
1640
|
-
let best = null;
|
|
1641
|
-
let bestN = 0;
|
|
1642
|
-
for (const [k, n] of map) if (n > bestN) {
|
|
1643
|
-
best = k;
|
|
1644
|
-
bestN = n;
|
|
1645
|
-
}
|
|
1646
|
-
return best;
|
|
1647
|
-
}
|
|
1648
|
-
function accumulateErrorCluster(clusters, traceId, span) {
|
|
1649
|
-
const signature = normalizeErrorSignature(span.status_message, span.name);
|
|
1650
|
-
let acc = clusters.get(signature);
|
|
1651
|
-
if (!acc) {
|
|
1652
|
-
acc = {
|
|
1653
|
-
signature,
|
|
1654
|
-
sample: span.status_message ?? span.name ?? "",
|
|
1655
|
-
traceIds: /* @__PURE__ */ new Set(),
|
|
1656
|
-
spanIds: [],
|
|
1657
|
-
spanCount: 0,
|
|
1658
|
-
spanNames: /* @__PURE__ */ new Map(),
|
|
1659
|
-
toolNames: /* @__PURE__ */ new Map()
|
|
1660
|
-
};
|
|
1661
|
-
clusters.set(signature, acc);
|
|
1662
|
-
}
|
|
1663
|
-
acc.traceIds.add(traceId);
|
|
1664
|
-
acc.spanCount += 1;
|
|
1665
|
-
if (acc.spanIds.length < ERROR_CLUSTER_EXEMPLARS && !acc.spanIds.includes(span.span_id)) acc.spanIds.push(span.span_id);
|
|
1666
|
-
bump(acc.spanNames, span.name);
|
|
1667
|
-
bump(acc.toolNames, span.tool_name);
|
|
1668
|
-
}
|
|
1669
|
-
function finalizeErrorClusters(clusters, errorTraceCount) {
|
|
1670
|
-
const out = [...clusters.values()].map((acc) => ({
|
|
1671
|
-
signature: acc.signature,
|
|
1672
|
-
status_message_sample: acc.sample,
|
|
1673
|
-
span_name: topKey(acc.spanNames),
|
|
1674
|
-
tool_name: topKey(acc.toolNames),
|
|
1675
|
-
trace_count: acc.traceIds.size,
|
|
1676
|
-
span_count: acc.spanCount,
|
|
1677
|
-
prevalence: errorTraceCount > 0 ? acc.traceIds.size / errorTraceCount : 0,
|
|
1678
|
-
exemplar_trace_ids: [...acc.traceIds].slice(0, ERROR_CLUSTER_EXEMPLARS),
|
|
1679
|
-
exemplar_span_ids: acc.spanIds.slice(0, ERROR_CLUSTER_EXEMPLARS)
|
|
1680
|
-
}));
|
|
1681
|
-
out.sort((a, b) => b.trace_count - a.trace_count || b.span_count - a.span_count);
|
|
1682
|
-
return out;
|
|
1683
|
-
}
|
|
1684
|
-
const TRACE_ANALYSIS_COMPLETION_INSTRUCTION = `This host consumes your executor result directly; there is no downstream responder for this run. Ignore generic executor guidance that says a responder will format the answer. You must produce the final report and findings yourself.
|
|
1685
|
-
|
|
1686
|
-
Return exactly one executable JavaScript program per turn. Never emit multiple JavaScript or code fences; put every tool call and the final call in that one program.
|
|
1687
|
-
|
|
1688
|
-
When the analysis is complete, call \`await final(${JSON.stringify("Submit the completed trace analysis.")}, { report, findings })\` exactly once. Do not return a one-argument string from \`final(...)\`.`;
|
|
1689
|
-
var TraceAnalysisTurnLimitError = class extends Error {
|
|
1690
|
-
analystId;
|
|
1691
|
-
maxTurns;
|
|
1692
|
-
constructor(analystId, maxTurns, cause) {
|
|
1693
|
-
super(`Trace analyst '${analystId}' reached maxTurns=${maxTurns} without a structured final result`, { cause });
|
|
1694
|
-
this.name = "TraceAnalysisTurnLimitError";
|
|
1695
|
-
this.analystId = analystId;
|
|
1696
|
-
this.maxTurns = maxTurns;
|
|
1697
|
-
}
|
|
1698
|
-
};
|
|
1699
|
-
async function runTraceAnalysisLoop(options) {
|
|
1700
|
-
validateLoopLimits(options);
|
|
1701
|
-
const config = {
|
|
1702
|
-
agentIdentity: {
|
|
1703
|
-
name: options.id,
|
|
1704
|
-
description: options.description
|
|
1705
|
-
},
|
|
1706
|
-
contextFields: [],
|
|
1707
|
-
runtime: new AxJSRuntime({
|
|
1708
|
-
permissions: [],
|
|
1709
|
-
blockDynamicImport: true,
|
|
1710
|
-
allowedModules: [],
|
|
1711
|
-
freezeIntrinsics: true,
|
|
1712
|
-
blockShadowRealm: true,
|
|
1713
|
-
preventGlobalThisExtensions: false
|
|
1714
|
-
}),
|
|
1715
|
-
maxSubAgentCalls: options.maxSubqueries,
|
|
1716
|
-
maxTurns: options.maxTurns,
|
|
1717
|
-
maxRuntimeChars: options.maxRuntimeChars,
|
|
1718
|
-
maxBatchedLlmQueryConcurrency: options.maxParallelSubqueries,
|
|
1719
|
-
promptLevel: "detailed",
|
|
1720
|
-
contextPolicy: {
|
|
1721
|
-
preset: "full",
|
|
1722
|
-
budget: "balanced"
|
|
1723
|
-
},
|
|
1724
|
-
functions: options.tools,
|
|
1725
|
-
executorOptions: {
|
|
1726
|
-
description: `${options.prompt.trim()}\n\n${TRACE_ANALYSIS_COMPLETION_INSTRUCTION}`,
|
|
1727
|
-
...options.model ? { model: options.model } : {},
|
|
1728
|
-
showThoughts: false,
|
|
1729
|
-
thinkingTokenBudget: "none"
|
|
1730
|
-
},
|
|
1731
|
-
...options.onTurn ? { actorTurnCallback: options.onTurn } : {},
|
|
1732
|
-
bubbleErrors: [TraceFileMissingError]
|
|
1733
|
-
};
|
|
1734
|
-
const analyst = options.findingType === "string" ? agent("question:string -> report:string, findings:string[]", config) : agent("question:string -> report:string, findings:json[]", config);
|
|
1735
|
-
const state = await analyst.executor.run(options.ai, { question: options.question }, options.signal ? { abortSignal: options.signal } : void 0);
|
|
1736
|
-
let completed;
|
|
1737
|
-
try {
|
|
1738
|
-
completed = options.findingType === "string" ? readTraceAnalysisCompletion(state.executorResult, "string") : readTraceAnalysisCompletion(state.executorResult, "object");
|
|
1739
|
-
} catch (error) {
|
|
1740
|
-
if (state.turnCount >= options.maxTurns) throw new TraceAnalysisTurnLimitError(options.id, options.maxTurns, error);
|
|
1741
|
-
throw new Error(`Trace analyst '${options.id}' stopped without a structured final result`, { cause: error });
|
|
1742
|
-
}
|
|
1743
|
-
return {
|
|
1744
|
-
...completed,
|
|
1745
|
-
usage: analyst.executor.getUsage(),
|
|
1746
|
-
chatLog: analyst.executor.getChatLog(),
|
|
1747
|
-
turnCount: state.turnCount
|
|
1748
|
-
};
|
|
1749
|
-
}
|
|
1750
|
-
function readTraceAnalysisCompletion(value, findingType) {
|
|
1751
|
-
if (!value || typeof value !== "object") throw new Error("Trace analyst did not return a structured final result");
|
|
1752
|
-
const completion = value;
|
|
1753
|
-
if (completion.type !== "final" || !Array.isArray(completion.args) || completion.args.length !== 2 || completion.args[0] !== "Submit the completed trace analysis.") throw new Error("Trace analyst did not return a structured final result");
|
|
1754
|
-
const payload = completion.args[1];
|
|
1755
|
-
if (!payload || typeof payload !== "object") throw new Error("Trace analyst final result must contain report and findings");
|
|
1756
|
-
const { report, findings } = payload;
|
|
1757
|
-
if (typeof report !== "string" || !Array.isArray(findings)) throw new Error("Trace analyst final result must contain report and findings");
|
|
1758
|
-
if (findingType === "string") {
|
|
1759
|
-
if (findings.some((finding) => typeof finding !== "string")) throw new Error("Trace analyst final result must contain string findings");
|
|
1760
|
-
return {
|
|
1761
|
-
report,
|
|
1762
|
-
findings
|
|
1763
|
-
};
|
|
1764
|
-
}
|
|
1765
|
-
if (findings.some((finding) => !finding || typeof finding !== "object" || Array.isArray(finding))) throw new Error("Trace analyst final result must contain object findings");
|
|
1766
|
-
return {
|
|
1767
|
-
report,
|
|
1768
|
-
findings
|
|
1769
|
-
};
|
|
1770
|
-
}
|
|
1771
|
-
function validateLoopLimits(options) {
|
|
1772
|
-
if (!Number.isSafeInteger(options.maxSubqueries) || options.maxSubqueries < 0) throw new TypeError("maxSubqueries must be a non-negative integer");
|
|
1773
|
-
if (!Number.isSafeInteger(options.maxParallelSubqueries) || options.maxParallelSubqueries < 1) throw new TypeError("maxParallelSubqueries must be a positive integer");
|
|
1774
|
-
}
|
|
1775
|
-
//#endregion
|
|
1776
|
-
//#region src/trace-analyst/tools.ts
|
|
1777
|
-
const TRACE_ANALYST_TOOL_NAMESPACE = "traces";
|
|
1778
|
-
/** Bind all seven trace reads without exposing an agent framework type. */
|
|
1779
|
-
function buildTraceAnalysisToolDescriptors(options) {
|
|
1780
|
-
const store = createBoundedTraceAnalysisStore(options.store, { budgets: options.budgets });
|
|
1781
|
-
return [
|
|
1782
|
-
{
|
|
1783
|
-
namespace: TRACE_ANALYST_TOOL_NAMESPACE,
|
|
1784
|
-
name: "getDatasetOverview",
|
|
1785
|
-
description: "Dataset rollup: total traces, raw_jsonl_bytes, services, agents, models, tools, and sample_trace_ids. Always call this first without a regex_pattern.",
|
|
1786
|
-
parameters: toTraceJsonSchema(traceStoreInputSchemas.getOverview),
|
|
1787
|
-
handler: async (args, context) => {
|
|
1788
|
-
const { filters } = parseTraceInput("getDatasetOverview", traceStoreInputSchemas.getOverview, args ?? {});
|
|
1789
|
-
return store.getOverview(filters, context);
|
|
1790
|
-
}
|
|
1791
|
-
},
|
|
1792
|
-
{
|
|
1793
|
-
namespace: TRACE_ANALYST_TOOL_NAMESPACE,
|
|
1794
|
-
name: "queryTraces",
|
|
1795
|
-
description: `Paginated trace summaries, at most ${TRACE_ANALYSIS_LIMITS.queryTraces} per call. Each summary carries raw_jsonl_bytes; narrow with indexed filters before regex_pattern.`,
|
|
1796
|
-
parameters: toTraceJsonSchema(traceStoreInputSchemas.queryTraces),
|
|
1797
|
-
handler: async (args, context) => {
|
|
1798
|
-
const input = parseTraceInput("queryTraces", traceStoreInputSchemas.queryTraces, args);
|
|
1799
|
-
return store.queryTraces(input, context);
|
|
1800
|
-
}
|
|
1801
|
-
},
|
|
1802
|
-
{
|
|
1803
|
-
namespace: TRACE_ANALYST_TOOL_NAMESPACE,
|
|
1804
|
-
name: "countTraces",
|
|
1805
|
-
description: "Count traces matching filters. Use as a cheap pre-flight before a regex_pattern scan.",
|
|
1806
|
-
parameters: toTraceJsonSchema(traceStoreInputSchemas.countTraces),
|
|
1807
|
-
handler: async (args, context) => {
|
|
1808
|
-
const { filters } = parseTraceInput("countTraces", traceStoreInputSchemas.countTraces, args ?? {});
|
|
1809
|
-
return store.countTraces(filters, context);
|
|
1810
|
-
}
|
|
1811
|
-
},
|
|
1812
|
-
{
|
|
1813
|
-
namespace: TRACE_ANALYST_TOOL_NAMESPACE,
|
|
1814
|
-
name: "viewTrace",
|
|
1815
|
-
description: "Return all spans for one trace with bounded attributes. Oversized responses carry an oversized summary instead of spans; continue with searchTrace or viewSpans.",
|
|
1816
|
-
parameters: toTraceJsonSchema(traceStoreInputSchemas.viewTrace.omit({ per_attribute_byte_cap: true })),
|
|
1817
|
-
handler: async (args, context) => {
|
|
1818
|
-
const input = parseTraceInput("viewTrace", traceStoreInputSchemas.viewTrace.omit({ per_attribute_byte_cap: true }), args);
|
|
1819
|
-
return store.viewTrace(input, context);
|
|
1820
|
-
}
|
|
1821
|
-
},
|
|
1822
|
-
{
|
|
1823
|
-
namespace: TRACE_ANALYST_TOOL_NAMESPACE,
|
|
1824
|
-
name: "viewSpans",
|
|
1825
|
-
description: `Read 1..${TRACE_ANALYSIS_LIMITS.viewSpans} specific spans. Every requested id is accounted for in spans, missing_span_ids, or omitted_span_ids; retry omitted ids.`,
|
|
1826
|
-
parameters: toTraceJsonSchema(traceStoreInputSchemas.viewSpans.omit({ per_attribute_byte_cap: true })),
|
|
1827
|
-
handler: async (args, context) => {
|
|
1828
|
-
const input = parseTraceInput("viewSpans", traceStoreInputSchemas.viewSpans.omit({ per_attribute_byte_cap: true }), args);
|
|
1829
|
-
return store.viewSpans(input, context);
|
|
1830
|
-
}
|
|
1831
|
-
},
|
|
1832
|
-
{
|
|
1833
|
-
namespace: TRACE_ANALYST_TOOL_NAMESPACE,
|
|
1834
|
-
name: "searchTrace",
|
|
1835
|
-
description: `Regex search across one trace, bounded to ${TRACE_ANALYSIS_LIMITS.searchMatches} hits. When has_more is true, refine the regex instead of treating the result as complete.`,
|
|
1836
|
-
parameters: toTraceJsonSchema(traceStoreInputSchemas.searchTrace),
|
|
1837
|
-
handler: async (args, context) => {
|
|
1838
|
-
const input = parseTraceInput("searchTrace", traceStoreInputSchemas.searchTrace, args);
|
|
1839
|
-
return store.searchTrace(input, context);
|
|
1840
|
-
}
|
|
1841
|
-
},
|
|
1842
|
-
{
|
|
1843
|
-
namespace: TRACE_ANALYST_TOOL_NAMESPACE,
|
|
1844
|
-
name: "searchSpan",
|
|
1845
|
-
description: `Regex search inside one span, bounded to ${TRACE_ANALYSIS_LIMITS.searchMatches} hits. Use after viewSpans omits or truncates a large span payload.`,
|
|
1846
|
-
parameters: toTraceJsonSchema(traceStoreInputSchemas.searchSpan),
|
|
1847
|
-
handler: async (args, context) => {
|
|
1848
|
-
const input = parseTraceInput("searchSpan", traceStoreInputSchemas.searchSpan, args);
|
|
1849
|
-
return store.searchSpan(input, context);
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
];
|
|
1853
|
-
}
|
|
1854
|
-
/** Adapt the canonical descriptors into Ax functions used by the built-in analyst. */
|
|
1855
|
-
function buildTraceAnalystTools(options) {
|
|
1856
|
-
return buildTraceAnalysisToolDescriptors(options).map((descriptor) => ({
|
|
1857
|
-
namespace: descriptor.namespace,
|
|
1858
|
-
name: descriptor.name,
|
|
1859
|
-
description: descriptor.description,
|
|
1860
|
-
parameters: descriptor.parameters,
|
|
1861
|
-
func: (args, extra) => descriptor.handler(args, extra?.abortSignal ? { signal: extra.abortSignal } : void 0)
|
|
1862
|
-
}));
|
|
1863
|
-
}
|
|
1864
|
-
function traceAnalystFunctionGroup(options) {
|
|
1865
|
-
return {
|
|
1866
|
-
namespace: TRACE_ANALYST_TOOL_NAMESPACE,
|
|
1867
|
-
title: "Trace Analysis",
|
|
1868
|
-
selectionCriteria: "Use for any inspection of OTLP-shaped trace data.",
|
|
1869
|
-
description: "Discovery, narrowing, and bounded deep reads over a JSONL trace dataset. Always call getDatasetOverview first.",
|
|
1870
|
-
functions: buildTraceAnalystTools(options)
|
|
1871
|
-
};
|
|
1872
|
-
}
|
|
1873
|
-
//#endregion
|
|
1874
|
-
export { applyToolSpanOtlpAttributes as A, extractOtlpAttributes as C, readOtlpStatus as D, projectOtlpFlatLine as E, isOtlpModelCall as M, traceSpanKindToOpenInferenceKind as N, spanEpochMillis as O, compareSpanTime as S, inferOtlpKind as T, TraceFileMalformedError as _, runTraceAnalysisLoop as a, TraceNotFoundError as b, otlpTextToTraceAnalysisStore as c, TRACE_ANALYST_TRUNCATION_MARKER_PREFIX as d, TRACE_ANALYSIS_LIMITS as f, TraceAnalysisValidationError as g, TraceAnalysisStoreContractError as h, traceAnalystFunctionGroup as i, classifyOtlpSpanRole as j, stringField as k, createBoundedTraceAnalysisStore as l, TraceAnalysisLimitError as m, buildTraceAnalysisToolDescriptors as n, OtlpFileTraceStore as o, SpanNotFoundError as p, buildTraceAnalystTools as r, createOtlpBufferTraceStore as s, TRACE_ANALYST_TOOL_NAMESPACE as t, DEFAULT_TRACE_ANALYST_BUDGETS as u, TraceFileMissingError as v, firstStringAttr as w, asString as x, TraceFileTooLargeError as y };
|
|
1875
|
-
|
|
1876
|
-
//# sourceMappingURL=tools-DZk2Jn64.js.map
|