@inflexa-ai/harness 0.13.0 → 0.14.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 (162) hide show
  1. package/dist/agents/conversation-agent.js +23 -16
  2. package/dist/agents/sandbox/cheminformatics-agent.js +1 -1
  3. package/dist/agents/sandbox/chromatin-agent.js +1 -1
  4. package/dist/agents/sandbox/dna-methylation-agent.js +1 -1
  5. package/dist/agents/sandbox/drug-repurposing-agent.js +5 -10
  6. package/dist/agents/sandbox/enrichment-agent.js +1 -1
  7. package/dist/agents/sandbox/genomic-variant-agent.js +1 -11
  8. package/dist/agents/sandbox/immune-profiling-agent.js +1 -1
  9. package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -1
  10. package/dist/agents/sandbox/network-agent.js +1 -1
  11. package/dist/agents/sandbox/pkpd-clinical-response-agent.js +3 -6
  12. package/dist/agents/sandbox/scientific-executor.js +1 -1
  13. package/dist/agents/sandbox/shared.js +7 -13
  14. package/dist/agents/sandbox/translational-safety-agent.js +4 -7
  15. package/dist/agents/sandbox/types.d.ts +5 -5
  16. package/dist/agents/sandbox/types.js +4 -4
  17. package/dist/app/chat-turn.js +4 -1
  18. package/dist/app/message-assembly.d.ts +5 -2
  19. package/dist/app/message-assembly.js +7 -2
  20. package/dist/app/run-activity.d.ts +13 -0
  21. package/dist/app/run-activity.js +51 -0
  22. package/dist/contracts/target-dossier.d.ts +10 -10
  23. package/dist/execution/report-runner.js +1 -1
  24. package/dist/execution/run-event-parts.d.ts +44 -0
  25. package/dist/execution/run-event-parts.js +0 -0
  26. package/dist/execution/run-event-stream.d.ts +91 -0
  27. package/dist/execution/run-event-stream.js +212 -0
  28. package/dist/execution/run-synthesis.js +1 -1
  29. package/dist/index.d.ts +5 -2
  30. package/dist/index.js +20 -1
  31. package/dist/input-sanitization.d.ts +14 -4
  32. package/dist/input-sanitization.js +27 -4
  33. package/dist/lib/storage.js +4 -1
  34. package/dist/loop/budget-exceeded.d.ts +18 -2
  35. package/dist/loop/budget-exceeded.js +49 -2
  36. package/dist/loop/run-agent.d.ts +22 -1
  37. package/dist/loop/run-agent.js +50 -5
  38. package/dist/loop/run-to-terminal.d.ts +0 -2
  39. package/dist/loop/run-to-terminal.js +8 -1
  40. package/dist/memory/ai-sdk-message-storage.d.ts +37 -4
  41. package/dist/memory/ai-sdk-message-storage.js +45 -4
  42. package/dist/memory/content-to-cortex.js +19 -3
  43. package/dist/memory/message-backfill.js +1 -1
  44. package/dist/memory/thread-history.js +34 -8
  45. package/dist/prompts/conversation.d.ts +1 -1
  46. package/dist/prompts/conversation.js +15 -6
  47. package/dist/prompts/literature-reviewer.d.ts +1 -1
  48. package/dist/prompts/literature-reviewer.js +16 -14
  49. package/dist/prompts/planner.js +15 -20
  50. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  51. package/dist/prompts/sandbox/drug-repurposing-agent.js +31 -22
  52. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  53. package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -10
  54. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  55. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +8 -7
  56. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  57. package/dist/prompts/sandbox/translational-safety-agent.js +20 -14
  58. package/dist/prompts/synthesis-agent.d.ts +1 -1
  59. package/dist/prompts/synthesis-agent.js +7 -7
  60. package/dist/providers/errors.d.ts +7 -0
  61. package/dist/providers/errors.js +88 -3
  62. package/dist/runtime/pools.d.ts +15 -1
  63. package/dist/runtime/pools.js +15 -1
  64. package/dist/sandbox/sandbox-step-translate.d.ts +15 -2
  65. package/dist/sandbox/sandbox-step-translate.js +17 -2
  66. package/dist/schemas/workflow-state.d.ts +2 -2
  67. package/dist/schemas/workflow-state.js +2 -2
  68. package/dist/state/data-profile.d.ts +53 -1
  69. package/dist/state/data-profile.js +66 -6
  70. package/dist/state/index.d.ts +3 -3
  71. package/dist/state/index.js +2 -2
  72. package/dist/state/init.js +37 -2
  73. package/dist/state/runs.d.ts +39 -0
  74. package/dist/state/runs.js +103 -0
  75. package/dist/tasks/data-profile-activity.d.ts +87 -0
  76. package/dist/tasks/data-profile-activity.js +56 -0
  77. package/dist/tasks/data-profile.js +66 -4
  78. package/dist/tools/bio/chembl.d.ts +1 -1
  79. package/dist/tools/bio/chembl.js +51 -54
  80. package/dist/tools/bio/comptox.js +35 -33
  81. package/dist/tools/bio/drug-gene-interactions.d.ts +76 -0
  82. package/dist/tools/bio/drug-gene-interactions.js +175 -0
  83. package/dist/tools/bio/gene-disease-evidence.d.ts +61 -0
  84. package/dist/tools/bio/gene-disease-evidence.js +187 -0
  85. package/dist/tools/bio/gene-preclinical-profile.d.ts +58 -0
  86. package/dist/tools/bio/gene-preclinical-profile.js +125 -0
  87. package/dist/tools/bio/index.d.ts +23 -16
  88. package/dist/tools/bio/index.js +29 -22
  89. package/dist/tools/bio/keys.d.ts +16 -9
  90. package/dist/tools/bio/keys.js +22 -13
  91. package/dist/tools/bio/lookup-annotation.d.ts +48 -0
  92. package/dist/tools/bio/lookup-annotation.js +173 -0
  93. package/dist/tools/bio/opentargets.d.ts +7 -11
  94. package/dist/tools/bio/opentargets.js +24 -35
  95. package/dist/tools/bio/pubchem.js +6 -4
  96. package/dist/tools/bio/pubmed.d.ts +15 -7
  97. package/dist/tools/bio/pubmed.js +79 -20
  98. package/dist/tools/bio/search-clinical-trials.d.ts +20 -1
  99. package/dist/tools/bio/search-clinical-trials.js +26 -5
  100. package/dist/tools/bio/search-faers.js +9 -3
  101. package/dist/tools/bio/search-gene.d.ts +0 -1
  102. package/dist/tools/bio/search-gene.js +4 -5
  103. package/dist/tools/bio/search-geo-datasets.js +10 -4
  104. package/dist/tools/bio/search-interactions.d.ts +8 -4
  105. package/dist/tools/bio/search-interactions.js +28 -12
  106. package/dist/tools/bio/target-safety.d.ts +59 -0
  107. package/dist/tools/bio/target-safety.js +191 -0
  108. package/dist/tools/execute-analysis.d.ts +1 -0
  109. package/dist/tools/execute-analysis.js +5 -5
  110. package/dist/tools/lib/dgidb-client.d.ts +49 -0
  111. package/dist/tools/lib/dgidb-client.js +168 -0
  112. package/dist/tools/lib/disgenet-client.d.ts +56 -0
  113. package/dist/tools/lib/disgenet-client.js +59 -0
  114. package/dist/tools/lib/drugbank-client.d.ts +83 -0
  115. package/dist/tools/lib/drugbank-client.js +95 -0
  116. package/dist/tools/lib/ensembl-client.d.ts +4 -1
  117. package/dist/tools/lib/ensembl-client.js +5 -3
  118. package/dist/tools/lib/gwas-catalog-client.d.ts +60 -0
  119. package/dist/tools/lib/gwas-catalog-client.js +136 -0
  120. package/dist/tools/lib/pharmgkb-client.d.ts +22 -0
  121. package/dist/tools/lib/pharmgkb-client.js +32 -0
  122. package/dist/tools/lib/pubmed-client.d.ts +57 -4
  123. package/dist/tools/lib/pubmed-client.js +57 -3
  124. package/dist/tools/lib/string-client.d.ts +25 -4
  125. package/dist/tools/lib/string-client.js +22 -5
  126. package/dist/tools/research/generate-plan.d.ts +16 -9
  127. package/dist/tools/research/generate-plan.js +115 -69
  128. package/dist/tools/research/inspect-data-profile.d.ts +31 -6
  129. package/dist/tools/research/inspect-data-profile.js +23 -7
  130. package/dist/tools/research/inspect-run.d.ts +33 -15
  131. package/dist/tools/research/inspect-run.js +155 -66
  132. package/dist/tools/research/literature-reviewer.js +7 -11
  133. package/dist/workflows/execute-analysis.d.ts +63 -0
  134. package/dist/workflows/execute-analysis.js +73 -2
  135. package/dist/workflows/sandbox-step.js +1 -11
  136. package/dist/workflows/target-assessment/decisions/index.d.ts +1 -1
  137. package/dist/workflows/target-assessment/phase4-assemble.d.ts +1 -1
  138. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +2 -2
  139. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
  140. package/package.json +1 -1
  141. package/dist/tools/bio/check-safety-panel.d.ts +0 -35
  142. package/dist/tools/bio/check-safety-panel.js +0 -76
  143. package/dist/tools/bio/get-impc-ko-profile.d.ts +0 -9
  144. package/dist/tools/bio/get-impc-ko-profile.js +0 -23
  145. package/dist/tools/bio/lookup-go-term.d.ts +0 -31
  146. package/dist/tools/bio/lookup-go-term.js +0 -116
  147. package/dist/tools/bio/search-bgee-expression.d.ts +0 -10
  148. package/dist/tools/bio/search-bgee-expression.js +0 -30
  149. package/dist/tools/bio/search-clinvar.d.ts +0 -13
  150. package/dist/tools/bio/search-clinvar.js +0 -28
  151. package/dist/tools/bio/search-dgidb.d.ts +0 -53
  152. package/dist/tools/bio/search-dgidb.js +0 -213
  153. package/dist/tools/bio/search-disgenet.d.ts +0 -30
  154. package/dist/tools/bio/search-disgenet.js +0 -84
  155. package/dist/tools/bio/search-drugbank.d.ts +0 -38
  156. package/dist/tools/bio/search-drugbank.js +0 -109
  157. package/dist/tools/bio/search-gwas-catalog.d.ts +0 -33
  158. package/dist/tools/bio/search-gwas-catalog.js +0 -172
  159. package/dist/tools/bio/search-pathway.d.ts +0 -12
  160. package/dist/tools/bio/search-pathway.js +0 -32
  161. package/dist/tools/bio/search-pharmgkb.d.ts +0 -17
  162. package/dist/tools/bio/search-pharmgkb.js +0 -52
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * opentargets — one tool over the Open Targets Platform: target-disease
3
- * association evidence (from a gene or from a disease) and the target's
4
- * curated safety liabilities.
3
+ * association evidence, read from a gene or from a disease.
4
+ *
5
+ * The curated safety liabilities this tool used to serve now belong to
6
+ * `target_safety`, which pairs them with the local secondary-pharmacology panel
7
+ * and resolves the Ensembl id they need.
5
8
  *
