@inflexa-ai/harness 0.35.8 → 0.36.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 (195) hide show
  1. package/dist/auth/types.d.ts +5 -14
  2. package/dist/auth/types.js +3 -10
  3. package/dist/billing/usage-recorder.d.ts +1 -1
  4. package/dist/contracts/analogy-report.d.ts +1 -1
  5. package/dist/contracts/analogy-report.js +1 -1
  6. package/dist/contracts/chat-parts.d.ts +2 -3
  7. package/dist/contracts/index.d.ts +0 -4
  8. package/dist/contracts/index.js +0 -2
  9. package/dist/index.d.ts +0 -7
  10. package/dist/index.js +0 -21
  11. package/dist/lib/metrics.d.ts +1 -1
  12. package/dist/lib/otel-spans.d.ts +0 -2
  13. package/dist/lib/otel-spans.js +0 -2
  14. package/dist/loop/run-agent.d.ts +1 -1
  15. package/dist/runtime/assemble.d.ts +0 -3
  16. package/dist/runtime/assemble.js +0 -3
  17. package/dist/state/index.d.ts +0 -2
  18. package/dist/state/index.js +0 -1
  19. package/dist/state/init.js +1 -60
  20. package/dist/state/purge-analysis.d.ts +1 -2
  21. package/dist/state/purge-analysis.js +1 -2
  22. package/dist/tasks/regulatory-corpus-boot-refresh.js +1 -1
  23. package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -3
  24. package/dist/tools/bio/gene-preclinical-profile.js +1 -3
  25. package/dist/tools/lib/chembl-client.d.ts +1 -4
  26. package/dist/tools/lib/chembl-client.js +1 -4
  27. package/dist/tools/lib/clinical-benchmarks-client.d.ts +1 -1
  28. package/dist/tools/lib/clinical-benchmarks-client.js +1 -1
  29. package/dist/tools/lib/clinical-trials-client.d.ts +0 -3
  30. package/dist/tools/lib/clinical-trials-client.js +0 -3
  31. package/dist/tools/lib/identifier-resolver.d.ts +5 -7
  32. package/dist/tools/lib/identifier-resolver.js +5 -7
  33. package/dist/tools/lib/iuphar-client.d.ts +1 -3
  34. package/dist/tools/lib/iuphar-client.js +1 -3
  35. package/dist/tools/lib/opentargets-client.d.ts +1 -1
  36. package/dist/tools/lib/opentargets-client.js +1 -1
  37. package/dist/tools/lib/protein-family-normalize.d.ts +5 -7
  38. package/dist/tools/lib/protein-family-normalize.js +5 -7
  39. package/dist/tools/lib/uniprot-client.d.ts +0 -7
  40. package/dist/tools/lib/uniprot-client.js +2 -9
  41. package/package.json +1 -1
  42. package/dist/contracts/target-assessment-row.d.ts +0 -85
  43. package/dist/contracts/target-assessment-row.js +0 -37
  44. package/dist/contracts/target-dossier.d.ts +0 -12423
  45. package/dist/contracts/target-dossier.js +0 -1282
  46. package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +0 -1
  47. package/dist/prompts/target-assessment/briefs/executive-recommendation.js +0 -85
  48. package/dist/prompts/target-assessment/briefs/index.d.ts +0 -4
  49. package/dist/prompts/target-assessment/briefs/index.js +0 -4
  50. package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +0 -1
  51. package/dist/prompts/target-assessment/briefs/liability-bullets.js +0 -114
  52. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +0 -1
  53. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +0 -37
  54. package/dist/prompts/target-assessment/briefs/translational-commentary.d.ts +0 -1
  55. package/dist/prompts/target-assessment/briefs/translational-commentary.js +0 -23
  56. package/dist/prompts/target-assessment/drugs-in-class.d.ts +0 -1
  57. package/dist/prompts/target-assessment/drugs-in-class.js +0 -29
  58. package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +0 -1
  59. package/dist/prompts/target-assessment/investigation/adversarial-critique.js +0 -67
  60. package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +0 -1
  61. package/dist/prompts/target-assessment/investigation/claim-reverification.js +0 -49
  62. package/dist/prompts/target-assessment/investigation/index.d.ts +0 -3
  63. package/dist/prompts/target-assessment/investigation/index.js +0 -3
  64. package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +0 -1
  65. package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +0 -49
  66. package/dist/prompts/target-assessment/modulator-triage.d.ts +0 -1
  67. package/dist/prompts/target-assessment/modulator-triage.js +0 -31
  68. package/dist/prompts/target-assessment/off-target-annotator.d.ts +0 -1
  69. package/dist/prompts/target-assessment/off-target-annotator.js +0 -28
  70. package/dist/prompts/target-assessment/tox-voice/core.d.ts +0 -1
  71. package/dist/prompts/target-assessment/tox-voice/core.js +0 -74
  72. package/dist/prompts/target-assessment/tox-voice/critique.d.ts +0 -7
  73. package/dist/prompts/target-assessment/tox-voice/critique.js +0 -109
  74. package/dist/prompts/target-assessment/tox-voice/exemplars.d.ts +0 -8
  75. package/dist/prompts/target-assessment/tox-voice/exemplars.js +0 -24
  76. package/dist/prompts/target-assessment/tox-voice/index.d.ts +0 -4
  77. package/dist/prompts/target-assessment/tox-voice/index.js +0 -4
  78. package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +0 -18
  79. package/dist/prompts/target-assessment/tox-voice/vocabulary.js +0 -61
  80. package/dist/state/target-assessments.d.ts +0 -68
  81. package/dist/state/target-assessments.js +0 -222
  82. package/dist/workflows/execute-target-assessment.d.ts +0 -118
  83. package/dist/workflows/execute-target-assessment.js +0 -631
  84. package/dist/workflows/target-assessment/assemblers/index.d.ts +0 -5
  85. package/dist/workflows/target-assessment/assemblers/index.js +0 -4
  86. package/dist/workflows/target-assessment/assemblers/literature.d.ts +0 -281
  87. package/dist/workflows/target-assessment/assemblers/literature.js +0 -971
  88. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +0 -118
  89. package/dist/workflows/target-assessment/assemblers/orchestrator.js +0 -945
  90. package/dist/workflows/target-assessment/assemblers/safety.d.ts +0 -374
  91. package/dist/workflows/target-assessment/assemblers/safety.js +0 -1068
  92. package/dist/workflows/target-assessment/assemblers/trials.d.ts +0 -30
  93. package/dist/workflows/target-assessment/assemblers/trials.js +0 -75
  94. package/dist/workflows/target-assessment/collectors/index.d.ts +0 -114
  95. package/dist/workflows/target-assessment/collectors/index.js +0 -538
  96. package/dist/workflows/target-assessment/coverage.d.ts +0 -97
  97. package/dist/workflows/target-assessment/coverage.js +0 -86
  98. package/dist/workflows/target-assessment/decisions/index.d.ts +0 -86
  99. package/dist/workflows/target-assessment/decisions/index.js +0 -127
  100. package/dist/workflows/target-assessment/fanout/index.d.ts +0 -32
  101. package/dist/workflows/target-assessment/fanout/index.js +0 -156
  102. package/dist/workflows/target-assessment/investigation/index.d.ts +0 -97
  103. package/dist/workflows/target-assessment/investigation/index.js +0 -419
  104. package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +0 -73
  105. package/dist/workflows/target-assessment/lib/approval-precedents.js +0 -185
  106. package/dist/workflows/target-assessment/lib/citation-validator.d.ts +0 -46
  107. package/dist/workflows/target-assessment/lib/citation-validator.js +0 -316
  108. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +0 -65
  109. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.js +0 -174
  110. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +0 -44
  111. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -152
  112. package/dist/workflows/target-assessment/lib/compute-derived.d.ts +0 -20
  113. package/dist/workflows/target-assessment/lib/compute-derived.js +0 -261
  114. package/dist/workflows/target-assessment/lib/compute-selectivity.d.ts +0 -15
  115. package/dist/workflows/target-assessment/lib/compute-selectivity.js +0 -18
  116. package/dist/workflows/target-assessment/lib/dedup-modulators.d.ts +0 -37
  117. package/dist/workflows/target-assessment/lib/dedup-modulators.js +0 -157
  118. package/dist/workflows/target-assessment/lib/evidence-conflict-detector.d.ts +0 -17
  119. package/dist/workflows/target-assessment/lib/evidence-conflict-detector.js +0 -100
  120. package/dist/workflows/target-assessment/lib/expression-constants.d.ts +0 -21
  121. package/dist/workflows/target-assessment/lib/expression-constants.js +0 -21
  122. package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +0 -20
  123. package/dist/workflows/target-assessment/lib/family-complex-supplement.js +0 -36
  124. package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +0 -72
  125. package/dist/workflows/target-assessment/lib/fda-label-safety.js +0 -166
  126. package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -30
  127. package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -44
  128. package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +0 -34
  129. package/dist/workflows/target-assessment/lib/hpo-organ-map.js +0 -63
  130. package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +0 -19
  131. package/dist/workflows/target-assessment/lib/impc-organ-map.js +0 -39
  132. package/dist/workflows/target-assessment/lib/intended-polypharm-filter.d.ts +0 -4
  133. package/dist/workflows/target-assessment/lib/intended-polypharm-filter.js +0 -24
  134. package/dist/workflows/target-assessment/lib/llm-step.d.ts +0 -80
  135. package/dist/workflows/target-assessment/lib/llm-step.js +0 -101
  136. package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +0 -40
  137. package/dist/workflows/target-assessment/lib/meddra-organ-map.js +0 -138
  138. package/dist/workflows/target-assessment/lib/recommendation-quality-gates.d.ts +0 -17
  139. package/dist/workflows/target-assessment/lib/recommendation-quality-gates.js +0 -99
  140. package/dist/workflows/target-assessment/lib/recommendation-sanitizer.d.ts +0 -2
  141. package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +0 -51
  142. package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +0 -25
  143. package/dist/workflows/target-assessment/lib/regulatory-actions.js +0 -176
  144. package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +0 -44
  145. package/dist/workflows/target-assessment/lib/safety-corroboration.js +0 -273
  146. package/dist/workflows/target-assessment/lib/structured-llm.d.ts +0 -61
  147. package/dist/workflows/target-assessment/lib/structured-llm.js +0 -102
  148. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +0 -59
  149. package/dist/workflows/target-assessment/lib/target-identity-filter.js +0 -95
  150. package/dist/workflows/target-assessment/lib/therapeutic-programs.d.ts +0 -20
  151. package/dist/workflows/target-assessment/lib/therapeutic-programs.js +0 -68
  152. package/dist/workflows/target-assessment/metrics.d.ts +0 -16
  153. package/dist/workflows/target-assessment/metrics.js +0 -30
  154. package/dist/workflows/target-assessment/phase4-assemble.d.ts +0 -3182
  155. package/dist/workflows/target-assessment/phase4-assemble.js +0 -39
  156. package/dist/workflows/target-assessment/phase5-persist.d.ts +0 -59
  157. package/dist/workflows/target-assessment/phase5-persist.js +0 -349
  158. package/dist/workflows/target-assessment/progress-stream.d.ts +0 -75
  159. package/dist/workflows/target-assessment/progress-stream.js +0 -110
  160. package/dist/workflows/target-assessment/progress.d.ts +0 -39
  161. package/dist/workflows/target-assessment/progress.js +0 -77
  162. package/dist/workflows/target-assessment/schemas.d.ts +0 -970
  163. package/dist/workflows/target-assessment/schemas.js +0 -318
  164. package/dist/workflows/target-assessment/steps/collectors/pathways.d.ts +0 -15
  165. package/dist/workflows/target-assessment/steps/collectors/pathways.js +0 -58
  166. package/dist/workflows/target-assessment/steps/fanout/aes-for-one-class-drug-step.d.ts +0 -34
  167. package/dist/workflows/target-assessment/steps/fanout/aes-for-one-class-drug-step.js +0 -31
  168. package/dist/workflows/target-assessment/steps/fanout/aes-for-one-trial-step.d.ts +0 -57
  169. package/dist/workflows/target-assessment/steps/fanout/aes-for-one-trial-step.js +0 -54
  170. package/dist/workflows/target-assessment/steps/fanout/faers-for-one-modulator-step.d.ts +0 -35
  171. package/dist/workflows/target-assessment/steps/fanout/faers-for-one-modulator-step.js +0 -32
  172. package/dist/workflows/target-assessment/steps/fanout/index.d.ts +0 -14
  173. package/dist/workflows/target-assessment/steps/fanout/index.js +0 -10
  174. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +0 -29
  175. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +0 -29
  176. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +0 -657
  177. package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -34
  178. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +0 -806
  179. package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -16
  180. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -28
  181. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -27
  182. package/dist/workflows/target-assessment/sub-workflows/index.d.ts +0 -15
  183. package/dist/workflows/target-assessment/sub-workflows/index.js +0 -11
  184. package/dist/workflows/target-assessment/sub-workflows/per-class-drug-aes.d.ts +0 -41
  185. package/dist/workflows/target-assessment/sub-workflows/per-class-drug-aes.js +0 -12
  186. package/dist/workflows/target-assessment/sub-workflows/per-modulator-faers.d.ts +0 -41
  187. package/dist/workflows/target-assessment/sub-workflows/per-modulator-faers.js +0 -12
  188. package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +0 -37
  189. package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.js +0 -12
  190. package/dist/workflows/target-assessment/sub-workflows/per-trial-aes.d.ts +0 -66
  191. package/dist/workflows/target-assessment/sub-workflows/per-trial-aes.js +0 -12
  192. package/dist/workflows/target-assessment/synthesis/index.d.ts +0 -607
  193. package/dist/workflows/target-assessment/synthesis/index.js +0 -354
  194. package/dist/workflows/target-assessment/synthesis-probes.d.ts +0 -56
  195. package/dist/workflows/target-assessment/synthesis-probes.js +0 -320
