diff --git a/src/main/worker/runtime/index.ts b/src/main/worker/runtime/index.ts index 45621112..8a2ebc2d 100644 --- a/src/main/worker/runtime/index.ts +++ b/src/main/worker/runtime/index.ts @@ -1,10 +1,10 @@ -import type { Agent, AgentMessage, AgentTool } from "@mariozechner/pi-agent-core"; +import type { Agent, AgentMessage } from "@mariozechner/pi-agent-core"; import { resolveModel, resolveExtraStreamHeaders } from "../../model-resolver"; import type { RunStatus } from "@shared/types"; import type { ChildMessage, RunJob } from "./helpers"; import { applyEnvOverrides, buildRedactor } from "./helpers"; import { createCodingTools } from "@mariozechner/pi-coding-agent"; -import { createPiAgent, resolveThinkingLevel } from "./pi-agent-factory"; +import { createPiAgent, resolveThinkingLevel, type ErasedAgentTool } from "./pi-agent-factory"; import type { WorkerRpcClient } from "../entry/rpc"; import { createHireAgentTool, HIRE_AGENT_TOOL_NAME } from "./internal-tools/hire-agent-tool"; import { createFileApprovalTool, FILE_APPROVAL_TOOL_NAME } from "./internal-tools/file-approval-tool"; @@ -149,20 +149,14 @@ export class WorkerRunner { const cwd = job.workspacePath ?? process.cwd(); const codingTools = createCodingTools(cwd); - // pi's `AgentTool` is generic over the concrete TypeBox - // schema; two tools with different schemas cannot share a single - // `AgentTool[]` without widening to the base shape. The - // internal-tool factories return properly-typed instances; we - // widen via unknown only at the array boundary so each execute - // signature stays type-checked at definition time. - const internalTools: AgentTool[] = []; + const internalTools: ErasedAgentTool[] = []; for (const toolName of job.internalTools ?? []) { switch (toolName) { case HIRE_AGENT_TOOL_NAME: - internalTools.push(createHireAgentTool(rpc, job.runId) as unknown as AgentTool); + internalTools.push(createHireAgentTool(rpc, job.runId)); break; case FILE_APPROVAL_TOOL_NAME: - internalTools.push(createFileApprovalTool(rpc, job.runId) as unknown as AgentTool); + internalTools.push(createFileApprovalTool(rpc, job.runId)); break; default: { // Exhaustiveness guard: a new entry in the `internalTools` @@ -173,7 +167,7 @@ export class WorkerRunner { } } } - const tools = [...codingTools, ...internalTools] as never; + const tools: ErasedAgentTool[] = [...codingTools, ...internalTools]; const agent = createPiAgent({ runId: job.runId, diff --git a/src/main/worker/runtime/pi-agent-factory.ts b/src/main/worker/runtime/pi-agent-factory.ts index 12c45b3d..501cf65c 100644 --- a/src/main/worker/runtime/pi-agent-factory.ts +++ b/src/main/worker/runtime/pi-agent-factory.ts @@ -16,6 +16,7 @@ import { Agent, type AgentMessage, + type AgentState, type ThinkingLevel, } from "@mariozechner/pi-agent-core"; import type { Api, Model } from "@mariozechner/pi-ai"; @@ -27,16 +28,14 @@ import { safeStreamSimple } from "./pi-safe-stream"; // graph so any drift in pi-agent-core hook shapes breaks the build here. import "./pi-hook-contract"; +export type ErasedAgentTool = AgentState["tools"][number]; + export interface PiAgentFactoryInput { runId: string; taskSessionId: string; systemPrompt: string; model: Model; - // `never` preserves the pre-C4 contract where the caller owns tool - // typing (pi-coding-agent's union is too wide to mirror here and C4 - // adds local internal tools with their own TSchema). The caller casts - // a concrete tools array at the `createPiAgent` site. - tools: never; + tools: ErasedAgentTool[]; priorMessages: AgentMessage[]; thinkingLevel: ThinkingLevel; apiKey: string; diff --git a/tests/unit/pi-agent-factory.test.ts b/tests/unit/pi-agent-factory.test.ts index 8b19dc56..366c5750 100644 --- a/tests/unit/pi-agent-factory.test.ts +++ b/tests/unit/pi-agent-factory.test.ts @@ -1,3 +1,5 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; import type { Model } from "@mariozechner/pi-ai"; import { describe, expect, it } from "vitest"; import { createPiAgent } from "../../src/main/worker/runtime/pi-agent-factory"; @@ -25,7 +27,7 @@ describe("pi agent factory", () => { taskSessionId: "session_1", systemPrompt: "You are an engineer.", model: createModel(), - tools: [] as never, + tools: [], priorMessages: [], thinkingLevel: "off", apiKey: "test-key", @@ -39,4 +41,14 @@ describe("pi agent factory", () => { expect(agent.toolExecution).toBe("sequential"); }); + + it("keeps worker tool arrays on the erased pi tool contract", () => { + const runnerSource = readFileSync(join(process.cwd(), "src/main/worker/runtime/index.ts"), "utf8"); + const factorySource = readFileSync(join(process.cwd(), "src/main/worker/runtime/pi-agent-factory.ts"), "utf8"); + + expect(runnerSource).not.toContain("as unknown as AgentTool"); + expect(runnerSource).not.toContain("as never"); + expect(factorySource).not.toContain("tools: never"); + expect(factorySource).toContain('type ErasedAgentTool = AgentState["tools"][number]'); + }); });