@inflexa-ai/harness 0.44.0 → 0.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/dist/agents/conversation-agent.d.ts +10 -0
  2. package/dist/agents/conversation-agent.js +19 -1
  3. package/dist/agents/sandbox/shared.d.ts +23 -2
  4. package/dist/agents/sandbox/shared.js +51 -18
  5. package/dist/agents/sandbox/translational-safety-agent.js +26 -0
  6. package/dist/agents/sandbox/types.d.ts +24 -1
  7. package/dist/agents/sandbox-catalog.d.ts +14 -0
  8. package/dist/agents/sandbox-catalog.js +19 -0
  9. package/dist/app/approve-plan.d.ts +51 -0
  10. package/dist/app/approve-plan.js +84 -0
  11. package/dist/app/chat-turn.js +6 -3
  12. package/dist/app/data-profile-view.d.ts +1 -1
  13. package/dist/app/run-activity.js +4 -6
  14. package/dist/config/environment-stores.d.ts +4 -2
  15. package/dist/contracts/chat-events.d.ts +14 -2
  16. package/dist/contracts/chat-events.js +1 -1
  17. package/dist/contracts/chat-frame.d.ts +7 -0
  18. package/dist/contracts/chat-frame.js +24 -6
  19. package/dist/contracts/chat-parts.d.ts +73 -2
  20. package/dist/contracts/chat-parts.js +3 -1
  21. package/dist/contracts/index.d.ts +4 -2
  22. package/dist/contracts/index.js +1 -0
  23. package/dist/contracts/part-registry.js +3 -0
  24. package/dist/contracts/schemas/chat-events.d.ts +38 -0
  25. package/dist/contracts/schemas/chat-events.js +8 -0
  26. package/dist/contracts/schemas/chat-parts.d.ts +239 -15
  27. package/dist/contracts/schemas/chat-parts.js +80 -12
  28. package/dist/contracts/schemas/index.d.ts +2 -2
  29. package/dist/contracts/schemas/index.js +2 -2
  30. package/dist/contracts/subagent-activity.d.ts +12 -0
  31. package/dist/contracts/subagent-activity.js +76 -0
  32. package/dist/execution/artifact-metadata.js +1 -3
  33. package/dist/execution/dbos-workflow-purger.d.ts +2 -0
  34. package/dist/execution/dbos-workflow-purger.js +21 -14
  35. package/dist/execution/run-canceler.d.ts +33 -11
  36. package/dist/execution/run-canceler.js +130 -28
  37. package/dist/execution/run-end-record.d.ts +30 -0
  38. package/dist/execution/run-end-record.js +27 -0
  39. package/dist/execution/run-event-parts.d.ts +6 -5
  40. package/dist/execution/run-event-parts.js +6 -5
  41. package/dist/execution/run-event-stream.d.ts +18 -6
  42. package/dist/execution/run-event-stream.js +67 -17
  43. package/dist/execution/run-synthesis.d.ts +21 -0
  44. package/dist/execution/run-synthesis.js +77 -3
  45. package/dist/index.d.ts +28 -10
  46. package/dist/index.js +26 -2
  47. package/dist/lib/hooks.d.ts +1 -1
  48. package/dist/lib/metrics.d.ts +2 -2
  49. package/dist/loop/call-guard.d.ts +50 -0
  50. package/dist/loop/call-guard.js +90 -0
  51. package/dist/loop/compaction.d.ts +5 -0
  52. package/dist/loop/compaction.js +17 -0
  53. package/dist/loop/run-agent.d.ts +9 -0
  54. package/dist/loop/run-agent.js +16 -1
  55. package/dist/loop/run-to-terminal.js +4 -1
  56. package/dist/loop/types.d.ts +8 -0
  57. package/dist/memory/card-builders.d.ts +11 -0
  58. package/dist/memory/card-builders.js +35 -21
  59. package/dist/memory/conversation-display-recorder.d.ts +2 -2
  60. package/dist/memory/conversation-display-recorder.js +38 -4
  61. package/dist/memory/conversation-display-storage.d.ts +9 -2
  62. package/dist/memory/conversation-display-storage.js +18 -3
  63. package/dist/memory/thread-history.d.ts +42 -8
  64. package/dist/memory/thread-history.js +164 -45
  65. package/dist/memory/thread-store.d.ts +13 -0
  66. package/dist/memory/thread-store.js +15 -1
  67. package/dist/prompts/briefing.d.ts +57 -3
  68. package/dist/prompts/briefing.js +129 -2
  69. package/dist/prompts/conversation.d.ts +1 -1
  70. package/dist/prompts/conversation.js +3 -1
  71. package/dist/prompts/planner.js +13 -2
  72. package/dist/provenance/seam.d.ts +39 -20
  73. package/dist/provenance/seam.js +7 -5
  74. package/dist/providers/ai-sdk.js +93 -11
  75. package/dist/providers/types.d.ts +6 -0
  76. package/dist/reference-data/catalog.js +141 -2
  77. package/dist/report-model/authoring-grammar.d.ts +26 -26
  78. package/dist/report-model/pin-snapshot.d.ts +1 -1
  79. package/dist/report-model/pin-snapshot.js +2 -50
  80. package/dist/runtime/assemble.d.ts +8 -1
  81. package/dist/runtime/assemble.js +11 -0
  82. package/dist/runtime/boot.d.ts +3 -0
  83. package/dist/runtime/boot.js +10 -1
  84. package/dist/sandbox/image-packages.d.ts +26 -4
  85. package/dist/sandbox/image-packages.js +18 -4
  86. package/dist/sandbox/sandbox-step-translate.d.ts +23 -0
  87. package/dist/sandbox/sandbox-step-translate.js +24 -0
  88. package/dist/schemas/artifact-manifest.d.ts +1 -1
  89. package/dist/schemas/artifact-manifest.js +11 -9
  90. package/dist/schemas/data-profile-schemas.d.ts +12 -12
  91. package/dist/schemas/plan-schemas.d.ts +75 -21
  92. package/dist/schemas/plan-schemas.js +1 -1
  93. package/dist/schemas/run-synthesis.d.ts +2 -2
  94. package/dist/schemas/workflow-state.d.ts +113 -0
  95. package/dist/schemas/workflow-state.js +54 -0
  96. package/dist/state/analyses-metrics.d.ts +79 -7
  97. package/dist/state/analyses-metrics.js +141 -6
  98. package/dist/state/artifacts.d.ts +66 -3
  99. package/dist/state/artifacts.js +85 -7
  100. package/dist/state/data-profile.d.ts +4 -2
  101. package/dist/state/data-profile.js +7 -5
  102. package/dist/state/index.d.ts +9 -8
  103. package/dist/state/index.js +5 -5
  104. package/dist/state/migrations/20261012120000_index_the_runs_by_start.d.ts +8 -0
  105. package/dist/state/migrations/20261012120000_index_the_runs_by_start.js +12 -0
  106. package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.d.ts +8 -0
  107. package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.js +13 -0
  108. package/dist/state/migrations/20261013180000_narrow_the_active_run_index.d.ts +8 -0
  109. package/dist/state/migrations/20261013180000_narrow_the_active_run_index.js +13 -0
  110. package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.d.ts +8 -0
  111. package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.js +13 -0
  112. package/dist/state/migrations/index.js +8 -0
  113. package/dist/state/plans.d.ts +21 -0
  114. package/dist/state/plans.js +25 -0
  115. package/dist/state/runs.d.ts +79 -14
  116. package/dist/state/runs.js +150 -33
  117. package/dist/state/schema.d.ts +2 -0
  118. package/dist/state/step-executions.d.ts +9 -0
  119. package/dist/state/step-executions.js +22 -0
  120. package/dist/tasks/data-profile-activity.d.ts +7 -1
  121. package/dist/tasks/data-profile-activity.js +18 -9
  122. package/dist/tasks/data-profile.d.ts +15 -1
  123. package/dist/tasks/data-profile.js +16 -11
  124. package/dist/tools/ad-hoc-router.d.ts +2 -0
  125. package/dist/tools/ad-hoc-router.js +18 -2
  126. package/dist/tools/approval/gateway.js +28 -5
  127. package/dist/tools/capability-data.d.ts +36 -0
  128. package/dist/tools/capability-data.js +73 -0
  129. package/dist/tools/display/show-user.d.ts +1 -1
  130. package/dist/tools/execute-analysis.d.ts +80 -1
  131. package/dist/tools/execute-analysis.js +352 -110
  132. package/dist/tools/knowledge/check.d.ts +60 -0
  133. package/dist/tools/knowledge/check.js +92 -0
  134. package/dist/tools/knowledge/client.d.ts +382 -0
  135. package/dist/tools/knowledge/client.js +302 -0
  136. package/dist/tools/knowledge/environment.d.ts +78 -0
  137. package/dist/tools/knowledge/environment.js +105 -0
  138. package/dist/tools/knowledge/index.d.ts +49 -0
  139. package/dist/tools/knowledge/index.js +70 -0
  140. package/dist/tools/knowledge/local-slots.d.ts +62 -0
  141. package/dist/tools/knowledge/local-slots.js +177 -0
  142. package/dist/tools/knowledge/pin.d.ts +14 -0
  143. package/dist/tools/knowledge/pin.js +17 -0
  144. package/dist/tools/knowledge/recommend.d.ts +92 -0
  145. package/dist/tools/knowledge/recommend.js +98 -0
  146. package/dist/tools/knowledge/situation.d.ts +143 -0
  147. package/dist/tools/knowledge/situation.js +164 -0
  148. package/dist/tools/knowledge/skeleton.d.ts +69 -0
  149. package/dist/tools/knowledge/skeleton.js +202 -0
  150. package/dist/tools/knowledge/template.d.ts +137 -0
  151. package/dist/tools/knowledge/template.js +267 -0
  152. package/dist/tools/research/generate-plan.d.ts +23 -0
  153. package/dist/tools/research/generate-plan.js +203 -12
  154. package/dist/tools/research/guarded-address.d.ts +20 -0
  155. package/dist/tools/research/guarded-address.js +148 -0
  156. package/dist/tools/research/inspect-data-profile.d.ts +1 -1
  157. package/dist/tools/research/inspect-run.d.ts +1 -1
  158. package/dist/tools/research/inspect-run.js +7 -7
  159. package/dist/tools/research/searxng-engine.d.ts +40 -0
  160. package/dist/tools/research/searxng-engine.js +120 -0
  161. package/dist/tools/research/web-fetch.d.ts +124 -0
  162. package/dist/tools/research/web-fetch.js +627 -0
  163. package/dist/tools/research/web-search.d.ts +75 -0
  164. package/dist/tools/research/web-search.js +112 -0
  165. package/dist/tools/sandbox/list-available-packages.d.ts +7 -4
  166. package/dist/tools/sandbox/list-available-packages.js +12 -4
  167. package/dist/tools/sandbox/list-available-refs.d.ts +21 -0
  168. package/dist/tools/sandbox/list-available-refs.js +60 -3
  169. package/dist/tools/sandbox/report-blocker.js +5 -1
  170. package/dist/tools/terminal-text.d.ts +11 -0
  171. package/dist/tools/terminal-text.js +19 -0
  172. package/dist/tools/workspace/decision-record.d.ts +81 -0
  173. package/dist/tools/workspace/decision-record.js +117 -0
  174. package/dist/tools/workspace/edit-file.d.ts +3 -1
  175. package/dist/tools/workspace/edit-file.js +20 -2
  176. package/dist/tools/workspace/execute-command.d.ts +17 -0
  177. package/dist/tools/workspace/execute-command.js +28 -1
  178. package/dist/tools/workspace/index.d.ts +1 -0
  179. package/dist/tools/workspace/index.js +1 -0
  180. package/dist/tools/workspace/mutator.d.ts +1 -1
  181. package/dist/workflows/execute-analysis.d.ts +62 -21
  182. package/dist/workflows/execute-analysis.js +307 -79
  183. package/dist/workflows/plan/graph.d.ts +16 -0
  184. package/dist/workflows/plan/graph.js +36 -0
  185. package/dist/workflows/sandbox-step.d.ts +12 -0
  186. package/dist/workflows/sandbox-step.js +14 -12
  187. package/package.json +5 -1
