@inflexa-ai/harness 0.39.2 → 0.40.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 +75 -12
  10. package/dist/app/chat-turn.js +149 -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,18 @@
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`, and `prepareChatTurn` is its first step. The
3
+ * host gives only its transport values, and the harness stores the opening, each round, and the outcome.
14
4
  */
15
5
  import type { Pool } from "pg";
6
+ import type { AgentSession } from "../auth/types.js";
7
+ import type { UsageRecorder } from "../billing/usage-recorder.js";
8
+ import type { TokenUsageRollup } from "../contracts/usage.js";
9
+ import type { DbError } from "../lib/db-result.js";
10
+ import { type AgentFinish } from "../loop/run-agent.js";
11
+ import type { EmitFn } from "../loop/types.js";
16
12
  import { type ThreadType } from "../memory/thread-store.js";
13
+ import type { AgentChat, PromptCachePolicy } from "../providers/types.js";
14
+ import type { ThreadAgentResolver } from "../runtime/assemble.js";
15
+ import type { AskApproval, AskRequest } from "../tools/approval/contract.js";
17
16
  import { type AssembledMessages } from "./message-assembly.js";
18
17
  import type { Logger } from "../lib/logger.js";
19
18
  import { type ProvenanceSeam } from "../provenance/seam.js";
@@ -49,3 +48,67 @@ export type PrepareChatTurnResult = ({
49
48
  * inside {@link assembleMessages} (applied once to the new user input only).
50
49
  */
51
50
  export declare function prepareChatTurn(deps: PrepareChatTurnDeps, params: PrepareChatTurnParams): Promise<PrepareChatTurnResult>;
51
+ /** The root loop of a chat turn compacts its view past this estimate, in tokens. */
52
+ export declare const DEFAULT_CONVERSATION_BUDGET = 150000;
53
+ export interface RunChatTurnDeps extends PrepareChatTurnDeps {
54
+ /** The agent of a turn comes from the thread type, which only the preparation knows. */
55
+ readonly agents: ThreadAgentResolver;
56
+ }
57
+ /** The transport values of one turn. */
58
+ export interface RunChatTurnParams {
59
+ readonly analysisId: string;
60
+ readonly threadId: string;
61
+ readonly userInput: string;
62
+ readonly session: AgentSession;
63
+ /** Makes the provider of the turn over the emit sink of the display recorder. */
64
+ readonly chat: (emit: EmitFn) => AgentChat;
65
+ readonly emit: EmitFn;
66
+ readonly signal: AbortSignal;
67
+ readonly usageRecorder: UsageRecorder;
68
+ /** The approval binding. It gets the emit sink of the display recorder, thus the display records each `data-ask`. */
69
+ readonly ask?: (request: AskRequest, emit: EmitFn) => Promise<AskApproval>;
70
+ /** Who sent the user message. */
71
+ readonly author?: string;
72
+ /** When the host opened the turn. Absent counts from the call. */
73
+ readonly startedAtMs?: number;
74
+ /** The cache policy of the root loop. Absent gives {@link CONVERSATION_PROMPT_CACHE}. */
75
+ readonly promptCache?: PromptCachePolicy;
76
+ /** The budget of the view of the root loop. Absent gives {@link DEFAULT_CONVERSATION_BUDGET}. */
77
+ readonly conversationBudget?: number;
78
+ }
79
+ export type ChatTurnOutcome = {
80
+ readonly status: "done";
81
+ readonly finish: AgentFinish;
82
+ } | {
83
+ readonly status: "aborted";
84
+ readonly finish?: AgentFinish;
85
+ } | {
86
+ readonly status: "failed";
87
+ readonly reason: string;
88
+ readonly cause: unknown;
89
+ };
90
+ export type ChatTurnResult = {
91
+ readonly kind: "prepare_failed";
92
+ readonly cause: unknown;
93
+ } | {
94
+ readonly kind: "not_found";
95
+ } | {
96
+ readonly kind: "agent_unresolved";
97
+ readonly threadType: ThreadType;
98
+ } | {
99
+ readonly kind: "ran";
100
+ readonly outcome: ChatTurnOutcome;
101
+ /** True when the opening of the turn landed. */
102
+ readonly opened: boolean;
103
+ /** The error of the last write, when rows of the turn or its close did not land. */
104
+ readonly storeError?: DbError;
105
+ readonly durationMs: number;
106
+ readonly turnUsage?: TokenUsageRollup;
107
+ /** The final text of a run that returned. */
108
+ readonly fallbackText?: string;
109
+ };
110
+ /**
111
+ * Run one chat turn: prepare it, resolve its agent, store its opening, run the root loop with a
112
+ * round sink that stores each round, and close the turn with its outcome.
113
+ */
114
+ export declare function runChatTurn(deps: RunChatTurnDeps, params: RunChatTurnParams): Promise<ChatTurnResult>;
@@ -1,24 +1,23 @@
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`, and `prepareChatTurn` is its first step. The
3
+ * host gives only its transport values, and the harness stores the opening, each round, and the outcome.
14
4
  */
