@inflexa-ai/harness 0.1.1 → 0.2.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 (268) hide show
  1. package/dist/agents/conversation-agent.d.ts +12 -11
  2. package/dist/agents/conversation-agent.js +30 -33
  3. package/dist/agents/report-builder.js +2 -1
  4. package/dist/agents/sandbox/bulk-transcriptomics-agent.js +1 -1
  5. package/dist/agents/sandbox/cheminformatics-agent.js +1 -19
  6. package/dist/agents/sandbox/chromatin-agent.js +1 -10
  7. package/dist/agents/sandbox/dna-methylation-agent.js +1 -10
  8. package/dist/agents/sandbox/drug-repurposing-agent.js +4 -14
  9. package/dist/agents/sandbox/enrichment-agent.js +1 -10
  10. package/dist/agents/sandbox/ephemeral-executor.js +1 -10
  11. package/dist/agents/sandbox/genomic-variant-agent.js +1 -3
  12. package/dist/agents/sandbox/immune-profiling-agent.js +1 -13
  13. package/dist/agents/sandbox/metabolomics-agent.js +1 -1
  14. package/dist/agents/sandbox/microbiome-agent.js +1 -1
  15. package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -12
  16. package/dist/agents/sandbox/multimodal-sc-agent.js +1 -1
  17. package/dist/agents/sandbox/network-agent.js +1 -10
  18. package/dist/agents/sandbox/pkpd-clinical-response-agent.js +2 -8
  19. package/dist/agents/sandbox/proteomics-agent.js +1 -1
  20. package/dist/agents/sandbox/scientific-executor.js +1 -10
  21. package/dist/agents/sandbox/shared.d.ts +17 -6
  22. package/dist/agents/sandbox/shared.js +49 -38
  23. package/dist/agents/sandbox/single-cell-agent.js +1 -1
  24. package/dist/agents/sandbox/spatial-omics-agent.js +1 -1
  25. package/dist/agents/sandbox/statistical-modeling-agent.js +1 -1
  26. package/dist/agents/sandbox/translational-safety-agent.js +3 -9
  27. package/dist/agents/sandbox/types.d.ts +1 -1
  28. package/dist/agents/system-prompt.d.ts +37 -11
  29. package/dist/agents/system-prompt.js +35 -12
  30. package/dist/app/data-profile-orientation.d.ts +34 -0
  31. package/dist/app/data-profile-orientation.js +117 -0
  32. package/dist/contracts/schemas/chat-parts.d.ts +9 -9
  33. package/dist/execution/artifact-metadata.js +2 -1
  34. package/dist/execution/ephemeral-runner.d.ts +13 -0
  35. package/dist/execution/ephemeral-runner.js +23 -1
  36. package/dist/execution/run-synthesis.js +20 -19
  37. package/dist/execution/step-summary.js +2 -1
  38. package/dist/index.d.ts +6 -2
  39. package/dist/index.js +19 -5
  40. package/dist/loop/__fixtures__/scripted-provider.d.ts +2 -2
  41. package/dist/loop/__fixtures__/scripted-provider.js +2 -1
  42. package/dist/loop/metrics.d.ts +33 -1
  43. package/dist/loop/metrics.js +56 -2
  44. package/dist/loop/run-agent.d.ts +13 -1
  45. package/dist/loop/run-agent.js +19 -4
  46. package/dist/memory/card-builders.d.ts +8 -1
  47. package/dist/memory/card-builders.js +20 -3
  48. package/dist/memory/reconstruct-cards.js +4 -3
  49. package/dist/memory/thread-history.d.ts +27 -2
  50. package/dist/memory/thread-history.js +45 -5
  51. package/dist/memory/working-memory.d.ts +93 -10
  52. package/dist/memory/working-memory.js +220 -84
  53. package/dist/prompts/SOUL.d.ts +22 -9
  54. package/dist/prompts/SOUL.js +56 -37
  55. package/dist/prompts/analogical-reasoner.d.ts +1 -1
  56. package/dist/prompts/analogical-reasoner.js +2 -2
  57. package/dist/prompts/briefing.d.ts +100 -0
  58. package/dist/prompts/briefing.js +166 -0
  59. package/dist/prompts/conversation.d.ts +1 -1
  60. package/dist/prompts/conversation.js +94 -440
  61. package/dist/prompts/literature-reviewer.d.ts +1 -1
  62. package/dist/prompts/literature-reviewer.js +10 -10
  63. package/dist/prompts/planner.js +28 -59
  64. package/dist/prompts/report-builder.d.ts +1 -1
  65. package/dist/prompts/report-builder.js +71 -24
  66. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  67. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +2 -7
  68. package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
  69. package/dist/prompts/sandbox/cheminformatics-agent.js +11 -15
  70. package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
  71. package/dist/prompts/sandbox/chromatin-agent.js +3 -7
  72. package/dist/prompts/sandbox/data-profiler.d.ts +1 -1
  73. package/dist/prompts/sandbox/data-profiler.js +13 -8
  74. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  75. package/dist/prompts/sandbox/dna-methylation-agent.js +2 -7
  76. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  77. package/dist/prompts/sandbox/drug-repurposing-agent.js +15 -16
  78. package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
  79. package/dist/prompts/sandbox/enrichment-agent.js +2 -9
  80. package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
  81. package/dist/prompts/sandbox/genomic-variant-agent.js +2 -6
  82. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  83. package/dist/prompts/sandbox/immune-profiling-agent.js +3 -8
  84. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  85. package/dist/prompts/sandbox/metabolomics-agent.js +1 -7
  86. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  87. package/dist/prompts/sandbox/microbiome-agent.js +2 -7
  88. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  89. package/dist/prompts/sandbox/multi-omics-integration-agent.js +2 -8
  90. package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
  91. package/dist/prompts/sandbox/multimodal-sc-agent.js +2 -7
  92. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  93. package/dist/prompts/sandbox/network-agent.js +2 -7
  94. package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
  95. package/dist/prompts/sandbox/proteomics-agent.js +2 -9
  96. package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
  97. package/dist/prompts/sandbox/single-cell-agent.js +2 -9
  98. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  99. package/dist/prompts/sandbox/spatial-omics-agent.js +3 -6
  100. package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
  101. package/dist/prompts/sandbox/statistical-modeling-agent.js +2 -7
  102. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  103. package/dist/prompts/sandbox/translational-safety-agent.js +13 -14
  104. package/dist/prompts/sandbox-standards.d.ts +22 -14
  105. package/dist/prompts/sandbox-standards.js +91 -71
  106. package/dist/prompts/synthesis-agent.d.ts +1 -1
  107. package/dist/prompts/synthesis-agent.js +33 -131
  108. package/dist/provenance/collector.js +1 -1
  109. package/dist/providers/ai-sdk.js +31 -4
  110. package/dist/providers/prompt-cache.d.ts +74 -0
  111. package/dist/providers/prompt-cache.js +77 -0
  112. package/dist/providers/types.d.ts +33 -0
  113. package/dist/reference-data/catalog.d.ts +131 -0
  114. package/dist/reference-data/catalog.js +348 -0
  115. package/dist/reference-data/receipt.d.ts +41 -0
  116. package/dist/reference-data/receipt.js +40 -0
  117. package/dist/schemas/data-profile-schemas.js +5 -3
  118. package/dist/schemas/plan-schemas.d.ts +21 -21
  119. package/dist/schemas/plan-schemas.js +9 -3
  120. package/dist/schemas/run-synthesis.d.ts +8 -4
  121. package/dist/schemas/run-synthesis.js +92 -56
  122. package/dist/schemas/validate-plan.js +1 -1
  123. package/dist/schemas/workflow-state.d.ts +7 -1
  124. package/dist/schemas/workflow-state.js +33 -19
  125. package/dist/state/artifacts.d.ts +13 -0
  126. package/dist/state/artifacts.js +27 -0
  127. package/dist/state/data-profile.d.ts +64 -5
  128. package/dist/state/index.d.ts +3 -3
  129. package/dist/state/index.js +1 -1
  130. package/dist/tasks/data-profile.d.ts +20 -2
  131. package/dist/tasks/data-profile.js +71 -22
  132. package/dist/tools/bio/chembl.d.ts +38 -0
  133. package/dist/tools/bio/chembl.js +138 -0
  134. package/dist/tools/bio/comptox.d.ts +50 -0
  135. package/dist/tools/bio/comptox.js +149 -0
  136. package/dist/tools/bio/get-impc-ko-profile.js +9 -6
  137. package/dist/tools/bio/index.d.ts +5 -17
  138. package/dist/tools/bio/index.js +5 -17
  139. package/dist/tools/bio/keys.d.ts +14 -9
  140. package/dist/tools/bio/keys.js +16 -16
  141. package/dist/tools/bio/opentargets.d.ts +38 -0
  142. package/dist/tools/bio/opentargets.js +89 -0
  143. package/dist/tools/bio/pubchem.d.ts +29 -0
  144. package/dist/tools/bio/pubchem.js +92 -0
  145. package/dist/tools/bio/pubmed.d.ts +51 -0
  146. package/dist/tools/bio/pubmed.js +100 -0
  147. package/dist/tools/bio/search-bgee-expression.js +12 -6
  148. package/dist/tools/bio/search-clinical-trials.js +16 -5
  149. package/dist/tools/bio/search-faers.js +8 -4
  150. package/dist/tools/bio/search-geo-datasets.js +20 -6
  151. package/dist/tools/bio/search-gwas-catalog.d.ts +1 -1
  152. package/dist/tools/bio/search-pharmgkb.js +10 -4
  153. package/dist/tools/display/index.d.ts +1 -0
  154. package/dist/tools/display/index.js +1 -0
  155. package/dist/tools/display/normalize-echart-spec.d.ts +61 -0
  156. package/dist/tools/display/normalize-echart-spec.js +211 -0
  157. package/dist/tools/display/show-user.d.ts +6 -1
  158. package/dist/tools/display/show-user.js +25 -12
  159. package/dist/tools/execute-plan.js +5 -2
  160. package/dist/tools/iterate-report.d.ts +168 -118
  161. package/dist/tools/iterate-report.js +334 -96
  162. package/dist/tools/lib/chembl-client.d.ts +12 -0
  163. package/dist/tools/lib/chembl-client.js +127 -0
  164. package/dist/tools/lib/ctx-ops.d.ts +224 -0
  165. package/dist/tools/lib/ctx-ops.js +450 -0
  166. package/dist/tools/lib/iuphar-client.js +10 -1
  167. package/dist/tools/lib/openfda-client.js +11 -3
  168. package/dist/tools/lib/pathway-client.js +10 -4
  169. package/dist/tools/lib/pubchem-ops.d.ts +74 -0
  170. package/dist/tools/lib/pubchem-ops.js +224 -0
  171. package/dist/tools/memory/update-working-memory.d.ts +45 -7
  172. package/dist/tools/memory/update-working-memory.js +111 -41
  173. package/dist/tools/report/index.d.ts +3 -2
  174. package/dist/tools/report/index.js +3 -2
  175. package/dist/tools/research/generate-analogy-report.js +25 -12
  176. package/dist/tools/research/generate-plan.d.ts +12 -5
  177. package/dist/tools/research/generate-plan.js +168 -37
  178. package/dist/tools/research/index.d.ts +1 -0
  179. package/dist/tools/research/index.js +1 -0
  180. package/dist/tools/research/inspect-data-profile.d.ts +85 -0
  181. package/dist/tools/research/inspect-data-profile.js +161 -0
  182. package/dist/tools/research/literature-reviewer.js +3 -4
  183. package/dist/tools/sandbox/list-available-packages.d.ts +79 -5
  184. package/dist/tools/sandbox/list-available-packages.js +152 -10
  185. package/dist/tools/sandbox/list-available-refs.d.ts +141 -5
  186. package/dist/tools/sandbox/list-available-refs.js +647 -134
  187. package/dist/tools/sandbox/report-blocker.d.ts +39 -9
  188. package/dist/tools/sandbox/report-blocker.js +43 -20
  189. package/dist/tools/workspace/read-file.js +4 -5
  190. package/dist/tools/workspace/run-exec.d.ts +4 -4
  191. package/dist/tools/workspace/run-exec.js +4 -4
  192. package/dist/tools/workspace/show-file.js +10 -6
  193. package/dist/tools/workspace/show-plan.js +12 -6
  194. package/dist/tools/workspace/workspace-search.d.ts +1 -1
  195. package/dist/tools/workspace/workspace-search.js +11 -5
  196. package/dist/workflows/execute-analysis.d.ts +43 -4
  197. package/dist/workflows/execute-analysis.js +107 -6
  198. package/dist/workflows/target-assessment/lib/citation-validator.js +2 -2
  199. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.js +2 -1
  200. package/dist/workspace/paths.d.ts +2 -2
  201. package/dist/workspace/paths.js +2 -2
  202. package/package.json +33 -33
  203. package/dist/execution/filesystem-artifact-registry.d.ts +0 -57
  204. package/dist/execution/filesystem-artifact-registry.js +0 -92
  205. package/dist/lib/identifiers.d.ts +0 -8
  206. package/dist/lib/identifiers.js +0 -10
  207. package/dist/lib/mime-detect.d.ts +0 -17
  208. package/dist/lib/mime-detect.js +0 -29
  209. package/dist/lib/streamed-download.d.ts +0 -28
  210. package/dist/lib/streamed-download.js +0 -111
  211. package/dist/lib/trace-context.d.ts +0 -11
  212. package/dist/lib/trace-context.js +0 -49
  213. package/dist/providers/llm-capabilities.d.ts +0 -33
  214. package/dist/providers/llm-capabilities.js +0 -58
  215. package/dist/schemas/render-step-prompt.d.ts +0 -32
  216. package/dist/schemas/render-step-prompt.js +0 -76
  217. package/dist/tools/bio/find-approval-precedent.d.ts +0 -32
  218. package/dist/tools/bio/find-approval-precedent.js +0 -72
  219. package/dist/tools/bio/get-article-details.d.ts +0 -11
  220. package/dist/tools/bio/get-article-details.js +0 -20
  221. package/dist/tools/bio/get-article-full-text.d.ts +0 -19
  222. package/dist/tools/bio/get-article-full-text.js +0 -32
  223. package/dist/tools/bio/get-bioactivity.d.ts +0 -11
  224. package/dist/tools/bio/get-bioactivity.js +0 -23
  225. package/dist/tools/bio/get-drug-info.d.ts +0 -9
  226. package/dist/tools/bio/get-drug-info.js +0 -22
  227. package/dist/tools/bio/get-mechanism.d.ts +0 -8
  228. package/dist/tools/bio/get-mechanism.js +0 -18
  229. package/dist/tools/bio/get-pubchem-assays.d.ts +0 -22
  230. package/dist/tools/bio/get-pubchem-assays.js +0 -107
  231. package/dist/tools/bio/get-pubchem-cross-refs.d.ts +0 -15
  232. package/dist/tools/bio/get-pubchem-cross-refs.js +0 -68
  233. package/dist/tools/bio/get-target-safety.d.ts +0 -18
  234. package/dist/tools/bio/get-target-safety.js +0 -26
  235. package/dist/tools/bio/search-compounds.d.ts +0 -22
  236. package/dist/tools/bio/search-compounds.js +0 -160
  237. package/dist/tools/bio/search-ctx-chemical.d.ts +0 -50
  238. package/dist/tools/bio/search-ctx-chemical.js +0 -130
  239. package/dist/tools/bio/search-ctx-exposure.d.ts +0 -55
  240. package/dist/tools/bio/search-ctx-exposure.js +0 -176
  241. package/dist/tools/bio/search-ctx-hazard.d.ts +0 -111
  242. package/dist/tools/bio/search-ctx-hazard.js +0 -178
  243. package/dist/tools/bio/search-opentargets.d.ts +0 -11
  244. package/dist/tools/bio/search-opentargets.js +0 -25
  245. package/dist/tools/bio/search-pubchem-compound.d.ts +0 -26
  246. package/dist/tools/bio/search-pubchem-compound.js +0 -110
  247. package/dist/tools/bio/search-pubmed.d.ts +0 -17
  248. package/dist/tools/bio/search-pubmed.js +0 -31
  249. package/dist/tools/bio/search-targets.d.ts +0 -9
  250. package/dist/tools/bio/search-targets.js +0 -20
  251. package/dist/tools/bio/search-toxcast.d.ts +0 -36
  252. package/dist/tools/bio/search-toxcast.js +0 -141
  253. package/dist/workflows/register-workflows.d.ts +0 -51
  254. package/dist/workflows/register-workflows.js +0 -48
  255. package/dist/workflows/resume-execute-analysis.d.ts +0 -70
  256. package/dist/workflows/resume-execute-analysis.js +0 -80
  257. package/dist/workflows/target-assessment/correction-loop.d.ts +0 -45
  258. package/dist/workflows/target-assessment/correction-loop.js +0 -62
  259. package/dist/workflows/target-assessment/steps/phase0-resolve.d.ts +0 -71
  260. package/dist/workflows/target-assessment/steps/phase0-resolve.js +0 -21
  261. package/dist/workflows/target-assessment/steps/phase1-aggregate.d.ts +0 -1018
  262. package/dist/workflows/target-assessment/steps/phase1-aggregate.js +0 -25
  263. package/dist/workspace/mount-strategy.d.ts +0 -50
  264. package/dist/workspace/mount-strategy.js +0 -67
  265. package/dist/workspace/provenance-collector.d.ts +0 -34
  266. package/dist/workspace/provenance-collector.js +0 -16
  267. package/dist/workspace/workspace-paths.d.ts +0 -26
  268. package/dist/workspace/workspace-paths.js +0 -31
