@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
@@ -1,25 +1,28 @@
1
1
  /**
2
2
  * inspectRun — inspect workflow runs for the current analysis.
3
3
  *
4
- * Dependency-bearing: the database `Pool` is captured by the factory
5
- * (see the harness-durable-runtime spec). The analysis id is read from the request-scoped `Session`,
6
- * not an ambient request context.
7
- *
8
- * Results are pull-only via this tool — the workflow does NOT write to the
9
- * conversation thread on completion. The retired `run-result-writer` push
10
- * payload (run status, step summaries, run synthesis, artifact paths) is
11
- * surfaced here on demand: the run row + step `summaryPath` references +
12
- * the `synthesisPath` for runs whose synthesis was produced let the
13
- * conversation agent `read_file` the artefacts it needs.
4
+ * Results are pull-only: workflows never append completion messages to a
5
+ * conversation. List mode is active-first and paged. Targeted mode makes
6
+ * readiness explicit and can perform one bounded, abort-aware wait.
14
7
  */
8
+ import { setTimeout as sleep } from "node:timers/promises";
15
9
  import { ok } from "neverthrow";
16
10
  import { z } from "zod";
17
- import { queryRun, queryRunsByAnalysis, queryStepsByRun } from "../../state/index.js";
18
11
  import { scopeResource } from "../../auth/types.js";
19
- import { SYNTHESIS_STEP_ID } from "../../workspace/paths.js";
20
12
  import { unwrapOrThrow } from "../../lib/result.js";
13
+ import { queryRun, queryRunsForInspection, queryStepsByRun } from "../../state/index.js";
14
+ import { SYNTHESIS_STEP_ID } from "../../workspace/paths.js";
21
15
  import { defineTool } from "../define-tool.js";
