@inflexa-ai/harness 0.30.3 → 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 (109) 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/tools/bio/chembl.js +4 -3
  8. package/dist/tools/bio/drug-gene-interactions.d.ts +2 -14
  9. package/dist/tools/bio/drug-gene-interactions.js +10 -18
  10. package/dist/tools/bio/gene-disease-evidence.d.ts +3 -1
  11. package/dist/tools/bio/gene-disease-evidence.js +12 -9
  12. package/dist/tools/bio/keys.js +1 -0
  13. package/dist/tools/bio/lookup-annotation.d.ts +43 -0
  14. package/dist/tools/bio/lookup-annotation.js +19 -4
  15. package/dist/tools/bio/pubchem.js +5 -3
  16. package/dist/tools/bio/search-clinical-trials.js +5 -4
  17. package/dist/tools/bio/search-geo-datasets.d.ts +39 -9
  18. package/dist/tools/bio/search-geo-datasets.js +38 -20
  19. package/dist/tools/lib/api-utils.d.ts +27 -2
  20. package/dist/tools/lib/api-utils.js +43 -3
  21. package/dist/tools/lib/bgee-client.d.ts +16 -0
  22. package/dist/tools/lib/bgee-client.js +16 -5
  23. package/dist/tools/lib/cbioportal-client.d.ts +20 -0
  24. package/dist/tools/lib/cbioportal-client.js +16 -8
  25. package/dist/tools/lib/chembl-client.d.ts +105 -3
  26. package/dist/tools/lib/chembl-client.js +196 -84
  27. package/dist/tools/lib/clinical-benchmarks-client.d.ts +5 -0
  28. package/dist/tools/lib/clinical-benchmarks-client.js +5 -0
  29. package/dist/tools/lib/clinical-trials-client.d.ts +131 -2
  30. package/dist/tools/lib/clinical-trials-client.js +32 -6
  31. package/dist/tools/lib/clinvar-client.d.ts +35 -0
  32. package/dist/tools/lib/clinvar-client.js +34 -28
  33. package/dist/tools/lib/ctx-ops.d.ts +106 -35
  34. package/dist/tools/lib/ctx-ops.js +135 -69
  35. package/dist/tools/lib/dgidb-client.d.ts +73 -0
  36. package/dist/tools/lib/dgidb-client.js +25 -13
  37. package/dist/tools/lib/disgenet-client.d.ts +117 -24
  38. package/dist/tools/lib/disgenet-client.js +127 -34
  39. package/dist/tools/lib/disgenet-config.d.ts +1 -1
  40. package/dist/tools/lib/disgenet-config.js +5 -3
  41. package/dist/tools/lib/drugbank-client.d.ts +57 -58
  42. package/dist/tools/lib/drugbank-client.js +69 -64
  43. package/dist/tools/lib/drugbank-config.d.ts +6 -1
  44. package/dist/tools/lib/drugbank-config.js +6 -1
  45. package/dist/tools/lib/ema-client.d.ts +53 -0
  46. package/dist/tools/lib/ema-client.js +19 -7
  47. package/dist/tools/lib/ensembl-client.d.ts +4 -0
  48. package/dist/tools/lib/ensembl-client.js +4 -0
  49. package/dist/tools/lib/gwas-catalog-client.d.ts +64 -1
  50. package/dist/tools/lib/gwas-catalog-client.js +21 -6
  51. package/dist/tools/lib/identifier-resolver.d.ts +17 -1
  52. package/dist/tools/lib/identifier-resolver.js +13 -4
  53. package/dist/tools/lib/impc-client.d.ts +4 -0
  54. package/dist/tools/lib/impc-client.js +4 -0
  55. package/dist/tools/lib/iuphar-client.d.ts +10 -5
  56. package/dist/tools/lib/iuphar-client.js +49 -13
  57. package/dist/tools/lib/monarch-client.d.ts +4 -0
  58. package/dist/tools/lib/monarch-client.js +4 -0
  59. package/dist/tools/lib/openfda-client.d.ts +34 -0
  60. package/dist/tools/lib/openfda-client.js +22 -8
  61. package/dist/tools/lib/opentargets-client.d.ts +99 -4
  62. package/dist/tools/lib/opentargets-client.js +97 -37
  63. package/dist/tools/lib/pathway-client.d.ts +45 -0
  64. package/dist/tools/lib/pathway-client.js +88 -8
  65. package/dist/tools/lib/pharmgkb-client.d.ts +35 -1
  66. package/dist/tools/lib/pharmgkb-client.js +22 -6
  67. package/dist/tools/lib/pharmgkb-config.d.ts +8 -1
  68. package/dist/tools/lib/pharmgkb-config.js +8 -1
  69. package/dist/tools/lib/pubchem-client.d.ts +22 -0
  70. package/dist/tools/lib/pubchem-client.js +36 -33
  71. package/dist/tools/lib/pubchem-ops.d.ts +85 -5
  72. package/dist/tools/lib/pubchem-ops.js +96 -68
  73. package/dist/tools/lib/pubmed-client.d.ts +4 -0
  74. package/dist/tools/lib/pubmed-client.js +4 -0
  75. package/dist/tools/lib/string-client.d.ts +52 -0
  76. package/dist/tools/lib/string-client.js +12 -7
  77. package/dist/tools/lib/uniprot-client.d.ts +4 -0
  78. package/dist/tools/lib/uniprot-client.js +4 -0
  79. package/dist/tools/research/context7-docs.d.ts +23 -0
  80. package/dist/tools/research/context7-docs.js +49 -9
  81. package/dist/tools/research/search-github-repos.d.ts +4 -0
  82. package/dist/tools/research/search-github-repos.js +4 -0
  83. package/dist/workflows/execute-target-assessment.js +4 -4
  84. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +2 -1
  85. package/dist/workflows/target-assessment/assemblers/orchestrator.js +3 -3
  86. package/dist/workflows/target-assessment/collectors/index.d.ts +8 -2
  87. package/dist/workflows/target-assessment/collectors/index.js +37 -10
  88. package/dist/workflows/target-assessment/fanout/index.d.ts +2 -1
  89. package/dist/workflows/target-assessment/fanout/index.js +2 -2
  90. package/dist/workflows/target-assessment/lib/safety-corroboration.js +2 -2
  91. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +3 -2
  92. package/dist/workflows/target-assessment/lib/target-identity-filter.js +16 -5
  93. package/dist/workflows/target-assessment/phase4-assemble.d.ts +61 -60
  94. package/dist/workflows/target-assessment/phase4-assemble.js +2 -2
  95. package/dist/workflows/target-assessment/schemas.d.ts +2 -8
  96. package/dist/workflows/target-assessment/schemas.js +8 -2
  97. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +1 -4
  98. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -4
  99. package/package.json +1 -1
  100. package/dist/agents/sandbox/__fixtures__/deps.d.ts +0 -23
  101. package/dist/agents/sandbox/__fixtures__/deps.js +0 -102
  102. package/dist/citations/__fixtures__/resolver.d.ts +0 -3
  103. package/dist/citations/__fixtures__/resolver.js +0 -9
  104. package/dist/loop/__fixtures__/scripted-provider.d.ts +0 -31
  105. package/dist/loop/__fixtures__/scripted-provider.js +0 -81
  106. package/dist/providers/__fixtures__/session.d.ts +0 -17
  107. package/dist/providers/__fixtures__/session.js +0 -20
  108. package/dist/tools/__fixtures__/tool-context.d.ts +0 -13
  109. package/dist/tools/__fixtures__/tool-context.js +0 -22