@@ -72,6 +72,19 @@ export interface EphemeralResult {
72
72
  * so the `run_ephemeral` tool can dispatch via `DBOS.startWorkflow`.
73
73
  */
74
74
  export declare function registerEphemeralWorkflow(deps: EphemeralDeps): (input: EphemeralWorkflowInput) => Promise<EphemeralResult>;
75
+ /**
76
+ * The executor's seed: its workspace frame, then the caller's ask.
77
+ *
78
+ * An ephemeral run is not scheduled by `executeAnalysis`, so no step briefing is
79
+ * composed for it — and the sandbox system prompt names no paths (it is static
80
+ * per agent type, so the provider's prompt cache can reuse it). The analysis
81
+ * root would otherwise reach the agent nowhere: `run_ephemeral`'s `prompt` is
82
+ * free text authored by the conversation agent.
83
+ *
84
+ * The frame differs from a step's, so this does not reuse `renderWorkspace`:
85
+ * there is no writable directory to name. The read-only tree IS the cwd.
86
+ */
87
+ export declare function ephemeralSeed(analysisId: string, prompt: string): string;
75
88
  /**
76
89
  * Run the ephemeral-executor agent against a fresh read-only sandbox.
77
90
  * Throws on sandbox start failure; otherwise returns the agent's final
@@ -51,6 +51,28 @@ const EPHEMERAL_SANDBOX_RESOURCES = { cpu: 4, memoryGb: 8 };
51
51
  export function registerEphemeralWorkflow(deps) {
52
52
  return DBOS.registerWorkflow((input) => runEphemeralBody(input, deps), { name: "ephemeral" });
53
53
  }
54
+ /**
55
+ * The executor's seed: its workspace frame, then the caller's ask.
56
+ *
57
+ * An ephemeral run is not scheduled by `executeAnalysis`, so no step briefing is
58
+ * composed for it — and the sandbox system prompt names no paths (it is static
59
+ * per agent type, so the provider's prompt cache can reuse it). The analysis
60
+ * root would otherwise reach the agent nowhere: `run_ephemeral`'s `prompt` is
61
+ * free text authored by the conversation agent.
62
+ *
63
+ * The frame differs from a step's, so this does not reuse `renderWorkspace`:
64
+ * there is no writable directory to name. The read-only tree IS the cwd.
65
+ */
66
+ export function ephemeralSeed(analysisId, prompt) {
67
+ const root = `/${analysisId}`;
68
+ return [
69
+ "## Workspace",
70
+ `- Analysis root (read-only): \`${root}\` — inputs at \`${root}/data/inputs/\`, prior runs at \`${root}/runs/\`.`,
71
+ `- The analysis root is also your cwd, and what relative paths resolve against. The sandbox is read-only: there is nowhere to write, so return every result inline.`,
72
+ "",
73
+ prompt,
74
+ ].join("\n");
75
+ }
54
76
  /**
55
77
  * Run the ephemeral-executor agent against a fresh read-only sandbox.
56
78
  * Throws on sandbox start failure; otherwise returns the agent's final
@@ -116,7 +138,7 @@ export async function runEphemeralBody(input, deps) {
116
138
  const agentDef = createEphemeralExecutorAgent(sandboxAgentDeps);
117
139
  const agent = input.maxIterations !== undefined ? { ...agentDef, maxIterations: input.maxIterations } : agentDef;
118
140
  const signal = new AbortController().signal;
119
- const { messages: finalMessages } = await runAgent(agent, [{ role: "user", content: input.prompt }], childSession, {
141
+ const { messages: finalMessages } = await runAgent(agent, [{ role: "user", content: ephemeralSeed(analysisId, input.prompt) }], childSession, {
120
142
  provider: deps.provider,
121
143
  signal,
122
144
  emit: () => { },
@@ -25,10 +25,12 @@ import { z } from "zod";
25
25
  import { writeFileWithinRoot } from "../lib/fs-helpers.js";
26
26
  import { RunSynthesisSchema } from "../schemas/run-synthesis.js";
27
27
  import { synthesisAgentPrompt } from "../prompts/synthesis-agent.js";
28
+ import { composeSystemPrompt } from "../agents/system-prompt.js";
28
29
  import { runDir } from "../workspace/paths.js";
29
30
  import { runToTerminal } from "../loop/run-to-terminal.js";
30
31
  import { passthroughStep } from "../loop/run-step.js";
31
32
  import { defineTool } from "../tools/define-tool.js";
33
+ import { createReportBlockerToolFor } from "../tools/sandbox/report-blocker.js";
32
34
  import { createLiteratureReviewerTool } from "../tools/research/literature-reviewer.js";
33
35
  // ── Agent identity / budgets ────────────────────────────────────────
34
36
  const AGENT_ID = "run-synthesizer";
@@ -125,13 +127,14 @@ function fullyValidate(candidate, ctx) {
125
127
  function buildSubmitTool(holder, ctx) {
126
128
  return defineTool({
127
129
  id: "submit_synthesis",
128
- description: "Submit the final synthesis. Re-validates payload against the schema " +
129
- "and semantic checks (stepId refs, theme→finding refs, keyReferences " +
130
- "PMIDs cited, numeric PMIDs). On success returns {accepted: true} — " +
131
- "STOP after this. On rejection returns {accepted: false, issues} — " +
132
- "fix the specific fields at each issue path and call again, or switch " +
133
- "to report_blocker if the synthesis cannot be made valid.",
134
- inputSchema: z.object({ synthesis: z.unknown() }),
130
+ description: "Submit the final synthesis. Re-validates the payload against the " +
131
+ "semantic checks the arg schema cannot express (stepId refs, " +
132
+ "theme→finding refs, keyReferences PMIDs cited, numeric PMIDs). On " +
133
+ "success returns {accepted: true} — STOP after this. On rejection " +
134
+ "returns {accepted: false, issues} — fix the specific fields at each " +
135
+ "issue path and call again, or switch to report_blocker if the " +
136
+ "synthesis cannot be made valid.",
137
+ inputSchema: z.object({ synthesis: RunSynthesisSchema }),
135
138
  execute: async (input) => {
136
139
  if (holder.outcome !== null) {
137
140
  return ok({
@@ -155,18 +158,16 @@ function buildSubmitTool(holder, ctx) {
155
158
  });
156
159
  }
157
160
  function buildBlockerTool(holder) {
158
- return defineTool({
159
- id: "report_blocker",
160
- description: "Terminal. Use when the run produced no synthesizable content (all " +
161
- "summaries empty, contradictory to the point of incoherence, or no " +
162
- "findings worth surfacing). Pass a short reason. Stop after calling.",
163
- inputSchema: z.object({ reason: z.string().min(1) }),
164
- execute: async (input) => {
165
- if (holder.outcome === null) {
166
- holder.outcome = { kind: "blocker", reason: input.reason };
167
- }
168
- return ok({ recorded: true });
161
+ return createReportBlockerToolFor({
162
+ record: (outcome) => {
163
+ if (holder.outcome === null)
164
+ holder.outcome = outcome;
169
165
  },
166
+ blockedWhen: "Ends run synthesis with no synthesis written. Use it when the run " +
167
+ "produced no synthesizable content (all step summaries empty, " +
168
+ "contradictory to the point of incoherence, or no findings worth " +
169
+ "surfacing) — not as an escape from a synthesis you could fix and " +
170
+ "submit.",
170
171
  });
171
172
  }
172
173
  /**
@@ -207,7 +208,7 @@ export async function generateRunSynthesis(input) {
207
208
  const tools = [submitTool, blockerTool, reviewer];
208
209
  const agent = {
209
210
  id: AGENT_ID,
210
- systemPrompt: synthesisAgentPrompt,
211
+ systemPrompt: composeSystemPrompt(synthesisAgentPrompt),
211
212
  model: input.model,
212
213
  tools,
213
214
  maxIterations: SYNTHESIZER_MAX_ITERATIONS,
@@ -28,6 +28,7 @@ import { forSubAgent } from "../auth/types.js";
28
28
  import { finalText, runAgent } from "../loop/run-agent.js";
29
29
  import { passthroughStep } from "../loop/run-step.js";
30
30
  import { stepSummaryPrompt } from "../prompts/execute-analysis/step-summary.js";
31
+ import { composeSystemPrompt } from "../agents/system-prompt.js";
31
32
  import { createReadFileTool } from "../tools/workspace/read-file.js";
32
33
  import { StepSummarySchema } from "../schemas/step-summary.js";
33
34
  import { incrementSummaryNullCount } from "./step-summary-metrics.js";
@@ -72,7 +73,7 @@ export async function generateStepSummary(opts) {
72
73
  const userPrompt = stepSummaryPrompt(opts.artifactPaths.join("\n"));
73
74
  const writer = {
74
75
  id: SUMMARY_AGENT_ID,
75
- systemPrompt: SYSTEM_PROMPT,
76
+ systemPrompt: composeSystemPrompt(SYSTEM_PROMPT),
76
77
  model: opts.modelId,
77
78
  tools: [createReadFileTool(opts.workspaceFs, opts.workingDir)],
78
79
  maxIterations: opts.maxIterations ?? DEFAULT_MAX_ITERATIONS,
package/dist/index.d.ts CHANGED
@@ -39,7 +39,8 @@ export { createConfiguredAiSdkProvider } from "./providers/ai-sdk.js";
39
39
  export type { AiSdkProviderConfig, ConfiguredAiSdkProviderDeps } from "./providers/ai-sdk.js";
40
40
  export { createEmbeddingProvider } from "./providers/embedding.js";
41
41
  export type { EmbeddingProviderDeps } from "./providers/embedding.js";
42
- export type { AgentChat, ChatProvider, EmbeddingProvider, ChatRequest, ChatResponse, ChatStreamEvent, ModelMessage } from "./providers/types.js";
42
+ export type { AgentChat, ChatProvider, EmbeddingProvider, ChatRequest, ChatResponse, ChatStreamEvent, ChatUsage, ModelMessage, PromptCachePolicy, } from "./providers/types.js";
43
+ export { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions } from "./providers/prompt-cache.js";
43
44
  export { toProviderError, isProviderError } from "./providers/errors.js";
44
45
  export type { ProviderError } from "./providers/errors.js";
45
46
  export { createStreamingChat } from "./providers/streaming-chat.js";
@@ -94,7 +95,6 @@ export type { PlanStep } from "./workflows/execute-analysis-scheduler.js";
94
95
  export { validatePlan } from "./schemas/validate-plan.js";
95
96
  export type { ValidatePlanOptions } from "./schemas/validate-plan.js";
96
97
  export type { ValidationResult } from "./schemas/validate-plan.js";
97
- export { renderStepPrompt } from "./schemas/render-step-prompt.js";
98
98
  export { upsertPlan, loadPlan } from "./state/plans.js";
99
99
  export type { UpsertPlanInput } from "./state/plans.js";
100
100
  export { insertRun, queryActiveRun, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError } from "./state/runs.js";
@@ -102,6 +102,10 @@ export type { InsertRunInput } from "./state/runs.js";
102
102
  export { queryStepsByRun } from "./state/step-executions.js";
103
103
  export type { CortexRunRow, StepExecutionRow, RunStatus } from "./state/schema.js";
104
104
  export type { DbError } from "./lib/db-result.js";
105
+ export { REFERENCE_DATA_CATALOG, REFERENCE_DATA_CATALOG_VERSION, ReferenceArtifactPathSchema, ReferenceArtifactSchema, ReferenceDataCatalogSchema, ReferenceDatasetSchema, ReferenceSha256Schema, UnknownReferenceDatasetError, referenceArtifactKey, resolveReferenceInstallPlan, } from "./reference-data/catalog.js";
106
+ export type { ReferenceArtifact, ReferenceDataCatalog, ReferenceDataset, ReferenceInstallPlan, ReferenceInstallPlanDataset, ReferenceIntegrity, } from "./reference-data/catalog.js";
107
+ export { REFERENCE_INSTALL_RECEIPT_VERSION, ReferenceInstallReceiptSchema, parseReferenceInstallReceipt } from "./reference-data/receipt.js";
108
+ export type { ReferenceInstallReceipt, ReferenceReceiptArtifact } from "./reference-data/receipt.js";
105
109
  export { queryActiveSandboxes } from "./state/active-sandboxes.js";
106
110
  export { runStepDir } from "./workspace/paths.js";
107
111
  export { registerSandboxReaper } from "./sandbox/reaper.js";
package/dist/index.js CHANGED
@@ -43,6 +43,14 @@ export { createAnthropicProvider } from "./providers/anthropic.js";
43
43
  // above is a convenience over this union's `anthropic` arm.
44
44
  export { createConfiguredAiSdkProvider } from "./providers/ai-sdk.js";
45
45
  export { createEmbeddingProvider } from "./providers/embedding.js";
46
+ // Prompt caching. `PromptCachePolicy` is the harness's vendor-neutral cache
47
+ // directive; a composition root sets it per run via `RunAgentOptions.promptCache`
48
+ // (defaulting to `DEFAULT_PROMPT_CACHE` — 5m — when omitted, since an agent loop
49
+ // re-sends its prefix every iteration and breaks even immediately). Hosts whose
50
+ // endpoint ignores cache directives — notably the Claude Max OAuth path — pass
51
+ // `"off"`; the `cortex.harness.agent.cache_*_tokens` metrics show which case a
52
+ // deployment is actually in.
53
+ export { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions } from "./providers/prompt-cache.js";
46
54
  // Provider error channel. `ProviderError` is the value `chat`/`embed` fail
47
55
  // with; `toProviderError` is its sole constructor. Exposed so an embedder
48
56
  // realizing its own `ChatProvider`/`EmbeddingProvider` fails with the exact
@@ -129,20 +137,26 @@ export { registerSandboxStep } from "./workflows/sandbox-step.js";
129
137
  // `SandboxAgentDeps`, then selects the per-step agent by id from this record;
130
138
  // `SANDBOX_AGENT_META` is the planner-facing meta the same ids key.
131
139
  export { createSandboxAgents, SANDBOX_AGENT_META } from "./agents/sandbox/index.js";
132
- // Plan schema, structural validation, and per-step prompt rendering — the gates
133
- // a plan passes before it can trigger a run, plus the prompt each step's agent
134
- // receives. `PlanStep` is the minimal `{ id, depends_on }` element type that
140
+ // Plan schema and structural validation — the gates a plan passes before it can
141
+ // trigger a run. `PlanStep` is the minimal `{ id, depends_on }` element type that
135
142
  // `ExecuteAnalysisInput.steps` is typed against; a parsed `AnalysisPlan`'s
136
- // richer `AnalysisStep[]` is assignable to it.
143
+ // richer `AnalysisStep[]` is assignable to it, and is also what
144
+ // `ExecuteAnalysisInput.planStepById` carries. There is no prompt renderer here:
145
+ // an embedder supplies the plan's step DATA and the harness composes each step's
146
+ // seed at dispatch, when the step's dependencies have actually produced something.
137
147
  export { AnalysisPlanSchema } from "./schemas/workflow-state.js";
138
148
  export { validatePlan } from "./schemas/validate-plan.js";
139
- export { renderStepPrompt } from "./schemas/render-step-prompt.js";
140
149
  // Plan + run + step ledgers around a trigger. `upsertPlan` takes a
141
150
  // caller-derived id and inserts-if-absent (the deterministic-id intake path);
142
151
  // the run-state calls mirror the chat trigger's reserve → dedup → status flow.
143
152
  export { upsertPlan, loadPlan } from "./state/plans.js";
144
153
  export { insertRun, queryActiveRun, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError } from "./state/runs.js";
145
154
  export { queryStepsByRun } from "./state/step-executions.js";
155
+ // Host-neutral reference-data catalog and shared installation receipts. Each
156
+ // artifact names the third-party upstream it is fetched from; embedders own all
157
+ // transfer/storage behavior but never choose a different source.
158
+ export { REFERENCE_DATA_CATALOG, REFERENCE_DATA_CATALOG_VERSION, ReferenceArtifactPathSchema, ReferenceArtifactSchema, ReferenceDataCatalogSchema, ReferenceDatasetSchema, ReferenceSha256Schema, UnknownReferenceDatasetError, referenceArtifactKey, resolveReferenceInstallPlan, } from "./reference-data/catalog.js";
159
+ export { REFERENCE_INSTALL_RECEIPT_VERSION, ReferenceInstallReceiptSchema, parseReferenceInstallReceipt } from "./reference-data/receipt.js";
146
160
  // Backs `WatchdogDeps.queryActiveSandboxes` when the embedder wires the watchdog.
147
161
  export { queryActiveSandboxes } from "./state/active-sandboxes.js";
148
162
  // Workspace-tree path convention — the root-relative step directory each
@@ -1,4 +1,4 @@
1
- import type { ChatResponse, ChatProvider, ChatRequest } from "../../providers/types.js";
1
+ import type { ChatResponse, ChatProvider, ChatRequest, ChatUsage } from "../../providers/types.js";
2
2
  import type { AgentSession as Session } from "../../auth/types.js";
3
3
  export type TextBlock = {
4
4
  type: "text";
@@ -20,7 +20,7 @@ export type ToolUseBlock = {
20
20
  input: unknown;
21
21
  };
22
22
  export type ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock;
23
- export declare function makeMessage(content: ContentBlock[], finishReason: ChatResponse["finishReason"] | "tool_use" | "end_turn" | "max_tokens" | "refusal"): ChatResponse;
23
+ export declare function makeMessage(content: ContentBlock[], finishReason: ChatResponse["finishReason"] | "tool_use" | "end_turn" | "max_tokens" | "refusal", usage?: ChatUsage): ChatResponse;
24
24
  export declare function textBlock(text: string): TextBlock;
25
25
  export declare function thinkingBlock(thinking: string, signature: string): ThinkingBlock;
26
26
  export declare function toolUseBlock(id: string, name: string, input: unknown): ToolUseBlock;
@@ -13,10 +13,11 @@ function mapFinishReason(finishReason) {
13
13
  return finishReason;
14
14
  }
15
15
  }
16
- export function makeMessage(content, finishReason) {
16
+ export function makeMessage(content, finishReason, usage) {
17
17
  return {
18
18
  message: { role: "assistant", content },
19
19
  finishReason: mapFinishReason(finishReason),
20
+ usage,
20
21
  };
21
22
  }
22
23
  export function textBlock(text) {
@@ -1,21 +1,53 @@
1
1
  /**
2
2
  * Agent-loop OTel metrics → Middleware.io.
3
3
  *
4
- * Every `runAgent` completion records two signals:
4
+ * Every `runAgent` completion records, keyed by `agent_id`:
5
5
  * - cortex.harness.agent.iterations — histogram of LLM iterations per run.
6
6
  * A creeping distribution is a leading indicator of prompt drift.
7
7
  * - cortex.harness.agent.cap_hits — counter, incremented once per run
8
8
  * that exhausted `maxIterations` and took the forced wrap-up path.
9
+ * - cortex.harness.agent.input_tokens / .output_tokens — counters, summed
10
+ * over every LLM call the run made (the wrap-up included).
11
+ * - cortex.harness.agent.cache_read_tokens — counter, prefix tokens served
12
+ * from the prompt cache.
13
+ * - cortex.harness.agent.cache_write_tokens — counter, prefix tokens written
14
+ * into it (billed at a premium; only pays off once something reads it).
15
+ *
16
+ * The two cache counters are what make prompt caching *observable*: the hit
17
+ * rate for an agent type is `cache_read_tokens / input_tokens` (the harness's
18
+ * `inputTokens` is the total billed prefix, cache reads included). A flat-zero
19
+ * `cache_read_tokens` against a non-zero `cache_write_tokens` means every write
20
+ * is being thrown away — either a cache defeater is shifting the prefix (see
21
+ * `providers/prompt-cache.ts`) or the endpoint ignores cache directives
22
+ * outright, as the Claude Max OAuth path does.
23
+ *
24
+ * Token counters are recorded only from what a provider actually reports; a
25
+ * provider that reports no usage contributes nothing rather than zero.
9
26
  *
10
27
  * Instruments are created lazily on first use so the loop binds to whatever
11
28
  * `MeterProvider` is registered globally at runtime (production: the OTLP
12
29
  * exporter from `harness/lib/otel.ts`; tests: an in-memory reader).
13
30
  */
31
+ import type { ChatUsage } from "../providers/types.js";
32
+ /**
33
+ * Token usage summed over every LLM call one `runAgent` made. Each field stays
34
+ * `undefined` until some call actually reports it, so "the provider told us
35
+ * nothing" never masquerades as a measured zero.
36
+ */
37
+ export interface AgentRunUsage {
38
+ inputTokens?: number;
39
+ outputTokens?: number;
40
+ cacheCreationInputTokens?: number;
41
+ cacheReadInputTokens?: number;
42
+ }
43
+ /** Fold one call's reported usage into a run's running total. */
44
+ export declare function addChatUsage(total: AgentRunUsage, usage: ChatUsage | undefined): void;
14
45
  /** Record one completed `runAgent` invocation. */
15
46
  export declare function recordAgentRun(run: {
16
47
  readonly agentId: string;
17
48
  readonly iterations: number;
18
49
  readonly cappedOut: boolean;
50
+ readonly usage?: AgentRunUsage;
19
51
  }): void;
20
52
  /**
21
53
  * Drop the memoized instruments so the next `recordAgentRun` rebinds to a
@@ -1,11 +1,28 @@
1
1
  /**
2
2
  * Agent-loop OTel metrics → Middleware.io.
3
3
  *
4
- * Every `runAgent` completion records two signals:
4
+ * Every `runAgent` completion records, keyed by `agent_id`:
5
5
  * - cortex.harness.agent.iterations — histogram of LLM iterations per run.
6
6
  * A creeping distribution is a leading indicator of prompt drift.
7
7
  * - cortex.harness.agent.cap_hits — counter, incremented once per run
8
8
  * that exhausted `maxIterations` and took the forced wrap-up path.
9
+ * - cortex.harness.agent.input_tokens / .output_tokens — counters, summed
10
+ * over every LLM call the run made (the wrap-up included).
11
+ * - cortex.harness.agent.cache_read_tokens — counter, prefix tokens served
12
+ * from the prompt cache.
13
+ * - cortex.harness.agent.cache_write_tokens — counter, prefix tokens written
14
+ * into it (billed at a premium; only pays off once something reads it).
15
+ *
16
+ * The two cache counters are what make prompt caching *observable*: the hit
17
+ * rate for an agent type is `cache_read_tokens / input_tokens` (the harness's
18
+ * `inputTokens` is the total billed prefix, cache reads included). A flat-zero
19
+ * `cache_read_tokens` against a non-zero `cache_write_tokens` means every write
20
+ * is being thrown away — either a cache defeater is shifting the prefix (see
21
+ * `providers/prompt-cache.ts`) or the endpoint ignores cache directives
22
+ * outright, as the Claude Max OAuth path does.
23
+ *
24
+ * Token counters are recorded only from what a provider actually reports; a
25
+ * provider that reports no usage contributes nothing rather than zero.
9
26
  *
10
27
  * Instruments are created lazily on first use so the loop binds to whatever
11
28
  * `MeterProvider` is registered globally at runtime (production: the OTLP
@@ -24,17 +41,54 @@ function getInstruments() {
24
41
  capHits: meter.createCounter("cortex.harness.agent.cap_hits", {
25
42
  description: "runAgent completions that exhausted maxIterations and took the " + "forced tool-less wrap-up path",
26
43
  }),
44
+ inputTokens: meter.createCounter("cortex.harness.agent.input_tokens", {
45
+ description: "Input tokens billed across every LLM call in a runAgent completion, cache reads included",
46
+ unit: "{token}",
47
+ }),
48
+ outputTokens: meter.createCounter("cortex.harness.agent.output_tokens", {
49
+ description: "Output tokens generated across every LLM call in a runAgent completion",
50
+ unit: "{token}",
51
+ }),
52
+ cacheReadTokens: meter.createCounter("cortex.harness.agent.cache_read_tokens", {
53
+ description: "Prompt-prefix tokens served from the provider's prompt cache",
54
+ unit: "{token}",
55
+ }),
56
+ cacheWriteTokens: meter.createCounter("cortex.harness.agent.cache_write_tokens", {
57
+ description: "Prompt-prefix tokens written into the provider's prompt cache",
58
+ unit: "{token}",
59
+ }),
27
60
  };
28
61
  }
29
62
  return instruments;
30
63
  }
64
+ /** Fold one call's reported usage into a run's running total. */
65
+ export function addChatUsage(total, usage) {
66
+ if (usage === undefined)
67
+ return;
68
+ for (const key of ["inputTokens", "outputTokens", "cacheCreationInputTokens", "cacheReadInputTokens"]) {
69
+ const value = usage[key];
70
+ if (value !== undefined)
71
+ total[key] = (total[key] ?? 0) + value;
72
+ }
73
+ }
31
74
  /** Record one completed `runAgent` invocation. */
32
75
  export function recordAgentRun(run) {
33
- const { iterations, capHits } = getInstruments();
76
+ const { iterations, capHits, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens } = getInstruments();
34
77
  const attributes = { agent_id: run.agentId };
35
78
  iterations.record(run.iterations, attributes);
36
79
  if (run.cappedOut)
37
80
  capHits.add(1, attributes);
81
+ const usage = run.usage;
82
+ if (usage === undefined)
83
+ return;
84
+ if (usage.inputTokens !== undefined)
85
+ inputTokens.add(usage.inputTokens, attributes);
86
+ if (usage.outputTokens !== undefined)
87
+ outputTokens.add(usage.outputTokens, attributes);
88
+ if (usage.cacheReadInputTokens !== undefined)
89
+ cacheReadTokens.add(usage.cacheReadInputTokens, attributes);
90
+ if (usage.cacheCreationInputTokens !== undefined)
91
+ cacheWriteTokens.add(usage.cacheCreationInputTokens, attributes);
38
92
  }
39
93
  /**
40
94
  * Drop the memoized instruments so the next `recordAgentRun` rebinds to a
@@ -1,6 +1,6 @@
1
1
  import { type FinishReason } from "ai";
2
2
  import type { AgentSession } from "../auth/types.js";
3
- import type { AgentChat } from "../providers/types.js";
3
+ import type { AgentChat, PromptCachePolicy } from "../providers/types.js";
4
4
  import type { AgentDefinition, EmitFn, LoopMessage, RunStep } from "./types.js";
5
5
  export interface AgentFinish {
6
6
  readonly reason: FinishReason | "max_iterations";
@@ -23,6 +23,18 @@ export interface RunAgentOptions {
23
23
  readonly runStep: RunStep;
24
24
  readonly formatStepName?: StepNameFormatter;
25
25
  readonly isFatalLoopError?: (err: unknown) => boolean;
26
+ /**
27
+ * Prompt-cache policy for every LLM call this run makes. Defaults to
28
+ * `DEFAULT_PROMPT_CACHE` (5m) — an agent loop always re-sends its prefix, so
29
+ * it breaks even by the second iteration. A host whose endpoint ignores or
30
+ * charges badly for cache directives passes `"off"`.
31
+ *
32
+ * The policy lives here, on the run, rather than on the provider, precisely
33
+ * so it applies to loops and *not* to the one-shot LLM calls made elsewhere
34
+ * (report generation, target-assessment steps): those would pay the
35
+ * cache-write premium for a cache nothing ever reads back.
36
+ */
37
+ readonly promptCache?: PromptCachePolicy;
26
38
  }
