@inflexa-ai/harness 0.29.0 → 0.30.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 (93) hide show
  1. package/dist/agents/conversation-agent.js +7 -4
  2. package/dist/agents/report-session-agent.js +1 -1
  3. package/dist/agents/sandbox/data-profiler.js +0 -1
  4. package/dist/agents/sandbox/immune-profiling-agent.js +3 -1
  5. package/dist/agents/sandbox/network-agent.js +0 -1
  6. package/dist/agents/sandbox/types.d.ts +1 -1
  7. package/dist/agents/sandbox/types.js +1 -1
  8. package/dist/config/resource-limits.d.ts +1 -1
  9. package/dist/config/resource-limits.js +1 -1
  10. package/dist/contracts/schemas/chat-parts.d.ts +12 -12
  11. package/dist/contracts/severity.d.ts +2 -2
  12. package/dist/contracts/target-dossier.d.ts +70 -70
  13. package/dist/data/safety-panel-schema.d.ts +4 -4
  14. package/dist/index.d.ts +2 -1
  15. package/dist/index.js +7 -0
  16. package/dist/loop/run-agent.d.ts +13 -1
  17. package/dist/loop/run-agent.js +2 -1
  18. package/dist/prompts/SOUL.d.ts +2 -2
  19. package/dist/prompts/SOUL.js +17 -1
  20. package/dist/prompts/conversation.d.ts +1 -1
  21. package/dist/prompts/conversation.js +43 -71
  22. package/dist/prompts/planner.js +57 -10
  23. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  24. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +3 -1
  25. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  26. package/dist/prompts/sandbox/dna-methylation-agent.js +18 -12
  27. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  28. package/dist/prompts/sandbox/immune-profiling-agent.js +6 -2
  29. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  30. package/dist/prompts/sandbox/metabolomics-agent.js +8 -4
  31. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  32. package/dist/prompts/sandbox/microbiome-agent.js +12 -6
  33. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  34. package/dist/prompts/sandbox/network-agent.js +9 -6
  35. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  36. package/dist/prompts/sandbox/spatial-omics-agent.js +4 -2
  37. package/dist/providers/ai-sdk.js +34 -1
  38. package/dist/providers/reasoning.d.ts +65 -0
  39. package/dist/providers/reasoning.js +81 -0
  40. package/dist/providers/types.d.ts +14 -0
  41. package/dist/schemas/data-profile-schemas.d.ts +4 -4
  42. package/dist/schemas/run-synthesis.d.ts +4 -4
  43. package/dist/tools/bio/chembl.d.ts +1 -1
  44. package/dist/tools/bio/chembl.js +15 -10
  45. package/dist/tools/bio/comptox.js +5 -1
  46. package/dist/tools/bio/drug-gene-interactions.js +5 -2
  47. package/dist/tools/bio/gene-disease-evidence.d.ts +19 -9
  48. package/dist/tools/bio/gene-disease-evidence.js +78 -22
  49. package/dist/tools/bio/gene-preclinical-profile.d.ts +33 -13
  50. package/dist/tools/bio/gene-preclinical-profile.js +63 -22
  51. package/dist/tools/bio/lookup-annotation.d.ts +1 -0
  52. package/dist/tools/bio/lookup-annotation.js +28 -10
  53. package/dist/tools/bio/opentargets.d.ts +10 -2
  54. package/dist/tools/bio/opentargets.js +42 -13
  55. package/dist/tools/bio/pubchem.js +5 -2
  56. package/dist/tools/bio/pubmed.js +9 -3
  57. package/dist/tools/bio/search-clinical-trials.d.ts +25 -25
  58. package/dist/tools/bio/search-clinical-trials.js +56 -18
  59. package/dist/tools/bio/search-faers.d.ts +26 -6
  60. package/dist/tools/bio/search-faers.js +70 -18
  61. package/dist/tools/bio/search-gene.d.ts +19 -3
  62. package/dist/tools/bio/search-gene.js +69 -12
  63. package/dist/tools/bio/search-geo-datasets.js +4 -1
  64. package/dist/tools/bio/search-interactions.js +6 -2
  65. package/dist/tools/bio/target-safety.d.ts +2 -2
  66. package/dist/tools/bio/target-safety.js +3 -2
  67. package/dist/tools/lib/cbioportal-client.js +25 -6
  68. package/dist/tools/lib/chembl-client.d.ts +3 -1
  69. package/dist/tools/lib/chembl-client.js +6 -1
  70. package/dist/tools/lib/gwas-catalog-client.d.ts +7 -6
  71. package/dist/tools/lib/gwas-catalog-client.js +18 -6
  72. package/dist/tools/lib/openfda-client.d.ts +7 -5
  73. package/dist/tools/lib/openfda-client.js +17 -9
  74. package/dist/tools/lib/opentargets-client.d.ts +20 -0
  75. package/dist/tools/lib/opentargets-client.js +45 -0
  76. package/dist/tools/lib/pathway-client.d.ts +7 -0
  77. package/dist/tools/lib/pathway-client.js +68 -1
  78. package/dist/tools/research/context7-docs.js +3 -3
  79. package/dist/tools/research/generate-analogy-report.js +2 -2
  80. package/dist/tools/research/generate-plan.d.ts +3 -0
  81. package/dist/tools/research/generate-plan.js +64 -5
  82. package/dist/tools/research/resolve-citation.js +8 -3
  83. package/dist/tools/research/search-arxiv.js +8 -3
  84. package/dist/tools/research/search-semantic-scholar.js +10 -4
  85. package/dist/workflows/sandbox-step.d.ts +6 -0
  86. package/dist/workflows/sandbox-step.js +31 -14
  87. package/dist/workflows/target-assessment/assemblers/safety.d.ts +1 -1
  88. package/dist/workflows/target-assessment/phase4-assemble.d.ts +17 -17
  89. package/dist/workflows/target-assessment/schemas.d.ts +12 -12
  90. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +6 -6
  91. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +6 -6
  92. package/dist/workflows/target-assessment/synthesis/index.d.ts +12 -12
  93. package/package.json +1 -1
