@inflexa-ai/harness 0.9.0 → 0.11.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.
- package/dist/agents/conversation-agent.js +3 -1
- package/dist/execution/run-synthesis.d.ts +7 -0
- package/dist/execution/run-synthesis.js +8 -2
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +11 -2
- package/dist/schemas/validate-plan.js +10 -5
- package/dist/tools/research/inspect-run.js +10 -3
- package/dist/tools/workspace/read-file.d.ts +5 -0
- package/dist/tools/workspace/read-file.js +20 -2
- package/dist/workflows/execute-analysis.d.ts +14 -1
- package/dist/workflows/execute-analysis.js +105 -8
- package/dist/workspace/paths.d.ts +10 -0
- package/dist/workspace/paths.js +10 -0
- package/package.json +1 -1
|
@@ -32,7 +32,7 @@ import { createWorkingMemory } from "../memory/working-memory.js";
|
|
|
32
32
|
import { conversationPrompt } from "../prompts/conversation.js";
|
|
33
33
|
import { composeSystemPrompt } from "./system-prompt.js";
|
|
34
34
|
// Bio-lookup leaf tools (pure — no dependencies).
|
|
35
|
-
import { searchGeneTool, searchPathwayTool, lookupGoTermTool, searchInteractionsTool, chemblTool, pubchemTool, openTargetsTool, searchPharmgkbTool, searchFaersTool, searchClinicalTrialsTool, searchGeoDatasetsTool, createSearchDgidbTool, searchBgeeExpressionTool, getImpcKoProfileTool, checkSafetyPanelTool, } from "../tools/bio/index.js";
|
|
35
|
+
import { searchGeneTool, searchPathwayTool, lookupGoTermTool, searchInteractionsTool, chemblTool, pubchemTool, openTargetsTool, searchGwasCatalogTool, searchPharmgkbTool, searchFaersTool, searchClinicalTrialsTool, searchGeoDatasetsTool, createSearchDgidbTool, searchBgeeExpressionTool, getImpcKoProfileTool, checkSafetyPanelTool, } from "../tools/bio/index.js";
|
|
36
36
|
import { createNcbiTools, createChemDbTools } from "../tools/bio/keys.js";
|
|
37
37
|
// Dependency-bearing tool factories.
|
|
38
38
|
import { createGeneratePlanTool, createLiteratureReviewerTool, createInspectRunTool, createInspectDataProfileTool, createGenerateAnalogyReportTool, } from "../tools/research/index.js";
|
|
@@ -68,6 +68,8 @@ export function createConversationAgent(deps) {
|
|
|
68
68
|
pubchemTool,
|
|
69
69
|
// Translational medicine.
|
|
70
70
|
openTargetsTool,
|
|
71
|
+
// Genetic evidence — SNP-trait associations for target validation / MR support.
|
|
72
|
+
searchGwasCatalogTool,
|
|
71
73
|
searchPharmgkbTool,
|
|
72
74
|
searchFaersTool,
|
|
73
75
|
searchClinicalTrialsTool,
|
|
@@ -30,6 +30,13 @@ import type { ChatProvider } from "../providers/types.js";
|
|
|
30
30
|
import type { RunSession } from "../auth/types.js";
|
|
31
31
|
import { type Tool } from "../tools/define-tool.js";
|
|
32
32
|
import type { BioToolKeys } from "../tools/bio/keys.js";
|
|
33
|
+
/**
|
|
34
|
+
* The synthesizer's agent identity — the `source.agentId` on its loop events
|
|
35
|
+
* and the `agent_id` of the run-phase `synthesis` row `executeAnalysis` writes
|
|
36
|
+
* to `cortex_step_executions`. One definition so the ledger row and the stream
|
|
37
|
+
* events can never disagree about who did the work.
|
|
38
|
+
*/
|
|
39
|
+
export declare const SYNTHESIS_AGENT_ID = "run-synthesizer";
|
|
33
40
|
type SynthesizerOutcome = {
|
|
34
41
|
kind: "submitted";
|
|
35
42
|
synthesis: RunSynthesis;
|
|
@@ -39,7 +39,13 @@ import { defineTool } from "../tools/define-tool.js";
|
|
|
39
39
|
import { createReportBlockerToolFor } from "../tools/sandbox/report-blocker.js";
|
|
40
40
|
import { createLiteratureReviewerTool } from "../tools/research/literature-reviewer.js";
|
|
41
41
|
// ── Agent identity / budgets ────────────────────────────────────────
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The synthesizer's agent identity — the `source.agentId` on its loop events
|
|
44
|
+
* and the `agent_id` of the run-phase `synthesis` row `executeAnalysis` writes
|
|
45
|
+
* to `cortex_step_executions`. One definition so the ledger row and the stream
|
|
46
|
+
* events can never disagree about who did the work.
|
|
47
|
+
*/
|
|
48
|
+
export const SYNTHESIS_AGENT_ID = "run-synthesizer";
|
|
43
49
|
/**
|
|
44
50
|
* Budget for the synthesizer loop: read summaries + 1–3 reviewer delegations
|
|
45
51
|
* + draft + 1–2 submit/fix cycles + headroom.
|
|
@@ -349,7 +355,7 @@ export async function generateRunSynthesis(input) {
|
|
|
349
355
|
const blockerTool = buildBlockerTool(holder);
|
|
350
356
|
const tools = [validateTool, submitTool, blockerTool, reviewer];
|
|
351
357
|
const agent = {
|
|
352
|
-
id:
|
|
358
|
+
id: SYNTHESIS_AGENT_ID,
|
|
353
359
|
systemPrompt: composeSystemPrompt(synthesisAgentPrompt),
|
|
354
360
|
model: input.model,
|
|
355
361
|
tools,
|
|
@@ -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)\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 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";
|
|
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";
|
|
@@ -47,6 +47,13 @@ Route by what you are asking for:
|
|
|
47
47
|
Do not start with filesystem exploration for general data questions.
|
|
48
48
|
Do not tell the user you cannot access their files — you can.
|
|
49
49
|
|
|
50
|
+
When the user points to data by name or location that is not yet an input of
|
|
51
|
+
this analysis, adding it as an input may be available to you as a host tool.
|
|
52
|
+
If so: list the candidates, confirm with the user, and add them so they get
|
|
53
|
+
profiled. Never tell the user to copy files into a folder to make them
|
|
54
|
+
visible — reach for the input tool if you have one, or say plainly that the
|
|
55
|
+
data must be added as an input and let the host handle how.
|
|
56
|
+
|
|
50
57
|
## Analysis Planning
|
|
51
58
|
|
|
52
59
|
When the user wants to run an analysis:
|
|
@@ -336,8 +343,10 @@ Instead:
|
|
|
336
343
|
binding for the rest of the analysis. If a plan or analysis choice would
|
|
337
344
|
break one — a different FDR threshold, an unpaired test on paired data —
|
|
338
345
|
surface the conflict to the user. Never just proceed.
|
|
339
|
-
- **Plan data exploration steps.** Data profiling runs
|
|
340
|
-
|
|
346
|
+
- **Plan data exploration steps.** Data profiling runs whenever the analysis
|
|
347
|
+
has inputs — at init, and again when inputs are added or removed during the
|
|
348
|
+
conversation. Never propose "explore data structure" or "initial
|
|
349
|
+
assessment" steps.
|
|
341
350
|
- **Call \`run_ephemeral\` multiple times for related data questions.** Each
|
|
342
351
|
call spins up a sandbox pod (~30-60s of overhead). Combine related data
|
|
343
352
|
questions into a single multi-part prompt — one call is far faster than
|
|
@@ -13,14 +13,17 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { KNOWN_AGENT_IDS } from "../agents/sandbox-catalog.js";
|
|
15
15
|
import { CycleError, DependencyError, topoSortIntoWaves } from "../execution/topo-sort.js";
|
|
16
|
-
import { isSafeId, STEP_SUBDIRS } from "../workspace/paths.js";
|
|
16
|
+
import { isSafeId, STEP_SUBDIRS, SYNTHESIS_STEP_ID } from "../workspace/paths.js";
|
|
17
17
|
const KNOWN_AGENTS = new Set(KNOWN_AGENT_IDS);
|
|
18
18
|
/**
|
|
19
19
|
* Reserved step-id names. A step id equal to an artifact subdirectory name
|
|
20
20
|
* would make its directory (`runs/{runId}/{stepId}`) collide with the
|
|
21
|
-
* subdirectory convention agents expect inside a step
|
|
21
|
+
* subdirectory convention agents expect inside a step, and
|
|
22
|
+
* {@link SYNTHESIS_STEP_ID} is the run-phase ledger row `executeAnalysis`
|
|
23
|
+
* writes for run-level synthesis — a plan step with that id would collide with
|
|
24
|
+
* the row's `(run_id, step_id)` primary key (see the harness-workspace-tools spec).
|
|
22
25
|
*/
|
|
23
|
-
const RESERVED_STEP_IDS = new Set(STEP_SUBDIRS);
|
|
26
|
+
const RESERVED_STEP_IDS = new Set([...STEP_SUBDIRS, SYNTHESIS_STEP_ID]);
|
|
24
27
|
/** Derive a filesystem-safe output prefix from a step ID. */
|
|
25
28
|
export function deriveOutputPrefix(stepId) {
|
|
26
29
|
return stepId
|
|
@@ -97,11 +100,13 @@ export function validatePlan(plan, options) {
|
|
|
97
100
|
`${ceiling.maxMemoryGb} per step — reduce memoryGb or restructure the step`);
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
|
-
// 5. Reserved step-id names (collide with artifact subdirectories
|
|
103
|
+
// 5. Reserved step-id names (collide with artifact subdirectories or the
|
|
104
|
+
// run-phase synthesis ledger row)
|
|
101
105
|
for (const step of plan.steps) {
|
|
102
106
|
if (RESERVED_STEP_IDS.has(step.id.toLowerCase())) {
|
|
103
107
|
errors.push(`Step "${step.id}" uses a reserved name — step ids must not be one of: ` +
|
|
104
|
-
`${
|
|
108
|
+
`${[...RESERVED_STEP_IDS].join(", ")} (the artifact subdirectory names collide with the ` +
|
|
109
|
+
`step-directory convention; "${SYNTHESIS_STEP_ID}" is the run-level synthesis phase)`);
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
// 6. Step-id path safety. The id becomes the `runs/{runId}/{stepId}` directory
|
|
@@ -16,6 +16,7 @@ import { ok } from "neverthrow";
|
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
import { queryRun, queryRunsByAnalysis, queryStepsByRun } from "../../state/index.js";
|
|
18
18
|
import { scopeResource } from "../../auth/types.js";
|
|
19
|
+
import { SYNTHESIS_STEP_ID } from "../../workspace/paths.js";
|
|
19
20
|
import { unwrapOrThrow } from "../../lib/result.js";
|
|
20
21
|
import { defineTool } from "../define-tool.js";
|
|
21
22
|
function formatRun(r, verbose) {
|
|
@@ -73,9 +74,15 @@ export function createInspectRunTool(pool) {
|
|
|
73
74
|
agentId: s.agentId,
|
|
74
75
|
wave: s.wave,
|
|
75
76
|
status: s.status,
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
77
|
+
// No per-step `summaryPath` when the file cannot exist: a seeded row
|
|
78
|
+
// (`pending`/`skipped`) never produced output, and the reserved
|
|
79
|
+
// `synthesis` phase is not a sandbox step — it writes the run-level
|
|
80
|
+
// `synthesis.json`, surfaced as the run's `synthesisPath`, not a
|
|
81
|
+
// `{stepId}/output/summary.md`. Emitting the per-step path for it would
|
|
82
|
+
// point the agent at a file that never exists.
|
|
83
|
+
...(s.status === "pending" || s.status === "skipped" || s.stepId === SYNTHESIS_STEP_ID
|
|
84
|
+
? {}
|
|
85
|
+
: { summaryPath: `runs/${input.runId}/${s.stepId}/output/summary.md` }),
|
|
79
86
|
...(verbose
|
|
80
87
|
? {
|
|
81
88
|
durationMs: s.durationMs,
|
|
@@ -30,6 +30,11 @@ type ReadFileOutput = {
|
|
|
30
30
|
} | {
|
|
31
31
|
status: "out_of_scope";
|
|
32
32
|
path: string;
|
|
33
|
+
} | {
|
|
34
|
+
status: "binary";
|
|
35
|
+
path: string;
|
|
36
|
+
mode: "head" | "tail" | "full";
|
|
37
|
+
totalSize?: number;
|
|
33
38
|
};
|
|
34
39
|
export declare function createReadFileTool(fs: WorkspaceFilesystem, workingDir?: string): import("../define-tool.js").Tool<{
|
|
35
40
|
path: string;
|
|
@@ -13,6 +13,18 @@ import { defineTool } from "../define-tool.js";
|
|
|
13
13
|
import { unwrapOrThrow } from "../../lib/result.js";
|
|
14
14
|
/** Default cap — large enough for typical CSV/MD/JSON, small enough to keep tool results out of context-blowing territory. */
|
|
15
15
|
const DEFAULT_MAX_BYTES = 256 * 1024;
|
|
16
|
+
/**
|
|
17
|
+
* Treat returned content as binary when it contains a NUL byte — `grep`'s
|
|
18
|
+
* `looksBinary` heuristic, applied here to the whole returned window rather than
|
|
19
|
+
* a 1 KiB probe. These exact bytes are what would be UTF-8-decoded into the
|
|
20
|
+
* transcript, and a NUL both renders that decode as lossy garbage (useless to
|
|
21
|
+
* the model) and cannot be persisted into the JSONB message store — Postgres
|
|
22
|
+
* rejects U+0000 with 22P05, failing the whole turn's append. Reporting the
|
|
23
|
+
* read as `binary` keeps a NUL out of both.
|
|
24
|
+
*/
|
|
25
|
+
function looksBinary(content) {
|
|
26
|
+
return content.includes(0);
|
|
27
|
+
}
|
|
16
28
|
const ReadFileInputSchema = z.object({
|
|
17
29
|
path: z
|
|
18
30
|
.string()
|
|
@@ -45,8 +57,10 @@ export function createReadFileTool(fs, workingDir) {
|
|
|
45
57
|
"as UTF-8 text. Output is capped at " +
|
|
46
58
|
`${DEFAULT_MAX_BYTES} bytes; pass headLines or tailLines to read a ` +
|
|
47
59
|
"specific window of a large file (typical for bio CSV/TSV/log files). " +
|
|
48
|
-
"Oversize reads come back truncated with a marker.
|
|
49
|
-
"
|
|
60
|
+
"Oversize reads come back truncated with a marker. This tool is for " +
|
|
61
|
+
"text; a binary file (one holding NUL bytes, e.g. a .gz or image) comes " +
|
|
62
|
+
"back as a `binary` data variant rather than decoded garbage. Missing " +
|
|
63
|
+
"paths and paths outside the analysis tree return a data variant — not an error.",
|
|
50
64
|
inputSchema: ReadFileInputSchema,
|
|
51
65
|
execute: async ({ path, headLines, tailLines }, ctx) => {
|
|
52
66
|
if (headLines !== undefined && tailLines !== undefined) {
|
|
@@ -67,6 +81,8 @@ export function createReadFileTool(fs, workingDir) {
|
|
|
67
81
|
const mode = headLines !== undefined ? "head" : tailLines !== undefined ? "tail" : "full";
|
|
68
82
|
switch (result.kind) {
|
|
69
83
|
case "ok":
|
|
84
|
+
if (looksBinary(result.content))
|
|
85
|
+
return ok({ status: "binary", path, mode });
|
|
70
86
|
return ok({
|
|
71
87
|
status: "ok",
|
|
72
88
|
path,
|
|
@@ -74,6 +90,8 @@ export function createReadFileTool(fs, workingDir) {
|
|
|
74
90
|
content: result.content.toString("utf8"),
|
|
75
91
|
});
|
|
76
92
|
case "truncated":
|
|
93
|
+
if (looksBinary(result.content))
|
|
94
|
+
return ok({ status: "binary", path, mode, totalSize: result.totalSize });
|
|
77
95
|
return ok({
|
|
78
96
|
status: "truncated",
|
|
79
97
|
path,
|
|
@@ -63,7 +63,7 @@ import type { MachineBudget, ResourceSpec } from "../config/resource-limits.js";
|
|
|
63
63
|
import type { RunSession } from "../auth/types.js";
|
|
64
64
|
import type { RunAuthorizer } from "../execution/run-authorizer.js";
|
|
65
65
|
import type { Logger } from "../lib/logger.js";
|
|
66
|
-
import type { SynthesisStatus } from "../state/index.js";
|
|
66
|
+
import type { SynthesisStatus, UpdateStepExecutionInput } from "../state/index.js";
|
|
67
67
|
import type { AnalysisStep } from "../schemas/workflow-state.js";
|
|
68
68
|
import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
|
|
69
69
|
import type { BioToolKeys } from "../tools/bio/keys.js";
|
|
@@ -288,6 +288,19 @@ export declare function composeStepSeed(args: {
|
|
|
288
288
|
runId: string;
|
|
289
289
|
deps: ExecuteAnalysisDeps;
|
|
290
290
|
}): Promise<string>;
|
|
291
|
+
/**
|
|
292
|
+
* Project a classified synthesis outcome onto the run-phase `synthesis` ledger
|
|
293
|
+
* row's terminal update (see the run-synthesis-outcome spec). The blocker keeps
|
|
294
|
+
* its reason in `blockedReason` — an honest agent-declared blocker, not an
|
|
295
|
+
* error — and a failure carries the same reason string
|
|
296
|
+
* `persist-synthesis-outcome` writes to `cortex_runs.synthesis_reason`, so the
|
|
297
|
+
* two ledgers cannot disagree about why synthesis died. Exported for the
|
|
298
|
+
* projection tests (the `buildChildInput` pattern).
|
|
299
|
+
*/
|
|
300
|
+
export declare function synthesisRowUpdate(outcome: {
|
|
301
|
+
status: SynthesisStatus;
|
|
302
|
+
reason: string | null;
|
|
303
|
+
}, durationMs: number): UpdateStepExecutionInput;
|
|
291
304
|
/**
|
|
292
305
|
* Run-level literature-grounded synthesis (no sandbox). Returns the
|
|
293
306
|
* quick-display findings for the run-completed card; empty when synthesis is
|
|
@@ -58,17 +58,17 @@
|
|
|
58
58
|
* - WRITES NOTHING to the conversation thread (results are pull-only
|
|
59
59
|
* via `inspectRun`)
|
|
60
60
|
*/
|
|
61
|
-
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
61
|
+
import { DBOS, Error as DBOSErrors } from "@dbos-inc/dbos-sdk";
|
|
62
62
|
import { mkdir } from "node:fs/promises";
|
|
63
63
|
import { join } from "node:path";
|
|
64
64
|
import { forStep } from "../auth/types.js";
|
|
65
65
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
66
66
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
67
|
-
import { RunDedupCollisionError, countArtifactsForRun, loadDataProfileStatus, queryActiveRun, queryStepArtifactPaths, seedStepExecutions, setRunSynthesisOutcome, suspendAnalysis as suspendAnalysisQuery, sweepPendingStepExecutions, updateRunStatus, } from "../state/index.js";
|
|
67
|
+
import { RunDedupCollisionError, countArtifactsForRun, insertStepExecution, loadDataProfileStatus, queryActiveRun, queryStepArtifactPaths, seedStepExecutions, setRunSynthesisOutcome, suspendAnalysis as suspendAnalysisQuery, sweepPendingStepExecutions, updateRunStatus, updateStepExecution, } from "../state/index.js";
|
|
68
68
|
import { isBudgetExceeded } from "../loop/budget-exceeded.js";
|
|
69
|
-
import { loadStepSummariesFromDisk } from "../execution/run-synthesis.js";
|
|
69
|
+
import { SYNTHESIS_AGENT_ID, loadStepSummariesFromDisk } from "../execution/run-synthesis.js";
|
|
70
70
|
import { MAX_UPSTREAM_ARTIFACTS, composeStepBriefing } from "../prompts/briefing.js";
|
|
71
|
-
import { runDir, runStepDir, stepSubdir, stepWritePrefix, toSandboxPath } from "../workspace/paths.js";
|
|
71
|
+
import { SYNTHESIS_STEP_ID, runDir, runStepDir, stepSubdir, stepWritePrefix, toSandboxPath } from "../workspace/paths.js";
|
|
72
72
|
import { isChatDataPart } from "../sandbox/sandbox-step-translate.js";
|
|
73
73
|
import { synthesizeRun } from "../app/synthesize-run.js";
|
|
74
74
|
import { computeTopologicalLevels, scheduleReady, validatePlanDag } from "./execute-analysis-scheduler.js";
|
|
@@ -222,6 +222,31 @@ async function collectUpstreamHandoffs(args) {
|
|
|
222
222
|
}
|
|
223
223
|
return handoffs;
|
|
224
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Project a classified synthesis outcome onto the run-phase `synthesis` ledger
|
|
227
|
+
* row's terminal update (see the run-synthesis-outcome spec). The blocker keeps
|
|
228
|
+
* its reason in `blockedReason` — an honest agent-declared blocker, not an
|
|
229
|
+
* error — and a failure carries the same reason string
|
|
230
|
+
* `persist-synthesis-outcome` writes to `cortex_runs.synthesis_reason`, so the
|
|
231
|
+
* two ledgers cannot disagree about why synthesis died. Exported for the
|
|
232
|
+
* projection tests (the `buildChildInput` pattern).
|
|
233
|
+
*/
|
|
234
|
+
export function synthesisRowUpdate(outcome, durationMs) {
|
|
235
|
+
switch (outcome.status) {
|
|
236
|
+
case "produced":
|
|
237
|
+
return { status: "completed", durationMs };
|
|
238
|
+
case "skipped_no_summaries":
|
|
239
|
+
return { status: "skipped", durationMs };
|
|
240
|
+
case "skipped_blocker":
|
|
241
|
+
return { status: "blocked", durationMs, blockedReason: outcome.reason };
|
|
242
|
+
case "failed":
|
|
243
|
+
return { status: "failed", durationMs, error: outcome.reason };
|
|
244
|
+
default: {
|
|
245
|
+
const _exhaustive = outcome.status;
|
|
246
|
+
throw new Error(`unhandled synthesis status: ${String(_exhaustive)}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
225
250
|
/**
|
|
226
251
|
* Run-level literature-grounded synthesis (no sandbox). Returns the
|
|
227
252
|
* quick-display findings for the run-completed card; empty when synthesis is
|
|
@@ -309,14 +334,32 @@ export async function runExecuteAnalysisBody(input, deps) {
|
|
|
309
334
|
// The seed's DO NOTHING conflict action makes this replay-safe; the agent
|
|
310
335
|
// fallback mirrors `buildChildInput` so seed and mark-running agree on the
|
|
311
336
|
// row a step's child later upserts.
|
|
337
|
+
//
|
|
338
|
+
// The run-phase `synthesis` row is seeded alongside the DAG — gated on
|
|
339
|
+
// enablement ONLY, because the other half of the run gate for actually
|
|
340
|
+
// executing synthesis (`final.completed.size > 0`) is unknowable until the
|
|
341
|
+
// scheduler returns. A seeded row whose gate later fails simply stays
|
|
342
|
+
// `pending` and the terminal sweep finalizes it to `skipped` — honest
|
|
343
|
+
// ("never ran, never will"), like any never-dispatched DAG step. Its wave
|
|
344
|
+
// sits after every DAG level so ledger-ordered readers render it last.
|
|
345
|
+
const synthesisEnabled = deps.synthesisEnabled ?? true;
|
|
346
|
+
const synthesisWave = Math.max(-1, ...levels.values()) + 1;
|
|
312
347
|
await DBOS.runStep(async () => {
|
|
313
|
-
|
|
348
|
+
const dagRows = input.steps.map((step) => ({
|
|
314
349
|
runId,
|
|
315
350
|
stepId: step.id,
|
|
316
351
|
analysisId: input.analysisId,
|
|
317
352
|
wave: levels.get(step.id) ?? 0,
|
|
318
353
|
agentId: input.agentByStepId[step.id] ?? "scientific-executor",
|
|
319
|
-
}))
|
|
354
|
+
}));
|
|
355
|
+
const synthesisRow = {
|
|
356
|
+
runId,
|
|
357
|
+
stepId: SYNTHESIS_STEP_ID,
|
|
358
|
+
analysisId: input.analysisId,
|
|
359
|
+
wave: synthesisWave,
|
|
360
|
+
agentId: SYNTHESIS_AGENT_ID,
|
|
361
|
+
};
|
|
362
|
+
unwrapOrThrow(await seedStepExecutions(deps.pool, synthesisEnabled ? [...dagRows, synthesisRow] : dagRows));
|
|
320
363
|
}, { name: "seed-step-executions" });
|
|
321
364
|
// One checkpointed clock read stamps the run's start; the terminal read in
|
|
322
365
|
// `collectAndComplete` subtracts it for the true run span. Checkpointed →
|
|
@@ -352,10 +395,37 @@ export async function runExecuteAnalysisBody(input, deps) {
|
|
|
352
395
|
let synthesisError = null;
|
|
353
396
|
let synthesisFindings = [];
|
|
354
397
|
// Null when synthesis never ran (disabled or no completed steps): the ledger
|
|
355
|
-
// columns stay NULL — "unknown", not a recorded skip
|
|
398
|
+
// columns stay NULL — "unknown", not a recorded skip — and the seeded
|
|
399
|
+
// `synthesis` row (if any) stays `pending` for the terminal sweep.
|
|
356
400
|
let synthesisOutcome = null;
|
|
357
|
-
const synthesisEnabled = deps.synthesisEnabled ?? true;
|
|
358
401
|
if (synthesisEnabled && final.completed.size > 0) {
|
|
402
|
+
// Bracketing clock reads for the row's duration_ms — checkpointed, so a
|
|
403
|
+
// recovered replay reuses the original instants (same rule as `startedAtMs`).
|
|
404
|
+
const synthStartedAtMs = await DBOS.now();
|
|
405
|
+
// Mark the seeded `synthesis` row running. The same upsert the DAG children
|
|
406
|
+
// use: it stamps started_at, and it CREATES the row outright when the seed
|
|
407
|
+
// never carried it (synthesisEnabled flipped true across a recovery
|
|
408
|
+
// redeploy) — the mark self-heals rather than trusting the seed's snapshot
|
|
409
|
+
// of the config. Log-don't-fail: a progress row must never fail an
|
|
410
|
+
// otherwise-healthy run. Cancellation still re-propagates — it is not a
|
|
411
|
+
// write failure, and swallowing it here would only defer it one operation.
|
|
412
|
+
try {
|
|
413
|
+
await DBOS.runStep(async () => {
|
|
414
|
+
unwrapOrThrow(await insertStepExecution(deps.pool, {
|
|
415
|
+
runId,
|
|
416
|
+
stepId: SYNTHESIS_STEP_ID,
|
|
417
|
+
analysisId: input.analysisId,
|
|
418
|
+
wave: synthesisWave,
|
|
419
|
+
agentId: SYNTHESIS_AGENT_ID,
|
|
420
|
+
childWorkflowId: null,
|
|
421
|
+
}));
|
|
422
|
+
}, { name: "mark-synthesis-running" });
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
if (err instanceof DBOSErrors.DBOSWorkflowCancelledError)
|
|
426
|
+
throw err;
|
|
427
|
+
logger.error("mark-synthesis-running failed", { runId, ...logger.errorFields(err) });
|
|
428
|
+
}
|
|
359
429
|
try {
|
|
360
430
|
const synthOut = await DBOS.runStep(() => synthesizeFindings({
|
|
361
431
|
analysisId: input.analysisId,
|
|
@@ -368,10 +438,37 @@ export async function runExecuteAnalysisBody(input, deps) {
|
|
|
368
438
|
synthesisOutcome = { status: synthOut.synthesisStatus, reason: synthOut.synthesisReason };
|
|
369
439
|
}
|
|
370
440
|
catch (err) {
|
|
441
|
+
// Cancellation is not a synthesis outcome: re-propagate unchanged (the
|
|
442
|
+
// sandbox-step rule), so a cancelled workflow is never recorded as a
|
|
443
|
+
// synthesis failure — neither on the row nor in cortex_runs. The row is
|
|
444
|
+
// deliberately left `running`: `collectAndComplete` never executes on
|
|
445
|
+
// this path either, so the run row is equally non-terminal and the pair
|
|
446
|
+
// stays consistent — the pre-existing cancelled-mid-flight wedge shape
|
|
447
|
+
// `inflexa run` already detects via its DBOS-status cross-check.
|
|
448
|
+
if (err instanceof DBOSErrors.DBOSWorkflowCancelledError)
|
|
449
|
+
throw err;
|
|
371
450
|
synthesisError = err;
|
|
372
451
|
synthesisOutcome = { status: "failed", reason: err instanceof Error ? err.message : String(err) };
|
|
373
452
|
logger.error("synthesizeFindings failed — failing the run", { runId, ...logger.errorFields(err) });
|
|
374
453
|
}
|
|
454
|
+
// Terminal stamp for the `synthesis` row — deliberately BEFORE
|
|
455
|
+
// `collectAndComplete` writes the run row's terminal status, so no reader
|
|
456
|
+
// can observe a terminal run beside a still-`running` synthesis row. Both
|
|
457
|
+
// branches above set `synthesisOutcome` (or threw), so it is non-null here
|
|
458
|
+
// and the compiler narrows it — no guard needed. Log-don't-fail, like the
|
|
459
|
+
// sibling finalisation writes.
|
|
460
|
+
const rowOutcome = synthesisOutcome;
|
|
461
|
+
try {
|
|
462
|
+
const synthEndedAtMs = await DBOS.now();
|
|
463
|
+
await DBOS.runStep(async () => {
|
|
464
|
+
unwrapOrThrow(await updateStepExecution(deps.pool, runId, SYNTHESIS_STEP_ID, synthesisRowUpdate(rowOutcome, synthEndedAtMs - synthStartedAtMs)));
|
|
465
|
+
}, { name: "persist-synthesis-step" });
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
if (err instanceof DBOSErrors.DBOSWorkflowCancelledError)
|
|
469
|
+
throw err;
|
|
470
|
+
logger.error("persist-synthesis-step failed", { runId, ...logger.errorFields(err) });
|
|
471
|
+
}
|
|
375
472
|
}
|
|
376
473
|
// (6) collectAndComplete — terminal block. Runs on EVERY path. When synthesis
|
|
377
474
|
// failed, force a failed terminal status so the charge/run-authorization close correctly
|
|
@@ -181,5 +181,15 @@ export declare function stepSubdir(stepBase: string, type: StepSubdirType): stri
|
|
|
181
181
|
export declare function reportDir(reportId: string): string;
|
|
182
182
|
/** All standard subdirectory types for a step. */
|
|
183
183
|
export declare const STEP_SUBDIRS: readonly StepSubdirType[];
|
|
184
|
+
/**
|
|
185
|
+
* Reserved id of the run-phase synthesis row in `cortex_step_executions` — the
|
|
186
|
+
* ledger row `executeAnalysis` writes for run-level synthesis so progress
|
|
187
|
+
* readers see the phase. Reserved here beside {@link STEP_SUBDIRS} because the
|
|
188
|
+
* reservation is a layout fact as much as a ledger one: a plan step with this
|
|
189
|
+
* id would collide with the row's primary key AND put its step directory
|
|
190
|
+
* `runs/{runId}/synthesis/` beside the run-level `runs/{runId}/synthesis.json`.
|
|
191
|
+
* Plan validation rejects it (see validate-plan).
|
|
192
|
+
*/
|
|
193
|
+
export declare const SYNTHESIS_STEP_ID = "synthesis";
|
|
184
194
|
/** Get all subdirectory paths for a step. */
|
|
185
195
|
export declare function allStepSubdirs(stepBase: string): string[];
|
package/dist/workspace/paths.js
CHANGED
|
@@ -213,6 +213,16 @@ export function reportDir(reportId) {
|
|
|
213
213
|
}
|
|
214
214
|
/** All standard subdirectory types for a step. */
|
|
215
215
|
export const STEP_SUBDIRS = ["scripts", "output", "figures", "logs", "notebooks"];
|
|
216
|
+
/**
|
|
217
|
+
* Reserved id of the run-phase synthesis row in `cortex_step_executions` — the
|
|
218
|
+
* ledger row `executeAnalysis` writes for run-level synthesis so progress
|
|
219
|
+
* readers see the phase. Reserved here beside {@link STEP_SUBDIRS} because the
|
|
220
|
+
* reservation is a layout fact as much as a ledger one: a plan step with this
|
|
221
|
+
* id would collide with the row's primary key AND put its step directory
|
|
222
|
+
* `runs/{runId}/synthesis/` beside the run-level `runs/{runId}/synthesis.json`.
|
|
223
|
+
* Plan validation rejects it (see validate-plan).
|
|
224
|
+
*/
|
|
225
|
+
export const SYNTHESIS_STEP_ID = "synthesis";
|
|
216
226
|
/** Get all subdirectory paths for a step. */
|
|
217
227
|
export function allStepSubdirs(stepBase) {
|
|
218
228
|
return STEP_SUBDIRS.map((type) => stepSubdir(stepBase, type));
|
package/package.json
CHANGED