27
39
  export declare function runAgent(agent: AgentDefinition, initial: readonly LoopMessage[], session: AgentSession, opts: RunAgentOptions): Promise<RunAgentResult>;
28
40
  export declare function finalText(messages: readonly LoopMessage[]): string;
@@ -1,8 +1,9 @@
1
1
  import { jsonSchema, tool as aiTool } from "ai";
2
2
  import { classifyProviderError } from "../providers/errors.js";
3
+ import { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions } from "../providers/prompt-cache.js";
3
4
  import { resultStep } from "./run-step.js";
4
5
  import { isToolError } from "../tools/define-tool.js";
5
- import { recordAgentRun } from "./metrics.js";
6
+ import { addChatUsage, recordAgentRun } from "./metrics.js";
6
7
  const TRUNCATED_PROSE_STEER = "Your previous reply was cut off at the output-token limit; continue concisely, or finish via your terminal tool.";
7
8
  const TRUNCATED_TOOL_USE_ERROR = "Your previous tool call was cut off at the output-token limit and was not executed. Retry with a smaller payload, writing in incremental pieces.";
8
9
  export const DEFAULT_STEP_NAME_FORMATTER = {
@@ -37,14 +38,21 @@ export async function runAgent(agent, initial, session, opts) {
37
38
  });