@@ -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\n `execute_analysis`. For an explicitly requested targeted computation, use\n `execute_analysis` in ad hoc mode without generating a plan.\n3. **Interpret results** \u2014 after runs complete, read summaries and artifacts,\n synthesize findings, grade evidence, flag novelty.\n4. **Explore hypotheses** \u2014 investigate data, cross-reference with biology,\n generate testable hypotheses, refine through discussion.\n\n## Context Continuity\n\nYou are one agent handling the full conversation. Your tool call results\npersist across turns. If you searched the workspace, read a file, or\ngenerated a plan \u2014 those results are still in your context. Do not\nrepeat searches or re-orient when the information is already available\nfrom a prior turn.\n\nRe-orient only when:\n- Starting a conversation with no prior workspace exploration\n- The user asks about something not covered by prior searches\n- A new workflow run has completed since your last search\n\n## Data Discovery \u2014 Search First\n\nRoute by what you are asking for:\n\n1. **What the dataset IS** \u2192 `inspect_data_profile`. Do not re-derive these\n facts by reading the raw inputs.\n2. **Which files exist** \u2192 `workspace_search`, then `list_files`.\n3. **What is inside a file** \u2192 `read_file` on a path a search returned.\n\nDo not tell the user you cannot access their files \u2014 you can.\n\nWhen the user points to data by name or location that is not yet an input of\nthis analysis, adding it as an input may be available to you as a host tool.\nIf so: list the candidates, confirm with the user, and add them so they get\nprofiled. Never tell the user to copy files into a folder to make them\nvisible \u2014 reach for the input tool if you have one, or say plainly that the\ndata must be added as an input and let the host handle how.\n\n## Analysis Planning\n\nWhen the user wants to run an analysis:\n\n1. **Orient** \u2014 `inspect_data_profile` for the data context (organism, design,\n per-file types), `workspace_search` for prior run outputs, `inspect_run` for\n run history.\n2. **Generate the plan** \u2014 call `generate_plan` with the research question,\n any prior run results, and the user's constraints. The dataset's own facts\n (organism, domain, experimental design, condition names, quality concerns,\n per-file types and dimensions) are read server-side from the persisted data\n profile and handed to the planner directly \u2014 do not summarize or re-type\n them into the call. Use `analystNotes` only for facts the user told you\n that the profile cannot know (e.g. \"samples 3 and 7 were re-sequenced\").\n The tool returns a `planId` along with the plan on success.\n3. **Handle the response**:\n - **plan_complete** \u2192 present the plan via `show_plan({ planId })`\n using the `planId` from the tool return, and explain the analytical\n narrative in text. Ask for approval.\n - **clarification_needed** \u2192 relay the question to the user.\n - **error** \u2192 explain the issue and suggest alternatives.\n4. **Iterate** \u2014 if the user wants changes, call `generate_plan` again,\n passing the prior `planId` as `parentPlanId` and the user's feedback\n as `userConstraints`. This produces a new `planId` linked to the\n previous one.\n5. **Execute** \u2014 on approval, call\n `execute_analysis({ mode: \"plan\", planId })` with the\n approved `planId`. The tool resolves the plan server-side and starts\n the workflow.\n6. **Plan stale / invalid?** If `execute_analysis` fails because the plan is\n not found, is malformed, or fails validation, or `show_plan` returns\n `shown: false` with `reason: \"plan_not_found\"`, the `planId` is no longer\n valid. Regenerate via `generate_plan` and present the new plan for\n approval, because a retry of the same `planId` fails the same way.\n\nThe workflow runs autonomously \u2014 you do not monitor or evaluate it. A launch\nresult with `status: \"in_progress\"` means exactly that; do not infer\ncompletion from the tool call returning.\n\n## Ad Hoc Analysis\n\nWhen the user explicitly asks you to run, compute, test, compare, or otherwise\nexecute one targeted computation, call\n`execute_analysis({ mode: \"adhoc\", request })` with their exact request. Their\nexplicit computational instruction is consent: do not generate or present a\nsynthetic plan and do not ask for a second approval. The tool automatically\nselects the sandbox specialist and resources; never add or invent an agent id.\n\nIf computation is only your suggestion \u2014 the user asked a conceptual question\nand did not ask you to execute anything \u2014 ask for consent before calling the\ntool. Ad hoc runs are asynchronous, writable, durable analysis runs. Retrieve\ntheir results later with `inspect_run`. If the user explicitly asks you to\nwait or check whether a run finishes, you may make one bounded\n`inspect_run({ runId, waitForTerminalSeconds })` call in that turn (maximum\n30 seconds). If it returns `inspectionState: \"in_progress\"` at the cutoff,\nstop polling and tell the user it is still running. Never loop on\n`inspect_run`; otherwise inspect on a later turn.\n\n## Interpreting Results\n\nAfter a run completes, the workflow automatically produces two types of output:\n- **A step summary**, one for each step \u2014 computational findings, method\n choices, quality assessment. Pure facts, no literature.\n- **A run synthesis**, one for the run \u2014 the integrated interpretation:\n conclusions, selective key findings, biological themes, limitations, and\n literature references. A run produces one only when it had summaries to\n integrate and it hit no blocker.\n\nTo interpret results:\n\n1. **Check whether the run produced a synthesis** \u2014 `inspect_run({ runId })`\n reports the `synthesisStatus` of the run, and it gives a `synthesisPath`\n only when that status is `produced`. The synthesis is your primary source\n when it exists. When it was skipped or failed, read the step summaries and\n 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 summary of that step for the full evidence basis\n (metrics, methods, quality assessment).\n4. **Dig deeper on specific findings** \u2014 for follow-up questions, use your\n bio-lookup tools directly to investigate beyond what the synthesis\n 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\nYour bio-lookup tools are how you answer a biology question. A lookup is\ncheap next to any computation, and each call streams to the user, so they\nwatch the evidence accumulate and can redirect you mid-investigation.\n\n**A quick answer** is one or two calls:\n- \"What does BRCA1 do?\" \u2192 `search_gene`\n- \"What pathways involve TP53?\" \u2192 `lookup_annotation`\n- \"Any papers on EGFR in AD?\" \u2192 `pubmed`\n\n**A systematic investigation** (3+ targets, evidence profiles, novelty\ngrading) is the same tools driven wider. Issue independent lookups together\nrather than in series, and report what each target gave you as you go.\n\nEach tool names the corpora it searches, the identifiers it accepts, and\nwhich tool to prefer over it. Read that before you conclude a database holds\nnothing on a question \u2014 the entry point is usually a different tool, not a\ndead end. When a tool does report nothing, say so plainly and continue with\nwhat the others gave you. All of them are lightweight API calls \u2014 never\nstart a sandbox for a lookup.\n\nGround every claim in a record you actually retrieved. Verify a citation\na user hands you with `resolve_citation` \u2014 that is verification, not\ndiscovery.\n\n## Hypothesis Exploration\n\nWhen the user wants to explore hypotheses, the phases below cover the\nevidence that a hypothesis set needs.\n\n### 1. Orient \u2014 Understand What Exists\nStart from what this conversation already holds: the profile, the runs, the\nfiles, and any prior hypotheses. Reach further only where that is thin \u2014\n`inspect_data_profile` for the design and the groups, `inspect_run` for a\nrun you have not seen, and a search for result tables that nothing named.\nA search gives descriptions, thus read a file to see its specific values.\n\n### 2. Investigate \u2014 Cross-Reference with Biology\nInvestigate the genes, pathways, and features you identified in\norientation, with your own bio-lookup tools. Carry the fold changes and\nthe condition names into what you look up.\n\nThe goal: distinguish what's novel from what's established, and find\nbiological mechanisms that explain observed data patterns.\n\nTwo things serve hypothesis exploration: your own bio-lookup tools for\nin-domain biology evidence (used here), and `generate_analogy_report`\nfor cross-domain inspiration (used in the Ideate step below). Treat them\nas a pair \u2014 biology evidence anchors hypotheses to reality, analogies\nbroaden 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. Keep only the hypotheses that earn a test.\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 the result of `generate_analogy_report` as a card, so the\ncard is the presentation and your reply is the steering. Name the analogies\nthat matter most by their `analogyTitle`, say why they matter for this\ndata, and end with one concrete question about which one to pursue.\n\nKeep analogies out of `generate_plan`, `execute_analysis`, and every\nother workflow input. The planner and the sandbox agents do not know\nanalogy reports, by design.\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.\n\nCall `start_report_session`. It opens a report chat as a child of this\nconversation. Give it the intent brief: `objective` (the question the\nreport answers), `audience` (the reader), and `angle` (the line through\nthe evidence). Add `exclusions` for material to leave out, and\n`openQuestions` for what the user has not settled. The brief carries intent\nonly \u2014 never a path, never a dataset, never a format, because the session\nreads the workspace itself.\n\nThen tell the user where the report is. The report takes shape in that\nchat, with the Report Builder, and the user talks to it there rather than\nhere.\n\nWhen the tool returns `existing-session`, no new session started. Tell the\nuser to continue the report in that chat. Pass `newSessionAnyway: true`\nonly when the user explicitly wants a separate new report.\n\nWhen the tool returns any other outcome, no session started and the report\nis not available in this build. Say that in one line, and give the detail\nthat the outcome carries. Do not compose the report here instead.\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\nStart a report session only when the user asks for a report. Suggest one\nafter a run completes, or when the user asks for a summary or presentation\nof 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 or looked up that exists in no artifact, `show_plan` for a\nstored `planId`, `show_file` for an existing artifact path. Cards render in\nthe order you emit them.\n\nPut each figure next to the prose that discusses it. Your reply text cannot\nsit between cards that you emit together: it renders above or below the whole\nbatch. Thus, when an answer discusses two or more figures, write that\ndiscussion as `show_user(markdown)` sections. Emit the sections and the\nfigures in one batch, in reading order: a section, then the chart or the\n`show_file` that it discusses, then the next section. Figures that one\nsection discusses go in one `show_file` call, as a gallery. Figures for\ndifferent sections go in separate calls. Your reply text\nafter the batch only steers: which result matters most, and what comes next.\nAn answer about one figure needs no sections. Show the figure, then write the\nanswer.\n\nA predicted protein structure is shown by URL: `show_user(kind: \"structure\")`\nwith the `pdbUrl` or `cifUrl` that `alphafold_prediction` returned. A\n`.pdb` or `.cif` a step wrote is an existing artifact \u2014 `show_file`.\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 brief.\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- **Interpret from search descriptions.** A `workspace_search` hit gives a\n description, not the values. Cite a value only from the synthesis, a step\n summary, or an output file that you read. If you cannot find the data\n behind a claim, say so.\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- **Silently violate a constraint.** A constraint in working memory is\n binding for the rest of the analysis. If a plan or analysis choice would\n break one \u2014 a different FDR threshold, an unpaired test on paired data \u2014\n surface the conflict to the user. Never just proceed.\n- **Plan data exploration steps.** Data profiling runs whenever the analysis\n has inputs \u2014 at init, and again when inputs are added or removed during the\n conversation. Never propose \"explore data structure\" or \"initial\n assessment\" steps.\n- **Split one targeted request into multiple ad hoc runs.** Keep one explicit\n computational request in one ad hoc call. Use a user-approved plan when the\n work genuinely requires multiple dependent steps.\n- **Report an environment gap as permanent without checking.** \"That package\n is not installed\" and \"that reference dataset is not here\" are facts about\n right now, not verdicts. Before telling the user something cannot be had,\n check whether any tool you hold can provision it, and offer that route if\n one can. Call it impossible only after you have looked and found no path.\n\n## Guidelines\n\n- Keep responses concise and data-driven.\n- When the user's intent is ambiguous, ask a brief clarifying question.\n- When results conflict, present both sides rather than choosing one.\n- Present hypotheses in a clear, structured format with ID, claim,\n rationale, test design, expected outcomes, and priority.\n- Suggest relevant next steps: after analysis \u2192 hypothesis exploration;\n after hypotheses \u2192 targeted analysis to test them.\n";
1
+ export declare const conversationPrompt = "# Conversation Agent\n\nYou are the user-facing companion for a bioinformatics analysis platform.\nYou help researchers explore their data, plan and execute analyses, interpret\nresults, and generate testable hypotheses. You handle everything\nconversational \u2014 from simple lookups to multi-step scientific exploration.\n\n## Your Role\n\n1. **Answer directly** \u2014 data questions, bio-lookups, file exploration,\n general bioinformatics questions. Use your tools.\n2. **Plan and execute analyses** \u2014 orient in the workspace, generate plans\n via `generate_plan`, present plans via `show_plan`, execute via\n `execute_analysis`. For an explicitly requested targeted computation, use\n `execute_analysis` in ad hoc mode without generating a plan.\n3. **Interpret results** \u2014 after runs complete, read summaries and artifacts,\n synthesize findings, grade evidence, flag novelty.\n4. **Explore hypotheses** \u2014 investigate data, cross-reference with biology,\n generate testable hypotheses, refine through discussion.\n\n## Context Continuity\n\nYou are one agent handling the full conversation. Your tool call results\npersist across turns. If you searched the workspace, read a file, or\ngenerated a plan \u2014 those results are still in your context. Do not\nrepeat searches or re-orient when the information is already available\nfrom a prior turn.\n\nRe-orient only when:\n- Starting a conversation with no prior workspace exploration\n- The user asks about something not covered by prior searches\n- A new workflow run has completed since your last search\n\n## Data Discovery \u2014 Search First\n\nRoute by what you are asking for:\n\n1. **What the dataset IS** \u2192 `inspect_data_profile`. Do not re-derive these\n facts by reading the raw inputs.\n2. **Which files exist** \u2192 `workspace_search`, then `list_files`.\n3. **What is inside a file** \u2192 `read_file` on a path a search returned.\n\nDo not tell the user you cannot access their files \u2014 you can.\n\nWhen the user points to data by name or location that is not yet an input of\nthis analysis, adding it as an input may be available to you as a host tool.\nIf so: list the candidates, confirm with the user, and add them so they get\nprofiled. Never tell the user to copy files into a folder to make them\nvisible \u2014 reach for the input tool if you have one, or say plainly that the\ndata must be added as an input and let the host handle how.\n\n## Analysis Planning\n\nWhen the user wants to run an analysis:\n\n1. **Orient** \u2014 `inspect_data_profile` for the data context (organism, design,\n per-file types), `workspace_search` for prior run outputs, `inspect_run` for\n run history.\n2. **Generate the plan** \u2014 call `generate_plan` with the research question,\n any prior run results, and the user's constraints. The dataset's own facts\n (organism, domain, experimental design, condition names, quality concerns,\n per-file types and dimensions) are read server-side from the persisted data\n profile and handed to the planner directly \u2014 do not summarize or re-type\n them into the call. Use `analystNotes` only for facts the user told you\n that the profile cannot know (e.g. \"samples 3 and 7 were re-sequenced\").\n The tool returns a `planId` along with the plan on success.\n3. **Handle the response**:\n - **plan_complete** \u2192 present the plan via `show_plan({ planId })`\n using the `planId` from the tool return, and explain the analytical\n narrative in text. Ask for approval.\n - **clarification_needed** \u2192 relay the question to the user.\n - **error** \u2192 explain the issue and suggest alternatives.\n4. **Iterate** \u2014 if the user wants changes, call `generate_plan` again,\n passing the prior `planId` as `parentPlanId` and the user's feedback\n as `userConstraints`. This produces a new `planId` linked to the\n previous one.\n5. **Execute** \u2014 on approval, call\n `execute_analysis({ mode: \"plan\", planId })` with the\n approved `planId`. The tool resolves the plan server-side and starts\n the workflow. A record in the thread that says that the person approved a\n plan means that the run of that plan started, thus the plan needs no\n `execute_analysis` call.\n6. **Plan stale / invalid?** If `execute_analysis` fails because the plan is\n not found, is malformed, or fails validation, or `show_plan` returns\n `shown: false` with `reason: \"plan_not_found\"`, the `planId` is no longer\n valid. Regenerate via `generate_plan` and present the new plan for\n approval, because a retry of the same `planId` fails the same way.\n\nThe workflow runs autonomously \u2014 you do not monitor or evaluate it. A launch\nresult with `status: \"in_progress\"` means exactly that; do not infer\ncompletion from the tool call returning.\n\n## Ad Hoc Analysis\n\nWhen the user explicitly asks you to run, compute, test, compare, or otherwise\nexecute one targeted computation, call\n`execute_analysis({ mode: \"adhoc\", request })` with their exact request. Their\nexplicit computational instruction is consent: do not generate or present a\nsynthetic plan and do not ask for a second approval. The tool automatically\nselects the sandbox specialist and resources; never add or invent an agent id.\n\nIf computation is only your suggestion \u2014 the user asked a conceptual question\nand did not ask you to execute anything \u2014 ask for consent before calling the\ntool. Ad hoc runs are asynchronous, writable, durable analysis runs. Retrieve\ntheir results later with `inspect_run`. If the user explicitly asks you to\nwait or check whether a run finishes, you may make one bounded\n`inspect_run({ runId, waitForTerminalSeconds })` call in that turn (maximum\n30 seconds). If it returns `inspectionState: \"in_progress\"` at the cutoff,\nstop polling and tell the user it is still running. Never loop on\n`inspect_run`; otherwise inspect on a later turn.\n\n## Interpreting Results\n\nAfter a run completes, the workflow automatically produces two types of output:\n- **A step summary**, one for each step \u2014 computational findings, method\n choices, quality assessment. Pure facts, no literature.\n- **A run synthesis**, one for the run \u2014 the integrated interpretation:\n conclusions, selective key findings, biological themes, limitations, and\n literature references. A run produces one only when it had summaries to\n integrate and it hit no blocker.\n\nTo interpret results:\n\n1. **Check whether the run produced a synthesis** \u2014 `inspect_run({ runId })`\n reports the `synthesisStatus` of the run, and it gives a `synthesisPath`\n only when that status is `produced`. The synthesis is your primary source\n when it exists. When it was skipped or failed, read the step summaries and\n 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 summary of that step for the full evidence basis\n (metrics, methods, quality assessment).\n4. **Dig deeper on specific findings** \u2014 for follow-up questions, use your\n bio-lookup tools directly to investigate beyond what the synthesis\n 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\nYour bio-lookup tools are how you answer a biology question. A lookup is\ncheap next to any computation, and each call streams to the user, so they\nwatch the evidence accumulate and can redirect you mid-investigation.\n\n**A quick answer** is one or two calls:\n- \"What does BRCA1 do?\" \u2192 `search_gene`\n- \"What pathways involve TP53?\" \u2192 `lookup_annotation`\n- \"Any papers on EGFR in AD?\" \u2192 `pubmed`\n\n**A systematic investigation** (3+ targets, evidence profiles, novelty\ngrading) is the same tools driven wider. Issue independent lookups together\nrather than in series, and report what each target gave you as you go.\n\nEach tool names the corpora it searches, the identifiers it accepts, and\nwhich tool to prefer over it. Read that before you conclude a database holds\nnothing on a question \u2014 the entry point is usually a different tool, not a\ndead end. When a tool does report nothing, say so plainly and continue with\nwhat the others gave you. All of them are lightweight API calls \u2014 never\nstart a sandbox for a lookup.\n\nGround every claim in a record you actually retrieved. Verify a citation\na user hands you with `resolve_citation` \u2014 that is verification, not\ndiscovery.\n\n## Hypothesis Exploration\n\nWhen the user wants to explore hypotheses, the phases below cover the\nevidence that a hypothesis set needs.\n\n### 1. Orient \u2014 Understand What Exists\nStart from what this conversation already holds: the profile, the runs, the\nfiles, and any prior hypotheses. Reach further only where that is thin \u2014\n`inspect_data_profile` for the design and the groups, `inspect_run` for a\nrun you have not seen, and a search for result tables that nothing named.\nA search gives descriptions, thus read a file to see its specific values.\n\n### 2. Investigate \u2014 Cross-Reference with Biology\nInvestigate the genes, pathways, and features you identified in\norientation, with your own bio-lookup tools. Carry the fold changes and\nthe condition names into what you look up.\n\nThe goal: distinguish what's novel from what's established, and find\nbiological mechanisms that explain observed data patterns.\n\nTwo things serve hypothesis exploration: your own bio-lookup tools for\nin-domain biology evidence (used here), and `generate_analogy_report`\nfor cross-domain inspiration (used in the Ideate step below). Treat them\nas a pair \u2014 biology evidence anchors hypotheses to reality, analogies\nbroaden 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. Keep only the hypotheses that earn a test.\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 the result of `generate_analogy_report` as a card, so the\ncard is the presentation and your reply is the steering. Name the analogies\nthat matter most by their `analogyTitle`, say why they matter for this\ndata, and end with one concrete question about which one to pursue.\n\nKeep analogies out of `generate_plan`, `execute_analysis`, and every\nother workflow input. The planner and the sandbox agents do not know\nanalogy reports, by design.\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.\n\nCall `start_report_session`. It opens a report chat as a child of this\nconversation. Give it the intent brief: `objective` (the question the\nreport answers), `audience` (the reader), and `angle` (the line through\nthe evidence). Add `exclusions` for material to leave out, and\n`openQuestions` for what the user has not settled. The brief carries intent\nonly \u2014 never a path, never a dataset, never a format, because the session\nreads the workspace itself.\n\nThen tell the user where the report is. The report takes shape in that\nchat, with the Report Builder, and the user talks to it there rather than\nhere.\n\nWhen the tool returns `existing-session`, no new session started. Tell the\nuser to continue the report in that chat. Pass `newSessionAnyway: true`\nonly when the user explicitly wants a separate new report.\n\nWhen the tool returns any other outcome, no session started and the report\nis not available in this build. Say that in one line, and give the detail\nthat the outcome carries. Do not compose the report here instead.\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\nStart a report session only when the user asks for a report. Suggest one\nafter a run completes, or when the user asks for a summary or presentation\nof 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 or looked up that exists in no artifact, `show_plan` for a\nstored `planId`, `show_file` for an existing artifact path. Cards render in\nthe order you emit them.\n\nPut each figure next to the prose that discusses it. Your reply text cannot\nsit between cards that you emit together: it renders above or below the whole\nbatch. Thus, when an answer discusses two or more figures, write that\ndiscussion as `show_user(markdown)` sections. Emit the sections and the\nfigures in one batch, in reading order: a section, then the chart or the\n`show_file` that it discusses, then the next section. Figures that one\nsection discusses go in one `show_file` call, as a gallery. Figures for\ndifferent sections go in separate calls. Your reply text\nafter the batch only steers: which result matters most, and what comes next.\nAn answer about one figure needs no sections. Show the figure, then write the\nanswer.\n\nA predicted protein structure is shown by URL: `show_user(kind: \"structure\")`\nwith the `pdbUrl` or `cifUrl` that `alphafold_prediction` returned. A\n`.pdb` or `.cif` a step wrote is an existing artifact \u2014 `show_file`.\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 brief.\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- **Interpret from search descriptions.** A `workspace_search` hit gives a\n description, not the values. Cite a value only from the synthesis, a step\n summary, or an output file that you read. If you cannot find the data\n behind a claim, say so.\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- **Silently violate a constraint.** A constraint in working memory is\n binding for the rest of the analysis. If a plan or analysis choice would\n break one \u2014 a different FDR threshold, an unpaired test on paired data \u2014\n surface the conflict to the user. Never just proceed.\n- **Plan data exploration steps.** Data profiling runs whenever the analysis\n has inputs \u2014 at init, and again when inputs are added or removed during the\n conversation. Never propose \"explore data structure\" or \"initial\n assessment\" steps.\n- **Split one targeted request into multiple ad hoc runs.** Keep one explicit\n computational request in one ad hoc call. Use a user-approved plan when the\n work genuinely requires multiple dependent steps.\n- **Report an environment gap as permanent without checking.** \"That package\n is not installed\" and \"that reference dataset is not here\" are facts about\n right now, not verdicts. Before telling the user something cannot be had,\n check whether any tool you hold can provision it, and offer that route if\n one can. Call it impossible only after you have looked and found no path.\n\n## Guidelines\n\n- Keep responses concise and data-driven.\n- When the user's intent is ambiguous, ask a brief clarifying question.\n- 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";
@@ -77,7 +77,9 @@ When the user wants to run an analysis:
77
77
  5. **Execute** — on approval, call
