@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,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
+ });
@@ -8,7 +8,7 @@
8
8
  * captures its deps and calls `defineTool`.
9
9
  *
10
10
  * `ToolContext` carries only request-scoped values every tool may need
11
- * (`session`, `signal`, `emit`, `runStep`, `ask`) — no pool, no sandbox, no
11
+ * (`invocationId`, `session`, `signal`, `emit`, `runStep`, `ask`) — no pool, no sandbox, no
12
12
  * logger. The error contract: an expected
13
13
  * outcome ("not found", "no results") stays in the ok channel as a data
14
14
  * variant (`ok({ found: false })`); an unexpected failure is an `err(ToolError)`
@@ -39,11 +39,15 @@ export type ToolExecutionMode = "step" | "workflow" | "inline";
39
39
  export declare function isToolError(value: unknown): value is ToolError;
40
40
  /**
41
41
  * The request-scoped values passed to every tool's `execute`. No injected
42
- * dependencies (see the harness-durable-runtime spec) — `session`, `signal`, `emit`, the `runStep`
42
+ * dependencies (see the harness-durable-runtime spec) — invocation identity,
43
+ * `session`, `signal`, `emit`, the `runStep`
43
44
  * durability seam (`passthroughStep` in chat, `DBOS.runStep` in workflows) a
44
45
  * tool uses to wrap its own durable work, and the `ask` user-approval seam.
45
46
  */
