@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
@@ -0,0 +1,59 @@
1
+ /**
2
+ * targetSafety — target-level, mechanism-based safety liability from the two
3
+ * sources that carry it: the curated secondary-pharmacology panel that ships
4
+ * with the harness, and Open Targets' curated liabilities.
5
+ *
6
+ * These were `check_safety_panel` and `opentargets({action:'safety'})`, and the
7
+ * panel tool's own description instructed calling them together — a pairing
8
+ * every caller had to remember, expressed in prose that had already gone stale
9
+ * (it named two tools that no longer exist). Both are keyed on the same target
10
+ * and answer the same question, so the pairing belongs in the tool.
11
+ *
12
+ * Identifier resolution is the tool's job, not the caller's: Open Targets
13
+ * accepts ONLY an Ensembl gene id, and it is taken from the panel entry when
14
+ * there is one and resolved from the gene symbol when there is not.
15
+ */
16
+ import { type SafetyTarget } from "../../data/safety-panel-schema.js";
17
+ import { type SafetyLiability } from "../lib/opentargets-client.js";
18
+ declare const SOURCES: readonly ["panel", "opentargets"];
19
+ type Source = (typeof SOURCES)[number];
20
+ type ConcreteType = "chembl_id" | "gene_symbol" | "uniprot" | "ensembl_id";
21
+ interface TargetRow {
22
+ input: string;
23
+ identifierType: ConcreteType;
24
+ /** The Ensembl id Open Targets was queried with, when one could be obtained. */
25
+ ensemblId?: string;
26
+ panelEntry?: SafetyTarget | null;
27
+ liabilities?: SafetyLiability[];
28
+ /** Liabilities Open Targets held before `liabilityLimit` trimmed them. */
29
+ liabilityCount?: number;
30
+ /** Why the Open Targets half is absent for this identifier, when it is. */
31
+ opentargetsNote?: string;
32
+ }
33
+ interface SourceOutcome {
34
+ source: Source;
35
+ status: "ok" | "no_data" | "unavailable";
36
+ /** Identifiers this source returned something for. */
37
+ matched: number;
38
+ detail?: string;
39
+ }
40
+ export declare const targetSafetyTool: import("../define-tool.js").Tool<{
41
+ identifiers: string[];
42
+ identifierType?: "auto" | "gene_symbol" | "uniprot" | "chembl_id" | "ensembl_id" | undefined;
43
+ sources?: ("opentargets" | "panel")[] | undefined;
44
+ filterOrgan?: "hematologic" | "metabolic" | "immune" | "hepatic" | "cns" | "renal" | "respiratory" | "cardiac" | "gi" | undefined;
45
+ minSeverity?: "high" | "medium" | "low" | undefined;
46
+ liabilityLimit?: number | undefined;
47
+ }, {
48
+ perSource: SourceOutcome[];
49
+ summary: {
50
+ totalInput: number;
51
+ panelMatched: number;
52
+ bySeverity: Record<"high" | "medium" | "low", number>;
53
+ byOrgan: Record<string, number>;
54
+ openTargetsWithLiabilities: number;
55
+ };
56
+ targets: TargetRow[];
57
+ panelVersion: string;
58
+ }>;
59
+ export {};
@@ -0,0 +1,191 @@
1
+ /**
2
+ * targetSafety — target-level, mechanism-based safety liability from the two
3
+ * sources that carry it: the curated secondary-pharmacology panel that ships
4
+ * with the harness, and Open Targets' curated liabilities.
5
+ *
6
+ * These were `check_safety_panel` and `opentargets({action:'safety'})`, and the
7
+ * panel tool's own description instructed calling them together — a pairing
8
+ * every caller had to remember, expressed in prose that had already gone stale
9
+ * (it named two tools that no longer exist). Both are keyed on the same target
10
+ * and answer the same question, so the pairing belongs in the tool.
11
+ *
12
+ * Identifier resolution is the tool's job, not the caller's: Open Targets
13
+ * accepts ONLY an Ensembl gene id, and it is taken from the panel entry when
14
+ * there is one and resolved from the gene symbol when there is not.
15
+ */
16
+ import { ok } from "neverthrow";
17
+ import { z } from "zod";
18
+ import { CHEMBL_RE, ORGAN_SYSTEMS, SEVERITIES, SafetyPanelFileSchema, UNIPROT_RE, } from "../../data/safety-panel-schema.js";
19
+ import panelData from "../../data/safety-panel.json" with { type: "json" };
20
+ import { defineTool } from "../define-tool.js";
21
+ import { resolveSymbolToEnsemblId } from "../lib/ensembl-client.js";
22
+ import { getTargetSafetyLiabilities } from "../lib/opentargets-client.js";
23
+ const PANEL = SafetyPanelFileSchema.parse(panelData);
24
+ const BY_CHEMBL = new Map(PANEL.targets.map((t) => [t.chembl_id, t]));
25
+ const BY_GENE = new Map(PANEL.targets.map((t) => [t.gene_symbol, t]));
26
+ const BY_UNIPROT = new Map(PANEL.targets.map((t) => [t.uniprot, t]));
27
+ const ENSG_RE = /^ENSG\d{11}$/;
28
+ /** Ordered weakest → strongest so `minSeverity` can be a numeric floor. */
29
+ const SEVERITY_ORDER = { low: 0, medium: 1, high: 2 };
30
+ const SOURCES = ["panel", "opentargets"];
31
+ /** Identifiers the panel can be swept for free; Open Targets costs a request each. */
32
+ const MAX_PANEL_IDENTIFIERS = 200;
33
+ const MAX_OPENTARGETS_IDENTIFIERS = 25;
34
+ const DEFAULT_LIABILITY_LIMIT = 10;
35
+ function detectType(id) {
36
+ if (CHEMBL_RE.test(id))
37
+ return "chembl_id";
38
+ if (ENSG_RE.test(id))
39
+ return "ensembl_id";
40
+ if (UNIPROT_RE.test(id))
41
+ return "uniprot";
42
+ return "gene_symbol";
43
+ }
44
+ function panelLookup(id, type) {
45
+ switch (type) {
46
+ case "chembl_id":
47
+ return BY_CHEMBL.get(id) ?? null;
48
+ case "gene_symbol":
49
+ return BY_GENE.get(id.toUpperCase()) ?? null;
50
+ case "uniprot":
51
+ return BY_UNIPROT.get(id) ?? null;
52
+ case "ensembl_id":
53
+ return PANEL.targets.find((t) => t.ensembl_gene_id === id) ?? null;
54
+ }
55
+ }
56
+ const inputSchema = z
57
+ .object({
58
+ identifiers: z
59
+ .array(z.string().min(1))
60
+ .min(1)
61
+ .max(MAX_PANEL_IDENTIFIERS)
62
+ .describe(`Target identifiers, 1–${MAX_PANEL_IDENTIFIERS} — gene symbols, ChEMBL target IDs, UniProt accessions, or Ensembl gene IDs, mixed ` +
63
+ `freely. Capped at ${MAX_OPENTARGETS_IDENTIFIERS} when source 'opentargets' is included, since that source costs a request per target.`),
64
+ identifierType: z
65
+ .enum(["chembl_id", "gene_symbol", "uniprot", "ensembl_id", "auto"])
66
+ .optional()
67
+ .describe("Default 'auto', detecting per input by shape (CHEMBL…, ENSG…, UniProt accession, else a gene symbol). Set it only to force one reading."),
68
+ sources: z
69
+ .array(z.enum(SOURCES))
70
+ .min(1)
71
+ .optional()
72
+ .describe("Default BOTH — the panel is a local lookup with no request cost, and they cover different ground. " +
73
+ "'panel': the curated secondary-pharmacology panel — a fast first-pass off-target screen giving organ system, severity and clinical " +
74
+ "consequence with references. Finite and hand-curated, so absence is NOT evidence of safety. " +
75
+ "'opentargets': curated target liabilities — event, affected biosamples, direction of effect, source. Needs an Ensembl gene id, taken " +
76
+ "from the panel entry or resolved from the symbol for you."),
77
+ filterOrgan: z.enum(ORGAN_SYSTEMS).optional().describe("'panel' only. Keep only matches in this organ system, when screening one liability class."),
78
+ minSeverity: z.enum(SEVERITIES).optional().describe("'panel' only. Drop matches below this severity; default 'low'. Use 'high' to triage a large set."),
79
+ liabilityLimit: z
80
+ .number()
81
+ .int()
82
+ .min(1)
83
+ .max(100)
84
+ .optional()
85
+ .describe(`'opentargets' only. Max liabilities per target (default ${DEFAULT_LIABILITY_LIMIT}, max 100); \`liabilityCount\` gives the true total.`),
86
+ })
87
+ .refine((d) => (d.sources ?? [...SOURCES]).includes("opentargets") === false || d.identifiers.length <= MAX_OPENTARGETS_IDENTIFIERS, {
88
+ message: `identifiers is capped at ${MAX_OPENTARGETS_IDENTIFIERS} when source 'opentargets' is included (one request per target). ` +
89
+ `Either narrow the list, or pass sources: ['panel'] to sweep up to ${MAX_PANEL_IDENTIFIERS} against the local panel first and follow up on the hits.`,
90
+ path: ["identifiers"],
91
+ });
92
+ export const targetSafetyTool = defineTool({
93
+ id: "target_safety",
94
+ description: "TARGET-level, mechanism-based safety liability — the curated secondary-pharmacology panel and Open Targets' curated liabilities in one call. " +
95
+ "Judges what engaging a target is likely to do to an organ system, before any specific molecule exists. Identifiers may be gene symbols, ChEMBL " +
96
+ "target IDs, UniProt accessions or Ensembl gene IDs, mixed.\n" +
97
+ "This is the safety of the TARGET, not of a drug. For a marketed molecule's post-market adverse events use search_faers; for a chemical's " +
98
+ "toxicology use comptox; for what removing the gene does in vivo use gene_preclinical_profile.\n" +
99
+ "ABSENCE IS NOT SAFETY. The panel is finite and hand-curated, so no match means only 'not on the panel'; an empty Open Targets liability list " +
100
+ "means no CURATED liability, not that none exists. Both are valid no-data — do not retry. Read `perSource` and each row's `opentargetsNote`: a " +
101
+ "ChEMBL or UniProt identifier absent from the panel cannot be resolved to an Ensembl id, so its Open Targets half is skipped rather than empty.",
102
+ inputSchema,
103
+ execute: async ({ identifiers, identifierType, sources, filterOrgan, minSeverity, liabilityLimit }) => {
104
+ const selected = sources ?? [...SOURCES];
105
+ const wantPanel = selected.includes("panel");
106
+ const wantOpenTargets = selected.includes("opentargets");
107
+ const severityFloor = SEVERITY_ORDER[minSeverity ?? "low"];
108
+ const limit = liabilityLimit ?? DEFAULT_LIABILITY_LIMIT;
109
+ const rows = identifiers.map((input) => {
110
+ const type = identifierType && identifierType !== "auto" ? identifierType : detectType(input);
111
+ const row = { input, identifierType: type };
112
+ if (wantPanel) {
113
+ const entry = panelLookup(input, type);
114
+ const organMatches = !entry || !filterOrgan || entry.organ_system === filterOrgan;
115
+ const severityMatches = !entry || SEVERITY_ORDER[entry.severity] >= severityFloor;
116
+ row.panelEntry = entry && organMatches && severityMatches ? entry : null;
117
+ }
118
+ return row;
119
+ });
120
+ let opentargetsError;
121
+ if (wantOpenTargets) {
122
+ await Promise.all(rows.map(async (row) => {
123
+ // Open Targets takes an ENSG only. The panel entry carries one for
124
+ // free; a gene symbol can be resolved; a ChEMBL or UniProt id that
125
+ // is not on the panel cannot, and is skipped explicitly.
126
+ const panelEnsembl = panelLookup(row.input, row.identifierType)?.ensembl_gene_id;
127
+ let ensemblId = row.identifierType === "ensembl_id" ? row.input : (panelEnsembl ?? null);
128
+ if (!ensemblId && row.identifierType === "gene_symbol") {
129
+ ensemblId = await resolveSymbolToEnsemblId(row.input);
130
+ if (!ensemblId) {
131
+ row.opentargetsNote = "gene symbol did not resolve to an Ensembl gene id";
132
+ return;
133
+ }
134
+ }
135
+ if (!ensemblId) {
136
+ row.opentargetsNote = `no Ensembl gene id available for a ${row.identifierType} that is not on the panel`;
137
+ return;
138
+ }
139
+ row.ensemblId = ensemblId;
140
+ try {
141
+ const result = await getTargetSafetyLiabilities(ensemblId);
142
+ if (!result) {
143
+ row.opentargetsNote = "no Open Targets record for this Ensembl gene id";
144
+ return;
145
+ }
146
+ row.liabilityCount = result.safetyLiabilities.length;
147
+ row.liabilities = result.safetyLiabilities.slice(0, limit);
148
+ }
149
+ catch (error) {
150
+ opentargetsError = error instanceof Error ? error.message : String(error);
151
+ row.opentargetsNote = "Open Targets could not be reached";
152
+ }
153
+ }));
154
+ }
155
+ const bySeverity = { high: 0, medium: 0, low: 0 };
156
+ const byOrgan = {};
157
+ let panelMatched = 0;
158
+ for (const row of rows) {
159
+ if (!row.panelEntry)
160
+ continue;
161
+ panelMatched++;
162
+ bySeverity[row.panelEntry.severity]++;
163
+ byOrgan[row.panelEntry.organ_system] = (byOrgan[row.panelEntry.organ_system] ?? 0) + 1;
164
+ }
165
+ const withLiabilities = rows.filter((r) => (r.liabilities?.length ?? 0) > 0).length;
166
+ const perSource = [];
167
+ if (wantPanel) {
168
+ perSource.push({ source: "panel", status: panelMatched > 0 ? "ok" : "no_data", matched: panelMatched });
169
+ }
170
+ if (wantOpenTargets) {
171
+ perSource.push({
172
+ source: "opentargets",
173
+ status: opentargetsError ? "unavailable" : withLiabilities > 0 ? "ok" : "no_data",
174
+ matched: withLiabilities,
175
+ ...(opentargetsError ? { detail: opentargetsError } : {}),
176
+ });
177
+ }
178
+ return ok({
179
+ perSource,
180
+ summary: {
181
+ totalInput: identifiers.length,
182
+ panelMatched,
183
+ bySeverity,
184
+ byOrgan: byOrgan,
185
+ openTargetsWithLiabilities: withLiabilities,
186
+ },
187
+ targets: rows,
188
+ panelVersion: PANEL.panel_version,
189
+ });
190
+ },
191
+ });
@@ -39,5 +39,6 @@ export declare function createExecuteAnalysisTool(deps: ExecuteAnalysisToolDeps)
39
39
  request?: string | undefined;
