mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-06 23:44:36 +00:00
Adds a built-in, opt-in codemode tool that runs model-written JavaScript in the codemode sandbox. Scripts call the other active tools as tools.<name>(args), including concurrently with Promise.all; only the return value, console output, and images attached with image() reach the model. Nested calls run through the agent loop via a new AgentToolContext (5th execute argument, ctx.executeTool for extension tools), so argument validation and the beforeToolCall/afterToolCall hooks apply. Hooks and tool_call/tool_result events carry the parent call id. Tools can declare outputSchema and return structuredContent; codemode passes that to scripts and falls back to text content otherwise. The codemode package gains globals, declaration rendering from JSON Schema, and optional deadlines.
116 lines
3.7 KiB
TypeScript
116 lines
3.7 KiB
TypeScript
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import type { ExtensionToolContext } from "../src/core/extensions/types.ts";
|
|
import { createEditToolDefinition } from "../src/core/tools/edit.ts";
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
async function createTempDir(): Promise<string> {
|
|
const dir = await mkdtemp(join(tmpdir(), "pi-edit-legacy-input-"));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(tempDirs.splice(0, tempDirs.length).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
});
|
|
|
|
describe("edit tool prepareArguments", () => {
|
|
it("keeps legacy fields out of the public schema", () => {
|
|
const definition = createEditToolDefinition(process.cwd());
|
|
expect(definition.parameters.properties).not.toHaveProperty("oldText");
|
|
expect(definition.parameters.properties).not.toHaveProperty("newText");
|
|
});
|
|
|
|
it("folds top-level oldText/newText into edits", () => {
|
|
const definition = createEditToolDefinition(process.cwd());
|
|
const prepared = definition.prepareArguments!({
|
|
path: "file.txt",
|
|
oldText: "before",
|
|
newText: "after",
|
|
});
|
|
expect(prepared).toEqual({
|
|
path: "file.txt",
|
|
edits: [{ oldText: "before", newText: "after" }],
|
|
});
|
|
});
|
|
|
|
it("appends legacy replacement to existing edits", () => {
|
|
const definition = createEditToolDefinition(process.cwd());
|
|
const prepared = definition.prepareArguments!({
|
|
path: "file.txt",
|
|
edits: [{ oldText: "a", newText: "b" }],
|
|
oldText: "c",
|
|
newText: "d",
|
|
});
|
|
expect(prepared).toEqual({
|
|
path: "file.txt",
|
|
edits: [
|
|
{ oldText: "a", newText: "b" },
|
|
{ oldText: "c", newText: "d" },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("passes through valid input unchanged", () => {
|
|
const definition = createEditToolDefinition(process.cwd());
|
|
const input = {
|
|
path: "file.txt",
|
|
edits: [{ oldText: "a", newText: "b" }],
|
|
};
|
|
const prepared = definition.prepareArguments!(input);
|
|
expect(prepared).toBe(input);
|
|
});
|
|
|
|
it("passes through non-object input unchanged", () => {
|
|
const definition = createEditToolDefinition(process.cwd());
|
|
expect(definition.prepareArguments!(null)).toBe(null);
|
|
expect(definition.prepareArguments!(undefined)).toBe(undefined);
|
|
expect(definition.prepareArguments!("garbage")).toBe("garbage");
|
|
});
|
|
|
|
it("prepared args execute correctly", async () => {
|
|
const dir = await createTempDir();
|
|
const filePath = join(dir, "legacy.txt");
|
|
await writeFile(filePath, "before\n", "utf8");
|
|
|
|
const definition = createEditToolDefinition(dir);
|
|
const prepared = definition.prepareArguments!({
|
|
path: "legacy.txt",
|
|
oldText: "before",
|
|
newText: "after",
|
|
});
|
|
|
|
const result = await definition.execute("tool-1", prepared, undefined, undefined, {} as ExtensionToolContext);
|
|
expect(result.content).toEqual([{ type: "text", text: "Successfully replaced 1 block(s) in legacy.txt." }]);
|
|
expect(await readFile(filePath, "utf8")).toBe("after\n");
|
|
});
|
|
});
|
|
|
|
describe("edit tool stringified edits", () => {
|
|
it("parses edits from a JSON string", () => {
|
|
const definition = createEditToolDefinition(process.cwd());
|
|
const prepared = definition.prepareArguments!({
|
|
path: "file.txt",
|
|
edits: JSON.stringify([{ oldText: "a", newText: "b" }]),
|
|
});
|
|
expect(prepared).toEqual({
|
|
path: "file.txt",
|
|
edits: [{ oldText: "a", newText: "b" }],
|
|
});
|
|
});
|
|
|
|
it("leaves edits alone when the string is not valid JSON", () => {
|
|
const definition = createEditToolDefinition(process.cwd());
|
|
const prepared = definition.prepareArguments!({
|
|
path: "file.txt",
|
|
edits: "not json",
|
|
});
|
|
expect(prepared).toEqual({
|
|
path: "file.txt",
|
|
edits: "not json",
|
|
});
|
|
});
|
|
});
|