@apo-ai/sdk 0.8.0 → 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,11 +8,11 @@ import {
8
8
  resetApoTracing,
9
9
  withApoRun,
10
10
  withApoRunSync
11
- } from "../chunk-IPQ6IIEG.js";
11
+ } from "../chunk-KLZLE3VQ.js";
12
12
  import {
13
13
  createApoTracer
14
- } from "../chunk-CKB3SUDO.js";
15
- import "../chunk-HLRXQAZ5.js";
14
+ } from "../chunk-IK4GJX7H.js";
15
+ import "../chunk-HRYXLRXJ.js";
16
16
  import {
17
17
  createApoOpenAI
18
18
  } from "../chunk-3B3L2H4Y.js";
@@ -22,6 +22,7 @@ import {
22
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
  }
@@ -504,13 +665,20 @@ ${formatJudgeValues(args.values)}`;
504
665
 
505
666
  ${deliverableText}`;
506
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)}`;
507
672
  const secondJudgePromise = secondJudgeModel ? callSecondJudge({
508
- state: `${systemPromptText}
673
+ state: `${briefingText}
674
+
675
+ ${secondJudgeDeliverableText}
509
676
 
510
677
  ${instructionText}`,
511
678
  model: secondJudgeModel,
512
679
  baseURL: resolveSecondJudgeBaseURL(baseURL),
513
- apiKey: resolveSecondJudgeAPIKey(apiKey)
680
+ apiKey: resolveSecondJudgeAPIKey(apiKey),
681
+ ...secondJudgeProjected ? { projected: true } : {}
514
682
  }) : void 0;
515
683
  const cacheKey = `${args.model}\0${briefingText}\0${deliverableText}`;
516
684
  const requestBody = (stream) => JSON.stringify({
@@ -1163,7 +1331,7 @@ function createAgentMethod(rec, judgeConfig, judgeScope, evidence, judgeTracer)
1163
1331
  label,
1164
1332
  false,
1165
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.",
1166
- { evaluator_type: "agent", location }
1334
+ { evaluator_type: "agent", location, outcome: "error" }
1167
1335
  );
1168
1336
  return;
1169
1337
  }
@@ -1271,6 +1439,8 @@ var TEST_METHOD_NAMES = [
1271
1439
  "messageIncludes",
1272
1440
  "maxTurns",
1273
1441
  "maxDurationMs",
1442
+ "maxTokens",
1443
+ "minTokens",
1274
1444
  "assert",
1275
1445
  "check",
1276
1446
  "judge",
@@ -1304,11 +1474,13 @@ function createJudgeMethod(rec, judgeConfig, judgeScope, judgeTracer) {
1304
1474
  label,
1305
1475
  false,
1306
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.",
1307
- { evaluator_type: "llm", location }
1477
+ // No judge means no verdict — the same bucket as an unreachable judge.
1478
+ { evaluator_type: "llm", location, outcome: "error" }
1308
1479
  );
1309
1480
  return;
1310
1481
  }
1311
1482
  const context2 = judgeScopeToContext(judgeScope);