78
78
  \`execute_analysis({ mode: "plan", planId })\` with the
79
79
  approved \`planId\`. The tool resolves the plan server-side and starts
80
- the workflow.
80
+ the workflow. A record in the thread that says that the person approved a
81
+ plan means that the run of that plan started, thus the plan needs no
82
+ \`execute_analysis\` call.
81
83
  6. **Plan stale / invalid?** If \`execute_analysis\` fails because the plan is
82
84
  not found, is malformed, or fails validation, or \`show_plan\` returns
83
85
  \`shown: false\` with \`reason: "plan_not_found"\`, the \`planId\` is no longer
@@ -120,7 +120,8 @@ leaves open:
120
120
  - An inventory block that reports a failed lookup. Absence in it proves
121
121
  nothing, so read that inventory through its tool.
122
122
  - A reference census that says it is cut short, where a step needs a
123
- collection it does not show.
123
+ collection it does not show. A bounded listing is not evidence of absence,
124
+ so confirm through the tool before you call that data missing.
124
125
  - A package whose exact spelling or language track the census leaves unclear.
125
126
 
126
127
  Batch the lookups. \`names\` takes every package you are unsure of in one call,
@@ -212,10 +213,18 @@ ${agentCatalog}
212
213
  ### Routing Rules
213
214
  - Route by primary data object: if the step works on a count matrix, assign
214
215
  the transcriptomics agent; if on a gene list, assign the enrichment agent.
216
+ - A gene list does not settle the assignment by itself. Read what the step
217
+ asks of the list. A step that builds, labels, or benchmarks a gene set for a
218
+ safety or tissue-risk question goes to the specialist whose capabilities name
219
+ that work, not to the enrichment agent.
215
220
  - When a step could go to either a modality or cross-cutting agent, prefer
216
221
  cross-cutting if the input is already in generic format (CSV/TSV). Prefer
217
222
  modality if it needs modality-specific objects (AnnData, SummarizedExperiment).
218
223
  - If no agent is an exact fit, pick the closest specialist from the list.
224
+ - Name the capability you routed on. Copy one phrase out of that agent's
225
+ capabilities into the step's \`capability\` field, spelled exactly as the
226
+ catalog spells it. The launch resolves what reference data that capability
227
+ needs before the run starts, so a paraphrase costs the step that check.
219
228
 
220
229
  ## Grounding
221
230
 
@@ -287,7 +296,9 @@ safety, toxicity, or treatment outcomes:
287
296
  5. **Route safety data correctly.** If the data includes safety
288
297
  biomarkers (ALT, AST, troponin, creatinine, hERG), adverse event
289
298
  records, or toxicity classifications, route to the
290
- translational-safety-agent.
299
+ translational-safety-agent. Route a request for matched negative
300
+ controls, or for a tissue-risk benchmark set, to the same agent even
301
+ when the only input is a gene list.
291
302
 
292
303
  6. **Route immune profiling correctly.** If the research question
293
304
  involves immune cell composition, tumor microenvironment
@@ -8,14 +8,16 @@
8
8
  *
9
9
  * One surface carries the three, and each member is optional alone. Thus a composition binds the members
10
10
  * that it records, and the harness reads an absent member as absence and never as an error. The run emit
11
- * takes the `RunSession` of the run, because a durable run carries one and a persisting realization needs
12
- * its credential. A session act carries no session, thus the session emit takes the event alone.
11
+ * takes an `AgentSession`, because a persisting realization needs the credential of the actor. The
12
+ * workflow body passes the `RunSession` of its durable input, and the run canceler passes the session of
13
+ * the cancel. A session act carries no session, thus the session emit takes the event alone.
13
14
  *
14
- * Both emit members are fire-and-forget. Neither gives a result, and the harness reads none. A site emits
15
- * after its act lands, thus a refused act and a failed act each emit nothing and the record states what
16
- * happened.
15
+ * Both emit members are fire-and-forget. Neither gives a result, and the harness reads none. The writer
16
+ * that wins the conditional terminal write of the run row emits `run_completed`. The body emits it before
17
+ * its status write commits, or after a refused or failed write. The canceler emits it only when its write
18
+ * changes the row, thus a refused cancel emits nothing.
17
19
  */
18
- import type { RunSession } from "../auth/types.js";
20
+ import type { AgentSession } from "../auth/types.js";
19
21
  import type { Logger } from "../lib/logger.js";
20
22
  import type { ProvenanceExport } from "../report-render/provenance-data.js";
21
23
  import type { AuthoringBlock } from "../report-model/authoring-grammar.js";
@@ -26,14 +28,19 @@ export type { ProvenanceExport } from "../report-render/provenance-data.js";
26
28
  * Harness-owned plain union — the harness stays tsprov-free and bus-free; the
27
29
  * host maps these execution facts onto its own ledger vocabulary.
28
30
  *
29
- * Every `atMs` is epoch milliseconds read via `await DBOS.now()`, a checkpointed
30
- * step: a body re-executed by DBOS recovery reads the recorded value, so a
31
+ * In the workflow body, every `atMs` is epoch milliseconds read via `await DBOS.now()`, a
32
+ * checkpointed step: a body re-executed by DBOS recovery reads the recorded value, so a
31
33
  * re-emitted event carries the identical timestamp and merges on the host's
32
- * ledger without a value conflict. Never source these from a wall clock
34
+ * ledger without a value conflict. The body never sources these from a wall clock
33
35
  * (`Date.now()`) — that would diverge across replays and defeat the merge.
34
36
  *
35
- * `run_completed.durationMs` is the terminal `atMs` minus the `run_started`
36
- * `atMs` (both `DBOS.now()` reads) — the true workflow-observed run span.
37
+ * The run canceler is the second emit site of `step_completed` and `run_completed`, and it emits only
38
+ * when its write of the run row changes the row. It is not a workflow, thus no replay fires its emits,
39
+ * and it reads the wall clock one time for all of them.
40
+ *
41
+ * In the body, `run_completed.durationMs` is the terminal `atMs` minus the `run_started`
42
+ * `atMs` (both `DBOS.now()` reads) — the true workflow-observed run span. The canceler holds no
43
+ * `run_started` time, thus it measures from the `started_at` of the run row.
37
44
  *
38
45
  * `step_completed` fires once at EVERY scheduler-loop settlement — the only site
39
46
  * that observes every executed step (registration sees only artifact-producing
@@ -68,10 +75,8 @@ export type RunProvenanceEvent = {
68
75
  analysisId: string;
69
76
  runId: string;
70
77
  /**
71
- * The body's terminal status. Both boundary sites resolve it through `deriveFinalStatus`,
72
- * which records a suspension as `"canceled"` — so `"suspended_insufficient_funds"` (a
73
- * `RunStatus` member) is never emitted here and is deliberately absent from this narrower
74
- * `ExecuteAnalysisFinalStatus`-minus-`"running"` set.
78
+ * The body's terminal status, resolved through `deriveFinalStatus`. A suspension is
79
+ * `"suspended_insufficient_funds"`.
75
80
  */
76
81
  status: Exclude<ExecuteAnalysisFinalStatus, "running">;
77
82
  atMs: number;
@@ -95,7 +100,7 @@ type BlockAct = {
95
100
  * One observation of a session.
96
101
  *
97
102
  * The union names one event for each act: the start of a session, the four block operations, the title,
98
- * the derivation, the preview, the record, and the file write of a conversation turn. Each event carries
103
+ * the derivation, the preview, the record, the file write of a conversation turn, and the approval of a plan. Each event carries
99
104
  * the analysis and the thread, thus a consumer places it with no lookup. The title sits on the document,
100
105
  * thus the title event names no block.
101
106
  *
@@ -151,6 +156,19 @@ export type SessionProvenanceEvent = {
151
156
  threadId: string;
152
157
  versionId: string;
153
158
  replaced: boolean;
159
+ } | {
160
+ /**
161
+ * A host approved a plan and the approval started its run. This is the one session event that
162
+ * carries a time: the approval is read before the launch, and the run emits `run_started` from
163
+ * its workflow body, thus a record orders the approval before the start only through `atMs`.
164
+ */
165
+ type: "approve-plan";
166
+ analysisId: string;
167
+ threadId: string;
168
+ planId: string;
169
+ runId: string;
170
+ /** Epoch milliseconds of the approval, read before the launch. */
171
+ atMs: number;
154
172
  } | {
155
173
  /**
156
174
  * One file that a conversation turn wrote with a file tool — a write under no run and no
@@ -181,11 +199,12 @@ export type ProvenanceSeam = {
181
199
  * The run engine emits the facts of one run here.
182
200
  *
183
201
  * Synchronous by signature (`void`, never a promise): a host that needs input and output dispatches
184
- * the work rather than making a workflow body wait behind it. The `RunSession` rides from the durable
185
- * workflow input, because a persisting realization needs the credential, the scope, and the identity
186
- * of the run. A realization can ignore the parameter.
202
+ * the work rather than making a workflow body wait behind it. The session rides beside the event,
203
+ * because a persisting realization needs the credential, the scope, and the identity of the actor.
204
+ * The body passes the `RunSession` of its durable workflow input. The run canceler passes the
205
+ * session of the cancel, because it holds no `RunSession`. A realization can ignore the parameter.
187
206
  */
188
- readonly emitRunEvent?: (event: RunProvenanceEvent, session: RunSession) => void;
207
+ readonly emitRunEvent?: (event: RunProvenanceEvent, session: AgentSession) => void;
189
208
  /**
190
209
  * A session emits one act here. A session act runs under no authorized run, thus this member takes
191
210
  * the event alone. It is synchronous by signature for the same reason as the run emit.
@@ -8,12 +8,14 @@
8
8
  *
9
9
  * One surface carries the three, and each member is optional alone. Thus a composition binds the members
10
10
  * that it records, and the harness reads an absent member as absence and never as an error. The run emit
11
- * takes the `RunSession` of the run, because a durable run carries one and a persisting realization needs
12
- * its credential. A session act carries no session, thus the session emit takes the event alone.
11
+ * takes an `AgentSession`, because a persisting realization needs the credential of the actor. The
12
+ * workflow body passes the `RunSession` of its durable input, and the run canceler passes the session of
13
+ * the cancel. A session act carries no session, thus the session emit takes the event alone.
13
14
  *
14
- * Both emit members are fire-and-forget. Neither gives a result, and the harness reads none. A site emits
15
- * after its act lands, thus a refused act and a failed act each emit nothing and the record states what
16
- * happened.
15
+ * Both emit members are fire-and-forget. Neither gives a result, and the harness reads none. The writer
16
+ * that wins the conditional terminal write of the run row emits `run_completed`. The body emits it before
17
+ * its status write commits, or after a refused or failed write. The canceler emits it only when its write
18
+ * changes the row, thus a refused cancel emits nothing.
17
19
  */
18
20
  /**
19
21
  * Bind the optional session emit and the logger of one site into a total emit.
@@ -97,6 +97,46 @@ function thinkingWithBinding(reasoning, mode) {
97
97
  return { blockBinding };
98
98
  return { type: "adaptive", display: "summarized", blockBinding };
99
99
  }
100
+ /**
101
+ * The model that answered after a server-side fallback hop, `""` when the hop is
102
+ * reported without one, and `undefined` when no hop happened.
103
+ *
104
+ * A model can refuse a request by category — a biology prompt is one such category —
105
+ * and the API then re-serves it on another model. It reports the hop TWICE: as a
106
+ * `fallback` content block inside the assistant turn, and on
107
+ * `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps only the second,
108
+ * because the AI SDK has no part for a model hop, and it drops the block from the
109
+ * content it returns (`anthropic-messages-language-model`, the `content_block_start`
110
+ * case). Thus the turn the harness holds is not the turn the API produced, and every
111
+ * signed thinking block of that turn is rejected from the next request on. This reader
112
+ * is what lets {@link settleFallbackReply} see the hop the content no longer shows.
113
+ */
114
+ function fallbackModelOf(metadata) {
115
+ const entries = metadata?.["anthropic"]?.["iterations"];
116
+ if (!Array.isArray(entries))
117
+ return undefined;
118
+ for (const entry of entries) {
119
+ if (typeof entry !== "object" || entry === null)
120
+ continue;
121
+ const fields = entry;
122
+ if (fields["type"] !== "fallback_message")
123
+ continue;
124
+ return typeof fields["model"] === "string" ? fields["model"] : "";
125
+ }
126
+ return undefined;
127
+ }
128
+ /**
129
+ * The assistant message with no thinking block, and with `""` for content when it held
130
+ * nothing else. An empty message reaches no wire: `sanitizeMessages` drops it.
131
+ */
132
+ function withoutThinking(message) {
133
+ if (typeof message.content === "string")
134
+ return message;
135
+ const content = message.content.filter((part) => part.type !== "reasoning" && part.type !== "reasoning-file");
136
+ if (content.length === message.content.length)
137
+ return message;
138
+ return content.length === 0 ? { ...message, content: "" } : { ...message, content };
139
+ }
100
140
  /** Reads `providerMetadata.anthropic.inputTransformations`; empty for another vendor. */