38
39
  let iterations = 0;
39
40
  let truncationRecoveries = 0;
41
+ // Resolved once, not per iteration: an identical options object across every
42
+ // call is itself part of the cache contract — the request prefix has to be
43
+ // byte-identical to be read back.
44
+ const providerOptions = promptCacheProviderOptions(opts.promptCache ?? DEFAULT_PROMPT_CACHE);
45
+ const usage = {};
40
46
  for (let i = 0; i < agent.maxIterations; i++) {
41
47
  iterations = i + 1;
42
48
  const request = {
43
49
  system: agent.systemPrompt,
44
50
  messages,
45
51
  tools: toolDefs,
52
+ providerOptions,
46
53
  };
47
54
  const reply = await resultStep(runStep)(formatStepName.llm(i), () => provider.chat(request, session, signal));
55
+ addChatUsage(usage, reply.usage);
48
56
  messages.push(reply.message);
49
57
  const toolCalls = toolCallParts(reply.message);
50
58
  if (reply.finishReason === "length") {
@@ -76,7 +84,7 @@ export async function runAgent(agent, initial, session, opts) {
76
84
  }
77
85
  if (reply.finishReason !== "tool-calls") {
78
86
  await emit({ type: "iteration", source, index: i, final: true });
79
- recordAgentRun({ agentId: agent.id, iterations, cappedOut: false });
87
+ recordAgentRun({ agentId: agent.id, iterations, cappedOut: false, usage });
80
88
  return { messages, finish: { reason: reply.finishReason, cappedOut: false, truncationRecoveries } };
81
89
  }
82
90
  await emit({ type: "iteration", source, index: i, final: false });
@@ -96,10 +104,17 @@ export async function runAgent(agent, initial, session, opts) {
96
104
  }
97
105
  messages.push({ role: "tool", content: results });
98
106
  }
99
- const wrapUp = await resultStep(runStep)(formatStepName.llm(agent.maxIterations), () => provider.chat({ system: agent.systemPrompt, messages, tools: {}, toolChoice: "none" }, session, signal));
107
+ // Cache defeater (known; not fixed here). Emptying the tool set changes the
108
+ // very front of the request prefix — tool definitions are cached ahead of
109
+ // system and history — so this call reads *nothing* back from the cache and
110
+ // rewrites the whole prefix from scratch. It still carries the cache options
111
+ // because it is the one call whose write is pure waste, and the
112
+ // cache_write_tokens counter is what makes that waste visible.
113
+ const wrapUp = await resultStep(runStep)(formatStepName.llm(agent.maxIterations), () => provider.chat({ system: agent.systemPrompt, messages, tools: {}, toolChoice: "none", providerOptions }, session, signal));
114
+ addChatUsage(usage, wrapUp.usage);
100
115
  messages.push(wrapUp.message);
101
116
  await emit({ type: "iteration", source, index: agent.maxIterations, final: true });
102
- recordAgentRun({ agentId: agent.id, iterations, cappedOut: true });
117
+ recordAgentRun({ agentId: agent.id, iterations, cappedOut: true, usage });
103
118
  return { messages, finish: { reason: "max_iterations", cappedOut: true, truncationRecoveries } };
104
119
  }