46
47
  export interface ToolContext {
48
+ /** Stable identity of this AI SDK tool call. Redelivery preserves it; a new
49
+ * model-issued call receives a new value. */
50
+ readonly invocationId: string;
47
51
  readonly session: AgentSession;
48
52
  readonly signal: AbortSignal;
49
53
  readonly emit: EmitFn;
@@ -8,7 +8,7 @@
8
8
  * captures its deps and calls `defineTool`.
9
9
  *
10
10
  * `ToolContext` carries only request-scoped values every tool may need
11
- * (`session`, `signal`, `emit`, `runStep`, `ask`) — no pool, no sandbox, no
11
+ * (`invocationId`, `session`, `signal`, `emit`, `runStep`, `ask`) — no pool, no sandbox, no
12
12
  * logger. The error contract: an expected
13
13
  * outcome ("not found", "no results") stays in the ok channel as a data
14
14
  * variant (`ok({ found: false })`); an unexpected failure is an `err(ToolError)`
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Unified async edge for approved plans and explicitly requested one-step ad
3
+ * hoc analyses. Both modes launch the ordinary executeAnalysis workflow.
4
+ */
5
+ import type { Pool } from "pg";
6
+ import type { ResourcePolicy } from "../config/resource-limits.js";
7
+ import type { RunAuthorizer } from "../execution/run-authorizer.js";
8
+ import type { RunLauncher } from "../execution/run-launcher.js";
9
+ import type { Logger } from "../lib/logger.js";
10
+ import type { ChatProvider } from "../providers/types.js";
11
+ import { type AnalysisPlan } from "../schemas/workflow-state.js";
12
+ import type { ExecuteAnalysisInput, ExecuteAnalysisResult } from "../workflows/execute-analysis.js";
13
+ import { type AdHocRoute } from "./ad-hoc-router.js";
14
+ type ExecuteAnalysisWorkflow = (input: ExecuteAnalysisInput) => Promise<ExecuteAnalysisResult>;
15
+ export interface ExecuteAnalysisToolDeps {
16
+ readonly pool: Pool;
17
+ readonly executeAnalysisWorkflow: ExecuteAnalysisWorkflow;
18
+ readonly runAuthorizer: RunAuthorizer;
19
+ readonly runLauncher: RunLauncher;
20
+ readonly resourcePolicy?: ResourcePolicy;
21
+ readonly utilityProvider: ChatProvider;
22
+ readonly utilityModel: string;
23
+ readonly logger?: Logger;
24
+ }
25
+ export declare class PlanNotFoundError extends Error {
26
+ constructor(planId: string);
27
+ }
28
+ export declare class PlanMalformedError extends Error {
29
+ constructor(planId: string);
30
+ }
31
+ export declare class PlanValidationError extends Error {
32
+ readonly errors: string[];
33
+ constructor(planId: string, errors: string[]);
34
+ }
35
+ export declare function buildAdHocPlan(request: string, route: AdHocRoute, createdAt?: string): AnalysisPlan;
36
+ export declare function createExecuteAnalysisTool(deps: ExecuteAnalysisToolDeps): import("./define-tool.js").Tool<{
37
+ mode: "plan" | "adhoc";
38
+ planId?: string | undefined;
39
+ request?: string | undefined;
40
+ }, {
41
+ runId: string;
42
+ status: "in_progress";
43
+ }>;
44
+ export {};
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Unified async edge for approved plans and explicitly requested one-step ad
3
+ * hoc analyses. Both modes launch the ordinary executeAnalysis workflow.
4
+ */
5
+ import { randomUUID } from "node:crypto";
6
+ import { ok } from "neverthrow";
7
+ import { z } from "zod";
8
+ import { SANDBOX_AGENT_META } from "../agents/sandbox/index.js";
9
+ import { DEFAULT_SANDBOX_MAX_STEPS } from "../config/resource-limits.js";
10
+ import { createNoopLogger } from "../lib/console-logger.js";
11
+ import { unwrapOrThrow } from "../lib/result.js";
12
+ import { buildRunCardData } from "../memory/card-builders.js";
13
+ import { AnalysisPlanSchema } from "../schemas/workflow-state.js";
14
+ import { validatePlan } from "../schemas/validate-plan.js";
15
+ import { RunDedupCollisionError, insertRun, loadPlan, queryActiveRun, reserveRunById, updateRunStatus, upsertPlan } from "../state/index.js";
16
+ import { routeAdHocRequest } from "./ad-hoc-router.js";
17
+ import { adHocPlanId, adHocRunId } from "./analysis-invocation.js";
18
+ import { defineTool } from "./define-tool.js";
19
+ const planIdSchema = z.string().regex(/^pln-[a-f0-9]{8}$/, "planId must be a pln-<8hex> value");
20
+ const inputSchema = z
21
+ .object({
22
+ mode: z.enum(["plan", "adhoc"]),
23
+ planId: planIdSchema.optional(),
24
+ request: z.string().min(1).optional(),
25
+ })
26
+ .superRefine((value, ctx) => {
27
+ if (value.mode === "plan" && (value.planId === undefined || value.request !== undefined)) {
28
+ ctx.addIssue({ code: "custom", message: "plan mode requires planId and forbids request" });
29
+ }
30
+ if (value.mode === "adhoc" && (value.request === undefined || value.planId !== undefined)) {
31
+ ctx.addIssue({ code: "custom", message: "adhoc mode requires request and forbids planId" });
32
+ }
33
+ });
34
+ export class PlanNotFoundError extends Error {
35
+ constructor(planId) {
36
+ super(`Plan ${planId} not found for this analysis`);
37
+ this.name = "PlanNotFoundError";
38
+ }
39
+ }
40
+ export class PlanMalformedError extends Error {
41
+ constructor(planId) {
42
+ super(`Plan ${planId} is malformed`);
43
+ this.name = "PlanMalformedError";
44
+ }
45
+ }
46
+ export class PlanValidationError extends Error {
47
+ errors;
48
+ constructor(planId, errors) {
49
+ super(`Plan ${planId} failed validation: ${errors.length} error(s)`);
50
+ this.errors = errors;
51
+ this.name = "PlanValidationError";
52
+ }
53
+ }
54
+ function isDedupCollision(err) {
55
+ return err instanceof RunDedupCollisionError || (err instanceof Error && err.name === "RunDedupCollisionError");
56
+ }
57
+ function planSummary(plan) {
58
+ return plan.title?.trim() || plan.analytical_narrative.trim().slice(0, 280);
59
+ }
60
+ function workflowInput(planId, plan, args) {
61
+ return {
62
+ analysisId: args.analysisId,
63
+ planId,
64
+ planSummary: planSummary(plan),
65
+ threadId: args.threadId,
66
+ steps: plan.steps.map((step) => ({ id: step.id, depends_on: step.depends_on ?? [] })),
67
+ planStepById: Object.fromEntries(plan.steps.map((step) => [step.id, step])),
68
+ agentByStepId: Object.fromEntries(plan.steps.map((step) => [step.id, step.agent ?? "unknown"])),
69
+ resourcesByStepId: Object.fromEntries(plan.steps.map((step) => {
70
+ if (!step.resources)
71
+ throw new Error(`Step "${step.id}" has no resources — validation should have rejected it`);
72
+ return [step.id, step.resources];
73
+ })),
74
+ timeoutByStepId: Object.fromEntries(plan.steps.filter((step) => step.timeout !== undefined).map((step) => [step.id, step.timeout])),
75
+ budget: args.budget,
76
+ runSession: args.runSession,
77
+ ownsMandate: args.ownsMandate,
78
+ synthesisEnabled: args.synthesisEnabled,
79
+ };
80
+ }
81
+ function validateStoredPlan(raw, planId) {
82
+ const parsed = AnalysisPlanSchema.safeParse(raw);
83
+ if (!parsed.success)
84
+ throw new PlanMalformedError(planId);
85
+ const result = validatePlan(parsed.data);
86
+ if (!result.valid)
87
+ throw new PlanValidationError(planId, result.errors);
88
+ return parsed.data;
89
+ }
90
+ function adHocTitle(request) {
91
+ const compact = request.trim().replace(/\s+/g, " ");
92
+ return compact.length <= 80 ? compact : `${compact.slice(0, 77)}…`;
93
+ }
94
+ export function buildAdHocPlan(request, route, createdAt = new Date().toISOString()) {
95
+ const meta = SANDBOX_AGENT_META[route.agentId];
96
+ const plan = {
97
+ title: adHocTitle(request),
98
+ analytical_narrative: `Execute one targeted analysis step for the explicit request: ${request}`,
99
+ created_at: createdAt,
100
+ steps: [
101
+ {
102
+ id: "T1S1",
103
+ name: "Ad hoc analysis",
104
+ track: "ad-hoc",
105
+ step_type: "analysis",
106
+ question: request,
107
+ context: `Automatically routed to ${route.agentId}. Routing rationale: ${route.rationale}`,
108
+ constraints: [
109
+ "Use the staged analysis inputs and persisted data profile; do not invent missing data.",
110
+ "Complete only this targeted request; do not expand it into a multi-step plan.",
111
+ ],
112
+ acceptance_criteria: [
113
+ "Persist every script needed to reproduce the computation.",
114
+ "Persist machine-readable result file(s), even when the answer is a single scalar.",
115
+ "Provide a direct result summary grounded in the persisted outputs.",
116
+ ],
117
+ caveats: route.fallbackClass ? [`Automatic routing fallback: ${route.fallbackClass}`] : undefined,
118
+ depends_on: [],
119
+ status: "pending",
120
+ resources: route.resources,
121
+ agent: route.agentId,
122
+ maxSteps: meta?.defaultMaxSteps ?? DEFAULT_SANDBOX_MAX_STEPS,
123
+ },
124
+ ],
125
+ };
126
+ return AnalysisPlanSchema.parse(plan);
127
+ }
128
+ async function persistedAdHocPlan(deps, args) {
129
+ const existing = unwrapOrThrow(await loadPlan(deps.pool, args.planId, { analysisId: args.analysisId }));
130
+ if (existing)
131
+ return validateStoredPlan(existing, args.planId);
132
+ const route = await routeAdHocRequest({
133
+ provider: deps.utilityProvider,
134
+ model: deps.utilityModel,
135
+ pool: deps.pool,
136
+ resourcePolicy: deps.resourcePolicy,
137
+ logger: deps.logger,
138
+ }, { analysisId: args.analysisId, request: args.request, session: args.ctx.session, signal: args.ctx.signal });
139
+ const candidate = buildAdHocPlan(args.request, route);
140
+ unwrapOrThrow(await upsertPlan(deps.pool, { planId: args.planId, analysisId: args.analysisId, plan: candidate }));
141
+ const stored = unwrapOrThrow(await loadPlan(deps.pool, args.planId, { analysisId: args.analysisId }));
142
+ if (!stored)
143
+ throw new Error(`Internal ad hoc plan ${args.planId} could not be reloaded`);
144
+ return validateStoredPlan(stored, args.planId);
145
+ }
146
+ export function createExecuteAnalysisTool(deps) {
147
+ const logger = (deps.logger ?? createNoopLogger()).named("execute-analysis-tool");
148
+ return defineTool({
149
+ id: "execute_analysis",
150
+ description: "Launch analysis computation asynchronously. Use mode=plan with the planId only after the user approved that stored plan. " +
151
+ "Use mode=adhoc with the user's exact targeted computational request when they explicitly asked to run/compute/test/compare it; " +
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 with status=in_progress; inspect results on a later turn.",
154
+ inputSchema,
155
+ execute: async (input, ctx) => {
156
+ if (ctx.session.scope.kind !== "analysis")
157
+ throw new Error("execute_analysis requires an analysis-scoped session");
158
+ if (!ctx.session.auth)
159
+ throw new Error("execute_analysis: session is missing its auth capability");
160
+ const analysisId = ctx.session.scope.analysisId;
161
+ const threadId = ctx.session.scope.threadId ?? null;
162
+ const planId = input.mode === "plan" ? input.planId : adHocPlanId(analysisId, ctx.invocationId);
163
+ const emitRunCard = async (runId) => {
164
+ const card = await buildRunCardData(deps.pool, { planId, analysisId, runId }).unwrapOr(null);
165
+ if (card)
166
+ await ctx.emit({ type: "data-run-card", source: ctx.session.provenance, data: card });
167
+ };
168
+ let plan;
169
+ if (input.mode === "plan") {
170
+ const raw = unwrapOrThrow(await loadPlan(deps.pool, planId, { analysisId }));
171
+ if (!raw)
172
+ throw new PlanNotFoundError(planId);
173
+ plan = validateStoredPlan(raw, planId);
174
+ const active = unwrapOrThrow(await queryActiveRun(deps.pool, analysisId, planId));
175
+ if (active) {
176
+ await emitRunCard(active.runId);
177
+ return ok({ runId: active.runId, status: "in_progress" });
178
+ }
179
+ }
180
+ else {
181
+ plan = await persistedAdHocPlan(deps, { analysisId, request: input.request, ctx, planId });
182
+ }
183
+ const runId = input.mode === "plan" ? randomUUID() : adHocRunId(analysisId, ctx.invocationId);
184
+ if (input.mode === "plan") {
185
+ try {
186
+ unwrapOrThrow(await insertRun(deps.pool, {
187
+ runId,
188
+ analysisId,
189
+ threadId,
190
+ workflowName: "executeAnalysis",
191
+ planId,
192
+ }));
193
+ }
194
+ catch (error) {
195
+ if (isDedupCollision(error)) {
196
+ const active = unwrapOrThrow(await queryActiveRun(deps.pool, analysisId, planId));
197
+ if (active) {
198
+ await emitRunCard(active.runId);
199
+ return ok({ runId: active.runId, status: "in_progress" });
200
+ }
201
+ }
202
+ throw error;
203
+ }
204
+ }
205
+ else {
206
+ const reservation = unwrapOrThrow(await reserveRunById(deps.pool, {
207
+ runId,
208
+ analysisId,
209
+ threadId,
210
+ workflowName: "executeAnalysis",
211
+ planId,
212
+ }));
213
+ if (!reservation.inserted) {
214
+ await emitRunCard(reservation.row.runId);
215
+ return ok({ runId: reservation.row.runId, status: "in_progress" });
216
+ }
217
+ }
218
+ let authorization;
219
+ try {
220
+ authorization = await deps.runAuthorizer.authorize({
221
+ auth: ctx.session.auth,
222
+ scope: ctx.session.scope,
223
+ provenance: ctx.session.provenance,
224
+ frame: { runId },
225
+ });
226
+ }
227
+ catch (error) {
228
+ await updateRunStatus(deps.pool, runId, "failed", "run authorization failed").match(() => { }, () => { });
229
+ throw error;
230
+ }
231
+ const wfInput = workflowInput(planId, plan, {
232
+ analysisId,
233
+ threadId,
234
+ runSession: authorization.runSession,
235
+ ownsMandate: authorization.ownsMandate,
236
+ budget: deps.resourcePolicy?.budget,
237
+ synthesisEnabled: input.mode === "plan",
238
+ });
239
+ try {
240
+ await deps.runLauncher.launch(deps.executeAnalysisWorkflow, { workflowId: runId }, wfInput);
241
+ }
242
+ catch (error) {
243
+ await deps.runAuthorizer.revoke(authorization, "workflow-start-failed").catch(() => { });
244
+ await updateRunStatus(deps.pool, runId, "failed", "workflow start failed").match(() => { }, () => { });
245
+ throw error;
246
+ }
247
+ logger.info("analysis launched", { analysisId, runId, planId, mode: input.mode });
248
+ await emitRunCard(runId);
249
+ return ok({ runId, status: "in_progress" });
250
+ },
251
+ });
252
+ }
@@ -130,7 +130,7 @@ const intentField = () => z
130
130
  .describe("Why this section exists and what to emphasize (e.g. 'hero — headline " +
131
131
  "finding', 'downplay if space is tight'). Emphasis, not layout — the " +
132
132
  "builder picks the components, sizing and alternation.");
133
- /** Free-text row transform — the reason to never pre-slice a CSV with `run_ephemeral`. */
133
+ /** Free-text row transform — the reason to avoid launching analysis just to pre-slice a CSV. */
134
134
  const transformField = () => z
135
135
  .string()
136
136
  .optional()
@@ -138,7 +138,7 @@ const transformField = () => z
138
138
  "asset — filter, derive, aggregate, sort (e.g. 'filter padj < 0.05 and " +
139
139
  "abs(log2FoldChange) > 1', 'compute -log10(padj) as neg_log_padj', 'group " +
140
140
  "by sample and sum count', 'sort by mean_count desc, take top 50'). Use " +
141
- "this instead of running `run_ephemeral` to pre-slice the file. The text " +
141
+ "this instead of launching an ad hoc analysis to pre-slice the file. The text " +
142
142
  "is rendered verbatim as a provenance footnote, so the data reads as " +
143
143
  "processed, not fabricated.");
