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
@@ -29,6 +29,33 @@ import type {
|
||||
BeforeToolCallRpcPayload,
|
||||
TransformContextRpcPayload,
|
||||
} from "./hook-rpc";
|
||||
import { routeInternalToolCall } from "./internal-tools/router";
|
||||
import type { InvokeInternalToolRpcPayload } from "@main/worker/runtime/internal-tools/internal-tool-rpc";
|
||||
|
||||
/**
|
||||
* Side-effect callbacks the internal-tool handlers invoke after a
|
||||
* successful DB write. These come from the same wire-api-server fabric
|
||||
* the HTTP routes funnel through, so thread integration / automation /
|
||||
* renderer domain-change notifications stay identical across both
|
||||
* entry points. Supplied lazily via `setInternalToolCallbacks` because
|
||||
* the graph builds `RuntimeBridge` before the orchestration/thread-
|
||||
* integration layer exists.
|
||||
*/
|
||||
export interface InternalToolCallbacks {
|
||||
onApprovalCreated: (
|
||||
approvalId: string,
|
||||
companyId: string,
|
||||
taskId: string | null,
|
||||
approvalType: string,
|
||||
summary: string,
|
||||
) => void;
|
||||
onApprovalResolved: (
|
||||
approvalId: string,
|
||||
companyId: string,
|
||||
decision: string,
|
||||
) => void;
|
||||
onDomainChanged: () => void;
|
||||
}
|
||||
|
||||
export interface RuntimeBridgeDeps {
|
||||
db: AppDatabase;
|
||||
@@ -46,7 +73,8 @@ type RpcMethod =
|
||||
| "prepareRun"
|
||||
| "beforeToolCall"
|
||||
| "afterToolCall"
|
||||
| "transformContext";
|
||||
| "transformContext"
|
||||
| "invokeInternalTool";
|
||||
|
||||
/**
|
||||
* Single owner of pi runtime side-effects in main.
|
||||
@@ -99,6 +127,7 @@ export class RuntimeBridge {
|
||||
private readonly costWriter: CostWriter;
|
||||
private readonly activeRuns = new Map<string, ActiveRunContext>();
|
||||
private readonly sessionStores = new Map<string, SqliteSessionStore>();
|
||||
private internalToolCallbacks: InternalToolCallbacks | null = null;
|
||||
/**
|
||||
* Per-run inbound-message registry. Primary identity is
|
||||
* `record.id` (the `agent_messages.id`). A monotonically allocated
|
||||
@@ -115,6 +144,20 @@ export class RuntimeBridge {
|
||||
this.costWriter = new CostWriter({ db: deps.db });
|
||||
}
|
||||
|
||||
/**
|
||||
* Late-bind the side-effect callbacks used by the internal-tool
|
||||
* router. The graph must call this exactly once before any tool
|
||||
* invocation fires; missing callbacks at RPC time throw rather than
|
||||
* silently skipping the notification, so a wiring regression surfaces
|
||||
* on the first tool call instead of as a stale-UI ghost.
|
||||
*/
|
||||
setInternalToolCallbacks(callbacks: InternalToolCallbacks): void {
|
||||
if (this.internalToolCallbacks) {
|
||||
throw new Error("RuntimeBridge.setInternalToolCallbacks called twice.");
|
||||
}
|
||||
this.internalToolCallbacks = callbacks;
|
||||
}
|
||||
|
||||
private registerActiveRun(context: ActiveRunContext): void {
|
||||
this.activeRuns.set(context.runId, context);
|
||||
this.getOrCreateStore(context.taskSessionId);
|
||||
@@ -519,12 +562,51 @@ export class RuntimeBridge {
|
||||
return this.handleAfterToolCallRpc(request.runId, request.payload as AfterToolCallRpcPayload);
|
||||
case "transformContext":
|
||||
return this.handleTransformContextRpc(request.runId, request.payload as TransformContextRpcPayload);
|
||||
case "invokeInternalTool":
|
||||
return this.handleInvokeInternalToolRpc(request.runId, request.payload as InvokeInternalToolRpcPayload);
|
||||
}
|
||||
} catch (error) {
|
||||
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
private handleInvokeInternalToolRpc(
|
||||
runId: string,
|
||||
payload: InvokeInternalToolRpcPayload,
|
||||
): { ok: true; result: unknown } | { ok: false; error: string } {
|
||||
const ctx = this.activeRuns.get(runId);
|
||||
if (!ctx) {
|
||||
return { ok: false, error: `invokeInternalTool: no active run ${runId}` };
|
||||
}
|
||||
const callbacks = this.internalToolCallbacks;
|
||||
if (!callbacks) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "invokeInternalTool: internal-tool callbacks not wired — service graph init regression.",
|
||||
};
|
||||
}
|
||||
const result = routeInternalToolCall(
|
||||
{
|
||||
hireAgent: {
|
||||
db: this.deps.db,
|
||||
onApprovalResolved: callbacks.onApprovalResolved,
|
||||
onDomainChanged: callbacks.onDomainChanged,
|
||||
logger: this.deps.logger,
|
||||
},
|
||||
fileApproval: {
|
||||
db: this.deps.db,
|
||||
onApprovalCreated: callbacks.onApprovalCreated,
|
||||
onApprovalResolved: callbacks.onApprovalResolved,
|
||||
onDomainChanged: callbacks.onDomainChanged,
|
||||
logger: this.deps.logger,
|
||||
},
|
||||
},
|
||||
{ companyId: ctx.companyId, agentId: ctx.agentId },
|
||||
payload,
|
||||
);
|
||||
return { ok: true, result };
|
||||
}
|
||||
|
||||
private async handlePrepareRunRpc(
|
||||
runId: string,
|
||||
payloadRaw: unknown,
|
||||
|
||||
Reference in new issue
Block a user