@inflexa-ai/harness 0.13.0 → 0.14.1

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 (162) hide show
  1. package/dist/agents/conversation-agent.js +23 -16
  2. package/dist/agents/sandbox/cheminformatics-agent.js +1 -1
  3. package/dist/agents/sandbox/chromatin-agent.js +1 -1
  4. package/dist/agents/sandbox/dna-methylation-agent.js +1 -1
  5. package/dist/agents/sandbox/drug-repurposing-agent.js +5 -10
  6. package/dist/agents/sandbox/enrichment-agent.js +1 -1
  7. package/dist/agents/sandbox/genomic-variant-agent.js +1 -11
  8. package/dist/agents/sandbox/immune-profiling-agent.js +1 -1
  9. package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -1
  10. package/dist/agents/sandbox/network-agent.js +1 -1
  11. package/dist/agents/sandbox/pkpd-clinical-response-agent.js +3 -6
  12. package/dist/agents/sandbox/scientific-executor.js +1 -1
  13. package/dist/agents/sandbox/shared.js +7 -13
  14. package/dist/agents/sandbox/translational-safety-agent.js +4 -7
  15. package/dist/agents/sandbox/types.d.ts +5 -5
  16. package/dist/agents/sandbox/types.js +4 -4
  17. package/dist/app/chat-turn.js +4 -1
  18. package/dist/app/message-assembly.d.ts +5 -2
  19. package/dist/app/message-assembly.js +7 -2
  20. package/dist/app/run-activity.d.ts +13 -0
  21. package/dist/app/run-activity.js +51 -0
  22. package/dist/contracts/target-dossier.d.ts +10 -10
  23. package/dist/execution/report-runner.js +1 -1
  24. package/dist/execution/run-event-parts.d.ts +44 -0
  25. package/dist/execution/run-event-parts.js +0 -0
  26. package/dist/execution/run-event-stream.d.ts +91 -0
  27. package/dist/execution/run-event-stream.js +212 -0
  28. package/dist/execution/run-synthesis.js +1 -1
  29. package/dist/index.d.ts +5 -2
  30. package/dist/index.js +20 -1
  31. package/dist/input-sanitization.d.ts +14 -4
  32. package/dist/input-sanitization.js +27 -4
  33. package/dist/lib/storage.js +4 -1
  34. package/dist/loop/budget-exceeded.d.ts +18 -2
  35. package/dist/loop/budget-exceeded.js +49 -2
  36. package/dist/loop/run-agent.d.ts +22 -1
  37. package/dist/loop/run-agent.js +91 -25
  38. package/dist/loop/run-to-terminal.d.ts +22 -4
  39. package/dist/loop/run-to-terminal.js +20 -4
  40. package/dist/memory/ai-sdk-message-storage.d.ts +37 -4
  41. package/dist/memory/ai-sdk-message-storage.js +45 -4
  42. package/dist/memory/content-to-cortex.js +19 -3
  43. package/dist/memory/message-backfill.js +1 -1
  44. package/dist/memory/thread-history.js +34 -8
  45. package/dist/prompts/conversation.d.ts +1 -1
  46. package/dist/prompts/conversation.js +15 -6
  47. package/dist/prompts/literature-reviewer.d.ts +1 -1
  48. package/dist/prompts/literature-reviewer.js +16 -14
  49. package/dist/prompts/planner.js +15 -20
  50. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  51. package/dist/prompts/sandbox/drug-repurposing-agent.js +31 -22
  52. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  53. package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -10
  54. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  55. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +8 -7
  56. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  57. package/dist/prompts/sandbox/translational-safety-agent.js +20 -14
  58. package/dist/prompts/synthesis-agent.d.ts +1 -1
  59. package/dist/prompts/synthesis-agent.js +7 -7
  60. package/dist/providers/errors.d.ts +7 -0
  61. package/dist/providers/errors.js +88 -3
  62. package/dist/runtime/pools.d.ts +15 -1
  63. package/dist/runtime/pools.js +15 -1
  64. package/dist/sandbox/sandbox-step-translate.d.ts +15 -2
  65. package/dist/sandbox/sandbox-step-translate.js +17 -2
  66. package/dist/schemas/workflow-state.d.ts +2 -2
  67. package/dist/schemas/workflow-state.js +2 -2
  68. package/dist/state/data-profile.d.ts +53 -1
  69. package/dist/state/data-profile.js +66 -6
  70. package/dist/state/index.d.ts +3 -3
  71. package/dist/state/index.js +2 -2
  72. package/dist/state/init.js +37 -2
  73. package/dist/state/runs.d.ts +39 -0
  74. package/dist/state/runs.js +103 -0
  75. package/dist/tasks/data-profile-activity.d.ts +87 -0
  76. package/dist/tasks/data-profile-activity.js +56 -0
  77. package/dist/tasks/data-profile.js +66 -4
  78. package/dist/tools/bio/chembl.d.ts +1 -1
  79. package/dist/tools/bio/chembl.js +51 -54
  80. package/dist/tools/bio/comptox.js +35 -33
  81. package/dist/tools/bio/drug-gene-interactions.d.ts +76 -0
  82. package/dist/tools/bio/drug-gene-interactions.js +175 -0
  83. package/dist/tools/bio/gene-disease-evidence.d.ts +61 -0
  84. package/dist/tools/bio/gene-disease-evidence.js +187 -0
  85. package/dist/tools/bio/gene-preclinical-profile.d.ts +58 -0
  86. package/dist/tools/bio/gene-preclinical-profile.js +125 -0
  87. package/dist/tools/bio/index.d.ts +23 -16
  88. package/dist/tools/bio/index.js +29 -22
  89. package/dist/tools/bio/keys.d.ts +16 -9
  90. package/dist/tools/bio/keys.js +22 -13
  91. package/dist/tools/bio/lookup-annotation.d.ts +48 -0
  92. package/dist/tools/bio/lookup-annotation.js +173 -0
  93. package/dist/tools/bio/opentargets.d.ts +7 -11
  94. package/dist/tools/bio/opentargets.js +24 -35
  95. package/dist/tools/bio/pubchem.js +6 -4
  96. package/dist/tools/bio/pubmed.d.ts +15 -7
  97. package/dist/tools/bio/pubmed.js +79 -20
  98. package/dist/tools/bio/search-clinical-trials.d.ts +20 -1
  99. package/dist/tools/bio/search-clinical-trials.js +26 -5
  100. package/dist/tools/bio/search-faers.js +9 -3
  101. package/dist/tools/bio/search-gene.d.ts +0 -1
  102. package/dist/tools/bio/search-gene.js +4 -5
  103. package/dist/tools/bio/search-geo-datasets.js +10 -4
  104. package/dist/tools/bio/search-interactions.d.ts +8 -4
  105. package/dist/tools/bio/search-interactions.js +28 -12
  106. package/dist/tools/bio/target-safety.d.ts +59 -0
  107. package/dist/tools/bio/target-safety.js +191 -0
  108. package/dist/tools/execute-analysis.d.ts +1 -0
  109. package/dist/tools/execute-analysis.js +5 -5
  110. package/dist/tools/lib/dgidb-client.d.ts +49 -0
  111. package/dist/tools/lib/dgidb-client.js +168 -0
  112. package/dist/tools/lib/disgenet-client.d.ts +56 -0
  113. package/dist/tools/lib/disgenet-client.js +59 -0
  114. package/dist/tools/lib/drugbank-client.d.ts +83 -0
  115. package/dist/tools/lib/drugbank-client.js +95 -0
  116. package/dist/tools/lib/ensembl-client.d.ts +4 -1
  117. package/dist/tools/lib/ensembl-client.js +5 -3
  118. package/dist/tools/lib/gwas-catalog-client.d.ts +60 -0
  119. package/dist/tools/lib/gwas-catalog-client.js +136 -0
  120. package/dist/tools/lib/pharmgkb-client.d.ts +22 -0
  121. package/dist/tools/lib/pharmgkb-client.js +32 -0
  122. package/dist/tools/lib/pubmed-client.d.ts +57 -4
  123. package/dist/tools/lib/pubmed-client.js +57 -3
  124. package/dist/tools/lib/string-client.d.ts +25 -4
  125. package/dist/tools/lib/string-client.js +22 -5
  126. package/dist/tools/research/generate-plan.d.ts +16 -9
  127. package/dist/tools/research/generate-plan.js +405 -85
  128. package/dist/tools/research/inspect-data-profile.d.ts +31 -6
  129. package/dist/tools/research/inspect-data-profile.js +23 -7
  130. package/dist/tools/research/inspect-run.d.ts +33 -15
  131. package/dist/tools/research/inspect-run.js +155 -66
  132. package/dist/tools/research/literature-reviewer.js +7 -11
  133. package/dist/workflows/execute-analysis.d.ts +63 -0
  134. package/dist/workflows/execute-analysis.js +73 -2
  135. package/dist/workflows/sandbox-step.js +1 -11
  136. package/dist/workflows/target-assessment/decisions/index.d.ts +1 -1
  137. package/dist/workflows/target-assessment/phase4-assemble.d.ts +1 -1
  138. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +2 -2
  139. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
  140. package/package.json +1 -1
  141. package/dist/tools/bio/check-safety-panel.d.ts +0 -35
  142. package/dist/tools/bio/check-safety-panel.js +0 -76
  143. package/dist/tools/bio/get-impc-ko-profile.d.ts +0 -9
  144. package/dist/tools/bio/get-impc-ko-profile.js +0 -23
  145. package/dist/tools/bio/lookup-go-term.d.ts +0 -31
  146. package/dist/tools/bio/lookup-go-term.js +0 -116
  147. package/dist/tools/bio/search-bgee-expression.d.ts +0 -10
  148. package/dist/tools/bio/search-bgee-expression.js +0 -30
  149. package/dist/tools/bio/search-clinvar.d.ts +0 -13
  150. package/dist/tools/bio/search-clinvar.js +0 -28
  151. package/dist/tools/bio/search-dgidb.d.ts +0 -53
  152. package/dist/tools/bio/search-dgidb.js +0 -213
  153. package/dist/tools/bio/search-disgenet.d.ts +0 -30
  154. package/dist/tools/bio/search-disgenet.js +0 -84
  155. package/dist/tools/bio/search-drugbank.d.ts +0 -38
  156. package/dist/tools/bio/search-drugbank.js +0 -109
  157. package/dist/tools/bio/search-gwas-catalog.d.ts +0 -33
  158. package/dist/tools/bio/search-gwas-catalog.js +0 -172
  159. package/dist/tools/bio/search-pathway.d.ts +0 -12
  160. package/dist/tools/bio/search-pathway.js +0 -32
  161. package/dist/tools/bio/search-pharmgkb.d.ts +0 -17
  162. package/dist/tools/bio/search-pharmgkb.js +0 -52
