diff --git a/src/agents/pi-embedded-runner.test.ts b/src/agents/pi-embedded-runner.test.ts index 4c29aa186..1ce3bc94d 100644 --- a/src/agents/pi-embedded-runner.test.ts +++ b/src/agents/pi-embedded-runner.test.ts @@ -1,6 +1,10 @@ +import type { AgentTool } from "@mariozechner/pi-agent-core"; +import { Type } from "@sinclair/typebox"; import { describe, expect, it } from "vitest"; - -import { buildEmbeddedSandboxInfo } from "./pi-embedded-runner.js"; +import { + buildEmbeddedSandboxInfo, + splitSdkTools, +} from "./pi-embedded-runner.js"; import type { SandboxContext } from "./sandbox.js"; describe("buildEmbeddedSandboxInfo", () => { @@ -45,3 +49,52 @@ describe("buildEmbeddedSandboxInfo", () => { }); }); }); + +function createStubTool(name: string): AgentTool { + return { + name, + label: name, + description: "", + parameters: Type.Object({}), + execute: async () => ({ content: [], details: {} }), + }; +} + +describe("splitSdkTools", () => { + const tools = [ + createStubTool("read"), + createStubTool("bash"), + createStubTool("edit"), + createStubTool("write"), + createStubTool("browser"), + ]; + + it("routes built-ins to custom tools when sandboxed", () => { + const { builtInTools, customTools } = splitSdkTools({ + tools, + sandboxEnabled: true, + }); + expect(builtInTools).toEqual([]); + expect(customTools.map((tool) => tool.name)).toEqual([ + "read", + "bash", + "edit", + "write", + "browser", + ]); + }); + + it("keeps built-ins as SDK tools when not sandboxed", () => { + const { builtInTools, customTools } = splitSdkTools({ + tools, + sandboxEnabled: false, + }); + expect(builtInTools.map((tool) => tool.name)).toEqual([ + "read", + "bash", + "edit", + "write", + ]); + expect(customTools.map((tool) => tool.name)).toEqual(["browser"]); + }); +}); diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index 7913664c4..8720ee265 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -1,7 +1,11 @@ import fs from "node:fs/promises"; import os from "node:os"; -import type { AgentMessage, ThinkingLevel } from "@mariozechner/pi-agent-core"; +import type { + AgentMessage, + AgentTool, + ThinkingLevel, +} from "@mariozechner/pi-agent-core"; import type { Api, AssistantMessage, Model } from "@mariozechner/pi-ai"; import { buildSystemPrompt, @@ -12,6 +16,7 @@ import { SettingsManager, type Skill, } from "@mariozechner/pi-coding-agent"; +import type { TSchema } from "@sinclair/typebox"; import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js"; import type { ReasoningLevel, @@ -249,6 +254,33 @@ export function buildEmbeddedSandboxInfo( }; } +const BUILT_IN_TOOL_NAMES = new Set(["read", "bash", "edit", "write"]); + +type AnyAgentTool = AgentTool; + +export function splitSdkTools(options: { + tools: AnyAgentTool[]; + sandboxEnabled: boolean; +}): { + builtInTools: AnyAgentTool[]; + customTools: ReturnType; +} { + // SDK rebuilds built-ins from cwd; route sandboxed versions as custom tools. + const { tools, sandboxEnabled } = options; + if (sandboxEnabled) { + return { + builtInTools: [], + customTools: toToolDefinitions(tools), + }; + } + return { + builtInTools: tools.filter((tool) => BUILT_IN_TOOL_NAMES.has(tool.name)), + customTools: toToolDefinitions( + tools.filter((tool) => !BUILT_IN_TOOL_NAMES.has(tool.name)), + ), + }; +} + export function queueEmbeddedPiMessage( sessionId: string, text: string, @@ -528,11 +560,10 @@ export async function compactEmbeddedPiSession(params: { agentDir, ); - const builtInToolNames = new Set(["read", "bash", "edit", "write"]); - const builtInTools = tools.filter((t) => builtInToolNames.has(t.name)); - const customTools = toToolDefinitions( - tools.filter((t) => !builtInToolNames.has(t.name)), - ); + const { builtInTools, customTools } = splitSdkTools({ + tools, + sandboxEnabled: !!sandbox?.enabled, + }); const { session } = await createAgentSession({ cwd: resolvedWorkspace, @@ -829,14 +860,10 @@ export async function runEmbeddedPiAgent(params: { agentDir, ); - // Split tools into built-in (recognized by pi-coding-agent SDK) and custom (clawdbot-specific) - const builtInToolNames = new Set(["read", "bash", "edit", "write"]); - const builtInTools = tools.filter((t) => - builtInToolNames.has(t.name), - ); - const customTools = toToolDefinitions( - tools.filter((t) => !builtInToolNames.has(t.name)), - ); + const { builtInTools, customTools } = splitSdkTools({ + tools, + sandboxEnabled: !!sandbox?.enabled, + }); const { session } = await createAgentSession({ cwd: resolvedWorkspace, diff --git a/src/agents/pi-embedded-subscribe.test.ts b/src/agents/pi-embedded-subscribe.test.ts index 72e2541c5..b50e02d96 100644 --- a/src/agents/pi-embedded-subscribe.test.ts +++ b/src/agents/pi-embedded-subscribe.test.ts @@ -162,7 +162,9 @@ describe("subscribeEmbeddedPiSession", () => { expect(onBlockReply).toHaveBeenCalledTimes(1); const payload = onBlockReply.mock.calls[0][0]; - expect(payload.text).toBe("_Reasoning:_\n_Because it helps_\n\nFinal answer"); + expect(payload.text).toBe( + "_Reasoning:_\n_Because it helps_\n\nFinal answer", + ); }); it("emits block replies on text_end and does not duplicate on message_end", () => {