@tangle-network/traces 0.23.0 → 1.0.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/README.md +61 -1078
- package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
- package/dist/ask-XLS3VANY.js +39 -0
- package/dist/check-YF6ZHPGG.js +322 -0
- package/dist/chunk-3I4PJ5Q7.js +25 -0
- package/dist/chunk-3T62R7ON.js +5695 -0
- package/dist/chunk-4VKPN432.js +138 -0
- package/dist/chunk-6CLC7DKP.js +492 -0
- package/dist/chunk-7CBV5RJ2.js +535 -0
- package/dist/chunk-B62DOFGI.js +273 -0
- package/dist/chunk-CL3OQOWX.js +407 -0
- package/dist/chunk-DLGAKCQJ.js +782 -0
- package/dist/chunk-DU534HAX.js +263 -0
- package/dist/chunk-GLUDGUGJ.js +232 -0
- package/dist/chunk-J2PM5SVN.js +503 -0
- package/dist/chunk-M3W6TZHQ.js +381 -0
- package/dist/chunk-NH7HCDLB.js +759 -0
- package/dist/chunk-NZ5Y7UEL.js +1217 -0
- package/dist/chunk-O52XK7OB.js +19 -0
- package/dist/chunk-PYH3PL3N.js +62 -0
- package/dist/chunk-RZL5GAWH.js +60 -0
- package/dist/chunk-TR6O4S4T.js +208 -0
- package/dist/chunk-TW43MR5D.js +259 -0
- package/dist/chunk-TYLFUX42.js +228 -0
- package/dist/chunk-UOLRCQHC.js +120 -0
- package/dist/chunk-UVE5ANVV.js +3921 -0
- package/dist/chunk-V2OCGX57.js +53 -0
- package/dist/cli.js +2086 -2050
- package/dist/diff-ZZ6KYBPP.js +16 -0
- package/dist/improvement-OJQ6365E.js +25 -0
- package/dist/index.d.ts +1235 -4357
- package/dist/index.js +53 -1204
- package/dist/live-Y64AJ6MA.js +14 -0
- package/dist/mcp-ZPGTJQYQ.js +85 -0
- package/dist/report-JAMXRTCW.js +338 -0
- package/dist/run-watch-PLWTIRSV.js +649 -0
- package/dist/supervisor-report-RMPLLBGZ.js +29 -0
- package/dist/supervisor-run-context-WO6VOTGV.js +30 -0
- package/dist/upload-R6NMKSGQ.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/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 +8 -7
- package/skills/inspect-agent-traces/SKILL.md +47 -110
- package/dist/chunk-COSWFESO.js +0 -20979
- 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,263 @@
|
|
|
1
|
+
// src/adapters/tool-io.ts
|
|
2
|
+
import { createHash as createHash2 } from "crypto";
|
|
3
|
+
|
|
4
|
+
// src/source-location.ts
|
|
5
|
+
import { createHash } from "crypto";
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
var SOURCE_ATTRIBUTE_PREFIX = "traces.source_record.";
|
|
8
|
+
var locations = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
function sourceFileId(path) {
|
|
10
|
+
return createHash("sha256").update(resolve(path)).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
function pointerPart(value) {
|
|
13
|
+
return value.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
14
|
+
}
|
|
15
|
+
function locateSourceObjects(value, path, bytes, offset = 0) {
|
|
16
|
+
if (!value || typeof value !== "object") return;
|
|
17
|
+
const record = {
|
|
18
|
+
sourceId: sourceFileId(path),
|
|
19
|
+
recordOffset: offset,
|
|
20
|
+
recordBytes: bytes.length,
|
|
21
|
+
recordSha256: createHash("sha256").update(bytes).digest("hex")
|
|
22
|
+
};
|
|
23
|
+
const pending = [{ value, pointer: "#" }];
|
|
24
|
+
while (pending.length > 0) {
|
|
25
|
+
const current = pending.pop();
|
|
26
|
+
locations.set(current.value, { ...record, fieldLocator: current.pointer });
|
|
27
|
+
for (const [key, child] of Object.entries(current.value)) {
|
|
28
|
+
if (child && typeof child === "object") {
|
|
29
|
+
pending.push({ value: child, pointer: `${current.pointer}/${pointerPart(key)}` });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function sourceOf(value, ...fields) {
|
|
35
|
+
if (!value || typeof value !== "object") return void 0;
|
|
36
|
+
const location = locations.get(value);
|
|
37
|
+
return location ? {
|
|
38
|
+
...location,
|
|
39
|
+
fieldLocator: `${location.fieldLocator}${fields.map((field) => `/${pointerPart(field)}`).join("")}`
|
|
40
|
+
} : void 0;
|
|
41
|
+
}
|
|
42
|
+
function textSources(value, field) {
|
|
43
|
+
if (!value || typeof value !== "object") return void 0;
|
|
44
|
+
const content = value[field];
|
|
45
|
+
if (typeof content === "string") return sourceOf(value, field);
|
|
46
|
+
if (!Array.isArray(content)) return void 0;
|
|
47
|
+
return content.flatMap((block, index) => {
|
|
48
|
+
if (typeof block === "string") {
|
|
49
|
+
const ref2 = sourceOf(value, field, String(index));
|
|
50
|
+
return ref2 ? [ref2] : [];
|
|
51
|
+
}
|
|
52
|
+
if (!block || typeof block !== "object" || typeof block.text !== "string") return [];
|
|
53
|
+
if (block.type !== void 0 && !["text", "input_text", "output_text"].includes(block.type)) return [];
|
|
54
|
+
const ref = sourceOf(block, "text");
|
|
55
|
+
return ref ? [ref] : [];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function sourceAttributes(attribute, sources) {
|
|
59
|
+
const refs = sources === void 0 ? [] : Array.isArray(sources) ? sources : [sources];
|
|
60
|
+
return refs.length === 0 ? {} : { [`${SOURCE_ATTRIBUTE_PREFIX}${attribute}`]: JSON.stringify(refs) };
|
|
61
|
+
}
|
|
62
|
+
function appendSourceAttributes(attributes, attribute, sources) {
|
|
63
|
+
const key = `${SOURCE_ATTRIBUTE_PREFIX}${attribute}`;
|
|
64
|
+
const added = sourceAttributes(attribute, sources)[key];
|
|
65
|
+
if (typeof added !== "string") return;
|
|
66
|
+
const prior = attributes[key];
|
|
67
|
+
const refs = typeof prior === "string" ? JSON.parse(prior) : [];
|
|
68
|
+
const known = new Set(refs.map((ref) => JSON.stringify(ref)));
|
|
69
|
+
for (const ref of JSON.parse(added)) {
|
|
70
|
+
if (!known.has(JSON.stringify(ref))) {
|
|
71
|
+
refs.push(ref);
|
|
72
|
+
known.add(JSON.stringify(ref));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
attributes[key] = JSON.stringify(refs);
|
|
76
|
+
}
|
|
77
|
+
function stripSourceAttributes(attributes) {
|
|
78
|
+
return Object.fromEntries(Object.entries(attributes).filter(([key]) => !key.startsWith(SOURCE_ATTRIBUTE_PREFIX)));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/adapters/tool-io.ts
|
|
82
|
+
var TOOL_IO_VALUE_MAX_BYTES = 16 * 1024;
|
|
83
|
+
var TOOL_CALL_LEVEL_ATTR = "traces.tool_call.level";
|
|
84
|
+
var INNER_TOOL_CALL_LEVEL = "inner";
|
|
85
|
+
function isInnerToolCall(attributes) {
|
|
86
|
+
return attributes[TOOL_CALL_LEVEL_ATTR] === INNER_TOOL_CALL_LEVEL;
|
|
87
|
+
}
|
|
88
|
+
var TOOL_IO_SIDES = ["input", "output"];
|
|
89
|
+
var LEGACY_TRUNCATION_MARKER = /\n\[truncated (\d+) bytes; sha256=[a-f0-9]{64}\]$/;
|
|
90
|
+
var COUNTED_TRUNCATION_MARKER = /\n\[truncated \d+ bytes\]$/;
|
|
91
|
+
var TRUNCATION_MARKER = /\n\[truncated\]$/;
|
|
92
|
+
function canonicalJson(value) {
|
|
93
|
+
const sort = (item) => {
|
|
94
|
+
if (Array.isArray(item)) return item.map(sort);
|
|
95
|
+
if (!item || typeof item !== "object") return item;
|
|
96
|
+
return Object.fromEntries(
|
|
97
|
+
Object.entries(item).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => [key, sort(entry)])
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
const json = JSON.stringify(sort(value));
|
|
101
|
+
if (json === void 0) throw new TypeError("tool I/O value is not JSON-serializable");
|
|
102
|
+
return json;
|
|
103
|
+
}
|
|
104
|
+
function serialize(value) {
|
|
105
|
+
if (value === void 0) return void 0;
|
|
106
|
+
if (typeof value === "string") {
|
|
107
|
+
const trimmed = value.trim();
|
|
108
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
109
|
+
try {
|
|
110
|
+
return { value: canonicalJson(JSON.parse(trimmed)), mimeType: "application/json" };
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { value, mimeType: "text/plain" };
|
|
115
|
+
}
|
|
116
|
+
return { value: canonicalJson(value), mimeType: "application/json" };
|
|
117
|
+
}
|
|
118
|
+
function utf8Prefix(value, maxBytes) {
|
|
119
|
+
let bytes = 0;
|
|
120
|
+
let end = 0;
|
|
121
|
+
for (const char of value) {
|
|
122
|
+
const charBytes = Buffer.byteLength(char);
|
|
123
|
+
if (bytes + charBytes > maxBytes) break;
|
|
124
|
+
bytes += charBytes;
|
|
125
|
+
end += char.length;
|
|
126
|
+
}
|
|
127
|
+
return Buffer.from(value.slice(0, end), "utf8").toString("utf8");
|
|
128
|
+
}
|
|
129
|
+
function truncatedValue(value, bytes) {
|
|
130
|
+
const longestMarker = `
|
|
131
|
+
[truncated ${bytes} bytes]`;
|
|
132
|
+
const prefix = utf8Prefix(value, TOOL_IO_VALUE_MAX_BYTES - Buffer.byteLength(longestMarker));
|
|
133
|
+
const omittedBytes = bytes - Buffer.byteLength(prefix);
|
|
134
|
+
return `${prefix}
|
|
135
|
+
[truncated ${omittedBytes} bytes]`;
|
|
136
|
+
}
|
|
137
|
+
function boundedString(value) {
|
|
138
|
+
if (value.length <= TOOL_IO_VALUE_MAX_BYTES / 4) return value;
|
|
139
|
+
const bytes = Buffer.byteLength(value);
|
|
140
|
+
return bytes > TOOL_IO_VALUE_MAX_BYTES ? truncatedValue(value, bytes) : value;
|
|
141
|
+
}
|
|
142
|
+
function valueAttributes(side, raw) {
|
|
143
|
+
const serialized = serialize(raw);
|
|
144
|
+
if (!serialized) return {};
|
|
145
|
+
const bytes = Buffer.byteLength(serialized.value);
|
|
146
|
+
const sha256 = createHash2("sha256").update(serialized.value).digest("hex");
|
|
147
|
+
const truncated = bytes > TOOL_IO_VALUE_MAX_BYTES;
|
|
148
|
+
const value = truncated ? truncatedValue(serialized.value, bytes) : serialized.value;
|
|
149
|
+
return {
|
|
150
|
+
[`${side}.value`]: value,
|
|
151
|
+
[`${side}.mime_type`]: serialized.mimeType,
|
|
152
|
+
[`traces.${side}.bytes`]: bytes,
|
|
153
|
+
[`traces.${side}.sha256`]: sha256,
|
|
154
|
+
[`traces.${side}.truncated`]: truncated
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function toolIoAttributes(io) {
|
|
158
|
+
return {
|
|
159
|
+
...valueAttributes("input", io.input),
|
|
160
|
+
...valueAttributes("output", io.output),
|
|
161
|
+
...io.input === void 0 ? {} : sourceAttributes("input.value", io.inputSource),
|
|
162
|
+
...io.output === void 0 ? {} : sourceAttributes("output.value", io.outputSource),
|
|
163
|
+
...io.argsCaptured === void 0 ? {} : { "tool.args_captured": io.argsCaptured }
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function toolArgumentsFromAttributes(attributes) {
|
|
167
|
+
const declared = attributes["tool.args_captured"];
|
|
168
|
+
const explicit = typeof declared === "boolean" ? declared : declared === "true" ? true : declared === "false" ? false : void 0;
|
|
169
|
+
const argsCaptured = explicit ?? attributes["input.value"] !== void 0;
|
|
170
|
+
return {
|
|
171
|
+
args: argsCaptured ? attributes["input.value"] : void 0,
|
|
172
|
+
argsCaptured
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function normalizeToolIoAttributes(attributes) {
|
|
176
|
+
for (const side of TOOL_IO_SIDES) {
|
|
177
|
+
const valueKey = `${side}.value`;
|
|
178
|
+
const mimeKey = `${side}.mime_type`;
|
|
179
|
+
const bytesKey = `traces.${side}.bytes`;
|
|
180
|
+
const digestKey = `traces.${side}.sha256`;
|
|
181
|
+
const truncatedKey = `traces.${side}.truncated`;
|
|
182
|
+
const rawValue = attributes[valueKey];
|
|
183
|
+
if (typeof rawValue !== "string") {
|
|
184
|
+
delete attributes[mimeKey];
|
|
185
|
+
delete attributes[bytesKey];
|
|
186
|
+
delete attributes[digestKey];
|
|
187
|
+
delete attributes[truncatedKey];
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
let value = rawValue.replace(LEGACY_TRUNCATION_MARKER, "\n[truncated]").replace(COUNTED_TRUNCATION_MARKER, "\n[truncated]");
|
|
191
|
+
let truncated = TRUNCATION_MARKER.test(value);
|
|
192
|
+
const unboundedBytes = Buffer.byteLength(value);
|
|
193
|
+
if (unboundedBytes > TOOL_IO_VALUE_MAX_BYTES) {
|
|
194
|
+
value = truncatedValue(value, unboundedBytes);
|
|
195
|
+
truncated = true;
|
|
196
|
+
}
|
|
197
|
+
attributes[valueKey] = value;
|
|
198
|
+
attributes[bytesKey] = Buffer.byteLength(value);
|
|
199
|
+
attributes[digestKey] = createHash2("sha256").update(value).digest("hex");
|
|
200
|
+
attributes[truncatedKey] = truncated;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function recordToolOutput(toolSpan, output, source) {
|
|
204
|
+
if (!toolSpan) return;
|
|
205
|
+
if (output === void 0) return;
|
|
206
|
+
delete toolSpan.attributes[`${SOURCE_ATTRIBUTE_PREFIX}output.value`];
|
|
207
|
+
Object.assign(toolSpan.attributes, valueAttributes("output", output));
|
|
208
|
+
Object.assign(toolSpan.attributes, sourceAttributes("output.value", source));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/time.ts
|
|
212
|
+
function parseIsoToEpochMs(ts) {
|
|
213
|
+
if (typeof ts !== "string" || ts.trim().length === 0) {
|
|
214
|
+
throw new TypeError("timestamp must be a non-empty string");
|
|
215
|
+
}
|
|
216
|
+
const value = ts.trim();
|
|
217
|
+
const n = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
|
|
218
|
+
if (!Number.isFinite(n) || !Number.isFinite(new Date(n).getTime())) {
|
|
219
|
+
throw new TypeError(`invalid timestamp: ${ts}`);
|
|
220
|
+
}
|
|
221
|
+
return n;
|
|
222
|
+
}
|
|
223
|
+
function parseSince(value) {
|
|
224
|
+
return parseTimeBound(value, "--since");
|
|
225
|
+
}
|
|
226
|
+
function relativeBoundMs(value) {
|
|
227
|
+
const m = value.match(/^(\d+)\s*([mhd])$/i);
|
|
228
|
+
if (!m) return void 0;
|
|
229
|
+
const unit = m[2].toLowerCase();
|
|
230
|
+
return Number(m[1]) * (unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5);
|
|
231
|
+
}
|
|
232
|
+
function parseTimeBound(value, flag) {
|
|
233
|
+
const relative = relativeBoundMs(value);
|
|
234
|
+
if (relative !== void 0) return Date.now() - relative;
|
|
235
|
+
const t = Date.parse(value);
|
|
236
|
+
if (Number.isNaN(t)) throw new Error(`${flag}: expected 30m / 2h / 7d or an ISO date, got "${value}"`);
|
|
237
|
+
return t;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export {
|
|
241
|
+
SOURCE_ATTRIBUTE_PREFIX,
|
|
242
|
+
sourceFileId,
|
|
243
|
+
locateSourceObjects,
|
|
244
|
+
sourceOf,
|
|
245
|
+
textSources,
|
|
246
|
+
sourceAttributes,
|
|
247
|
+
appendSourceAttributes,
|
|
248
|
+
stripSourceAttributes,
|
|
249
|
+
TOOL_IO_VALUE_MAX_BYTES,
|
|
250
|
+
TOOL_CALL_LEVEL_ATTR,
|
|
251
|
+
INNER_TOOL_CALL_LEVEL,
|
|
252
|
+
isInnerToolCall,
|
|
253
|
+
canonicalJson,
|
|
254
|
+
boundedString,
|
|
255
|
+
toolIoAttributes,
|
|
256
|
+
toolArgumentsFromAttributes,
|
|
257
|
+
normalizeToolIoAttributes,
|
|
258
|
+
recordToolOutput,
|
|
259
|
+
parseIsoToEpochMs,
|
|
260
|
+
parseSince,
|
|
261
|
+
relativeBoundMs,
|
|
262
|
+
parseTimeBound
|
|
263
|
+
};
|
|
@@ -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
|
+
};
|