@inflexa-ai/harness 0.17.0 → 0.19.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 (249) hide show
  1. package/dist/agents/report-builder.d.ts +2 -0
  2. package/dist/agents/report-builder.js +2 -0
  3. package/dist/agents/report-session-agent.d.ts +56 -0
  4. package/dist/agents/report-session-agent.js +72 -0
  5. package/dist/app/report-session-runtime.d.ts +72 -0
  6. package/dist/app/report-session-runtime.js +157 -0
  7. package/dist/app/spawn-report-session.d.ts +85 -0
  8. package/dist/app/spawn-report-session.js +83 -0
  9. package/dist/contracts/chat-events.d.ts +9 -0
  10. package/dist/contracts/index.d.ts +6 -2
  11. package/dist/contracts/index.js +3 -1
  12. package/dist/contracts/organ-system.d.ts +49 -0
  13. package/dist/contracts/organ-system.js +67 -0
  14. package/dist/contracts/report-blocks.d.ts +564 -0
  15. package/dist/contracts/report-blocks.js +117 -0
  16. package/dist/contracts/report-reference.d.ts +459 -0
  17. package/dist/contracts/report-reference.js +256 -0
  18. package/dist/contracts/schemas/chat-events.d.ts +2 -0
  19. package/dist/contracts/schemas/chat-events.js +2 -0
  20. package/dist/contracts/severity.d.ts +15 -0
  21. package/dist/contracts/severity.js +10 -0
  22. package/dist/contracts/target-dossier.d.ts +8675 -12972
  23. package/dist/contracts/target-dossier.js +534 -591
  24. package/dist/data/safety-panel-schema.d.ts +18 -5
  25. package/dist/data/safety-panel-schema.js +4 -4
  26. package/dist/execution/artifact-metadata.js +1 -0
  27. package/dist/execution/report-runner.js +1 -3
  28. package/dist/execution/run-synthesis.js +2 -0
  29. package/dist/index.d.ts +6 -0
  30. package/dist/index.js +19 -0
  31. package/dist/lib/async-utils.d.ts +16 -4
  32. package/dist/lib/async-utils.js +20 -15
  33. package/dist/lib/host-concurrency.js +1 -0
  34. package/dist/loop/__fixtures__/scripted-provider.js +23 -2
  35. package/dist/loop/run-agent.js +58 -10
  36. package/dist/loop/tool-detail.d.ts +40 -0
  37. package/dist/loop/tool-detail.js +35 -3
  38. package/dist/loop/types.d.ts +13 -0
  39. package/dist/memory/thread-history.d.ts +10 -0
  40. package/dist/memory/thread-history.js +12 -1
  41. package/dist/memory/thread-store.d.ts +4 -4
  42. package/dist/memory/thread-store.js +18 -9
  43. package/dist/prompts/report-session.d.ts +1 -0
  44. package/dist/prompts/report-session.js +89 -0
  45. package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +1 -1
  46. package/dist/prompts/target-assessment/briefs/executive-recommendation.js +5 -5
  47. package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +1 -1
  48. package/dist/prompts/target-assessment/briefs/liability-bullets.js +1 -1
  49. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +1 -1
  50. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +11 -3
  51. package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +1 -0
  52. package/dist/prompts/target-assessment/investigation/adversarial-critique.js +67 -0
  53. package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +1 -0
  54. package/dist/prompts/target-assessment/investigation/claim-reverification.js +49 -0
  55. package/dist/prompts/target-assessment/investigation/index.d.ts +3 -0
  56. package/dist/prompts/target-assessment/investigation/index.js +3 -0
  57. package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +1 -0
  58. package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +49 -0
  59. package/dist/prompts/target-assessment/tox-voice/index.d.ts +1 -1
  60. package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +8 -5
  61. package/dist/prompts/target-assessment/tox-voice/vocabulary.js +9 -20
  62. package/dist/providers/__fixtures__/session.d.ts +5 -4
  63. package/dist/providers/__fixtures__/session.js +4 -4
  64. package/dist/providers/ai-sdk.d.ts +77 -0
  65. package/dist/providers/ai-sdk.js +149 -19
  66. package/dist/providers/errors.d.ts +14 -0
  67. package/dist/providers/errors.js +103 -18
  68. package/dist/providers/types.d.ts +16 -0
  69. package/dist/providers/types.js +10 -1
  70. package/dist/report-model/block-walk.d.ts +56 -0
  71. package/dist/report-model/block-walk.js +89 -0
  72. package/dist/report-model/draft-finish.d.ts +60 -0
  73. package/dist/report-model/draft-finish.js +56 -0
  74. package/dist/report-model/draft-operations.d.ts +125 -0
  75. package/dist/report-model/draft-operations.js +341 -0
  76. package/dist/report-model/draft-read.d.ts +59 -0
  77. package/dist/report-model/draft-read.js +109 -0
  78. package/dist/report-model/draft.d.ts +51 -0
  79. package/dist/report-model/draft.js +32 -0
  80. package/dist/report-model/fixture-resolver.d.ts +8 -0
  81. package/dist/report-model/fixture-resolver.js +252 -0
  82. package/dist/report-model/pin-snapshot.d.ts +30 -0
  83. package/dist/report-model/pin-snapshot.js +40 -0
  84. package/dist/report-model/reference-resolver.d.ts +113 -0
  85. package/dist/report-model/reference-resolver.js +60 -0
  86. package/dist/report-model/structural-validation.d.ts +22 -0
  87. package/dist/report-model/structural-validation.js +68 -0
  88. package/dist/report-model/validate.d.ts +72 -0
  89. package/dist/report-model/validate.js +95 -0
  90. package/dist/report-render/chart.d.ts +33 -0
  91. package/dist/report-render/chart.js +538 -0
  92. package/dist/report-render/page.d.ts +127 -0
  93. package/dist/report-render/page.js +307 -0
  94. package/dist/report-render/references.d.ts +32 -0
  95. package/dist/report-render/references.js +44 -0
  96. package/dist/report-render/render.d.ts +25 -0
  97. package/dist/report-render/render.js +125 -0
  98. package/dist/report-render/script-json.d.ts +13 -0
  99. package/dist/report-render/script-json.js +15 -0
  100. package/dist/report-render/types.d.ts +39 -0
  101. package/dist/report-render/types.js +10 -0
  102. package/dist/report-render/value-bridge.d.ts +62 -0
  103. package/dist/report-render/value-bridge.js +82 -0
  104. package/dist/report-render/views/chart-view.d.ts +11 -0
  105. package/dist/report-render/views/chart-view.js +23 -0
  106. package/dist/report-render/views/page-view.d.ts +20 -0
  107. package/dist/report-render/views/page-view.js +49 -0
  108. package/dist/report-render/views/prose.d.ts +28 -0
  109. package/dist/report-render/views/prose.js +75 -0
  110. package/dist/report-render/views/references-view.d.ts +20 -0
  111. package/dist/report-render/views/references-view.js +64 -0
  112. package/dist/report-render/views/values.d.ts +42 -0
  113. package/dist/report-render/views/values.js +43 -0
  114. package/dist/runtime/assemble.d.ts +15 -0
  115. package/dist/runtime/assemble.js +28 -6
  116. package/dist/runtime/boot.d.ts +3 -3
  117. package/dist/runtime/boot.js +15 -4
  118. package/dist/runtime/dbos.d.ts +4 -4
  119. package/dist/state/artifacts.d.ts +16 -0
  120. package/dist/state/artifacts.js +18 -0
  121. package/dist/state/init.js +54 -0
  122. package/dist/state/purge-analysis.js +2 -0
  123. package/dist/state/report-session-state.d.ts +112 -0
  124. package/dist/state/report-session-state.js +150 -0
  125. package/dist/state/report-versions.d.ts +129 -0
  126. package/dist/state/report-versions.js +167 -0
  127. package/dist/state/snapshot-parse.d.ts +34 -0
  128. package/dist/state/snapshot-parse.js +82 -0
  129. package/dist/tasks/data-profile.js +1 -0
  130. package/dist/tools/ad-hoc-router.d.ts +7 -2
  131. package/dist/tools/ad-hoc-router.js +2 -1
  132. package/dist/tools/bio/chembl.js +1 -0
  133. package/dist/tools/bio/comptox.js +1 -0
  134. package/dist/tools/bio/drug-gene-interactions.js +1 -0
  135. package/dist/tools/bio/gene-disease-evidence.js +1 -0
  136. package/dist/tools/bio/gene-preclinical-profile.js +1 -0
  137. package/dist/tools/bio/keys.d.ts +3 -2
  138. package/dist/tools/bio/keys.js +3 -2
  139. package/dist/tools/bio/lookup-annotation.js +1 -0
  140. package/dist/tools/bio/opentargets.d.ts +3 -3
  141. package/dist/tools/bio/opentargets.js +4 -3
  142. package/dist/tools/bio/pubchem.js +1 -0
  143. package/dist/tools/bio/search-clinical-trials.js +1 -0
  144. package/dist/tools/bio/search-faers.js +1 -0
  145. package/dist/tools/bio/search-geo-datasets.js +1 -0
  146. package/dist/tools/bio/search-interactions.js +1 -0
  147. package/dist/tools/bio/target-safety.d.ts +4 -6
  148. package/dist/tools/bio/target-safety.js +7 -6
  149. package/dist/tools/define-tool.d.ts +19 -12
  150. package/dist/tools/define-tool.js +4 -2
  151. package/dist/tools/display/show-user.js +1 -0
  152. package/dist/tools/iterate-report.js +2 -0
  153. package/dist/tools/lib/chembl-client.d.ts +2 -0
  154. package/dist/tools/lib/chembl-client.js +1 -0
  155. package/dist/tools/lib/monarch-client.d.ts +42 -0
  156. package/dist/tools/lib/monarch-client.js +100 -0
  157. package/dist/tools/report/build-report.js +1 -0
  158. package/dist/tools/report/mint-preview-url.js +1 -0
  159. package/dist/tools/report/preview-snapshot.js +1 -0
  160. package/dist/tools/report/submit-report.js +1 -0
  161. package/dist/tools/report/version-fs.js +4 -0
  162. package/dist/tools/report-authoring/authoring-tools.d.ts +241 -0
  163. package/dist/tools/report-authoring/authoring-tools.js +460 -0
  164. package/dist/tools/report-session/index.d.ts +8 -0
  165. package/dist/tools/report-session/index.js +8 -0
  166. package/dist/tools/report-session/preview-report.d.ts +90 -0
  167. package/dist/tools/report-session/preview-report.js +245 -0
  168. package/dist/tools/research/context7-docs.js +2 -0
  169. package/dist/tools/research/generate-analogy-report.js +1 -0
  170. package/dist/tools/research/generate-plan.js +7 -2
  171. package/dist/tools/research/inspect-data-profile.js +4 -0
  172. package/dist/tools/research/literature-reviewer.js +1 -0
  173. package/dist/tools/research/search-arxiv.js +1 -0
  174. package/dist/tools/research/search-github-repos.js +1 -0
  175. package/dist/tools/research/search-semantic-scholar.js +1 -0
  176. package/dist/tools/sandbox/list-available-packages.js +23 -0
  177. package/dist/tools/sandbox/list-available-refs.js +36 -0
  178. package/dist/tools/sandbox/report-blocker.js +1 -0
  179. package/dist/tools/sandbox/skills.js +2 -0
  180. package/dist/tools/workspace/file-stat.js +1 -0
  181. package/dist/tools/workspace/show-file.js +2 -0
  182. package/dist/tools/workspace/show-plan.js +1 -0
  183. package/dist/workflows/execute-analysis.d.ts +5 -1
  184. package/dist/workflows/execute-analysis.js +5 -5
  185. package/dist/workflows/execute-target-assessment.d.ts +13 -2
  186. package/dist/workflows/execute-target-assessment.js +78 -16
  187. package/dist/workflows/target-assessment/assemblers/index.d.ts +1 -1
  188. package/dist/workflows/target-assessment/assemblers/index.js +1 -1
  189. package/dist/workflows/target-assessment/assemblers/literature.d.ts +9 -27
  190. package/dist/workflows/target-assessment/assemblers/literature.js +32 -48
  191. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +17 -9
  192. package/dist/workflows/target-assessment/assemblers/orchestrator.js +195 -85
  193. package/dist/workflows/target-assessment/assemblers/safety.d.ts +70 -48
  194. package/dist/workflows/target-assessment/assemblers/safety.js +125 -42
  195. package/dist/workflows/target-assessment/collectors/index.d.ts +5 -2
  196. package/dist/workflows/target-assessment/collectors/index.js +48 -5
  197. package/dist/workflows/target-assessment/coverage.d.ts +43 -27
  198. package/dist/workflows/target-assessment/coverage.js +33 -29
  199. package/dist/workflows/target-assessment/fanout/index.js +1 -0
  200. package/dist/workflows/target-assessment/investigation/index.d.ts +97 -0
  201. package/dist/workflows/target-assessment/investigation/index.js +419 -0
  202. package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +26 -5
  203. package/dist/workflows/target-assessment/lib/approval-precedents.js +95 -29
  204. package/dist/workflows/target-assessment/lib/citation-validator.js +1 -1
  205. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +2 -2
  206. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +2 -3
  207. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -4
  208. package/dist/workflows/target-assessment/lib/compute-derived.d.ts +6 -5
  209. package/dist/workflows/target-assessment/lib/compute-derived.js +14 -5
  210. package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +5 -6
  211. package/dist/workflows/target-assessment/lib/family-complex-supplement.js +2 -3
  212. package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +72 -0
  213. package/dist/workflows/target-assessment/lib/fda-label-safety.js +166 -0
  214. package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -9
  215. package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -10
  216. package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +34 -0
  217. package/dist/workflows/target-assessment/lib/hpo-organ-map.js +63 -0
  218. package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +19 -0
  219. package/dist/workflows/target-assessment/lib/impc-organ-map.js +39 -0
  220. package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +24 -14
  221. package/dist/workflows/target-assessment/lib/meddra-organ-map.js +59 -47
  222. package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +1 -1
  223. package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +2 -3
  224. package/dist/workflows/target-assessment/lib/regulatory-actions.js +33 -4
  225. package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +44 -0
  226. package/dist/workflows/target-assessment/lib/safety-corroboration.js +273 -0
  227. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +5 -7
  228. package/dist/workflows/target-assessment/lib/target-identity-filter.js +5 -7
  229. package/dist/workflows/target-assessment/metrics.d.ts +1 -6
  230. package/dist/workflows/target-assessment/metrics.js +1 -13
  231. package/dist/workflows/target-assessment/phase4-assemble.d.ts +2147 -807
  232. package/dist/workflows/target-assessment/phase4-assemble.js +19 -19
  233. package/dist/workflows/target-assessment/phase5-persist.d.ts +20 -21
  234. package/dist/workflows/target-assessment/phase5-persist.js +56 -173
  235. package/dist/workflows/target-assessment/schemas.d.ts +46 -1
  236. package/dist/workflows/target-assessment/schemas.js +19 -0
  237. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +1 -0
  238. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +2 -0
  239. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +26 -62
  240. package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -4
  241. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +27 -154
  242. package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -6
  243. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -6
  244. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -18
  245. package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +1 -0
  246. package/dist/workflows/target-assessment/synthesis/index.d.ts +52 -5
  247. package/dist/workflows/target-assessment/synthesis/index.js +4 -1
  248. package/dist/workflows/target-assessment/synthesis-probes.d.ts +3 -3
  249. package/package.json +4 -1