105
120
  function toolCallParts(message) {
@@ -63,7 +63,14 @@ export declare function buildRunCardData(pool: Pool, opts: {
63
63
  runId?: string;
64
64
  }): ResultAsync<RunCardData | null, DbError>;
65
65
  /** Build the `data-presentation` payload from the `show_user` tool input.
66
- * Self-contained — the card lives entirely in the input. */
66
+ * Self-contained — the card lives entirely in the input.
67
+ *
68
+ * An `echart` spec passes through `normalizeEchartSpec` HERE rather than in the tool body, because
69
+ * this builder is the single construction site of a `PresentationContent`: normalizing in
70
+ * `show_user.execute` alone would leave the reconstruct-on-read path (which sees only the persisted
71
+ * raw `tool_use` input) rendering the un-normalized chart, breaking the byte-identical live/replay
72
+ * card this module exists to guarantee. The `id` stays keyed to the raw input, which both paths
73
+ * hold identically, so normalization cannot move a card's identity. */
67
74
  export declare function buildPresentationCardData(input: Record<string, unknown>): PresentationCardData | null;
68
75
  /** Max files a `show_file` group may reference; multiple render as a gallery. */
69
76
  export declare const MAX_FILES = 10;
@@ -23,9 +23,13 @@ import { tryQuery } from "../lib/db-result.js";
23
23
  import { tryFs } from "../lib/fs-result.js";
24
24
  import { AnalysisPlanSchema } from "../schemas/workflow-state.js";
25
25
  import { loadPlan } from "../state/index.js";
26
+ import { normalizeEchartSpec } from "../tools/display/normalize-echart-spec.js";
26
27
  import { validatePath } from "../tools/lib/path-validation.js";
27
28
  import { latestPreviewVersion, previewDir, PREVIEWS_ROOT } from "../workspace/paths.js";
28
29
  const PREVIEW_META_FILE = "preview-meta.json";
30
+ function isRecord(value) {
31
+ return typeof value === "object" && value !== null && !Array.isArray(value);
32
+ }
29
33
  /** Deterministic presentation id — must match `show_user`'s. */
30
34
  function presentationId(input) {
31
35
  const hash = createHash("sha256")
@@ -101,15 +105,28 @@ export function buildRunCardData(pool, opts) {
101
105
  });
102
106
  }
