@apo-ai/sdk 0.16.1 → 0.17.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.
@@ -9,20 +9,70 @@ import {
9
9
  resolveApoAuthHeaders,
10
10
  withApoRun,
11
11
  withApoRunSync
12
- } from "../chunk-YWUTUQPJ.js";
12
+ } from "../chunk-CSOY6OSP.js";
13
13
  import {
14
14
  createApoTracer
15
15
  } from "../chunk-OZWOLCJ5.js";
16
16
  import "../chunk-HRYXLRXJ.js";
17
17
  import {
18
18
  createApoOpenAI
19
- } from "../chunk-NSMKSPBU.js";
19
+ } from "../chunk-Z7DVZ34X.js";
20
20
  import {
21
21
  createApoAnthropic
22
- } from "../chunk-4BQSCD6T.js";
22
+ } from "../chunk-6U6V7VKZ.js";
23
23
  import {
24
24
  servingHostFromBaseURL
25
- } from "../chunk-JN7RHBTS.js";
25
+ } from "../chunk-KWIYQSWK.js";
26
+
27
+ // src/agent-task/trace-projection/retrieval.ts
28
+ function parseRetrievedDocuments(output) {
29
+ const array = documentsArray(output);
30
+ if (array === void 0) return void 0;
31
+ const documents = [];
32
+ for (const item of array) {
33
+ const document = parseDocument(item);
34
+ if (document !== void 0) documents.push(document);
35
+ }
36
+ return documents;
37
+ }
38
+ function documentsFromObservation(obs) {
39
+ return parseRetrievedDocuments(obs.toolResult) ?? parseRetrievedDocuments(obs.output);
40
+ }
41
+ function retrievalQueryFrom(obs) {
42
+ return queryIn(obs.toolParameters) ?? queryIn(obs.input);
43
+ }
44
+ function queryIn(value) {
45
+ if (value != null && typeof value === "object") {
46
+ const query = value.query;
47
+ if (typeof query === "string") return query;
48
+ }
49
+ return void 0;
50
+ }
51
+ function documentsArray(output) {
52
+ if (Array.isArray(output)) return output;
53
+ if (output != null && typeof output === "object") {
54
+ const wrapped = output.documents;
55
+ if (Array.isArray(wrapped)) return wrapped;
56
+ }
57
+ return void 0;
58
+ }
59
+ function parseDocument(item) {
60
+ if (typeof item === "string") {
61
+ return item.length > 0 ? { content: item } : void 0;
62
+ }
63
+ if (item == null || typeof item !== "object") return void 0;
64
+ const record = item;
65
+ const rawContent = record.content ?? record.text;
66
+ const content = typeof rawContent === "string" ? rawContent : "";
67
+ const id = typeof record.id === "string" && record.id.length > 0 ? record.id : void 0;
68
+ if (content.length === 0 && id === void 0) return void 0;
69
+ const document = { content };
70
+ if (id !== void 0) document.id = id;
71
+ if (typeof record.score === "number" && Number.isFinite(record.score)) {
72
+ document.score = record.score;
73
+ }
74
+ return document;
75
+ }
26
76
 
27
77
  // src/agent-task/run/steer-scheduler.ts
28
78
  var TASK_STEER_SPAN_NAME = "task.steer";
