@inflexa-ai/harness 0.39.2 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +104 -12
- package/dist/app/chat-turn.js +180 -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,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* route so callers other than the route (e.g. a CLI) can reuse it. It owns,
|
|
6
|
-
* in order: thread-ownership resolution, best-effort title seeding, analysis
|
|
7
|
-
* status load, and message assembly. It deliberately owns NONE of the
|
|
8
|
-
* transport orchestration (streaming/SSE/queue/status codes) — that stays in
|
|
9
|
-
* the caller. A turn is `prepareChatTurn → runAgent(own emit) → appendTurn`.
|
|
10
|
-
*
|
|
11
|
-
* Returns a typed result: `not_found` when the `threadId` is owned by a
|
|
12
|
-
* different analysis (indistinguishable from absent), otherwise `ok` with the
|
|
13
|
-
* assembled `messages` and the standalone `userMessage` the caller persists.
|
|
2
|
+
* The chat turn of a host: a turn is `runChatTurn`, which is `openChatTurn` and then the `run` of the
|
|
3
|
+
* open turn. `prepareChatTurn` is the first step of the open. The host gives only its transport
|
|
4
|
+
* values, and the harness stores the opening, each round, and the outcome.
|
|
14
5
|
*/
|
|
15
6
|
import type { Pool } from "pg";
|
|
7
|
+
import type { RequestSession } from "../auth/types.js";
|
|
8
|
+
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
9
|
+
import type { TokenUsageRollup } from "../contracts/usage.js";
|
|
10
|
+
import type { DbError } from "../lib/db-result.js";
|
|
11
|
+
import { type AgentFinish } from "../loop/run-agent.js";
|
|
12
|
+
import type { AgentDefinition, EmitFn } from "../loop/types.js";
|
|
16
13
|
import { type ThreadType } from "../memory/thread-store.js";
|
|
14
|
+
import type { AgentChat, PromptCachePolicy } from "../providers/types.js";
|
|
15
|
+
import type { ThreadAgentResolver } from "../runtime/assemble.js";
|
|
16
|
+
import type { AskApproval, AskRequest } from "../tools/approval/contract.js";
|
|
17
17
|
import { type AssembledMessages } from "./message-assembly.js";
|
|
18
18
|
import type { Logger } from "../lib/logger.js";
|
|
19
19
|
import { type ProvenanceSeam } from "../provenance/seam.js";
|
|
@@ -49,3 +49,95 @@ export type PrepareChatTurnResult = ({
|
|
|
49
49
|
* inside {@link assembleMessages} (applied once to the new user input only).
|
|
50
50
|
*/
|
|
51
51
|
export declare function prepareChatTurn(deps: PrepareChatTurnDeps, params: PrepareChatTurnParams): Promise<PrepareChatTurnResult>;
|
|
52
|
+
/** The root loop of a chat turn compacts its view past this estimate, in tokens. */
|
|
53
|
+
export declare const DEFAULT_CONVERSATION_BUDGET = 150000;
|
|
54
|
+
export interface RunChatTurnDeps extends PrepareChatTurnDeps {
|
|
55
|
+
/** The agent of a turn comes from the thread type, which only the preparation knows. */
|
|
56
|
+
readonly agents: ThreadAgentResolver;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The session of a turn without its provenance. The harness sets the provenance from the agent that
|
|
60
|
+
* it resolves, because only the preparation knows the thread type.
|
|
61
|
+
*/
|
|
62
|
+
export type ChatTurnSession = Omit<RequestSession, "provenance">;
|
|
63
|
+
/** What opens a turn: the thread and the user input. */
|
|
64
|
+
export interface OpenChatTurnParams {
|
|
65
|
+
readonly analysisId: string;
|
|
66
|
+
readonly threadId: string;
|
|
67
|
+
readonly userInput: string;
|
|
68
|
+
}
|
|
69
|
+
/** The transport values of a turn that is open. */
|
|
70
|
+
export interface RunOpenChatTurnParams {
|
|
71
|
+
readonly session: ChatTurnSession;
|
|
72
|
+
/** Makes the provider of the turn over the emit sink of the display recorder. */
|
|
73
|
+
readonly chat: (emit: EmitFn) => AgentChat;
|
|
74
|
+
readonly emit: EmitFn;
|
|
75
|
+
readonly signal: AbortSignal;
|
|
76
|
+
readonly usageRecorder: UsageRecorder;
|
|
77
|
+
/** The approval binding. It gets the emit sink of the display recorder, thus the display records each `data-ask`. */
|
|
78
|
+
readonly ask?: (request: AskRequest, emit: EmitFn) => Promise<AskApproval>;
|
|
79
|
+
/** Who sent the user message. */
|
|
80
|
+
readonly author?: string;
|
|
81
|
+
/** When the host opened the turn. Absent counts from the call to {@link openChatTurn}. */
|
|
82
|
+
readonly startedAtMs?: number;
|
|
83
|
+
/** The cache policy of the root loop. Absent gives {@link CONVERSATION_PROMPT_CACHE}. */
|
|
84
|
+
readonly promptCache?: PromptCachePolicy;
|
|
85
|
+
/** The budget of the view of the root loop. Absent gives {@link DEFAULT_CONVERSATION_BUDGET}. */
|
|
86
|
+
readonly conversationBudget?: number;
|
|
87
|
+
}
|
|
88
|
+
/** The values of one whole turn. */
|
|
89
|
+
export type RunChatTurnParams = OpenChatTurnParams & RunOpenChatTurnParams;
|
|
90
|
+
export type ChatTurnOutcome = {
|
|
91
|
+
readonly status: "done";
|
|
92
|
+
readonly finish: AgentFinish;
|
|
93
|
+
} | {
|
|
94
|
+
readonly status: "aborted";
|
|
95
|
+
readonly finish?: AgentFinish;
|
|
96
|
+
} | {
|
|
97
|
+
readonly status: "failed";
|
|
98
|
+
readonly reason: string;
|
|
99
|
+
readonly cause: unknown;
|
|
100
|
+
};
|
|
101
|
+
/** A turn that ended before its run. It wrote no row and no turn record. */
|
|
102
|
+
export type ChatTurnRefusal = {
|
|
103
|
+
readonly kind: "prepare_failed";
|
|
104
|
+
readonly cause: unknown;
|
|
105
|
+
} | {
|
|
106
|
+
readonly kind: "not_found";
|
|
107
|
+
} | {
|
|
108
|
+
readonly kind: "agent_unresolved";
|
|
109
|
+
readonly threadType: ThreadType;
|
|
110
|
+
};
|
|
111
|
+
export interface ChatTurnRan {
|
|
112
|
+
readonly kind: "ran";
|
|
113
|
+
readonly outcome: ChatTurnOutcome;
|
|
114
|
+
/** True when the opening of the turn landed. */
|
|
115
|
+
readonly opened: boolean;
|
|
116
|
+
/** The error of the last write, when rows of the turn or its close did not land. */
|
|
117
|
+
readonly storeError?: DbError;
|
|
118
|
+
readonly durationMs: number;
|
|
119
|
+
readonly turnUsage?: TokenUsageRollup;
|
|
120
|
+
/** The final text of a run that returned. */
|
|
121
|
+
readonly fallbackText?: string;
|
|
122
|
+
}
|
|
123
|
+
export type ChatTurnResult = ChatTurnRefusal | ChatTurnRan;
|
|
124
|
+
/**
|
|
125
|
+
* A turn that is prepared, with its agent resolved. Call `run` one time: each call stores a new
|
|
126
|
+
* opening of the same user message.
|
|
127
|
+
*/
|
|
128
|
+
export interface OpenChatTurn {
|
|
129
|
+
readonly kind: "ready";
|
|
130
|
+
/** The agent that runs the turn. The harness stamps its provenance on the session of the run. */
|
|
131
|
+
readonly agent: AgentDefinition;
|
|
132
|
+
readonly run: (params: RunOpenChatTurnParams) => Promise<ChatTurnRan>;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Open one chat turn: prepare it and resolve its agent. A refusal comes back before any row of the
|
|
136
|
+
* turn exists, thus a host can answer it before it starts a stream.
|
|
137
|
+
*/
|
|
138
|
+
export declare function openChatTurn(deps: RunChatTurnDeps, params: OpenChatTurnParams): Promise<OpenChatTurn | ChatTurnRefusal>;
|
|
139
|
+
/**
|
|
140
|
+
* Run one chat turn: open it, store its opening, run the root loop with a round sink that stores
|
|
141
|
+
* each round, and close the turn with its outcome.
|
|
142
|
+
*/
|
|
143
|
+
export declare function runChatTurn(deps: RunChatTurnDeps, params: RunChatTurnParams): Promise<ChatTurnResult>;
|
package/dist/app/chat-turn.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* route so callers other than the route (e.g. a CLI) can reuse it. It owns,
|
|
6
|
-
* in order: thread-ownership resolution, best-effort title seeding, analysis
|
|
7
|
-
* status load, and message assembly. It deliberately owns NONE of the
|
|
8
|
-
* transport orchestration (streaming/SSE/queue/status codes) — that stays in
|
|
9
|
-
* the caller. A turn is `prepareChatTurn → runAgent(own emit) → appendTurn`.
|
|
10
|
-
*
|
|
11
|
-
* Returns a typed result: `not_found` when the `threadId` is owned by a
|
|
12
|
-
* different analysis (indistinguishable from absent), otherwise `ok` with the
|
|
13
|
-
* assembled `messages` and the standalone `userMessage` the caller persists.
|
|
2
|
+
* The chat turn of a host: a turn is `runChatTurn`, which is `openChatTurn` and then the `run` of the
|
|
3
|
+
* open turn. `prepareChatTurn` is the first step of the open. The host gives only its transport
|
|
4
|
+
* values, and the harness stores the opening, each round, and the outcome.
|
|
14
5
|
*/
|
|
15
6
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
7
|
+
import { finalText, runAgent } from "../loop/run-agent.js";
|
|
8
|
+
import { passthroughStep } from "../loop/run-step.js";
|
|
9
|
+
import { compactionExchangeOf } from "../memory/ai-sdk-message-storage.js";
|
|
10
|
+
import { createConversationDisplayRecorder } from "../memory/conversation-display-recorder.js";
|
|
16
11
|
import { deriveThreadTitle } from "../memory/derive-thread-title.js";
|
|
17
|
-
import { createThreadHistory } from "../memory/thread-history.js";
|
|
12
|
+
import { conversationRecordTurn, createThreadHistory } from "../memory/thread-history.js";
|
|
18
13
|
import { createThreadStore } from "../memory/thread-store.js";
|
|
19
14
|
import { createWorkingMemory } from "../memory/working-memory.js";
|
|
15
|
+
import { findProviderError } from "../providers/errors.js";
|
|
16
|
+
import { MEMORY_COMPACTION_REQUEST, SUMMARY_COMPACTION_REQUEST } from "../prompts/compaction.js";
|
|
17
|
+
import { CONVERSATION_PROMPT_CACHE } from "../providers/prompt-cache.js";
|
|
20
18
|
import { loadAnalysisStatus, queryNonTerminalRunsByAnalysis } from "../state/index.js";
|
|
21
|
-
import {
|
|
19
|
+
import { createToolOutputStore } from "../state/tool-outputs.js";
|
|
20
|
+
import { suspensionOfFailure } from "../workflows/suspension.js";
|
|
21
|
+
import { assembleMessages, contextRecordsFor } from "./message-assembly.js";
|
|
22
22
|
import { renderRunActivity, renderRunActivityUnavailable, RUN_ACTIVITY_DETAIL_LIMIT } from "./run-activity.js";
|
|
23
23
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
24
24
|
import { bindSessionEmit } from "../provenance/seam.js";
|
|
@@ -42,7 +42,7 @@ export async function prepareChatTurn(deps, params) {
|
|
|
42
42
|
return { kind: "not_found" };
|
|
43
43
|
}
|
|
44
44
|
// The type a caller resolves the turn's agent from, and the type that the
|
|
45
|
-
// message assembly reads for its
|
|
45
|
+
// message assembly reads for its context records. An existing row carries
|
|
46
46
|
// its own. An absent one defaults to `conversation` — the store's own
|
|
47
47
|
// default — so a best-effort create that fails non-fatally still leaves a
|
|
48
48
|
// usable type; a successful create overrides it from the returned row.
|
|
@@ -79,19 +79,178 @@ export async function prepareChatTurn(deps, params) {
|
|
|
79
79
|
catch (err) {
|
|
80
80
|
logger.warn("title-seed failed (non-fatal)", logger.errorFields(err));
|
|
81
81
|
}
|
|
82
|
-
const analysisState = await loadAnalysisStatus(pool, analysisId).unwrapOr(null);
|
|
83
|
-
const runActivityContext = await queryNonTerminalRunsByAnalysis(pool, analysisId, RUN_ACTIVITY_DETAIL_LIMIT).match((activity) => renderRunActivity(activity), () => renderRunActivityUnavailable());
|
|
84
82
|
const history = createThreadHistory(pool);
|
|
85
|
-
const { messages, userMessage } = await assembleMessages({
|
|
83
|
+
const { messages, userMessage, contextRecords } = await assembleMessages({
|
|
86
84
|
threadId,
|
|
87
85
|
threadType,
|
|
88
86
|
analysisId,
|
|
89
87
|
userInput,
|
|
90
|
-
|
|
91
|
-
runActivityContext,
|
|
88
|
+
...(await readTurnContext(pool, analysisId)),
|
|
92
89
|
history,
|
|
93
90
|
workingMemory: createWorkingMemory(pool),
|
|
94
91
|
...(deps.logger ? { logger: deps.logger } : {}),
|
|
95
92
|
});
|
|
96
|
-
return { kind: "ok", threadType, messages, userMessage };
|
|
93
|
+
return { kind: "ok", threadType, messages, userMessage, contextRecords };
|
|
94
|
+
}
|
|
95
|
+
/** The analysis context and the run activity of a turn. A failed read gives the fallback of its kind. */
|
|
96
|
+
async function readTurnContext(pool, analysisId) {
|
|
97
|
+
const analysisState = await loadAnalysisStatus(pool, analysisId).unwrapOr(null);
|
|
98
|
+
const runActivityContext = await queryNonTerminalRunsByAnalysis(pool, analysisId, RUN_ACTIVITY_DETAIL_LIMIT).match((activity) => renderRunActivity(activity), () => renderRunActivityUnavailable());
|
|
99
|
+
return { analysisContext: analysisState?.context ?? null, runActivityContext };
|
|
100
|
+
}
|
|
101
|
+
/** The root loop of a chat turn compacts its view past this estimate, in tokens. */
|
|
102
|
+
export const DEFAULT_CONVERSATION_BUDGET = 150_000;
|
|
103
|
+
/**
|
|
104
|
+
* Open one chat turn: prepare it and resolve its agent. A refusal comes back before any row of the
|
|
105
|
+
* turn exists, thus a host can answer it before it starts a stream.
|
|
106
|
+
*/
|
|
107
|
+
export async function openChatTurn(deps, params) {
|
|
108
|
+
const openedAtMs = Date.now();
|
|
109
|
+
let prepared;
|
|
110
|
+
try {
|
|
111
|
+
prepared = await prepareChatTurn(deps, params);
|
|
112
|
+
}
|
|
113
|
+
catch (cause) {
|
|
114
|
+
return { kind: "prepare_failed", cause };
|
|
115
|
+
}
|
|
116
|
+
if (prepared.kind === "not_found")
|
|
117
|
+
return prepared;
|
|
118
|
+
const agent = deps.agents.forThread(prepared.threadType);
|
|
119
|
+
if (agent.isErr())
|
|
120
|
+
return { kind: "agent_unresolved", threadType: prepared.threadType };
|
|
121
|
+
const ready = prepared;
|
|
122
|
+
return {
|
|
123
|
+
kind: "ready",
|
|
124
|
+
agent: agent.value,
|
|
125
|
+
run: (run) => runOpenTurn(deps, params, ready, agent.value, { ...run, startedAtMs: run.startedAtMs ?? openedAtMs }),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Run one chat turn: open it, store its opening, run the root loop with a round sink that stores
|
|
130
|
+
* each round, and close the turn with its outcome.
|
|
131
|
+
*/
|
|
132
|
+
export async function runChatTurn(deps, params) {
|
|
133
|
+
const { analysisId, threadId, userInput, ...run } = params;
|
|
134
|
+
const opened = await openChatTurn(deps, { analysisId, threadId, userInput });
|
|
135
|
+
if (opened.kind !== "ready")
|
|
136
|
+
return opened;
|
|
137
|
+
return opened.run(run);
|
|
138
|
+
}
|
|
139
|
+
async function runOpenTurn(deps, turn, prepared, agent, params) {
|
|
140
|
+
const logger = (deps.logger ?? createNoopLogger()).named("harness.chat");
|
|
141
|
+
const session = { ...params.session, provenance: { agentId: agent.id, callPath: [agent.id] } };
|
|
142
|
+
const history = createThreadHistory(deps.pool, deps.logger);
|
|
143
|
+
const recorder = createConversationDisplayRecorder({
|
|
144
|
+
userText: userTextOf(prepared.userMessage),
|
|
145
|
+
topLevelCallPath: session.provenance.callPath,
|
|
146
|
+
sink: params.emit,
|
|
147
|
+
});
|
|
148
|
+
// The groups that did not land yet, in the order of the turn. A failed write keeps them for the next write.
|
|
149
|
+
let pending = [
|
|
150
|
+
{
|
|
151
|
+
modelMessages: [prepared.userMessage, ...prepared.contextRecords],
|
|
152
|
+
displayMessages: recorder.takeOpening(),
|
|
153
|
+
...(params.author === undefined ? {} : { author: params.author }),
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
let startSeq;
|
|
157
|
+
let storeError;
|
|
158
|
+
// A store fault never stops the turn: the error rides the result.
|
|
159
|
+
const flush = async (close) => {
|
|
160
|
+
if (pending.length === 0 && close === undefined)
|
|
161
|
+
return;
|
|
162
|
+
const closing = close === undefined ? {} : { close };
|
|
163
|
+
const write = startSeq === undefined ? { opening: pending[0], rounds: pending.slice(1), ...closing } : { startSeq, rounds: pending, ...closing };
|
|
164
|
+
(await history.writeTurn(turn.threadId, write)).match((written) => {
|
|
165
|
+
pending = [];
|
|
166
|
+
startSeq = written.startSeq;
|
|
167
|
+
storeError = undefined;
|
|
168
|
+
}, (error) => {
|
|
169
|
+
storeError = error;
|
|
170
|
+
logger.warn("chat turn write failed", { threadId: turn.threadId, op: error.op, ...logger.errorFields(error.cause) });
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
await flush();
|
|
174
|
+
const { ask } = params;
|
|
175
|
+
let outcome;
|
|
176
|
+
let fallbackText;
|
|
177
|
+
try {
|
|
178
|
+
const run = await runAgent(agent, prepared.messages, session, {
|
|
179
|
+
provider: params.chat(recorder.emit),
|
|
180
|
+
signal: params.signal,
|
|
181
|
+
emit: recorder.emit,
|
|
182
|
+
runStep: passthroughStep,
|
|
183
|
+
usageRecorder: params.usageRecorder,
|
|
184
|
+
toolOutputStore: createToolOutputStore(deps.pool),
|
|
185
|
+
promptCache: params.promptCache ?? CONVERSATION_PROMPT_CACHE,
|
|
186
|
+
compaction: compactionPolicy(deps, turn.analysisId, params, prepared.threadType, agent, recorder.emit),
|
|
187
|
+
...(deps.logger === undefined ? {} : { logger: deps.logger }),
|
|
188
|
+
...(ask === undefined ? {} : { ask: (request) => ask(request, recorder.emit) }),
|
|
189
|
+
onRound: async (round) => {
|
|
190
|
+
pending.push({ modelMessages: round.messages, displayMessages: recorder.takeRound(round.messages) });
|
|
191
|
+
await flush();
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
outcome = run.finish.reason === "aborted" ? { status: "aborted", finish: run.finish } : { status: "done", finish: run.finish };
|
|
195
|
+
// An abort can end the run inside an exchange, and the text of an exchange is a summary, and no reply.
|
|
196
|
+
fallbackText = finalText(run.messages.filter((message) => compactionExchangeOf(message) === undefined));
|
|
197
|
+
}
|
|
198
|
+
catch (cause) {
|
|
199
|
+
// A provider failure that races an abort stays a failure, thus both facts must hold.
|
|
200
|
+
const aborted = params.signal.aborted && cause instanceof Error && cause.name === "AbortError";
|
|
201
|
+
outcome = aborted ? { status: "aborted" } : { status: "failed", reason: failureReasonOf(cause), cause };
|
|
202
|
+
}
|
|
203
|
+
const durationMs = Date.now() - params.startedAtMs;
|
|
204
|
+
const turnUsage = outcome.status === "failed" ? undefined : outcome.finish?.turnUsage;
|
|
205
|
+
await flush({
|
|
206
|
+
status: outcome.status,
|
|
207
|
+
...(outcome.status === "failed" ? { reason: outcome.reason, note: conversationRecordTurn(failureNote(outcome.reason)) } : {}),
|
|
208
|
+
...(turnUsage === undefined ? {} : { turnUsage }),
|
|
209
|
+
turnDurationMs: durationMs,
|
|
210
|
+
});
|
|
211
|
+
return {
|
|
212
|
+
kind: "ran",
|
|
213
|
+
outcome,
|
|
214
|
+
opened: startSeq !== undefined,
|
|
215
|
+
...(storeError === undefined ? {} : { storeError }),
|
|
216
|
+
durationMs,
|
|
217
|
+
...(turnUsage === undefined ? {} : { turnUsage: { ...turnUsage } }),
|
|
218
|
+
...(fallbackText === undefined ? {} : { fallbackText }),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/** The sanitized text of the user message, thus the display never holds a secret that the model does not see. */
|
|
222
|
+
function userTextOf(message) {
|
|
223
|
+
if (typeof message.content === "string")
|
|
224
|
+
return message.content;
|
|
225
|
+
return message.content.map((part) => (part.type === "text" ? part.text : "")).join("");
|
|
226
|
+
}
|
|
227
|
+
const WORKING_MEMORY_TOOL_ID = "update_working_memory";
|
|
228
|
+
function compactionPolicy(deps, analysisId, params, threadType, agent, emit) {
|
|
229
|
+
// A report thread reads a frozen copy of the working memory, thus its agent declares no memory tool.
|
|
230
|
+
const remembers = agent.tools.some((tool) => tool.id === WORKING_MEMORY_TOOL_ID);
|
|
231
|
+
const workingMemory = createWorkingMemory(deps.pool);
|
|
232
|
+
return {
|
|
233
|
+
budget: params.conversationBudget ?? DEFAULT_CONVERSATION_BUDGET,
|
|
234
|
+
// A streaming provider sends each text delta with no source, thus a delta of the summary would show as a reply.
|
|
235
|
+
provider: params.chat((event) => (event.type === "text-delta" ? undefined : emit(event))),
|
|
236
|
+
request: remembers ? MEMORY_COMPACTION_REQUEST : SUMMARY_COMPACTION_REQUEST,
|
|
237
|
+
mask: remembers ? { allow: [WORKING_MEMORY_TOOL_ID] } : "none",
|
|
238
|
+
keepFirstTurn: threadType === "report",
|
|
239
|
+
recordsAfter: async (view) => contextRecordsFor({ threadType, analysisId, workingMemory, ...(await readTurnContext(deps.pool, analysisId)) }, view),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/** The reason of a failed turn. It never holds the error message, because each later turn sends the note to the vendor. */
|
|
243
|
+
function failureReasonOf(err) {
|
|
244
|
+
const suspension = suspensionOfFailure(err);
|
|
245
|
+
if (suspension !== undefined)
|
|
246
|
+
return suspension.reason;
|
|
247
|
+
const providerError = findProviderError(err);
|
|
248
|
+
if (providerError?.type === "auth")
|
|
249
|
+
return "The model endpoint refused the credential.";
|
|
250
|
+
if (providerError !== undefined)
|
|
251
|
+
return "The model request failed.";
|
|
252
|
+
return "The turn stopped on an internal error.";
|
|
253
|
+
}
|
|
254
|
+
function failureNote(reason) {
|
|
255
|
+
return ["[Turn Failed]", `The turn stopped before it finished. Reason: ${reason}`, "The rounds above this note ran, and their results are stored."].join("\n");
|
|
97
256
|
}
|