@inflexa-ai/harness 0.30.2 → 0.31.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 (123) hide show
  1. package/dist/contracts/target-dossier.d.ts +61 -61
  2. package/dist/execution/run-synthesis.js +7 -2
  3. package/dist/lib/zod-issues.d.ts +8 -0
  4. package/dist/lib/zod-issues.js +13 -0
  5. package/dist/literature/sources/semantic-scholar.d.ts +34 -0
  6. package/dist/literature/sources/semantic-scholar.js +15 -4
  7. package/dist/sandbox/cpu-files.d.ts +37 -0
  8. package/dist/sandbox/cpu-files.js +55 -0
  9. package/dist/sandbox/docker-client.d.ts +15 -0
  10. package/dist/sandbox/docker-client.js +100 -11
  11. package/dist/sandbox/k8s-client.d.ts +5 -0
  12. package/dist/sandbox/k8s-client.js +128 -19
  13. package/dist/sandbox/thread-env.d.ts +29 -0
  14. package/dist/sandbox/thread-env.js +49 -0
  15. package/dist/state/data-profile.d.ts +35 -0
  16. package/dist/state/data-profile.js +56 -0
  17. package/dist/state/index.d.ts +1 -1
  18. package/dist/state/index.js +1 -1
  19. package/dist/tasks/data-profile.d.ts +16 -1
  20. package/dist/tasks/data-profile.js +31 -10
  21. package/dist/tools/bio/chembl.js +4 -3
  22. package/dist/tools/bio/drug-gene-interactions.d.ts +2 -14
  23. package/dist/tools/bio/drug-gene-interactions.js +10 -18
  24. package/dist/tools/bio/gene-disease-evidence.d.ts +3 -1
  25. package/dist/tools/bio/gene-disease-evidence.js +12 -9
  26. package/dist/tools/bio/keys.js +1 -0
  27. package/dist/tools/bio/lookup-annotation.d.ts +43 -0
  28. package/dist/tools/bio/lookup-annotation.js +19 -4
  29. package/dist/tools/bio/pubchem.js +5 -3
  30. package/dist/tools/bio/search-clinical-trials.js +5 -4
  31. package/dist/tools/bio/search-geo-datasets.d.ts +39 -9
  32. package/dist/tools/bio/search-geo-datasets.js +38 -20
  33. package/dist/tools/lib/api-utils.d.ts +27 -2
  34. package/dist/tools/lib/api-utils.js +43 -3
  35. package/dist/tools/lib/bgee-client.d.ts +16 -0
  36. package/dist/tools/lib/bgee-client.js +16 -5
  37. package/dist/tools/lib/cbioportal-client.d.ts +20 -0
  38. package/dist/tools/lib/cbioportal-client.js +16 -8
  39. package/dist/tools/lib/chembl-client.d.ts +105 -3
  40. package/dist/tools/lib/chembl-client.js +196 -84
  41. package/dist/tools/lib/clinical-benchmarks-client.d.ts +5 -0
  42. package/dist/tools/lib/clinical-benchmarks-client.js +5 -0
  43. package/dist/tools/lib/clinical-trials-client.d.ts +131 -2
  44. package/dist/tools/lib/clinical-trials-client.js +32 -6
  45. package/dist/tools/lib/clinvar-client.d.ts +35 -0
  46. package/dist/tools/lib/clinvar-client.js +34 -28
  47. package/dist/tools/lib/ctx-ops.d.ts +106 -35
  48. package/dist/tools/lib/ctx-ops.js +135 -69
  49. package/dist/tools/lib/dgidb-client.d.ts +73 -0
  50. package/dist/tools/lib/dgidb-client.js +25 -13
  51. package/dist/tools/lib/disgenet-client.d.ts +117 -24
  52. package/dist/tools/lib/disgenet-client.js +127 -34
  53. package/dist/tools/lib/disgenet-config.d.ts +1 -1
  54. package/dist/tools/lib/disgenet-config.js +5 -3
  55. package/dist/tools/lib/drugbank-client.d.ts +57 -58
  56. package/dist/tools/lib/drugbank-client.js +69 -64
  57. package/dist/tools/lib/drugbank-config.d.ts +6 -1
  58. package/dist/tools/lib/drugbank-config.js +6 -1
  59. package/dist/tools/lib/ema-client.d.ts +53 -0
  60. package/dist/tools/lib/ema-client.js +19 -7
  61. package/dist/tools/lib/ensembl-client.d.ts +4 -0
  62. package/dist/tools/lib/ensembl-client.js +4 -0
  63. package/dist/tools/lib/gwas-catalog-client.d.ts +64 -1
  64. package/dist/tools/lib/gwas-catalog-client.js +21 -6
  65. package/dist/tools/lib/identifier-resolver.d.ts +17 -1
  66. package/dist/tools/lib/identifier-resolver.js +13 -4
  67. package/dist/tools/lib/impc-client.d.ts +4 -0
  68. package/dist/tools/lib/impc-client.js +4 -0
  69. package/dist/tools/lib/iuphar-client.d.ts +10 -5
  70. package/dist/tools/lib/iuphar-client.js +49 -13
  71. package/dist/tools/lib/monarch-client.d.ts +4 -0
  72. package/dist/tools/lib/monarch-client.js +4 -0
  73. package/dist/tools/lib/openfda-client.d.ts +34 -0
  74. package/dist/tools/lib/openfda-client.js +22 -8
  75. package/dist/tools/lib/opentargets-client.d.ts +99 -4
  76. package/dist/tools/lib/opentargets-client.js +97 -37
  77. package/dist/tools/lib/pathway-client.d.ts +45 -0
  78. package/dist/tools/lib/pathway-client.js +88 -8
  79. package/dist/tools/lib/pharmgkb-client.d.ts +35 -1
  80. package/dist/tools/lib/pharmgkb-client.js +22 -6
  81. package/dist/tools/lib/pharmgkb-config.d.ts +8 -1
  82. package/dist/tools/lib/pharmgkb-config.js +8 -1
  83. package/dist/tools/lib/pubchem-client.d.ts +22 -0
  84. package/dist/tools/lib/pubchem-client.js +36 -33
  85. package/dist/tools/lib/pubchem-ops.d.ts +85 -5
  86. package/dist/tools/lib/pubchem-ops.js +96 -68
  87. package/dist/tools/lib/pubmed-client.d.ts +4 -0
  88. package/dist/tools/lib/pubmed-client.js +4 -0
  89. package/dist/tools/lib/string-client.d.ts +52 -0
  90. package/dist/tools/lib/string-client.js +12 -7
  91. package/dist/tools/lib/uniprot-client.d.ts +4 -0
  92. package/dist/tools/lib/uniprot-client.js +4 -0
  93. package/dist/tools/research/context7-docs.d.ts +23 -0
  94. package/dist/tools/research/context7-docs.js +49 -9
  95. package/dist/tools/research/search-github-repos.d.ts +4 -0
  96. package/dist/tools/research/search-github-repos.js +4 -0
  97. package/dist/workflows/execute-target-assessment.js +4 -4
  98. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +2 -1
  99. package/dist/workflows/target-assessment/assemblers/orchestrator.js +3 -3
  100. package/dist/workflows/target-assessment/collectors/index.d.ts +8 -2
  101. package/dist/workflows/target-assessment/collectors/index.js +37 -10
  102. package/dist/workflows/target-assessment/fanout/index.d.ts +2 -1
  103. package/dist/workflows/target-assessment/fanout/index.js +2 -2
  104. package/dist/workflows/target-assessment/lib/safety-corroboration.js +2 -2
  105. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +3 -2
  106. package/dist/workflows/target-assessment/lib/target-identity-filter.js +16 -5
  107. package/dist/workflows/target-assessment/phase4-assemble.d.ts +61 -60
  108. package/dist/workflows/target-assessment/phase4-assemble.js +2 -2
  109. package/dist/workflows/target-assessment/schemas.d.ts +2 -8
  110. package/dist/workflows/target-assessment/schemas.js +8 -2
  111. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +1 -4
  112. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -4
  113. package/package.json +1 -1
  114. package/dist/agents/sandbox/__fixtures__/deps.d.ts +0 -23
  115. package/dist/agents/sandbox/__fixtures__/deps.js +0 -102
  116. package/dist/citations/__fixtures__/resolver.d.ts +0 -3
  117. package/dist/citations/__fixtures__/resolver.js +0 -9
  118. package/dist/loop/__fixtures__/scripted-provider.d.ts +0 -31
  119. package/dist/loop/__fixtures__/scripted-provider.js +0 -81
  120. package/dist/providers/__fixtures__/session.d.ts +0 -17
  121. package/dist/providers/__fixtures__/session.js +0 -20
  122. package/dist/tools/__fixtures__/tool-context.d.ts +0 -13
  123. package/dist/tools/__fixtures__/tool-context.js +0 -22