@@ -277,6 +327,18 @@ var TraceView = class _TraceView {
277
327
  get toolNamesInOrder() {
278
328
  return this.toolCalls.map((c) => c.name);
279
329
  }
330
+ /** Retrievals derived from `RETRIEVER` observations, in invocation order. */
331
+ get retrievals() {
332
+ return this.sortedObservations.filter((o) => o.type === "RETRIEVER").map((o) => {
333
+ const retrieval = { spanId: o.spanId, status: o.status };
334
+ const query = retrievalQueryFrom(o);
335
+ if (query !== void 0) retrieval.query = query;
336
+ const documents = documentsFromObservation(o);
337
+ if (documents !== void 0) retrieval.documents = documents;
338
+ if (o.startedAt !== void 0) retrieval.startedAt = o.startedAt;
339
+ return retrieval;
340
+ });
341
+ }
280
342
  /** Skill loads derived from `SKILL` observations, in invocation order. */
281
343
  get skillLoads() {
282
344
  return this.sortedObservations.filter((o) => o.type === "SKILL").map((o) => ({ spanId: o.spanId, skill: o.name, startedAt: o.startedAt }));
@@ -2860,7 +2922,7 @@ function createAgentMethod(rec, judgeConfig, judgeScope, evidence, judgeTracer,
2860
2922
  rec.record(
2861
2923
  label,
2862
2924
  false,
2863
- "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)\n\u2022 ANTHROPIC_MODEL + ANTHROPIC_API_KEY (Anthropic direct \u2014 needs a tool-calling-capable model; or ANTHROPIC_AUTH_TOKEN for plan credits)\nOr pass { judge } to runTask() programmatically.",
2925
+ "No judge model configured for t.agent. Set one of:\n\u2022 AGENT_TASK_OPENROUTER_MODEL or OPENROUTER_MODEL + OPENROUTER_API_KEY (OpenRouter, needs a tool-calling-capable model)\n\u2022 OPENAI_MODEL + OPENAI_API_KEY (OpenAI direct)\n\u2022 ANTHROPIC_MODEL + ANTHROPIC_API_KEY (Anthropic direct, needs a tool-calling-capable model, or ANTHROPIC_AUTH_TOKEN for plan credits)\nOr pass { judge } to runTask() programmatically.",
2864
2926
  { evaluator_type: "agent", location, outcome: "error" }
2865
2927
  );
2866
2928
  return;
@@ -2961,6 +3023,246 @@ function createAgentMethod(rec, judgeConfig, judgeScope, evidence, judgeTracer,
2961
3023
  };
2962
3024
  }
2963
3025
 
3026
+ // src/agent-task/checks/grounded.ts
3027
+ var GROUNDEDNESS_RUBRIC = [
3028
+ "Value 1 is an answer. Value 2 is the set of source documents the answer was supposed to rely on.",
3029
+ "Decide whether the answer is grounded in the sources. Check every factual claim in the answer. A claim is supported only when a source states it, or it follows directly from what a source states. Greetings, hedges, and statements that the answer cannot help are not factual claims.",
3030
+ "PASS when every factual claim is supported. FAIL when any factual claim is unsupported or contradicted by the sources, even if it is true in the real world. Groundedness measures the answer against these sources, not against your own knowledge. In your reasoning, name each unsupported claim."
3031
+ ].join("\n\n");
3032
+ function formatTraceSources(retrievals) {
3033
+ const seen = /* @__PURE__ */ new Set();
3034
+ const documents = [];
3035
+ for (const retrieval of retrievals) {
3036
+ for (const document of retrieval.documents ?? []) {
3037
+ const key = document.id ?? document.content;
3038
+ if (seen.has(key)) continue;
3039
+ seen.add(key);
3040
+ documents.push(document);
3041
+ }
3042
+ }
3043
+ return formatSources(documents);
3044
+ }
3045
+ function formatAuthorSources(sources) {
3046
+ return formatSources(sources.map((content) => ({ content })));
3047
+ }
3048
+ function formatSources(documents) {
3049
+ if (documents.length === 0) return "(no documents were retrieved)";
3050
+ return documents.map((document, i) => {
3051
+ const heading = document.id === void 0 ? `[${i + 1}]` : `[${i + 1}] ${document.id}`;
3052
+ return `${heading}
3053
+ ${document.content}`;
3054
+ }).join("\n\n");
3055
+ }
3056
+
3057
+ // src/agent-task/checks/embedding.ts
3058
+ function isEmbeddingHttpConfig(provider) {
3059
+ return typeof provider.model === "string";
3060
+ }
3061
+ function resolveEmbeddingConfig(provider, override) {
3062
+ const base = resolveEmbeddingProvider(provider);
3063
+ if (!override) return base;
3064
+ if (base?.custom && !override.model) return base;
3065
+ const baseConfig = base?.config;
3066
+ if (!override.model && !baseConfig) return void 0;
3067
+ return {
3068
+ config: {
3069
+ model: override.model ?? baseConfig.model,
3070
+ baseURL: override.baseURL ?? baseConfig?.baseURL,
3071
+ apiKey: override.apiKey ?? baseConfig?.apiKey,
3072
+ dimensions: override.dimensions ?? baseConfig?.dimensions
3073
+ }
3074
+ };
3075
+ }
3076
+ function resolveEmbeddingProvider(provider) {
3077
+ if (!provider) return void 0;
3078
+ if ("config" in provider || "custom" in provider) return provider;
3079
+ return isEmbeddingHttpConfig(provider) ? { config: provider } : { custom: provider };
3080
+ }
3081
+ var EmbeddingUnavailableError = class extends Error {
3082
+ constructor(message) {
3083
+ super(message);
3084
+ this.name = "EmbeddingUnavailableError";
3085
+ }
3086
+ };
3087
+ function createEmbeddingClient() {
3088
+ const resolved = /* @__PURE__ */ new Map();
3089
+ const inflight = /* @__PURE__ */ new Map();
3090
+ return {
3091
+ async embed(texts, provider) {
3092
+ const inputs = texts.map((text) => text.replace(/\r?\n/g, " "));
3093
+ const keyed = inputs.map((text) => ({ text, key: cacheKeyFor(text, provider) }));
3094
+ const seen = /* @__PURE__ */ new Set();
3095
+ const needFetch = keyed.filter(({ key }) => {
3096
+ if (resolved.has(key) || inflight.has(key) || seen.has(key)) return false;
3097
+ seen.add(key);
3098
+ return true;
3099
+ });
3100
+ let fetchedMeta;
3101
+ if (needFetch.length > 0) {
3102
+ const fetchPromise = (async () => fetchEmbeddings(needFetch.map((n) => n.text), provider))();
3103
+ for (let i = 0; i < needFetch.length; i++) {
3104
+ const promise = fetchPromise.then((fetched) => {
3105
+ fetchedMeta = {
3106
+ dimensions: fetched.dimensions,
3107
+ ...fetched.servedModel !== void 0 ? { servedModel: fetched.servedModel } : {},
3108
+ ...fetched.tokens !== void 0 ? { tokens: fetched.tokens } : {},
3109
+ ...fetched.latencyMs !== void 0 ? { latencyMs: fetched.latencyMs } : {}
3110
+ };
3111
+ return fetched.vectors[i];
3112
+ });
3113
+ inflight.set(needFetch[i].key, promise);
3114
+ }
3115
+ void Promise.allSettled(needFetch.map(({ key }) => inflight.get(key))).then(() => {
3116
+ for (const { key } of needFetch) inflight.delete(key);
3117
+ });
3118
+ }
3119
+ const vectors = await Promise.all(
3120
+ keyed.map(async ({ key }) => {
3121
+ const inFlight = inflight.get(key);
3122
+ if (inFlight !== void 0) {
3123
+ const vector = await inFlight;
3124
+ resolved.set(key, vector);
3125
+ return vector;
3126
+ }
3127
+ const cached = resolved.get(key);
3128
+ if (cached !== void 0) return cached;
3129
+ throw new EmbeddingUnavailableError("embedding vector vanished: concurrent cache race");
3130
+ })
3131
+ );
3132
+ const dims = vectors[0].length;
3133
+ if (vectors.some((v) => v.length !== dims)) {
3134
+ throw new EmbeddingUnavailableError("provider returned vectors of mixed dimensionality");
3135
+ }
3136
+ return {
3137
+ vectors,
3138
+ dimensions: dims,
3139
+ ...fetchedMeta?.servedModel !== void 0 ? { servedModel: fetchedMeta.servedModel } : {},
3140
+ ...fetchedMeta?.tokens !== void 0 ? { tokens: fetchedMeta.tokens } : {},
3141
+ ...fetchedMeta?.latencyMs !== void 0 ? { latencyMs: fetchedMeta.latencyMs } : {}
3142
+ };
3143
+ }
3144
+ };
3145
+ }
3146
+ function cacheKeyFor(text, provider) {
3147
+ if (provider.custom) return `custom|${text}`;
3148
+ const c = provider.config;
3149
+ return `${c.model}|${c.baseURL ?? ""}|${c.dimensions ?? "auto"}|${text}`;
3150
+ }
3151
+ async function fetchEmbeddings(inputs, provider) {
3152
+ const startedAt = Date.now();
3153
+ if (provider.custom) {
3154
+ const vectors = await provider.custom.embed(inputs);
3155
+ if (!Array.isArray(vectors) || vectors.length !== inputs.length) {
3156
+ throw new EmbeddingUnavailableError(
3157
+ `custom embed function returned ${vectors?.length ?? 0} vectors for ${inputs.length} inputs`
3158
+ );
3159
+ }
3160
+ return { vectors, dimensions: vectors[0]?.length ?? 0, latencyMs: Date.now() - startedAt };
3161
+ }
3162
+ const config = provider.config;
3163
+ const call = (omitDimensions) => requestEmbeddings(config, inputs, omitDimensions);
3164
+ const first = await callWithRetry(() => call(false));
3165
+ const result = first.kind === "rejected-dimensions" ? await callWithRetry(() => call(true)) : first;
3166
+ if (result.kind !== "ok") throw new Error(result.message);
3167
+ return {
3168
+ vectors: result.vectors,
3169
+ ...result.servedModel ? { servedModel: result.servedModel } : {},
3170
+ dimensions: result.vectors[0]?.length ?? 0,
3171
+ ...result.tokens ? { tokens: result.tokens } : {},
3172
+ latencyMs: Date.now() - startedAt
3173
+ };
3174
+ }
3175
+ async function callWithRetry(call) {
3176
+ const first = await call();
3177
+ if (first.kind !== "error" || first.status !== 429) return first;
3178
+ const retryAfter = Number(first.message.match(/retry-after (\d+)/)?.[1] ?? 1);
3179
+ await new Promise((resolve5) => setTimeout(resolve5, Math.min(retryAfter, 10) * 1e3));
3180
+ return call();
3181
+ }
3182
+ async function requestEmbeddings(config, inputs, omitDimensions) {
3183
+ if (!config.baseURL) {
3184
+ return {
3185
+ kind: "error",
3186
+ message: "no embeddings endpoint configured: set APO_EMBEDDING_BASE_URL (or pass baseURL), e.g. https://openrouter.ai/api/v1 or a local server like http://localhost:11434/v1"
3187
+ };
3188
+ }
3189
+ const url = `${config.baseURL.replace(/\/$/, "")}/embeddings`;
3190
+ let response;
3191
+ try {
3192
+ response = await fetch(url, {
3193
+ method: "POST",
3194
+ headers: {
3195
+ "content-type": "application/json",
3196
+ ...config.apiKey ? { authorization: `Bearer ${config.apiKey}` } : {}
3197
+ },
3198
+ body: JSON.stringify({
3199
+ model: config.model,
3200
+ input: inputs,
3201
+ ...omitDimensions ? {} : config.dimensions !== void 0 ? { dimensions: config.dimensions } : {}
3202
+ })
3203
+ });
3204
+ } catch (error) {
3205
+ return { kind: "error", message: `embeddings endpoint unreachable: ${error instanceof Error ? error.message : String(error)}` };
3206
+ }
3207
+ if (response.status === 400) {
3208
+ const body2 = await response.text();
3209
+ if (/dimensions/i.test(body2)) return { kind: "rejected-dimensions", message: body2 };
3210
+ return { kind: "error", status: 400, message: `embeddings request rejected (400): ${body2}` };
3211
+ }
3212
+ if (response.status === 429) {
3213
+ const retryAfter = response.headers.get("retry-after");
3214
+ return { kind: "error", status: 429, message: `embeddings rate-limited (429), retry-after ${retryAfter ?? 1}` };
3215
+ }
3216
+ if (!response.ok) {
3217
+ const body2 = await response.text();
3218
+ return { kind: "error", status: response.status, message: `embeddings request failed (${response.status}): ${body2.slice(0, 500)}` };
3219
+ }
3220
+ const body = await response.json();
3221
+ const data = body.data;
3222
+ if (!Array.isArray(data) || data.length !== inputs.length) {
3223
+ return { kind: "error", message: `embeddings response malformed: expected ${inputs.length} vectors, got ${data?.length ?? "none"}` };
3224
+ }
3225
+ const vectors = Array.from({ length: data.length });
3226
+ data.forEach((item, position) => {
3227
+ const vector = item.embedding;
3228
+ if (!Array.isArray(vector) || vector.some((v) => typeof v !== "number")) {
3229
+ throw new EmbeddingUnavailableError("embeddings response malformed: a returned embedding is not a number array");
3230
+ }
3231
+ const slot = typeof item.index === "number" && item.index >= 0 && item.index < data.length ? item.index : position;
3232
+ vectors[slot] = vector;
3233
+ });
3234
+ if (vectors.some((v) => v === void 0)) {
3235
+ return { kind: "error", message: "embeddings response malformed: index field named a slot outside the response" };
3236
+ }
3237
+ return {
3238
+ kind: "ok",
3239
+ vectors,
3240
+ ...typeof body.model === "string" ? { servedModel: body.model } : {},
3241
+ ...typeof body.usage?.prompt_tokens === "number" ? { tokens: { input: body.usage.prompt_tokens } } : {}
3242
+ };
3243
+ }
3244
+ function cosineSimilarity(a, b) {
3245
+ if (a.length !== b.length) return Number.NaN;
3246
+ let dot = 0;
3247
+ let normA = 0;
3248
+ let normB = 0;
3249
+ for (let i = 0; i < a.length; i++) {
3250
+ dot += a[i] * b[i];
3251
+ normA += a[i] * a[i];
3252
+ normB += b[i] * b[i];
3253
+ }
3254
+ if (normA === 0 || normB === 0) return 0;
3255
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
3256
+ }
3257
+ function vectorMagnitude(v) {
3258
+ let sum = 0;
3259
+ for (const x of v) sum += x * x;
3260
+ return Math.sqrt(sum);
3261
+ }
3262
+ function embeddingSettingsSignature(model, dimensions) {
3263
+ return `${model}:${dimensions}d:cosine:apo-v1`;
3264
+ }
3265
+
2964
3266
  // src/agent-task/checks/t.ts
2965
3267
  function resolveJudgeConfig(judgeConfig, override) {
2966
3268
  const model = override?.model ?? judgeConfig?.model;
@@ -2982,6 +3284,8 @@ var TEST_METHOD_NAMES = [
2982
3284
  "usedNoTools",
2983
3285
  "maxToolCalls",
2984
3286
  "noFailedActions",
3287
+ "retrieved",
3288
+ "maxRetrievals",
2985
3289
  "loadedSkill",
2986
3290
  "calledSubagent",
2987
3291
  "messageIncludes",
@@ -2992,9 +3296,12 @@ var TEST_METHOD_NAMES = [
2992
3296
  "steerDelivered",
2993
3297
  "afterSteer",
2994
3298
  "assert",
3299
+ "not",
2995
3300
  "check",
2996
3301
  "judge",
2997
- "agent"
3302
+ "semantic",
3303
+ "agent",
3304
+ "grounded"
2998
3305
  ];
2999
3306
  function describeName(m) {
3000
3307
  if (typeof m === "string") return `"${m}"`;
@@ -3075,7 +3382,7 @@ function createJudgeMethod(rec, judgeConfig, judgeScope, judgeTracer) {
3075
3382
  rec.record(
3076
3383
  label,
3077
3384
  false,
3078
- "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)\n\u2022 ANTHROPIC_MODEL + ANTHROPIC_API_KEY (Anthropic direct \u2014 or ANTHROPIC_AUTH_TOKEN for plan credits)\nOr pass { judge } to runTask() programmatically.",
3385
+ "No judge model configured. Set one of:\n\u2022 AGENT_TASK_OPENROUTER_MODEL or OPENROUTER_MODEL + OPENROUTER_API_KEY (OpenRouter, 200+ models, one account)\n\u2022 OPENAI_MODEL + OPENAI_API_KEY (OpenAI direct)\n\u2022 ANTHROPIC_MODEL + ANTHROPIC_API_KEY (Anthropic direct, or ANTHROPIC_AUTH_TOKEN for plan credits)\nOr pass { judge } to runTask() programmatically.",
3079
3386
  // No judge means no verdict — the same bucket as an unreachable judge.
3080
3387
  { evaluator_type: "llm", location, outcome: "error" }
3081
3388
  );
@@ -3184,7 +3491,10 @@ function createJudgeMethod(rec, judgeConfig, judgeScope, judgeTracer) {
3184
3491
  text: res?.reasoning?.slice(0, 2e3) ?? "",
3185
3492
  verdict: {
3186
3493
  reasoning: res?.reasoning?.slice(0, 2e3) ?? null,
3187
- pass: res?.pass ?? null
3494
+ pass: res?.pass ?? null,
3495
+ // A judge that gave no verdict still reports pass: false;
3496
+ // the outcome tells a trace reader it is not a "no".
3497
+ ...res?.unavailable ? { outcome: "error" } : {}
3188
3498
  }
3189
3499
  };
3190
3500
  },
@@ -3255,6 +3565,143 @@ function judgeScopeToContext(scope) {
3255
3565
  ...deliverableNames && deliverableNames.length > 0 ? { deliverableNames } : {}
3256
3566
  };
3257
3567
  }
3568
+ function createSemanticMethod(rec, embedding, judgeScope) {
3569
+ const client = embedding?.client ?? createEmbeddingClient();
3570
+ const previewOf = (text) => text.length > 120 ? `${text.slice(0, 117)}...` : text;
3571
+ return async (value, reference, opts) => {
3572
+ const label = opts?.label ?? "semantic";
3573
+ const location = rec.captureLocation();
3574
+ const threshold = opts.threshold;
3575
+ if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
3576
+ rec.record(
3577
+ label,
3578
+ false,
3579
+ `t.semantic: threshold must be a finite number in [0, 1], got ${threshold}`,
3580
+ { evaluator_type: "embedding", location }
3581
+ );
3582
+ return;
3583
+ }
3584
+ const resolved = resolveEmbeddingConfig(embedding?.provider, opts.embedding);
3585
+ if (!resolved) {
3586
+ rec.record(
3587
+ label,
3588
+ false,
3589
+ "No embedding model configured. Set one of:\n\u2022 APO_EMBEDDING_MODEL + APO_EMBEDDING_API_KEY (+ APO_EMBEDDING_BASE_URL for a local server)\n\u2022 OPENROUTER_API_KEY (defaults to openai/text-embedding-3-small via OpenRouter)\n\u2022 OPENAI_API_KEY (defaults to text-embedding-3-small)\nOr pass { embedding } to runTask() programmatically.",
3590
+ { evaluator_type: "embedding", location, outcome: "error" }
3591
+ );
3592
+ return;
3593
+ }
3594
+ const valueText = typeof value === "string" ? value : describeValue(value);
3595
+ const modelName = resolved.config?.model ?? "custom";
3596
+ const tracer = embedding?.tracer;
3597
+ const grade = (result) => {
3598
+ const [valueVector, referenceVector] = result.vectors;
3599
+ const similarity2 = cosineSimilarity(valueVector, referenceVector);
3600
+ if (!Number.isFinite(similarity2)) {
3601
+ throw new Error("cosine similarity undefined: provider returned non-finite vector components");
3602
+ }
3603
+ const rounded = Math.round(similarity2 * 1e4) / 1e4;
3604
+ return {
3605
+ similarity: rounded,
3606
+ pass: rounded >= threshold,
3607
+ signature: embeddingSettingsSignature(modelName, result.dimensions),
3608
+ dimensions: result.dimensions,
3609
+ ...result.servedModel ? { servedModel: result.servedModel } : {},
3610
+ ...result.tokens ? { tokens: result.tokens } : {},
3611
+ ...result.latencyMs !== void 0 ? { latencyMs: result.latencyMs } : {},
3612
+ zeroMagnitude: vectorMagnitude(valueVector) === 0 || vectorMagnitude(referenceVector) === 0
3613
+ };
3614
+ };
3615
+ let spanId;
3616
+ const call = () => client.embed([valueText, reference], resolved);
3617
+ try {
3618
+ const verdict = await (tracer ? tracer.step(
3619
+ {
3620
+ step_name: `embedding:${judgeScope?.checkName ?? label}`,
3621
+ observation_type: "EMBEDDING",
3622
+ model: modelName,
3623
+ ...servingHostFromBaseURL(resolved.config?.baseURL),
3624
+ input: { model: modelName, threshold },
3625
+ summarize: (r) => {
3626
+ const res = r;
3627
+ const text = `cosine ${res.similarity} vs threshold ${threshold}` + (res.pass ? ": pass" : ": fail");
3628
+ return {
3629
+ text,
3630
+ verdict: {
3631
+ pass: res.pass,
3632
+ similarity: res.similarity,
3633
+ threshold,
3634
+ settings: res.signature
3635
+ }
3636
+ };
3637
+ },
3638
+ usage: (r) => {
3639
+ const tokens = r.tokens;
3640
+ return tokens?.input !== void 0 ? { prompt_tokens: tokens.input } : void 0;
3641
+ }
3642
+ },
3643
+ async (id) => {
3644
+ spanId = id;
3645
+ return grade(await call());
3646
+ }
3647
+ ) : grade(await call()));
3648
+ const delta = verdict.similarity - threshold;
3649
+ let reasoning = verdict.pass ? `semantic similarity ${verdict.similarity.toFixed(4)} \u2265 ${threshold.toFixed(4)} [${verdict.signature}]` : `semantic similarity ${verdict.similarity.toFixed(4)} < ${threshold.toFixed(4)} (delta ${delta.toFixed(4)}) [${verdict.signature}]`;
3650
+ if (Math.abs(delta) < 0.02) {
3651
+ reasoning += ". Verdict is marginal: within 0.02 of the threshold, it will flip on model drift";
3652
+ }
3653
+ if (verdict.zeroMagnitude) {
3654
+ reasoning += " (zero-magnitude vector: the provider returned an empty direction)";
3655
+ }
3656
+ const metadata = {
3657
+ model: modelName,
3658
+ ...verdict.servedModel ? { served_model: verdict.servedModel } : {},
3659
+ dimensions: verdict.dimensions,
3660
+ similarity: verdict.similarity,
3661
+ threshold,
3662
+ metric: "cosine",
3663
+ settings_hash: verdict.signature,
3664
+ ...verdict.tokens ? { tokens: verdict.tokens } : {},
3665
+ ...verdict.latencyMs !== void 0 ? { latency_ms: verdict.latencyMs } : {},
3666
+ ...isTraceableSpanId(spanId) ? { span_id: spanId } : {},
3667
+ value_preview: previewOf(valueText),
3668
+ reference_preview: previewOf(reference)
3669
+ };
3670
+ rec.record(label, verdict.pass, reasoning, {
3671
+ evaluator_type: "embedding",
3672
+ expected: `cosine \u2265 ${threshold.toFixed(4)} (${modelName}:${verdict.dimensions}d)`,
3673
+ received: verdict.similarity,
3674
+ location,
3675
+ embedding: metadata
3676
+ });
3677
+ if (tracer && isTraceableSpanId(spanId)) {
3678
+ try {
3679
+ await tracer.score({
3680
+ name: "semantic_similarity",
3681
+ value: verdict.similarity,
3682
+ dataType: "NUMERIC",
3683
+ source: "EVAL",
3684
+ observationId: spanId
3685
+ }).catch(() => void 0);
3686
+ } catch {
3687
+ }
3688
+ }
3689
+ } catch (error) {
3690
+ rec.record(
3691
+ label,
3692
+ false,
3693
+ `embedding check failed: ${error instanceof Error ? error.message : String(error)}`,
3694
+ {
3695
+ evaluator_type: "embedding",
3696
+ expected: `cosine \u2265 ${threshold.toFixed(4)}`,
3697
+ received: valueText,
3698
+ location,
3699
+ outcome: "error"
3700
+ }
3701
+ );
3702
+ }
3703
+ };
3704
+ }
3258
3705
  var CAPABILITY_LABELS = {
3259
3706
  messages: "message",
3260
3707
  tools: "tool",
@@ -3262,9 +3709,10 @@ var CAPABILITY_LABELS = {
3262
3709
  timing: "timing",
3263
3710
  skills: "skill",
3264
3711
  subagents: "subagent",
3265
- usage: "token usage"
3712
+ usage: "token usage",
3713
+ retrieval: "retrieval"
3266
3714
  };
3267
- function createTraceTestContext(view, rec, judgeConfig, judgeScope, agentEvidence, judgeTracer, judgeTools) {
3715
+ function createTraceTestContext(view, rec, judgeConfig, judgeScope, agentEvidence, judgeTracer, judgeTools, embedding) {
3268
3716
  const unsupported = (id, capability) => {
3269
3717
  const label = CAPABILITY_LABELS[capability];
3270
3718
  rec.record(id, false, `${label} evidence is unavailable in this trace projection`, {
@@ -3274,7 +3722,24 @@ function createTraceTestContext(view, rec, judgeConfig, judgeScope, agentEvidenc
3274
3722
  });
3275
3723
  };
3276
3724
  const isAvailable = (c) => view.requireCapability(c) === "available";
3277
- return {
3725
+ const traceSourcesFor = (label) => {
3726
+ if (!isAvailable("retrieval")) {
3727
+ unsupported(label, "retrieval");
3728
+ return void 0;
3729
+ }
3730
+ const opaque = view.retrievals.filter((r) => r.documents === void 0).length;
3731
+ if (opaque > 0) {
3732
+ rec.record(label, false, `${opaque} retrieval(s) returned results in an unrecognized shape, so the sources are incomplete`, {
3733
+ evaluator_type: "llm",
3734
+ outcome: "unsupported",
3735
+ expected: "every retrieval's documents known",
3736
+ received: `${opaque} unrecognized retrieval result(s)`
3737
+ });
3738
+ return void 0;
3739
+ }
3740
+ return formatTraceSources(view.retrievals);
3741
+ };
3742
+ const context2 = {
3278
3743
  calledTool(name, opts) {
3279
3744
  if (!isAvailable("tools")) {
3280
3745
  unsupported(`calledTool(${describeName(name)})`, "tools");
@@ -3372,6 +3837,51 @@ function createTraceTestContext(view, rec, judgeConfig, judgeScope, agentEvidenc
3372
3837
  received: `${failed}`
3373
3838
  });
3374
3839
  },
3840
+ retrieved(document, opts) {
3841
+ requireTopK(opts?.topK);
3842
+ const id = `retrieved(${describeDocument(document)}${describeRetrievedOptions(opts)})`;
3843
+ if (!isAvailable("retrieval")) {
3844
+ unsupported(id, "retrieval");
3845
+ return;
3846
+ }
3847
+ const expected = `${describeDocument(document)} retrieved${describeRetrievedOptions(opts)}`;
3848
+ const scoped = view.retrievals.filter(
3849
+ (r) => opts?.query === void 0 || (r.query?.includes(opts.query) ?? false)
3850
+ );
3851
+ const hit = findRetrievedDocument(scoped, document, opts?.topK);
3852
+ if (hit !== void 0) {
3853
+ rec.record(id, true, "", { expected, received: hit });
3854
+ return;
3855
+ }
3856
+ const opaque = scoped.filter((r) => r.documents === void 0).length;
3857
+ if (opaque > 0) {
3858
+ rec.record(
3859
+ id,
3860
+ false,
3861
+ `${opaque} retrieval(s) in scope returned results in an unrecognized shape`,
3862
+ { outcome: "unsupported", expected, received: `${opaque} unrecognized retrieval result(s)` }
3863
+ );
3864
+ return;
3865
+ }
3866
+ rec.record(
3867
+ id,
3868
+ false,
3869
+ scoped.length === 0 ? `no retrieval ran with a query containing "${opts?.query ?? ""}"` : `no retrieval returned ${describeDocument(document)}${describeRetrievedOptions(opts)}`,
3870
+ { expected, received: summarizeRetrievals(scoped) }
3871
+ );
3872
+ },
3873
+ maxRetrievals(n) {
3874
+ requireBudget("maxRetrievals", n);
3875
+ if (view.requireCapability("retrieval") === "unavailable") {
3876
+ unsupported(`maxRetrievals(${n})`, "retrieval");
3877
+ return;
3878
+ }
3879
+ const count = view.retrievals.length;
3880
+ rec.record(`maxRetrievals(${n})`, count <= n, count <= n ? "" : `expected \u2264 ${n} retrievals, got ${count}`, {
3881
+ expected: `\u2264 ${n} retrievals`,
3882
+ received: `${count}`
3883
+ });
3884
+ },
3375
3885
  loadedSkill(skill) {
3376
3886
  if (!isAvailable("skills")) {
3377
3887
  unsupported(`loadedSkill("${skill}")`, "skills");
@@ -3459,7 +3969,8 @@ function createTraceTestContext(view, rec, judgeConfig, judgeScope, agentEvidenc
3459
3969
  judgeScope,
3460
3970
  agentEvidence,
3461
3971
  judgeTracer,
3462
- judgeTools
3972
+ judgeTools,
3973
+ embedding
3463
3974
  )
3464
3975
  ),
3465
3976
  check(value, matcher, label) {
@@ -3474,7 +3985,38 @@ function createTraceTestContext(view, rec, judgeConfig, judgeScope, agentEvidenc
3474
3985
  // judge does not consult trace capabilities; the scope carries the
3475
3986
  // task/check frame for prompt builders (#161).
3476
3987
  judge: trackPending(rec, createJudgeMethod(rec, judgeConfig, judgeScope, judgeTracer)),
3477
- agent: trackPending(rec, createAgentMethod(rec, judgeConfig, judgeScope, agentEvidence ?? { deliverables: {}, view }, judgeTracer, judgeTools))
3988
+ agent: trackPending(rec, createAgentMethod(rec, judgeConfig, judgeScope, agentEvidence ?? { deliverables: {}, view }, judgeTracer, judgeTools)),
3989
+ // Like t.judge: value-plane check, no capability gating — but traced, so
3990
+ // the embeddings call shows up as an EMBEDDING observation.
3991
+ semantic: trackPending(rec, createSemanticMethod(rec, embedding, judgeScope)),
3992
+ async grounded(value, opts) {
3993
+ const label = opts?.label ?? "grounded";
3994
+ const sources = opts?.sources ?? "trace";
3995
+ const sourceList = sources === "trace" ? traceSourcesFor(label) : formatAuthorSources(sources);
3996
+ if (sourceList === void 0) return;
3997
+ await context2.judge([value, sourceList], GROUNDEDNESS_RUBRIC, {
3998
+ label,
3999
+ judge: opts?.judge,
4000
+ expect: opts?.expect
4001
+ });
4002
+ },
4003
+ get not() {
4004
+ return createNotContext(context2, rec);
4005
+ }
4006
+ };
4007
+ return context2;
4008
+ }
4009
+ function createNotContext(t, rec) {
4010
+ return {
4011
+ calledTool: (name, opts) => rec.negated(() => t.calledTool(name, opts)),
4012
+ toolOrder: (names) => rec.negated(() => t.toolOrder(names)),
4013
+ loadedSkill: (skill) => rec.negated(() => t.loadedSkill(skill)),
4014
+ calledSubagent: (agent) => rec.negated(() => t.calledSubagent(agent)),
4015
+ messageIncludes: (token) => rec.negated(() => t.messageIncludes(token)),
4016
+ retrieved: (document, opts) => rec.negated(() => t.retrieved(document, opts)),
4017
+ check(value, matcher, label) {
4018
+ rec.negated(() => t.check(value, matcher, label));
4019
+ }
3478
4020
  };
3479
4021
  }
3480
4022
  function requireBudget(method, n) {
@@ -3579,14 +4121,76 @@ function matchesTraceToolCall(call, name, opts) {
3579
4121
  }
3580
4122
  return true;
3581
4123
  }
4124
+ function findRetrievedDocument(retrievals, document, topK) {
4125
+ for (const retrieval of retrievals) {
4126
+ const documents = retrieval.documents ?? [];
4127
+ const limit = topK === void 0 ? documents.length : Math.min(topK, documents.length);
4128
+ for (let i = 0; i < limit; i++) {
4129
+ if (matchesDocument(documents[i], document)) {
4130
+ return `rank ${i + 1} for query "${retrieval.query ?? "(none recorded)"}"`;
4131
+ }
4132
+ }
4133
+ }
4134
+ return void 0;
4135
+ }
4136
+ function matchesDocument(doc, matcher) {
4137
+ if (typeof matcher === "string") {
4138
+ return doc.id === matcher || (doc.id?.startsWith(`${matcher}#`) ?? false);
4139
+ }
4140
+ if ("id" in matcher) return doc.id === matcher.id;
4141
+ if ("idPrefix" in matcher) return doc.id?.startsWith(matcher.idPrefix) ?? false;
4142
+ return doc.content.includes(matcher.contentIncludes);
4143
+ }
4144
+ function describeDocument(matcher) {
4145
+ if (typeof matcher === "string") return `"${matcher}"`;
4146
+ if ("id" in matcher) return `{ id: "${matcher.id}" }`;
4147
+ if ("idPrefix" in matcher) return `{ idPrefix: "${matcher.idPrefix}" }`;
4148
+ return `{ contentIncludes: "${matcher.contentIncludes}" }`;
4149
+ }
4150
+ function describeRetrievedOptions(opts) {
4151
+ const parts = [];
4152
+ if (opts?.topK !== void 0) parts.push(`in top ${opts.topK}`);
4153
+ if (opts?.query !== void 0) parts.push(`for query containing "${opts.query}"`);
4154
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
4155
+ }
4156
+ function summarizeRetrievals(retrievals) {
4157
+ if (retrievals.length === 0) return "(no retrievals)";
4158
+ return retrievals.map((r) => {
4159
+ const documents = (r.documents ?? []).map((d) => d.id ?? `"${d.content.slice(0, 40)}"`);
4160
+ const shown = documents.slice(0, 10).join(", ") + (documents.length > 10 ? ", ..." : "");
4161
+ return `"${r.query ?? "(none recorded)"}": [${shown}]`;
4162
+ }).join("; ");
4163
+ }
4164
+ function requireTopK(topK) {
4165
+ if (topK !== void 0 && (!Number.isInteger(topK) || topK < 1)) {
4166
+ throw new TypeError(`retrieved: topK must be a positive integer, got ${topK}`);
4167
+ }
4168
+ }
3582
4169
 
3583
4170
  // src/agent-task/checks/recorder.ts
3584
4171
  var Recorder = class {
3585
4172
  constructor(locate) {
3586
4173
  this.records = [];
3587
4174
  this.pending = [];
4175
+ this.negating = false;
3588
4176
  this.locate = locate;
3589
4177
  }
4178
+ /**
4179
+ * Run `fn` with every assertion it records negated (`t.not.*`): a pass
4180
+ * becomes a fail and a fail becomes a pass. Outcomes that are not verdicts
4181
+ * (`unsupported`, `error`) keep their outcome, so missing evidence can never
4182
+ * become a pass by negation. Only synchronous assertions are negatable,
4183
+ * which keeps the scope from leaking into a later record.
4184
+ */
4185
+ negated(fn) {
4186
+ const previous = this.negating;
4187
+ this.negating = true;
4188
+ try {
4189
+ fn();
4190
+ } finally {
4191
+ this.negating = previous;
4192
+ }
4193
+ }
3590
4194
  /**
3591
4195
  * Capture the call site location synchronously. Use this when a record will
3592
4196
  * happen AFTER an `await` (e.g. `t.judge`): once an async function resumes,
@@ -3622,7 +4226,7 @@ var Recorder = class {
3622
4226
  */
3623
4227
  record(id, pass, reasoning, extra) {
3624
4228
  const location = extra?.location ?? (this.locate ? this.locate(new Error().stack ?? "") : void 0);
3625
- this.records.push({
4229
+ const result = {
3626
4230
  id,
3627
4231
  pass,
3628
4232
  reasoning,
@@ -3631,13 +4235,29 @@ var Recorder = class {
3631
4235
  received: extra?.received,
3632
4236
  evaluator_type: extra?.evaluator_type,
3633
4237
  judge: extra?.judge,
4238
+ embedding: extra?.embedding,
3634
4239
  outcome: extra?.outcome
3635
- });
4240
+ };
4241
+ this.records.push(this.negating ? negate(result) : result);
3636
4242
  }
3637
4243
  get all() {
3638
4244
  return this.records;
3639
4245
  }
3640
4246
  };
4247
+ function negate(result) {
4248
+ const negated = {
4249
+ ...result,
4250
+ id: `not.${result.id}`,
4251
+ expected: result.expected === void 0 ? void 0 : `not ${result.expected}`
4252
+ };
4253
+ if (result.outcome === "unsupported" || result.outcome === "error") return negated;
4254
+ return {
4255
+ ...negated,
4256
+ pass: !result.pass,
4257
+ reasoning: result.pass ? `expected ${result.id} not to hold, but it did` : "",
4258
+ outcome: result.outcome === void 0 ? void 0 : result.pass ? "fail" : "pass"
4259
+ };
4260
+ }
3641
4261
 
3642
4262
  // src/agent-task/checks/compact.ts
3643
4263
  import { createHash as createHash4 } from "crypto";
@@ -3872,12 +4492,18 @@ async function runTraceChecks(args) {
3872
4492
  const view = new TraceView(args.snapshot);
3873
4493
  const locate = args.moduleUrl && args.displayFile ? (stack) => parseCheckLocation(stack, args.moduleUrl, args.displayFile) : void 0;
3874
4494
  const taskMeta = readTaskMeta(args.task);
4495
+ const embedding = args.embedding ? { ...args.embedding, ...args.embedding.tracer ? {} : optionalTracer(args.judgeTracer) } : { client: createEmbeddingClient(), ...optionalTracer(args.judgeTracer) };
3875
4496
  const results = await Promise.all(
3876
- registry.map((check) => runOneCheck(check, args, view, taskMeta, locate))
4497
+ registry.map((check) => runOneCheck(check, args, embedding, view, taskMeta, locate))
3877
4498
  );
3878
4499
  return results;
3879
4500
  }
3880
- async function runOneCheck(check, args, view, taskMeta, locate) {
4501
+ function optionalTracer(tracer) {
4502
+ if (!tracer) return {};
4503
+ const candidate = tracer;
4504
+ return typeof candidate.step === "function" && typeof candidate.score === "function" ? { tracer: candidate } : {};
4505
+ }
4506
+ async function runOneCheck(check, args, embedding, view, taskMeta, locate) {
3881
4507
  const runCheck = async () => {
3882
4508
  const rec = new Recorder(locate);
3883
4509
  const reads = trackDeliverableReads(args.deliverables);
@@ -3901,7 +4527,8 @@ async function runOneCheck(check, args, view, taskMeta, locate) {
3901
4527
  ...args.historyUnavailableReason ? { historyUnavailable: args.historyUnavailableReason } : {}
3902
4528
  },
3903
4529
  args.judgeTracer,
3904
- args.judgeTools
4530
+ args.judgeTools,
4531
+ embedding
3905
4532
  );
3906
4533
  let thrownLocation;
3907
4534
  try {
@@ -3921,9 +4548,11 @@ async function runOneCheck(check, args, view, taskMeta, locate) {
3921
4548
  const pass = failed.length === 0;
3922
4549
  const outcome = rollUpCheckOutcome(failed);
3923
4550
  const judged = rec.all.filter(isJudgedAssertion);
3924
- const reasoning = failed.length > 0 ? failed.map((r) => r.reasoning || r.id).join("; ") : judged.length > 0 ? judged.map((r) => r.reasoning).filter(Boolean).join("; ") || "passed" : rec.all.length > 0 ? "passed" : "no assertions recorded";
4551
+ const semantic = rec.all.filter((r) => r.evaluator_type === "embedding");
4552
+ const reasoning = failed.length > 0 ? failed.map((r) => r.reasoning || r.id).join("; ") : judged.length > 0 || semantic.length > 0 ? [...judged, ...semantic].map((r) => r.reasoning).filter(Boolean).join("; ") || "passed" : rec.all.length > 0 ? "passed" : "no assertions recorded";
3925
4553
  const location = failed.find((r) => r.location)?.location;
3926
4554
  const judge = rec.all.find((r) => r.judge)?.judge;
4555
+ const embeddingMeta = rec.all.find((r) => r.embedding)?.embedding;
3927
4556
  return {
3928
4557
  id: check.id,
3929
4558
  pass,
@@ -3931,6 +4560,7 @@ async function runOneCheck(check, args, view, taskMeta, locate) {
3931
4560
  evaluator_type: deriveEvaluatorType(rec.all),
3932
4561
  ...outcome ? { outcome } : {},
3933
4562
  ...judge ? { judge } : {},
4563
+ ...embeddingMeta ? { embedding: embeddingMeta } : {},
3934
4564
  ...location ? { location } : {},
3935
4565
  ...args.displayFile ? { source_file: args.displayFile } : {},
3936
4566
  ...rec.all.length > 0 ? { assertions: rec.all.map((a) => ({ ...a })) } : {},
@@ -3960,19 +4590,27 @@ function isJudgedAssertion(assertion) {
3960
4590
  function deriveEvaluatorType(assertions) {
3961
4591
  let sawLlm = false;
3962
4592
  let sawAgent = false;
4593
+ let sawEmbedding = false;
3963
4594
  let sawDeterministic = false;
3964
4595
  for (const assertion of assertions) {
3965
4596
  if (isJudgedAssertion(assertion)) {
3966
4597
  if (assertion.evaluator_type === "agent") sawAgent = true;
3967
4598
  else sawLlm = true;
4599
+ } else if (assertion.evaluator_type === "embedding") {
4600
+ sawEmbedding = true;
3968
4601
  } else {
3969
4602
  sawDeterministic = true;
3970
4603
  }
3971
4604
  }
3972
- if (!sawLlm && !sawAgent) return "code";
3973
- if (sawDeterministic) return "mixed";
3974
- if (sawAgent && !sawLlm) return "agent";
3975
- return "llm";
4605
+ const evaluatorKinds = [sawLlm, sawAgent, sawEmbedding].filter(Boolean).length;
4606
+ if (evaluatorKinds === 0 && !sawDeterministic) return "code";
4607
+ if (evaluatorKinds === 1 && !sawDeterministic) {
4608
+ if (sawAgent) return "agent";
4609
+ if (sawEmbedding) return "embedding";
4610
+ return "llm";
4611
+ }
4612
+ if (evaluatorKinds > 0) return "mixed";
4613
+ return "code";
3976
4614
  }
3977
4615
  function summarizeCheckForSpan(result) {
3978
4616
  const check = result;
@@ -3981,6 +4619,7 @@ function summarizeCheckForSpan(result) {
3981
4619
  text: reasoning,
3982
4620
  verdict: {
3983
4621
  pass: check.pass,
4622
+ ...check.outcome ? { outcome: check.outcome } : {},
3984
4623
  reasoning,
3985
4624
  evaluator_type: check.evaluator_type,
3986
4625
  ...check.judge ? { judge_model: check.judge.model } : {},
@@ -4017,6 +4656,7 @@ async function loadAndRunFlowChecks(checksPath, args, brokenDeliverables = {}) {
4017
4656
  ...args.judgeTools ? { judgeTools: args.judgeTools } : {},
4018
4657
  ...args.historyPlane ? { historyPlane: args.historyPlane } : {},
4019
4658
  ...args.judgeTracer ? { judgeTracer: args.judgeTracer } : {},
4659
+ ...args.embedding ? { embedding: args.embedding } : {},
4020
4660
  moduleUrl,
4021
4661
  displayFile: basename(checksPath)
4022
4662
  });
@@ -4817,6 +5457,7 @@ function deriveCapabilities(observations, hierarchyAmbiguous) {
4817
5457
  const hasMessages = observations.some(
4818
5458
  (o) => o.type === "GENERATION" && o.messages && o.messages.length > 0
4819
5459
  );
5460
+ const retrievals = observations.filter((o) => o.type === "RETRIEVER");
4820
5461
  return {
4821
5462
  messages: hasMessages ? "available" : "unavailable",
4822
5463
  tools: hasType("TOOL") ? "available" : "unavailable",
@@ -4829,7 +5470,8 @@ function deriveCapabilities(observations, hierarchyAmbiguous) {
4829
5470
  // Usage is attributed to turns through parentSpanId; with an ambiguous
4830
5471
  // step id the chain is broken, so a per-turn or per-run sum could
4831
5472
  // silently drop turns.
4832
- usage: !observations.some((o) => o.usage !== void 0) ? "unavailable" : hierarchyAmbiguous ? "partial" : "available"
5473
+ usage: !observations.some((o) => o.usage !== void 0) ? "unavailable" : hierarchyAmbiguous ? "partial" : "available",
5474
+ retrieval: retrievals.length === 0 ? "unavailable" : retrievals.some((o) => documentsFromObservation(o) !== void 0) ? "available" : "partial"
4833
5475
  };
4834
5476
  }
4835
5477
  function usageFrom(params) {
@@ -4845,6 +5487,7 @@ function createProjectionTee(real) {
4845
5487
  const observations = [];
4846
5488
  const pending = /* @__PURE__ */ new Map();
4847
5489
  const realIdByTeeId = /* @__PURE__ */ new Map();
5490
+ const exportedSpanIds = /* @__PURE__ */ new Set();
4848
5491
  const teeIdByRealStepId = /* @__PURE__ */ new Map();
4849
5492
  const ambiguousRealStepIds = /* @__PURE__ */ new Set();
4850
5493
  const activeStep = new AsyncLocalStorage();
@@ -4882,6 +5525,7 @@ function createProjectionTee(real) {
4882
5525
  } else if (params?.output !== void 0) {
4883
5526
  obs.output = params.output;
4884
5527
  }
5528
+ if (p.type === "RETRIEVER" && p.input !== void 0) obs.input = p.input;
4885
5529
  if (p.type === "GENERATION" && params?.output != null) {
4886
5530
  const text = extractText(params.output);
4887
5531
  if (text !== "") {
@@ -4921,6 +5565,7 @@ function createProjectionTee(real) {
4921
5565
  ambiguousRealStepIds.add(realSpanId);
4922
5566
  } else {
4923
5567
  teeIdByRealStepId.set(realSpanId, obsSpanId);
5568
+ exportedSpanIds.add(realSpanId);
4924
5569
  }
4925
5570
  }
4926
5571
  return activeStep.run(obsSpanId, () => fn(realSpanId));
@@ -4980,6 +5625,9 @@ function createProjectionTee(real) {
4980
5625
  realIdByTeeId.delete(spanId);
4981
5626
  try {
4982
5627
  real.endSpan(realId, endParams);
5628
+ if (realId != null && realId !== spanId) {
5629
+ exportedSpanIds.add(realId);
5630
+ }
4983
5631
  } catch {
4984
5632
  }
4985
5633
  },
@@ -5009,7 +5657,32 @@ function createProjectionTee(real) {
5009
5657
  }
5010
5658
  },
5011
5659
  async traceRetriever(query, fn) {
5012
- return real.traceRetriever(query, fn);
5660
+ const obsSpanId = recordStart({
5661
+ step_name: "retrieve",
5662
+ observation_type: "RETRIEVER",
5663
+ input: { query }
5664
+ });
5665
+ const start = monotonicNowMs();
5666
+ try {
5667
+ const result = await real.traceRetriever(query, fn);
5668
+ const documents = parseRetrievedDocuments(result);
5669
+ complete(obsSpanId, {
5670
+ latency_ms: round3(monotonicNowMs() - start),
5671
+ // Recognized results are stored normalized so every reader sees one
5672
+ // typed shape. Anything else passes through raw, and the retrieval
5673
+ // capability derives "partial" from it.
5674
+ output: documents !== void 0 ? { documents } : result
5675
+ });
5676
+ return result;
5677
+ } catch (error) {
5678
+ const message = error instanceof Error ? error.message : String(error);
5679
+ complete(obsSpanId, {
5680
+ latency_ms: round3(monotonicNowMs() - start),
5681
+ status_message: message,
5682
+ level: "ERROR"
5683
+ });
5684
+ throw error;
5685
+ }
5013
5686
  },
5014
5687
  async traceChain(name, fn) {
5015
5688
  return real.traceChain(name, fn);
@@ -5046,14 +5719,22 @@ function createProjectionTee(real) {
5046
5719
  observations: [...observations]
5047
5720
  };
5048
5721
  },
5049
- injectObservations(injected) {
5722
+ injectObservations(injected, opts) {
5050
5723
  observations.push(...injected);
5724
+ if (opts?.remote !== false) {
5725
+ for (const observation of injected) {
5726
+ exportedSpanIds.add(observation.spanId);
5727
+ }
5728
+ }
5051
5729
  observations.sort((a, b) => {
5052
5730
  const at = a.startedAt ?? "";
5053
5731
  const bt = b.startedAt ?? "";
5054
5732
  if (at !== bt) return at < bt ? -1 : 1;
5055
5733
  return a.spanId < b.spanId ? -1 : 1;
5056
5734
  });
5735
+ },
5736
+ getExportedSpanIds() {
5737
+ return new Set(exportedSpanIds);
5057
5738
  }
5058
5739
  };
5059
5740
  }
@@ -6065,6 +6746,11 @@ function resolveJudgeToolsForTask(runLevel, taskLevel, taskDir) {
6065
6746
  if (!layered?.mcp) return layered;
6066
6747
  return { mcp: resolveMcpServerPaths(layered.mcp, taskDir) };
6067
6748
  }
6749
+ function buildEmbeddingRuntime(runLevel, taskLevel) {
6750
+ const provider = resolveEmbeddingConfig(runLevel, taskLevel);
6751
+ if (!provider) return void 0;
6752
+ return { provider, client: createEmbeddingClient() };
6753
+ }
6068
6754
  async function runTask(taskDir, options) {
6069
6755
  const loaded = options?.loaded ?? await loadTask(taskDir);
6070
6756
  const trace2 = options?.tracing;
@@ -6082,19 +6768,19 @@ async function runTask(taskDir, options) {
6082
6768
  const phase1 = await captureExecution(loaded, options, traceContext);
6083
6769
  const canonical = await readCanonicalSnapshot(
6084
6770
  trace2,
6085
- phase1.replayObservationFloor
6771
+ phase1.exportedSpanIds
6086
6772
  );
6087
6773
  if (canonical) phase1.snapshot = canonical;
6088
6774
  return evaluate(loaded, options, phase1, traceContext);
6089
6775
  }
6090
6776
  );
6091
6777
  }
6092
- async function readCanonicalSnapshot(trace2, minObservations) {
6778
+ async function readCanonicalSnapshot(trace2, exportedSpanIds, opts = {}) {
6093
6779
  if (!trace2.taskRunId) return null;
6094
6780
  const endpoint = process.env.AGENT_TASK_TRACE_ENDPOINT;
6095
6781
  const authToken = process.env.APO_AUTH_TOKEN;
6096
6782
  if (!endpoint || !authToken) return null;
6097
- const deadlineMs = 3e4;
6783
+ const deadlineMs = opts.deadlineMs ?? 3e4;
6098
6784
  const start = Date.now();
6099
6785
  try {
6100
6786
  let snapshot = await readTaskRunProjection({
@@ -6102,25 +6788,17 @@ async function readCanonicalSnapshot(trace2, minObservations) {
6102
6788
  authToken,
6103
6789
  taskRunId: trace2.taskRunId,
6104
6790
  deadlineMs,
6105
- // Fresh-run readback: the run row was pre-created by the caller/executor
6106
- // flow, and the subprocess-exported trace may still be in flight when
6107
- // the checks phase starts — a 409 "no trace yet" must wait for the
6108
- // claim, not fall back to a local snapshot that cannot see the
6109
- // subprocess's spans.
6110
- // Fresh-run readback: the run row was pre-created by the caller/executor
6111
- // flow, and the subprocess-exported trace may still be in flight when
6112
- // the checks phase starts — a 409 "no trace yet" must wait for the
6113
- // claim, not fall back to a local snapshot that cannot see the
6114
- // subprocess's spans.
6791
+ // Fresh-run readback: the trace claim may still be landing, so the
6792
+ // 409 must be waited out rather than fallen back from.
6115
6793
  retryNoTrace: true
6116
6794
  });
6117
- while (minObservations !== void 0 && snapshot.observations.length < minObservations) {
6795
+ while (!containsAllSpanIds(snapshot, exportedSpanIds)) {
6118
6796
  const remaining = deadlineMs - (Date.now() - start);
6119
6797
  if (remaining <= 0) {
6120
6798
  console.error(
6121
- `[AgentTask] Projection still missing replayed observations after ${deadlineMs}ms (${snapshot.observations.length}/${minObservations}); continuing with the current snapshot`
6799
+ `[AgentTask] Projection missing ${countMissingSpanIds(snapshot, exportedSpanIds)}/${exportedSpanIds.size} spans the run exported after ${deadlineMs}ms; using the local snapshot`
6122
6800
  );
6123
- break;
6801
+ return null;
6124
6802
  }
6125
6803
  await new Promise((resolve5) => setTimeout(resolve5, 500));
6126
6804
  snapshot = await readTaskRunProjection({
@@ -6139,6 +6817,27 @@ async function readCanonicalSnapshot(trace2, minObservations) {
6139
6817
  return null;
6140
6818
  }
6141
6819
  }
6820
+ function containsAllSpanIds(snapshot, required) {
6821
+ if (required.size === 0) return true;
6822
+ const present = new Set(
6823
+ snapshot.observations.map((observation) => observation.spanId)
6824
+ );
6825
+ for (const id of required) {
6826
+ if (!present.has(id)) return false;
6827
+ }
6828
+ return true;
6829
+ }
6830
+ function countMissingSpanIds(snapshot, required) {
6831
+ if (required.size === 0) return 0;
6832
+ const present = new Set(
6833
+ snapshot.observations.map((observation) => observation.spanId)
6834
+ );
6835
+ let missing = 0;
6836
+ for (const id of required) {
6837
+ if (!present.has(id)) missing++;
6838
+ }
6839
+ return missing;
6840
+ }
6142
6841
  async function executeAdapterRun(loaded, options, rawTrace, continueWith) {
6143
6842
  const {
6144
6843
  task: task2,
@@ -6273,7 +6972,7 @@ async function executeAdapterRun(loaded, options, rawTrace, continueWith) {
6273
6972
  endpoint: process.env.AGENT_TASK_TRACE_ENDPOINT
6274
6973
  })
6275
6974
  );
6276
- tee.injectObservations(replay.observations);
6975
+ tee.injectObservations(replay.observations, { remote: replay.exported });
6277
6976
  }
6278
6977
  return await continueWith({
6279
6978
  collected,
@@ -6281,7 +6980,8 @@ async function executeAdapterRun(loaded, options, rawTrace, continueWith) {
6281
6980
  runConfiguration,
6282
6981
  replay,
6283
6982
  trace: trace2,
6284
- getSnapshot: () => tee.getSnapshot()
6983
+ getSnapshot: () => tee.getSnapshot(),
6984
+ getExportedSpanIds: () => tee.getExportedSpanIds()
6285
6985
  });
6286
6986
  } catch (error) {
6287
6987
  throw withRunConfiguration(error, runConfiguration);
@@ -6334,6 +7034,7 @@ async function executeLoadedTask(loaded, options, rawTrace, traceRunId) {
6334
7034
  async () => {
6335
7035
  const judgeConfig = resolveJudgeConfig(options?.judge, task2.judge);
6336
7036
  const judgeTools = resolveJudgeToolsForTask(options?.judgeTools, task2.judgeTools, absoluteDir);
7037
+ const embeddingRuntime = buildEmbeddingRuntime(options?.embedding, task2.embedding);
6337
7038
  const historyPlane = await freezeHistoryPlaneFromEnv(task2.id);
6338
7039
  if (!inlineChecks) {
6339
7040
  return loadAndRunFlowChecks(
@@ -6346,7 +7047,8 @@ async function executeLoadedTask(loaded, options, rawTrace, traceRunId) {
6346
7047
  ...judgeConfig ? { judgeConfig } : {},
6347
7048
  ...judgeTools ? { judgeTools } : {},
6348
7049
  judgeTracer: trace2,
6349
- ...historyPlane ? { historyPlane } : {}
7050
+ ...historyPlane ? { historyPlane } : {},
7051
+ ...embeddingRuntime ? { embedding: embeddingRuntime } : {}
6350
7052
  },
6351
7053
  validationResults.brokenDeliverables
6352
7054
  );
@@ -6363,6 +7065,7 @@ async function executeLoadedTask(loaded, options, rawTrace, traceRunId) {
6363
7065
  ...judgeTools ? { judgeTools } : {},
6364
7066
  judgeTracer: trace2,
6365
7067
  ...historyPlane ? { historyPlane } : {},
7068
+ ...embeddingRuntime ? { embedding: embeddingRuntime } : {},
6366
7069
  moduleUrl,
6367
7070
  displayFile: evalFileName
6368
7071
  });
@@ -6386,11 +7089,9 @@ async function captureExecution(loaded, options, rawTrace) {
6386
7089
  collected: outcome.collected,
6387
7090
  transcriptTurns: outcome.transcriptTurns,
6388
7091
  runConfiguration: outcome.runConfiguration,
6389
- // Freeze the snapshot before the root span ends — Phase 2 reads it.
7092
+ // Freeze the snapshot before the root span ends: Phase 2 reads it.
6390
7093
  snapshot: outcome.getSnapshot(),
6391
- // The floor for the canonical read-back: everything the local snapshot
6392
- // holds (runner spans + replayed observations) should project remotely.
6393
- ...outcome.replay ? { replayObservationFloor: outcome.getSnapshot().observations.length } : {}
7094
+ exportedSpanIds: outcome.getExportedSpanIds()
6394
7095
  }));
6395
7096
  }
6396
7097
  async function evaluate(loaded, options, phase1, judgeTracer) {
@@ -6411,9 +7112,9 @@ async function evaluate(loaded, options, phase1, judgeTracer) {
6411
7112
  );
6412
7113
  const judgeConfig = resolveJudgeConfig(options?.judge, task2.judge);
6413
7114
  const judgeTools = resolveJudgeToolsForTask(options?.judgeTools, task2.judgeTools, absoluteDir);
7115
+ const embeddingRuntime = buildEmbeddingRuntime(options?.embedding, task2.embedding);
6414
7116
  const historyPlane = await freezeHistoryPlaneFromEnv(task2.id);
6415
7117
  const runChecks = async () => inlineChecks ? runTraceChecks({
6416
- ...judgeTools ? { judgeTools } : {},
6417
7118
  snapshot: phase1.snapshot,
6418
7119
  deliverables,
6419
7120
  files,
@@ -6422,6 +7123,7 @@ async function evaluate(loaded, options, phase1, judgeTracer) {
6422
7123
  ...judgeTools ? { judgeTools } : {},
6423
7124
  ...judgeTracer ? { judgeTracer } : {},
6424
7125
  ...historyPlane ? { historyPlane } : {},
7126
+ ...embeddingRuntime ? { embedding: embeddingRuntime } : {},
6425
7127
  moduleUrl,
6426
7128
  displayFile: evalFileName
6427
7129
  }) : loadAndRunFlowChecks(
@@ -6434,7 +7136,8 @@ async function evaluate(loaded, options, phase1, judgeTracer) {
6434
7136
  ...judgeConfig ? { judgeConfig } : {},
6435
7137
  ...judgeTools ? { judgeTools } : {},
6436
7138
  ...judgeTracer ? { judgeTracer } : {},
6437
- ...historyPlane ? { historyPlane } : {}
7139
+ ...historyPlane ? { historyPlane } : {},
7140
+ ...embeddingRuntime ? { embedding: embeddingRuntime } : {}
6438
7141
  },
6439
7142
  validationResults.brokenDeliverables
6440
7143
  );
@@ -6994,13 +7697,23 @@ function createOtelAgentTaskTraceClient(config) {
6994
7697
  if (params.provider) {
6995
7698
  span.setAttribute("gen_ai.provider.name", params.provider);
6996
7699
  }
7700
+ if (params.api_provider) {
7701
+ span.setAttribute("apo.llm.api_provider", params.api_provider);
7702
+ }
7703
+ if (params.serving_host) {
7704
+ span.setAttribute("apo.llm.serving_host", params.serving_host);
7705
+ }
7706
+ if (params.observation_type === "GENERATION") {
7707
+ span.setAttribute("apo.llm.request_kind", "request");
7708
+ }
6997
7709
  if (params.route) {
6998
7710
  span.setAttribute("apo.llm.route", params.route);
6999
7711
  }
7000
7712
  const isTool = params.observation_type === "TOOL";
7713
+ const usesToolChannels = isTool || params.observation_type === "RETRIEVER";
7001
7714
  if (params.input) {
7002
- if (isTool) {
7003
- if (params.tool_name) {
7715
+ if (usesToolChannels) {
7716
+ if (isTool && params.tool_name) {
7004
7717
  span.setAttribute("gen_ai.tool.name", params.tool_name);
7005
7718
  }
7006
7719
  span.setAttribute("gen_ai.tool.call.arguments", JSON.stringify(params.input));
@@ -7034,7 +7747,7 @@ function createOtelAgentTaskTraceClient(config) {
7034
7747
  const latency = params.latency_ms ?? round32(monotonicNowMs2() - active.startedAt);
7035
7748
  active.span.setAttribute("latency_ms", latency);
7036
7749
  if (params.output !== void 0) {
7037
- if (active.observationType === "TOOL") {
7750
+ if (active.observationType === "TOOL" || active.observationType === "RETRIEVER") {
7038
7751
  const payload = params.output && typeof params.output === "object" ? params.output : { value: params.output };
7039
7752
  active.span.setAttribute("gen_ai.tool.call.result", JSON.stringify(payload));
7040
7753
  } else if (params.output.text) {
@@ -7093,6 +7806,12 @@ function createOtelAgentTaskTraceClient(config) {
7093
7806
  if (params.provider) {
7094
7807
  active.span.setAttribute("gen_ai.provider.name", params.provider);
7095
7808
  }
7809
+ if (params.serving_host) {
7810
+ active.span.setAttribute("apo.llm.serving_host", params.serving_host);
7811
+ }
7812
+ if (params.api_provider) {
7813
+ active.span.setAttribute("apo.llm.api_provider", params.api_provider);
7814
+ }
7096
7815
  if (params.route) {
7097
7816
  active.span.setAttribute("apo.llm.route", params.route);
7098
7817
  }
@@ -7129,6 +7848,9 @@ function createOtelAgentTaskTraceClient(config) {
7129
7848
  if (params.tags?.length) {
7130
7849
  rootSpan.setAttribute("apo.run.tags", JSON.stringify(params.tags));
7131
7850
  }
7851
+ if (params.environment) {
7852
+ rootSpan.setAttribute("apo.run.environment", params.environment);
7853
+ }
7132
7854
  const rootMeta = params.rootSpan?.metadata;
7133
7855
  const taskRunId = params.taskRunId ?? rootMeta?.["apo.task.run.id"] ?? params.run_metadata?.["agent_task_run_id"];
7134
7856
  if (taskRunId) {
@@ -7424,15 +8146,55 @@ async function loadTaskRuntime(_taskDir) {
7424
8146
  judge: resolveJudgeFromEnv(),
7425
8147
  // Env/file layer for MCP evidence servers (APO_JUDGE_MCP) — seeds the
7426
8148
  // run-level config, so explicit runTask({ judgeTools }) always wins.
7427
- judgeTools: await resolveJudgeToolsFromEnv()
8149
+ judgeTools: await resolveJudgeToolsFromEnv(),
8150
+ embedding: resolveEmbeddingFromEnv()
7428
8151
  };
7429
8152
  }
8153
+ function resolveEmbeddingFromEnv() {
8154
+ const model = process.env.APO_EMBEDDING_MODEL?.trim();
8155
+ if (model) {
8156
+ return {
8157
+ model,
8158
+ ...process.env.APO_EMBEDDING_BASE_URL ? { baseURL: process.env.APO_EMBEDDING_BASE_URL } : {},
8159
+ ...process.env.APO_EMBEDDING_API_KEY ? { apiKey: process.env.APO_EMBEDDING_API_KEY } : {},
8160
+ ...process.env.APO_EMBEDDING_DIMENSIONS ? { dimensions: parseDimensions(process.env.APO_EMBEDDING_DIMENSIONS) } : {}
8161
+ };
8162
+ }
8163
+ if (process.env.OPENROUTER_API_KEY) {
8164
+ return {
8165
+ model: "openai/text-embedding-3-small",
8166
+ // The endpoint is named here, never defaulted at request time — a
8167
+ // host is only ever contacted because a layer resolved it.
8168
+ baseURL: process.env.OPENROUTER_BASE_URL ?? "https://openrouter.ai/api/v1",
8169
+ apiKey: process.env.OPENROUTER_API_KEY
8170
+ };
8171
+ }
8172
+ if (process.env.OPENAI_API_KEY) {
8173
+ return {
8174
+ model: "text-embedding-3-small",
8175
+ apiKey: process.env.OPENAI_API_KEY,
8176
+ baseURL: process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"
8177
+ };
8178
+ }
8179
+ return void 0;
8180
+ }
8181
+ function parseDimensions(raw) {
8182
+ const parsed = Number.parseInt(raw, 10);
8183
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
8184
+ }
7430
8185
  function resolveJudgeFromEnv() {
7431
- const openRouterModel = process.env.OPENROUTER_MODEL;
7432
- const openAIModel = process.env.OPENAI_MODEL;
7433
- const anthropicModel = process.env.ANTHROPIC_MODEL;
7434
- const model = openRouterModel ?? openAIModel ?? anthropicModel;
8186
+ const judgeOverride = firstEnvValue("AGENT_TASK_JUDGE_MODEL");
8187
+ const openRouterModel = firstEnvValue("AGENT_TASK_OPENROUTER_MODEL", "OPENROUTER_MODEL");
8188
+ const openAIModel = firstEnvValue("OPENAI_MODEL");
8189
+ const anthropicModel = firstEnvValue("ANTHROPIC_MODEL", "AGENT_TASK_ANTHROPIC_MODEL");
8190
+ const model = judgeOverride ?? openRouterModel ?? openAIModel ?? anthropicModel;
7435
8191
  if (!model) return void 0;
8192
+ if (judgeOverride && !openRouterModel && !openAIModel && !anthropicModel) {
8193
+ return {
8194
+ model,
8195
+ ...process.env.APO_JUDGE_MODE?.trim() === "cascade" ? { mode: "cascade" } : {}
8196
+ };
8197
+ }
7436
8198
  return {
7437
8199
  model,
7438
8200
  baseURL: openRouterModel ? process.env.OPENROUTER_BASE_URL : openAIModel ? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1" : process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com",
@@ -7453,6 +8215,13 @@ function resolveJudgeFromEnv() {
7453
8215
  ...process.env.APO_JUDGE_MODE?.trim() === "cascade" ? { mode: "cascade" } : {}
7454
8216
  };
7455
8217
  }
8218
+ function firstEnvValue(...names) {
8219
+ for (const name of names) {
8220
+ const value = process.env[name]?.trim();
8221
+ if (value) return value;
8222
+ }
8223
+ return void 0;
8224
+ }
7456
8225
  async function runTaskDir(taskDir, options) {
7457
8226
  const [loaded, runtime] = await Promise.all([
7458
8227
  loadTask(taskDir),
@@ -7738,14 +8507,17 @@ export {
7738
8507
  compactChecksForSubmission,
7739
8508
  connectMcpServers,
7740
8509
  contentSha256,
8510
+ cosineSimilarity,
7741
8511
  createApoAnthropic,
7742
8512
  createApoOpenAI,
7743
8513
  createApoTracer,
8514
+ createEmbeddingClient,
7744
8515
  defineAdapter,
7745
8516
  defineTask,
7746
8517
  describe2 as describe,
7747
8518
  detectTranscriptSource,
7748
8519
  discoverAgentTaskDirs,
8520
+ embeddingSettingsSignature,
7749
8521
  equals,
7750
8522
  evaluateComputeExpression,
7751
8523
  expandSecretPlaceholders,
@@ -7777,6 +8549,7 @@ export {
7777
8549
  resetTaskRegistry,
7778
8550
  resetTaskSteers,
7779
8551
  resetTaskTurn,
8552
+ resolveEmbeddingConfig,
7780
8553
  resolveMcpServerPaths,
7781
8554
  resolveOtlpTracesUrl,
7782
8555
  resolveTurn,