103
107
  /** Build the `data-presentation` payload from the `show_user` tool input.
104
- * Self-contained — the card lives entirely in the input. */
108
+ * Self-contained — the card lives entirely in the input.
109
+ *
110
+ * An `echart` spec passes through `normalizeEchartSpec` HERE rather than in the tool body, because
111
+ * this builder is the single construction site of a `PresentationContent`: normalizing in
112
+ * `show_user.execute` alone would leave the reconstruct-on-read path (which sees only the persisted
113
+ * raw `tool_use` input) rendering the un-normalized chart, breaking the byte-identical live/replay
114
+ * card this module exists to guarantee. The `id` stays keyed to the raw input, which both paths
115
+ * hold identically, so normalization cannot move a card's identity. */
105
116
  export function buildPresentationCardData(input) {
106
117
  const { kind, title, ...rest } = input;
107
118
  if (typeof kind !== "string")
108
119
  return null;
120
+ const cardTitle = typeof title === "string" ? title : undefined;
121
+ const content = kind === "echart" && isRecord(rest.spec)
122
+ ? { kind, ...rest, spec: normalizeEchartSpec(rest.spec, { title: cardTitle }) }
123
+ : // Every other kind (and an echart the model called without a `spec`) is carried through
124
+ // untouched — there is no layout to normalize, and no spec to invent.
125
+ { kind, ...rest };
109
126
  return {
110
127
  id: presentationId(input),
111
- ...(typeof title === "string" ? { title } : {}),
112
- content: { kind, ...rest },
128
+ ...(cardTitle !== undefined ? { title: cardTitle } : {}),
129
+ content,
113
130
  };
114
131
  }
