@inflexa-ai/harness 0.7.0 → 0.7.2
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.
- package/dist/agents/conversation-agent.d.ts +8 -9
- package/dist/agents/conversation-agent.js +7 -2
- package/dist/agents/sandbox/shared.d.ts +2 -13
- package/dist/config/environment-stores.d.ts +42 -0
- package/dist/config/environment-stores.js +11 -0
- package/dist/contracts/chat-parts.d.ts +4 -2
- package/dist/contracts/schemas/chat-parts.d.ts +8 -0
- package/dist/contracts/schemas/chat-parts.js +1 -1
- package/dist/execution/ephemeral-runner.d.ts +2 -13
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +5 -0
- package/dist/prompts/planner.js +13 -0
- package/dist/provenance/collector.d.ts +39 -2
- package/dist/provenance/collector.js +62 -33
- package/dist/provenance/exec-frame.d.ts +14 -2
- package/dist/provenance/exec-frame.js +27 -4
- package/dist/reference-data/catalog.js +680 -1
- package/dist/tasks/data-profile.d.ts +2 -13
- package/dist/tools/research/generate-plan.d.ts +2 -8
- package/dist/tools/research/generate-plan.js +14 -7
- package/dist/tools/sandbox/list-available-packages.d.ts +2 -10
- package/dist/tools/sandbox/list-available-refs.d.ts +6 -9
- package/dist/tools/sandbox/list-available-refs.js +11 -4
- package/dist/tools/workspace/execute-command.js +1 -0
- package/dist/workflows/execute-analysis.d.ts +10 -5
- package/dist/workflows/execute-analysis.js +102 -36
- package/dist/workflows/sandbox-step.d.ts +25 -0
- package/dist/workflows/sandbox-step.js +20 -9
- package/package.json +2 -1
|
@@ -38,6 +38,7 @@ import type { Tool } from "../tools/define-tool.js";
|
|
|
38
38
|
import type { WorkspaceFilesystem } from "../workspace/filesystem.js";
|
|
39
39
|
import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
40
40
|
import { type BioToolKeys } from "../tools/bio/keys.js";
|
|
41
|
+
import type { EnvironmentStorePaths } from "../config/environment-stores.js";
|
|
41
42
|
import type { RunAuthorizer } from "../execution/run-authorizer.js";
|
|
42
43
|
import type { RunLauncher } from "../execution/run-launcher.js";
|
|
43
44
|
import { type SubmitReportDeps } from "../tools/iterate-report.js";
|
|
@@ -45,8 +46,13 @@ import type { EphemeralWorkflowInput, EphemeralResult } from "../execution/ephem
|
|
|
45
46
|
import type { Logger } from "../lib/logger.js";
|
|
46
47
|
/** Canonical agent id — the single source of truth. */
|
|
47
48
|
export declare const CONVERSATION_AGENT_ID: "conversation-agent";
|
|
48
|
-
/**
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
* The shared dependencies the composition root explodes apart. The environment
|
|
51
|
+
* store paths give this agent and its planner the same view of what is staged and
|
|
52
|
+
* what is importable that a sandbox agent has — read host-side, so answering
|
|
53
|
+
* "does this environment have X?" costs a lookup rather than a container.
|
|
54
|
+
*/
|
|
55
|
+
export interface ConversationAgentDeps extends EnvironmentStorePaths {
|
|
50
56
|
/** Operational logging seam; omitted falls back to no-op. */
|
|
51
57
|
readonly logger?: Logger;
|
|
52
58
|
/** The LLM seam every loop-driving tool runs its sub-agent on. */
|
|
@@ -112,13 +118,6 @@ export interface ConversationAgentDeps {
|
|
|
112
118
|
* built-in tool.
|
|
113
119
|
*/
|
|
114
120
|
readonly hostTools?: readonly Tool[];
|
|
115
|
-
/**
|
|
116
|
-
* Host path of the reference store — the same bytes sandboxes mount at
|
|
117
|
-
* `/mnt/refs`. Gives `generate_plan` reference discovery, so a plan can be
|
|
118
|
-
* grounded in what this environment actually holds. Omit when no store is
|
|
119
|
-
* provisioned.
|
|
120
|
-
*/
|
|
121
|
-
readonly refStorePath?: string;
|
|
122
121
|
}
|
|
123
122
|
/** Build the conversation `AgentDefinition` with every tool fully dep-bound. */
|
|
124
123
|
export declare function createConversationAgent(deps: ConversationAgentDeps): AgentDefinition;
|
|
@@ -39,6 +39,7 @@ import { createGeneratePlanTool, createLiteratureReviewerTool, createInspectRunT
|
|
|
39
39
|
import { createFileStatTool, createGrepTool, createListFilesTool, createReadFileTool, createShowPlanTool, createWorkspaceSearchTool, showFileTool, } from "../tools/workspace/index.js";
|
|
40
40
|
import { showUserTool } from "../tools/display/index.js";
|
|
41
41
|
import { createUpdateWorkingMemoryTool } from "../tools/memory/index.js";
|
|
42
|
+
import { createListAvailablePackagesTool } from "../tools/sandbox/list-available-packages.js";
|
|
42
43
|
import { createListAvailableRefsTool } from "../tools/sandbox/list-available-refs.js";
|
|
43
44
|
import { createExecutePlanTool } from "../tools/execute-plan.js";
|
|
44
45
|
import { createRunEphemeralTool } from "../tools/run-ephemeral.js";
|
|
@@ -49,7 +50,7 @@ export const CONVERSATION_AGENT_ID = "conversation-agent";
|
|
|
49
50
|
const CONVERSATION_MAX_ITERATIONS = 50;
|
|
50
51
|
/** Build the conversation `AgentDefinition` with every tool fully dep-bound. */
|
|
51
52
|
export function createConversationAgent(deps) {
|
|
52
|
-
const { provider, pool, embedding, workspaceFs, model, executeAnalysisWorkflow, ephemeralWorkflow, resolveWorkspaceRoot, runAuthorizer, runLauncher, createPreviewPublisher, bioKeys, templatesDir, skillsDir, chrome, resourcePolicy, hostTools, refStorePath, } = deps;
|
|
53
|
+
const { provider, pool, embedding, workspaceFs, model, executeAnalysisWorkflow, ephemeralWorkflow, resolveWorkspaceRoot, runAuthorizer, runLauncher, createPreviewPublisher, bioKeys, templatesDir, skillsDir, chrome, resourcePolicy, hostTools, refStorePath, packagesFile, } = deps;
|
|
53
54
|
const workingMemory = createWorkingMemory(pool);
|
|
54
55
|
const ncbi = createNcbiTools(bioKeys);
|
|
55
56
|
const chemDb = createChemDbTools(bioKeys);
|
|
@@ -85,11 +86,15 @@ export function createConversationAgent(deps) {
|
|
|
85
86
|
// blind is how a user gets asked to download something already installed, or
|
|
86
87
|
// told nothing is missing when it is; this is the only tool that can tell.
|
|
87
88
|
createListAvailableRefsTool({ ...(refStorePath ? { refStorePath } : {}) }),
|
|
89
|
+
// What is importable inside a sandbox. Reads the same manifest a sandbox agent
|
|
90
|
+
// reads, host-side — so "is scanpy available?" is a manifest lookup here rather
|
|
91
|
+
// than a whole ephemeral container spun up to run one import.
|
|
92
|
+
createListAvailablePackagesTool({ ...(packagesFile ? { packagesFile } : {}) }),
|
|
88
93
|
// Execution.
|
|
89
94
|
createInspectRunTool(pool),
|
|
90
95
|
// The dataset's own record. No file backs it — the DB row is the only copy.
|
|
91
96
|
createInspectDataProfileTool(pool),
|
|
92
|
-
createGeneratePlanTool({ provider, pool, model, resourcePolicy, ...(refStorePath ? { refStorePath } : {}) }),
|
|
97
|
+
createGeneratePlanTool({ provider, pool, model, resourcePolicy, ...(refStorePath ? { refStorePath } : {}), ...(packagesFile ? { packagesFile } : {}) }),
|
|
93
98
|
createExecutePlanTool({
|
|
94
99
|
pool,
|
|
95
100
|
executeAnalysisWorkflow,
|
|
@@ -25,6 +25,7 @@ import type { ProvenanceCollector as LineageCollector } from "../../provenance/c
|
|
|
25
25
|
import { type BioToolKeys } from "../../tools/bio/keys.js";
|
|
26
26
|
import { type BlockerHolder } from "../../tools/sandbox/report-blocker.js";
|
|
27
27
|
import type { AgentMeta, SandboxToolName } from "./types.js";
|
|
28
|
+
import type { EnvironmentStorePaths } from "../../config/environment-stores.js";
|
|
28
29
|
import type { Logger } from "../../lib/logger.js";
|
|
29
30
|
/**
|
|
30
31
|
* Tools every sandbox agent receives — sandbox-environment introspection,
|
|
@@ -50,7 +51,7 @@ export interface SandboxStepCoords {
|
|
|
50
51
|
readonly deadlineMs: () => number;
|
|
51
52
|
}
|
|
52
53
|
/** The shared dependency graph every sandbox agent draws from. */
|
|
53
|
-
export interface SandboxAgentDeps {
|
|
54
|
+
export interface SandboxAgentDeps extends EnvironmentStorePaths {
|
|
54
55
|
/** Operational logging seam; omitted falls back to no-op. */
|
|
55
56
|
readonly logger?: Logger;
|
|
56
57
|
readonly provider: ChatProvider;
|
|
@@ -69,18 +70,6 @@ export interface SandboxAgentDeps {
|
|
|
69
70
|
readonly model: string;
|
|
70
71
|
/** Absolute path to the skills tree. Omit to skip the skill tools. */
|
|
71
72
|
readonly skillsDir?: string;
|
|
72
|
-
/**
|
|
73
|
-
* Host path of the reference store — the same bytes the sandbox mounts at
|
|
74
|
-
* `/mnt/refs`. Omit when no store is provisioned; reference discovery then
|
|
75
|
-
* reports the store as unavailable rather than failing.
|
|
76
|
-
*/
|
|
77
|
-
readonly refStorePath?: string;
|
|
78
|
-
/**
|
|
79
|
-
* Host path of the library store's `packages.txt`. Omit when the host mounts
|
|
80
|
-
* the store at the sandbox's own path; a host whose store is baked into the
|
|
81
|
-
* image must inject its extracted copy, or the inventory reads as unknown.
|
|
82
|
-
*/
|
|
83
|
-
readonly packagesFile?: string;
|
|
84
73
|
/** Per-step coordinates resolved at the workflow body. */
|
|
85
74
|
readonly step: SandboxStepCoords;
|
|
86
75
|
/** API keys for the external bio/chem data sources. */
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two host paths that describe what an analysis environment can actually do:
|
|
3
|
+
* the reference data staged for it, and the packages installed in its sandbox.
|
|
4
|
+
*
|
|
5
|
+
* They are declared once, here, because every agent-facing deps bag that carries
|
|
6
|
+
* them means the same thing by them — and when each bag documented them itself,
|
|
7
|
+
* the prose drifted per site (the same field explained in terms of "the executor",
|
|
8
|
+
* "the profiler", "the planner") while the contract stayed identical. Extending
|
|
9
|
+
* this interface keeps one description of a field that has one meaning.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Host paths of the environment's two read-only stores.
|
|
13
|
+
*
|
|
14
|
+
* Both are HOST paths, deliberately: reading them host-side is what lets the
|
|
15
|
+
* conversation agent and the planner answer "what does this environment hold?"
|
|
16
|
+
* before any sandbox exists, and they are the same bytes a sandbox agent sees
|
|
17
|
+
* through its mounts — so every agent gets one answer rather than a per-vantage
|
|
18
|
+
* one. Neither store is ever written through these paths.
|
|
19
|
+
*
|
|
20
|
+
* Both are optional, and absence is a NORMAL state rather than an error: an
|
|
21
|
+
* omitted path makes its tool report the store as unavailable or unknown, which
|
|
22
|
+
* is a truthful answer an agent can act on. Absence must never be reported as a
|
|
23
|
+
* failure, and never papered over with a guessed path.
|
|
24
|
+
*/
|
|
25
|
+
export interface EnvironmentStorePaths {
|
|
26
|
+
/**
|
|
27
|
+
* Host path of the reference store — the same bytes sandboxes mount at
|
|
28
|
+
* `/mnt/refs`. Omit when no store is provisioned; reference discovery then
|
|
29
|
+
* reports the store as unavailable. Note that an installed store reads as
|
|
30
|
+
* absent if this is omitted, which is indistinguishable to an agent from
|
|
31
|
+
* having no data — so omit it only when there is genuinely nothing staged.
|
|
32
|
+
*/
|
|
33
|
+
readonly refStorePath?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Host path of the library store's `packages.txt` — the inventory of what is
|
|
36
|
+
* importable inside a sandbox. Omit when the host mounts the store at the
|
|
37
|
+
* sandbox's own path, which makes the container path correct as-is; a host
|
|
38
|
+
* whose store is baked into the image and never bind-mounted must inject the
|
|
39
|
+
* path to its own extracted copy, or the inventory reads as unknown.
|
|
40
|
+
*/
|
|
41
|
+
readonly packagesFile?: string;
|
|
42
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two host paths that describe what an analysis environment can actually do:
|
|
3
|
+
* the reference data staged for it, and the packages installed in its sandbox.
|
|
4
|
+
*
|
|
5
|
+
* They are declared once, here, because every agent-facing deps bag that carries
|
|
6
|
+
* them means the same thing by them — and when each bag documented them itself,
|
|
7
|
+
* the prose drifted per site (the same field explained in terms of "the executor",
|
|
8
|
+
* "the profiler", "the planner") while the contract stayed identical. Extending
|
|
9
|
+
* this interface keeps one description of a field that has one meaning.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -105,8 +105,10 @@ export interface RunStartedPart {
|
|
|
105
105
|
stepCount: number;
|
|
106
106
|
}
|
|
107
107
|
/** `"pending"` = waiting on dependencies; `"queued"` = dependencies satisfied,
|
|
108
|
-
* held until in-flight steps free machine-budget capacity
|
|
109
|
-
|
|
108
|
+
* held until in-flight steps free machine-budget capacity; `"skipped"` =
|
|
109
|
+
* unreachable — an upstream dependency failed or blocked, so the step can
|
|
110
|
+
* never be dispatched. */
|
|
111
|
+
export type StepStatus = "pending" | "queued" | "running" | "completed" | "failed" | "skipped";
|
|
110
112
|
export interface DagStepState {
|
|
111
113
|
id: string;
|
|
112
114
|
name: string;
|
|
@@ -135,6 +135,8 @@ export declare const StepStatusSchema: z.ZodEnum<{
|
|
|
135
135
|
running: "running";
|
|
136
136
|
completed: "completed";
|
|
137
137
|
failed: "failed";
|
|
138
|
+
skipped: "skipped";
|
|
139
|
+
queued: "queued";
|
|
138
140
|
}>;
|
|
139
141
|
export declare const DagStepStateSchema: z.ZodObject<{
|
|
140
142
|
id: z.ZodString;
|
|
@@ -145,6 +147,8 @@ export declare const DagStepStateSchema: z.ZodObject<{
|
|
|
145
147
|
running: "running";
|
|
146
148
|
completed: "completed";
|
|
147
149
|
failed: "failed";
|
|
150
|
+
skipped: "skipped";
|
|
151
|
+
queued: "queued";
|
|
148
152
|
}>;
|
|
149
153
|
level: z.ZodNumber;
|
|
150
154
|
dependsOn: z.ZodArray<z.ZodString>;
|
|
@@ -166,6 +170,8 @@ export declare const DagStatePartSchema: z.ZodObject<{
|
|
|
166
170
|
running: "running";
|
|
167
171
|
completed: "completed";
|
|
168
172
|
failed: "failed";
|
|
173
|
+
skipped: "skipped";
|
|
174
|
+
queued: "queued";
|
|
169
175
|
}>;
|
|
170
176
|
level: z.ZodNumber;
|
|
171
177
|
dependsOn: z.ZodArray<z.ZodString>;
|
|
@@ -530,6 +536,8 @@ export declare const CortexChatPartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
|
|
|
530
536
|
running: "running";
|
|
531
537
|
completed: "completed";
|
|
532
538
|
failed: "failed";
|
|
539
|
+
skipped: "skipped";
|
|
540
|
+
queued: "queued";
|
|
533
541
|
}>;
|
|
534
542
|
level: z.ZodNumber;
|
|
535
543
|
dependsOn: z.ZodArray<z.ZodString>;
|
|
@@ -96,7 +96,7 @@ export const RunStartedPartSchema = z.object({
|
|
|
96
96
|
stepCount: z.number(),
|
|
97
97
|
});
|
|
98
98
|
// ── DAG State ───────────────────────────────────────────────────────
|
|
99
|
-
export const StepStatusSchema = z.enum(["pending", "running", "completed", "failed"]);
|
|
99
|
+
export const StepStatusSchema = z.enum(["pending", "queued", "running", "completed", "failed", "skipped"]);
|
|
100
100
|
export const DagStepStateSchema = z
|
|
101
101
|
.object({
|
|
102
102
|
id: z.string(),
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import type { Pool } from "pg";
|
|
25
25
|
import type { RunSession } from "../auth/types.js";
|
|
26
|
+
import type { EnvironmentStorePaths } from "../config/environment-stores.js";
|
|
26
27
|
import type { ResourcePolicy } from "../config/resource-limits.js";
|
|
27
28
|
import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
|
|
28
29
|
import type { SandboxClient } from "../sandbox/client.js";
|
|
@@ -35,7 +36,7 @@ export declare const EPHEMERAL_AGENT_ID = "ephemeral-executor";
|
|
|
35
36
|
/** Workflow-id prefix — the boot-time never-recover sweep matches on it. */
|
|
36
37
|
export declare const EPHEMERAL_WORKFLOW_PREFIX: "ephemeral:";
|
|
37
38
|
/** The body's construction-time deps — closed over at registration. */
|
|
38
|
-
export interface EphemeralDeps {
|
|
39
|
+
export interface EphemeralDeps extends EnvironmentStorePaths {
|
|
39
40
|
/** Operational logging seam; omitted falls back to no-op. */
|
|
40
41
|
readonly logger?: Logger;
|
|
41
42
|
readonly provider: ChatProvider;
|
|
@@ -52,18 +53,6 @@ export interface EphemeralDeps {
|
|
|
52
53
|
readonly bioKeys: BioToolKeys;
|
|
53
54
|
/** Host resource policy — its `ephemeral` spec overrides the default sandbox size. */
|
|
54
55
|
readonly resourcePolicy?: ResourcePolicy;
|
|
55
|
-
/**
|
|
56
|
-
* Host path of the library store's `packages.txt`. Omit when the host mounts the
|
|
57
|
-
* store at the sandbox's own path; a host whose store is baked into the image must
|
|
58
|
-
* inject its extracted copy, or the executor reads the inventory as unknown.
|
|
59
|
-
*/
|
|
60
|
-
readonly packagesFile?: string;
|
|
61
|
-
/**
|
|
62
|
-
* Host path of the reference store. Omit only when none is provisioned: the
|
|
63
|
-
* executor carries reference discovery, and without this it reports an installed
|
|
64
|
-
* store as absent.
|
|
65
|
-
*/
|
|
66
|
-
readonly refStorePath?: string;
|
|
67
56
|
}
|
|
68
57
|
/**
|
|
69
58
|
* Workflow input. JSON-serialisable — the authorized `RunSession` carries the
|
|
@@ -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\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, format)\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/format\nmatters and 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 at analysis init.\n Never propose \"explore data structure\" or \"initial 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\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 `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\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, format)\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/format\nmatters and 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 at analysis init.\n Never propose \"explore data structure\" or \"initial 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";
|
|
@@ -342,6 +342,11 @@ Instead:
|
|
|
342
342
|
call spins up a sandbox pod (~30-60s of overhead). Combine related data
|
|
343
343
|
questions into a single multi-part prompt — one call is far faster than
|
|
344
344
|
two sequential ones.
|
|
345
|
+
- **Report an environment gap as permanent without checking.** "That package
|
|
346
|
+
is not installed" and "that reference dataset is not here" are facts about
|
|
347
|
+
right now, not verdicts. Before telling the user something cannot be had,
|
|
348
|
+
check whether any tool you hold can provision it, and offer that route if
|
|
349
|
+
one can. Call it impossible only after you have looked and found no path.
|
|
345
350
|
|
|
346
351
|
## Guidelines
|
|
347
352
|
|
package/dist/prompts/planner.js
CHANGED
|
@@ -110,6 +110,19 @@ before relying on it.
|
|
|
110
110
|
annotation rather than the run. The distinction is whether the research question
|
|
111
111
|
survives without it — not whether the step does.
|
|
112
112
|
|
|
113
|
+
### Available Packages
|
|
114
|
+
A step can only use what is already installed in the sandbox — nothing installs at
|
|
115
|
+
run time. A step that leans on an absent library is a guaranteed failure discovered
|
|
116
|
+
only once the run reaches it, which is the most expensive moment to learn it.
|
|
117
|
+
|
|
118
|
+
- When a step depends on a specific library, confirm it is importable before you
|
|
119
|
+
commit the step to it. Checking a handful of names is one cheap call.
|
|
120
|
+
- Prefer what is present over what you would reach for by habit — an equivalent
|
|
121
|
+
installed package beats the canonical one that is not there.
|
|
122
|
+
- When nothing installed can do the step's work, treat it exactly as you would an
|
|
123
|
+
absent reference: \`request_clarification\`, rather than plan a step that can
|
|
124
|
+
only report failure.
|
|
125
|
+
|
|
113
126
|
### Resource Estimation
|
|
114
127
|
${resourceEstimationSection(resourcePolicy)}
|
|
115
128
|
|
|
@@ -39,6 +39,25 @@ export interface InputClassificationContext {
|
|
|
39
39
|
stepId?: string;
|
|
40
40
|
runId?: string;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The outcome of classifying one read: an input edge the step may assert, or
|
|
44
|
+
* an explicit refusal naming the producing step it would have pointed at.
|
|
45
|
+
*
|
|
46
|
+
* A refusal is a value rather than a thrown error or a `data` classification.
|
|
47
|
+
* Throwing would fail an exec over bookkeeping, and reclassifying as `data`
|
|
48
|
+
* would launder a sibling's scratch file into an attestable analysis-level
|
|
49
|
+
* input — a different fabrication, not a fix. The scraped ids ride along
|
|
50
|
+
* because they are the only thing that makes a drop attributable to a
|
|
51
|
+
* producing step in a log record.
|
|
52
|
+
*/
|
|
53
|
+
export type ReadClassification = {
|
|
54
|
+
readonly admissible: true;
|
|
55
|
+
readonly context: InputClassificationContext;
|
|
56
|
+
} | {
|
|
57
|
+
readonly admissible: false;
|
|
58
|
+
readonly refRunId: string;
|
|
59
|
+
readonly refStepId: string;
|
|
60
|
+
};
|
|
42
61
|
/**
|
|
43
62
|
* Build classification context for a file read using known step metadata.
|
|
44
63
|
*
|
|
@@ -50,7 +69,7 @@ export interface InputClassificationContext {
|
|
|
50
69
|
* any dependsOn entry), this falls back to path extraction — see the
|
|
51
70
|
* prior-run branch below for why.
|
|
52
71
|
*/
|
|
53
|
-
export declare function classifyReadPath(relativePath: string, ownStepId: string, ownRunId: string, dependsOn?: string[]):
|
|
72
|
+
export declare function classifyReadPath(relativePath: string, ownStepId: string, ownRunId: string, dependsOn?: string[]): ReadClassification;
|
|
54
73
|
export interface ProvenanceCollectorOptions {
|
|
55
74
|
stepId: string;
|
|
56
75
|
runId: string;
|
|
@@ -75,12 +94,26 @@ export declare class ProvenanceCollector {
|
|
|
75
94
|
private readonly fileToolRecords;
|
|
76
95
|
/** Command execution records, keyed by output path (relative to /artifacts). */
|
|
77
96
|
private readonly commandRecords;
|
|
97
|
+
/** Refused read paths already narrated, so the same edge is reported once per step. */
|
|
98
|
+
private readonly narratedRefusals;
|
|
78
99
|
constructor(opts: ProvenanceCollectorOptions);
|
|
79
100
|
/**
|
|
80
101
|
* Set the input file ID map (passed by reference from the data-mount filesystem).
|
|
81
102
|
* Called after construction when the data mount filesystem is available.
|
|
82
103
|
*/
|
|
83
104
|
setInputFileIdMap(map: Map<string, string>): void;
|
|
105
|
+
/**
|
|
106
|
+
* Claim the right to narrate a refused read, true only the first time this
|
|
107
|
+
* step sees the path.
|
|
108
|
+
*
|
|
109
|
+
* A refusal reports one fact — a lineage edge that will not exist — and the
|
|
110
|
+
* fact does not change when the same file is read again. Reads dedup within
|
|
111
|
+
* one frame but not across them, and a step issues many execs against a tree
|
|
112
|
+
* whose watch set spans the whole analysis, so an undeduped narration is a
|
|
113
|
+
* steady stream rather than an exception. The signal exists to be noticed;
|
|
114
|
+
* emitting it at that volume is what teaches a reader to filter it away.
|
|
115
|
+
*/
|
|
116
|
+
claimRefusalNarration(relativePath: string): boolean;
|
|
84
117
|
/**
|
|
85
118
|
* Track a file read from a read-only mount.
|
|
86
119
|
* Called by sandbox exec provenance frame processing.
|
|
@@ -92,8 +125,12 @@ export declare class ProvenanceCollector {
|
|
|
92
125
|
* @param hash - SHA-256 hash of the file content, or null if file no longer exists
|
|
93
126
|
* @param context - Explicit classification context from the caller. When provided,
|
|
94
127
|
* the collector uses it directly instead of classifying from the path.
|
|
128
|
+
* @returns the tracked ref, or `null` when the caller supplied no context and the
|
|
129
|
+
* path classifies as inadmissible — an undeclared same-run sibling, which must
|
|
130
|
+
* not become an attestation target.
|
|
95
131
|
*/
|
|
96
|
-
trackInputAccess(mountPath: string, relativePath: string, hash: string | null, context
|
|
132
|
+
trackInputAccess(mountPath: string, relativePath: string, hash: string | null, context: InputClassificationContext): InputRef;
|
|
133
|
+
trackInputAccess(mountPath: string, relativePath: string, hash: string | null): InputRef | null;
|
|
97
134
|
/**
|
|
98
135
|
* Record a file tool write (write_file, edit_file).
|
|
99
136
|
* Called after the data-mount filesystem records the artifact.
|
|
@@ -42,32 +42,45 @@ function inferScriptPath(command, args) {
|
|
|
42
42
|
*/
|
|
43
43
|
export function classifyReadPath(relativePath, ownStepId, ownRunId, dependsOn) {
|
|
44
44
|
if (relativePath.startsWith("data/") || relativePath.startsWith("dataprofile/")) {
|
|
45
|
-
return { source: "data" };
|
|
45
|
+
return { admissible: true, context: { source: "data" } };
|
|
46
46
|
}
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const upstreamPrefix = `runs/${ownRunId}/${depStepId}/`;
|
|
56
|
-
if (relativePath.startsWith(upstreamPrefix)) {
|
|
57
|
-
return { source: "upstream", stepId: depStepId, runId: ownRunId };
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// Same-run sibling step. dependsOn only drives topo-sort ordering, not
|
|
62
|
-
// read authorization, so a read outside dependsOn is still a valid
|
|
63
|
-
// upstream input — just extract the stepId from the path.
|
|
47
|
+
// Everything under this run resolves off ONE segment — the first one after
|
|
48
|
+
// the run id, which names the step owning that subtree. It is read once and
|
|
49
|
+
// compared, rather than each case prefix-matching `runs/{runId}/{id}/`
|
|
50
|
+
// independently, because a trailing separator is not guaranteed to be
|
|
51
|
+
// present: an `opendir` of a step directory surfaces the bare name. Under
|
|
52
|
+
// per-case prefixes every such directory read misses its own case and lands
|
|
53
|
+
// on the refusal below — a step refusing an edge to itself, or to a
|
|
54
|
+
// dependency it did declare.
|
|
64
55
|
const ownRunPrefix = `runs/${ownRunId}/`;
|
|
65
56
|
if (relativePath.startsWith(ownRunPrefix)) {
|
|
66
57
|
const afterRun = relativePath.slice(ownRunPrefix.length);
|
|
67
58
|
const slashIdx = afterRun.indexOf("/");
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
return { source: "
|
|
59
|
+
const head = slashIdx >= 0 ? afterRun.slice(0, slashIdx) : afterRun;
|
|
60
|
+
if (head === ownStepId) {
|
|
61
|
+
return { admissible: true, context: { source: "artifacts", stepId: ownStepId, runId: ownRunId } };
|
|
62
|
+
}
|
|
63
|
+
if (dependsOn?.includes(head)) {
|
|
64
|
+
return { admissible: true, context: { source: "upstream", stepId: head, runId: ownRunId } };
|
|
65
|
+
}
|
|
66
|
+
// A same-run step this one never declared. It carries no ordering
|
|
67
|
+
// guarantee relative to this step: its directory is mounted read-write in
|
|
68
|
+
// its own container and may be mid-write, or hold a scratch file deleted
|
|
69
|
+
// before this step reconciles, so nothing observed there is a stable
|
|
70
|
+
// artifact this step can be said to have consumed. Declaration is the
|
|
71
|
+
// available proof of stability — the scheduler admits a step only once
|
|
72
|
+
// every declared dependency has completed, and a completed step never
|
|
73
|
+
// writes into its tree again. An absent `dependsOn` therefore refuses
|
|
74
|
+
// every sibling: absence of the declaration is not evidence of stability.
|
|
75
|
+
//
|
|
76
|
+
// `head` is the refusal's diagnostic attribution only; unlike the
|
|
77
|
+
// prior-run branch below it feeds no classification. It is a step id for
|
|
78
|
+
// any path that descends into a subtree, and a best guess for a bare
|
|
79
|
+
// name, which the run directory also admits for its own run-scoped files
|
|
80
|
+
// (`runs/{runId}/synthesis.json`) — indistinguishable from a step
|
|
81
|
+
// directory by path alone, and refused either way.
|
|
82
|
+
if (head) {
|
|
83
|
+
return { admissible: false, refRunId: ownRunId, refStepId: head };
|
|
71
84
|
}
|
|
72
85
|
}
|
|
73
86
|
// Prior-run fallback: a read under `runs/{otherRunId}/...` that is neither
|
|
@@ -80,10 +93,10 @@ export function classifyReadPath(relativePath, ownStepId, ownRunId, dependsOn) {
|
|
|
80
93
|
const priorRunId = parts[1];
|
|
81
94
|
const priorStepId = parts[2];
|
|
82
95
|
if (priorRunId && priorStepId) {
|
|
83
|
-
return { source: "prior", stepId: priorStepId, runId: priorRunId };
|
|
96
|
+
return { admissible: true, context: { source: "prior", stepId: priorStepId, runId: priorRunId } };
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
|
-
return { source: "data" };
|
|
99
|
+
return { admissible: true, context: { source: "data" } };
|
|
87
100
|
}
|
|
88
101
|
export class ProvenanceCollector {
|
|
89
102
|
stepId;
|
|
@@ -103,6 +116,8 @@ export class ProvenanceCollector {
|
|
|
103
116
|
fileToolRecords = new Map();
|
|
104
117
|
/** Command execution records, keyed by output path (relative to /artifacts). */
|
|
105
118
|
commandRecords = new Map();
|
|
119
|
+
/** Refused read paths already narrated, so the same edge is reported once per step. */
|
|
120
|
+
narratedRefusals = new Set();
|
|
106
121
|
constructor(opts) {
|
|
107
122
|
this.stepId = opts.stepId;
|
|
108
123
|
this.runId = opts.runId;
|
|
@@ -117,24 +132,38 @@ export class ProvenanceCollector {
|
|
|
117
132
|
this.inputFileIdMap = map;
|
|
118
133
|
}
|
|
119
134
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
135
|
+
* Claim the right to narrate a refused read, true only the first time this
|
|
136
|
+
* step sees the path.
|
|
122
137
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* the collector uses it directly instead of classifying from the path.
|
|
138
|
+
* A refusal reports one fact — a lineage edge that will not exist — and the
|
|
139
|
+
* fact does not change when the same file is read again. Reads dedup within
|
|
140
|
+
* one frame but not across them, and a step issues many execs against a tree
|
|
141
|
+
* whose watch set spans the whole analysis, so an undeduped narration is a
|
|
142
|
+
* steady stream rather than an exception. The signal exists to be noticed;
|
|
143
|
+
* emitting it at that volume is what teaches a reader to filter it away.
|
|
130
144
|
*/
|
|
145
|
+
claimRefusalNarration(relativePath) {
|
|
146
|
+
if (this.narratedRefusals.has(relativePath))
|
|
147
|
+
return false;
|
|
148
|
+
this.narratedRefusals.add(relativePath);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
131
151
|
trackInputAccess(mountPath, relativePath, hash, context) {
|
|
132
152
|
const key = `${mountPath}:${relativePath}`;
|
|
133
153
|
const existing = this.inputAccesses.get(key);
|
|
134
154
|
if (existing)
|
|
135
155
|
return existing;
|
|
136
156
|
// Use explicit context if provided, otherwise fall back to path classification
|
|
137
|
-
|
|
157
|
+
let classification;
|
|
158
|
+
if (context) {
|
|
159
|
+
classification = context;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const classified = classifyReadPath(relativePath, this.stepId, this.runId, this.dependsOn);
|
|
163
|
+
if (!classified.admissible)
|
|
164
|
+
return null;
|
|
165
|
+
classification = classified.context;
|
|
166
|
+
}
|
|
138
167
|
// A still-absolute `relativePath` is a report the caller could not
|
|
139
168
|
// relativize — it names somewhere outside the mount. It rides verbatim:
|
|
140
169
|
// prepending the mount root would forge an in-tree name for a foreign
|
|
@@ -8,10 +8,20 @@
|
|
|
8
8
|
* `runs/{priorRunId}/...`) so `classifyReadPath` and the collector's
|
|
9
9
|
* `stepPrefix` normalization see the shapes they expect.
|
|
10
10
|
*
|
|
11
|
+
* A read naming a same-run sibling this step never declared is refused here,
|
|
12
|
+
* before it reaches the collector: a ref the collector never holds cannot
|
|
13
|
+
* become an attestation target, so the edge is unrepresentable rather than
|
|
14
|
+
* merely filtered downstream. Each refused path is logged once per step — an
|
|
15
|
+
* edge asserted over a step that was still writing is invisible by nature, so a
|
|
16
|
+
* silent drop would rebuild exactly the blind spot the refusal exists to close,
|
|
17
|
+
* while re-narrating a path already reported adds no fact and buries the rest.
|
|
18
|
+
*
|
|
11
19
|
* Degrades safely: a disabled or absent frame records the command with no
|
|
12
|
-
* inputs and no writes (its outputs fall back to leaves at registration)
|
|
13
|
-
*
|
|
20
|
+
* inputs and no writes (its outputs fall back to leaves at registration), and a
|
|
21
|
+
* frame whose every read is refused still records its command. It never throws —
|
|
22
|
+
* provenance is best-effort and must not fail an exec.
|
|
14
23
|
*/
|
|
24
|
+
import type { Logger } from "../lib/logger.js";
|
|
15
25
|
import type { ProvenanceFrame } from "../sandbox/types.js";
|
|
16
26
|
import { type ProvenanceCollector } from "./collector.js";
|
|
17
27
|
/**
|
|
@@ -32,6 +42,8 @@ export interface FeedExecFrameArgs {
|
|
|
32
42
|
readonly durationMs: number | null;
|
|
33
43
|
/** The frame surfaced on `ExecResult.provenance` (may be absent). */
|
|
34
44
|
readonly provenance?: ProvenanceFrame;
|
|
45
|
+
/** Diagnostics seam. Left unwired, refusals still happen but go unnarrated. */
|
|
46
|
+
readonly logger?: Logger;
|
|
35
47
|
}
|
|
36
48
|
/**
|
|
37
49
|
* Translate one exec's frame into a collector record. Per-command input
|