feat(coding-agent): added multi-cell Python execution with persistent kernel

- Added multi-cell Python execution with sequential processing in persistent kernel.
- Changed Python tool API to use cells array instead of single code parameter.
- Renamed workdir parameter to cwd across Bash and Python tools for consistency.
- Fixed indentation adjustment logic for mixed indentation levels in patch tool.
This commit is contained in:
can1357
2026-01-21 10:20:11 +01:00
parent 7f6d4ccf80
commit a5293b2fa6
15 changed files with 203 additions and 86 deletions
+14
View File
@@ -1,6 +1,20 @@
# Changelog
## [Unreleased]
### Added
- Added multi-cell Python execution with sequential processing in persistent kernel
- Added cell titles for better Python code organization and debugging
### Changed
- Changed Python tool to use `cells` array instead of single `code` parameter
- Renamed `workdir` parameter to `cwd` in Bash and Python tools for consistency
- Updated Python tool to display cell-by-cell output when multiple cells are provided
### Fixed
- Fixed indentation adjustment logic to handle edge cases with mixed indentation levels
## [6.9.0] - 2026-01-21
### Removed
+5 -7
View File
@@ -20,9 +20,7 @@ export const BASH_DEFAULT_PREVIEW_LINES = 10;
const bashSchema = Type.Object({
command: Type.String({ description: "Bash command to execute" }),
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
workdir: Type.Optional(
Type.String({ description: "Working directory for the command (default: current directory)" }),
),
cwd: Type.Optional(Type.String({ description: "Working directory for the command (default: current directory)" })),
});
export interface BashToolDetails {
@@ -53,7 +51,7 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
public async execute(
_toolCallId: string,
{ command, timeout, workdir }: { command: string; timeout?: number; workdir?: string },
{ command, timeout, cwd }: { command: string; timeout?: number; cwd?: string },
signal?: AbortSignal,
onUpdate?: AgentToolUpdateCallback<BashToolDetails>,
ctx?: AgentToolContext,
@@ -73,7 +71,7 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
}
}
const commandCwd = workdir ? resolveToCwd(workdir, this.session.cwd) : this.session.cwd;
const commandCwd = cwd ? resolveToCwd(cwd, this.session.cwd) : this.session.cwd;
let cwdStat: Awaited<ReturnType<Bun.BunFile["stat"]>>;
try {
cwdStat = await Bun.file(commandCwd).stat();
@@ -143,7 +141,7 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
interface BashRenderArgs {
command?: string;
timeout?: number;
workdir?: string;
cwd?: string;
}
interface BashRenderContext {
@@ -166,7 +164,7 @@ export const bashToolRenderer = {
const command = args.command || uiTheme.format.ellipsis;
const prompt = uiTheme.fg("accent", "$");
const cwd = process.cwd();
let displayWorkdir = args.workdir;
let displayWorkdir = args.cwd;
if (displayWorkdir) {
const resolvedCwd = resolve(cwd);
@@ -221,7 +221,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
});
} else if (!isTestEnv && getPreludeDocs().length === 0) {
const sessionFile = session.getSessionFile?.() ?? undefined;
const warmSessionId = sessionFile ? `session:${sessionFile}:workdir:${session.cwd}` : `cwd:${session.cwd}`;
const warmSessionId = sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`;
void warmPythonEnvironment(session.cwd, warmSessionId, session.settings?.getPythonSharedGateway?.()).catch(
(err) => {
logger.warn("Failed to warm Python environment", {
@@ -48,7 +48,7 @@ export { DEFAULT_FUZZY_THRESHOLD, findContextLine, findMatch as findEditMatch, f
// Normalization
export {
adjustIndentation as adjustNewTextIndentation,
adjustIndentation,
detectLineEnding,
normalizeToLF,
restoreLineEndings,
@@ -193,12 +193,21 @@ export function normalizeForFuzzy(line: string): string {
export function adjustIndentation(oldText: string, actualText: string, newText: string): string {
const oldMin = minIndent(oldText);
const actualMin = minIndent(actualText);
const newMin = minIndent(newText);
const delta = actualMin - oldMin;
if (delta === 0) {
return newText;
}
if (newMin === actualMin) {
return newText;
}
if (newMin !== oldMin) {
return newText;
}
const indentChar = detectIndentChar(actualText);
const lines = newText.split("\n");
+104 -42
View File
@@ -39,15 +39,26 @@ function groupPreludeHelpers(helpers: PreludeHelper[]): PreludeCategory[] {
}
export const pythonSchema = Type.Object({
code: Type.String({ description: "Python code to execute" }),
timeoutMs: Type.Optional(Type.Number({ description: "Timeout in milliseconds (default: 30000)" })),
workdir: Type.Optional(
Type.String({ description: "Working directory for the command (default: current directory)" }),
cells: Type.Array(
Type.Object({
code: Type.String({
description:
"Python code for this cell. Keep it focused (imports, helper, test, use). No narrative text—put explanations in the assistant message or in the cell title.",
}),
title: Type.Optional(
Type.String({ description: "Short label for the cell (e.g., 'imports', 'parse helper')." }),
),
}),
{
description:
"Python cells to execute sequentially. Each cell runs in the same kernel—imports and variables persist. Keep cells small: one logical step each (import, define, test, use). If a cell fails, fix only that cell; earlier cells' state remains.",
},
),
timeoutMs: Type.Optional(Type.Number({ description: "Timeout in milliseconds (default: 30000)" })),
cwd: Type.Optional(Type.String({ description: "Working directory for the command (default: current directory)" })),
reset: Type.Optional(Type.Boolean({ description: "Restart the kernel before executing this code" })),
});
export type PythonToolParams = { code: string; timeout?: number; workdir?: string; reset?: boolean };
export type PythonToolParams = Static<typeof pythonSchema>;
export type PythonToolResult = {
content: Array<{ type: "text"; text: string }>;
@@ -151,7 +162,7 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
throw new Error("Python tool requires a session when not using proxy executor");
}
const { code, timeoutMs = 30000, workdir, reset } = params;
const { cells, timeoutMs = 30000, cwd, reset } = params;
const controller = new AbortController();
const onAbort = () => controller.abort();
signal?.addEventListener("abort", onAbort, { once: true });
@@ -161,7 +172,7 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
throw new Error("Aborted");
}
const commandCwd = workdir ? resolveToCwd(workdir, this.session.cwd) : this.session.cwd;
const commandCwd = cwd ? resolveToCwd(cwd, this.session.cwd) : this.session.cwd;
let cwdStat: Awaited<ReturnType<Bun.BunFile["stat"]>>;
try {
cwdStat = await Bun.file(commandCwd).stat();
@@ -179,15 +190,14 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
const images: ImageContent[] = [];
const sessionFile = this.session.getSessionFile?.() ?? undefined;
const sessionId = sessionFile ? `session:${sessionFile}:workdir:${commandCwd}` : `cwd:${commandCwd}`;
const executorOptions: PythonExecutorOptions = {
const sessionId = sessionFile ? `session:${sessionFile}:cwd:${commandCwd}` : `cwd:${commandCwd}`;
const baseExecutorOptions: Omit<PythonExecutorOptions, "reset"> = {
cwd: commandCwd,
timeoutMs,
signal: controller.signal,
sessionId,
kernelMode: this.session.settings?.getPythonKernelMode?.() ?? "session",
useSharedGateway: this.session.settings?.getPythonSharedGateway?.() ?? true,
reset,
onChunk: (chunk) => {
const chunkBytes = Buffer.byteLength(chunk, "utf-8");
tailChunks.push({ text: chunk, bytes: chunkBytes });
@@ -209,26 +219,65 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
},
};
const result = await executePython(code, executorOptions);
const statusEvents: PythonStatusEvent[] = [];
for (const output of result.displayOutputs) {
if (output.type === "json") {
jsonOutputs.push(output.data);
const cellOutputs: string[] = [];
let lastFullOutputPath: string | undefined;
for (let i = 0; i < cells.length; i++) {
const cell = cells[i];
const isFirstCell = i === 0;
const executorOptions: PythonExecutorOptions = {
...baseExecutorOptions,
reset: isFirstCell ? reset : false,
};
const result = await executePython(cell.code, executorOptions);
for (const output of result.displayOutputs) {
if (output.type === "json") {
jsonOutputs.push(output.data);
}
if (output.type === "image") {
images.push({ type: "image", data: output.data, mimeType: output.mimeType });
}
if (output.type === "status") {
statusEvents.push(output.event);
}
}
if (output.type === "image") {
images.push({ type: "image", data: output.data, mimeType: output.mimeType });
if (result.fullOutputPath) {
lastFullOutputPath = result.fullOutputPath;
}
if (output.type === "status") {
statusEvents.push(output.event);
const cellOutput = result.output.trim();
if (cells.length > 1) {
const cellHeader = `[${i + 1}/${cells.length}]`;
const cellTitle = cell.title ? ` ${cell.title}` : "";
if (cellOutput) {
cellOutputs.push(`${cellHeader}${cellTitle}\n${cellOutput}`);
} else {
cellOutputs.push(`${cellHeader}${cellTitle} (ok)`);
}
} else if (cellOutput) {
cellOutputs.push(cellOutput);
}
if (result.cancelled) {
const errorMsg = result.output || "Command aborted";
throw new Error(cells.length > 1 ? `Cell ${i + 1} aborted: ${errorMsg}` : errorMsg);
}
if (result.exitCode !== 0 && result.exitCode !== undefined) {
const combinedOutput = cellOutputs.join("\n\n");
throw new Error(
cells.length > 1
? `${combinedOutput}\n\nCell ${i + 1} failed (exit code ${result.exitCode}). Earlier cells succeeded—their state persists. Fix only cell ${i + 1}.`
: `${combinedOutput}\n\nCommand exited with code ${result.exitCode}`,
);
}
}
if (result.cancelled) {
throw new Error(result.output || "Command aborted");
}
const truncation = truncateTail(result.output);
const combinedOutput = cellOutputs.join("\n\n");
const truncation = truncateTail(combinedOutput);
let outputText =
truncation.content || (jsonOutputs.length > 0 || images.length > 0 ? "(no text output)" : "(no output)");
let details: PythonToolDetails | undefined;
@@ -236,14 +285,14 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
if (truncation.truncated) {
details = {
truncation,
fullOutputPath: result.fullOutputPath,
fullOutputPath: lastFullOutputPath,
jsonOutputs: jsonOutputs,
images,
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
};
outputText += formatTailTruncationNotice(truncation, {
fullOutputPath: result.fullOutputPath,
originalContent: result.output,
fullOutputPath: lastFullOutputPath,
originalContent: combinedOutput,
});
}
@@ -255,11 +304,6 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
};
}
if (result.exitCode !== 0 && result.exitCode !== undefined) {
outputText += `\n\nCommand exited with code ${result.exitCode}`;
throw new Error(outputText);
}
return { content: [{ type: "text", text: outputText }], details };
} finally {
signal?.removeEventListener("abort", onAbort);
@@ -268,9 +312,9 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
}
interface PythonRenderArgs {
code?: string;
cells?: Array<{ code: string; title?: string }>;
timeout?: number;
workdir?: string;
cwd?: string;
}
interface PythonRenderContext {
@@ -603,10 +647,10 @@ function renderStatusEvents(events: PythonStatusEvent[], theme: Theme, expanded:
export const pythonToolRenderer = {
renderCall(args: PythonRenderArgs, uiTheme: Theme): Component {
const ui = new ToolUIKit(uiTheme);
const code = args.code || uiTheme.format.ellipsis;
const cells = args.cells ?? [];
const prompt = uiTheme.fg("accent", ">>>");
const cwd = process.cwd();
let displayWorkdir = args.workdir;
let displayWorkdir = args.cwd;
if (displayWorkdir) {
const resolvedCwd = resolve(cwd);
@@ -622,11 +666,29 @@ export const pythonToolRenderer = {
}
}
const cmdText = displayWorkdir
? `${prompt} ${uiTheme.fg("dim", `cd ${displayWorkdir} &&`)} ${code}`
: `${prompt} ${code}`;
const text = ui.title(cmdText);
return new Text(text, 0, 0);
const workdirPrefix = displayWorkdir ? uiTheme.fg("dim", `cd ${displayWorkdir} && `) : "";
if (cells.length === 0) {
const text = ui.title(`${prompt} ${workdirPrefix}${uiTheme.format.ellipsis}`);
return new Text(text, 0, 0);
}
if (cells.length === 1) {
const cell = cells[0];
const label = cell.title ? `${cell.title}: ` : "";
const text = ui.title(`${prompt} ${workdirPrefix}${label}${cell.code}`);
return new Text(text, 0, 0);
}
// Multiple cells: show each with index
const lines: string[] = [];
for (let i = 0; i < cells.length; i++) {
const cellPrompt = uiTheme.fg("accent", `[${i + 1}]`);
const prefix = i === 0 ? workdirPrefix : "";
const label = cells[i].title ? `${cells[i].title}: ` : "";
lines.push(ui.title(`${cellPrompt} ${prefix}${label}${cells[i].code}`));
}
return new Text(lines.join("\n"), 0, 0);
},
renderResult(
@@ -16,7 +16,7 @@ import { checkPythonKernelAvailability } from "../../python-kernel";
import type { ToolSession } from "..";
import { LspTool } from "../lsp/index";
import type { LspParams } from "../lsp/types";
import { PythonTool } from "../python";
import { PythonTool, type PythonToolParams } from "../python";
import { subprocessToolRegistry } from "./subprocess-tool-registry";
import {
type AgentDefinition,
@@ -789,11 +789,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
const timeoutSignal = createTimeoutSignal(timeoutMs);
const combinedSignal = combineSignals([signal, callController.signal, timeoutSignal]);
try {
const result = await pythonTool.execute(
request.callId,
request.params as { code: string; timeout?: number; workdir?: string; reset?: boolean },
combinedSignal,
);
const result = await pythonTool.execute(request.callId, request.params as PythonToolParams, combinedSignal);
postMessageSafe({
type: "python_tool_result",
callId: request.callId,
@@ -363,7 +363,7 @@ function createMCPProxyTool(metadata: MCPToolMetadata): CustomTool<TSchema> {
}
function getPythonCallTimeoutMs(params: PythonToolParams): number | undefined {
const timeout = params.timeout;
const timeout = params.timeoutMs;
if (typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0) {
return Math.max(1000, Math.round(timeout * 1000) + 1000);
}
@@ -1,4 +1,42 @@
Executes Python code in an IPython kernel (session or per-call) with optional timeout.
Executes Python cells sequentially in a persistent IPython kernel.
## How to use (REPL discipline)
The kernel persists between calls and between cells. **Imports, variables, and functions survive.** Use this.
**Work incrementally:**
- One logical step per cell (imports, define a function, test it, use it)
- Pass multiple small cells in one call—they execute sequentially
- Define small functions you can reuse and debug individually
- Put explanations in the assistant message or cell title, **not** inside code
**When something fails:**
- The error tells you which cell failed (e.g., "Cell 3 failed")
- Earlier cells already ran—their state persists in the kernel
- Resubmit with only the fixed cell (or the fixed cell + remaining cells)
- Do NOT rewrite working cells or re-import modules
**Anti-patterns to avoid:**
- Putting everything in one giant cell
- Re-importing modules you already imported
- Rewriting working code when only one part failed
- Large functions that are hard to debug piece by piece
```python
# BAD: One giant cell
cells: [{
"title": "all-in-one",
"code": "import json\nfrom pathlib import Path\ndef process_all_files():\n # 50 lines...\n pass\nresult = process_all_files()"
}]
# GOOD: Multiple small cells
cells: [
{"title": "imports", "code": "import json\nfrom pathlib import Path"},
{"title": "parse helper", "code": "def parse_config(path):\n return json.loads(Path(path).read_text())"},
{"title": "test helper", "code": "parse_config('config.json')"},
{"title": "use helper", "code": "configs = [parse_config(p) for p in Path('.').glob('*.json')]"}
]
```
## When to use Python
@@ -75,7 +113,7 @@ cols(read("data.tsv"), 0, 2, sep="\t")
- Code executes as IPython cells; users see the full cell output (including rendered figures, tables, etc.)
- Kernel persists for the session by default; per-call mode uses a fresh kernel each call. Use `reset: true` to clear state when session mode is active
- Use `workdir` parameter instead of `os.chdir()` in tool call
- Use `cwd` parameter instead of `os.chdir()` in tool call
- Use `plt.show()` to display figures
- Use `display()` from IPython.display for rich output (HTML, Markdown, images, etc.)
- Output streams in real time, truncated after 50KB
@@ -114,7 +114,7 @@ describe.skipIf(!shouldRun)("PYTHON_PRELUDE integration", () => {
print("MISSING=" + ",".join(missing))
`;
const result = await tool.execute("tool-call-1", { code });
const result = await tool.execute("tool-call-1", { cells: [{ code }] });
const output = result.content.find((item) => item.type === "text")?.text ?? "";
expect(output).toContain("HELPERS_OK=1");
expect(output).toContain("DOCS_OK=1");
+9 -9
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { adjustNewTextIndentation, DEFAULT_FUZZY_THRESHOLD, findEditMatch } from "../src/core/tools/patch";
import { adjustIndentation, DEFAULT_FUZZY_THRESHOLD, findEditMatch } from "../src/core/tools/patch";
describe("findEditMatch", () => {
describe("exact matching", () => {
@@ -149,12 +149,12 @@ describe("findEditMatch", () => {
});
});
describe("adjustNewTextIndentation", () => {
describe("adjustIndentation", () => {
test("adds indentation when actualText is more indented than oldText", () => {
const oldText = "foo\nbar";
const actualText = " foo\n bar";
const newText = "foo\nbaz\nbar";
const result = adjustNewTextIndentation(oldText, actualText, newText);
const result = adjustIndentation(oldText, actualText, newText);
expect(result).toBe(" foo\n baz\n bar");
});
@@ -162,7 +162,7 @@ describe("adjustNewTextIndentation", () => {
const oldText = " foo\n bar";
const actualText = " foo\n bar";
const newText = " foo\n baz";
const result = adjustNewTextIndentation(oldText, actualText, newText);
const result = adjustIndentation(oldText, actualText, newText);
expect(result).toBe(" foo\n baz");
});
@@ -170,7 +170,7 @@ describe("adjustNewTextIndentation", () => {
const oldText = "foo\n\nbar";
const actualText = " foo\n\n bar";
const newText = "foo\n\nbaz";
const result = adjustNewTextIndentation(oldText, actualText, newText);
const result = adjustIndentation(oldText, actualText, newText);
expect(result).toBe(" foo\n\n baz");
});
@@ -178,7 +178,7 @@ describe("adjustNewTextIndentation", () => {
const oldText = " foo";
const actualText = " foo";
const newText = " bar";
const result = adjustNewTextIndentation(oldText, actualText, newText);
const result = adjustIndentation(oldText, actualText, newText);
expect(result).toBe(" bar");
});
@@ -186,7 +186,7 @@ describe("adjustNewTextIndentation", () => {
const oldText = "foo";
const actualText = "\t\tfoo";
const newText = "bar";
const result = adjustNewTextIndentation(oldText, actualText, newText);
const result = adjustIndentation(oldText, actualText, newText);
expect(result).toBe("\t\tbar");
});
@@ -194,7 +194,7 @@ describe("adjustNewTextIndentation", () => {
const oldText = "if (x) {\n return y;\n}";
const actualText = " if (x) {\n return y;\n }";
const newText = "if (x) {\n return z;\n}";
const result = adjustNewTextIndentation(oldText, actualText, newText);
const result = adjustIndentation(oldText, actualText, newText);
expect(result).toBe(" if (x) {\n return z;\n }");
});
@@ -202,7 +202,7 @@ describe("adjustNewTextIndentation", () => {
const oldText = " foo";
const actualText = "foo";
const newText = " bar";
const result = adjustNewTextIndentation(oldText, actualText, newText);
const result = adjustIndentation(oldText, actualText, newText);
// Should remove up to 4 chars, but line only has 2, so remove 2
expect(result).toBe("bar");
});
@@ -77,13 +77,13 @@ describe("python tool settings", () => {
const session = createSession(testDir, { getPythonKernelMode: () => "per-call" });
const pythonTool = new PythonTool(session);
await pythonTool.execute("tool-call", { code: "print(1)" });
await pythonTool.execute("tool-call", { cells: [{ code: "print(1)" }] });
expect(executeSpy).toHaveBeenCalledWith(
"print(1)",
expect.objectContaining({
kernelMode: "per-call",
sessionId: `session:session.json:workdir:${testDir}`,
sessionId: `session:session.json:cwd:${testDir}`,
}),
);
});
@@ -40,7 +40,7 @@ describe("python tool execution", () => {
const tool = new PythonTool(createSession(tempDir.path));
const result = await tool.execute(
"call-id",
{ code: "print('hi')", timeoutMs: 5000, workdir: tempDir.path, reset: true },
{ cells: [{ code: "print('hi')" }], timeoutMs: 5000, cwd: tempDir.path, reset: true },
undefined,
undefined,
undefined,
@@ -51,7 +51,7 @@ describe("python tool execution", () => {
expect.objectContaining({
cwd: tempDir.path,
timeoutMs: 5000,
sessionId: `session:session-file:workdir:${tempDir.path}`,
sessionId: `session:session-file:cwd:${tempDir.path}`,
kernelMode: "per-call",
reset: true,
}),
@@ -56,11 +56,11 @@ describe("python tool schema", () => {
};
expect(schema.type).toBe("object");
expect(schema.properties.code.type).toBe("string");
expect(schema.properties.cells.type).toBe("array");
expect(schema.properties.timeoutMs.type).toBe("number");
expect(schema.properties.workdir.type).toBe("string");
expect(schema.properties.cwd.type).toBe("string");
expect(schema.properties.reset.type).toBe("boolean");
expect(schema.required).toEqual(["code"]);
expect(schema.required).toEqual(["cells"]);
});
});
+9 -9
View File
@@ -162,7 +162,7 @@ async function runSingleTask(
task: EditTask,
runIndex: number,
config: BenchmarkConfig,
workDir: string,
cwd: string,
expectedDir: string,
cliPath: string,
): Promise<TaskRunResult> {
@@ -193,7 +193,7 @@ async function runSingleTask(
};
try {
await appendFile(logFile, `{"type":"meta","task":"${task.id}","run":${runIndex},"workDir":"${workDir}"}\n`);
await appendFile(logFile, `{"type":"meta","task":"${task.id}","run":${runIndex},"workDir":"${cwd}"}\n`);
const env: Record<string, string> = { OMP_NO_TITLE: "1" };
if (config.editVariant !== undefined) {
@@ -209,7 +209,7 @@ async function runSingleTask(
client = new RpcClient({
cliPath,
cwd: workDir,
cwd,
provider: config.provider,
model: config.model,
args: ["--tools", "read,edit,write,ls"],
@@ -323,9 +323,9 @@ ${
patchApplied = toolStats.edit > 0;
const verification = await verifyExpectedFiles(expectedDir, workDir);
const verification = await verifyExpectedFiles(expectedDir, cwd);
if (config.autoFormat) {
await formatDirectory(workDir);
await formatDirectory(cwd);
}
verificationPassed = verification.success;
@@ -382,7 +382,7 @@ ${
async function runBatchedTask(
item: TaskRunItem,
config: BenchmarkConfig,
workDir: string,
cwd: string,
expectedDir: string,
client: RpcClient,
): Promise<TaskRunResult> {
@@ -416,7 +416,7 @@ async function runBatchedTask(
try {
await appendFile(
logFile,
`{"type":"meta","task":"${task.id}","run":${runIndex},"workDir":"${workDir}","batched":true}\n`,
`{"type":"meta","task":"${task.id}","run":${runIndex},"workDir":"${cwd}","batched":true}\n`,
);
const promptWithContext = buildPrompt(task, config);
@@ -465,9 +465,9 @@ async function runBatchedTask(
patchApplied = toolStats.edit > 0;
const filesToVerify = task.files.length > 0 ? task.files : undefined;
const verification = await verifyExpectedFileSubset(expectedDir, workDir, filesToVerify);
const verification = await verifyExpectedFileSubset(expectedDir, cwd, filesToVerify);
if (config.autoFormat) {
await formatDirectory(workDir);
await formatDirectory(cwd);
}
verificationPassed = verification.success;