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
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user