15
5
  import { unwrapOrThrow } from "../lib/result.js";
6
+ import { finalText, runAgent } from "../loop/run-agent.js";
7
+ import { passthroughStep } from "../loop/run-step.js";
8
+ import { compactionExchangeOf } from "../memory/ai-sdk-message-storage.js";
9
+ import { createConversationDisplayRecorder } from "../memory/conversation-display-recorder.js";
16
10
  import { deriveThreadTitle } from "../memory/derive-thread-title.js";
17
- import { createThreadHistory } from "../memory/thread-history.js";
11
+ import { conversationRecordTurn, createThreadHistory } from "../memory/thread-history.js";
18
12
  import { createThreadStore } from "../memory/thread-store.js";
19
13
  import { createWorkingMemory } from "../memory/working-memory.js";
14
+ import { findProviderError } from "../providers/errors.js";
15
+ import { MEMORY_COMPACTION_REQUEST, SUMMARY_COMPACTION_REQUEST } from "../prompts/compaction.js";
16
+ import { CONVERSATION_PROMPT_CACHE } from "../providers/prompt-cache.js";
20
17
  import { loadAnalysisStatus, queryNonTerminalRunsByAnalysis } from "../state/index.js";
21
- import { assembleMessages } from "./message-assembly.js";
18
+ import { createToolOutputStore } from "../state/tool-outputs.js";
19
+ import { suspensionOfFailure } from "../workflows/suspension.js";
20
+ import { assembleMessages, contextRecordsFor } from "./message-assembly.js";
22
21
  import { renderRunActivity, renderRunActivityUnavailable, RUN_ACTIVITY_DETAIL_LIMIT } from "./run-activity.js";
23
22
  import { createNoopLogger } from "../lib/console-logger.js";
24
23
  import { bindSessionEmit } from "../provenance/seam.js";
@@ -42,7 +41,7 @@ export async function prepareChatTurn(deps, params) {
42
41
  return { kind: "not_found" };
43
42
  }
44
43
  // 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
44
+ // message assembly reads for its context records. An existing row carries
46
45
  // its own. An absent one defaults to `conversation` — the store's own
47
46
  // default — so a best-effort create that fails non-fatally still leaves a
48
47
  // usable type; a successful create overrides it from the returned row.
