@inflexa-ai/harness 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/agents/conversation-agent.js +23 -16
  2. package/dist/agents/sandbox/cheminformatics-agent.js +1 -1
  3. package/dist/agents/sandbox/chromatin-agent.js +1 -1
  4. package/dist/agents/sandbox/dna-methylation-agent.js +1 -1
  5. package/dist/agents/sandbox/drug-repurposing-agent.js +5 -10
  6. package/dist/agents/sandbox/enrichment-agent.js +1 -1
  7. package/dist/agents/sandbox/genomic-variant-agent.js +1 -11
  8. package/dist/agents/sandbox/immune-profiling-agent.js +1 -1
  9. package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -1
  10. package/dist/agents/sandbox/network-agent.js +1 -1
  11. package/dist/agents/sandbox/pkpd-clinical-response-agent.js +3 -6
  12. package/dist/agents/sandbox/scientific-executor.js +1 -1
  13. package/dist/agents/sandbox/shared.js +7 -13
  14. package/dist/agents/sandbox/translational-safety-agent.js +4 -7
  15. package/dist/agents/sandbox/types.d.ts +5 -5
  16. package/dist/agents/sandbox/types.js +4 -4
  17. package/dist/app/chat-turn.js +4 -1
  18. package/dist/app/message-assembly.d.ts +5 -2
  19. package/dist/app/message-assembly.js +7 -2
  20. package/dist/app/run-activity.d.ts +13 -0
  21. package/dist/app/run-activity.js +51 -0
  22. package/dist/contracts/target-dossier.d.ts +10 -10
  23. package/dist/execution/report-runner.js +1 -1
  24. package/dist/execution/run-event-parts.d.ts +44 -0
  25. package/dist/execution/run-event-parts.js +0 -0
  26. package/dist/execution/run-event-stream.d.ts +91 -0
  27. package/dist/execution/run-event-stream.js +212 -0
  28. package/dist/execution/run-synthesis.js +1 -1
  29. package/dist/index.d.ts +5 -2
  30. package/dist/index.js +20 -1
  31. package/dist/input-sanitization.d.ts +14 -4
  32. package/dist/input-sanitization.js +27 -4
  33. package/dist/lib/storage.js +4 -1
  34. package/dist/loop/budget-exceeded.d.ts +18 -2
  35. package/dist/loop/budget-exceeded.js +49 -2
  36. package/dist/loop/run-agent.d.ts +22 -1
  37. package/dist/loop/run-agent.js +50 -5
  38. package/dist/loop/run-to-terminal.d.ts +0 -2
  39. package/dist/loop/run-to-terminal.js +8 -1
  40. package/dist/memory/ai-sdk-message-storage.d.ts +37 -4
  41. package/dist/memory/ai-sdk-message-storage.js +45 -4
  42. package/dist/memory/content-to-cortex.js +19 -3
  43. package/dist/memory/message-backfill.js +1 -1
  44. package/dist/memory/thread-history.js +34 -8
  45. package/dist/prompts/conversation.d.ts +1 -1
  46. package/dist/prompts/conversation.js +15 -6
  47. package/dist/prompts/literature-reviewer.d.ts +1 -1
  48. package/dist/prompts/literature-reviewer.js +16 -14
  49. package/dist/prompts/planner.js +15 -20
  50. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  51. package/dist/prompts/sandbox/drug-repurposing-agent.js +31 -22
  52. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  53. package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -10
  54. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  55. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +8 -7
  56. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  57. package/dist/prompts/sandbox/translational-safety-agent.js +20 -14
  58. package/dist/prompts/synthesis-agent.d.ts +1 -1
  59. package/dist/prompts/synthesis-agent.js +7 -7
  60. package/dist/providers/errors.d.ts +7 -0
  61. package/dist/providers/errors.js +88 -3
  62. package/dist/runtime/pools.d.ts +15 -1
  63. package/dist/runtime/pools.js +15 -1
  64. package/dist/sandbox/sandbox-step-translate.d.ts +15 -2
  65. package/dist/sandbox/sandbox-step-translate.js +17 -2
  66. package/dist/schemas/workflow-state.d.ts +2 -2
  67. package/dist/schemas/workflow-state.js +2 -2
  68. package/dist/state/data-profile.d.ts +53 -1
  69. package/dist/state/data-profile.js +66 -6
  70. package/dist/state/index.d.ts +3 -3
  71. package/dist/state/index.js +2 -2
  72. package/dist/state/init.js +37 -2
  73. package/dist/state/runs.d.ts +39 -0
  74. package/dist/state/runs.js +103 -0
  75. package/dist/tasks/data-profile-activity.d.ts +87 -0
  76. package/dist/tasks/data-profile-activity.js +56 -0
  77. package/dist/tasks/data-profile.js +66 -4
  78. package/dist/tools/bio/chembl.d.ts +1 -1
  79. package/dist/tools/bio/chembl.js +51 -54
  80. package/dist/tools/bio/comptox.js +35 -33
  81. package/dist/tools/bio/drug-gene-interactions.d.ts +76 -0
  82. package/dist/tools/bio/drug-gene-interactions.js +175 -0
  83. package/dist/tools/bio/gene-disease-evidence.d.ts +61 -0
  84. package/dist/tools/bio/gene-disease-evidence.js +187 -0
  85. package/dist/tools/bio/gene-preclinical-profile.d.ts +58 -0
  86. package/dist/tools/bio/gene-preclinical-profile.js +125 -0
  87. package/dist/tools/bio/index.d.ts +23 -16
  88. package/dist/tools/bio/index.js +29 -22
  89. package/dist/tools/bio/keys.d.ts +16 -9
  90. package/dist/tools/bio/keys.js +22 -13
  91. package/dist/tools/bio/lookup-annotation.d.ts +48 -0
  92. package/dist/tools/bio/lookup-annotation.js +173 -0
  93. package/dist/tools/bio/opentargets.d.ts +7 -11
  94. package/dist/tools/bio/opentargets.js +24 -35
  95. package/dist/tools/bio/pubchem.js +6 -4
  96. package/dist/tools/bio/pubmed.d.ts +15 -7
  97. package/dist/tools/bio/pubmed.js +79 -20
  98. package/dist/tools/bio/search-clinical-trials.d.ts +20 -1
  99. package/dist/tools/bio/search-clinical-trials.js +26 -5
  100. package/dist/tools/bio/search-faers.js +9 -3
  101. package/dist/tools/bio/search-gene.d.ts +0 -1
  102. package/dist/tools/bio/search-gene.js +4 -5
  103. package/dist/tools/bio/search-geo-datasets.js +10 -4
  104. package/dist/tools/bio/search-interactions.d.ts +8 -4
  105. package/dist/tools/bio/search-interactions.js +28 -12
  106. package/dist/tools/bio/target-safety.d.ts +59 -0
  107. package/dist/tools/bio/target-safety.js +191 -0
  108. package/dist/tools/execute-analysis.d.ts +1 -0
  109. package/dist/tools/execute-analysis.js +5 -5
  110. package/dist/tools/lib/dgidb-client.d.ts +49 -0
  111. package/dist/tools/lib/dgidb-client.js +168 -0
  112. package/dist/tools/lib/disgenet-client.d.ts +56 -0
  113. package/dist/tools/lib/disgenet-client.js +59 -0
  114. package/dist/tools/lib/drugbank-client.d.ts +83 -0
  115. package/dist/tools/lib/drugbank-client.js +95 -0
  116. package/dist/tools/lib/ensembl-client.d.ts +4 -1
  117. package/dist/tools/lib/ensembl-client.js +5 -3
  118. package/dist/tools/lib/gwas-catalog-client.d.ts +60 -0
  119. package/dist/tools/lib/gwas-catalog-client.js +136 -0
  120. package/dist/tools/lib/pharmgkb-client.d.ts +22 -0
  121. package/dist/tools/lib/pharmgkb-client.js +32 -0
  122. package/dist/tools/lib/pubmed-client.d.ts +57 -4
  123. package/dist/tools/lib/pubmed-client.js +57 -3
  124. package/dist/tools/lib/string-client.d.ts +25 -4
  125. package/dist/tools/lib/string-client.js +22 -5
  126. package/dist/tools/research/generate-plan.d.ts +16 -9
  127. package/dist/tools/research/generate-plan.js +115 -69
  128. package/dist/tools/research/inspect-data-profile.d.ts +31 -6
  129. package/dist/tools/research/inspect-data-profile.js +23 -7
  130. package/dist/tools/research/inspect-run.d.ts +33 -15
  131. package/dist/tools/research/inspect-run.js +155 -66
  132. package/dist/tools/research/literature-reviewer.js +7 -11
  133. package/dist/workflows/execute-analysis.d.ts +63 -0
  134. package/dist/workflows/execute-analysis.js +73 -2
  135. package/dist/workflows/sandbox-step.js +1 -11
  136. package/dist/workflows/target-assessment/decisions/index.d.ts +1 -1
  137. package/dist/workflows/target-assessment/phase4-assemble.d.ts +1 -1
  138. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +2 -2
  139. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
  140. package/package.json +1 -1
  141. package/dist/tools/bio/check-safety-panel.d.ts +0 -35
  142. package/dist/tools/bio/check-safety-panel.js +0 -76
  143. package/dist/tools/bio/get-impc-ko-profile.d.ts +0 -9
  144. package/dist/tools/bio/get-impc-ko-profile.js +0 -23
  145. package/dist/tools/bio/lookup-go-term.d.ts +0 -31
  146. package/dist/tools/bio/lookup-go-term.js +0 -116
  147. package/dist/tools/bio/search-bgee-expression.d.ts +0 -10
  148. package/dist/tools/bio/search-bgee-expression.js +0 -30
  149. package/dist/tools/bio/search-clinvar.d.ts +0 -13
  150. package/dist/tools/bio/search-clinvar.js +0 -28
  151. package/dist/tools/bio/search-dgidb.d.ts +0 -53
  152. package/dist/tools/bio/search-dgidb.js +0 -213
  153. package/dist/tools/bio/search-disgenet.d.ts +0 -30
  154. package/dist/tools/bio/search-disgenet.js +0 -84
  155. package/dist/tools/bio/search-drugbank.d.ts +0 -38
  156. package/dist/tools/bio/search-drugbank.js +0 -109
  157. package/dist/tools/bio/search-gwas-catalog.d.ts +0 -33
  158. package/dist/tools/bio/search-gwas-catalog.js +0 -172
  159. package/dist/tools/bio/search-pathway.d.ts +0 -12
  160. package/dist/tools/bio/search-pathway.js +0 -32
  161. package/dist/tools/bio/search-pharmgkb.d.ts +0 -17
  162. package/dist/tools/bio/search-pharmgkb.js +0 -52