115
132
  /** Max files a `show_file` group may reference; multiple render as a gallery. */
@@ -19,9 +19,10 @@ export function createCardResolver(pool, analysisId, workspaceRoot) {
19
19
  if (block.type !== "tool_use")
20
20
  return null;
21
21
  const input = (block.input ?? {});
22
- // The harness tool id is `iterate_report`; legacy transcripts carry
23
- // `iterateReport` — accept both.
24
- if (block.name === "iterate_report" || block.name === "iterateReport") {
22
+ // The report tool is `submit_report`; legacy transcripts carry the
23
+ // former ids `iterate_report` / `iterateReport` — accept all three. The
24
+ // brief-carrying field is still `report`, so the card reads identically.
25
+ if (block.name === "submit_report" || block.name === "iterate_report" || block.name === "iterateReport") {
25
26
  const report = (input.report ?? null);
26
27
  const card = await buildPreviewCardData(workspaceRoot, {
27
28
  previewId: typeof input.previewId === "string" ? input.previewId : undefined,
@@ -54,8 +54,11 @@ export interface ThreadHistory {
54
54
  */
55
55
  appendTurn(threadId: string, messages: readonly ModelMessage[]): ResultAsync<void, DbError>;
56
56
  /**
57
- * Return the most recent messages whose cumulative `tokens` fit
58
- * `tokenBudget`, oldest-first, snapped to a valid AI SDK model-message sequence.
57
+ * Return a recent-turns window that fits `tokenBudget`, oldest-first,
58
+ * snapped to a valid AI SDK model-message sequence. The window START advances
59
+ * in whole `EVICTION_BLOCK_TURNS` blocks (see the constant) so the prompt-cache
60
+ * prefix stays byte-stable across appends; the window may therefore carry a
61
+ * little less than the budget would strictly allow, but never more.
59
62
  */
60
63
  loadRecent(threadId: string, tokenBudget: number): ResultAsync<ModelMessage[], DbError>;
61
64
  /**
@@ -72,6 +75,28 @@ export interface ThreadHistory {
72
75
  * freshly-registered `MeterProvider`. Test-only.
73
76
  */
74
77
  export declare function __resetThreadHistoryMetricsForTest(): void;
78
+ /**
79
+ * Chunked-eviction block size, in whole turns — the granularity at which
80
+ * `loadRecent`'s retained window may advance its START turn.
81
+ *
82
+ * `loadRecent` snaps its eviction count UP to a multiple of this block, so the
83
+ * window's first turn — hence `messages[0]` and the whole tools+system+history
84
+ * prompt-cache prefix — stays byte-identical across a run of appends and shifts
85
+ * only once per block instead of once per turn. That single shift is the only
86
+ * message-cache miss the block costs.
87
+ *
88
+ * The block is at once (a) the cache-miss cadence — one prefix shift per
89
+ * `EVICTION_BLOCK_TURNS` appends once a thread is over budget — and (b) the most
90
+ * extra context ever sacrificed: snapping up drops up to `EVICTION_BLOCK_TURNS -
91
+ * 1` oldest turns the budget alone would have kept, trading a little history for
92
+ * a still prefix. 4 holds the prefix for ~4 turns at a cost of at most 3 turns of
93
+ * headroom — negligible against a ~120k-token budget that spans far more turns,
94
+ * while cutting cache misses on a long thread roughly fourfold. A turn-count
95
+ * block (not a token block) is deliberate: a cache miss is triggered by the START
96
+ * turn moving, a per-turn event, so counting in turns makes the miss cadence
97
+ * exact and independent of how large any individual turn is.
98
+ */
99
+ export declare const EVICTION_BLOCK_TURNS = 4;
75
100
  /**
76
101
  * Create a `ThreadHistory` bound to a Postgres pool — a factory closure
77
102
  * capturing `pool` (dependency injection per the harness-durable-runtime spec). The `messages` table is