@inflexa-ai/harness 0.16.0 → 0.18.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 (295) hide show
  1. package/dist/agents/conversation-agent.d.ts +3 -0
  2. package/dist/agents/conversation-agent.js +5 -2
  3. package/dist/agents/report-builder.d.ts +2 -0
  4. package/dist/agents/report-builder.js +2 -0
  5. package/dist/agents/sandbox/shared.d.ts +3 -0
  6. package/dist/agents/sandbox/shared.js +5 -0
  7. package/dist/agents/sandbox/types.d.ts +1 -1
  8. package/dist/app/chat-turn.d.ts +2 -0
  9. package/dist/app/chat-turn.js +11 -2
  10. package/dist/app/spawn-report-session.d.ts +85 -0
  11. package/dist/app/spawn-report-session.js +83 -0
  12. package/dist/app/synthesize-run.d.ts +3 -0
  13. package/dist/app/synthesize-run.js +2 -1
  14. package/dist/citations/__fixtures__/resolver.d.ts +3 -0
  15. package/dist/citations/__fixtures__/resolver.js +9 -0
  16. package/dist/citations/aggregate.d.ts +3 -0
  17. package/dist/citations/aggregate.js +44 -0
  18. package/dist/citations/clients/arxiv.d.ts +6 -0
  19. package/dist/citations/clients/arxiv.js +34 -0
  20. package/dist/citations/clients/common.d.ts +11 -0
  21. package/dist/citations/clients/common.js +43 -0
  22. package/dist/citations/clients/crossref.d.ts +8 -0
  23. package/dist/citations/clients/crossref.js +98 -0
  24. package/dist/citations/clients/doi-registry.d.ts +3 -0
  25. package/dist/citations/clients/doi-registry.js +95 -0
  26. package/dist/citations/clients/pubmed.d.ts +6 -0
  27. package/dist/citations/clients/pubmed.js +83 -0
  28. package/dist/citations/clients/semantic-scholar.d.ts +6 -0
  29. package/dist/citations/clients/semantic-scholar.js +60 -0
  30. package/dist/citations/compare.d.ts +9 -0
  31. package/dist/citations/compare.js +72 -0
  32. package/dist/citations/match.d.ts +11 -0
  33. package/dist/citations/match.js +173 -0
  34. package/dist/citations/normalize.d.ts +31 -0
  35. package/dist/citations/normalize.js +187 -0
  36. package/dist/citations/plan.d.ts +2 -0
  37. package/dist/citations/plan.js +59 -0
  38. package/dist/citations/resolve.d.ts +28 -0
  39. package/dist/citations/resolve.js +238 -0
  40. package/dist/citations/types.d.ts +583 -0
  41. package/dist/citations/types.js +129 -0
  42. package/dist/contracts/chat-events.d.ts +9 -0
  43. package/dist/contracts/index.d.ts +6 -2
  44. package/dist/contracts/index.js +3 -1
  45. package/dist/contracts/organ-system.d.ts +49 -0
  46. package/dist/contracts/organ-system.js +67 -0
  47. package/dist/contracts/report-blocks.d.ts +564 -0
  48. package/dist/contracts/report-blocks.js +117 -0
  49. package/dist/contracts/report-reference.d.ts +459 -0
  50. package/dist/contracts/report-reference.js +256 -0
  51. package/dist/contracts/schemas/chat-events.d.ts +2 -0
  52. package/dist/contracts/schemas/chat-events.js +2 -0
  53. package/dist/contracts/schemas/chat-parts.d.ts +8 -8
  54. package/dist/contracts/severity.d.ts +15 -0
  55. package/dist/contracts/severity.js +10 -0
  56. package/dist/contracts/target-dossier.d.ts +8673 -12970
  57. package/dist/contracts/target-dossier.js +534 -591
  58. package/dist/data/safety-panel-schema.d.ts +18 -5
  59. package/dist/data/safety-panel-schema.js +4 -4
  60. package/dist/execution/artifact-metadata.js +1 -0
  61. package/dist/execution/report-runner.js +1 -3
  62. package/dist/execution/run-synthesis.d.ts +3 -0
  63. package/dist/execution/run-synthesis.js +3 -0
  64. package/dist/index.d.ts +12 -1
  65. package/dist/index.js +22 -0
  66. package/dist/lib/async-utils.d.ts +16 -4
  67. package/dist/lib/async-utils.js +20 -15
  68. package/dist/lib/cache.d.ts +8 -0
  69. package/dist/lib/cache.js +35 -0
  70. package/dist/lib/host-concurrency.js +1 -0
  71. package/dist/lib/rate-limit.d.ts +35 -0
  72. package/dist/lib/rate-limit.js +121 -0
  73. package/dist/literature/sources/arxiv.d.ts +22 -0
  74. package/dist/literature/sources/arxiv.js +103 -0
  75. package/dist/literature/sources/http.d.ts +28 -0
  76. package/dist/literature/sources/http.js +97 -0
  77. package/dist/literature/sources/pubmed.d.ts +68 -0
  78. package/dist/literature/sources/pubmed.js +266 -0
  79. package/dist/literature/sources/semantic-scholar.d.ts +24 -0
  80. package/dist/literature/sources/semantic-scholar.js +71 -0
  81. package/dist/loop/__fixtures__/scripted-provider.js +23 -2
  82. package/dist/loop/run-agent.js +58 -10
  83. package/dist/loop/tool-detail.d.ts +40 -0
  84. package/dist/loop/tool-detail.js +35 -3
  85. package/dist/loop/types.d.ts +13 -0
  86. package/dist/memory/thread-history.d.ts +10 -0
  87. package/dist/memory/thread-history.js +12 -1
  88. package/dist/memory/thread-store.d.ts +142 -25
  89. package/dist/memory/thread-store.js +228 -40
  90. package/dist/prompts/literature-reviewer.d.ts +1 -1
  91. package/dist/prompts/literature-reviewer.js +2 -0
  92. package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +1 -1
  93. package/dist/prompts/target-assessment/briefs/executive-recommendation.js +5 -5
  94. package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +1 -1
  95. package/dist/prompts/target-assessment/briefs/liability-bullets.js +1 -1
  96. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +1 -1
  97. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +11 -3
  98. package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +1 -0
  99. package/dist/prompts/target-assessment/investigation/adversarial-critique.js +67 -0
  100. package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +1 -0
  101. package/dist/prompts/target-assessment/investigation/claim-reverification.js +49 -0
  102. package/dist/prompts/target-assessment/investigation/index.d.ts +3 -0
  103. package/dist/prompts/target-assessment/investigation/index.js +3 -0
  104. package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +1 -0
  105. package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +49 -0
  106. package/dist/prompts/target-assessment/tox-voice/index.d.ts +1 -1
  107. package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +8 -5
  108. package/dist/prompts/target-assessment/tox-voice/vocabulary.js +9 -20
  109. package/dist/providers/__fixtures__/session.d.ts +5 -4
  110. package/dist/providers/__fixtures__/session.js +4 -4
  111. package/dist/report-model/block-walk.d.ts +56 -0
  112. package/dist/report-model/block-walk.js +89 -0
  113. package/dist/report-model/draft-finish.d.ts +60 -0
  114. package/dist/report-model/draft-finish.js +56 -0
  115. package/dist/report-model/draft-operations.d.ts +125 -0
  116. package/dist/report-model/draft-operations.js +341 -0
  117. package/dist/report-model/draft-read.d.ts +59 -0
  118. package/dist/report-model/draft-read.js +109 -0
  119. package/dist/report-model/draft.d.ts +51 -0
  120. package/dist/report-model/draft.js +32 -0
  121. package/dist/report-model/fixture-resolver.d.ts +8 -0
  122. package/dist/report-model/fixture-resolver.js +252 -0
  123. package/dist/report-model/mint-snapshot.d.ts +30 -0
  124. package/dist/report-model/mint-snapshot.js +40 -0
  125. package/dist/report-model/reference-resolver.d.ts +113 -0
  126. package/dist/report-model/reference-resolver.js +60 -0
  127. package/dist/report-model/structural-validation.d.ts +22 -0
  128. package/dist/report-model/structural-validation.js +68 -0
  129. package/dist/report-model/validate.d.ts +40 -0
  130. package/dist/report-model/validate.js +80 -0
  131. package/dist/report-render/chart.d.ts +33 -0
  132. package/dist/report-render/chart.js +538 -0
  133. package/dist/report-render/page.d.ts +127 -0
  134. package/dist/report-render/page.js +307 -0
  135. package/dist/report-render/references.d.ts +32 -0
  136. package/dist/report-render/references.js +44 -0
  137. package/dist/report-render/render.d.ts +25 -0
  138. package/dist/report-render/render.js +125 -0
  139. package/dist/report-render/script-json.d.ts +13 -0
  140. package/dist/report-render/script-json.js +15 -0
  141. package/dist/report-render/types.d.ts +39 -0
  142. package/dist/report-render/types.js +10 -0
  143. package/dist/report-render/views/chart-view.d.ts +11 -0
  144. package/dist/report-render/views/chart-view.js +23 -0
  145. package/dist/report-render/views/page-view.d.ts +20 -0
  146. package/dist/report-render/views/page-view.js +49 -0
  147. package/dist/report-render/views/prose.d.ts +28 -0
  148. package/dist/report-render/views/prose.js +75 -0
  149. package/dist/report-render/views/references-view.d.ts +20 -0
  150. package/dist/report-render/views/references-view.js +64 -0
  151. package/dist/report-render/views/values.d.ts +42 -0
  152. package/dist/report-render/views/values.js +43 -0
  153. package/dist/runtime/assemble.d.ts +50 -4
  154. package/dist/runtime/assemble.js +47 -3
  155. package/dist/runtime/boot.d.ts +3 -3
  156. package/dist/runtime/boot.js +22 -5
  157. package/dist/runtime/dbos.d.ts +4 -4
  158. package/dist/state/artifacts.d.ts +16 -0
  159. package/dist/state/artifacts.js +18 -0
  160. package/dist/state/init.js +77 -1
  161. package/dist/state/purge-analysis.js +1 -0
  162. package/dist/state/report-versions.d.ts +132 -0
  163. package/dist/state/report-versions.js +194 -0
  164. package/dist/state/schema.d.ts +2 -2
  165. package/dist/tasks/data-profile.js +1 -0
  166. package/dist/tools/bio/chembl.js +1 -0
  167. package/dist/tools/bio/comptox.js +1 -0
  168. package/dist/tools/bio/drug-gene-interactions.js +1 -0
  169. package/dist/tools/bio/gene-disease-evidence.js +1 -0
  170. package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -1
  171. package/dist/tools/bio/gene-preclinical-profile.js +1 -0
  172. package/dist/tools/bio/keys.d.ts +4 -2
  173. package/dist/tools/bio/keys.js +3 -2
  174. package/dist/tools/bio/lookup-annotation.js +1 -0
  175. package/dist/tools/bio/opentargets.d.ts +3 -3
  176. package/dist/tools/bio/opentargets.js +4 -3
  177. package/dist/tools/bio/pubchem.js +1 -0
  178. package/dist/tools/bio/pubmed.js +4 -4
  179. package/dist/tools/bio/search-clinical-trials.js +1 -0
  180. package/dist/tools/bio/search-faers.js +1 -0
  181. package/dist/tools/bio/search-geo-datasets.js +1 -0
  182. package/dist/tools/bio/search-interactions.js +1 -0
  183. package/dist/tools/bio/target-safety.d.ts +4 -6
  184. package/dist/tools/bio/target-safety.js +7 -6
  185. package/dist/tools/define-tool.d.ts +19 -12
  186. package/dist/tools/define-tool.js +4 -2
  187. package/dist/tools/display/show-user.js +1 -0
  188. package/dist/tools/iterate-report.js +2 -0
  189. package/dist/tools/lib/chembl-client.d.ts +2 -0
  190. package/dist/tools/lib/chembl-client.js +1 -0
  191. package/dist/tools/lib/monarch-client.d.ts +42 -0
  192. package/dist/tools/lib/monarch-client.js +100 -0
  193. package/dist/tools/lib/ncbi-utils.d.ts +3 -58
  194. package/dist/tools/lib/ncbi-utils.js +2 -286
  195. package/dist/tools/lib/pubmed-client.d.ts +5 -12
  196. package/dist/tools/lib/pubmed-client.js +20 -79
  197. package/dist/tools/report/build-report.js +1 -0
  198. package/dist/tools/report/mint-preview-url.js +1 -0
  199. package/dist/tools/report/preview-snapshot.js +1 -0
  200. package/dist/tools/report/submit-report.js +1 -0
  201. package/dist/tools/report/version-fs.js +4 -0
  202. package/dist/tools/report-authoring/authoring-tools.d.ts +134 -0
  203. package/dist/tools/report-authoring/authoring-tools.js +352 -0
  204. package/dist/tools/research/context7-docs.js +2 -0
  205. package/dist/tools/research/generate-analogy-report.js +3 -2
  206. package/dist/tools/research/generate-plan.js +3 -0
  207. package/dist/tools/research/index.d.ts +1 -0
  208. package/dist/tools/research/index.js +1 -0
  209. package/dist/tools/research/inspect-data-profile.js +4 -0
  210. package/dist/tools/research/inspect-run.d.ts +1 -1
  211. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  212. package/dist/tools/research/literature-reviewer.js +3 -0
  213. package/dist/tools/research/resolve-citation.d.ts +3 -0
  214. package/dist/tools/research/resolve-citation.js +15 -0
  215. package/dist/tools/research/search-arxiv.d.ts +3 -19
  216. package/dist/tools/research/search-arxiv.js +10 -105
  217. package/dist/tools/research/search-github-repos.js +1 -0
  218. package/dist/tools/research/search-semantic-scholar.d.ts +8 -33
  219. package/dist/tools/research/search-semantic-scholar.js +30 -88
  220. package/dist/tools/sandbox/list-available-packages.js +23 -0
  221. package/dist/tools/sandbox/list-available-refs.js +37 -2
  222. package/dist/tools/sandbox/report-blocker.js +1 -0
  223. package/dist/tools/sandbox/skills.js +2 -0
  224. package/dist/tools/workspace/file-stat.js +1 -0
  225. package/dist/tools/workspace/show-file.js +2 -0
  226. package/dist/tools/workspace/show-plan.js +1 -0
  227. package/dist/workflows/execute-analysis.d.ts +8 -1
  228. package/dist/workflows/execute-analysis.js +6 -5
  229. package/dist/workflows/execute-target-assessment.d.ts +13 -2
  230. package/dist/workflows/execute-target-assessment.js +78 -16
  231. package/dist/workflows/sandbox-step.d.ts +5 -0
  232. package/dist/workflows/sandbox-step.js +1 -0
  233. package/dist/workflows/target-assessment/assemblers/index.d.ts +1 -1
  234. package/dist/workflows/target-assessment/assemblers/index.js +1 -1
  235. package/dist/workflows/target-assessment/assemblers/literature.d.ts +9 -27
  236. package/dist/workflows/target-assessment/assemblers/literature.js +32 -48
  237. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +17 -9
  238. package/dist/workflows/target-assessment/assemblers/orchestrator.js +195 -85
  239. package/dist/workflows/target-assessment/assemblers/safety.d.ts +70 -48
  240. package/dist/workflows/target-assessment/assemblers/safety.js +125 -42
  241. package/dist/workflows/target-assessment/collectors/index.d.ts +5 -2
  242. package/dist/workflows/target-assessment/collectors/index.js +48 -5
  243. package/dist/workflows/target-assessment/coverage.d.ts +43 -27
  244. package/dist/workflows/target-assessment/coverage.js +33 -29
  245. package/dist/workflows/target-assessment/fanout/index.js +1 -0
  246. package/dist/workflows/target-assessment/investigation/index.d.ts +97 -0
  247. package/dist/workflows/target-assessment/investigation/index.js +419 -0
  248. package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +26 -5
  249. package/dist/workflows/target-assessment/lib/approval-precedents.js +95 -29
  250. package/dist/workflows/target-assessment/lib/citation-validator.js +1 -1
  251. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +2 -2
  252. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +2 -3
  253. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -4
  254. package/dist/workflows/target-assessment/lib/compute-derived.d.ts +6 -5
  255. package/dist/workflows/target-assessment/lib/compute-derived.js +14 -5
  256. package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +5 -6
  257. package/dist/workflows/target-assessment/lib/family-complex-supplement.js +2 -3
  258. package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +72 -0
  259. package/dist/workflows/target-assessment/lib/fda-label-safety.js +166 -0
  260. package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -9
  261. package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -10
  262. package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +34 -0
  263. package/dist/workflows/target-assessment/lib/hpo-organ-map.js +63 -0
  264. package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +19 -0
  265. package/dist/workflows/target-assessment/lib/impc-organ-map.js +39 -0
  266. package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +24 -14
  267. package/dist/workflows/target-assessment/lib/meddra-organ-map.js +59 -47
  268. package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +1 -1
  269. package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +2 -3
  270. package/dist/workflows/target-assessment/lib/regulatory-actions.js +33 -4
  271. package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +44 -0
  272. package/dist/workflows/target-assessment/lib/safety-corroboration.js +273 -0
  273. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +5 -7
  274. package/dist/workflows/target-assessment/lib/target-identity-filter.js +5 -7
  275. package/dist/workflows/target-assessment/metrics.d.ts +1 -6
  276. package/dist/workflows/target-assessment/metrics.js +1 -13
  277. package/dist/workflows/target-assessment/phase4-assemble.d.ts +2149 -809
  278. package/dist/workflows/target-assessment/phase4-assemble.js +19 -19
  279. package/dist/workflows/target-assessment/phase5-persist.d.ts +20 -21
  280. package/dist/workflows/target-assessment/phase5-persist.js +56 -173
  281. package/dist/workflows/target-assessment/schemas.d.ts +48 -3
  282. package/dist/workflows/target-assessment/schemas.js +19 -0
  283. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +1 -0
  284. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +2 -0
  285. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +27 -63
  286. package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -4
  287. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +28 -155
  288. package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -6
  289. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -6
  290. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -18
  291. package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +1 -0
  292. package/dist/workflows/target-assessment/synthesis/index.d.ts +52 -5
  293. package/dist/workflows/target-assessment/synthesis/index.js +4 -1
  294. package/dist/workflows/target-assessment/synthesis-probes.d.ts +3 -3
  295. package/package.json +4 -1
