Inject hire_agent and file_approval into CEO agent (C4 part 3/5)

Wire the two internal tools from parts 1-2 end-to-end:

- `invokeInternalTool` RPC method added to RuntimeBridge; worker tool
  `execute` calls route here, handler dispatches by tool name via the
  new `agent-runtime/internal-tools/router.ts`.
- `InternalToolCallbacks` (`onApprovalCreated` / `onApprovalResolved`
  / `onDomainChanged`) are late-bound on the bridge via
  `setInternalToolCallbacks`; `wire-api-server` now cross-wires the
  same callbacks used by the HTTP approval route so thread integration
  / automation fire identically across both entry points.
- `RunJob.internalTools` (new field) carries the per-run tool list
  main decides at dispatch time via `orchestration/internal-tool-
  selector.ts`. Selection rule: `hire_agent` → any agent with
  `canRequestHire` authority; `file_approval` → CEOs only (scoped to
  the `approve_ceo_strategy` use case).
- Worker `createPiAgent` call site now assembles `codingTools +
  internalTools` before handing to pi. Exhaustiveness guard on the
  switch catches new tool names at compile time.

No prompt-level wiring yet — the `initial-user-turn` and
`team-building.en.md` updates ship in parts 4 and 5 so this commit
only adds the tool surface without changing the CEO's observable
behaviour (the existing HTTP path is still the one the prompt tells
the CEO to call).

tests: pnpm typecheck + pnpm test:unit (529) + pnpm test:eval (8/8) green
This commit is contained in:
ZacharyZhang-NY committed 2026-04-20 14:25:44 -04:00
1 parent d65f156732
commit b839ce4f23
9 files changed
+265 -6

No files matched your search

+8
View File
@@ -21,6 +21,14 @@ export interface RunJob {
executionMode: "ai_ceo" | "ai_cofounder";
/** Seed user turn that kicks off the pi agent loop. Never empty. */
initialUserTurn: string;
/**
* Internal tools (`hire_agent`, `file_approval`) to register on this
* run's agent in addition to the default coding tools. Determined on
* main side before enqueue (see `pi-agent-factory.ts:inject hire_agent
* and file_approval into CEO agent`) so the worker never needs to
* inspect company/agent state. Empty array = no extra tools.
*/
internalTools: ReadonlyArray<"hire_agent" | "file_approval">;
}
/**
+29 -2
View File
@@ -1,4 +1,4 @@
import type { Agent, AgentMessage } from "@mariozechner/pi-agent-core";
import type { Agent, AgentMessage, AgentTool } from "@mariozechner/pi-agent-core";
import { resolveModel, resolveExtraStreamHeaders } from "../../model-resolver";
import type { RunStatus } from "@shared/types";
import type { ChildMessage, RunJob } from "./helpers";
@@ -6,6 +6,8 @@ import { applyEnvOverrides, buildRedactor } from "./helpers";
import { createCodingTools } from "@mariozechner/pi-coding-agent";
import { createPiAgent, resolveThinkingLevel } 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";
interface PrepareRunResult {
provider: string;
@@ -138,7 +140,32 @@ export class WorkerRunner {
const extraHeaders = resolveExtraStreamHeaders(prepared.provider, prepared.apiKey);
const cwd = job.workspacePath ?? process.cwd();
const tools = createCodingTools(cwd) as never;
const codingTools = createCodingTools(cwd);
// pi's `AgentTool<TSchema>` is generic over the concrete TypeBox
// 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 ?? []) {
switch (toolName) {
case HIRE_AGENT_TOOL_NAME:
internalTools.push(createHireAgentTool(rpc, job.runId) as unknown as AgentTool);
break;
case FILE_APPROVAL_TOOL_NAME:
internalTools.push(createFileApprovalTool(rpc, job.runId) as unknown as AgentTool);
break;
default: {
// Exhaustiveness guard: a new entry in the `internalTools`
// array without a matching case should be a compile-time
// regression.
const _exhaustive: never = toolName;
throw new Error(`Unknown internal tool requested: ${String(_exhaustive)}`);
}
}
}
const tools = [...codingTools, ...internalTools] as never;
const agent = createPiAgent({
runId: job.runId,
@@ -30,6 +30,10 @@ export interface PiAgentFactoryInput {
taskSessionId: string;
systemPrompt: string;
model: Model<Api>;
// `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;
priorMessages: AgentMessage[];
thinkingLevel: ThinkingLevel;