@@ -18,74 +18,74 @@ import { ok } from "neverthrow";
18
18
  import { z } from "zod";
19
19
  import { defineTool } from "../define-tool.js";
20
20
  import { getBioactivity, getDrugInfo, getMechanism, searchCompounds, searchTargets } from "../lib/chembl-client.js";
21
- /** Per-action record caps, preserved from the five tools this one replaces. */
21
+ /**
22
+ * Per-action record caps. The ceilings are what the endpoints will serve; the
23
+ * defaults are deliberately far below them — 500 activity rows is ~125k
24
+ * characters, which answers no question that the top 25 by potency does not.
25
+ */
22
26
  const LIMITS = {
23
27
  /** `compounds` / `bioactivity` — activity-scale reads. */
24
- wide: 500,
28
+ wideMax: 500,
29
+ wideDefault: 25,
25
30
  /** `drug` / `targets` — resolution reads, where a long list is noise. */
26
- narrow: 25,
31
+ narrowMax: 25,
32
+ narrowDefault: 10,
27
33
  };
28
34
  const inputSchema = z
29
35
  .object({
30
36
  action: z
31
37
  .enum(["compounds", "drug", "mechanism", "bioactivity", "targets"])
32
- .describe("Which ChEMBL lookup to run — each names the params it needs and the fields it returns.\n" +
33
- "'compounds' (query + searchType) — molecules by target, name, or SMILES; resolves a named compound to its ChEMBL ID " +
34
- "and structure, or lists the compounds assayed against a target. Returns chemblId, preferredCompoundName, " +
35
- "canonicalSmiles, molecularWeight, alogp, molecularFormula.\n" +
36
- "'drug' (query) — the drug registry by drug name or disease indication: 'what drugs treat X?', 'is Y approved, and " +
37
- "since when?'. Returns moleculeChemblId, preferredName, maxPhase (4 = approved), moleculeType (small molecule, " +
38
- "antibody, …), firstApproval year, indication. When the drug endpoint is empty it falls back to a molecule search " +
39
- "filtered to max_phase >= 4; rows from that fallback carry indication: null.\n" +
40
- "'mechanism' (chemblId) — the curated mechanism of action of ONE molecule: 'how does drug X work?'. Returns " +
41
- "mechanismOfAction (prose), actionType (INHIBITOR, AGONIST, ANTAGONIST, …), targetChemblId + the resolved targetName, " +
42
- "moleculeChemblId. ChEMBL curates mechanisms mainly for clinical/approved molecules, so tool compounds often have none.\n" +
43
- "'bioactivity' (chemblId + idType) — measured activity rows (IC50, EC50, Ki, Kd, …): the curated, quotable potency " +
44
- "data for a compound or a target. Returns standardType, standardValue + standardUnits, pchemblValue (normalized " +
45
- "-log10 potency), assayChemblId, assayType, compoundChemblId, targetChemblId.\n" +
46
- "'targets' (query) — resolve a gene symbol or protein name to a ChEMBL target: the step that produces the target ID " +
47
- "used by bioactivity (idType='target') and compounds (searchType='target'). Returns targetChemblId, preferredName, " +
48
- "targetType (SINGLE PROTEIN, PROTEIN COMPLEX, …), organism, geneNames. Results span organisms — check `organism` " +
49
- "before using an ID, since the top hit for a human gene symbol may be a non-human ortholog."),
38
+ .describe("Which ChEMBL lookup to run; each names its params and return fields.\n" +
39
+ "'compounds' (query + searchType) — molecules by target, name or SMILES: resolve a named compound to its ID and structure, or list " +
40
+ "what was assayed against a target. → chemblId, preferredCompoundName, canonicalSmiles, molecularWeight, alogp, molecularFormula.\n" +
41
+ "'drug' (query) — the drug registry by drug name or indication: 'what treats X?', 'is Y approved, since when?'. → moleculeChemblId, " +
42
+ "preferredName, maxPhase (4 = approved), moleculeType, firstApproval, indication. If the drug endpoint is empty it falls back to a " +
43
+ "max_phase >= 4 molecule search, whose rows carry indication: null.\n" +
44
+ "'mechanism' (chemblId, molecule only) — curated mechanism of ONE molecule: 'how does X work?'. → mechanismOfAction, actionType " +
45
+ "(INHIBITOR, AGONIST, …), targetChemblId + targetName, moleculeChemblId. Curated mainly for clinical/approved molecules, so tool " +
46
+ "compounds often have none.\n" +
47
+ "'bioactivity' (chemblId + idType) — measured activity rows: the curated, QUOTABLE potency. → standardType, standardValue + " +
48
+ "standardUnits, pchemblValue (normalized -log10), assayChemblId, assayType, compoundChemblId, targetChemblId.\n" +
49
+ "'targets' (query) — resolve a gene symbol or protein name to a ChEMBL target, producing the ID that bioactivity (idType='target') " +
50
+ "and compounds (searchType='target') need. → targetChemblId, preferredName, targetType, organism, geneNames. Check `organism`: the " +
51
+ "top hit for a human symbol may be a non-human ortholog."),
50
52
  query: z
51
53
  .string()
52
54
  .min(1)
53
55
  .optional()
54
- .describe("Required for 'compounds', 'drug', 'targets'. compounds: must match searchType — a target name/gene symbol or target " +
55
- "ChEMBL ID, a compound name, or a SMILES string. drug: a drug name (e.g. 'imatinib') or a disease indication " +
56
- "(e.g. 'melanoma'). targets: a gene symbol (e.g. 'EGFR', 'ABL1'), a protein name, or a ChEMBL target ID."),
56
+ .describe("Required for 'compounds', 'drug', 'targets'. compounds: must match searchType — a target name/gene symbol or target ChEMBL ID, a " +
57
+ "compound name, or a SMILES. drug: a drug name ('imatinib') or an indication ('melanoma'). targets: a gene symbol ('EGFR'), a " +
58
+ "protein name, or a ChEMBL target ID."),
57
59
  searchType: z
58
60
  .enum(["target", "compound", "smiles"])
59
61
  .optional()
60
- .describe("Required for 'compounds' — how to read `query`. 'target': resolve it to a ChEMBL target, then return the compounds " +
61
- "assayed against that target. 'compound': free-text search over molecule names. 'smiles': flexible (flexmatch) " +
62
- "structure search on canonical SMILES."),
62
+ .describe("Required for 'compounds' — how to read `query`. 'target': resolve to a ChEMBL target, then return what was assayed against it. " +
63
+ "'compound': free-text over molecule names. 'smiles': flexmatch structure search."),
63
64
  chemblId: z
64
65
  .string()
65
66
  .min(1)
66
67
  .optional()
67
- .describe("Required for 'mechanism' and 'bioactivity'. A ChEMBL molecule ID (e.g. 'CHEMBL25' = aspirin) or — for bioactivity " +
68
- "with idType='target' — a target ID (e.g. 'CHEMBL203' = EGFR). 'mechanism' takes a molecule ID only, never a target ID."),
68
+ .describe("Required for 'mechanism' and 'bioactivity'. A molecule ID ('CHEMBL25' = aspirin), or — for bioactivity with idType='target' — a target " +
69
+ "ID ('CHEMBL203' = EGFR). 'mechanism' takes a molecule ID only."),
69
70
  idType: z
70
71
  .enum(["compound", "target"])
71
72
  .optional()
72
- .describe("Required for 'bioactivity' — which side of the activity table `chemblId` indexes. 'compound': chemblId is a molecule " +
73
- "ID; returns everything that molecule was assayed against. 'target': chemblId is a target ID; returns every compound " +
74
- "assayed against that target."),
73
+ .describe("Required for 'bioactivity' — which side of the activity table `chemblId` indexes. 'compound': everything that molecule was assayed " +
74
+ "against. 'target': every compound assayed against that target."),
75
75
  activityType: z
76
76
  .string()
77
77
  .optional()
78
- .describe("'bioactivity' only. Exact ChEMBL standard_type filter, e.g. 'IC50', 'EC50', 'Ki', 'Kd'. Matched exactly " +
79
- "(case-sensitive); omit to get all activity types."),
78
+ .describe("'bioactivity' only. Exact, case-sensitive standard_type filter — 'IC50', 'EC50', 'Ki', 'Kd'. Omit for all types."),
80
79
  limit: z
81
80
  .number()
82
81
  .int()
83
82
  .min(1)
84
- .max(LIMITS.wide)
83
+ .max(LIMITS.wideMax)
85
84
  .optional()
86
- .describe(`Max records. 'compounds'/'bioactivity': 1–${LIMITS.wide} (default ${LIMITS.wide}). 'drug'/'targets': 1–${LIMITS.narrow} ` +
87
- `(default ${LIMITS.narrow}). Ignored by 'mechanism'. With searchType='target' it caps the activity rows scanned, so ` +
88
- `fewer unique compounds usually come back.`),
85
+ .describe(`Max records. 'compounds'/'bioactivity': 1–${LIMITS.wideMax} (default ${LIMITS.wideDefault}). 'drug'/'targets': 1–${LIMITS.narrowMax} ` +
86
+ `(default ${LIMITS.narrowDefault}). Ignored by 'mechanism'. Rows are UNSORTED, so raising this is the only way to widen the window — ` +
87
+ `do it to survey an SAR series or a full assay panel, not to browse. With searchType='target' it caps the activity rows scanned, so ` +
88
+ `fewer unique compounds come back.`),
89
89
  })