@@ -79,19 +78,148 @@ export async function prepareChatTurn(deps, params) {
79
78
  catch (err) {
80
79
  logger.warn("title-seed failed (non-fatal)", logger.errorFields(err));
81
80
  }
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
81
  const history = createThreadHistory(pool);
85
- const { messages, userMessage } = await assembleMessages({
82
+ const { messages, userMessage, contextRecords } = await assembleMessages({
86
83
  threadId,
87
84
  threadType,
88
85
  analysisId,
89
86
  userInput,
90
- analysisContext: analysisState?.context ?? null,
91
- runActivityContext,
87
+ ...(await readTurnContext(pool, analysisId)),
92
88
  history,
93
89
  workingMemory: createWorkingMemory(pool),
94
90
  ...(deps.logger ? { logger: deps.logger } : {}),
95
91
  });
96
- return { kind: "ok", threadType, messages, userMessage };
92
+ return { kind: "ok", threadType, messages, userMessage, contextRecords };
93
+ }
94
+ /** The analysis context and the run activity of a turn. A failed read gives the fallback of its kind. */
95
+ async function readTurnContext(pool, analysisId) {
96
+ const analysisState = await loadAnalysisStatus(pool, analysisId).unwrapOr(null);
97
+ const runActivityContext = await queryNonTerminalRunsByAnalysis(pool, analysisId, RUN_ACTIVITY_DETAIL_LIMIT).match((activity) => renderRunActivity(activity), () => renderRunActivityUnavailable());
98
+ return { analysisContext: analysisState?.context ?? null, runActivityContext };
99
+ }
100
+ /** The root loop of a chat turn compacts its view past this estimate, in tokens. */
101
+ export const DEFAULT_CONVERSATION_BUDGET = 150_000;
102
+ /**
103
+ * Run one chat turn: prepare it, resolve its agent, store its opening, run the root loop with a
104
+ * round sink that stores each round, and close the turn with its outcome.
105
+ */
106
+ export async function runChatTurn(deps, params) {
107
+ const logger = (deps.logger ?? createNoopLogger()).named("harness.chat");
108
+ const startedAtMs = params.startedAtMs ?? Date.now();
109
+ let prepared;
110
+ try {
111
+ prepared = await prepareChatTurn(deps, { analysisId: params.analysisId, threadId: params.threadId, userInput: params.userInput });
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 history = createThreadHistory(deps.pool, deps.logger);
122
+ const recorder = createConversationDisplayRecorder({ userText: params.userInput, topLevelCallPath: params.session.provenance.callPath, sink: params.emit });
123
+ // The groups that did not land yet, in the order of the turn. A failed write keeps them for the next write.
124
+ let pending = [
125
+ {
126
+ modelMessages: [prepared.userMessage, ...prepared.contextRecords],
127
+ displayMessages: recorder.takeOpening(),
128
+ ...(params.author === undefined ? {} : { author: params.author }),
129
+ },
130
+ ];
131
+ let startSeq;
132
+ let storeError;
133
+ // A store fault never stops the turn: the error rides the result.
134
+ const flush = async (close) => {
135
+ if (pending.length === 0 && close === undefined)
136
+ return;
137
+ const closing = close === undefined ? {} : { close };
138
+ const write = startSeq === undefined ? { opening: pending[0], rounds: pending.slice(1), ...closing } : { startSeq, rounds: pending, ...closing };
139
+ (await history.writeTurn(params.threadId, write)).match((written) => {
140
+ pending = [];
141
+ startSeq = written.startSeq;
142
+ storeError = undefined;
143
+ }, (error) => {
144
+ storeError = error;
145
+ logger.warn("chat turn write failed", { threadId: params.threadId, op: error.op, ...logger.errorFields(error.cause) });
146
+ });
147
+ };
148
+ await flush();
149
+ const { ask } = params;
150
+ let outcome;
151
+ let fallbackText;
152
+ try {
153
+ const run = await runAgent(agent.value, prepared.messages, params.session, {
154
+ provider: params.chat(recorder.emit),
155
+ signal: params.signal,
156
+ emit: recorder.emit,
157
+ runStep: passthroughStep,
158
+ usageRecorder: params.usageRecorder,
159
+ toolOutputStore: createToolOutputStore(deps.pool),
160
+ promptCache: params.promptCache ?? CONVERSATION_PROMPT_CACHE,
161
+ compaction: compactionPolicy(deps, params, prepared.threadType, agent.value, recorder.emit),
162
+ ...(deps.logger === undefined ? {} : { logger: deps.logger }),
163
+ ...(ask === undefined ? {} : { ask: (request) => ask(request, recorder.emit) }),
164
+ onRound: async (round) => {
165
+ pending.push({ modelMessages: round.messages, displayMessages: recorder.takeRound(round.messages) });
166
+ await flush();
167
+ },
168
+ });
169
+ outcome = run.finish.reason === "aborted" ? { status: "aborted", finish: run.finish } : { status: "done", finish: run.finish };
170
+ // An abort can end the run inside an exchange, and the text of an exchange is a summary, and no reply.
171
+ fallbackText = finalText(run.messages.filter((message) => compactionExchangeOf(message) === undefined));
172
+ }
173
+ catch (cause) {
174
+ // A provider failure that races an abort stays a failure, thus both facts must hold.
175
+ const aborted = params.signal.aborted && cause instanceof Error && cause.name === "AbortError";
176
+ outcome = aborted ? { status: "aborted" } : { status: "failed", reason: failureReasonOf(cause), cause };
177
+ }
178
+ const durationMs = Date.now() - startedAtMs;
179
+ const turnUsage = outcome.status === "failed" ? undefined : outcome.finish?.turnUsage;
180
+ await flush({
181
+ status: outcome.status,
182
+ ...(outcome.status === "failed" ? { reason: outcome.reason, note: conversationRecordTurn(failureNote(outcome.reason)) } : {}),
183
+ ...(turnUsage === undefined ? {} : { turnUsage }),
184
+ turnDurationMs: durationMs,
185
+ });
186
+ return {
187
+ kind: "ran",
188
+ outcome,
189
+ opened: startSeq !== undefined,
190
+ ...(storeError === undefined ? {} : { storeError }),
191
+ durationMs,
192
+ ...(turnUsage === undefined ? {} : { turnUsage: { ...turnUsage } }),
193
+ ...(fallbackText === undefined ? {} : { fallbackText }),
194
+ };
195
+ }
196
+ const WORKING_MEMORY_TOOL_ID = "update_working_memory";
197
+ function compactionPolicy(deps, params, threadType, agent, emit) {
198
+ // A report thread reads a frozen copy of the working memory, thus its agent declares no memory tool.
199
+ const remembers = agent.tools.some((tool) => tool.id === WORKING_MEMORY_TOOL_ID);
200
+ const workingMemory = createWorkingMemory(deps.pool);
201
+ return {
202
+ budget: params.conversationBudget ?? DEFAULT_CONVERSATION_BUDGET,
203
+ // A streaming provider sends each text delta with no source, thus a delta of the summary would show as a reply.
204
+ provider: params.chat((event) => (event.type === "text-delta" ? undefined : emit(event))),
205
+ request: remembers ? MEMORY_COMPACTION_REQUEST : SUMMARY_COMPACTION_REQUEST,
206
+ mask: remembers ? { allow: [WORKING_MEMORY_TOOL_ID] } : "none",
207
+ keepFirstTurn: threadType === "report",
208
+ recordsAfter: async (view) => contextRecordsFor({ threadType, analysisId: params.analysisId, workingMemory, ...(await readTurnContext(deps.pool, params.analysisId)) }, view),
209
+ };
210
+ }
211
+ /** The reason of a failed turn. It never holds the error message, because each later turn sends the note to the vendor. */
212
+ function failureReasonOf(err) {
213
+ const suspension = suspensionOfFailure(err);
214
+ if (suspension !== undefined)
215
+ return suspension.reason;
216
+ const providerError = findProviderError(err);
217
+ if (providerError?.type === "auth")
218
+ return "The model endpoint refused the credential.";
219
+ if (providerError !== undefined)
220
+ return "The model request failed.";
221
+ return "The turn stopped on an internal error.";
222
+ }
223
+ function failureNote(reason) {
224
+ 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
225
  }
@@ -1,38 +1,10 @@
1
1
  /**
2
- * Chat-turn message assembly.
2
+ * Chat-turn message assembly: the view of the latest compaction marker, the user message, and then the
3
+ * context records. A kind gets a new record only when the view holds none of that kind, or when the hash
4
+ * of its text differs. The root loop compacts the view past its budget.
3
5
  *
4
- * Builds the `messages` array `runAgent` receives for one conversation turn:
5
- *
6
- * [ ...loadRecent(threadId, budget) ← stable, cacheable prefix
7
- * {user: cortex_analysis_state.context}, ← tail
8
- * {user: runActivityContext}, ← tail
9
- * {user: render(workingMemory)}, ← tail, `conversation` thread only
10
- * {user: normalizeUnicode(redactSecrets(input))} ] ← tail
11
- *
12
- * `system + tools + history` is the cacheable prefix — it only extends
13
- * turn-to-turn. Run activity, working memory, and analysis context go in the
14
- * **tail** as `user` messages: they change every turn, so a system-message
15
- * placement would bust the Anthropic cache prefix.
16
- *
17
- * A `report` thread does not get the working-memory tail. The spawn copies the
18
- * working-memory render into the child transcript at the anchor. A live render
19
- * sees state past that anchor, and that breaks the knowledge cap of a report
20
- * session. The analysis context and the run activity stay on both thread types.
21
- *
22
- * The history window of a `report` thread also keeps the first turn. That turn
23
- * is the seed, and it is the one record of the brief and of the copied render.
24
- * The window evicts the oldest turn first, thus a long session would drop its
25
- * own charter and keep only its tools. A `conversation` thread evicts as before,
26
- * because its live tail carries the working memory on each turn.
27
- *
28
- * Sanitization (`redactSecrets`, `normalizeUnicode`) is applied **once**, to
29
- * the new user input only — never to history, assistant messages, tool
30
- * results, the analysis context, or the rendered working memory.
31
- *
32
- * The returned `userMessage` is the sanitized user input on its own — the
33
- * route persists `[userMessage, ...loop output]` via `appendTurn`, so the
34
- * tail injections (ephemeral, re-derived each turn) are never written to the
35
- * thread store.
6
+ * The view of a `report` thread keeps the first turn, because that turn is the seed: the one record of
7
+ * the brief and of the copied working memory.
36
8
  */
37
9
  import type { ModelMessage } from "ai";
38
10
  import type { Logger } from "../lib/logger.js";
@@ -40,16 +12,10 @@ import type { LoopMessage } from "../loop/types.js";
40
12
  import type { ThreadHistory } from "../memory/thread-history.js";
41
13
  import type { ThreadType } from "../memory/thread-store.js";
42
14
  import type { WorkingMemoryStore } from "../memory/working-memory.js";
43
- /**
44
- * Token budget for the `loadRecent` history window. Sized to leave headroom
45
- * under the model context window for the system prompt, tool definitions,
46
- * the three tail messages, and the output budget.
47
- */
48
- export declare const DEFAULT_HISTORY_TOKEN_BUDGET = 120000;
49
15
  export interface AssembleMessagesArgs {
50
16
  /** The conversation thread — a UI-generated UUID, never the analysisId. */
51
17
  readonly threadId: string;
52
- /** The type of the thread. A `report` thread drops the working-memory tail, and its window keeps the seed. */
18
+ /** The type of the thread. A `report` thread gets no working-memory record, and its view keeps the seed. */
53
19
  readonly threadType: ThreadType;
54
20
  /** The analysis scope — keys working memory and (separately) the context. */
55
21
  readonly analysisId: string;
@@ -57,29 +23,27 @@ export interface AssembleMessagesArgs {
57
23
  readonly userInput: string;
58
24
  /** `cortex_analysis_state.context`, already read by the route. `null` when absent. */
59
25
  readonly analysisContext: string | null;
60
- /** Fresh, non-persisted analysis-wide run activity rendered by chat-turn preparation. */
26
+ /** The analysis-wide run activity that chat-turn preparation rendered for this turn. */
61
27
  readonly runActivityContext: string;
62
- /** The conversation message store — supplies the history window. */
28
+ /** The conversation message store — supplies the view of the thread. */
63
29
  readonly history: ThreadHistory;
64
- /** The working-memory store — rendered into the tail. */
30
+ /** The working-memory store — rendered into a context record. */
65
31
  readonly workingMemory: WorkingMemoryStore;
66
- /** History-window token budget. Defaults to {@link DEFAULT_HISTORY_TOKEN_BUDGET}. */
67
- readonly tokenBudget?: number;
68
32
  /** Diagnostic seam for the history repair record; omitted falls back to no-op. */
69
33
  readonly logger?: Logger;
70
34
  }
71
35
  export interface AssembledMessages {
72
36
  /** The full message array for `runAgent`. */
73
37
  readonly messages: LoopMessage[];
74
- /**
75
- * The sanitized user input as its own message — the genuine turn start.
76
- * The route persists this plus the loop's output; the tail injections are
77
- * not persisted.
78
- */
38
+ /** The sanitized user input as its own message — the genuine turn start. */
79
39
  readonly userMessage: ModelMessage;
40
+ /** The context records after the user message. The turn stores them with the user message. */
41
+ readonly contextRecords: readonly ModelMessage[];
80
42
  }
81
43
  /**
82
- * Assemble the message array for one chat turn. Async — it reads the history
83
- * window and renders working memory.
44
+ * Assemble the message array for one chat turn. Async — it reads the view of
45
+ * the thread and renders working memory.
84
46
  */
85
47
  export declare function assembleMessages(args: AssembleMessagesArgs): Promise<AssembledMessages>;
48
+ /** The context records of one turn, in the order analysis context, run activity, working memory. */
49
+ export declare function contextRecordsFor(args: Pick<AssembleMessagesArgs, "threadType" | "analysisId" | "analysisContext" | "runActivityContext" | "workingMemory">, history: readonly ModelMessage[]): Promise<ModelMessage[]>;