@inflexa-ai/harness 0.39.2 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +2 -3
  2. package/dist/agents/conversation-agent.d.ts +3 -0
  3. package/dist/agents/conversation-agent.js +13 -2
  4. package/dist/agents/report-session-agent.d.ts +3 -0
  5. package/dist/agents/report-session-agent.js +2 -0
  6. package/dist/agents/sandbox/cheminformatics-agent.js +0 -3
  7. package/dist/agents/sandbox/shared.d.ts +11 -2
  8. package/dist/agents/sandbox/shared.js +4 -0
  9. package/dist/app/chat-turn.d.ts +104 -12
  10. package/dist/app/chat-turn.js +180 -21
  11. package/dist/app/message-assembly.d.ts +16 -52
  12. package/dist/app/message-assembly.js +41 -68
  13. package/dist/app/run-activity.d.ts +4 -6
  14. package/dist/app/run-activity.js +7 -27
  15. package/dist/app/spawn-report-session.d.ts +6 -11
  16. package/dist/app/spawn-report-session.js +5 -6
  17. package/dist/app/synthesize-run.d.ts +3 -0
  18. package/dist/app/synthesize-run.js +1 -0
  19. package/dist/billing/usage-recorder.d.ts +3 -4
  20. package/dist/billing/usage-recorder.js +3 -4
  21. package/dist/contracts/chat-parts.d.ts +11 -1
  22. package/dist/contracts/chat-parts.js +1 -0
  23. package/dist/contracts/index.d.ts +1 -1
  24. package/dist/contracts/part-registry.js +1 -0
  25. package/dist/contracts/report-blocks.d.ts +8 -8
  26. package/dist/contracts/schemas/chat-parts.d.ts +32 -9
  27. package/dist/contracts/schemas/chat-parts.js +10 -0
  28. package/dist/execution/artifact-metadata.d.ts +26 -33
  29. package/dist/execution/artifact-metadata.js +44 -106
  30. package/dist/execution/post-step-pipeline.d.ts +23 -16
  31. package/dist/execution/post-step-pipeline.js +19 -21
  32. package/dist/execution/run-synthesis.d.ts +3 -0
  33. package/dist/execution/run-synthesis.js +11 -1
  34. package/dist/execution/step-summary.d.ts +21 -30
  35. package/dist/execution/step-summary.js +30 -57
  36. package/dist/index.d.ts +9 -8
  37. package/dist/index.js +15 -34
  38. package/dist/lib/zod-issues.js +2 -4
  39. package/dist/loop/compaction.d.ts +20 -0
  40. package/dist/loop/compaction.js +2 -0
  41. package/dist/loop/continue-agent.d.ts +40 -0
  42. package/dist/loop/continue-agent.js +38 -0
  43. package/dist/loop/metrics.d.ts +30 -22
  44. package/dist/loop/metrics.js +51 -22
  45. package/dist/loop/run-agent.d.ts +111 -13
  46. package/dist/loop/run-agent.js +467 -220
  47. package/dist/loop/run-to-terminal.d.ts +16 -17
  48. package/dist/loop/run-to-terminal.js +24 -35
  49. package/dist/loop/tool-mask.d.ts +23 -0
  50. package/dist/loop/tool-mask.js +46 -0
  51. package/dist/loop/tool-output.d.ts +48 -0
  52. package/dist/loop/tool-output.js +79 -0
  53. package/dist/loop/types.d.ts +6 -3
  54. package/dist/memory/ai-sdk-message-storage.d.ts +49 -2
  55. package/dist/memory/ai-sdk-message-storage.js +78 -2
  56. package/dist/memory/conversation-display-recorder.d.ts +6 -0
  57. package/dist/memory/conversation-display-recorder.js +64 -22
  58. package/dist/memory/conversation-display-replay.d.ts +5 -14
  59. package/dist/memory/conversation-display-replay.js +66 -32
  60. package/dist/memory/conversation-display-storage.d.ts +2 -1
  61. package/dist/memory/conversation-display-storage.js +2 -1
  62. package/dist/memory/conversation-view.d.ts +53 -0
  63. package/dist/memory/conversation-view.js +130 -0
  64. package/dist/memory/count-tokens.d.ts +5 -7
  65. package/dist/memory/count-tokens.js +7 -9
  66. package/dist/memory/thread-history.d.ts +74 -102
  67. package/dist/memory/thread-history.js +181 -246
  68. package/dist/memory/thread-store.d.ts +3 -3
  69. package/dist/memory/thread-store.js +7 -2
  70. package/dist/memory/tool-call-integrity.d.ts +15 -21
  71. package/dist/memory/tool-call-integrity.js +31 -38
  72. package/dist/memory/working-memory.d.ts +2 -5
  73. package/dist/memory/working-memory.js +2 -5
  74. package/dist/prompts/SOUL.d.ts +2 -2
  75. package/dist/prompts/SOUL.js +2 -7
  76. package/dist/prompts/analogical-reasoner.d.ts +1 -1
  77. package/dist/prompts/analogical-reasoner.js +16 -32
  78. package/dist/prompts/briefing.js +1 -1
  79. package/dist/prompts/compaction.d.ts +4 -0
  80. package/dist/prompts/compaction.js +24 -0
  81. package/dist/prompts/conversation.d.ts +1 -1
  82. package/dist/prompts/conversation.js +26 -35
  83. package/dist/prompts/execute-analysis/step-summary.js +7 -5
  84. package/dist/prompts/literature-reviewer.d.ts +1 -1
  85. package/dist/prompts/literature-reviewer.js +29 -34
  86. package/dist/prompts/planner.js +15 -29
  87. package/dist/prompts/report-session.d.ts +1 -1
  88. package/dist/prompts/report-session.js +2 -33
  89. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  90. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +1 -2
  91. package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
  92. package/dist/prompts/sandbox/cheminformatics-agent.js +10 -29
  93. package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
  94. package/dist/prompts/sandbox/chromatin-agent.js +1 -2
  95. package/dist/prompts/sandbox/data-profiler.js +6 -3
  96. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  97. package/dist/prompts/sandbox/dna-methylation-agent.js +2 -7
  98. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  99. package/dist/prompts/sandbox/drug-repurposing-agent.js +20 -24
  100. package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
  101. package/dist/prompts/sandbox/enrichment-agent.js +3 -2
  102. package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
  103. package/dist/prompts/sandbox/genomic-variant-agent.js +0 -2
  104. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  105. package/dist/prompts/sandbox/immune-profiling-agent.js +7 -14
  106. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  107. package/dist/prompts/sandbox/metabolomics-agent.js +0 -2
  108. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  109. package/dist/prompts/sandbox/microbiome-agent.js +14 -21
  110. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  111. package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -19
  112. package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
  113. package/dist/prompts/sandbox/multimodal-sc-agent.js +4 -5
  114. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  115. package/dist/prompts/sandbox/network-agent.js +7 -8
  116. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  117. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +19 -19
  118. package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
  119. package/dist/prompts/sandbox/proteomics-agent.js +3 -4
  120. package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
  121. package/dist/prompts/sandbox/scientific-executor.js +7 -10
  122. package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
  123. package/dist/prompts/sandbox/single-cell-agent.js +2 -4
  124. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  125. package/dist/prompts/sandbox/spatial-omics-agent.js +3 -5
  126. package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
  127. package/dist/prompts/sandbox/statistical-modeling-agent.js +1 -1
  128. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  129. package/dist/prompts/sandbox/translational-safety-agent.js +10 -19
  130. package/dist/prompts/sandbox-standards.d.ts +7 -7
  131. package/dist/prompts/sandbox-standards.js +74 -76
  132. package/dist/prompts/synthesis-agent.d.ts +1 -1
  133. package/dist/prompts/synthesis-agent.js +16 -42
  134. package/dist/providers/ai-sdk.d.ts +39 -6
  135. package/dist/providers/ai-sdk.js +117 -18
  136. package/dist/providers/errors.d.ts +2 -0
  137. package/dist/providers/errors.js +5 -0
  138. package/dist/providers/prompt-cache.d.ts +43 -41
  139. package/dist/providers/prompt-cache.js +47 -41
  140. package/dist/providers/reasoning.d.ts +10 -21
  141. package/dist/providers/reasoning.js +10 -21
  142. package/dist/providers/types.d.ts +21 -5
  143. package/dist/report-model/authoring-grammar.d.ts +2 -2
  144. package/dist/runtime/assemble.d.ts +9 -7
  145. package/dist/runtime/assemble.js +7 -3
  146. package/dist/sandbox/create-sandbox.d.ts +2 -2
  147. package/dist/sandbox/submit-exec.d.ts +1 -1
  148. package/dist/schemas/artifact-manifest.d.ts +4 -4
  149. package/dist/state/init.js +39 -4
  150. package/dist/state/purge-analysis.js +4 -0
  151. package/dist/state/tool-outputs.d.ts +4 -0
  152. package/dist/state/tool-outputs.js +30 -0
  153. package/dist/tasks/data-profile.d.ts +3 -0
  154. package/dist/tasks/data-profile.js +2 -0
  155. package/dist/tools/ad-hoc-router.d.ts +8 -0
  156. package/dist/tools/ad-hoc-router.js +21 -2
  157. package/dist/tools/display/show-user.d.ts +1 -1
  158. package/dist/tools/execute-analysis.d.ts +3 -0
  159. package/dist/tools/execute-analysis.js +9 -1
  160. package/dist/tools/read-tool-output.d.ts +7 -0
  161. package/dist/tools/read-tool-output.js +130 -0
  162. package/dist/tools/research/generate-analogy-report.d.ts +10 -26
  163. package/dist/tools/research/generate-analogy-report.js +76 -163
  164. package/dist/tools/research/generate-plan.d.ts +3 -0
  165. package/dist/tools/research/generate-plan.js +4 -1
  166. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  167. package/dist/tools/research/literature-reviewer.js +3 -0
  168. package/dist/tools/sandbox/report-blocker.d.ts +18 -5
  169. package/dist/tools/sandbox/report-blocker.js +54 -6
  170. package/dist/tools/sandbox/submit-file-metadata.d.ts +30 -0
  171. package/dist/tools/sandbox/submit-file-metadata.js +67 -0
  172. package/dist/tools/start-report-session.d.ts +1 -3
  173. package/dist/tools/start-report-session.js +0 -2
  174. package/dist/tools/workspace/execute-command.js +7 -4
  175. package/dist/tools/workspace/result-bounds.d.ts +4 -4
  176. package/dist/tools/workspace/result-bounds.js +6 -5
  177. package/dist/tools/workspace/workspace-search.d.ts +1 -1
  178. package/dist/workflows/execute-analysis.d.ts +3 -0
  179. package/dist/workflows/execute-analysis.js +1 -0
  180. package/dist/workflows/sandbox-step.d.ts +30 -9
  181. package/dist/workflows/sandbox-step.js +37 -8
  182. package/package.json +9 -9
  183. package/dist/providers/anthropic.d.ts +0 -25
  184. package/dist/providers/anthropic.js +0 -28
