@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.
@@ -8,20 +8,21 @@ import {
8
8
  resetApoTracing,
9
9
  withApoRun,
10
10
  withApoRunSync
11
- } from "../chunk-GETY34VI.js";
11
+ } from "../chunk-KLZLE3VQ.js";
12
12
  import {
13
13
  createApoTracer
14
- } from "../chunk-QHRWPVPZ.js";
15
- import "../chunk-HWZVL6TS.js";
14
+ } from "../chunk-IK4GJX7H.js";
15
+ import "../chunk-HRYXLRXJ.js";
16
16
  import {
17
17
  createApoOpenAI
18
- } from "../chunk-6ZRW5IW6.js";
18
+ } from "../chunk-3B3L2H4Y.js";
19
19
  import {
20
20
  createApoAnthropic
21
- } from "../chunk-SZDSIPXC.js";
22
- import "../chunk-GDB6SDGN.js";
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.snapshot.capabilities[capability] === "available";
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 = { model: args.model };
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 = 12e4;
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: `${systemPromptText}
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
- return runWithSharedPrefix(cacheKey, async () => {
426
- const startedAt = Date.now();
427
- const response = await fetch(`${baseURL}/chat/completions`, {
428
- method: "POST",
429
- signal: AbortSignal.timeout(JUDGE_TIMEOUT_MS),
430
- headers: {
431
- "Content-Type": "application/json",
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
- role: "system",
439
- content: [
440
- { type: "text", text: briefingText },
441
- {
442
- type: "text",
443
- text: deliverableText,
444
- cache_control: { type: "ephemeral" }
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
- { role: "user", content: instructionText }
449
- ],
450
- temperature: 0,
451
- response_format: { type: "json_object" }
452
- })
453
- });
454
- if (!response.ok) {
455
- const body = await response.text().catch(() => "");
456
- throw new Error(`Judge API ${response.status}: ${body.slice(0, 200)}`);
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
- const data = await response.json();
459
- const text = data.choices?.[0]?.message?.content ?? "";
460
- const outputTokens = data.usage?.completion_tokens;
461
- const isEmpty = !text.trim();
462
- const reportedZeroTokens = data.usage !== void 0 && outputTokens === 0;
463
- if (isEmpty || reportedZeroTokens) {
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 treated as a failure.",
467
- judge: {
468
- model: args.model,
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
- const guard = budgetGuard();
614
- if (guard) return { error: guard };
615
- if (!Object.prototype.hasOwnProperty.call(evidence.deliverables, name)) {
616
- return { error: `unknown deliverable: ${name}` };
617
- }
618
- const content = renderValue(evidence.deliverables[name]);
619
- const slice = content.slice(offset, offset + limit);
620
- const overflow = accountRead(
621
- "read_deliverable",
622
- JSON.stringify({ name, offset, limit }),
623
- slice,
624
- () => `read budget exhausted (${budget.maxReadBytes} bytes); call finish_verdict now`
625
- );
626
- if (overflow) return { error: overflow };
627
- return { name, total_bytes: content.length, offset, returned: slice.length, content: slice };
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
- const guard = budgetGuard();
636
- if (guard) return { error: guard };
637
- if (!Object.prototype.hasOwnProperty.call(evidence.deliverables, name)) {
638
- return { error: `unknown deliverable: ${name}` };
639
- }
640
- if (pattern.length > SEARCH_MAX_PATTERN) {
641
- return { error: `pattern too long (max ${SEARCH_MAX_PATTERN} chars)` };
642
- }
643
- let re;
644
- try {
645
- re = new RegExp(pattern, "gi");
646
- } catch (error) {
647
- return { error: `invalid regex: ${error instanceof Error ? error.message : String(error)}` };
648
- }
649
- const content = renderValue(evidence.deliverables[name]).slice(0, SEARCH_SCAN_LIMIT);
650
- const hits = [];
651
- let m;
652
- let execs = 0;
653
- while ((m = re.exec(content)) && hits.length < SEARCH_HITS) {
654
- if (++execs > SEARCH_MAX_EXEC) break;
655
- const start = Math.max(0, m.index - SEARCH_CONTEXT);
656
- hits.push({ at_byte: m.index, context: content.slice(start, m.index + m[0].length + SEARCH_CONTEXT) });
657
- if (m.index === re.lastIndex) re.lastIndex++;
658
- }
659
- const served = JSON.stringify({ total_bytes: content.length, match_count: hits.length, matches: hits });
660
- const overflow = accountRead(
661
- "search_deliverable",
662
- JSON.stringify({ name, pattern }),
663
- served,
664
- () => `read budget exhausted (${budget.maxReadBytes} bytes); call finish_verdict now`
665
- );
666
- if (overflow) return { error: overflow };
667
- return { total_bytes: content.length, match_count: hits.length, matches: hits };
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
- const guard = budgetGuard();
676
- if (guard) return { error: guard };
677
- return evidence.history.runs;
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
- const guard = budgetGuard();
686
- if (guard) return { error: guard };
687
- const detail = await evidence.history.getRun(run_id);
688
- const served = JSON.stringify(detail);
689
- const overflow = accountRead(
690
- "get_run",
691
- JSON.stringify({ run_id }),
692
- served,
693
- () => `read budget exhausted (${budget.maxReadBytes} bytes); call finish_verdict now`
694
- );
695
- if (overflow) return { error: overflow };
696
- return detail;
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
- const guard = budgetGuard();
705
- if (guard) return { error: guard };
706
- return {
707
- task_id: scope?.taskId,
708
- description: scope?.taskDescription ?? "(unavailable)",
709
- deliverables: Object.keys(evidence.deliverables)
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
- const guard = budgetGuard();
721
- if (guard) return { error: guard };
722
- const view = evidence.view;
723
- if (!view) {
724
- return {
725
- status: "unsupported",
726
- detail: "no trace projection exists for this run \u2014 judge on deliverables instead"
727
- };
728
- }
729
- if (view.requireCapability("tools") !== "available") {
730
- return { status: "unsupported", detail: "tool-call evidence is unavailable in this trace projection" };
731
- }
732
- 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) }));
733
- const reply = view.requireCapability("messages") === "available" && view.reply ? view.reply.slice(0, 1500) : void 0;
734
- return { turns: view.turnCount, tool_calls: calls, ...reply !== void 0 ? { final_reply: reply } : {} };
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
- const result = await runAgentSession({
939
- instruction,
940
- model: effective.model,
941
- baseURL: effective.baseURL,
942
- apiKey: effective.apiKey,
943
- evidence: evidence ?? { deliverables: {} },
944
- scope: judgeScope,
945
- exhibits,
946
- tools: opts?.tools,
947
- budget: opts?.budget
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 createJudgeMethod(rec, judgeConfig, judgeScope) {
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
- { evaluator_type: "llm", location }
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
- try {
1036
- const { pass, reasoning, judge } = await callJudge({
1037
- values: valueArray,
1038
- instruction,
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
- baseURL: effective.baseURL,
1041
- apiKey: effective.apiKey,
1042
- prompt: effective.prompt,
1043
- ...context2 ? { context: context2 } : {}
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: opts.parent_call_id ?? real.rootSpanId,
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, fn);
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
- return real.traceTool(name, params, fn);
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
- // src/agent-task/tracing.ts
2723
- function createNoopAgentTaskTraceContext() {
2724
- const noopFn = async (fn) => fn();
2725
- return {
2726
- runId: "agent-task-untraced",
2727
- rootSpanId: "agent-task-untraced-root",
2728
- async step(options, fn) {
2729
- return fn(options.id ?? "agent-task-untraced-step");
2730
- },
2731
- recordEvent(options) {
2732
- return options.id ?? "agent-task-untraced-event";
2733
- },
2734
- endRoot() {
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
- const phase1 = await trace2.client.traceRun(
3432
+ return trace2.client.traceRun(
2791
3433
  buildTraceRunOptions(loaded, trace2),
2792
- async (traceContext) => captureExecution(loaded, options, 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 checksResults = await (inlineChecks ? runTraceChecks({
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
- async (spanId) => session.sendUserTurn(userTurn, {
3113
- trace: trace2,
3114
- turnNumber: turnNum,
3115
- parentSpanId: spanId
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
- total: results.length,
3174
- passCount,
3175
- failCount: results.length - passCount,
3176
- results: results.map((r) => ({
3177
- id: r.id,
3178
- pass: r.pass,
3179
- reasoning: r.reasoning,
3180
- evaluator_type: r.evaluator_type,
3181
- judge_model: r.judge?.model
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 passCounts = /* @__PURE__ */ new Map();
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
- const results = await runChecksOnce(loaded, proxied, snapshot, judgeConfig);
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 stability = primary.map((result) => ({
3321
- check_id: result.id,
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
- hasFailures ||= !result.value.pass;
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
- console.log(`${summary.pass ? "PASS" : "FAIL"} ${summary.taskId}`);
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 failed = summaries.length - passed;
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