@probelabs/visor 0.1.181 → 0.1.182
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/defaults/code-talk.yaml +80 -14
- package/defaults/engineer.yaml +33 -15
- package/defaults/skills/code-explorer.yaml +5 -0
- package/dist/agent-protocol/a2a-frontend.d.ts +10 -0
- package/dist/agent-protocol/a2a-frontend.d.ts.map +1 -1
- package/dist/agent-protocol/task-evaluator.d.ts +52 -0
- package/dist/agent-protocol/task-evaluator.d.ts.map +1 -0
- package/dist/agent-protocol/task-store.d.ts +5 -3
- package/dist/agent-protocol/task-store.d.ts.map +1 -1
- package/dist/agent-protocol/tasks-cli-handler.d.ts.map +1 -1
- package/dist/agent-protocol/tasks-tui.d.ts +34 -0
- package/dist/agent-protocol/tasks-tui.d.ts.map +1 -0
- package/dist/agent-protocol/trace-serializer.d.ts +90 -0
- package/dist/agent-protocol/trace-serializer.d.ts.map +1 -0
- package/dist/agent-protocol/track-execution.d.ts +2 -0
- package/dist/agent-protocol/track-execution.d.ts.map +1 -1
- package/dist/cli-main.d.ts.map +1 -1
- package/dist/defaults/code-talk.yaml +80 -14
- package/dist/defaults/engineer.yaml +33 -15
- package/dist/defaults/skills/code-explorer.yaml +5 -0
- package/dist/docs/commands.md +57 -14
- package/dist/docs/configuration.md +2 -0
- package/dist/docs/guides/graceful-restart.md +178 -0
- package/dist/docs/observability.md +69 -0
- package/dist/docs/production-deployment.md +17 -0
- package/dist/email/polling-runner.d.ts +4 -0
- package/dist/email/polling-runner.d.ts.map +1 -1
- package/dist/generated/config-schema.d.ts +70 -6
- package/dist/generated/config-schema.d.ts.map +1 -1
- package/dist/generated/config-schema.json +73 -6
- package/dist/index.js +5006 -886
- package/dist/output/traces/{run-2026-03-17T13-58-29-402Z.ndjson → run-2026-03-18T19-02-50-465Z.ndjson} +84 -84
- package/dist/{traces/run-2026-03-17T13-59-10-403Z.ndjson → output/traces/run-2026-03-18T19-03-30-428Z.ndjson} +2037 -2037
- package/dist/providers/mcp-custom-sse-server.d.ts +4 -0
- package/dist/providers/mcp-custom-sse-server.d.ts.map +1 -1
- package/dist/runners/graceful-restart.d.ts +46 -0
- package/dist/runners/graceful-restart.d.ts.map +1 -0
- package/dist/runners/mcp-server-runner.d.ts +12 -0
- package/dist/runners/mcp-server-runner.d.ts.map +1 -1
- package/dist/runners/runner-factory.d.ts.map +1 -1
- package/dist/runners/runner-host.d.ts +12 -0
- package/dist/runners/runner-host.d.ts.map +1 -1
- package/dist/runners/runner.d.ts +12 -0
- package/dist/runners/runner.d.ts.map +1 -1
- package/dist/sdk/{a2a-frontend-IWOUJOIZ.mjs → a2a-frontend-4LP3MLTS.mjs} +47 -5
- package/dist/sdk/a2a-frontend-4LP3MLTS.mjs.map +1 -0
- package/dist/sdk/a2a-frontend-5J3UNFY4.mjs +1718 -0
- package/dist/sdk/a2a-frontend-5J3UNFY4.mjs.map +1 -0
- package/dist/sdk/{a2a-frontend-BDACLGMA.mjs → a2a-frontend-MU5EO2HZ.mjs} +35 -1
- package/dist/sdk/a2a-frontend-MU5EO2HZ.mjs.map +1 -0
- package/dist/sdk/{check-provider-registry-4YKTEDKF.mjs → check-provider-registry-MHXQGUNN.mjs} +7 -7
- package/dist/sdk/{check-provider-registry-4YFVBGYU.mjs → check-provider-registry-RRWCXSTG.mjs} +3 -3
- package/dist/sdk/{check-provider-registry-67ZLGDDQ.mjs → check-provider-registry-Y33CRFVD.mjs} +7 -7
- package/dist/sdk/{chunk-DGIH6EX3.mjs → chunk-4AXAVXG5.mjs} +151 -281
- package/dist/sdk/chunk-4AXAVXG5.mjs.map +1 -0
- package/dist/sdk/{chunk-VMVIM4JB.mjs → chunk-4I3TJ7UJ.mjs} +37 -7
- package/dist/sdk/chunk-4I3TJ7UJ.mjs.map +1 -0
- package/dist/sdk/{chunk-VXC2XNQJ.mjs → chunk-5J3DNRF7.mjs} +3 -3
- package/dist/sdk/{chunk-7YZSSO4X.mjs → chunk-6DPPP7LD.mjs} +10 -10
- package/dist/sdk/chunk-7ERVRLDV.mjs +296 -0
- package/dist/sdk/chunk-7ERVRLDV.mjs.map +1 -0
- package/dist/sdk/{chunk-4DVP6KVC.mjs → chunk-7Z2WHX2J.mjs} +71 -30
- package/dist/sdk/chunk-7Z2WHX2J.mjs.map +1 -0
- package/dist/sdk/chunk-ANUT54HW.mjs +1502 -0
- package/dist/sdk/chunk-ANUT54HW.mjs.map +1 -0
- package/dist/sdk/{chunk-J73GEFPT.mjs → chunk-DHETLQIX.mjs} +2 -2
- package/dist/sdk/{chunk-QGBASDYP.mjs → chunk-JCOSKBMP.mjs} +71 -30
- package/dist/sdk/chunk-JCOSKBMP.mjs.map +1 -0
- package/dist/sdk/chunk-MK7ONH47.mjs +739 -0
- package/dist/sdk/chunk-MK7ONH47.mjs.map +1 -0
- package/dist/sdk/chunk-QXT47ZHR.mjs +390 -0
- package/dist/sdk/chunk-QXT47ZHR.mjs.map +1 -0
- package/dist/sdk/chunk-V75NEIXL.mjs +296 -0
- package/dist/sdk/chunk-V75NEIXL.mjs.map +1 -0
- package/dist/sdk/chunk-ZOF5QT6U.mjs +5943 -0
- package/dist/sdk/chunk-ZOF5QT6U.mjs.map +1 -0
- package/dist/sdk/{config-TSA5FUOM.mjs → config-2STD74CJ.mjs} +2 -2
- package/dist/sdk/config-JE4HKTWW.mjs +16 -0
- package/dist/sdk/{failure-condition-evaluator-HTPB5FYW.mjs → failure-condition-evaluator-5DZYMCGW.mjs} +4 -4
- package/dist/sdk/failure-condition-evaluator-R6DCDJAV.mjs +18 -0
- package/dist/sdk/{github-frontend-3SDFCCKI.mjs → github-frontend-3PSCKPAJ.mjs} +4 -4
- package/dist/sdk/github-frontend-L3F5JXPJ.mjs +1394 -0
- package/dist/sdk/github-frontend-L3F5JXPJ.mjs.map +1 -0
- package/dist/sdk/{host-QE4L7UXE.mjs → host-54CHV2LW.mjs} +3 -3
- package/dist/sdk/{host-VBBSLUWG.mjs → host-WAU6CT42.mjs} +3 -3
- package/dist/sdk/{host-CVH2CSHM.mjs → host-X5ZZCEWN.mjs} +2 -2
- package/dist/sdk/{routing-YVMTKFDZ.mjs → routing-CVQT4KHX.mjs} +5 -5
- package/dist/sdk/routing-EBAE5SSO.mjs +26 -0
- package/dist/sdk/{schedule-tool-Z5VG67JK.mjs → schedule-tool-POY3CDZL.mjs} +7 -7
- package/dist/sdk/{schedule-tool-ADUXTCY7.mjs → schedule-tool-R2OAATUS.mjs} +7 -7
- package/dist/sdk/{schedule-tool-ZMX3Y7LF.mjs → schedule-tool-Z6QYL2B3.mjs} +3 -3
- package/dist/sdk/{schedule-tool-handler-N7UNABOA.mjs → schedule-tool-handler-J4NUETJ6.mjs} +3 -3
- package/dist/sdk/{schedule-tool-handler-PCERK6ZZ.mjs → schedule-tool-handler-JMAKHPI7.mjs} +7 -7
- package/dist/sdk/{schedule-tool-handler-QOJVFRB4.mjs → schedule-tool-handler-MWFUIQKR.mjs} +7 -7
- package/dist/sdk/sdk.d.mts +33 -0
- package/dist/sdk/sdk.d.ts +33 -0
- package/dist/sdk/sdk.js +2058 -342
- package/dist/sdk/sdk.js.map +1 -1
- package/dist/sdk/sdk.mjs +6 -6
- package/dist/sdk/task-evaluator-HLNXKKVV.mjs +1278 -0
- package/dist/sdk/task-evaluator-HLNXKKVV.mjs.map +1 -0
- package/dist/sdk/{trace-helpers-KXDOJWBL.mjs → trace-helpers-HL5FBX65.mjs} +3 -3
- package/dist/sdk/trace-helpers-WJXYVV4S.mjs +29 -0
- package/dist/sdk/trace-helpers-WJXYVV4S.mjs.map +1 -0
- package/dist/sdk/trace-reader-ZY77OFNM.mjs +266 -0
- package/dist/sdk/trace-reader-ZY77OFNM.mjs.map +1 -0
- package/dist/sdk/track-execution-MKIQXP2C.mjs +136 -0
- package/dist/sdk/track-execution-MKIQXP2C.mjs.map +1 -0
- package/dist/sdk/track-execution-YUXQ6WQH.mjs +136 -0
- package/dist/sdk/track-execution-YUXQ6WQH.mjs.map +1 -0
- package/dist/sdk/{workflow-check-provider-NTHC5ZBF.mjs → workflow-check-provider-SE5I7EMA.mjs} +7 -7
- package/dist/sdk/workflow-check-provider-SE5I7EMA.mjs.map +1 -0
- package/dist/sdk/{workflow-check-provider-SRIMWKLQ.mjs → workflow-check-provider-VKYGI5GK.mjs} +3 -3
- package/dist/sdk/workflow-check-provider-VKYGI5GK.mjs.map +1 -0
- package/dist/sdk/{workflow-check-provider-CJXW2Z4F.mjs → workflow-check-provider-YDGZRI3Z.mjs} +7 -7
- package/dist/sdk/workflow-check-provider-YDGZRI3Z.mjs.map +1 -0
- package/dist/slack/socket-runner.d.ts +12 -0
- package/dist/slack/socket-runner.d.ts.map +1 -1
- package/dist/teams/webhook-runner.d.ts +4 -0
- package/dist/teams/webhook-runner.d.ts.map +1 -1
- package/dist/telegram/polling-runner.d.ts +2 -0
- package/dist/telegram/polling-runner.d.ts.map +1 -1
- package/dist/traces/{run-2026-03-17T13-58-29-402Z.ndjson → run-2026-03-18T19-02-50-465Z.ndjson} +84 -84
- package/dist/{output/traces/run-2026-03-17T13-59-10-403Z.ndjson → traces/run-2026-03-18T19-03-30-428Z.ndjson} +2037 -2037
- package/dist/types/config.d.ts +33 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/whatsapp/webhook-runner.d.ts +4 -0
- package/dist/whatsapp/webhook-runner.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/sdk/a2a-frontend-BDACLGMA.mjs.map +0 -1
- package/dist/sdk/a2a-frontend-IWOUJOIZ.mjs.map +0 -1
- package/dist/sdk/chunk-4DVP6KVC.mjs.map +0 -1
- package/dist/sdk/chunk-DGIH6EX3.mjs.map +0 -1
- package/dist/sdk/chunk-QGBASDYP.mjs.map +0 -1
- package/dist/sdk/chunk-VMVIM4JB.mjs.map +0 -1
- /package/dist/sdk/{check-provider-registry-4YFVBGYU.mjs.map → check-provider-registry-MHXQGUNN.mjs.map} +0 -0
- /package/dist/sdk/{check-provider-registry-4YKTEDKF.mjs.map → check-provider-registry-RRWCXSTG.mjs.map} +0 -0
- /package/dist/sdk/{check-provider-registry-67ZLGDDQ.mjs.map → check-provider-registry-Y33CRFVD.mjs.map} +0 -0
- /package/dist/sdk/{chunk-VXC2XNQJ.mjs.map → chunk-5J3DNRF7.mjs.map} +0 -0
- /package/dist/sdk/{chunk-7YZSSO4X.mjs.map → chunk-6DPPP7LD.mjs.map} +0 -0
- /package/dist/sdk/{chunk-J73GEFPT.mjs.map → chunk-DHETLQIX.mjs.map} +0 -0
- /package/dist/sdk/{config-TSA5FUOM.mjs.map → config-2STD74CJ.mjs.map} +0 -0
- /package/dist/sdk/{failure-condition-evaluator-HTPB5FYW.mjs.map → config-JE4HKTWW.mjs.map} +0 -0
- /package/dist/sdk/{routing-YVMTKFDZ.mjs.map → failure-condition-evaluator-5DZYMCGW.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-ADUXTCY7.mjs.map → failure-condition-evaluator-R6DCDJAV.mjs.map} +0 -0
- /package/dist/sdk/{github-frontend-3SDFCCKI.mjs.map → github-frontend-3PSCKPAJ.mjs.map} +0 -0
- /package/dist/sdk/{host-CVH2CSHM.mjs.map → host-54CHV2LW.mjs.map} +0 -0
- /package/dist/sdk/{host-QE4L7UXE.mjs.map → host-WAU6CT42.mjs.map} +0 -0
- /package/dist/sdk/{host-VBBSLUWG.mjs.map → host-X5ZZCEWN.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-Z5VG67JK.mjs.map → routing-CVQT4KHX.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-ZMX3Y7LF.mjs.map → routing-EBAE5SSO.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-handler-N7UNABOA.mjs.map → schedule-tool-POY3CDZL.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-handler-PCERK6ZZ.mjs.map → schedule-tool-R2OAATUS.mjs.map} +0 -0
- /package/dist/sdk/{schedule-tool-handler-QOJVFRB4.mjs.map → schedule-tool-Z6QYL2B3.mjs.map} +0 -0
- /package/dist/sdk/{trace-helpers-KXDOJWBL.mjs.map → schedule-tool-handler-J4NUETJ6.mjs.map} +0 -0
- /package/dist/sdk/{workflow-check-provider-CJXW2Z4F.mjs.map → schedule-tool-handler-JMAKHPI7.mjs.map} +0 -0
- /package/dist/sdk/{workflow-check-provider-NTHC5ZBF.mjs.map → schedule-tool-handler-MWFUIQKR.mjs.map} +0 -0
- /package/dist/sdk/{workflow-check-provider-SRIMWKLQ.mjs.map → trace-helpers-HL5FBX65.mjs.map} +0 -0
|
@@ -0,0 +1,1278 @@
|
|
|
1
|
+
import {
|
|
2
|
+
init_logger,
|
|
3
|
+
logger
|
|
4
|
+
} from "./chunk-FT3I25QV.mjs";
|
|
5
|
+
import "./chunk-UCMJJ3IM.mjs";
|
|
6
|
+
import {
|
|
7
|
+
__esm,
|
|
8
|
+
__require
|
|
9
|
+
} from "./chunk-J7LXIPZS.mjs";
|
|
10
|
+
|
|
11
|
+
// src/agent-protocol/trace-serializer.ts
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as readline from "readline";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
function resolveBackendConfig(overrides) {
|
|
16
|
+
const explicit = process.env.VISOR_TRACE_BACKEND;
|
|
17
|
+
return {
|
|
18
|
+
type: overrides?.type || explicit || "auto",
|
|
19
|
+
grafanaUrl: overrides?.grafanaUrl || process.env.GRAFANA_URL,
|
|
20
|
+
grafanaDatasourceId: overrides?.grafanaDatasourceId || process.env.GRAFANA_TEMPO_DATASOURCE_ID,
|
|
21
|
+
jaegerUrl: overrides?.jaegerUrl || process.env.JAEGER_URL,
|
|
22
|
+
traceDir: overrides?.traceDir || process.env.VISOR_TRACE_DIR || "output/traces",
|
|
23
|
+
authToken: overrides?.authToken || process.env.GRAFANA_TOKEN
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function parseOTLPResponse(data) {
|
|
27
|
+
const spans = [];
|
|
28
|
+
if (data.batches) {
|
|
29
|
+
for (const batch of data.batches) {
|
|
30
|
+
for (const ss of batch.scopeSpans || []) {
|
|
31
|
+
for (const s of ss.spans || []) {
|
|
32
|
+
spans.push(normalizeOTLPSpan(s));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return spans;
|
|
37
|
+
}
|
|
38
|
+
if (data.data && Array.isArray(data.data)) {
|
|
39
|
+
for (const trace of data.data) {
|
|
40
|
+
for (const s of trace.spans || []) {
|
|
41
|
+
spans.push(normalizeJaegerSpan(s, trace.traceID));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return spans;
|
|
45
|
+
}
|
|
46
|
+
return spans;
|
|
47
|
+
}
|
|
48
|
+
function normalizeOTLPSpan(s) {
|
|
49
|
+
const startNs = parseInt(s.startTimeUnixNano || "0", 10);
|
|
50
|
+
const endNs = parseInt(s.endTimeUnixNano || "0", 10);
|
|
51
|
+
const traceId = decodeOTLPId(s.traceId);
|
|
52
|
+
const spanId = decodeOTLPId(s.spanId);
|
|
53
|
+
const parentSpanId = s.parentSpanId ? decodeOTLPId(s.parentSpanId) : void 0;
|
|
54
|
+
const attributes = {};
|
|
55
|
+
for (const attr of s.attributes || []) {
|
|
56
|
+
const val = attr.value;
|
|
57
|
+
if (val.stringValue !== void 0) attributes[attr.key] = val.stringValue;
|
|
58
|
+
else if (val.intValue !== void 0) attributes[attr.key] = parseInt(val.intValue, 10);
|
|
59
|
+
else if (val.boolValue !== void 0) attributes[attr.key] = val.boolValue;
|
|
60
|
+
else if (val.doubleValue !== void 0) attributes[attr.key] = val.doubleValue;
|
|
61
|
+
}
|
|
62
|
+
const events = [];
|
|
63
|
+
for (const evt of s.events || []) {
|
|
64
|
+
const evtAttrs = {};
|
|
65
|
+
for (const a of evt.attributes || []) {
|
|
66
|
+
const v = a.value;
|
|
67
|
+
if (v.stringValue !== void 0) evtAttrs[a.key] = v.stringValue;
|
|
68
|
+
else if (v.intValue !== void 0) evtAttrs[a.key] = parseInt(v.intValue, 10);
|
|
69
|
+
}
|
|
70
|
+
events.push({ name: evt.name, attributes: evtAttrs });
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
traceId,
|
|
74
|
+
spanId,
|
|
75
|
+
parentSpanId,
|
|
76
|
+
name: s.name || "unknown",
|
|
77
|
+
startTimeMs: startNs / 1e6,
|
|
78
|
+
endTimeMs: endNs / 1e6,
|
|
79
|
+
durationMs: (endNs - startNs) / 1e6,
|
|
80
|
+
attributes,
|
|
81
|
+
events,
|
|
82
|
+
status: s.status?.code === 2 ? "error" : "ok"
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function normalizeJaegerSpan(s, traceId) {
|
|
86
|
+
const attributes = {};
|
|
87
|
+
for (const tag of s.tags || []) {
|
|
88
|
+
attributes[tag.key] = tag.value;
|
|
89
|
+
}
|
|
90
|
+
const events = [];
|
|
91
|
+
for (const log of s.logs || []) {
|
|
92
|
+
const evtAttrs = {};
|
|
93
|
+
for (const f of log.fields || []) evtAttrs[f.key] = f.value;
|
|
94
|
+
events.push({ name: evtAttrs["event"] || "log", attributes: evtAttrs });
|
|
95
|
+
}
|
|
96
|
+
const startUs = s.startTime || 0;
|
|
97
|
+
const durationUs = s.duration || 0;
|
|
98
|
+
return {
|
|
99
|
+
traceId,
|
|
100
|
+
spanId: s.spanID,
|
|
101
|
+
parentSpanId: s.references?.find((r) => r.refType === "CHILD_OF")?.spanID,
|
|
102
|
+
name: s.operationName || "unknown",
|
|
103
|
+
startTimeMs: startUs / 1e3,
|
|
104
|
+
endTimeMs: (startUs + durationUs) / 1e3,
|
|
105
|
+
durationMs: durationUs / 1e3,
|
|
106
|
+
attributes,
|
|
107
|
+
events,
|
|
108
|
+
status: attributes["otel.status_code"] === "ERROR" || attributes["error"] === true ? "error" : "ok"
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function decodeOTLPId(id) {
|
|
112
|
+
if (!id) return "";
|
|
113
|
+
if (/^[0-9a-f]+$/i.test(id)) return id.toLowerCase();
|
|
114
|
+
try {
|
|
115
|
+
return Buffer.from(id, "base64").toString("hex");
|
|
116
|
+
} catch {
|
|
117
|
+
return id;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function parseLocalNDJSONSpans(spans) {
|
|
121
|
+
return spans.map((s) => {
|
|
122
|
+
const startMs = timeValueToMs(s.startTime || [0, 0]);
|
|
123
|
+
const endMs = timeValueToMs(s.endTime || s.startTime || [0, 0]);
|
|
124
|
+
const events = (s.events || []).map((e) => ({
|
|
125
|
+
name: e.name,
|
|
126
|
+
attributes: e.attributes || {}
|
|
127
|
+
}));
|
|
128
|
+
return {
|
|
129
|
+
traceId: s.traceId || "",
|
|
130
|
+
spanId: s.spanId || "",
|
|
131
|
+
parentSpanId: s.parentSpanId || void 0,
|
|
132
|
+
name: s.name || "unknown",
|
|
133
|
+
startTimeMs: startMs,
|
|
134
|
+
endTimeMs: endMs,
|
|
135
|
+
durationMs: endMs - startMs,
|
|
136
|
+
attributes: s.attributes || {},
|
|
137
|
+
events,
|
|
138
|
+
status: s.status?.code === 2 ? "error" : "ok"
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function timeValueToMs(tv) {
|
|
143
|
+
return tv[0] * 1e3 + tv[1] / 1e6;
|
|
144
|
+
}
|
|
145
|
+
async function fetchTraceSpans(traceId, config) {
|
|
146
|
+
const cfg = resolveBackendConfig(config);
|
|
147
|
+
const tryGrafana = cfg.type === "grafana" || cfg.type === "auto";
|
|
148
|
+
const tryJaeger = cfg.type === "jaeger" || cfg.type === "auto";
|
|
149
|
+
const tryFile = cfg.type === "file" || cfg.type === "auto";
|
|
150
|
+
if (tryGrafana) {
|
|
151
|
+
const spans = await fetchFromGrafanaTempo(traceId, cfg);
|
|
152
|
+
if (spans && spans.length > 0) return spans;
|
|
153
|
+
}
|
|
154
|
+
if (tryJaeger) {
|
|
155
|
+
const spans = await fetchFromJaeger(traceId, cfg);
|
|
156
|
+
if (spans && spans.length > 0) return spans;
|
|
157
|
+
}
|
|
158
|
+
if (tryFile) {
|
|
159
|
+
const spans = await fetchFromLocalFiles(traceId, cfg);
|
|
160
|
+
if (spans && spans.length > 0) return spans;
|
|
161
|
+
}
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
async function fetchFromGrafanaTempo(traceId, cfg) {
|
|
165
|
+
let grafanaUrl = cfg.grafanaUrl;
|
|
166
|
+
if (!grafanaUrl) {
|
|
167
|
+
const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
168
|
+
if (otlpEndpoint) {
|
|
169
|
+
const url = new URL(otlpEndpoint);
|
|
170
|
+
const host = url.hostname;
|
|
171
|
+
for (const port of ["3000", "8001", "80"]) {
|
|
172
|
+
try {
|
|
173
|
+
const testUrl = `http://${host}:${port}/api/health`;
|
|
174
|
+
const resp = await httpGet(testUrl, cfg.authToken, 2e3);
|
|
175
|
+
if (resp && resp.includes('"database"')) {
|
|
176
|
+
grafanaUrl = `http://${host}:${port}`;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (!grafanaUrl) return null;
|
|
185
|
+
try {
|
|
186
|
+
let dsId = cfg.grafanaDatasourceId;
|
|
187
|
+
if (!dsId) {
|
|
188
|
+
const dsResp = await httpGet(`${grafanaUrl}/api/datasources`, cfg.authToken);
|
|
189
|
+
if (dsResp) {
|
|
190
|
+
const datasources = JSON.parse(dsResp);
|
|
191
|
+
const tempo = datasources.find((d) => d.type === "tempo");
|
|
192
|
+
if (tempo) dsId = tempo.id;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!dsId) return null;
|
|
196
|
+
const traceUrl = `${grafanaUrl}/api/datasources/proxy/${dsId}/api/traces/${traceId}`;
|
|
197
|
+
const resp = await httpGet(traceUrl, cfg.authToken);
|
|
198
|
+
if (!resp) return null;
|
|
199
|
+
const data = JSON.parse(resp);
|
|
200
|
+
return parseOTLPResponse(data);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
logger.debug(`[TraceSerializer] Grafana Tempo fetch failed: ${err}`);
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function fetchFromJaeger(traceId, cfg) {
|
|
207
|
+
let jaegerUrl = cfg.jaegerUrl;
|
|
208
|
+
if (!jaegerUrl) {
|
|
209
|
+
const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
210
|
+
const host = otlpEndpoint ? new URL(otlpEndpoint).hostname : "localhost";
|
|
211
|
+
for (const port of ["16686"]) {
|
|
212
|
+
try {
|
|
213
|
+
const testUrl = `http://${host}:${port}/api/services`;
|
|
214
|
+
const resp = await httpGet(testUrl, void 0, 2e3);
|
|
215
|
+
if (resp && resp.includes('"data"')) {
|
|
216
|
+
jaegerUrl = `http://${host}:${port}`;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!jaegerUrl) return null;
|
|
224
|
+
try {
|
|
225
|
+
const traceUrl = `${jaegerUrl}/api/traces/${traceId}`;
|
|
226
|
+
const resp = await httpGet(traceUrl, cfg.authToken);
|
|
227
|
+
if (!resp) return null;
|
|
228
|
+
const data = JSON.parse(resp);
|
|
229
|
+
return parseOTLPResponse(data);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
logger.debug(`[TraceSerializer] Jaeger fetch failed: ${err}`);
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function fetchFromLocalFiles(traceId, cfg) {
|
|
236
|
+
const traceFile = await findTraceFile(traceId, cfg.traceDir);
|
|
237
|
+
if (!traceFile) return null;
|
|
238
|
+
try {
|
|
239
|
+
const { parseNDJSONTrace } = await import("./trace-reader-ZY77OFNM.mjs");
|
|
240
|
+
const trace = await parseNDJSONTrace(traceFile);
|
|
241
|
+
return parseLocalNDJSONSpans(trace.spans);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
logger.debug(`[TraceSerializer] Local file parse failed: ${err}`);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function httpGet(url, authToken, timeoutMs) {
|
|
248
|
+
try {
|
|
249
|
+
const controller = new AbortController();
|
|
250
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs || 1e4);
|
|
251
|
+
const headers = {};
|
|
252
|
+
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
|
253
|
+
const resp = await fetch(url, {
|
|
254
|
+
signal: controller.signal,
|
|
255
|
+
headers
|
|
256
|
+
});
|
|
257
|
+
clearTimeout(timeout);
|
|
258
|
+
if (!resp.ok) return null;
|
|
259
|
+
return await resp.text();
|
|
260
|
+
} catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async function findTraceFile(traceId, traceDir) {
|
|
265
|
+
const dir = traceDir || process.env.VISOR_TRACE_DIR || "output/traces";
|
|
266
|
+
if (!fs.existsSync(dir)) return null;
|
|
267
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".ndjson"));
|
|
268
|
+
for (const file of files) {
|
|
269
|
+
const filePath = path.join(dir, file);
|
|
270
|
+
try {
|
|
271
|
+
const firstLine = await readFirstLine(filePath);
|
|
272
|
+
if (!firstLine) continue;
|
|
273
|
+
const parsed = JSON.parse(firstLine);
|
|
274
|
+
if (parsed.traceId === traceId) return filePath;
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
async function readFirstLine(filePath) {
|
|
281
|
+
return new Promise((resolve, reject) => {
|
|
282
|
+
const stream = fs.createReadStream(filePath, { encoding: "utf-8" });
|
|
283
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
284
|
+
let resolved = false;
|
|
285
|
+
rl.on("line", (line) => {
|
|
286
|
+
if (!resolved) {
|
|
287
|
+
resolved = true;
|
|
288
|
+
rl.close();
|
|
289
|
+
stream.destroy();
|
|
290
|
+
resolve(line.trim() || null);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
rl.on("close", () => {
|
|
294
|
+
if (!resolved) resolve(null);
|
|
295
|
+
});
|
|
296
|
+
rl.on("error", reject);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
function isNoiseSpan(span) {
|
|
300
|
+
return NOISE_SPAN_NAMES.has(span.name);
|
|
301
|
+
}
|
|
302
|
+
function isWrapperSpan(span) {
|
|
303
|
+
return WRAPPER_SPAN_NAMES.has(span.name);
|
|
304
|
+
}
|
|
305
|
+
function buildSpanTree(spans) {
|
|
306
|
+
const filtered = spans.filter((s) => !isNoiseSpan(s));
|
|
307
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
308
|
+
for (const span of filtered) {
|
|
309
|
+
nodeMap.set(span.spanId, { span, children: [] });
|
|
310
|
+
}
|
|
311
|
+
let root;
|
|
312
|
+
for (const span of filtered) {
|
|
313
|
+
const node = nodeMap.get(span.spanId);
|
|
314
|
+
if (!span.parentSpanId) {
|
|
315
|
+
root = node;
|
|
316
|
+
} else {
|
|
317
|
+
let parentId = span.parentSpanId;
|
|
318
|
+
while (parentId && !nodeMap.has(parentId)) {
|
|
319
|
+
const parentSpan = spans.find((s) => s.spanId === parentId);
|
|
320
|
+
parentId = parentSpan?.parentSpanId;
|
|
321
|
+
}
|
|
322
|
+
if (parentId) {
|
|
323
|
+
const parent = nodeMap.get(parentId);
|
|
324
|
+
if (parent) parent.children.push(node);
|
|
325
|
+
} else if (!root) {
|
|
326
|
+
root = node;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (!root) {
|
|
331
|
+
const sorted = [...nodeMap.values()].sort((a, b) => b.span.durationMs - a.span.durationMs);
|
|
332
|
+
root = sorted[0] || { span: filtered[0], children: [] };
|
|
333
|
+
}
|
|
334
|
+
const sortChildren = (node) => {
|
|
335
|
+
node.children.sort((a, b) => a.span.startTimeMs - b.span.startTimeMs);
|
|
336
|
+
node.children.forEach(sortChildren);
|
|
337
|
+
};
|
|
338
|
+
sortChildren(root);
|
|
339
|
+
const unwrap = (node) => {
|
|
340
|
+
node.children = node.children.map(unwrap);
|
|
341
|
+
const newChildren = [];
|
|
342
|
+
for (const child of node.children) {
|
|
343
|
+
if (isWrapperSpan(child.span)) {
|
|
344
|
+
newChildren.push(...child.children);
|
|
345
|
+
} else {
|
|
346
|
+
newChildren.push(child);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
node.children = newChildren;
|
|
350
|
+
return node;
|
|
351
|
+
};
|
|
352
|
+
unwrap(root);
|
|
353
|
+
const removeDelegateEchos = (node) => {
|
|
354
|
+
const hasDelegateChild = node.children.some((c) => c.span.name === "search.delegate");
|
|
355
|
+
if (hasDelegateChild) {
|
|
356
|
+
node.children = node.children.filter((c) => {
|
|
357
|
+
if (c.span.name !== "probe.event.tool.result") return true;
|
|
358
|
+
const toolName = c.span.attributes["tool.name"];
|
|
359
|
+
return toolName !== "search";
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
node.children.forEach(removeDelegateEchos);
|
|
363
|
+
};
|
|
364
|
+
removeDelegateEchos(root);
|
|
365
|
+
return root;
|
|
366
|
+
}
|
|
367
|
+
function dedupeKey(text) {
|
|
368
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 100).toLowerCase();
|
|
369
|
+
}
|
|
370
|
+
function dedupeOrRegister(ctx, kind, text, spanName) {
|
|
371
|
+
if (!text || text.length < 20) return null;
|
|
372
|
+
const key = dedupeKey(text);
|
|
373
|
+
if (!key) return null;
|
|
374
|
+
const map = ctx[kind];
|
|
375
|
+
const existing = map.get(key);
|
|
376
|
+
if (existing && existing !== spanName) {
|
|
377
|
+
return existing;
|
|
378
|
+
}
|
|
379
|
+
const otherMap = kind === "outputs" ? ctx.intents : ctx.outputs;
|
|
380
|
+
const crossRef = otherMap.get(key);
|
|
381
|
+
if (crossRef && crossRef !== spanName) {
|
|
382
|
+
map.set(key, spanName);
|
|
383
|
+
return crossRef;
|
|
384
|
+
}
|
|
385
|
+
map.set(key, spanName);
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
async function serializeTraceForPrompt(traceIdOrPath, maxChars, backendConfig, taskResponse) {
|
|
389
|
+
let spans;
|
|
390
|
+
if (traceIdOrPath.includes("/") || traceIdOrPath.endsWith(".ndjson")) {
|
|
391
|
+
const { parseNDJSONTrace } = await import("./trace-reader-ZY77OFNM.mjs");
|
|
392
|
+
const trace = await parseNDJSONTrace(traceIdOrPath);
|
|
393
|
+
spans = parseLocalNDJSONSpans(trace.spans);
|
|
394
|
+
} else {
|
|
395
|
+
spans = await fetchTraceSpans(traceIdOrPath, backendConfig);
|
|
396
|
+
}
|
|
397
|
+
if (spans.length === 0) {
|
|
398
|
+
return "(no trace data available)";
|
|
399
|
+
}
|
|
400
|
+
const tree = buildSpanTree(spans);
|
|
401
|
+
const routeIntentTopic = extractRouteIntentTopic(spans);
|
|
402
|
+
const fullOutput = (maxChars ?? 4e3) > 1e5;
|
|
403
|
+
return renderSpanYaml(tree, spans, {
|
|
404
|
+
maxChars: maxChars ?? 4e3,
|
|
405
|
+
fallbackIntent: routeIntentTopic,
|
|
406
|
+
fullOutput,
|
|
407
|
+
taskResponse
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function renderSpanYaml(tree, allSpans, opts) {
|
|
411
|
+
const fullOutput = opts?.fullOutput ?? false;
|
|
412
|
+
const maxLen = fullOutput ? 1e5 : 120;
|
|
413
|
+
const dedup = { outputs: /* @__PURE__ */ new Map(), intents: /* @__PURE__ */ new Map() };
|
|
414
|
+
const lines = [];
|
|
415
|
+
renderYamlNode(tree, 0, lines, dedup, opts?.fallbackIntent, fullOutput, maxLen);
|
|
416
|
+
if (opts?.taskResponse) {
|
|
417
|
+
while (lines.length > 0 && /^\s*output:\s*=\s*\S+/.test(lines[lines.length - 1])) {
|
|
418
|
+
lines.pop();
|
|
419
|
+
}
|
|
420
|
+
const ml = fullOutput ? 1e5 : 500;
|
|
421
|
+
const text = opts.taskResponse.replace(/\*\*/g, "").replace(/`/g, "").trim();
|
|
422
|
+
if (fullOutput) {
|
|
423
|
+
lines.push(" response: |");
|
|
424
|
+
for (const line of text.split("\n")) {
|
|
425
|
+
lines.push(` ${line}`);
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
const truncated = truncate(text.replace(/\n/g, " "), ml);
|
|
429
|
+
lines.push(` response: ${truncated}`);
|
|
430
|
+
if (text.length > ml) {
|
|
431
|
+
lines.push(" # use --full for complete response");
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return lines.join("\n");
|
|
436
|
+
}
|
|
437
|
+
function renderYamlNode(node, indent, lines, dedup, fallbackIntent, fullOutput, maxLen, parentSpan) {
|
|
438
|
+
const pad = " ".repeat(indent);
|
|
439
|
+
const attrs = node.span.attributes;
|
|
440
|
+
const duration = formatDurationMs(node.span.durationMs);
|
|
441
|
+
const name = node.span.name;
|
|
442
|
+
const ml = maxLen ?? 120;
|
|
443
|
+
const parentCheckId = parentSpan?.attributes["visor.check.id"];
|
|
444
|
+
const parentCheckName = parentCheckId ? String(parentCheckId).replace(/^visor\.check\./, "") : void 0;
|
|
445
|
+
const displayName = name === "ai.request" && parentCheckName ? parentCheckName : String(attrs["visor.check.id"] || name).replace(/^visor\.check\./, "");
|
|
446
|
+
const toolName = attrs["tool.name"] || attrs["visor.tool.name"];
|
|
447
|
+
if (toolName) {
|
|
448
|
+
const toolInput = extractToolInput(String(toolName), attrs);
|
|
449
|
+
const toolResultLen = attrs["tool.result.length"] || attrs["tool.result.count"];
|
|
450
|
+
const tn = String(toolName);
|
|
451
|
+
const isSearchTool = tn === "search" || tn === "searchCode" || tn === "search_code";
|
|
452
|
+
const numLen = toolResultLen ? Number(toolResultLen) : -1;
|
|
453
|
+
const noResults = isSearchTool && numLen >= 0 && numLen < 500;
|
|
454
|
+
const resultSize = noResults ? " \u2192 no results" : toolResultLen ? ` \u2192 ${formatSize(numLen)}` : "";
|
|
455
|
+
const successMark = attrs["tool.success"] === false ? " \u2717" : "";
|
|
456
|
+
lines.push(`${pad}- ${tn}(${toolInput})${resultSize}${successMark}`);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (name === "search.delegate") {
|
|
460
|
+
const query = attrs["search.query"] || "";
|
|
461
|
+
lines.push(`${pad}search.delegate("${truncate(String(query), 80)}") \u2014 ${duration}:`);
|
|
462
|
+
for (const child of node.children) {
|
|
463
|
+
renderYamlNode(
|
|
464
|
+
child,
|
|
465
|
+
indent + 1,
|
|
466
|
+
lines,
|
|
467
|
+
dedup,
|
|
468
|
+
fallbackIntent,
|
|
469
|
+
fullOutput,
|
|
470
|
+
maxLen,
|
|
471
|
+
node.span
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (name === "ai.request") {
|
|
477
|
+
const model = attrs["ai.model"] || attrs["gen_ai.request.model"] || "?";
|
|
478
|
+
const tokensIn = attrs["ai.input_length"] || attrs["gen_ai.usage.input_tokens"] || "";
|
|
479
|
+
const tokensOut = attrs["gen_ai.usage.output_tokens"] || "";
|
|
480
|
+
const tokenParts = [];
|
|
481
|
+
if (tokensIn) tokenParts.push(`${tokensIn} in`);
|
|
482
|
+
if (tokensOut) tokenParts.push(`${tokensOut} out`);
|
|
483
|
+
const tokenStr = tokenParts.length > 0 ? ` \u2014 ${tokenParts.join(", ")}` : "";
|
|
484
|
+
const hasChildren2 = node.children.length > 0;
|
|
485
|
+
lines.push(`${pad}ai: ${model} \u2014 ${duration}${tokenStr}${hasChildren2 ? ":" : ""}`);
|
|
486
|
+
const aiInput = String(attrs["ai.input"] || "");
|
|
487
|
+
let intent = extractAIIntent(aiInput, ml);
|
|
488
|
+
if (!intent && parentSpan) {
|
|
489
|
+
const promptPreview = String(
|
|
490
|
+
parentSpan.attributes["visor.provider.request.prompt.preview"] || ""
|
|
491
|
+
);
|
|
492
|
+
if (promptPreview) intent = extractAIIntent(promptPreview, ml);
|
|
493
|
+
if (!intent) {
|
|
494
|
+
const inputOutputs = String(parentSpan.attributes["visor.check.input.outputs"] || "");
|
|
495
|
+
if (inputOutputs) {
|
|
496
|
+
try {
|
|
497
|
+
const o = JSON.parse(inputOutputs);
|
|
498
|
+
const t = o["route-intent"]?.topic;
|
|
499
|
+
if (t) intent = truncate(String(t), ml);
|
|
500
|
+
} catch {
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (!intent && fallbackIntent && parentSpan?.name !== "search.delegate") {
|
|
506
|
+
intent = fallbackIntent;
|
|
507
|
+
}
|
|
508
|
+
if (intent) {
|
|
509
|
+
const intentRef = dedupeOrRegister(dedup, "intents", intent, displayName);
|
|
510
|
+
if (intentRef) {
|
|
511
|
+
lines.push(`${pad} intent: = ${intentRef}`);
|
|
512
|
+
} else {
|
|
513
|
+
lines.push(`${pad} intent: ${intent}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
for (const child of node.children) {
|
|
517
|
+
renderYamlNode(
|
|
518
|
+
child,
|
|
519
|
+
indent + 1,
|
|
520
|
+
lines,
|
|
521
|
+
dedup,
|
|
522
|
+
fallbackIntent,
|
|
523
|
+
fullOutput,
|
|
524
|
+
maxLen,
|
|
525
|
+
node.span
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
if (parentSpan) {
|
|
529
|
+
const checkOutput = String(parentSpan.attributes["visor.check.output"] || "");
|
|
530
|
+
if (checkOutput) {
|
|
531
|
+
renderYamlOutput(
|
|
532
|
+
checkOutput,
|
|
533
|
+
`${pad} `,
|
|
534
|
+
"output",
|
|
535
|
+
displayName,
|
|
536
|
+
dedup,
|
|
537
|
+
lines,
|
|
538
|
+
fullOutput,
|
|
539
|
+
ml
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (name === "visor.run") {
|
|
546
|
+
const source = attrs["visor.run.source"] || "";
|
|
547
|
+
const visorVersion = attrs["visor.version"] || "";
|
|
548
|
+
const probeVersion = attrs["probe.version"] || "";
|
|
549
|
+
const slackUser = attrs["slack.user_id"] || "";
|
|
550
|
+
lines.push(`${pad}visor.run:`);
|
|
551
|
+
lines.push(`${pad} trace_id: ${node.span.traceId}`);
|
|
552
|
+
if (visorVersion) lines.push(`${pad} visor: ${visorVersion}`);
|
|
553
|
+
if (probeVersion) lines.push(`${pad} probe: ${probeVersion}`);
|
|
554
|
+
if (source) lines.push(`${pad} source: ${source}`);
|
|
555
|
+
if (slackUser) lines.push(`${pad} slack_user: ${slackUser}`);
|
|
556
|
+
lines.push(`${pad} duration: ${duration}`);
|
|
557
|
+
for (const child of node.children) {
|
|
558
|
+
renderYamlNode(
|
|
559
|
+
child,
|
|
560
|
+
indent + 1,
|
|
561
|
+
lines,
|
|
562
|
+
dedup,
|
|
563
|
+
fallbackIntent,
|
|
564
|
+
fullOutput,
|
|
565
|
+
maxLen,
|
|
566
|
+
node.span
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const checkId = attrs["visor.check.id"];
|
|
572
|
+
const checkType = attrs["visor.check.type"];
|
|
573
|
+
if (checkId || name.startsWith("visor.check.")) {
|
|
574
|
+
const cleanName = String(checkId || name).replace(/^visor\.check\./, "");
|
|
575
|
+
const errMark2 = node.span.status === "error" ? " \u2717" : "";
|
|
576
|
+
lines.push(`${pad}${cleanName}:${errMark2}`);
|
|
577
|
+
if (checkType) lines.push(`${pad} type: ${checkType}`);
|
|
578
|
+
lines.push(`${pad} duration: ${duration}`);
|
|
579
|
+
const inputContext = String(attrs["visor.check.input.context"] || "");
|
|
580
|
+
const inputOutputs = String(attrs["visor.check.input.outputs"] || "");
|
|
581
|
+
const question = extractQuestionFromContext(inputContext, inputOutputs);
|
|
582
|
+
if (question || inputOutputs && inputOutputs !== "{}") {
|
|
583
|
+
renderYamlInput(inputOutputs, `${pad} `, lines, fullOutput, ml, question);
|
|
584
|
+
}
|
|
585
|
+
for (const child of node.children) {
|
|
586
|
+
renderYamlNode(
|
|
587
|
+
child,
|
|
588
|
+
indent + 1,
|
|
589
|
+
lines,
|
|
590
|
+
dedup,
|
|
591
|
+
fallbackIntent,
|
|
592
|
+
fullOutput,
|
|
593
|
+
maxLen,
|
|
594
|
+
node.span
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
const hasDirectAiChild = node.children.some((c) => c.span.name === "ai.request");
|
|
598
|
+
if (!hasDirectAiChild) {
|
|
599
|
+
const output = String(attrs["visor.check.output"] || "");
|
|
600
|
+
if (output) {
|
|
601
|
+
renderYamlOutput(output, `${pad} `, "output", cleanName, dedup, lines, fullOutput, ml);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const errMark = node.span.status === "error" ? " \u2717" : "";
|
|
607
|
+
const hasChildren = node.children.length > 0;
|
|
608
|
+
lines.push(`${pad}${name} \u2014 ${duration}${errMark}${hasChildren ? ":" : ""}`);
|
|
609
|
+
for (const child of node.children) {
|
|
610
|
+
renderYamlNode(child, indent + 1, lines, dedup, fallbackIntent, fullOutput, maxLen, node.span);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function renderYamlOutput(rawOutput, pad, label, spanName, dedup, lines, fullOutput, maxLen) {
|
|
614
|
+
const ml = maxLen ?? 120;
|
|
615
|
+
let obj;
|
|
616
|
+
try {
|
|
617
|
+
obj = JSON.parse(rawOutput);
|
|
618
|
+
} catch {
|
|
619
|
+
obj = parseTruncatedJson(rawOutput);
|
|
620
|
+
if (!obj || typeof obj !== "object") return;
|
|
621
|
+
}
|
|
622
|
+
if (typeof obj === "object" && !Array.isArray(obj)) {
|
|
623
|
+
const keys = Object.keys(obj);
|
|
624
|
+
if (keys.length === 1 && typeof obj[keys[0]] === "object" && obj[keys[0]] !== null) {
|
|
625
|
+
obj = obj[keys[0]];
|
|
626
|
+
}
|
|
627
|
+
const objKeys = Object.keys(obj);
|
|
628
|
+
if (objKeys.length === 1 && objKeys[0] === "text" && typeof obj.text === "string") {
|
|
629
|
+
const text = obj.text.replace(/\*\*/g, "").replace(/`/g, "").trim();
|
|
630
|
+
const flat = text.replace(/\n/g, " ");
|
|
631
|
+
const preview2 = fullOutput ? flat : truncate(flat, ml);
|
|
632
|
+
const ref2 = dedupeOrRegister(dedup, "outputs", truncate(flat, 100), spanName);
|
|
633
|
+
if (ref2) {
|
|
634
|
+
lines.push(`${pad}${label}: = ${ref2}`);
|
|
635
|
+
} else {
|
|
636
|
+
lines.push(`${pad}${label}: ${preview2}`);
|
|
637
|
+
}
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const preview = formatJsonPreview(obj, 200);
|
|
642
|
+
if (!preview) return;
|
|
643
|
+
const ref = dedupeOrRegister(dedup, "outputs", preview, spanName);
|
|
644
|
+
if (ref) {
|
|
645
|
+
lines.push(`${pad}${label}: = ${ref}`);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
renderYamlValue(obj, pad, label, lines, fullOutput, ml);
|
|
649
|
+
}
|
|
650
|
+
function renderYamlValue(val, pad, key, lines, fullOutput, maxLen, depth) {
|
|
651
|
+
const ml = maxLen ?? 120;
|
|
652
|
+
const d = depth ?? 0;
|
|
653
|
+
if (val === null || val === void 0) return;
|
|
654
|
+
if (typeof val === "boolean" || typeof val === "number") {
|
|
655
|
+
lines.push(`${pad}${key}: ${val}`);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (typeof val === "string") {
|
|
659
|
+
if (val.startsWith("{") || val.startsWith("[")) return;
|
|
660
|
+
const clean = val.replace(/\*\*/g, "").replace(/`/g, "").trim();
|
|
661
|
+
if (fullOutput && clean.length > 100 && clean.includes("\n")) {
|
|
662
|
+
lines.push(`${pad}${key}: |`);
|
|
663
|
+
for (const line of clean.split("\n").slice(0, fullOutput ? 500 : 5)) {
|
|
664
|
+
lines.push(`${pad} ${line}`);
|
|
665
|
+
}
|
|
666
|
+
} else {
|
|
667
|
+
const flat = clean.replace(/\n/g, " ");
|
|
668
|
+
const truncVal = fullOutput ? flat : truncate(flat, ml);
|
|
669
|
+
lines.push(`${pad}${key}: ${truncVal}`);
|
|
670
|
+
}
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (Array.isArray(val)) {
|
|
674
|
+
if (val.length === 0) {
|
|
675
|
+
lines.push(`${pad}${key}: []`);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const maxItems = fullOutput ? 20 : 3;
|
|
679
|
+
lines.push(`${pad}${key}:`);
|
|
680
|
+
for (let i = 0; i < Math.min(val.length, maxItems); i++) {
|
|
681
|
+
const item = val[i];
|
|
682
|
+
if (typeof item === "object" && item !== null) {
|
|
683
|
+
const entries = Object.entries(item).filter(
|
|
684
|
+
([k]) => k !== "raw" && k !== "skills" && k !== "tags"
|
|
685
|
+
);
|
|
686
|
+
if (entries.length > 0) {
|
|
687
|
+
const [firstKey, firstVal] = entries[0];
|
|
688
|
+
if (firstVal === null || firstVal === void 0 || typeof firstVal !== "object") {
|
|
689
|
+
const sv = typeof firstVal === "string" ? fullOutput ? firstVal.split("\n")[0] : truncate(firstVal.split("\n")[0], ml) : String(firstVal ?? "");
|
|
690
|
+
lines.push(`${pad} - ${firstKey}: ${sv}`);
|
|
691
|
+
} else {
|
|
692
|
+
lines.push(`${pad} - ${firstKey}:`);
|
|
693
|
+
for (const [ck, cv] of Object.entries(firstVal)) {
|
|
694
|
+
if (ck === "raw" || ck === "skills" || ck === "tags") continue;
|
|
695
|
+
renderYamlValue(cv, `${pad} `, ck, lines, fullOutput, ml, d + 2);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
for (let j = 1; j < entries.length; j++) {
|
|
699
|
+
const [k, v] = entries[j];
|
|
700
|
+
renderYamlValue(v, `${pad} `, k, lines, fullOutput, ml, d + 1);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
lines.push(`${pad} - ${truncate(String(item), ml)}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (val.length > maxItems) {
|
|
708
|
+
lines.push(`${pad} # ... ${val.length - maxItems} more`);
|
|
709
|
+
}
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (typeof val === "object") {
|
|
713
|
+
if (d > 3) {
|
|
714
|
+
const keys = Object.keys(val);
|
|
715
|
+
lines.push(`${pad}${key}: {${keys.slice(0, 4).join(", ")}${keys.length > 4 ? ", ..." : ""}}`);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
lines.push(`${pad}${key}:`);
|
|
719
|
+
for (const [k, v] of Object.entries(val)) {
|
|
720
|
+
if (k === "raw" || k === "skills" || k === "tags") continue;
|
|
721
|
+
renderYamlValue(v, `${pad} `, k, lines, fullOutput, ml, d + 1);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
function extractQuestionFromContext(contextStr, inputOutputsStr) {
|
|
726
|
+
if (!contextStr) return void 0;
|
|
727
|
+
try {
|
|
728
|
+
const ctx = JSON.parse(contextStr);
|
|
729
|
+
const outputs = ctx.outputs || {};
|
|
730
|
+
const routeIntent = outputs["route-intent"];
|
|
731
|
+
if (routeIntent) {
|
|
732
|
+
const topic = routeIntent.topic || routeIntent.intent || routeIntent.question;
|
|
733
|
+
if (topic && typeof topic === "string") return topic;
|
|
734
|
+
if (typeof routeIntent === "string") return routeIntent;
|
|
735
|
+
}
|
|
736
|
+
const args = ctx.args || {};
|
|
737
|
+
if (args.topic && typeof args.topic === "string") return args.topic;
|
|
738
|
+
if (args.question && typeof args.question === "string") return args.question;
|
|
739
|
+
if (args.intent && typeof args.intent === "string") return args.intent;
|
|
740
|
+
for (const key of Object.keys(outputs)) {
|
|
741
|
+
const val = outputs[key];
|
|
742
|
+
if (typeof val === "object" && val !== null) {
|
|
743
|
+
if (val.topic && typeof val.topic === "string") {
|
|
744
|
+
try {
|
|
745
|
+
const depOutputs = JSON.parse(inputOutputsStr);
|
|
746
|
+
if (depOutputs[key]) continue;
|
|
747
|
+
} catch {
|
|
748
|
+
}
|
|
749
|
+
return val.topic;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
} catch {
|
|
754
|
+
const topicMatch = contextStr.match(/"topic"\s*:\s*"([^"]+)"/);
|
|
755
|
+
if (topicMatch) return topicMatch[1];
|
|
756
|
+
}
|
|
757
|
+
return void 0;
|
|
758
|
+
}
|
|
759
|
+
function renderYamlInput(inputOutputsStr, pad, lines, fullOutput, maxLen, question) {
|
|
760
|
+
const ml = maxLen ?? 120;
|
|
761
|
+
if (question) {
|
|
762
|
+
lines.push(`${pad}input: ${truncate(question, fullOutput ? 1e5 : ml)}`);
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
const inputs = JSON.parse(inputOutputsStr);
|
|
766
|
+
if (typeof inputs !== "object" || inputs === null) return;
|
|
767
|
+
const keys = Object.keys(inputs);
|
|
768
|
+
if (keys.length === 0) return;
|
|
769
|
+
if (!question) lines.push(`${pad}input:`);
|
|
770
|
+
for (const key of keys) {
|
|
771
|
+
const val = inputs[key];
|
|
772
|
+
if (typeof val === "object" && val !== null) {
|
|
773
|
+
renderYamlValue(val, `${pad} `, key, lines, fullOutput, ml, 0);
|
|
774
|
+
} else {
|
|
775
|
+
lines.push(`${pad} ${key}: ${truncate(String(val), ml)}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
} catch {
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function extractToolInput(toolName, attrs) {
|
|
782
|
+
const result = String(attrs["tool.result"] || "");
|
|
783
|
+
const explicitInput = String(attrs["tool.input"] || "");
|
|
784
|
+
if (explicitInput) return truncate(explicitInput, 80);
|
|
785
|
+
switch (toolName) {
|
|
786
|
+
case "search": {
|
|
787
|
+
const patMatch = result.match(/Pattern: (.+)/);
|
|
788
|
+
const pathMatch = result.match(/Path: \S+\/([^\s/]+)\s/);
|
|
789
|
+
const pattern = patMatch ? patMatch[1].trim() : "";
|
|
790
|
+
const inPath = pathMatch ? pathMatch[1] : "";
|
|
791
|
+
if (pattern && inPath) return `"${truncate(pattern, 50)}" in ${inPath}`;
|
|
792
|
+
if (pattern) return `"${truncate(pattern, 60)}"`;
|
|
793
|
+
return "";
|
|
794
|
+
}
|
|
795
|
+
case "extract": {
|
|
796
|
+
const fileMatch = result.match(/Files to extract:\n\s*(\S+)/);
|
|
797
|
+
if (fileMatch) {
|
|
798
|
+
const filePath = fileMatch[1];
|
|
799
|
+
const parts = filePath.split("/");
|
|
800
|
+
return parts.length > 2 ? parts.slice(-2).join("/") : parts[parts.length - 1];
|
|
801
|
+
}
|
|
802
|
+
return "";
|
|
803
|
+
}
|
|
804
|
+
case "listFiles": {
|
|
805
|
+
const pathMatch = result.match(/^(\S+):/);
|
|
806
|
+
if (pathMatch) {
|
|
807
|
+
const parts = pathMatch[1].split("/");
|
|
808
|
+
return parts[parts.length - 1] || "";
|
|
809
|
+
}
|
|
810
|
+
return "";
|
|
811
|
+
}
|
|
812
|
+
default:
|
|
813
|
+
return truncate(explicitInput, 60);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
function extractAIIntent(input, maxLen = 150) {
|
|
817
|
+
if (!input || input.length < 20) return "";
|
|
818
|
+
const qMatch = input.match(/<question>([\s\S]*?)<\/question>/);
|
|
819
|
+
if (qMatch) return truncate(qMatch[1].trim(), maxLen);
|
|
820
|
+
const crMatch = input.match(/## Current Request\s*\n(?:User: )?(.+)/);
|
|
821
|
+
if (crMatch) return truncate(crMatch[1].trim(), maxLen);
|
|
822
|
+
const userMatch = input.match(/(?:^|\n)User: (.+)/);
|
|
823
|
+
if (userMatch) return truncate(userMatch[1].trim(), maxLen);
|
|
824
|
+
const pmMatch = input.match(/Primary message[^:]*:\s*\n(.+)/);
|
|
825
|
+
if (pmMatch) return truncate(pmMatch[1].trim(), maxLen);
|
|
826
|
+
return "";
|
|
827
|
+
}
|
|
828
|
+
function formatJsonPreview(obj, maxLen) {
|
|
829
|
+
if (obj === null || obj === void 0) return "";
|
|
830
|
+
if (typeof obj !== "object") return truncate(String(obj), maxLen);
|
|
831
|
+
if (Array.isArray(obj)) {
|
|
832
|
+
if (obj.length === 0) return "[]";
|
|
833
|
+
const first = typeof obj[0] === "object" && obj[0] !== null ? obj[0].project_id || obj[0].id || obj[0].name || Object.keys(obj[0])[0] || "..." : String(obj[0]);
|
|
834
|
+
return `[${obj.length}] ${truncate(String(first), 30)}${obj.length > 1 ? ", ..." : ""}`;
|
|
835
|
+
}
|
|
836
|
+
const parts = [];
|
|
837
|
+
let len = 2;
|
|
838
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
839
|
+
if (key === "raw" || key === "skills" || key === "tags") continue;
|
|
840
|
+
let valStr;
|
|
841
|
+
if (val === null || val === void 0) continue;
|
|
842
|
+
if (typeof val === "boolean") valStr = String(val);
|
|
843
|
+
else if (typeof val === "number") valStr = String(val);
|
|
844
|
+
else if (typeof val === "string") {
|
|
845
|
+
if (val.startsWith("{") || val.startsWith("[")) continue;
|
|
846
|
+
const clean = val.replace(/\*\*/g, "").replace(/^#+\s*/gm, "").replace(/`/g, "").trim();
|
|
847
|
+
valStr = `"${truncate(clean.split("\n")[0], Math.min(80, maxLen / 3))}"`;
|
|
848
|
+
} else if (Array.isArray(val)) valStr = `[${val.length}]`;
|
|
849
|
+
else if (typeof val === "object") valStr = `{${Object.keys(val).length} keys}`;
|
|
850
|
+
else valStr = "...";
|
|
851
|
+
const part = `${key}: ${valStr}`;
|
|
852
|
+
if (len + part.length + 2 > maxLen) {
|
|
853
|
+
parts.push("...");
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
parts.push(part);
|
|
857
|
+
len += part.length + 2;
|
|
858
|
+
}
|
|
859
|
+
return `{${parts.join(", ")}}`;
|
|
860
|
+
}
|
|
861
|
+
function parseTruncatedJson(input) {
|
|
862
|
+
let pos = 0;
|
|
863
|
+
const len = input.length;
|
|
864
|
+
function skipWhitespace() {
|
|
865
|
+
while (pos < len && " \n\r".includes(input[pos])) pos++;
|
|
866
|
+
}
|
|
867
|
+
function parseString() {
|
|
868
|
+
if (input[pos] !== '"') return "";
|
|
869
|
+
pos++;
|
|
870
|
+
let result = "";
|
|
871
|
+
while (pos < len) {
|
|
872
|
+
const ch = input[pos];
|
|
873
|
+
if (ch === "\\" && pos + 1 < len) {
|
|
874
|
+
const next = input[pos + 1];
|
|
875
|
+
if (next === "n") {
|
|
876
|
+
result += "\n";
|
|
877
|
+
pos += 2;
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
if (next === "t") {
|
|
881
|
+
result += " ";
|
|
882
|
+
pos += 2;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (next === '"') {
|
|
886
|
+
result += '"';
|
|
887
|
+
pos += 2;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
if (next === "\\") {
|
|
891
|
+
result += "\\";
|
|
892
|
+
pos += 2;
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
result += next;
|
|
896
|
+
pos += 2;
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
if (ch === '"') {
|
|
900
|
+
pos++;
|
|
901
|
+
return result;
|
|
902
|
+
}
|
|
903
|
+
result += ch;
|
|
904
|
+
pos++;
|
|
905
|
+
}
|
|
906
|
+
return result;
|
|
907
|
+
}
|
|
908
|
+
function parseNumber() {
|
|
909
|
+
const start = pos;
|
|
910
|
+
if (input[pos] === "-") pos++;
|
|
911
|
+
while (pos < len && input[pos] >= "0" && input[pos] <= "9") pos++;
|
|
912
|
+
if (pos < len && input[pos] === ".") {
|
|
913
|
+
pos++;
|
|
914
|
+
while (pos < len && input[pos] >= "0" && input[pos] <= "9") pos++;
|
|
915
|
+
}
|
|
916
|
+
return Number(input.slice(start, pos));
|
|
917
|
+
}
|
|
918
|
+
function parseValue() {
|
|
919
|
+
skipWhitespace();
|
|
920
|
+
if (pos >= len) return void 0;
|
|
921
|
+
const ch = input[pos];
|
|
922
|
+
if (ch === '"') return parseString();
|
|
923
|
+
if (ch === "{") return parseObject();
|
|
924
|
+
if (ch === "[") return parseArray();
|
|
925
|
+
if (ch === "t" && input.slice(pos, pos + 4) === "true") {
|
|
926
|
+
pos += 4;
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
929
|
+
if (ch === "f" && input.slice(pos, pos + 5) === "false") {
|
|
930
|
+
pos += 5;
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
if (ch === "n" && input.slice(pos, pos + 4) === "null") {
|
|
934
|
+
pos += 4;
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
if (ch === "-" || ch >= "0" && ch <= "9") return parseNumber();
|
|
938
|
+
return void 0;
|
|
939
|
+
}
|
|
940
|
+
function parseObject() {
|
|
941
|
+
const obj = {};
|
|
942
|
+
pos++;
|
|
943
|
+
skipWhitespace();
|
|
944
|
+
while (pos < len && input[pos] !== "}") {
|
|
945
|
+
skipWhitespace();
|
|
946
|
+
if (pos >= len || input[pos] !== '"') break;
|
|
947
|
+
const key = parseString();
|
|
948
|
+
skipWhitespace();
|
|
949
|
+
if (pos >= len || input[pos] !== ":") {
|
|
950
|
+
obj[key] = void 0;
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
pos++;
|
|
954
|
+
const val = parseValue();
|
|
955
|
+
if (val !== void 0) obj[key] = val;
|
|
956
|
+
skipWhitespace();
|
|
957
|
+
if (pos < len && input[pos] === ",") pos++;
|
|
958
|
+
}
|
|
959
|
+
if (pos < len && input[pos] === "}") pos++;
|
|
960
|
+
return obj;
|
|
961
|
+
}
|
|
962
|
+
function parseArray() {
|
|
963
|
+
const arr = [];
|
|
964
|
+
pos++;
|
|
965
|
+
skipWhitespace();
|
|
966
|
+
while (pos < len && input[pos] !== "]") {
|
|
967
|
+
const val = parseValue();
|
|
968
|
+
if (val !== void 0) arr.push(val);
|
|
969
|
+
else break;
|
|
970
|
+
skipWhitespace();
|
|
971
|
+
if (pos < len && input[pos] === ",") pos++;
|
|
972
|
+
skipWhitespace();
|
|
973
|
+
}
|
|
974
|
+
if (pos < len && input[pos] === "]") pos++;
|
|
975
|
+
return arr;
|
|
976
|
+
}
|
|
977
|
+
return parseValue();
|
|
978
|
+
}
|
|
979
|
+
function extractRouteIntentTopic(spans) {
|
|
980
|
+
const riSpan = spans.find((s) => s.attributes["visor.check.id"] === "route-intent");
|
|
981
|
+
if (riSpan) {
|
|
982
|
+
const output = String(riSpan.attributes["visor.check.output"] || "");
|
|
983
|
+
if (output) {
|
|
984
|
+
try {
|
|
985
|
+
const obj = JSON.parse(output);
|
|
986
|
+
if (obj.topic) return truncate(String(obj.topic), 150);
|
|
987
|
+
} catch {
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const classifySpan = spans.find((s) => s.attributes["visor.check.id"] === "classify");
|
|
992
|
+
if (classifySpan) {
|
|
993
|
+
const output = String(classifySpan.attributes["visor.check.output"] || "");
|
|
994
|
+
if (output) {
|
|
995
|
+
try {
|
|
996
|
+
const obj = JSON.parse(output);
|
|
997
|
+
if (obj.topic) return truncate(String(obj.topic), 150);
|
|
998
|
+
} catch {
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return void 0;
|
|
1003
|
+
}
|
|
1004
|
+
function formatSize(chars) {
|
|
1005
|
+
if (chars < 1e3) return `${chars} chars`;
|
|
1006
|
+
return `${(chars / 1e3).toFixed(1)}k chars`;
|
|
1007
|
+
}
|
|
1008
|
+
function formatDurationMs(ms) {
|
|
1009
|
+
if (ms < 0) return "0ms";
|
|
1010
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
1011
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
1012
|
+
const mins = Math.floor(ms / 6e4);
|
|
1013
|
+
const secs = Math.round(ms % 6e4 / 1e3);
|
|
1014
|
+
return `${mins}m ${secs}s`;
|
|
1015
|
+
}
|
|
1016
|
+
function truncate(str, max) {
|
|
1017
|
+
if (typeof str !== "string") return "";
|
|
1018
|
+
if (str.length <= max) return str;
|
|
1019
|
+
const tail = Math.min(100, Math.floor(max / 3));
|
|
1020
|
+
const head = max - tail - 19;
|
|
1021
|
+
if (head < 10) return str.slice(0, max - 3) + "...";
|
|
1022
|
+
return str.slice(0, head) + " ...[truncated]... " + str.slice(-tail);
|
|
1023
|
+
}
|
|
1024
|
+
var NOISE_SPAN_NAMES, WRAPPER_SPAN_NAMES;
|
|
1025
|
+
var init_trace_serializer = __esm({
|
|
1026
|
+
"src/agent-protocol/trace-serializer.ts"() {
|
|
1027
|
+
"use strict";
|
|
1028
|
+
init_logger();
|
|
1029
|
+
NOISE_SPAN_NAMES = /* @__PURE__ */ new Set([
|
|
1030
|
+
"engine.state.init",
|
|
1031
|
+
"engine.state.waveplanning",
|
|
1032
|
+
"engine.state.planready",
|
|
1033
|
+
"visor.sandbox.stopAll"
|
|
1034
|
+
]);
|
|
1035
|
+
WRAPPER_SPAN_NAMES = /* @__PURE__ */ new Set([
|
|
1036
|
+
"engine.state.leveldispatch",
|
|
1037
|
+
"visor.ai_check",
|
|
1038
|
+
"probe.delegation"
|
|
1039
|
+
]);
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
// src/agent-protocol/task-evaluator.ts
|
|
1044
|
+
import crypto from "crypto";
|
|
1045
|
+
function buildEvaluationSchema(includeExecution) {
|
|
1046
|
+
const schema = {
|
|
1047
|
+
type: "object",
|
|
1048
|
+
required: ["response_quality", "overall_rating", "summary"],
|
|
1049
|
+
properties: {
|
|
1050
|
+
response_quality: {
|
|
1051
|
+
type: "object",
|
|
1052
|
+
required: ["rating", "category", "relevance", "completeness", "actionable", "reasoning"],
|
|
1053
|
+
properties: {
|
|
1054
|
+
rating: { type: "integer", minimum: 1, maximum: 5 },
|
|
1055
|
+
category: {
|
|
1056
|
+
type: "string",
|
|
1057
|
+
enum: ["excellent", "good", "adequate", "poor", "off-topic", "error"]
|
|
1058
|
+
},
|
|
1059
|
+
relevance: { type: "boolean" },
|
|
1060
|
+
completeness: { type: "boolean" },
|
|
1061
|
+
actionable: { type: "boolean" },
|
|
1062
|
+
reasoning: { type: "string" }
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
overall_rating: { type: "integer", minimum: 1, maximum: 5 },
|
|
1066
|
+
summary: { type: "string" }
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
if (includeExecution) {
|
|
1070
|
+
schema.required.push("execution_quality");
|
|
1071
|
+
schema.properties.execution_quality = {
|
|
1072
|
+
type: "object",
|
|
1073
|
+
required: ["rating", "category", "reasoning"],
|
|
1074
|
+
properties: {
|
|
1075
|
+
rating: { type: "integer", minimum: 1, maximum: 5 },
|
|
1076
|
+
category: { type: "string", enum: ["efficient", "adequate", "wasteful", "error"] },
|
|
1077
|
+
unnecessary_tool_calls: { type: "integer" },
|
|
1078
|
+
reasoning: { type: "string" }
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
return schema;
|
|
1083
|
+
}
|
|
1084
|
+
async function evaluateTask(taskId, store, config) {
|
|
1085
|
+
const { rows } = store.listTasksRaw({ limit: 500 });
|
|
1086
|
+
const match = rows.find((r) => r.id === taskId || r.id.startsWith(taskId));
|
|
1087
|
+
if (!match) {
|
|
1088
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
1089
|
+
}
|
|
1090
|
+
const fullTask = store.getTask(match.id);
|
|
1091
|
+
if (!fullTask) {
|
|
1092
|
+
throw new Error(`Task data not found: ${match.id}`);
|
|
1093
|
+
}
|
|
1094
|
+
const requestText = match.request_message || "No request text available";
|
|
1095
|
+
let responseText = "No response available";
|
|
1096
|
+
if (fullTask.status?.message) {
|
|
1097
|
+
const parts = fullTask.status.message.parts ?? [];
|
|
1098
|
+
const textPart = parts.find((p) => typeof p.text === "string");
|
|
1099
|
+
if (textPart) {
|
|
1100
|
+
responseText = textPart.text;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (fullTask.status.state === "failed" && responseText === "No response available") {
|
|
1104
|
+
return {
|
|
1105
|
+
response_quality: {
|
|
1106
|
+
rating: 1,
|
|
1107
|
+
category: "error",
|
|
1108
|
+
relevance: false,
|
|
1109
|
+
completeness: false,
|
|
1110
|
+
actionable: false,
|
|
1111
|
+
reasoning: "Task failed without producing a response."
|
|
1112
|
+
},
|
|
1113
|
+
overall_rating: 1,
|
|
1114
|
+
summary: "Task failed without producing a response."
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
let traceTree;
|
|
1118
|
+
const traceId = match.metadata?.trace_id;
|
|
1119
|
+
const traceFile = match.metadata?.trace_file;
|
|
1120
|
+
if (traceFile || traceId) {
|
|
1121
|
+
try {
|
|
1122
|
+
const traceRef = traceFile || traceId;
|
|
1123
|
+
traceTree = await serializeTraceForPrompt(
|
|
1124
|
+
traceRef,
|
|
1125
|
+
1e6,
|
|
1126
|
+
{ traceDir: config?.traceDir },
|
|
1127
|
+
responseText !== "No response available" ? responseText : void 0
|
|
1128
|
+
);
|
|
1129
|
+
if (traceTree === "(no trace data available)") {
|
|
1130
|
+
traceTree = void 0;
|
|
1131
|
+
}
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
logger.debug(
|
|
1134
|
+
`[TaskEvaluator] Failed to load trace: ${err instanceof Error ? err.message : err}`
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
const systemPrompt = config?.prompt || process.env.VISOR_EVAL_PROMPT || DEFAULT_EVALUATION_PROMPT;
|
|
1139
|
+
const hasTrace = !!traceTree;
|
|
1140
|
+
let userPrompt;
|
|
1141
|
+
if (traceTree) {
|
|
1142
|
+
userPrompt = `<user_request>
|
|
1143
|
+
${requestText}
|
|
1144
|
+
</user_request>
|
|
1145
|
+
|
|
1146
|
+
<execution_trace>
|
|
1147
|
+
${traceTree}
|
|
1148
|
+
</execution_trace>`;
|
|
1149
|
+
} else {
|
|
1150
|
+
userPrompt = `<user_request>
|
|
1151
|
+
${requestText}
|
|
1152
|
+
</user_request>
|
|
1153
|
+
|
|
1154
|
+
<agent_response>
|
|
1155
|
+
${responseText}
|
|
1156
|
+
</agent_response>`;
|
|
1157
|
+
}
|
|
1158
|
+
const { ProbeAgent } = __require("@probelabs/probe");
|
|
1159
|
+
const model = config?.model || process.env.VISOR_EVAL_MODEL || process.env.VISOR_JUDGE_MODEL || void 0;
|
|
1160
|
+
const provider = config?.provider || process.env.VISOR_EVAL_PROVIDER || void 0;
|
|
1161
|
+
const agentOptions = {
|
|
1162
|
+
sessionId: `visor-task-eval-${Date.now()}`,
|
|
1163
|
+
systemPrompt,
|
|
1164
|
+
maxIterations: 1,
|
|
1165
|
+
disableTools: true
|
|
1166
|
+
};
|
|
1167
|
+
if (model) agentOptions.model = model;
|
|
1168
|
+
if (provider) agentOptions.provider = provider;
|
|
1169
|
+
if (config?.apiKey) {
|
|
1170
|
+
const envKey = provider === "openai" ? "OPENAI_API_KEY" : provider === "anthropic" ? "ANTHROPIC_API_KEY" : "GOOGLE_API_KEY";
|
|
1171
|
+
process.env[envKey] = config.apiKey;
|
|
1172
|
+
}
|
|
1173
|
+
const agent = new ProbeAgent(agentOptions);
|
|
1174
|
+
if (typeof agent.initialize === "function") {
|
|
1175
|
+
await agent.initialize();
|
|
1176
|
+
}
|
|
1177
|
+
const jsonSchema = buildEvaluationSchema(hasTrace);
|
|
1178
|
+
const schemaStr = JSON.stringify(jsonSchema);
|
|
1179
|
+
const response = await agent.answer(userPrompt, void 0, { schema: schemaStr });
|
|
1180
|
+
let result;
|
|
1181
|
+
try {
|
|
1182
|
+
const cleaned = response.replace(/^```(?:json)?\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
1183
|
+
result = JSON.parse(cleaned);
|
|
1184
|
+
} catch {
|
|
1185
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
1186
|
+
if (jsonMatch) {
|
|
1187
|
+
result = JSON.parse(jsonMatch[0]);
|
|
1188
|
+
} else {
|
|
1189
|
+
throw new Error(`Failed to parse evaluation response as JSON: ${response.slice(0, 200)}`);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return result;
|
|
1193
|
+
}
|
|
1194
|
+
async function evaluateAndStore(taskId, store, config) {
|
|
1195
|
+
const result = await evaluateTask(taskId, store, config);
|
|
1196
|
+
const { rows } = store.listTasksRaw({ limit: 500 });
|
|
1197
|
+
const match = rows.find((r) => r.id === taskId || r.id.startsWith(taskId));
|
|
1198
|
+
if (match) {
|
|
1199
|
+
store.addArtifact(match.id, {
|
|
1200
|
+
artifact_id: crypto.randomUUID(),
|
|
1201
|
+
name: "evaluation",
|
|
1202
|
+
parts: [{ text: JSON.stringify(result), media_type: "application/json" }]
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
return result;
|
|
1206
|
+
}
|
|
1207
|
+
var DEFAULT_EVALUATION_PROMPT;
|
|
1208
|
+
var init_task_evaluator = __esm({
|
|
1209
|
+
"src/agent-protocol/task-evaluator.ts"() {
|
|
1210
|
+
init_logger();
|
|
1211
|
+
init_trace_serializer();
|
|
1212
|
+
DEFAULT_EVALUATION_PROMPT = `You are a task response quality evaluator for an AI agent system called Visor.
|
|
1213
|
+
|
|
1214
|
+
You will receive the user's original request and an execution trace inside <execution_trace> tags. The trace is a YAML-formatted view of the entire agent execution, including the final response. When no trace is available, the agent response is provided directly.
|
|
1215
|
+
|
|
1216
|
+
## How to Read the Execution Trace
|
|
1217
|
+
|
|
1218
|
+
The trace is a tree of spans representing the agent's execution pipeline:
|
|
1219
|
+
|
|
1220
|
+
**Top-level: \`visor.run\`** \u2014 The root span with metadata:
|
|
1221
|
+
- \`trace_id\`: Unique execution identifier
|
|
1222
|
+
- \`visor\` / \`probe\`: Software versions
|
|
1223
|
+
- \`source\`: Where the request came from (e.g., "slack", "cli")
|
|
1224
|
+
- \`duration\`: Total wall-clock time
|
|
1225
|
+
|
|
1226
|
+
**Checks** \u2014 Named processing steps (e.g., \`route-intent\`, \`explore-code\`, \`generate-response\`):
|
|
1227
|
+
- \`type\`: "ai" (LLM-powered), "script" (deterministic), or "workflow" (sub-pipeline)
|
|
1228
|
+
- \`duration\`: How long this step took
|
|
1229
|
+
- \`input\`: What was passed to this check \u2014 may include an \`intent\` (the user's question as understood by the router) and dependency outputs
|
|
1230
|
+
- \`output\`: The check's result \u2014 may be structured JSON or plain text
|
|
1231
|
+
|
|
1232
|
+
**AI blocks** (\`ai: model-name\`) \u2014 Individual LLM calls within checks:
|
|
1233
|
+
- Shows model used, duration, and token counts (input/output)
|
|
1234
|
+
- \`intent\`: The question or instruction sent to the LLM
|
|
1235
|
+
|
|
1236
|
+
**Tool calls** \u2014 Listed as \`- toolName(input) \u2192 size\`:
|
|
1237
|
+
- \`search("query" in repo)\`: Code search. "\u2192 no results" means nothing was found; otherwise shows result size
|
|
1238
|
+
- \`extract(file/path)\`: File content extraction with result size
|
|
1239
|
+
- \`listFiles(dir)\`: Directory listing
|
|
1240
|
+
- \`bash()\`: Shell command execution
|
|
1241
|
+
|
|
1242
|
+
**Delegations** (\`search.delegate("query")\`) \u2014 Sub-agent searches:
|
|
1243
|
+
- Contains their own AI blocks and tool calls
|
|
1244
|
+
- Used for complex multi-step code exploration
|
|
1245
|
+
|
|
1246
|
+
**The \`response\` field** at the end of the trace is the final answer sent back to the user. This is the primary output to evaluate.
|
|
1247
|
+
|
|
1248
|
+
**Symbols:**
|
|
1249
|
+
- \`\u2717\` marks failed/error spans
|
|
1250
|
+
- \`= check-name\` means output is identical to that check's output (deduplication)
|
|
1251
|
+
|
|
1252
|
+
## Evaluation Criteria
|
|
1253
|
+
|
|
1254
|
+
**Response Quality** (1-5):
|
|
1255
|
+
- **Relevance**: Does the response directly address what the user asked? A response about the wrong topic or that misunderstands the question scores low.
|
|
1256
|
+
- **Completeness**: Does it fully answer the question? Partial answers, missing key details, or surface-level responses score lower.
|
|
1257
|
+
- **Actionable**: Can the user act on this information? Vague or generic advice scores lower than specific, concrete answers with code references.
|
|
1258
|
+
- Rating: 5=excellent (thorough, specific, directly useful), 4=good (answers well but minor gaps), 3=adequate (addresses question but lacks depth), 2=poor (partially relevant or very incomplete), 1=off-topic or error
|
|
1259
|
+
|
|
1260
|
+
**Execution Quality** (1-5, only when trace is provided):
|
|
1261
|
+
- **Efficiency**: Were tool calls necessary and well-targeted? Good search queries that find results on the first try score high.
|
|
1262
|
+
- **Redundancy**: Were there duplicate searches, unnecessary re-searches with slightly different queries, or tools called for information already available?
|
|
1263
|
+
- **Delegation quality**: Were search delegations productive? Did they explore relevant code paths?
|
|
1264
|
+
- **Token usage**: Was input context kept reasonable, or did the agent load excessive amounts of code?
|
|
1265
|
+
- Rating: 5=efficient (minimal, targeted tool use), 4=adequate (minor redundancy), 3=some waste (noticeable unnecessary calls), 2=wasteful (many redundant searches or delegations), 1=error/broken execution
|
|
1266
|
+
|
|
1267
|
+
**Overall Rating** (1-5): Weighted combination \u2014 response quality matters most, execution quality is secondary. A perfect response from a wasteful execution still scores 3-4 overall.
|
|
1268
|
+
|
|
1269
|
+
You MUST respond with valid JSON matching the provided schema. Be specific in your reasoning \u2014 reference actual check names, tool calls, or response content.`;
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
init_task_evaluator();
|
|
1273
|
+
export {
|
|
1274
|
+
DEFAULT_EVALUATION_PROMPT,
|
|
1275
|
+
evaluateAndStore,
|
|
1276
|
+
evaluateTask
|
|
1277
|
+
};
|
|
1278
|
+
//# sourceMappingURL=task-evaluator-HLNXKKVV.mjs.map
|