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

@@ -0,0 +1,42 @@
/**
* Decide which internal tools (`hire_agent`, `file_approval`) to attach
* to a given run's agent (C4 part 3/5).
*
* The selector runs on main at dispatch time — the worker never
* re-derives this. Rule set, in order:
*
* - An agent that `canRequestHire` (CEO, HR, dept lead with reports,
* anyone with org-management authority) receives `hire_agent`.
* - A CEO (reportsTo null) additionally receives `file_approval` so
* the `approve_ceo_strategy` flow works without the HTTP shim. Peer
* agents use the HTTP path until the file_approval surface is
* generalised.
*
* No hedging, no sentinel values: if the DB lookup throws the caller
* is already in a broken state and should not silently fall through
* to an empty tool set.
*/
import type { AppDatabase } from "@main/database";
import type { AgentRecord } from "@shared/types";
import { canRequestHire } from "@main/agent-permissions";
export type InternalToolName = "hire_agent" | "file_approval";
export function resolveInternalToolsForAgent(
db: AppDatabase,
agent: AgentRecord,
): InternalToolName[] {
const tools: InternalToolName[] = [];
if (canRequestHire(db, agent.companyId, agent.id)) {
tools.push("hire_agent");
}
// `file_approval` is currently scoped to the CEO (no manager) because
// the canonical use case (`approve_ceo_strategy`) is CEO-only and
// the broader approval surface ships via the HTTP route for now.
// Widen the predicate when non-CEO agents need tool-call approvals.
if (agent.reportsTo === null) {
tools.push("file_approval");
}
return tools;
}