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:
1 parent
d65f156732
commit
b839ce4f23
9 files changed
+265
-6
No files matched your search
@@ -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">;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user