@inflexa-ai/harness 0.42.2 → 0.43.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/agents/sandbox/shared.d.ts +1 -6
- package/dist/agents/sandbox/shared.js +0 -15
- package/dist/app/chat-turn.js +13 -1
- package/dist/config/environment-stores.d.ts +4 -2
- package/dist/contracts/chat-events.d.ts +9 -1
- package/dist/contracts/chat-frame.d.ts +3 -3
- package/dist/contracts/chat-frame.js +8 -3
- package/dist/contracts/chat-parts.d.ts +4 -1
- package/dist/contracts/data-profile.d.ts +4 -6
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/report-blocks.d.ts +1364 -75
- package/dist/contracts/report-blocks.js +221 -29
- package/dist/contracts/report-reference.js +1 -1
- package/dist/contracts/schemas/chat-events.d.ts +13 -0
- package/dist/contracts/schemas/chat-events.js +5 -0
- package/dist/contracts/schemas/chat-parts.d.ts +2 -2
- package/dist/contracts/schemas/index.d.ts +1 -1
- package/dist/contracts/schemas/index.js +1 -1
- package/dist/execution/input-signature.d.ts +7 -13
- package/dist/execution/input-signature.js +0 -0
- package/dist/execution/run-canceler.d.ts +3 -2
- package/dist/execution/run-canceler.js +3 -2
- package/dist/execution/run-event-stream.d.ts +16 -2
- package/dist/execution/run-event-stream.js +54 -3
- package/dist/index.d.ts +1 -8
- package/dist/index.js +5 -16
- package/dist/input-scan/enrich.d.ts +0 -2
- package/dist/input-scan/enrich.js +0 -1
- package/dist/lib/metrics.js +6 -7
- package/dist/lib/otel-spans.d.ts +4 -9
- package/dist/lib/otel-spans.js +3 -8
- package/dist/loop/run-agent.d.ts +4 -2
- package/dist/loop/run-agent.js +38 -23
- package/dist/memory/thread-history.d.ts +1 -1
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +17 -0
- package/dist/provenance/exec-frame.d.ts +1 -1
- package/dist/report-model/authoring-grammar.d.ts +416 -17
- package/dist/report-model/authoring-grammar.js +25 -4
- package/dist/report-model/block-walk.d.ts +20 -4
- package/dist/report-model/block-walk.js +64 -7
- package/dist/report-model/draft-finish.d.ts +6 -2
- package/dist/report-model/draft-finish.js +6 -1
- package/dist/report-model/production-resolver.d.ts +8 -0
- package/dist/report-model/production-resolver.js +1 -1
- package/dist/report-model/validate.d.ts +20 -10
- package/dist/report-model/validate.js +29 -13
- package/dist/report-render/chart-export.d.ts +52 -0
- package/dist/report-render/chart-export.js +192 -0
- package/dist/report-render/chart-presets.d.ts +46 -47
- package/dist/report-render/chart-presets.js +31 -101
- package/dist/report-render/chart-renderers.d.ts +163 -0
- package/dist/report-render/chart-renderers.js +1055 -0
- package/dist/report-render/chart-toolbox.d.ts +124 -0
- package/dist/report-render/chart-toolbox.js +595 -0
- package/dist/report-render/chart.d.ts +60 -27
- package/dist/report-render/chart.js +2323 -524
- package/dist/report-render/design.d.ts +476 -16
- package/dist/report-render/design.js +390 -56
- package/dist/report-render/figures/common.d.ts +321 -0
- package/dist/report-render/figures/common.js +759 -0
- package/dist/report-render/figures/dendrogram.d.ts +36 -0
- package/dist/report-render/figures/dendrogram.js +130 -0
- package/dist/report-render/figures/dense.d.ts +111 -0
- package/dist/report-render/figures/dense.js +204 -0
- package/dist/report-render/figures/dotplot.d.ts +28 -0
- package/dist/report-render/figures/dotplot.js +234 -0
- package/dist/report-render/figures/embedding.d.ts +43 -0
- package/dist/report-render/figures/embedding.js +360 -0
- package/dist/report-render/figures/equal-units.d.ts +101 -0
- package/dist/report-render/figures/equal-units.js +215 -0
- package/dist/report-render/figures/forest.d.ts +20 -0
- package/dist/report-render/figures/forest.js +398 -0
- package/dist/report-render/figures/gsea.d.ts +30 -0
- package/dist/report-render/figures/gsea.js +391 -0
- package/dist/report-render/figures/heatmap.d.ts +40 -0
- package/dist/report-render/figures/heatmap.js +395 -0
- package/dist/report-render/figures/index.d.ts +135 -0
- package/dist/report-render/figures/index.js +100 -0
- package/dist/report-render/figures/km.d.ts +16 -0
- package/dist/report-render/figures/km.js +406 -0
- package/dist/report-render/figures/label-room.d.ts +130 -0
- package/dist/report-render/figures/label-room.js +341 -0
- package/dist/report-render/figures/locuszoom.d.ts +50 -0
- package/dist/report-render/figures/locuszoom.js +675 -0
- package/dist/report-render/figures/lollipop.d.ts +16 -0
- package/dist/report-render/figures/lollipop.js +247 -0
- package/dist/report-render/figures/ma.d.ts +20 -0
- package/dist/report-render/figures/ma.js +114 -0
- package/dist/report-render/figures/manhattan.d.ts +60 -0
- package/dist/report-render/figures/manhattan.js +343 -0
- package/dist/report-render/figures/oncoprint.d.ts +41 -0
- package/dist/report-render/figures/oncoprint.js +420 -0
- package/dist/report-render/figures/pca.d.ts +37 -0
- package/dist/report-render/figures/pca.js +241 -0
- package/dist/report-render/figures/qq.d.ts +30 -0
- package/dist/report-render/figures/qq.js +182 -0
- package/dist/report-render/figures/roc.d.ts +17 -0
- package/dist/report-render/figures/roc.js +116 -0
- package/dist/report-render/figures/sankey.d.ts +48 -0
- package/dist/report-render/figures/sankey.js +353 -0
- package/dist/report-render/figures/upset.d.ts +60 -0
- package/dist/report-render/figures/upset.js +316 -0
- package/dist/report-render/figures/volcano.d.ts +25 -0
- package/dist/report-render/figures/volcano.js +166 -0
- package/dist/report-render/hybrid-svg.d.ts +75 -0
- package/dist/report-render/hybrid-svg.js +504 -0
- package/dist/report-render/number-format.d.ts +19 -0
- package/dist/report-render/number-format.js +25 -1
- package/dist/report-render/page.d.ts +41 -4
- package/dist/report-render/page.js +247 -12
- package/dist/report-render/render.d.ts +13 -4
- package/dist/report-render/render.js +37 -6
- package/dist/report-render/types.d.ts +23 -2
- package/dist/report-render/value-bridge.d.ts +38 -3
- package/dist/report-render/value-bridge.js +137 -6
- package/dist/report-render/views/chart-view.d.ts +42 -6
- package/dist/report-render/views/chart-view.js +70 -9
- package/dist/report-render/views/page-view.js +9 -10
- package/dist/runtime/assemble.js +2 -2
- package/dist/sandbox/client.d.ts +23 -32
- package/dist/sandbox/client.js +3 -11
- package/dist/sandbox/create-sandbox.d.ts +9 -24
- package/dist/sandbox/create-sandbox.js +15 -30
- package/dist/sandbox/docker-client.d.ts +12 -22
- package/dist/sandbox/docker-client.js +29 -43
- package/dist/sandbox/exec.d.ts +58 -0
- package/dist/sandbox/exec.js +197 -0
- package/dist/sandbox/identity.d.ts +5 -5
- package/dist/sandbox/identity.js +7 -10
- package/dist/sandbox/k8s-client.d.ts +4 -10
- package/dist/sandbox/k8s-client.js +5 -12
- package/dist/sandbox/liveness.d.ts +5 -12
- package/dist/sandbox/liveness.js +5 -12
- package/dist/sandbox/notification-sweep.d.ts +4 -7
- package/dist/sandbox/notification-sweep.js +4 -7
- package/dist/sandbox/reaper.d.ts +3 -3
- package/dist/sandbox/reaper.js +3 -3
- package/dist/sandbox/sandbox-error.d.ts +10 -11
- package/dist/sandbox/sandbox-error.js +10 -11
- package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
- package/dist/sandbox/sandbox-step-translate.js +1 -1
- package/dist/sandbox/types.d.ts +31 -99
- package/dist/sandbox/types.js +17 -84
- package/dist/state/active-sandboxes.d.ts +7 -32
- package/dist/state/active-sandboxes.js +11 -46
- package/dist/state/index.d.ts +1 -2
- package/dist/state/index.js +1 -1
- package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.d.ts +7 -0
- package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.js +12 -0
- package/dist/state/migrations/index.js +2 -0
- package/dist/state/schema.d.ts +1 -5
- package/dist/state/schema.js +1 -6
- package/dist/state/step-executions.js +1 -2
- package/dist/tasks/data-profile.js +7 -20
- package/dist/tasks/derive-table-exec.d.ts +4 -14
- package/dist/tasks/derive-table-exec.js +9 -24
- package/dist/tasks/extract-values.d.ts +2 -2
- package/dist/tasks/extract-values.js +4 -7
- package/dist/tools/define-tool.d.ts +56 -13
- package/dist/tools/define-tool.js +28 -1
- package/dist/tools/execute-analysis.js +3 -3
- package/dist/tools/report-session/derive-table.d.ts +8 -9
- package/dist/tools/report-session/derive-table.js +3 -4
- package/dist/tools/report-session/preview-report.js +4 -49
- package/dist/tools/sandbox/list-available-packages.d.ts +1 -1
- package/dist/tools/sandbox/list-available-packages.js +5 -4
- package/dist/tools/workspace/edit-file.d.ts +5 -4
- package/dist/tools/workspace/edit-file.js +10 -13
- package/dist/tools/workspace/execute-command.d.ts +27 -42
- package/dist/tools/workspace/execute-command.js +42 -38
- package/dist/tools/workspace/index.d.ts +5 -6
- package/dist/tools/workspace/index.js +5 -6
- package/dist/tools/workspace/mutator.d.ts +43 -8
- package/dist/tools/workspace/mutator.js +45 -34
- package/dist/tools/workspace/run-exec.d.ts +3 -12
- package/dist/tools/workspace/run-exec.js +7 -24
- package/dist/tools/workspace/scan-inputs.d.ts +0 -3
- package/dist/tools/workspace/scan-inputs.js +0 -1
- package/dist/tools/workspace/write-file.d.ts +12 -3
- package/dist/tools/workspace/write-file.js +17 -13
- package/dist/workflows/sandbox-step.d.ts +7 -11
- package/dist/workflows/sandbox-step.js +24 -27
- package/package.json +5 -3
- package/dist/sandbox/await-exec.d.ts +0 -143
- package/dist/sandbox/await-exec.js +0 -417
- package/dist/sandbox/deliver-exec-event.d.ts +0 -25
- package/dist/sandbox/deliver-exec-event.js +0 -29
- package/dist/sandbox/digest.d.ts +0 -14
- package/dist/sandbox/digest.js +0 -17
- package/dist/sandbox/exec-id.d.ts +0 -12
- package/dist/sandbox/exec-id.js +0 -20
- package/dist/sandbox/hmac.d.ts +0 -62
- package/dist/sandbox/hmac.js +0 -72
- package/dist/sandbox/submit-exec.d.ts +0 -38
- package/dist/sandbox/submit-exec.js +0 -80
- package/dist/sandbox/watchdog.d.ts +0 -71
- package/dist/sandbox/watchdog.js +0 -161
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The container of one report-session derivation, as a registered DBOS workflow.
|
|
3
3
|
*
|
|
4
|
-
* The `derive_table` tool runs inside one live chat turn, and a chat turn is not a workflow
|
|
5
|
-
* of
|
|
4
|
+
* The `derive_table` tool runs inside one live chat turn, and a chat turn is not a workflow. An exec runs as
|
|
5
|
+
* a durable step of a workflow, and the reaper maps each container to the workflow that owns it. Thus the
|
|
6
6
|
* container lives here, behind a registered workflow, and the tool starts it and awaits the handle.
|
|
7
7
|
*
|
|
8
8
|
* The shape is the shape of `tasks/extract-values.ts`: a body that a test drives directly, a registration
|
|
9
9
|
* that closes over the construction-time deps, and a trigger that starts the workflow and gives the result
|
|
10
10
|
* back. The tool holds the trigger as a plain function, thus no module under `tools/` imports DBOS.
|
|
11
11
|
*
|
|
12
|
-
* The body owns the whole machine lifetime. It creates the container,
|
|
12
|
+
* The body owns the whole machine lifetime. It creates the container, runs one exec to its terminal
|
|
13
13
|
* result, and tears the container down on both paths. A teardown fault reaches the log alone, because the
|
|
14
14
|
* work is done by then.
|
|
15
|
-
*
|
|
16
|
-
* The exec id is `${workflowId}:${stepId}:${fnId}`, the one shape that `workflowIdFromExec` parses. A
|
|
17
|
-
* callback host reads the owner workflow out of the exec id alone, thus a flat id makes the completion
|
|
18
|
-
* callback unroutable and it lands as a `400`. The pull backstop still settles such an exec, but it pays
|
|
19
|
-
* the quiet interval first.
|
|
20
15
|
*/
|
|
21
16
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
22
17
|
import { err, ok } from "neverthrow";
|
|
@@ -26,7 +21,7 @@ import { mintSandboxIdentity } from "../sandbox/identity.js";
|
|
|
26
21
|
import { keepSuspendingRefusal } from "../sandbox/sandbox-error.js";
|
|
27
22
|
import { suspensionOfSpawnRefusal } from "../workflows/suspension.js";
|
|
28
23
|
import { buildDerivationExec, DERIVATION_DEADLINE_MS, DERIVATION_RESOURCES, DERIVE_RUN_LITERAL, DERIVE_STEP_LITERAL, } from "../tools/report-session/derive-table.js";
|
|
29
|
-
/** The
|
|
24
|
+
/** The exec progress callback. A derivation reports no live activity, thus the callback drops each event. */
|
|
30
25
|
const noopEmit = () => { };
|
|
31
26
|
/**
|
|
32
27
|
* The body, exported so a test drives it without a registered workflow.
|
|
@@ -39,9 +34,6 @@ const noopEmit = () => { };
|
|
|
39
34
|
export async function runDeriveTableExecBody(input, deps) {
|
|
40
35
|
const logger = (deps.logger ?? createNoopLogger()).named("derive-table-exec").with({ analysisId: input.analysisId });
|
|
41
36
|
const workflowId = DBOS.workflowID ?? deriveTableExecWorkflowId(input.executionId);
|
|
42
|
-
// One workflow submits one exec, thus the function id is a constant. The three segments are what a
|
|
43
|
-
// callback host parses to find the workflow that awaits this exec.
|
|
44
|
-
const execId = `${workflowId}:${DERIVE_STEP_LITERAL}:fn-0`;
|
|
45
37
|
const spawned = keepSuspendingRefusal(await deps.sandboxClient.createSandbox(forStep(input.runSession, DERIVE_STEP_LITERAL), { childWorkflowId: workflowId, resources: DERIVATION_RESOURCES, writableTail: input.writableTail }, mintSandboxIdentity(DERIVE_RUN_LITERAL)));
|
|
46
38
|
if (spawned.isErr()) {
|
|
47
39
|
const suspension = suspensionOfSpawnRefusal(spawned.error);
|
|
@@ -50,17 +42,15 @@ export async function runDeriveTableExecBody(input, deps) {
|
|
|
50
42
|
}
|
|
51
43
|
const sandbox = spawned.value;
|
|
52
44
|
try {
|
|
53
|
-
// A checkpointed clock, not `Date.now()`:
|
|
54
|
-
// wall-clock deadline that grew on replay would shift which loop iteration crosses it.
|
|
45
|
+
// A checkpointed clock, not `Date.now()`: a wall-clock deadline would grow on each recovery.
|
|
55
46
|
const deadline = (await (deps.now ?? (() => DBOS.now()))()) + DERIVATION_DEADLINE_MS;
|
|
56
|
-
|
|
47
|
+
const request = buildDerivationExec({
|
|
57
48
|
script: input.script,
|
|
58
|
-
execId,
|
|
59
49
|
workingDir: input.workingDir,
|
|
60
50
|
inputs: input.inputs,
|
|
61
51
|
output: input.output,
|
|
62
|
-
})
|
|
63
|
-
return ok(await deps.sandboxClient.
|
|
52
|
+
});
|
|
53
|
+
return ok(await deps.sandboxClient.exec(sandbox, request, noopEmit, deadline));
|
|
64
54
|
}
|
|
65
55
|
finally {
|
|
66
56
|
try {
|
|
@@ -78,12 +68,7 @@ export async function runDeriveTableExecBody(input, deps) {
|
|
|
78
68
|
export function registerDeriveTableExecWorkflow(deps) {
|
|
79
69
|
return DBOS.registerWorkflow((input) => runDeriveTableExecBody(input, deps), { name: "derive-table-exec" });
|
|
80
70
|
}
|
|
81
|
-
/**
|
|
82
|
-
* The workflow id of one derivation.
|
|
83
|
-
*
|
|
84
|
-
* It carries exactly one colon, because `workflowIdFromExec` recovers the id by a strip of the last two
|
|
85
|
-
* colon-delimited segments of the exec id. The execution id is unique already, thus no nonce joins it.
|
|
86
|
-
*/
|
|
71
|
+
/** The workflow id of one derivation. The execution id is unique already, thus no nonce joins it. */
|
|
87
72
|
export function deriveTableExecWorkflowId(executionId) {
|
|
88
73
|
return `${DERIVE_RUN_LITERAL}:${executionId}`;
|
|
89
74
|
}
|
|
@@ -18,7 +18,7 @@ import type { Logger } from "../lib/logger.js";
|
|
|
18
18
|
import type { ExtractionArm, ExtractionArtifact, ExtractionRequest } from "../report-model/production-resolver.js";
|
|
19
19
|
import type { SandboxClient } from "../sandbox/client.js";
|
|
20
20
|
import { type Suspension } from "../workflows/suspension.js";
|
|
21
|
-
import type {
|
|
21
|
+
import type { ExecRequest, ExecResult } from "../sandbox/types.js";
|
|
22
22
|
import { type ExtractValuesResult } from "./extract-values-script.js";
|
|
23
23
|
export type { ExtractValuesResult } from "./extract-values-script.js";
|
|
24
24
|
/**
|
|
@@ -60,7 +60,7 @@ export interface ExtractValuesWorkflowInput {
|
|
|
60
60
|
*
|
|
61
61
|
* The function is pure, thus a test asserts the command shape without a sandbox.
|
|
62
62
|
*/
|
|
63
|
-
export declare function buildExtractionExec(analysisId: string, requests: readonly ExtractionRequest[]
|
|
63
|
+
export declare function buildExtractionExec(analysisId: string, requests: readonly ExtractionRequest[]): ExecRequest;
|
|
64
64
|
/**
|
|
65
65
|
* Parse the script output from the exec result. A synthetic failure, a timeout, and a non-zero exit each
|
|
66
66
|
* throw, because each means the script did not emit its map. A stdout that is not the map shape throws too.
|
|
@@ -35,7 +35,7 @@ const EXTRACTION_DEADLINE_MS = 300_000;
|
|
|
35
35
|
* carries no file size. A later change can size it from the batch.
|
|
36
36
|
*/
|
|
37
37
|
const EXTRACTION_RESOURCES = { cpu: 2, memoryGb: 8 };
|
|
38
|
-
/** The
|
|
38
|
+
/** The exec progress callback. The extraction pass reports no live activity, thus the callback drops each event. */
|
|
39
39
|
const noopEmit = () => { };
|
|
40
40
|
/**
|
|
41
41
|
* Build the sandbox exec for one extraction pass. The command runs the fixed script through `python3 -c`.
|
|
@@ -50,11 +50,10 @@ const noopEmit = () => { };
|
|
|
50
50
|
*
|
|
51
51
|
* The function is pure, thus a test asserts the command shape without a sandbox.
|
|
52
52
|
*/
|
|
53
|
-
export function buildExtractionExec(analysisId, requests
|
|
53
|
+
export function buildExtractionExec(analysisId, requests) {
|
|
54
54
|
const scriptRequests = requests.map((request) => ({ path: request.path, format: request.format, hash: request.hash }));
|
|
55
55
|
return {
|
|
56
56
|
command: ["python3", "-c", EXTRACTION_SCRIPT],
|
|
57
|
-
execId,
|
|
58
57
|
cwd: `/${analysisId}`,
|
|
59
58
|
env: { [EXTRACTION_INPUT_ENV]: JSON.stringify(scriptRequests) },
|
|
60
59
|
timeoutSeconds: Math.floor(EXTRACTION_DEADLINE_MS / 1000),
|
|
@@ -113,11 +112,9 @@ export async function runExtractValuesBody(input, deps) {
|
|
|
113
112
|
}
|
|
114
113
|
const sandbox = spawned.value;
|
|
115
114
|
try {
|
|
116
|
-
// A checkpointed clock, not `Date.now()`:
|
|
117
|
-
// wall-clock deadline that grew on replay would shift which loop iteration crosses it.
|
|
115
|
+
// A checkpointed clock, not `Date.now()`: a wall-clock deadline would grow on each recovery.
|
|
118
116
|
const deadlineAbs = (await DBOS.now()) + EXTRACTION_DEADLINE_MS;
|
|
119
|
-
await deps.sandboxClient.
|
|
120
|
-
const result = await deps.sandboxClient.awaitExec(sandbox, executionId, noopEmit, deadlineAbs);
|
|
117
|
+
const result = await deps.sandboxClient.exec(sandbox, buildExtractionExec(analysisId, requests), noopEmit, deadlineAbs);
|
|
121
118
|
const map = parseExtractionOutput(result);
|
|
122
119
|
await revoke("extract-values-completed");
|
|
123
120
|
return ok(map);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* captures its deps and calls `defineTool`.
|
|
9
9
|
*
|
|
10
10
|
* `ToolContext` carries only request-scoped values every tool may need
|
|
11
|
-
* (`invocationId`, `session`, `signal`, `emit`, `
|
|
11
|
+
* (`invocationId`, `session`, `signal`, `emit`, `ask`, `turnUsage`) — no pool, no sandbox, no
|
|
12
12
|
* logger. The error contract: an expected
|
|
13
13
|
* outcome ("not found", "no results") stays in the ok channel as a data
|
|
14
14
|
* variant (`ok({ found: false })`); an unexpected failure is an `err(ToolError)`
|
|
@@ -19,7 +19,7 @@ import type { Result } from "neverthrow";
|
|
|
19
19
|
import { z } from "zod";
|
|
20
20
|
import type { AgentSession } from "../auth/types.js";
|
|
21
21
|
import type { AgentRunUsage } from "../loop/metrics.js";
|
|
22
|
-
import type { EmitFn
|
|
22
|
+
import type { EmitFn } from "../loop/types.js";
|
|
23
23
|
import type { AskApproval, AskRequest } from "./approval/contract.js";
|
|
24
24
|
export type { EmitFn };
|
|
25
25
|
/**
|
|
@@ -35,7 +35,13 @@ export interface ToolError {
|
|
|
35
35
|
readonly retryable: boolean;
|
|
36
36
|
readonly cause?: unknown;
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* How the loop runs a tool call. `step` (the default) wraps the whole call in
|
|
40
|
+
* one durable step, thus a replay returns the cached result and never runs the
|
|
41
|
+
* body again. `inline` runs the call with no step, for a tool whose effects are
|
|
42
|
+
* not durable work.
|
|
43
|
+
*/
|
|
44
|
+
export type ToolExecutionMode = "step" | "inline";
|
|
39
45
|
/** Runtime guard — does a `Result`'s error value carry the `ToolError` shape? */
|
|
40
46
|
export declare function isToolError(value: unknown): value is ToolError;
|
|
41
47
|
/**
|
|
@@ -78,13 +84,37 @@ export declare function withToolResultImages<T extends object>(value: T, images:
|
|
|
78
84
|
* in the same shape.
|
|
79
85
|
*/
|
|
80
86
|
export declare function readToolResultImages(value: unknown): ToolResultImage[];
|
|
87
|
+
/**
|
|
88
|
+
* The reserved key that carries the call record on a tool ok value.
|
|
89
|
+
*
|
|
90
|
+
* A call record is what a call leaves for process-local state, for example the
|
|
91
|
+
* lineage collector of a sandbox step. A replay returns the cached result of a
|
|
92
|
+
* step-mode call and never runs its body, thus a side effect of the body on
|
|
93
|
+
* such state is lost on recovery. The record goes into the cached dispatch
|
|
94
|
+
* instead, and the loop hands it to `foldCallRecord` after the call settles, on
|
|
95
|
+
* the first run and on each replay alike.
|
|
96
|
+
*
|
|
97
|
+
* It is a symbol, thus `JSON.stringify` omits it and the model never reads it.
|
|
98
|
+
* The record itself must be plain JSON, because the step cache serializes it.
|
|
99
|
+
*/
|
|
100
|
+
export declare const toolCallRecordKey: unique symbol;
|
|
101
|
+
/** A tool ok value that carries a call record under the reserved key. */
|
|
102
|
+
export type WithToolCallRecord<T, R> = T & {
|
|
103
|
+
readonly [toolCallRecordKey]: R;
|
|
104
|
+
};
|
|
105
|
+
/** The type of the call record that a tool ok type carries. A variant with no record contributes nothing. */
|
|
106
|
+
export type ToolCallRecordOf<Output> = Output extends {
|
|
107
|
+
readonly [toolCallRecordKey]: infer R;
|
|
108
|
+
} ? R : never;
|
|
109
|
+
/** Attach a call record to a tool ok value. The value keeps its own fields. */
|
|
110
|
+
export declare function withToolCallRecord<T extends object, R>(value: T, record: R): WithToolCallRecord<T, R>;
|
|
111
|
+
/** Read the call record that a tool ok value carries, or `undefined` when it carries none. */
|
|
112
|
+
export declare function readToolCallRecord(value: unknown): unknown;
|
|
81
113
|
/**
|
|
82
114
|
* The request-scoped values passed to every tool's `execute`. No injected
|
|
83
115
|
* dependencies (see the harness-durable-runtime spec) — invocation identity,
|
|
84
|
-
* `session`, `signal`, `emit`, the `
|
|
85
|
-
*
|
|
86
|
-
* tool uses to wrap its own durable work, the `ask` user-approval seam, and the
|
|
87
|
-
* turn's usage accumulator a sub-agent-running tool hands to its child loop.
|
|
116
|
+
* `session`, `signal`, `emit`, the `ask` user-approval seam, and the turn's
|
|
117
|
+
* usage accumulator a sub-agent-running tool hands to its child loop.
|
|
88
118
|
*/
|
|
89
119
|
export interface ToolContext {
|
|
90
120
|
/** Stable identity of this AI SDK tool call. Redelivery preserves it; a new
|
|
@@ -93,11 +123,6 @@ export interface ToolContext {
|
|
|
93
123
|
readonly session: AgentSession;
|
|
94
124
|
readonly signal: AbortSignal;
|
|
95
125
|
readonly emit: EmitFn;
|
|
96
|
-
/**
|
|
97
|
-
* Wrap durable work in a replay-cached step. The loop namespaces the name
|
|
98
|
-
* under the tool's own step name, so a tool just passes a short local label.
|
|
99
|
-
*/
|
|
100
|
-
readonly runStep: RunStep;
|
|
101
126
|
/**
|
|
102
127
|
* Pause for an explicit user decision on a concrete action. Resolves with the
|
|
103
128
|
* approval (`once`/`always`) or throws `AskRejectedError` on denial. Resolves
|
|
@@ -117,7 +142,7 @@ export interface ToolContext {
|
|
|
117
142
|
}
|
|
118
143
|
/**
|
|
119
144
|
* A packaged tool: identity, the Zod input contract, the emitted AI SDK input
|
|
120
|
-
* schema, execution mode, the optional
|
|
145
|
+
* schema, execution mode, the optional hooks, and the executor.
|
|
121
146
|
*/
|
|
122
147
|
export interface Tool<Input = unknown, Output = unknown> {
|
|
123
148
|
readonly id: string;
|
|
@@ -129,6 +154,11 @@ export interface Tool<Input = unknown, Output = unknown> {
|
|
|
129
154
|
describeCall?(input: Input): string;
|
|
130
155
|
/** See {@link ToolDefinition.describeResult}. Absent when the tool declares none. */
|
|
131
156
|
describeResult?(input: Input, result: Output): string;
|
|
157
|
+
/**
|
|
158
|
+
* See {@link ToolDefinition.foldCallRecord}. The record arrives untyped: the
|
|
159
|
+
* loop passes back only what the `execute` of the same tool attached.
|
|
160
|
+
*/
|
|
161
|
+
foldCallRecord?(record: unknown): void;
|
|
132
162
|
execute(input: Input, ctx: ToolContext): Promise<Result<Output, ToolError>>;
|
|
133
163
|
}
|
|
134
164
|
export interface ToolDefinition<Schema extends z.ZodType, Output> {
|
|
@@ -199,6 +229,19 @@ export interface ToolDefinition<Schema extends z.ZodType, Output> {
|
|
|
199
229
|
* The hook never reaches the model.
|
|
200
230
|
*/
|
|
201
231
|
readonly describeResult?: (input: z.infer<Schema>, result: Output) => string;
|
|
232
|
+
/**
|
|
233
|
+
* Fold the call record of one settled call into process-local state. The
|
|
234
|
+
* record is the one that `execute` attached with `withToolCallRecord`.
|
|
235
|
+
*
|
|
236
|
+
* The loop runs the hook after the round settles, in the order of the tool
|
|
237
|
+
* calls, on the first run and on each replay alike. Thus state that a
|
|
238
|
+
* replay rebuilds from scratch sees the same records in the same order. A
|
|
239
|
+
* call whose ok value carries no record, and a failed call, never reach it.
|
|
240
|
+
*
|
|
241
|
+
* The hook is synchronous. It must never fail a call: the call has settled,
|
|
242
|
+
* and its result may already be cached. The loop logs a throw and goes on.
|
|
243
|
+
*/
|
|
244
|
+
readonly foldCallRecord?: (record: ToolCallRecordOf<Output>) => void;
|
|
202
245
|
execute(input: z.infer<Schema>, ctx: ToolContext): Promise<Result<Output, ToolError>>;
|
|
203
246
|
}
|
|
204
247
|
/**
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* captures its deps and calls `defineTool`.
|
|
9
9
|
*
|
|
10
10
|
* `ToolContext` carries only request-scoped values every tool may need
|
|
11
|
-
* (`invocationId`, `session`, `signal`, `emit`, `
|
|
11
|
+
* (`invocationId`, `session`, `signal`, `emit`, `ask`, `turnUsage`) — no pool, no sandbox, no
|
|
12
12
|
* logger. The error contract: an expected
|
|
13
13
|
* outcome ("not found", "no results") stays in the ok channel as a data
|
|
14
14
|
* variant (`ok({ found: false })`); an unexpected failure is an `err(ToolError)`
|
|
@@ -62,6 +62,30 @@ export function readToolResultImages(value) {
|
|
|
62
62
|
const entries = Array.isArray(carried) ? carried : [carried];
|
|
63
63
|
return entries.filter(isToolResultImage);
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* The reserved key that carries the call record on a tool ok value.
|
|
67
|
+
*
|
|
68
|
+
* A call record is what a call leaves for process-local state, for example the
|
|
69
|
+
* lineage collector of a sandbox step. A replay returns the cached result of a
|
|
70
|
+
* step-mode call and never runs its body, thus a side effect of the body on
|
|
71
|
+
* such state is lost on recovery. The record goes into the cached dispatch
|
|
72
|
+
* instead, and the loop hands it to `foldCallRecord` after the call settles, on
|
|
73
|
+
* the first run and on each replay alike.
|
|
74
|
+
*
|
|
75
|
+
* It is a symbol, thus `JSON.stringify` omits it and the model never reads it.
|
|
76
|
+
* The record itself must be plain JSON, because the step cache serializes it.
|
|
77
|
+
*/
|
|
78
|
+
export const toolCallRecordKey = Symbol("toolCallRecord");
|
|
79
|
+
/** Attach a call record to a tool ok value. The value keeps its own fields. */
|
|
80
|
+
export function withToolCallRecord(value, record) {
|
|
81
|
+
return { ...value, [toolCallRecordKey]: record };
|
|
82
|
+
}
|
|
83
|
+
/** Read the call record that a tool ok value carries, or `undefined` when it carries none. */
|
|
84
|
+
export function readToolCallRecord(value) {
|
|
85
|
+
if (typeof value !== "object" || value === null)
|
|
86
|
+
return undefined;
|
|
87
|
+
return value[toolCallRecordKey];
|
|
88
|
+
}
|
|
65
89
|
/**
|
|
66
90
|
* Package a `Tool`. The emitted JSON Schema must have a top-level
|
|
67
91
|
* `"type": "object"` — model tool calling rejects anything else. A `z.discriminatedUnion`
|
|
@@ -95,6 +119,9 @@ export function defineTool(def) {
|
|
|
95
119
|
// declares no result hook carries no key, thus "has a hook" is one
|
|
96
120
|
// property check on either side.
|
|
97
121
|
...(typeof def.describeResult === "function" ? { describeResult: def.describeResult } : {}),
|
|
122
|
+
// The loop passes back only a record that this tool's `execute`
|
|
123
|
+
// attached, thus the record has the declared type.
|
|
124
|
+
...(typeof def.foldCallRecord === "function" ? { foldCallRecord: def.foldCallRecord } : {}),
|
|
98
125
|
execute: def.execute,
|
|
99
126
|
};
|
|
100
127
|
}
|
|
@@ -274,7 +274,7 @@ export function buildAdHocPlan(request, route, createdAt = new Date().toISOStrin
|
|
|
274
274
|
* would pay a file read on every ad hoc launch to learn nothing — while the
|
|
275
275
|
* router, with no resolution, already keeps every entry that parses.
|
|
276
276
|
*/
|
|
277
|
-
function adHocPackageResolver(deps) {
|
|
277
|
+
function adHocPackageResolver(deps, analysisId) {
|
|
278
278
|
if (!deps.farmLockFile && !deps.imagePackagesFile && !deps.readPoolInventory)
|
|
279
279
|
return undefined;
|
|
280
280
|
const inventory = {
|
|
@@ -283,7 +283,7 @@ function adHocPackageResolver(deps) {
|
|
|
283
283
|
...(deps.readPoolInventory === undefined ? {} : { readPoolInventory: deps.readPoolInventory }),
|
|
284
284
|
};
|
|
285
285
|
return async (names) => {
|
|
286
|
-
const read = await readInventorySections(inventory);
|
|
286
|
+
const read = await readInventorySections(inventory, analysisId);
|
|
287
287
|
// An inventory that cannot answer must not drop a name: the router
|
|
288
288
|
// keeps every parsed entry, and the link pass judges at launch.
|
|
289
289
|
if (read.kind === "unavailable")
|
|
@@ -296,7 +296,7 @@ async function persistedAdHocPlan(deps, args) {
|
|
|
296
296
|
const existing = unwrapOrThrow(await loadPlan(deps.pool, args.planId, { analysisId: args.analysisId }));
|
|
297
297
|
if (existing)
|
|
298
298
|
return validateStoredPlan(existing, args.planId);
|
|
299
|
-
const resolvePackages = adHocPackageResolver(deps);
|
|
299
|
+
const resolvePackages = adHocPackageResolver(deps, args.analysisId);
|
|
300
300
|
const route = await routeAdHocRequest({
|
|
301
301
|
provider: deps.utilityProvider,
|
|
302
302
|
model: deps.utilityModel,
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
* condition one time, up front, the same discipline as the eyes. A per-attempt failure would instead read as
|
|
30
30
|
* a transient fault, and it would invite a repeat of a call that can never pass.
|
|
31
31
|
*
|
|
32
|
-
* The container runs behind a seam, and never in the turn. An
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* The container runs behind a seam, and never in the turn. An exec is a durable step of a workflow, and a
|
|
33
|
+
* report turn is not a workflow. Thus the tool takes an injected runner, and a registered workflow owns the
|
|
34
|
+
* container. The tool holds no sandbox client, and it imports no DBOS.
|
|
35
35
|
*/
|
|
36
36
|
import { type Result } from "neverthrow";
|
|
37
37
|
import { z } from "zod";
|
|
@@ -40,7 +40,7 @@ import type { Suspension } from "../../workflows/suspension.js";
|
|
|
40
40
|
import type { RunAuthorizer } from "../../execution/run-authorizer.js";
|
|
41
41
|
import { type Logger } from "../../lib/logger.js";
|
|
42
42
|
import type { ResourceSpec } from "../../config/resource-limits.js";
|
|
43
|
-
import type {
|
|
43
|
+
import type { ExecRequest, ExecResult } from "../../sandbox/types.js";
|
|
44
44
|
import type { DerivationSource, ReportSessionStateStore } from "../../state/report-session-state.js";
|
|
45
45
|
import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
|
|
46
46
|
import { type Tool } from "../define-tool.js";
|
|
@@ -156,11 +156,10 @@ export interface DerivationInputMount {
|
|
|
156
156
|
*/
|
|
157
157
|
export declare function buildDerivationExec(args: {
|
|
158
158
|
readonly script: string;
|
|
159
|
-
readonly execId: string;
|
|
160
159
|
readonly workingDir: string;
|
|
161
160
|
readonly inputs: readonly DerivationInputMount[];
|
|
162
161
|
readonly output: string;
|
|
163
|
-
}):
|
|
162
|
+
}): ExecRequest;
|
|
164
163
|
/**
|
|
165
164
|
* Give the failure of an exec result, or `undefined` when the script ran to a clean exit.
|
|
166
165
|
*
|
|
@@ -190,9 +189,9 @@ export interface DeriveTableExecInput {
|
|
|
190
189
|
/**
|
|
191
190
|
* Run one derivation exec and give the terminal result back.
|
|
192
191
|
*
|
|
193
|
-
* The composition realizes it over a registered workflow, thus the container
|
|
194
|
-
*
|
|
195
|
-
*
|
|
192
|
+
* The composition realizes it over a registered workflow, thus the container and its exec live inside a
|
|
193
|
+
* workflow. A fault of the sandbox rejects the promise, and the tool turns that rejection into one short
|
|
194
|
+
* detail.
|
|
196
195
|
*/
|
|
197
196
|
export type DeriveTableRunner = (input: DeriveTableExecInput) => Promise<Result<ExecResult, Suspension>>;
|
|
198
197
|
/**
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
* condition one time, up front, the same discipline as the eyes. A per-attempt failure would instead read as
|
|
30
30
|
* a transient fault, and it would invite a repeat of a call that can never pass.
|
|
31
31
|
*
|
|
32
|
-
* The container runs behind a seam, and never in the turn. An
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* The container runs behind a seam, and never in the turn. An exec is a durable step of a workflow, and a
|
|
33
|
+
* report turn is not a workflow. Thus the tool takes an injected runner, and a registered workflow owns the
|
|
34
|
+
* container. The tool holds no sandbox client, and it imports no DBOS.
|
|
35
35
|
*/
|
|
36
36
|
import { ok } from "neverthrow";
|
|
37
37
|
import { unlink } from "node:fs/promises";
|
|
@@ -90,7 +90,6 @@ const NO_SANDBOX_DETAIL = "the composition gives no sandbox, thus this session c
|
|
|
90
90
|
export function buildDerivationExec(args) {
|
|
91
91
|
return {
|
|
92
92
|
command: ["python3", "-c", args.script],
|
|
93
|
-
execId: args.execId,
|
|
94
93
|
cwd: args.workingDir,
|
|
95
94
|
env: {
|
|
96
95
|
[DERIVE_INPUT_ENV]: JSON.stringify(args.inputs.map((input) => ({ path: input.path, hash: input.hash }))),
|
|
@@ -52,7 +52,7 @@ import { referencedPaths, walkBlocks } from "../../report-model/block-walk.js";
|
|
|
52
52
|
import { computeDraftHash } from "../../report-model/draft-hash.js";
|
|
53
53
|
import { finishDraft } from "../../report-model/draft-finish.js";
|
|
54
54
|
import { resolveDocumentReferences } from "../../report-model/validate.js";
|
|
55
|
-
import { bridgeValues } from "../../report-render/value-bridge.js";
|
|
55
|
+
import { bridgeValues, collectResolutions } from "../../report-render/value-bridge.js";
|
|
56
56
|
import { resolvePageAssetFromInstallation } from "../../report-render/asset-lookup.js";
|
|
57
57
|
import { ASSETS_DIR, DEPS_DIR, derivationScriptName, PAGE_ASSETS, stagedSource, tableSidecarName } from "../../report-render/assets.js";
|
|
58
58
|
import { renderReportPage } from "../../report-render/render.js";
|
|
@@ -77,51 +77,6 @@ function assetFileName(file) {
|
|
|
77
77
|
function figureSourcePolicy(file) {
|
|
78
78
|
return `assets/${assetFileName(file)}`;
|
|
79
79
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Pair each block with the resolved value of its binding, in document order.
|
|
82
|
-
*
|
|
83
|
-
* A value-bearing kind carries the resolved value of its one reference, looked up by the block id from
|
|
84
|
-
* the map that the resolution pass filled. A no-value kind carries none. Where a reference sits in a
|
|
85
|
-
* block is the knowledge of `block-walk.ts`, thus this walk reads the block id alone and never a binding
|
|
86
|
-
* field.
|
|
87
|
-
*
|
|
88
|
-
* The switch is exhaustive over the eight block kinds. A ninth kind reaches the end with no return, and
|
|
89
|
-
* the declared return type fails the build. Thus the walk cannot drop a kind in silence. A value-bearing
|
|
90
|
-
* block reaches here only when its reference resolved, because an unresolved reference short-circuits
|
|
91
|
-
* before this walk.
|
|
92
|
-
*/
|
|
93
|
-
function collectResolutions(blocks, resolvedByBlock) {
|
|
94
|
-
const resolutions = [];
|
|
95
|
-
const visit = (block) => {
|
|
96
|
-
switch (block.kind) {
|
|
97
|
-
case "section":
|
|
98
|
-
resolutions.push({ blockId: block.id, kind: "section" });
|
|
99
|
-
for (const child of block.blocks) {
|
|
100
|
-
visit(child);
|
|
101
|
-
}
|
|
102
|
-
return;
|
|
103
|
-
case "text":
|
|
104
|
-
case "claim":
|
|
105
|
-
case "citation":
|
|
106
|
-
resolutions.push({ blockId: block.id, kind: block.kind });
|
|
107
|
-
return;
|
|
108
|
-
case "metric":
|
|
109
|
-
case "table":
|
|
110
|
-
case "chart":
|
|
111
|
-
case "figure": {
|
|
112
|
-
const resolved = resolvedByBlock.get(block.id);
|
|
113
|
-
if (resolved !== undefined) {
|
|
114
|
-
resolutions.push({ blockId: block.id, kind: block.kind, resolved });
|
|
115
|
-
}
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
for (const block of blocks) {
|
|
121
|
-
visit(block);
|
|
122
|
-
}
|
|
123
|
-
return resolutions;
|
|
124
|
-
}
|
|
125
80
|
/**
|
|
126
81
|
* The sidecar of each table block of the document, in document order.
|
|
127
82
|
*
|
|
@@ -201,11 +156,11 @@ function scriptAssets(records) {
|
|
|
201
156
|
* references. On a clean pass the resolutions map onto the render model in document order.
|
|
202
157
|
*/
|
|
203
158
|
async function resolveDocument(document, resolver, snapshot) {
|
|
204
|
-
const { resolvedByBlock, failures } = await resolveDocumentReferences(document.sections, snapshot, resolver);
|
|
159
|
+
const { resolvedByBlock, resolvedBySlot, failures } = await resolveDocumentReferences(document.sections, snapshot, resolver);
|
|
205
160
|
if (failures.length > 0) {
|
|
206
161
|
return { resolutions: [], unresolved: failures };
|
|
207
162
|
}
|
|
208
|
-
return { resolutions: collectResolutions(document.sections, resolvedByBlock), unresolved: [] };
|
|
163
|
+
return { resolutions: collectResolutions(document.sections, resolvedByBlock, resolvedBySlot), unresolved: [] };
|
|
209
164
|
}
|
|
210
165
|
/**
|
|
211
166
|
* Resolve the page root, stage each bound image and each manifest entry, and write the page.
|
|
@@ -456,7 +411,7 @@ export function createPreviewReportTool(deps) {
|
|
|
456
411
|
// the renderer moves it into a data asset and no part of this path reads a field of it. Absence
|
|
457
412
|
// gives a page with no provenance asset, and each other part of the render is unchanged.
|
|
458
413
|
const provenance = await readProvenance(analysisId);
|
|
459
|
-
const rendered = renderReportPage(document, bridged.value, {
|
|
414
|
+
const rendered = await renderReportPage(document, bridged.value, {
|
|
460
415
|
records: snapshot.citationRecords,
|
|
461
416
|
derivations: used.map((record) => chainOf(record, sessionDir)),
|
|
462
417
|
provenance,
|
|
@@ -140,7 +140,7 @@ export type InventoryRead = {
|
|
|
140
140
|
* here, thus the census a model sees and the check a launch runs never
|
|
141
141
|
* disagree about what the source holds.
|
|
142
142
|
*/
|
|
143
|
-
export declare function readInventorySections(deps: ListAvailablePackagesDeps): Promise<InventoryRead>;
|
|
143
|
+
export declare function readInventorySections(deps: ListAvailablePackagesDeps, analysisId: string): Promise<InventoryRead>;
|
|
144
144
|
/**
|
|
145
145
|
* Create the package inventory over the bound source: the pool-scope reader
|
|
146
146
|
* when the embedder binds one (a conversation or planning surface), the
|
|
@@ -400,7 +400,7 @@ export function queryPackages(rawSections, { names, query, language, limit }) {
|
|
|
400
400
|
* here, thus the census a model sees and the check a launch runs never
|
|
401
401
|
* disagree about what the source holds.
|
|
402
402
|
*/
|
|
403
|
-
export async function readInventorySections(deps) {
|
|
403
|
+
export async function readInventorySections(deps, analysisId) {
|
|
404
404
|
// The image record merges in when the host can read a valid one. An absent
|
|
405
405
|
// record and an invalid record both merge nothing, and neither is an
|
|
406
406
|
// error: the record is an enrichment, and the farm or pool inventory stays
|
|
@@ -422,8 +422,9 @@ export async function readInventorySections(deps) {
|
|
|
422
422
|
}
|
|
423
423
|
// Both farm container paths are tried when the host injects none, because
|
|
424
424
|
// the path keys on the declared toolchain and this read carries none.
|
|
425
|
+
const farmLockFile = typeof deps.farmLockFile === "function" ? deps.farmLockFile(analysisId) : deps.farmLockFile;
|
|
425
426
|
let lock = null;
|
|
426
|
-
for (const candidate of
|
|
427
|
+
for (const candidate of farmLockFile ? [farmLockFile] : DEFAULT_FARM_LOCK_FILES) {
|
|
427
428
|
lock = readFarmLockFile(candidate).unwrapOr(null);
|
|
428
429
|
if (lock !== null)
|
|
429
430
|
break;
|
|
@@ -502,8 +503,8 @@ export function createListAvailablePackagesTool(deps = {}) {
|
|
|
502
503
|
return `${language} packages`;
|
|
503
504
|
return "full package list";
|
|
504
505
|
},
|
|
505
|
-
execute: async (input) => {
|
|
506
|
-
const read = await readInventorySections(deps);
|
|
506
|
+
execute: async (input, ctx) => {
|
|
507
|
+
const read = await readInventorySections(deps, ctx.session.scope.analysisId);
|
|
507
508
|
// An unreadable inventory is an expected environment state — model it as an
|
|
508
509
|
// `available: false` data variant telling the caller the set is UNKNOWN,
|
|
509
510
|
// WITH the reason: without it, a structural fault (a damaged dependency
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* (compiled with the `gm` flags — multiline anchors), `new_string` may use
|
|
12
12
|
* capture-group references, and exactly one of `replace_all=true` or
|
|
13
13
|
* `expected_matches` gates the write. Provenance hashes the post-edit
|
|
14
|
-
* content —
|
|
14
|
+
* content — the mutator records it through the fold of this tool.
|
|
15
15
|
*/
|
|
16
|
+
import { type WithToolCallRecord } from "../define-tool.js";
|
|
16
17
|
import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
|
|
17
|
-
import type { WorkspaceMutator, WriteFileResult } from "./mutator.js";
|
|
18
|
+
import type { WorkspaceMutator, WriteFileResult, WriteRecord } from "./mutator.js";
|
|
18
19
|
/** Outcome of an `edit_file` call — expected outcomes are data variants, never throws. */
|
|
19
20
|
export type EditFileResult = {
|
|
20
21
|
readonly status: "file_not_found";
|
|
@@ -43,14 +44,14 @@ export type EditFileResult = {
|
|
|
43
44
|
readonly lines: readonly number[];
|
|
44
45
|
} | Exclude<WriteFileResult, {
|
|
45
46
|
status: "ok";
|
|
46
|
-
}> | {
|
|
47
|
+
}> | WithToolCallRecord<{
|
|
47
48
|
readonly status: "ok";
|
|
48
49
|
readonly path: string;
|
|
49
50
|
readonly replacements: number;
|
|
50
51
|
readonly bytesWritten: number;
|
|
51
52
|
/** Regex mode only: 1-based line numbers where the matches started. */
|
|
52
53
|
readonly lines?: readonly number[];
|
|
53
|
-
}
|
|
54
|
+
}, WriteRecord>;
|
|
54
55
|
export interface EditFileDeps {
|
|
55
56
|
readonly mutator: WorkspaceMutator;
|
|
56
57
|
/** Read seam used to fetch current file content for search/replace. */
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
* (compiled with the `gm` flags — multiline anchors), `new_string` may use
|
|
12
12
|
* capture-group references, and exactly one of `replace_all=true` or
|
|
13
13
|
* `expected_matches` gates the write. Provenance hashes the post-edit
|
|
14
|
-
* content —
|
|
14
|
+
* content — the mutator records it through the fold of this tool.
|
|
15
15
|
*/
|
|
16
16
|
import { ok } from "neverthrow";
|
|
17
17
|
import { z } from "zod";
|
|
18
|
-
import { defineTool } from "../define-tool.js";
|
|
18
|
+
import { defineTool, withToolCallRecord } from "../define-tool.js";
|
|
19
19
|
import { unwrapOrThrow } from "../../lib/result.js";
|
|
20
20
|
const EditFileInputSchema = z.object({
|
|
21
21
|
path: z
|
|
@@ -108,9 +108,6 @@ function replaceRegex(content, pattern, replacement, expected) {
|
|
|
108
108
|
export function createEditFileTool(deps) {
|
|
109
109
|
return defineTool({
|
|
110
110
|
id: "edit_file",
|
|
111
|
-
// The mutator wraps the disk mutation in `ctx.runStep` itself, so the
|
|
112
|
-
// body runs unwrapped in the workflow body (see the harness-tools spec).
|
|
113
|
-
executionMode: "workflow",
|
|
114
111
|
description: "Edit a file in your working directory by replacing specific text. " +
|
|
115
112
|
"Read the file first to get the exact text. When replace_all is false " +
|
|
116
113
|
"(default), old_string must occur exactly once. For a bulk pattern " +
|
|
@@ -166,24 +163,24 @@ export function createEditFileTool(deps) {
|
|
|
166
163
|
lines: replaced.lines,
|
|
167
164
|
});
|
|
168
165
|
}
|
|
169
|
-
const
|
|
166
|
+
const write = await deps.mutator.writeFile({
|
|
170
167
|
path,
|
|
171
168
|
content: replaced.content,
|
|
172
169
|
toolName: "edit_file",
|
|
173
170
|
invocationId: ctx.invocationId,
|
|
174
|
-
runStep: ctx.runStep,
|
|
175
171
|
session: ctx.session,
|
|
176
172
|
});
|
|
177
|
-
if (
|
|
178
|
-
return ok(
|
|
173
|
+
if (write.status !== "ok")
|
|
174
|
+
return ok(write);
|
|
179
175
|
const lines = replaced.lines;
|
|
180
|
-
return ok({
|
|
176
|
+
return ok(withToolCallRecord({
|
|
181
177
|
status: "ok",
|
|
182
|
-
path:
|
|
178
|
+
path: write.path,
|
|
183
179
|
replacements: replaced.replacements,
|
|
184
|
-
bytesWritten:
|
|
180
|
+
bytesWritten: write.bytesWritten,
|
|
185
181
|
...(lines === undefined ? {} : { lines }),
|
|
186
|
-
});
|
|
182
|
+
}, write.record));
|
|
187
183
|
},
|
|
184
|
+
foldCallRecord: (record) => deps.mutator.recordWrite(record),
|
|
188
185
|
});
|
|
189
186
|
}
|