22
- function formatRun(r, verbose) {
16
+ const DEFAULT_PAGE_SIZE = 50;
17
+ const POLL_INTERVAL_MS = 1_000;
18
+ const systemClock = {
19
+ now: Date.now,
20
+ wait: (ms, signal) => sleep(ms, undefined, { signal }),
21
+ };
22
+ function isTerminal(status) {
23
+ return status === "completed" || status === "partial" || status === "failed" || status === "canceled";
24
+ }
25
+ function formatRun(r, verbose, exposePaths = isTerminal(r.status)) {
23
26
  return {
24
27
  runId: r.runId,
25
28
  workflowName: r.workflowName,
@@ -27,74 +30,160 @@ function formatRun(r, verbose) {
27
30
  startedAt: r.startedAt,
28
31
  completedAt: r.completedAt,
29
32
  planId: r.planId,
30
- // synthesis.json is written only when synthesis actually produced a
31
- // result; a skipped/failed synthesis leaves no file, so pointing at a
32
- // path would send the agent to a stale or absent artifact. The recorded
33
- // outcome — surfaced as synthesisStatus (and synthesisReason when set) —
34
- // lets a consumer tell "produced" from "skipped/failed/unknown" without
35
- // reading a file.
36
- synthesisPath: r.synthesisStatus === "produced" ? `runs/${r.runId}/synthesis.json` : null,
33
+ synthesisPath: exposePaths && r.synthesisStatus === "produced" ? `runs/${r.runId}/synthesis.json` : null,
37
34
  synthesisStatus: r.synthesisStatus,
38
35
  ...(r.synthesisReason !== null ? { synthesisReason: r.synthesisReason } : {}),
39
36
  ...(verbose ? { error: r.error } : {}),
40
37
  };
41
38
  }
42
- export function createInspectRunTool(pool) {
39
+ function formatSteps(runId, stepRows, verbose, exposePaths) {
40
+ return stepRows.map((s) => ({
41
+ stepId: s.stepId,
42
+ agentId: s.agentId,
43
+ wave: s.wave,
44
+ status: s.status,
45
+ ...(exposePaths && s.status !== "pending" && s.status !== "skipped" && s.stepId !== SYNTHESIS_STEP_ID
46
+ ? { summaryPath: `runs/${runId}/${s.stepId}/output/summary.md` }
47
+ : {}),
48
+ ...(verbose
49
+ ? {
50
+ durationMs: s.durationMs,
51
+ error: s.error,
52
+ attempts: s.attempts,
53
+ lastErrorClass: s.lastErrorClass,
54
+ finishReason: s.finishReason,
55
+ hitMaxSteps: s.hitMaxSteps,
56
+ }
57
+ : {}),
58
+ }));
59
+ }
60
+ const inputSchema = z
61
+ .object({
62
+ runId: z.string().optional().describe("Specific run to inspect. Omit to list runs."),
63
+ page: z.number().int().min(1).optional().describe("List page, starting at 1. Valid only without runId."),
64
+ pageSize: z.number().int().min(1).max(100).optional().describe("Rows per list page (1-100). Valid only without runId."),
65
+ waitForTerminalSeconds: z
66
+ .number()
67
+ .int()
68
+ .min(1)
69
+ .max(30)
70
+ .optional()
71
+ .describe("Wait up to this many seconds for a targeted running run to leave running state."),
72
+ includeDiagnostics: z
73
+ .boolean()
74
+ .optional()
75
+ .describe("Include failure/retry telemetry: run.error and step.{durationMs,error,attempts,lastErrorClass,finishReason,hitMaxSteps}. Default false."),
76
+ })
77
+ .superRefine((input, refinement) => {
78
+ if (input.runId && (input.page !== undefined || input.pageSize !== undefined)) {
79
+ refinement.addIssue({
80
+ code: z.ZodIssueCode.custom,
81
+ message: "page and pageSize are valid only when runId is omitted",
82
+ path: input.page !== undefined ? ["page"] : ["pageSize"],
83
+ });
84
+ }
85
+ if (!input.runId && input.waitForTerminalSeconds !== undefined) {
86
+ refinement.addIssue({
87
+ code: z.ZodIssueCode.custom,
88
+ message: "waitForTerminalSeconds requires runId",
89
+ path: ["waitForTerminalSeconds"],
90
+ });
91
+ }
92
+ });
93
+ /**
94
+ * Build the run-inspection tool. The optional clock is an internal testing
95
+ * seam for deterministic cutoff and transition coverage.
96
+ */
97
+ export function createInspectRunTool(pool, clock = systemClock) {
43
98
  return defineTool({
44
99
  id: "inspect_run",
45
- description: "Inspect workflow runs for this analysis. " +
46
- "Without a runId: lists all runs. " +
47
- "With a runId: returns step-level details with paths to summary.md, plus each run's synthesisStatus and a synthesisPath to synthesis.json only when synthesis was produced. " +
48
- "Default response is lightweight — pass includeDiagnostics:true to add error/duration/retry telemetry. " +
49
- "Read summary.md or synthesis.json for findings, or use workspace search with type:summary or type:synthesis.",
50
- inputSchema: z.object({
51
- runId: z.string().optional().describe("Specific run to inspect. Omit to list all runs."),
52
- includeDiagnostics: z
53
- .boolean()
54
- .optional()
55
- .describe("Include failure/retry telemetry: run.error and step.{durationMs,error,attempts,lastErrorClass,finishReason,hitMaxSteps}. Default false."),
56
- }),
100
+ description: "Inspect workflow runs for this analysis. Without a runId, returns an active-first paged list (running, suspended, then terminal history). " +
101
+ "With a runId, returns an explicit inspectionState and advertises output paths only after the run is terminal. " +
102
+ "For a user-directed check, waitForTerminalSeconds can wait once for 1-30 seconds; if the cutoff returns in_progress, stop polling for that turn. " +
103
+ "Default response is lightweight — pass includeDiagnostics:true to add error/duration/retry telemetry.",
104
+ inputSchema,
57
105
  execute: async (input, ctx) => {
58
106
  const resourceId = scopeResource(ctx.session.scope).resourceId;
59
107
  const verbose = input.includeDiagnostics === true;
60
108
  if (!input.runId) {
61
- const runs = unwrapOrThrow(await queryRunsByAnalysis(pool, resourceId));
62
- if (runs.length === 0) {
63
- return ok({ message: "No runs found for this analysis." });
64
- }
65
- return ok({ runs: runs.map((r) => formatRun(r, verbose)) });
109
+ const page = input.page ?? 1;
110
+ const pageSize = input.pageSize ?? DEFAULT_PAGE_SIZE;
111
+ const offset = (page - 1) * pageSize;
112
+ const result = unwrapOrThrow(await queryRunsForInspection(pool, resourceId, { limit: pageSize, offset }));
113
+ return ok({
114
+ runs: result.runs.map((run) => formatRun(run, verbose)),
115
+ total: result.total,
116
+ page,
117
+ pageSize,
118
+ hasMore: offset + result.runs.length < result.total,
119
+ });
66
120
  }
67
- const run = unwrapOrThrow(await queryRun(pool, input.runId));
121
+ const waitSeconds = input.waitForTerminalSeconds;
122
+ let run = unwrapOrThrow(await queryRun(pool, input.runId));
68
123
  if (!run || run.analysisId !== resourceId) {
69
- return ok({ message: `Run ${input.runId} not found.` });
124
+ return ok({
125
+ inspectionState: "not_found",
126
+ message: `Run ${input.runId} was not found in this analysis.`,
127
+ ...(waitSeconds === undefined ? {} : { wait: { requestedSeconds: waitSeconds, cutoffReached: false } }),
128
+ });
70
129
  }
71
- const stepRows = unwrapOrThrow(await queryStepsByRun(pool, input.runId));
72
- const steps = stepRows.map((s) => ({
73
- stepId: s.stepId,
74
- agentId: s.agentId,
75
- wave: s.wave,
76
- status: s.status,
77
- // No per-step `summaryPath` when the file cannot exist: a seeded row
78
- // (`pending`/`skipped`) never produced output, and the reserved
79
- // `synthesis` phase is not a sandbox step — it writes the run-level
80
- // `synthesis.json`, surfaced as the run's `synthesisPath`, not a
81
- // `{stepId}/output/summary.md`. Emitting the per-step path for it would
82
- // point the agent at a file that never exists.
83
- ...(s.status === "pending" || s.status === "skipped" || s.stepId === SYNTHESIS_STEP_ID
84
- ? {}
85
- : { summaryPath: `runs/${input.runId}/${s.stepId}/output/summary.md` }),
86
- ...(verbose
87
- ? {
88
- durationMs: s.durationMs,
89
- error: s.error,
90
- attempts: s.attempts,
91
- lastErrorClass: s.lastErrorClass,
92
- finishReason: s.finishReason,
93
- hitMaxSteps: s.hitMaxSteps,
130
+ const selfWait = waitSeconds !== undefined && ctx.session.runFrame?.runId === input.runId;
131
+ let cutoffReached = false;
132
+ if (waitSeconds !== undefined && run.status === "running" && !selfWait) {
133
+ const deadline = clock.now() + waitSeconds * 1_000;
134
+ while (run.status === "running") {
135
+ const remaining = deadline - clock.now();
136
+ if (remaining <= 0) {
137
+ cutoffReached = true;
138
+ break;
94
139
  }
95
- : {}),
96
- }));
97
- return ok({ run: formatRun(run, verbose), steps });
140
+ await clock.wait(Math.min(POLL_INTERVAL_MS, remaining), ctx.signal);
141
+ run = unwrapOrThrow(await queryRun(pool, input.runId));
142
+ if (!run || run.analysisId !== resourceId) {
143
+ return ok({
144
+ inspectionState: "not_found",
145
+ message: `Run ${input.runId} was not found in this analysis.`,
146
+ wait: { requestedSeconds: waitSeconds, cutoffReached: false },
147
+ });
148
+ }
149
+ }
150
+ }
151
+ const terminal = isTerminal(run.status);
152
+ const stepRows = unwrapOrThrow(await queryStepsByRun(pool, input.runId));
153
+ const steps = formatSteps(input.runId, stepRows, verbose, terminal);
154
+ const wait = waitSeconds === undefined ? {} : { wait: { requestedSeconds: waitSeconds, cutoffReached } };
155
+ if (run.status === "running") {
156
+ const message = selfWait
157
+ ? "This run is still in progress. Results are not ready, and the enclosing workflow cannot finish while its current step waits for itself."
158
+ : cutoffReached
159
+ ? "This run is still in progress after the requested wait cutoff. Results are not ready; stop polling for this turn."
160
+ : "This run is in progress. Results are not ready.";
161
+ return ok({
162
+ inspectionState: "in_progress",
163
+ message,
164
+ run: formatRun(run, verbose, false),
165
+ steps,
166
+ elapsedMs: Math.max(0, clock.now() - Date.parse(run.startedAt)),
167
+ ...wait,
168
+ ...(selfWait ? { selfWaitPrevented: true } : {}),
169
+ });
170
+ }
171
+ if (run.status === "suspended_insufficient_funds") {
172
+ return ok({
173
+ inspectionState: "suspended",
174
+ message: "This run is suspended and will not progress until it is resumed.",
175
+ run: formatRun(run, verbose, false),
176
+ steps,
177
+ ...wait,
178
+ });
179
+ }
180
+ return ok({
181
+ inspectionState: "terminal",
182
+ message: `This run is terminal with status ${run.status}.`,
183
+ run: formatRun(run, verbose, true),
184
+ steps,
185
+ ...wait,
186
+ });
98
187
  },
99
188
  });
100
189
  }
@@ -22,14 +22,11 @@ import { literatureReviewerPrompt } from "../../prompts/literature-reviewer.js";
22
22
  import { composeSystemPrompt } from "../../agents/system-prompt.js";
23
23
  import { forSubAgent } from "../../auth/types.js";
24
24
  import { defineTool } from "../define-tool.js";
25
- import { createNcbiTools } from "../bio/keys.js";
26
- import { getImpcKoProfileTool } from "../bio/get-impc-ko-profile.js";
27
- import { lookupGoTermTool } from "../bio/lookup-go-term.js";
28
- import { searchBgeeExpressionTool } from "../bio/search-bgee-expression.js";
29
- import { createSearchDgidbTool } from "../bio/search-dgidb.js";
25
+ import { createChemDbTools, createNcbiTools } from "../bio/keys.js";
26
+ import { genePreclinicalProfileTool } from "../bio/gene-preclinical-profile.js";
27
+ import { lookupAnnotationTool } from "../bio/lookup-annotation.js";
30
28
  import { searchGeneTool } from "../bio/search-gene.js";
31
29
  import { searchInteractionsTool } from "../bio/search-interactions.js";
32
- import { searchPathwayTool } from "../bio/search-pathway.js";
33
30
  /** Sub-agent identity — appended to `callPath`, set as `agentId`. */
34
31
  const AGENT_ID = "literature-reviewer";
35
32
  /** Multi-tool research budget — search → read → synthesize over many genes. */
@@ -37,15 +34,14 @@ const REVIEWER_MAX_ITERATIONS = 30;
37
34
  /** Build the `literature_reviewer` delegation tool bound to its provider. */
38
35
  export function createLiteratureReviewerTool(deps) {
39
36
  const ncbi = createNcbiTools(deps.bioKeys);
37
+ const chemDb = createChemDbTools(deps.bioKeys, { ...(deps.logger ? { logger: deps.logger } : {}) });
40
38
  const reviewerTools = [
41
39
  searchGeneTool,
42
- searchPathwayTool,
43
- lookupGoTermTool,
40
+ lookupAnnotationTool,
44
41
  searchInteractionsTool,
45
42
  ncbi.pubmed,
46
- createSearchDgidbTool({ ...(deps.logger ? { logger: deps.logger } : {}) }),
47
- searchBgeeExpressionTool,
48
- getImpcKoProfileTool,
43
+ chemDb.drugGeneInteractions,
44
+ genePreclinicalProfileTool,
49
45
  ];
50
46
  const agent = {
51
47
  id: AGENT_ID,
@@ -115,6 +115,11 @@ export interface ExecuteAnalysisInput {
115
115
  * dependency-satisfied step starts immediately — the legacy fan-out.
116
116
  */
117
117
  readonly budget?: MachineBudget;
118
+ /**
119
+ * Replay-stable run-level synthesis decision. Absent preserves the legacy
120
+ * behavior for workflows persisted before this field existed.
121
+ */
122
+ readonly synthesisEnabled?: boolean;
118
123
  /**
119
124
  * Durable `RunSession` minted at the async edge by `executePlan`.
120
125
  * Carries the run-authorization credential, identity, scope, and `runFrame.runId`.
@@ -204,6 +209,49 @@ export type RunProvenanceEvent = {
204
209
  /** `atMs − run_started.atMs`: the workflow-observed run span in ms. */
205
210
  durationMs: number;
206
211
  };
212
+ /**
213
+ * A step's in-run display state. Wider than the ledger's `StepExecutionStatus`: `queued` is a
214
+ * scheduler-only condition (dependency-satisfied but held for budget capacity) that never reaches a
215
+ * database row, and `skipped` is asserted mid-run for a doomed dependent whose ledger row stays
216
+ * `pending` until the terminal sweep.
217
+ */
218
+ export type StepRuntimeStatus = "pending" | "queued" | "running" | "completed" | "failed" | "skipped";
219
+ /**
220
+ * One step's state inside a {@link RunObservation}. `id` is the plan slug the ledger and the
221
+ * dependency edges use; `name` is the human phrase the plan author wrote, which is the only field
222
+ * that says what the step is FOR. `durationMs` and `error` appear only once the step has settled
223
+ * in a way that produced them.
224
+ */
225
+ export interface RunStepObservation {
226
+ readonly id: string;
227
+ readonly name: string;
228
+ readonly agent: string;
229
+ readonly status: StepRuntimeStatus;
230
+ readonly durationMs?: number;
231
+ readonly error?: string;
232
+ }
233
+ /**
234
+ * A whole-run snapshot handed to {@link ExecuteAnalysisDeps.observeRun}.
235
+ *
236
+ * Deliberately a SNAPSHOT rather than a transition event. The observer is invoked from the
237
+ * workflow body, so DBOS re-execution on recovery re-fires the whole sequence; a re-fired snapshot
238
+ * is idempotent for any consumer that renders from the newest one it has seen, whereas re-fired
239
+ * granular events would force every consumer to rebuild ordering and dedupe by identity. A
240
+ * consumer that misses an invocation loses nothing permanent — the next transition restates the
241
+ * full truth.
242
+ *
243
+ * Derived aggregates (completion counts, run duration) are deliberately ABSENT. They are
244
+ * computable from `steps` and recorded authoritatively on the run's ledger row; carrying them here
245
+ * would mint a second source that can disagree with the ledger a host also reads.
246
+ */
247
+ export interface RunObservation {
248
+ readonly runId: string;
249
+ readonly analysisId: string;
250
+ /** `running` for every mid-flight snapshot; the derived terminal status on the last one. */
251
+ readonly status: ExecuteAnalysisFinalStatus;
252
+ /** Every plan step, including ones that have not started — never a delta. */
253
+ readonly steps: readonly RunStepObservation[];
254
+ }
207
255
  /**
208
256
  * Construction-time deps for the parent workflow. The order-of-operations is
209
257
  * the body's own: it brackets the run, dispatches children, and synthesizes
@@ -230,11 +278,6 @@ export interface ExecuteAnalysisDeps {
230
278
  readonly bioKeys: BioToolKeys;
231
279
  /** Run-level billing-bracket seam (managed: external bracket; OSS: no-op). */
232
280
  readonly runCharge: RunCharge;
233
- /**
234
- * When false, skip run-level synthesis entirely (findings stay empty).
235
- * Config, not a seam. Defaults to true.
236
- */
237
- readonly synthesisEnabled?: boolean;
238
281
  /** Run-authorization seam — the terminal path revokes through `revoke`. */
239
282
  readonly runAuthorizer: RunAuthorizer;
240
283
  /**
@@ -247,6 +290,26 @@ export interface ExecuteAnalysisDeps {
247
290
  * guarded so a throwing observer never fails the run.
248
291
  */
249
292
  readonly emitProvenance?: (event: RunProvenanceEvent) => void;
293
+ /**
294
+ * Optional, fire-and-forget observation of the run's live shape, for a host driving a UI.
295
+ *
296
+ * Independent of {@link emitProvenance} in every direction: neither is implemented in terms of
297
+ * the other, they share no payload, and supplying one does not imply the other. They exist
298
+ * separately because provenance closes a signed, hash-chained record under a single-writer
299
+ * lock, while this is a lossy-tolerant display channel — coupling them would put a UI repaint
300
+ * behind the chain's write discipline and force provenance to carry step names and agent ids it
301
+ * has no reason to record.
302
+ *
303
+ * Invoked directly in the workflow body (NOT wrapped in `DBOS.runStep`) for the same reason
304
+ * `emitProvenance` is: body re-execution on recovery must re-fire it, and a cached step would
305
+ * suppress that. Idempotency comes free for rendering because the payload is a whole-run
306
+ * snapshot; a host taking a DURABLE side effect on one must key it by run id and status itself.
307
+ *
308
+ * Synchronous by signature (`void`, never a promise) so a host needing I/O has to dispatch it
309
+ * rather than await it inside the body's critical path. Every call site is guarded, so a
310
+ * throwing observer never fails the run.
311
+ */
312
+ readonly observeRun?: (observation: RunObservation) => void;
250
313
  }
251
314
  /**
252
315
  * Project the sandbox-step input handed to each child. The parent owns the
@@ -97,6 +97,52 @@ function emitProvenanceGuarded(deps, event) {
97
97
  logger.error("emitProvenance threw", { runId: event.runId, event: event.type, ...logger.errorFields(err) });
98
98
  }
99
99
  }
100
+ /**
101
+ * Build the whole-run snapshot from the workflow input and the scheduler's live step state.
102
+ *
103
+ * Pure, so every emission point produces an identically-shaped payload from one place — a second
104
+ * construction path is exactly how "a transition happened" acquires two definitions that drift.
105
+ * The human `name` joins through `input.planStepById` because `input.steps` is the scheduler's
106
+ * narrow `{id, depends_on}` projection and carries no plan vocabulary.
107
+ */
108
+ function buildRunObservation(args) {
109
+ const { input, runId, status, stepStates } = args;
110
+ return {
111
+ runId,
112
+ analysisId: input.analysisId,
113
+ status,
114
+ steps: input.steps.map((s) => {
115
+ const rt = stepStates.get(s.id);
116
+ return {
117
+ id: s.id,
118
+ name: input.planStepById[s.id]?.name ?? s.id,
119
+ agent: input.agentByStepId[s.id] ?? "unknown",
120
+ // A step absent from the map has not been reached yet; `pending` is its honest state.
121
+ status: rt?.status ?? "pending",
122
+ ...(rt?.durationMs !== undefined ? { durationMs: rt.durationMs } : {}),
123
+ ...(rt?.error !== undefined ? { error: rt.error } : {}),
124
+ };
125
+ }),
126
+ };
127
+ }
128
+ /**
129
+ * Fire the optional run observer, isolating a throwing host callback from the workflow.
130
+ *
131
+ * Deliberately a sibling of {@link emitProvenanceGuarded} rather than a shared helper: the two
132
+ * seams carry unrelated payloads and must stay independently removable, and one shared guard would
133
+ * have to pick a single logger namespace, making it lie about whichever seam it is not named for.
134
+ */
135
+ function observeRunGuarded(deps, observation) {
136
+ if (!deps.observeRun)
137
+ return;
138
+ const logger = (deps.logger ?? createNoopLogger()).named("executeAnalysis.observe");
139
+ try {
140
+ deps.observeRun(observation);
141
+ }
142
+ catch (err) {
143
+ logger.error("observeRun threw", { runId: observation.runId, status: observation.status, ...logger.errorFields(err) });
144
+ }
145
+ }
100
146
  /**
101
147
  * Project the sandbox-step input handed to each child. The parent owns the
102
148
  * level computation and the seed (`prompt`, composed by `composeStepSeed` at
@@ -342,7 +388,7 @@ export async function runExecuteAnalysisBody(input, deps) {
342
388
  // `pending` and the terminal sweep finalizes it to `skipped` — honest
343
389
  // ("never ran, never will"), like any never-dispatched DAG step. Its wave
344
390
  // sits after every DAG level so ledger-ordered readers render it last.
345
- const synthesisEnabled = deps.synthesisEnabled ?? true;
391
+ const synthesisEnabled = input.synthesisEnabled ?? true;
346
392
  const synthesisWave = Math.max(-1, ...levels.values()) + 1;
347
393
  await DBOS.runStep(async () => {
348
394
  const dagRows = input.steps.map((step) => ({
@@ -379,6 +425,10 @@ export async function runExecuteAnalysisBody(input, deps) {
379
425
  stepCount: input.steps.length,
380
426
  atMs: startedAtMs,
381
427
  });
428
+ // Opening snapshot: every step pending. The scheduler's own map does not exist yet, and an
429
+ // empty one yields exactly that — so a host has the run's full shape before the first dispatch
430
+ // rather than learning the step list from whichever transition happens to land first.
431
+ observeRunGuarded(deps, buildRunObservation({ input, runId, status: "running", stepStates: new Map() }));
382
432
  // (2-4) Scheduler loop + failure isolation + pause cascade.
383
433
  const final = await runSchedulerLoop({
384
434
  input,
@@ -403,10 +453,8 @@ export async function runExecuteAnalysisBody(input, deps) {
403
453
  // recovered replay reuses the original instants (same rule as `startedAtMs`).
404
454
  const synthStartedAtMs = await DBOS.now();
405
455
  // Mark the seeded `synthesis` row running. The same upsert the DAG children
406
- // use: it stamps started_at, and it CREATES the row outright when the seed
407
- // never carried it (synthesisEnabled flipped true across a recovery
408
- // redeploy) — the mark self-heals rather than trusting the seed's snapshot
409
- // of the config. Log-don't-fail: a progress row must never fail an
456
+ // use: it stamps started_at, and it creates the row outright if a legacy
457
+ // persisted input did not seed it. Log-don't-fail: a progress row must never fail an
410
458
  // otherwise-healthy run. Cancellation still re-propagates — it is not a
411
459
  // write failure, and swallowing it here would only defer it one operation.
412
460
  try {
@@ -492,6 +540,11 @@ export async function runExecuteAnalysisBody(input, deps) {
492
540
  synthesisOutcome,
493
541
  deps,
494
542
  });
543
+ // Terminal snapshot, fired here rather than inside `collectAndComplete` because this is the
544
+ // first point where BOTH the derived final status and the scheduler's final step map are in
545
+ // hand. It precedes the budget self-cancel below on purpose: that branch raises out of the
546
+ // body, so an observer placed after it would never see a budget-paused run's last state.
547
+ observeRunGuarded(deps, buildRunObservation({ input, runId, status: result.status, stepStates: final.stepStates }));
495
548
  // Synthesis failure takes priority over the budget-exceeded cascade: a run
496
549
  // whose synthesis threw is definitively failed (ERROR), not a resumable
497
550
  // budget pause. Only self-cancel when synthesis succeeded.
@@ -604,7 +657,14 @@ async function runSchedulerLoop(args) {
604
657
  const rt = stepRuntime.get(s.id);
605
658
  const entry = {
606
659
  id: s.id,
607
- name: s.id,
660
+ // `input.steps` is the SCHEDULER's projection — `{id, depends_on}` and nothing
661
+ // else — so the human name has to be joined from the snapshotted plan step. A
662
+ // consumer reading `name` is reading it to say what the step is FOR, which the
663
+ // id (a `T{track}S{step}` slug) cannot answer. Falling back to the id keeps the
664
+ // field populated for a step missing from the snapshot rather than emitting an
665
+ // empty label; `planStepById` is complete by contract (dispatch throws on a
666
+ // miss), so the fallback is unreachable in practice.
667
+ name: input.planStepById[s.id]?.name ?? s.id,
608
668
  agent: input.agentByStepId[s.id] ?? "unknown",
609
669
  status: rt.status,
610
670
  level: levels.get(s.id) ?? 0,
@@ -614,9 +674,18 @@ async function runSchedulerLoop(args) {
614
674
  entry.durationMs = rt.durationMs;
615
675
  if (rt.error !== undefined)
616
676
  entry.error = rt.error;
677
+ // `artifactCount` and `summary` are declared on `DagStepState` and deliberately not
678
+ // set: the parent holds neither at snapshot time, and a placeholder would make a
679
+ // consumer's rendering confidently wrong rather than honestly incomplete.
617
680
  return entry;
618
681
  }),
619
682
  });
683
+ // The host observer rides the DAG emission rather than getting its own instrumentation
684
+ // points: these call sites already ARE the complete set of run-state transitions, and a
685
+ // second set would drift from this one. Status is `running` here by construction — the
686
+ // scheduler loop only emits while it is still scheduling; the terminal snapshot is fired by
687
+ // the body once `collectAndComplete` has derived the final status.
688
+ observeRunGuarded(deps, buildRunObservation({ input, runId, status: "running", stepStates: stepRuntime }));
620
689
  };
621
690
  const dbosParentId = DBOS.workflowID ?? workflowId;
622
691
  const dispatchReady = async () => {
@@ -843,7 +912,7 @@ async function runSchedulerLoop(args) {
843
912
  });
844
913
  }
845
914
  await drainBudgetExceededNotifications(budgetExceededChildIds);
846
- return { completed, failed, canceled, budgetExceeded, failureReason };
915
+ return { completed, failed, canceled, budgetExceeded, failureReason, stepStates: stepRuntime };
847
916
  }
848
917
  async function drainBudgetExceededNotifications(childIds) {
849
918
  while (true) {
@@ -33,7 +33,7 @@ import { unwrapOrThrow } from "../lib/result.js";
33
33
  import { isBudgetExceeded } from "../loop/budget-exceeded.js";
34
34
  import { runAgent } from "../loop/run-agent.js";
35
35
  import { durableStep } from "../loop/run-step.js";
36
- import { activityForTool, applyTreeDelta, isChatDataPart, sandboxTreeDelta } from "../sandbox/sandbox-step-translate.js";
36
+ import { activityForTool, applyTreeDelta, isChatDataPart, sandboxTreeDelta, stepPartId } from "../sandbox/sandbox-step-translate.js";
37
37
  import { forSubAgent } from "../auth/types.js";
38
38
  import { walkStepArtifacts } from "../execution/post-step.js";
39
39
  import { collectStepOutputs, generateStepFileMetadata, generateStepSummaryAndWrite, reconcileAndRegisterStepArtifacts, vectorIndexStepOutputs, } from "../execution/post-step-pipeline.js";
@@ -61,16 +61,6 @@ function nextFunctionIdFactory() {
61
61
  let n = 0;
62
62
  return () => `fn-${(n++).toString(36)}`;
63
63
  }
64
- /**
65
- * Stable reconciliation id for a step's reconciling parts
66
- * (`data-step-activity`, `data-step-file-tree`). One id per (runId, stepId)
67
- * so the run-stream fold collapses every phase transition latest-wins onto
68
- * a single frame. The client keys these by `stepId`, but the server fold
69
- * keys by part `id`, so the id MUST be unique per step within the run.
70
- */
71
- function stepPartId(kind, runId, stepId) {
72
- return `${kind}-${runId}-${stepId}`;
73
- }
74
64
  /**
75
65
  * Map an internal step failure to a user-safe string. Internal detail (file
76
66
  * paths, content hashes, managed-root rejection reasons, stack traces) stays in the pod
@@ -43,9 +43,9 @@ export declare const DrugsInClassOutputSchema: z.ZodObject<{
43
43
  firstApproval: z.ZodNullable<z.ZodNumber>;
44
44
  moleculeType: z.ZodNullable<z.ZodString>;
45
45
  sources: z.ZodArray<z.ZodEnum<{
46
- chembl: "chembl";
47
46
  dgidb: "dgidb";
48
47
  drugbank: "drugbank";
48
+ chembl: "chembl";
49
49
  }>>;
50
50
  }, z.core.$strip>>;
51
51
  total: z.ZodNumber;
@@ -1707,8 +1707,8 @@ export declare const Phase4OutputSchema: z.ZodObject<{
1707
1707
  medium: "medium";
1708
1708
  low: "low";
1709
1709
  absent: "absent";
1710
- not_loaded: "not_loaded";
1711
1710
  no_data: "no_data";
1711
+ not_loaded: "not_loaded";
1712
1712
  }>;
1713
1713
  }, z.core.$strip>>;
1714
1714
  per_species_coverage: z.ZodRecord<z.ZodString, z.ZodEnum<{
@@ -53,9 +53,9 @@ export declare const Phase2AggregateInputSchema: z.ZodObject<{
53
53
  firstApproval: z.ZodNullable<z.ZodNumber>;
54
54
  moleculeType: z.ZodNullable<z.ZodString>;
55
55
  sources: z.ZodArray<z.ZodEnum<{
56
- chembl: "chembl";
57
56
  dgidb: "dgidb";
58
57
  drugbank: "drugbank";
58
+ chembl: "chembl";
59
59
  }>>;
60
60
  }, z.core.$strip>>;
61
61
  total: z.ZodNumber;
@@ -672,9 +672,9 @@ export declare const Phase2BundleSchema: z.ZodObject<{
672
672
  firstApproval: z.ZodNullable<z.ZodNumber>;
673
673
  moleculeType: z.ZodNullable<z.ZodString>;
674
674
  sources: z.ZodArray<z.ZodEnum<{
675
- chembl: "chembl";
676
675
  dgidb: "dgidb";
677
676
  drugbank: "drugbank";
677
+ chembl: "chembl";
678
678
  }>>;
679
679
  }, z.core.$strip>>;
680
680
  total: z.ZodNumber;
@@ -757,9 +757,9 @@ export declare const Phase3BundleSchema: z.ZodObject<{
757
757
  firstApproval: z.ZodNullable<z.ZodNumber>;
758
758
  moleculeType: z.ZodNullable<z.ZodString>;
759
759
  sources: z.ZodArray<z.ZodEnum<{
760
- chembl: "chembl";
761
760
  dgidb: "dgidb";
762
761
  drugbank: "drugbank";
762
+ chembl: "chembl";
763
763
  }>>;
764
764
  }, z.core.$strip>>;
765
765
  total: z.ZodNumber;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Host-agnostic bioinformatics agent harness \u2014 hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,9 +0,0 @@
1
- /**
2
- * Ephemeral Executor — read-only sandbox agent for quick computations.
3
- * Cannot create files; step conventions don't apply.
4
- */
5
- import type { AgentDefinition } from "../../loop/types.js";
6
- import { type SandboxAgentDeps } from "./shared.js";
7
- import type { AgentMeta } from "./types.js";
8
- export declare const meta: AgentMeta;
9
- export declare function createEphemeralExecutorAgent(deps: SandboxAgentDeps): AgentDefinition;