@@ -0,0 +1,245 @@
1
+ /**
2
+ * The render-and-preview tool of a report session.
3
+ *
4
+ * The tool renders the current draft of a thread. It runs the finish first, because a mid-composition
5
+ * draft is not a `ReportDocument`. A gap list returns as data, and no render runs. On a pass the tool
6
+ * resolves each reference through the injected resolver, bridges the resolved values into the render model,
7
+ * and renders the page.
8
+ *
9
+ * The page and its staged image assets land in `report-sessions/{threadId}/` under the workspace root. That
10
+ * namespace belongs to this path alone, thus the tool never writes under the old `previews/` or `reports/`
11
+ * trees. The result carries the absolute page path, thus a local host shows the page with no seam.
12
+ *
13
+ * The result carries no access grant. `PreviewPublisher` authorizes the URL space
14
+ * `previews/{analysisId}/{previewId}` (`contracts/content-url.ts`), and that space cannot name a page of
15
+ * this tree. A hosted view of a session page is a later capability, with a URL space of its own.
16
+ *
17
+ * Each degraded condition is a typed outcome in the ok channel: a session refusal, a gap list, a resolver
18
+ * absence, an unresolved reference, a bridge mismatch, a render problem, a figure that escapes the
19
+ * workspace root, and a write failure. The tool never throws for one of them. The filesystem speaks the
20
+ * throw protocol, and the workspace-root seam signals an unresolvable resource the same way. Thus the
21
+ * write runs through the `tryFs` glue, which turns a genuine fault into the ok-channel outcome and lets a
22
+ * control-flow exception propagate.
23
+ */
24
+ import { err, ok } from "neverthrow";
25
+ import { copyFile, mkdir, writeFile } from "node:fs/promises";
26
+ import { extname, join } from "node:path";
27
+ import { z } from "zod";
28
+ import { resolveWorkspacePath } from "../../workspace/paths.js";
29
+ import { createNoopLogger } from "../../lib/console-logger.js";
30
+ import { describeFsError, tryFsWrite } from "../../lib/fs-result.js";
31
+ import { defaultErrorFields } from "../../lib/logger.js";
32
+ import { finishDraft } from "../../report-model/draft-finish.js";
33
+ import { resolveDocumentReferences } from "../../report-model/validate.js";
34
+ import { bridgeValues } from "../../report-render/value-bridge.js";
35
+ import { renderReportPage } from "../../report-render/render.js";
36
+ import { defineTool } from "../define-tool.js";
37
+ import { openReportThread } from "../report-authoring/authoring-tools.js";
38
+ /** The empty input. The tool renders the current draft of the thread, thus it needs no field. */
39
+ const previewReportInput = z.object({});
40
+ /**
41
+ * The staged file name of one bound image: its content hash with the original extension.
42
+ *
43
+ * The colon of the `algorithm:hex` hash is not safe in a file name and in a relative source, thus the slug
44
+ * replaces each non-word run with one dash. The hash is unique, thus two figures with one basename cannot
45
+ * collide on the staged name.
46
+ */
47
+ function assetFileName(file) {
48
+ const slug = file.hash.replace(/[^a-z0-9]+/gi, "-");
49
+ return slug + extname(file.path);
50
+ }
51
+ /** The relative source of a staged figure. The page directory is self-contained, thus the source is `assets/<name>`. */
52
+ function figureSourcePolicy(file) {
53
+ return `assets/${assetFileName(file)}`;
54
+ }
55
+ /**
56
+ * Pair each block with the resolved value of its binding, in document order.
57
+ *
58
+ * A value-bearing kind carries the resolved value of its one reference, looked up by the block id from
59
+ * the map that the resolution pass filled. A no-value kind carries none. Where a reference sits in a
60
+ * block is the knowledge of `block-walk.ts`, thus this walk reads the block id alone and never a binding
61
+ * field.
62
+ *
63
+ * The switch is exhaustive over the eight block kinds. A ninth kind reaches the end with no return, and
64
+ * the declared return type fails the build. Thus the walk cannot drop a kind in silence. A value-bearing
65
+ * block reaches here only when its reference resolved, because an unresolved reference short-circuits
66
+ * before this walk.
67
+ */
68
+ function collectResolutions(blocks, resolvedByBlock) {
69
+ const resolutions = [];
70
+ const visit = (block) => {
71
+ switch (block.kind) {
72
+ case "section":
73
+ resolutions.push({ blockId: block.id, kind: "section" });
74
+ for (const child of block.blocks) {
75
+ visit(child);
76
+ }
77
+ return;
78
+ case "text":
79
+ case "claim":
80
+ case "citation":
81
+ resolutions.push({ blockId: block.id, kind: block.kind });
82
+ return;
83
+ case "metric":
84
+ case "table":
85
+ case "chart":
86
+ case "figure": {
87
+ const resolved = resolvedByBlock.get(block.id);
88
+ if (resolved !== undefined) {
89
+ resolutions.push({ blockId: block.id, kind: block.kind, resolved });
90
+ }
91
+ return;
92
+ }
93
+ }
94
+ };
95
+ for (const block of blocks) {
96
+ visit(block);
97
+ }
98
+ return resolutions;
99
+ }
100
+ /**
101
+ * Resolve each reference of the document, and split the resolutions from the unresolved references.
102
+ *
103
+ * The shared `resolveDocumentReferences` walks the tree, resolves each reference under the concurrency
104
+ * bound, and runs the chart-encoding match, thus the preview and the record gate refuse the same
105
+ * references. On a clean pass the resolutions map onto the render model in document order.
106
+ */
107
+ async function resolveDocument(document, resolver, snapshot) {
108
+ const { resolvedByBlock, failures } = await resolveDocumentReferences(document.sections, snapshot, resolver);
109
+ if (failures.length > 0) {
110
+ return { resolutions: [], unresolved: failures };
111
+ }
112
+ return { resolutions: collectResolutions(document.sections, resolvedByBlock), unresolved: [] };
113
+ }
114
+ /**
115
+ * Resolve the page root, stage each bound image, and write the page.
116
+ *
117
+ * The workspace-root seam signals an unresolvable resource by a throw, thus the resolution sits inside
118
+ * the protection and its throw becomes a value. Each `fs` call runs through `tryFsWrite`, the one
119
+ * sanctioned `fs` guard, thus a genuine I/O fault becomes a value and a control-flow exception such as a
120
+ * cancellation propagates.
121
+ *
122
+ * A bound figure names a snapshot path, which is untrusted. A registered `../../` path escapes the root,
123
+ * thus the containment test runs before any copy and reuses the one workspace-path resolver. A source
124
+ * outside the root refuses and names the block, and no copy runs.
125
+ */
126
+ async function renderToWorkspace(args) {
127
+ let root;
128
+ try {
129
+ root = args.resolveWorkspaceRoot(args.analysisId);
130
+ }
131
+ catch (cause) {
132
+ return err({ kind: "fs", error: { type: "read_failed", op: "preview.resolveWorkspaceRoot", path: args.analysisId, cause } });
133
+ }
134
+ const sessionDir = join(root, "report-sessions", args.threadId);
135
+ const assetsDir = join(sessionDir, "assets");
136
+ const pagePath = join(sessionDir, "index.html");
137
+ // Each bound figure, contained and deduplicated by its staged name, mapped to its host source.
138
+ const sources = new Map();
139
+ for (const resolution of args.resolutions) {
140
+ if (resolution.kind !== "figure" || resolution.resolved.type !== "file") {
141
+ continue;
142
+ }
143
+ const file = resolution.resolved;
144
+ const name = assetFileName(file);
145
+ if (sources.has(name)) {
146
+ continue;
147
+ }
148
+ const resolved = resolveWorkspacePath({ workspaceRoot: root, analysisId: args.analysisId, path: file.path });
149
+ if (resolved.kind !== "ok") {
150
+ return err({ kind: "figure-out-of-scope", blockId: resolution.blockId, path: file.path });
151
+ }
152
+ sources.set(name, resolved.absolute);
153
+ }
154
+ const madeSession = await tryFsWrite("preview.mkdir", () => mkdir(sessionDir, { recursive: true }), { path: sessionDir });
155
+ if (madeSession.isErr()) {
156
+ return err({ kind: "fs", error: madeSession.error });
157
+ }
158
+ if (sources.size > 0) {
159
+ const madeAssets = await tryFsWrite("preview.mkdir", () => mkdir(assetsDir, { recursive: true }), { path: assetsDir });
160
+ if (madeAssets.isErr()) {
161
+ return err({ kind: "fs", error: madeAssets.error });
162
+ }
163
+ for (const [name, source] of sources) {
164
+ const copied = await tryFsWrite("preview.copyFile", () => copyFile(source, join(assetsDir, name)), { path: source });
165
+ if (copied.isErr()) {
166
+ return err({ kind: "fs", error: copied.error });
167
+ }
168
+ }
169
+ }
170
+ const wrote = await tryFsWrite("preview.writeFile", () => writeFile(pagePath, args.page, "utf8"), { path: pagePath });
171
+ if (wrote.isErr()) {
172
+ return err({ kind: "fs", error: wrote.error });
173
+ }
174
+ return ok(pagePath);
175
+ }
176
+ /**
177
+ * Make the render-and-preview tool over the session-state gateway and the render seams.
178
+ *
179
+ * The tool reads the thread id from the scope of the call, and it loads the state through the gateway. Thus
180
+ * one factory serves every thread. The tool holds no per-session value, and it resolves the root of the
181
+ * analysis of the call.
182
+ */
183
+ export function createPreviewReportTool(deps) {
184
+ const logger = (deps.logger ?? createNoopLogger()).named("preview-report");
185
+ return defineTool({
186
+ id: "preview_report",
187
+ description: "Render the current draft to a self-contained HTML page, and give back the page path. " +
188
+ "The tool finishes the draft first: an incomplete draft gives back the gap list, and no page renders. " +
189
+ "On a pass it resolves each reference, stages each bound image beside the page, and writes the page. " +
190
+ "Use it to see the report, and to confirm that each reference resolves.",
191
+ inputSchema: previewReportInput,
192
+ executionMode: "inline",
193
+ describeCall: "none",
194
+ execute: async (_input, ctx) => {
195
+ const opened = await openReportThread(deps.gateway, ctx.session.scope);
196
+ if (opened.isErr()) {
197
+ return ok({ outcome: "refused", refusal: opened.error });
198
+ }
199
+ const { threadId, analysisId, state } = opened.value;
200
+ const { document: draft, snapshot } = state;
201
+ const finished = finishDraft(draft, snapshot);
202
+ if (!finished.valid) {
203
+ return ok({ outcome: "gaps", gaps: finished.gaps });
204
+ }
205
+ const document = finished.document;
206
+ if (deps.resolver === undefined) {
207
+ return ok({ outcome: "resolver-unavailable" });
208
+ }
209
+ const { resolutions, unresolved } = await resolveDocument(document, deps.resolver, snapshot);
210
+ if (unresolved.length > 0) {
211
+ return ok({ outcome: "unresolved-references", unresolved });
212
+ }
213
+ const bridged = bridgeValues(resolutions, figureSourcePolicy);
214
+ if (bridged.isErr()) {
215
+ return ok({ outcome: "bridge-mismatch", mismatches: bridged.error });
216
+ }
217
+ const rendered = renderReportPage(document, bridged.value);
218
+ if (rendered.isErr()) {
219
+ return ok({ outcome: "render-problems", problems: rendered.error });
220
+ }
221
+ const written = await renderToWorkspace({
222
+ resolveWorkspaceRoot: deps.resolveWorkspaceRoot,
223
+ analysisId,
224
+ threadId,
225
+ resolutions,
226
+ page: rendered.value,
227
+ });
228
+ if (written.isErr()) {
229
+ const failure = written.error;
230
+ if (failure.kind === "figure-out-of-scope") {
231
+ // A bound figure names a path outside the workspace root. The record gate refuses the
232
+ // same figure, thus the preview refuses it too and names the block.
233
+ logger.warn("a bound figure escapes the workspace root", { threadId, analysisId, blockId: failure.blockId, path: failure.path });
234
+ return ok({ outcome: "figure-out-of-scope", blockId: failure.blockId, path: failure.path });
235
+ }
236
+ // An unresolvable root, a full volume, and a denied write each arrive here as a value. The
237
+ // tool contract is ok-channel data for each degraded condition, thus the fault becomes an
238
+ // outcome. The log keeps the full fault, because the outcome carries the description alone.
239
+ logger.warn("the page did not land", { threadId, analysisId, ...defaultErrorFields(failure.error.cause) });
240
+ return ok({ outcome: "write-failed", detail: describeFsError(failure.error) });
241
+ }
242
+ return ok({ outcome: "rendered", pagePath: written.value });
243
+ },
244
+ });
245
+ }
@@ -25,6 +25,7 @@ export const resolveLibraryIdTool = defineTool({
25
25
  libraryName: z.string().describe("Package or library name to search for (e.g., 'scanpy', 'pysam', 'lifelines')."),
26
26
  query: z.string().describe("What you need help with — used to rank results by relevance (e.g., 'how to run differential expression')."),
27
27
  }),