40
40
  }, {
41
41
  runId: string;
42
+ status: "in_progress";
42
43
  }>;
43
44
  export {};
@@ -150,7 +150,7 @@ export function createExecuteAnalysisTool(deps) {
150
150
  description: "Launch analysis computation asynchronously. Use mode=plan with the planId only after the user approved that stored plan. " +
151
151
  "Use mode=adhoc with the user's exact targeted computational request when they explicitly asked to run/compute/test/compare it; " +
152
152
  "that explicit request is consent and needs no synthetic-plan approval. If computation is merely your suggestion, ask first. " +
153
- "You never choose the sandbox specialist: ad hoc routing does that automatically. Returns runId; inspect results on a later turn.",
153
+ "You never choose the sandbox specialist: ad hoc routing does that automatically. Returns runId with status=in_progress; inspect results on a later turn.",
154
154
  inputSchema,
155
155
  execute: async (input, ctx) => {
156
156
  if (ctx.session.scope.kind !== "analysis")
@@ -174,7 +174,7 @@ export function createExecuteAnalysisTool(deps) {
174
174
  const active = unwrapOrThrow(await queryActiveRun(deps.pool, analysisId, planId));
175
175
  if (active) {
176
176
  await emitRunCard(active.runId);
177
- return ok({ runId: active.runId });
177
+ return ok({ runId: active.runId, status: "in_progress" });
178
178
  }
179
179
  }
180
180
  else {
@@ -196,7 +196,7 @@ export function createExecuteAnalysisTool(deps) {
196
196
  const active = unwrapOrThrow(await queryActiveRun(deps.pool, analysisId, planId));
197
197
  if (active) {
198
198
  await emitRunCard(active.runId);
199
- return ok({ runId: active.runId });
199
+ return ok({ runId: active.runId, status: "in_progress" });
200
200
  }
201
201
  }
202
202
  throw error;
@@ -212,7 +212,7 @@ export function createExecuteAnalysisTool(deps) {
212
212
  }));
213
213
  if (!reservation.inserted) {
214
214
  await emitRunCard(reservation.row.runId);
215
- return ok({ runId: reservation.row.runId });
215
+ return ok({ runId: reservation.row.runId, status: "in_progress" });
216
216
  }
217
217
  }