90
90
  .refine((d) => !(d.action === "compounds" || d.action === "drug" || d.action === "targets") || (d.query !== undefined && d.query.length > 0), {
91
91
  message: "query is required when action is 'compounds', 'drug', or 'targets'",
@@ -104,35 +104,32 @@ const inputSchema = z
104
104
  message: "idType is required when action is 'bioactivity' — 'compound' if chemblId is a molecule ID, 'target' if it is a target ID",
105
105
  path: ["idType"],
106
106
  })
107
- .refine((d) => !(d.action === "drug" || d.action === "targets") || d.limit === undefined || d.limit <= LIMITS.narrow, {
108
- message: `limit is capped at ${LIMITS.narrow} for action 'drug' and 'targets' (only 'compounds' and 'bioactivity' reach ${LIMITS.wide})`,
107
+ .refine((d) => !(d.action === "drug" || d.action === "targets") || d.limit === undefined || d.limit <= LIMITS.narrowMax, {
108
+ message: `limit is capped at ${LIMITS.narrowMax} for action 'drug' and 'targets' (only 'compounds' and 'bioactivity' reach ${LIMITS.wideMax})`,
109
109
  path: ["limit"],
110
110
  });
111
111
  export const chemblTool = defineTool({
112
112
  id: "chembl",
113
- description: "ChEMBL — the manually curated database of drug-like bioactives (~2.4M compounds), the targets they were measured " +
114
- "against, their mechanisms, and their approval status. One tool, five lookups; pick with `action` (its description gives " +
115
- "each action's params and return fields).\n" +
116
- "IDs are resolved, never guessed: a `chemblId` comes from a prior action='compounds'/'drug' (molecule IDs) or action='targets' " +
117
- "(target IDs) call, or from pubchem action='crossrefs' — a compound or gene name is not a ChEMBL ID.\n" +
118
- "ChEMBL is curated, so prefer it over PubChem for anything you will quote or build on: use action='bioactivity' rather than " +
119
- "pubchem action='assays' whenever you will quote a number (PubChem assay summaries are broader HTS screening outcomes), and resolve " +
120
- "compounds here first. If ChEMBL does not find the compound, resolve it in PubChem with pubchem action='compound' and bridge back " +
121
- "to a ChEMBL ID via pubchem action='crossrefs'.\n" +
122
- "An empty array is a valid 'no match' / no-data answer, not an error — do not retry the same call.",
113
+ description: "ChEMBL — the manually curated database of drug-like bioactives (~2.4M compounds), the targets they were measured against, their mechanisms and " +
114
+ "their approval status. Five lookups; pick with `action`, which gives each one's params and return fields.\n" +
115
+ "IDs are resolved, never guessed: a `chemblId` comes from a prior action='compounds'/'drug' (molecules) or action='targets' (targets), or from " +
116
+ "pubchem action='crossrefs'. A compound or gene name is not a ChEMBL ID.\n" +
117
+ "Being curated, ChEMBL is what you quote from: prefer action='bioactivity' over pubchem action='assays' for any number you will cite, and resolve " +
118
+ "compounds here first. If ChEMBL misses the compound, resolve it via pubchem action='compound' and bridge back with pubchem action='crossrefs'.\n" +
119
+ "An empty array is valid no-data, not an error — do not retry the same call.",
123
120
  inputSchema,
124
121
  execute: async ({ action, query, searchType, chemblId, idType, activityType, limit }) => {
125
122
  switch (action) {
126
123
  case "compounds":
127
- return ok({ compounds: await searchCompounds(query, searchType, limit ?? LIMITS.wide) });
124
+ return ok({ compounds: await searchCompounds(query, searchType, limit ?? LIMITS.wideDefault) });
128
125
  case "drug":
129
- return ok({ drugs: await getDrugInfo(query, limit ?? LIMITS.narrow) });
126
+ return ok({ drugs: await getDrugInfo(query, limit ?? LIMITS.narrowDefault) });
130
127
  case "mechanism":
131
128
  return ok({ mechanisms: await getMechanism(chemblId) });
132
129
  case "bioactivity":
133
- return ok({ activities: await getBioactivity(chemblId, idType, { activityType, limit: limit ?? LIMITS.wide }) });
130
+ return ok({ activities: await getBioactivity(chemblId, idType, { activityType, limit: limit ?? LIMITS.wideDefault }) });
134
131
  case "targets":
135
- return ok({ targets: await searchTargets(query, limit ?? LIMITS.narrow) });
132
+ return ok({ targets: await searchTargets(query, limit ?? LIMITS.narrowDefault) });
136
133
  }
137
134
  },
138
135
  });
@@ -31,48 +31,50 @@ const inputSchema = z
31
31
  .object({
32
32
  dataset: z
33
33
  .enum(["toxcast", "hazard", "chemical", "exposure"])
34
- .describe("Which CompTox dataset to query. " +
35
- "'toxcast' — ToxCast/Tox21 in-vitro high-throughput screening; profiles toxicological liability across hundreds of assay endpoints (nuclear receptors, stress response, " +
36
- "mitochondrial toxicity), e.g. 'is this compound an endocrine disruptor?'. Returns the resolved chemical plus assay results — endpoint name, AC50, hit call, sorted by " +
37
- "AC50 ascending — alongside totalAssays, activeAssays and activeHitRate, which always describe the full tested panel rather than the returned slice. " +
38
- "'hazard' — ToxValDB / ToxRefDB IN-VIVO hazard: the points of departure a safety evaluation rests on. Returns dose-response rows (toxvalType such as NOAEL/LOAEL/LD50/BMD, " +
39
- "with value, units, study type and duration, species, exposure route, effect), genotoxicity assay summaries, and cancer classifications. " +
40
- "'chemical' — identity and physicochemical property profile from the CompTox Chemicals Dashboard, and the way to obtain the DTXSID the other datasets key off. Returns " +
41
- "DTXSID, CASRN, SMILES, InChIKey, formula and mass, plus (unless includeProperties is false) experimental and predicted property summaries — logP, water solubility, " +
42
- "vapor pressure, melting/boiling point, bioconcentration factor, Henry's law constant. " +
43
- "'exposure' — the exposure half of a risk assessment (e.g. pairing a ToxCast AC50 against predicted exposure for a bioactivity-exposure ratio). Returns SEEM predictions " +
44
- "(production volume and per-pathway exposure probabilities — dietary, residential, pesticide, industrial; NOT a daily-intake dose), HTTK toxicokinetic parameters, " +
45
- "reported functional-use categories, and consumer-product composition."),
34
+ .describe("Which CompTox dataset to query; each names its return fields.\n" +
35
+ "'toxcast' — ToxCast/Tox21 in-vitro HTS across hundreds of endpoints (nuclear receptors, stress response, mitochondrial toxicity): 'is " +
36
+ "this an endocrine disruptor?'. → the resolved chemical plus assay rows (endpoint, AC50, hit call; AC50 ascending), and totalAssays / " +
37
+ "activeAssays / activeHitRate, which always describe the FULL tested panel, not the returned slice.\n" +
38
+ "'hazard' — ToxValDB / ToxRefDB IN-VIVO hazard: the points of departure a safety evaluation rests on. → dose-response rows (toxvalType " +
39
+ "NOAEL/LOAEL/LD50/BMD with value, units, study type and duration, species, route, effect), genotoxicity summaries, cancer classifications.\n" +
40
+ "'chemical' — identity and physicochemical profile, and the way to get the DTXSID the other datasets key off. → DTXSID, CASRN, SMILES, " +
41
+ "InChIKey, formula, mass, plus (unless includeProperties is false) experimental and predicted properties — logP, water solubility, vapor " +
42
+ "pressure, melting/boiling point, bioconcentration factor, Henry's law constant.\n" +
43
+ "'exposure' — the exposure half of a risk assessment (e.g. an AC50 against predicted exposure for a bioactivity-exposure ratio). → SEEM " +
44
+ "predictions (production volume and per-pathway probabilities: dietary, residential, pesticide, industrial — NOT a daily-intake dose), " +
45
+ "HTTK toxicokinetic parameters, reported functional uses, consumer-product composition."),
46
46
  query: z
47
47
  .string()
48
- .describe("Chemical identifier (all datasets). A DTXSID (e.g. 'DTXSID7020182') is used directly with no lookup; a CASRN (e.g. '80-05-7'), a chemical name (e.g. 'bisphenol A'), " +
49
- "or — for dataset 'chemical' — an InChIKey is resolved by EXACT match, so a non-canonical or misspelled name yields found: false."),
48
+ .describe("Chemical identifier (all datasets). A DTXSID ('DTXSID7020182') is used directly; a CASRN ('80-05-7'), a name ('bisphenol A'), or — for " +
49
+ "dataset 'chemical' — an InChIKey is resolved by EXACT match, so a non-canonical or misspelled name yields found: false."),
50
50
  dataType: z
51
51
  .enum(["toxval", "genetox", "cancer", "seem", "httk", "functional-use", "product-data", "all"])
52
52
  .optional()
53
- .describe("Which sections to fetch — datasets 'hazard' and 'exposure' only; ignored by 'toxcast' and 'chemical', which each return one dataset. Default 'all'. " +
54
- "For dataset 'hazard': 'toxval' (ToxValDB dose-response values — NOAELs, LOAELs, LD50s, BMDs), 'genetox' (genotoxicity assay summaries), 'cancer' (cancer " +
55
- "classifications — IARC/EPA/NTP and similar), or 'all'. " +
56
- "For dataset 'exposure': 'seem' (SEEM exposure-pathway predictions and production volume), 'httk' (high-throughput toxicokinetic parameters — clearance, Css, protein " +
57
- "binding, …), 'functional-use' (reported chemical use categories), 'product-data' (consumer-product composition), or 'all'. " +
58
- "'all' fetches every section of that dataset concurrently."),
53
+ .describe("Which sections to fetch — datasets 'hazard' and 'exposure' only, ignored by the single-dataset 'toxcast' and 'chemical'. Default 'all', " +
54
+ "which fetches every section of that dataset concurrently. " +
55
+ "'hazard': 'toxval' (dose-response — NOAELs, LOAELs, LD50s, BMDs), 'genetox' (genotoxicity summaries), 'cancer' (IARC/EPA/NTP-style " +
56
+ "classifications). " +
57
+ "'exposure': 'seem' (exposure-pathway predictions + production volume), 'httk' (toxicokinetics — clearance, Css, protein binding), " +
58
+ "'functional-use' (reported use categories), 'product-data' (consumer-product composition)."),
59
59
  activeOnly: z
60
60
  .boolean()
61
61
  .optional()
62
- .describe("Dataset 'toxcast' only. Default true — return only hit (active) assays. Set false for the whole tested panel including inactives, needed to judge selectivity or a low hit rate."),
62
+ .describe("'toxcast' only. Default true — hit (active) assays only. False for the whole tested panel including inactives, needed to judge selectivity " +
63
+ "or a low hit rate."),
63
64
  includeProperties: z
64
65
  .boolean()
65
66
  .optional()
66
- .describe("Dataset 'chemical' only. Default true — also fetch the experimental/predicted property summaries. Set false to skip that second call when only identity and mass are needed."),
67
+ .describe("'chemical' only. Default true — also fetch experimental/predicted properties. False skips that second call when only identity and mass are needed."),
67
68
  limit: z
68
69
  .number()
69
70
  .int()
70
71
  .min(1)
71
72
  .max(200)
72
73
  .optional()
73
- .describe("Max rows returned; ignored by dataset 'chemical'. Defaults and ceilings differ per dataset: 'toxcast' default 50, max 200, applied after the activeOnly filter; " +
74
- "'hazard' default 30, max 100, applied to toxval and genetox only — cancer ALWAYS returns every row; 'exposure' default 25, max 100, applied to functional-use and " +
75
- "product-data only — seem and httk are ALWAYS returned whole."),
74
+ .describe("Max rows; ignored by 'chemical'. Per dataset: 'toxcast' default 25 / max 200, applied after activeOnly and AC50-ascending so the default " +
75
+ "keeps the most potent hits; 'hazard' default 30 / max 100, applied to toxval and genetox only — cancer always returns every row (only " +
76
+ "ever a handful); 'exposure' default 25 / max 100, applied to functional-use and product-data only — seem is one record and httk a short " +
77
+ "parameter list, both returned whole."),
76
78
  })
77
79
  .refine((d) => (d.dataset !== "toxcast" && d.dataset !== "chemical") || d.dataType === undefined || d.dataType === "all", {
78
80
  message: "dataType applies only to dataset 'hazard' (toxval | genetox | cancer) and dataset 'exposure' (seem | httk | functional-use | product-data) — " +
@@ -96,13 +98,13 @@ const inputSchema = z
96
98
  export function createComptoxTool(deps) {
97
99
  return defineTool({
98
100
  id: "comptox",
99
- description: "Query EPA's CompTox (CTX) chemical-safety databases — pick the dataset with `dataset`: 'toxcast' (in-vitro HTS bioactivity), 'hazard' (in-vivo hazard: NOAEL/LOAEL/LD50, " +
100
- "genotoxicity, cancer classifications), 'chemical' (identity + physicochemical/ADMET properties), 'exposure' (SEEM exposure predictions, toxicokinetics, uses, products). " +
101
- "See `dataset` for what each returns. Every dataset resolves the same `query` (DTXSID, CASRN, or exact chemical name) to a DTXSID first. " +
102
- "Requires EPA_CCTE_API_KEY — a missing key fails the call terminally: do NOT retry, tell the user the key needs configuring and proceed without EPA data. " +
103
- "found: false means the query did not resolve to a chemical — valid no-data, do not retry the same string; a present-but-empty section is likewise valid no-data. " +
104
- "This is environmental/industrial-chemical safety data: for drug-like compounds prefer the PubChem or ChEMBL tools — dataset 'chemical' returns `detail.pubchemCid`, which " +
105
- "bridges back to them.",
101
+ description: "EPA's CompTox (CTX) chemical-safety databases — four datasets behind `dataset`: 'toxcast' (in-vitro HTS bioactivity), 'hazard' (in-vivo: " +
102
+ "NOAEL/LOAEL/LD50, genotoxicity, cancer), 'chemical' (identity + physicochemical/ADMET), 'exposure' (SEEM predictions, toxicokinetics, uses, " +
103
+ "products). See `dataset` for what each returns; all four resolve `query` to a DTXSID first.\n" +
104
+ "Requires EPA_CCTE_API_KEY — a missing key fails terminally: do NOT retry, tell the user the key needs configuring and proceed without EPA data.\n" +
105
+ "found: false means the query did not resolve to a chemical; a present-but-empty section is likewise valid no-data. Do not retry either.\n" +
106
+ "This is environmental/industrial-chemical data: for drug-like compounds prefer PubChem or ChEMBL — dataset 'chemical' returns " +
107
+ "`detail.pubchemCid` to bridge back.",
106
108
  inputSchema,
107
109
  execute: async (input) => {
108
110
  const headers = getEpaCcteHeaders(deps.apiKey);
@@ -115,7 +117,7 @@ export function createComptoxTool(deps) {
115
117
  case "toxcast": {
116
118
  const bioactivity = await fetchToxcastBioactivity(dtxsid, headers, {
117
119
  activeOnly: input.activeOnly ?? true,
118
- limit: input.limit ?? 50,
120
+ limit: input.limit ?? 25,
119
121
  });
120
122
  return ok({ found: true, chemical: { dtxsid, preferredName, casrn, ...bioactivity } });
121
123
  }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * drugGeneInteractions — one tool over the three drug↔gene corpora: DGIdb,
3
+ * DrugBank, and PharmGKB.
4
+ *
5
+ * These were three tools with the same input skeleton (`query` + a gene/drug
6
+ * discriminator + `limit`) answering one question in two directions: what drugs
7
+ * hit this gene, and what genes does this drug act on. DGIdb *aggregates* the
8
+ * other two (it names ChEMBL, DrugBank, PharmGKB and GuideToPharmacology among
9
+ * its 30+ sources), so the choice between them was never a routing decision —
10
+ * it is coverage, and it rides in `sources`.
11
+ *
12
+ * Unlike `gene_disease_evidence`, the default here is the AGGREGATOR ALONE:
13
+ * DGIdb already covers what the other two hold, so fanning out by default would
14
+ * mostly return the same interactions three times.
15
+ *
16
+ * Each source runs independently and reports its own outcome in `perSource`, so
17
+ * a missing DrugBank key degrades that corpus rather than the whole call.
18
+ */
19
+ import type { Logger } from "../../lib/logger.js";
20
+ import { type DgidbResult } from "../lib/dgidb-client.js";
21
+ import { type DrugResult } from "../lib/drugbank-client.js";
22
+ import { type PharmgkbAnnotation } from "../lib/pharmgkb-client.js";
23
+ declare const SOURCES: readonly ["dgidb", "drugbank", "pharmgkb"];
24
+ type Source = (typeof SOURCES)[number];
25
+ interface SourceOutcome {
26
+ source: Source;
27
+ status: "ok" | "no_data" | "unavailable";
28
+ returned: number;
29
+ detail?: string;
30
+ }
31
+ /**
32
+ * A DrugBank record projected for context. `includeDrugRecord` decides whether
33
+ * the five prose fields arrive whole or as previews — five 500-character fields
34
+ * across ten records is 25k characters of narrative that a drug-vs-gene
35
+ * question rarely needs.
36
+ */
37
+ interface ProjectedDrug {
38
+ drugbankId: string;
39
+ name: string;
40
+ type: string;
41
+ groups: string[];
42
+ categories: string[];
43
+ halfLife: string;
44
+ targets: DrugResult["targets"];
45
+ targetCount: number;
46
+ interactions: DrugResult["interactions"];
47
+ interactionCount: number;
48
+ description: string;
49
+ indication: string;
50
+ pharmacodynamics: string;
51
+ mechanismOfAction: string;
52
+ toxicity: string;
53
+ proseTruncated: boolean;
54
+ }
55
+ interface DrugGeneOutput {
56
+ direction: "gene_to_drugs" | "drug_to_genes";
57
+ perSource: SourceOutcome[];
58
+ dgidb?: DgidbResult[];
59
+ drugbank?: ProjectedDrug[];
60
+ pharmgkb?: PharmgkbAnnotation[];
61
+ }
62
+ export declare function createDrugGeneInteractionsTool(deps: {
63
+ drugbankApiKey: string;
64
+ logger?: Logger;
65
+ }): import("../define-tool.js").Tool<{
66
+ query: string | string[];
67
+ direction: "gene_to_drugs" | "drug_to_genes";
68
+ sources?: ("dgidb" | "drugbank" | "pharmgkb")[] | undefined;
69
+ limit?: number | undefined;
70
+ interactionTypes?: string[] | undefined;
71
+ sourceDbs?: string[] | undefined;
72
+ minSources?: number | undefined;
73
+ includeAttributes?: boolean | undefined;
74
+ includeDrugRecord?: boolean | undefined;
75
+ }, DrugGeneOutput>;
76
+ export {};
@@ -0,0 +1,175 @@
1
+ /**
2
+ * drugGeneInteractions — one tool over the three drug↔gene corpora: DGIdb,
3
+ * DrugBank, and PharmGKB.
4
+ *
5
+ * These were three tools with the same input skeleton (`query` + a gene/drug
6
+ * discriminator + `limit`) answering one question in two directions: what drugs
7
+ * hit this gene, and what genes does this drug act on. DGIdb *aggregates* the
8
+ * other two (it names ChEMBL, DrugBank, PharmGKB and GuideToPharmacology among
9
+ * its 30+ sources), so the choice between them was never a routing decision —
10
+ * it is coverage, and it rides in `sources`.
11
+ *
12
+ * Unlike `gene_disease_evidence`, the default here is the AGGREGATOR ALONE:
13
+ * DGIdb already covers what the other two hold, so fanning out by default would
14
+ * mostly return the same interactions three times.
15
+ *
16
+ * Each source runs independently and reports its own outcome in `perSource`, so
17
+ * a missing DrugBank key degrades that corpus rather than the whole call.
18
+ */
19
+ import { ok } from "neverthrow";
20
+ import { z } from "zod";
21
+ import { createNoopLogger } from "../../lib/console-logger.js";
22
+ import { defineTool } from "../define-tool.js";
23
+ import { searchDgidb } from "../lib/dgidb-client.js";
24
+ import { searchDrugbank } from "../lib/drugbank-client.js";
25
+ import { searchPharmgkb } from "../lib/pharmgkb-client.js";
26
+ const SOURCES = ["dgidb", "drugbank", "pharmgkb"];
27
+ const DEFAULT_LIMIT = 20;
28
+ const MAX_LIMIT = 200;
29
+ const MAX_INPUTS = 25;
30
+ /** Prose kept per DrugBank field when the full record is not requested. */
31
+ const PROSE_PREVIEW_CHARS = 200;
32
+ /** Targets and interactions kept per DrugBank record. */
33
+ const MAX_DRUG_TARGETS = 10;
34
+ const MAX_DRUG_INTERACTIONS = 10;
35
+ const inputSchema = z.object({
36
+ query: z
37
+ .union([z.string().min(1), z.array(z.string().min(1)).min(1).max(MAX_INPUTS)])
38
+ .describe(`One identifier or up to ${MAX_INPUTS} — batch rather than calling per identifier. 'gene_to_drugs': HUGO symbols. 'drug_to_genes': drug ` +
39
+ "names, DrugBank IDs (DB00619), or DGIdb concept IDs. Only 'dgidb' batches; the others read the FIRST and report the rest as skipped."),
40
+ direction: z
41
+ .enum(["gene_to_drugs", "drug_to_genes"])
42
+ .describe("'gene_to_drugs' — what drugs act on these genes ('what is druggable in my gene set?'). 'drug_to_genes' — what genes these drugs act on " +
43
+ "(target deconvolution, polypharmacology)."),
44
+ sources: z
45
+ .array(z.enum(SOURCES))
46
+ .min(1)
47
+ .optional()
48
+ .describe("Corpora to query in parallel. Default ['dgidb'] ALONE — it aggregates the other two, so add them only for what they uniquely hold. " +
49
+ "'dgidb': 30+ aggregated sources, per-interaction source count as confidence; the only one that batches. " +
50
+ "'drugbank': the curated drug RECORD (indication, pharmacodynamics, mechanism, toxicity, half-life, DDIs); needs DRUGBANK_API_KEY. " +
51
+ "'pharmgkb': pharmacogenomic annotations graded 1A…4 (1A guideline-backed); exact match only — 'CYP2D6' resolves, 'cytochrome P450 2D6' does not."),
52
+ limit: z
53
+ .number()
54
+ .int()
55
+ .min(1)
56
+ .max(MAX_LIMIT)
57
+ .optional()
58
+ .describe(`Max records per identifier per source (default ${DEFAULT_LIMIT}, max ${MAX_LIMIT}). DGIdb rows are ordered by source count descending; ` +
59
+ "`totalInteractions` gives the pre-trim count."),
60
+ interactionTypes: z.array(z.string()).optional().describe("'dgidb' only. Case-insensitive substring filter on interaction type, e.g. ['inhibitor']."),
61
+ sourceDbs: z.array(z.string()).optional().describe("'dgidb' only. Keep only interactions from these underlying databases (substring), e.g. ['chembl']."),
62
+ minSources: z.number().int().min(1).optional().describe("'dgidb' only. Drop interactions with fewer than this many supporting sources; default 1."),
63
+ includeAttributes: z
64
+ .boolean()
65
+ .optional()
66
+ .describe("'dgidb' only. Default FALSE — attributes are verbose free-text pairs. True only when you need the assay detail, not the interaction's existence."),
67
+ includeDrugRecord: z
68
+ .boolean()
69
+ .optional()
70
+ .describe(`'drugbank' only. Default FALSE — the five prose fields come back as ${PROSE_PREVIEW_CHARS}-char previews with proseTruncated: true. ` +
71
+ "True for full text; narrow `limit` when you do."),
72
+ });
73
+ function projectDrug(drug, includeFullProse) {
74
+ const prose = (value) => (includeFullProse ? value : value.slice(0, PROSE_PREVIEW_CHARS));
75
+ return {
76
+ drugbankId: drug.drugbankId,
77
+ name: drug.name,
78
+ type: drug.type,
79
+ groups: drug.groups,
80
+ categories: drug.categories,
81
+ halfLife: drug.halfLife,
82
+ targets: drug.targets.slice(0, MAX_DRUG_TARGETS),
83
+ targetCount: drug.targets.length,
84
+ interactions: drug.interactions.slice(0, MAX_DRUG_INTERACTIONS),
85
+ interactionCount: drug.interactions.length,
86
+ description: prose(drug.description),
87
+ indication: prose(drug.indication),
88
+ pharmacodynamics: prose(drug.pharmacodynamics),
89
+ mechanismOfAction: prose(drug.mechanismOfAction),
90
+ toxicity: prose(drug.toxicity),
91
+ proseTruncated: !includeFullProse,
92
+ };
93
+ }
94
+ /**
95
+ * Run one corpus, folding its failure into an `unavailable` outcome so a dead
96
+ * or unkeyed source cannot take down a call the others could answer.
97
+ */
98
+ async function runSource(source, fetch) {
99
+ try {
100
+ const records = await fetch();
101
+ return { outcome: { source, status: records.length > 0 ? "ok" : "no_data", returned: records.length }, records };
102
+ }
103
+ catch (error) {
104
+ return {
105
+ outcome: { source, status: "unavailable", returned: 0, detail: error instanceof Error ? error.message : String(error) },
106
+ records: [],
107
+ };
108
+ }
109
+ }
110
+ export function createDrugGeneInteractionsTool(deps) {
111
+ const logger = (deps.logger ?? createNoopLogger()).named("drug_gene_interactions");
112
+ return defineTool({
113
+ id: "drug_gene_interactions",
114
+ description: "Drug↔gene interactions across DGIdb, DrugBank and PharmGKB, in one call — 'what drugs hit these genes?' and 'what genes does this drug act " +
115
+ "on?'. See `direction` and `sources` for the modes.\n" +
116
+ "It answers whether an interaction is KNOWN, by whom, and of what type. For quotable POTENCY (IC50/Ki) use chembl({action:'bioactivity'}) instead.\n" +
117
+ "Verbose fields (`includeAttributes`, `includeDrugRecord`) are off by default — both cost a lot of context and are rarely what the question needs.\n" +
118
+ "ALWAYS read `perSource` before concluding an interaction is unknown: 'no_data' means that corpus has nothing; 'unavailable' means it could " +
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
+ "is not in DGIdb at all, usually a non-HUGO symbol or a brand name. Neither is worth retrying unchanged.",
121
+ inputSchema,
122
+ execute: async (input) => {
123
+ const inputs = typeof input.query === "string" ? [input.query] : input.query;
124
+ const selected = input.sources ?? ["dgidb"];
125
+ const limit = input.limit ?? DEFAULT_LIMIT;
126
+ const isGeneSide = input.direction === "gene_to_drugs";
127
+ const output = { direction: input.direction, perSource: [] };
128
+ const [dgidb, drugbank, pharmgkb] = await Promise.all([
129
+ selected.includes("dgidb")
130
+ ? runSource("dgidb", () => searchDgidb(inputs, isGeneSide ? "gene" : "drug", {
131
+ limit,
132
+ ...(input.interactionTypes ? { interactionTypes: input.interactionTypes } : {}),
133
+ ...(input.sourceDbs ? { sourceDbs: input.sourceDbs } : {}),
134
+ ...(input.minSources !== undefined ? { minSources: input.minSources } : {}),
135
+ ...(input.includeAttributes !== undefined ? { includeAttributes: input.includeAttributes } : {}),
136
+ }, (errors) => logger.warn("dgidb partial errors alongside data", { errors })))
137
+ : null,
138
+ selected.includes("drugbank")
139
+ ? runSource("drugbank", async () => {
140
+ if (!deps.drugbankApiKey)
141
+ throw new Error("DRUGBANK_API_KEY is not configured");
142
+ const drugs = await searchDrugbank(deps.drugbankApiKey, inputs[0], isGeneSide ? "target" : "drug", limit);
143
+ return drugs.map((d) => projectDrug(d, input.includeDrugRecord ?? false));
144
+ })
145
+ : null,
146
+ selected.includes("pharmgkb")
147
+ ? runSource("pharmgkb", async () => {
148
+ const res = await searchPharmgkb(inputs[0], isGeneSide ? "gene" : "drug", limit);
149
+ return res.annotations;
150
+ })
151
+ : null,
152
+ ]);
153
+ // DrugBank and PharmGKB take one identifier per call; say so on the
154
+ // outcome rather than letting the extra inputs vanish silently.
155
+ const skipped = inputs.length > 1 ? `read only '${inputs[0]}' — this source takes one identifier per call` : "";
156
+ const noteSkipped = (outcome) => {
157
+ const detail = [outcome.detail, skipped].filter(Boolean).join("; ");
158
+ return detail ? { ...outcome, detail } : outcome;
159
+ };
160
+ if (dgidb) {
161
+ output.perSource.push(dgidb.outcome);
162
+ output.dgidb = dgidb.records;
163
+ }
164
+ if (drugbank) {
165
+ output.perSource.push(noteSkipped(drugbank.outcome));
166
+ output.drugbank = drugbank.records;
167
+ }
168
+ if (pharmgkb) {
169
+ output.perSource.push(noteSkipped(pharmgkb.outcome));
170
+ output.pharmgkb = pharmgkb.records;
171
+ }
172
+ return ok(output);
173
+ },
174
+ });
175
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * geneDiseaseEvidence — one tool over the three gene↔disease evidence corpora:
3
+ * the NHGRI-EBI GWAS Catalog, DisGeNET, and NCBI ClinVar.
4
+ *
5
+ * These were three tools with the same input skeleton (`query` + a
6
+ * gene/disease/variant discriminator + a strength threshold + `limit`)
7
+ * answering the same question — what genetic evidence links this gene to this
8
+ * disease or trait. Picking between them was never a routing decision an agent
9
+ * should spend a turn on; it is corpus coverage, so it rides in `sources` and
10
+ * defaults to all of them. `search_pathway`'s `source: kegg | reactome | both`
11
+ * is the same shape.
12
+ *
13
+ * Each source runs independently and reports its own outcome in `perSource`: a
14
+ * missing DisGeNET key or a GWAS Catalog outage degrades that one corpus to
15
+ * `unavailable` instead of failing a call the other two could have answered.
16
+ *
17
+ * The input is a flat object with a `queryType` discriminator — not a
18
+ * `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
19
+ * a top-level `"type":"object"`).
20
+ */
21
+ import { type ClinvarVariant } from "../lib/clinvar-client.js";
22
+ import { type Gda } from "../lib/disgenet-client.js";
23
+ import { type GwasAssociation } from "../lib/gwas-catalog-client.js";
24
+ declare const SOURCES: readonly ["gwas", "disgenet", "clinvar"];
25
+ type Source = (typeof SOURCES)[number];
26
+ /**
27
+ * Per-source outcome. `unavailable` (key absent, corpus erroring) and
28
+ * `not_applicable` (the corpus has no lookup for this queryType) are both
29
+ * normal states the caller must be able to tell apart from genuine no-data —
30
+ * otherwise an unkeyed DisGeNET reads as "no genetic support".
31
+ */
32
+ interface SourceOutcome {
33
+ source: Source;
34
+ status: "ok" | "no_data" | "unavailable" | "not_applicable";
35
+ returned: number;
36
+ totalFound?: number;
37
+ detail?: string;
38
+ }
39
+ interface GeneDiseaseEvidenceOutput {
40
+ query: string;
41
+ queryType: "gene" | "disease" | "variant";
42
+ perSource: SourceOutcome[];
43
+ gwas?: GwasAssociation[];
44
+ disgenet?: Gda[];
45
+ clinvar?: ClinvarVariant[];
46
+ }
47
+ export declare function createGeneDiseaseEvidenceTool(deps: {
48
+ ncbiApiKey?: string;
49
+ disgenetApiKey: string;
50
+ }): import("../define-tool.js").Tool<{
51
+ query: string;
52
+ queryType: "gene" | "disease" | "variant";
53
+ sources?: ("clinvar" | "gwas" | "disgenet")[] | undefined;
54
+ limit?: number | undefined;
55
+ pValueThreshold?: number | undefined;
56
+ minScore?: number | undefined;
57
+ disgenetSource?: "ALL" | "CURATED" | "ANIMAL_MODELS" | "BEFREE" | undefined;
58
+ clinicalSignificance?: "pathogenic" | "likely-pathogenic" | "benign" | "likely-benign" | "uncertain" | undefined;
59
+ informativeOnly?: boolean | undefined;
60
+ }, GeneDiseaseEvidenceOutput>;
61
+ export {};