@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
@@ -1,23 +1,43 @@
1
1
  /**
2
- * iterate_report — creates or iterates on a report.
2
+ * Report authoring tools for the conversation agent: `plan_report` + `submit_report`.
3
3
  *
4
- * Two modes:
5
- * - Creation (v1): pass `report` with title/audience/sources/sections.
6
- * Pre-flight stages every source into the preview's assets/ dir,
7
- * enriches the brief with kind/size/columns/headRows/rowCount, and
8
- * hands the report-builder agent a complete brief — no discovery, no
9
- * staging in the LLM loop.
10
- * - Iteration (v2+): pass `modifications` (natural language) with the
11
- * existing `previewId`. Optional top-level `sources` adds new assets
12
- * on top of the existing assets/ dir.
4
+ * WHY TWO TOOLS. The report brief (title/audience/sources plus the section
5
+ * union — narrative/metrics/figure/table/chart, each with its own
6
+ * encoding/transform/asset sub-fields) is a ~12k-char JSON schema. Every
7
+ * registered tool's schema ships on EVERY conversation turn, so parking that
8
+ * brief on an always-on tool would tax every turn for a tool used only when a
9
+ * report is actually built. We split the single former tool in two and deliver
10
+ * the heavy schema just-in-time:
13
11
  *
14
- * Emits a `data-report-preview` chat data part on success or `data-report-preview-failed`
15
- * on pre-flight or builder failure. The hosted preview surface is reached
16
- * through an injected `PreviewPublisher` seam (managed default mints a
17
- * short-lived run-authorization grant; local default returns "unavailable").
12
+ * - `plan_report` — a tiny, always-on trigger (no-arg). Its `execute` RETURNS
13
+ * the brief schema (`z.toJSONSchema(ReportBriefSchema)`) plus the authoring
14
+ * rules as its tool RESULT, so the full contract enters context only on a
15
+ * report-building turn — and thereafter rides in the cached history prefix,
16
+ * not at full price on every request.
17
+ * - `submit_report` — takes the composed brief and does the real work
18
+ * (pre-flight staging, previewId/versioning, `runReportIteration`, preview
19
+ * publishing). It accepts the brief as `unknown` so the 12k schema stays off
20
+ * the always-on surface, then validates it against `ReportBriefSchema` INSIDE
21
+ * `execute`, returning `{ ok: false, issues }` as DATA the model can fix (not
22
+ * a thrown error). That is the same trade `validate_plan` makes: a runtime
23
+ * contract in exchange for keeping the schema out of every request. The small
24
+ * iterate-mode + common fields (modifications / previewId / baseVersion /
25
+ * sources / format) stay fully typed — they cost little and guide iteration.
18
26
  *
19
- * The 4 custom report tools the builder drives are constructed inside
20
- * `runReportIteration` — see design.md Decision #9.
27
+ * On a valid brief, `submit_report`'s behaviour is identical to the former
28
+ * `iterate_report`: two modes (Create v1 via `report`; Iterate v2+ via
29
+ * `modifications` + `previewId`), pre-flight stages every source into the
30
+ * preview's assets/ dir, and it emits a `data-report-preview` chat data part on
31
+ * success or `data-report-preview-failed` on pre-flight/builder failure. The
32
+ * hosted preview surface is reached through an injected `PreviewPublisher` seam
33
+ * (managed default mints a short-lived run-authorization grant; local default
34
+ * returns "unavailable"). The 4 custom report tools the builder drives are
35
+ * constructed inside `runReportIteration` — see design.md Decision #9.
36
+ *
37
+ * Naming note: the report-builder SUB-agent has its OWN same-id `submit_report`
38
+ * terminal tool (`tools/report/submit-report.ts`). The two never share a tool
39
+ * roster — this one is the conversation agent's brief-submission entry point;
40
+ * that one is the builder's finalize gate.
21
41
  */
22
42
  import { ok } from "neverthrow";
23
43
  import { z } from "zod";
