@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.
- package/README.md +2 -3
- package/dist/agents/conversation-agent.d.ts +3 -0
- package/dist/agents/conversation-agent.js +13 -2
- package/dist/agents/report-session-agent.d.ts +3 -0
- package/dist/agents/report-session-agent.js +2 -0
- package/dist/agents/sandbox/cheminformatics-agent.js +0 -3
- package/dist/agents/sandbox/shared.d.ts +11 -2
- package/dist/agents/sandbox/shared.js +4 -0
- package/dist/app/chat-turn.d.ts +75 -12
- package/dist/app/chat-turn.js +149 -21
- package/dist/app/message-assembly.d.ts +16 -52
- package/dist/app/message-assembly.js +41 -68
- package/dist/app/run-activity.d.ts +4 -6
- package/dist/app/run-activity.js +7 -27
- package/dist/app/spawn-report-session.d.ts +6 -11
- package/dist/app/spawn-report-session.js +5 -6
- package/dist/app/synthesize-run.d.ts +3 -0
- package/dist/app/synthesize-run.js +1 -0
- package/dist/billing/usage-recorder.d.ts +3 -4
- package/dist/billing/usage-recorder.js +3 -4
- package/dist/contracts/chat-parts.d.ts +11 -1
- package/dist/contracts/chat-parts.js +1 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/part-registry.js +1 -0
- package/dist/contracts/report-blocks.d.ts +8 -8
- package/dist/contracts/schemas/chat-parts.d.ts +32 -9
- package/dist/contracts/schemas/chat-parts.js +10 -0
- package/dist/execution/artifact-metadata.d.ts +26 -33
- package/dist/execution/artifact-metadata.js +44 -106
- package/dist/execution/post-step-pipeline.d.ts +23 -16
- package/dist/execution/post-step-pipeline.js +19 -21
- package/dist/execution/run-synthesis.d.ts +3 -0
- package/dist/execution/run-synthesis.js +11 -1
- package/dist/execution/step-summary.d.ts +21 -30
- package/dist/execution/step-summary.js +30 -57
- package/dist/index.d.ts +9 -8
- package/dist/index.js +15 -34
- package/dist/lib/zod-issues.js +2 -4
- package/dist/loop/compaction.d.ts +20 -0
- package/dist/loop/compaction.js +2 -0
- package/dist/loop/continue-agent.d.ts +40 -0
- package/dist/loop/continue-agent.js +38 -0
- package/dist/loop/metrics.d.ts +30 -22
- package/dist/loop/metrics.js +51 -22
- package/dist/loop/run-agent.d.ts +111 -13
- package/dist/loop/run-agent.js +467 -220
- package/dist/loop/run-to-terminal.d.ts +16 -17
- package/dist/loop/run-to-terminal.js +24 -35
- package/dist/loop/tool-mask.d.ts +23 -0
- package/dist/loop/tool-mask.js +46 -0
- package/dist/loop/tool-output.d.ts +48 -0
- package/dist/loop/tool-output.js +79 -0
- package/dist/loop/types.d.ts +6 -3
- package/dist/memory/ai-sdk-message-storage.d.ts +49 -2
- package/dist/memory/ai-sdk-message-storage.js +78 -2
- package/dist/memory/conversation-display-recorder.d.ts +6 -0
- package/dist/memory/conversation-display-recorder.js +64 -22
- package/dist/memory/conversation-display-replay.d.ts +5 -14
- package/dist/memory/conversation-display-replay.js +66 -32
- package/dist/memory/conversation-display-storage.d.ts +2 -1
- package/dist/memory/conversation-display-storage.js +2 -1
- package/dist/memory/conversation-view.d.ts +53 -0
- package/dist/memory/conversation-view.js +130 -0
- package/dist/memory/count-tokens.d.ts +5 -7
- package/dist/memory/count-tokens.js +7 -9
- package/dist/memory/thread-history.d.ts +74 -102
- package/dist/memory/thread-history.js +181 -246
- package/dist/memory/thread-store.d.ts +3 -3
- package/dist/memory/thread-store.js +7 -2
- package/dist/memory/tool-call-integrity.d.ts +15 -21
- package/dist/memory/tool-call-integrity.js +31 -38
- package/dist/memory/working-memory.d.ts +2 -5
- package/dist/memory/working-memory.js +2 -5
- package/dist/prompts/SOUL.d.ts +2 -2
- package/dist/prompts/SOUL.js +2 -7
- package/dist/prompts/analogical-reasoner.d.ts +1 -1
- package/dist/prompts/analogical-reasoner.js +16 -32
- package/dist/prompts/briefing.js +1 -1
- package/dist/prompts/compaction.d.ts +4 -0
- package/dist/prompts/compaction.js +24 -0
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +26 -35
- package/dist/prompts/execute-analysis/step-summary.js +7 -5
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +29 -34
- package/dist/prompts/planner.js +15 -29
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +2 -33
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +1 -2
- package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/cheminformatics-agent.js +10 -29
- package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
- package/dist/prompts/sandbox/chromatin-agent.js +1 -2
- package/dist/prompts/sandbox/data-profiler.js +6 -3
- package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
- package/dist/prompts/sandbox/dna-methylation-agent.js +2 -7
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +20 -24
- package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
- package/dist/prompts/sandbox/enrichment-agent.js +3 -2
- package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
- package/dist/prompts/sandbox/genomic-variant-agent.js +0 -2
- package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/immune-profiling-agent.js +7 -14
- package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/metabolomics-agent.js +0 -2
- package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
- package/dist/prompts/sandbox/microbiome-agent.js +14 -21
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -19
- package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multimodal-sc-agent.js +4 -5
- package/dist/prompts/sandbox/network-agent.d.ts +1 -1
- package/dist/prompts/sandbox/network-agent.js +7 -8
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +19 -19
- package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/proteomics-agent.js +3 -4
- package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
- package/dist/prompts/sandbox/scientific-executor.js +7 -10
- package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
- package/dist/prompts/sandbox/single-cell-agent.js +2 -4
- package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/spatial-omics-agent.js +3 -5
- package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/statistical-modeling-agent.js +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.js +10 -19
- package/dist/prompts/sandbox-standards.d.ts +7 -7
- package/dist/prompts/sandbox-standards.js +74 -76
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +16 -42
- package/dist/providers/ai-sdk.d.ts +39 -6
- package/dist/providers/ai-sdk.js +117 -18
- package/dist/providers/errors.d.ts +2 -0
- package/dist/providers/errors.js +5 -0
- package/dist/providers/prompt-cache.d.ts +43 -41
- package/dist/providers/prompt-cache.js +47 -41
- package/dist/providers/reasoning.d.ts +10 -21
- package/dist/providers/reasoning.js +10 -21
- package/dist/providers/types.d.ts +21 -5
- package/dist/report-model/authoring-grammar.d.ts +2 -2
- package/dist/runtime/assemble.d.ts +9 -7
- package/dist/runtime/assemble.js +7 -3
- package/dist/sandbox/create-sandbox.d.ts +2 -2
- package/dist/sandbox/submit-exec.d.ts +1 -1
- package/dist/schemas/artifact-manifest.d.ts +4 -4
- package/dist/state/init.js +39 -4
- package/dist/state/purge-analysis.js +4 -0
- package/dist/state/tool-outputs.d.ts +4 -0
- package/dist/state/tool-outputs.js +30 -0
- package/dist/tasks/data-profile.d.ts +3 -0
- package/dist/tasks/data-profile.js +2 -0
- package/dist/tools/ad-hoc-router.d.ts +8 -0
- package/dist/tools/ad-hoc-router.js +21 -2
- package/dist/tools/display/show-user.d.ts +1 -1
- package/dist/tools/execute-analysis.d.ts +3 -0
- package/dist/tools/execute-analysis.js +9 -1
- package/dist/tools/read-tool-output.d.ts +7 -0
- package/dist/tools/read-tool-output.js +130 -0
- package/dist/tools/research/generate-analogy-report.d.ts +10 -26
- package/dist/tools/research/generate-analogy-report.js +76 -163
- package/dist/tools/research/generate-plan.d.ts +3 -0
- package/dist/tools/research/generate-plan.js +4 -1
- package/dist/tools/research/literature-reviewer.d.ts +3 -0
- package/dist/tools/research/literature-reviewer.js +3 -0
- package/dist/tools/sandbox/report-blocker.d.ts +18 -5
- package/dist/tools/sandbox/report-blocker.js +54 -6
- package/dist/tools/sandbox/submit-file-metadata.d.ts +30 -0
- package/dist/tools/sandbox/submit-file-metadata.js +67 -0
- package/dist/tools/start-report-session.d.ts +1 -3
- package/dist/tools/start-report-session.js +0 -2
- package/dist/tools/workspace/execute-command.js +7 -4
- package/dist/tools/workspace/result-bounds.d.ts +4 -4
- package/dist/tools/workspace/result-bounds.js +6 -5
- package/dist/tools/workspace/workspace-search.d.ts +1 -1
- package/dist/workflows/execute-analysis.d.ts +3 -0
- package/dist/workflows/execute-analysis.js +1 -0
- package/dist/workflows/sandbox-step.d.ts +30 -9
- package/dist/workflows/sandbox-step.js +37 -8
- package/package.json +9 -9
- package/dist/providers/anthropic.d.ts +0 -25
- 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
|
-
|
|
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
|
|
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({
|
|
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
|
|
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,
|
package/dist/app/chat-turn.d.ts
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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>;
|
package/dist/app/chat-turn.js
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
28
|
+
/** The conversation message store — supplies the view of the thread. */
|
|
63
29
|
readonly history: ThreadHistory;
|
|
64
|
-
/** The working-memory store — rendered into
|
|
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
|
|
83
|
-
*
|
|
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[]>;
|