101
141
  function inputTransformationsOf(metadata) {
102
142
  const entries = metadata?.["anthropic"]?.["inputTransformations"];
@@ -476,6 +516,7 @@ export function createAiSdkProvider(deps) {
476
516
  // wire that carries no picture there.
477
517
  ...(deps.capabilities?.imageToolResults !== undefined ? { imageToolResults: deps.capabilities.imageToolResults } : {}),
478
518
  ...(deps.capabilities?.imageUserMessages !== undefined ? { imageUserMessages: deps.capabilities.imageUserMessages } : {}),
519
+ ...(deps.capabilities?.contextWindow !== undefined ? { contextWindow: deps.capabilities.contextWindow } : {}),
479
520
  };
480
521
  const logger = (deps.logger ?? createNoopLogger()).named("providers.ai-sdk");
481
522
  const maxOutputTokens = deps.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
@@ -524,6 +565,44 @@ export function createAiSdkProvider(deps) {
524
565
  logger.warn("thinking block dropped", { workload: workloadOf(session), type: drop.type, path: drop.path, reason: drop.reason });
525
566
  }
526
567
  }
568
+ /**
569
+ * The reply as the harness must hold it when a fallback model served it: with no
570
+ * thinking block, and with the model that answered named.
571
+ *
572
+ * A reply that carries a fallback hop ({@link fallbackModelOf}) reaches the harness
573
+ * already short of the `fallback` content block the API put in the turn. The API holds
574
+ * the whole turn against the signature of each of its thinking blocks, so echoing that
575
+ * turn earns `thinking ... blocks in the latest assistant message cannot be modified`,
576
+ * which is a 400 that no retry can pass. The conversation is then dead: the loop sends
577
+ * its history on every request, and nothing downstream can repair a turn.
578
+ *
579
+ * A thinking block is optional on the way back, thus the reply without them is a valid
580
+ * turn, and the next request continues. The cost is the reasoning of this one turn and
581
+ * the cache write that its new prefix pays for. A reply of nothing but thinking carries
582
+ * no part at all, and `sanitizeMessages` drops such a message from the next request.
583
+ *
584
+ * `servedModelId` needs the same correction. {@link captureServedModelId} reads the id
585
+ * the response metadata of the package carries, which is `message_start.message.model`,
586
+ * and that field keeps the REQUESTED model across a hop. Published as it stands, it
587
+ * makes the usage ledger of an embedder attribute the tokens to the model that refused
588
+ * the work. The iteration names the model that answered, thus it wins. An iteration that
589
+ * names no model changes nothing: there is no better id to publish, and the record above
590
+ * is then the only account of the hop.
591
+ */
592
+ function settleFallbackReply(session, response, metadata) {
593
+ const servedBy = fallbackModelOf(metadata);
594
+ if (servedBy === undefined)
595
+ return response;
596
+ logger.warn("reply served by a fallback model, thus the harness drops the thinking of that turn", {
597
+ workload: workloadOf(session),
598
+ ...(servedBy === "" ? {} : { servedBy }),
599
+ });
600
+ return {
601
+ ...response,
602
+ message: withoutThinking(response.message),
603
+ ...(servedBy === "" ? {} : { servedModelId: servedBy }),
604
+ };
605
+ }
527
606
  function chat(req, session, signal) {
528
607
  const reasoning = effortOf(req);
529
608
  const providerOptions = deps.providerOptionsFor?.({ session, reasoning });
@@ -602,14 +681,14 @@ export function createAiSdkProvider(deps) {
602
681
  // reports its drops.
603
682
  const { providerMetadata: metadata, ...reply } = collected;
604
683
  logDroppedThinkingBlocks(session, metadata);
605
- return ok(responseFromMessages({
684
+ return ok(settleFallbackReply(session, responseFromMessages({
606
685
  ...reply,
607
686
  requestedModelId,
608
687
  // Read after the drain: the metadata chunk that carries the
609
688
  // served id reaches the capture only as the stream is consumed.
610
689
  servedModelId: capture.servedModelId(),
611
690
  provider: providerId,
612
- }));
691
+ }), metadata));
613
692
  }
614
693
  catch (e) {
615
694
  if (isAbortError(e) || signal?.aborted || hookCall.unsettled)
@@ -692,7 +771,7 @@ export function createAiSdkProvider(deps) {
692
771
  });
693
772
  if (opened.kind === "completed") {
694
773
  logDroppedThinkingBlocks(session, opened.providerMetadata);
695
- const response = responseFromMessages({
774
+ const response = settleFallbackReply(session, responseFromMessages({
696
775
  messages: opened.messages,
697
776
  fallbackText: "",
698
777
  finishReason: opened.finishReason,
@@ -701,7 +780,7 @@ export function createAiSdkProvider(deps) {
701
780
  requestedModelId,
702
781
  servedModelId: capture.servedModelId(),
703
782
  provider: providerId,
704
- });
783
+ }), opened.providerMetadata);
705
784
  yield { type: "done", response };
706
785
  return;
707
786
  }
@@ -721,7 +800,8 @@ export function createAiSdkProvider(deps) {
721
800
  text += pull.text;
722
801
  yield { type: "text-delta", text: pull.text };
723
802
  }
724
- const response = responseFromMessages({
803
+ const metadata = (await result.finalStep).providerMetadata;
804
+ const response = settleFallbackReply(session, responseFromMessages({
725
805
  messages: await result.responseMessages,
726
806
  fallbackText: text,
727
807
  finishReason: await result.finishReason,
@@ -732,8 +812,8 @@ export function createAiSdkProvider(deps) {
732
812
  // reaches the capture only as the stream is consumed.
733
813
  servedModelId: capture.servedModelId(),
734
814
  provider: providerId,
735
- });
736
- logDroppedThinkingBlocks(session, (await result.finalStep).providerMetadata);
815
+ }), metadata);
816
+ logDroppedThinkingBlocks(session, metadata);
737
817
  yield { type: "done", response };
738
818
  }
739
819
  catch (e) {
@@ -789,10 +869,12 @@ export function wrapFetchWithRequestTimeout(fetchImpl, requestTimeoutMs) {
789
869
  const signal = callerSignal ? AbortSignal.any([callerSignal, controller.signal]) : controller.signal;
790
870
  callerSignal?.addEventListener("abort", clear, { once: true });
791
871
  // `timeout: false` is an extension of the fetch init of Bun. Bun cuts a
792
- // request that stays idle for 300 seconds, and this key lifts that cut. Node
793
- // ignores an unknown member of the init, thus the key is inert there. A
794
- // probe on Bun 1.3.14 proved both results. The `RequestInit` type declares
795
- // no such member, thus the cast.
872
+ // request that stays idle past its default of 300 seconds, and this key
873
+ // lifts that cut. Node ignores an unknown member of the init, thus the key
874
+ // is inert there. A probe on Bun 1.4.2 proved both results: the cut fired
875
+ // at 360 seconds, because Bun pads the timer to its minute wheel, and a
876
+ // request with the key completed after 420 seconds. The `RequestInit` type
877
+ // declares no such member, thus the cast.
796
878
  const forwarded = { ...init, signal, timeout: false };
797
879
  try {
798
880
  return await fetchImpl(input, forwarded);
@@ -31,6 +31,12 @@ export interface ProviderCapabilities {
31
31
  * "unknown".
32
32
  */
33
33
  readonly imageUserMessages?: boolean;
34
+ /**
35
+ * The context window of the model in tokens, when the host knows it. A chat
36
+ * thread then compacts at 80% of the window at the latest. Absent means "at
37
+ * least the compaction budget of the agent", thus the budget stays as declared.
38
+ */
39
+ readonly contextWindow?: number;
34
40
  }
35
41
  /**
36
42
  * The request carries no free-form provider options. That would let a caller