mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
b5f425ad15
Add AgentTool.prepareArguments and ToolDefinition.prepareArguments hook that runs before schema validation in the agent loop. This lets tools silently accept legacy argument shapes from resumed old sessions without polluting the public schema. The built-in edit tool uses this to fold legacy top-level oldText/newText into edits[] when resuming sessions that predate the edits-only schema. - AgentTool/ToolDefinition: typed prepareArguments returning Static<TParameters> - agent-loop: prepareToolCallArguments() runs before validateToolArguments() - edit tool: prepareEditArguments folds legacy fields, validateEditInput is strict - Documented in extensions.md with edit-tool example
91 lines
3.0 KiB
TypeScript
91 lines
3.0 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 { ExtensionContext } from "../src/core/extensions/types.js";
|
|
import { createEditToolDefinition } from "../src/core/tools/edit.js";
|
|
|
|
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 ExtensionContext);
|
|
expect(result.content).toEqual([{ type: "text", text: "Successfully replaced 1 block(s) in legacy.txt." }]);
|
|
expect(await readFile(filePath, "utf8")).toBe("after\n");
|
|
});
|
|
});
|