@@ -1,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,
@@ -209,6 +209,49 @@ export type RunProvenanceEvent = {
209
209
  /** `atMs − run_started.atMs`: the workflow-observed run span in ms. */
210
210
  durationMs: number;
211
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
+ }
212
255
  /**
213
256
  * Construction-time deps for the parent workflow. The order-of-operations is
214
257
  * the body's own: it brackets the run, dispatches children, and synthesizes
@@ -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
@@ -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,
@@ -490,6 +540,11 @@ export async function runExecuteAnalysisBody(input, deps) {
490
540
  synthesisOutcome,
491
541
  deps,
492
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 }));
493
548
  // Synthesis failure takes priority over the budget-exceeded cascade: a run
494
549
  // whose synthesis threw is definitively failed (ERROR), not a resumable
495
550
  // budget pause. Only self-cancel when synthesis succeeded.
@@ -602,7 +657,14 @@ async function runSchedulerLoop(args) {
602
657
  const rt = stepRuntime.get(s.id);
603
658
  const entry = {
604
659
  id: s.id,
605
- 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,
606
668
  agent: input.agentByStepId[s.id] ?? "unknown",
607
669
  status: rt.status,
608
670
  level: levels.get(s.id) ?? 0,
@@ -612,9 +674,18 @@ async function runSchedulerLoop(args) {
612
674
  entry.durationMs = rt.durationMs;
613
675
  if (rt.error !== undefined)
614
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.
615
680
  return entry;
616
681
  }),
617
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 }));
618
689
  };
619
690
  const dbosParentId = DBOS.workflowID ?? workflowId;
620
691
  const dispatchReady = async () => {
@@ -841,7 +912,7 @@ async function runSchedulerLoop(args) {
841
912
  });
842
913
  }
843
914
  await drainBudgetExceededNotifications(budgetExceededChildIds);
844
- return { completed, failed, canceled, budgetExceeded, failureReason };
915
+ return { completed, failed, canceled, budgetExceeded, failureReason, stepStates: stepRuntime };
845
916
  }
846
917
  async function drainBudgetExceededNotifications(childIds) {
847
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.13.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,35 +0,0 @@
1
- /**
2
- * checkSafetyPanel — look up identifiers against the curated
3
- * secondary-pharmacology safety target panel.
4
- */
5
- export declare const checkSafetyPanelTool: import("../define-tool.js").Tool<{
6
- identifiers: string[];
7
- identifier_type: "auto" | "gene_symbol" | "uniprot" | "chembl_id";
8
- filter_organ?: "hematologic" | "metabolic" | "immune" | "hepatic" | "cns" | "renal" | "respiratory" | "cardiac" | "gi" | undefined;
9
- }, {
10
- matches: {
11
- input: string;
12
- matched_entry: {
13
- chembl_id: string;
14
- gene_symbol: string;
15
- uniprot: string;
16
- name: string;
17
- organ_system: "hematologic" | "metabolic" | "immune" | "hepatic" | "cns" | "renal" | "respiratory" | "cardiac" | "gi";
18
- clinical_consequence: string;
19
- severity: "high" | "medium" | "low";
20
- references: string[];
21
- ensembl_gene_id?: string | undefined;
22
- } | null;
23
- }[];
24
- summary: {
25
- total_input: number;
26
- matched: number;
27
- by_severity: {
28
- high: number;
29
- medium: number;
30
- low: number;
31
- };
32
- by_organ: Record<string, number>;
33
- };
34
- panel_version: string;
35
- }>;