@@ -0,0 +1,194 @@
1
+ /**
2
+ * The report version store -- the append-only record of report versions.
3
+ *
4
+ * A version is one immutable row. It holds the block document, the pinned
5
+ * snapshot, and the anchor. The store records a version and reads a version. It
6
+ * never updates a recorded row, thus a correction is a new version.
7
+ *
8
+ * A thread holds at most one version, and a named unique constraint on the thread
9
+ * id enforces it. The record reads nothing before the insert. A second record for
10
+ * one thread trips the constraint, and the store maps that trip to the typed
11
+ * refusal thread_already_holds_version.
12
+ *
13
+ * The store keeps the snapshot and the anchor as given, and it never mints a
14
+ * snapshot. A read parses the stored document and the stored snapshot with the
15
+ * current schemas. A row that fails a parse reads as a typed error, and it does
16
+ * not crash. An absent row reads as a normal absence.
17
+ *
18
+ * A version outlives its thread. The anchor columns sit on the row, thus a
19
+ * deleted thread leaves a recorded version in place. Only a purge of the analysis
20
+ * removes the version, through the analysis-id cascade.
21
+ */
22
+ import { randomUUID } from "node:crypto";
23
+ import { err, errAsync, ok } from "neverthrow";
24
+ import { z } from "zod";
25
+ import { ReportDocumentSchema } from "../contracts/report-blocks.js";
26
+ import { tryMutation, tryQuery } from "../lib/db-result.js";
27
+ /**
28
+ * The name of the unique constraint on thread_id. Postgres reports it on a
29
+ * duplicate-thread refusal, and the record matches it to map the trip to the
30
+ * thread_already_holds_version refusal.
31
+ */
32
+ const THREAD_UNIQUE_CONSTRAINT = "cortex_report_versions_one_per_thread";
33
+ /**
34
+ * One pinned artifact of a stored snapshot. The value carries the content hash,
35
+ * an optional file type, and optional rows. The `z.ZodType<ReportSnapshot>`
36
+ * annotation below keeps this schema and the `ArtifactSnapshot` type of the
37
+ * reference model in step, thus a change to one shows as a compile error there.
38
+ */
39
+ const ArtifactSnapshotSchema = z.object({
40
+ hash: z.string(),
41
+ fileType: z.string().nullable().optional(),
42
+ rows: z.array(z.record(z.string(), z.union([z.string(), z.number()]))).optional(),
43
+ });
44
+ /**
45
+ * The stored-snapshot schema. The annotation ties the parse output to the
46
+ * `ReportSnapshot` type of the reference model, thus the store and the structural
47
+ * validation never disagree about the snapshot shape. The mint never fills `rows`,
48
+ * but the schema admits what the type admits.
49
+ */
50
+ const ReportSnapshotSchema = z.object({
51
+ artifacts: z.record(z.string(), ArtifactSnapshotSchema),
52
+ citations: z.array(z.string()).optional(),
53
+ });
54
+ function reduceIssues(error) {
55
+ return error.issues.map((issue) => ({
56
+ path: issue.path.map((segment) => String(segment)).join("."),
57
+ message: issue.message,
58
+ }));
59
+ }
60
+ const _assertRecordError = true;
61
+ const _assertReadError = true;
62
+ /**
63
+ * The full row projection every read shares. `parent_seq` is a bigint, thus the
64
+ * driver hands it back as text, and `rowToVersion` is the single place that turns
65
+ * it into a number.
66
+ */
67
+ const VERSION_COLUMNS = "version_id, analysis_id, thread_id, parent_thread_id, parent_seq::text AS parent_seq, parent_version_id, document, snapshot, created_at";
68
+ /**
69
+ * The insert. The row carries no ordinal, thus the statement writes the given
70
+ * columns directly. `created_at` takes the column default.
71
+ */
72
+ const INSERT_SQL = `INSERT INTO cortex_report_versions
73
+ (version_id, analysis_id, thread_id, parent_thread_id, parent_seq,
74
+ parent_version_id, document, snapshot)
75
+ VALUES ($1, $2, $3, $4, $5::bigint, $6, $7::jsonb, $8::jsonb)`;
76
+ function rowToVersion(row) {
77
+ const document = ReportDocumentSchema.safeParse(row.document);
78
+ if (!document.success) {
79
+ return err({ type: "corrupt_version", op: "report-versions.read", versionId: row.version_id, part: "document", issues: reduceIssues(document.error) });
80
+ }
81
+ const snapshot = ReportSnapshotSchema.safeParse(row.snapshot);
82
+ if (!snapshot.success) {
83
+ return err({ type: "corrupt_version", op: "report-versions.read", versionId: row.version_id, part: "snapshot", issues: reduceIssues(snapshot.error) });
84
+ }
85
+ return ok({
86
+ versionId: row.version_id,
87
+ analysisId: row.analysis_id,
88
+ threadId: row.thread_id,
89
+ parentThreadId: row.parent_thread_id,
90
+ parentSeq: row.parent_seq === null ? null : Number(row.parent_seq),
91
+ parentVersionId: row.parent_version_id,
92
+ document: document.data,
93
+ snapshot: snapshot.data,
94
+ createdAt: row.created_at,
95
+ });
96
+ }
97
+ /**
98
+ * Map an insert failure to a record refusal. A trip of the thread constraint is
99
+ * thread_already_holds_version. Every other DbError stays unchanged.
100
+ */
101
+ function mapInsertError(error, threadId) {
102
+ if (error.type === "constraint_violation" && error.constraint === THREAD_UNIQUE_CONSTRAINT) {
103
+ return { type: "thread_already_holds_version", op: "report-versions.record", threadId };
104
+ }
105
+ return error;
106
+ }
107
+ /**
108
+ * Create a `ReportVersionStore` bound to a Postgres pool. The
109
+ * `cortex_report_versions` table is provisioned by the state-init DDL.
110
+ */
111
+ export function createReportVersionStore({ pool }) {
112
+ /**
113
+ * The analysis scope of a parent version, or `null` when no such row exists.
114
+ * Absence is not a verdict here: an unknown parent id falls through to the
115
+ * insert, where the self foreign key refuses it as a `DbError`.
116
+ */
117
+ function parentAnalysis(parentVersionId) {
118
+ return tryQuery("report-versions.record.parentScope", () => pool.query({
119
+ text: "SELECT analysis_id FROM cortex_report_versions WHERE version_id = $1",
120
+ values: [parentVersionId],
121
+ })).map(({ rows }) => rows[0]?.analysis_id ?? null);
122
+ }
123
+ function record(input) {
124
+ const parsed = ReportDocumentSchema.safeParse(input.document);
125
+ if (!parsed.success) {
126
+ return errAsync({ type: "malformed_document", op: "report-versions.record", issues: reduceIssues(parsed.error) });
127
+ }
128
+ const document = parsed.data;
129
+ // The parse guards the row against a caller bug through a cast. The store
130
+ // stores the given value, thus the parse changes no value.
131
+ const parsedSnapshot = ReportSnapshotSchema.safeParse(input.snapshot);
132
+ if (!parsedSnapshot.success) {
133
+ return errAsync({ type: "malformed_snapshot", op: "report-versions.record", issues: reduceIssues(parsedSnapshot.error) });
134
+ }
135
+ const insert = () => {
136
+ const versionId = randomUUID();
137
+ return tryMutation("report-versions.record.insert", async () => {
138
+ await pool.query({
139
+ text: INSERT_SQL,
140
+ values: [
141
+ versionId,
142
+ input.analysisId,
143
+ input.threadId,
144
+ input.parentThreadId,
145
+ input.parentSeq,
146
+ input.parentVersionId ?? null,
147
+ JSON.stringify(document),
148
+ JSON.stringify(input.snapshot),
149
+ ],
150
+ });
151
+ return { versionId };
152
+ }).mapErr((error) => mapInsertError(error, input.threadId));
153
+ };
154
+ if (input.parentVersionId === undefined) {
155
+ return insert();
156
+ }
157
+ const parentVersionId = input.parentVersionId;
158
+ return parentAnalysis(parentVersionId).andThen((parentAnalysisId) => {
159
+ if (parentAnalysisId !== null && parentAnalysisId !== input.analysisId) {
160
+ return errAsync({
161
+ type: "parent_analysis_mismatch",
162
+ op: "report-versions.record",
163
+ analysisId: input.analysisId,
164
+ parentVersionId,
165
+ parentAnalysisId,
166
+ });
167
+ }
168
+ return insert();
169
+ });
170
+ }
171
+ function getVersion(versionId) {
172
+ return tryQuery("report-versions.getVersion", () => pool.query({
173
+ text: `SELECT ${VERSION_COLUMNS} FROM cortex_report_versions WHERE version_id = $1`,
174
+ values: [versionId],
175
+ })).andThen(({ rows }) => {
176
+ const row = rows[0];
177
+ if (!row)
178
+ return ok(null);
179
+ return rowToVersion(row);
180
+ });
181
+ }
182
+ function getThreadVersion(threadId) {
183
+ return tryQuery("report-versions.getThreadVersion", () => pool.query({
184
+ text: `SELECT ${VERSION_COLUMNS} FROM cortex_report_versions WHERE thread_id = $1`,
185
+ values: [threadId],
186
+ })).andThen(({ rows }) => {
187
+ const row = rows[0];
188
+ if (!row)
189
+ return ok(null);
190
+ return rowToVersion(row);
191
+ });
192
+ }
193
+ return { record, getVersion, getThreadVersion };
194
+ }
@@ -46,10 +46,10 @@ export declare const ArtifactRowSchema: z.ZodObject<{
46
46
  }, z.core.$strip>;
