fix(runtime): erase pi tool array type safely
This commit is contained in:
1 parent
710448a75f
commit
9f35dc7446
3 files changed
+23
-18
No files matched your search
@@ -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 { resolveModel, resolveExtraStreamHeaders } from "../../model-resolver";
|
||||||
import type { RunStatus } from "@shared/types";
|
import type { RunStatus } from "@shared/types";
|
||||||
import type { ChildMessage, RunJob } from "./helpers";
|
import type { ChildMessage, RunJob } from "./helpers";
|
||||||
import { applyEnvOverrides, buildRedactor } from "./helpers";
|
import { applyEnvOverrides, buildRedactor } from "./helpers";
|
||||||
import { createCodingTools } from "@mariozechner/pi-coding-agent";
|
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 type { WorkerRpcClient } from "../entry/rpc";
|
||||||
import { createHireAgentTool, HIRE_AGENT_TOOL_NAME } from "./internal-tools/hire-agent-tool";
|
import { createHireAgentTool, HIRE_AGENT_TOOL_NAME } from "./internal-tools/hire-agent-tool";
|
||||||
import { createFileApprovalTool, FILE_APPROVAL_TOOL_NAME } from "./internal-tools/file-approval-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 cwd = job.workspacePath ?? process.cwd();
|
||||||
const codingTools = createCodingTools(cwd);
|
const codingTools = createCodingTools(cwd);
|
||||||
// pi's `AgentTool<TSchema>` is generic over the concrete TypeBox
|
const internalTools: ErasedAgentTool[] = [];
|
||||||
// schema; two tools with different schemas cannot share a single
|
|
||||||
// `AgentTool<TSchema>[]` 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[] = [];
|
|
||||||
for (const toolName of job.internalTools ?? []) {
|
for (const toolName of job.internalTools ?? []) {
|
||||||
switch (toolName) {
|
switch (toolName) {
|
||||||
case HIRE_AGENT_TOOL_NAME:
|
case HIRE_AGENT_TOOL_NAME:
|
||||||
internalTools.push(createHireAgentTool(rpc, job.runId) as unknown as AgentTool);
|
internalTools.push(createHireAgentTool(rpc, job.runId));
|
||||||
break;
|
break;
|
||||||
case FILE_APPROVAL_TOOL_NAME:
|
case FILE_APPROVAL_TOOL_NAME:
|
||||||
internalTools.push(createFileApprovalTool(rpc, job.runId) as unknown as AgentTool);
|
internalTools.push(createFileApprovalTool(rpc, job.runId));
|
||||||
break;
|
break;
|
||||||
default: {
|
default: {
|
||||||
// Exhaustiveness guard: a new entry in the `internalTools`
|
// 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({
|
const agent = createPiAgent({
|
||||||
runId: job.runId,
|
runId: job.runId,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
import {
|
import {
|
||||||
Agent,
|
Agent,
|
||||||
type AgentMessage,
|
type AgentMessage,
|
||||||
|
type AgentState,
|
||||||
type ThinkingLevel,
|
type ThinkingLevel,
|
||||||
} from "@mariozechner/pi-agent-core";
|
} from "@mariozechner/pi-agent-core";
|
||||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
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.
|
// graph so any drift in pi-agent-core hook shapes breaks the build here.
|
||||||
import "./pi-hook-contract";
|
import "./pi-hook-contract";
|
||||||
|
|
||||||
|
export type ErasedAgentTool = AgentState["tools"][number];
|
||||||
|
|
||||||
export interface PiAgentFactoryInput {
|
export interface PiAgentFactoryInput {
|
||||||
runId: string;
|
runId: string;
|
||||||
taskSessionId: string;
|
taskSessionId: string;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
model: Model<Api>;
|
model: Model<Api>;
|
||||||
// `never` preserves the pre-C4 contract where the caller owns tool
|
tools: ErasedAgentTool[];
|
||||||
// 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;
|
|
||||||
priorMessages: AgentMessage[];
|
priorMessages: AgentMessage[];
|
||||||
thinkingLevel: ThinkingLevel;
|
thinkingLevel: ThinkingLevel;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
import type { Model } from "@mariozechner/pi-ai";
|
import type { Model } from "@mariozechner/pi-ai";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { createPiAgent } from "../../src/main/worker/runtime/pi-agent-factory";
|
import { createPiAgent } from "../../src/main/worker/runtime/pi-agent-factory";
|
||||||
@@ -25,7 +27,7 @@ describe("pi agent factory", () => {
|
|||||||
taskSessionId: "session_1",
|
taskSessionId: "session_1",
|
||||||
systemPrompt: "You are an engineer.",
|
systemPrompt: "You are an engineer.",
|
||||||
model: createModel(),
|
model: createModel(),
|
||||||
tools: [] as never,
|
tools: [],
|
||||||
priorMessages: [],
|
priorMessages: [],
|
||||||
thinkingLevel: "off",
|
thinkingLevel: "off",
|
||||||
apiKey: "test-key",
|
apiKey: "test-key",
|
||||||
@@ -39,4 +41,14 @@ describe("pi agent factory", () => {
|
|||||||
|
|
||||||
expect(agent.toolExecution).toBe("sequential");
|
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]');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user