28
+ describeCall: "none",
28
29
  execute: async ({ libraryName, query }) => {
29
30
  const result = await apiFetchValidated(`${CONTEXT7_BASE}/search?query=${encodeURIComponent(libraryName)}&topic=${encodeURIComponent(query)}`, Context7SearchResponseSchema, { maxRetries: 1 });
30
31
  if (result.isErr())
@@ -48,6 +49,7 @@ export const queryDocsTool = defineTool({
48
49
  libraryId: z.string().describe("Context7 library ID from resolve-library-id (e.g., '/scverse/scanpy')."),
49
50
  query: z.string().describe("Specific question about the library (e.g., 'rank_genes_groups function parameters and usage')."),
50
51
  }),
52
+ describeCall: "none",
51
53
  execute: async ({ libraryId, query }) => {
52
54
  const result = await apiFetchValidated(`${CONTEXT7_BASE}/docs?libraryId=${encodeURIComponent(libraryId)}&query=${encodeURIComponent(query)}`, Context7DocsResponseSchema, { maxRetries: 1 });
53
55
  if (result.isErr())
@@ -193,6 +193,7 @@ export function createGenerateAnalogyReportTool(deps) {
193
193
  "latency on the same failure). Surface the message and ask the user to " +
194
194
  "narrow the problem.",
195
195
  inputSchema: generateAnalogyReportInputSchema,
196
+ describeCall: "none",
196
197
  execute: async (input, ctx) => {
197
198
  const childSession = forSubAgent(ctx.session, AGENT_ID);
198
199
  // Phase 1+2: drive the research agent over the cross-domain toolset.
@@ -30,6 +30,7 @@ import { composeSystemPrompt } from "../../agents/system-prompt.js";
30
30
  import { DEFAULT_SALVAGE_ITERATIONS, runToTerminal } from "../../loop/run-to-terminal.js";
31
31
  import { passthroughStep } from "../../loop/run-step.js";
32
32
  import { forSubAgent, scopeResource } from "../../auth/types.js";
33
+ import { effectiveDeadlineMs } from "../../providers/types.js";
33
34
  import { defineTool } from "../define-tool.js";
34
35
  import { createListAvailablePackagesTool } from "../sandbox/list-available-packages.js";
35
36
  import { createListAvailableRefsTool } from "../sandbox/list-available-refs.js";
@@ -344,6 +345,7 @@ function buildInnerTools(holder, trace, persistCtx, pool, resourcePolicy, logger
344
345
  // structured issue reporting.
345
346
  plan: z.union([PlannerPlanSchema, z.unknown()]).describe("Candidate plan; malformed values are reported in issues."),
346
347
  }),
348
+ describeCall: "none",
347
349
  execute: async (input) => {
348
350
  if (holder.outcome !== null) {
349
351
  trace.duplicateTerminalCalls++;
@@ -416,6 +418,7 @@ function buildInnerTools(holder, trace, persistCtx, pool, resourcePolicy, logger
416
418
  question: z.string().min(1),
417
419
  questionContext: z.string().optional(),
418
420
  }),
421
+ describeCall: "none",
419
422
  execute: async (input) => {
420
423
  if (holder.outcome !== null) {
421
424
  trace.duplicateTerminalCalls++;
@@ -670,6 +673,7 @@ export function createGeneratePlanTool(deps) {
670
673
  .optional()
671
674
  .describe("The planId of the prior plan being iterated on. Set only when " + "the user is asking for modifications to an existing plan."),
672
675
  }),
676
+ describeCall: ({ researchQuestion }) => researchQuestion,
673
677
  execute: async (input, ctx) => {
674
678
  const analysisId = scopeResource(ctx.session.scope).resourceId;
675
679
  // Bound per invocation: `analysisId` comes from the call-time session, not from
@@ -773,6 +777,7 @@ export function createGeneratePlanTool(deps) {
773
777
  ...(input.priorRuns ? ["", "## Prior Run Results", input.priorRuns] : []),
774
778
  ...(input.userConstraints ? ["", "## User Constraints", input.userConstraints] : []),
775
779
  ].join("\n");
780
+ const planTimeoutMs = effectiveDeadlineMs(deps.conversation.provider, PLAN_TIMEOUT_MS);
776
781
  // Opens the invocation. Two things are only knowable here and both are
777
782
  // first-order suspects when a planner runs long and returns nothing: the size
778
783
  // of the seed it was handed, and which of its blocks made it that size. The
@@ -785,7 +790,7 @@ export function createGeneratePlanTool(deps) {
785
790
  model: deps.conversation.model,
786
791
  maxIterations: PLANNER_MAX_ITERATIONS,
787
792
  salvageIterations: DEFAULT_SALVAGE_ITERATIONS,
788
- timeoutMs: PLAN_TIMEOUT_MS,
793
+ timeoutMs: planTimeoutMs,
789
794
  grounding: grounding.kind,
790
795
  ...(input.parentPlanId ? { parentPlanId: input.parentPlanId } : {}),
791
796
  seedChars: {
@@ -815,7 +820,7 @@ export function createGeneratePlanTool(deps) {
815
820
  };
816
821
  // Merge the outer abort signal with a wall-clock timeout — either
817
822
  // cancels the planner promptly.
818
- const signal = AbortSignal.any([ctx.signal, AbortSignal.timeout(PLAN_TIMEOUT_MS)]);
823
+ const signal = AbortSignal.any([ctx.signal, AbortSignal.timeout(planTimeoutMs)]);
819
824
  let runError = null;
820
825
  let run = null;
821
826
  try {
@@ -98,6 +98,10 @@ export function createInspectDataProfileTool(pool) {
98
98
  .optional()
99
99
  .describe(`Per-file records per page. Only used when scope is 'files'. Default ${DEFAULT_PAGE_SIZE}, max ${MAX_PAGE_SIZE}.`),
100
100
  }),
101
+ // `execute` defaults both fields internally, and the hook applies the same
102
+ // defaults. Without them the ordinary call, which names no field at all,
103
+ // gives no detail. `page` acts only on the `files` scope, as in `execute`.
104
+ describeCall: ({ scope, page }) => ((scope ?? "overview") === "files" ? `files (page ${page ?? 1})` : "overview"),
101
105
  execute: async (input, ctx) => {
102
106
  const resourceId = scopeResource(ctx.session.scope).resourceId;
103
107
  const status = unwrapOrThrow(await loadDataProfileStatus(pool, resourceId));
@@ -68,6 +68,7 @@ export function createLiteratureReviewerTool(deps) {
68
68
  "biological features to investigate and the experimental " +
69
69
  "context. The reviewer sees only this — include everything it needs."),
70
70
  }),
71
+ describeCall: "none",
71
72
  execute: async ({ brief }, ctx) => {
72
73
  const { messages: transcript } = await runAgent(agent, [{ role: "user", content: brief }], forSubAgent(ctx.session, AGENT_ID), {
73
74
  provider: deps.provider,
@@ -18,6 +18,7 @@ export const searchArxivTool = defineTool({
18
18
  .describe('Optional arXiv category filters (e.g., ["cs.LG", "math.OC"]). ' + "Combined with the query using AND."),
19
19
  limit: z.number().int().min(1).max(20).default(10).describe("Maximum results (1–20, default 10)."),
20
20
  }),
21
+ describeCall: "none",
21
22
  execute: async ({ query, categories, limit }, context) => {
22
23
  const result = await source.search({ query, ...(categories === undefined ? {} : { categories }), limit }, context.signal);
23
24
  if (result.status !== "ok")
@@ -61,6 +61,7 @@ export function createSearchGithubReposTool(deps) {
61
61
  language: z.string().optional().describe('Optional language filter (e.g., "python").'),
62
62
  limit: z.number().int().min(1).max(20).default(10).describe("Maximum results (1–20, default 10)."),
63
63
  }),
64
+ describeCall: "none",
64
65
  execute: async ({ query, language, limit }) => {
65
66
  const q = language ? `${query} language:${language}` : query;
66
67
  const params = new URLSearchParams({
@@ -24,6 +24,7 @@ export function createSearchSemanticScholarTool(options = {}) {
24
24
  '"reinforcement learning for control" or "feedback stabilisation in oscillators".'),
25
25
  limit: z.number().int().min(1).max(20).default(10).describe("Maximum number of results to return (1–20, default 10)."),
26
26
  }),
27
+ describeCall: "none",
27
28
  execute: async ({ query, limit }, context) => {
28
29
  const result = await source.search(query, limit, context.signal);
29
30
  if (result.status !== "ok")
@@ -26,6 +26,7 @@ import { readFile } from "node:fs/promises";
26
26
  import { ok } from "neverthrow";
27
27
  import { z } from "zod";
28
28
  import { defineTool } from "../define-tool.js";
29
+ import { capCodePoints, DETAIL_NEEDLE_MAX_LENGTH } from "../../loop/tool-detail.js";
29
30
  /**
30
31
  * Where the list lives when the host mounts the library store at the same path
31
32
  * the sandbox does. Hosts that do not — the store is baked into the image and
@@ -187,6 +188,28 @@ export function createListAvailablePackagesTool(deps = {}) {
187
188
  .optional()
188
189
  .describe(`Cap the packages listed. Omit to get the whole set — the store fits well within the ${DEFAULT_LIMIT} ceiling. Ignored when \`names\` is given.`),
189
190
  }),
191
+ // `queryPackages` returns on `names` before it reads another field, thus
192
+ // the presence check comes first. An empty array is not a presence check.
193
+ // `names` are exact identifiers a caller checks for, thus they name the
194
+ // call as they are. A blank query counts as absent, because `queryPackages`
195
+ // trims the needle and applies no filter when nothing is left.
196
+ //
197
+ // A needle rides behind `matching "…"`, because a bare needle reads as a
198
+ // package name. The needle takes its own bound, thus the emit-site cap
199
+ // cannot cut inside the mark and leave the quote open. `language` stays a
200
+ // trailing qualifier, exactly as `execute` treats it.
201
+ describeCall: ({ names, query, language }) => {
202
+ if (names !== undefined && names.length > 0)
203
+ return names.join(", ");
204
+ const needle = query?.trim();
205
+ if (needle !== undefined && needle !== "") {
206
+ const marked = `matching "${capCodePoints(needle, DETAIL_NEEDLE_MAX_LENGTH)}"`;
207
+ return language === undefined ? marked : `${marked} (${language})`;
208
+ }
209
+ if (language !== undefined)
210
+ return `${language} packages`;
211
+ return "full package list";
212
+ },
190
213
  execute: async (input) => {
191
214
  // An unreadable inventory is an expected environment state — model it as an
192
215
  // `available: false` data variant telling the caller the set is UNKNOWN.
@@ -27,6 +27,7 @@ import { lstat, readFile, readdir } from "node:fs/promises";
27
27
  import { join as hostJoin, posix as posixPath } from "node:path";
28
28
  import { ok } from "neverthrow";
29
29
  import { z } from "zod";
30
+ import { capCodePoints, DETAIL_MAX_LENGTH, DETAIL_NEEDLE_MAX_LENGTH } from "../../loop/tool-detail.js";
30
31
  import { REFERENCE_DATA_CATALOG } from "../../reference-data/catalog.js";
31
32
  import { parseReferenceInstallReceipt } from "../../reference-data/receipt.js";
32
33
  import { defineTool } from "../define-tool.js";
@@ -510,6 +511,41 @@ export function createListAvailableRefsTool(deps) {
510
511
  "The store is provisioned by the host and is NOT frozen: datasets can be added through the host's own provisioning path, and whatever is added shows up on a later call. " +
511
512
  "So before you report a gap as permanent, check whether any tool you hold reaches that provisioning path — offer that route if one exists, and call the gap permanent only when none does.",
512
513
  inputSchema: ListAvailableRefsInputSchema,
514
+ // `execute` resolves `path ?? category` for the subtree it inspects, and
515
+ // it ignores `category` when `path` is present. `query` is an ADDITIVE
516
+ // filter over that subtree, thus the target keeps naming the call and the
517
+ // filter rides beside it.
518
+ //
519
+ // A needle rides behind `matching "…"`: a bare needle reads as a path, and
520
+ // a filtered subtree must name both halves, not the target alone. The two
521
+ // halves join with a space and a word, as `grep` joins its own two halves.
522
+ // A separator glyph belongs to a host, which owns its line and its own
523
+ // vocabulary; the harness renders nothing and thus borrows nothing.
524
+ //
525
+ // Each half carries its own bound. The emit-site cap cuts the TAIL, and
526
+ // `path` admits 4096 bytes, thus one long target would swallow the very
527
+ // filter this detail exists to state. The needle takes a fixed budget, and
528
+ // the target takes what the bounded filter leaves. Then both halves reach
529
+ // a reader, and each one marks its own cut.
530
+ //
531
+ // A blank value counts as absent, because `execute` treats it that way: it
532
+ // browses the store root for a blank target, and it applies no filter for a
533
+ // blank needle. The precedence stays `??` and never `||`, thus a blank
534
+ // `path` still suppresses `category` exactly as `execute` does.
535
+ describeCall: ({ path, category, query }) => {
536
+ const target = path ?? category;
537
+ const named = target !== undefined && target.trim() !== "" ? target : undefined;
538
+ const needle = query?.trim();
539
+ const filter = needle === undefined || needle === "" ? undefined : `matching "${capCodePoints(needle, DETAIL_NEEDLE_MAX_LENGTH)}"`;
540
+ if (named === undefined)
541
+ return filter ?? "full reference store";
542
+ if (filter === undefined)
543
+ return named;
544
+ // The budget comes off the rendered filter, never off a count of its
545
+ // parts, thus it cannot drift from the template above. One code point
546
+ // pays for the joining space.
547
+ return `${capCodePoints(named, DETAIL_MAX_LENGTH - [...filter].length - 1)} ${filter}`;
548
+ },
513
549
  execute: async ({ path, category, query, limit }) => {
514
550
  // `category` is shorthand for a top-level path; an explicit `path` wins.
515
551
  const requestedInput = path ?? category;
@@ -33,6 +33,7 @@ export function createReportBlockerToolFor(deps) {
33
33
  id: "report_blocker",
34
34
  description: `Terminal. ${deps.blockedWhen} Pass a clear reason. Stop immediately after calling — take no further actions.`,
35
35
  inputSchema: ReportBlockerInputSchema,
36
+ describeCall: "none",
36
37
  execute: async (input) => {
37
38
  deps.record({ kind: "blocker", reason: input.reason });
38
39
  return ok({
@@ -54,6 +54,7 @@ export function createSkillTools(deps) {
54
54
  query: z.string().min(1).describe("Keywords to match (e.g. 'PyDESeq2 contrast syntax')."),
55
55
  topK: z.number().int().min(1).max(MAX_TOP_K).optional().describe(`Max results to return. Defaults to ${DEFAULT_TOP_K}.`),
56
56
  }),
57
+ describeCall: "none",
57
58
  execute: async ({ query, topK }) => {
58
59
  if (allowed.size === 0) {
59
60
  return ok({ status: "no_skills" });
@@ -114,6 +115,7 @@ export function createSkillTools(deps) {
114
115
  skill: z.string().min(1).describe("Skill name (one of your declared skills)."),
115
116
  path: z.string().min(1).describe("File path within the skill, e.g. 'SKILL.md' or 'references/pydeseq2-api.md'."),
116
117
  }),
118
+ describeCall: "none",
117
119
  execute: async ({ skill, path }) => {
118
120
  if (!allowed.has(skill)) {
119
121
  return ok({ status: "skill_not_declared", skill });
@@ -22,6 +22,7 @@ export function createFileStatTool(fs, workingDir) {
22
22
  "in bytes. Use before reading to size a file. Missing and out-of-scope " +
23
23
  "paths return a data variant, not an error.",
24
24
  inputSchema: FileStatInputSchema,
25
+ describeCall: "none",
25
26
  execute: async ({ path }, ctx) => {
26
27
  const result = unwrapOrThrow(await fs.stat({ session: ctx.session, path, workingDir }));
27
28
  switch (result.kind) {
@@ -38,6 +38,8 @@ export const showFileTool = defineTool({
38
38
  "Cards render in call order, so to interleave figures with prose alternate `show_user(markdown)` and `show_file` calls " +
39
39
  "(markdown image syntax pointing at a workspace file does NOT render — this tool is the only way to show one).",
40
40
  inputSchema: ShowFileInputSchema,
41
+ // `.min(1)` on `files` makes index 0 total after the parse.
42
+ describeCall: ({ files }) => (files.length === 1 ? files[0].path : `${files.length} files`),
41
43
  execute: async (input, ctx) => {
42
44
  const card = buildFileReferenceCardData(input);
43
45
  // A malformed/traversal path yields no card — an expected outcome the model can self-correct
@@ -32,6 +32,7 @@ export function createShowPlanTool(pool) {
32
32
  .describe("Plan ID (`pln-<8hex>`) from `generate_plan` (newly created) or `inspect_run` (each run carries the planId it ran)"),
33
33
  title: z.string().optional().describe("Optional card heading; defaults to the plan's own title"),
34
34
  }),
35
+ describeCall: ({ planId }) => planId,
35
36
  execute: async ({ planId, title }, ctx) => {
36
37
  const resourceId = scopeResource(ctx.session.scope).resourceId;
37
38
  // "Plan not found" / "plan no longer parses" are expected outcomes —
@@ -298,8 +298,12 @@ export interface ExecuteAnalysisDeps {
298
298
  * point. Idempotency is the consumer's job, achieved via the deterministic
299
299
  * identifiers the events carry, not via step caching. Every call site is
300
300
  * guarded so a throwing observer never fails the run.
301
+ *
302
+ * Events arrive with the run's `RunSession` from durable workflow input — a
303
+ * persisting realization needs its credential, scope, and identity.
304
+ * Implementations may ignore the parameter.
301
305
  */
302
- readonly emitProvenance?: (event: RunProvenanceEvent) => void;
306
+ readonly emitProvenance?: (event: RunProvenanceEvent, session: RunSession) => void;
303
307
  /**
304
308
  * Optional, fire-and-forget observation of the run's live shape, for a host driving a UI.
305
309
  *
@@ -87,12 +87,12 @@ function emitStreamPart(part) {
87
87
  * swallowed, never corrupt run state — integrity enforcement lives at the
88
88
  * host's own ledger, not here.
89
89
  */
90
- function emitProvenanceGuarded(deps, event) {
90
+ function emitProvenanceGuarded(deps, event, session) {
91
91
  const logger = (deps.logger ?? createNoopLogger()).named("executeAnalysis");
92
92
  if (!deps.emitProvenance)
93
93
  return;
94
94
  try {
95
- deps.emitProvenance(event);
95
+ deps.emitProvenance(event, session);
96
96
  }
97
97
  catch (err) {
98
98
  logger.error("emitProvenance threw", { runId: event.runId, event: event.type, ...logger.errorFields(err) });
@@ -437,7 +437,7 @@ export async function runExecuteAnalysisBody(input, deps) {
437
437
  planSummary: input.planSummary,
438
438
  stepCount: input.steps.length,
439
439
  atMs: startedAtMs,
440
- });
440
+ }, input.runSession);
441
441
  // Opening snapshot: every step pending. The scheduler's own map does not exist yet, and an
442
442
  // empty one yields exactly that — so a host has the run's full shape before the first dispatch
443
443
  // rather than learning the step list from whichever transition happens to land first.
@@ -930,7 +930,7 @@ async function runSchedulerLoop(args) {
930
930
  status: stepStatus,
931
931
  ...(stepDurationMs !== undefined ? { durationMs: stepDurationMs } : {}),
932
932
  atMs: settledAtMs,
933
- });
933
+ }, input.runSession);
934
934
  }
935
935
  await drainBudgetExceededNotifications(budgetExceededChildIds);
936
936
  return {
@@ -1000,7 +1000,7 @@ async function collectAndComplete(args) {
1000
1000
  status,
1001
1001
  atMs: terminalAtMs,
1002
1002
  durationMs: terminalAtMs - startedAtMs,
1003
- });
1003
+ }, input.runSession);
1004
1004
  try {
1005
1005
  await DBOS.runStep(async () => {
1006
1006
  unwrapOrThrow(await updateRunStatus(deps.pool, runId, status, failureReason ?? (status === "canceled" && !budgetExceeded ? "external_cancel" : null)));
@@ -6,13 +6,16 @@
6
6
  *
7
7
  * Phase 0 `phase0Resolve` — only step allowed to throw
8
8
  * (terminates → `target-unresolved`)
9
- * Phase 1 14 collectors in parallel — coverage-envelope failures
9
+ * Phase 1 15 collectors in parallel — coverage-envelope failures
10
10
  * Progress `emitProgress("deciding")`
11
11
  * Phase 2 2 decisions in parallel — LLM calls, attempt-numbered
12
12
  * Progress `emitProgress("fanning_out")`
13
13
  * Phase 3 4 fan-out blocks in parallel — per-item HTTP, withHost semaphore
14
14
  * Progress `emitProgress("assembling")`
15
- * Phase 4 `phase4Assemble` — deterministic
15
+ * Phase 4 `phase4Assemble` — aggregation + cached annotator
16
+ * Phase 4a `ta-approval-precedents` — one openFDA lookup, two projections
17
+ * Phase 4b `assembleSafetyCorroboration` — pure fold, no step of its own
18
+ * Phase 4c `investigateClaims` — propose / critique / re-verify / converge
16
19
  * Progress `emitProgress("synthesizing")`
17
20
  * Phase 5a 3 per-section syntheses in parallel
18
21
  * Phase 5b `dossierRecommendation` sequential
@@ -38,6 +41,7 @@ import type { Logger } from "../lib/logger.js";
38
41
  import type { UsageRecorder } from "../billing/usage-recorder.js";
39
42
  import type { AgentChat } from "../providers/types.js";
40
43
  import { TargetAssessmentInputSchema } from "./target-assessment/schemas.js";
44
+ import { type ClaimInvestigationConfig } from "./target-assessment/investigation/index.js";
41
45
  import { type ProgressPhase } from "./target-assessment/progress.js";
42
46
  export declare const ExecuteTargetAssessmentInputSchema: z.ZodObject<{
43
47
  assessmentId: z.ZodString;
@@ -91,6 +95,13 @@ export interface ExecuteTargetAssessmentDeps {
91
95
  readonly synthesisModel: string;
92
96
  /** LLM usage-accounting seam for the assessment's agent loops; omitted falls back to the no-op recorder. */
93
97
  readonly usageRecorder?: UsageRecorder;
98
+ /**
99
+ * Bounds the claim-investigation phase runs under. Omitted falls back to
100
+ * `DEFAULT_CLAIM_INVESTIGATION_CONFIG` — the values are cost and termination
101
+ * bounds, not domain constants, so an embedder wanting a deeper pass sets
102
+ * its own and the dossier reports what was in force.
103
+ */
104
+ readonly claimInvestigation?: Partial<ClaimInvestigationConfig>;
94
105
  }
95
106
  /**
96
107
  * Register the executeTargetAssessment workflow with DBOS. Caller is