@tangle-network/agent-eval 0.119.0 → 0.120.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +25 -3
  2. package/README.md +0 -26
  3. package/dist/analyst/index.d.ts +30 -39
  4. package/dist/analyst/index.js +9 -5
  5. package/dist/analyst/index.js.map +1 -1
  6. package/dist/benchmarks/index.d.ts +11 -5
  7. package/dist/benchmarks/index.js +7 -7
  8. package/dist/campaign/index.d.ts +121 -66
  9. package/dist/campaign/index.js +26 -10
  10. package/dist/{chunk-RLCJQ6VZ.js → chunk-32BZXMSO.js} +385 -120
  11. package/dist/chunk-32BZXMSO.js.map +1 -0
  12. package/dist/{chunk-XJ7JVCHB.js → chunk-3A246TSA.js} +2 -2
  13. package/dist/{chunk-LMJ2TGWJ.js → chunk-JM2SKQMS.js} +2 -2
  14. package/dist/{chunk-F6YUH3L4.js → chunk-JN2FCO5W.js} +14 -168
  15. package/dist/chunk-JN2FCO5W.js.map +1 -0
  16. package/dist/{chunk-D7AEXSM5.js → chunk-PICTDURQ.js} +2 -2
  17. package/dist/{chunk-6QIM2EAP.js → chunk-PXD6ZFNY.js} +99 -1
  18. package/dist/chunk-PXD6ZFNY.js.map +1 -0
  19. package/dist/{chunk-GERDAIAL.js → chunk-QWMPPZ3X.js} +3 -3
  20. package/dist/{chunk-7A4LIMMY.js → chunk-S5TT5R3L.js} +229 -97
  21. package/dist/chunk-S5TT5R3L.js.map +1 -0
  22. package/dist/{chunk-ZYHJNKI3.js → chunk-ULOKLHIQ.js} +42 -5
  23. package/dist/chunk-ULOKLHIQ.js.map +1 -0
  24. package/dist/{chunk-FIUFRXP3.js → chunk-WW2A73HW.js} +49 -91
  25. package/dist/chunk-WW2A73HW.js.map +1 -0
  26. package/dist/{chunk-PAHNGS65.js → chunk-XDIRG3TO.js} +175 -11
  27. package/dist/chunk-XDIRG3TO.js.map +1 -0
  28. package/dist/contract/index.d.ts +76 -43
  29. package/dist/contract/index.js +596 -32
  30. package/dist/contract/index.js.map +1 -1
  31. package/dist/index.d.ts +92 -75
  32. package/dist/index.js +38 -35
  33. package/dist/index.js.map +1 -1
  34. package/dist/openapi.json +1 -1
  35. package/dist/rl.d.ts +11 -5
  36. package/dist/{run-campaign-OBXSN5WK.js → run-campaign-HNFPJET4.js} +2 -2
  37. package/dist/traces.d.ts +39 -25
  38. package/dist/traces.js +3 -5
  39. package/docs/trace-analysis.md +6 -2
  40. package/package.json +2 -7
  41. package/dist/chunk-6QIM2EAP.js.map +0 -1
  42. package/dist/chunk-7A4LIMMY.js.map +0 -1
  43. package/dist/chunk-F6YUH3L4.js.map +0 -1
  44. package/dist/chunk-FIUFRXP3.js.map +0 -1
  45. package/dist/chunk-PAHNGS65.js.map +0 -1
  46. package/dist/chunk-RLCJQ6VZ.js.map +0 -1
  47. package/dist/chunk-ZYHJNKI3.js.map +0 -1
  48. package/dist/primeintellect/index.d.ts +0 -311
  49. package/dist/primeintellect/index.js +0 -348
  50. package/dist/primeintellect/index.js.map +0 -1
  51. /package/dist/{chunk-XJ7JVCHB.js.map → chunk-3A246TSA.js.map} +0 -0
  52. /package/dist/{chunk-LMJ2TGWJ.js.map → chunk-JM2SKQMS.js.map} +0 -0
  53. /package/dist/{chunk-D7AEXSM5.js.map → chunk-PICTDURQ.js.map} +0 -0
  54. /package/dist/{chunk-GERDAIAL.js.map → chunk-QWMPPZ3X.js.map} +0 -0
  55. /package/dist/{run-campaign-OBXSN5WK.js.map → run-campaign-HNFPJET4.js.map} +0 -0
@@ -8,9 +8,9 @@ import {
8
8
  CostLedger
9
9
  } from "./chunk-JHOJHHU7.js";
10
10
  import {
11
- TraceFileMissingError,
12
- buildTraceAnalystTools
13
- } from "./chunk-6QIM2EAP.js";
11
+ buildTraceAnalystTools,
12
+ runTraceAnalysisLoop
13
+ } from "./chunk-PXD6ZFNY.js";
14
14
  import {
15
15
  validateAgentProfileCell
16
16
  } from "./chunk-XJYR7XFV.js";