@@ -68,7 +88,7 @@ const SourceSchema = z.object({
68
88
  const StatItemSchema = z.object({
69
89
  label: z.string(),
70
90
  value: z.union([z.string(), z.number()]),
71
- unit: z.string().optional(),
91
+ unit: z.string().optional().describe("Unit rendered next to the value (e.g. '%', 'genes', 'kb')."),
72
92
  });
73
93
  const SortSpecSchema = z.object({
74
94
  column: z.string(),
@@ -76,97 +96,180 @@ const SortSpecSchema = z.object({
76
96
  });
77
97
  const ChartTypeSchema = z.enum(["bar", "line", "scatter", "histogram", "box", "heatmap", "pie"]);
78
98
  const ChartEncodingSchema = z.object({
79
- x: z.string().optional(),
80
- y: z.union([z.string(), z.array(z.string()).min(1)]).optional(),
81
- group: z.string().optional(),
82
- value: z.string().optional(),
99
+ x: z.string().optional().describe("Column on the x / category axis."),
100
+ y: z
101
+ .union([z.string(), z.array(z.string()).min(1)])
102
+ .optional()
103
+ .describe("Column on the y axis; an array plots one series per column."),
104
+ group: z.string().optional().describe("Column to split series (and colour) by."),
105
+ value: z.string().optional().describe("Value column for pie / heatmap-shaped charts."),
83
106
  });
84
107
  const INLINE_CHART_ROW_CAP = 500;
85
108
  const ChartInlineDataSchema = z.object({
86
- columns: z.array(z.string()).min(1),
109
+ columns: z.array(z.string()).min(1).describe("Column names of the inline rows, in render order."),
87
110
  rows: z
88
111
  .array(z.record(z.string(), z.union([z.string(), z.number(), z.null()])))
89
112
  .min(1)
90
- .max(INLINE_CHART_ROW_CAP),
113
+ .max(INLINE_CHART_ROW_CAP)
114
+ .describe("The pre-computed rows. Every value must come from a file you actually read — never fabricate, never estimate."),
91
115
  source: z
92
116
  .string()
93
117
  .min(1)
94
- .describe("Short free-text describing where the values came from (e.g. " +
95
- "'aggregated significant-gene counts from runs/r1/.../sig.csv, " +
96
- "runs/r2/.../sig.csv, runs/r3/.../sig.csv'). The builder renders " +
97
- "this as a footnote under the chart."),
118
+ .describe("Where the values came from — cite the real files (e.g. 'aggregated " +
119
+ "significant-gene counts from runs/r1/.../sig.csv, runs/r2/.../sig.csv'). " +
120
+ "The builder renders this verbatim as a footnote under the chart."),
98
121
  });
122
+ /**
123
+ * The editorial-emphasis channel every section carries. A fresh instance per
124
+ * section type — Zod inlines reused schemas rather than sharing a `$defs` ref.
125
+ */
126
+ const intentField = () => z
127
+ .string()
128
+ .describe("Why this section exists and what to emphasize (e.g. 'hero — headline " +
129
+ "finding', 'downplay if space is tight'). Emphasis, not layout — the " +
130
+ "builder picks the components, sizing and alternation.");
131
+ /** Free-text row transform — the reason to never pre-slice a CSV with `run_ephemeral`. */
132
+ const transformField = () => z
133
+ .string()
134
+ .optional()
135
+ .describe("Free-text row transform the builder applies client-side to the loaded " +
136
+ "asset — filter, derive, aggregate, sort (e.g. 'filter padj < 0.05 and " +
137
+ "abs(log2FoldChange) > 1', 'compute -log10(padj) as neg_log_padj', 'group " +
138
+ "by sample and sum count', 'sort by mean_count desc, take top 50'). Use " +
139
+ "this instead of running `run_ephemeral` to pre-slice the file. The text " +
140
+ "is rendered verbatim as a provenance footnote, so the data reads as " +
141
+ "processed, not fabricated.");
99
142
  const SectionSchema = z.discriminatedUnion("type", [
100
- z.object({
143
+ z
144
+ .object({
101
145
  type: z.literal("narrative"),
102
146
  title: z.string(),
103
- intent: z.string(),
104
- content: z.object({ prose: z.string().min(1) }),
105
- }),
106
- z.object({
147
+ intent: intentField(),
148
+ content: z.object({
149
+ prose: z.string().min(1).describe("Markdown prose you wrote. The builder never opens your data files to write it."),
150
+ }),
151
+ })
152
+ .describe("Prose you wrote — context, story, interpretation of the results."),
153
+ z
154
+ .object({
107
155
  type: z.literal("metrics"),
108
156
  title: z.string(),
109
- intent: z.string(),
110
- content: z.object({ stats: z.array(StatItemSchema).min(1) }),
111
- }),
112
- z.object({
157
+ intent: intentField(),
158
+ content: z.object({
159
+ stats: z.array(StatItemSchema).min(1).describe("Headline numbers you already derived — the builder does not compute them."),
160
+ }),
161
+ })
162
+ .describe("Labeled numbers you extracted — headline stats, at-a-glance summary."),
163
+ z
164
+ .object({
113
165
  type: z.literal("figure"),
114
166
  title: z.string(),
115
- intent: z.string(),
167
+ intent: intentField(),
116
168
  content: z.object({
117
- imageAsset: z.string().min(1),
169
+ imageAsset: z.string().min(1).describe("Staged asset name, not a path — a `sources[].as`, or the basename of a `sources[].path`."),
118
170
  caption: z.string().optional(),
119
171
  }),
120
- }),
121
- z.object({
172
+ })
173
+ .describe("A static image the analysis already produced (PNG/SVG). Prefer `chart` " +
174
+ "whenever the underlying data file exists — a chart is interactive, themed " +
175
+ "and re-encodable on iteration. Reach for `figure` only when the user asks " +
176
+ "for the existing image, the visual is genuinely image-only (e.g. a heatmap " +
177
+ "baked by an R script with no exportable data), or the image carries " +
178
+ "annotations the data does not."),
179
+ z
180
+ .object({
122
181
  type: z.literal("table"),
123
182
  title: z.string(),
124
- intent: z.string(),
183
+ intent: intentField(),
125
184
  content: z.object({
126
- dataAsset: z.string().min(1),
127
- columns: z.array(z.string()).optional(),
128
- topN: z.number().int().positive().optional(),
129
- sortBy: SortSpecSchema.optional(),
130
- transform: z.string().optional(),
185
+ dataAsset: z.string().min(1).describe("Staged asset name, not a path — `.csv`, `.tsv`, or `.json` (array of row objects)."),
186
+ columns: z.array(z.string()).optional().describe("Column subset to render, in order. Omit to render every column."),
187
+ topN: z.number().int().positive().optional().describe("Keep only the first N rows, applied after `sortBy` and `transform`."),
188
+ sortBy: SortSpecSchema.optional().describe("Sort applied before `topN`."),
189
+ transform: transformField(),
131
190
  caption: z.string().optional(),
132
191
  }),
133
- }),
134
- z.object({
192
+ })
193
+ .describe("A tabular asset rendered as a table. Pick a column subset and `topN` that fit the audience."),
194
+ z
195
+ .object({
135
196
  type: z.literal("chart"),
136
197
  title: z.string(),
137
- intent: z.string(),
198
+ intent: intentField(),
138
199
  content: z
139
200
  .object({
140
- dataAsset: z.string().min(1).optional(),
141
- data: ChartInlineDataSchema.optional(),
142
- chartType: ChartTypeSchema,
143
- encoding: ChartEncodingSchema,
144
- topN: z.number().int().positive().optional(),
145
- sortBy: SortSpecSchema.optional(),
146
- transform: z.string().optional(),
201
+ dataAsset: z
202
+ .string()
203
+ .min(1)
204
+ .optional()
205
+ .describe("Staged asset name, not a path — `.csv`, `.tsv`, or `.json` (an array " +
206
+ "of row objects, or a pre-built ECharts option object the builder " +
207
+ "hands straight to `setOption`)."),
208
+ data: ChartInlineDataSchema.optional().describe("Escape hatch: inline rows for a cross-file aggregate you computed " +
209
+ "yourself from analysis files you ACTUALLY read (e.g. significant-gene " +
210
+ "counts pulled from several runs). Prefer `dataAsset`. More than one " +
211
+ "inline-data chart per report is a smell — the aggregate belongs in a " +
212
+ "persisted derived CSV."),
213
+ chartType: ChartTypeSchema.describe("Editorial choice — bar vs scatter changes the story. Always required, " + "including for a pre-built ECharts option asset."),
214
+ encoding: ChartEncodingSchema.describe("Which column feeds which channel, resolved AFTER `transform` — it may " +
215
+ "name a column the transform derives. Always required: pass `{}` when " +
216
+ "`dataAsset` is a pre-built ECharts option object."),
217
+ topN: z.number().int().positive().optional().describe("Keep only the first N rows, applied after `sortBy` and `transform`."),
218
+ sortBy: SortSpecSchema.optional().describe("Sort applied before `topN`."),
219
+ transform: transformField(),
147
220
  caption: z.string().optional(),
148
221
  })
149
222
  .refine((v) => (v.dataAsset !== undefined) !== (v.data !== undefined), { message: "Chart content needs exactly one of `dataAsset` or `data`." })
150
223
  .refine((v) => !(v.data !== undefined && v.transform !== undefined), {
151
224
  message: "`transform` is for dataAsset only — inline `data` is already pre-computed.",
152
225
  }),
153
- }),
154
- z.object({
226
+ })
227
+ .describe("A tabular asset (or inline rows) rendered as an interactive ECharts plot. " +
228
+ "Default to this over `figure` whenever the data file exists. To show the " +
229
+ "same data as both a chart and a table, write two sections against the same " +
230
+ "`dataAsset` — the file is staged once."),
231
+ z
232
+ .object({
155
233
  type: z.literal("methods"),
156
234
  title: z.string(),
157
- intent: z.string(),
158
- content: z.object({ prose: z.string().min(1) }),
159
- }),
235
+ intent: intentField(),
236
+ content: z.object({
237
+ prose: z.string().min(1).describe("Markdown prose you wrote. The builder never opens your data files to write it."),
238
+ }),
239
+ })
240
+ .describe("Prose you wrote — what was done, how, and with which parameters."),
160
241
  ]);
161
- const ReportSchema = z.object({
162
- title: z.string(),
163
- audience: z.string(),
164
- styleGuidance: z.string().optional(),
165
- sources: z.array(SourceSchema).default([]),
166
- sections: z.array(SectionSchema).min(1),
242
+ /**
243
+ * The report brief the conversation agent composes: title/audience + sources +
244
+ * the section union. This is the ~12k-char schema that `plan_report` hands back
245
+ * just-in-time and that `submit_report` validates its `report` field against
246
+ * inside `execute`. Exported for `plan_report`, `submit_report`, and the
247
+ * brief-shape tests.
248
+ */
249
+ export const ReportBriefSchema = z.object({
250
+ title: z.string().describe("Report title. Persisted with the preview and reused as the card title on every later version."),
251
+ audience: z
252
+ .string()
253
+ .describe("Who reads this (e.g. 'wet-lab collaborators', 'a computational-biology PI'). The builder tunes tone, density and jargon to it."),
254
+ styleGuidance: z.string().optional().describe("Optional art direction for the builder (e.g. 'dense and print-ready', 'no hero imagery')."),
255
+ sources: z
256
+ .array(SourceSchema)
257
+ .default([])
258
+ .describe("Every file the report renders — CSV/TSV, PNG/SVG, JSON. A section's " +
259
+ "`imageAsset` / `dataAsset` must name one of these staged assets or the " +
260
+ "call fails. Do NOT list markdown or synthesis.json: those are inputs to " +
261
+ "the prose you write, not to the renderer."),
262
+ sections: z.array(SectionSchema).min(1).describe("The report's sections, in the order they are rendered."),
167
263
  });
168
- /** Exported for schema-validation tests — the underlying Zod schema. */
169
- export const iterateReportInputSchema = z
264
+ /**
265
+ * `submit_report`'s always-on input surface. The heavy brief rides in `report`
266
+ * as `unknown` — typed that way ON PURPOSE so the ~12k `ReportBriefSchema` does
267
+ * NOT ship on every conversation turn; `execute` validates it against
268
+ * `ReportBriefSchema` and returns issues as data. The iterate-mode + common
269
+ * fields stay fully typed (they are small and guide iteration). Exported for
270
+ * the envelope-validation tests.
271
+ */
272
+ export const submitReportInputSchema = z
170
273
  .object({
171
274
  previewId: z
172
275
  .string()
@@ -174,36 +277,169 @@ export const iterateReportInputSchema = z
174
277
  .max(64)
175
278
  .optional()
176
279
  .describe("Preview group ID. Omit for new reports (auto-generated). " + "Required for iterations — must match the existing preview."),
177
- baseVersion: z.number().int().min(1).optional().describe("Version to iterate from (1-based). Defaults to latest."),
280
+ baseVersion: z
281
+ .number()
282
+ .int()
283
+ .min(1)
284
+ .optional()
285
+ .describe("Version to branch from (1-based) — use when the user prefers an earlier version. Defaults to the latest."),
178
286
  format: z.enum(["html", "pdf"]).default("html").describe("Output format. Defaults to 'html' — only set explicitly for PDF."),
179
- report: ReportSchema.optional().describe("CREATION ONLY (v1). Mutually exclusive with modifications."),
287
+ report: z
288
+ .unknown()
289
+ .optional()
290
+ .describe("CREATION ONLY (v1): the composed report brief. Get its schema + " +
291
+ "authoring rules from `plan_report` first. Validated on submit — an " +
292
+ "invalid brief comes back as `{ ok: false, issues }`, not a thrown error. " +
293
+ "Mutually exclusive with `modifications`."),
180
294
  modifications: z
181
295
  .string()
182
296
  .optional()
183
- .describe("ITERATION ONLY (v2+). Natural-language modification instructions. " + "The report-builder agent receives only this text — be specific."),
297
+ .describe("ITERATION ONLY (v2+). Natural-language change instructions. The " +
298
+ "report-builder sees ONLY this text plus the previous version's template " +
299
+ "— name what changes and leave everything else alone."),
184
300
  sources: z
185
301
  .array(SourceSchema)
186
302
  .optional()
187
- .describe("Iteration only — additional assets to stage on top of the existing " + "assets/ dir. For creation, put sources inside `report.sources`."),
303
+ .describe("Iteration only — additional assets to stage on top of the existing " + "assets/ dir. For creation, put sources inside the brief's `sources`."),
188
304
  })
189
305
  .refine((data) => (data.report !== undefined) !== (data.modifications !== undefined), {
190
306
  message: "Exactly one of `report` or `modifications` must be provided.",
191
307
  })
192
- .refine((data) => !(data.report && data.sources), {
193
- message: "Top-level `sources` is for iteration mode. For creation, put sources inside `report.sources`.",
308
+ .refine((data) => !(data.report !== undefined && data.sources), {
309
+ message: "Top-level `sources` is for iteration mode. For creation, put sources inside the brief's `sources`.",
194
310
  });
195
- export function createIterateReportTool(deps) {
311
+ // ── plan_report: the just-in-time authoring contract ────────────────
312
+ /**
313
+ * The authoring rules `plan_report` hands back alongside the brief schema.
314
+ * These are the cross-field, when-to-use rules the schema's per-field
315
+ * `.describe()` text cannot carry on its own — pulled from the former
316
+ * `iterate_report` description and `prompts/report-builder.ts`. Guidance for
317
+ * the conversation agent composing the brief, NOT for the builder.
318
+ */
319
+ const REPORT_AUTHORING_RULES = `# Composing a report brief
320
+
321
+ The report-builder agent NEVER sees the analysis tree — it receives only the
322
+ brief you compose, so the brief must be complete. You write the prose, the
323
+ numbers, and the chart encodings; the builder only does layout and visual
324
+ treatment. Pass the composed brief as \`submit_report\`'s \`report\` field.
325
+
326
+ ## Create vs iterate
327
+ - CREATE (v1): pass \`report\` (this brief), omit \`previewId\`. Every CSV / image /
328
+ JSON the report renders goes in \`report.sources\` — pre-flight stages each into
329
+ the preview's assets/ dir and parses its columns, first 5 rows, and row count
330
+ into the brief for you.
331
+ - ITERATE (v2+): pass \`modifications\` (natural-language change instructions)
332
+ plus the existing \`previewId\`, and NEVER \`report\` — passing \`report\` builds a
333
+ fresh report and discards all prior work. New data files go in the top-level
334
+ \`sources\`. \`baseVersion\` branches from an earlier version instead of the latest.
335
+
336
+ ## Sources
337
+ - Every file the report renders — CSV/TSV, PNG/SVG, JSON — must be listed in
338
+ \`sources\`. A section's \`imageAsset\` / \`dataAsset\` must name one of these staged
339
+ assets (its \`as\`, or the basename of its \`path\`) or the call fails.
340
+ - Markdown is NOT a source. Keep summary.md / synthesis.json out of \`sources\`;
341
+ their content reaches the report as prose you write into \`narrative\` /
342
+ \`methods\` sections.
343
+
344
+ ## Do NOT reach for run_ephemeral first
345
+ - Not to peek at a CSV — pre-flight already parsed its columns and head rows.
346
+ - Not to filter, slice, rank, or derive columns from a single CSV about to be
347
+ rendered — \`chart.content.transform\` and \`table.content.transform\` do exactly
348
+ that client-side, and the transform text renders as a provenance footnote.
349
+ - run_ephemeral is only for computation no single section transform covers:
350
+ cross-file aggregation, statistics needing a real numerical library, or a
351
+ derived CSV the report then lists as a fresh source.
352
+
353
+ ## Choosing a section type
354
+ - narrative — prose you wrote: context, story, interpretation.
355
+ - methods — prose you wrote: what was done, how, with which parameters.
356
+ - metrics — labeled headline numbers you already derived (the builder does not
357
+ compute them).
358
+ - table — a tabular asset rendered as a table; pick a column subset + topN that
359
+ fit the audience.
360
+ - chart — a tabular asset (or inline rows) rendered as an interactive ECharts
361
+ plot. PREFER chart over figure whenever the data file exists — a chart is
362
+ interactive, themed, and re-encodable on iteration.
363
+ - figure — a static image the analysis already produced (PNG/SVG). Reach for it
364
+ only when the user asks for the existing image, the visual is genuinely
365
+ image-only, or the image carries annotations the data does not.
366
+
367
+ ## Inline chart data
368
+ \`chart.content.data\` is an escape hatch for a cross-file aggregate you computed
369
+ yourself from files you ACTUALLY read. Prefer \`dataAsset\`. Never fabricate or
370
+ estimate values — every inline value must come from a file you read, and cite
371
+ the real files in \`data.source\`. More than one inline-data chart per report is a
372
+ smell — persist the aggregate as a derived CSV instead.
373
+
374
+ ## Result
375
+ \`submit_report\` returns the preview id + version and emits a preview card.
376
+ Pre-flight and builder failures come back as an \`error\` string on the result,
377
+ not as a thrown error. An invalid brief comes back as \`{ ok: false, issues }\` —
378
+ fix the named fields and resubmit.`;
379
+ /** Precomputed once at module load — the brief schema `plan_report` returns as data. */
380
+ const REPORT_BRIEF_JSON_SCHEMA = z.toJSONSchema(ReportBriefSchema);
381
+ /**
382
+ * `plan_report` — the tiny, always-on trigger (no args). Its result delivers
383
+ * the brief schema + authoring rules just-in-time, so the ~12k contract enters
384
+ * context only on a report-building turn (and thereafter rides in the cached
385
+ * history prefix). Compose the brief from what it returns, then call
386
+ * `submit_report`. Pure logic, no deps — hence a module-scope leaf tool.
387
+ */
388
+ export const planReportTool = defineTool({
389
+ id: "plan_report",
390
+ description: "Start building or iterating a report. Returns the report-brief schema " +
391
+ "and the authoring rules as its result — the full contract you compose " +
392
+ "against — so it is not carried on every turn. Call this first, compose " +
393
+ "the brief it describes, then call `submit_report` with it. (Iterating an " +
394
+ "existing preview with `modifications` needs no brief — you may go " +
395
+ "straight to `submit_report`.)",
396
+ inputSchema: z.object({}),
397
+ executionMode: "inline",
398
+ execute: async () => ok({
399
+ schema: REPORT_BRIEF_JSON_SCHEMA,
400
+ rules: REPORT_AUTHORING_RULES,
401
+ }),
402
+ });
403
+ /**
404
+ * `submit_report` — takes the composed brief and drives the report iteration.
405
+ * On a valid brief its behaviour is byte-for-byte the former `iterate_report`:
406
+ * pre-flight staging, previewId/versioning, `runReportIteration`, preview
407
+ * publishing. The brief rides as `unknown` on the wire (keeping its ~12k schema
408
+ * off the always-on surface) and is validated against `ReportBriefSchema` here.
409
+ */
410
+ export function createReportSubmitTool(deps) {
196
411
  return defineTool({
197
- id: "iterate_report",
198
- description: "Create or iterate on an HTML/PDF report. Two modes — use exactly one:\n" +
199
- "• CREATION (v1): pass `report` with title/audience/sources/sections, " +
200
- "omit `previewId`. Pre-flight stages every source and hands the " +
201
- "report-builder agent a complete brief.\n" +
202
- "• ITERATION (v2+): pass `modifications` (natural language) with the " +
203
- "existing `previewId`. Optional top-level `sources` adds new assets.\n" +
204
- "Returns the preview ID and version. Emits a preview chat data part.",
205
- inputSchema: iterateReportInputSchema,
412
+ id: "submit_report",
413
+ description: "Submit a composed report brief to build or iterate an HTML/PDF report " +
414
+ "(the report-builder renders it; it never sees the analysis tree). Call " +
415
+ "`plan_report` FIRST to get the brief schema + authoring rules, compose " +
416
+ "the brief, then call this. CREATE: pass `report` (the composed brief), " +
417
+ "omit `previewId`. ITERATE: pass `modifications` + the existing " +
418
+ "`previewId`, and NEVER `report`. An invalid brief comes back as " +
419
+ "`{ ok: false, issues }` — fix the named fields and resubmit (see " +
420
+ "`plan_report` for the schema); pre-flight and builder failures come back " +
421
+ "as an `error` string. Returns the preview id + version and emits a " +
422
+ "preview card.",
423
+ inputSchema: submitReportInputSchema,
206
424
  execute: async (input, ctx) => {
425
+ // The brief rides as `unknown` so its ~12k schema stays off the
426
+ // always-on tool surface. Validate it here and return issues as DATA
427
+ // (the validate_plan trade) — never a thrown error.
428
+ let brief;
429
+ if (input.report !== undefined) {
430
+ const parsed = ReportBriefSchema.safeParse(input.report);
431
+ if (!parsed.success) {
432
+ return ok({
433
+ ok: false,
434
+ issues: parsed.error.issues.map((i) => ({
435
+ path: i.path.join(".") || "(root)",
436
+ message: i.message,
437
+ })),
438
+ hint: "The `report` brief did not match the schema. Call `plan_report` for the full schema, fix the fields named in `issues`, and resubmit.",
439
+ });
440
+ }
441
+ brief = parsed.data;
442
+ }
207
443
  const { resourceId } = scopeResource(ctx.session.scope);
208
444
  const analysisRoot = deps.resolveWorkspaceRoot(resourceId);
209
445
  // Zod fills the default at parse time but the static type keeps it
@@ -215,7 +451,7 @@ export function createIterateReportTool(deps) {
215
451
  const metaPathAbs = join(previewRootAbs, PREVIEW_META_FILE);
216
452
  // Iteration mode: recover the title from the creation-time meta file
217
453
  // so the data-report-preview part keeps the original title across versions.
218
- const existingMeta = input.report ? null : await readPreviewMeta(metaPathAbs);
454
+ const existingMeta = brief ? null : await readPreviewMeta(metaPathAbs);
219
455
  // Build the preview-publishing seam for this iteration. The managed
220
456
  // default mints a 1h scoped access grant so preview calls outlive the chat
221
457
  // turn; the local default returns an "unavailable" publisher.
@@ -239,7 +475,7 @@ export function createIterateReportTool(deps) {
239
475
  });
240
476
  }
241
477
  // ── Pre-flight: stage declared sources into assets/. ────────
242
- const sources = input.report?.sources ?? input.sources ?? [];
478
+ const sources = brief?.sources ?? input.sources ?? [];
243
479
  let staged = [];
244
480
  if (sources.length > 0) {
245
481
  const result = await stageReportAssets({
@@ -269,9 +505,9 @@ export function createIterateReportTool(deps) {
269
505
  }
270
506
  // Cross-check creation-mode briefs — every section asset reference
271
507
  // must be present in staged[].
272
- if (input.report) {
508
+ if (brief) {
273
509
  const stagedNames = new Set(staged.map((s) => s.name));
274
- const missing = collectMissingAssetRefs(input.report.sections, stagedNames);
510
+ const missing = collectMissingAssetRefs(brief.sections, stagedNames);
275
511
  if (missing.length > 0) {
276
512
  const reason = `section asset references not staged: ${missing.join(", ")} (pass them in report.sources)`;
277
513
  await ctx.emit({
@@ -292,7 +528,7 @@ export function createIterateReportTool(deps) {
292
528
  });
293
529
  }
294
530
  }
295
- const prompt = input.report ? buildCreationPrompt(input.report, format, staged) : buildModificationPrompt(input.modifications, format, staged);
531
+ const prompt = brief ? buildCreationPrompt(brief, format, staged) : buildModificationPrompt(input.modifications, format, staged);
296
532
  const result = await runReportIteration({
297
533
  provider: deps.provider,
298
534
  pool: deps.pool,
@@ -331,20 +567,20 @@ export function createIterateReportTool(deps) {
331
567
  });
332
568
  }
333
569
  // Persist title on creation so iteration runs can recover it.
334
- if (input.report) {
570
+ if (brief) {
335
571
  try {
336
572
  await mkdir(previewRootAbs, { recursive: true });
337
573
  await writePreviewMeta(metaPathAbs, {
338
- title: input.report.title,
339
- audience: input.report.audience,
574
+ title: brief.title,
575
+ audience: brief.audience,
340
576
  format,
341
577
  });
342
578
  }
343
579
  catch (err) {
344
- console.warn(`[iterate-report] failed to persist preview meta: ${err instanceof Error ? err.message : err}`);
580
+ console.warn(`[submit-report] failed to persist preview meta: ${err instanceof Error ? err.message : err}`);
345
581
  }
346
582
  }
347
- const previewTitle = input.report?.title ?? existingMeta?.title ?? "Report";
583
+ const previewTitle = brief?.title ?? existingMeta?.title ?? "Report";
348
584
  await ctx.emit({
349
585
  type: "data-report-preview",
350
586
  data: {
@@ -468,7 +704,9 @@ export function stagedAssetsBlock(staged, opts = {}) {
468
704
  return out.join("\n");
469
705
  }
470
706
  function escapePipe(s) {
471
- return s.replace(/\|/g, "\\|");
707
+ // Escape backslashes first so a literal `\` in the cell cannot combine with
708
+ // the escape we add and re-expose an unescaped `|` that breaks the table.
709
+ return s.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
472
710
  }
473
711
  function formatSectionContent(s) {
474
712
  switch (s.type) {
@@ -3,6 +3,16 @@
3
3
  *
4
4
  * Used directly by target-assessment workflow steps and by tool wrappers.
5
5
  */
6
+ export interface ChemblCompound {
7
+ chemblId: string;
8
+ preferredCompoundName: string | null;
9
+ canonicalSmiles: string | null;
10
+ molecularWeight: number | null;
11
+ alogp: number | null;
12
+ molecularFormula: string | null;
13
+ }
14
+ /** How `searchCompounds` interprets its query string. */
15
+ export type CompoundSearchType = "target" | "compound" | "smiles";
6
16
  export interface ChemblTarget {
7
17
  targetChemblId: string;
8
18
  preferredName: string | null;
@@ -53,6 +63,8 @@ export interface ChemblPolypharmHit {
53
63
  standardUnits: string | null;
54
64
  pchemblValue: number | null;
55
65
  }
66
+ /** Search ChEMBL for compounds by target, compound name, or SMILES. */
67
+ export declare function searchCompounds(query: string, searchType: CompoundSearchType, limit?: number): Promise<ChemblCompound[]>;
56
68
  export declare function searchTargets(query: string, limit?: number): Promise<ChemblTarget[]>;
57
69
  export declare function getBioactivity(chemblId: string, type: "compound" | "target", options?: {
58
70
  activityType?: string;