@@ -8,13 +8,17 @@ import { getEnrichment, getInteractionNetwork, getInteractionPartners } from "..
8
8
  const DEFAULT_LIMIT = 20;
9
9
  export const searchInteractionsTool = defineTool({
10
10
  id: "search_interactions",
11
- description: "Query STRING DB for protein-protein interactions and gene-set functional enrichment. " +
11
+ description: "Query the STRING database — the protein-protein association network of EMBL, which folds experimental, database, co-expression and text-mined " +
12
+ "evidence into one confidence score — for interactions and gene-set functional enrichment. " +
12
13
  "'partners' — the one-hop interaction partners of the input proteins, score-sorted: 'what else does this protein work with?'. " +
13
14
  "'network' — the interactions AMONG the input proteins only, no new nodes: 'is my gene set actually connected?'. " +
14
15
  "'enrichment' — statistical over-representation of GO, KEGG, Reactome and Pfam terms in the input set, FDR-sorted: the enrichment TEST, as " +
15
16
  "opposed to lookup_annotation, which just reads a vocabulary. " +
17
+ "ACCEPTED IDENTIFIERS: STRING resolves each one itself, so a HUGO gene symbol ('TP53'), a UniProt accession ('P04637'), an Ensembl protein ID " +
18
+ "('ENSP00000269305') and a full protein name all work, mixed in one call. `species` takes an NCBI Taxonomy ID (9606 = human).\n" +
16
19
  "`limit` applies to all three actions and defaults small: a network over N proteins grows with N² edges. `totalEdges` / `totalTerms` give the " +
17
- "pre-trim counts. An empty result is valid no-data (unconnected set, no enriched term, unresolvable identifiers) — do not retry.",
20
+ "pre-trim counts. An empty result is valid no-data (unconnected set, no enriched term, unresolvable identifiers) — report it and continue, do " +
21
+ "not retry.",
18
22
  inputSchema: z.object({
19
23
  identifiers: z.array(z.string()).min(1).max(100).describe("Protein/gene identifiers (e.g. ['TP53', 'BRCA1'])"),
20
24
  species: z.number().int().default(9606).describe("NCBI Taxonomy ID (9606 = human, 10090 = mouse)"),
@@ -40,14 +40,14 @@ export declare const targetSafetyTool: import("../define-tool.js").Tool<{
40
40
  identifierType?: "auto" | "gene_symbol" | "uniprot" | "chembl_id" | "ensembl_id" | undefined;
41
41
  sources?: ("opentargets" | "panel")[] | undefined;
42
42
  filterOrgan?: "hematologic" | "metabolic" | "immune" | "hepatic" | "musculoskeletal" | "cns" | "renal" | "reproductive" | "respiratory" | "cardiac" | "vascular" | "pns" | "gi" | "pancreas" | "endocrine_thyroid" | "dermatologic" | "ocular" | "oncology" | undefined;
43
- minSeverity?: "high" | "medium" | "low" | undefined;
43
+ minSeverity?: "low" | "medium" | "high" | undefined;
44
44
  liabilityLimit?: number | undefined;
45
45
  }, {
46
46
  perSource: SourceOutcome[];
47
47
  summary: {
48
48
  totalInput: number;
49
49
  panelMatched: number;
50
- bySeverity: Record<"high" | "medium" | "low", number>;
50
+ bySeverity: Record<"low" | "medium" | "high", number>;
51
51
  byOrgan: Record<string, number>;
52
52
  openTargetsWithLiabilities: number;
53
53
  };
@@ -92,8 +92,9 @@ const inputSchema = z
92
92
  export const targetSafetyTool = defineTool({
93
93
  id: "target_safety",
94
94
  description: "TARGET-level, mechanism-based safety liability — the curated secondary-pharmacology panel and Open Targets' curated liabilities in one call. " +
95
- "Judges what engaging a target is likely to do to an organ system, before any specific molecule exists. Identifiers may be gene symbols, ChEMBL " +
96
- "target IDs, UniProt accessions or Ensembl gene IDs, mixed.\n" +
95
+ "Judges what engaging a target is likely to do to an organ system, before any specific molecule exists.\n" +
96
+ "ACCEPTED IDENTIFIERS, mixed freely in one call: a HUGO gene symbol ('HERG', 'KCNH2'), a ChEMBL target ID ('CHEMBL240'), a UniProt accession " +
97
+ "('Q12809') and an Ensembl gene ID ('ENSG00000055118').\n" +
97
98
  "This is the safety of the TARGET, not of a drug. For a marketed molecule's post-market adverse events use search_faers; for a chemical's " +
98
99
  "toxicology use comptox; for what removing the gene does in vivo use gene_preclinical_profile.\n" +
99
100
  "ABSENCE IS NOT SAFETY. The panel is finite and hand-curated, so no match means only 'not on the panel'; an empty Open Targets liability list " +
@@ -36,6 +36,8 @@ const RawCancerStudySchema = z.object({
36
36
  cancerType: z.object({ name: z.string().optional(), cancerTypeId: z.string().optional() }).optional(),
37
37
  name: z.string().optional(),
38
38
  description: z.string().optional(),
39
+ /** The denominator of a study. The sample-list SUMMARY projection carries no count. */
40
+ allSampleCount: z.number().optional(),
39
41
  });
40
42
  async function listStudies() {
41
43
  const url = `${CBIOPORTAL_BASE}/studies?projection=SUMMARY&pageSize=10000`;
@@ -44,6 +46,23 @@ async function listStudies() {
44
46
  throw new Error(describeApiError(res.error));
45
47
  return res.value ?? [];
46
48
  }
49
+ /**
50
+ * The cancer-type vocabulary, keyed by its id. The study SUMMARY projection
51
+ * carries the id alone, thus a row would otherwise report `esca` where it must
52
+ * report `Esophageal Adenocarcinoma`.
53
+ */
54
+ async function cancerTypeNames() {
55
+ const url = `${CBIOPORTAL_BASE}/cancer-types?pageSize=10000`;
56
+ const res = await apiFetchValidated(url, z.array(z.object({ cancerTypeId: z.string().optional(), name: z.string().optional() })), { headers: HEADERS });
57
+ const names = new Map();
58
+ if (res.isErr())
59
+ return names;
60
+ for (const row of res.value ?? []) {
61
+ if (row.cancerTypeId && row.name)
62
+ names.set(row.cancerTypeId, row.name);
63
+ }
64
+ return names;
65
+ }
47
66
  async function listAllSampleLists() {
48
67
  const url = `${CBIOPORTAL_BASE}/sample-lists?projection=SUMMARY&pageSize=10000`;
49
68
  const res = await apiFetchValidated(url, z.array(RawSampleListSchema), { headers: HEADERS });
@@ -98,7 +117,7 @@ export async function getSomaticMutationFrequencies(geneSymbol, options = {}) {
98
117
  const entrezGeneId = await resolveEntrezId(geneSymbol);
99
118
  if (entrezGeneId == null)
100
119
  return { entrezGeneId: null, rows: [] };
101
- const [studies, profiles, sampleLists] = await Promise.all([listStudies(), listMutationProfiles(), listAllSampleLists()]);
120
+ const [studies, profiles, sampleLists, typeNames] = await Promise.all([listStudies(), listMutationProfiles(), listAllSampleLists(), cancerTypeNames()]);
102
121
  const allSampleListByStudy = new Map();
103
122
  for (const sl of sampleLists) {
104
123
  if (sl.category === "all_cases_in_study") {
@@ -122,12 +141,12 @@ export async function getSomaticMutationFrequencies(geneSymbol, options = {}) {
122
141
  }
123
142
  const byCancerType = new Map();
124
143
  for (const studyId of studiesWithMutations) {
125
- const sl = allSampleListByStudy.get(studyId);
126
- if (!sl?.sampleCount)
127
- continue;
128
144
  const study = studyById.get(studyId);
145
+ const sampleCount = study?.allSampleCount ?? 0;
146
+ if (sampleCount === 0)
147
+ continue;
129
148
  const cancerTypeId = study?.cancerType?.cancerTypeId ?? study?.cancerTypeId ?? "unknown";
130
- const cancerTypeName = study?.cancerType?.name ?? cancerTypeId;
149
+ const cancerTypeName = study?.cancerType?.name ?? typeNames.get(cancerTypeId) ?? cancerTypeId;
131
150
  if (!byCancerType.has(cancerTypeId)) {
132
151
  byCancerType.set(cancerTypeId, {
133
152
  name: cancerTypeName,
@@ -137,7 +156,7 @@ export async function getSomaticMutationFrequencies(geneSymbol, options = {}) {
137
156
  });
138
157
  }
139
158
  const bucket = byCancerType.get(cancerTypeId);
140
- bucket.total += sl.sampleCount;
159
+ bucket.total += sampleCount;
141
160
  bucket.mutated += mutatedStudySamples.get(studyId)?.size ?? 0;
142
161
  bucket.studies.add(studyId);
143
162
  }
@@ -68,7 +68,9 @@ export interface ChemblPolypharmHit {
68
68
  /** Search ChEMBL for compounds by target, compound name, or SMILES. */
69
69
  export declare function searchCompounds(query: string, searchType: CompoundSearchType, limit?: number): Promise<ChemblCompound[]>;
70
70
  export declare function searchTargets(query: string, limit?: number): Promise<ChemblTarget[]>;
71
- export declare function getBioactivity(chemblId: string, type: "compound" | "target", options?: {
71
+ /** Which column of the ChEMBL activity table the supplied id indexes. */
72
+ export type BioactivityIdType = "compound" | "target" | "assay";
73
+ export declare function getBioactivity(chemblId: string, type: BioactivityIdType, options?: {
72
74
  activityType?: string;
73
75
  limit?: number;
74
76
  }): Promise<ChemblActivity[]>;
@@ -241,9 +241,14 @@ export async function searchTargets(query, limit = 25) {
241
241
  }
242
242
  return (res.value.targets ?? []).map(mapTarget);
243
243
  }
244
+ const ACTIVITY_ID_FIELD = {
245
+ compound: "molecule_chembl_id",
246
+ target: "target_chembl_id",
247
+ assay: "assay_chembl_id",
248
+ };
244
249
  export async function getBioactivity(chemblId, type, options = {}) {
245
250
  const limit = options.limit ?? 500;
246
- const idParam = type === "compound" ? `molecule_chembl_id=${encodeURIComponent(chemblId)}` : `target_chembl_id=${encodeURIComponent(chemblId)}`;
251
+ const idParam = `${ACTIVITY_ID_FIELD[type]}=${encodeURIComponent(chemblId)}`;
247
252
  let url = `${CHEMBL_BASE}/activity.json?${idParam}&limit=${limit}`;
248
253
  if (options.activityType) {
249
254
  url += `&standard_type=${encodeURIComponent(options.activityType)}`;
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Pure async client functions for the NHGRI-EBI GWAS Catalog REST API.
3
3
  *
4
- * Public, no key required. Three search paths: by gene (SNP-by-gene lookup then
5
- * per-SNP associations), by trait (EFO trait search then its associations), and
6
- * by variant (direct rsID associations).
4
+ * Public, no key required. Four search paths: by gene (SNP-by-gene lookup then
5
+ * per-SNP associations), by trait (EFO trait search then its associations), by
6
+ * variant (direct rsID associations), and by study (the associations of one
7
+ * GCST study accession).
7
8
  */
8
9
  import { z } from "zod";
9
10
  export interface GwasAssociation {
@@ -21,7 +22,7 @@ export interface GwasAssociation {
21
22
  pubmedId: string;
22
23
  sampleSize: string;
23
24
  }
24
- export type GwasSearchType = "gene" | "trait" | "variant";
25
+ export type GwasSearchType = "gene" | "trait" | "variant" | "study";
25
26
  declare const RawGwasAssociationSchema: z.ZodObject<{
26
27
  _snpRsId: z.ZodOptional<z.ZodString>;
27
28
  loci: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -42,10 +43,10 @@ declare const RawGwasAssociationSchema: z.ZodObject<{
42
43
  }, z.core.$strip>>;
43
44
  pvalueMantissa: z.ZodOptional<z.ZodNumber>;
44
45
  pvalueExponent: z.ZodOptional<z.ZodNumber>;
45
- riskFrequency: z.ZodOptional<z.ZodString>;
46
+ riskFrequency: z.ZodOptional<z.ZodNullable<z.ZodString>>;
46
47
  orPerCopyNum: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
47
48
  betaNum: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
48
- range: z.ZodOptional<z.ZodString>;
49
+ range: z.ZodOptional<z.ZodNullable<z.ZodString>>;
49
50
  }, z.core.$strip>;
50
51
  type RawGwasAssociation = z.infer<typeof RawGwasAssociationSchema>;
51
52
  export declare function mapAssociation(a: RawGwasAssociation): GwasAssociation;
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Pure async client functions for the NHGRI-EBI GWAS Catalog REST API.
3
3
  *
4
- * Public, no key required. Three search paths: by gene (SNP-by-gene lookup then
5
- * per-SNP associations), by trait (EFO trait search then its associations), and
6
- * by variant (direct rsID associations).
4
+ * Public, no key required. Four search paths: by gene (SNP-by-gene lookup then
5
+ * per-SNP associations), by trait (EFO trait search then its associations), by
6
+ * variant (direct rsID associations), and by study (the associations of one
7
+ * GCST study accession).
7
8
  */
8
9
  import { z } from "zod";
9
10
  import { apiFetchValidated, describeApiError } from "./api-utils.js";
@@ -30,10 +31,10 @@ const RawGwasAssociationSchema = z.object({
30
31
  study: RawGwasStudySchema.optional(),
31
32
  pvalueMantissa: z.number().optional(),
32
33
  pvalueExponent: z.number().optional(),
33
- riskFrequency: z.string().optional(),
34
+ riskFrequency: z.string().nullable().optional(),
34
35
  orPerCopyNum: z.number().nullable().optional(),
35
36
  betaNum: z.number().nullable().optional(),
36
- range: z.string().optional(),
37
+ range: z.string().nullable().optional(),
37
38
  });
38
39
  const GwasEmbeddedSchema = z.object({
39
40
  _embedded: z.object({ associations: z.array(RawGwasAssociationSchema).optional() }).optional(),
@@ -115,6 +116,12 @@ export async function searchGwasCatalog(query, searchType, options = {}) {
115
116
  const rsId = query.startsWith("rs") ? query : `rs${query}`;
116
117
  associationsUrl = `${GWAS_BASE}/singleNucleotidePolymorphisms/${rsId}/associations?projection=associationBySnp`;
117
118
  }
119
+ else if (searchType === "study") {
120
+ // The study endpoint keys on the accession alone. The projection nests
121
+ // the study record on each association, thus `mapAssociation` reads the
122
+ // accession, the PubMed id and the sample size without a second request.
123
+ associationsUrl = `${GWAS_BASE}/studies/${encodeURIComponent(query)}/associations?projection=associationByStudy&size=${limit}`;
124
+ }
118
125
  else {
119
126
  const traitRes = await apiFetchValidated(`${GWAS_BASE}/efoTraits/search/findBySearchQuery?query=${encodeURIComponent(query)}`, GwasTraitSearchResponseSchema, {
120
127
  headers: GWAS_HEADERS,
@@ -127,8 +134,13 @@ export async function searchGwasCatalog(query, searchType, options = {}) {
127
134
  associationsUrl = `${GWAS_BASE}/efoTraits/${traitUri.split("/").pop()}/associations?size=${limit}`;
128
135
  }
129
136
  const res = await apiFetchValidated(associationsUrl, GwasEmbeddedSchema, { headers: GWAS_HEADERS });
130
- if (res.isErr())
137
+ if (res.isErr()) {
138
+ // An accession or an rsID that the catalog does not hold is absence, not
139
+ // a failure, thus it returns an empty record set.
140
+ if (res.error.type === "http_status" && res.error.status === 404)
141
+ return { totalFound: 0, associations: [] };
131
142
  throw new Error(describeApiError(res.error));
143
+ }
132
144
  const rawAssocs = res.value?._embedded?.associations ?? [];
133
145
  const totalFound = res.value?.page?.totalElements ?? rawAssocs.length;
134
146
  const filtered = rawAssocs.filter((a) => (a.pvalueMantissa ?? 1) * Math.pow(10, a.pvalueExponent ?? 0) <= pValueThreshold).slice(0, limit);
@@ -43,11 +43,13 @@ export declare function getFaersByDrug(drugName: string, options?: {
43
43
  */
44
44
  export declare function getFaersSeriousness(drugName: string): Promise<SeriousnessProfile | null>;
45
45
  /**
46
- * Fetch FDA Structured Product Label entries by generic drug name. Returns
47
- * the most recent N labels (sorted by effective_time desc); each row carries
48
- * the boxed-warning text, the Section 5 warnings_and_cautions excerpt, and a
49
- * REMS indicator. Returns [] on 404 / no match.
46
+ * Fetch FDA Structured Product Label entries by drug name. The name matches
47
+ * the generic name or the brand name, thus a caller that holds only a brand
48
+ * name reads the generic name back off each row. Returns the most recent N
49
+ * labels (sorted by effective_time desc). Each row carries the boxed-warning
50
+ * text, the Section 5 warnings_and_cautions excerpt, and a REMS indicator.
51
+ * Returns [] on 404 / no match.
50
52
  */
51
- export declare function getDrugLabelActions(genericName: string, options?: {
53
+ export declare function getDrugLabelActions(drugName: string, options?: {
52
54
  limit?: number;
53
55
  }): Promise<DrugLabelAction[]>;
@@ -42,11 +42,17 @@ const OpenFdaLabelSchema = z.object({
42
42
  function escapeQueryPhrase(value) {
43
43
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
44
44
  }
45
+ /**
46
+ * A boolean operator is separated by a space, never by a literal `+`. The whole
47
+ * search string goes through `encodeURIComponent`, which turns a `+` into
48
+ * `%2B`. openFDA then reads that as a character of the term and matches
49
+ * nothing, but it reads `%20` as the separator that it is.
50
+ */
45
51
  function buildSearch(drugName, serious) {
46
52
  const escaped = escapeQueryPhrase(drugName);
47
53
  let search = `patient.drug.openfda.generic_name:"${escaped}"`;
48
54
  if (serious)
49
- search += "+AND+serious:1";
55
+ search += " AND serious:1";
50
56
  return search;
51
57
  }
52
58
  /** Top adverse-reaction terms by count for a generic drug name. */
@@ -98,7 +104,7 @@ export async function getFaersSeriousness(drugName) {
98
104
  };
99
105
  }
100
106
  async function countWith(field) {
101
- const q = `${search}+AND+${field}:1`;
107
+ const q = `${search} AND ${field}:1`;
102
108
  const url = `${OPENFDA_BASE}?search=${encodeURIComponent(q)}&limit=1`;
103
109
  const res = await apiFetchValidated(url, FaersMetaResponseSchema);
104
110
  if (res.isErr())
@@ -180,15 +186,17 @@ const OpenFdaLabelResponseSchema = z.object({
180
186
  results: z.array(OpenFdaLabelSchema.transform(mapLabel)).optional(),
181
187
  });
182
188
  /**
183
- * Fetch FDA Structured Product Label entries by generic drug name. Returns
184
- * the most recent N labels (sorted by effective_time desc); each row carries
185
- * the boxed-warning text, the Section 5 warnings_and_cautions excerpt, and a
186
- * REMS indicator. Returns [] on 404 / no match.
189
+ * Fetch FDA Structured Product Label entries by drug name. The name matches
190
+ * the generic name or the brand name, thus a caller that holds only a brand
191
+ * name reads the generic name back off each row. Returns the most recent N
192
+ * labels (sorted by effective_time desc). Each row carries the boxed-warning
193
+ * text, the Section 5 warnings_and_cautions excerpt, and a REMS indicator.
194
+ * Returns [] on 404 / no match.
187
195
  */
188
- export async function getDrugLabelActions(genericName, options = {}) {
196
+ export async function getDrugLabelActions(drugName, options = {}) {
189
197
  const limit = options.limit ?? 5;
190
- const escaped = escapeQueryPhrase(genericName);
191
- const search = `openfda.generic_name:"${escaped}"`;
198
+ const escaped = escapeQueryPhrase(drugName);
199
+ const search = `(openfda.generic_name:"${escaped}" OR openfda.brand_name:"${escaped}")`;
192
200
  const url = `${OPENFDA_LABEL_BASE}?search=${encodeURIComponent(search)}` + `&sort=effective_time:desc&limit=${limit}`;
193
201
  const res = await apiFetchValidated(url, OpenFdaLabelResponseSchema);
194
202
  if (res.isErr()) {
@@ -35,6 +35,17 @@ export interface SafetyLiability {
35
35
  effects: string | null;
36
36
  source: string;
37
37
  }
38
+ /**
39
+ * One disease or trait that Open Targets holds, as the free-text resolver
40
+ * returns it. The `id` is the identifier that `searchDiseaseAssociations`
41
+ * accepts. Open Targets keys a disease on the EFO ontology, which imports
42
+ * MONDO, Orphanet and HP, thus an id can read `EFO_…`, `MONDO_…` or `HP_…`.
43
+ */
44
+ export interface DiseaseCandidate {
45
+ id: string;
46
+ name: string;
47
+ description: string | null;
48
+ }
38
49
  export interface BaselineExpressionEntry {
39
50
  tissueId: string;
40
51
  tissueLabel: string;
@@ -51,6 +62,15 @@ export interface BaselineExpressionEntry {
51
62
  export declare function searchTargetAssociations(ensemblId: string, limit?: number): Promise<TargetInfo | null>;
52
63
  /** Fetch target associations for a disease (EFO id). */
53
64
  export declare function searchDiseaseAssociations(efoId: string, limit?: number): Promise<Association[]>;
65
+ /**
66
+ * Resolve a free-text disease or trait name to the disease ids that
67
+ * `searchDiseaseAssociations` accepts. A name that matches nothing gives an
68
+ * empty candidate list, which is a normal outcome and not an error.
69
+ */
70
+ export declare function searchDiseases(query: string, limit?: number): Promise<{
71
+ total: number;
72
+ candidates: DiseaseCandidate[];
73
+ }>;
54
74
  /** Fetch known safety liabilities for a target. */
55
75
  export declare function getTargetSafetyLiabilities(ensemblId: string): Promise<{
56
76
  targetSymbol: string;
@@ -50,6 +50,18 @@ const DISEASE_QUERY = `
50
50
  }
51
51
  }
52
52
  `;
53
+ const DISEASE_SEARCH_QUERY = `
54
+ query DiseaseSearch($queryString: String!, $size: Int!) {
55
+ search(queryString: $queryString, entityNames: ["disease"], page: { size: $size, index: 0 }) {
56
+ total
57
+ hits {
58
+ id
59
+ name
60
+ description
61
+ }
62
+ }
63
+ }
64
+ `;
53
65
  const SAFETY_QUERY = `
54
66
  query TargetSafety($ensemblId: String!) {
55
67
  target(ensemblId: $ensemblId) {
@@ -147,6 +159,22 @@ const DiseaseAssociationsDataSchema = z.object({
147
159
  .nullable()
148
160
  .optional(),
149
161
  });
162
+ const DiseaseSearchDataSchema = z.object({
163
+ search: z
164
+ .object({
165
+ total: z.number().nullable().optional(),
166
+ hits: z
167
+ .array(z.object({
168
+ id: z.string(),
169
+ name: z.string().nullable().optional(),
170
+ description: z.string().nullable().optional(),
171
+ }))
172
+ .nullable()
173
+ .optional(),
174
+ })
175
+ .nullable()
176
+ .optional(),
177
+ });
150
178
  const TargetSafetyDataSchema = z.object({
151
179
  target: z
152
180
  .object({
@@ -253,6 +281,23 @@ export async function searchDiseaseAssociations(efoId, limit = 25) {
253
281
  literaturePmids: [],
254
282
  }));
255
283
  }
284
+ /**
285
+ * Resolve a free-text disease or trait name to the disease ids that
286
+ * `searchDiseaseAssociations` accepts. A name that matches nothing gives an
287
+ * empty candidate list, which is a normal outcome and not an error.
288
+ */
289
+ export async function searchDiseases(query, limit = 10) {
290
+ const data = await gqlFetch(DISEASE_SEARCH_QUERY, { queryString: query, size: limit }, DiseaseSearchDataSchema);
291
+ const hits = data.search?.hits ?? [];
292
+ return {
293
+ total: data.search?.total ?? hits.length,
294
+ candidates: hits.map((hit) => ({
295
+ id: hit.id,
296
+ name: hit.name ?? "",
297
+ description: hit.description ?? null,
298
+ })),
299
+ };
300
+ }
256
301
  /** Fetch known safety liabilities for a target. */
257
302
  export async function getTargetSafetyLiabilities(ensemblId) {
258
303
  const data = await gqlFetch(SAFETY_QUERY, { ensemblId }, TargetSafetyDataSchema);
@@ -23,5 +23,12 @@ export interface PathwaySearchOptions {
23
23
  export declare function searchKegg(query: string, organism: string, maxResults: number, includeGenes: boolean): Promise<Pathway[]>;
24
24
  export declare function searchReactome(query: string, species: string, maxResults: number, includeGenes: boolean): Promise<Pathway[]>;
25
25
  export declare function searchPathways(query: string, options?: PathwaySearchOptions): Promise<Pathway[]>;
26
+ /**
27
+ * Read one pathway by its own identifier. The prefix of the identifier picks
28
+ * the database, thus the caller passes the id back exactly as a search
29
+ * returned it. An identifier that neither database holds gives null, which is
30
+ * absence and not a failure.
31
+ */
32
+ export declare function getPathwayById(id: string): Promise<Pathway | null>;
26
33
  /** Pathway memberships for a gene symbol — used directly by §3.7 collector. */
27
34
  export declare function getPathwayMemberships(geneSymbol: string): Promise<Pathway[]>;
@@ -6,7 +6,11 @@
6
6
  import { z } from "zod";
7
7
  import { apiFetch, apiFetchValidated, parseTSV } from "./api-utils.js";
8
8
  const KEGG_BASE = "https://rest.kegg.jp";
9
- const REACTOME_BASE = "https://reactome.org/ContentService";
9
+ // The ContentService of the primary host answers 404 on each path. The release
10
+ // host serves the same API, and a probe of `search/query`, `data/query/{stId}`
11
+ // and `data/participants/{stId}` returned 200 on it. The web pages that a result
12
+ // links to stay on the primary host, thus only the API base moves.
13
+ const REACTOME_BASE = "https://release.reactome.org/ContentService";
10
14
  export function stripHtmlAndCollapseWs(s) {
11
15
  // Strip tags to a fixpoint: a single pass can leave a `<…>` behind when
12
16
  // removing an inner tag lets the surrounding angle brackets form a new one,
@@ -40,6 +44,12 @@ const ReactomeSearchResponseSchema = z.object({
40
44
  });
41
45
  /** Participant list — `searchReactome` reads `displayName` per participant. */
42
46
  const ReactomeParticipantsSchema = z.array(z.object({ displayName: z.string().optional() }));
47
+ /** One Reactome event record, as `/data/query/{stId}` serves it. */
48
+ const ReactomeEntrySchema = z.object({
49
+ stId: z.string().optional(),
50
+ displayName: z.string().optional(),
51
+ summation: z.array(z.object({ text: z.string().optional() })).optional(),
52
+ });
43
53
  /** Participant list — `getPathwayMemberships` reads `refEntities` per participant. */
44
54
  const ReactomeParticipantEntitiesSchema = z.array(z.object({
45
55
  refEntities: z.array(z.object({ schemaClass: z.string().optional(), identifier: z.string().optional() })).optional(),
@@ -126,6 +136,63 @@ export async function searchPathways(query, options = {}) {
126
136
  const results = await Promise.all(tasks);
127
137
  return results.flat();
128
138
  }
139
+ /** A Reactome stable identifier, for example `R-HSA-109581`. */
140
+ const REACTOME_ID_RE = /^R-[A-Z]{3}-\d+(\.\d+)?$/;
141
+ /** A KEGG pathway identifier, for example `hsa04010` or `map04010`. */
142
+ const KEGG_ID_RE = /^[a-z]{3,4}\d{5}$/;
143
+ /** Read one field out of a KEGG flat-file record. */
144
+ function keggField(record, field) {
145
+ const match = new RegExp(`^${field}\\s+(.*)$`, "m").exec(record);
146
+ return match?.[1]?.trim() || null;
147
+ }
148
+ async function getKeggPathway(id) {
149
+ const res = await apiFetch(`${KEGG_BASE}/get/${encodeURIComponent(id)}`, { parseAs: "text" });
150
+ if (res.isErr())
151
+ return null;
152
+ const record = res.value;
153
+ const name = keggField(record, "NAME");
154
+ if (!name)
155
+ return null;
156
+ const description = keggField(record, "DESCRIPTION");
157
+ return {
158
+ id,
159
+ name,
160
+ source: "kegg",
161
+ ...(description ? { description } : {}),
162
+ url: `https://www.kegg.jp/pathway/${id}`,
163
+ };
164
+ }
165
+ async function getReactomePathway(id) {
166
+ const res = await apiFetchValidated(`${REACTOME_BASE}/data/query/${encodeURIComponent(id)}`, ReactomeEntrySchema, {
167
+ headers: { Accept: "application/json" },
168
+ });
169
+ if (res.isErr())
170
+ return null;
171
+ const entry = res.value;
172
+ const stId = entry.stId ?? id;
173
+ const description = entry.summation?.map((s) => s.text ?? "").find((text) => text.length > 0);
174
+ return {
175
+ id: stId,
176
+ name: stripHtmlAndCollapseWs(entry.displayName ?? ""),
177
+ source: "reactome",
178
+ ...(description ? { description: stripHtmlAndCollapseWs(description) } : {}),
179
+ url: `https://reactome.org/content/detail/${stId}`,
180
+ };
181
+ }
182
+ /**
183
+ * Read one pathway by its own identifier. The prefix of the identifier picks
184
+ * the database, thus the caller passes the id back exactly as a search
185
+ * returned it. An identifier that neither database holds gives null, which is
186
+ * absence and not a failure.
187
+ */
188
+ export async function getPathwayById(id) {
189
+ const trimmed = id.trim();
190
+ if (REACTOME_ID_RE.test(trimmed))
191
+ return getReactomePathway(trimmed);
192
+ if (KEGG_ID_RE.test(trimmed))
193
+ return getKeggPathway(trimmed);
194
+ return null;
195
+ }
129
196
  /** Pathway memberships for a gene symbol — used directly by §3.7 collector. */
130
197
  export async function getPathwayMemberships(geneSymbol) {
131
198
  const [kegg, reactome] = await Promise.all([searchKegg(geneSymbol, "hsa", 25, false), searchReactome(geneSymbol, "Homo sapiens", 25, false)]);
@@ -20,7 +20,7 @@ const Context7SearchResponseSchema = z.object({
20
20
  const Context7DocsResponseSchema = z.object({ content: z.string().optional() });
21
21
  export const resolveLibraryIdTool = defineTool({
22
22
  id: "resolve_library_id",
23
- description: "Resolve a package name (e.g., 'scanpy', 'DESeq2', 'scikit-learn') to a Context7 library ID. Call this BEFORE queryDocs to get the library ID needed for documentation lookup. Returns matching libraries ranked by relevance.",
23
+ description: "Resolve a package name (e.g., 'scanpy', 'DESeq2', 'scikit-learn') to a Context7 library ID. Call this BEFORE `query_docs` to get the library ID needed for documentation lookup. Returns matching libraries ranked by relevance.",
24
24
  inputSchema: z.object({
25
25
  libraryName: z.string().describe("Package or library name to search for (e.g., 'scanpy', 'pysam', 'lifelines')."),
26
26
  query: z.string().describe("What you need help with — used to rank results by relevance (e.g., 'how to run differential expression')."),
@@ -44,9 +44,9 @@ export const resolveLibraryIdTool = defineTool({
44
44
  });
45
45
  export const queryDocsTool = defineTool({
46
46
  id: "query_docs",
47
- description: "Query up-to-date documentation and code examples for a library. You must call resolve-library-id first to get the libraryId. Use this to verify function signatures, parameters, and usage patterns before writing code.",
47
+ description: "Query up-to-date documentation and code examples for a library. You must call `resolve_library_id` first to get the libraryId. Use this to verify function signatures, parameters, and usage patterns before writing code.",
48
48
  inputSchema: z.object({
49
- libraryId: z.string().describe("Context7 library ID from resolve-library-id (e.g., '/scverse/scanpy')."),
49
+ libraryId: z.string().describe("Context7 library ID from `resolve_library_id` (for example '/scverse/scanpy')."),
50
50
  query: z.string().describe("Specific question about the library (e.g., 'rank_genes_groups function parameters and usage')."),
51
51
  }),
52
52
  describeCall: "none",
@@ -178,8 +178,8 @@ export function createGenerateAnalogyReportTool(deps) {
178
178
  "stuck', 'brainstorm ideas', 'are there precedents outside biology'. The " +
179
179
  "trigger is exploratory INTENT, not the words 'analogy' or 'cross-domain' " +
180
180
  "— users rarely name it, so infer it and reach for the tool proactively.\n" +
181
- "NOT for: in-domain literature review (use `literature_reviewer` or " +
182
- "`pubmed`), single-gene factual lookup (use `search_gene`), or " +
181
+ "NOT for: in-domain literature review (use `pubmed` and the other " +
182
+ "bio-lookup tools), single-gene factual lookup (use `search_gene`), or " +
183
183
  "execution-mode turns where the user just wants the next step done. It " +
184
184
  "drives a multi-step research sub-agent over live literature search, so " +
185
185
  "it is slow — never spend it on a fact you could look up.\n" +
@@ -28,6 +28,7 @@ import { type ChatProvider } from "../../providers/types.js";
28
28
  import type { UsageRecorder } from "../../billing/usage-recorder.js";
29
29
  import { type Tool } from "../define-tool.js";
30
30
  import type { EnvironmentStorePaths } from "../../config/environment-stores.js";
31
+ import { type BioToolKeys } from "../bio/keys.js";
31
32
  import { type ResourcePolicy } from "../../config/resource-limits.js";
32
33
  import type { Logger } from "../../lib/logger.js";
33
34
  export interface GeneratePlanDeps extends EnvironmentStorePaths {
@@ -52,6 +53,8 @@ export interface GeneratePlanDeps extends EnvironmentStorePaths {
52
53
  readonly logger?: Logger;
53
54
  /** LLM usage-accounting seam for the planner loop; omitted falls back to the no-op recorder. */
54
55
  readonly usageRecorder?: UsageRecorder;
56
+ /** API keys for the search tools the planner uses to ground a plan. */
57
+ readonly bioKeys: BioToolKeys;
55
58
  }
56
59
  /** Build the `generate_plan` tool bound to its provider and pool. */
57
60
  export declare function createGeneratePlanTool(deps: GeneratePlanDeps): Tool;