@inflexa-ai/harness 0.11.0 → 0.13.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 (66) hide show
  1. package/dist/agents/conversation-agent.d.ts +9 -15
  2. package/dist/agents/conversation-agent.js +9 -15
  3. package/dist/agents/sandbox/index.js +0 -3
  4. package/dist/agents/sandbox/shared.d.ts +2 -2
  5. package/dist/agents/sandbox/shared.js +1 -1
  6. package/dist/config/resource-limits.d.ts +0 -7
  7. package/dist/config/resource-limits.js +0 -1
  8. package/dist/execution/dbos-run-launcher.js +1 -26
  9. package/dist/execution/run-authorizer.d.ts +1 -1
  10. package/dist/execution/run-authorizer.js +1 -1
  11. package/dist/execution/run-launcher.d.ts +2 -18
  12. package/dist/execution/run-launcher.js +2 -4
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.js +4 -6
  15. package/dist/loop/run-agent.js +1 -0
  16. package/dist/memory/reconstruct-cards.d.ts +1 -1
  17. package/dist/memory/reconstruct-cards.js +7 -5
  18. package/dist/memory/thread-history.js +39 -1
  19. package/dist/prompts/conversation.d.ts +1 -1
  20. package/dist/prompts/conversation.js +27 -10
  21. package/dist/prompts/planner.d.ts +3 -0
  22. package/dist/prompts/planner.js +1 -1
  23. package/dist/prompts/sandbox-standards.d.ts +4 -4
  24. package/dist/prompts/sandbox-standards.js +5 -5
  25. package/dist/runtime/assemble.d.ts +5 -10
  26. package/dist/runtime/assemble.js +0 -4
  27. package/dist/runtime/boot.d.ts +2 -2
  28. package/dist/runtime/boot.js +1 -1
  29. package/dist/runtime/dbos.d.ts +4 -5
  30. package/dist/runtime/dbos.js +6 -7
  31. package/dist/sandbox/mount-plan.d.ts +1 -1
  32. package/dist/sandbox/sandbox-error.d.ts +1 -1
  33. package/dist/sandbox/sandbox-error.js +1 -1
  34. package/dist/sandbox/types.d.ts +1 -1
  35. package/dist/schemas/validate-plan.d.ts +1 -1
  36. package/dist/state/index.d.ts +4 -4
  37. package/dist/state/index.js +2 -2
  38. package/dist/state/plans.js +1 -1
  39. package/dist/state/runs.d.ts +10 -0
  40. package/dist/state/runs.js +47 -0
  41. package/dist/tools/__fixtures__/tool-context.js +1 -0
  42. package/dist/tools/ad-hoc-router.d.ts +31 -0
  43. package/dist/tools/ad-hoc-router.js +132 -0
  44. package/dist/tools/analysis-invocation.d.ts +4 -0
  45. package/dist/tools/analysis-invocation.js +17 -0
  46. package/dist/tools/bio/search-geo-datasets.js +2 -1
  47. package/dist/tools/define-tool.d.ts +6 -2
  48. package/dist/tools/define-tool.js +1 -1
  49. package/dist/tools/execute-analysis.d.ts +43 -0
  50. package/dist/tools/execute-analysis.js +252 -0
  51. package/dist/tools/iterate-report.js +7 -6
  52. package/dist/tools/research/generate-plan.js +1 -1
  53. package/dist/tools/workspace/list-files.js +4 -3
  54. package/dist/workflows/execute-analysis.d.ts +5 -5
  55. package/dist/workflows/execute-analysis.js +3 -5
  56. package/package.json +1 -1
  57. package/dist/agents/sandbox/ephemeral-executor.d.ts +0 -9
  58. package/dist/agents/sandbox/ephemeral-executor.js +0 -20
  59. package/dist/execution/ephemeral-runner.d.ts +0 -98
  60. package/dist/execution/ephemeral-runner.js +0 -189
  61. package/dist/prompts/sandbox/ephemeral-executor.d.ts +0 -1
  62. package/dist/prompts/sandbox/ephemeral-executor.js +0 -66
  63. package/dist/tools/execute-plan.d.ts +0 -64
  64. package/dist/tools/execute-plan.js +0 -211
  65. package/dist/tools/run-ephemeral.d.ts +0 -33
  66. package/dist/tools/run-ephemeral.js +0 -65
@@ -11,13 +11,10 @@
11
11
  * The system prompt is static composition — SOUL kernel + SOUL conversational
12
12
  * + the conversation prompt — with no processor pipeline.
13
13
  *
14
- * `executePlan` is wired here — it launches the DBOS `executeAnalysis` parent
14
+ * `execute_analysis` is wired here — it launches the DBOS `executeAnalysis` parent
15
15
  * workflow under `workflowId = runId` (the bare run UUID) through the
