@inflexa-ai/harness 0.12.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 (207) hide show
  1. package/dist/agents/conversation-agent.d.ts +9 -15
  2. package/dist/agents/conversation-agent.js +32 -31
  3. package/dist/agents/sandbox/cheminformatics-agent.js +1 -1
  4. package/dist/agents/sandbox/chromatin-agent.js +1 -1
  5. package/dist/agents/sandbox/dna-methylation-agent.js +1 -1
  6. package/dist/agents/sandbox/drug-repurposing-agent.js +5 -10
  7. package/dist/agents/sandbox/enrichment-agent.js +1 -1
  8. package/dist/agents/sandbox/genomic-variant-agent.js +1 -11
  9. package/dist/agents/sandbox/immune-profiling-agent.js +1 -1
  10. package/dist/agents/sandbox/index.js +0 -3
  11. package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -1
  12. package/dist/agents/sandbox/network-agent.js +1 -1
  13. package/dist/agents/sandbox/pkpd-clinical-response-agent.js +3 -6
  14. package/dist/agents/sandbox/scientific-executor.js +1 -1
  15. package/dist/agents/sandbox/shared.d.ts +2 -2
  16. package/dist/agents/sandbox/shared.js +8 -14
  17. package/dist/agents/sandbox/translational-safety-agent.js +4 -7
  18. package/dist/agents/sandbox/types.d.ts +5 -5
  19. package/dist/agents/sandbox/types.js +4 -4
  20. package/dist/app/chat-turn.js +4 -1
  21. package/dist/app/message-assembly.d.ts +5 -2
  22. package/dist/app/message-assembly.js +7 -2
  23. package/dist/app/run-activity.d.ts +13 -0
  24. package/dist/app/run-activity.js +51 -0
  25. package/dist/config/resource-limits.d.ts +0 -7
  26. package/dist/config/resource-limits.js +0 -1
  27. package/dist/contracts/target-dossier.d.ts +10 -10
  28. package/dist/execution/dbos-run-launcher.js +1 -26
  29. package/dist/execution/report-runner.js +1 -1
  30. package/dist/execution/run-authorizer.d.ts +1 -1
  31. package/dist/execution/run-authorizer.js +1 -1
  32. package/dist/execution/run-event-parts.d.ts +44 -0
  33. package/dist/execution/run-event-parts.js +0 -0
  34. package/dist/execution/run-event-stream.d.ts +91 -0
  35. package/dist/execution/run-event-stream.js +212 -0
  36. package/dist/execution/run-launcher.d.ts +2 -18
  37. package/dist/execution/run-launcher.js +2 -4
  38. package/dist/execution/run-synthesis.js +1 -1
  39. package/dist/index.d.ts +7 -4
  40. package/dist/index.js +23 -6
  41. package/dist/input-sanitization.d.ts +14 -4
  42. package/dist/input-sanitization.js +27 -4
  43. package/dist/lib/storage.js +4 -1
  44. package/dist/loop/budget-exceeded.d.ts +18 -2
  45. package/dist/loop/budget-exceeded.js +49 -2
  46. package/dist/loop/run-agent.d.ts +22 -1
  47. package/dist/loop/run-agent.js +51 -5
  48. package/dist/loop/run-to-terminal.d.ts +0 -2
  49. package/dist/loop/run-to-terminal.js +8 -1
  50. package/dist/memory/ai-sdk-message-storage.d.ts +37 -4
  51. package/dist/memory/ai-sdk-message-storage.js +45 -4
  52. package/dist/memory/content-to-cortex.js +19 -3
  53. package/dist/memory/message-backfill.js +1 -1
  54. package/dist/memory/reconstruct-cards.d.ts +1 -1
  55. package/dist/memory/reconstruct-cards.js +7 -5
  56. package/dist/memory/thread-history.js +72 -8
  57. package/dist/prompts/conversation.d.ts +1 -1
  58. package/dist/prompts/conversation.js +41 -15
  59. package/dist/prompts/literature-reviewer.d.ts +1 -1
  60. package/dist/prompts/literature-reviewer.js +16 -14
  61. package/dist/prompts/planner.d.ts +3 -0
  62. package/dist/prompts/planner.js +16 -21
  63. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  64. package/dist/prompts/sandbox/drug-repurposing-agent.js +31 -22
  65. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  66. package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -10
  67. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  68. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +8 -7
  69. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  70. package/dist/prompts/sandbox/translational-safety-agent.js +20 -14
  71. package/dist/prompts/sandbox-standards.d.ts +4 -4
  72. package/dist/prompts/sandbox-standards.js +5 -5
  73. package/dist/prompts/synthesis-agent.d.ts +1 -1
  74. package/dist/prompts/synthesis-agent.js +7 -7
  75. package/dist/providers/errors.d.ts +7 -0
  76. package/dist/providers/errors.js +88 -3
  77. package/dist/runtime/assemble.d.ts +5 -10
  78. package/dist/runtime/assemble.js +0 -4
  79. package/dist/runtime/boot.d.ts +2 -2
  80. package/dist/runtime/boot.js +1 -1
  81. package/dist/runtime/dbos.d.ts +4 -5
  82. package/dist/runtime/dbos.js +6 -7
  83. package/dist/runtime/pools.d.ts +15 -1
  84. package/dist/runtime/pools.js +15 -1
  85. package/dist/sandbox/mount-plan.d.ts +1 -1
  86. package/dist/sandbox/sandbox-error.d.ts +1 -1
  87. package/dist/sandbox/sandbox-error.js +1 -1
  88. package/dist/sandbox/sandbox-step-translate.d.ts +15 -2
  89. package/dist/sandbox/sandbox-step-translate.js +17 -2
  90. package/dist/sandbox/types.d.ts +1 -1
  91. package/dist/schemas/validate-plan.d.ts +1 -1
  92. package/dist/schemas/workflow-state.d.ts +2 -2
  93. package/dist/schemas/workflow-state.js +2 -2
  94. package/dist/state/data-profile.d.ts +53 -1
  95. package/dist/state/data-profile.js +66 -6
  96. package/dist/state/index.d.ts +6 -6
  97. package/dist/state/index.js +4 -4
  98. package/dist/state/init.js +37 -2
  99. package/dist/state/plans.js +1 -1
  100. package/dist/state/runs.d.ts +49 -0
  101. package/dist/state/runs.js +150 -0
  102. package/dist/tasks/data-profile-activity.d.ts +87 -0
  103. package/dist/tasks/data-profile-activity.js +56 -0
  104. package/dist/tasks/data-profile.js +66 -4
  105. package/dist/tools/__fixtures__/tool-context.js +1 -0
  106. package/dist/tools/ad-hoc-router.d.ts +31 -0
  107. package/dist/tools/ad-hoc-router.js +132 -0
  108. package/dist/tools/analysis-invocation.d.ts +4 -0
  109. package/dist/tools/analysis-invocation.js +17 -0
  110. package/dist/tools/bio/chembl.d.ts +1 -1
  111. package/dist/tools/bio/chembl.js +51 -54
  112. package/dist/tools/bio/comptox.js +35 -33
  113. package/dist/tools/bio/drug-gene-interactions.d.ts +76 -0
  114. package/dist/tools/bio/drug-gene-interactions.js +175 -0
  115. package/dist/tools/bio/gene-disease-evidence.d.ts +61 -0
  116. package/dist/tools/bio/gene-disease-evidence.js +187 -0
  117. package/dist/tools/bio/gene-preclinical-profile.d.ts +58 -0
  118. package/dist/tools/bio/gene-preclinical-profile.js +125 -0
  119. package/dist/tools/bio/index.d.ts +23 -16
  120. package/dist/tools/bio/index.js +29 -22
  121. package/dist/tools/bio/keys.d.ts +16 -9
  122. package/dist/tools/bio/keys.js +22 -13
  123. package/dist/tools/bio/lookup-annotation.d.ts +48 -0
  124. package/dist/tools/bio/lookup-annotation.js +173 -0
  125. package/dist/tools/bio/opentargets.d.ts +7 -11
  126. package/dist/tools/bio/opentargets.js +24 -35
  127. package/dist/tools/bio/pubchem.js +6 -4
  128. package/dist/tools/bio/pubmed.d.ts +15 -7
  129. package/dist/tools/bio/pubmed.js +79 -20
  130. package/dist/tools/bio/search-clinical-trials.d.ts +20 -1
  131. package/dist/tools/bio/search-clinical-trials.js +26 -5
  132. package/dist/tools/bio/search-faers.js +9 -3
  133. package/dist/tools/bio/search-gene.d.ts +0 -1
  134. package/dist/tools/bio/search-gene.js +4 -5
  135. package/dist/tools/bio/search-geo-datasets.js +10 -4
  136. package/dist/tools/bio/search-interactions.d.ts +8 -4
  137. package/dist/tools/bio/search-interactions.js +28 -12
  138. package/dist/tools/bio/target-safety.d.ts +59 -0
  139. package/dist/tools/bio/target-safety.js +191 -0
  140. package/dist/tools/define-tool.d.ts +6 -2
  141. package/dist/tools/define-tool.js +1 -1
  142. package/dist/tools/execute-analysis.d.ts +44 -0
  143. package/dist/tools/execute-analysis.js +252 -0
  144. package/dist/tools/iterate-report.js +7 -6
  145. package/dist/tools/lib/dgidb-client.d.ts +49 -0
  146. package/dist/tools/lib/dgidb-client.js +168 -0
  147. package/dist/tools/lib/disgenet-client.d.ts +56 -0
  148. package/dist/tools/lib/disgenet-client.js +59 -0
  149. package/dist/tools/lib/drugbank-client.d.ts +83 -0
  150. package/dist/tools/lib/drugbank-client.js +95 -0
  151. package/dist/tools/lib/ensembl-client.d.ts +4 -1
  152. package/dist/tools/lib/ensembl-client.js +5 -3
  153. package/dist/tools/lib/gwas-catalog-client.d.ts +60 -0
  154. package/dist/tools/lib/gwas-catalog-client.js +136 -0
  155. package/dist/tools/lib/pharmgkb-client.d.ts +22 -0
  156. package/dist/tools/lib/pharmgkb-client.js +32 -0
  157. package/dist/tools/lib/pubmed-client.d.ts +57 -4
  158. package/dist/tools/lib/pubmed-client.js +57 -3
  159. package/dist/tools/lib/string-client.d.ts +25 -4
  160. package/dist/tools/lib/string-client.js +22 -5
  161. package/dist/tools/research/generate-plan.d.ts +16 -9
  162. package/dist/tools/research/generate-plan.js +116 -70
  163. package/dist/tools/research/inspect-data-profile.d.ts +31 -6
  164. package/dist/tools/research/inspect-data-profile.js +23 -7
  165. package/dist/tools/research/inspect-run.d.ts +33 -15
  166. package/dist/tools/research/inspect-run.js +155 -66
  167. package/dist/tools/research/literature-reviewer.js +7 -11
  168. package/dist/workflows/execute-analysis.d.ts +68 -5
  169. package/dist/workflows/execute-analysis.js +76 -7
  170. package/dist/workflows/sandbox-step.js +1 -11
  171. package/dist/workflows/target-assessment/decisions/index.d.ts +1 -1
  172. package/dist/workflows/target-assessment/phase4-assemble.d.ts +1 -1
  173. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +2 -2
  174. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
  175. package/package.json +1 -1
  176. package/dist/agents/sandbox/ephemeral-executor.d.ts +0 -9
  177. package/dist/agents/sandbox/ephemeral-executor.js +0 -20
  178. package/dist/execution/ephemeral-runner.d.ts +0 -98
  179. package/dist/execution/ephemeral-runner.js +0 -189
  180. package/dist/prompts/sandbox/ephemeral-executor.d.ts +0 -1
  181. package/dist/prompts/sandbox/ephemeral-executor.js +0 -66
  182. package/dist/tools/bio/check-safety-panel.d.ts +0 -35
  183. package/dist/tools/bio/check-safety-panel.js +0 -76
  184. package/dist/tools/bio/get-impc-ko-profile.d.ts +0 -9
  185. package/dist/tools/bio/get-impc-ko-profile.js +0 -23
  186. package/dist/tools/bio/lookup-go-term.d.ts +0 -31
  187. package/dist/tools/bio/lookup-go-term.js +0 -116
  188. package/dist/tools/bio/search-bgee-expression.d.ts +0 -10
  189. package/dist/tools/bio/search-bgee-expression.js +0 -30
  190. package/dist/tools/bio/search-clinvar.d.ts +0 -13
  191. package/dist/tools/bio/search-clinvar.js +0 -28
  192. package/dist/tools/bio/search-dgidb.d.ts +0 -53
  193. package/dist/tools/bio/search-dgidb.js +0 -213
  194. package/dist/tools/bio/search-disgenet.d.ts +0 -30
  195. package/dist/tools/bio/search-disgenet.js +0 -84
  196. package/dist/tools/bio/search-drugbank.d.ts +0 -38
  197. package/dist/tools/bio/search-drugbank.js +0 -109
  198. package/dist/tools/bio/search-gwas-catalog.d.ts +0 -33
  199. package/dist/tools/bio/search-gwas-catalog.js +0 -172
  200. package/dist/tools/bio/search-pathway.d.ts +0 -12
  201. package/dist/tools/bio/search-pathway.js +0 -32
  202. package/dist/tools/bio/search-pharmgkb.d.ts +0 -17
  203. package/dist/tools/bio/search-pharmgkb.js +0 -52
  204. package/dist/tools/execute-plan.d.ts +0 -64
  205. package/dist/tools/execute-plan.js +0 -211
  206. package/dist/tools/run-ephemeral.d.ts +0 -33
  207. package/dist/tools/run-ephemeral.js +0 -65
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Pure async client functions for the NHGRI-EBI GWAS Catalog REST API.
3
+ *
4
+ * Public, no key required. Three search paths: by gene (SNP-by-gene lookup then
5
+ * per-SNP associations), by trait (EFO trait search then its associations), and
6
+ * by variant (direct rsID associations).
7
+ */
8
+ import { z } from "zod";
9
+ import { apiFetchValidated, describeApiError } from "./api-utils.js";
10
+ import { GWAS_BASE, GWAS_HEADERS } from "./gwas-catalog-config.js";
11
+ // GWAS Catalog raw wire shapes (HAL+JSON), validated at the fetch boundary.
12
+ // Every field is optional because the API omits absent values; leaf strings
13
+ // that the API can return as an explicit `null` are `.nullable()`.
14
+ const RawGwasStudySchema = z.object({
15
+ accessionId: z.string().optional(),
16
+ pubmedId: z.string().optional(),
17
+ initialSampleSize: z.string().optional(),
18
+ });
19
+ // `_snpRsId` is not on the wire — the 'gene' search path assigns it after
20
+ // parsing (`a._snpRsId = snp.rsId`), so it stays a writable optional field.
21
+ const RawGwasAssociationSchema = z.object({
22
+ _snpRsId: z.string().optional(),
23
+ loci: z
24
+ .array(z.object({
25
+ strongestRiskAlleles: z.array(z.object({ riskAlleleName: z.string().nullable().optional() })).optional(),
26
+ authorReportedGenes: z.array(z.object({ geneName: z.string().nullable().optional() })).optional(),
27
+ }))
28
+ .optional(),
29
+ efoTraits: z.array(z.object({ trait: z.string().nullable().optional() })).optional(),
30
+ study: RawGwasStudySchema.optional(),
31
+ pvalueMantissa: z.number().optional(),
32
+ pvalueExponent: z.number().optional(),
33
+ riskFrequency: z.string().optional(),
34
+ orPerCopyNum: z.number().nullable().optional(),
35
+ betaNum: z.number().nullable().optional(),
36
+ range: z.string().optional(),
37
+ });
38
+ const GwasEmbeddedSchema = z.object({
39
+ _embedded: z.object({ associations: z.array(RawGwasAssociationSchema).optional() }).optional(),
40
+ page: z.object({ totalElements: z.number().optional() }).optional(),
41
+ });
42
+ const GwasTraitSearchResponseSchema = z.object({
43
+ _embedded: z
44
+ .object({
45
+ efoTraits: z.array(z.object({ _links: z.object({ self: z.object({ href: z.string().optional() }).optional() }).optional() })).optional(),
46
+ })
47
+ .optional(),
48
+ });
49
+ const GwasSnpSearchResponseSchema = z.object({
50
+ _embedded: z
51
+ .object({
52
+ singleNucleotidePolymorphisms: z
53
+ .array(z.object({
54
+ rsId: z.string().optional(),
55
+ _links: z.object({ associations: z.object({ href: z.string().optional() }).optional() }).optional(),
56
+ }))
57
+ .optional(),
58
+ })
59
+ .optional(),
60
+ });
61
+ export function mapAssociation(a) {
62
+ const loci = a.loci ?? [];
63
+ const riskAlleles = loci.flatMap((l) => l.strongestRiskAlleles?.map((ra) => String(ra.riskAlleleName ?? "")) ?? []);
64
+ const genes = loci.flatMap((l) => l.authorReportedGenes?.map((g) => String(g.geneName ?? "")) ?? []);
65
+ const traits = (a.efoTraits ?? []).map((t) => String(t.trait ?? ""));
66
+ const study = a.study ?? {};
67
+ const pMantissa = a.pvalueMantissa ?? 0;
68
+ const pExponent = a.pvalueExponent ?? 0;
69
+ return {
70
+ rsId: String(a._snpRsId ?? riskAlleles[0]?.split("-")[0] ?? ""),
71
+ pValue: pMantissa * Math.pow(10, pExponent),
72
+ pValueMantissa: pMantissa,
73
+ pValueExponent: pExponent,
74
+ riskAllele: riskAlleles.join(", "),
75
+ riskFrequency: String(a.riskFrequency ?? ""),
76
+ orBeta: (a.orPerCopyNum ?? a.betaNum ?? null),
77
+ ci: String(a.range ?? ""),
78
+ trait: traits.join("; "),
79
+ mappedGenes: [...new Set(genes)],
80
+ studyAccession: String(study.accessionId ?? ""),
81
+ pubmedId: String(study.pubmedId ?? ""),
82
+ sampleSize: String(study.initialSampleSize ?? ""),
83
+ };
84
+ }
85
+ /** Max SNPs whose associations the 'gene' path walks; each is one request. */
86
+ const GENE_PATH_MAX_SNPS = 10;
87
+ export async function searchGwasCatalog(query, searchType, options = {}) {
88
+ const pValueThreshold = options.pValueThreshold ?? 5e-8;
89
+ const limit = options.limit ?? 25;
90
+ if (searchType === "gene") {
91
+ const url = `${GWAS_BASE}/singleNucleotidePolymorphisms/search/findByGene?geneName=${encodeURIComponent(query)}&projection=snpByGene`;
92
+ const snpRes = await apiFetchValidated(url, GwasSnpSearchResponseSchema, { headers: GWAS_HEADERS });
93
+ if (snpRes.isErr())
94
+ throw new Error(describeApiError(snpRes.error));
95
+ const snps = snpRes.value?._embedded?.singleNucleotidePolymorphisms ?? [];
96
+ const allAssocs = [];
97
+ for (const snp of snps.slice(0, Math.min(limit, GENE_PATH_MAX_SNPS))) {
98
+ const assocLink = snp?._links?.associations?.href;
99
+ if (!assocLink)
100
+ continue;
101
+ const aRes = await apiFetchValidated(assocLink, GwasEmbeddedSchema, { headers: GWAS_HEADERS });
102
+ if (aRes.isOk()) {
103
+ for (const a of aRes.value?._embedded?.associations ?? []) {
104
+ a._snpRsId = snp.rsId;
105
+ allAssocs.push(a);
106
+ }
107
+ }
108
+ if (allAssocs.length >= limit)
109
+ break;
110
+ }
111
+ return { totalFound: allAssocs.length, associations: allAssocs.slice(0, limit).map(mapAssociation) };
112
+ }
113
+ let associationsUrl;
114
+ if (searchType === "variant") {
115
+ const rsId = query.startsWith("rs") ? query : `rs${query}`;
116
+ associationsUrl = `${GWAS_BASE}/singleNucleotidePolymorphisms/${rsId}/associations?projection=associationBySnp`;
117
+ }
118
+ else {
119
+ const traitRes = await apiFetchValidated(`${GWAS_BASE}/efoTraits/search/findBySearchQuery?query=${encodeURIComponent(query)}`, GwasTraitSearchResponseSchema, {
120
+ headers: GWAS_HEADERS,
121
+ });
122
+ if (traitRes.isErr())
123
+ throw new Error(describeApiError(traitRes.error));
124
+ const traitUri = (traitRes.value?._embedded?.efoTraits ?? [])[0]?._links?.self?.href;
125
+ if (!traitUri)
126
+ return { totalFound: 0, associations: [] };
127
+ associationsUrl = `${GWAS_BASE}/efoTraits/${traitUri.split("/").pop()}/associations?size=${limit}`;
128
+ }
129
+ const res = await apiFetchValidated(associationsUrl, GwasEmbeddedSchema, { headers: GWAS_HEADERS });
130
+ if (res.isErr())
131
+ throw new Error(describeApiError(res.error));
132
+ const rawAssocs = res.value?._embedded?.associations ?? [];
133
+ const totalFound = res.value?.page?.totalElements ?? rawAssocs.length;
134
+ const filtered = rawAssocs.filter((a) => (a.pvalueMantissa ?? 1) * Math.pow(10, a.pvalueExponent ?? 0) <= pValueThreshold).slice(0, limit);
135
+ return { totalFound, associations: filtered.map(mapAssociation) };
136
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Pure async client functions for PharmGKB clinical annotations.
3
+ *
4
+ * Public API, no key required. Matching is an exact field filter, not a search:
5
+ * `location.genes.symbol` for a gene, `relatedChemicals.name` for a drug.
6
+ */
7
+ /**
8
+ * One PharmGKB clinical annotation.
9
+ *
10
+ * `levelOfEvidence` is the only strength signal this endpoint carries — the
11
+ * phenotype, guideline source and summary live on other PharmGKB resources, so
12
+ * they are absent here rather than present-and-null.
13
+ */
14
+ export interface PharmgkbAnnotation {
15
+ gene: string;
16
+ drug: string;
17
+ levelOfEvidence: string;
18
+ }
19
+ export declare function searchPharmgkb(query: string, searchType: "gene" | "drug", limit?: number): Promise<{
20
+ annotations: PharmgkbAnnotation[];
21
+ totalFound: number;
22
+ }>;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Pure async client functions for PharmGKB clinical annotations.
3
+ *
4
+ * Public API, no key required. Matching is an exact field filter, not a search:
5
+ * `location.genes.symbol` for a gene, `relatedChemicals.name` for a drug.
6
+ */
7
+ import { z } from "zod";
8
+ import { apiFetchValidated, describeApiError } from "./api-utils.js";
9
+ import { PHARMGKB_BASE, PHARMGKB_HEADERS } from "./pharmgkb-config.js";
10
+ // Raw PharmGKB clinicalAnnotation wire shape, validated at the fetch boundary.
11
+ // Every field is optional because the API omits absent values.
12
+ const PharmgkbAnnotationSchema = z.object({
13
+ location: z.object({ genes: z.array(z.object({ symbol: z.string().optional() })).optional() }).optional(),
14
+ relatedChemicals: z.array(z.object({ name: z.string().optional() })).optional(),
15
+ levelOfEvidence: z.object({ term: z.string().optional() }).optional(),
16
+ });
17
+ const PharmgkbResponseSchema = z.object({
18
+ data: z.array(PharmgkbAnnotationSchema).optional(),
19
+ });
20
+ export async function searchPharmgkb(query, searchType, limit = 20) {
21
+ const filter = searchType === "gene" ? `location.genes.symbol=${encodeURIComponent(query)}` : `relatedChemicals.name=${encodeURIComponent(query)}`;
22
+ const res = await apiFetchValidated(`${PHARMGKB_BASE}/clinicalAnnotation?${filter}`, PharmgkbResponseSchema, { headers: PHARMGKB_HEADERS });
23
+ if (res.isErr())
24
+ throw new Error(describeApiError(res.error));
25
+ const data = res.value?.data ?? [];
26
+ const annotations = data.slice(0, limit).map((ann) => ({
27
+ gene: ann.location?.genes?.map((g) => g.symbol).join(", ") ?? query,
28
+ drug: ann.relatedChemicals?.map((c) => c.name).join(", ") ?? "",
29
+ levelOfEvidence: ann.levelOfEvidence?.term ?? "Unknown",
30
+ }));
31
+ return { annotations, totalFound: data.length };
32
+ }
@@ -4,8 +4,8 @@
4
4
  * Used by §3.11 (Key Papers), §3.10.4 (Preclinical literature), and the
5
5
  * pubmed-index Phase-1 collector.
6
6
  */
7
- import { type ArticleDetail, type PubMedSummary, type FullTextResult } from "./ncbi-utils.js";
8
- export type { PubMedSummary, ArticleDetail, FullTextResult };
7
+ import { type ArticleDetail, type ArticleSection, type PubMedSummary, type FullTextResult } from "./ncbi-utils.js";
8
+ export type { PubMedSummary, ArticleDetail, ArticleSection, FullTextResult };
9
9
  export interface SearchOptions {
10
10
  maxResults?: number;
11
11
  sort?: "relevance" | "date";
@@ -19,10 +19,63 @@ export declare function searchPubmed(ncbiApiKey: string | undefined, query: stri
19
19
  totalFound: number;
20
20
  results: PubMedSummary[];
21
21
  }>;
22
+ export interface DetailOptions {
23
+ /** Authors kept per article before collapsing the rest into `authorCount`. */
24
+ maxAuthors?: number;
25
+ /** MeSH terms kept per article; 0 drops the list entirely. */
26
+ maxMeshTerms?: number;
27
+ }
28
+ /**
29
+ * One article as the tool returns it: `ArticleDetail` with the two unbounded
30
+ * list fields trimmed. A consortium paper can carry hundreds of authors and
31
+ * dozens of MeSH headings — neither is worth the context, so the counts travel
32
+ * instead of the tails.
33
+ */
34
+ export interface BoundedArticleDetail extends ArticleDetail {
35
+ authorCount: number;
36
+ meshTermCount: number;
37
+ }
22
38
  /** Fetch full PubMed article details (efetch + ID Converter for PMC). */
23
39
  export declare function getArticleDetails(ncbiApiKey: string | undefined, pmids: string[]): Promise<{
24
40
  articles: ArticleDetail[];
25
41
  notFound: string[];
26
42
  }>;
27
- /** Fetch open-access full text from PMC. Returns null when unavailable. */
28
- export declare function getArticleFullText(ncbiApiKey: string | undefined, pmcId: string): Promise<FullTextResult | null>;
43
+ /** Trim the two unbounded list fields on each article, recording their true sizes. */
44
+ export declare function boundArticleDetails(articles: ArticleDetail[], options?: DetailOptions): BoundedArticleDetail[];
45
+ export interface FullTextOptions {
46
+ /** Case-insensitive substring match against section headings. */
47
+ sections?: string[];
48
+ /** Character budget across the returned section bodies. */
49
+ maxChars?: number;
50
+ }
51
+ /**
52
+ * A full text trimmed to a character budget. `outline` always lists EVERY
53
+ * section in the article with its size and whether the body came back, so one
54
+ * call is enough to decide which sections a follow-up should request — the
55
+ * alternative is the agent guessing headings it has not seen.
56
+ */
57
+ export interface BoundedFullText {
58
+ fullText: string;
59
+ sections: ArticleSection[];
60
+ outline: {
61
+ heading: string;
62
+ chars: number;
63
+ included: boolean;
64
+ }[];
65
+ totalChars: number;
66
+ returnedChars: number;
67
+ truncated: boolean;
68
+ }
69
+ /**
70
+ * Fetch open-access full text from PMC, trimmed to a section filter and a
71
+ * character budget. Returns null when the article is not open-access.
72
+ *
73
+ * A research article routinely parses to 40–80k characters, so returning the
74
+ * whole body by default would spend more context on one paper than the entire
75
+ * tool surface costs to attach. Sections are kept whole (a half-sentence of
76
+ * Methods is worse than none) and admitted in document order until the budget
77
+ * would be exceeded.
78
+ */
79
+ export declare function getArticleFullText(ncbiApiKey: string | undefined, pmcId: string, options?: FullTextOptions): Promise<BoundedFullText | null>;
80
+ /** Apply the section filter and character budget to a parsed full text. */
81
+ export declare function boundFullText(parsed: FullTextResult, options?: FullTextOptions): BoundedFullText;
@@ -75,8 +75,29 @@ export async function getArticleDetails(ncbiApiKey, pmids) {
75
75
  const notFound = pmids.filter((id) => !foundPmids.has(id));
76
76
  return { articles, notFound };
77
77
  }
78
- /** Fetch open-access full text from PMC. Returns null when unavailable. */
79
- export async function getArticleFullText(ncbiApiKey, pmcId) {
78
+ /** Trim the two unbounded list fields on each article, recording their true sizes. */
79
+ export function boundArticleDetails(articles, options = {}) {
80
+ const maxAuthors = options.maxAuthors ?? 5;
81
+ const maxMeshTerms = options.maxMeshTerms ?? 10;
82
+ return articles.map((a) => ({
83
+ ...a,
84
+ authors: a.authors.slice(0, maxAuthors),
85
+ authorCount: a.authors.length,
86
+ meshTerms: a.meshTerms.slice(0, maxMeshTerms),
87
+ meshTermCount: a.meshTerms.length,
88
+ }));
89
+ }
90
+ /**
91
+ * Fetch open-access full text from PMC, trimmed to a section filter and a
92
+ * character budget. Returns null when the article is not open-access.
93
+ *
94
+ * A research article routinely parses to 40–80k characters, so returning the
95
+ * whole body by default would spend more context on one paper than the entire
96
+ * tool surface costs to attach. Sections are kept whole (a half-sentence of
97
+ * Methods is worse than none) and admitted in document order until the budget
98
+ * would be exceeded.
99
+ */
100
+ export async function getArticleFullText(ncbiApiKey, pmcId, options = {}) {
80
101
  const numericId = pmcId.replace(/^PMC/i, "");
81
102
  const url = ncbiUrl(ncbiApiKey, `${NCBI_BASE}/efetch.fcgi`, {
82
103
  db: "pmc",
@@ -87,5 +108,38 @@ export async function getArticleFullText(ncbiApiKey, pmcId) {
87
108
  const result = await apiFetch(url, { parseAs: "text" });
88
109
  if (result.isErr())
89
110
  throw new Error(describeApiError(result.error));
90
- return parsePmcFullText(result.value);
111
+ const parsed = parsePmcFullText(result.value);
112
+ if (!parsed)
113
+ return null;
114
+ return boundFullText(parsed, options);
115
+ }
116
+ /** Apply the section filter and character budget to a parsed full text. */
117
+ export function boundFullText(parsed, options = {}) {
118
+ const maxChars = options.maxChars ?? 12_000;
119
+ const needles = (options.sections ?? []).map((s) => s.toLowerCase()).filter(Boolean);
120
+ const totalChars = parsed.sections.reduce((n, s) => n + s.text.length, 0);
121
+ const outline = [];
122
+ const kept = [];
123
+ let returnedChars = 0;
124
+ for (const section of parsed.sections) {
125
+ const matchesFilter = needles.length === 0 || needles.some((n) => section.heading.toLowerCase().includes(n));
126
+ // A section is admitted whole or not at all. The first section is always
127
+ // admitted even if it alone busts the budget — a single-section article
128
+ // would otherwise come back empty with no way to ask for less.
129
+ const fits = returnedChars + section.text.length <= maxChars || kept.length === 0;
130
+ const included = matchesFilter && fits;
131
+ if (included) {
132
+ kept.push(section);
133
+ returnedChars += section.text.length;
134
+ }
135
+ outline.push({ heading: section.heading, chars: section.text.length, included });
136
+ }
137
+ return {
138
+ fullText: kept.map((s) => (s.heading ? `## ${s.heading}\n\n${s.text}` : s.text)).join("\n\n"),
139
+ sections: kept,
140
+ outline,
141
+ totalChars,
142
+ returnedChars,
143
+ truncated: returnedChars < totalChars,
144
+ };
91
145
  }
@@ -27,7 +27,28 @@ export interface InteractionOptions {
27
27
  }
28
28
  /** Fetch interaction partners (one-hop) for the given identifiers. */
29
29
  export declare function getInteractionPartners(identifiers: string[], options?: InteractionOptions): Promise<StringInteraction[]>;
30
- /** Fetch the network among the given identifiers. */
31
- export declare function getInteractionNetwork(identifiers: string[], options?: InteractionOptions): Promise<StringInteraction[]>;
32
- /** Functional enrichment for a gene set. */
33
- export declare function getEnrichment(identifiers: string[], species?: number): Promise<StringEnrichment[]>;
30
+ /**
31
+ * Fetch the network among the given identifiers.
32
+ *
33
+ * STRING has no server-side row cap on `/network` — edge count grows with the
34
+ * square of the input set, so `limit` truncates the score-sorted rows here.
35
+ * `totalEdges` reports the true count so a caller can tell a trimmed network
36
+ * from a sparse one.
37
+ */
38
+ export declare function getInteractionNetwork(identifiers: string[], options?: InteractionOptions): Promise<{
39
+ interactions: StringInteraction[];
40
+ totalEdges: number;
41
+ }>;
42
+ /**
43
+ * Functional enrichment for a gene set.
44
+ *
45
+ * STRING returns every enriched term across all categories (GO, KEGG,
46
+ * Reactome, Pfam, …) with the full member-gene list per term, so `limit`
47
+ * truncates the FDR-sorted rows and `totalTerms` reports what was there.
48
+ */
49
+ export declare function getEnrichment(identifiers: string[], species?: number, options?: {
50
+ limit?: number;
51
+ }): Promise<{
52
+ enrichment: StringEnrichment[];
53
+ totalTerms: number;
54
+ }>;
@@ -73,7 +73,14 @@ export async function getInteractionPartners(identifiers, options = {}) {
73
73
  throw new Error(describeApiError(res.error));
74
74
  return res.value.sort((a, b) => b.score - a.score);
75
75
  }
76
- /** Fetch the network among the given identifiers. */
76
+ /**
77
+ * Fetch the network among the given identifiers.
78
+ *
79
+ * STRING has no server-side row cap on `/network` — edge count grows with the
80
+ * square of the input set, so `limit` truncates the score-sorted rows here.
81
+ * `totalEdges` reports the true count so a caller can tell a trimmed network
82
+ * from a sparse one.
83
+ */
77
84
  export async function getInteractionNetwork(identifiers, options = {}) {
78
85
  const params = buildParams(identifiers, options.species ?? 9606, {
79
86
  required_score: options.minScore ?? 400,
@@ -81,13 +88,23 @@ export async function getInteractionNetwork(identifiers, options = {}) {
81
88
  const res = await apiFetchValidated(`${STRING_BASE}/network?${params}`, z.array(StringInteractionSchema));
82
89
  if (res.isErr())
83
90
  throw new Error(describeApiError(res.error));
84
- return res.value.sort((a, b) => b.score - a.score);
91
+ const sorted = res.value.sort((a, b) => b.score - a.score);
92
+ const limit = options.limit ?? sorted.length;
93
+ return { interactions: sorted.slice(0, limit), totalEdges: sorted.length };
85
94
  }
86
- /** Functional enrichment for a gene set. */
87
- export async function getEnrichment(identifiers, species = 9606) {
95
+ /**
96
+ * Functional enrichment for a gene set.
97
+ *
98
+ * STRING returns every enriched term across all categories (GO, KEGG,
99
+ * Reactome, Pfam, …) with the full member-gene list per term, so `limit`
100
+ * truncates the FDR-sorted rows and `totalTerms` reports what was there.
101
+ */
102
+ export async function getEnrichment(identifiers, species = 9606, options = {}) {
88
103
  const params = buildParams(identifiers, species);
89
104
  const res = await apiFetchValidated(`${STRING_BASE}/enrichment?${params}`, z.array(StringEnrichmentSchema));
90
105
  if (res.isErr())
91
106
  throw new Error(describeApiError(res.error));
92
- return res.value.sort((a, b) => a.fdr - b.fdr);
107
+ const sorted = res.value.sort((a, b) => a.fdr - b.fdr);
108
+ const limit = options.limit ?? sorted.length;
109
+ return { enrichment: sorted.slice(0, limit), totalTerms: sorted.length };
93
110
  }
@@ -11,11 +11,10 @@
11
11
  * database already holds, and it would lose fidelity at every hop.
12
12
  *
13
13
  * The tool drives an internal "planner" agent that communicates outcomes
14
- * EXCLUSIVELY via four terminal tool calls:
14
+ * EXCLUSIVELY via three terminal tool calls:
15
15
  *
16
- * - validate_plan(plan) → non-terminal dry-run: an any-shape candidate
17
- * in, {valid, issues} out (schema + semantic)
18
- * - submit_plan(plan) → terminal success: re-validates + persists
16
+ * - submit_plan(plan) → terminal success: re-validates + persists;
17
+ * rejected candidates return structured issues
19
18
  * - request_clarification(…) → terminal: planner needs more context
20
19
  * - report_blocker(reason) → terminal: no viable plan
21
20
  *
@@ -29,19 +28,27 @@ import { type ChatProvider } from "../../providers/types.js";
29
28
  import { type Tool } from "../define-tool.js";
30
29
  import type { EnvironmentStorePaths } from "../../config/environment-stores.js";
31
30
  import { type ResourcePolicy } from "../../config/resource-limits.js";
31
+ import type { Logger } from "../../lib/logger.js";
32
32
  export interface GeneratePlanDeps extends EnvironmentStorePaths {
33
- /** The LLM seam the planner loop runs on. */
34
- readonly provider: ChatProvider;
33
+ /**
34
+ * The conversation-role backend the planner is pinned to. Keeping the
35
+ * provider and its model label in one value prevents composition roots from
36
+ * accidentally pairing the planner with the sandbox or utility role.
37
+ */
38
+ readonly conversation: {
39
+ readonly provider: ChatProvider;
40
+ readonly model: string;
41
+ };
35
42
  /** Database pool — plan persistence and prior-plan loading. */
36
43
  readonly pool: Pool;
37
- /** Model id — provenance / metric label; the provider owns the wire model. */
38
- readonly model: string;
39
44
  /**
40
45
  * Host resource policy — stated to the planner as concrete per-step
41
- * ceilings and enforced by `validate_plan`. Absent, the prompt keeps its
46
+ * ceilings and enforced by `submit_plan`. Absent, the prompt keeps its
42
47
  * default guidance and validation skips the ceiling check.
43
48
  */
44
49
  readonly resourcePolicy?: ResourcePolicy;
50
+ /** Diagnostic sink. Absent, the tool runs silently — see `RunAgentOptions.logger`. */
51
+ readonly logger?: Logger;
45
52
  }
46
53
  /** Build the `generate_plan` tool bound to its provider and pool. */
47
54
  export declare function createGeneratePlanTool(deps: GeneratePlanDeps): Tool;