@@ -140,8 +140,8 @@ export declare const EntitySchema: z.ZodObject<{
140
140
  displayName: z.ZodString;
141
141
  entityType: z.ZodEnum<{
142
142
  gene: "gene";
143
- rna: "rna";
144
143
  protein: "protein";
144
+ rna: "rna";
145
145
  complex: "complex";
146
146
  }>;
147
147
  ontology: z.ZodString;
@@ -5985,8 +5985,8 @@ export declare const DossierSchema: z.ZodObject<{
5985
5985
  displayName: z.ZodString;
5986
5986
  entityType: z.ZodEnum<{
5987
5987
  gene: "gene";
5988
- rna: "rna";
5989
5988
  protein: "protein";
5989
+ rna: "rna";
5990
5990
  complex: "complex";
5991
5991
  }>;
5992
5992
  ontology: z.ZodString;
@@ -9243,64 +9243,6 @@ export type Dossier = z.infer<typeof DossierSchema>;
9243
9243
  * and validates the complete dossier against `DossierSchema`.
9244
9244
  */
9245
9245
  export declare const DossierBodySchema: z.ZodObject<{
9246
- drug_interactions: z.ZodDiscriminatedUnion<[z.ZodObject<{
9247
- coverage: z.ZodLiteral<"available">;
9248
- data: z.ZodObject<{
9249
- rows: z.ZodArray<z.ZodObject<{
9250
- drug_id: z.ZodNullable<z.ZodString>;
9251
- drug_name: z.ZodString;
9252
- best_score: z.ZodNumber;
9253
- predicates: z.ZodArray<z.ZodString>;
9254
- sources: z.ZodArray<z.ZodString>;
9255
- paper_count: z.ZodNumber;
9256
- dominant_direction: z.ZodEnum<{
9257
- unknown: "unknown";
9258
- positive: "positive";
9259
- negative: "negative";
9260
- mixed: "mixed";
9261
- }>;
9262
- has_human_evidence: z.ZodBoolean;
9263
- has_clinical_evidence: z.ZodBoolean;
9264
- evidence: z.ZodArray<z.ZodObject<{
9265
- pmid: z.ZodOptional<z.ZodString>;
9266
- doi: z.ZodOptional<z.ZodString>;
9267
- accession: z.ZodOptional<z.ZodString>;
9268
- source: z.ZodString;
9269
- predicate: z.ZodOptional<z.ZodString>;
9270
- score: z.ZodOptional<z.ZodNumber>;
9271
- strength: z.ZodOptional<z.ZodString>;
9272
- is_human: z.ZodOptional<z.ZodBoolean>;
9273
- is_clinical: z.ZodOptional<z.ZodBoolean>;
9274
- excerpt: z.ZodOptional<z.ZodString>;
9275
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
9276
- regulatory_reference: z.ZodOptional<z.ZodObject<{
9277
- document: z.ZodString;
9278
- section: z.ZodOptional<z.ZodString>;
9279
- doc_id: z.ZodOptional<z.ZodString>;
9280
- doc_url: z.ZodOptional<z.ZodString>;
9281
- }, z.core.$strip>>;
9282
- }, z.core.$strip>>;
9283
- }, z.core.$strip>>;
9284
- }, z.core.$strip>;
9285
- dropped_count: z.ZodOptional<z.ZodNumber>;
9286
- inference_path: z.ZodOptional<z.ZodString>;
9287
- truncated: z.ZodOptional<z.ZodBoolean>;
9288
- }, z.core.$strip>, z.ZodObject<{
9289
- coverage: z.ZodLiteral<"queried_no_data">;
9290
- error: z.ZodOptional<z.ZodObject<{
9291
- message: z.ZodString;
9292
- source: z.ZodOptional<z.ZodString>;
9293
- }, z.core.$strip>>;
9294
- inference_path: z.ZodOptional<z.ZodString>;
9295
- }, z.core.$strip>, z.ZodObject<{
9296
- coverage: z.ZodLiteral<"filtered">;
9297
- filter: z.ZodString;
9298
- dropped_count: z.ZodNumber;
9299
- inference_path: z.ZodOptional<z.ZodString>;
9300
- }, z.core.$strip>, z.ZodObject<{
9301
- coverage: z.ZodLiteral<"not_loaded">;
9302
- reason: z.ZodOptional<z.ZodString>;
9303
- }, z.core.$strip>], "coverage">;
9304
9246
  tractability: z.ZodDiscriminatedUnion<[z.ZodObject<{
9305
9247
  coverage: z.ZodLiteral<"available">;
9306
9248
  data: z.ZodObject<{
@@ -9348,8 +9290,8 @@ export declare const DossierBodySchema: z.ZodObject<{
9348
9290
  displayName: z.ZodString;
9349
9291
  entityType: z.ZodEnum<{
9350
9292
  gene: "gene";
9351
- rna: "rna";
9352
9293
  protein: "protein";
9294
+ rna: "rna";
9353
9295
  complex: "complex";
9354
9296
  }>;
9355
9297
  ontology: z.ZodString;
@@ -9647,6 +9589,64 @@ export declare const DossierBodySchema: z.ZodObject<{
9647
9589
  coverage: z.ZodLiteral<"not_loaded">;
9648
9590
  reason: z.ZodOptional<z.ZodString>;
9649
9591
  }, z.core.$strip>], "coverage">;
9592
+ drug_interactions: z.ZodDiscriminatedUnion<[z.ZodObject<{
9593
+ coverage: z.ZodLiteral<"available">;
9594
+ data: z.ZodObject<{
9595
+ rows: z.ZodArray<z.ZodObject<{
9596
+ drug_id: z.ZodNullable<z.ZodString>;
9597
+ drug_name: z.ZodString;
9598
+ best_score: z.ZodNumber;
9599
+ predicates: z.ZodArray<z.ZodString>;
9600
+ sources: z.ZodArray<z.ZodString>;
9601
+ paper_count: z.ZodNumber;
9602
+ dominant_direction: z.ZodEnum<{
9603
+ unknown: "unknown";
9604
+ positive: "positive";
9605
+ negative: "negative";
9606
+ mixed: "mixed";
9607
+ }>;
9608
+ has_human_evidence: z.ZodBoolean;
9609
+ has_clinical_evidence: z.ZodBoolean;
9610
+ evidence: z.ZodArray<z.ZodObject<{
9611
+ pmid: z.ZodOptional<z.ZodString>;
9612
+ doi: z.ZodOptional<z.ZodString>;
9613
+ accession: z.ZodOptional<z.ZodString>;
9614
+ source: z.ZodString;
9615
+ predicate: z.ZodOptional<z.ZodString>;
9616
+ score: z.ZodOptional<z.ZodNumber>;
9617
+ strength: z.ZodOptional<z.ZodString>;
9618
+ is_human: z.ZodOptional<z.ZodBoolean>;
9619
+ is_clinical: z.ZodOptional<z.ZodBoolean>;
9620
+ excerpt: z.ZodOptional<z.ZodString>;
9621
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
9622
+ regulatory_reference: z.ZodOptional<z.ZodObject<{
9623
+ document: z.ZodString;
9624
+ section: z.ZodOptional<z.ZodString>;
9625
+ doc_id: z.ZodOptional<z.ZodString>;
9626
+ doc_url: z.ZodOptional<z.ZodString>;
9627
+ }, z.core.$strip>>;
9628
+ }, z.core.$strip>>;
9629
+ }, z.core.$strip>>;
9630
+ }, z.core.$strip>;
9631
+ dropped_count: z.ZodOptional<z.ZodNumber>;
9632
+ inference_path: z.ZodOptional<z.ZodString>;
9633
+ truncated: z.ZodOptional<z.ZodBoolean>;
9634
+ }, z.core.$strip>, z.ZodObject<{
9635
+ coverage: z.ZodLiteral<"queried_no_data">;
9636
+ error: z.ZodOptional<z.ZodObject<{
9637
+ message: z.ZodString;
9638
+ source: z.ZodOptional<z.ZodString>;
9639
+ }, z.core.$strip>>;
9640
+ inference_path: z.ZodOptional<z.ZodString>;
9641
+ }, z.core.$strip>, z.ZodObject<{
9642
+ coverage: z.ZodLiteral<"filtered">;
9643
+ filter: z.ZodString;
9644
+ dropped_count: z.ZodNumber;
9645
+ inference_path: z.ZodOptional<z.ZodString>;
9646
+ }, z.core.$strip>, z.ZodObject<{
9647
+ coverage: z.ZodLiteral<"not_loaded">;
9648
+ reason: z.ZodOptional<z.ZodString>;
9649
+ }, z.core.$strip>], "coverage">;
9650
9650
  clinical_development: z.ZodObject<{
9651
9651
  trials: z.ZodDiscriminatedUnion<[z.ZodObject<{
9652
9652
  coverage: z.ZodLiteral<"available">;
@@ -33,6 +33,7 @@ import { BiologicalThemeSchema, KeyReferenceSchema, RunSynthesisSchema, Synthesi
33
33
  import { synthesisAgentPrompt } from "../prompts/synthesis-agent.js";
34
34
  import { composeSystemPrompt } from "../agents/system-prompt.js";
35
35
  import { runDir } from "../workspace/paths.js";
36
+ import { decodeObjectString } from "../lib/zod-issues.js";
36
37
  import { runToTerminal } from "../loop/run-to-terminal.js";
37
38
  import { passthroughStep } from "../loop/run-step.js";
38
39
  import { defineTool } from "../tools/define-tool.js";
@@ -262,17 +263,21 @@ function buildValidateTool(ctx) {
262
263
  "field-by-field synthesis schema is the arg schema of " +
263
264
  "submit_synthesis — this tool deliberately does not restate it. " +
264
265
  "Non-terminal: call as often as needed to iterate toward a clean " +
265
- "synthesis, then submit_synthesis.",
266
+ "synthesis, then submit_synthesis. A candidate that arrives as a " +
267
+ "JSON-encoded string is decoded before the checks run, so the " +
268
+ "result describes the synthesis inside the string.",
266
269
  // Deliberately permissive: a structurally-invalid candidate must reach
267
270
  // `execute` so the model gets a structured {valid:false, issues} result —
268
271
  // including semantic issues — instead of a bare Zod rejection at the loop's
269
272
  // input boundary. `execute` re-parses against RunSynthesisSchema itself.
273
+ // The permissive schema also means the loop's string repair never runs
274
+ // here, so `execute` decodes a JSON-encoded string itself.
270
275
  inputSchema: z.object({
271
276
  synthesis: z.unknown().describe("The candidate synthesis, in any shape. Field-by-field schema: see submit_synthesis."),
272
277
  }),
273
278
  describeCall: "none",
274
279
  execute: async (input) => {
275
- const result = fullyValidate(input.synthesis, ctx);
280
+ const result = fullyValidate(decodeObjectString(input.synthesis), ctx);
276
281
  return ok(result.valid ? { valid: true, issues: [] } : { valid: false, issues: result.issues });
277
282
  },
278
283
  });
@@ -82,3 +82,11 @@ export declare function hintForZodIssue(issue: z.core.$ZodIssue, input: unknown)
82
82
  * is not re-examined — one pass, no recursion — and no input causes a throw.
83
83
  */
84
84
  export declare function repairToolInput(input: unknown, error: z.ZodError): unknown | undefined;
85
+ /**
86
+ * Decode a value that a model sent as a JSON-encoded object string, with or
87
+ * without a wrapper artifact. The decoded object is returned when the string
88
+ * parses to one. Each other value comes back as it is, thus a caller can apply
89
+ * this to an argument of any type. This is the repair that `repairToolInput`
90
+ * applies at the loop boundary, for a tool whose schema accepts the string.
91
+ */
92
+ export declare function decodeObjectString(value: unknown): unknown;
@@ -131,6 +131,19 @@ export function repairToolInput(input, error) {
131
131
  return undefined;
132
132
  }
133
133
  }
134
+ /**
135
+ * Decode a value that a model sent as a JSON-encoded object string, with or
136
+ * without a wrapper artifact. The decoded object is returned when the string
137
+ * parses to one. Each other value comes back as it is, thus a caller can apply
138
+ * this to an argument of any type. This is the repair that `repairToolInput`
139
+ * applies at the loop boundary, for a tool whose schema accepts the string.
140
+ */
141
+ export function decodeObjectString(value) {
142
+ if (typeof value !== "string" || value.length > MAX_REPAIRABLE_STRING_LENGTH)
143
+ return value;
144
+ const parsed = parseAs(value, "object") ?? parseAs(stripWrappers(value).text, "object");
145
+ return parsed === undefined ? value : parsed.value;
146
+ }
134
147
  /**
135
148
  * The measurement of a string that broke its maximum length, or `undefined` for
136
149
  * any other issue.
@@ -1,4 +1,38 @@
1
+ /**
2
+ * Absence policy: Semantic Scholar gives each requested key, and it sends an
3
+ * explicit `null` for an absent value such as `abstract` or `year`. An absent
4
+ * `venue` is the empty string.
5
+ */
6
+ import { z } from "zod";
1
7
  import { type SourceHttpOptions, type SourceHttpResult } from "./http.js";
8
+ export declare const ExactPaperSchema: z.ZodObject<{
9
+ title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
10
+ abstract: z.ZodOptional<z.ZodNullable<z.ZodString>>;
11
+ year: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
12
+ venue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
13
+ citationCount: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
14
+ url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
15
+ authors: z.ZodOptional<z.ZodArray<z.ZodObject<{
16
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
17
+ }, z.core.$strip>>>;
18
+ externalIds: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>>;
19
+ paperId: z.ZodString;
20
+ }, z.core.$loose>;
21
+ export declare const SearchResponseSchema: z.ZodObject<{
22
+ data: z.ZodOptional<z.ZodArray<z.ZodObject<{
23
+ paperId: z.ZodOptional<z.ZodString>;
24
+ title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
25
+ abstract: z.ZodOptional<z.ZodNullable<z.ZodString>>;
26
+ year: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
27
+ venue: z.ZodOptional<z.ZodNullable<z.ZodString>>;
28
+ citationCount: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
29
+ url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
30
+ authors: z.ZodOptional<z.ZodArray<z.ZodObject<{
31
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
32
+ }, z.core.$strip>>>;
33
+ externalIds: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>>;
34
+ }, z.core.$loose>>>;
35
+ }, z.core.$strip>;
2
36
  export interface SemanticScholarPaper {
3
37
  id: string;
4
38
  title?: string;
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Absence policy: Semantic Scholar gives each requested key, and it sends an
3
+ * explicit `null` for an absent value such as `abstract` or `year`. An absent
4
+ * `venue` is the empty string.
5
+ */
1
6
  import { z } from "zod";
2
7
  import { requestJson } from "./http.js";
3
8
  const BASE_URL = "https://api.semanticscholar.org/graph/v1";
@@ -12,15 +17,21 @@ const WirePaperSchema = z
12
17
  citationCount: z.number().nullable().optional(),
13
18
  url: z.string().nullable().optional(),
14
19
  authors: z.array(z.object({ name: z.string().nullable().optional() })).optional(),
15
- externalIds: z.record(z.string(), z.unknown()).nullable().optional(),
20
+ // An external id is a string, but `CorpusId` is a JSON integer on every
21
+ // observed paper. A string-only value type drops it at the map site below.
22
+ externalIds: z
23
+ .record(z.string(), z.union([z.string(), z.number()]))
24
+ .nullable()
25
+ .optional(),
16
26
  })
17
27
  .passthrough();
18
- const ExactPaperSchema = WirePaperSchema.extend({ paperId: z.string() });
19
- const SearchResponseSchema = z.object({ data: z.array(WirePaperSchema).optional() });
28
+ export const ExactPaperSchema = WirePaperSchema.extend({ paperId: z.string() });
29
+ export const SearchResponseSchema = z.object({ data: z.array(WirePaperSchema).optional() });
30
+ /** Give one text form to each external id, thus a numeric `CorpusId` reaches a caller that keys on a string. */
20
31
  function stringExternalIds(value) {
21
32
  if (value === null || value === undefined)
22
33
  return undefined;
23
- const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
34
+ const entries = Object.entries(value).map(([key, raw]) => [key, typeof raw === "string" ? raw : String(raw)]);
24
35
  return entries.length === 0 ? undefined : Object.fromEntries(entries);
25
36
  }
26
37
  function mapPaper(raw) {
@@ -38,9 +38,10 @@ const inputSchema = z
38
38
  .describe("Which ChEMBL lookup to run; each names its params and return fields.\n" +
39
39
  "'compounds' (query + searchType) — molecules by target, name or SMILES: resolve a named compound to its ID and structure, or list " +
40
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" +
41
+ "'drug' (query) — approved drugs by indication or by drug name: 'what treats X?', 'is Y approved, since when?'. → moleculeChemblId, " +
42
+ "preferredName, maxPhase (4 = approved), moleculeType, firstApproval, indication. A disease term reads the curated indication " +
43
+ "registry, highest phase first; a query that matches no disease term reads the approved molecules (max_phase >= 4) instead, and " +
44
+ "their indications come back with them.\n" +
44
45
  "'mechanism' (chemblId, molecule only) — curated mechanism of ONE molecule: 'how does X work?'. → mechanismOfAction, actionType " +
45
46
  "(INHIBITOR, AGONIST, …), targetChemblId + targetName, moleculeChemblId. Curated mainly for clinical/approved molecules, so tool " +
46
47
  "compounds often have none.\n" +
@@ -18,7 +18,6 @@
18
18
  */
19
19
  import type { Logger } from "../../lib/logger.js";
20
20
  import { type DgidbResult } from "../lib/dgidb-client.js";
21
- import { type DrugResult } from "../lib/drugbank-client.js";
22
21
  import { type PharmgkbAnnotation } from "../lib/pharmgkb-client.js";
23
22
  declare const SOURCES: readonly ["dgidb", "drugbank", "pharmgkb"];
24
23
  type Source = (typeof SOURCES)[number];
@@ -30,26 +29,15 @@ interface SourceOutcome {
30
29
  }
31
30
  /**
32
31
  * 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.
32
+ * the description arrives whole or as a preview — a 500-character field across
33
+ * ten records is narrative that a drug-vs-gene question rarely needs.
36
34
  */
37
35
  interface ProjectedDrug {
38
36
  drugbankId: string;
39
37
  name: string;
40
38
  type: string;
41
39
  groups: string[];
42
- categories: string[];
43
- halfLife: string;
44
- targets: DrugResult["targets"];
45
- targetCount: number;
46
- interactions: DrugResult["interactions"];
47
- interactionCount: number;
48
40
  description: string;
49
- indication: string;
50
- pharmacodynamics: string;
51
- mechanismOfAction: string;
52
- toxicity: string;
53
41
  proseTruncated: boolean;
54
42
  }
55
43
  interface DrugGeneOutput {
@@ -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 });