16
16
  * `RunLauncher` seam and returns the runId (results are pull-only via
17
- * `inspectRun` on a later turn). `run_ephemeral`
18
- * is wired here too — it mints a run authorization, starts the turn-scoped DBOS
19
- * `ephemeral` workflow, and awaits the result inline; chat disconnect cancels
20
- * the workflow and it is never recovered. Report authoring is wired here as the
17
+ * `inspectRun` on a later turn). Report authoring is wired here as the
21
18
  * pair `plan_report` (returns the report-brief schema + authoring rules
22
19
  * just-in-time as its result) + `submit_report` (validates the composed brief
23
20
  * and drives in-process Nunjucks rendering via the `report-builder` agent, no
@@ -42,7 +39,6 @@ import type { EnvironmentStorePaths } from "../config/environment-stores.js";
42
39
  import type { RunAuthorizer } from "../execution/run-authorizer.js";
43
40
  import type { RunLauncher } from "../execution/run-launcher.js";
44
41
  import { type SubmitReportDeps } from "../tools/iterate-report.js";
45
- import type { EphemeralWorkflowInput, EphemeralResult } from "../execution/ephemeral-runner.js";
46
42
  import type { Logger } from "../lib/logger.js";
47
43
  /** Canonical agent id — the single source of truth. */
48
44
  export declare const CONVERSATION_AGENT_ID: "conversation-agent";
@@ -65,28 +61,26 @@ export interface ConversationAgentDeps extends EnvironmentStorePaths {
65
61
  readonly workspaceFs: WorkspaceFilesystem;
66
62
  /** Model id — provenance / metric label; the provider owns the wire model. */
67
63
  readonly model: string;
64
+ /** Dedicated lower-cost model/provider for ad hoc specialist routing. */
65
+ readonly utilityProvider: ChatProvider;
66
+ readonly utilityModel: string;
68
67
  /**
69
68
  * Registered `executeAnalysis` workflow callable — produced by
70
- * `registerExecuteAnalysis` (wired by `assembleCoreRuntime`). `executePlan`
69
+ * `registerExecuteAnalysis` (wired by `assembleCoreRuntime`). `execute_analysis`
71
70
  * launches it through the `RunLauncher` seam to start the run.
72
71
  */
73
72
  readonly executeAnalysisWorkflow: (input: ExecuteAnalysisInput) => Promise<ExecuteAnalysisResult>;
74
- /**
75
- * Registered `ephemeral` workflow — `run_ephemeral` mints a run authorization,
76
- * starts it, and awaits the result inline within the chat turn.
77
- */
78
- readonly ephemeralWorkflow: (input: EphemeralWorkflowInput) => Promise<EphemeralResult>;
79
73
  /** Workspace-root resolution seam (see workspace/paths.ts). */
80
74
  readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
81
75
  /**
82
76
  * Async-edge run-authorization seam — injected, not constructed here.
83
- * `execute_plan` and `run_ephemeral` turn the caller's opaque auth into a
77
+ * `execute_analysis` turns the caller's opaque auth into a
84
78
  * durable `RunSession` through it. The managed root injects the platform
85
79
  * realization; the OSS root injects the local one.
86
80
  */
87
81
  readonly runAuthorizer: RunAuthorizer;
88
82
  /**
89
- * Durable-run launch seam — `execute_plan` and `run_ephemeral` start their
83
+ * Durable-run launch seam — `execute_analysis` starts its
90
84
  * workflows through it so the durability engine stays out of the tools.
91
85
  */
92
86
  readonly runLauncher: RunLauncher;
@@ -107,7 +101,7 @@ export interface ConversationAgentDeps extends EnvironmentStorePaths {
107
101
  /**
108
102
  * Host resource policy — per-step ceilings + machine budget. `generate_plan`
109
103
  * states the ceilings to the planner and validates against them;
110
- * `execute_plan` snapshots the budget into the workflow input. Absent,
104
+ * `execute_analysis` snapshots the budget into the workflow input. Absent,
111
105
  * planning guidance and scheduling keep their legacy behavior.
112
106
  */
113
107
  readonly resourcePolicy?: ResourcePolicy;
@@ -11,13 +11,10 @@
11
11
  * The system prompt is static composition — SOUL kernel + SOUL conversational
12
12
  * + the conversation prompt — with no processor pipeline.
13
13
  *
14
- * `executePlan` is wired here — it launches the DBOS `executeAnalysis` parent
14
+ * `execute_analysis` is wired here — it launches the DBOS `executeAnalysis` parent
15
15
  * workflow under `workflowId = runId` (the bare run UUID) through the
16
16
  * `RunLauncher` seam and returns the runId (results are pull-only via
17
- * `inspectRun` on a later turn). `run_ephemeral`
18
- * is wired here too — it mints a run authorization, starts the turn-scoped DBOS
19
- * `ephemeral` workflow, and awaits the result inline; chat disconnect cancels
20
- * the workflow and it is never recovered. Report authoring is wired here as the
17
+ * `inspectRun` on a later turn). Report authoring is wired here as the
21
18
  * pair `plan_report` (returns the report-brief schema + authoring rules
22
19
  * just-in-time as its result) + `submit_report` (validates the composed brief
23
20
  * and drives in-process Nunjucks rendering via the `report-builder` agent, no
@@ -41,8 +38,7 @@ import { showUserTool } from "../tools/display/index.js";
41
38
  import { createUpdateWorkingMemoryTool } from "../tools/memory/index.js";
42
39
  import { createListAvailablePackagesTool } from "../tools/sandbox/list-available-packages.js";
43
40
  import { createListAvailableRefsTool } from "../tools/sandbox/list-available-refs.js";
44
- import { createExecutePlanTool } from "../tools/execute-plan.js";
45
- import { createRunEphemeralTool } from "../tools/run-ephemeral.js";
41
+ import { createExecuteAnalysisTool } from "../tools/execute-analysis.js";
46
42
  import { planReportTool, createReportSubmitTool } from "../tools/iterate-report.js";
47
43
  /** Canonical agent id — the single source of truth. */
48
44
  export const CONVERSATION_AGENT_ID = "conversation-agent";
@@ -50,7 +46,7 @@ export const CONVERSATION_AGENT_ID = "conversation-agent";
50
46
  const CONVERSATION_MAX_ITERATIONS = 50;
51
47
  /** Build the conversation `AgentDefinition` with every tool fully dep-bound. */
52
48
  export function createConversationAgent(deps) {
53
- const { provider, pool, embedding, workspaceFs, model, executeAnalysisWorkflow, ephemeralWorkflow, resolveWorkspaceRoot, runAuthorizer, runLauncher, createPreviewPublisher, bioKeys, templatesDir, skillsDir, chrome, resourcePolicy, hostTools, refStorePath, packagesFile, } = deps;
49
+ const { provider, pool, embedding, workspaceFs, model, utilityProvider, utilityModel, executeAnalysisWorkflow, resolveWorkspaceRoot, runAuthorizer, runLauncher, createPreviewPublisher, bioKeys, templatesDir, skillsDir, chrome, resourcePolicy, hostTools, refStorePath, packagesFile, } = deps;
54
50
  const workingMemory = createWorkingMemory(pool);
55
51
  const ncbi = createNcbiTools(bioKeys);
56
52
  const chemDb = createChemDbTools(bioKeys);
@@ -90,24 +86,22 @@ export function createConversationAgent(deps) {
90
86
  createListAvailableRefsTool({ ...(refStorePath ? { refStorePath } : {}) }),
91
87
  // What is importable inside a sandbox. Reads the same manifest a sandbox agent
92
88
  // reads, host-side — so "is scanpy available?" is a manifest lookup here rather
93
- // than a whole ephemeral container spun up to run one import.
89
+ // than launching analysis computation to run one import.
94
90
  createListAvailablePackagesTool({ ...(packagesFile ? { packagesFile } : {}) }),
95
91
  // Execution.
96
92
  createInspectRunTool(pool),
97
93
  // The dataset's own record. No file backs it — the DB row is the only copy.
98
94
  createInspectDataProfileTool(pool),
99
95
  createGeneratePlanTool({ provider, pool, model, resourcePolicy, ...(refStorePath ? { refStorePath } : {}), ...(packagesFile ? { packagesFile } : {}) }),
100
- createExecutePlanTool({
96
+ createExecuteAnalysisTool({
101
97
  pool,
102
98
  executeAnalysisWorkflow,
103
99
  runAuthorizer,
104
100
  runLauncher,
105
101
  resourcePolicy,
106
- }),
107
- createRunEphemeralTool({
108
- workflow: ephemeralWorkflow,
109
- runAuthorizer,
110
- runLauncher,
102
+ utilityProvider,
103
+ utilityModel,
104
+ ...(deps.logger ? { logger: deps.logger } : {}),
111
105
  }),
112
106
  // Report authoring: the always-on `plan_report` trigger delivers the
113
107
  // heavy brief schema + rules just-in-time as its result; `submit_report`
@@ -18,7 +18,6 @@ import { createDataProfilerAgent, meta as dataProfilerMeta } from "./data-profil
18
18
  import { createDnaMethylationAgent, meta as dnaMethylationMeta } from "./dna-methylation-agent.js";
19
19
  import { createDrugRepurposingAgent, meta as drugRepurposingMeta } from "./drug-repurposing-agent.js";
20
20
  import { createEnrichmentAgent, meta as enrichmentMeta } from "./enrichment-agent.js";
21
- import { createEphemeralExecutorAgent, meta as ephemeralExecutorMeta } from "./ephemeral-executor.js";
22
21
  import { createGenomicVariantAgent, meta as genomicVariantMeta } from "./genomic-variant-agent.js";
23
22
  import { createImmuneProfilingAgent, meta as immuneProfilingMeta } from "./immune-profiling-agent.js";
24
23
  import { createMetabolomicsAgent, meta as metabolomicsMeta } from "./metabolomics-agent.js";
@@ -62,7 +61,6 @@ export const SANDBOX_AGENT_META = {
62
61
  [immuneProfilingMeta.id]: immuneProfilingMeta,
63
62
  [drugRepurposingMeta.id]: drugRepurposingMeta,
64
63
  [scientificExecutorMeta.id]: scientificExecutorMeta,
65
- [ephemeralExecutorMeta.id]: ephemeralExecutorMeta,
66
64
  };
67
65
  /**
68
66
  * Build every sandbox `AgentDefinition` from the shared deps. The returned
@@ -91,6 +89,5 @@ export function createSandboxAgents(deps) {
91
89
  [immuneProfilingMeta.id]: createImmuneProfilingAgent(deps),
92
90
  [drugRepurposingMeta.id]: createDrugRepurposingAgent(deps),
93
91
  [scientificExecutorMeta.id]: createScientificExecutorAgent(deps),
94
- [ephemeralExecutorMeta.id]: createEphemeralExecutorAgent(deps),
95
92
  };
96
93
  }
@@ -78,7 +78,7 @@ export interface SandboxAgentDeps extends EnvironmentStorePaths {
78
78
  * Per-run blocker cell (see the harness-sandbox-agents spec). When present, the agent gets a
79
79
  * `report_blocker` tool that records `{ kind: "blocker", reason }` into it;
80
80
  * the sandbox-step body reads `holder.outcome` after the loop. Omit for
81
- * agents that have no terminal status to declare (ephemeral, data profiler).
81
+ * agents that have no terminal status to declare (such as the data profiler).
82
82
  */
83
83
  readonly blockerHolder?: BlockerHolder;
84
84
  }
@@ -89,7 +89,7 @@ export interface SandboxAgentPromptOptions {
89
89
  /**
90
90
  * Enforced read-only: omit `write_file` + `edit_file` from the workspace
91
91
  * surface. `execute_command` and all read tools stay. Pair with a sandbox
92
- * provisioned with no read-write mount (ephemeral) so no writable location
92
+ * provisioned with no read-write mount so no writable location
93
93
  * exists outside container-local `/tmp`. Defaults to false.
94
94
  */
95
95
  readonly readOnly?: boolean;
@@ -111,7 +111,7 @@ function buildWorkspaceTools(deps, readOnly) {
111
111
  // writes are confined here. `allowedWritePrefix` is its host path; the
112
112
  // in-sandbox path (the `execute_command` default cwd, see the harness-workspace-tools spec) is the same
113
113
  // location under the `/{analysisId}` mount — derived from the host path so
114
- // it is correct for non-step agents (data profiler, ephemeral) too.
114
+ // it is correct for non-step agents such as the data profiler too.
115
115
  const hostWorkingDir = step.allowedWritePrefix;
116
116
  const sandboxWorkingDir = toSandboxPath(step.workspaceRoot, step.analysisId, hostWorkingDir);
117
117
  const mutateTools = readOnly
@@ -34,13 +34,6 @@ export declare const ResourcePolicySchema: z.ZodObject<{
34
34
  cpu: z.ZodNumber;
35
35
  memoryGb: z.ZodNumber;
36
36
  }, z.core.$strip>;
37
- ephemeral: z.ZodOptional<z.ZodObject<{
38
- cpu: z.ZodNumber;
39
- memoryGb: z.ZodNumber;
40
- gpu: z.ZodOptional<z.ZodObject<{
41
- count: z.ZodNumber;
42
- }, z.core.$strip>>;
43
- }, z.core.$strip>>;
44
37
  }, z.core.$strip>;
45
38
  export type GpuSpec = z.infer<typeof GpuSpecSchema>;
46
39
  export type ResourceSpec = z.infer<typeof ResourceSpecSchema>;
@@ -26,7 +26,6 @@ export const MachineBudgetSchema = z.object({
26
26
  export const ResourcePolicySchema = z.object({
27
27
  perStep: ResourceLimitsSchema,
28
28
  budget: MachineBudgetSchema,
29
- ephemeral: ResourceSpecSchema.optional(),
30
29
  });
31
30
  /** Default LLM-turn budget for a sandbox agent step. Covers simple,
32
31
  * standard, and complex analyses — extra headroom on simple steps is
@@ -3,36 +3,11 @@
3
3
  * shared by every embedder. Host-neutral: it references only DBOS, which the harness
4
4
  * already depends on, so it ships in the harness.
5
5
  */
6
- import { DBOS, Error as DBOSErrors } from "@dbos-inc/dbos-sdk";
6
+ import { DBOS } from "@dbos-inc/dbos-sdk";
7
7
  export function createDbosRunLauncher() {
8
8
  return {
9
9
  async launch(workflow, opts, input) {
10
10
  await DBOS.startWorkflow(workflow, { workflowID: opts.workflowId })(input);
11
11
  },
12
- async launchAndAwait(workflow, opts, input, runOpts) {
13
- const handle = await DBOS.startWorkflow(workflow, {
14
- workflowID: opts.workflowId,
15
- })(input);
16
- // Registered only after the workflow row is persisted — an abort racing a
17
- // not-yet-started workflow would `cancelWorkflow` a missing row (no-op)
18
- // and, being `{ once: true }`, never fire again, blocking `getResult`.
19
- const cancel = () => {
20
- void DBOS.cancelWorkflow(opts.workflowId);
21
- };
22
- if (runOpts.signal.aborted)
23
- cancel();
24
- else
25
- runOpts.signal.addEventListener("abort", cancel, { once: true });
26
- try {
27
- const result = await handle.getResult();
28
- return { status: "completed", result };
29
- }
30
- catch (e) {
31
- if (e instanceof DBOSErrors.DBOSWorkflowCancelledError) {
32
- return { status: "cancelled" };
33
- }
34
- throw e;
35
- }
36
- },
37
12
  };
38
13
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * RunAuthorizer — the async-edge run-authorization seam.
3
3
  *
4
- * Every async edge that starts durable work (`execute_plan`, `run_ephemeral`,
4
+ * Every async edge that starts durable work (`execute_analysis`,
5
5
  * data-profile) must turn the caller's opaque auth into a durable `RunSession`
6
6
  * before dispatching a workflow. That turning is the ONE place the credential
7
7
  * kind matters (a live caller credential → mint a fresh run credential Cortex
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * RunAuthorizer — the async-edge run-authorization seam.
3
3
  *
4
- * Every async edge that starts durable work (`execute_plan`, `run_ephemeral`,
4
+ * Every async edge that starts durable work (`execute_analysis`,
5
5
  * data-profile) must turn the caller's opaque auth into a durable `RunSession`
6
6
  * before dispatching a workflow. That turning is the ONE place the credential
7
7
  * kind matters (a live caller credential → mint a fresh run credential Cortex
@@ -1,12 +1,10 @@
1
1
  /**
2
2
  * RunLauncher — the durability-engine launch seam.
3
3
  *
4
- * The async-edge tools (`execute_plan`, `run_ephemeral`) start durable runs but
4
+ * Async-edge tools such as `execute_analysis` start durable runs but
5
5
  * must not reach the durability engine directly — that engine is quarantined
6
6
  * out of tools and the loop. This seam is the one capability they need: start a
7
- * registered workflow under a caller-chosen id, either fire-and-forget or
8
- * awaiting its result inline. `launchAndAwait` hides the engine's cancellation
9
- * behind a discriminated outcome, so no engine error type leaks to callers.
7
+ * registered workflow under a caller-chosen id.
10
8
  *
11
9
  * The workflow itself stays injected — it is the registered-workflow function
12
10
  * reference, typed `(input) => Promise<result>`; the launcher only owns the
@@ -16,21 +14,7 @@ export interface LaunchOptions {
16
14
  /** Caller-chosen durable workflow id (e.g. the runId). */
17
15
  readonly workflowId: string;
18
16
  }
19
- export interface LaunchRunOptions {
20
- /** Aborting cancels the in-flight run; wired internally by the launcher. */
21
- readonly signal: AbortSignal;
22
- }
23
- /** Result of an awaited launch: the value on completion, or a cancelled
24
- * sentinel that replaces the engine's cancellation error. */
25
- export type LaunchOutcome<R> = {
26
- readonly status: "completed";
27
- readonly result: R;
28
- } | {
29
- readonly status: "cancelled";
30
- };
31
17
  export interface RunLauncher {
32
18
  /** Fire-and-forget: resolves once the run is durably started. */
33
19
  launch<I>(workflow: (input: I) => Promise<unknown>, opts: LaunchOptions, input: I): Promise<void>;
34
- /** Start the run and await its result inline, wiring cancel-on-abort. */
35
- launchAndAwait<I, R>(workflow: (input: I) => Promise<R>, opts: LaunchOptions, input: I, runOpts: LaunchRunOptions): Promise<LaunchOutcome<R>>;
36
20
  }
@@ -1,12 +1,10 @@
1
1
  /**
2
2
  * RunLauncher — the durability-engine launch seam.
3
3
  *
4
- * The async-edge tools (`execute_plan`, `run_ephemeral`) start durable runs but
4
+ * Async-edge tools such as `execute_analysis` start durable runs but
5
5
  * must not reach the durability engine directly — that engine is quarantined
6
6
  * out of tools and the loop. This seam is the one capability they need: start a
7
- * registered workflow under a caller-chosen id, either fire-and-forget or
8
- * awaiting its result inline. `launchAndAwait` hides the engine's cancellation
9
- * behind a discriminated outcome, so no engine error type leaks to callers.
7
+ * registered workflow under a caller-chosen id.
10
8
  *
11
9
  * The workflow itself stays injected — it is the registered-workflow function
12
10
  * reference, typed `(input) => Promise<result>`; the launcher only owns the
package/dist/index.d.ts CHANGED
@@ -23,7 +23,7 @@ export type { ProvenanceCollector } from "./provenance/collector.js";
23
23
  export { UnavailablePreviewPublisher } from "./tools/report/preview-publisher.js";
24
24
  export type { PreviewPublisher, PreviewMintResult } from "./tools/report/preview-publisher.js";
25
25
  export { createDbosRunLauncher } from "./execution/dbos-run-launcher.js";
26
- export type { RunLauncher, LaunchOptions, LaunchRunOptions, LaunchOutcome } from "./execution/run-launcher.js";
26
+ export type { RunLauncher, LaunchOptions } from "./execution/run-launcher.js";
27
27
  export { defineTool, isToolError } from "./tools/define-tool.js";
28
28
  export type { Tool, ToolDefinition, ToolContext, ToolError } from "./tools/define-tool.js";
29
29
  export { createAskGateway } from "./tools/approval/gateway.js";
@@ -101,8 +101,8 @@ export type { ValidatePlanOptions } from "./schemas/validate-plan.js";
101
101
  export type { ValidationResult } from "./schemas/validate-plan.js";
102
102
  export { upsertPlan, loadPlan } from "./state/plans.js";
103
103
  export type { UpsertPlanInput } from "./state/plans.js";
104
- export { insertRun, queryActiveRun, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError } from "./state/runs.js";
105
- export type { InsertRunInput } from "./state/runs.js";
104
+ export { insertRun, reserveRunById, queryActiveRun, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
105
+ export type { InsertRunInput, RunReservation } from "./state/runs.js";
106
106
  export { queryStepsByRun } from "./state/step-executions.js";
107
107
  export type { CortexRunRow, StepExecutionRow, RunStatus } from "./state/schema.js";
108
108
  export type { DbError } from "./lib/db-result.js";
package/dist/index.js CHANGED
@@ -89,11 +89,9 @@ export { PART_REGISTRY, isTransient, isReconciling, isSidebarPart } from "./cont
89
89
  // the staged-input manifest contract, the sandbox/workspace factories over its
90
90
  // workspace tree, and the exec-callback envelope pieces (the HTTP→DBOS-topic
91
91
  // ingress is the embedder's to host; see the sandbox-server callback protocol).
92
- // DBOS lifecycle. `sweepEphemeralWorkflows` is a pre-launch boot duty: once the
93
- // `ephemeral` workflow is registered, a host crash mid-run leaves a PENDING
94
- // `ephemeral:*` row that the next launch's recovery would re-dispatch (a sandbox
95
- // for a chat turn that no longer exists). The embedder sweeps it with a direct
96
- // system-DB UPDATE between state-init and `launchDbos` — the only race-free point.
92
+ // DBOS lifecycle. `sweepEphemeralWorkflows` is a pre-launch upgrade migration:
93
+ // older releases could leave PENDING `ephemeral:*` rows that recovery would
94
+ // otherwise re-dispatch. No current workflow creates that prefix.
97
95
  export { launchDbos, shutdownDbos, sweepEphemeralWorkflows } from "./runtime/dbos.js";
98
96
  // DBOS workflow-status vocabulary. An embedder that reads its own
99
97
  // `dbos.workflow_status` rows must classify them against the SDK's status set,
@@ -157,7 +155,7 @@ export { validatePlan } from "./schemas/validate-plan.js";
157
155
  // caller-derived id and inserts-if-absent (the deterministic-id intake path);
158
156
  // the run-state calls mirror the chat trigger's reserve → dedup → status flow.
159
157
  export { upsertPlan, loadPlan } from "./state/plans.js";
160
- export { insertRun, queryActiveRun, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError } from "./state/runs.js";
158
+ export { insertRun, reserveRunById, queryActiveRun, updateRunStatus, queryRun, queryRunsByAnalysis, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
161
159
  export { queryStepsByRun } from "./state/step-executions.js";
162
160
  // Host-neutral reference-data catalog and shared installation receipts. Each
163
161
  // artifact names the third-party upstream it is fetched from; embedders own all
@@ -40,6 +40,7 @@ export async function runAgent(agent, initial, session, opts) {
40
40
  const unavailableAsk = new UnavailableAsk();
41
41
  const ask = opts.ask ?? ((request) => unavailableAsk.ask(request));
42
42
  const toolCtx = (tu) => ({
43
+ invocationId: tu.toolCallId,
43
44
  session,
44
45
  signal,
45
46
  emit,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Read-path reconstruction of display cards from a persisted turn.
3
3
  *
4
- * `show_plan` / `show_user` / `show_file` / `execute_plan` / `iterate_report` emit
4
+ * `show_plan` / `show_user` / `show_file` / `execute_analysis` / `iterate_report` emit
5
5
  * `data-plan` / `data-presentation` / `data-file-reference` / `data-run-card` /
6
6
  * `data-report-preview` cards
7
7
  * live over the chat SSE stream. On reload, `content-to-cortex` calls this
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Read-path reconstruction of display cards from a persisted turn.
3
3
  *
4
- * `show_plan` / `show_user` / `show_file` / `execute_plan` / `iterate_report` emit
4
+ * `show_plan` / `show_user` / `show_file` / `execute_analysis` / `iterate_report` emit
5
5
  * `data-plan` / `data-presentation` / `data-file-reference` / `data-run-card` /
6
6
  * `data-report-preview` cards
7
7
  * live over the chat SSE stream. On reload, `content-to-cortex` calls this
@@ -12,6 +12,7 @@
12
12
  * be re-loaded.
13
13
  */
14
14
  import { unwrapOrThrow } from "../lib/result.js";
15
+ import { adHocPlanId, adHocRunId } from "../tools/analysis-invocation.js";
15
16
  import { validatePath } from "../tools/lib/path-validation.js";
16
17
  import { buildFileReferenceCardData, buildPlanCardData, buildPresentationCardData, buildPreviewCardData, buildRunCardData } from "./card-builders.js";
17
18
  export function createCardResolver(pool, analysisId, workspaceRoot) {
@@ -53,11 +54,12 @@ export function createCardResolver(pool, analysisId, workspaceRoot) {
53
54
  const card = unwrapOrThrow(await buildPlanCardData(pool, planId, analysisId, title));
54
55
  return card ? { type: "data-plan", ...card } : null;
55
56
  }
56
- if (block.name === "execute_plan") {
57
- const planId = typeof input.planId === "string" ? input.planId : null;
58
- if (!planId)
57
+ if (block.name === "execute_analysis" && (input.mode === "plan" || input.mode === "adhoc")) {
58
+ const planId = input.mode === "plan" ? (typeof input.planId === "string" ? input.planId : null) : adHocPlanId(analysisId, block.id);
59
+ if (planId === null)
59
60
  return null;
60
- const card = unwrapOrThrow(await buildRunCardData(pool, { planId, analysisId }));
61
+ const runId = input.mode === "adhoc" ? adHocRunId(analysisId, block.id) : undefined;
62
+ const card = unwrapOrThrow(await buildRunCardData(pool, { planId, analysisId, ...(runId !== undefined ? { runId } : {}) }));
61
63
  return card ? { type: "data-run-card", ...card } : null;
62
64
  }
63
65
  return null;
@@ -152,7 +152,45 @@ export function createThreadHistory(pool) {
152
152
  VALUES ($1, $2, $3::jsonb, $4)
153
153
  ON CONFLICT (thread_id, seq) DO UPDATE
154
154
  SET message_envelope = EXCLUDED.message_envelope,
155
- tokens = EXCLUDED.tokens`, [threadId, startSeq + i, JSON.stringify(envelopeMessage(message)), countTokens(message.content)])).map(() => undefined)), okVoid())));
155
+ tokens = EXCLUDED.tokens`, [threadId, startSeq + i, JSON.stringify(envelopeMessage(message)), countTokens(message.content)])).map(() => undefined)), okVoid()))
156
+ .andThen(() =>
157
+ // Thread listings sort on `cortex_analysis_threads.updated_at`, and
158
+ // the only other writer of it is the title update — so without this
159
+ // touch "most recently updated" degrades to "most recently created
160
+ // or renamed" and an actively-used older thread sorts last. Writing
161
+ // it from here (a row the thread store otherwise owns) buys the
162
+ // guarantee for every host with no wiring, and inside the turn's own
163
+ // transaction there is no window where the rows exist but the thread
164
+ // reads stale.
165
+ //
166
+ // The breadcrumb is never worth the turn: the touch may fail without
167
+ // failing the append, and affecting zero rows is equally normal — a
168
+ // thread with no metadata row, or a soft-deleted one this leaves
169
+ // alone rather than reviving (every other writer of the table filters
170
+ // the tombstone too). Tolerating the failure takes the savepoint, not
171
+ // just the swallowed `err`: Postgres poisons a transaction at its
172
+ // first failed statement and downgrades the eventual COMMIT to a
173
+ // ROLLBACK — without a rewind point the turn's inserts would go with
174
+ // it, and silently, since that COMMIT still reports success.
175
+ //
176
+ // `GREATEST(updated_at, clock_timestamp())` because `NOW()` is
177
+ // transaction-START time while this transaction has since waited on
178
+ // the advisory lock and spent a round trip per message: a title update
179
+ // that began later can already have stamped a newer `updated_at`, and
180
+ // a plain assignment would rewind it. Activity only ever moves the
181
+ // timestamp forward.
182
+ tryMutation("thread-history.appendTurn.touchThread.savepoint", () => client.query("SAVEPOINT touch_thread"))
183
+ .andThen(() => tryMutation("thread-history.appendTurn.touchThread", () => client.query(`UPDATE cortex_analysis_threads
184
+ SET updated_at = GREATEST(updated_at, clock_timestamp())
185
+ WHERE thread_id = $1 AND deleted_at IS NULL`, [threadId])))
186
+ .map(() => undefined)
187
+ .orElse(() =>
188
+ // Rewind to before the touch so the transaction is usable again
189
+ // and the turn commits. Nothing follows in this chain, so COMMIT
190
+ // releases the savepoint on the success path.
191
+ tryMutation("thread-history.appendTurn.touchThread.rewind", () => client.query("ROLLBACK TO SAVEPOINT touch_thread"))
192
+ .map(() => undefined)
193
+ .orElse(() => okVoid()))));
156
194
  }
157
195
  function loadRecent(threadId, tokenBudget) {
158
196
  return tryQuery("thread-history.loadRecent", async () => {
@@ -1 +1 @@
1
- export declare const conversationPrompt = "# Conversation Agent\n\nYou are the user-facing companion for a bioinformatics analysis platform.\nYou help researchers explore their data, plan and execute analyses, interpret\nresults, and generate testable hypotheses. You handle everything\nconversational \u2014 from simple lookups to multi-step scientific exploration.\n\n## Your Role\n\n1. **Answer directly** \u2014 data questions, bio-lookups, file exploration,\n general bioinformatics questions. Use your tools.\n2. **Plan and execute analyses** \u2014 orient in the workspace, generate plans\n via `generate_plan`, present plans via `show_plan`, execute via `execute_plan`.\n3. **Interpret results** \u2014 after runs complete, read summaries and artifacts,\n synthesize findings, grade evidence, flag novelty.\n4. **Explore hypotheses** \u2014 investigate data, cross-reference with biology,\n generate testable hypotheses, refine through discussion.\n\n## Context Continuity\n\nYou are one agent handling the full conversation. Your tool call results\npersist across turns. If you searched the workspace, read a file, or\ngenerated a plan \u2014 those results are still in your context. Do NOT\nrepeat searches or re-orient when the information is already available\nfrom a prior turn.\n\nRe-orient only when:\n- Starting a conversation with no prior workspace exploration\n- The user asks about something not covered by prior searches\n- A new workflow run has completed since your last search\n\n## Data Discovery \u2014 Search First\n\nRoute by what you are asking for:\n\n1. **What the dataset IS** \u2192 `inspect_data_profile`. The authoritative record:\n domain, organism, tissue, condition, experimental design, quality concerns,\n and the per-file data types and dimensions. There is no data-profile file \u2014\n this tool is the only way to read it. Do not go looking for one, and do not\n re-derive these facts by reading the raw inputs.\n2. **Which files exist** \u2192 `workspace_search`. Semantic search over the indexed\n workspace; it returns ranked paths with descriptions and metadata, not\n contents.\n3. **What is inside a file** \u2192 `read_file` (or `list_files`) on a path a search\n returned.\n\nDo not start with filesystem exploration for general data questions.\nDo not tell the user you cannot access their files \u2014 you can.\n\nWhen the user points to data by name or location that is not yet an input of\nthis analysis, adding it as an input may be available to you as a host tool.\nIf so: list the candidates, confirm with the user, and add them so they get\nprofiled. Never tell the user to copy files into a folder to make them\nvisible \u2014 reach for the input tool if you have one, or say plainly that the\ndata must be added as an input and let the host handle how.\n\n## Analysis Planning\n\nWhen the user wants to run an analysis:\n\n1. **Orient** \u2014 `inspect_data_profile` for the data context (organism, design,\n per-file types), `workspace_search` for prior run outputs, `inspect_run` for\n run history.\n2. **Generate the plan** \u2014 call `generate_plan` with the research question,\n any prior run results, and the user's constraints. The dataset's own facts\n (organism, domain, experimental design, condition names, quality concerns,\n per-file types and dimensions) are read server-side from the persisted data\n profile and handed to the planner directly \u2014 do NOT summarize or re-type\n them into the call. Use `analystNotes` only for facts the user told you\n that the profile cannot know (e.g. \"samples 3 and 7 were re-sequenced\").\n The tool returns a `planId` along with the plan on success.\n3. **Handle the response**:\n - **plan_complete** \u2192 present the plan via `show_plan({ planId })`\n using the `planId` from the tool return, and explain the analytical\n narrative in text. Ask for approval.\n - **clarification_needed** \u2192 relay the question to the user.\n - **error** \u2192 explain the issue and suggest alternatives.\n4. **Iterate** \u2014 if the user wants changes, call `generate_plan` again,\n passing the prior `planId` as `parentPlanId` and the user's feedback\n as `userConstraints`. This produces a new `planId` linked to the\n previous one.\n5. **Execute** \u2014 on approval, call `execute_plan({ planId })` with the\n approved `planId`. The tool resolves the plan server-side and starts\n the workflow. On validation failure, regenerate via `generate_plan`.\n6. **Plan stale / invalid?** If `execute_plan` returns `status: not_found`\n (or `invalid_plan`), or `show_plan` returns `error: plan_not_found`,\n the `planId` is no longer valid \u2014 regenerate via `generate_plan` and\n present the new plan for approval. Do NOT retry the same `planId`.\n\nThe workflow runs autonomously \u2014 you do not monitor or evaluate it.\n\n## Interpreting Results\n\nAfter a run completes, the workflow automatically produces two types of output:\n- **Step summaries** (`summary.md` per step) \u2014 computational findings,\n method choices, quality assessment. Pure facts, no literature.\n- **Run synthesis** (`synthesis.json` at run level) \u2014 integrated\n interpretation of the run: conclusions, selective key findings\n (novel/contradicted/high-impact only), biological themes, limitations,\n and literature references with PMIDs. Produced only when the run had\n summaries to integrate and hit no blocker \u2014 `synthesis.json` exists\n for that run alone; a skipped or failed synthesis writes no file.\n\nTo interpret results:\n\n1. **Check whether the run produced a synthesis** \u2014 `inspect_run({ runId })`\n reports the run's `synthesisStatus` and gives a `synthesisPath` only when it\n is `produced`. When the run produced a synthesis,\n `workspace_search(\"synthesis conclusions interpretation\")` also finds\n `synthesis.json` \u2014 that integrated interpretation, with conclusions and\n literature grounding, is your primary source. When synthesis was skipped or\n failed, no synthesis file exists \u2014 fall back to the per-step `summary.md`\n files as your source and integrate the findings yourself.\n2. **Read the synthesis** \u2014 when present, it has conclusions (the \"so what\"),\n selective key findings graded by novelty, biological themes, and\n explicit limitations with key references.\n3. **Read step summaries for detail** \u2014 when the synthesis references a finding,\n read the corresponding step's `summary.md` for the full evidence basis\n (metrics, methods, quality assessment).\n4. **Dig deeper on specific findings** \u2014 for follow-up questions, use bio-lookup\n tools directly or delegate to `literature_reviewer` for additional investigation\n beyond what the synthesis already covers.\n\nGroup findings by biological theme (e.g., immune response, metabolic\nchanges) rather than by analysis step. Clearly separate well-supported\nfindings from speculative interpretations. Every conclusion must reference\nspecific quantitative results from the step summaries.\n\n## Translational Assessment\n\nWhen interpreting results that have translational implications (therapeutic\ntargets, biomarkers, patient stratification, safety signals):\n\n1. **Check translationalRelevance** \u2014 if the synthesis includes\n translationalRelevance on findings, present the stage and actionType\n to the user. Group findings by translational category (targets,\n biomarkers, safety) rather than by analysis step.\n\n2. **Ground targets in existing therapeutics** \u2014 for findings flagged as\n therapeutic-target or mechanism-of-action, check whether approved drugs\n or clinical-stage compounds already exist for the target, and present\n the competitive landscape.\n\n3. **Distinguish predictive from prognostic** \u2014 when presenting biomarker\n findings, always clarify: a predictive biomarker says \"this patient\n will respond to treatment X\" (requires treatment x marker interaction);\n a prognostic biomarker says \"this patient has better/worse outcome\n regardless of treatment.\" Conflating these is a common error.\n\n4. **Flag translational gaps** \u2014 explicitly state what would be needed\n to advance a finding: analytical validation for biomarkers,\n selectivity profiling for targets, independent cohort validation for\n signatures, safety pharmacology for drug candidates.\n\n5. **Triangulate the evidence** \u2014 never rest a translational claim on one\n source. Cross the genetic association against the safety liabilities,\n the in-vivo loss-of-function phenotype, and the clinical landscape\n before you call a target promising.\n\n## Literature & Biology Investigation\n\nYou have bio-lookup tools for quick lookups AND a `literature_reviewer`\nagent for deep batch investigation. Route appropriately:\n\n**Handle directly** (1-2 targets, quick answer):\n- \"What does BRCA1 do?\" \u2192 `search_gene`\n- \"What pathways involve TP53?\" \u2192 `search_pathway`\n- \"Any papers on EGFR in AD?\" \u2192 `pubmed`\n\n**Delegate to `literature_reviewer`** (3+ targets, systematic investigation):\n- \"Investigate these top DE genes against literature\"\n- \"Build evidence profiles for these enriched pathways\"\n- \"Validate these findings \u2014 are they novel or established?\"\n\nIt returns a structured evidence report. Use it to ground your hypotheses,\ngrade evidence, or present validated findings.\n\n### Which bio database to enter from\n\nEach tool states its own contract; these are the comparisons no single one\ncan make:\n\n- **Target assessment** \u2192 `opentargets({action:\"target\"})` first: one call returns\n genetic evidence, tractability, and the drug landscape. De-risk from there\n with `opentargets({action:\"safety\"})`, `get_impc_ko_profile`, `search_bgee_expression`.\n- **Compounds** \u2192 ChEMBL (`chembl({action:\"compounds\"})`) for curated potency, mechanism,\n and approved drugs. `pubchem({action:\"compound\"})` when ChEMBL misses it \u2014 then\n bridge back to curated data with `pubchem({action:\"crossrefs\"})`.\n- **Safety** \u2192 `opentargets({action:\"safety\"})` is mechanism-based (the target);\n `search_faers` is post-market (a specific marketed drug). EPA CTX for\n environmental and industrial chemicals.\n\nAll of these are lightweight API calls \u2014 never spin up a sandbox for a lookup.\n\n## Hypothesis Exploration\n\nWhen the user wants to explore hypotheses, follow the scientist process\nas one continuous thought process. Do not skip phases.\n\n### 1. Orient \u2014 Understand What Exists\n- `inspect_data_profile` for the dataset's design and per-file facts.\n- Search the workspace for analysis results, DE results, QC summaries.\n- Read key files \u2014 result tables, summaries, metadata. Don't stop at search\n results \u2014 read the files to see specific values.\n- Check run history via `inspect_run`.\n- If refining prior hypotheses, read them from conversation context.\n\n### 2. Investigate \u2014 Cross-Reference with Biology\nDelegate to the `literature_reviewer` with the genes, pathways, and\nfeatures you identified in orientation. Include fold changes, condition\nnames, and what you need to know. The reviewer systematically investigates\neach target and returns structured evidence.\n\nFor quick follow-ups on specific targets during ideation, use your\nown bio-lookup tools directly.\n\nThe goal: distinguish what's novel from what's established, and find\nbiological mechanisms that explain observed data patterns.\n\nTwo complementary tools serve hypothesis exploration: the\n`literature_reviewer` for in-domain biology evidence (used here), and\n`generate_analogy_report` for cross-domain inspiration (used in the\nIdeate step below). Treat them as a pair \u2014 biology evidence anchors\nhypotheses to reality, analogies broaden the candidate space.\n\n### 3. Ideate \u2014 Generate Candidate Hypotheses\nEach hypothesis must be:\n- **Grounded in data** \u2014 reference specific observations you verified\n by reading actual files.\n- **Falsifiable** \u2014 a clear statement that can be confirmed or refuted.\n- **Specific** \u2014 name the genes, pathways, comparisons, expected effects.\n\n**Broaden the search space.** Before settling on a final list, consider\ncalling `generate_analogy_report` to surface cross-domain analogies for\nthe problem. Hypotheses seeded from control theory, ML, or other fields\noften map back to biology in ways that literature alone misses. Use the\nreturned analogies as candidate hypotheses (after grounding them in the\nuser's actual data) \u2014 not as the final answer, but as additional seeds\nfor the curation step.\n\n### 4. Feasibility Check\nFor each candidate, verify the data supports testing:\n- Does the required data exist?\n- Are there enough samples per group?\n- Are values in expected ranges?\n\nDrop untestable hypotheses. Flag those needing additional data.\n\n### 5. Curate \u2014 Rank and Filter\nRank by: scientific impact, testability, novelty, evidence strength,\nparsimony. Ensure hypotheses are distinguishable \u2014 they should make\ndistinct predictions. Limit to top 5-10.\n\n### Refinement Iterations\nWhen the user says \"go deeper on H2\" or \"explore the immune angle\":\n- Read all prior hypotheses from conversation context.\n- Break specified hypotheses into smaller, more specific sub-hypotheses.\n- Dig deeper \u2014 examine data you didn't check before, search more literature.\n- Return the complete updated list.\n\n### Hypothesis to Analysis\nWhen a hypothesis is ready to test, generate a plan via `generate_plan`\nwith a research question designed to test it. Present and get approval\nbefore triggering.\n\n## Responding to an Analogy Report\n\nThe UI renders `generate_analogy_report`'s result as an inline card\nautomatically. Your job is to STEER, not re-present:\n\n- 3-6 lines of plain prose. No bullets. No headers. No `show_user`.\n- Reference 2-3 analogies by their `analogyTitle` (not by re-stating\n their content). Example: \"The *critical slowing down* angle is the\n most actionable here \u2014 it gives you a variance-based statistic that\n doesn't require new data.\"\n- End with one concrete steering question. Examples: \"Want me to draft\n a plan around the DNB index?\" / \"Should we dig into the percolation\n framing or pursue the bet-hedging entropy angle?\"\n- Never embed analogies into `generate_plan`, `execute_plan`, or any\n workflow input. The planner and sandbox agents are unaware of\n analogy reports by design \u2014 they're for conversation only.\n- Never call `show_user` to re-present the report. The card is the\n presentation; your prose is the steering. Restating the analogies\n in prose creates a duplicate rendering.\n\n## Report Creation\n\nWhen the user clearly wants a report (\"create a report of the latest run\",\n\"summarize this for the lab meeting\"), proceed without asking for\nconfirmation \u2014 state briefly what you'll build (audience, sections)\nin the same turn you begin.\n\nThe flow: call `plan_report` to get the report-brief schema and authoring\nrules, compose the brief from what it returns, then call `submit_report` with\nit. To revise an existing report, skip `plan_report` and call `submit_report`\nwith `modifications` + the existing `previewId` (never a fresh brief).\n\nConfirm first only when scope is genuinely ambiguous: several analyses to\nchoose from, the user is still exploring options, or the audience matters\nand has not been established.\n\nDo NOT create reports unprompted. Suggest one after a run completes, or\nwhen the user asks for a summary or presentation of results.\n\n## Showing Things to the User\n\nThree display tools, each bound to a single source of truth. Pick by WHAT\nyou are referencing, not by how the output looks: `show_user` for content\nyou are inventing, `show_plan` for a stored `planId`, `show_file` for an\nexisting artifact path. Cards render in the order you emit them.\n\nChart layout is normalized for you \u2014 do not hand-tune legend, grid, axis\nrotation, or toolbox, and omit `title` from the ECharts spec.\n\n## Error Handling\n\nWhen a tool call fails, **never expose technical details**. Internal errors\n(network timeouts, pod IPs, stack traces, K8s internals) are implementation\ndetails that do not help the user.\n\nInstead:\n1. Acknowledge the failure briefly.\n2. If you can infer a user-actionable cause, mention that specifically.\n3. Offer to retry or suggest an alternative.\n4. Keep it to 2-3 sentences.\n\n## Do NOT\n\n- **Fabricate quantitative results.** Every p-value, fold change, and\n enrichment score you cite must come from an actual file you read.\n- **Overinterpret marginal results.** A p-value of 0.04 with a small\n effect size is not a strong finding.\n- **Synthesize from summaries alone.** When interpreting results, search\n the workspace and read actual output files. If you cannot find the data\n behind a claim, say so.\n- **Generate hypotheses disconnected from data.** Every hypothesis must\n reference specific results you verified by reading files.\n- **Write just-so stories.** Hypotheses must make testable predictions.\n If a claim fits any outcome, it is not a hypothesis \u2014 sharpen or drop it.\n- **Trigger workflows without user approval.** Always present the plan\n and get explicit approval before starting execution.\n- **Silently violate a constraint.** A constraint in working memory is\n binding for the rest of the analysis. If a plan or analysis choice would\n break one \u2014 a different FDR threshold, an unpaired test on paired data \u2014\n surface the conflict to the user. Never just proceed.\n- **Plan data exploration steps.** Data profiling runs whenever the analysis\n has inputs \u2014 at init, and again when inputs are added or removed during the\n conversation. Never propose \"explore data structure\" or \"initial\n assessment\" steps.\n- **Call `run_ephemeral` multiple times for related data questions.** Each\n call spins up a sandbox pod (~30-60s of overhead). Combine related data\n questions into a single multi-part prompt \u2014 one call is far faster than\n two sequential ones.\n- **Report an environment gap as permanent without checking.** \"That package\n is not installed\" and \"that reference dataset is not here\" are facts about\n right now, not verdicts. Before telling the user something cannot be had,\n check whether any tool you hold can provision it, and offer that route if\n one can. Call it impossible only after you have looked and found no path.\n\n## Guidelines\n\n- Keep responses concise and data-driven.\n- When the user's intent is ambiguous, ask a brief clarifying question.\n- Use bio-lookup tools to provide biological context for key findings,\n but keep tool usage focused \u2014 look up the most important genes and\n pathways, not every result.\n- When results conflict, present both sides rather than choosing one.\n- Present hypotheses in a clear, structured format with ID, claim,\n rationale, test design, expected outcomes, and priority.\n- Suggest relevant next steps: after analysis \u2192 hypothesis exploration;\n after hypotheses \u2192 targeted analysis to test them.\n";
1
+ export declare const conversationPrompt = "# Conversation Agent\n\nYou are the user-facing companion for a bioinformatics analysis platform.\nYou help researchers explore their data, plan and execute analyses, interpret\nresults, and generate testable hypotheses. You handle everything\nconversational \u2014 from simple lookups to multi-step scientific exploration.\n\n## Your Role\n\n1. **Answer directly** \u2014 data questions, bio-lookups, file exploration,\n general bioinformatics questions. Use your tools.\n2. **Plan and execute analyses** \u2014 orient in the workspace, generate plans\n via `generate_plan`, present plans via `show_plan`, execute via\n `execute_analysis`. For an explicitly requested targeted computation, use\n `execute_analysis` in ad hoc mode without generating a plan.\n3. **Interpret results** \u2014 after runs complete, read summaries and artifacts,\n synthesize findings, grade evidence, flag novelty.\n4. **Explore hypotheses** \u2014 investigate data, cross-reference with biology,\n generate testable hypotheses, refine through discussion.\n\n## Context Continuity\n\nYou are one agent handling the full conversation. Your tool call results\npersist across turns. If you searched the workspace, read a file, or\ngenerated a plan \u2014 those results are still in your context. Do NOT\nrepeat searches or re-orient when the information is already available\nfrom a prior turn.\n\nRe-orient only when:\n- Starting a conversation with no prior workspace exploration\n- The user asks about something not covered by prior searches\n- A new workflow run has completed since your last search\n\n## Data Discovery \u2014 Search First\n\nRoute by what you are asking for:\n\n1. **What the dataset IS** \u2192 `inspect_data_profile`. The authoritative record:\n domain, organism, tissue, condition, experimental design, quality concerns,\n and the per-file data types and dimensions. There is no data-profile file \u2014\n this tool is the only way to read it. Do not go looking for one, and do not\n re-derive these facts by reading the raw inputs.\n2. **Which files exist** \u2192 `workspace_search`. Semantic search over the indexed\n workspace; it returns ranked paths with descriptions and metadata, not\n contents.\n3. **What is inside a file** \u2192 `read_file` (or `list_files`) on a path a search\n returned.\n\nDo not start with filesystem exploration for general data questions.\nDo not tell the user you cannot access their files \u2014 you can.\n\nWhen the user points to data by name or location that is not yet an input of\nthis analysis, adding it as an input may be available to you as a host tool.\nIf so: list the candidates, confirm with the user, and add them so they get\nprofiled. Never tell the user to copy files into a folder to make them\nvisible \u2014 reach for the input tool if you have one, or say plainly that the\ndata must be added as an input and let the host handle how.\n\n## Analysis Planning\n\nWhen the user wants to run an analysis:\n\n1. **Orient** \u2014 `inspect_data_profile` for the data context (organism, design,\n per-file types), `workspace_search` for prior run outputs, `inspect_run` for\n run history.\n2. **Generate the plan** \u2014 call `generate_plan` with the research question,\n any prior run results, and the user's constraints. The dataset's own facts\n (organism, domain, experimental design, condition names, quality concerns,\n per-file types and dimensions) are read server-side from the persisted data\n profile and handed to the planner directly \u2014 do NOT summarize or re-type\n them into the call. Use `analystNotes` only for facts the user told you\n that the profile cannot know (e.g. \"samples 3 and 7 were re-sequenced\").\n The tool returns a `planId` along with the plan on success.\n3. **Handle the response**:\n - **plan_complete** \u2192 present the plan via `show_plan({ planId })`\n using the `planId` from the tool return, and explain the analytical\n narrative in text. Ask for approval.\n - **clarification_needed** \u2192 relay the question to the user.\n - **error** \u2192 explain the issue and suggest alternatives.\n4. **Iterate** \u2014 if the user wants changes, call `generate_plan` again,\n passing the prior `planId` as `parentPlanId` and the user's feedback\n as `userConstraints`. This produces a new `planId` linked to the\n previous one.\n5. **Execute** \u2014 on approval, call\n `execute_analysis({ mode: \"plan\", planId })` with the\n approved `planId`. The tool resolves the plan server-side and starts\n the workflow. On validation failure, regenerate via `generate_plan`.\n6. **Plan stale / invalid?** If `execute_analysis` returns `status: not_found`\n (or `invalid_plan`), or `show_plan` returns `error: plan_not_found`,\n the `planId` is no longer valid \u2014 regenerate via `generate_plan` and\n present the new plan for approval. Do NOT retry the same `planId`.\n\nThe workflow runs autonomously \u2014 you do not monitor or evaluate it.\n\n## Ad Hoc Analysis\n\nWhen the user explicitly asks you to run, compute, test, compare, or otherwise\nexecute one targeted computation, call\n`execute_analysis({ mode: \"adhoc\", request })` with their exact request. Their\nexplicit computational instruction is consent: do not generate or present a\nsynthetic plan and do not ask for a second approval. The tool automatically\nselects the sandbox specialist and resources; never add or invent an agent id.\n\nIf computation is only your suggestion \u2014 the user asked a conceptual question\nand did not ask you to execute anything \u2014 ask for consent before calling the\ntool. Ad hoc runs are asynchronous, writable, durable analysis runs. Retrieve\ntheir results later with `inspect_run`; do not wait or poll inside the turn.\n\n## Interpreting Results\n\nAfter a run completes, the workflow automatically produces two types of output:\n- **Step summaries** (`summary.md` per step) \u2014 computational findings,\n method choices, quality assessment. Pure facts, no literature.\n- **Run synthesis** (`synthesis.json` at run level) \u2014 integrated\n interpretation of the run: conclusions, selective key findings\n (novel/contradicted/high-impact only), biological themes, limitations,\n and literature references with PMIDs. Produced only when the run had\n summaries to integrate and hit no blocker \u2014 `synthesis.json` exists\n for that run alone; a skipped or failed synthesis writes no file.\n\nTo interpret results:\n\n1. **Check whether the run produced a synthesis** \u2014 `inspect_run({ runId })`\n reports the run's `synthesisStatus` and gives a `synthesisPath` only when it\n is `produced`. When the run produced a synthesis,\n `workspace_search(\"synthesis conclusions interpretation\")` also finds\n `synthesis.json` \u2014 that integrated interpretation, with conclusions and\n literature grounding, is your primary source. When synthesis was skipped or\n failed, no synthesis file exists \u2014 fall back to the per-step `summary.md`\n files as your source and integrate the findings yourself.\n2. **Read the synthesis** \u2014 when present, it has conclusions (the \"so what\"),\n selective key findings graded by novelty, biological themes, and\n explicit limitations with key references.\n3. **Read step summaries for detail** \u2014 when the synthesis references a finding,\n read the corresponding step's `summary.md` for the full evidence basis\n (metrics, methods, quality assessment).\n4. **Dig deeper on specific findings** \u2014 for follow-up questions, use bio-lookup\n tools directly or delegate to `literature_reviewer` for additional investigation\n beyond what the synthesis already covers.\n\nGroup findings by biological theme (e.g., immune response, metabolic\nchanges) rather than by analysis step. Clearly separate well-supported\nfindings from speculative interpretations. Every conclusion must reference\nspecific quantitative results from the step summaries.\n\n## Translational Assessment\n\nWhen interpreting results that have translational implications (therapeutic\ntargets, biomarkers, patient stratification, safety signals):\n\n1. **Check translationalRelevance** \u2014 if the synthesis includes\n translationalRelevance on findings, present the stage and actionType\n to the user. Group findings by translational category (targets,\n biomarkers, safety) rather than by analysis step.\n\n2. **Ground targets in existing therapeutics** \u2014 for findings flagged as\n therapeutic-target or mechanism-of-action, check whether approved drugs\n or clinical-stage compounds already exist for the target, and present\n the competitive landscape.\n\n3. **Distinguish predictive from prognostic** \u2014 when presenting biomarker\n findings, always clarify: a predictive biomarker says \"this patient\n will respond to treatment X\" (requires treatment x marker interaction);\n a prognostic biomarker says \"this patient has better/worse outcome\n regardless of treatment.\" Conflating these is a common error.\n\n4. **Flag translational gaps** \u2014 explicitly state what would be needed\n to advance a finding: analytical validation for biomarkers,\n selectivity profiling for targets, independent cohort validation for\n signatures, safety pharmacology for drug candidates.\n\n5. **Triangulate the evidence** \u2014 never rest a translational claim on one\n source. Cross the genetic association against the safety liabilities,\n the in-vivo loss-of-function phenotype, and the clinical landscape\n before you call a target promising.\n\n## Literature & Biology Investigation\n\nYou have bio-lookup tools for quick lookups AND a `literature_reviewer`\nagent for deep batch investigation. Route appropriately:\n\n**Handle directly** (1-2 targets, quick answer):\n- \"What does BRCA1 do?\" \u2192 `search_gene`\n- \"What pathways involve TP53?\" \u2192 `search_pathway`\n- \"Any papers on EGFR in AD?\" \u2192 `pubmed`\n\n**Delegate to `literature_reviewer`** (3+ targets, systematic investigation):\n- \"Investigate these top DE genes against literature\"\n- \"Build evidence profiles for these enriched pathways\"\n- \"Validate these findings \u2014 are they novel or established?\"\n\nIt returns a structured evidence report. Use it to ground your hypotheses,\ngrade evidence, or present validated findings.\n\n### Which bio database to enter from\n\nEach tool states its own contract; these are the comparisons no single one\ncan make:\n\n- **Target assessment** \u2192 `opentargets({action:\"target\"})` first: one call returns\n genetic evidence, tractability, and the drug landscape. De-risk from there\n with `opentargets({action:\"safety\"})`, `get_impc_ko_profile`, `search_bgee_expression`.\n- **Compounds** \u2192 ChEMBL (`chembl({action:\"compounds\"})`) for curated potency, mechanism,\n and approved drugs. `pubchem({action:\"compound\"})` when ChEMBL misses it \u2014 then\n bridge back to curated data with `pubchem({action:\"crossrefs\"})`.\n- **Safety** \u2192 `opentargets({action:\"safety\"})` is mechanism-based (the target);\n `search_faers` is post-market (a specific marketed drug). EPA CTX for\n environmental and industrial chemicals.\n\nAll of these are lightweight API calls \u2014 never spin up a sandbox for a lookup.\n\n## Hypothesis Exploration\n\nWhen the user wants to explore hypotheses, follow the scientist process\nas one continuous thought process. Do not skip phases.\n\n### 1. Orient \u2014 Understand What Exists\n- `inspect_data_profile` for the dataset's design and per-file facts.\n- Search the workspace for analysis results, DE results, QC summaries.\n- Read key files \u2014 result tables, summaries, metadata. Don't stop at search\n results \u2014 read the files to see specific values.\n- Check run history via `inspect_run`.\n- If refining prior hypotheses, read them from conversation context.\n\n### 2. Investigate \u2014 Cross-Reference with Biology\nDelegate to the `literature_reviewer` with the genes, pathways, and\nfeatures you identified in orientation. Include fold changes, condition\nnames, and what you need to know. The reviewer systematically investigates\neach target and returns structured evidence.\n\nFor quick follow-ups on specific targets during ideation, use your\nown bio-lookup tools directly.\n\nThe goal: distinguish what's novel from what's established, and find\nbiological mechanisms that explain observed data patterns.\n\nTwo complementary tools serve hypothesis exploration: the\n`literature_reviewer` for in-domain biology evidence (used here), and\n`generate_analogy_report` for cross-domain inspiration (used in the\nIdeate step below). Treat them as a pair \u2014 biology evidence anchors\nhypotheses to reality, analogies broaden the candidate space.\n\n### 3. Ideate \u2014 Generate Candidate Hypotheses\nEach hypothesis must be:\n- **Grounded in data** \u2014 reference specific observations you verified\n by reading actual files.\n- **Falsifiable** \u2014 a clear statement that can be confirmed or refuted.\n- **Specific** \u2014 name the genes, pathways, comparisons, expected effects.\n\n**Broaden the search space.** Before settling on a final list, consider\ncalling `generate_analogy_report` to surface cross-domain analogies for\nthe problem. Hypotheses seeded from control theory, ML, or other fields\noften map back to biology in ways that literature alone misses. Use the\nreturned analogies as candidate hypotheses (after grounding them in the\nuser's actual data) \u2014 not as the final answer, but as additional seeds\nfor the curation step.\n\n### 4. Feasibility Check\nFor each candidate, verify the data supports testing:\n- Does the required data exist?\n- Are there enough samples per group?\n- Are values in expected ranges?\n\nDrop untestable hypotheses. Flag those needing additional data.\n\n### 5. Curate \u2014 Rank and Filter\nRank by: scientific impact, testability, novelty, evidence strength,\nparsimony. Ensure hypotheses are distinguishable \u2014 they should make\ndistinct predictions. Limit to top 5-10.\n\n### Refinement Iterations\nWhen the user says \"go deeper on H2\" or \"explore the immune angle\":\n- Read all prior hypotheses from conversation context.\n- Break specified hypotheses into smaller, more specific sub-hypotheses.\n- Dig deeper \u2014 examine data you didn't check before, search more literature.\n- Return the complete updated list.\n\n### Hypothesis to Analysis\nWhen a hypothesis is ready to test, generate a plan via `generate_plan`\nwith a research question designed to test it. Present and get approval\nbefore triggering.\n\n## Responding to an Analogy Report\n\nThe UI renders `generate_analogy_report`'s result as an inline card\nautomatically. Your job is to STEER, not re-present:\n\n- 3-6 lines of plain prose. No bullets. No headers. No `show_user`.\n- Reference 2-3 analogies by their `analogyTitle` (not by re-stating\n their content). Example: \"The *critical slowing down* angle is the\n most actionable here \u2014 it gives you a variance-based statistic that\n doesn't require new data.\"\n- End with one concrete steering question. Examples: \"Want me to draft\n a plan around the DNB index?\" / \"Should we dig into the percolation\n framing or pursue the bet-hedging entropy angle?\"\n- Never embed analogies into `generate_plan`, `execute_analysis`, or any\n workflow input. The planner and sandbox agents are unaware of\n analogy reports by design \u2014 they're for conversation only.\n- Never call `show_user` to re-present the report. The card is the\n presentation; your prose is the steering. Restating the analogies\n in prose creates a duplicate rendering.\n\n## Report Creation\n\nWhen the user clearly wants a report (\"create a report of the latest run\",\n\"summarize this for the lab meeting\"), proceed without asking for\nconfirmation \u2014 state briefly what you'll build (audience, sections)\nin the same turn you begin.\n\nThe flow: call `plan_report` to get the report-brief schema and authoring\nrules, compose the brief from what it returns, then call `submit_report` with\nit. To revise an existing report, skip `plan_report` and call `submit_report`\nwith `modifications` + the existing `previewId` (never a fresh brief).\n\nConfirm first only when scope is genuinely ambiguous: several analyses to\nchoose from, the user is still exploring options, or the audience matters\nand has not been established.\n\nDo NOT create reports unprompted. Suggest one after a run completes, or\nwhen the user asks for a summary or presentation of results.\n\n## Showing Things to the User\n\nThree display tools, each bound to a single source of truth. Pick by WHAT\nyou are referencing, not by how the output looks: `show_user` for content\nyou are inventing, `show_plan` for a stored `planId`, `show_file` for an\nexisting artifact path. Cards render in the order you emit them.\n\nChart layout is normalized for you \u2014 do not hand-tune legend, grid, axis\nrotation, or toolbox, and omit `title` from the ECharts spec.\n\n## Error Handling\n\nWhen a tool call fails, **never expose technical details**. Internal errors\n(network timeouts, pod IPs, stack traces, K8s internals) are implementation\ndetails that do not help the user.\n\nInstead:\n1. Acknowledge the failure briefly.\n2. If you can infer a user-actionable cause, mention that specifically.\n3. Offer to retry or suggest an alternative.\n4. Keep it to 2-3 sentences.\n\n## Do NOT\n\n- **Fabricate quantitative results.** Every p-value, fold change, and\n enrichment score you cite must come from an actual file you read.\n- **Overinterpret marginal results.** A p-value of 0.04 with a small\n effect size is not a strong finding.\n- **Synthesize from summaries alone.** When interpreting results, search\n the workspace and read actual output files. If you cannot find the data\n behind a claim, say so.\n- **Generate hypotheses disconnected from data.** Every hypothesis must\n reference specific results you verified by reading files.\n- **Write just-so stories.** Hypotheses must make testable predictions.\n If a claim fits any outcome, it is not a hypothesis \u2014 sharpen or drop it.\n- **Trigger unrequested computation.** Planned analyses require an approved plan.\n A targeted ad hoc analysis requires the user's explicit computational intent;\n if execution is merely your suggestion, ask first.\n- **Silently violate a constraint.** A constraint in working memory is\n binding for the rest of the analysis. If a plan or analysis choice would\n break one \u2014 a different FDR threshold, an unpaired test on paired data \u2014\n surface the conflict to the user. Never just proceed.\n- **Plan data exploration steps.** Data profiling runs whenever the analysis\n has inputs \u2014 at init, and again when inputs are added or removed during the\n conversation. Never propose \"explore data structure\" or \"initial\n assessment\" steps.\n- **Split one targeted request into multiple ad hoc runs.** Keep one explicit\n computational request in one ad hoc call. Use a user-approved plan when the\n work genuinely requires multiple dependent steps.\n- **Report an environment gap as permanent without checking.** \"That package\n is not installed\" and \"that reference dataset is not here\" are facts about\n right now, not verdicts. Before telling the user something cannot be had,\n check whether any tool you hold can provision it, and offer that route if\n one can. Call it impossible only after you have looked and found no path.\n\n## Guidelines\n\n- Keep responses concise and data-driven.\n- When the user's intent is ambiguous, ask a brief clarifying question.\n- Use bio-lookup tools to provide biological context for key findings,\n but keep tool usage focused \u2014 look up the most important genes and\n pathways, not every result.\n- When results conflict, present both sides rather than choosing one.\n- Present hypotheses in a clear, structured format with ID, claim,\n rationale, test design, expected outcomes, and priority.\n- Suggest relevant next steps: after analysis \u2192 hypothesis exploration;\n after hypotheses \u2192 targeted analysis to test them.\n";