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:
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user