@apo-ai/sdk 0.7.1 → 0.9.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 +2 -1
- package/dist/agent-task/integrations/ai-sdk.d.ts +1 -1
- package/dist/agent-task/integrations/ai-sdk.js +2 -2
- package/dist/agent-task/integrations/anthropic.d.ts +1 -1
- package/dist/agent-task/integrations/anthropic.js +2 -2
- package/dist/agent-task/integrations/openai.d.ts +2 -2
- package/dist/agent-task/integrations/openai.js +2 -2
- package/dist/agent-task/public.d.ts +169 -10
- package/dist/agent-task/public.js +1007 -288
- package/dist/agent-task/public.js.map +1 -1
- package/dist/{chunk-6ZRW5IW6.js → chunk-3B3L2H4Y.js} +5 -2
- package/dist/chunk-3B3L2H4Y.js.map +1 -0
- package/dist/{chunk-GDB6SDGN.js → chunk-3OQ3JBFJ.js} +2 -1
- package/dist/chunk-3OQ3JBFJ.js.map +1 -0
- package/dist/{chunk-SZDSIPXC.js → chunk-43OI6GP7.js} +2 -2
- package/dist/{chunk-HWZVL6TS.js → chunk-HRYXLRXJ.js} +12 -7
- package/dist/chunk-HRYXLRXJ.js.map +1 -0
- package/dist/{chunk-QHRWPVPZ.js → chunk-IK4GJX7H.js} +2 -2
- package/dist/{chunk-GETY34VI.js → chunk-KLZLE3VQ.js} +3 -2
- package/dist/{chunk-GETY34VI.js.map → chunk-KLZLE3VQ.js.map} +1 -1
- package/dist/otel/index.js +2 -2
- package/dist/{tracing-DVLUqD-i.d.ts → tracing-CevT0hQg.d.ts} +13 -0
- package/package.json +1 -1
- package/dist/chunk-6ZRW5IW6.js.map +0 -1
- package/dist/chunk-GDB6SDGN.js.map +0 -1
- package/dist/chunk-HWZVL6TS.js.map +0 -1
- /package/dist/{chunk-SZDSIPXC.js.map → chunk-43OI6GP7.js.map} +0 -0
- /package/dist/{chunk-QHRWPVPZ.js.map → chunk-IK4GJX7H.js.map} +0 -0
|
@@ -8,20 +8,21 @@ import {
|
|
|
8
8
|
resetApoTracing,
|
|
9
9
|
withApoRun,
|
|
10
10
|
withApoRunSync
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-KLZLE3VQ.js";
|
|
12
12
|
import {
|
|
13
13
|
createApoTracer
|
|
14
|
-
} from "../chunk-
|
|
15
|
-
import "../chunk-
|
|
14
|
+
} from "../chunk-IK4GJX7H.js";
|
|
15
|
+
import "../chunk-HRYXLRXJ.js";
|
|
16
16
|
import {
|
|
17
17
|
createApoOpenAI
|
|
18
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-3B3L2H4Y.js";
|
|
19
19
|
import {
|
|
20
20
|
createApoAnthropic
|
|
21
|
-
} from "../chunk-
|
|
22
|
-
import "../chunk-
|
|
21
|
+
} from "../chunk-43OI6GP7.js";
|
|
22
|
+
import "../chunk-3OQ3JBFJ.js";
|
|
23
23
|
|
|
24
24
|
// src/agent-task/trace-projection/view.ts
|
|
25
|
+
var TASK_TURN_SPAN_NAME = "task.turn";
|
|
25
26
|
function invocationOrderKey(obs) {
|
|
26
27
|
const hasTs = obs.startedAt != null ? 0 : 1;
|
|
27
28
|
return [hasTs, obs.startedAt ?? "", obs.spanId];
|
|
@@ -32,11 +33,11 @@ var TraceView = class {
|
|
|
32
33
|
}
|
|
33
34
|
/** Evidence availability for a capability. */
|
|
34
35
|
requireCapability(capability) {
|
|
35
|
-
return this.snapshot.capabilities[capability];
|
|
36
|
+
return this.snapshot.capabilities[capability] ?? "unavailable";
|
|
36
37
|
}
|
|
37
38
|
/** Whether a capability is reported as available (not partial/unavailable). */
|
|
38
39
|
isAvailable(capability) {
|
|
39
|
-
return this.
|
|
40
|
+
return this.requireCapability(capability) === "available";
|
|
40
41
|
}
|
|
41
42
|
/** All chat messages flattened across generation observations, in order. */
|
|
42
43
|
get messages() {
|
|
@@ -115,6 +116,135 @@ var TraceView = class {
|
|
|
115
116
|
}
|
|
116
117
|
return void 0;
|
|
117
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Task Turns in invocation order, or `undefined` when timing evidence is
|
|
121
|
+
* not available. Each is one `task.turn` span: the adapter's whole
|
|
122
|
+
* `sendUserTurn` call for that turn.
|
|
123
|
+
*/
|
|
124
|
+
get turns() {
|
|
125
|
+
if (!this.isAvailable("timing")) return void 0;
|
|
126
|
+
return this.turnSpans.map((o, i) => ({
|
|
127
|
+
turnNumber: i + 1,
|
|
128
|
+
spanId: o.spanId,
|
|
129
|
+
...o.durationMs !== void 0 ? { durationMs: o.durationMs } : {},
|
|
130
|
+
status: o.status
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Tokens the agent under test spent: usage on observations inside
|
|
135
|
+
* `task.turn` spans (every turn, or only turn `turn`). Work outside the
|
|
136
|
+
* turns — the evaluation phase's judges, adapter setup — is not the
|
|
137
|
+
* agent's and is excluded. `undefined` when usage evidence is not
|
|
138
|
+
* available or the requested turn does not exist.
|
|
139
|
+
*/
|
|
140
|
+
/** How many Task Turns ran — independent of timing evidence. */
|
|
141
|
+
get turnCountFromSpans() {
|
|
142
|
+
return this.turnSpans.length;
|
|
143
|
+
}
|
|
144
|
+
tokens(kind, opts) {
|
|
145
|
+
if (!this.isAvailable("usage")) return void 0;
|
|
146
|
+
const turnSpans = this.turnSpans;
|
|
147
|
+
let roots;
|
|
148
|
+
if (opts?.turn !== void 0) {
|
|
149
|
+
const one = turnSpans[opts.turn - 1];
|
|
150
|
+
if (!one) return void 0;
|
|
151
|
+
roots = [one];
|
|
152
|
+
} else {
|
|
153
|
+
roots = turnSpans;
|
|
154
|
+
}
|
|
155
|
+
const tally = { tokens: 0, reported: 0, unreported: 0 };
|
|
156
|
+
const children = this.childrenByParent;
|
|
157
|
+
const isLlmCall = (o) => o.type === "GENERATION" || o.usage != null;
|
|
158
|
+
const isStep = (o) => isLlmCall(o) && o.type !== "TOOL" && o.type !== "AGENT";
|
|
159
|
+
const order = [];
|
|
160
|
+
const visited = new Set(roots.map((r) => r.spanId));
|
|
161
|
+
const topLevel = roots.flatMap((r) => children.get(r.spanId) ?? []);
|
|
162
|
+
const stack = [...topLevel];
|
|
163
|
+
const scopeChildren = /* @__PURE__ */ new Map();
|
|
164
|
+
while (stack.length > 0) {
|
|
165
|
+
const obs = stack.pop();
|
|
166
|
+
if (visited.has(obs.spanId)) continue;
|
|
167
|
+
visited.add(obs.spanId);
|
|
168
|
+
order.push(obs);
|
|
169
|
+
const kids = (children.get(obs.spanId) ?? []).filter((c) => !visited.has(c.spanId));
|
|
170
|
+
scopeChildren.set(obs.spanId, kids);
|
|
171
|
+
for (const kid of kids) stack.push(kid);
|
|
172
|
+
}
|
|
173
|
+
const effective = /* @__PURE__ */ new Map();
|
|
174
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
175
|
+
const obs = order[i];
|
|
176
|
+
const { count, complete } = tokenCount(obs, kind);
|
|
177
|
+
if (count !== void 0) tally.reported += 1;
|
|
178
|
+
const ownKnown = complete && obs.status !== "error";
|
|
179
|
+
let steps;
|
|
180
|
+
let stepsUnreported = 0;
|
|
181
|
+
let separate;
|
|
182
|
+
let separateUnreported = 0;
|
|
183
|
+
for (const child of scopeChildren.get(obs.spanId) ?? []) {
|
|
184
|
+
const c = effective.get(child.spanId);
|
|
185
|
+
if (!c) continue;
|
|
186
|
+
if (isStep(child)) {
|
|
187
|
+
if (c.tokens !== void 0) steps = (steps ?? 0) + c.tokens;
|
|
188
|
+
stepsUnreported += c.unreported;
|
|
189
|
+
} else {
|
|
190
|
+
if (c.tokens !== void 0) separate = (separate ?? 0) + c.tokens;
|
|
191
|
+
separateUnreported += c.unreported;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const own = count === void 0 ? steps : steps === void 0 ? count : Math.max(count, steps);
|
|
195
|
+
const unreported = (isLlmCall(obs) && !ownKnown ? 1 : 0) + (isLlmCall(obs) && ownKnown ? 0 : stepsUnreported) + separateUnreported;
|
|
196
|
+
effective.set(obs.spanId, {
|
|
197
|
+
tokens: own === void 0 && separate === void 0 ? void 0 : (own ?? 0) + (separate ?? 0),
|
|
198
|
+
unreported
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
for (const obs of topLevel) {
|
|
202
|
+
const e = effective.get(obs.spanId);
|
|
203
|
+
if (!e) continue;
|
|
204
|
+
tally.tokens += e.tokens ?? 0;
|
|
205
|
+
tally.unreported += e.unreported;
|
|
206
|
+
effective.delete(obs.spanId);
|
|
207
|
+
}
|
|
208
|
+
return tally;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The run's own Task Turns: `task.turn` observations with no `task.turn`
|
|
212
|
+
* ancestor (a nested run or a user span of the same name inside a turn is
|
|
213
|
+
* part of that turn, not a turn of its own). Turns run sequentially, so
|
|
214
|
+
* invocation order is turn order.
|
|
215
|
+
*/
|
|
216
|
+
get turnSpans() {
|
|
217
|
+
if (this._turnSpans !== void 0) return this._turnSpans;
|
|
218
|
+
const byId = new Map(this.snapshot.observations.map((o) => [o.spanId, o]));
|
|
219
|
+
const insideAnotherTurn = (obs) => {
|
|
220
|
+
const visited = /* @__PURE__ */ new Set([obs.spanId]);
|
|
221
|
+
let parentId = obs.parentSpanId;
|
|
222
|
+
while (parentId !== void 0 && !visited.has(parentId)) {
|
|
223
|
+
visited.add(parentId);
|
|
224
|
+
const parent = byId.get(parentId);
|
|
225
|
+
if (!parent) return false;
|
|
226
|
+
if (parent.name === TASK_TURN_SPAN_NAME) return true;
|
|
227
|
+
parentId = parent.parentSpanId;
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
};
|
|
231
|
+
this._turnSpans = this.sortedObservations.filter(
|
|
232
|
+
(o) => o.name === TASK_TURN_SPAN_NAME && !insideAnotherTurn(o)
|
|
233
|
+
);
|
|
234
|
+
return this._turnSpans;
|
|
235
|
+
}
|
|
236
|
+
get childrenByParent() {
|
|
237
|
+
if (this._children !== void 0) return this._children;
|
|
238
|
+
const children = /* @__PURE__ */ new Map();
|
|
239
|
+
for (const obs of this.snapshot.observations) {
|
|
240
|
+
if (obs.parentSpanId == null) continue;
|
|
241
|
+
const list = children.get(obs.parentSpanId);
|
|
242
|
+
if (list) list.push(obs);
|
|
243
|
+
else children.set(obs.parentSpanId, [obs]);
|
|
244
|
+
}
|
|
245
|
+
this._children = children;
|
|
246
|
+
return children;
|
|
247
|
+
}
|
|
118
248
|
/**
|
|
119
249
|
* Observations sorted deterministically by invocation time then span ID.
|
|
120
250
|
* Memoized per TraceView instance since the snapshot is immutable.
|
|
@@ -130,6 +260,18 @@ var TraceView = class {
|
|
|
130
260
|
return sorted;
|
|
131
261
|
}
|
|
132
262
|
};
|
|
263
|
+
function tokenCount(obs, kind) {
|
|
264
|
+
const known = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
265
|
+
const input = known(obs.usage?.inputTokens);
|
|
266
|
+
const output = known(obs.usage?.outputTokens);
|
|
267
|
+
if (kind === "input") return { count: input, complete: input !== void 0 };
|
|
268
|
+
if (kind === "output") return { count: output, complete: output !== void 0 };
|
|
269
|
+
if (input === void 0 && output === void 0) return { count: void 0, complete: false };
|
|
270
|
+
return {
|
|
271
|
+
count: (input ?? 0) + (output ?? 0),
|
|
272
|
+
complete: input !== void 0 && output !== void 0
|
|
273
|
+
};
|
|
274
|
+
}
|
|
133
275
|
|
|
134
276
|
// src/agent-task/checks/matchers.ts
|
|
135
277
|
function asString(value) {
|
|
@@ -253,9 +395,24 @@ function decisionsEndpoint(chatBaseURL) {
|
|
|
253
395
|
const base = chatBaseURL.replace(/\/v1\/?$/, "");
|
|
254
396
|
return `${base}/alpha/decisions`;
|
|
255
397
|
}
|
|
398
|
+
var INPUT_TOO_LARGE_MARKERS = [
|
|
399
|
+
"max_tokens_exceeded",
|
|
400
|
+
"context_length_exceeded",
|
|
401
|
+
"maximum context length"
|
|
402
|
+
];
|
|
403
|
+
function isInputTooLargeRejection(status, body) {
|
|
404
|
+
if (status !== 400 && status !== 413) return false;
|
|
405
|
+
return INPUT_TOO_LARGE_MARKERS.some((marker2) => body.includes(marker2));
|
|
406
|
+
}
|
|
407
|
+
function estimateTokens(text) {
|
|
408
|
+
return Math.round(text.length / 4 / 1e3) * 1e3;
|
|
409
|
+
}
|
|
256
410
|
async function callSecondJudge(args) {
|
|
257
411
|
const startedAt = Date.now();
|
|
258
|
-
const evidence = {
|
|
412
|
+
const evidence = {
|
|
413
|
+
model: args.model,
|
|
414
|
+
...args.projected ? { projected: true } : {}
|
|
415
|
+
};
|
|
259
416
|
try {
|
|
260
417
|
const response = await fetch(decisionsEndpoint(args.baseURL), {
|
|
261
418
|
method: "POST",
|
|
@@ -281,6 +438,10 @@ async function callSecondJudge(args) {
|
|
|
281
438
|
});
|
|
282
439
|
if (!response.ok) {
|
|
283
440
|
const body = await response.text().catch(() => "");
|
|
441
|
+
if (isInputTooLargeRejection(response.status, body)) {
|
|
442
|
+
evidence.skipped = `input ~${estimateTokens(args.state).toLocaleString("en-US")} tokens exceeds ${args.model}'s context limit \u2014 project a smaller view with t.judge(value, instruction, { secondJudgeValue })`;
|
|
443
|
+
return evidence;
|
|
444
|
+
}
|
|
284
445
|
evidence.error = `Second judge API ${response.status}: ${body.slice(0, 200)}`;
|
|
285
446
|
return evidence;
|
|
286
447
|
}
|
|
@@ -314,6 +475,12 @@ async function callSecondJudge(args) {
|
|
|
314
475
|
}
|
|
315
476
|
|
|
316
477
|
// src/agent-task/checks/judge.ts
|
|
478
|
+
var JudgeUnavailableError = class extends Error {
|
|
479
|
+
constructor(message) {
|
|
480
|
+
super(message);
|
|
481
|
+
this.name = "JudgeUnavailableError";
|
|
482
|
+
}
|
|
483
|
+
};
|
|
317
484
|
var VERDICT_FIRST_CONTRACT = 'Respond with ONLY a JSON object: {"pass": true/false, "reasoning": "your reasoning"}';
|
|
318
485
|
var REASONING_FIRST_CONTRACT = 'Respond with ONLY a JSON object: {"reasoning": "your reasoning", "pass": true/false}';
|
|
319
486
|
function isJudgeVerdictFirstOverrideEnabled() {
|
|
@@ -389,7 +556,92 @@ function parseJudgeJson(raw) {
|
|
|
389
556
|
reasoning: "Judge response could not be parsed as JSON \u2014 the verdict is unknown, so this check is treated as a failure. The model's raw response is available in the judge metadata."
|
|
390
557
|
};
|
|
391
558
|
}
|
|
392
|
-
var JUDGE_TIMEOUT_MS =
|
|
559
|
+
var JUDGE_TIMEOUT_MS = 3e5;
|
|
560
|
+
var JUDGE_IDLE_TIMEOUT_MS = 9e4;
|
|
561
|
+
var JUDGE_RETRY_DELAY_MS = 1e3;
|
|
562
|
+
var JUDGE_RETRY_AFTER_CAP_MS = 2e4;
|
|
563
|
+
var JUDGE_MIN_RETRY_BUDGET_MS = 6e4;
|
|
564
|
+
function isRetryableStatus(status) {
|
|
565
|
+
return status === 429 || status >= 500;
|
|
566
|
+
}
|
|
567
|
+
function retryDelayMs(retryAfter) {
|
|
568
|
+
if (!retryAfter) return JUDGE_RETRY_DELAY_MS;
|
|
569
|
+
const seconds = Number(retryAfter);
|
|
570
|
+
const ms = Number.isFinite(seconds) ? seconds * 1e3 : Date.parse(retryAfter) - Date.now();
|
|
571
|
+
if (!Number.isFinite(ms)) return JUDGE_RETRY_DELAY_MS;
|
|
572
|
+
return Math.min(Math.max(ms, JUDGE_RETRY_DELAY_MS), JUDGE_RETRY_AFTER_CAP_MS);
|
|
573
|
+
}
|
|
574
|
+
async function readCompletion(response, onData) {
|
|
575
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
576
|
+
if (!contentType.includes("text/event-stream")) {
|
|
577
|
+
const data = await response.json();
|
|
578
|
+
return {
|
|
579
|
+
text: data.choices?.[0]?.message?.content ?? "",
|
|
580
|
+
usage: data.usage,
|
|
581
|
+
finishReason: data.choices?.[0]?.finish_reason ?? void 0
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
let text = "";
|
|
585
|
+
let usage;
|
|
586
|
+
let finishReason;
|
|
587
|
+
let eventType = "message";
|
|
588
|
+
let dataLines = [];
|
|
589
|
+
let finished = false;
|
|
590
|
+
const dispatch = () => {
|
|
591
|
+
const payload = dataLines.join("\n").trim();
|
|
592
|
+
const type = eventType;
|
|
593
|
+
eventType = "message";
|
|
594
|
+
dataLines = [];
|
|
595
|
+
if (payload === "[DONE]") finished = true;
|
|
596
|
+
if (!payload || finished) return;
|
|
597
|
+
onData();
|
|
598
|
+
let chunk;
|
|
599
|
+
try {
|
|
600
|
+
chunk = JSON.parse(payload);
|
|
601
|
+
} catch {
|
|
602
|
+
chunk = void 0;
|
|
603
|
+
}
|
|
604
|
+
if (type === "error" || chunk?.error) {
|
|
605
|
+
const error = chunk?.error;
|
|
606
|
+
const message = typeof error === "string" ? error : error?.message ?? (chunk ? void 0 : payload);
|
|
607
|
+
throw new JudgeUnavailableError(`Judge stream error: ${message ?? "unknown"}`);
|
|
608
|
+
}
|
|
609
|
+
if (!chunk) return;
|
|
610
|
+
const choice = chunk.choices?.[0];
|
|
611
|
+
text += choice?.delta?.content ?? "";
|
|
612
|
+
if (choice?.finish_reason) finishReason = choice.finish_reason;
|
|
613
|
+
if (chunk.usage) usage = chunk.usage;
|
|
614
|
+
};
|
|
615
|
+
const handleLine = (line) => {
|
|
616
|
+
if (line === "") return dispatch();
|
|
617
|
+
if (line.startsWith(":")) return;
|
|
618
|
+
const colon = line.indexOf(":");
|
|
619
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
620
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
621
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
622
|
+
if (field === "data") dataLines.push(value);
|
|
623
|
+
else if (field === "event") eventType = value;
|
|
624
|
+
};
|
|
625
|
+
const reader = response.body?.getReader();
|
|
626
|
+
if (!reader) return { text, usage, finishReason };
|
|
627
|
+
const decoder = new TextDecoder();
|
|
628
|
+
let buffer = "";
|
|
629
|
+
for (; ; ) {
|
|
630
|
+
const { done, value } = await reader.read();
|
|
631
|
+
if (done) break;
|
|
632
|
+
buffer += decoder.decode(value, { stream: true });
|
|
633
|
+
const lines = buffer.split(/\r\n|\r(?!$)|\n/);
|
|
634
|
+
buffer = lines.pop() ?? "";
|
|
635
|
+
for (const line of lines) {
|
|
636
|
+
handleLine(line);
|
|
637
|
+
if (finished) return { text, usage, finishReason };
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
buffer += decoder.decode();
|
|
641
|
+
for (const line of buffer.split(/\r\n|\r|\n/)) if (line) handleLine(line);
|
|
642
|
+
dispatch();
|
|
643
|
+
return { text, usage, finishReason };
|
|
644
|
+
}
|
|
393
645
|
var prefixQueues = /* @__PURE__ */ new Map();
|
|
394
646
|
function runWithSharedPrefix(key, task2) {
|
|
395
647
|
const prev = prefixQueues.get(key) ?? Promise.resolve();
|
|
@@ -413,84 +665,171 @@ ${formatJudgeValues(args.values)}`;
|
|
|
413
665
|
|
|
414
666
|
${deliverableText}`;
|
|
415
667
|
const secondJudgeModel = resolveSecondJudgeModel();
|
|
668
|
+
const secondJudgeProjected = args.secondJudgeValue !== void 0;
|
|
669
|
+
const secondJudgeValues = args.secondJudgeValue ?? args.values;
|
|
670
|
+
const secondJudgeDeliverableText = `Values to evaluate:
|
|
671
|
+
${formatJudgeValues(secondJudgeValues)}`;
|
|
416
672
|
const secondJudgePromise = secondJudgeModel ? callSecondJudge({
|
|
417
|
-
state: `${
|
|
673
|
+
state: `${briefingText}
|
|
674
|
+
|
|
675
|
+
${secondJudgeDeliverableText}
|
|
418
676
|
|
|
419
677
|
${instructionText}`,
|
|
420
678
|
model: secondJudgeModel,
|
|
421
679
|
baseURL: resolveSecondJudgeBaseURL(baseURL),
|
|
422
|
-
apiKey: resolveSecondJudgeAPIKey(apiKey)
|
|
680
|
+
apiKey: resolveSecondJudgeAPIKey(apiKey),
|
|
681
|
+
...secondJudgeProjected ? { projected: true } : {}
|
|
423
682
|
}) : void 0;
|
|
424
683
|
const cacheKey = `${args.model}\0${briefingText}\0${deliverableText}`;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
433
|
-
},
|
|
434
|
-
body: JSON.stringify({
|
|
435
|
-
model: args.model,
|
|
436
|
-
messages: [
|
|
684
|
+
const requestBody = (stream) => JSON.stringify({
|
|
685
|
+
model: args.model,
|
|
686
|
+
messages: [
|
|
687
|
+
{
|
|
688
|
+
role: "system",
|
|
689
|
+
content: [
|
|
690
|
+
{ type: "text", text: briefingText },
|
|
437
691
|
{
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
692
|
+
type: "text",
|
|
693
|
+
text: deliverableText,
|
|
694
|
+
cache_control: { type: "ephemeral" }
|
|
695
|
+
}
|
|
696
|
+
]
|
|
697
|
+
},
|
|
698
|
+
{ role: "user", content: instructionText }
|
|
699
|
+
],
|
|
700
|
+
temperature: 0,
|
|
701
|
+
response_format: { type: "json_object" },
|
|
702
|
+
...stream ? { stream: true, stream_options: { include_usage: true } } : {}
|
|
703
|
+
});
|
|
704
|
+
const attempt = async (stream, deadline) => {
|
|
705
|
+
const controller = new AbortController();
|
|
706
|
+
const expire = (reason, name) => () => controller.abort(new DOMException(reason, name));
|
|
707
|
+
const total = setTimeout(
|
|
708
|
+
expire(`no complete response within ${JUDGE_TIMEOUT_MS / 1e3}s`, "TimeoutError"),
|
|
709
|
+
Math.max(deadline - Date.now(), 0)
|
|
710
|
+
);
|
|
711
|
+
const stalled = expire(`no data for ${JUDGE_IDLE_TIMEOUT_MS / 1e3}s`, "IdleTimeoutError");
|
|
712
|
+
let idle;
|
|
713
|
+
const onData = () => {
|
|
714
|
+
clearTimeout(idle);
|
|
715
|
+
idle = setTimeout(stalled, JUDGE_IDLE_TIMEOUT_MS);
|
|
716
|
+
};
|
|
717
|
+
try {
|
|
718
|
+
let response;
|
|
719
|
+
try {
|
|
720
|
+
response = await fetch(`${baseURL}/chat/completions`, {
|
|
721
|
+
method: "POST",
|
|
722
|
+
signal: controller.signal,
|
|
723
|
+
headers: {
|
|
724
|
+
"Content-Type": "application/json",
|
|
725
|
+
...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
|
|
447
726
|
},
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
727
|
+
body: requestBody(stream)
|
|
728
|
+
});
|
|
729
|
+
} catch (error) {
|
|
730
|
+
return transportFailure("Judge request failed", controller.signal.reason ?? error);
|
|
731
|
+
}
|
|
732
|
+
if (!response.ok) {
|
|
733
|
+
const body = await response.text().catch(() => "");
|
|
734
|
+
return {
|
|
735
|
+
kind: "unavailable",
|
|
736
|
+
error: new JudgeUnavailableError(`Judge API ${response.status}: ${body.slice(0, 200)}`),
|
|
737
|
+
retryable: isRetryableStatus(response.status),
|
|
738
|
+
...isRetryableStatus(response.status) ? { retryDelayMs: retryDelayMs(response.headers.get("retry-after")) } : {},
|
|
739
|
+
// Name the field, not the word: gateways echo request params in
|
|
740
|
+
// unrelated 400 bodies.
|
|
741
|
+
...stream && response.status === 400 && /stream_options|["'`]stream["'`]/.test(body) ? { streamRejected: true } : {}
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
return { kind: "completion", completion: await readCompletion(response, onData) };
|
|
746
|
+
} catch (error) {
|
|
747
|
+
return transportFailure("Judge response failed", controller.signal.reason ?? error);
|
|
748
|
+
}
|
|
749
|
+
} finally {
|
|
750
|
+
clearTimeout(total);
|
|
751
|
+
clearTimeout(idle);
|
|
752
|
+
controller.abort();
|
|
457
753
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
754
|
+
};
|
|
755
|
+
const isTruncated = ({ text, usage, finishReason }) => {
|
|
756
|
+
if (!text.trim()) return true;
|
|
757
|
+
if (usage !== void 0 && usage.completion_tokens === 0) return true;
|
|
758
|
+
if (finishReason !== "length" && finishReason !== "content_filter" && finishReason !== "error") {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
JSON.parse(text);
|
|
763
|
+
return false;
|
|
764
|
+
} catch {
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
return runWithSharedPrefix(cacheKey, async () => {
|
|
769
|
+
const startedAt = Date.now();
|
|
770
|
+
const deadline = startedAt + JUDGE_TIMEOUT_MS;
|
|
771
|
+
let stream = true;
|
|
772
|
+
let result = await attempt(stream, deadline);
|
|
773
|
+
const truncated = result.kind === "completion" && isTruncated(result.completion);
|
|
774
|
+
const streamRejected = result.kind === "unavailable" && result.streamRejected === true;
|
|
775
|
+
const retryable = result.kind === "unavailable" && result.retryable;
|
|
776
|
+
if (truncated || streamRejected || retryable) {
|
|
777
|
+
const delay = result.kind === "unavailable" && result.retryDelayMs !== void 0 ? result.retryDelayMs : streamRejected ? 0 : JUDGE_RETRY_DELAY_MS;
|
|
778
|
+
if (deadline - Date.now() - delay >= JUDGE_MIN_RETRY_BUDGET_MS) {
|
|
779
|
+
if (streamRejected) stream = false;
|
|
780
|
+
await new Promise((resolve4) => setTimeout(resolve4, delay));
|
|
781
|
+
const first = result;
|
|
782
|
+
result = await attempt(stream, deadline);
|
|
783
|
+
if (result.kind === "unavailable" && first.kind === "unavailable") {
|
|
784
|
+
result = {
|
|
785
|
+
...result,
|
|
786
|
+
error: new JudgeUnavailableError(
|
|
787
|
+
`${first.error.message} (retry: ${result.error.message})`
|
|
788
|
+
)
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (result.kind === "unavailable") throw result.error;
|
|
794
|
+
const { text, usage } = result.completion;
|
|
795
|
+
const judge = {
|
|
796
|
+
model: args.model,
|
|
797
|
+
contract: judgeContractInUse(),
|
|
798
|
+
prompt: { system: systemPromptText, user: instructionText },
|
|
799
|
+
response: text,
|
|
800
|
+
tokens: parseJudgeUsage(usage),
|
|
801
|
+
latency_ms: Date.now() - startedAt,
|
|
802
|
+
secondJudge: await secondJudgePromise
|
|
803
|
+
};
|
|
804
|
+
if (isTruncated(result.completion)) {
|
|
464
805
|
return {
|
|
465
806
|
pass: false,
|
|
466
|
-
reasoning: "Judge returned an empty or truncated response \u2014 likely a transient provider failure. The verdict is unknown, so this check is
|
|
467
|
-
judge
|
|
468
|
-
|
|
469
|
-
contract: judgeContractInUse(),
|
|
470
|
-
prompt: { system: systemPromptText, user: instructionText },
|
|
471
|
-
response: text,
|
|
472
|
-
tokens: parseJudgeUsage(data.usage),
|
|
473
|
-
latency_ms: Date.now() - startedAt,
|
|
474
|
-
secondJudge: await secondJudgePromise
|
|
475
|
-
}
|
|
807
|
+
reasoning: "Judge returned an empty or truncated response \u2014 likely a transient provider failure. The verdict is unknown, so this check is recorded as a judge error, not a verdict.",
|
|
808
|
+
judge,
|
|
809
|
+
unavailable: true
|
|
476
810
|
};
|
|
477
811
|
}
|
|
478
812
|
const parsed = parseJudgeJson(text);
|
|
479
813
|
return {
|
|
480
814
|
pass: parsed.pass === true,
|
|
481
815
|
reasoning: typeof parsed.reasoning === "string" ? parsed.reasoning : "",
|
|
482
|
-
judge
|
|
483
|
-
model: args.model,
|
|
484
|
-
contract: judgeContractInUse(),
|
|
485
|
-
prompt: { system: systemPromptText, user: instructionText },
|
|
486
|
-
response: text,
|
|
487
|
-
tokens: parseJudgeUsage(data.usage),
|
|
488
|
-
latency_ms: Date.now() - startedAt,
|
|
489
|
-
secondJudge: await secondJudgePromise
|
|
490
|
-
}
|
|
816
|
+
judge
|
|
491
817
|
};
|
|
492
818
|
});
|
|
493
819
|
}
|
|
820
|
+
function transportFailure(prefix, error) {
|
|
821
|
+
if (error instanceof JudgeUnavailableError) {
|
|
822
|
+
return { kind: "unavailable", error, retryable: true };
|
|
823
|
+
}
|
|
824
|
+
const timedOut = error instanceof Error && error.name === "TimeoutError";
|
|
825
|
+
return {
|
|
826
|
+
kind: "unavailable",
|
|
827
|
+
error: new JudgeUnavailableError(
|
|
828
|
+
`${prefix}: ${error instanceof Error ? error.message : String(error)}`
|
|
829
|
+
),
|
|
830
|
+
retryable: !timedOut
|
|
831
|
+
};
|
|
832
|
+
}
|
|
494
833
|
function assembleBriefing(args) {
|
|
495
834
|
if (!args.prompt) {
|
|
496
835
|
return {
|
|
@@ -521,6 +860,53 @@ ${args.instruction}`
|
|
|
521
860
|
|
|
522
861
|
// src/agent-task/checks/agent-session.ts
|
|
523
862
|
import { createHash } from "crypto";
|
|
863
|
+
|
|
864
|
+
// src/agent-task/tracing.ts
|
|
865
|
+
function createNoopAgentTaskTraceContext() {
|
|
866
|
+
const noopFn = async (fn) => fn();
|
|
867
|
+
return {
|
|
868
|
+
runId: "agent-task-untraced",
|
|
869
|
+
rootSpanId: "agent-task-untraced-root",
|
|
870
|
+
async step(options, fn) {
|
|
871
|
+
return fn(options.id ?? "agent-task-untraced-step");
|
|
872
|
+
},
|
|
873
|
+
recordEvent(options) {
|
|
874
|
+
return options.id ?? "agent-task-untraced-event";
|
|
875
|
+
},
|
|
876
|
+
endRoot() {
|
|
877
|
+
},
|
|
878
|
+
traceTool(_name, _params, fn) {
|
|
879
|
+
return noopFn(fn);
|
|
880
|
+
},
|
|
881
|
+
traceRetriever(_query, fn) {
|
|
882
|
+
return noopFn(fn);
|
|
883
|
+
},
|
|
884
|
+
traceChain(_name, fn) {
|
|
885
|
+
return noopFn(fn);
|
|
886
|
+
},
|
|
887
|
+
traceAgent(_name, fn) {
|
|
888
|
+
return noopFn(fn);
|
|
889
|
+
},
|
|
890
|
+
traceGuardrail(_name, fn) {
|
|
891
|
+
return noopFn(fn);
|
|
892
|
+
},
|
|
893
|
+
traceEmbedding(_model, _input, fn) {
|
|
894
|
+
return noopFn(fn);
|
|
895
|
+
},
|
|
896
|
+
async score(_params) {
|
|
897
|
+
},
|
|
898
|
+
createSpan(_options) {
|
|
899
|
+
return "agent-task-untraced-span";
|
|
900
|
+
},
|
|
901
|
+
endSpan(_spanId, _params) {
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function isTraceableSpanId(spanId) {
|
|
906
|
+
return spanId !== void 0 && !spanId.startsWith("agent-task-untraced");
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/agent-task/checks/agent-session.ts
|
|
524
910
|
var DEFAULT_BUDGET = {
|
|
525
911
|
maxTurns: 12,
|
|
526
912
|
maxToolCalls: 24,
|
|
@@ -581,7 +967,7 @@ function extractVerdict(steps) {
|
|
|
581
967
|
return void 0;
|
|
582
968
|
}
|
|
583
969
|
function buildEvidenceTools(args) {
|
|
584
|
-
const { z, tool, evidence, scope, budget, ledger, stepIndexOf, includeTrace } = args;
|
|
970
|
+
const { z, tool, evidence, scope, budget, ledger, stepIndexOf, includeTrace, includeHistory, tracer } = args;
|
|
585
971
|
const budgetGuard = () => {
|
|
586
972
|
ledger.toolCallsUsed += 1;
|
|
587
973
|
if (ledger.toolCallsUsed > budget.maxToolCalls) {
|
|
@@ -600,6 +986,7 @@ function buildEvidenceTools(args) {
|
|
|
600
986
|
});
|
|
601
987
|
return ledger.readBytesUsed > budget.maxReadBytes ? overflow() : null;
|
|
602
988
|
};
|
|
989
|
+
const span = (name, input, fn) => tracer ? tracer.traceTool(name, input, fn) : fn();
|
|
603
990
|
const tools = {
|
|
604
991
|
read_deliverable: tool({
|
|
605
992
|
description: `Read one of this run's deliverables by name, with optional offset/limit for pagination (max ${MAX_READ_WINDOW} bytes per call).`,
|
|
@@ -610,21 +997,23 @@ function buildEvidenceTools(args) {
|
|
|
610
997
|
}),
|
|
611
998
|
execute: async (input) => {
|
|
612
999
|
const { name, offset, limit } = input;
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
1000
|
+
return span("read_deliverable", input, async () => {
|
|
1001
|
+
const guard = budgetGuard();
|
|
1002
|
+
if (guard) return { error: guard };
|
|
1003
|
+
if (!Object.prototype.hasOwnProperty.call(evidence.deliverables, name)) {
|
|
1004
|
+
return { error: `unknown deliverable: ${name}` };
|
|
1005
|
+
}
|
|
1006
|
+
const content = renderValue(evidence.deliverables[name]);
|
|
1007
|
+
const slice = content.slice(offset, offset + limit);
|
|
1008
|
+
const overflow = accountRead(
|
|
1009
|
+
"read_deliverable",
|
|
1010
|
+
JSON.stringify({ name, offset, limit }),
|
|
1011
|
+
slice,
|
|
1012
|
+
() => `read budget exhausted (${budget.maxReadBytes} bytes); call finish_verdict now`
|
|
1013
|
+
);
|
|
1014
|
+
if (overflow) return { error: overflow };
|
|
1015
|
+
return { name, total_bytes: content.length, offset, returned: slice.length, content: slice };
|
|
1016
|
+
});
|
|
628
1017
|
}
|
|
629
1018
|
}),
|
|
630
1019
|
search_deliverable: tool({
|
|
@@ -632,49 +1021,53 @@ function buildEvidenceTools(args) {
|
|
|
632
1021
|
inputSchema: z.object({ name: z.string(), pattern: z.string() }),
|
|
633
1022
|
execute: async (input) => {
|
|
634
1023
|
const { name, pattern } = input;
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
1024
|
+
return span("search_deliverable", input, async () => {
|
|
1025
|
+
const guard = budgetGuard();
|
|
1026
|
+
if (guard) return { error: guard };
|
|
1027
|
+
if (!Object.prototype.hasOwnProperty.call(evidence.deliverables, name)) {
|
|
1028
|
+
return { error: `unknown deliverable: ${name}` };
|
|
1029
|
+
}
|
|
1030
|
+
if (pattern.length > SEARCH_MAX_PATTERN) {
|
|
1031
|
+
return { error: `pattern too long (max ${SEARCH_MAX_PATTERN} chars)` };
|
|
1032
|
+
}
|
|
1033
|
+
let re;
|
|
1034
|
+
try {
|
|
1035
|
+
re = new RegExp(pattern, "gi");
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
return { error: `invalid regex: ${error instanceof Error ? error.message : String(error)}` };
|
|
1038
|
+
}
|
|
1039
|
+
const content = renderValue(evidence.deliverables[name]).slice(0, SEARCH_SCAN_LIMIT);
|
|
1040
|
+
const hits = [];
|
|
1041
|
+
let m;
|
|
1042
|
+
let execs = 0;
|
|
1043
|
+
while ((m = re.exec(content)) && hits.length < SEARCH_HITS) {
|
|
1044
|
+
if (++execs > SEARCH_MAX_EXEC) break;
|
|
1045
|
+
const start = Math.max(0, m.index - SEARCH_CONTEXT);
|
|
1046
|
+
hits.push({ at_byte: m.index, context: content.slice(start, m.index + m[0].length + SEARCH_CONTEXT) });
|
|
1047
|
+
if (m.index === re.lastIndex) re.lastIndex++;
|
|
1048
|
+
}
|
|
1049
|
+
const served = JSON.stringify({ total_bytes: content.length, match_count: hits.length, matches: hits });
|
|
1050
|
+
const overflow = accountRead(
|
|
1051
|
+
"search_deliverable",
|
|
1052
|
+
JSON.stringify({ name, pattern }),
|
|
1053
|
+
served,
|
|
1054
|
+
() => `read budget exhausted (${budget.maxReadBytes} bytes); call finish_verdict now`
|
|
1055
|
+
);
|
|
1056
|
+
if (overflow) return { error: overflow };
|
|
1057
|
+
return { total_bytes: content.length, match_count: hits.length, matches: hits };
|
|
1058
|
+
});
|
|
668
1059
|
}
|
|
669
1060
|
}),
|
|
670
|
-
...evidence.history ? {
|
|
1061
|
+
...includeHistory && evidence.history ? {
|
|
671
1062
|
list_runs: tool({
|
|
672
1063
|
description: "List runs of this task \u2014 id, status, pass/fail, model, check counts. The run under judgment is flagged; use get_run for a prior run's full check report.",
|
|
673
1064
|
inputSchema: z.object({}),
|
|
674
1065
|
execute: async () => {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1066
|
+
return span("list_runs", {}, async () => {
|
|
1067
|
+
const guard = budgetGuard();
|
|
1068
|
+
if (guard) return { error: guard };
|
|
1069
|
+
return evidence.history.runs;
|
|
1070
|
+
});
|
|
678
1071
|
}
|
|
679
1072
|
}),
|
|
680
1073
|
get_run: tool({
|
|
@@ -682,18 +1075,20 @@ function buildEvidenceTools(args) {
|
|
|
682
1075
|
inputSchema: z.object({ run_id: z.string() }),
|
|
683
1076
|
execute: async (input) => {
|
|
684
1077
|
const { run_id } = input;
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1078
|
+
return span("get_run", input, async () => {
|
|
1079
|
+
const guard = budgetGuard();
|
|
1080
|
+
if (guard) return { error: guard };
|
|
1081
|
+
const detail = await evidence.history.getRun(run_id);
|
|
1082
|
+
const served = JSON.stringify(detail);
|
|
1083
|
+
const overflow = accountRead(
|
|
1084
|
+
"get_run",
|
|
1085
|
+
JSON.stringify({ run_id }),
|
|
1086
|
+
served,
|
|
1087
|
+
() => `read budget exhausted (${budget.maxReadBytes} bytes); call finish_verdict now`
|
|
1088
|
+
);
|
|
1089
|
+
if (overflow) return { error: overflow };
|
|
1090
|
+
return detail;
|
|
1091
|
+
});
|
|
697
1092
|
}
|
|
698
1093
|
})
|
|
699
1094
|
} : {},
|
|
@@ -701,13 +1096,15 @@ function buildEvidenceTools(args) {
|
|
|
701
1096
|
description: "The task this run executed: id, description, deliverable names.",
|
|
702
1097
|
inputSchema: z.object({}),
|
|
703
1098
|
execute: async () => {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1099
|
+
return span("get_task_definition", {}, async () => {
|
|
1100
|
+
const guard = budgetGuard();
|
|
1101
|
+
if (guard) return { error: guard };
|
|
1102
|
+
return {
|
|
1103
|
+
task_id: scope?.taskId,
|
|
1104
|
+
description: scope?.taskDescription ?? "(unavailable)",
|
|
1105
|
+
deliverables: Object.keys(evidence.deliverables)
|
|
1106
|
+
};
|
|
1107
|
+
});
|
|
711
1108
|
}
|
|
712
1109
|
})
|
|
713
1110
|
};
|
|
@@ -717,21 +1114,23 @@ function buildEvidenceTools(args) {
|
|
|
717
1114
|
inputSchema: z.object({ query: z.string().describe("substring filter on tool names; empty = all") }),
|
|
718
1115
|
execute: async (input) => {
|
|
719
1116
|
const { query } = input;
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1117
|
+
return span("get_trace", input, async () => {
|
|
1118
|
+
const guard = budgetGuard();
|
|
1119
|
+
if (guard) return { error: guard };
|
|
1120
|
+
const view = evidence.view;
|
|
1121
|
+
if (!view) {
|
|
1122
|
+
return {
|
|
1123
|
+
status: "unsupported",
|
|
1124
|
+
detail: "no trace projection exists for this run \u2014 judge on deliverables instead"
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
if (view.requireCapability("tools") !== "available") {
|
|
1128
|
+
return { status: "unsupported", detail: "tool-call evidence is unavailable in this trace projection" };
|
|
1129
|
+
}
|
|
1130
|
+
const calls = view.toolCalls.filter((c) => query ? c.name.includes(query) : true).slice(0, 30).map((c) => ({ name: c.name, input: renderValue(c.input).slice(0, 200) }));
|
|
1131
|
+
const reply = view.requireCapability("messages") === "available" && view.reply ? view.reply.slice(0, 1500) : void 0;
|
|
1132
|
+
return { turns: view.turnCount, tool_calls: calls, ...reply !== void 0 ? { final_reply: reply } : {} };
|
|
1133
|
+
});
|
|
735
1134
|
}
|
|
736
1135
|
});
|
|
737
1136
|
}
|
|
@@ -858,7 +1257,9 @@ async function runAgentSession(spec) {
|
|
|
858
1257
|
budget,
|
|
859
1258
|
ledger,
|
|
860
1259
|
stepIndexOf: () => liveSteps.length,
|
|
861
|
-
includeTrace: spec.tools?.trace !== false
|
|
1260
|
+
includeTrace: spec.tools?.trace !== false,
|
|
1261
|
+
includeHistory: spec.tools?.history !== false,
|
|
1262
|
+
tracer: spec.tracer
|
|
862
1263
|
});
|
|
863
1264
|
const system = buildBriefing(spec.scope, spec.evidence, budget);
|
|
864
1265
|
const exhibitsBlock = spec.exhibits?.length ? "\n\nValues submitted for judgment:\n" + spec.exhibits.map((v, i) => ` [${i + 1}] ${renderValue(v).slice(0, RECORD_RESULT_LIMIT)}`).join("\n") : "";
|
|
@@ -918,7 +1319,7 @@ async function runAgentSession(spec) {
|
|
|
918
1319
|
latency_ms: Date.now() - started
|
|
919
1320
|
};
|
|
920
1321
|
}
|
|
921
|
-
function createAgentMethod(rec, judgeConfig, judgeScope, evidence) {
|
|
1322
|
+
function createAgentMethod(rec, judgeConfig, judgeScope, evidence, judgeTracer) {
|
|
922
1323
|
return async (instruction, opts) => {
|
|
923
1324
|
const label = opts?.label ?? "agent";
|
|
924
1325
|
const location = rec.captureLocation();
|
|
@@ -930,22 +1331,58 @@ function createAgentMethod(rec, judgeConfig, judgeScope, evidence) {
|
|
|
930
1331
|
label,
|
|
931
1332
|
false,
|
|
932
1333
|
"No judge model configured for t.agent. Set one of:\n\u2022 OPENROUTER_MODEL + OPENROUTER_API_KEY (OpenRouter \u2014 needs a tool-calling-capable model)\n\u2022 OPENAI_MODEL + OPENAI_API_KEY (OpenAI direct)\nOr pass { judge } to runTask() programmatically.",
|
|
933
|
-
{ evaluator_type: "agent", location }
|
|
1334
|
+
{ evaluator_type: "agent", location, outcome: "error" }
|
|
934
1335
|
);
|
|
935
1336
|
return;
|
|
936
1337
|
}
|
|
1338
|
+
const run = () => runAgentSession({
|
|
1339
|
+
instruction,
|
|
1340
|
+
model: effective.model,
|
|
1341
|
+
baseURL: effective.baseURL,
|
|
1342
|
+
apiKey: effective.apiKey,
|
|
1343
|
+
evidence: evidence ?? { deliverables: {} },
|
|
1344
|
+
scope: judgeScope,
|
|
1345
|
+
exhibits,
|
|
1346
|
+
tools: opts?.tools,
|
|
1347
|
+
budget: opts?.budget,
|
|
1348
|
+
tracer: judgeTracer
|
|
1349
|
+
});
|
|
937
1350
|
try {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1351
|
+
let sessionSpanId;
|
|
1352
|
+
const result = await (judgeTracer ? judgeTracer.step(
|
|
1353
|
+
{
|
|
1354
|
+
step_name: `t.agent:${judgeScope?.checkName ?? label}`,
|
|
1355
|
+
observation_type: "AGENT",
|
|
1356
|
+
// The span carries the model + tokens so the backend prices the
|
|
1357
|
+
// judge session and trace rollups count its spend (issue #288).
|
|
1358
|
+
model: effective.model,
|
|
1359
|
+
input: { model: effective.model, instruction },
|
|
1360
|
+
usage: (r) => {
|
|
1361
|
+
const usage = r?.usage;
|
|
1362
|
+
return usage ? {
|
|
1363
|
+
prompt_tokens: usage.input_tokens,
|
|
1364
|
+
completion_tokens: usage.output_tokens
|
|
1365
|
+
} : void 0;
|
|
1366
|
+
},
|
|
1367
|
+
summarize: (r) => {
|
|
1368
|
+
const res = r;
|
|
1369
|
+
const reasoning = res?.verdict?.reasoning?.slice(0, 2e3) ?? `session ended without a verdict (${res?.outcome ?? "unknown"})`;
|
|
1370
|
+
return {
|
|
1371
|
+
text: reasoning,
|
|
1372
|
+
// reasoning-first, matching the judge response
|
|
1373
|
+
// contract's default order (#163).
|
|
1374
|
+
verdict: {
|
|
1375
|
+
reasoning,
|
|
1376
|
+
pass: res?.verdict?.pass ?? null
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
},
|
|
1381
|
+
async (spanId) => {
|
|
1382
|
+
sessionSpanId = spanId;
|
|
1383
|
+
return run();
|
|
1384
|
+
}
|
|
1385
|
+
) : run());
|
|
949
1386
|
const judge = {
|
|
950
1387
|
model: effective.model,
|
|
951
1388
|
temperature: 0,
|
|
@@ -956,7 +1393,8 @@ function createAgentMethod(rec, judgeConfig, judgeScope, evidence) {
|
|
|
956
1393
|
},
|
|
957
1394
|
...result.cost !== void 0 ? { cost: result.cost } : {},
|
|
958
1395
|
latency_ms: result.latency_ms,
|
|
959
|
-
session: result.session
|
|
1396
|
+
session: result.session,
|
|
1397
|
+
...isTraceableSpanId(sessionSpanId) ? { span_id: sessionSpanId } : {}
|
|
960
1398
|
};
|
|
961
1399
|
const detail = result.outcome === "error" ? `agentic judge session errored before verdicting: ${result.error ?? "unknown error"}` : `agentic judge session ended without a verdict (budget exhausted after ${result.session.usage?.steps ?? "?"} steps; last tool: ${result.session.steps?.at(-1)?.tool_calls?.map((c) => c.name).join(", ") ?? "none"})`;
|
|
962
1400
|
rec.record(label, result.verdict?.pass ?? false, result.verdict?.reasoning ?? detail, {
|
|
@@ -1001,6 +1439,8 @@ var TEST_METHOD_NAMES = [
|
|
|
1001
1439
|
"messageIncludes",
|
|
1002
1440
|
"maxTurns",
|
|
1003
1441
|
"maxDurationMs",
|
|
1442
|
+
"maxTokens",
|
|
1443
|
+
"minTokens",
|
|
1004
1444
|
"assert",
|
|
1005
1445
|
"check",
|
|
1006
1446
|
"judge",
|
|
@@ -1016,7 +1456,14 @@ function matchName(name, m) {
|
|
|
1016
1456
|
if (m instanceof RegExp) return m.test(name);
|
|
1017
1457
|
return m(name);
|
|
1018
1458
|
}
|
|
1019
|
-
function
|
|
1459
|
+
function trackPending(rec, method) {
|
|
1460
|
+
return ((...args) => {
|
|
1461
|
+
const promise = method(...args);
|
|
1462
|
+
rec.track(promise);
|
|
1463
|
+
return promise;
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
function createJudgeMethod(rec, judgeConfig, judgeScope, judgeTracer) {
|
|
1020
1467
|
return async (values, instruction, opts) => {
|
|
1021
1468
|
const label = opts?.label ?? "judge";
|
|
1022
1469
|
const location = rec.captureLocation();
|
|
@@ -1027,21 +1474,59 @@ function createJudgeMethod(rec, judgeConfig, judgeScope) {
|
|
|
1027
1474
|
label,
|
|
1028
1475
|
false,
|
|
1029
1476
|
"No judge model configured. Set one of:\n\u2022 OPENROUTER_MODEL + OPENROUTER_API_KEY (OpenRouter \u2014 works with 200+ models, one account)\n\u2022 OPENAI_MODEL + OPENAI_API_KEY (OpenAI direct)\nOr pass { judge } to runTask() programmatically.",
|
|
1030
|
-
|
|
1477
|
+
// No judge means no verdict — the same bucket as an unreachable judge.
|
|
1478
|
+
{ evaluator_type: "llm", location, outcome: "error" }
|
|
1031
1479
|
);
|
|
1032
1480
|
return;
|
|
1033
1481
|
}
|
|
1034
1482
|
const context2 = judgeScopeToContext(judgeScope);
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1483
|
+
const secondJudgeValue = opts?.secondJudgeValue;
|
|
1484
|
+
const call = () => callJudge({
|
|
1485
|
+
values: valueArray,
|
|
1486
|
+
instruction,
|
|
1487
|
+
model: effective.model,
|
|
1488
|
+
baseURL: effective.baseURL,
|
|
1489
|
+
apiKey: effective.apiKey,
|
|
1490
|
+
prompt: effective.prompt,
|
|
1491
|
+
...context2 ? { context: context2 } : {},
|
|
1492
|
+
...secondJudgeValue !== void 0 ? {
|
|
1493
|
+
secondJudgeValue: Array.isArray(secondJudgeValue) ? secondJudgeValue : [secondJudgeValue]
|
|
1494
|
+
} : {}
|
|
1495
|
+
});
|
|
1496
|
+
let judgeSpanId;
|
|
1497
|
+
const traced = judgeTracer ? judgeTracer.step(
|
|
1498
|
+
{
|
|
1499
|
+
step_name: `judge:${judgeScope?.checkName ?? label}`,
|
|
1500
|
+
// A single-shot judge call is an LLM generation — project it
|
|
1501
|
+
// with the same observation semantics as the main agent's.
|
|
1502
|
+
observation_type: "GENERATION",
|
|
1503
|
+
// The span carries the model + tokens so the backend prices the
|
|
1504
|
+
// judge call and trace rollups count its spend (issue #288).
|
|
1039
1505
|
model: effective.model,
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1506
|
+
input: { model: effective.model, instruction },
|
|
1507
|
+
summarize: (r) => {
|
|
1508
|
+
const res = r;
|
|
1509
|
+
return {
|
|
1510
|
+
text: res?.reasoning?.slice(0, 2e3) ?? "",
|
|
1511
|
+
verdict: {
|
|
1512
|
+
reasoning: res?.reasoning?.slice(0, 2e3) ?? null,
|
|
1513
|
+
pass: res?.pass ?? null
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
},
|
|
1517
|
+
usage: (r) => {
|
|
1518
|
+
const tokens = r?.judge?.tokens;
|
|
1519
|
+
return tokens ? { prompt_tokens: tokens.input, completion_tokens: tokens.output } : void 0;
|
|
1520
|
+
}
|
|
1521
|
+
},
|
|
1522
|
+
async (spanId) => {
|
|
1523
|
+
judgeSpanId = spanId;
|
|
1524
|
+
return call();
|
|
1525
|
+
}
|
|
1526
|
+
) : call();
|
|
1527
|
+
try {
|
|
1528
|
+
const { pass, reasoning, judge, unavailable } = await traced;
|
|
1529
|
+
if (isTraceableSpanId(judgeSpanId)) judge.span_id = judgeSpanId;
|
|
1045
1530
|
rec.record(label, pass, reasoning, {
|
|
1046
1531
|
evaluator_type: "llm",
|
|
1047
1532
|
judge,
|
|
@@ -1050,7 +1535,8 @@ function createJudgeMethod(rec, judgeConfig, judgeScope) {
|
|
|
1050
1535
|
// dashboard can render it with the right viewer — Markdown for
|
|
1051
1536
|
// prose, a JSON tree for structured objects.
|
|
1052
1537
|
received: valueArray.length === 1 ? valueArray[0] : valueArray,
|
|
1053
|
-
location
|
|
1538
|
+
location,
|
|
1539
|
+
...unavailable ? { outcome: "error" } : {}
|
|
1054
1540
|
});
|
|
1055
1541
|
} catch (error) {
|
|
1056
1542
|
rec.record(
|
|
@@ -1061,7 +1547,8 @@ function createJudgeMethod(rec, judgeConfig, judgeScope) {
|
|
|
1061
1547
|
evaluator_type: "llm",
|
|
1062
1548
|
expected: instruction,
|
|
1063
1549
|
received: valueArray.length === 1 ? valueArray[0] : valueArray,
|
|
1064
|
-
location
|
|
1550
|
+
location,
|
|
1551
|
+
outcome: "error"
|
|
1065
1552
|
}
|
|
1066
1553
|
);
|
|
1067
1554
|
}
|
|
@@ -1083,9 +1570,10 @@ var CAPABILITY_LABELS = {
|
|
|
1083
1570
|
errors: "error",
|
|
1084
1571
|
timing: "timing",
|
|
1085
1572
|
skills: "skill",
|
|
1086
|
-
subagents: "subagent"
|
|
1573
|
+
subagents: "subagent",
|
|
1574
|
+
usage: "token usage"
|
|
1087
1575
|
};
|
|
1088
|
-
function createTraceTestContext(view, rec, judgeConfig, judgeScope, agentEvidence) {
|
|
1576
|
+
function createTraceTestContext(view, rec, judgeConfig, judgeScope, agentEvidence, judgeTracer) {
|
|
1089
1577
|
const unsupported = (id, capability) => {
|
|
1090
1578
|
const label = CAPABILITY_LABELS[capability];
|
|
1091
1579
|
rec.record(id, false, `${label} evidence is unavailable in this trace projection`, {
|
|
@@ -1238,7 +1726,11 @@ function createTraceTestContext(view, rec, judgeConfig, judgeScope, agentEvidenc
|
|
|
1238
1726
|
received: `${turns}`
|
|
1239
1727
|
});
|
|
1240
1728
|
},
|
|
1241
|
-
maxDurationMs(n) {
|
|
1729
|
+
maxDurationMs(n, opts) {
|
|
1730
|
+
if (opts?.turn !== void 0) {
|
|
1731
|
+
recordTurnDuration(view, rec, n, opts.turn, unsupported);
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1242
1734
|
if (!isAvailable("timing")) {
|
|
1243
1735
|
unsupported(`maxDurationMs(${n})`, "timing");
|
|
1244
1736
|
return;
|
|
@@ -1253,6 +1745,12 @@ function createTraceTestContext(view, rec, judgeConfig, judgeScope, agentEvidenc
|
|
|
1253
1745
|
received: `${ms}ms`
|
|
1254
1746
|
});
|
|
1255
1747
|
},
|
|
1748
|
+
maxTokens(n, opts) {
|
|
1749
|
+
recordTokenBudget(view, rec, "max", n, opts, unsupported);
|
|
1750
|
+
},
|
|
1751
|
+
minTokens(n, opts) {
|
|
1752
|
+
recordTokenBudget(view, rec, "min", n, opts, unsupported);
|
|
1753
|
+
},
|
|
1256
1754
|
assert(label, predicate) {
|
|
1257
1755
|
const pass = predicate(view);
|
|
1258
1756
|
rec.record(label, pass, pass ? "" : `assertion "${label}" failed`, {
|
|
@@ -1271,10 +1769,99 @@ function createTraceTestContext(view, rec, judgeConfig, judgeScope, agentEvidenc
|
|
|
1271
1769
|
},
|
|
1272
1770
|
// judge does not consult trace capabilities; the scope carries the
|
|
1273
1771
|
// task/check frame for prompt builders (#161).
|
|
1274
|
-
judge: createJudgeMethod(rec, judgeConfig, judgeScope),
|
|
1275
|
-
agent: createAgentMethod(rec, judgeConfig, judgeScope, agentEvidence ?? { deliverables: {}, view })
|
|
1772
|
+
judge: trackPending(rec, createJudgeMethod(rec, judgeConfig, judgeScope, judgeTracer)),
|
|
1773
|
+
agent: trackPending(rec, createAgentMethod(rec, judgeConfig, judgeScope, agentEvidence ?? { deliverables: {}, view }, judgeTracer))
|
|
1276
1774
|
};
|
|
1277
1775
|
}
|
|
1776
|
+
function requireBudget(method, n) {
|
|
1777
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
1778
|
+
throw new TypeError(`${method}: the budget must be a finite, non-negative number, got ${n}`);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
var TOKEN_KINDS = ["input", "output", "total"];
|
|
1782
|
+
function requireTurnNumber(method, turn2) {
|
|
1783
|
+
if (!Number.isInteger(turn2) || turn2 < 1) {
|
|
1784
|
+
throw new TypeError(`${method}: turn must be a positive integer (1-based), got ${turn2}`);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
function recordTurnDuration(view, rec, n, turn2, unsupported) {
|
|
1788
|
+
requireBudget("maxDurationMs", n);
|
|
1789
|
+
requireTurnNumber("maxDurationMs", turn2);
|
|
1790
|
+
const id = `maxDurationMs(${n}, turn ${turn2})`;
|
|
1791
|
+
const turns = view.turns;
|
|
1792
|
+
if (turns === void 0) {
|
|
1793
|
+
unsupported(id, "timing");
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
const record = turns[turn2 - 1];
|
|
1797
|
+
if (!record) {
|
|
1798
|
+
rec.record(id, false, `turn ${turn2} did not run (the run had ${turns.length} turn(s))`, {
|
|
1799
|
+
expected: `turn ${turn2} \u2264 ${n}ms`,
|
|
1800
|
+
received: `${turns.length} turn(s)`
|
|
1801
|
+
});
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
const ms = record.durationMs;
|
|
1805
|
+
if (ms === void 0) {
|
|
1806
|
+
unsupported(id, "timing");
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
rec.record(id, ms <= n, ms <= n ? "" : `expected turn ${turn2} \u2264 ${n}ms, took ${ms}ms`, {
|
|
1810
|
+
expected: `turn ${turn2} \u2264 ${n}ms`,
|
|
1811
|
+
received: `${ms}ms`
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
function recordTokenBudget(view, rec, bound, n, opts, unsupported) {
|
|
1815
|
+
const method = bound === "max" ? "maxTokens" : "minTokens";
|
|
1816
|
+
const kind = opts?.kind ?? "total";
|
|
1817
|
+
const turn2 = opts?.turn;
|
|
1818
|
+
requireBudget(method, n);
|
|
1819
|
+
if (!TOKEN_KINDS.includes(kind)) {
|
|
1820
|
+
throw new TypeError(`${method}: kind must be one of ${TOKEN_KINDS.join(", ")}, got ${String(kind)}`);
|
|
1821
|
+
}
|
|
1822
|
+
if (turn2 !== void 0) requireTurnNumber(method, turn2);
|
|
1823
|
+
const scope = turn2 === void 0 ? "" : `, turn ${turn2}`;
|
|
1824
|
+
const id = `${method}(${n}${kind === "total" ? "" : `, ${kind}`}${scope})`;
|
|
1825
|
+
const op = bound === "max" ? "\u2264" : "\u2265";
|
|
1826
|
+
const expected = `${op} ${n} ${kind} tokens${turn2 === void 0 ? "" : ` in turn ${turn2}`}`;
|
|
1827
|
+
if (view.requireCapability("usage") !== "available") {
|
|
1828
|
+
unsupported(id, "usage");
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
if (turn2 !== void 0) {
|
|
1832
|
+
const turnCount = view.turnCountFromSpans;
|
|
1833
|
+
if (turn2 > turnCount) {
|
|
1834
|
+
rec.record(id, false, `turn ${turn2} did not run (the run had ${turnCount} turn(s))`, {
|
|
1835
|
+
expected,
|
|
1836
|
+
received: `${turnCount} turn(s)`
|
|
1837
|
+
});
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
const tally = view.tokens(kind, turn2 === void 0 ? void 0 : { turn: turn2 });
|
|
1842
|
+
if (tally === void 0 || tally.reported === 0 && tally.unreported === 0) {
|
|
1843
|
+
rec.record(id, false, `no LLM call${turn2 === void 0 ? " inside a turn" : ` in turn ${turn2}`} reported ${kind} token usage`, {
|
|
1844
|
+
outcome: "unsupported",
|
|
1845
|
+
expected,
|
|
1846
|
+
received: "no usage-bearing LLM calls in scope"
|
|
1847
|
+
});
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
const received = tally.unreported > 0 ? `\u2265 ${tally.tokens} tokens (${tally.unreported} LLM call(s) with unknown or errored usage)` : `${tally.tokens} tokens`;
|
|
1851
|
+
const within = bound === "max" ? tally.tokens <= n : tally.tokens >= n;
|
|
1852
|
+
if (tally.unreported > 0 && !(bound === "min" && within)) {
|
|
1853
|
+
rec.record(id, false, `usage is incomplete: ${tally.unreported} LLM call(s) in scope have unknown or errored ${kind} usage`, {
|
|
1854
|
+
outcome: "unsupported",
|
|
1855
|
+
expected,
|
|
1856
|
+
received
|
|
1857
|
+
});
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
rec.record(id, within, within ? "" : `expected ${expected}, got ${tally.tokens}`, {
|
|
1861
|
+
expected,
|
|
1862
|
+
received
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1278
1865
|
function matchesTraceToolCall(call, name, opts) {
|
|
1279
1866
|
if (!matchName(call.name, name)) return false;
|
|
1280
1867
|
if (opts?.input !== void 0 && !matchValue(call.input, opts.input)) {
|
|
@@ -1293,16 +1880,9 @@ function matchesTraceToolCall(call, name, opts) {
|
|
|
1293
1880
|
var Recorder = class {
|
|
1294
1881
|
constructor(locate) {
|
|
1295
1882
|
this.records = [];
|
|
1883
|
+
this.pending = [];
|
|
1296
1884
|
this.locate = locate;
|
|
1297
1885
|
}
|
|
1298
|
-
/**
|
|
1299
|
-
* Record an assertion.
|
|
1300
|
-
*
|
|
1301
|
-
* - ``extra.location`` overrides auto-capture (used by the runner for thrown
|
|
1302
|
-
* errors, whose relevant stack is the error's own, not the call site).
|
|
1303
|
-
* - ``extra.expected`` / ``extra.received`` carry the structured values for
|
|
1304
|
-
* testing-framework-style display.
|
|
1305
|
-
*/
|
|
1306
1886
|
/**
|
|
1307
1887
|
* Capture the call site location synchronously. Use this when a record will
|
|
1308
1888
|
* happen AFTER an `await` (e.g. `t.judge`): once an async function resumes,
|
|
@@ -1314,6 +1894,28 @@ var Recorder = class {
|
|
|
1314
1894
|
captureLocation() {
|
|
1315
1895
|
return this.locate ? this.locate(new Error().stack ?? "") : void 0;
|
|
1316
1896
|
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Register an async evaluation (``t.judge`` / ``t.agent``) started by this
|
|
1899
|
+
* check. If the check returns without awaiting it, the runner still waits
|
|
1900
|
+
* for its record via ``settlePending`` — a dropped await must not let the
|
|
1901
|
+
* check vacuously pass ("no assertions recorded") while the verdict is
|
|
1902
|
+
* still in flight.
|
|
1903
|
+
*/
|
|
1904
|
+
track(promise) {
|
|
1905
|
+
this.pending.push(promise);
|
|
1906
|
+
}
|
|
1907
|
+
/** Wait until every tracked evaluation has settled (and recorded). */
|
|
1908
|
+
async settlePending() {
|
|
1909
|
+
await Promise.allSettled(this.pending);
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Record an assertion.
|
|
1913
|
+
*
|
|
1914
|
+
* - ``extra.location`` overrides auto-capture (used by the runner for thrown
|
|
1915
|
+
* errors, whose relevant stack is the error's own, not the call site).
|
|
1916
|
+
* - ``extra.expected`` / ``extra.received`` carry the structured values for
|
|
1917
|
+
* testing-framework-style display.
|
|
1918
|
+
*/
|
|
1317
1919
|
record(id, pass, reasoning, extra) {
|
|
1318
1920
|
const location = extra?.location ?? (this.locate ? this.locate(new Error().stack ?? "") : void 0);
|
|
1319
1921
|
this.records.push({
|
|
@@ -1425,8 +2027,10 @@ async function runTraceChecks(args) {
|
|
|
1425
2027
|
{
|
|
1426
2028
|
deliverables: args.deliverables,
|
|
1427
2029
|
view,
|
|
1428
|
-
...args.historyPlane ? { history: args.historyPlane } : {}
|
|
1429
|
-
|
|
2030
|
+
...args.historyPlane ? { history: args.historyPlane } : {},
|
|
2031
|
+
...args.historyUnavailableReason ? { historyUnavailable: args.historyUnavailableReason } : {}
|
|
2032
|
+
},
|
|
2033
|
+
args.judgeTracer
|
|
1430
2034
|
);
|
|
1431
2035
|
let thrownLocation;
|
|
1432
2036
|
try {
|
|
@@ -1441,8 +2045,10 @@ async function runTraceChecks(args) {
|
|
|
1441
2045
|
location: thrownLocation
|
|
1442
2046
|
});
|
|
1443
2047
|
}
|
|
2048
|
+
await rec.settlePending();
|
|
1444
2049
|
const failed = rec.all.filter((r) => !r.pass);
|
|
1445
2050
|
const pass = failed.length === 0;
|
|
2051
|
+
const outcome = rollUpCheckOutcome(failed);
|
|
1446
2052
|
const reasoning = failed.length > 0 ? failed.map((r) => r.reasoning || r.id).join("; ") : rec.all.length > 0 ? "passed" : "no assertions recorded";
|
|
1447
2053
|
const location = failed.find((r) => r.location)?.location;
|
|
1448
2054
|
const judge = rec.all.find((r) => r.judge)?.judge;
|
|
@@ -1451,6 +2057,7 @@ async function runTraceChecks(args) {
|
|
|
1451
2057
|
pass,
|
|
1452
2058
|
reasoning,
|
|
1453
2059
|
evaluator_type: "code",
|
|
2060
|
+
...outcome ? { outcome } : {},
|
|
1454
2061
|
...judge ? { judge } : {},
|
|
1455
2062
|
...location ? { location } : {},
|
|
1456
2063
|
...args.displayFile ? { source_file: args.displayFile } : {},
|
|
@@ -1465,6 +2072,14 @@ async function runTraceChecks(args) {
|
|
|
1465
2072
|
);
|
|
1466
2073
|
return results;
|
|
1467
2074
|
}
|
|
2075
|
+
function rollUpCheckOutcome(failed) {
|
|
2076
|
+
if (failed.length === 0) return void 0;
|
|
2077
|
+
const verdictless = failed.filter(
|
|
2078
|
+
(r) => r.outcome === "error" || r.outcome === "unsupported"
|
|
2079
|
+
);
|
|
2080
|
+
if (verdictless.length < failed.length) return void 0;
|
|
2081
|
+
return verdictless.some((r) => r.outcome === "error") ? "error" : "unsupported";
|
|
2082
|
+
}
|
|
1468
2083
|
async function loadAndRunFlowChecks(checksPath, args, brokenDeliverables = {}) {
|
|
1469
2084
|
if (!checksPath) return [];
|
|
1470
2085
|
resetFlowChecks();
|
|
@@ -1477,6 +2092,7 @@ async function loadAndRunFlowChecks(checksPath, args, brokenDeliverables = {}) {
|
|
|
1477
2092
|
task: args.task,
|
|
1478
2093
|
judgeConfig: args.judgeConfig,
|
|
1479
2094
|
...args.historyPlane ? { historyPlane: args.historyPlane } : {},
|
|
2095
|
+
...args.judgeTracer ? { judgeTracer: args.judgeTracer } : {},
|
|
1480
2096
|
moduleUrl,
|
|
1481
2097
|
displayFile: basename(checksPath)
|
|
1482
2098
|
});
|
|
@@ -2347,6 +2963,7 @@ function resolveDeliverableSchema(definition) {
|
|
|
2347
2963
|
}
|
|
2348
2964
|
|
|
2349
2965
|
// src/agent-task/trace-projection/projection-tee.ts
|
|
2966
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2350
2967
|
function monotonicNowMs() {
|
|
2351
2968
|
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
2352
2969
|
return performance.now();
|
|
@@ -2386,7 +3003,7 @@ function extractText(output) {
|
|
|
2386
3003
|
}
|
|
2387
3004
|
return "";
|
|
2388
3005
|
}
|
|
2389
|
-
function deriveCapabilities(observations) {
|
|
3006
|
+
function deriveCapabilities(observations, hierarchyAmbiguous) {
|
|
2390
3007
|
const hasType = (t) => observations.some((o) => o.type === t);
|
|
2391
3008
|
const hasMessages = observations.some(
|
|
2392
3009
|
(o) => o.type === "GENERATION" && o.messages && o.messages.length > 0
|
|
@@ -2399,13 +3016,29 @@ function deriveCapabilities(observations) {
|
|
|
2399
3016
|
errors: "available",
|
|
2400
3017
|
timing: "available",
|
|
2401
3018
|
skills: hasType("SKILL") ? "available" : "unavailable",
|
|
2402
|
-
subagents: hasType("AGENT") ? "available" : "unavailable"
|
|
3019
|
+
subagents: hasType("AGENT") ? "available" : "unavailable",
|
|
3020
|
+
// Usage is attributed to turns through parentSpanId; with an ambiguous
|
|
3021
|
+
// step id the chain is broken, so a per-turn or per-run sum could
|
|
3022
|
+
// silently drop turns.
|
|
3023
|
+
usage: !observations.some((o) => o.usage !== void 0) ? "unavailable" : hierarchyAmbiguous ? "partial" : "available"
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
function usageFrom(params) {
|
|
3027
|
+
if (params?.prompt_tokens === void 0 && params?.completion_tokens === void 0) {
|
|
3028
|
+
return void 0;
|
|
3029
|
+
}
|
|
3030
|
+
return {
|
|
3031
|
+
...params.prompt_tokens !== void 0 ? { inputTokens: params.prompt_tokens } : {},
|
|
3032
|
+
...params.completion_tokens !== void 0 ? { outputTokens: params.completion_tokens } : {}
|
|
2403
3033
|
};
|
|
2404
3034
|
}
|
|
2405
3035
|
function createProjectionTee(real) {
|
|
2406
3036
|
const observations = [];
|
|
2407
3037
|
const pending = /* @__PURE__ */ new Map();
|
|
2408
3038
|
const realIdByTeeId = /* @__PURE__ */ new Map();
|
|
3039
|
+
const teeIdByRealStepId = /* @__PURE__ */ new Map();
|
|
3040
|
+
const ambiguousRealStepIds = /* @__PURE__ */ new Set();
|
|
3041
|
+
const activeStep = new AsyncLocalStorage();
|
|
2409
3042
|
let spanCounter = 0;
|
|
2410
3043
|
const rootStartMs = monotonicNowMs();
|
|
2411
3044
|
const rootStartIso = new Date(rootStartMs).toISOString();
|
|
@@ -2427,6 +3060,8 @@ function createProjectionTee(real) {
|
|
|
2427
3060
|
};
|
|
2428
3061
|
if (p.parentSpanId) obs.parentSpanId = p.parentSpanId;
|
|
2429
3062
|
if (p.model) obs.model = p.model;
|
|
3063
|
+
const usage = usageFrom(params);
|
|
3064
|
+
if (usage) obs.usage = usage;
|
|
2430
3065
|
if (isError && params?.status_message) obs.errorMessage = params.status_message;
|
|
2431
3066
|
const meta = params?.metadata;
|
|
2432
3067
|
if (p.type === "TOOL") {
|
|
@@ -2449,9 +3084,10 @@ function createProjectionTee(real) {
|
|
|
2449
3084
|
function recordStart(opts) {
|
|
2450
3085
|
spanCounter += 1;
|
|
2451
3086
|
const spanId = `${real.rootSpanId}-${String(spanCounter).padStart(6, "0")}`;
|
|
3087
|
+
const parent = opts.parent_call_id ?? activeStep.getStore() ?? real.rootSpanId;
|
|
2452
3088
|
pending.set(spanId, {
|
|
2453
3089
|
spanId,
|
|
2454
|
-
parentSpanId:
|
|
3090
|
+
parentSpanId: teeIdByRealStepId.get(parent) ?? parent,
|
|
2455
3091
|
type: observationTypeFor(opts.observation_type),
|
|
2456
3092
|
name: opts.step_name ?? "step",
|
|
2457
3093
|
toolName: opts.tool_name ?? void 0,
|
|
@@ -2469,11 +3105,22 @@ function createProjectionTee(real) {
|
|
|
2469
3105
|
const obsSpanId = recordStart(opts);
|
|
2470
3106
|
const start = monotonicNowMs();
|
|
2471
3107
|
try {
|
|
2472
|
-
const result = await real.step(opts,
|
|
3108
|
+
const result = await real.step(opts, (realSpanId) => {
|
|
3109
|
+
if (realSpanId && !ambiguousRealStepIds.has(realSpanId)) {
|
|
3110
|
+
if (teeIdByRealStepId.has(realSpanId)) {
|
|
3111
|
+
teeIdByRealStepId.delete(realSpanId);
|
|
3112
|
+
ambiguousRealStepIds.add(realSpanId);
|
|
3113
|
+
} else {
|
|
3114
|
+
teeIdByRealStepId.set(realSpanId, obsSpanId);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
return activeStep.run(obsSpanId, () => fn(realSpanId));
|
|
3118
|
+
});
|
|
2473
3119
|
complete(obsSpanId, {
|
|
2474
3120
|
latency_ms: round3(monotonicNowMs() - start),
|
|
2475
3121
|
output: opts.summarize?.(result),
|
|
2476
|
-
metadata: opts.metadata
|
|
3122
|
+
metadata: opts.metadata,
|
|
3123
|
+
...opts.usage?.(result)
|
|
2477
3124
|
});
|
|
2478
3125
|
return result;
|
|
2479
3126
|
} catch (error) {
|
|
@@ -2528,7 +3175,29 @@ function createProjectionTee(real) {
|
|
|
2528
3175
|
}
|
|
2529
3176
|
},
|
|
2530
3177
|
async traceTool(name, params, fn) {
|
|
2531
|
-
|
|
3178
|
+
const obsSpanId = recordStart({
|
|
3179
|
+
step_name: `tool ${name}`,
|
|
3180
|
+
observation_type: "TOOL",
|
|
3181
|
+
tool_name: name,
|
|
3182
|
+
input: params
|
|
3183
|
+
});
|
|
3184
|
+
const start = monotonicNowMs();
|
|
3185
|
+
try {
|
|
3186
|
+
const result = await real.traceTool(name, params, fn);
|
|
3187
|
+
complete(obsSpanId, {
|
|
3188
|
+
latency_ms: round3(monotonicNowMs() - start),
|
|
3189
|
+
output: result
|
|
3190
|
+
});
|
|
3191
|
+
return result;
|
|
3192
|
+
} catch (error) {
|
|
3193
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3194
|
+
complete(obsSpanId, {
|
|
3195
|
+
latency_ms: round3(monotonicNowMs() - start),
|
|
3196
|
+
status_message: message,
|
|
3197
|
+
level: "ERROR"
|
|
3198
|
+
});
|
|
3199
|
+
throw error;
|
|
3200
|
+
}
|
|
2532
3201
|
},
|
|
2533
3202
|
async traceRetriever(query, fn) {
|
|
2534
3203
|
return real.traceRetriever(query, fn);
|
|
@@ -2564,7 +3233,7 @@ function createProjectionTee(real) {
|
|
|
2564
3233
|
durationMs: round3(endMs - rootStartMs),
|
|
2565
3234
|
complete: true
|
|
2566
3235
|
},
|
|
2567
|
-
capabilities: deriveCapabilities(observations),
|
|
3236
|
+
capabilities: deriveCapabilities(observations, ambiguousRealStepIds.size > 0),
|
|
2568
3237
|
observations: [...observations]
|
|
2569
3238
|
};
|
|
2570
3239
|
}
|
|
@@ -2716,49 +3385,22 @@ function aggregateResult(checksResults) {
|
|
|
2716
3385
|
return { checks: checksResults, pass: false };
|
|
2717
3386
|
}
|
|
2718
3387
|
const pass = checksResults.every((r) => r.pass);
|
|
2719
|
-
return { checks: checksResults, pass };
|
|
3388
|
+
return isNoVerdict(checksResults) ? { checks: checksResults, pass, noVerdict: true } : { checks: checksResults, pass };
|
|
2720
3389
|
}
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
},
|
|
2736
|
-
traceTool(_name, _params, fn) {
|
|
2737
|
-
return noopFn(fn);
|
|
2738
|
-
},
|
|
2739
|
-
traceRetriever(_query, fn) {
|
|
2740
|
-
return noopFn(fn);
|
|
2741
|
-
},
|
|
2742
|
-
traceChain(_name, fn) {
|
|
2743
|
-
return noopFn(fn);
|
|
2744
|
-
},
|
|
2745
|
-
traceAgent(_name, fn) {
|
|
2746
|
-
return noopFn(fn);
|
|
2747
|
-
},
|
|
2748
|
-
traceGuardrail(_name, fn) {
|
|
2749
|
-
return noopFn(fn);
|
|
2750
|
-
},
|
|
2751
|
-
traceEmbedding(_model, _input, fn) {
|
|
2752
|
-
return noopFn(fn);
|
|
2753
|
-
},
|
|
2754
|
-
async score(_params) {
|
|
2755
|
-
},
|
|
2756
|
-
createSpan(_options) {
|
|
2757
|
-
return "agent-task-untraced-span";
|
|
2758
|
-
},
|
|
2759
|
-
endSpan(_spanId, _params) {
|
|
2760
|
-
}
|
|
2761
|
-
};
|
|
3390
|
+
function isNoVerdict(checksResults) {
|
|
3391
|
+
const failing = checksResults.filter((r) => !r.pass);
|
|
3392
|
+
return failing.length > 0 && failing.every((r) => checkOutcome(r) === "error");
|
|
3393
|
+
}
|
|
3394
|
+
function checkOutcome(check) {
|
|
3395
|
+
if (check.pass) return void 0;
|
|
3396
|
+
const assertions = check.assertions;
|
|
3397
|
+
if (assertions && assertions.length > 0) {
|
|
3398
|
+
const failed = assertions.filter((a) => !a.pass).map((a) => a.outcome);
|
|
3399
|
+
if (failed.length === 0) return void 0;
|
|
3400
|
+
if (!failed.every((o) => o === "error" || o === "unsupported")) return void 0;
|
|
3401
|
+
return failed.includes("error") ? "error" : "unsupported";
|
|
3402
|
+
}
|
|
3403
|
+
return check.outcome;
|
|
2762
3404
|
}
|
|
2763
3405
|
|
|
2764
3406
|
// src/agent-task/run/runTask.ts
|
|
@@ -2787,13 +3429,15 @@ async function runTask(taskDir, options) {
|
|
|
2787
3429
|
void 0
|
|
2788
3430
|
);
|
|
2789
3431
|
}
|
|
2790
|
-
|
|
3432
|
+
return trace2.client.traceRun(
|
|
2791
3433
|
buildTraceRunOptions(loaded, trace2),
|
|
2792
|
-
async (traceContext) =>
|
|
3434
|
+
async (traceContext) => {
|
|
3435
|
+
const phase1 = await captureExecution(loaded, options, traceContext);
|
|
3436
|
+
const canonical = await readCanonicalSnapshot(trace2);
|
|
3437
|
+
if (canonical) phase1.snapshot = canonical;
|
|
3438
|
+
return evaluate(loaded, options, phase1, traceContext);
|
|
3439
|
+
}
|
|
2793
3440
|
);
|
|
2794
|
-
const canonical = await readCanonicalSnapshot(trace2);
|
|
2795
|
-
if (canonical) phase1.snapshot = canonical;
|
|
2796
|
-
return evaluate(loaded, options, phase1);
|
|
2797
3441
|
}
|
|
2798
3442
|
async function readCanonicalSnapshot(trace2) {
|
|
2799
3443
|
if (!trace2.taskRunId) return null;
|
|
@@ -2991,6 +3635,7 @@ async function executeLoadedTask(loaded, options, rawTrace, traceRunId) {
|
|
|
2991
3635
|
files,
|
|
2992
3636
|
task: task2,
|
|
2993
3637
|
...judgeConfig ? { judgeConfig } : {},
|
|
3638
|
+
judgeTracer: trace2,
|
|
2994
3639
|
...historyPlane ? { historyPlane } : {}
|
|
2995
3640
|
},
|
|
2996
3641
|
validationResults.brokenDeliverables
|
|
@@ -3005,6 +3650,7 @@ async function executeLoadedTask(loaded, options, rawTrace, traceRunId) {
|
|
|
3005
3650
|
files,
|
|
3006
3651
|
task: task2,
|
|
3007
3652
|
...judgeConfig ? { judgeConfig } : {},
|
|
3653
|
+
judgeTracer: trace2,
|
|
3008
3654
|
...historyPlane ? { historyPlane } : {},
|
|
3009
3655
|
moduleUrl,
|
|
3010
3656
|
displayFile: evalFileName
|
|
@@ -3033,7 +3679,7 @@ async function captureExecution(loaded, options, rawTrace) {
|
|
|
3033
3679
|
snapshot: outcome.getSnapshot()
|
|
3034
3680
|
}));
|
|
3035
3681
|
}
|
|
3036
|
-
async function evaluate(loaded, options, phase1) {
|
|
3682
|
+
async function evaluate(loaded, options, phase1, judgeTracer) {
|
|
3037
3683
|
const {
|
|
3038
3684
|
task: task2,
|
|
3039
3685
|
adapter,
|
|
@@ -3051,12 +3697,13 @@ async function evaluate(loaded, options, phase1) {
|
|
|
3051
3697
|
);
|
|
3052
3698
|
const judgeConfig = resolveJudgeConfig(options?.judge, task2.judge);
|
|
3053
3699
|
const historyPlane = await freezeHistoryPlaneFromEnv(task2.id);
|
|
3054
|
-
const
|
|
3700
|
+
const runChecks = async () => inlineChecks ? runTraceChecks({
|
|
3055
3701
|
snapshot: phase1.snapshot,
|
|
3056
3702
|
deliverables,
|
|
3057
3703
|
files,
|
|
3058
3704
|
task: task2,
|
|
3059
3705
|
...judgeConfig ? { judgeConfig } : {},
|
|
3706
|
+
...judgeTracer ? { judgeTracer } : {},
|
|
3060
3707
|
...historyPlane ? { historyPlane } : {},
|
|
3061
3708
|
moduleUrl,
|
|
3062
3709
|
displayFile: evalFileName
|
|
@@ -3068,10 +3715,22 @@ async function evaluate(loaded, options, phase1) {
|
|
|
3068
3715
|
files,
|
|
3069
3716
|
task: task2,
|
|
3070
3717
|
...judgeConfig ? { judgeConfig } : {},
|
|
3718
|
+
...judgeTracer ? { judgeTracer } : {},
|
|
3071
3719
|
...historyPlane ? { historyPlane } : {}
|
|
3072
3720
|
},
|
|
3073
3721
|
validationResults.brokenDeliverables
|
|
3074
|
-
)
|
|
3722
|
+
);
|
|
3723
|
+
const hasChecksModule = inlineChecks || checksPath !== null;
|
|
3724
|
+
const checksResults = await (judgeTracer && hasChecksModule ? judgeTracer.step(
|
|
3725
|
+
{
|
|
3726
|
+
step_name: "checks.run",
|
|
3727
|
+
observation_type: "CHAIN",
|
|
3728
|
+
input: { sourceFile: inlineChecks ? evalFileName : checksPath },
|
|
3729
|
+
metadata: { sourceFile: inlineChecks ? evalFileName : checksPath },
|
|
3730
|
+
summarize: (result2) => summarizeEvaluationResults(result2)
|
|
3731
|
+
},
|
|
3732
|
+
runChecks
|
|
3733
|
+
) : runChecks());
|
|
3075
3734
|
const result = aggregateResult(checksResults);
|
|
3076
3735
|
return {
|
|
3077
3736
|
task: task2,
|
|
@@ -3109,11 +3768,18 @@ async function runTurnLoop(loaded, options, session, trace2, rawTrace) {
|
|
|
3109
3768
|
metadata: { turnNumber: turnNum },
|
|
3110
3769
|
summarize: (r) => ({ response: r.response })
|
|
3111
3770
|
},
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3771
|
+
// Re-scope the run context to this turn so GenAI spans the OTel
|
|
3772
|
+
// processor captures parent under the turn span, not the run root —
|
|
3773
|
+
// turn-scoped assertions (t.maxTokens({ turn })) walk that chain.
|
|
3774
|
+
async (spanId) => {
|
|
3775
|
+
const run = getActiveApoRun();
|
|
3776
|
+
const send = () => session.sendUserTurn(userTurn, {
|
|
3777
|
+
trace: trace2,
|
|
3778
|
+
turnNumber: turnNum,
|
|
3779
|
+
parentSpanId: spanId
|
|
3780
|
+
});
|
|
3781
|
+
return run ? withApoRun({ ...run, parentSpanId: spanId, turnNumber: turnNum }, send) : send();
|
|
3782
|
+
}
|
|
3117
3783
|
);
|
|
3118
3784
|
turnTranscript.push({
|
|
3119
3785
|
turnNumber: turnNum,
|
|
@@ -3169,17 +3835,29 @@ async function cleanupAdapter(loaded, trace2, state, session) {
|
|
|
3169
3835
|
}
|
|
3170
3836
|
function summarizeEvaluationResults(results) {
|
|
3171
3837
|
const passCount = results.filter((r) => r.pass).length;
|
|
3838
|
+
const noVerdictCount = results.filter(
|
|
3839
|
+
(r) => !r.pass && (r.outcome === "error" || r.outcome === "unsupported")
|
|
3840
|
+
).length;
|
|
3841
|
+
const failCount = results.length - passCount - noVerdictCount;
|
|
3842
|
+
const suffix = noVerdictCount > 0 ? ` (${noVerdictCount} no-verdict)` : "";
|
|
3172
3843
|
return {
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3844
|
+
text: `${passCount}/${results.length} checks passed${suffix}`,
|
|
3845
|
+
verdict: {
|
|
3846
|
+
total: results.length,
|
|
3847
|
+
passCount,
|
|
3848
|
+
failCount,
|
|
3849
|
+
noVerdictCount,
|
|
3850
|
+
results: results.map((r) => ({
|
|
3851
|
+
id: r.id,
|
|
3852
|
+
pass: r.pass,
|
|
3853
|
+
reasoning: r.reasoning?.slice(0, 2e3),
|
|
3854
|
+
evaluator_type: r.evaluator_type,
|
|
3855
|
+
judge_model: r.judge?.model,
|
|
3856
|
+
...r.outcome ? { outcome: r.outcome } : {},
|
|
3857
|
+
...r.group_id ? { group_id: r.group_id } : {},
|
|
3858
|
+
...r.group_name ? { group_name: r.group_name } : {}
|
|
3859
|
+
}))
|
|
3860
|
+
}
|
|
3183
3861
|
};
|
|
3184
3862
|
}
|
|
3185
3863
|
function buildTraceRunOptions(loaded, tracing) {
|
|
@@ -3229,12 +3907,20 @@ var RejudgeError = class extends Error {
|
|
|
3229
3907
|
};
|
|
3230
3908
|
var TERMINAL_VERDICT_STATUSES = /* @__PURE__ */ new Set(["passed", "failed"]);
|
|
3231
3909
|
var MAX_SAMPLES = 50;
|
|
3910
|
+
var NO_VERDICT_MESSAGE_PREFIX = "No verdict: ";
|
|
3911
|
+
function isJudgeNoVerdictRun(detail) {
|
|
3912
|
+
if (detail.status !== "error" || detail.pass_result != null) return false;
|
|
3913
|
+
if (detail.no_verdict_reason !== void 0) return detail.no_verdict_reason === "judge";
|
|
3914
|
+
const generations = detail.generation_execution;
|
|
3915
|
+
const generationsDominate = generations != null && generations.total > 0 && generations.errored * 2 > generations.total;
|
|
3916
|
+
return (detail.error_message ?? "").startsWith(NO_VERDICT_MESSAGE_PREFIX) && detail.failed_checks === 0 && (detail.errored_checks ?? 0) > 0 && !generationsDominate;
|
|
3917
|
+
}
|
|
3232
3918
|
async function rejudgeTaskRun(runId, endpoints, options = {}) {
|
|
3233
3919
|
const api = createApiClient(endpoints);
|
|
3234
3920
|
const progress = options.onProgress ?? (() => {
|
|
3235
3921
|
});
|
|
3236
3922
|
const detail = await api.get(`/v1/agent-task-runs/${runId}`);
|
|
3237
|
-
if (!TERMINAL_VERDICT_STATUSES.has(detail.status)) {
|
|
3923
|
+
if (!TERMINAL_VERDICT_STATUSES.has(detail.status) && !isJudgeNoVerdictRun(detail)) {
|
|
3238
3924
|
throw new RejudgeError(
|
|
3239
3925
|
`Run ${runId} is '${detail.status}', not a completed run with a verdict \u2014 there is nothing to replay against yet`
|
|
3240
3926
|
);
|
|
@@ -3306,22 +3992,14 @@ async function rejudgeTaskRun(runId, endpoints, options = {}) {
|
|
|
3306
3992
|
loaded.adapter.deliverables
|
|
3307
3993
|
);
|
|
3308
3994
|
const proxied = proxyBrokenDeliverables(deliverables, validation.brokenDeliverables);
|
|
3309
|
-
const
|
|
3310
|
-
let primary = [];
|
|
3995
|
+
const sampled = [];
|
|
3311
3996
|
const judgeConfig = resolveJudgeConfig(options.judge, loaded.task.judge);
|
|
3312
3997
|
for (let sample = 0; sample < samples; sample++) {
|
|
3313
3998
|
if (samples > 1) progress(`judging sample ${sample + 1}/${samples}`);
|
|
3314
|
-
|
|
3315
|
-
if (sample === 0) primary = results;
|
|
3316
|
-
for (const result of results) {
|
|
3317
|
-
passCounts.set(result.id, (passCounts.get(result.id) ?? 0) + (result.pass ? 1 : 0));
|
|
3318
|
-
}
|
|
3999
|
+
sampled.push(await runChecksOnce(loaded, proxied, snapshot, judgeConfig));
|
|
3319
4000
|
}
|
|
3320
|
-
const
|
|
3321
|
-
|
|
3322
|
-
passes: passCounts.get(result.id) ?? 0,
|
|
3323
|
-
samples
|
|
3324
|
-
}));
|
|
4001
|
+
const primary = sampled[0] ?? [];
|
|
4002
|
+
const stability = checkStability(sampled);
|
|
3325
4003
|
const aggregate = aggregateResult(primary);
|
|
3326
4004
|
return {
|
|
3327
4005
|
runId,
|
|
@@ -3335,6 +4013,7 @@ async function rejudgeTaskRun(runId, endpoints, options = {}) {
|
|
|
3335
4013
|
samples,
|
|
3336
4014
|
checks: aggregate.checks,
|
|
3337
4015
|
pass: aggregate.pass,
|
|
4016
|
+
...aggregate.noVerdict ? { noVerdict: true } : {},
|
|
3338
4017
|
stability,
|
|
3339
4018
|
traceSnapshotAvailable: snapshotResult.available,
|
|
3340
4019
|
taskDirUsed: taskDir
|
|
@@ -3344,6 +4023,24 @@ async function rejudgeTaskRun(runId, endpoints, options = {}) {
|
|
|
3344
4023
|
if (scaffoldDir) rmSync(scaffoldDir, { recursive: true, force: true });
|
|
3345
4024
|
}
|
|
3346
4025
|
}
|
|
4026
|
+
function checkStability(sampled) {
|
|
4027
|
+
const passCounts = /* @__PURE__ */ new Map();
|
|
4028
|
+
const errorCounts = /* @__PURE__ */ new Map();
|
|
4029
|
+
for (const results of sampled) {
|
|
4030
|
+
for (const result of results) {
|
|
4031
|
+
passCounts.set(result.id, (passCounts.get(result.id) ?? 0) + (result.pass ? 1 : 0));
|
|
4032
|
+
if (checkOutcome(result) === "error") {
|
|
4033
|
+
errorCounts.set(result.id, (errorCounts.get(result.id) ?? 0) + 1);
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
return (sampled[0] ?? []).map((result) => ({
|
|
4038
|
+
check_id: result.id,
|
|
4039
|
+
passes: passCounts.get(result.id) ?? 0,
|
|
4040
|
+
samples: sampled.length,
|
|
4041
|
+
...errorCounts.get(result.id) ? { errored: errorCounts.get(result.id) } : {}
|
|
4042
|
+
}));
|
|
4043
|
+
}
|
|
3347
4044
|
async function runChecksOnce(loaded, deliverables, snapshot, judgeConfig) {
|
|
3348
4045
|
if (loaded.inlineChecks) {
|
|
3349
4046
|
return runTraceChecks({
|
|
@@ -3416,7 +4113,8 @@ function degenerateSnapshot(runId) {
|
|
|
3416
4113
|
errors: "unavailable",
|
|
3417
4114
|
timing: "unavailable",
|
|
3418
4115
|
skills: "unavailable",
|
|
3419
|
-
subagents: "unavailable"
|
|
4116
|
+
subagents: "unavailable",
|
|
4117
|
+
usage: "unavailable"
|
|
3420
4118
|
},
|
|
3421
4119
|
observations: []
|
|
3422
4120
|
};
|
|
@@ -3544,6 +4242,7 @@ function createOtelAgentTaskTraceClient(config) {
|
|
|
3544
4242
|
activeSpans.set(spanId, {
|
|
3545
4243
|
span,
|
|
3546
4244
|
startedAt: monotonicNowMs2(),
|
|
4245
|
+
startWallMs: Date.now(),
|
|
3547
4246
|
model: params.model || "unknown",
|
|
3548
4247
|
observationType: params.observation_type
|
|
3549
4248
|
});
|
|
@@ -3561,6 +4260,13 @@ function createOtelAgentTaskTraceClient(config) {
|
|
|
3561
4260
|
const payload = params.output && typeof params.output === "object" ? params.output : { value: params.output };
|
|
3562
4261
|
active.span.setAttribute("gen_ai.tool.call.result", JSON.stringify(payload));
|
|
3563
4262
|
} else if (params.output.text) {
|
|
4263
|
+
active.span.setAttribute("gen_ai.response.text", String(params.output.text));
|
|
4264
|
+
if (params.output.verdict !== void 0) {
|
|
4265
|
+
active.span.setAttribute(
|
|
4266
|
+
"gen_ai.tool.call.result",
|
|
4267
|
+
JSON.stringify(params.output.verdict)
|
|
4268
|
+
);
|
|
4269
|
+
}
|
|
3564
4270
|
active.span.setAttribute("gen_ai.output.messages", JSON.stringify([{
|
|
3565
4271
|
role: "assistant",
|
|
3566
4272
|
parts: [{ type: "text", content: String(params.output.text) }]
|
|
@@ -3585,6 +4291,12 @@ function createOtelAgentTaskTraceClient(config) {
|
|
|
3585
4291
|
if (params.completion_tokens !== void 0) {
|
|
3586
4292
|
active.span.setAttribute("gen_ai.usage.output_tokens", params.completion_tokens);
|
|
3587
4293
|
}
|
|
4294
|
+
if (params.reasoning_tokens !== void 0) {
|
|
4295
|
+
active.span.setAttribute(
|
|
4296
|
+
"gen_ai.usage.reasoning.output_tokens",
|
|
4297
|
+
params.reasoning_tokens
|
|
4298
|
+
);
|
|
4299
|
+
}
|
|
3588
4300
|
if (params.status_message) {
|
|
3589
4301
|
active.span.setAttribute("apo.status_message", params.status_message);
|
|
3590
4302
|
}
|
|
@@ -3594,7 +4306,7 @@ function createOtelAgentTaskTraceClient(config) {
|
|
|
3594
4306
|
if (params.metadata) {
|
|
3595
4307
|
active.span.setAttribute("apo.metadata", JSON.stringify(params.metadata));
|
|
3596
4308
|
}
|
|
3597
|
-
active.span.end();
|
|
4309
|
+
active.span.end(new Date(active.startWallMs + latency));
|
|
3598
4310
|
activeSpans.delete(params.id);
|
|
3599
4311
|
}
|
|
3600
4312
|
return {
|
|
@@ -3628,6 +4340,7 @@ function createOtelAgentTaskTraceClient(config) {
|
|
|
3628
4340
|
activeSpans.set(rootSpanId, {
|
|
3629
4341
|
span: rootSpan,
|
|
3630
4342
|
startedAt: monotonicNowMs2(),
|
|
4343
|
+
startWallMs: Date.now(),
|
|
3631
4344
|
model: "trace"
|
|
3632
4345
|
});
|
|
3633
4346
|
let callCount = 1;
|
|
@@ -3653,7 +4366,8 @@ function createOtelAgentTaskTraceClient(config) {
|
|
|
3653
4366
|
id: spanId,
|
|
3654
4367
|
latency_ms: round32(monotonicNowMs2() - startedAt),
|
|
3655
4368
|
output: options.summarize?.(result),
|
|
3656
|
-
metadata: options.metadata
|
|
4369
|
+
metadata: options.metadata,
|
|
4370
|
+
...options.usage?.(result)
|
|
3657
4371
|
});
|
|
3658
4372
|
callCount += 1;
|
|
3659
4373
|
return result;
|
|
@@ -3932,6 +4646,7 @@ async function runTaskDir(taskDir) {
|
|
|
3932
4646
|
taskDir: loaded.taskDir,
|
|
3933
4647
|
taskId: loaded.task.id,
|
|
3934
4648
|
pass: result.result.pass,
|
|
4649
|
+
...result.result.noVerdict ? { noVerdict: true } : {},
|
|
3935
4650
|
checks: result.result.checks,
|
|
3936
4651
|
adapterName: loaded.adapter.name,
|
|
3937
4652
|
traceRunId: result.traceRunId,
|
|
@@ -4008,7 +4723,8 @@ async function runAgentTaskCli(argv, cwd = process.cwd()) {
|
|
|
4008
4723
|
if (result.status === "fulfilled") {
|
|
4009
4724
|
summaries.push(result.value);
|
|
4010
4725
|
printTaskSummary(result.value, cwd);
|
|
4011
|
-
|
|
4726
|
+
if (result.value.noVerdict) hasErrors = true;
|
|
4727
|
+
else hasFailures ||= !result.value.pass;
|
|
4012
4728
|
} else {
|
|
4013
4729
|
hasErrors = true;
|
|
4014
4730
|
printTaskError(taskDir, cwd, result.reason);
|
|
@@ -4056,7 +4772,8 @@ function printHelp() {
|
|
|
4056
4772
|
function printTaskSummary(summary, cwd) {
|
|
4057
4773
|
const relativeTaskDir = relative(cwd, summary.taskDir);
|
|
4058
4774
|
console.log("");
|
|
4059
|
-
|
|
4775
|
+
const verdict = summary.noVerdict ? "NO VERDICT" : summary.pass ? "PASS" : "FAIL";
|
|
4776
|
+
console.log(`${verdict} ${summary.taskId}`);
|
|
4060
4777
|
console.log(`Path: ${relativeTaskDir}`);
|
|
4061
4778
|
printResultGroup("Checks", summary.checks);
|
|
4062
4779
|
if (summary.checks.length === 0 && !summary.pass) {
|
|
@@ -4083,12 +4800,14 @@ function printTaskError(taskDir, cwd, error) {
|
|
|
4083
4800
|
}
|
|
4084
4801
|
function printOverallSummary(summaries, hasErrors) {
|
|
4085
4802
|
const passed = summaries.filter((summary) => summary.pass).length;
|
|
4086
|
-
const
|
|
4803
|
+
const noVerdict = summaries.filter((summary) => summary.noVerdict).length;
|
|
4804
|
+
const failed = summaries.length - passed - noVerdict;
|
|
4087
4805
|
console.log("");
|
|
4088
4806
|
console.log("Summary");
|
|
4089
4807
|
console.log(`- total: ${summaries.length}`);
|
|
4090
4808
|
console.log(`- passed: ${passed}`);
|
|
4091
4809
|
console.log(`- failed: ${failed}`);
|
|
4810
|
+
if (noVerdict > 0) console.log(`- no verdict: ${noVerdict}`);
|
|
4092
4811
|
console.log(`- errors: ${hasErrors ? "yes" : "no"}`);
|
|
4093
4812
|
}
|
|
4094
4813
|
|