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;
}
+4
View File
@@ -14,6 +14,7 @@ import type { WorkspaceLockMode, EnqueueRunPayload } from "./types";
import { normalizeCommentsForPrompt } from "../messaging/normalize";
import { getConnectorModelPolicy, validateConnectorModelSelection } from "../connector-model-policy";
import { resolveInitialUserTurn } from "./initial-user-turn";
import { resolveInternalToolsForAgent } from "./internal-tool-selector";
import { logAppError } from "../errors/index.js";
export interface RunDispatchParams {
@@ -364,6 +365,8 @@ export function buildRunPayload(params: RunDispatchParams, deps: TurnContextDeps
{ db },
);
const internalTools = resolveInternalToolsForAgent(db, agent);
return {
runId,
companyId,
@@ -382,5 +385,6 @@ export function buildRunPayload(params: RunDispatchParams, deps: TurnContextDeps
taskSessionId: taskSessionId ?? runId,
executionMode: db.company.getCompanyExecutionMode(companyId) as "ai_ceo" | "ai_cofounder",
initialUserTurn,
internalTools,
};
}
+8
View File
@@ -46,6 +46,14 @@ export interface EnqueueRunPayload {
* on a concrete action cue instead of a context-free `Continue.`.
*/
initialUserTurn: string;
/**
* Internal tools main decides to inject for this specific run (C4).
* Currently only CEOs with hire authority receive `hire_agent` and
* `file_approval`; any other agent's `internalTools` is empty. The
* decision is made at dispatch time from the resolved CEO mode so the
* worker never re-derives it.
*/
internalTools: ReadonlyArray<"hire_agent" | "file_approval">;
}
export interface OrchestrationDependencies {