@@ -27,12 +27,14 @@ var configuredModels = /* @__PURE__ */ new WeakMap();
27
27
  function createAnalystAi(config) {
28
28
  const model = config.model.trim();
29
29
  if (!model) throw new TypeError("createAnalystAi: model must be a non-empty string");
30
- const service = ai({
30
+ const args = {
31
31
  name: config.provider ?? "openai",
32
32
  apiKey: config.apiKey,
33
- apiURL: config.baseUrl,
33
+ ...config.baseUrl ? { apiURL: config.baseUrl } : {},
34
+ ...config.headers ? { headers: config.headers } : {},
34
35
  config: { model }
35
- });
36
+ };
37
+ const service = ai(args);
36
38
  configuredModels.set(service, model);
37
39
  return service;
38
40
  }
@@ -589,9 +591,6 @@ That produced no valid findings. The report DOES describe issues \u2014 re-extra
589
591
  return { findings: [], outcome: "extraction_failed" };
590
592
  }
591
593
 
592
- // src/analyst/kind-factory.ts
593
- import { AxJSRuntime, agent } from "@ax-llm/ax";
594
-
595
594
  // src/analyst/ax-cost-service.ts
596
595
  function meterAxChatService(ai2, options) {
597
596
  assertPositiveInteger(options.maxOutputTokens, "maxOutputTokens");
@@ -699,10 +698,17 @@ function costReceiptFromAxResponse(response, fallbackModel) {
699
698
  validUsage(tokens.reasoningTokens) ? tokens.reasoningTokens : 0,
700
699
  thoughtsTokens
701
700
  );
702
- const outputTokens = tokens.completionTokens + thoughtsTokens;
701
+ const separateReasoningTokens = reasoningTokens > tokens.completionTokens ? reasoningTokens : 0;
702
+ const additionalOutputTokens = Math.max(thoughtsTokens, separateReasoningTokens);
703
+ const outputTokens = tokens.completionTokens + additionalOutputTokens;
703
704
  const directTotal = tokens.promptTokens + tokens.completionTokens;
704
- const classifiedTotal = directTotal + thoughtsTokens + totalCacheTokens;
705
- if (reasoningTokens > outputTokens || tokens.totalTokens !== directTotal && tokens.totalTokens !== classifiedTotal) {
705
+ const validTotals = /* @__PURE__ */ new Set([
706
+ directTotal,
707
+ directTotal + totalCacheTokens,
708
+ directTotal + additionalOutputTokens,
709
+ directTotal + totalCacheTokens + additionalOutputTokens
710
+ ]);
711
+ if (!validTotals.has(tokens.totalTokens)) {
706
712
  return {
707
713
  model,
708
714
  inputTokens: 0,
@@ -751,7 +757,8 @@ function containsUnboundedOrCacheableContent(request) {
751
757
  if (request.functions?.some((fn) => fn.cache === true)) return true;
752
758
  return request.chatPrompt.some((message) => {
753
759
  if (message.cache === true) return true;
754
- return Array.isArray(message.content) && message.content.some((part) => part.cache === true || part.type !== "text");
760
+ if (!("content" in message) || !Array.isArray(message.content)) return false;
761
+ return message.content.some((part) => part.cache === true || part.type !== "text");
755
762
  });
756
763
  }
757
764
  function modelName(value) {
@@ -827,6 +834,7 @@ function validateUsageSettlementTimeout(timeoutMs) {
827
834
 
828
835
  // src/analyst/kind-factory.ts
829
836
  function createTraceAnalystKind(spec, opts) {
837
+ rejectRemovedKindOptions(spec);
830
838
  const version = opts.versionSuffix ? `${spec.version}+${opts.versionSuffix}` : spec.version;
831
839
  const model = resolveAnalystModel(opts.ai, opts.model);
832
840
  const minimumEvidenceCitations = spec.minimumEvidenceCitations ?? 1;
@@ -838,7 +846,7 @@ function createTraceAnalystKind(spec, opts) {
838
846
  id: spec.id,
839
847
  description: spec.description,
840
848
  inputKind: "trace-store",
841
- cost: spec.cost,
849
+ cost: { ...spec.cost, settlement_timeout_ms: settlementTimeoutMs },
842
850
  version,
843
851
  async analyze(store, ctx) {
844
852
  const maxOutputTokens = spec.maxOutputTokens ?? 4096;
@@ -859,62 +867,37 @@ function createTraceAnalystKind(spec, opts) {
859
867
  });
860
868
  try {
861
869
  const tools = spec.buildTools(store);
862
- const maxDepth = spec.recursion?.maxDepth ?? 0;
863
- const maxParallel = spec.recursion?.maxParallelSubagents ?? 2;
870
+ const maxSubqueries = spec.subqueries?.maxCalls ?? 0;
871
+ const maxParallel = spec.subqueries?.maxParallel ?? 2;
864
872
  const priorContext = renderPriorFindings(ctx.priorFindings);
865
873
  const upstreamContext = renderUpstreamFindings(ctx.upstreamFindings);
866
- const functions = tools;
867
874
  const actorDescription = spec.actorDescription.trim() + priorContext + upstreamContext + "\n\n" + RAW_FINDING_SCHEMA_PROMPT + (minimumEvidenceCitations > 1 ? `
868
875
 
869
- This kind requires at least ${minimumEvidenceCitations} evidence citations per finding; rows with fewer are rejected.` : "") + "\n\nFirst write `report`: a concise free-form prose diagnosis of what the traces show \u2014 what succeeded, what was suboptimal or failed \u2014 with concrete trace ids and numbers. THEN return the structured `findings` array (it MAY be empty when there is nothing to report). Call `final({ report, findings })` exactly once when done.";
870
- const ax = agent(
871
- "question:string -> report:string, findings:json[]",
872
- {
873
- agentIdentity: {
874
- name: spec.id,
875
- description: spec.description
876
- },
877
- contextFields: ["question"],
878
- runtime: new AxJSRuntime({
879
- permissions: [],
880
- blockDynamicImport: true,
881
- allowedModules: [],
882
- freezeIntrinsics: true,
883
- blockShadowRealm: true,
884
- preventGlobalThisExtensions: false
885
- }),
886
- mode: maxDepth > 0 ? "advanced" : "simple",
887
- recursionOptions: maxDepth > 0 ? { maxDepth } : void 0,
888
- maxTurns: spec.maxTurns ?? 12,
889
- maxRuntimeChars: spec.maxRuntimeChars ?? 6e3,
890
- maxBatchedLlmQueryConcurrency: maxParallel,
891
- promptLevel: "detailed",
892
- // Trace analysis depends on exact prior tool results and runtime variables.
893
- contextPolicy: { preset: "full", budget: "balanced" },
894
- functions,
895
- actorOptions: {
896
- description: actorDescription,
897
- model,
898
- showThoughts: false,
899
- thinkingTokenBudget: "none"
900
- },
901
- responderOptions: {
902
- description: spec.responderDescription ?? "Pass through the actor's `report` prose verbatim, and format the `findings` array exactly as the actor produced it. Do not add, drop, or summarize entries.",
903
- model,
904
- showThoughts: false
905
- },
906
- bubbleErrors: [TraceFileMissingError]
907
- }
908
- );
876
+ This kind requires at least ${minimumEvidenceCitations} evidence citations per finding; rows with fewer are rejected.` : "") + "\n\nFirst write `report`: a concise free-form prose diagnosis of what the traces show \u2014 what succeeded, what was suboptimal or failed \u2014 with concrete trace ids and numbers. THEN return the structured `findings` array (it MAY be empty when there is nothing to report).";
909
877
  ctx.log?.(`analyst.kind ${spec.id} forward`, {
910
- max_depth: maxDepth,
878
+ max_subqueries: maxSubqueries,
911
879
  tool_count: tools.length,
912
880
  tags: ctx.tags
913
881
  });
914
- const result = await ax.forward(meteredAi, { question: deriveQuestion(ctx, spec) });
882
+ const completed = await runTraceAnalysisLoop({
883
+ id: spec.id,
884
+ description: spec.description,
885
+ prompt: actorDescription,
886
+ question: deriveQuestion(ctx, spec),
887
+ ai: meteredAi,
888
+ model,
889
+ tools,
890
+ findingType: "object",
891
+ maxSubqueries,
892
+ maxParallelSubqueries: maxParallel,
893
+ maxTurns: spec.maxTurns ?? 12,
894
+ maxRuntimeChars: spec.maxRuntimeChars ?? 6e3,
895
+ ...ctx.signal ? { signal: ctx.signal } : {}
896
+ });
897
+ const { report, findings: submittedFindings } = completed;
915
898
  const expectedSubjects = KIND_EXPECTED_SUBJECTS[spec.id];
916
899
  const out = [];
917
- const rawRows = Array.isArray(result.findings) ? result.findings : [];
900
+ const rawRows = submittedFindings;
918
901
  let rejectedWrongKind = 0;
919
902
  let rejectedInsufficientEvidence = 0;
920
903
  const processRow = (parsed) => {
@@ -973,7 +956,6 @@ This kind requires at least ${minimumEvidenceCitations} evidence citations per f
973
956
  rejected_wrong_subject: rejectedWrongKind,
974
957
  rejected_insufficient_evidence: rejectedInsufficientEvidence
975
958
  });
976
- const report = typeof result.report === "string" ? result.report : "";
977
959
  if (out.length === 0 && report.trim().length >= 200) {
978
960
  if (opts.recovery) {
979
961
  const wrongKindBefore = rejectedWrongKind;
@@ -1039,6 +1021,23 @@ This kind requires at least ${minimumEvidenceCitations} evidence citations per f
1039
1021
  }
1040
1022
  };
1041
1023
  }
1024
+ function rejectRemovedKindOptions(spec) {
1025
+ const supplied = spec;
1026
+ const migrations = [
1027
+ ["recursion", "subqueries"],
1028
+ ["responderDescription", "actorDescription"],
1029
+ ["maxDepth", "subqueries"],
1030
+ ["maxParallelSubagents", "subqueries.maxParallel"],
1031
+ ["subagentDescription", "actorDescription"]
1032
+ ];
1033
+ for (const [removed, replacement] of migrations) {
1034
+ if (removed in supplied) {
1035
+ throw new TypeError(
1036
+ `createTraceAnalystKind: '${removed}' is unsupported; use '${replacement}'`
1037
+ );
1038
+ }
1039
+ }
1040
+ }
1042
1041
  function deriveQuestion(ctx, spec) {
1043
1042
  const focus = ctx.tags?.focus?.trim();
1044
1043
  const task = `Analyze this trace dataset with the available tools and report ${spec.area} findings. ${spec.description}`;
@@ -1146,11 +1145,7 @@ DISCOVERY \u2192 CLUSTER \u2192 CITE protocol:
1146
1145
 
1147
1146
  If the dataset has no failures, return an empty findings array \u2014 do NOT pad with low-confidence speculation.
1148
1147
 
1149
- **Delegate aggressively.** The recursion budget is there to be used:
1150
- - After your first \`getDatasetOverview\` + \`queryTraces\` calls, you should have 3-6 candidate failure clusters in mind. Spawn one \`llmQuery\` per cluster in a single batch \u2014 they investigate in parallel.
1151
- - A sub-investigator that finds its cluster is actually two distinct modes should split again at its own level. Recursion is meant to discover sub-modes, not to do trivial drilling that the parent could do in-line.
1152
- - Pass narrow context to each subagent: { question: 'investigate the auth-revoked-mid-run cluster', context: { trace_ids: ['abc', 'def'], suspected_root_cause: 'token refresh skipped on idle sessions' } }. Subagents need enough context to skip re-discovery but not the whole conversation.
1153
- - Each subagent returns candidate cluster evidence; the parent merges it into the final finding set.
1148
+ **Use subqueries over loaded evidence.** After the first scan, load representative span excerpts for each candidate cluster. Then send one bounded \`llmQuery\` per cluster in one batch, including the exact excerpts and asking it to classify the root cause. Subqueries cannot call trace tools. Merge or split clusters yourself from their classifications and the cited source evidence.
1154
1149
 
1155
1150
  OBSERVABILITY rules:
1156
1151
  - Each non-final turn must emit at least one \`console.log\` for evidence.
@@ -1159,17 +1154,17 @@ var FAILURE_MODE_KIND_SPEC = {
1159
1154
  id: "failure-mode",
1160
1155
  description: "Clusters trace-dataset failures into distinct failure modes with cited evidence and a short recommended action.",
1161
1156
  area: "failure-mode",
1162
- version: "1.1.0",
1157
+ version: "1.2.0",
1163
1158
  actorDescription: ACTOR_PROMPT,
1164
1159
  buildTools: (store) => buildTraceToolsForGroup("all", store),
1165
- recursion: { maxDepth: 3, maxParallelSubagents: 4 },
1160
+ subqueries: { maxCalls: 8, maxParallel: 4 },
1166
1161
  maxTurns: 24,
1167
1162
  cost: { kind: "llm" }
1168
1163
  };
1169
1164
 
1170
1165
  // src/analyst/kinds/improvement.ts
1171
1166
  var subjectGrammar2 = findingSubjectGrammarPromptFor("improvement");
1172
- var ACTOR_PROMPT2 = `You are a recursive-self-improvement analyst. Your job is to propose **concrete, locus-named edits** the agent's runtime should adopt to fix the failure modes, knowledge gaps, and poisonings present in this dataset.
1167
+ var ACTOR_PROMPT2 = `You are a self-improvement analyst. Your job is to propose **concrete, locus-named edits** the agent's runtime should adopt to fix the failure modes, knowledge gaps, and poisonings present in this dataset.
1173
1168
 
1174
1169
  Upstream analysts have already classified the problems. Your job is to convert each problem into a *change to make* and grade its expected leverage. Each finding is one proposed edit.
1175
1170
 
@@ -1189,8 +1184,8 @@ DISCOVERY \u2192 CANDIDATE-FIXES \u2192 COMPETE \u2192 CITE protocol:
1189
1184
  - **Skill / MCP / hook / subagent** \u2014 change the reusable profile component responsible for the behavior
1190
1185
  - **Workflow / rollout policy** \u2014 change orchestration, budget, sampling, or stopping behavior
1191
1186
  - **Code** \u2014 change an implementation path when profile edits cannot repair the behavior
1192
- 3. **Compete candidate fixes via subagents.** For each failure cluster, spawn one \`llmQuery\` per candidate-fix axis you want to evaluate. Each subagent's job: simulate the fix on the cited traces and report (i) likely effect, (ii) side effects, (iii) implementation cost as small/medium/large. Pass the cluster's failing trace_ids and the candidate axis as context.
1193
- 4. After subagents return, **pick the winning candidate per cluster** based on (effect / cost) and emit ONE finding. Discard the losing candidates \u2014 the output is the recommendation, not the candidate set.
1187
+ 3. **Compare candidate fixes with bounded subqueries.** Load the representative failure excerpts, then send one \`llmQuery\` per candidate-fix axis the same evidence. Ask for likely effect, side effects, and implementation scope. Subqueries cannot call trace tools; trace ids alone are insufficient context.
1188
+ 4. After the comparisons return, **pick the winning candidate per cluster** based on expected effect and risk, then emit ONE finding. Keep the alternatives and rejection reasons in the rationale so the recommendation is auditable.
1194
1189
  5. **Cross-reference upstream findings.** Cite prior failure-mode or knowledge-gap findings as \`finding://<prior-finding-id>\`. This builds the dependency graph that lets the dashboard show "fix #X resolves failure modes A, B, C."
1195
1190
 
1196
1191
  For each winning recommendation, emit ONE finding. Use one exact locus from the subject grammar and state the edit in one sentence. Match leverage to the source failure's severity; use medium for quality-of-life changes and info for cleanup with no behavioral effect. Cite the targeted \`finding://<id>\` when available and the most representative span when useful. Quote the problem being fixed. Use confidence 0.85+ for a mechanical fix to a well-evidenced failure, 0.6-0.8 when judgment is required, and <0.5 for speculation. Explain in at most two sentences why this candidate beat its alternatives. The recommended action must be the literal diff, quoted replacement, tool description, or setting change.
@@ -1205,10 +1200,10 @@ var IMPROVEMENT_KIND_SPEC = {
1205
1200
  id: "improvement",
1206
1201
  description: "Converts upstream failure / gap / poisoning findings into concrete locus-named edits (prompt, tool-doc, RAG, scaffolding) with leverage grades.",
1207
1202
  area: "improvement",
1208
- version: "1.1.0",
1203
+ version: "1.2.0",
1209
1204
  actorDescription: ACTOR_PROMPT2,
1210
1205
  buildTools: (store) => buildTraceToolsForGroup("all", store),
1211
- recursion: { maxDepth: 3, maxParallelSubagents: 4 },
1206
+ subqueries: { maxCalls: 8, maxParallel: 4 },
1212
1207
  maxTurns: 30,
1213
1208
  maxRuntimeChars: 12e3,
1214
1209
  cost: { kind: "llm" }
@@ -1237,7 +1232,7 @@ DISCOVERY \u2192 ATTRIBUTE-TO-LAYER \u2192 CITE protocol:
1237
1232
  3. For each gap, identify the **layer of the runtime that should have prevented it** and use its exact locus from the subject grammar above.
1238
1233
  4. For each defensible gap, emit ONE finding. Use an exact locus from the subject grammar and name the missing or stale knowledge (for example, "wiki has no page on invoice line-item shape; agent re-derived it from raw spans"). Rate it high when it caused failure or a clarifying question, medium for unnecessary turns, and low for minor inefficiency. Cite the span where the question, correction, retrieval miss, or stale result surfaced and quote it exactly. Use confidence 0.85+ when the agent articulated the gap and 0.6-0.8 when inferred. Recommend a concrete wiki edit for an agent-knowledge locus or a prompt/tool-description edit otherwise.
1239
1234
 
1240
- **Delegate per layer.** After your first scan, you should have candidates spread across \`agent-knowledge:*\`, \`websearch:outdated\`, \`tool-doc:*\`, \`system-prompt:*\`, and \`memory:*\`. Spawn one \`llmQuery\` per layer in parallel \u2014 each subagent runs a focused detection (e.g. the \`agent-knowledge\` subagent looks for both missing-pages AND stale-pages; the \`websearch\` subagent looks specifically for date staleness signals; the \`tool-doc\` subagent looks for tool-call argument errors a fuller description would have prevented). Merge their evidence into the final finding set.
1235
+ **Compare layers over loaded evidence.** After the first scan, load the exact excerpts behind candidates across \`agent-knowledge:*\`, \`websearch:outdated\`, \`tool-doc:*\`, \`system-prompt:*\`, and \`memory:*\`. Use one bounded \`llmQuery\` per layer to classify those excerpts. Subqueries cannot call trace tools. Merge their classifications into the final finding set only when the source excerpts support them.
1241
1236
 
1242
1237
  Do NOT report a gap that the agent later recovered from cleanly within the same turn \u2014 that's resilience, not a gap. Cite the *non-recovery* version when both exist.
1243
1238
 
@@ -1247,10 +1242,10 @@ var KNOWLEDGE_GAP_KIND_SPEC = {
1247
1242
  id: "knowledge-gap",
1248
1243
  description: "Identifies missing or stale pieces of knowledge \u2014 primarily against the agent-knowledge wiki \u2014 and attributes each to the runtime layer (wiki page, claim, raw source, websearch, tool-doc, system-prompt, memory) that should have held it.",
1249
1244
  area: "knowledge-gap",
1250
- version: "1.1.0",
1245
+ version: "1.2.0",
1251
1246
  actorDescription: ACTOR_PROMPT3,
1252
1247
  buildTools: (store) => buildTraceToolsForGroup("discoveryAndSearch", store),
1253
- recursion: { maxDepth: 2, maxParallelSubagents: 4 },
1248
+ subqueries: { maxCalls: 5, maxParallel: 4 },
1254
1249
  maxTurns: 18,
1255
1250
  cost: { kind: "llm" }
1256
1251
  };
@@ -1277,7 +1272,7 @@ DISCOVERY \u2192 DUAL-VERIFY \u2192 CITE protocol:
1277
1272
  - Confirm the belief is actually false in this trace's own evidence (cite the span that contradicts it)
1278
1273
  Only emit a finding when both halves are nailed down. If you can only nail one, drop it \u2014 single-evidence poisoning findings are too speculative to be useful.
1279
1274
 
1280
- **Delegate the dual-verify.** Use the recursion budget so each candidate poisoning gets one subagent investigating "did the agent act?" and one investigating "is the belief false?". After your first scan, fire off N parallel \`llmQuery\` pairs (one cluster per pair). Subagents return their findings; you accept only the ones where BOTH halves of the pair were confirmed.
1275
+ **Independently assess both halves.** Load the action excerpt and contradicting excerpt yourself, then send bounded \`llmQuery\` calls the exact evidence for "did the agent act?" and "does the trace contradict the belief?" Subqueries cannot call trace tools. Accept a poisoning only when both assessments and the source excerpts support it.
1281
1276
 
1282
1277
  For each confirmed poisoning, emit ONE finding. Use the source of the false belief as the exact subject. State "agent believed X (from source S); trace evidence shows X is false." Rate it critical for a wrong user-visible action, high when caught internally after significant waste, and medium for inefficiency. Cite BOTH the action span and the contradicting span with exact quotes. Use confidence 0.85+ when both halves have exact quotes and 0.6-0.8 when one half is inferred. Recommend the literal source correction: update the wiki claim, invalidate and re-curate the raw source, or replace the stale prompt/tool instruction.
1283
1278
 
@@ -1289,10 +1284,10 @@ var KNOWLEDGE_POISONING_KIND_SPEC = {
1289
1284
  id: "knowledge-poisoning",
1290
1285
  description: "Identifies confident-but-wrong actions caused by stale memory, contradicting RAG, deprecated tool docs, or outdated system-prompt instructions.",
1291
1286
  area: "knowledge-poisoning",
1292
- version: "1.1.0",
1287
+ version: "1.2.0",
1293
1288
  actorDescription: ACTOR_PROMPT4,
1294
1289
  buildTools: (store) => buildTraceToolsForGroup("all", store),
1295
- recursion: { maxDepth: 2, maxParallelSubagents: 4 },
1290
+ subqueries: { maxCalls: 8, maxParallel: 4 },
1296
1291
  maxTurns: 20,
1297
1292
  minimumEvidenceCitations: 2,
1298
1293
  cost: { kind: "llm" }
@@ -1322,6 +1317,14 @@ var AnalystRegistry = class {
1322
1317
  if (!analyst.version) {
1323
1318
  throw new Error(`AnalystRegistry.register: analyst "${analyst.id}" must declare a version`);
1324
1319
  }
1320
+ if (analyst.cost.kind === "deterministic" && analyst.cost.settlement_timeout_ms !== void 0) {
1321
+ throw new TypeError(
1322
+ `AnalystRegistry.register: deterministic analyst "${analyst.id}" cannot declare settlement_timeout_ms`
1323
+ );
1324
+ }
1325
+ if (analyst.cost.settlement_timeout_ms !== void 0) {
1326
+ validateUsageSettlementTimeout(analyst.cost.settlement_timeout_ms);
1327
+ }
1325
1328
  this.analysts.set(analyst.id, analyst);
1326
1329
  }
1327
1330
  list() {
@@ -1356,7 +1359,10 @@ var AnalystRegistry = class {
1356
1359
  const hooks = this.options.hooks ?? {};
1357
1360
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1358
1361
  const started = Date.now();
1359
- const deadlineMs = runOpts.timeoutMs ? started + runOpts.timeoutMs : void 0;
1362
+ const timeoutMs = validateTimeout(runOpts.timeoutMs);
1363
+ const deadlineMs = timeoutMs === void 0 ? void 0 : started + timeoutMs;
1364
+ const timeoutSignal = timeoutMs === void 0 ? void 0 : AbortSignal.timeout(timeoutMs);
1365
+ const runSignal = combineAbortSignals(runOpts.signal, timeoutSignal);
1360
1366
  const selected = this.selectAnalysts(runOpts);
1361
1367
  const budget = runOpts.budget ?? this.options.defaultBudget;
1362
1368
  validateBudgetPolicy(budget);
@@ -1380,9 +1386,16 @@ var AnalystRegistry = class {
1380
1386
  }
1381
1387
  for (const analyst of selected) {
1382
1388
  const t0 = Date.now();
1389
+ if (runSignal?.aborted) {
1390
+ const summary2 = abortedBeforeStartSummary(analyst, runSignal);
1391
+ summaries.push(summary2);
1392
+ log(`[analyst] skip ${analyst.id} \u2014 run aborted`, { runId, reason: summary2.reason });
1393
+ yield { type: "analyst-skipped", summary: summary2 };
1394
+ continue;
1395
+ }
1383
1396
  const input = this.routeInput(analyst, inputs);
1384
1397
  if (input.kind === "missing") {
1385
- const summary = {
1398
+ const summary2 = {
1386
1399
  analyst_id: analyst.id,
1387
1400
  status: "skipped",
1388
1401
  reason: `missing input of kind '${analyst.inputKind}'`,
@@ -1391,10 +1404,13 @@ var AnalystRegistry = class {
1391
1404
  cost_usd: 0,
1392
1405
  usage: zeroUsage()
1393
1406
  };
1394
- summaries.push(summary);
1407
+ summaries.push(summary2);
1395
1408
  log(`[analyst] skip ${analyst.id} \u2014 missing input`, { runId, kind: analyst.inputKind });
1396
- await hooks.onAfterAnalyze?.({ analyst, summary, findings: [], runId });
1397
- yield { type: "analyst-skipped", summary };
1409
+ await waitForHook(
1410
+ hooks.onAfterAnalyze ? () => hooks.onAfterAnalyze?.({ analyst, summary: summary2, findings: [], runId }) : void 0,
1411
+ runSignal
1412
+ );
1413
+ yield { type: "analyst-skipped", summary: summary2 };
1398
1414
  continue;
1399
1415
  }
1400
1416
  const perBudget = allocateBudget(budget, {
@@ -1414,7 +1430,7 @@ var AnalystRegistry = class {
1414
1430
  chat: this.options.chat,
1415
1431
  tags: runOpts.tags,
1416
1432
  log: (msg, fields) => log(`[${analyst.id}] ${msg}`, { runId, correlationId, ...fields }),
1417
- signal: runOpts.signal,
1433
+ signal: runSignal,
1418
1434
  priorFindings: selectPriorFindings(runOpts.priorFindings, analyst.id),
1419
1435
  upstreamFindings: runOpts.chainFindings && allFindings.length > 0 ? [...allFindings] : void 0,
1420
1436
  recordUsage: (receipt) => {
@@ -1422,15 +1438,32 @@ var AnalystRegistry = class {
1422
1438
  usageReceipts.push(receipt);
1423
1439
  }
1424
1440
  };
1425
- await hooks.onBeforeAnalyze?.({ analyst, ctx, runId });
1441
+ await waitForHook(
1442
+ hooks.onBeforeAnalyze ? () => hooks.onBeforeAnalyze?.({ analyst, ctx, runId }) : void 0,
1443
+ runSignal
1444
+ );
1445
+ if (runSignal?.aborted) {
1446
+ const summary2 = abortedBeforeStartSummary(analyst, runSignal, Date.now() - t0);
1447
+ summaries.push(summary2);
1448
+ log(`[analyst] skip ${analyst.id} \u2014 run aborted`, { runId, reason: summary2.reason });
1449
+ yield { type: "analyst-skipped", summary: summary2 };
1450
+ continue;
1451
+ }
1426
1452
  const effectiveBudget = validateEffectiveBudget(ctx.budgetUsd, remainingUsd, analyst.id);
1427
1453
  yield {
1428
1454
  type: "analyst-started",
1429
1455
  analyst_id: analyst.id,
1430
1456
  started_at: new Date(t0).toISOString()
1431
1457
  };
1458
+ let findings;
1459
+ let summary;
1432
1460
  try {
1433
- const findings = await analyst.analyze(input.value, ctx);
1461
+ if (runSignal?.aborted) throw abortReason(runSignal);
1462
+ findings = await waitForOperation(
1463
+ analyst.analyze(input.value, ctx),
1464
+ runSignal,
1465
+ analystAbortGraceMs(analyst)
1466
+ );
1434
1467
  const latency = Date.now() - t0;
1435
1468
  const usage = resolveUsage(analyst, findings, usageReceipts);
1436
1469
  const cost = knownCostUsd(usage);
@@ -1439,7 +1472,7 @@ var AnalystRegistry = class {
1439
1472
  remainingUsd = Math.max(0, remainingUsd - budgetDebit(usage, effectiveBudget));
1440
1473
  }
1441
1474
  allFindings.push(...findings);
1442
- const summary = {
1475
+ summary = {
1443
1476
  analyst_id: analyst.id,
1444
1477
  status: "ok",
1445
1478
  findings_count: findings.length,
@@ -1464,12 +1497,10 @@ var AnalystRegistry = class {
1464
1497
  cost_captured: false
1465
1498
  });
1466
1499
  }
1467
- await hooks.onAfterAnalyze?.({ analyst, summary, findings, runId });
1468
- yield { type: "analyst-completed", summary, findings };
1469
1500
  } catch (err) {
1470
1501
  const latency = Date.now() - t0;
1471
1502
  const e = err instanceof Error ? err : new Error(String(err));
1472
- const hookFindings = await hooks.onError?.({ analyst, error: e, runId }) ?? [];
1503
+ const hookFindings = runSignal?.aborted ? [] : await hooks.onError?.({ analyst, error: e, runId }) ?? [];
1473
1504
  if (hookFindings.length) allFindings.push(...hookFindings);
1474
1505
  const usage = resolveUsage(analyst, hookFindings, usageReceipts);
1475
1506
  const cost = knownCostUsd(usage);
@@ -1477,7 +1508,7 @@ var AnalystRegistry = class {
1477
1508
  if (typeof remainingUsd === "number") {
1478
1509
  remainingUsd = Math.max(0, remainingUsd - budgetDebit(usage, effectiveBudget));
1479
1510
  }
1480
- const summary = {
1511
+ const summary2 = {
1481
1512
  analyst_id: analyst.id,
1482
1513
  status: "failed",
1483
1514
  findings_count: hookFindings.length,
@@ -1486,7 +1517,7 @@ var AnalystRegistry = class {
1486
1517
  usage,
1487
1518
  error: { class: e.constructor.name, message: e.message }
1488
1519
  };
1489
- summaries.push(summary);
1520
+ summaries.push(summary2);
1490
1521
  log(`[analyst] FAIL ${analyst.id}`, {
1491
1522
  runId,
1492
1523
  error_class: e.constructor.name,
@@ -1501,9 +1532,18 @@ var AnalystRegistry = class {
1501
1532
  cost_captured: false
1502
1533
  });
1503
1534
  }
1504
- await hooks.onAfterAnalyze?.({ analyst, summary, findings: hookFindings, runId });
1505
- yield { type: "analyst-completed", summary, findings: hookFindings };
1535
+ await waitForHook(
1536
+ hooks.onAfterAnalyze ? () => hooks.onAfterAnalyze?.({ analyst, summary: summary2, findings: hookFindings, runId }) : void 0,
1537
+ runSignal
1538
+ );
1539
+ yield { type: "analyst-completed", summary: summary2, findings: hookFindings };
1540
+ continue;
1506
1541
  }
1542
+ await waitForHook(
1543
+ hooks.onAfterAnalyze ? () => hooks.onAfterAnalyze?.({ analyst, summary, findings, runId }) : void 0,
1544
+ runSignal
1545
+ );
1546
+ yield { type: "analyst-completed", summary, findings };
1507
1547
  }
1508
1548
  const result = {
1509
1549
  run_id: runId,
@@ -1517,7 +1557,10 @@ var AnalystRegistry = class {
1517
1557
  summaries.map((summary) => summary.usage?.cost ?? { kind: "uncaptured", usd: null })
1518
1558
  )
1519
1559
  };
1520
- await hooks.onComplete?.({ result });
1560
+ await waitForHook(
1561
+ hooks.onComplete ? () => hooks.onComplete?.({ result }) : void 0,
1562
+ runSignal
1563
+ );
1521
1564
  yield { type: "run-completed", result };
1522
1565
  }
1523
1566
  selectAnalysts(opts) {
@@ -1549,6 +1592,95 @@ var AnalystRegistry = class {
1549
1592
  }
1550
1593
  }
1551
1594
  };
1595
+ function validateTimeout(timeoutMs) {
1596
+ if (timeoutMs === void 0) return void 0;
1597
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) {
1598
+ throw new TypeError(
1599
+ "RegistryRunOpts.timeoutMs must be a positive safe integer no greater than 2147483647"
1600
+ );
1601
+ }
1602
+ return timeoutMs;
1603
+ }
1604
+ function combineAbortSignals(caller, timeout) {
1605
+ if (!caller) return timeout;
1606
+ if (!timeout) return caller;
1607
+ return AbortSignal.any([caller, timeout]);
1608
+ }
1609
+ async function waitForOperation(operation, signal, abortGraceMs) {
1610
+ if (!signal) return operation;
1611
+ if (signal.aborted) {
1612
+ void operation.catch(() => {
1613
+ });
1614
+ throw abortReason(signal);
1615
+ }
1616
+ return new Promise((resolve, reject) => {
1617
+ let settlementTimer;
1618
+ const cleanup = () => {
1619
+ signal.removeEventListener("abort", onAbort);
1620
+ if (settlementTimer) clearTimeout(settlementTimer);
1621
+ };
1622
+ const onAbort = () => {
1623
+ if (abortGraceMs === 0) {
1624
+ cleanup();
1625
+ reject(abortReason(signal));
1626
+ return;
1627
+ }
1628
+ settlementTimer = setTimeout(() => {
1629
+ cleanup();
1630
+ reject(abortReason(signal));
1631
+ }, abortGraceMs);
1632
+ };
1633
+ signal.addEventListener("abort", onAbort, { once: true });
1634
+ operation.then(
1635
+ (value) => {
1636
+ cleanup();
1637
+ if (signal.aborted) reject(abortReason(signal));
1638
+ else resolve(value);
1639
+ },
1640
+ (error) => {
1641
+ cleanup();
1642
+ reject(signal.aborted ? abortReason(signal) : error);
1643
+ }
1644
+ );
1645
+ });
1646
+ }
1647
+ async function waitForHook(operation, signal) {
1648
+ if (operation === void 0 || signal?.aborted) return void 0;
1649
+ try {
1650
+ return await waitForOperation(
1651
+ Promise.resolve().then(() => {
1652
+ if (signal?.aborted) throw abortReason(signal);
1653
+ return operation();
1654
+ }),
1655
+ signal,
1656
+ 0
1657
+ );
1658
+ } catch (error) {
1659
+ if (signal?.aborted) return void 0;
1660
+ throw error;
1661
+ }
1662
+ }
1663
+ function analystAbortGraceMs(analyst) {
1664
+ if (analyst.cost.kind === "deterministic") return 0;
1665
+ const settlementMs = validateUsageSettlementTimeout(analyst.cost.settlement_timeout_ms);
1666
+ if (settlementMs === 0) return 0;
1667
+ return Math.min(settlementMs + 100, 2147483647);
1668
+ }
1669
+ function abortedBeforeStartSummary(analyst, signal, latencyMs = 0) {
1670
+ const reason = abortReason(signal);
1671
+ return {
1672
+ analyst_id: analyst.id,
1673
+ status: "skipped",
1674
+ reason: `${reason.name}: ${reason.message}`,
1675
+ findings_count: 0,
1676
+ latency_ms: latencyMs,
1677
+ cost_usd: 0,
1678
+ usage: zeroUsage()
1679
+ };
1680
+ }
1681
+ function abortReason(signal) {
1682
+ return signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
1683
+ }
1552
1684
  function allocateBudget(policy, args) {
1553
1685
  if (!policy) return void 0;
1554
1686
  if (policy.allocate) {
@@ -2533,4 +2665,4 @@ export {
2533
2665
  aggregateRunScore,
2534
2666
  clamp012 as clamp01
2535
2667
  };
2536
- //# sourceMappingURL=chunk-7A4LIMMY.js.map
2668
+ //# sourceMappingURL=chunk-S5TT5R3L.js.map