@@ -29,9 +29,6 @@ const MAX_LIMIT = 200;
29
29
  const MAX_INPUTS = 25;
30
30
  /** Prose kept per DrugBank field when the full record is not requested. */
31
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
32
  const inputSchema = z.object({
36
33
  query: z
37
34
  .union([z.string().min(1), z.array(z.string().min(1)).min(1).max(MAX_INPUTS)])
@@ -47,7 +44,8 @@ const inputSchema = z.object({
47
44
  .optional()
48
45
  .describe("Corpora to query in parallel. Default ['dgidb'] ALONE — it aggregates the other two, so add them only for what they uniquely hold. " +
49
46
  "'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. " +
47
+ "'drugbank': the curated drug RECORD of the Discovery API (name, description, modality type, approval groups); it carries no indication, " +
48
+ "pharmacodynamics, mechanism, toxicity, half-life or drug-drug interaction, because that API serves none of them. Needs DRUGBANK_API_KEY. " +
51
49
  "'pharmgkb': pharmacogenomic annotations graded 1A…4 (1A guideline-backed); exact match only — 'CYP2D6' resolves, 'cytochrome P450 2D6' does not."),
52
50
  limit: z
53
51
  .number()
@@ -56,7 +54,7 @@ const inputSchema = z.object({
56
54
  .max(MAX_LIMIT)
57
55
  .optional()
58
56
  .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."),
57
+ "`totalInteractions` gives the pre-trim count. DrugBank serves one page of at most 50 records, thus a larger limit still returns at most 50 there."),
60
58
  interactionTypes: z.array(z.string()).optional().describe("'dgidb' only. Case-insensitive substring filter on interaction type, e.g. ['inhibitor']."),
61
59
  sourceDbs: z.array(z.string()).optional().describe("'dgidb' only. Keep only interactions from these underlying databases (substring), e.g. ['chembl']."),
62
60
  minSources: z.number().int().min(1).optional().describe("'dgidb' only. Drop interactions with fewer than this many supporting sources; default 1."),
@@ -67,33 +65,27 @@ const inputSchema = z.object({
67
65
  includeDrugRecord: z
68
66
  .boolean()
69
67
  .optional()
70
- .describe(`'drugbank' only. Default FALSE — the five prose fields come back as ${PROSE_PREVIEW_CHARS}-char previews with proseTruncated: true. ` +
68
+ .describe(`'drugbank' only. Default FALSE — the description comes back as a ${PROSE_PREVIEW_CHARS}-char preview with proseTruncated: true. ` +
71
69
  "True for full text; narrow `limit` when you do."),
72
70
  });
73
71
  function projectDrug(drug, includeFullProse) {
74
- const prose = (value) => (includeFullProse ? value : value.slice(0, PROSE_PREVIEW_CHARS));
75
72
  return {
76
73
  drugbankId: drug.drugbankId,
77
74
  name: drug.name,
78
75
  type: drug.type,
79
76
  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),
77
+ description: includeFullProse ? drug.description : drug.description.slice(0, PROSE_PREVIEW_CHARS),
91
78
  proseTruncated: !includeFullProse,
92
79
  };
93
80
  }
94
81
  /**
95
82
  * Run one corpus, folding its failure into an `unavailable` outcome so a dead
96
83
  * or unkeyed source cannot take down a call the others could answer.
84
+ *
85
+ * `unavailable` reports that the corpus was not reached. An empty result is a
86
+ * complete answer, thus it reports `no_data`. ClinPGx encodes "no annotation" as
87
+ * an HTTP 404, and its client maps that miss to an empty list before it arrives
88
+ * here. As a result an unannotated gene reads as `no_data`, not as an outage.
97
89
  */
98
90
  async function runSource(source, fetch) {
99
91
  try {
@@ -24,6 +24,7 @@
24
24
  * — an accession of that corpus — marks the others `not_applicable` rather
25
25
  * than sending them a query they cannot read.
26
26
  */
27
+ import type { Logger } from "../../lib/logger.js";
27
28
  import { type MutationFrequency } from "../lib/cbioportal-client.js";
28
29
  import { type ClinvarVariant } from "../lib/clinvar-client.js";
29
30
  import { type Gda } from "../lib/disgenet-client.js";
@@ -57,6 +58,7 @@ interface GeneDiseaseEvidenceOutput {
57
58
  export declare function createGeneDiseaseEvidenceTool(deps: {
58
59
  ncbiApiKey?: string;
59
60
  disgenetApiKey: string;
61
+ logger?: Logger;
60
62
  }): import("../define-tool.js").Tool<{
61
63
  query: string;
62
64
  queryType: "gene" | "disease" | "variant" | "gwas_study" | "clinvar_accession";
@@ -64,7 +66,7 @@ export declare function createGeneDiseaseEvidenceTool(deps: {
64
66
  limit?: number | undefined;
65
67
  pValueThreshold?: number | undefined;
66
68
  minScore?: number | undefined;
67
- disgenetSource?: "ALL" | "CURATED" | "ANIMAL_MODELS" | "BEFREE" | undefined;
69
+ disgenetSource?: "ALL" | "BIOBANK" | "CHEMBL" | "CLINICALTRIALS" | "CLINGEN" | "CLINPGX" | "CLINVAR" | "CURATED" | "FINNGEN" | "GENCC" | "GWASCAT" | "HPO" | "INFERRED" | "MGD_HUMAN" | "MGD_MOUSE" | "MODELS" | "ORPHANET" | "PHEWASCAT" | "PSYGENET" | "RGD_HUMAN" | "RGD_RAT" | "TEXTMINING_HUMAN" | "TEXTMINING_MODELS" | "UKBIOBANK" | "UNIPROT" | undefined;
68
70
  clinicalSignificance?: "pathogenic" | "likely-pathogenic" | "benign" | "likely-benign" | "uncertain" | undefined;
69
71
  informativeOnly?: boolean | undefined;
70
72
  }, GeneDiseaseEvidenceOutput>;
@@ -29,7 +29,7 @@ import { z } from "zod";
29
29
  import { defineTool } from "../define-tool.js";
30
30
  import { getSomaticMutationFrequencies } from "../lib/cbioportal-client.js";
31
31
  import { filterInformative, searchClinvar } from "../lib/clinvar-client.js";
32
- import { searchDisgenet } from "../lib/disgenet-client.js";
32
+ import { DISGENET_SOURCES, searchDisgenet } from "../lib/disgenet-client.js";
33
33
  import { searchGwasCatalog } from "../lib/gwas-catalog-client.js";
34
34
  const SOURCES = ["gwas", "disgenet", "clinvar", "cbioportal"];
35
35
  const QUERY_TYPES = ["gene", "disease", "variant", "gwas_study", "clinvar_accession"];
@@ -69,7 +69,8 @@ const inputSchema = z.object({
69
69
  .string()
70
70
  .min(1)
71
71
  .describe("The entity, matching `queryType`. gene: a HUGO symbol ('PCSK9'); DisGeNET also takes an Entrez ID, and cBioPortal takes the symbol " +
72
- "directly. disease: a disease or trait name ('LDL cholesterol'); DisGeNET also takes a UMLS CUI ('C0006142'). variant: a dbSNP rsID " +
72
+ "directly. disease: a disease or trait name ('asthma'); the GWAS Catalog needs the exact EFO trait label, and DisGeNET also takes a UMLS " +
73
+ "CUI ('C0006142'). variant: a dbSNP rsID " +
73
74
  "('rs11591147'). gwas_study: a GWAS Catalog study accession ('GCST000392'). clinvar_accession: a ClinVar accession ('VCV000012345', " +
74
75
  "'RCV000009910') or a bare variation ID."),
75
76
  queryType: z
@@ -106,9 +107,10 @@ const inputSchema = z.object({
106
107
  .describe("'gwas' only. P-value ceiling; default 5e-8 (genome-wide significance). Raise for suggestive hits."),
107
108
  minScore: z.number().min(0).max(1).optional().describe("'disgenet' only. Minimum GDA score 0–1; default 0.1. Higher = stronger evidence."),
108
109
  disgenetSource: z
109
- .enum(["ALL", "CURATED", "ANIMAL_MODELS", "BEFREE"])
110
+ .enum(DISGENET_SOURCES)
110
111
  .optional()
111
- .describe("'disgenet' only. Provenance filter; default 'ALL'. Prefer 'CURATED' for anything you will quote — 'BEFREE' is text-mined and noisy."),
112
+ .describe("'disgenet' only. Provenance filter; default 'ALL'. Prefer 'CURATED' for anything you will quote — 'TEXTMINING_HUMAN' and " +
113
+ "'TEXTMINING_MODELS' are text-mined and noisy, and 'MODELS' is animal-model evidence."),
112
114
  clinicalSignificance: z
113
115
  .enum(["pathogenic", "likely-pathogenic", "benign", "likely-benign", "uncertain"])
114
116
  .optional()
@@ -158,8 +160,10 @@ export function createGeneDiseaseEvidenceTool(deps) {
158
160
  "and the drug landscape. Reach here for the underlying records: actual SNPs, effect sizes, scores, pathogenicity calls and somatic mutation " +
159
161
  "frequencies with their studies and PMIDs.\n" +
160
162
  "ACCEPTED IDENTIFIERS, each named by its `queryType`: a HUGO gene symbol or an Entrez ID ('PCSK9', '5008'); a disease or trait name or a UMLS " +
161
- "CUI ('LDL cholesterol', 'C0006142'); a dbSNP rsID ('rs11591147'); a GWAS Catalog study accession ('GCST000392'); and a ClinVar accession " +
162
- "('VCV000012345'). The last two read one record back by the accession that an earlier result of this same tool reported.\n" +
163
+ "CUI ('asthma', 'C0006142'); a dbSNP rsID ('rs11591147'); a GWAS Catalog study accession ('GCST000392'); and a ClinVar accession " +
164
+ "('VCV000012345'). The last two read one record back by the accession that an earlier result of this same tool reported. " +
165
+ "The GWAS Catalog matches the EXACT EFO trait label (case-insensitive): 'asthma' resolves, a paraphrase such as 'LDL cholesterol' reads as " +
166
+ "no_data there ('low density lipoprotein cholesterol measurement' is the label) while the other corpora still answer it.\n" +
163
167
  "ALWAYS read `perSource` before concluding anything is absent. 'no_data' means that corpus genuinely has nothing; 'unavailable' means it " +
164
168
  "could not be reached (a missing DISGENET_API_KEY lands here — tell the user and proceed with the others); 'not_applicable' means that " +
165
169
  "corpus has no lookup for this queryType. Only 'no_data' is evidence of absence, and none is worth retrying unchanged.",
@@ -197,12 +201,11 @@ export function createGeneDiseaseEvidenceTool(deps) {
197
201
  ? runSource("disgenet", async () => {
198
202
  if (!deps.disgenetApiKey)
199
203
  throw new Error("DISGENET_API_KEY is not configured");
200
- const records = await searchDisgenet(deps.disgenetApiKey, input.query, input.queryType === "gene" ? "gene" : "disease", {
204
+ return await searchDisgenet(deps.disgenetApiKey, input.query, input.queryType === "gene" ? "gene" : "disease", {
201
205
  limit,
202
206
  ...(input.minScore !== undefined ? { minScore: input.minScore } : {}),
203
207
  ...(input.disgenetSource !== undefined ? { source: input.disgenetSource } : {}),
204
- });
205
- return { records };
208
+ }, deps.logger);
206
209
  })
207
210
  : null,
208
211
  selected.includes("clinvar")
@@ -30,6 +30,7 @@ export function createChemDbTools(keys, deps = {}) {
30
30
  geneDiseaseEvidence: createGeneDiseaseEvidenceTool({
31
31
  disgenetApiKey: keys.disgenet,
32
32
  ...(keys.ncbi ? { ncbiApiKey: keys.ncbi } : {}),
33
+ ...(deps.logger ? { logger: deps.logger } : {}),
33
34
  }),
34
35
  drugGeneInteractions: createDrugGeneInteractionsTool({
35
36
  drugbankApiKey: keys.drugbank,
@@ -13,7 +13,12 @@
13
13
  * schema and made conditionally required by `.refine`, so a malformed call
14
14
  * fails at the loop boundary naming the missing field instead of reaching an
15
15
  * API request that cannot be built.
16
+ *
17
+ * Absence policy: QuickGO gives each key and sends an explicit `null` for an
18
+ * absent value. Thus a maybe-absent QuickGO field carries `.nullable()`, and
19
+ * `pathway-client.ts` holds the policy of KEGG and Reactome.
16
20
  */
21
+ import { z } from "zod";
17
22
  import { type Pathway } from "../lib/pathway-client.js";
18
23
  interface GoTerm {
19
24
  id: string;
@@ -29,6 +34,44 @@ interface GoAnnotation {
29
34
  evidenceCode?: string;
30
35
  qualifier?: string;
31
36
  }
37
+ export declare const GoTermResponseSchema: z.ZodPipe<z.ZodObject<{
38
+ results: z.ZodOptional<z.ZodArray<z.ZodObject<{
39
+ id: z.ZodOptional<z.ZodString>;
40
+ name: z.ZodOptional<z.ZodString>;
41
+ definition: z.ZodOptional<z.ZodNullable<z.ZodObject<{
42
+ text: z.ZodOptional<z.ZodString>;
43
+ }, z.core.$strip>>>;
44
+ aspect: z.ZodOptional<z.ZodString>;
45
+ }, z.core.$strip>>>;
46
+ }, z.core.$strip>, z.ZodTransform<GoTerm[], {
47
+ results?: {
48
+ id?: string | undefined;
49
+ name?: string | undefined;
50
+ definition?: {
51
+ text?: string | undefined;
52
+ } | null | undefined;
53
+ aspect?: string | undefined;
54
+ }[] | undefined;
55
+ }>>;
56
+ export declare const GoAnnotationResponseSchema: z.ZodPipe<z.ZodObject<{
57
+ results: z.ZodOptional<z.ZodArray<z.ZodObject<{
58
+ geneProductId: z.ZodOptional<z.ZodString>;
59
+ goId: z.ZodOptional<z.ZodString>;
60
+ goName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
61
+ goAspect: z.ZodOptional<z.ZodString>;
62
+ goEvidence: z.ZodOptional<z.ZodString>;
63
+ qualifier: z.ZodOptional<z.ZodString>;
64
+ }, z.core.$strip>>>;
65
+ }, z.core.$strip>, z.ZodTransform<GoAnnotation[], {
66
+ results?: {
67
+ geneProductId?: string | undefined;
68
+ goId?: string | undefined;
69
+ goName?: string | null | undefined;
70
+ goAspect?: string | undefined;
71
+ goEvidence?: string | undefined;
72
+ qualifier?: string | undefined;
73
+ }[] | undefined;
74
+ }>>;
32
75
  type AnnotationOutput = {
33
76
  terms?: GoTerm[];
34
77
  annotations?: GoAnnotation[];
@@ -13,6 +13,10 @@
13
13
  * schema and made conditionally required by `.refine`, so a malformed call
14
14
  * fails at the loop boundary naming the missing field instead of reaching an
15
15
  * API request that cannot be built.
16
+ *
17
+ * Absence policy: QuickGO gives each key and sends an explicit `null` for an
18
+ * absent value. Thus a maybe-absent QuickGO field carries `.nullable()`, and
19
+ * `pathway-client.ts` holds the policy of KEGG and Reactome.
16
20
  */
17
21
  import { ok } from "neverthrow";
18
22
  import { z } from "zod";
@@ -22,11 +26,19 @@ import { getPathwayById, searchPathways } from "../lib/pathway-client.js";
22
26
  const QUICKGO_BASE = "https://www.ebi.ac.uk/QuickGO/services";
23
27
  const HEADERS = { Accept: "application/json" };
24
28
  const DEFAULT_LIMIT = 10;
29
+ /**
30
+ * The fields that the annotation search must name.
31
+ *
32
+ * QuickGO serves the label of a GO term only when the request asks for it. A
33
+ * request that does not ask gets `"goName": null` on each row, thus every
34
+ * annotation reads as an identifier with no name.
35
+ */
36
+ const ANNOTATION_INCLUDE_FIELDS = "goName";
25
37
  // QuickGO term response, validated at the fetch boundary. The `.transform`
26
38
  // carries the (context-free) rename from the raw wire fields to the `GoTerm[]`
27
39
  // we return, so parsing IS both the validation and the normalization — there
28
40
  // is no separate raw interface or mapper to keep in sync.
29
- const GoTermResponseSchema = z
41
+ export const GoTermResponseSchema = z
30
42
  .object({
31
43
  results: z
32
44
  .array(z.object({
@@ -43,13 +55,16 @@ const GoTermResponseSchema = z
43
55
  definition: t.definition?.text ?? undefined,
44
56
  aspect: t.aspect ?? undefined,
45
57
  })));
46
- const GoAnnotationResponseSchema = z
58
+ // QuickGO answers a `goName` with an explicit `null` on each row unless the
59
+ // request names the field. `ANNOTATION_INCLUDE_FIELDS` names it, and `goName`
60
+ // stays nullable because the wire can still send the null.
61
+ export const GoAnnotationResponseSchema = z
47
62
  .object({
48
63
  results: z
49
64
  .array(z.object({
50
65
  geneProductId: z.string().optional(),
51
66
  goId: z.string().optional(),
52
- goName: z.string().optional(),
67
+ goName: z.string().nullable().optional(),
53
68
  goAspect: z.string().optional(),
54
69
  goEvidence: z.string().optional(),
55
70
  qualifier: z.string().optional(),
@@ -177,7 +192,7 @@ export const lookupAnnotationTool = defineTool({
177
192
  }
178
193
  if (geneProductId) {
179
194
  tasks.push((async () => {
180
- const params = new URLSearchParams({ geneProductId, limit: String(cap) });
195
+ const params = new URLSearchParams({ geneProductId, limit: String(cap), includeFields: ANNOTATION_INCLUDE_FIELDS });
181
196
  if (taxonId)
182
197
  params.set("taxonId", String(taxonId));
183
198
  const res = await apiFetchValidated(`${QUICKGO_BASE}/annotation/search?${params}`, GoAnnotationResponseSchema, { headers: HEADERS });
@@ -21,9 +21,11 @@ const inputSchema = z
21
21
  .describe("Which PubChem lookup to run. " +
22
22
  "'compound' — resolve `query` (in the namespace named by `searchBy`) to a compound. Returns results[]: identity (cid, canonicalSmiles, inchi, inchiKey, iupacName, " +
23
23
  "molecularFormula) and computed properties (molecularWeight, xlogp, tpsa, hbondDonorCount, hbondAcceptorCount, rotatableBondCount, complexity). " +
24
- "'crossrefs' — external registry ids for a `cid`. Returns crossRefs[]: a flat list of { source, id } pairs across ChEMBL, DrugBank, KEGG, PDB and many other " +
25
- "registries; filter by `source` for the one you want. This is the bridge out of PubChem. " +
26
- "'assays' — bioassay screening summaries for a `cid`; answers 'has this compound ever been screened, and against what?'. Returns assays[]: aid, assayName, targetName, " +
24
+ "'crossrefs' — external registry ids for a `cid`. Returns crossRefs[]: a flat list of { source, id } entries. `source` names the registry that the id pattern " +
25
+ "identifies — ChEMBL, ChEBI, DrugBank, KEGG, HMDB, CAS, PDB or UNII — and it is null for an id of any other registry; filter by `source` for the one you want. " +
26
+ "This is the bridge out of PubChem. " +
27
+ "'assays' — bioassay screening summaries for a `cid`; answers 'has this compound ever been screened, and against what?'. Returns assays[]: aid, assayName, targetName " +
28
+ "(the target accession, for example a UniProt or PDB id), " +
27
29
  "activityOutcome (Active / Inactive / Inconclusive / Unspecified), activityValue. Broad HTS screening coverage, NOT curated potency — for numbers you intend to quote " +
28
30
  "(IC50/Ki), prefer chembl action='bioactivity'."),
29
31
  query: z
@@ -26,8 +26,8 @@ export const searchClinicalTrialsTool = defineTool({
26
26
  "read a result, as opposed to finding a trial.\n" +
27
27
  "ACCEPTED IDENTIFIERS: `query` is free text matched across the whole study record, so a gene symbol ('KRAS'), a drug or brand name ('imatinib', " +
28
28
  "'Gleevec'), a condition ('melanoma') or an NCT ID ('NCT00000102') all work. `nctId` takes one NCT ID and nothing else.\n" +
29
- "`phase` and `status` are optional server-side filters — omit them to see the full landscape; totalFound reports the true match count even when it " +
30
- "exceeds the page returned.\n" +
29
+ "`status` is an optional server-side filter and `phase` narrows the returned page. Omit both to see the full landscape. totalFound reports the true " +
30
+ "match count of `query` plus `status`, thus it can exceed the trials that a phase-narrowed answer carries.\n" +
31
31
  "ABSENCE IS NORMAL. An empty trials array is a valid 'nothing matched' (often an over-narrow filter) and a null trial from 'details' means the " +
32
32
  "registry holds no such NCT ID — report it and move on, do not retry the identical call. A trial with no posted results carries empty outcomes " +
33
33
  "and adverse events, which means unreported, never zero.",
@@ -45,8 +45,9 @@ export const searchClinicalTrialsTool = defineTool({
45
45
  phase: z
46
46
  .enum(["EARLY_PHASE1", "PHASE1", "PHASE2", "PHASE3", "PHASE4"])
47
47
  .optional()
48
- .describe("'search' only. Optional exact phase filter. Omit for all phases (observational and expanded-access studies carry no phase and are " +
49
- "excluded when this is set)."),
48
+ .describe("'search' only. Optional exact phase filter, applied to the returned page rather than to the whole query. Omit for all phases " +
49
+ "(observational and expanded-access studies carry no phase and are excluded when this is set). A narrow phase over a broad query " +
50
+ "can give fewer trials than `limit` asks for, even when the registry holds more."),
50
51
  status: z
51
52
  .enum(["RECRUITING", "ACTIVE_NOT_RECRUITING", "COMPLETED", "NOT_YET_RECRUITING", "TERMINATED", "WITHDRAWN", "SUSPENDED"])
52
53
  .optional()
@@ -1,6 +1,44 @@
1
1
  /**
2
2
  * searchGeoDatasets — search NCBI GEO for public gene expression datasets.
3
3
  */
4
+ import { z } from "zod";
5
+ export declare const GeoEsearchResponseSchema: z.ZodObject<{
6
+ esearchresult: z.ZodOptional<z.ZodObject<{
7
+ idlist: z.ZodOptional<z.ZodArray<z.ZodString>>;
8
+ count: z.ZodOptional<z.ZodString>;
9
+ }, z.core.$strip>>;
10
+ }, z.core.$strip>;
11
+ /** One dataset row, as the tool returns it. */
12
+ export interface GeoDataset {
13
+ accession: string;
14
+ title: string;
15
+ summary: string;
16
+ platform: string | null;
17
+ sampleCount: number | null;
18
+ organism: string | null;
19
+ pubmedIds: (string | number)[];
20
+ }
21
+ export declare const GeoEsummaryResponseSchema: z.ZodObject<{
22
+ result: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodCatch<z.ZodOptional<z.ZodObject<{
23
+ accession: z.ZodOptional<z.ZodString>;
24
+ gse: z.ZodOptional<z.ZodString>;
25
+ title: z.ZodOptional<z.ZodString>;
26
+ summary: z.ZodOptional<z.ZodString>;
27
+ gpl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
28
+ n_samples: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
29
+ taxon: z.ZodOptional<z.ZodNullable<z.ZodString>>;
30
+ pubmedids: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
31
+ error: z.ZodOptional<z.ZodString>;
32
+ }, z.core.$strip>>>>>;
33
+ }, z.core.$strip>;
34
+ /**
35
+ * Map one esummary answer onto the dataset rows.
36
+ *
37
+ * `ids` is the id list of the search, and it names the order of the rows. The
38
+ * function is pure, thus the golden-fixture table exercises it against a stored
39
+ * payload.
40
+ */
41
+ export declare function mapGeoDatasets(ids: string[], summary: z.infer<typeof GeoEsummaryResponseSchema>): GeoDataset[];
4
42
  export declare const searchGeoDatasetsTool: import("../define-tool.js").Tool<{
5
43
  query: string;
6
44
  datasetType: "gse" | "gds";
@@ -8,13 +46,5 @@ export declare const searchGeoDatasetsTool: import("../define-tool.js").Tool<{
8
46
  organism?: string | undefined;
9
47
  }, {
10
48
  totalFound: number;
11
- datasets: {
12
- accession: string;
13
- title: string;
14
- summary: string;
15
- platform: string | null;
16
- sampleCount: number | null;
17
- organism: string | null;
18
- pubmedIds: (string | number)[];
19
- }[];
49
+ datasets: GeoDataset[];
20
50
  }>;
@@ -10,7 +10,7 @@ const ESEARCH_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils";
10
10
  // optional because the API omits absent values; the mapping in `execute` keys
11
11
  // off the esearch `idlist` (request context) and reshapes each summary, so the
12
12
  // schemas model only the raw wire.
13
- const GeoEsearchResponseSchema = z.object({
13
+ export const GeoEsearchResponseSchema = z.object({
14
14
  esearchresult: z
15
15
  .object({
16
16
  idlist: z.array(z.string()).optional(),
@@ -18,24 +18,58 @@ const GeoEsearchResponseSchema = z.object({
18
18
  })
19
19
  .optional(),
20
20
  });
21
+ // `gpl` is the platform accession, as a bare number string ("20301"). esummary
22
+ // serves no `platform` key at any level. `error` marks the 200-with-error form:
23
+ // esummary answers HTTP 200 for an unknown uid and puts `{"uid": …, "error": …}`
24
+ // under that key, which carries no dataset.
21
25
  const GeoDatasetSummarySchema = z.object({
22
26
  accession: z.string().optional(),
23
27
  gse: z.string().optional(),
24
28
  title: z.string().optional(),
25
29
  summary: z.string().optional(),
26
30
  gpl: z.string().nullable().optional(),
27
- platform: z.string().nullable().optional(),
28
31
  n_samples: z.union([z.string(), z.number()]).optional(),
29
32
  taxon: z.string().nullable().optional(),
30
33
  pubmedids: z.array(z.union([z.string(), z.number()])).optional(),
34
+ error: z.string().optional(),
31
35
  });
32
36
  // The esummary `result` object mixes a `uids` string array in with the
33
37
  // numeric-keyed dataset summaries; `.catch(undefined)` on the record value lets
34
38
  // that `uids` entry (and any drifted summary) fall through as undefined rather
35
39
  // than rejecting the whole response — the mapping reads only the numeric ids.
36
- const GeoEsummaryResponseSchema = z.object({
40
+ export const GeoEsummaryResponseSchema = z.object({
37
41
  result: z.record(z.string(), GeoDatasetSummarySchema.optional().catch(undefined)).optional(),
38
42
  });
43
+ /**
44
+ * Map one esummary answer onto the dataset rows.
45
+ *
46
+ * `ids` is the id list of the search, and it names the order of the rows. The
47
+ * function is pure, thus the golden-fixture table exercises it against a stored
48
+ * payload.
49
+ */
50
+ export function mapGeoDatasets(ids, summary) {
51
+ const result = summary?.result ?? {};
52
+ const datasets = [];
53
+ for (const id of ids) {
54
+ const r = result[id];
55
+ if (!r)
56
+ continue;
57
+ // An error record carries no dataset. Without this guard it becomes a
58
+ // row of empty strings with a fabricated accession.
59
+ if (r.error !== undefined)
60
+ continue;
61
+ datasets.push({
62
+ accession: r.accession ?? r.gse ?? `GDS${id}`,
63
+ title: r.title ?? "",
64
+ summary: (r.summary ?? "").slice(0, 300),
65
+ platform: r.gpl ?? null,
66
+ sampleCount: r.n_samples ? Number(r.n_samples) : null,
67
+ organism: r.taxon ?? null,
68
+ pubmedIds: r.pubmedids ?? [],
69
+ });
70
+ }
71
+ return datasets;
72
+ }
39
73
  export const searchGeoDatasetsTool = defineTool({
40
74
  id: "search_geo_datasets",
41
75
  description: "Search the NCBI Gene Expression Omnibus (GEO) — the public functional-genomics repository of the U.S. National Library of Medicine — for " +
@@ -88,23 +122,7 @@ export const searchGeoDatasetsTool = defineTool({
88
122
  const summaryRes = await apiFetchValidated(summaryUrl, GeoEsummaryResponseSchema);
89
123
  if (summaryRes.isErr())
90
124
  throw new Error(describeApiError(summaryRes.error));
91
- const result = summaryRes.value?.result ?? {};
92
- const datasets = ids
93
- .map((id) => {
94
- const r = result[id];
95
- if (!r)
96
- return null;
97
- return {
98
- accession: r.accession ?? r.gse ?? `GDS${id}`,
99
- title: r.title ?? "",
100
- summary: (r.summary ?? "").slice(0, 300),
101
- platform: r.gpl ?? r.platform ?? null,
102
- sampleCount: r.n_samples ? Number(r.n_samples) : null,
103
- organism: r.taxon ?? null,
104
- pubmedIds: r.pubmedids ?? [],
105
- };
106
- })
107
- .filter((d) => d !== null);
125
+ const datasets = mapGeoDatasets(ids, summaryRes.value);
108
126
  const totalFound = Number(searchRes.value?.esearchresult?.count) || datasets.length;
109
127
  return ok({ totalFound, datasets });
110
128
  },
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Shared HTTP fetch helper for bioinformatics API tools.
3
3
  *
4
- * Provides retry-with-backoff for rate-limited APIs (429/503), request
4
+ * Provides retry-with-backoff for a rate-limited or a transiently-broken API
5
+ * (429/502/503/504), request
5
6
  * timeouts, and tab-delimited text parsing. `apiFetch` is the external-call
6
7
  * boundary, so it is where a throw becomes a `Result`: it returns a
7
8
  * `ResultAsync<T, ApiError>` rather than throwing — callers branch on
@@ -35,7 +36,7 @@ export type ApiError = {
35
36
  readonly issues: string;
36
37
  };
37
38
  /**
38
- * Fetch a URL with retry on 429/503, exponential backoff, and timeout.
39
+ * Fetch a URL with retry on 429/502/503/504, exponential backoff, and timeout.
39
40
  * `err` carries the structured failure; a non-ok HTTP status is reported as
40
41
  * `http_status` so callers can branch on a concrete code (a 404 is usually an
41
42
  * expected "not found" → a data variant; see `isUnexpectedApiError`).
@@ -73,6 +74,30 @@ export declare function describeApiError(e: ApiError): string;
73
74
  * throwing (or returning `err`).
74
75
  */
75
76
  export declare function isUnexpectedApiError(e: ApiError): boolean;
77
+ /**
78
+ * Read a number that a provider serializes as a JSON string.
79
+ *
80
+ * A Tastypie `decimal` field (ChEMBL `max_phase`, `standard_value`,
81
+ * `pchembl_value`) and the eutils `esearch` counts arrive as strings, and the
82
+ * same value arrives as a number from a different serializer of the same
83
+ * provider. One helper answers for both encodings, thus no call site invents its
84
+ * own rule.
85
+ *
86
+ * A value that is not a finite number gives `null`. As a result a caller never
87
+ * sees `NaN`, which compares false against itself and poisons every downstream
88
+ * threshold.
89
+ */
90
+ export declare function parseWireNumber(value: string | number | null | undefined): number | null;
91
+ /**
92
+ * The schema form of `parseWireNumber`, for a field that a provider serializes
93
+ * as a string or as a number.
94
+ *
95
+ * The helper accepts neither `null` nor an omission, because absence is a
96
+ * per-provider policy and no field is widened without evidence. A caller
97
+ * composes `.nullable()`, `.optional()`, or both, per the policy of its
98
+ * provider.
99
+ */
100
+ export declare const zWireNumber: z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<number | null, string | number>>;
76
101
  /**
77
102
  * Parse tab-separated text into rows of columns (e.g., KEGG responses).
78
103
  * Filters out empty lines.
@@ -1,17 +1,22 @@
1
1
  /**
2
2
  * Shared HTTP fetch helper for bioinformatics API tools.
3
3
  *
4
- * Provides retry-with-backoff for rate-limited APIs (429/503), request
4
+ * Provides retry-with-backoff for a rate-limited or a transiently-broken API
5
+ * (429/502/503/504), request
5
6
  * timeouts, and tab-delimited text parsing. `apiFetch` is the external-call
6
7
  * boundary, so it is where a throw becomes a `Result`: it returns a
7
8
  * `ResultAsync<T, ApiError>` rather than throwing — callers branch on
8
9
  * `isErr()` and read `value` / `error` (see `result.ts` for the house rules).
9
10
  */
10
11
  import { ResultAsync, err, ok } from "neverthrow";
12
+ import { z } from "zod";
11
13
  import { sleep } from "../../lib/async-utils.js";
12
- const RETRYABLE_STATUSES = new Set([429, 503]);
14
+ // 502 and 504 sit here next to 429 and 503 because a gateway in front of a bio
15
+ // provider answers with one of them while the origin restarts, and it sends an
16
+ // HTML body. That answer is transient, thus a retry gets the real payload.
17
+ const RETRYABLE_STATUSES = new Set([429, 502, 503, 504]);
13
18
  /**
14
- * Fetch a URL with retry on 429/503, exponential backoff, and timeout.
19
+ * Fetch a URL with retry on 429/502/503/504, exponential backoff, and timeout.
15
20
  * `err` carries the structured failure; a non-ok HTTP status is reported as
16
21
  * `http_status` so callers can branch on a concrete code (a 404 is usually an
17
22
  * expected "not found" → a data variant; see `isUnexpectedApiError`).
@@ -116,6 +121,41 @@ export function describeApiError(e) {
116
121
  export function isUnexpectedApiError(e) {
117
122
  return !(e.type === "http_status" && e.status >= 400 && e.status < 500);
118
123
  }
124
+ /**
125
+ * Read a number that a provider serializes as a JSON string.
126
+ *
127
+ * A Tastypie `decimal` field (ChEMBL `max_phase`, `standard_value`,
128
+ * `pchembl_value`) and the eutils `esearch` counts arrive as strings, and the
129
+ * same value arrives as a number from a different serializer of the same
130
+ * provider. One helper answers for both encodings, thus no call site invents its
131
+ * own rule.
132
+ *
133
+ * A value that is not a finite number gives `null`. As a result a caller never
134
+ * sees `NaN`, which compares false against itself and poisons every downstream
135
+ * threshold.
136
+ */
137
+ export function parseWireNumber(value) {
138
+ if (value === null || value === undefined)
139
+ return null;
140
+ if (typeof value === "number")
141
+ return Number.isFinite(value) ? value : null;
142
+ const trimmed = value.trim();
143
+ // `Number("")` and `Number(" ")` give 0, thus an empty string must not reach it.
144
+ if (trimmed === "")
145
+ return null;
146
+ const parsed = Number(trimmed);
147
+ return Number.isFinite(parsed) ? parsed : null;
148
+ }
149
+ /**
150
+ * The schema form of `parseWireNumber`, for a field that a provider serializes
151
+ * as a string or as a number.
152
+ *
153
+ * The helper accepts neither `null` nor an omission, because absence is a
154
+ * per-provider policy and no field is widened without evidence. A caller
155
+ * composes `.nullable()`, `.optional()`, or both, per the policy of its
156
+ * provider.
157
+ */
158
+ export const zWireNumber = z.union([z.string(), z.number()]).transform((value) => parseWireNumber(value));
119
159
  /**
120
160
  * Parse tab-separated text into rows of columns (e.g., KEGG responses).
121
161
  * Filters out empty lines.
@@ -5,7 +5,12 @@
5
5
  * lookups for the configured non-human species (Ensembl REST), then
6
6
  * queries Bgee per (species, ortholog ENSG) for tissue-level expression
7
7
  * calls. Returns a per-species table of tissue × expression-rank.
8
+ *
9
+ * Absence policy: Bgee and the Ensembl calls in this file omit the key of an
10
+ * absent value, and neither one sends an explicit `null`. Thus a maybe-absent
11
+ * field carries `.optional()`, not `.nullable()`.
8
12
  */
13
+ import { z } from "zod";
9
14
  export declare const SUPPORTED_SPECIES: readonly ["homo_sapiens", "mus_musculus", "rattus_norvegicus", "canis_lupus_familiaris", "macaca_mulatta"];
10
15
  export type SupportedSpecies = (typeof SUPPORTED_SPECIES)[number];
11
16
  export declare const SPECIES_TAXON: Record<SupportedSpecies, number>;
@@ -31,6 +36,17 @@ export interface MultiSpeciesExpression {
31
36
  notFound: string[];
32
37
  }
33
38
  export declare function bucketRank(expressionState: string, score: number | null): ExpressionRank;
39
+ export declare const EnsemblLookupSchema: z.ZodObject<{
40
+ id: z.ZodOptional<z.ZodString>;
41
+ }, z.core.$strip>;
42
+ export declare const EnsemblHomologyResponseSchema: z.ZodObject<{
43
+ data: z.ZodOptional<z.ZodArray<z.ZodObject<{
44
+ homologies: z.ZodOptional<z.ZodArray<z.ZodObject<{
45
+ id: z.ZodOptional<z.ZodString>;
46
+ type: z.ZodOptional<z.ZodString>;
47
+ }, z.core.$strip>>>;
48
+ }, z.core.$strip>>>;
49
+ }, z.core.$strip>;
34
50
  export declare function parseExpressionResponse(raw: unknown): TissueRow[];
35
51
  /** Fetch multi-species expression for a human gene symbol. */
36
52
  export declare function getMultiSpeciesExpression(geneSymbol: string, species?: SupportedSpecies[]): Promise<MultiSpeciesExpression>;