1483
+ const secondJudgeValue = opts?.secondJudgeValue;
1312
1484
  const call = () => callJudge({
1313
1485
  values: valueArray,
1314
1486
  instruction,
@@ -1316,7 +1488,10 @@ function createJudgeMethod(rec, judgeConfig, judgeScope, judgeTracer) {
1316
1488
  baseURL: effective.baseURL,
1317
1489
  apiKey: effective.apiKey,
1318
1490
  prompt: effective.prompt,
1319
- ...context2 ? { context: context2 } : {}
1491
+ ...context2 ? { context: context2 } : {},
1492
+ ...secondJudgeValue !== void 0 ? {
1493
+ secondJudgeValue: Array.isArray(secondJudgeValue) ? secondJudgeValue : [secondJudgeValue]
1494
+ } : {}
1320
1495
  });
1321
1496
  let judgeSpanId;
1322
1497
  const traced = judgeTracer ? judgeTracer.step(
@@ -1395,7 +1570,8 @@ var CAPABILITY_LABELS = {
1395
1570
  errors: "error",
1396
1571
  timing: "timing",
1397
1572
  skills: "skill",
1398
- subagents: "subagent"
1573
+ subagents: "subagent",
1574
+ usage: "token usage"
1399
1575
  };
1400
1576
  function createTraceTestContext(view, rec, judgeConfig, judgeScope, agentEvidence, judgeTracer) {
1401
1577
  const unsupported = (id, capability) => {
@@ -1550,7 +1726,11 @@ function createTraceTestContext(view, rec, judgeConfig, judgeScope, agentEvidenc
1550
1726
  received: `${turns}`
1551
1727
  });
1552
1728
  },
1553
- maxDurationMs(n) {
1729
+ maxDurationMs(n, opts) {
1730
+ if (opts?.turn !== void 0) {
1731
+ recordTurnDuration(view, rec, n, opts.turn, unsupported);
1732
+ return;
1733
+ }
1554
1734
  if (!isAvailable("timing")) {
1555
1735
  unsupported(`maxDurationMs(${n})`, "timing");
1556
1736
  return;
@@ -1565,6 +1745,12 @@ function createTraceTestContext(view, rec, judgeConfig, judgeScope, agentEvidenc
1565
1745
  received: `${ms}ms`
1566
1746
  });
1567
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
+ },
1568
1754
  assert(label, predicate) {
1569
1755
  const pass = predicate(view);
1570
1756
  rec.record(label, pass, pass ? "" : `assertion "${label}" failed`, {
@@ -1587,6 +1773,95 @@ function createTraceTestContext(view, rec, judgeConfig, judgeScope, agentEvidenc
1587
1773
  agent: trackPending(rec, createAgentMethod(rec, judgeConfig, judgeScope, agentEvidence ?? { deliverables: {}, view }, judgeTracer))
1588
1774
  };
1589
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
+ }
1590
1865
  function matchesTraceToolCall(call, name, opts) {
1591
1866
  if (!matchName(call.name, name)) return false;
1592
1867
  if (opts?.input !== void 0 && !matchValue(call.input, opts.input)) {
@@ -2688,6 +2963,7 @@ function resolveDeliverableSchema(definition) {
2688
2963
  }
2689
2964
 
2690
2965
  // src/agent-task/trace-projection/projection-tee.ts
2966
+ import { AsyncLocalStorage } from "async_hooks";
2691
2967
  function monotonicNowMs() {
2692
2968
  if (typeof performance !== "undefined" && typeof performance.now === "function") {
2693
2969
  return performance.now();
@@ -2727,7 +3003,7 @@ function extractText(output) {
2727
3003
  }
2728
3004
  return "";
2729
3005
  }
2730
- function deriveCapabilities(observations) {
3006
+ function deriveCapabilities(observations, hierarchyAmbiguous) {
2731
3007
  const hasType = (t) => observations.some((o) => o.type === t);
2732
3008
  const hasMessages = observations.some(
2733
3009
  (o) => o.type === "GENERATION" && o.messages && o.messages.length > 0
@@ -2740,13 +3016,29 @@ function deriveCapabilities(observations) {
2740
3016
  errors: "available",
2741
3017
  timing: "available",
2742
3018
  skills: hasType("SKILL") ? "available" : "unavailable",
2743
- 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 } : {}
2744
3033
  };
2745
3034
  }
2746
3035
  function createProjectionTee(real) {
2747
3036
  const observations = [];
2748
3037
  const pending = /* @__PURE__ */ new Map();
2749
3038
  const realIdByTeeId = /* @__PURE__ */ new Map();
3039
+ const teeIdByRealStepId = /* @__PURE__ */ new Map();
3040
+ const ambiguousRealStepIds = /* @__PURE__ */ new Set();
3041
+ const activeStep = new AsyncLocalStorage();
2750
3042
  let spanCounter = 0;
2751
3043
  const rootStartMs = monotonicNowMs();
2752
3044
  const rootStartIso = new Date(rootStartMs).toISOString();
@@ -2768,6 +3060,8 @@ function createProjectionTee(real) {
2768
3060
  };
2769
3061
  if (p.parentSpanId) obs.parentSpanId = p.parentSpanId;
2770
3062
  if (p.model) obs.model = p.model;
3063
+ const usage = usageFrom(params);
3064
+ if (usage) obs.usage = usage;
2771
3065
  if (isError && params?.status_message) obs.errorMessage = params.status_message;
2772
3066
  const meta = params?.metadata;
2773
3067
  if (p.type === "TOOL") {
@@ -2790,9 +3084,10 @@ function createProjectionTee(real) {
2790
3084
  function recordStart(opts) {
2791
3085
  spanCounter += 1;
2792
3086
  const spanId = `${real.rootSpanId}-${String(spanCounter).padStart(6, "0")}`;
3087
+ const parent = opts.parent_call_id ?? activeStep.getStore() ?? real.rootSpanId;
2793
3088
  pending.set(spanId, {
2794
3089
  spanId,
2795
- parentSpanId: opts.parent_call_id ?? real.rootSpanId,
3090
+ parentSpanId: teeIdByRealStepId.get(parent) ?? parent,
2796
3091
  type: observationTypeFor(opts.observation_type),
2797
3092
  name: opts.step_name ?? "step",
2798
3093
  toolName: opts.tool_name ?? void 0,
@@ -2810,11 +3105,22 @@ function createProjectionTee(real) {
2810
3105
  const obsSpanId = recordStart(opts);
2811
3106
  const start = monotonicNowMs();
2812
3107
  try {
2813
- 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
+ });
2814
3119
  complete(obsSpanId, {
2815
3120
  latency_ms: round3(monotonicNowMs() - start),
2816
3121
  output: opts.summarize?.(result),
2817
- metadata: opts.metadata
3122
+ metadata: opts.metadata,
3123
+ ...opts.usage?.(result)
2818
3124
  });
2819
3125
  return result;
2820
3126
  } catch (error) {
@@ -2927,7 +3233,7 @@ function createProjectionTee(real) {
2927
3233
  durationMs: round3(endMs - rootStartMs),
2928
3234
  complete: true
2929
3235
  },
2930
- capabilities: deriveCapabilities(observations),
3236
+ capabilities: deriveCapabilities(observations, ambiguousRealStepIds.size > 0),
2931
3237
  observations: [...observations]
2932
3238
  };
2933
3239
  }
@@ -3079,7 +3385,22 @@ function aggregateResult(checksResults) {
3079
3385
  return { checks: checksResults, pass: false };
3080
3386
  }
3081
3387
  const pass = checksResults.every((r) => r.pass);
3082
- return { checks: checksResults, pass };
3388
+ return isNoVerdict(checksResults) ? { checks: checksResults, pass, noVerdict: true } : { checks: checksResults, pass };
3389
+ }
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;
3083
3404
  }
3084
3405
 
3085
3406
  // src/agent-task/run/runTask.ts
@@ -3447,11 +3768,18 @@ async function runTurnLoop(loaded, options, session, trace2, rawTrace) {
3447
3768
  metadata: { turnNumber: turnNum },
3448
3769
  summarize: (r) => ({ response: r.response })
3449
3770
  },
3450
- async (spanId) => session.sendUserTurn(userTurn, {
3451
- trace: trace2,
3452
- turnNumber: turnNum,
3453
- parentSpanId: spanId
3454
- })
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
+ }
3455
3783
  );
3456
3784
  turnTranscript.push({
3457
3785
  turnNumber: turnNum,
@@ -3579,12 +3907,20 @@ var RejudgeError = class extends Error {
3579
3907
  };
3580
3908
  var TERMINAL_VERDICT_STATUSES = /* @__PURE__ */ new Set(["passed", "failed"]);
3581
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
+ }
3582
3918
  async function rejudgeTaskRun(runId, endpoints, options = {}) {
3583
3919
  const api = createApiClient(endpoints);
3584
3920
  const progress = options.onProgress ?? (() => {
3585
3921
  });
3586
3922
  const detail = await api.get(`/v1/agent-task-runs/${runId}`);
3587
- if (!TERMINAL_VERDICT_STATUSES.has(detail.status)) {
3923
+ if (!TERMINAL_VERDICT_STATUSES.has(detail.status) && !isJudgeNoVerdictRun(detail)) {
3588
3924
  throw new RejudgeError(
3589
3925
  `Run ${runId} is '${detail.status}', not a completed run with a verdict \u2014 there is nothing to replay against yet`
3590
3926
  );
@@ -3656,27 +3992,14 @@ async function rejudgeTaskRun(runId, endpoints, options = {}) {
3656
3992
  loaded.adapter.deliverables
3657
3993
  );
3658
3994
  const proxied = proxyBrokenDeliverables(deliverables, validation.brokenDeliverables);
3659
- const passCounts = /* @__PURE__ */ new Map();
3660
- const errorCounts = /* @__PURE__ */ new Map();
3661
- let primary = [];
3995
+ const sampled = [];
3662
3996
  const judgeConfig = resolveJudgeConfig(options.judge, loaded.task.judge);
3663
3997
  for (let sample = 0; sample < samples; sample++) {
3664
3998
  if (samples > 1) progress(`judging sample ${sample + 1}/${samples}`);
3665
- const results = await runChecksOnce(loaded, proxied, snapshot, judgeConfig);
3666
- if (sample === 0) primary = results;
3667
- for (const result of results) {
3668
- passCounts.set(result.id, (passCounts.get(result.id) ?? 0) + (result.pass ? 1 : 0));
3669
- if (!result.pass && result.outcome === "error") {
3670
- errorCounts.set(result.id, (errorCounts.get(result.id) ?? 0) + 1);
3671
- }
3672
- }
3999
+ sampled.push(await runChecksOnce(loaded, proxied, snapshot, judgeConfig));
3673
4000
  }
3674
- const stability = primary.map((result) => ({
3675
- check_id: result.id,
3676
- passes: passCounts.get(result.id) ?? 0,
3677
- samples,
3678
- ...errorCounts.get(result.id) ? { errored: errorCounts.get(result.id) } : {}
3679
- }));
4001
+ const primary = sampled[0] ?? [];
4002
+ const stability = checkStability(sampled);
3680
4003
  const aggregate = aggregateResult(primary);
3681
4004
  return {
3682
4005
  runId,
@@ -3690,6 +4013,7 @@ async function rejudgeTaskRun(runId, endpoints, options = {}) {
3690
4013
  samples,
3691
4014
  checks: aggregate.checks,
3692
4015
  pass: aggregate.pass,
4016
+ ...aggregate.noVerdict ? { noVerdict: true } : {},
3693
4017
  stability,
3694
4018
  traceSnapshotAvailable: snapshotResult.available,
3695
4019
  taskDirUsed: taskDir
@@ -3699,6 +4023,24 @@ async function rejudgeTaskRun(runId, endpoints, options = {}) {
3699
4023
  if (scaffoldDir) rmSync(scaffoldDir, { recursive: true, force: true });
3700
4024
  }
3701
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
+ }
3702
4044
  async function runChecksOnce(loaded, deliverables, snapshot, judgeConfig) {
3703
4045
  if (loaded.inlineChecks) {
3704
4046
  return runTraceChecks({
@@ -3771,7 +4113,8 @@ function degenerateSnapshot(runId) {
3771
4113
  errors: "unavailable",
3772
4114
  timing: "unavailable",
3773
4115
  skills: "unavailable",
3774
- subagents: "unavailable"
4116
+ subagents: "unavailable",
4117
+ usage: "unavailable"
3775
4118
  },
3776
4119
  observations: []
3777
4120
  };
@@ -4303,6 +4646,7 @@ async function runTaskDir(taskDir) {
4303
4646
  taskDir: loaded.taskDir,
4304
4647
  taskId: loaded.task.id,
4305
4648
  pass: result.result.pass,
4649
+ ...result.result.noVerdict ? { noVerdict: true } : {},
4306
4650
  checks: result.result.checks,
4307
4651
  adapterName: loaded.adapter.name,
4308
4652
  traceRunId: result.traceRunId,
@@ -4379,7 +4723,8 @@ async function runAgentTaskCli(argv, cwd = process.cwd()) {
4379
4723
  if (result.status === "fulfilled") {
4380
4724
  summaries.push(result.value);
4381
4725
  printTaskSummary(result.value, cwd);
4382
- hasFailures ||= !result.value.pass;
4726
+ if (result.value.noVerdict) hasErrors = true;
4727
+ else hasFailures ||= !result.value.pass;
4383
4728
  } else {
4384
4729
  hasErrors = true;
4385
4730
  printTaskError(taskDir, cwd, result.reason);
@@ -4427,7 +4772,8 @@ function printHelp() {
4427
4772
  function printTaskSummary(summary, cwd) {
4428
4773
  const relativeTaskDir = relative(cwd, summary.taskDir);
4429
4774
  console.log("");
4430
- 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}`);
4431
4777
  console.log(`Path: ${relativeTaskDir}`);
4432
4778
  printResultGroup("Checks", summary.checks);
4433
4779
  if (summary.checks.length === 0 && !summary.pass) {
@@ -4454,12 +4800,14 @@ function printTaskError(taskDir, cwd, error) {
4454
4800
  }
4455
4801
  function printOverallSummary(summaries, hasErrors) {
4456
4802
  const passed = summaries.filter((summary) => summary.pass).length;
4457
- const failed = summaries.length - passed;
4803
+ const noVerdict = summaries.filter((summary) => summary.noVerdict).length;
4804
+ const failed = summaries.length - passed - noVerdict;
4458
4805
  console.log("");
4459
4806
  console.log("Summary");
4460
4807
  console.log(`- total: ${summaries.length}`);
4461
4808
  console.log(`- passed: ${passed}`);
4462
4809
  console.log(`- failed: ${failed}`);
4810
+ if (noVerdict > 0) console.log(`- no verdict: ${noVerdict}`);
4463
4811
  console.log(`- errors: ${hasErrors ? "yes" : "no"}`);
4464
4812
  }
4465
4813