package/README.md CHANGED
@@ -31,8 +31,7 @@ Import the curated, embedder-facing surface from the package root. Every deep su
31
31
  import {
32
32
  assembleCoreRuntime, // host-neutral composition point
33
33
  createConversationAgent,
34
- createAnthropicProvider, // ChatProvider — Anthropic
35
- createConfiguredAiSdkProvider, // ChatProvider — any OpenAI-compatible endpoint
34
+ createConfiguredAiSdkProvider, // ChatProvider — Anthropic, OpenAI, or an OpenAI-compatible endpoint
36
35
  createEmbeddingProvider,
37
36
  createLocalRunAuthorizer, // RunAuthorizer seam (local default)
38
37
  createNoopRunCharge, // RunCharge seam
@@ -97,7 +96,7 @@ function pinoAsHarnessLogger(p: pino.Logger, names: readonly string[] = []): Log
97
96
 
98
97
  ## How it executes
99
98
 
100
- Chat is a plain in-process turn (`prepareChatTurn` → `runAgent` → `appendTurn`); the harness ships no HTTP layer, so the host owns the route. Compute-heavy work runs as **durable workflows** — `executeAnalysis` starts a child workflow per plan step once its dependencies have completed, and each step drives a sandbox agent inside a container. The same `runAgent` primitive runs in both modes; durability and the event sink are injected, so the loop body is identical.
99
+ Chat is a plain in-process turn. A turn is `runChatTurn`, and the harness stores the opening, each round, and the outcome of the turn. The harness ships no HTTP layer, thus the host owns the route. Compute-heavy work runs as **durable workflows** — `executeAnalysis` starts a child workflow per plan step once its dependencies have completed, and each step drives a sandbox agent inside a container. The same `runAgent` primitive runs in both modes; durability and the event sink are injected, so the loop body is identical.
101
100
 
102
101
  The sandbox protocol is submit-then-retrieve, which is what lets a long run survive a host restart: the host `POST /exec`s a command and retrieves the result over one of two transports — **poll** (the default: the host asks, the sandbox initiates nothing and needs no network egress) or **callback** (opt-in: the sandbox POSTs signed callbacks to an ingress the embedder runs). Both exec endpoints are HMAC signature-authenticated. See the [`harness-sandbox-exec`](./openspec/specs/harness-sandbox-exec/) spec.
103
102
 
@@ -29,6 +29,7 @@ import type { EnsureSessionStateResult } from "../app/report-session-runtime.js"
29
29
  import type { ResourcePolicy } from "../config/resource-limits.js";
30
30
  import type { ChromeConfig } from "../lib/chrome.js";
31
31
  import type { AcquireEyes } from "../lib/eyes.js";
32
+ import type { ToolOutputStore } from "../loop/tool-output.js";
32
33
  import type { AgentDefinition } from "../loop/types.js";
33
34
  import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
34
35
  import type { Tool } from "../tools/define-tool.js";
@@ -60,6 +61,8 @@ export interface ConversationAgentDeps extends EnvironmentStorePaths {
60
61
  * Omitted falls back to the no-op recorder.
61
62
  */
62
63
  readonly usageRecorder?: UsageRecorder;
64
+ /** Adds `read_tool_output`, and each loop-driving tool gives the same store to its sub-agent. */
65
+ readonly toolOutputStore?: ToolOutputStore;
63
66
  /** Shared host-side bibliographic verification service. */
64
67
  readonly citationResolver: CitationResolver;
65
68
  /** The LLM seam every loop-driving tool runs its sub-agent on. */
@@ -23,6 +23,7 @@
23
23
  * confined to the analysis root, with one provenance session event per
24
24
  * successful write.
25
25
  */
26
+ import { createReadToolOutputTool } from "../tools/read-tool-output.js";
26
27
  import { createWorkingMemory } from "../memory/working-memory.js";
27
28
  import { conversationPrompt } from "../prompts/conversation.js";
28
29
  import { composeSystemPrompt } from "./system-prompt.js";
@@ -45,7 +46,7 @@ export const CONVERSATION_AGENT_ID = "conversation-agent";
45
46
  const CONVERSATION_MAX_ITERATIONS = 200;
46
47
  /** Build the conversation `AgentDefinition` with every tool fully dep-bound. */
47
48
  export function createConversationAgent(deps) {
48
- const { provider, pool, embedding, workspaceFs, model, utilityProvider, utilityModel, executeAnalysisWorkflow, runAuthorizer, runLauncher, bioKeys, chrome, resourcePolicy, hostTools, refStorePath, farmLockFile, imagePackagesFile, readPoolInventory, usageRecorder, citationResolver, } = deps;
49
+ const { provider, pool, embedding, workspaceFs, model, utilityProvider, utilityModel, executeAnalysisWorkflow, runAuthorizer, runLauncher, bioKeys, chrome, resourcePolicy, hostTools, refStorePath, farmLockFile, imagePackagesFile, readPoolInventory, usageRecorder, toolOutputStore, citationResolver, } = deps;
49
50
  const workingMemory = createWorkingMemory(pool);
50
51
  const ncbi = createNcbiTools(bioKeys);
51
52
  const chemDb = createChemDbTools(bioKeys, { ...(deps.logger ? { logger: deps.logger } : {}) });
@@ -121,6 +122,7 @@ export function createConversationAgent(deps) {
121
122
  pool,
122
123
  resourcePolicy,
123
124
  usageRecorder,
125
+ ...(toolOutputStore ? { toolOutputStore } : {}),
124
126
  bioKeys,
125
127
  ...(refStorePath ? { refStorePath } : {}),
126
128
  ...(farmLockFile ? { farmLockFile } : {}),
@@ -141,6 +143,7 @@ export function createConversationAgent(deps) {
141
143
  ...(readPoolInventory ? { readPoolInventory } : {}),
142
144
  ...(deps.extendAnalysisFarm ? { extendAnalysisFarm: deps.extendAnalysisFarm } : {}),
143
145
  ...(deps.logger ? { logger: deps.logger } : {}),
146
+ ...(usageRecorder ? { usageRecorder } : {}),
144
147
  }),
145
148
  // The one report path. The tool starts a report thread as a child of the
146
149
  // conversation, and the user composes the report there with the Report
@@ -158,13 +161,21 @@ export function createConversationAgent(deps) {
158
161
  createShowPlanTool(pool),
159
162
  showFileTool,
160
163
  // Cross-domain analogy generation (sub-agent as a loop-driving tool).
161
- createGenerateAnalogyReportTool({ provider, model, bioKeys, usageRecorder }),
164
+ createGenerateAnalogyReportTool({
165
+ provider,
166
+ model,
167
+ bioKeys,
168
+ usageRecorder,
169
+ ...(toolOutputStore ? { toolOutputStore } : {}),
170
+ ...(deps.logger ? { logger: deps.logger } : {}),
171
+ }),
162
172
  // Workspace semantic search + raw read/grep over the read seam.
163
173
  createWorkspaceSearchTool(pool, embedding),
164
174
  createReadFileTool(workspaceFs),
165
175
  createListFilesTool(workspaceFs),
166
176
  createFileStatTool(workspaceFs),
167
177
  createGrepTool(workspaceFs),
178
+ ...(toolOutputStore ? [createReadToolOutputTool(toolOutputStore)] : []),
168
179
  // Workspace write pair over the session-scoped mutate seam. The working
169
180
  // directory of this agent is the analysis root, so a write lands
170
181
  // anywhere inside its own analysis tree; `..`, a foreign analysis, and
@@ -25,6 +25,7 @@
25
25
  import type { Pool } from "pg";
26
26
  import type { AuthContext } from "../auth/types.js";
27
27
  import type { RunAuthorizer } from "../execution/run-authorizer.js";
28
+ import type { ToolOutputStore } from "../loop/tool-output.js";
28
29
  import type { AgentDefinition } from "../loop/types.js";
29
30
  import type { DeriveTableRunner } from "../tools/report-session/derive-table.js";
30
31
  import type { ReportSessionStateStore } from "../state/report-session-state.js";
@@ -125,6 +126,8 @@ export interface ReportSessionAgentDeps {
125
126
  readonly provenance?: ProvenanceSeam;
126
127
  /** Operational logging seam; omitted falls back to no-op. */
127
128
  readonly logger?: Logger;
129
+ /** Adds `read_tool_output`. The loop of a report turn must keep its texts in the same store. */
130
+ readonly toolOutputStore?: ToolOutputStore;
128
131
  }
129
132
  /** Build the report `AgentDefinition` with every tool bound to its deps. */
130
133
  export declare function createReportSessionAgent(deps: ReportSessionAgentDeps): AgentDefinition;
@@ -22,6 +22,7 @@
22
22
  * same composition as the conversation agent. Thus the prompt prefix stays constant
23
23
  * across threads, and the provider cache reuses it.
24
24
  */
25
+ import { createReadToolOutputTool } from "../tools/read-tool-output.js";
25
26
  import { createReportAuthoringTools } from "../tools/report-authoring/authoring-tools.js";
26
27
  import { createDeriveTableTool, createExaminePageTool, createListPinnedArtifactsTool, createPreviewReportTool, createRecordVersionTool, } from "../tools/report-session/index.js";
27
28
  import { createFileStatTool, createGrepTool, createListFilesTool, createReadFileTool, createWorkspaceSearchTool } from "../tools/workspace/index.js";
@@ -50,6 +51,7 @@ export function createReportSessionAgent(deps) {
50
51
  createListFilesTool(workspaceFs),
51
52
  createFileStatTool(workspaceFs),
52
53
  createGrepTool(workspaceFs),
54
+ ...(deps.toolOutputStore ? [createReadToolOutputTool(deps.toolOutputStore)] : []),
53
55
  createWorkspaceSearchTool(pool, embedding),
54
56
  createInspectRunTool(pool),
55
57
  createInspectDataProfileTool(pool),
@@ -20,8 +20,6 @@ export const meta = {
20
20
  "structure-activity relationship",
21
21
  "target engagement assessment",
22
22
  "occupancy estimation",
23
- "drug perturbation signature matching",
24
- "CMap-style connectivity scoring",
25
23
  "selectivity profiling",
26
24
  "kinase selectivity analysis",
27
25
  ],
@@ -34,7 +32,6 @@ export const meta = {
34
32
  "sar-data",
35
33
  "target-engagement-data",
36
34
  "selectivity-panel-data",
37
- "perturbation-signatures",
38
35
  ],
39
36
  skills: ["cheminformatics", "shared/omics-general"],
40
37
  tools: [...BASE_SANDBOX_TOOLS, "pubmed", "searchGene", "lookupAnnotation", "searchInteractions", "chembl", "drugGeneInteractions", "pubchem"],
@@ -17,6 +17,7 @@
17
17
  * so misconfigured agents fail at startup, not at the first LLM call.
18
18
  */
19
19
  import type { Pool } from "pg";
20
+ import type { ToolOutputStore } from "../../loop/tool-output.js";
20
21
  import type { AgentDefinition } from "../../loop/types.js";
21
22
  import type { ChatProvider, EmbeddingProvider } from "../../providers/types.js";
22
23
  import type { SandboxClient } from "../../sandbox/client.js";
@@ -25,6 +26,7 @@ import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
25
26
  import type { ProvenanceCollector as LineageCollector } from "../../provenance/collector.js";
26
27
  import { type BioToolKeys } from "../../tools/bio/keys.js";
27
28
  import { type BlockerHolder } from "../../tools/sandbox/report-blocker.js";
29
+ import { type FileMetadataCell } from "../../tools/sandbox/submit-file-metadata.js";
28
30
  import type { AgentMeta, SandboxToolName } from "./types.js";
29
31
  import type { EnvironmentStorePaths } from "../../config/environment-stores.js";
30
32
  import type { Logger } from "../../lib/logger.js";
@@ -81,10 +83,17 @@ export interface SandboxAgentDeps extends EnvironmentStorePaths {
81
83
  /**
82
84
  * Per-run blocker cell (see the harness-sandbox-agents spec). When present, the agent gets a
83
85
  * `report_blocker` tool that records `{ kind: "blocker", reason }` into it;
84
- * the sandbox-step body reads `holder.outcome` after the loop. Omit for
85
- * agents that have no terminal status to declare (such as the data profiler).
86
+ * the sandbox-step body reads the blocker from the transcript, falling back to
87
+ * the cell. Omit for agents that have no terminal status to declare (such as the data profiler).
86
88
  */
87
89
  readonly blockerHolder?: BlockerHolder;
90
+ /**
91
+ * When present, the agent declares `submit_file_metadata` from its first request so the
92
+ * tool list stays stable across continuations; the task masks it, the continuation unmasks it.
93
+ */
94
+ readonly fileMetadata?: FileMetadataCell;
95
+ /** Adds `read_tool_output`. The loop of the agent must keep its texts in the same store. */
96
+ readonly toolOutputStore?: ToolOutputStore;
88
97
  /**
89
98
  * The farm-extension seam of the embedder. When it is bound, every
90
99
  * sandbox agent gets the `link_packages` tool in its always-on substrate,
@@ -36,7 +36,9 @@ import { createScanInputsTool } from "../../tools/workspace/scan-inputs.js";
36
36
  import { createEditFileTool, createExecuteCommandTool, createWorkspaceMutator, createWriteFileTool } from "../../tools/workspace/index.js";
37
37
  // Skills (declared per agent via meta.skills).
38
38
  import { createSkillTools } from "../../tools/sandbox/skills.js";
39
+ import { createReadToolOutputTool } from "../../tools/read-tool-output.js";
39
40
  import { createReportBlockerTool } from "../../tools/sandbox/report-blocker.js";
41
+ import { createSubmitFileMetadataTool } from "../../tools/sandbox/submit-file-metadata.js";
40
42
  import { toSandboxPath } from "../../workspace/paths.js";
41
43
  import { setActiveExecId } from "../../state/index.js";
42
44
  import { SANDBOX_AGENT_DEFAULT_MAX_ITERATIONS } from "./types.js";
@@ -198,6 +200,7 @@ export function createSandboxAgent(deps, meta, body, opts = {}) {
198
200
  const skillTools = deps.skillsDir ? Object.values(createSkillTools({ skillsDir: deps.skillsDir, skills: meta.skills })) : [];
199
201
  const tools = [
200
202
  ...buildWorkspaceTools(deps, opts.readOnly ?? false),
203
+ ...(deps.toolOutputStore ? [createReadToolOutputTool(deps.toolOutputStore)] : []),
201
204
  // Always-on, not in the `meta.tools` allowlist: the data profile is the only
202
205
  // record of what the analysis's input dataset IS, and no file carries it (the
203
206
  // profiler's scratch tree is deleted on completion). An agent that cannot pull
@@ -211,6 +214,7 @@ export function createSandboxAgent(deps, meta, body, opts = {}) {
211
214
  ...skillTools,
212
215
  ...resolveSandboxTools(deps, meta.tools),
213
216
  ...(deps.blockerHolder ? [createReportBlockerTool(deps.blockerHolder)] : []),
217
+ ...(deps.fileMetadata ? [createSubmitFileMetadataTool(deps.fileMetadata)] : []),
214
218
  ];
215
219
  return {
216
220
  id: meta.id,
@@ -1,19 +1,19 @@
1
1
  /**
2
- * Chat-turn preparation — the host-agnostic assembly half of one chat turn.
3
- *
4
- * This is the PREPARATION half of a conversation turn, lifted out of the HTTP
5
- * route so callers other than the route (e.g. a CLI) can reuse it. It owns,
6
- * in order: thread-ownership resolution, best-effort title seeding, analysis
7
- * status load, and message assembly. It deliberately owns NONE of the
8
- * transport orchestration (streaming/SSE/queue/status codes) — that stays in
9
- * the caller. A turn is `prepareChatTurn → runAgent(own emit) → appendTurn`.
10
- *
11
- * Returns a typed result: `not_found` when the `threadId` is owned by a
12
- * different analysis (indistinguishable from absent), otherwise `ok` with the
13
- * assembled `messages` and the standalone `userMessage` the caller persists.
2
+ * The chat turn of a host: a turn is `runChatTurn`, which is `openChatTurn` and then the `run` of the
3
+ * open turn. `prepareChatTurn` is the first step of the open. The host gives only its transport
4
+ * values, and the harness stores the opening, each round, and the outcome.
14
5
  */
15
6
  import type { Pool } from "pg";
7
+ import type { RequestSession } from "../auth/types.js";
8
+ import type { UsageRecorder } from "../billing/usage-recorder.js";
9
+ import type { TokenUsageRollup } from "../contracts/usage.js";
10
+ import type { DbError } from "../lib/db-result.js";
11
+ import { type AgentFinish } from "../loop/run-agent.js";
12
+ import type { AgentDefinition, EmitFn } from "../loop/types.js";
16
13
  import { type ThreadType } from "../memory/thread-store.js";
14
+ import type { AgentChat, PromptCachePolicy } from "../providers/types.js";
15
+ import type { ThreadAgentResolver } from "../runtime/assemble.js";
16
+ import type { AskApproval, AskRequest } from "../tools/approval/contract.js";
17
17
  import { type AssembledMessages } from "./message-assembly.js";
18
18
  import type { Logger } from "../lib/logger.js";
19
19
  import { type ProvenanceSeam } from "../provenance/seam.js";
@@ -49,3 +49,95 @@ export type PrepareChatTurnResult = ({
49
49
  * inside {@link assembleMessages} (applied once to the new user input only).
50
50
  */
51
51
  export declare function prepareChatTurn(deps: PrepareChatTurnDeps, params: PrepareChatTurnParams): Promise<PrepareChatTurnResult>;
52
+ /** The root loop of a chat turn compacts its view past this estimate, in tokens. */
53
+ export declare const DEFAULT_CONVERSATION_BUDGET = 150000;
54
+ export interface RunChatTurnDeps extends PrepareChatTurnDeps {
55
+ /** The agent of a turn comes from the thread type, which only the preparation knows. */
56
+ readonly agents: ThreadAgentResolver;
57
+ }
58
+ /**
59
+ * The session of a turn without its provenance. The harness sets the provenance from the agent that
60
+ * it resolves, because only the preparation knows the thread type.
61
+ */
62
+ export type ChatTurnSession = Omit<RequestSession, "provenance">;
63
+ /** What opens a turn: the thread and the user input. */
64
+ export interface OpenChatTurnParams {
65
+ readonly analysisId: string;
66
+ readonly threadId: string;
67
+ readonly userInput: string;
68
+ }
69
+ /** The transport values of a turn that is open. */
70
+ export interface RunOpenChatTurnParams {
71
+ readonly session: ChatTurnSession;
72
+ /** Makes the provider of the turn over the emit sink of the display recorder. */
73
+ readonly chat: (emit: EmitFn) => AgentChat;
74
+ readonly emit: EmitFn;
75
+ readonly signal: AbortSignal;
76
+ readonly usageRecorder: UsageRecorder;
77
+ /** The approval binding. It gets the emit sink of the display recorder, thus the display records each `data-ask`. */
78
+ readonly ask?: (request: AskRequest, emit: EmitFn) => Promise<AskApproval>;
79
+ /** Who sent the user message. */
80
+ readonly author?: string;
81
+ /** When the host opened the turn. Absent counts from the call to {@link openChatTurn}. */
82
+ readonly startedAtMs?: number;
83
+ /** The cache policy of the root loop. Absent gives {@link CONVERSATION_PROMPT_CACHE}. */
84
+ readonly promptCache?: PromptCachePolicy;
85
+ /** The budget of the view of the root loop. Absent gives {@link DEFAULT_CONVERSATION_BUDGET}. */
86
+ readonly conversationBudget?: number;
87
+ }
88
+ /** The values of one whole turn. */
89
+ export type RunChatTurnParams = OpenChatTurnParams & RunOpenChatTurnParams;
90
+ export type ChatTurnOutcome = {
91
+ readonly status: "done";
92
+ readonly finish: AgentFinish;
93
+ } | {
94
+ readonly status: "aborted";
95
+ readonly finish?: AgentFinish;
96
+ } | {
97
+ readonly status: "failed";
98
+ readonly reason: string;
99
+ readonly cause: unknown;
100
+ };
101
+ /** A turn that ended before its run. It wrote no row and no turn record. */
102
+ export type ChatTurnRefusal = {
103
+ readonly kind: "prepare_failed";
104
+ readonly cause: unknown;
105
+ } | {
106
+ readonly kind: "not_found";
107
+ } | {
108
+ readonly kind: "agent_unresolved";
109
+ readonly threadType: ThreadType;
110
+ };
111
+ export interface ChatTurnRan {
112
+ readonly kind: "ran";
113
+ readonly outcome: ChatTurnOutcome;
114
+ /** True when the opening of the turn landed. */
115
+ readonly opened: boolean;
116
+ /** The error of the last write, when rows of the turn or its close did not land. */
117
+ readonly storeError?: DbError;
118
+ readonly durationMs: number;
119
+ readonly turnUsage?: TokenUsageRollup;
120
+ /** The final text of a run that returned. */
121
+ readonly fallbackText?: string;
122
+ }
123
+ export type ChatTurnResult = ChatTurnRefusal | ChatTurnRan;
124
+ /**
125
+ * A turn that is prepared, with its agent resolved. Call `run` one time: each call stores a new
126
+ * opening of the same user message.
127
+ */
128
+ export interface OpenChatTurn {
129
+ readonly kind: "ready";
130
+ /** The agent that runs the turn. The harness stamps its provenance on the session of the run. */
131
+ readonly agent: AgentDefinition;
132
+ readonly run: (params: RunOpenChatTurnParams) => Promise<ChatTurnRan>;
133
+ }
134
+ /**
135
+ * Open one chat turn: prepare it and resolve its agent. A refusal comes back before any row of the
136
+ * turn exists, thus a host can answer it before it starts a stream.
137
+ */
138
+ export declare function openChatTurn(deps: RunChatTurnDeps, params: OpenChatTurnParams): Promise<OpenChatTurn | ChatTurnRefusal>;
139
+ /**
140
+ * Run one chat turn: open it, store its opening, run the root loop with a round sink that stores
141
+ * each round, and close the turn with its outcome.
142
+ */
143
+ export declare function runChatTurn(deps: RunChatTurnDeps, params: RunChatTurnParams): Promise<ChatTurnResult>;
@@ -1,24 +1,24 @@
1
1
  /**
2
- * Chat-turn preparation — the host-agnostic assembly half of one chat turn.
3
- *
4
- * This is the PREPARATION half of a conversation turn, lifted out of the HTTP
5
- * route so callers other than the route (e.g. a CLI) can reuse it. It owns,
6
- * in order: thread-ownership resolution, best-effort title seeding, analysis
7
- * status load, and message assembly. It deliberately owns NONE of the
8
- * transport orchestration (streaming/SSE/queue/status codes) — that stays in
9
- * the caller. A turn is `prepareChatTurn → runAgent(own emit) → appendTurn`.
10
- *
11
- * Returns a typed result: `not_found` when the `threadId` is owned by a
12
- * different analysis (indistinguishable from absent), otherwise `ok` with the
13
- * assembled `messages` and the standalone `userMessage` the caller persists.
2
+ * The chat turn of a host: a turn is `runChatTurn`, which is `openChatTurn` and then the `run` of the
3
+ * open turn. `prepareChatTurn` is the first step of the open. The host gives only its transport
4
+ * values, and the harness stores the opening, each round, and the outcome.
14
5
  */
15
6
  import { unwrapOrThrow } from "../lib/result.js";
7
+ import { finalText, runAgent } from "../loop/run-agent.js";
8
+ import { passthroughStep } from "../loop/run-step.js";
9
+ import { compactionExchangeOf } from "../memory/ai-sdk-message-storage.js";
10
+ import { createConversationDisplayRecorder } from "../memory/conversation-display-recorder.js";
16
11
  import { deriveThreadTitle } from "../memory/derive-thread-title.js";
17
- import { createThreadHistory } from "../memory/thread-history.js";
12
+ import { conversationRecordTurn, createThreadHistory } from "../memory/thread-history.js";
18
13
  import { createThreadStore } from "../memory/thread-store.js";
19
14
  import { createWorkingMemory } from "../memory/working-memory.js";
15
+ import { findProviderError } from "../providers/errors.js";
16
+ import { MEMORY_COMPACTION_REQUEST, SUMMARY_COMPACTION_REQUEST } from "../prompts/compaction.js";
17
+ import { CONVERSATION_PROMPT_CACHE } from "../providers/prompt-cache.js";
20
18
  import { loadAnalysisStatus, queryNonTerminalRunsByAnalysis } from "../state/index.js";
21
- import { assembleMessages } from "./message-assembly.js";
19
+ import { createToolOutputStore } from "../state/tool-outputs.js";
20
+ import { suspensionOfFailure } from "../workflows/suspension.js";
21
+ import { assembleMessages, contextRecordsFor } from "./message-assembly.js";
22
22
  import { renderRunActivity, renderRunActivityUnavailable, RUN_ACTIVITY_DETAIL_LIMIT } from "./run-activity.js";
23
23
  import { createNoopLogger } from "../lib/console-logger.js";
24
24
  import { bindSessionEmit } from "../provenance/seam.js";
@@ -42,7 +42,7 @@ export async function prepareChatTurn(deps, params) {
42
42
  return { kind: "not_found" };
43
43
  }
44
44
  // The type a caller resolves the turn's agent from, and the type that the
45
- // message assembly reads for its tail. An existing row carries
45
+ // message assembly reads for its context records. An existing row carries
46
46
  // its own. An absent one defaults to `conversation` — the store's own
47
47
  // default — so a best-effort create that fails non-fatally still leaves a
48
48
  // usable type; a successful create overrides it from the returned row.
@@ -79,19 +79,178 @@ export async function prepareChatTurn(deps, params) {
79
79
  catch (err) {
80
80
  logger.warn("title-seed failed (non-fatal)", logger.errorFields(err));
81
81
  }
82
- const analysisState = await loadAnalysisStatus(pool, analysisId).unwrapOr(null);
83
- const runActivityContext = await queryNonTerminalRunsByAnalysis(pool, analysisId, RUN_ACTIVITY_DETAIL_LIMIT).match((activity) => renderRunActivity(activity), () => renderRunActivityUnavailable());
84
82
  const history = createThreadHistory(pool);
85
- const { messages, userMessage } = await assembleMessages({
83
+ const { messages, userMessage, contextRecords } = await assembleMessages({
86
84
  threadId,
87
85
  threadType,
88
86
  analysisId,
89
87
  userInput,
90
- analysisContext: analysisState?.context ?? null,
91
- runActivityContext,
88
+ ...(await readTurnContext(pool, analysisId)),
92
89
  history,
93
90
  workingMemory: createWorkingMemory(pool),
94
91
  ...(deps.logger ? { logger: deps.logger } : {}),
95
92
  });
96
- return { kind: "ok", threadType, messages, userMessage };
93
+ return { kind: "ok", threadType, messages, userMessage, contextRecords };
94
+ }
95
+ /** The analysis context and the run activity of a turn. A failed read gives the fallback of its kind. */
96
+ async function readTurnContext(pool, analysisId) {
97
+ const analysisState = await loadAnalysisStatus(pool, analysisId).unwrapOr(null);
98
+ const runActivityContext = await queryNonTerminalRunsByAnalysis(pool, analysisId, RUN_ACTIVITY_DETAIL_LIMIT).match((activity) => renderRunActivity(activity), () => renderRunActivityUnavailable());
99
+ return { analysisContext: analysisState?.context ?? null, runActivityContext };
100
+ }
101
+ /** The root loop of a chat turn compacts its view past this estimate, in tokens. */
102
+ export const DEFAULT_CONVERSATION_BUDGET = 150_000;
103
+ /**
104
+ * Open one chat turn: prepare it and resolve its agent. A refusal comes back before any row of the
105
+ * turn exists, thus a host can answer it before it starts a stream.
106
+ */
107
+ export async function openChatTurn(deps, params) {
108
+ const openedAtMs = Date.now();
109
+ let prepared;
110
+ try {
111
+ prepared = await prepareChatTurn(deps, params);
112
+ }
113
+ catch (cause) {
114
+ return { kind: "prepare_failed", cause };
115
+ }
116
+ if (prepared.kind === "not_found")
117
+ return prepared;
118
+ const agent = deps.agents.forThread(prepared.threadType);
119
+ if (agent.isErr())
120
+ return { kind: "agent_unresolved", threadType: prepared.threadType };
121
+ const ready = prepared;
122
+ return {
123
+ kind: "ready",
124
+ agent: agent.value,
125
+ run: (run) => runOpenTurn(deps, params, ready, agent.value, { ...run, startedAtMs: run.startedAtMs ?? openedAtMs }),
126
+ };
127
+ }
128
+ /**
129
+ * Run one chat turn: open it, store its opening, run the root loop with a round sink that stores
130
+ * each round, and close the turn with its outcome.
131
+ */
132
+ export async function runChatTurn(deps, params) {
133
+ const { analysisId, threadId, userInput, ...run } = params;
134
+ const opened = await openChatTurn(deps, { analysisId, threadId, userInput });
135
+ if (opened.kind !== "ready")
136
+ return opened;
137
+ return opened.run(run);
138
+ }
139
+ async function runOpenTurn(deps, turn, prepared, agent, params) {
140
+ const logger = (deps.logger ?? createNoopLogger()).named("harness.chat");
141
+ const session = { ...params.session, provenance: { agentId: agent.id, callPath: [agent.id] } };
142
+ const history = createThreadHistory(deps.pool, deps.logger);
143
+ const recorder = createConversationDisplayRecorder({
144
+ userText: userTextOf(prepared.userMessage),
145
+ topLevelCallPath: session.provenance.callPath,
146
+ sink: params.emit,
147
+ });
148
+ // The groups that did not land yet, in the order of the turn. A failed write keeps them for the next write.
149
+ let pending = [
150
+ {
151
+ modelMessages: [prepared.userMessage, ...prepared.contextRecords],
152
+ displayMessages: recorder.takeOpening(),
153
+ ...(params.author === undefined ? {} : { author: params.author }),
154
+ },
155
+ ];
156
+ let startSeq;
157
+ let storeError;
158
+ // A store fault never stops the turn: the error rides the result.
159
+ const flush = async (close) => {
160
+ if (pending.length === 0 && close === undefined)
161
+ return;
162
+ const closing = close === undefined ? {} : { close };
163
+ const write = startSeq === undefined ? { opening: pending[0], rounds: pending.slice(1), ...closing } : { startSeq, rounds: pending, ...closing };
164
+ (await history.writeTurn(turn.threadId, write)).match((written) => {
165
+ pending = [];
166
+ startSeq = written.startSeq;
167
+ storeError = undefined;
168
+ }, (error) => {
169
+ storeError = error;
170
+ logger.warn("chat turn write failed", { threadId: turn.threadId, op: error.op, ...logger.errorFields(error.cause) });
171
+ });
172
+ };
173
+ await flush();
174
+ const { ask } = params;
175
+ let outcome;
176
+ let fallbackText;
177
+ try {
178
+ const run = await runAgent(agent, prepared.messages, session, {
179
+ provider: params.chat(recorder.emit),
180
+ signal: params.signal,
181
+ emit: recorder.emit,
182
+ runStep: passthroughStep,
183
+ usageRecorder: params.usageRecorder,
184
+ toolOutputStore: createToolOutputStore(deps.pool),
185
+ promptCache: params.promptCache ?? CONVERSATION_PROMPT_CACHE,
186
+ compaction: compactionPolicy(deps, turn.analysisId, params, prepared.threadType, agent, recorder.emit),
187
+ ...(deps.logger === undefined ? {} : { logger: deps.logger }),
188
+ ...(ask === undefined ? {} : { ask: (request) => ask(request, recorder.emit) }),
189
+ onRound: async (round) => {
190
+ pending.push({ modelMessages: round.messages, displayMessages: recorder.takeRound(round.messages) });
191
+ await flush();
192
+ },
193
+ });
194
+ outcome = run.finish.reason === "aborted" ? { status: "aborted", finish: run.finish } : { status: "done", finish: run.finish };
195
+ // An abort can end the run inside an exchange, and the text of an exchange is a summary, and no reply.
196
+ fallbackText = finalText(run.messages.filter((message) => compactionExchangeOf(message) === undefined));
197
+ }
198
+ catch (cause) {
199
+ // A provider failure that races an abort stays a failure, thus both facts must hold.
200
+ const aborted = params.signal.aborted && cause instanceof Error && cause.name === "AbortError";
201
+ outcome = aborted ? { status: "aborted" } : { status: "failed", reason: failureReasonOf(cause), cause };
202
+ }
203
+ const durationMs = Date.now() - params.startedAtMs;
204
+ const turnUsage = outcome.status === "failed" ? undefined : outcome.finish?.turnUsage;
205
+ await flush({
206
+ status: outcome.status,
207
+ ...(outcome.status === "failed" ? { reason: outcome.reason, note: conversationRecordTurn(failureNote(outcome.reason)) } : {}),
208
+ ...(turnUsage === undefined ? {} : { turnUsage }),
209
+ turnDurationMs: durationMs,
210
+ });
211
+ return {
212
+ kind: "ran",
213
+ outcome,
214
+ opened: startSeq !== undefined,
215
+ ...(storeError === undefined ? {} : { storeError }),
216
+ durationMs,
217
+ ...(turnUsage === undefined ? {} : { turnUsage: { ...turnUsage } }),
218
+ ...(fallbackText === undefined ? {} : { fallbackText }),
219
+ };
220
+ }
221
+ /** The sanitized text of the user message, thus the display never holds a secret that the model does not see. */
222
+ function userTextOf(message) {
223
+ if (typeof message.content === "string")
224
+ return message.content;
225
+ return message.content.map((part) => (part.type === "text" ? part.text : "")).join("");
226
+ }
227
+ const WORKING_MEMORY_TOOL_ID = "update_working_memory";
228
+ function compactionPolicy(deps, analysisId, params, threadType, agent, emit) {
229
+ // A report thread reads a frozen copy of the working memory, thus its agent declares no memory tool.
230
+ const remembers = agent.tools.some((tool) => tool.id === WORKING_MEMORY_TOOL_ID);
231
+ const workingMemory = createWorkingMemory(deps.pool);
232
+ return {
233
+ budget: params.conversationBudget ?? DEFAULT_CONVERSATION_BUDGET,
234
+ // A streaming provider sends each text delta with no source, thus a delta of the summary would show as a reply.
235
+ provider: params.chat((event) => (event.type === "text-delta" ? undefined : emit(event))),
236
+ request: remembers ? MEMORY_COMPACTION_REQUEST : SUMMARY_COMPACTION_REQUEST,
237
+ mask: remembers ? { allow: [WORKING_MEMORY_TOOL_ID] } : "none",
238
+ keepFirstTurn: threadType === "report",
239
+ recordsAfter: async (view) => contextRecordsFor({ threadType, analysisId, workingMemory, ...(await readTurnContext(deps.pool, analysisId)) }, view),
240
+ };
241
+ }
242
+ /** The reason of a failed turn. It never holds the error message, because each later turn sends the note to the vendor. */
243
+ function failureReasonOf(err) {
244
+ const suspension = suspensionOfFailure(err);
245
+ if (suspension !== undefined)
246
+ return suspension.reason;
247
+ const providerError = findProviderError(err);
248
+ if (providerError?.type === "auth")
249
+ return "The model endpoint refused the credential.";
250
+ if (providerError !== undefined)
251
+ return "The model request failed.";
252
+ return "The turn stopped on an internal error.";
253
+ }
254
+ function failureNote(reason) {
255
+ return ["[Turn Failed]", `The turn stopped before it finished. Reason: ${reason}`, "The rounds above this note ran, and their results are stored."].join("\n");
97
256
  }