47
47
  export type ArtifactRow = z.infer<typeof ArtifactRowSchema>;
48
48
  export declare const RunStatus: z.ZodEnum<{
49
+ partial: "partial";
49
50
  running: "running";
50
51
  completed: "completed";
51
52
  failed: "failed";
52
- partial: "partial";
53
53
  canceled: "canceled";
54
54
  suspended_insufficient_funds: "suspended_insufficient_funds";
55
55
  }>;
@@ -77,10 +77,10 @@ export declare const CortexRunRowSchema: z.ZodObject<{
77
77
  threadId: z.ZodNullable<z.ZodString>;
78
78
  workflowName: z.ZodString;
79
79
  status: z.ZodEnum<{
80
+ partial: "partial";
80
81
  running: "running";
81
82
  completed: "completed";
82
83
  failed: "failed";
83
- partial: "partial";
84
84
  canceled: "canceled";
85
85
  suspended_insufficient_funds: "suspended_insufficient_funds";
86
86
  }>;
@@ -272,6 +272,7 @@ export async function runDataProfileBody(input, deps) {
272
272
  id: "submit_profile",
273
273
  description: "Submit the profiling results. Call this tool once after completing " + "all profiling work — it validates and records your findings.",
274
274
  inputSchema: ProfilerOutputSchema,
275
+ describeCall: "none",
275
276
  execute: async (input) => {
276
277
  capturedProfile = input;
277
278
  return ok({ status: "accepted" });
@@ -118,6 +118,7 @@ export const chemblTool = defineTool({
118
118
  "compounds here first. If ChEMBL misses the compound, resolve it via pubchem action='compound' and bridge back with pubchem action='crossrefs'.\n" +
119
119
  "An empty array is valid no-data, not an error — do not retry the same call.",
120
120
  inputSchema,
121
+ describeCall: "none",
121
122
  execute: async ({ action, query, searchType, chemblId, idType, activityType, limit }) => {
122
123
  switch (action) {
123
124
  case "compounds":
@@ -106,6 +106,7 @@ export function createComptoxTool(deps) {
106
106
  "This is environmental/industrial-chemical data: for drug-like compounds prefer PubChem or ChEMBL — dataset 'chemical' returns " +
107
107
  "`detail.pubchemCid` to bridge back.",
108
108
  inputSchema,
109
+ describeCall: "none",
109
110
  execute: async (input) => {
110
111
  const headers = getEpaCcteHeaders(deps.apiKey);
111
112
  const resolved = await resolveDtxsid(input.query, headers);
@@ -119,6 +119,7 @@ export function createDrugGeneInteractionsTool(deps) {
119
119
  "not be reached (a missing DRUGBANK_API_KEY lands here — tell the user and proceed with DGIdb). A `found: false` entry means the identifier " +
120
120
  "is not in DGIdb at all, usually a non-HUGO symbol or a brand name. Neither is worth retrying unchanged.",
121
121
  inputSchema,
122
+ describeCall: "none",
122
123
  execute: async (input) => {
123
124
  const inputs = typeof input.query === "string" ? [input.query] : input.query;
124
125
  const selected = input.sources ?? ["dgidb"];
@@ -118,6 +118,7 @@ export function createGeneDiseaseEvidenceTool(deps) {
118
118
  "could not be reached (a missing DISGENET_API_KEY lands here — tell the user and proceed with the other two); 'not_applicable' means that " +
119
119
  "corpus has no lookup for this queryType. Only 'no_data' is evidence of absence, and none is worth retrying unchanged.",
120
120
  inputSchema,
121
+ describeCall: "none",
121
122
  execute: async (input) => {
122
123
  const applicable = applicableSources(input.queryType);
123
124
  const requested = input.sources ?? applicable;
@@ -51,7 +51,7 @@ export declare const genePreclinicalProfileTool: import("../define-tool.js").Too
51
51
  geneSymbol: string;
52
52
  include?: ("expression" | "knockout")[] | undefined;
53
53
  species?: ("homo_sapiens" | "mus_musculus" | "rattus_norvegicus" | "canis_lupus_familiaris" | "macaca_mulatta")[] | undefined;
54
- minRank?: "high" | "medium" | "low" | "absent" | undefined;
54
+ minRank?: "absent" | "high" | "medium" | "low" | undefined;
55
55
  tissueLimit?: number | undefined;
56
56
  phenotypeLimit?: number | undefined;
57
57
  }, PreclinicalOutput>;
@@ -105,6 +105,7 @@ export const genePreclinicalProfileTool = defineTool({
105
105
  "SPARSE OUTPUT IS VALID NO-DATA, not an error — do not retry. A null humanEnsemblId means the symbol did not resolve; a null mouseMarkerSymbol " +
106
106
  "with no phenotype terms means the gene has not been IMPC-phenotyped, which is common.",
107
107
  inputSchema,
108
+ describeCall: "none",
108
109
  execute: async ({ geneSymbol, include, species, minRank, tissueLimit, phenotypeLimit }) => {
109
110
  const halves = include ?? ["expression", "knockout"];
110
111
  const wantExpression = halves.includes("expression");
@@ -13,13 +13,15 @@ export interface BioToolKeys {
13
13
  readonly epaCcte: string;
14
14
  readonly ncbi?: string;
15
15
  readonly github?: string;
16
+ readonly semanticScholar?: string;
16
17
  }
17
18
  /**
18
19
  * The NCBI-backed literature tool, built from the shared key slice.
19
20
  *
20
21
  * `pubmed` is the consolidated literature tool (search / details / fulltext
21
- * behind one `action`). ClinVar is no longer its own tool — it is one of the
22
- * corpora behind `gene_disease_evidence`, which also needs the NCBI key.
22
+ * behind one `action`). ClinVar reaches a caller through
23
+ * `gene_disease_evidence`, as one of the corpora behind it, and it needs the
24
+ * same NCBI key.
23
25
  */
24
26
  export declare function createNcbiTools(keys: BioToolKeys): {
25
27
  pubmed: Tool;
@@ -6,8 +6,9 @@ import { createPubMedTool } from "./pubmed.js";
6
6
  * The NCBI-backed literature tool, built from the shared key slice.
7
7
  *
8
8
  * `pubmed` is the consolidated literature tool (search / details / fulltext
9
- * behind one `action`). ClinVar is no longer its own tool — it is one of the
10
- * corpora behind `gene_disease_evidence`, which also needs the NCBI key.
9
+ * behind one `action`). ClinVar reaches a caller through
10
+ * `gene_disease_evidence`, as one of the corpora behind it, and it needs the
11
+ * same NCBI key.
11
12
  */
12
13
  export function createNcbiTools(keys) {
13
14
  return {
@@ -122,6 +122,7 @@ export const lookupAnnotationTool = defineTool({
122
122
  "search_interactions({action:'enrichment'}), which does the statistics.\n" +
123
123
  "An empty terms / annotations / pathways array is valid no-data — do not retry the same input.",
124
124
  inputSchema,
125
+ describeCall: "none",
125
126
  execute: async ({ vocabulary, query, goId, geneProductId, taxonId, organism, includeGenes, limit }) => {
126
127
  const cap = limit ?? DEFAULT_LIMIT;
127
128
  if (vocabulary !== "go") {
@@ -2,9 +2,9 @@
2
2
  * opentargets — one tool over the Open Targets Platform: target-disease
3
3
  * association evidence, read from a gene or from a disease.
4
4
  *
5
- * The curated safety liabilities this tool used to serve now belong to
6
- * `target_safety`, which pairs them with the local secondary-pharmacology panel
7
- * and resolves the Ensembl id they need.
5
+ * Curated safety liabilities are not served here: `target_safety` owns them,
6
+ * because it pairs them with the local secondary-pharmacology panel and
7
+ * resolves the Ensembl id they need.
8
8
  *
9
9
  * The input is a flat object with an `action` discriminator — not a
10
10
  * `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
@@ -2,9 +2,9 @@
2
2
  * opentargets — one tool over the Open Targets Platform: target-disease
3
3
  * association evidence, read from a gene or from a disease.
4
4
  *
5
- * The curated safety liabilities this tool used to serve now belong to
6
- * `target_safety`, which pairs them with the local secondary-pharmacology panel
7
- * and resolves the Ensembl id they need.
5
+ * Curated safety liabilities are not served here: `target_safety` owns them,
6
+ * because it pairs them with the local secondary-pharmacology panel and
7
+ * resolves the Ensembl id they need.
8
8
  *
9
9
  * The input is a flat object with an `action` discriminator — not a
10
10
  * `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
@@ -62,6 +62,7 @@ export const openTargetsTool = defineTool({
62
62
  "disease name silently returns an EMPTY result rather than an error — resolve a symbol to its ENSG id with search_gene first.\n" +
63
63
  "NO-DATA IS FINAL — do not retry the same id. Empty associations mean no evidence, or an unresolvable id.",
64
64
  inputSchema,
65
+ describeCall: "none",
65
66
  execute: async (input) => {
66
67
  const limit = input.limit ?? DEFAULT_LIMIT;
67
68
  switch (input.action) {
@@ -76,6 +76,7 @@ export const pubchemTool = defineTool({
76
76
  "then chembl action='mechanism' / 'bioactivity' there for curated activity data. " +
77
77
  "An empty results / crossRefs / assays array is valid no-data (no match; the CID is in no external registry; the compound was never screened) — do not retry.",
78
78
  inputSchema,
79
+ describeCall: "none",
79
80
  execute: async (input) => {
80
81
  switch (input.action) {
81
82
  case "compound":
@@ -140,16 +140,16 @@ export function createPubMedTool(deps) {
140
140
  return `fulltext ${input.pmcId ?? ""}`;
141
141
  }
142
142
  },
143
- execute: async (input) => {
143
+ execute: async (input, context) => {
144
144
  switch (input.action) {
145
145
  case "search":
146
146
  return ok(await searchPubmed(deps.ncbiApiKey, input.query, {
147
147
  maxResults: input.maxResults ?? DEFAULTS.searchResults,
148
148
  ...(input.sort ? { sort: input.sort } : {}),
149
149
  ...(input.dateRange ? { dateRange: input.dateRange } : {}),
150
- }));
150
+ }, context.signal));
151
151
  case "details": {
152
- const { articles, notFound } = await getArticleDetails(deps.ncbiApiKey, input.pmids);
152
+ const { articles, notFound } = await getArticleDetails(deps.ncbiApiKey, input.pmids, context.signal);
153
153
  return ok({
154
154
  articles: boundArticleDetails(articles, {
155
155
  maxAuthors: input.maxAuthors ?? DEFAULTS.maxAuthors,
@@ -162,7 +162,7 @@ export function createPubMedTool(deps) {
162
162
  const parsed = await getArticleFullText(deps.ncbiApiKey, input.pmcId, {
163
163
  ...(input.sections ? { sections: input.sections } : {}),
164
164
  maxChars: input.maxChars ?? DEFAULTS.maxChars,
165
- });
165
+ }, context.signal);
166
166
  // "Not open-access" is an expected outcome — a data variant, not an error.
167
167
  if (!parsed)
168
168
  return ok({ pmcId: input.pmcId, available: false });
@@ -39,6 +39,7 @@ export const searchClinicalTrialsTool = defineTool({
39
39
  "landscape. Set true for the full protocol prose, and narrow `limit` when you do: a full summary runs to several thousand characters " +
40
40
  "per trial."),
41
41
  }),
42
+ describeCall: "none",
42
43
  execute: async ({ query, phase, status, limit = 10, includeSummaries = false }) => {
43
44
  const result = await searchTrials(query, { phase, status, limit });
44
45
  const trials = includeSummaries
@@ -23,6 +23,7 @@ export const searchFaersTool = defineTool({
23
23
  .describe("Max distinct adverse-reaction terms to return, most-reported first (default 15, max 100). totalReports covers the whole drug regardless."),
24
24
  serious: z.boolean().default(false).describe("When true, count only reports flagged serious (death, hospitalization, life-threatening, disabling)."),
25
25
  }),
26
+ describeCall: "none",
26
27
  execute: async ({ drugName, limit = 15, serious = false }) => {
27
28
  const result = await getFaersByDrug(drugName, { limit, serious });
28
29
  return ok({
@@ -63,6 +63,7 @@ export const searchGeoDatasetsTool = defineTool({
63
63
  .default(10)
64
64
  .describe("Max datasets to return (default 10, max 50). totalFound reports the true match count regardless."),
65
65
  }),
66
+ describeCall: "none",
66
67
  execute: async ({ query, organism, datasetType = "gse", limit = 10 }) => {
67
68
  let searchQuery = query;
68
69
  // Escape backslashes first, then quotes, so a `\` in the organism value
@@ -35,6 +35,7 @@ export const searchInteractionsTool = defineTool({
35
35
  .describe(`Max records returned — partners, network edges, or enriched terms depending on action (default ${DEFAULT_LIMIT}, max 500). ` +
36
36
  "Rows are ordered best-first (score descending for interactions, FDR ascending for enrichment), so the default keeps the strongest."),
37
37
  }),
38
+ describeCall: "none",
38
39
  execute: async ({ identifiers, species, action, minScore, limit }) => {
39
40
  if (action === "enrichment") {
40
41
  return ok(await getEnrichment(identifiers, species, { limit }));
@@ -3,11 +3,9 @@
3
3
  * sources that carry it: the curated secondary-pharmacology panel that ships
4
4
  * with the harness, and Open Targets' curated liabilities.
5
5
  *
6
- * These were `check_safety_panel` and `opentargets({action:'safety'})`, and the
7
- * panel tool's own description instructed calling them together — a pairing
8
- * every caller had to remember, expressed in prose that had already gone stale
9
- * (it named two tools that no longer exist). Both are keyed on the same target
10
- * and answer the same question, so the pairing belongs in the tool.
6
+ * Both sources are keyed on the same target and answer the same question, so
7
+ * the pairing belongs in the tool rather than in prose a caller has to
8
+ * remember: one call consults both and merges what they carry.
11
9
  *
12
10
  * Identifier resolution is the tool's job, not the caller's: Open Targets
13
11
  * accepts ONLY an Ensembl gene id, and it is taken from the panel entry when
@@ -41,7 +39,7 @@ export declare const targetSafetyTool: import("../define-tool.js").Tool<{
41
39
  identifiers: string[];
42
40
  identifierType?: "auto" | "gene_symbol" | "uniprot" | "chembl_id" | "ensembl_id" | undefined;
43
41
  sources?: ("opentargets" | "panel")[] | undefined;
44
- filterOrgan?: "hematologic" | "metabolic" | "immune" | "hepatic" | "cns" | "renal" | "respiratory" | "cardiac" | "gi" | undefined;
42
+ filterOrgan?: "hematologic" | "metabolic" | "immune" | "hepatic" | "musculoskeletal" | "cns" | "renal" | "reproductive" | "respiratory" | "cardiac" | "vascular" | "pns" | "gi" | "pancreas" | "endocrine_thyroid" | "dermatologic" | "ocular" | "oncology" | undefined;
45
43
  minSeverity?: "high" | "medium" | "low" | undefined;
46
44
  liabilityLimit?: number | undefined;
47
45
  }, {
@@ -3,11 +3,9 @@
3
3
  * sources that carry it: the curated secondary-pharmacology panel that ships
4
4
  * with the harness, and Open Targets' curated liabilities.
5
5
  *
6
- * These were `check_safety_panel` and `opentargets({action:'safety'})`, and the
7
- * panel tool's own description instructed calling them together — a pairing
8
- * every caller had to remember, expressed in prose that had already gone stale
9
- * (it named two tools that no longer exist). Both are keyed on the same target
10
- * and answer the same question, so the pairing belongs in the tool.
6
+ * Both sources are keyed on the same target and answer the same question, so
7
+ * the pairing belongs in the tool rather than in prose a caller has to
8
+ * remember: one call consults both and merges what they carry.
11
9
  *
12
10
  * Identifier resolution is the tool's job, not the caller's: Open Targets
13
11
  * accepts ONLY an Ensembl gene id, and it is taken from the panel entry when
@@ -15,7 +13,9 @@
15
13
  */
16
14
  import { ok } from "neverthrow";
17
15
  import { z } from "zod";
18
- import { CHEMBL_RE, ORGAN_SYSTEMS, SEVERITIES, SafetyPanelFileSchema, UNIPROT_RE, } from "../../data/safety-panel-schema.js";
16
+ import { ORGAN_SYSTEMS } from "../../contracts/organ-system.js";
17
+ import { SEVERITIES } from "../../contracts/severity.js";
18
+ import { CHEMBL_RE, SafetyPanelFileSchema, UNIPROT_RE } from "../../data/safety-panel-schema.js";
19
19
  import panelData from "../../data/safety-panel.json" with { type: "json" };
20
20
  import { defineTool } from "../define-tool.js";
21
21
  import { resolveSymbolToEnsemblId } from "../lib/ensembl-client.js";
@@ -100,6 +100,7 @@ export const targetSafetyTool = defineTool({
100
100
  "means no CURATED liability, not that none exists. Both are valid no-data — do not retry. Read `perSource` and each row's `opentargetsNote`: a " +
101
101
  "ChEMBL or UniProt identifier absent from the panel cannot be resolved to an Ensembl id, so its Open Targets half is skipped rather than empty.",
102
102
  inputSchema,
103
+ describeCall: "none",
103
104
  execute: async ({ identifiers, identifierType, sources, filterOrgan, minSeverity, liabilityLimit }) => {
104
105
  const selected = sources ?? [...SOURCES];
105
106
  const wantPanel = selected.includes("panel");
@@ -95,20 +95,28 @@ export interface ToolDefinition<Schema extends z.ZodType, Output> {
95
95
  readonly inputSchema: Schema;
96
96
  readonly executionMode?: ToolExecutionMode;
97
97
  /**
98
- * One line naming what THIS call is doing — the call-time counterpart of
98
+ * One line that names what THIS call does — the call-time counterpart of
99
99
  * `description`. `description` self-describes the tool at attach time, so an
100
- * agent knows what it holds; `describeCall` self-describes one invocation, so
100
+ * agent knows what it holds. `describeCall` self-describes one invocation, so
101
101
  * a surface can render four `update_working_memory` calls as four distinct
102
102
  * lines instead of four identical chips.
103
103
  *
104
- * It lives here, beside `inputSchema`, because that is the one place the
105
- * compiler checks the two against each other. A host-side formatter reading
106
- * `input.path` by string key breaks silently the day the schema moves; this
107
- * one fails to build. The tool author writes it because the tool author knows
108
- * which field matters.
104
+ * The decision is REQUIRED. Give a hook, or give the literal `"none"`.
105
+ * `"none"` declares that the input of the tool cannot distinguish its calls,
106
+ * thus a hook can only restate the name of the tool. `defineTool` consumes
107
+ * the sentinel at construction, and the sentinel never reaches a packaged
108
+ * tool. Thus a reader of a `Tool` sees a function, or sees nothing. A hook
109
+ * that returns the string `"none"` is still a hook, because the discriminator
110
+ * is `typeof`, not equality.
109
111
  *
110
- * Synchronous and pure — no I/O, no ambient state. Dispatch is what the user
111
- * waits on, and a description must never be able to fail a call: the loop
112
+ * A hook lives here, beside `inputSchema`, because that is the one place
113
+ * where the compiler checks the two against each other. A host-side formatter
114
+ * that reads `input.path` by string key breaks silently the day the schema
115
+ * moves. This one fails to build. The tool author writes it, because the tool
116
+ * author knows which field matters.
117
+ *
118
+ * A hook is synchronous and pure — no I/O, no ambient state. Dispatch is what
119
+ * the user waits on, and a description must never fail a call: the loop
112
120
  * validates the input against `inputSchema` first, guards the call, and drops
113
121
  * the detail on any failure (see the tool-call-detail capability).
114
122
  *
@@ -116,10 +124,9 @@ export interface ToolDefinition<Schema extends z.ZodType, Output> {
116
124
  * removed, secrets redacted, length capped — happens once at the emit site,
117
125
  * never here.
118
126
  *
119
- * OPTIONAL, and absence is a normal state: a tool without it is observable
120
- * exactly as it is today. The hook never reaches the model.
127
+ * The hook never reaches the model.
121
128
  */
122
- describeCall?(input: z.infer<Schema>): string;
129
+ describeCall: ((input: z.infer<Schema>) => string) | "none";
123
130
  execute(input: z.infer<Schema>, ctx: ToolContext): Promise<Result<Output, ToolError>>;
124
131
  }
125
132
  /**
@@ -44,9 +44,11 @@ export function defineTool(def) {
44
44
  inputSchema: def.inputSchema,
45
45
  jsonSchema,
46
46
  executionMode,
47
- // Spread rather than assigned: a tool that declares no hook carries no
47
+ // Spread rather than assigned: a tool that declines carries no
48
48
  // `describeCall` key at all, so "has a hook" is one property check.
49
- ...(def.describeCall === undefined ? {} : { describeCall: def.describeCall }),
49
+ // `typeof` is the discriminator, not equality with the sentinel, so a
50
+ // hook that returns the string `"none"` still packages as a hook.
51
+ ...(typeof def.describeCall === "function" ? { describeCall: def.describeCall } : {}),
50
52
  execute: def.execute,
51
53
  };
52
54
  }
@@ -61,6 +61,7 @@ export const showUserTool = defineTool({
61
61
  "One card per call, rendered in call order; an identical repeat call resolves to the same card, so compose the " +
62
62
  "finished content before calling rather than re-rendering a draft.",
63
63
  inputSchema: ShowUserInputSchema,
64
+ describeCall: ({ kind, title }) => (title === undefined ? kind : `${kind}: ${title}`),
64
65
  execute: async (input, ctx) => {
65
66
  // `dataPath` is a reference the host resolves at render time; validate its
66
67
  // shape only (existence is a render-time host concern), and treat a
@@ -413,6 +413,7 @@ export const planReportTool = defineTool({
413
413
  "existing preview with `modifications` needs no brief — you may go " +
414
414
  "straight to `submit_report`.)",
415
415
  inputSchema: z.object({}),
416
+ describeCall: "none",
416
417
  executionMode: "inline",
417
418
  execute: async () => ok({
418
419
  schema: REPORT_BRIEF_JSON_SCHEMA,
@@ -472,6 +473,7 @@ export function createReportSubmitTool(deps) {
472
473
  "as an `error` string. Returns the preview id + version and emits a " +
473
474
  "preview card.",
474
475
  inputSchema: submitReportInputSchema,
476
+ describeCall: "none",
475
477
  execute: async (input, ctx) => {
476
478
  // The brief rides as `unknown` so its ~12k schema stays off the
477
479
  // always-on tool surface. Validate it here and return issues as DATA
@@ -58,6 +58,8 @@ export interface ChemblPolypharmHit {
58
58
  moleculeChemblId: string;
59
59
  targetChemblId: string;
60
60
  targetName: string | null;
61
+ /** The assay the reported activity was measured in, when ChEMBL names one. */
62
+ assayChemblId: string | null;
61
63
  standardType: string | null;
62
64
  standardValue: number | null;
63
65
  standardUnits: string | null;
@@ -431,6 +431,7 @@ export async function getModulatorPolypharmacology(moleculeChemblId, options = {
431
431
  moleculeChemblId,
432
432
  targetChemblId: tid,
433
433
  targetName: null,
434
+ assayChemblId: raw.assay_chembl_id ?? null,
434
435
  standardType: raw.standard_type ?? null,
435
436
  standardValue: parseNumeric(raw.standard_value),
436
437
  standardUnits: raw.standard_units ?? null,