@@ -1,419 +0,0 @@
1
- /**
2
- * The claim-investigation phase.
3
- *
4
- * Corroboration says which organs several sources agree on. Agreement is not
5
- * adjudication: sources that all re-import one curation agree perfectly and are
6
- * wrong together. This phase interrogates each corroborated organ claim —
7
- * propose a mechanism, argue against it, re-verify, converge — and records what
8
- * survived, plus every candidate it did not get to and why.
9
- *
10
- * Three LLM steps per round, chosen per step rather than uniformly:
11
- *
12
- * - **propose** and **re-verify** are single-shot structured extractions
13
- * (`structuredLlmCall`). Everything they reason over is already in the
14
- * prompt; a retrieval surface would only let them ground an answer in
15
- * something the run never collected.
16
- * - **critique** is a real agent loop (`runToTerminal` over `runAgent`) with a
17
- * literature tool and a `defineTool` terminal recorder. Disconfirmation is a
18
- * search task: the dossier is assembled to state what the run found, not to
19
- * refute it, so a critic confined to it can only observe that the evidence
20
- * is thin — which is the rubber stamp this step exists to avoid.
21
- *
22
- * Nothing here scores a claim. The verdict is a four-word vocabulary the model
23
- * states, no threshold decides survival, and no claim is removed from the
24
- * dossier by anything this phase concludes.
25
- *
26
- * Durability arrives through the injected `runStep` seam and through
27
- * `structuredLlmCall`'s own attempt-named steps; this module imports no
28
- * durability engine.
29
- */
30
- import { ok } from "neverthrow";
31
- import { z } from "zod";
32
- import { composeSystemPrompt } from "../../../agents/system-prompt.js";
33
- import { createNoopLogger } from "../../../lib/console-logger.js";
34
- import { runToTerminal } from "../../../loop/run-to-terminal.js";
35
- import { adversarialCritiquePrompt, claimReverificationBrief, mechanismProposalBrief } from "../../../prompts/target-assessment/investigation/index.js";
36
- import { defineTool } from "../../../tools/define-tool.js";
37
- import { forSubAgent } from "../../../auth/types.js";
38
- import { structuredLlmCall } from "../lib/structured-llm.js";
39
- export const DEFAULT_CLAIM_INVESTIGATION_CONFIG = {
40
- claimBudget: 6,
41
- roundBound: 2,
42
- criticMaxIterations: 8,
43
- };
44
- function isBudgetCancel(value) {
45
- return typeof value === "object" && value !== null && "kind" in value && value.kind === "budget-exceeded";
46
- }
47
- // ── The model-facing claim contract ──────────────────────────────────
48
- /**
49
- * What a step is asked to return as support.
50
- *
51
- * Structurally the claim contract, minus the locator refinement — a refinement
52
- * cannot be expressed in a tool input schema, so requiring it here would mean
53
- * requiring it in prose and hoping. {@link resolveSupport} enforces it instead,
54
- * at this producer's own boundary.
55
- */
56
- const ModelEvidenceSchema = z.object({
57
- source: z.string().min(1).describe("Which corpus or record set the evidence came from."),
58
- pmid: z.string().optional().describe("PubMed identifier, digits only, of a record you actually read."),
59
- doi: z.string().optional().describe("Digital object identifier of a record you actually read."),
60
- accession: z.string().optional().describe("Database accession of a record you actually read."),
61
- regulatory_reference: z
62
- .object({
63
- document: z.string().min(1).describe("The regulatory document, e.g. an NDA/BLA application number."),
64
- section: z.string().optional().describe("The label section the statement came from."),
65
- })
66
- .optional()
67
- .describe("Set only when the evidence is a regulatory document you were shown or retrieved."),
68
- excerpt: z.string().optional().describe("Short quotation or paraphrase of what the record says."),
69
- });
70
- const ModelSupportSchema = z.discriminatedUnion("state", [
71
- z.object({
72
- state: z.literal("scored"),
73
- evidence: z
74
- .array(ModelEvidenceSchema)
75
- .min(1)
76
- .describe("Records supporting the claim. Each needs a pmid, doi, accession, or regulatory reference you retrieved."),
77
- }),
78
- z.object({
79
- state: z.literal("unknown"),
80
- reason: z.string().min(1).describe("One line saying why no record supports the claim. A complete, expected answer."),
81
- }),
82
- ]);
83
- /**
84
- * Resolve model-supplied support onto the claim contract.
85
- *
86
- * Evidence naming only a source resolves to nothing a reader can check, so it is
87
- * dropped; a scored claim left with no admissible evidence becomes `unknown`
88
- * with a reason saying so. Degrading here rather than failing keeps a
89
- * hallucinated citation from taking the whole assessment down at schema
90
- * validation, and keeps the honest answer cheaper than the invented one.
91
- */
92
- function resolveSupport(support) {
93
- if (support.state === "unknown")
94
- return { state: "unknown", reason: support.reason };
95
- const admissible = support.evidence.filter((e) => Boolean(e.pmid ?? e.doi ?? e.accession ?? e.regulatory_reference));
96
- if (admissible.length === 0) {
97
- return { state: "unknown", reason: "the cited support named a source but carried no publication identifier, doi, accession, or regulatory reference" };
98
- }
99
- return { state: "scored", evidence: admissible };
100
- }
101
- // ── Step output schemas ──────────────────────────────────────────────
102
- const MechanismProposalOutputSchema = z.object({
103
- mechanism: z.string().nullable().describe("How the target produces the liability in this organ. Null when the evidence supports none."),
104
- support: ModelSupportSchema,
105
- });
106
- const CritiqueOutputSchema = z.object({
107
- objection: z.string().min(1).describe("The concrete case against the claim. Not 'the evidence is limited'."),
108
- support: ModelSupportSchema,
109
- });
110
- const ReverificationOutputSchema = z.object({
111
- verdict: z.enum(["upheld", "weakened", "overturned", "undetermined"]).describe("What the record supports once the objection is weighed."),
112
- support: ModelSupportSchema,
113
- });
114
- // ── Prompt payloads ──────────────────────────────────────────────────
115
- /**
116
- * What a step is shown about the organ under investigation.
117
- *
118
- * The corroboration row already carries every contributing source, its signal,
119
- * and the record it came from, so the claim's whole evidential basis travels as
120
- * one value. The rollup row adds the assessment's own per-organ risk read.
121
- */
122
- function claimContext(row, dossier) {
123
- const rollup = dossier.safety_profile.organ_rollup;
124
- const rollupRow = rollup.coverage === "available" ? rollup.data.rows.find((r) => r.organ === row.organ) : undefined;
125
- return JSON.stringify({
126
- target: dossier.entity.symbol,
127
- organ: row.organ,
128
- corroboration: {
129
- corroborating_sources: row.corroborating_sources,
130
- independent_source_count: row.independent_source_count,
131
- contributions: row.contributions,
132
- },
133
- organ_rollup: rollupRow ?? null,
134
- }, null, 2);
135
- }
136
- const PROPOSER_AGENT_ID = "claim-mechanism-proposer";
137
- const CRITIC_AGENT_ID = "claim-adversarial-critic";
138
- const VERIFIER_AGENT_ID = "claim-reverifier";
139
- async function proposeMechanism(row, dossier, round, priorObjection, { deps }) {
140
- const prompt = [
141
- `Propose a mechanism for the ${row.organ} liability of ${dossier.entity.symbol}.`,
142
- "",
143
- claimContext(row, dossier),
144
- ...(priorObjection ? ["", "A previous round raised this objection. Answer it or revise the mechanism:", priorObjection] : []),
145
- ].join("\n");
146
- const result = await structuredLlmCall({
147
- ...(deps.logger ? { logger: deps.logger } : {}),
148
- stepName: `ta-investigate:propose:${row.organ}:${deps.attempt}:${round}`,
149
- agentId: PROPOSER_AGENT_ID,
150
- provider: deps.chatProvider,
151
- session: forSubAgent(deps.session, PROPOSER_AGENT_ID),
152
- system: composeSystemPrompt(mechanismProposalBrief),
153
- prompt,
154
- schema: MechanismProposalOutputSchema,
155
- model: deps.model,
156
- ...(deps.signal ? { signal: deps.signal } : {}),
157
- });
158
- if (result.kind === "budget-exceeded")
159
- return result;
160
- const out = result.value;
161
- if (out.mechanism === null || out.mechanism.trim().length === 0)
162
- return { kind: "ok", value: null };
163
- return { kind: "ok", value: { statement: out.mechanism, support: resolveSupport(out.support) } };
164
- }
165
- /**
166
- * Namespace the critic loop's own step names.
167
- *
168
- * `runAgent` names its steps `llm-{i}` / `tool-{name}-{id}`, which are unique
169
- * only within one run. Several claims are investigated concurrently and each
170
- * runs several rounds, so without this every one of them would write into the
171
- * same durable cache slots and replay another claim's answers.
172
- */
173
- function criticStepNames(organ, attempt, round) {
174
- const prefix = `ta-investigate:critique:${organ}:${attempt}:${round}`;
175
- return {
176
- llm: (i) => `${prefix}:llm-${i}`,
177
- tool: (name, id) => `${prefix}:tool-${name}-${id}`,
178
- };
179
- }
180
- async function critiqueClaim(row, dossier, mechanism, round, { deps, config }) {
181
- // A mutable holder rather than a plain `let`: the cell is written from the
182
- // tool's `execute` closure, which the compiler cannot see, so a bare local
183
- // would still read as `null` at every use after the loop returns.
184
- const cell = { recorded: null };
185
- const recordCritique = defineTool({
186
- id: "record_critique",
187
- description: "Record your case against the claim and finish. Call exactly once, with the strongest objection you " +
188
- "could build and the counter-evidence you actually retrieved. If you found no record that disconfirms " +
189
- "the claim, record the objection with support state 'unknown' and a one-line reason — that is a " +
190
- "complete answer, not a failure.",
191
- inputSchema: CritiqueOutputSchema,
192
- describeCall: ({ objection }) => objection,
193
- executionMode: "inline",
194
- execute: (input) => {
195
- cell.recorded = input;
196
- return Promise.resolve(ok({ recorded: true }));
197
- },
198
- });
199
- const agent = {
200
- id: CRITIC_AGENT_ID,
201
- systemPrompt: composeSystemPrompt(adversarialCritiquePrompt),
202
- model: deps.model,
203
- tools: [...deps.critiqueTools, recordCritique],
204
- maxIterations: config.criticMaxIterations,
205
- };
206
- const brief = [
207
- `Argue that the ${row.organ} liability claim about ${dossier.entity.symbol} does not hold.`,
208
- "",
209
- "Proposed mechanism:",
210
- mechanism.statement,
211
- "",
212
- "Evidence the assessment collected for this organ:",
213
- claimContext(row, dossier),
214
- ].join("\n");
215
- const controller = new AbortController();
216
- await runToTerminal(agent, [{ role: "user", content: brief }], forSubAgent(deps.session, CRITIC_AGENT_ID), {
217
- provider: deps.chatProvider,
218
- signal: deps.signal ?? controller.signal,
219
- emit: () => { },
220
- runStep: deps.runStep,
221
- formatStepName: criticStepNames(row.organ, deps.attempt, round),
222
- resolved: () => cell.recorded !== null,
223
- ...(deps.logger ? { logger: deps.logger } : {}),
224
- ...(deps.usageRecorder ? { usageRecorder: deps.usageRecorder } : {}),
225
- }, {
226
- tools: [recordCritique],
227
- nudge: "You have not recorded a critique. Call `record_critique` now with the case you built; if you found no disconfirming record, use support state 'unknown' with a one-line reason.",
228
- });
229
- const outcome = cell.recorded;
230
- if (outcome === null)
231
- return null;
232
- return { objection: outcome.objection, support: resolveSupport(outcome.support) };
233
- }
234
- async function reverifyClaim(row, dossier, mechanism, critique, round, { deps }) {
235
- const prompt = [
236
- `Re-verify the ${row.organ} liability claim about ${dossier.entity.symbol}.`,
237
- "",
238
- "Proposed mechanism:",
239
- mechanism.statement,
240
- "",
241
- "Objection raised against it:",
242
- critique.objection,
243
- "",
244
- "Counter-evidence offered with the objection:",
245
- JSON.stringify(critique.support, null, 2),
246
- "",
247
- "Evidence the assessment collected for this organ:",
248
- claimContext(row, dossier),
249
- ].join("\n");
250
- const result = await structuredLlmCall({
251
- ...(deps.logger ? { logger: deps.logger } : {}),
252
- stepName: `ta-investigate:reverify:${row.organ}:${deps.attempt}:${round}`,
253
- agentId: VERIFIER_AGENT_ID,
254
- provider: deps.chatProvider,
255
- session: forSubAgent(deps.session, VERIFIER_AGENT_ID),
256
- system: composeSystemPrompt(claimReverificationBrief),
257
- prompt,
258
- schema: ReverificationOutputSchema,
259
- model: deps.model,
260
- ...(deps.signal ? { signal: deps.signal } : {}),
261
- });
262
- if (result.kind === "budget-exceeded")
263
- return result;
264
- return { kind: "ok", value: { verdict: result.value.verdict, support: resolveSupport(result.value.support) } };
265
- }
266
- /**
267
- * Investigate one organ claim to convergence.
268
- *
269
- * The loop stops on the first of three conditions: a terminal verdict, a verdict
270
- * that repeats the previous round's, or the configured round bound. Critiques
271
- * are keyed by organ, so a later round's objection replaces the earlier one it
272
- * was written in answer to.
273
- */
274
- async function investigateOneClaim(row, dossier, stepDeps) {
275
- const logger = (stepDeps.deps.logger ?? createNoopLogger()).named("ta-claim-investigation").with({ organ: row.organ });
276
- let mechanism = null;
277
- let critique = null;
278
- let verdict = null;
279
- let previousVerdict = null;
280
- let support = { state: "unknown", reason: "the investigation recorded no verdict support" };
281
- let convergence = "round_bound_reached";
282
- let roundsRun = 0;
283
- for (let round = 1; round <= stepDeps.config.roundBound; round += 1) {
284
- roundsRun = round;
285
- let proposed;
286
- try {
287
- proposed = await proposeMechanism(row, dossier, round, critique?.objection ?? null, stepDeps);
288
- }
289
- catch (err) {
290
- logger.warn("mechanism proposal failed", { round, ...logger.errorFields(err) });
291
- return { kind: "unavailable", organ: row.organ, detail: `mechanism proposal failed: ${messageOf(err)}` };
292
- }
293
- if (proposed.kind === "budget-exceeded")
294
- return proposed;
295
- if (proposed.value === null) {
296
- return { kind: "unavailable", organ: row.organ, detail: "no mechanism was proposable from the evidence collected for this organ" };
297
- }
298
- mechanism = proposed.value;
299
- let rounded;
300
- try {
301
- rounded = await critiqueClaim(row, dossier, mechanism, round, stepDeps);
302
- }
303
- catch (err) {
304
- logger.warn("adversarial critique failed", { round, ...logger.errorFields(err) });
305
- return { kind: "unavailable", organ: row.organ, detail: `adversarial critique failed: ${messageOf(err)}` };
306
- }
307
- if (rounded === null) {
308
- // A verdict reached without the adversarial step is the rubber stamp
309
- // this phase exists to avoid, so the claim goes uninvestigated rather
310
- // than acquiring an unchallenged verdict.
311
- return { kind: "unavailable", organ: row.organ, detail: "the adversarial critic recorded no objection" };
312
- }
313
- critique = rounded;
314
- let verified;
315
- try {
316
- verified = await reverifyClaim(row, dossier, mechanism, critique, round, stepDeps);
317
- }
318
- catch (err) {
319
- logger.warn("re-verification failed", { round, ...logger.errorFields(err) });
320
- return { kind: "unavailable", organ: row.organ, detail: `re-verification failed: ${messageOf(err)}` };
321
- }
322
- if (verified.kind === "budget-exceeded")
323
- return verified;
324
- verdict = verified.value.verdict;
325
- support = verified.value.support;
326
- if (verdict === "upheld" || verdict === "overturned") {
327
- convergence = "verdict_terminal";
328
- break;
329
- }
330
- if (verdict === previousVerdict) {
331
- convergence = "verdict_settled";
332
- break;
333
- }
334
- previousVerdict = verdict;
335
- }
336
- if (verdict === null || mechanism === null) {
337
- return { kind: "unavailable", organ: row.organ, detail: "the investigation completed no round" };
338
- }
339
- return {
340
- kind: "investigated",
341
- row: {
342
- organ: row.organ,
343
- mechanism,
344
- critique,
345
- verdict,
346
- rounds_run: roundsRun,
347
- convergence,
348
- support,
349
- },
350
- };
351
- }
352
- function messageOf(err) {
353
- return err instanceof Error ? err.message : String(err);
354
- }
355
- // ── The phase ────────────────────────────────────────────────────────
356
- /**
357
- * Interrogate the run's corroborated organ claims.
358
- *
359
- * Coverage is the honest account of what happened: `not_loaded` when there was
360
- * no corroboration record to work from, `queried_no_data` when the fold produced
361
- * no corroborated organ, and `available` once at least one candidate reached the
362
- * phase — including when every one of them ended up in `not_investigated`, since
363
- * that list is what the section has to say in that case.
364
- */
365
- export async function investigateClaims(input, deps) {
366
- const config = { ...DEFAULT_CLAIM_INVESTIGATION_CONFIG, ...deps.config };
367
- const bounds = { round_bound: config.roundBound, claim_budget: config.claimBudget };
368
- if (input.corroboration.coverage === "not_loaded") {
369
- return { coverage: "not_loaded", reason: "no corroboration record was assembled for this run" };
370
- }
371
- const candidates = input.corroboration.coverage === "available" ? input.corroboration.data.rows : [];
372
- if (candidates.length === 0) {
373
- return { coverage: "queried_no_data", error: { message: "no corroborated organ claim reached the investigation" } };
374
- }
375
- const admitted = candidates.slice(0, config.claimBudget);
376
- const notInvestigated = candidates.slice(config.claimBudget).map((row) => ({
377
- organ: row.organ,
378
- reason: "exceeded_claim_budget",
379
- detail: `the run investigates at most ${config.claimBudget} corroborated claims; this one ranked below that`,
380
- }));
381
- // Organs the assessment's own per-organ risk read carries, which the
382
- // corroboration fold never admitted. They are candidates a reader would
383
- // expect to see interrogated, so their absence is stated rather than left
384
- // to be noticed.
385
- const corroboratedOrgans = new Set(candidates.map((r) => r.organ));
386
- const rollup = input.dossier.safety_profile.organ_rollup;
387
- if (rollup.coverage === "available") {
388
- for (const rollupRow of rollup.data.rows) {
389
- if (corroboratedOrgans.has(rollupRow.organ))
390
- continue;
391
- notInvestigated.push({
392
- organ: rollupRow.organ,
393
- reason: "not_corroborated",
394
- detail: "the per-organ risk rollup carries this organ, but fewer independent sources corroborated it than the fold requires",
395
- });
396
- }
397
- }
398
- const outcomes = await Promise.all(admitted.map((row) => investigateOneClaim(row, input.dossier, { deps, config })));
399
- const cancelled = outcomes.find(isBudgetCancel);
400
- if (cancelled)
401
- return cancelled;
402
- const rows = [];
403
- for (const outcome of outcomes) {
404
- if (outcome.kind === "investigated")
405
- rows.push(outcome.row);
406
- else if (outcome.kind === "unavailable") {
407
- notInvestigated.push({ organ: outcome.organ, reason: "investigation_unavailable", detail: outcome.detail });
408
- }
409
- }
410
- rows.sort((a, b) => a.organ.localeCompare(b.organ));
411
- notInvestigated.sort((a, b) => a.organ.localeCompare(b.organ) || a.reason.localeCompare(b.reason));
412
- const droppedByBudget = candidates.length - admitted.length;
413
- return {
414
- coverage: "available",
415
- data: { rows, not_investigated: notInvestigated, ...bounds },
416
- ...(droppedByBudget > 0 ? { dropped_count: droppedByBudget } : {}),
417
- };
418
- }
419
- export { isBudgetCancel as isClaimInvestigationBudgetExceeded };
@@ -1,73 +0,0 @@
1
- /**
2
- * Approval-precedent grounding for Phase-5 synthesis.
3
- *
4
- * Synthesis runs single-shot forced-`submit` LLM calls and can therefore call
5
- * no tool. This module does the openFDA / Drugs@FDA lookup deterministically
6
- * before synthesis so the fetched precedents can be injected into the synthesis
7
- * prompts as a static markdown block. The fetch, in-memory TTL cache, and
8
- * mapping mirror what a per-turn tool would have done, minus the tool wrapper.
9
- *
10
- * One fetch produces one typed form: each precedent keeps its label's safety
11
- * sections as `LabelSafetyText`. The markdown block is a projection of that
12
- * form, not a replacement for it — the same sections also project onto
13
- * per-organ regulatory signals via `segmentLabelSafety`.
14
- *
15
- * The lookup is cached in-process through the shared `BoundedTtlCache`: two
16
- * assessments in the same therapeutic area ask openFDA the same question, and
17
- * the answer moves on the scale of a label revision, not a run.
18
- */
19
- import type { DossierBody } from "../../../contracts/target-dossier.js";
20
- import { type FdaLabelSafety, type LabelSafetyText } from "./fda-label-safety.js";
21
- export type PrecedentModality = "small_molecule" | "biologic" | "gene_therapy" | "cell_therapy";
22
- export interface FetchApprovalPrecedentsInput {
23
- indication: string;
24
- modality?: PrecedentModality;
25
- mechanism?: string;
26
- }
27
- export interface Precedent {
28
- /** Null when openFDA published the label without one; such a label cannot be cited. */
29
- application_number: string | null;
30
- brand_name?: string;
31
- generic_name?: string;
32
- approval_date?: string;
33
- safety_sections: LabelSafetyText[];
34
- }
35
- /**
36
- * Query openFDA for prior approvals in a given indication. Returns
37
- * NDA/BLA application numbers, generic and brand names, approval dates
38
- * (label effective time), and label-section excerpts.
39
- *
40
- * openFDA answers a zero-match search with 404 — an expected "no precedents"
41
- * outcome, returned uncached so a later, more specific query is not masked.
42
- * Any other non-ok status (5xx, 429) is an unexpected failure and throws; the
43
- * caller wraps the throw.
44
- */
45
- export declare function fetchApprovalPrecedents(input: FetchApprovalPrecedentsInput): Promise<{
46
- precedents: Precedent[];
47
- }>;
48
- /** Test-only — drops the in-memory cache to avoid cross-test bleed. */
49
- export declare function __resetApprovalPrecedentCacheForTest(): void;
50
- /**
51
- * Pick the openFDA query term for a Phase-4 dossier: the top `indications`
52
- * row by `composite_score` (only when indications coverage is available and
53
- * has rows), falling back to the inferred therapeutic area, else null.
54
- */
55
- export declare function pickIndicationForPrecedents(dossier: DossierBody): string | null;
56
- /**
57
- * Project the fetched precedents onto the typed label form the per-organ
58
- * segmentation reads. A label without an application number is skipped: its
59
- * signals would carry no locator, and an unciteable signal is not evidence.
60
- */
61
- export declare function precedentLabelSafety(precedents: readonly Precedent[]): FdaLabelSafety[];
62
- /**
63
- * Render the fetched precedents as a markdown block for injection into the
64
- * synthesis prompts. The block always begins with the `## FDA approval
65
- * precedents` header, which the synthesis briefs reference verbatim.
66
- *
67
- * Synthesis is a single-shot forced-`submit` call that can reach no tool, so
68
- * this block is the whole of what the model gets: a plain string, no citation
69
- * the model is expected to resolve itself.
70
- */
71
- export declare function renderApprovalPrecedents(indication: string | null, result: {
72
- precedents: Precedent[];
73
- } | null): string;
@@ -1,185 +0,0 @@
1
- /**
2
- * Approval-precedent grounding for Phase-5 synthesis.
3
- *
4
- * Synthesis runs single-shot forced-`submit` LLM calls and can therefore call
5
- * no tool. This module does the openFDA / Drugs@FDA lookup deterministically
6
- * before synthesis so the fetched precedents can be injected into the synthesis
7
- * prompts as a static markdown block. The fetch, in-memory TTL cache, and
8
- * mapping mirror what a per-turn tool would have done, minus the tool wrapper.
9
- *
10
- * One fetch produces one typed form: each precedent keeps its label's safety
11
- * sections as `LabelSafetyText`. The markdown block is a projection of that
12
- * form, not a replacement for it — the same sections also project onto
13
- * per-organ regulatory signals via `segmentLabelSafety`.
14
- *
15
- * The lookup is cached in-process through the shared `BoundedTtlCache`: two
16
- * assessments in the same therapeutic area ask openFDA the same question, and
17
- * the answer moves on the scale of a label revision, not a run.
18
- */
19
- import { BoundedTtlCache } from "../../../lib/cache.js";
20
- import { LABEL_SAFETY_SECTIONS } from "./fda-label-safety.js";
21
- /** Per-block ingest budget — enough prose to segment, bounded for the step cache. */
22
- const SECTION_INGEST_CHARS = 4000;
23
- /** Per-section budget in the rendered markdown, which rides in every synthesis prompt. */
24
- const SECTION_RENDER_CHARS = 1000;
25
- function ingestSafetySections(r) {
26
- const sections = [];
27
- const push = (section, blocks) => {
28
- for (const block of blocks ?? []) {
29
- const text = block.trim();
30
- if (text.length === 0)
31
- continue;
32
- sections.push({ section, text: text.slice(0, SECTION_INGEST_CHARS) });
33
- }
34
- };
35
- push("boxed_warning", r.boxed_warning);
36
- // Current labels publish Section 5 as `warnings_and_precautions`; older ones
37
- // publish the same prose as `warnings`. Ingesting both files it twice.
38
- if (r.warnings_and_precautions?.length)
39
- push("warnings_and_precautions", r.warnings_and_precautions);
40
- else
41
- push("warnings", r.warnings);
42
- push("contraindications", r.contraindications);
43
- return sections;
44
- }
45
- const TTL_MS = 60 * 60 * 1000;
46
- /**
47
- * One entry per (indication, modality, mechanism) an assessment resolves, and a
48
- * host serves many assessments from one process — hence a bound, not a Map that
49
- * only ever grows.
50
- */
51
- const CACHE_MAXIMUM = 64;
52
- let cache = new BoundedTtlCache(CACHE_MAXIMUM);
53
- /**
54
- * Query openFDA for prior approvals in a given indication. Returns
55
- * NDA/BLA application numbers, generic and brand names, approval dates
56
- * (label effective time), and label-section excerpts.
57
- *
58
- * openFDA answers a zero-match search with 404 — an expected "no precedents"
59
- * outcome, returned uncached so a later, more specific query is not masked.
60
- * Any other non-ok status (5xx, 429) is an unexpected failure and throws; the
61
- * caller wraps the throw.
62
- */
63
- export async function fetchApprovalPrecedents(input) {
64
- const { indication, modality, mechanism } = input;
65
- const key = JSON.stringify({
66
- indication: indication.toLowerCase(),
67
- modality,
68
- mechanism,
69
- });
70
- const cached = cache.get(key);
71
- if (cached)
72
- return cached;
73
- const term = indication.replace(/"/g, "");
74
- const url = new URL("https://api.fda.gov/drug/label.json");
75
- url.searchParams.set("search", `indications_and_usage:"${term}"`);
76
- url.searchParams.set("limit", "10");
77
- const res = await fetch(url);
78
- if (res.status === 404)
79
- return { precedents: [] };
80
- if (!res.ok) {
81
- throw new Error(`openFDA label query failed: HTTP ${res.status}`);
82
- }
83
- const json = (await res.json());
84
- const precedents = (json.results ?? []).slice(0, 10).map((r) => ({
85
- application_number: r.openfda?.application_number?.[0] ?? null,
86
- brand_name: r.openfda?.brand_name?.[0],
87
- generic_name: r.openfda?.generic_name?.[0],
88
- approval_date: r.effective_time,
89
- safety_sections: ingestSafetySections(r),
90
- }));
91
- const value = { precedents };
92
- cache.set(key, value, TTL_MS);
93
- return value;
94
- }
95
- /** Test-only — drops the in-memory cache to avoid cross-test bleed. */
96
- export function __resetApprovalPrecedentCacheForTest() {
97
- cache = new BoundedTtlCache(CACHE_MAXIMUM);
98
- }
99
- /**
100
- * Pick the openFDA query term for a Phase-4 dossier: the top `indications`
101
- * row by `composite_score` (only when indications coverage is available and
102
- * has rows), falling back to the inferred therapeutic area, else null.
103
- */
104
- export function pickIndicationForPrecedents(dossier) {
105
- const indications = dossier.indications;
106
- if (indications.coverage === "available" && indications.data.rows.length > 0) {
107
- const top = indications.data.rows.reduce((best, row) => (row.composite_score > best.composite_score ? row : best));
108
- if (top.disease_name)
109
- return top.disease_name;
110
- }
111
- return dossier.liability_summary.inferred_therapeutic_area ?? null;
112
- }
113
- /**
114
- * Project the fetched precedents onto the typed label form the per-organ
115
- * segmentation reads. A label without an application number is skipped: its
116
- * signals would carry no locator, and an unciteable signal is not evidence.
117
- */
118
- export function precedentLabelSafety(precedents) {
119
- const labels = [];
120
- for (const p of precedents) {
121
- if (p.application_number === null || p.safety_sections.length === 0)
122
- continue;
123
- labels.push({
124
- application_number: p.application_number,
125
- drug_name: p.generic_name ?? p.brand_name ?? p.application_number,
126
- effective_time: p.approval_date,
127
- sections: p.safety_sections,
128
- });
129
- }
130
- return labels;
131
- }
132
- /**
133
- * The prose a reader meets for one section: the longest block published under
134
- * it. openFDA returns both the highlights summary and the full section under
135
- * the same key, and the full section is the one worth grounding on.
136
- */
137
- function renderableSections(precedent) {
138
- const longest = new Map();
139
- for (const { section, text } of precedent.safety_sections) {
140
- const current = longest.get(section);
141
- if (current === undefined || text.length > current.length)
142
- longest.set(section, text);
143
- }
144
- return LABEL_SAFETY_SECTIONS.filter((section) => longest.has(section)).map((section) => [section, longest.get(section).slice(0, SECTION_RENDER_CHARS)]);
145
- }
146
- /**
147
- * Render the fetched precedents as a markdown block for injection into the
148
- * synthesis prompts. The block always begins with the `## FDA approval
149
- * precedents` header, which the synthesis briefs reference verbatim.
150
- *
151
- * Synthesis is a single-shot forced-`submit` call that can reach no tool, so
152
- * this block is the whole of what the model gets: a plain string, no citation
153
- * the model is expected to resolve itself.
154
- */
155
- export function renderApprovalPrecedents(indication, result) {
156
- const header = "## FDA approval precedents";
157
- if (indication === null) {
158
- return [header, "", "No indication could be resolved from the dossier, so no FDA approval precedents were queried."].join("\n");
159
- }
160
- const precedents = result?.precedents ?? [];
161
- if (precedents.length === 0) {
162
- return [
163
- header,
164
- "",
165
- `No FDA approval precedents were found for "${indication}". Do not assert class precedents that are not present in the dossier.`,
166
- ].join("\n");
167
- }
168
- const lines = [
169
- header,
170
- "",
171
- `Prior FDA approvals retrieved for "${indication}". Ground class-precedent and disposition claims in these records; do not invent precedents beyond this list and the dossier.`,
172
- "",
173
- ];
174
- for (const p of precedents) {
175
- const generic = p.generic_name ?? "unknown generic";
176
- const brand = p.brand_name ?? "unknown brand";
177
- const date = p.approval_date ?? "unknown date";
178
- const application = p.application_number ?? "unknown application number";
179
- lines.push(`- ${generic} (${brand}), ${application}, approved ${date}`);
180
- for (const [section, excerpt] of renderableSections(p)) {
181
- lines.push(` - ${section}: ${excerpt}`);
182
- }
183
- }
184
- return lines.join("\n");
185
- }