6
9
  * The input is a flat object with an `action` discriminator — not a
7
10
  * `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
@@ -15,22 +18,15 @@
15
18
  * result rather than an error. Naming the two identifiers apart is what makes
16
19
  * that contract visible at the call site.
17
20
  */
18
- import { type Association, type SafetyLiability, type TargetInfo } from "../lib/opentargets-client.js";
21
+ import { type Association, type TargetInfo } from "../lib/opentargets-client.js";
19
22
  type OpenTargetsOutput = {
20
23
  targetInfo: TargetInfo | null;
21
24
  associations: Association[];
22
25
  } | {
23
26
  associations: Association[];
24
- } | {
25
- found: false;
26
- ensemblId: string;
27
- } | {
28
- found: true;
29
- targetSymbol: string;
30
- safetyLiabilities: SafetyLiability[];
31
27
  };
32
28
  export declare const openTargetsTool: import("../define-tool.js").Tool<{
33
- action: "target" | "disease" | "safety";
29
+ action: "target" | "disease";
34
30
  ensemblId?: string | undefined;
35
31
  efoId?: string | undefined;
36
32
  limit?: number | undefined;
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * opentargets — one tool over the Open Targets Platform: target-disease
3
- * association evidence (from a gene or from a disease) and the target's
4
- * curated safety liabilities.
3
+ * association evidence, read from a gene or from a disease.
4
+ *
5
+ * The curated safety liabilities this tool used to serve now belong to
6
+ * `target_safety`, which pairs them with the local secondary-pharmacology panel
7
+ * and resolves the Ensembl id they need.
5
8
  *
6
9
  * The input is a flat object with an `action` discriminator — not a
7
10
  * `z.discriminatedUnion`, which `defineTool` rejects (model tool calling needs
@@ -18,32 +21,30 @@
18
21
  import { ok } from "neverthrow";
19
22
  import { z } from "zod";
20
23
  import { defineTool } from "../define-tool.js";
21
- import { getTargetSafetyLiabilities, searchDiseaseAssociations, searchTargetAssociations, } from "../lib/opentargets-client.js";
24
+ import { searchDiseaseAssociations, searchTargetAssociations } from "../lib/opentargets-client.js";
25
+ const DEFAULT_LIMIT = 10;
22
26
  const inputSchema = z
23
27
  .object({
24
28
  action: z
25
- .enum(["target", "disease", "safety"])
29
+ .enum(["target", "disease"])
26
30
  .describe("'target' (needs ensemblId) — the diseases associated with the gene: targetInfo (approvedSymbol, approvedName, " +
27
31
  "tractability across small molecule / antibody / other modalities) plus associations[], each with an overall score and " +
28
32
  "its per-datatype breakdown — genetic association, known drug, literature, animal model, somatic mutation. " +
29
33
  "'disease' (needs efoId) — the targets ranked for that disease; each association carries targetId/targetSymbol/targetName " +
30
- "and the same score breakdown. " +
31
- "'safety' (needs ensemblId) — the target's curated safety liabilities: found: true with targetSymbol and " +
32
- "safetyLiabilities[] { event, biosamples (affected tissues), effects (direction of effect), source }. This is TARGET-level, " +
33
- "mechanism-based safety — for the post-market adverse events of a specific marketed DRUG use search_faers instead."),
34
- ensemblId: z.string().optional().describe("Ensembl gene ID, e.g. ENSG00000141510 (TP53). Required for action 'target' and 'safety'."),
34
+ "and the same score breakdown."),
35
+ ensemblId: z.string().optional().describe("Ensembl gene ID, e.g. ENSG00000141510 (TP53). Required for action 'target'."),
35
36
  efoId: z.string().optional().describe("EFO disease ID, e.g. EFO_0000311. Required for action 'disease'."),
36
37
  limit: z
37
38
  .number()
38
39
  .int()
39
40
  .min(1)
40
41
  .max(100)
41
- .default(25)
42
42
  .optional()
43
- .describe("Max associations to return (default 25, max 100), ordered by descending association score. Ignored by 'safety'."),
43
+ .describe(`Max associations to return (default ${DEFAULT_LIMIT}, max 100), ordered by DESCENDING association score. The default is the top ` +
44
+ `${DEFAULT_LIMIT}, which is where the evidence is; raise it only when you are surveying the long tail rather than ranking.`),
44
45
  })
45
- .refine((d) => d.action === "disease" || (d.ensemblId !== undefined && d.ensemblId.trim().length > 0), {
46
- message: "ensemblId is required when action is 'target' or 'safety' — Open Targets accepts only Ensembl gene IDs (ENSG…), " +
46
+ .refine((d) => d.action !== "target" || (d.ensemblId !== undefined && d.ensemblId.trim().length > 0), {
47
+ message: "ensemblId is required when action is 'target' — Open Targets accepts only Ensembl gene IDs (ENSG…), " +
47
48
  "never gene symbols. Resolve the symbol to an ENSG id with search_gene, then retry.",
48
49
  path: ["ensemblId"],
49
50
  })
@@ -53,37 +54,25 @@ const inputSchema = z
53
54
  });
54
55
  export const openTargetsTool = defineTool({
55
56
  id: "opentargets",
56
- description: "Query the Open Targets Platform — the preferred FIRST call for target assessment, since one 'target' query yields the " +
57
- "genetic evidence, tractability, and drug landscape together, and 'safety' adds the mechanism-based liabilities. See the " +
58
- "action parameter for what each mode needs and returns.\n" +
59
- "IDENTIFIERS ONLY: it accepts an Ensembl gene ID (action 'target'/'safety') or an EFO disease ID (action 'disease'). A bare " +
60
- "gene symbol or a free-text disease name silently returns an EMPTY result rather than an error — resolve a symbol to its ENSG " +
61
- "id with search_gene first.\n" +
62
- "NO-DATA IS FINAL — do not retry the same id. Empty associations mean no evidence (or an unresolvable id); safety found: false " +
63
- "means no Open Targets record for that Ensembl id (usually a wrong id); safety found: true with an empty safetyLiabilities " +
64
- "array means no curated liability.",
57
+ description: "Query the Open Targets Platform — the preferred FIRST call for target assessment, since one 'target' query yields the genetic evidence, " +
58
+ "tractability, and drug landscape together, already scored and ranked. See the action parameter for what each mode needs and returns.\n" +
59
+ "It is the integrated view; the underlying records live elsewhere. For the raw genetic evidence (individual SNPs, effect sizes, GDA scores, " +
60
+ "variant pathogenicity) use gene_disease_evidence. For this target's mechanism-based safety liabilities use target_safety.\n" +
61
+ "IDENTIFIERS ONLY: it accepts an Ensembl gene ID (action 'target') or an EFO disease ID (action 'disease'). A bare gene symbol or a free-text " +
62
+ "disease name silently returns an EMPTY result rather than an error — resolve a symbol to its ENSG id with search_gene first.\n" +
63
+ "NO-DATA IS FINAL — do not retry the same id. Empty associations mean no evidence, or an unresolvable id.",
65
64
  inputSchema,
66
65
  execute: async (input) => {
66
+ const limit = input.limit ?? DEFAULT_LIMIT;
67
67
  switch (input.action) {
68
68
  case "target": {
69
- const targetInfo = await searchTargetAssociations(input.ensemblId, input.limit);
69
+ const targetInfo = await searchTargetAssociations(input.ensemblId, limit);
70
70
  return ok({ targetInfo, associations: targetInfo?.associations ?? [] });
71
71
  }
72
72
  case "disease": {
73
- const associations = await searchDiseaseAssociations(input.efoId, input.limit);
73
+ const associations = await searchDiseaseAssociations(input.efoId, limit);
74
74
  return ok({ associations });
75
75
  }
76
- case "safety": {
77
- const result = await getTargetSafetyLiabilities(input.ensemblId);
78
- // "Target not found" is an expected outcome — a data variant, not an error.
79
- if (!result)
80
- return ok({ found: false, ensemblId: input.ensemblId });
81
- return ok({
82
- found: true,
83
- targetSymbol: result.targetSymbol,
84
- safetyLiabilities: result.safetyLiabilities,
85
- });
86
- }
87
76
  }
88
77
  },
89
78
  });
@@ -44,14 +44,16 @@ const inputSchema = z
44
44
  activeOnly: z
45
45
  .boolean()
46
46
  .optional()
47
- .describe("Action 'assays' only. Default false (all outcomes). Set true to keep only rows whose activityOutcome is 'Active'."),
47
+ .describe("Action 'assays' only. Default TRUE — only rows whose activityOutcome is 'Active'. A widely screened compound has thousands of " +
48
+ "inactive rows and they answer nothing on their own; set false only when a LOW hit rate is the finding you are after (selectivity, " +
49
+ "or confirming a compound was tested and came back clean)."),
48
50
  limit: z
49
51
  .number()
50
52
  .int()
51
53
  .min(1)
52
54
  .max(500)
53
55
  .optional()
54
- .describe("Action 'assays' only. Max assay records to return (default 50, max 500). Applied after the activeOnly filter."),
56
+ .describe("Action 'assays' only. Max assay records to return (default 25, max 500). Applied after the activeOnly filter."),
55
57
  })
56
58
  .refine((d) => d.action !== "compound" || (d.query !== undefined && d.query.trim().length > 0), {
57
59
  message: "query is required when action is 'compound' — the identifier to resolve (a name, SMILES, InChI, InChIKey, or CID)",
@@ -83,8 +85,8 @@ export const pubchemTool = defineTool({
83
85
  case "assays":
84
86
  return ok({
85
87
  assays: await fetchPubchemAssays(input.cid, {
86
- activeOnly: input.activeOnly ?? false,
87
- limit: input.limit ?? 50,
88
+ activeOnly: input.activeOnly ?? true,
89
+ limit: input.limit ?? 25,
88
90
  }),
89
91
  });
90
92
  }
@@ -16,24 +16,28 @@
16
16
  * turns PMIDs into abstracts + the PMC id, fulltext turns a PMC id into the
17
17
  * article body — which is why they share a tool: the identifier for each step
18
18
  * is read off the previous step's result.
19
+ *
20
+ * Every step is bounded, because this is the tool most able to flood a context:
21
+ * an author list can run to hundreds of names, and one research article parses
22
+ * to 40–80k characters — more than the entire tool surface costs to attach.
23
+ * `details` trims the two unbounded list fields; `fulltext` admits whole
24
+ * sections up to a character budget and always returns the article's full
25
+ * section outline so a follow-up can name what it wants.
19
26
  */
20
- import type { ArticleSection } from "../lib/ncbi-utils.js";
21
- import { type ArticleDetail, type PubMedSummary } from "../lib/pubmed-client.js";
27
+ import { type BoundedArticleDetail, type BoundedFullText, type PubMedSummary } from "../lib/pubmed-client.js";
22
28
  type PubMedOutput = {
23
29
  totalFound: number;
24
30
  results: PubMedSummary[];
25
31
  } | {
26
- articles: ArticleDetail[];
32
+ articles: BoundedArticleDetail[];
27
33
  notFound: string[];
28
34
  } | {
29
35
  pmcId: string;
30
36
  available: false;
31
- } | {
37
+ } | ({
32
38
  pmcId: string;
33
39
  available: true;
34
- fullText: string;
35
- sections: ArticleSection[];
36
- };
40
+ } & BoundedFullText);
37
41
  export declare function createPubMedTool(deps: {
38
42
  ncbiApiKey?: string;
39
43
  }): import("../define-tool.js").Tool<{
@@ -46,6 +50,10 @@ export declare function createPubMedTool(deps: {
46
50
  to: string;
47
51
  } | undefined;
48
52
  pmids?: string[] | undefined;
53
+ maxAuthors?: number | undefined;
54
+ maxMeshTerms?: number | undefined;
49
55
  pmcId?: string | undefined;
56
+ sections?: string[] | undefined;
57
+ maxChars?: number | undefined;
50
58
  }, PubMedOutput>;
51
59
  export {};
@@ -16,27 +16,42 @@
16
16
  * turns PMIDs into abstracts + the PMC id, fulltext turns a PMC id into the
17
17
  * article body — which is why they share a tool: the identifier for each step
18
18
  * is read off the previous step's result.
19
+ *
20
+ * Every step is bounded, because this is the tool most able to flood a context:
21
+ * an author list can run to hundreds of names, and one research article parses
22
+ * to 40–80k characters — more than the entire tool surface costs to attach.
23
+ * `details` trims the two unbounded list fields; `fulltext` admits whole
24
+ * sections up to a character budget and always returns the article's full
25
+ * section outline so a follow-up can name what it wants.
19
26
  */
20
27
  import { ok } from "neverthrow";
21
28
  import { z } from "zod";
22
29
  import { defineTool } from "../define-tool.js";
23
- import { getArticleDetails, getArticleFullText, searchPubmed } from "../lib/pubmed-client.js";
30
+ import { boundArticleDetails, getArticleDetails, getArticleFullText, searchPubmed, } from "../lib/pubmed-client.js";
31
+ const DEFAULTS = { searchResults: 10, maxAuthors: 5, maxMeshTerms: 10, maxChars: 12_000 };
24
32
  const inputSchema = z
25
33
  .object({
26
34
  action: z
27
35
  .enum(["search", "details", "fulltext"])
28
36
  .describe("'search' (needs query) — returns totalFound plus, per hit, PMID, title, journal, year and an author summary. " +
29
- "'details' (needs pmids, max 20 per call) — each article's full abstract, authors, journal, year, DOI, MeSH terms, and " +
30
- "pmcId (non-null only when open-access full text exists in PubMed Central). " +
31
- "'fulltext' (needs pmcId, taken from a 'details' result) — the body of ONE open-access PMC article as plain text with " +
32
- "section headers preserved; available: false when the article is not open-access."),
37
+ "'details' (needs pmids, max 20 per call) — each article's full abstract, journal, year, DOI, pmcId (non-null only when open-access " +
38
+ "full text exists in PubMed Central), plus the leading authors and MeSH terms with authorCount / meshTermCount giving the true totals. " +
39
+ "'fulltext' (needs pmcId, taken from a 'details' result) — the body of ONE open-access PMC article as plain text with section headers " +
40
+ "preserved, trimmed to maxChars; `outline` lists every section with its size and whether it came back, and truncated says whether " +
41
+ "anything was left out. available: false when the article is not open-access."),
33
42
  query: z
34
43
  .string()
35
44
  .optional()
36
45
  .describe("Required for action 'search'. PubMed query syntax — MeSH terms (\"breast neoplasms\"[MeSH]), field tags " +
37
46
  '([Title/Abstract], [Author], [Gene]), Boolean AND/OR/NOT. Example: "BRCA1"[Gene] AND "drug resistance"[Title/Abstract].'),
38
- maxResults: z.number().int().min(1).max(50).default(10).optional().describe("action 'search': max results, 1–50 (default 10)."),
39
- sort: z.enum(["relevance", "date"]).default("relevance").optional().describe("action 'search': sort order (default \"relevance\")."),
47
+ maxResults: z
48
+ .number()
49
+ .int()
50
+ .min(1)
51
+ .max(50)
52
+ .optional()
53
+ .describe(`action 'search': max results, 1–50 (default ${DEFAULTS.searchResults}). totalFound reports the true match count regardless.`),
54
+ sort: z.enum(["relevance", "date"]).optional().describe("action 'search': sort order (default \"relevance\")."),
40
55
  dateRange: z
41
56
  .object({
42
57
  from: z.string().describe("Start date in YYYY/MM/DD format"),
@@ -49,8 +64,36 @@ const inputSchema = z
49
64
  .min(1)
50
65
  .max(20)
51
66
  .optional()
52
- .describe("Required for action 'details'. Non-empty array of PMID strings, max 20 — batch them, do not call once per PMID."),
67
+ .describe("Required for action 'details'. Non-empty array of PMID strings, max 20 — batch them, do not call once per PMID. Each article returns a " +
68
+ "FULL abstract, so 20 is a large result; pass the handful you actually intend to read."),
69
+ maxAuthors: z
70
+ .number()
71
+ .int()
72
+ .min(0)
73
+ .max(100)
74
+ .optional()
75
+ .describe(`action 'details': leading authors kept per article (default ${DEFAULTS.maxAuthors}, 0 for none); authorCount gives the true total.`),
76
+ maxMeshTerms: z
77
+ .number()
78
+ .int()
79
+ .min(0)
80
+ .max(100)
81
+ .optional()
82
+ .describe(`action 'details': MeSH terms kept per article (default ${DEFAULTS.maxMeshTerms}, 0 for none); meshTermCount gives the true total.`),
53
83
  pmcId: z.string().optional().describe("Required for action 'fulltext'. A PMC ID (e.g. \"PMC1234567\") copied from a 'details' result — not a PMID."),
84
+ sections: z
85
+ .array(z.string())
86
+ .optional()
87
+ .describe("action 'fulltext': case-insensitive substring filter on section headings, e.g. ['methods','results']. Omit first to get the outline, " +
88
+ "then name what you need — far cheaper than raising maxChars."),
89
+ maxChars: z
90
+ .number()
91
+ .int()
92
+ .min(500)
93
+ .max(200_000)
94
+ .optional()
95
+ .describe(`action 'fulltext': character budget across returned section bodies (default ${DEFAULTS.maxChars}). Sections are admitted whole, in ` +
96
+ "document order, until the budget would be exceeded."),
54
97
  })
55
98
  .refine((d) => d.action !== "search" || (d.query !== undefined && d.query.trim().length > 0), {
56
99
  message: "query is required when action is 'search'",
@@ -63,6 +106,14 @@ const inputSchema = z
63
106
  .refine((d) => d.action !== "fulltext" || (d.pmcId !== undefined && d.pmcId.trim().length > 0), {
64
107
  message: "pmcId is required when action is 'fulltext' — copy it from the pmcId field of a 'details' result; a PMID is not a PMC ID",
65
108
  path: ["pmcId"],
109
+ })
110
+ .refine((d) => d.action === "fulltext" || (d.sections === undefined && d.maxChars === undefined), {
111
+ message: "sections and maxChars apply to action 'fulltext' only",
112
+ path: ["action"],
113
+ })
114
+ .refine((d) => d.action === "details" || (d.maxAuthors === undefined && d.maxMeshTerms === undefined), {
115
+ message: "maxAuthors and maxMeshTerms apply to action 'details' only",
116
+ path: ["action"],
66
117
  });
67
118
  export function createPubMedTool(deps) {
68
119
  return defineTool({
@@ -70,29 +121,37 @@ export function createPubMedTool(deps) {
70
121
  description: "Search and read the biomedical literature via PubMed / PubMed Central (NCBI E-utilities). The three actions form a chain " +
71
122
  "— search, then details on the relevant hits, then fulltext; see the action parameter for what each needs and returns. Read " +
72
123
  "fulltext sparingly — only where the 'details' abstract is not enough, and only for articles that have a pmcId (open-access). " +
124
+ "A full article is larger than everything else this tool returns combined, so it comes back trimmed to a character budget: take the " +
125
+ "`outline` from the first call and re-request the specific `sections` you need rather than raising maxChars. " +
73
126
  "available: false is an expected outcome, not an error — do not retry it.",
74
127
  inputSchema,
75
128
  execute: async (input) => {
76
129
  switch (input.action) {
77
130
  case "search":
78
131
  return ok(await searchPubmed(deps.ncbiApiKey, input.query, {
79
- maxResults: input.maxResults,
80
- sort: input.sort,
81
- dateRange: input.dateRange,
132
+ maxResults: input.maxResults ?? DEFAULTS.searchResults,
133
+ ...(input.sort ? { sort: input.sort } : {}),
134
+ ...(input.dateRange ? { dateRange: input.dateRange } : {}),
82
135
  }));
83
- case "details":
84
- return ok(await getArticleDetails(deps.ncbiApiKey, input.pmids));
136
+ case "details": {
137
+ const { articles, notFound } = await getArticleDetails(deps.ncbiApiKey, input.pmids);
138
+ return ok({
139
+ articles: boundArticleDetails(articles, {
140
+ maxAuthors: input.maxAuthors ?? DEFAULTS.maxAuthors,
141
+ maxMeshTerms: input.maxMeshTerms ?? DEFAULTS.maxMeshTerms,
142
+ }),
143
+ notFound,
144
+ });
145
+ }
85
146
  case "fulltext": {
86
- const parsed = await getArticleFullText(deps.ncbiApiKey, input.pmcId);
147
+ const parsed = await getArticleFullText(deps.ncbiApiKey, input.pmcId, {
148
+ ...(input.sections ? { sections: input.sections } : {}),
149
+ maxChars: input.maxChars ?? DEFAULTS.maxChars,
150
+ });
87
151
  // "Not open-access" is an expected outcome — a data variant, not an error.
88
152
  if (!parsed)
89
153
  return ok({ pmcId: input.pmcId, available: false });
90
- return ok({
91
- pmcId: input.pmcId,
92
- available: true,
93
- fullText: parsed.fullText,
94
- sections: parsed.sections,
95
- });
154
+ return ok({ pmcId: input.pmcId, available: true, ...parsed });
96
155
  }
97
156
  }
98
157
  },
@@ -4,9 +4,28 @@
4
4
  export declare const searchClinicalTrialsTool: import("../define-tool.js").Tool<{
5
5
  query: string;
6
6
  limit: number;
7
+ includeSummaries: boolean;
7
8
  phase?: "EARLY_PHASE1" | "PHASE1" | "PHASE2" | "PHASE3" | "PHASE4" | undefined;
8
9
  status?: "RECRUITING" | "ACTIVE_NOT_RECRUITING" | "COMPLETED" | "NOT_YET_RECRUITING" | undefined;
9
10
  }, {
10
11
  totalFound: number;
11
- trials: import("../lib/clinical-trials-client.js").ClinicalTrial[];
12
+ trials: import("../lib/clinical-trials-client.js").ClinicalTrial[] | {
13
+ briefSummary: string | null;
14
+ summaryTruncated: boolean;
15
+ nctId: string;
16
+ title: string;
17
+ officialTitle: string | null;
18
+ status: string;
19
+ phase: string | null;
20
+ studyType: string | null;
21
+ primaryPurpose: string | null;
22
+ conditions: string[];
23
+ interventions: string[];
24
+ interventionDetails: import("../lib/clinical-trials-client.js").ClinicalTrialIntervention[];
25
+ enrollmentCount: number | null;
26
+ startDate: string | null;
27
+ primaryCompletionDate: string | null;
28
+ whyStopped: string | null;
29
+ sponsor: string | null;
30
+ }[];
12
31
  }>;
@@ -5,11 +5,13 @@ import { ok } from "neverthrow";
5
5
  import { z } from "zod";
6
6
  import { defineTool } from "../define-tool.js";
7
7
  import { searchTrials } from "../lib/clinical-trials-client.js";
8
+ /** briefSummary preview length when `includeSummaries` is off. */
9
+ const SUMMARY_PREVIEW_CHARS = 300;
8
10
  export const searchClinicalTrialsTool = defineTool({
9
11
  id: "search_clinical_trials",
10
12
  description: "Search ClinicalTrials.gov to map the clinical development landscape for a target, drug, or indication — what is being tried, at what phase, by whom, and what stopped. " +
11
- "Returns totalFound plus per trial: NCT ID, title, status, phase, conditions, interventions, enrollment, start and completion dates, sponsor, brief summary, and whyStopped " +
12
- "(the termination reason, when there is one). " +
13
+ "Returns totalFound plus per trial: NCT ID, title, status, phase, conditions, interventions, enrollment, start and completion dates, sponsor, whyStopped " +
14
+ "(the termination reason, when there is one) and a briefSummary preview — set includeSummaries for the full protocol prose. " +
13
15
  "`query` is free text matched across the whole study record, so a gene symbol, a drug name, a condition, or an NCT ID all work. " +
14
16
  "`phase` and `status` are optional server-side filters — omit them to see the full landscape; totalFound reports the true match count even when it exceeds the page returned. " +
15
17
  "An empty trials array is a valid 'nothing matched' (often an over-narrow filter) — do not retry the identical query.",
@@ -23,10 +25,29 @@ export const searchClinicalTrialsTool = defineTool({
23
25
  .enum(["RECRUITING", "ACTIVE_NOT_RECRUITING", "COMPLETED", "NOT_YET_RECRUITING"])
24
26
  .optional()
25
27
  .describe("Optional recruitment-status filter. Omit for all statuses — note TERMINATED/WITHDRAWN trials are not selectable here and appear only in an unfiltered search."),
26
- limit: z.number().int().min(1).max(50).default(20).describe("Max trials to return (default 20, max 50)."),
28
+ limit: z
29
+ .number()
30
+ .int()
31
+ .min(1)
32
+ .max(50)
33
+ .default(10)
34
+ .describe("Max trials to return (default 10, max 50). totalFound reports the true match count regardless."),
35
+ includeSummaries: z
36
+ .boolean()
37
+ .default(false)
38
+ .describe("Default FALSE — briefSummary comes back as a 300-character preview and detailedDescription is omitted, which is enough to map a " +
39
+ "landscape. Set true for the full protocol prose, and narrow `limit` when you do: a full summary runs to several thousand characters " +
40
+ "per trial."),
27
41
  }),
28
- execute: async ({ query, phase, status, limit = 20 }) => {
42
+ execute: async ({ query, phase, status, limit = 10, includeSummaries = false }) => {
29
43
  const result = await searchTrials(query, { phase, status, limit });
30
- return ok({ totalFound: result.totalFound, trials: result.trials });
44
+ const trials = includeSummaries
45
+ ? result.trials
46
+ : result.trials.map(({ detailedDescription: _detailedDescription, ...trial }) => ({
47
+ ...trial,
48
+ briefSummary: trial.briefSummary === null ? null : trial.briefSummary.slice(0, SUMMARY_PREVIEW_CHARS),
49
+ summaryTruncated: (trial.briefSummary?.length ?? 0) > SUMMARY_PREVIEW_CHARS,
50
+ }));
51
+ return ok({ totalFound: result.totalFound, trials });
31
52
  },
32
53
  });
@@ -10,14 +10,20 @@ export const searchFaersTool = defineTool({
10
10
  description: "Search FDA FAERS for post-market adverse-event reports on one marketed drug — the real-world safety signal for a specific molecule. " +
11
11
  "Returns totalReports and adverseEvents[]: { reaction (MedDRA preferred term), count }, most-reported first. " +
12
12
  "These are spontaneous report counts, NOT incidence rates: there is no denominator and reporting is heavily biased, so use them to rank signals, never to state a rate. " +
13
- "Matching is on the openFDA GENERIC name only — 'imatinib' works, the brand name 'Gleevec' does not. For mechanism-based liabilities of a target (rather than a drug), use opentargets({action:\"safety\"}). " +
13
+ "Matching is on the openFDA GENERIC name only — 'imatinib' works, the brand name 'Gleevec' does not. For mechanism-based liabilities of a TARGET (rather than a marketed drug), use target_safety. " +
14
14
  "An empty adverseEvents array is valid no-data (drug absent from FAERS under that generic name) — do not retry.",
15
15
  inputSchema: z.object({
16
16
  drugName: z.string().describe("Generic (INN) drug name, e.g. 'imatinib', 'pembrolizumab'. Brand names do not match."),
17
- limit: z.number().int().min(1).max(100).default(25).describe("Max distinct adverse-reaction terms to return (default 25, max 100)."),
17
+ limit: z
18
+ .number()
19
+ .int()
20
+ .min(1)
21
+ .max(100)
22
+ .default(15)
23
+ .describe("Max distinct adverse-reaction terms to return, most-reported first (default 15, max 100). totalReports covers the whole drug regardless."),
18
24
  serious: z.boolean().default(false).describe("When true, count only reports flagged serious (death, hospitalization, life-threatening, disabling)."),
19
25
  }),
20
- execute: async ({ drugName, limit = 25, serious = false }) => {
26
+ execute: async ({ drugName, limit = 15, serious = false }) => {
21
27
  const result = await getFaersByDrug(drugName, { limit, serious });
22
28
  return ok({
23
29
  totalReports: result.totalReports,
@@ -4,7 +4,6 @@
4
4
  export declare const searchGeneTool: import("../define-tool.js").Tool<{
5
5
  symbols: string[];
6
6
  species: string;
7
- expand: boolean;
8
7
  }, {
9
8
  genes: import("../lib/ensembl-client.js").GeneInfo[];
10
9
  notFound: string[];
@@ -7,13 +7,12 @@ import { defineTool } from "../define-tool.js";
7
7
  import { lookupGenes } from "../lib/ensembl-client.js";
8
8
  export const searchGeneTool = defineTool({
9
9
  id: "search_gene",
10
- description: "Look up gene information from Ensembl by gene symbol. " +
11
- "Returns Ensembl IDs, genomic coordinates, biotype, and descriptions. " +
12
- "Supports batch lookups of up to 200 symbols at once.",
10
+ description: "Resolve gene symbols against Ensembl — the identifier step other tools depend on. Per symbol: Ensembl gene ID (ENSG…), coordinates, strand, " +
11
+ "biotype, assembly, description; plus notFound[]. Gene-level only — no transcripts or exons. Batch up to 200 symbols per call. " +
12
+ "notFound is valid no-data (deprecated, aliased or mistyped symbol, or wrong species) — do not retry unchanged.",
13
13
  inputSchema: z.object({
14
14
  symbols: z.array(z.string()).min(1).max(200).describe("Gene symbols to look up (e.g. ['BRCA1', 'TP53'])"),
15
15
  species: z.string().default("homo_sapiens").describe("Species name (e.g. 'homo_sapiens', 'mus_musculus')"),
16
- expand: z.boolean().default(false).describe("Include transcript/exon details in the response"),
17
16
  }),
18
- execute: async ({ symbols, species, expand }) => ok(await lookupGenes(symbols, { species, expand })),
17
+ execute: async ({ symbols, species }) => ok(await lookupGenes(symbols, { species })),
19
18
  });
@@ -39,7 +39,7 @@ const GeoEsummaryResponseSchema = z.object({
39
39
  export const searchGeoDatasetsTool = defineTool({
40
40
  id: "search_geo_datasets",
41
41
  description: "Search NCBI GEO for public gene-expression datasets by disease, tissue, or experimental condition — use it to find external validation cohorts or to cite published data. " +
42
- "Returns totalFound plus datasets[]: { accession (GSE…/GDS…), title, summary (truncated to 500 chars), platform, sampleCount, organism, pubmedIds }. " +
42
+ "Returns totalFound plus datasets[]: { accession (GSE…/GDS…), title, summary (truncated to 300 chars), platform, sampleCount, organism, pubmedIds }. " +
43
43
  "HARD CAVEAT: this tool returns metadata only, and sandbox containers have no network — never plan an in-sandbox step that downloads a GEO dataset. " +
44
44
  "Bringing the data itself in is a capability of the surrounding environment, not of this tool: consult the tools you actually have rather than assuming it is possible or that it is not. " +
45
45
  "An empty datasets array is a valid 'nothing matched' — do not retry the identical query.",
@@ -55,9 +55,15 @@ export const searchGeoDatasetsTool = defineTool({
55
55
  .enum(["gds", "gse"])
56
56
  .default("gse")
57
57
  .describe("'gse' (default) — GEO Series: raw submitter-deposited studies, far more numerous. 'gds' — curated DataSets: fewer, but normalized and value-added."),
58
- limit: z.number().int().min(1).max(50).default(15).describe("Max datasets to return (default 15, max 50)."),
58
+ limit: z
59
+ .number()
60
+ .int()
61
+ .min(1)
62
+ .max(50)
63
+ .default(10)
64
+ .describe("Max datasets to return (default 10, max 50). totalFound reports the true match count regardless."),
59
65
  }),
60
- execute: async ({ query, organism, datasetType = "gse", limit = 15 }) => {
66
+ execute: async ({ query, organism, datasetType = "gse", limit = 10 }) => {
61
67
  let searchQuery = query;
62
68
  // Escape backslashes first, then quotes, so a `\` in the organism value
63
69
  // cannot smuggle an unescaped `"` past the escaping and break out of the
