@tangle-network/traces 0.23.1 → 1.0.1
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/README.md +61 -1078
- package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
- package/dist/ask-2KF4KSPB.js +39 -0
- package/dist/check-YF6ZHPGG.js +322 -0
- package/dist/chunk-2XXABIRL.js +25 -0
- package/dist/chunk-4VKPN432.js +138 -0
- package/dist/chunk-6GOG5KIK.js +53 -0
- package/dist/chunk-7USMM4MA.js +5695 -0
- package/dist/chunk-CG5QHQO4.js +492 -0
- package/dist/chunk-CQ6UYZG6.js +1217 -0
- package/dist/chunk-DU534HAX.js +263 -0
- package/dist/chunk-FC4V25CG.js +503 -0
- package/dist/chunk-GLUDGUGJ.js +232 -0
- package/dist/chunk-GNRXKQEQ.js +120 -0
- package/dist/chunk-K2KH2HHD.js +228 -0
- package/dist/chunk-M3W6TZHQ.js +381 -0
- package/dist/chunk-O52XK7OB.js +19 -0
- package/dist/chunk-PYH3PL3N.js +62 -0
- package/dist/chunk-QLVP37YB.js +782 -0
- package/dist/chunk-QUHUMZ4W.js +759 -0
- package/dist/chunk-REO2JQJY.js +535 -0
- package/dist/chunk-RZL5GAWH.js +60 -0
- package/dist/chunk-TOCNGML5.js +273 -0
- package/dist/chunk-UQSAGXPH.js +208 -0
- package/dist/chunk-VFOLC4OX.js +407 -0
- package/dist/chunk-WF7PTDCZ.js +3921 -0
- package/dist/chunk-YAX72TST.js +259 -0
- package/dist/cli.js +2086 -2050
- package/dist/diff-J4RAIOXO.js +16 -0
- package/dist/improvement-LCCVGGUS.js +25 -0
- package/dist/index.d.ts +1235 -4357
- package/dist/index.js +53 -1204
- package/dist/live-XXL7VZWB.js +14 -0
- package/dist/mcp-EYIDKBEK.js +85 -0
- package/dist/report-E3G2T5TJ.js +338 -0
- package/dist/run-watch-RAOOAXOF.js +649 -0
- package/dist/supervisor-report-RMPLLBGZ.js +29 -0
- package/dist/supervisor-run-context-WO6VOTGV.js +30 -0
- package/dist/upload-3R7GRSKS.js +20 -0
- package/dist/verify-safe-C5ZXZOZ2.js +8 -0
- package/docs/analyze.md +76 -0
- package/docs/ask.md +88 -0
- package/docs/bundle.md +40 -0
- package/docs/check.md +77 -0
- package/docs/convert.md +44 -0
- package/docs/develop.md +32 -0
- package/docs/diff.md +38 -0
- package/docs/facts.md +114 -0
- package/docs/mcp.md +36 -0
- package/docs/migrate-1.0.md +33 -0
- package/docs/parsers.md +66 -0
- package/docs/release-1.0.0.md +147 -0
- package/docs/release-1.0.1.md +4 -0
- package/docs/sdk.md +77 -0
- package/docs/select.md +70 -0
- package/docs/skills.md +46 -0
- package/docs/upload.md +47 -0
- package/docs/watch-jsonl.v1.example.jsonl +5 -0
- package/docs/watch.md +66 -0
- package/examples/README.md +9 -0
- package/examples/custom-analyst.ts +65 -74
- package/install.sh +22 -2
- package/package.json +10 -9
- package/skills/inspect-agent-traces/SKILL.md +47 -110
- package/dist/chunk-7DVZCCZI.js +0 -20983
- package/docs/replay-verify.md +0 -123
- package/docs/stream-findings.svg +0 -39
- package/docs/trace-analysts.md +0 -643
- package/examples/custom-backend.ts +0 -21
- package/examples/external-engines.ts +0 -62
- package/examples/improvement-config.mjs +0 -63
- package/examples/observe-and-alert.ts +0 -19
- package/examples/register-harness.ts +0 -26
- package/skills/build-trace-analyst/SKILL.md +0 -76
- package/skills/build-trace-analyst/agents/openai.yaml +0 -4
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_QUESTION_TIMEOUT_MS
|
|
3
|
+
} from "./chunk-O52XK7OB.js";
|
|
4
|
+
|
|
5
|
+
// src/analyst-model-call.ts
|
|
6
|
+
import { accessSync, constants, readFileSync } from "fs";
|
|
7
|
+
import { delimiter, join } from "path";
|
|
8
|
+
import {
|
|
9
|
+
canonicalAgentProfileDigest,
|
|
10
|
+
canonicalCandidateDigest
|
|
11
|
+
} from "@tangle-network/agent-interface";
|
|
12
|
+
import { profileOptimizerModelCall } from "@tangle-network/agent-runtime/kernel";
|
|
13
|
+
import { createDspyRlmTraceEngine } from "@tangle-network/agent-eval/analyst";
|
|
14
|
+
var TANGLE_ROUTER_BASE_URL = "https://router.tangle.tools/v1";
|
|
15
|
+
var BRIDGE_MODULE = "agent_eval_rpc.dspy_rlm_bridge";
|
|
16
|
+
var ANALYST_MAX_OUTPUT_TOKENS = 16384;
|
|
17
|
+
function bridgeVersion() {
|
|
18
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
19
|
+
const version = pkg.dependencies?.["@tangle-network/agent-eval"];
|
|
20
|
+
if (typeof version !== "string" || !version) {
|
|
21
|
+
throw new Error("package.json is missing the @tangle-network/agent-eval dependency");
|
|
22
|
+
}
|
|
23
|
+
return version;
|
|
24
|
+
}
|
|
25
|
+
function bridgeRunner(env) {
|
|
26
|
+
if (env.TRACES_PYTHON) return { command: env.TRACES_PYTHON };
|
|
27
|
+
return { command: "uvx", args: ["--from", `agent-eval-rpc[dspy]==${bridgeVersion()}`, "python", "-m", BRIDGE_MODULE] };
|
|
28
|
+
}
|
|
29
|
+
function runnable(command, path) {
|
|
30
|
+
const candidates = command.includes("/") ? [command] : (path ?? "").split(delimiter).filter(Boolean).map((dir) => join(dir, command));
|
|
31
|
+
return candidates.some((candidate) => {
|
|
32
|
+
try {
|
|
33
|
+
accessSync(candidate, constants.X_OK);
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function missingAskRequirements(env = process.env) {
|
|
41
|
+
const missing = [];
|
|
42
|
+
if (!env.TANGLE_API_KEY && !env.OPENAI_API_KEY) {
|
|
43
|
+
missing.push(
|
|
44
|
+
"a model key: set TANGLE_API_KEY for the Tangle router, or OPENAI_API_KEY (OPENAI_BASE_URL names any other OpenAI-compatible endpoint)"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const bridge = env.TRACES_PYTHON ? runnable(env.TRACES_PYTHON, env.PATH) : runnable("uvx", env.PATH);
|
|
48
|
+
if (!bridge) {
|
|
49
|
+
missing.push(
|
|
50
|
+
env.TRACES_PYTHON ? `the Python bridge: TRACES_PYTHON=${env.TRACES_PYTHON} is not an executable` : `the Python bridge: install uv (https://docs.astral.sh/uv/), or set TRACES_PYTHON to a Python with agent-eval-rpc[dspy]==${bridgeVersion()}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return missing;
|
|
54
|
+
}
|
|
55
|
+
var UnservableModelError = class extends Error {
|
|
56
|
+
failure;
|
|
57
|
+
constructor(failure) {
|
|
58
|
+
super(failure.message);
|
|
59
|
+
this.name = "UnservableModelError";
|
|
60
|
+
this.failure = failure;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var TRANSIENT_STATUSES = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504, 520, 522, 524]);
|
|
64
|
+
var UNSERVABLE_MODEL = /model_not_allowed|model_not_found|provider_pricing_unavailable|not in this key's allow-list|unknown model/iu;
|
|
65
|
+
function endpointWords(body) {
|
|
66
|
+
const message = /"message":"((?:[^"\\]|\\.)*)/u.exec(body)?.[1];
|
|
67
|
+
return (message ?? body).replace(/\\"/gu, '"').slice(0, 240);
|
|
68
|
+
}
|
|
69
|
+
function classifyModelFailure(error, model) {
|
|
70
|
+
const served = /reported model ("[^"]*") but AgentProfile requires ("[^"]*")/u.exec(error);
|
|
71
|
+
if (served) {
|
|
72
|
+
return {
|
|
73
|
+
kind: "permanent",
|
|
74
|
+
failure: {
|
|
75
|
+
code: "model",
|
|
76
|
+
message: `the endpoint served ${served[1]} for --model ${served[2]}, and ask refuses an answer from a model it did not ask for; pass the provider-qualified model id (for example zai/glm-5.3)`
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const refused = /\brouter (\d{3}): (.*)$/su.exec(error);
|
|
81
|
+
if (!refused) {
|
|
82
|
+
return /router network failure|router request timeout|ECONNRESET|ETIMEDOUT|socket hang up|fetch failed/u.test(error) ? { kind: "transient", reason: "the endpoint could not be reached" } : { kind: "other" };
|
|
83
|
+
}
|
|
84
|
+
const status = Number(refused[1]);
|
|
85
|
+
const words = endpointWords(refused[2]);
|
|
86
|
+
if (status === 401 || status === 402 || status === 403) {
|
|
87
|
+
return {
|
|
88
|
+
kind: "permanent",
|
|
89
|
+
failure: { code: "auth", message: `the model endpoint refused the key (${status}: ${words}). Check TANGLE_API_KEY, or OPENAI_API_KEY and OPENAI_BASE_URL` }
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (UNSERVABLE_MODEL.test(refused[2])) {
|
|
93
|
+
return {
|
|
94
|
+
kind: "permanent",
|
|
95
|
+
failure: { code: "model", message: `the model endpoint cannot serve --model ${model} (${status}: ${words})` }
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return TRANSIENT_STATUSES.has(status) ? { kind: "transient", reason: `${status}: ${words}` } : { kind: "other" };
|
|
99
|
+
}
|
|
100
|
+
var MODEL_CALL_ATTEMPTS = 10;
|
|
101
|
+
var FIRST_RETRY_DELAY_MS = 2e3;
|
|
102
|
+
var MAX_RETRY_DELAY_MS = 6e4;
|
|
103
|
+
function retryDelayMs(attempt) {
|
|
104
|
+
const delay = Math.min(MAX_RETRY_DELAY_MS, FIRST_RETRY_DELAY_MS * 2 ** (attempt - 1));
|
|
105
|
+
return Math.round(delay * (0.75 + Math.random() * 0.5));
|
|
106
|
+
}
|
|
107
|
+
function pause(ms, signal) {
|
|
108
|
+
if (signal?.aborted) return Promise.resolve(false);
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
const done = (slept) => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
signal?.removeEventListener("abort", aborted);
|
|
113
|
+
resolve(slept);
|
|
114
|
+
};
|
|
115
|
+
const aborted = () => done(false);
|
|
116
|
+
const timer = setTimeout(() => done(true), ms);
|
|
117
|
+
signal?.addEventListener("abort", aborted, { once: true });
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function retryingModelCall(call, opts) {
|
|
121
|
+
return async (request) => {
|
|
122
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
123
|
+
const result = await call(request);
|
|
124
|
+
if (result.succeeded) return result;
|
|
125
|
+
const failure = classifyModelFailure(result.error, opts.model);
|
|
126
|
+
if (failure.kind === "permanent") {
|
|
127
|
+
opts.onPermanentFailure?.(failure.failure);
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
if (failure.kind === "other" || attempt >= MODEL_CALL_ATTEMPTS) return result;
|
|
131
|
+
const delay = retryDelayMs(attempt);
|
|
132
|
+
opts.log?.(
|
|
133
|
+
`[analyst] WARN model call failed (${failure.reason}); attempt ${attempt + 1} of ${MODEL_CALL_ATTEMPTS} in ${(delay / 1e3).toFixed(1)} s`
|
|
134
|
+
);
|
|
135
|
+
if (!await pause(delay, request.signal)) return result;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function createAnalystModelOwner(opts) {
|
|
140
|
+
const profile = {
|
|
141
|
+
name: "traces-analyst",
|
|
142
|
+
harness: "cli-base",
|
|
143
|
+
model: {
|
|
144
|
+
provider: opts.provider,
|
|
145
|
+
default: opts.model,
|
|
146
|
+
reasoningEffort: "none",
|
|
147
|
+
maxVisibleOutputTokens: ANALYST_MAX_OUTPUT_TOKENS,
|
|
148
|
+
metadata: {
|
|
149
|
+
// Stream every analyst call. The Tangle Router bounds a buffered request by a deadline that
|
|
150
|
+
// ends when response headers are sent, and a thinking model's long recursive step outlives
|
|
151
|
+
// it (`request_deadline_exceeded`). A streamed response sends headers at the first byte.
|
|
152
|
+
stream: true,
|
|
153
|
+
// One attempt per call: `retryingModelCall` decides what to retry from the endpoint's
|
|
154
|
+
// status and code, because Runtime's retry reads the status alone.
|
|
155
|
+
retry: { maxAttempts: 1 }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
call: profileOptimizerModelCall({
|
|
161
|
+
profile,
|
|
162
|
+
context: "traces analyst",
|
|
163
|
+
executor: {
|
|
164
|
+
backend: "router",
|
|
165
|
+
routerBaseUrl: opts.baseUrl,
|
|
166
|
+
routerKey: opts.apiKey
|
|
167
|
+
}
|
|
168
|
+
}),
|
|
169
|
+
callRef: canonicalCandidateDigest({
|
|
170
|
+
profile: canonicalAgentProfileDigest(profile),
|
|
171
|
+
endpoint: opts.baseUrl
|
|
172
|
+
}),
|
|
173
|
+
profile
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function analysisEngineFromEnv(opts) {
|
|
177
|
+
const env = opts.env ?? process.env;
|
|
178
|
+
const tangleKey = env.TANGLE_API_KEY;
|
|
179
|
+
const openAiKey = env.OPENAI_API_KEY;
|
|
180
|
+
const apiKey = tangleKey || openAiKey;
|
|
181
|
+
if (!apiKey) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
"model-backed analysis needs a model key: TANGLE_API_KEY for the Tangle router (the default endpoint), or OPENAI_API_KEY for OpenAI. Set OPENAI_BASE_URL to target any other OpenAI-compatible gateway. Deterministic analysis needs no key."
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const baseUrl = env.OPENAI_BASE_URL || (tangleKey ? TANGLE_ROUTER_BASE_URL : "https://api.openai.com/v1");
|
|
187
|
+
const owner = createAnalystModelOwner({
|
|
188
|
+
apiKey,
|
|
189
|
+
baseUrl,
|
|
190
|
+
model: opts.model,
|
|
191
|
+
provider: baseUrl === TANGLE_ROUTER_BASE_URL ? "tangle-router" : baseUrl.startsWith("https://api.openai.com/") ? "openai" : "openai-compatible"
|
|
192
|
+
});
|
|
193
|
+
const log = opts.log;
|
|
194
|
+
try {
|
|
195
|
+
return createDspyRlmTraceEngine({
|
|
196
|
+
call: retryingModelCall(owner.call, {
|
|
197
|
+
model: opts.model,
|
|
198
|
+
...log ? { log } : {},
|
|
199
|
+
...opts.onPermanentFailure ? { onPermanentFailure: opts.onPermanentFailure } : {}
|
|
200
|
+
}),
|
|
201
|
+
callRef: owner.callRef,
|
|
202
|
+
recordExecution: (observation) => {
|
|
203
|
+
log?.(
|
|
204
|
+
`[analyst] model call ${observation.sequence} ${observation.succeeded ? "ok" : "FAIL"} ${observation.model}`,
|
|
205
|
+
observation.succeeded ? void 0 : { error: observation.error }
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
model: opts.model,
|
|
209
|
+
maxOutputTokens: ANALYST_MAX_OUTPUT_TOKENS,
|
|
210
|
+
// The engine's own default ceiling is $1 per investigation, a proxy-side
|
|
211
|
+
// ceiling separate from any shared ledger; the caller's allocation wins.
|
|
212
|
+
...opts.maxCostUsd !== void 0 && Number.isFinite(opts.maxCostUsd) && opts.maxCostUsd > 0 ? { maxCostUsd: opts.maxCostUsd } : {},
|
|
213
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_QUESTION_TIMEOUT_MS,
|
|
214
|
+
runner: bridgeRunner(env)
|
|
215
|
+
});
|
|
216
|
+
} catch (error) {
|
|
217
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
218
|
+
if (/no pricing is configured/u.test(message)) {
|
|
219
|
+
throw new UnservableModelError({ code: "model", message: `ask cannot price --model ${opts.model}, so it cannot bound the spend: ${message}` });
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export {
|
|
226
|
+
ANALYST_MAX_OUTPUT_TOKENS,
|
|
227
|
+
bridgeVersion,
|
|
228
|
+
missingAskRequirements,
|
|
229
|
+
UnservableModelError,
|
|
230
|
+
createAnalystModelOwner,
|
|
231
|
+
analysisEngineFromEnv
|
|
232
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
orderSpans,
|
|
3
|
+
ruleContext,
|
|
4
|
+
runRules
|
|
5
|
+
} from "./chunk-REO2JQJY.js";
|
|
6
|
+
import {
|
|
7
|
+
parseSession,
|
|
8
|
+
scanSessions
|
|
9
|
+
} from "./chunk-7USMM4MA.js";
|
|
10
|
+
import {
|
|
11
|
+
sessionIdFromAttributes
|
|
12
|
+
} from "./chunk-UQSAGXPH.js";
|
|
13
|
+
|
|
14
|
+
// src/live.ts
|
|
15
|
+
var sleep = (ms, signal) => new Promise((resolve) => {
|
|
16
|
+
if (signal?.aborted) {
|
|
17
|
+
resolve();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const onAbort = () => {
|
|
21
|
+
clearTimeout(t);
|
|
22
|
+
resolve();
|
|
23
|
+
};
|
|
24
|
+
const t = setTimeout(() => {
|
|
25
|
+
signal?.removeEventListener("abort", onAbort);
|
|
26
|
+
resolve();
|
|
27
|
+
}, ms);
|
|
28
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
29
|
+
});
|
|
30
|
+
function errorText(error) {
|
|
31
|
+
return error instanceof Error ? error.message : String(error);
|
|
32
|
+
}
|
|
33
|
+
async function watchSource(source, opts) {
|
|
34
|
+
const intervalMs = Math.max(250, opts.intervalMs ?? 5e3);
|
|
35
|
+
const seenSessions = /* @__PURE__ */ new Set();
|
|
36
|
+
const spanCounts = /* @__PURE__ */ new Map();
|
|
37
|
+
const seenFindings = /* @__PURE__ */ new Set();
|
|
38
|
+
while (!opts.signal?.aborted) {
|
|
39
|
+
let sessions = 0;
|
|
40
|
+
let newSpans = 0;
|
|
41
|
+
let findings = 0;
|
|
42
|
+
const unread = [];
|
|
43
|
+
for await (const { session, spans } of source(unread)) {
|
|
44
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
45
|
+
const ordered = orderSpans(spans);
|
|
46
|
+
const key = `${session.harness}:${session.sessionId}`;
|
|
47
|
+
sessions += 1;
|
|
48
|
+
newSpans += Math.max(0, ordered.length - (spanCounts.get(key) ?? 0));
|
|
49
|
+
spanCounts.set(key, ordered.length);
|
|
50
|
+
if (!seenSessions.has(key)) {
|
|
51
|
+
seenSessions.add(key);
|
|
52
|
+
await opts.onEvent({
|
|
53
|
+
schemaVersion: 1,
|
|
54
|
+
kind: "traces.stream.session",
|
|
55
|
+
event: "session",
|
|
56
|
+
generatedAt,
|
|
57
|
+
session,
|
|
58
|
+
spanCount: ordered.length,
|
|
59
|
+
startedAt: ordered[0]?.start_time ?? null,
|
|
60
|
+
endedAt: ordered.at(-1)?.end_time ?? null
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
for (const finding of runRules(ruleContext(ordered, { session, generatedAt }), { extraAnalysts: opts.extraAnalysts })) {
|
|
64
|
+
if (seenFindings.has(finding.id)) continue;
|
|
65
|
+
seenFindings.add(finding.id);
|
|
66
|
+
findings += 1;
|
|
67
|
+
await opts.onEvent({ schemaVersion: 1, kind: "traces.stream.finding", event: "finding", generatedAt, finding });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
await opts.onEvent({
|
|
71
|
+
schemaVersion: 1,
|
|
72
|
+
kind: "traces.stream.tick",
|
|
73
|
+
event: "tick",
|
|
74
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
75
|
+
sessions,
|
|
76
|
+
newSpans,
|
|
77
|
+
findings,
|
|
78
|
+
unreadSessions: unread
|
|
79
|
+
});
|
|
80
|
+
if (opts.once || opts.signal?.aborted) break;
|
|
81
|
+
await sleep(intervalMs, opts.signal);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function sessionSource(opts) {
|
|
85
|
+
return async function* (unread) {
|
|
86
|
+
const skip = (error, ref) => {
|
|
87
|
+
unread.push({ sessionId: ref?.sessionId ?? null, source: ref?.path ?? "a session store", reason: errorText(error) });
|
|
88
|
+
opts.onError?.(error, ref);
|
|
89
|
+
};
|
|
90
|
+
const named = opts.sessions;
|
|
91
|
+
if (named) {
|
|
92
|
+
for (const { adapter, ref } of named) {
|
|
93
|
+
if (opts.signal?.aborted) return;
|
|
94
|
+
let spans;
|
|
95
|
+
try {
|
|
96
|
+
spans = await parseSession(adapter, ref, { corruptionMode: opts.corruptionMode, signal: opts.signal });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
skip(error, ref);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const root = spans.find((span) => span.parent_span_id === null) ?? spans[0];
|
|
102
|
+
const sessionId = (root && sessionIdFromAttributes(root.attributes)) ?? ref.sessionId;
|
|
103
|
+
yield { session: { harness: adapter.harness, sessionId, cwd: ref.cwd, path: ref.path }, spans };
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const scanned = scanSessions({ ...opts, sinceMs: opts.sinceMs ?? Date.now() - (opts.windowMs ?? 30 * 6e4), onError: skip });
|
|
108
|
+
for await (const { adapter, ref, spans } of scanned) {
|
|
109
|
+
yield { session: { harness: adapter.harness, sessionId: ref.sessionId, cwd: ref.cwd, path: ref.path }, spans };
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async function streamSessions(opts) {
|
|
114
|
+
await watchSource(sessionSource(opts), opts);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export {
|
|
118
|
+
watchSource,
|
|
119
|
+
streamSessions
|
|
120
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readRefSpans
|
|
3
|
+
} from "./chunk-VFOLC4OX.js";
|
|
4
|
+
import {
|
|
5
|
+
commandTextFromInput,
|
|
6
|
+
isSynthesizedSpan
|
|
7
|
+
} from "./chunk-WF7PTDCZ.js";
|
|
8
|
+
import {
|
|
9
|
+
ATTR,
|
|
10
|
+
SESSION_CWD_ATTR
|
|
11
|
+
} from "./chunk-UQSAGXPH.js";
|
|
12
|
+
import {
|
|
13
|
+
canonicalJson
|
|
14
|
+
} from "./chunk-DU534HAX.js";
|
|
15
|
+
|
|
16
|
+
// src/diff.ts
|
|
17
|
+
import { createHash } from "crypto";
|
|
18
|
+
import { epochMillis } from "@tangle-network/agent-eval/diagnosis";
|
|
19
|
+
import { diffSteps, diffStepsFromSpans } from "@tangle-network/agent-eval/pipelines";
|
|
20
|
+
import { OPENINFERENCE_SPAN_KIND, TOOL_NAME } from "@tangle-network/agent-eval/trace-attributes";
|
|
21
|
+
var TEXT_LIMIT = 300;
|
|
22
|
+
var WORKDIR_KEYS = ["workdir", "cwd"];
|
|
23
|
+
var FILE_KEYS = ["file_path", "filePath", "notebook_path"];
|
|
24
|
+
var RECORDED_CWD_KEYS = [SESSION_CWD_ATTR, ATTR.ENVIRONMENT_CWD];
|
|
25
|
+
var PATCH_HEADER = /\*\*\* (Add|Update|Delete|Move to) File: ([^\n"\\]+)/g;
|
|
26
|
+
var WORKTREE_PARENT = /^(.*?\/(?:_wt|\.worktrees|worktrees)\/[^/]+)\/./;
|
|
27
|
+
var TEMP_DIR = /(?<![\w.~-])(?:\/private)?(?:\/tmp|\/var\/tmp|\/var\/folders\/[^/\s]+\/[^/\s]+\/T)\/[^/\s'"`;|&<>(){}]+/g;
|
|
28
|
+
var HEX_RUN = /(?<![0-9A-Za-z])(?:[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}|[0-9A-F]{8}(?:-[0-9A-F]{4}){3}-[0-9A-F]{12}|[0-9a-f]{7,}|[0-9A-F]{7,})(?![0-9A-Za-z])/g;
|
|
29
|
+
function hexId(run) {
|
|
30
|
+
return run.includes("-") || /[0-9]/.test(run) && /[a-f]/i.test(run) ? "<hex>" : run;
|
|
31
|
+
}
|
|
32
|
+
function stringOf(record, keys) {
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
const value = record[key];
|
|
35
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function jsonObject(text) {
|
|
40
|
+
if (!text.trimStart().startsWith("{")) return void 0;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(text);
|
|
43
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
44
|
+
} catch {
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function patchedFiles(text) {
|
|
49
|
+
if (!text.includes("*** Begin Patch")) return null;
|
|
50
|
+
const headers = [...text.matchAll(PATCH_HEADER)].map((match) => `${match[1]} File: ${match[2].trim()}`);
|
|
51
|
+
return headers.length > 0 ? headers.join("; ") : null;
|
|
52
|
+
}
|
|
53
|
+
function callArgument(span) {
|
|
54
|
+
const raw = span.attributes["input.value"];
|
|
55
|
+
if (raw === void 0 || raw === null || raw === "") return { argument: null, workdir: null };
|
|
56
|
+
const text = typeof raw === "string" ? raw : canonicalJson(raw);
|
|
57
|
+
const input = jsonObject(text);
|
|
58
|
+
const workdir = input ? stringOf(input, WORKDIR_KEYS) : null;
|
|
59
|
+
const command = commandTextFromInput(text);
|
|
60
|
+
if (command !== void 0) return { argument: command, workdir };
|
|
61
|
+
if (!input) return { argument: patchedFiles(text) ?? text, workdir: null };
|
|
62
|
+
const file = stringOf(input, FILE_KEYS);
|
|
63
|
+
if (file) return { argument: file, workdir };
|
|
64
|
+
const patch = Object.values(input).find((value) => typeof value === "string" && value.includes("*** Begin Patch"));
|
|
65
|
+
const patched = patch === void 0 ? null : patchedFiles(patch);
|
|
66
|
+
if (patched) return { argument: patched, workdir };
|
|
67
|
+
const rest = Object.fromEntries(Object.entries(input).filter(([key]) => !WORKDIR_KEYS.includes(key)));
|
|
68
|
+
return { argument: canonicalJson(rest), workdir };
|
|
69
|
+
}
|
|
70
|
+
function escapeRegExp(text) {
|
|
71
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
72
|
+
}
|
|
73
|
+
function normalizer(cwds) {
|
|
74
|
+
const places = /* @__PURE__ */ new Map();
|
|
75
|
+
const add = (path, placeholder) => {
|
|
76
|
+
if (path.length < 2 || places.has(path)) return;
|
|
77
|
+
places.set(path, placeholder);
|
|
78
|
+
places.set(path.replace(/[^A-Za-z0-9]/g, "-"), placeholder);
|
|
79
|
+
};
|
|
80
|
+
for (const cwd of cwds) add(cwd, "<cwd>");
|
|
81
|
+
for (const cwd of cwds) {
|
|
82
|
+
const worktree = WORKTREE_PARENT.exec(cwd)?.[1];
|
|
83
|
+
if (worktree) add(worktree, "<worktree>");
|
|
84
|
+
}
|
|
85
|
+
const ordered = [...places.keys()].sort((x, y) => y.length - x.length);
|
|
86
|
+
const place = ordered.length === 0 ? null : new RegExp(`(?<![\\w.~-])(?:${ordered.map(escapeRegExp).join("|")})(?![\\w.@%+=,~-])`, "g");
|
|
87
|
+
return (text) => {
|
|
88
|
+
const placed = place ? text.replace(place, (match) => places.get(match)) : text;
|
|
89
|
+
return placed.replace(TEMP_DIR, "<tmp>").replace(HEX_RUN, hexId);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function isToolCall(span) {
|
|
93
|
+
return span.attributes[OPENINFERENCE_SPAN_KIND] === "TOOL" && !isSynthesizedSpan(span);
|
|
94
|
+
}
|
|
95
|
+
function toolName(span) {
|
|
96
|
+
const name = span.attributes[TOOL_NAME];
|
|
97
|
+
return typeof name === "string" && name.length > 0 ? name : span.name.replace(/^tool\./, "");
|
|
98
|
+
}
|
|
99
|
+
function oneLine(text, limit) {
|
|
100
|
+
const line = text.replace(/\s+/g, " ").trim();
|
|
101
|
+
return line.length > limit ? `${line.slice(0, limit)}\u2026` : line;
|
|
102
|
+
}
|
|
103
|
+
function containmentOrder(spans) {
|
|
104
|
+
const byId = /* @__PURE__ */ new Map();
|
|
105
|
+
for (const span of spans) if (!byId.has(span.span_id)) byId.set(span.span_id, span);
|
|
106
|
+
const shapes = [...byId.values()].map((span) => ({
|
|
107
|
+
traceId: span.trace_id,
|
|
108
|
+
spanId: span.span_id,
|
|
109
|
+
parentSpanId: span.parent_span_id,
|
|
110
|
+
name: span.name,
|
|
111
|
+
kind: String(span.attributes[OPENINFERENCE_SPAN_KIND] ?? "UNKNOWN"),
|
|
112
|
+
startMs: epochMillis(span.start_time),
|
|
113
|
+
endMs: epochMillis(span.end_time),
|
|
114
|
+
status: span.status.code,
|
|
115
|
+
statusMessage: null,
|
|
116
|
+
toolName: null,
|
|
117
|
+
model: null,
|
|
118
|
+
inputDigest: null,
|
|
119
|
+
attributes: {}
|
|
120
|
+
}));
|
|
121
|
+
return diffStepsFromSpans(shapes).map((step) => byId.get(step.id));
|
|
122
|
+
}
|
|
123
|
+
async function readRunSide(ref) {
|
|
124
|
+
const spans = await readRefSpans(ref);
|
|
125
|
+
const traces = [...new Set(spans.map((span) => span.trace_id))];
|
|
126
|
+
if (traces.length !== 1) {
|
|
127
|
+
throw new Error(traces.length === 0 ? `${ref} holds no spans` : `${ref} holds ${traces.length} runs; pick one with ${ref}#<trace id>: ${traces.slice(0, 10).join(", ")}`);
|
|
128
|
+
}
|
|
129
|
+
const calls = containmentOrder(spans).filter(isToolCall).map((span) => ({ span, ...callArgument(span) }));
|
|
130
|
+
const cwds = /* @__PURE__ */ new Set();
|
|
131
|
+
const add = (value) => {
|
|
132
|
+
if (typeof value === "string" && value.startsWith("/")) cwds.add(value.replace(/\/+$/, ""));
|
|
133
|
+
};
|
|
134
|
+
for (const span of spans) for (const key of RECORDED_CWD_KEYS) add(span.attributes[key]);
|
|
135
|
+
if (cwds.size === 0) for (const span of spans) add(span.attributes[ATTR.CWD]);
|
|
136
|
+
for (const call of calls) add(call.workdir);
|
|
137
|
+
const normalize = normalizer([...cwds]);
|
|
138
|
+
const steps = calls.map(({ span, argument }) => {
|
|
139
|
+
const normalized = argument === null ? null : normalize(argument);
|
|
140
|
+
return {
|
|
141
|
+
id: span.span_id,
|
|
142
|
+
name: toolName(span),
|
|
143
|
+
kind: "TOOL",
|
|
144
|
+
fields: {
|
|
145
|
+
status: span.status.code,
|
|
146
|
+
args: normalized === null ? null : createHash("sha256").update(normalized).digest("hex").slice(0, 16)
|
|
147
|
+
},
|
|
148
|
+
text: normalized === null ? null : oneLine(normalized, TEXT_LIMIT)
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
return { ref, traceId: traces[0], spans: spans.length, cwds: [...cwds].sort(), steps };
|
|
152
|
+
}
|
|
153
|
+
function agreementCaveat(a, b) {
|
|
154
|
+
if (a.length === 0 && b.length === 0) return "neither run made a tool call";
|
|
155
|
+
const decided = (steps) => steps.some((step) => step.fields.status === "OK" || step.fields.status === "ERROR");
|
|
156
|
+
if (!decided(a) && !decided(b)) return "no tool call on either side records an OK or ERROR status (all UNSET)";
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
async function diffRuns(a, b) {
|
|
160
|
+
const [left, right] = await Promise.all([readRunSide(a), readRunSide(b)]);
|
|
161
|
+
const keyed = (side) => left.traceId === right.traceId ? [...side.steps] : side.steps.map((step) => ({ ...step, id: `${side.traceId}\0${step.id}` }));
|
|
162
|
+
const diff = diffSteps(keyed(left), keyed(right));
|
|
163
|
+
const caveat = diff.firstDivergence ? void 0 : agreementCaveat(left.steps, right.steps);
|
|
164
|
+
return { kind: "traces.run_diff", a: left, b: right, diff, ...caveat ? { caveat } : {} };
|
|
165
|
+
}
|
|
166
|
+
var SHOWN = 120;
|
|
167
|
+
function shown(text, other) {
|
|
168
|
+
if (text === null) return "(input not recorded)";
|
|
169
|
+
if (other === null || other === text || text.length <= SHOWN) return oneLine(text, SHOWN);
|
|
170
|
+
let at = 0;
|
|
171
|
+
while (at < text.length && text[at] === other[at]) at += 1;
|
|
172
|
+
if (at < SHOWN - 20) return oneLine(text, SHOWN);
|
|
173
|
+
const start = at - 40;
|
|
174
|
+
return `\u2026${oneLine(text.slice(start), SHOWN)}`;
|
|
175
|
+
}
|
|
176
|
+
function stepLine(side, step, other, count) {
|
|
177
|
+
if (!step) return ` ${side} (no more tool calls: it made ${count})`;
|
|
178
|
+
const status = step.fields.status === "OK" ? "" : ` [${String(step.fields.status)}]`;
|
|
179
|
+
return ` ${side} ${step.name}${status} ${shown(step.text, other?.text ?? null)}`;
|
|
180
|
+
}
|
|
181
|
+
function why(first) {
|
|
182
|
+
switch (first.kind) {
|
|
183
|
+
case "changed":
|
|
184
|
+
return first.differences.map((d) => d.field === "args" ? "argument" : d.field).join(", ");
|
|
185
|
+
case "replaced":
|
|
186
|
+
return "tool";
|
|
187
|
+
case "only-in-a":
|
|
188
|
+
return "a call only A makes";
|
|
189
|
+
case "only-in-b":
|
|
190
|
+
return "a call only B makes";
|
|
191
|
+
case "reordered":
|
|
192
|
+
return "order: each call is made elsewhere in the other run";
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function sideLine(label, side) {
|
|
196
|
+
const cwd = side.cwds.length === 0 ? "" : ` \xB7 cwd ${side.cwds[0]}${side.cwds.length > 1 ? ` (+${side.cwds.length - 1})` : ""}`;
|
|
197
|
+
return `${label} ${side.ref} ${side.steps.length} tool call${side.steps.length === 1 ? "" : "s"}${cwd}`;
|
|
198
|
+
}
|
|
199
|
+
function renderRunDiff(report) {
|
|
200
|
+
const { a, b, diff } = report;
|
|
201
|
+
const lines = [sideLine("A", a), sideLine("B", b)];
|
|
202
|
+
const first = diff.firstDivergence;
|
|
203
|
+
if (!first) {
|
|
204
|
+
lines.push(report.caveat ? `no difference in the steps, but no agreement on outcome either: ${report.caveat}` : `no difference: all ${a.steps.length} tool calls agree on tool, status and argument`);
|
|
205
|
+
return `${lines.join("\n")}
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
const stepA = first.a === null ? void 0 : a.steps[first.a];
|
|
209
|
+
const stepB = first.b === null ? void 0 : b.steps[first.b];
|
|
210
|
+
const agreed = first.index === 0 ? "no step before it" : first.index === 1 ? "step 1 agrees" : `steps 1-${first.index} agree`;
|
|
211
|
+
lines.push(
|
|
212
|
+
`first difference at step ${first.index + 1} (${why(first)}); ${agreed}:`,
|
|
213
|
+
stepLine("A", stepA, stepB, a.steps.length),
|
|
214
|
+
stepLine("B", stepB, stepA, b.steps.length)
|
|
215
|
+
);
|
|
216
|
+
if (stepA && stepB && stepA.text === stepB.text && stepA.fields.args !== stepB.fields.args) {
|
|
217
|
+
lines.push(` (the arguments differ after their first ${TEXT_LIMIT} characters)`);
|
|
218
|
+
}
|
|
219
|
+
const changed = diff.pairs.filter((pair) => pair.differences.length > 0).length;
|
|
220
|
+
lines.push(`overall: ${diff.pairs.length} paired (${changed} differ), ${diff.onlyInA.length} only in A, ${diff.onlyInB.length} only in B`);
|
|
221
|
+
return `${lines.join("\n")}
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export {
|
|
226
|
+
diffRuns,
|
|
227
|
+
renderRunDiff
|
|
228
|
+
};
|