144
144
  const SectionSchema = z.discriminatedUnion("type", [
@@ -359,14 +359,15 @@ treatment. Pass the composed brief as \`submit_report\`'s \`report\` field.
359
359
  their content reaches the report as prose you write into \`narrative\` /
360
360
  \`methods\` sections.
361
361
 
362
- ## Do NOT reach for run_ephemeral first
362
+ ## Do NOT launch ad hoc analysis for a section transform
363
363
  - Not to peek at a CSV — pre-flight already parsed its columns and head rows.
364
364
  - Not to filter, slice, rank, or derive columns from a single CSV about to be
365
365
  rendered — \`chart.content.transform\` and \`table.content.transform\` do exactly
366
366
  that client-side, and the transform text renders as a provenance footnote.
367
- - run_ephemeral is only for computation no single section transform covers:
368
- cross-file aggregation, statistics needing a real numerical library, or a
369
- derived CSV the report then lists as a fresh source.
367
+ - A durable ad hoc analysis is appropriate only when the user explicitly asks
368
+ for computation no single section transform covers: cross-file aggregation,
369
+ statistics needing a real numerical library, or a derived CSV the report then
370
+ lists as a fresh source.
370
371
 
371
372
  ## Choosing a section type
372
373
  - narrative — prose you wrote: context, story, interpretation.