@@ -87,7 +93,7 @@ export const searchGeoDatasetsTool = defineTool({
87
93
  return {
88
94
  accession: r.accession ?? r.gse ?? `GDS${id}`,
89
95
  title: r.title ?? "",
90
- summary: (r.summary ?? "").slice(0, 500),
96
+ summary: (r.summary ?? "").slice(0, 300),
91
97
  platform: r.gpl ?? r.platform ?? null,
92
98
  sampleCount: r.n_samples ? Number(r.n_samples) : null,
93
99
  organism: r.taxon ?? null,
@@ -1,16 +1,20 @@
1
1
  /**
2
2
  * searchInteractions — query protein-protein interactions via STRING DB.
3
3
  */
4
- import { getEnrichment, getInteractionPartners } from "../lib/string-client.js";
4
+ import { type StringEnrichment, type StringInteraction } from "../lib/string-client.js";
5
5
  type InteractionsOutput = {
6
- enrichment: Awaited<ReturnType<typeof getEnrichment>>;
6
+ enrichment: StringEnrichment[];
7
+ totalTerms: number;
7
8
  } | {
8
- interactions: Awaited<ReturnType<typeof getInteractionPartners>>;
9
+ interactions: StringInteraction[];
10
+ } | {
11
+ interactions: StringInteraction[];
12
+ totalEdges: number;
9
13
  };
10
14
  export declare const searchInteractionsTool: import("../define-tool.js").Tool<{
11
15
  identifiers: string[];
12
16
  species: number;
13
- action: "partners" | "network" | "enrichment";
17
+ action: "enrichment" | "partners" | "network";
14
18
  minScore: number;
15
19
  limit: number;
16
20
  }, InteractionsOutput>;
@@ -5,27 +5,43 @@ import { ok } from "neverthrow";
5
5
  import { z } from "zod";
6
6
  import { defineTool } from "../define-tool.js";
7
7
  import { getEnrichment, getInteractionNetwork, getInteractionPartners } from "../lib/string-client.js";
8
+ const DEFAULT_LIMIT = 20;
8
9
  export const searchInteractionsTool = defineTool({
9
10
  id: "search_interactions",
10
- description: "Query protein-protein interactions from STRING DB. " +
11
- "Actions: 'partners' finds interaction partners for proteins, " +
12
- "'network' gets the full interaction network between listed proteins, " +
13
- "'enrichment' returns functional enrichment analysis (GO, KEGG, Reactome).",
11
+ description: "Query STRING DB for protein-protein interactions and gene-set functional enrichment. " +
12
+ "'partners' — the one-hop interaction partners of the input proteins, score-sorted: 'what else does this protein work with?'. " +
13
+ "'network' — the interactions AMONG the input proteins only, no new nodes: 'is my gene set actually connected?'. " +
14
+ "'enrichment' — statistical over-representation of GO, KEGG, Reactome and Pfam terms in the input set, FDR-sorted: the enrichment TEST, as " +
15
+ "opposed to lookup_annotation, which just reads a vocabulary. " +
16
+ "`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.",
14
18
  inputSchema: z.object({
15
19
  identifiers: z.array(z.string()).min(1).max(100).describe("Protein/gene identifiers (e.g. ['TP53', 'BRCA1'])"),
16
20
  species: z.number().int().default(9606).describe("NCBI Taxonomy ID (9606 = human, 10090 = mouse)"),
17
21
  action: z.enum(["partners", "network", "enrichment"]).default("partners").describe("Query type"),
18
- minScore: z.number().int().min(0).max(1000).default(400).describe("Minimum confidence score (400=medium, 700=high, 900=highest)"),
19
- limit: z.number().int().min(1).max(100).default(20).describe("Max interaction partners (partners action only)"),
22
+ minScore: z
23
+ .number()
24
+ .int()
25
+ .min(0)
26
+ .max(1000)
27
+ .default(400)
28
+ .describe("Minimum confidence score, 'partners' and 'network' only (400=medium, 700=high, 900=highest). Raise it to cut a large network down."),
29
+ limit: z
30
+ .number()
31
+ .int()
32
+ .min(1)
33
+ .max(500)
34
+ .default(DEFAULT_LIMIT)
35
+ .describe(`Max records returned — partners, network edges, or enriched terms depending on action (default ${DEFAULT_LIMIT}, max 500). ` +
36
+ "Rows are ordered best-first (score descending for interactions, FDR ascending for enrichment), so the default keeps the strongest."),
20
37
  }),
21
38
  execute: async ({ identifiers, species, action, minScore, limit }) => {
22
39
  if (action === "enrichment") {
23
- const enrichment = await getEnrichment(identifiers, species);
24
- return ok({ enrichment });
40
+ return ok(await getEnrichment(identifiers, species, { limit }));
25
41
  }
26
- const interactions = action === "partners"
27
- ? await getInteractionPartners(identifiers, { species, minScore, limit })
28
- : await getInteractionNetwork(identifiers, { species, minScore });
29
- return ok({ interactions });
42
+ if (action === "network") {
43
+ return ok(await getInteractionNetwork(identifiers, { species, minScore, limit }));
44
+ }
45
+ return ok({ interactions: await getInteractionPartners(identifiers, { species, minScore, limit }) });
30
46
  },
31
47
  });