mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +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.
159 lines
5.4 KiB
TypeScript
159 lines
5.4 KiB
TypeScript
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { getModel } from "@earendil-works/pi-ai/compat";
|
|
import { Type } from "typebox";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { createAgentSessionFromServices, createAgentSessionServices } from "../src/core/agent-session-services.ts";
|
|
import { DefaultResourceLoader } from "../src/core/resource-loader.ts";
|
|
import { type CreateAgentSessionOptions, createAgentSession, type InlineExtension } from "../src/core/sdk.ts";
|
|
import { SessionManager } from "../src/core/session-manager.ts";
|
|
import { SettingsManager } from "../src/core/settings-manager.ts";
|
|
|
|
type ToolOptions = Pick<CreateAgentSessionOptions, "tools" | "excludeTools" | "noTools" | "customTools">;
|
|
|
|
describe("defaultTools setting", () => {
|
|
let tempDir: string;
|
|
let agentDir: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = join(tmpdir(), `pi-default-tools-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
agentDir = join(tempDir, "agent");
|
|
mkdirSync(agentDir, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (tempDir && existsSync(tempDir)) {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
async function createSession(
|
|
defaultTools: string[],
|
|
options: ToolOptions = {},
|
|
extensionFactories: InlineExtension[] = [],
|
|
) {
|
|
const settingsManager = SettingsManager.inMemory({ defaultTools });
|
|
const resourceLoader = new DefaultResourceLoader({
|
|
cwd: tempDir,
|
|
agentDir,
|
|
settingsManager,
|
|
extensionFactories,
|
|
});
|
|
await resourceLoader.reload();
|
|
|
|
return (
|
|
await createAgentSession({
|
|
cwd: tempDir,
|
|
agentDir,
|
|
model: getModel("anthropic", "claude-sonnet-4-5")!,
|
|
settingsManager,
|
|
sessionManager: SessionManager.inMemory(tempDir),
|
|
resourceLoader,
|
|
...options,
|
|
})
|
|
).session;
|
|
}
|
|
|
|
it("uses the configured list as the initial built-in selection", async () => {
|
|
const session = await createSession(["grep", "find"]);
|
|
|
|
expect(
|
|
session
|
|
.getAllTools()
|
|
.map((tool) => tool.name)
|
|
.sort(),
|
|
).toEqual(["bash", "codemode", "edit", "find", "grep", "ls", "powershell", "read", "write"]);
|
|
expect(session.getActiveToolNames()).toEqual(["grep", "find"]);
|
|
expect(session.systemPrompt).toContain("- grep:");
|
|
expect(session.systemPrompt).not.toContain("- read:");
|
|
session.dispose();
|
|
});
|
|
|
|
it("can select powershell instead of bash", async () => {
|
|
const session = await createSession(["read", "powershell", "edit", "write"]);
|
|
|
|
expect(session.getActiveToolNames()).toEqual(["read", "powershell", "edit", "write"]);
|
|
expect(session.systemPrompt).toContain("- powershell: Execute PowerShell commands");
|
|
expect(session.systemPrompt).not.toContain("- bash:");
|
|
session.dispose();
|
|
});
|
|
|
|
it("keeps extension and SDK custom tools enabled", async () => {
|
|
const session = await createSession(
|
|
["grep"],
|
|
{
|
|
customTools: [
|
|
{
|
|
name: "sdk_tool",
|
|
label: "SDK Tool",
|
|
description: "SDK custom tool",
|
|
parameters: Type.Object({}),
|
|
execute: async () => ({ content: [{ type: "text", text: "ok" }], details: {} }),
|
|
},
|
|
],
|
|
},
|
|
[
|
|
(pi) => {
|
|
pi.registerTool({
|
|
name: "static_tool",
|
|
label: "Static Tool",
|
|
description: "Statically registered extension tool",
|
|
parameters: Type.Object({}),
|
|
execute: async () => ({ content: [{ type: "text", text: "ok" }], details: {} }),
|
|
});
|
|
pi.on("session_start", () => {
|
|
pi.registerTool({
|
|
name: "dynamic_tool",
|
|
label: "Dynamic Tool",
|
|
description: "Dynamically registered extension tool",
|
|
parameters: Type.Object({}),
|
|
execute: async () => ({ content: [{ type: "text", text: "ok" }], details: {} }),
|
|
});
|
|
});
|
|
},
|
|
],
|
|
);
|
|
await session.bindExtensions({});
|
|
|
|
expect(session.getActiveToolNames().sort()).toEqual(["dynamic_tool", "grep", "sdk_tool", "static_tool"]);
|
|
expect(session.getAllTools().map((tool) => tool.name)).toEqual(
|
|
expect.arrayContaining(["read", "dynamic_tool", "sdk_tool", "static_tool"]),
|
|
);
|
|
session.dispose();
|
|
});
|
|
|
|
it("preserves explicit tool option precedence", async () => {
|
|
const allowlistedSession = await createSession(["grep"], { tools: ["read"] });
|
|
expect(allowlistedSession.getActiveToolNames()).toEqual(["read"]);
|
|
allowlistedSession.dispose();
|
|
|
|
const excludedSession = await createSession(["read", "grep"], { excludeTools: ["read"] });
|
|
expect(excludedSession.getActiveToolNames()).toEqual(["grep"]);
|
|
excludedSession.dispose();
|
|
|
|
const toolLessSession = await createSession(["read"], { noTools: "all" });
|
|
expect(toolLessSession.getAllTools()).toEqual([]);
|
|
expect(toolLessSession.getActiveToolNames()).toEqual([]);
|
|
toolLessSession.dispose();
|
|
});
|
|
|
|
it("applies through service-based session creation", async () => {
|
|
const settingsManager = SettingsManager.inMemory({ defaultTools: ["ls"] });
|
|
const services = await createAgentSessionServices({ cwd: tempDir, agentDir, settingsManager });
|
|
const { session } = await createAgentSessionFromServices({
|
|
services,
|
|
sessionManager: SessionManager.inMemory(tempDir),
|
|
model: getModel("anthropic", "claude-sonnet-4-5")!,
|
|
});
|
|
|
|
expect(
|
|
session
|
|
.getAllTools()
|
|
.map((tool) => tool.name)
|
|
.sort(),
|
|
).toEqual(["bash", "codemode", "edit", "find", "grep", "ls", "powershell", "read", "write"]);
|
|
expect(session.getActiveToolNames()).toEqual(["ls"]);
|
|
session.dispose();
|
|
});
|
|
});
|