218
218
  let authorization;
@@ -246,7 +246,7 @@ export function createExecuteAnalysisTool(deps) {
246
246
  }
247
247
  logger.info("analysis launched", { analysisId, runId, planId, mode: input.mode });
248
248
  await emitRunCard(runId);
249
- return ok({ runId });
249
+ return ok({ runId, status: "in_progress" });
250
250
  },
251
251
  });
252
252
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Pure async client functions for the Drug-Gene Interaction Database (DGIdb)
3
+ * GraphQL API. Free, no key required.
4
+ *
5
+ * DGIdb aggregates 30+ sources (ChEMBL, DrugBank, TTD, PharmGKB,
6
+ * GuideToPharmacology, CIViC, …) and reports per-interaction source counts,
7
+ * which the caller uses as a confidence signal.
8
+ */
9
+ export interface DgidbInteraction {
10
+ geneName: string;
11
+ drugName: string;
12
+ drugConceptId?: string;
13
+ interactionTypes: {
14
+ type: string;
15
+ directionality?: string;
16
+ }[];
17
+ interactionScore?: number;
18
+ sourceCount: number;
19
+ sources: string[];
20
+ publicationCount: number;
21
+ pmids: string[];
22
+ attributes?: {
23
+ name: string;
24
+ value: string;
25
+ }[];
26
+ }
27
+ export interface DgidbResult {
28
+ input: string;
29
+ found: boolean;
30
+ interactions: DgidbInteraction[];
31
+ /** Interactions this input had before `limit` trimmed them. */
32
+ totalInteractions: number;
33
+ }
34
+ export interface DgidbFilters {
35
+ interactionTypes?: string[];
36
+ sourceDbs?: string[];
37
+ minSources?: number;
38
+ includeAttributes?: boolean;
39
+ limit?: number;
40
+ }
41
+ /**
42
+ * Query DGIdb for interactions of the given genes or drugs.
43
+ *
44
+ * `onPartialErrors` receives GraphQL partial errors that arrive alongside
45
+ * usable data — the client itself never logs, so the caller decides.
46
+ */
47
+ export declare function searchDgidb(inputs: string[], searchType: "gene" | "drug", filters?: DgidbFilters, onPartialErrors?: (errors: {
48
+ message?: string;
49
+ }[]) => void): Promise<DgidbResult[]>;
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Pure async client functions for the Drug-Gene Interaction Database (DGIdb)
3
+ * GraphQL API. Free, no key required.
4
+ *
5
+ * DGIdb aggregates 30+ sources (ChEMBL, DrugBank, TTD, PharmGKB,
6
+ * GuideToPharmacology, CIViC, …) and reports per-interaction source counts,
7
+ * which the caller uses as a confidence signal.
8
+ */
9
+ import { z } from "zod";
10
+ import { apiFetchValidated, describeApiError } from "./api-utils.js";
11
+ const DGIDB_GRAPHQL_URL = "https://dgidb.org/api/graphql";
12
+ const GENE_INTERACTIONS_QUERY = `query($genes: [String!]!) {
13
+ genes(names: $genes) {
14
+ nodes {
15
+ name
16
+ interactions {
17
+ interactionScore
18
+ interactionTypes { type directionality }
19
+ drug { name conceptId }
20
+ interactionAttributes { name value }
21
+ publications { pmid }
22
+ sources { sourceDbName }
23
+ }
24
+ }
25
+ }
26
+ }`;
27
+ const DRUG_INTERACTIONS_QUERY = `query($drugs: [String!]!) {
28
+ drugs(names: $drugs) {
29
+ nodes {
30
+ name
31
+ conceptId
32
+ interactions {
33
+ interactionScore
34
+ interactionTypes { type directionality }
35
+ gene { name }
36
+ interactionAttributes { name value }
37
+ publications { pmid }
38
+ sources { sourceDbName }
39
+ }
40
+ }
41
+ }
42
+ }`;
43
+ // Shape of the DGIdb GraphQL payload as it arrives on the wire. Validated at
44
+ // the fetch boundary; `mapInteraction` normalizes each record. Most fields are
45
+ // optional/nullable to match GraphQL's per-field nullability — but
46
+ // `RawNode.name` is required because we key the node map on
47
+ // `name.toLowerCase()`; a node missing it is a contract break we surface as an
48
+ // `invalid_response` error rather than a runtime TypeError.
49
+ const RawInteractionSchema = z.object({
50
+ interactionScore: z.number().nullable().optional(),
51
+ interactionTypes: z.array(z.object({ type: z.string().optional(), directionality: z.string().nullable().optional() })).optional(),
52
+ drug: z.object({ name: z.string().optional(), conceptId: z.string().nullable().optional() }).nullable().optional(),
53
+ gene: z.object({ name: z.string().optional() }).nullable().optional(),
54
+ interactionAttributes: z.array(z.object({ name: z.string().optional(), value: z.string().optional() })).optional(),
55
+ publications: z.array(z.object({ pmid: z.union([z.string(), z.number()]).optional() })).optional(),
56
+ sources: z.array(z.object({ sourceDbName: z.string().optional() })).optional(),
57
+ });
58
+ const RawNodeSchema = z.object({
59
+ name: z.string(),
60
+ conceptId: z.string().nullable().optional(),
61
+ interactions: z.array(RawInteractionSchema).optional(),
62
+ });
63
+ const DgidbResponseSchema = z.object({
64
+ data: z
65
+ .object({
66
+ genes: z.object({ nodes: z.array(RawNodeSchema).optional() }).optional(),
67
+ drugs: z.object({ nodes: z.array(RawNodeSchema).optional() }).optional(),
68
+ })
69
+ .optional(),
70
+ errors: z.array(z.object({ message: z.string().optional() })).optional(),
71
+ });
72
+ /** PMIDs kept per interaction — the count travels for the rest. */
73
+ const MAX_PMIDS = 5;
74
+ /** Attributes kept per interaction, and the per-value character ceiling. */
75
+ const MAX_ATTRIBUTES = 10;
76
+ const MAX_ATTRIBUTE_CHARS = 160;
77
+ function mapInteraction(raw, perspective, inputSide, includeAttributes) {
78
+ const sources = (raw.sources ?? []).map((s) => s.sourceDbName ?? "").filter(Boolean);
79
+ const pmids = (raw.publications ?? []).map((p) => (p.pmid != null ? String(p.pmid) : "")).filter(Boolean);
80
+ const types = (raw.interactionTypes ?? []).map((t) => ({
81
+ type: t.type ?? "",
82
+ ...(t.directionality ? { directionality: t.directionality } : {}),
83
+ }));
84
+ const geneName = perspective === "gene" ? (inputSide.geneName ?? "") : (raw.gene?.name ?? "");
85
+ const drugName = perspective === "drug" ? (inputSide.drugName ?? "") : (raw.drug?.name ?? "");
86
+ const drugConceptId = perspective === "drug" ? inputSide.drugConceptId : (raw.drug?.conceptId ?? undefined);
87
+ return {
88
+ geneName,
89
+ drugName,
90
+ ...(drugConceptId ? { drugConceptId } : {}),
91
+ interactionTypes: types,
92
+ ...(raw.interactionScore != null ? { interactionScore: raw.interactionScore } : {}),
93
+ sourceCount: sources.length,
94
+ sources,
95
+ publicationCount: pmids.length,
96
+ pmids: pmids.slice(0, MAX_PMIDS),
97
+ ...(includeAttributes
98
+ ? {
99
+ attributes: (raw.interactionAttributes ?? []).slice(0, MAX_ATTRIBUTES).map((a) => ({
100
+ name: a.name ?? "",
101
+ value: (a.value ?? "").slice(0, MAX_ATTRIBUTE_CHARS),
102
+ })),
103
+ }
104
+ : {}),
105
+ };
106
+ }
107
+ function applyFilters(interactions, filters) {
108
+ let out = interactions;
109
+ if (filters.interactionTypes?.length) {
110
+ const needles = filters.interactionTypes.map((s) => s.toLowerCase());
111
+ out = out.filter((i) => i.interactionTypes.some((t) => needles.some((n) => t.type.toLowerCase().includes(n))));
112
+ }
113
+ if (filters.sourceDbs?.length) {
114
+ const needles = filters.sourceDbs.map((s) => s.toLowerCase());
115
+ out = out.filter((i) => i.sources.some((s) => needles.some((n) => s.toLowerCase().includes(n))));
116
+ }
117
+ const minSources = filters.minSources ?? 1;
118
+ if (minSources > 0) {
119
+ out = out.filter((i) => i.sourceCount >= minSources);
120
+ }
121
+ return out;
122
+ }
123
+ /**
124
+ * Query DGIdb for interactions of the given genes or drugs.
125
+ *
126
+ * `onPartialErrors` receives GraphQL partial errors that arrive alongside
127
+ * usable data — the client itself never logs, so the caller decides.
128
+ */
129
+ export async function searchDgidb(inputs, searchType, filters = {}, onPartialErrors) {
130
+ const isGene = searchType === "gene";
131
+ const res = await apiFetchValidated(DGIDB_GRAPHQL_URL, DgidbResponseSchema, {
132
+ method: "POST",
133
+ headers: { "Content-Type": "application/json" },
134
+ body: JSON.stringify({
135
+ query: isGene ? GENE_INTERACTIONS_QUERY : DRUG_INTERACTIONS_QUERY,
136
+ variables: { [isGene ? "genes" : "drugs"]: inputs },
137
+ }),
138
+ });
139
+ if (res.isErr())
140
+ throw new Error(describeApiError(res.error));
141
+ const body = res.value;
142
+ if (!body || !body.data)
143
+ throw new Error("DGIdb returned no data");
144
+ if (body.errors?.length)
145
+ onPartialErrors?.(body.errors.slice(0, 3));
146
+ const nodes = (isGene ? body.data.genes?.nodes : body.data.drugs?.nodes) ?? [];
147
+ const nodeMap = new Map();
148
+ for (const node of nodes) {
149
+ nodeMap.set(node.name.toLowerCase(), node);
150
+ if (!isGene && node.conceptId)
151
+ nodeMap.set(node.conceptId.toLowerCase(), node);
152
+ }
153
+ const limit = filters.limit ?? 20;
154
+ return inputs.map((input) => {
155
+ const node = nodeMap.get(input.toLowerCase());
156
+ if (!node)
157
+ return { input, found: false, interactions: [], totalInteractions: 0 };
158
+ const inputSide = isGene ? { geneName: node.name } : { drugName: node.name, drugConceptId: node.conceptId ?? undefined };
159
+ const mapped = (node.interactions ?? []).map((raw) => mapInteraction(raw, searchType, inputSide, filters.includeAttributes ?? false));
160
+ const filtered = applyFilters(mapped, filters);
161
+ const sorted = filtered.sort((a, b) => {
162
+ if (b.sourceCount !== a.sourceCount)
163
+ return b.sourceCount - a.sourceCount;
164
+ return (b.interactionScore ?? 0) - (a.interactionScore ?? 0);
165
+ });
166
+ return { input, found: true, interactions: sorted.slice(0, limit), totalInteractions: sorted.length };
167
+ });
168
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Pure async client functions for DisGeNET gene-disease associations (GDAs).
3
+ *
4
+ * Requires a DisGeNET API key; `getDisgenetHeaders` throws when it is absent.
5
+ */
6
+ import { z } from "zod";
7
+ declare const GdaSchema: z.ZodPipe<z.ZodObject<{
8
+ gene_symbol: z.ZodOptional<z.ZodNullable<z.ZodString>>;
9
+ gene_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
10
+ geneid: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
11
+ disease_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
12
+ diseaseid: z.ZodOptional<z.ZodNullable<z.ZodString>>;
13
+ disease_type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
14
+ score: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
15
+ ei: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
16
+ year_initial: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
17
+ year_final: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
18
+ pmid_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
19
+ source: z.ZodOptional<z.ZodNullable<z.ZodString>>;
20
+ }, z.core.$strip>, z.ZodTransform<{
21
+ geneSymbol: string;
22
+ geneName: string;
23
+ geneId: number;
24
+ diseaseName: string;
25
+ diseaseId: string;
26
+ diseaseType: string;
27
+ score: number;
28
+ evidenceIndex: number;
29
+ yearInitial: number | null;
30
+ yearFinal: number | null;
31
+ nPmids: number;
32
+ source: string;
33
+ }, {
34
+ gene_symbol?: string | null | undefined;
35
+ gene_name?: string | null | undefined;
36
+ geneid?: number | null | undefined;
37
+ disease_name?: string | null | undefined;
38
+ diseaseid?: string | null | undefined;
39
+ disease_type?: string | null | undefined;
40
+ score?: number | null | undefined;
41
+ ei?: number | null | undefined;
42
+ year_initial?: number | null | undefined;
43
+ year_final?: number | null | undefined;
44
+ pmid_count?: number | null | undefined;
45
+ source?: string | null | undefined;
46
+ }>>;
47
+ export type Gda = z.infer<typeof GdaSchema>;
48
+ export type DisgenetSearchType = "gene" | "disease";
49
+ export type DisgenetSource = "ALL" | "CURATED" | "ANIMAL_MODELS" | "BEFREE";
50
+ export interface DisgenetSearchOptions {
51
+ minScore?: number;
52
+ source?: DisgenetSource;
53
+ limit?: number;
54
+ }
55
+ export declare function searchDisgenet(apiKey: string, query: string, searchType: DisgenetSearchType, options?: DisgenetSearchOptions): Promise<Gda[]>;
56
+ export {};
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Pure async client functions for DisGeNET gene-disease associations (GDAs).
3
+ *
4
+ * Requires a DisGeNET API key; `getDisgenetHeaders` throws when it is absent.
5
+ */
6
+ import { z } from "zod";
7
+ import { apiFetchValidated, describeApiError } from "./api-utils.js";
8
+ import { DISGENET_BASE, getDisgenetHeaders } from "./disgenet-config.js";
9
+ // A single schema that both validates and normalizes one DisGeNET GDA record:
10
+ // the `.object(...)` half is the snake_case wire shape (every field optional —
11
+ // the API omits absent values), the `.transform(...)` half maps it to the
12
+ // camelCase `Gda` we return. Parsing IS the validation (`apiFetchValidated`
13
+ // runs it over the JSON), and because the transform rides on the schema,
14
+ // `z.infer` below is the OUTPUT type — no separate raw interface or mapper.
15
+ const GdaSchema = z
16
+ .object({
17
+ gene_symbol: z.string().nullable().optional(),
18
+ gene_name: z.string().nullable().optional(),
19
+ geneid: z.number().nullable().optional(),
20
+ disease_name: z.string().nullable().optional(),
21
+ diseaseid: z.string().nullable().optional(),
22
+ disease_type: z.string().nullable().optional(),
23
+ score: z.number().nullable().optional(),
24
+ ei: z.number().nullable().optional(),
25
+ year_initial: z.number().nullable().optional(),
26
+ year_final: z.number().nullable().optional(),
27
+ pmid_count: z.number().nullable().optional(),
28
+ source: z.string().nullable().optional(),
29
+ })
30
+ .transform((gda) => ({
31
+ geneSymbol: gda.gene_symbol ?? "",
32
+ geneName: gda.gene_name ?? "",
33
+ geneId: gda.geneid ?? 0,
34
+ diseaseName: gda.disease_name ?? "",
35
+ diseaseId: gda.diseaseid ?? "",
36
+ diseaseType: gda.disease_type ?? "",
37
+ score: gda.score ?? 0,
38
+ evidenceIndex: gda.ei ?? 0,
39
+ yearInitial: gda.year_initial ?? null,
40
+ yearFinal: gda.year_final ?? null,
41
+ nPmids: gda.pmid_count ?? 0,
42
+ source: gda.source ?? "",
43
+ }));
44
+ const GdaListSchema = z.array(GdaSchema);
45
+ export async function searchDisgenet(apiKey, query, searchType, options = {}) {
46
+ const headers = getDisgenetHeaders(apiKey);
47
+ const minScore = options.minScore ?? 0.1;
48
+ const limit = options.limit ?? 25;
49
+ const source = options.source ?? "ALL";
50
+ const path = searchType === "gene" ? `gene/${encodeURIComponent(query)}` : `disease/${encodeURIComponent(query)}`;
51
+ let url = `${DISGENET_BASE}/gda/${path}?min_score=${minScore}&limit=${limit}`;
52
+ if (source !== "ALL")
53
+ url += `&source=${source}`;
54
+ const res = await apiFetchValidated(url, GdaListSchema, { headers });
55
+ if (res.isErr())
56
+ throw new Error(describeApiError(res.error));
57
+ // Already validated + normalized to camelCase by GdaSchema's transform.
58
+ return res.value;
59
+ }