feat(code-mode): route code mode through code_agent_run tool + live approvals

Replace the acpx shell-out with a structured code_agent_run tool that drives the
ACP engine directly, streaming the agent's tool calls / diffs / plan into the chat
and surfacing permission requests inline.

- shared: code-mode.ts zod schemas; add code-run-event + code-run-permission-request
  RunEvent variants (stream to the renderer over the existing runs:events channel);
  codeRun:resolvePermission IPC channel.
- core: CodePermissionRegistry (promise-based mid-run approvals — the LLM tool-loop's
  pre-call gate can't model a mid-execution wait); register codeModeManager +
  codePermissionRegistry in awilix.
- core: code_agent_run builtin tool (streams via ctx.publish, asks via the registry,
  cancels on ctx.signal, returns the agent summary). CodeModeConfig.approvalPolicy
  (ask | auto-approve-reads | yolo; default ask). Exclude the tool from the headless
  background-task / live-note / inline-task agents so they can't block on an approval.
- main: codeRun:resolvePermission handler -> registry.resolve.
- rewrite the code-with-agents skill and the runtime "Code Mode (Active)" block to call
  code_agent_run instead of emitting npx acpx commands.
This commit is contained in:
Gagancreates 2026-06-01 13:57:53 +05:30
parent 99ef643c8e
commit 80ed635300
14 changed files with 293 additions and 137 deletions

View file

@ -0,0 +1,70 @@
import z from "zod";
// Shared zod schemas for the ACP code-mode engine. Single source of truth: the
// core engine re-exports the inferred TS types, and runs.ts builds the RunEvent
// variants that carry these to the renderer.
export const CodingAgent = z.enum(["claude", "codex"]);
export type CodingAgent = z.infer<typeof CodingAgent>;
// How the permission broker answers the agent's requests before any per-tool
// "always allow" memory is applied. `yolo` is the safe, scoped equivalent of
// `claude --dangerously-skip-permissions` (our toggle, not a CLI flag).
export const ApprovalPolicy = z.enum(["ask", "auto-approve-reads", "yolo"]);
export type ApprovalPolicy = z.infer<typeof ApprovalPolicy>;
export const PermissionDecision = z.enum(["allow_once", "allow_always", "reject"]);
export type PermissionDecision = z.infer<typeof PermissionDecision>;
// What the UI needs to render a permission card.
export const PermissionAsk = z.object({
toolCallId: z.string().optional(),
title: z.string(),
kind: z.string().optional(), // tool kind, e.g. "edit" | "execute" | "read"
isRead: z.boolean(),
});
export type PermissionAsk = z.infer<typeof PermissionAsk>;
// Normalized per-run stream items. The engine maps raw ACP session/update
// notifications onto this union; the renderer renders them.
export const CodeRunEvent = z.discriminatedUnion("type", [
// role distinguishes the agent's own output from replayed user turns
// (loadSession streams the whole prior conversation back on resume).
z.object({ type: z.literal("message"), role: z.enum(["agent", "user"]), text: z.string() }),
z.object({ type: z.literal("thought") }),
z.object({
type: z.literal("tool_call"),
id: z.string().optional(),
title: z.string().optional(),
kind: z.string().optional(),
status: z.string().optional(),
}),
z.object({
type: z.literal("tool_call_update"),
id: z.string().optional(),
status: z.string().optional(),
diffs: z.array(z.string()),
}),
z.object({
type: z.literal("plan"),
entries: z.array(z.object({
content: z.string(),
status: z.string().optional(),
priority: z.string().optional(),
})),
}),
z.object({
type: z.literal("permission"),
ask: PermissionAsk,
decision: z.union([PermissionDecision, z.literal("cancelled")]),
auto: z.boolean(),
}),
z.object({ type: z.literal("other"), sessionUpdate: z.string() }),
]);
export type CodeRunEvent = z.infer<typeof CodeRunEvent>;
export const RunPromptResult = z.object({
stopReason: z.string(),
sessionId: z.string(),
});
export type RunPromptResult = z.infer<typeof RunPromptResult>;

View file

@ -19,6 +19,7 @@ import { ZListToolkitsResponse } from './composio.js';
import { BrowserStateSchema } from './browser-control.js';
import { BillingInfoSchema } from './billing.js';
import { EmailBlockSchema, GmailThreadSchema } from './blocks.js';
import { PermissionDecision } from './code-mode.js';
// ============================================================================
// Runtime Validation Schemas (Single Source of Truth)
@ -440,6 +441,16 @@ const ipcSchemas = {
success: z.literal(true),
}),
},
// Answer a mid-run permission request from a code_agent_run coding turn.
'codeRun:resolvePermission': {
req: z.object({
requestId: z.string(),
decision: PermissionDecision,
}),
res: z.object({
success: z.literal(true),
}),
},
'codeMode:checkAgentStatus': {
req: z.null(),
res: z.object({

View file

@ -1,5 +1,6 @@
import { LlmStepStreamEvent } from "./llm-step-events.js";
import { Message, ToolCallPart } from "./message.js";
import { CodeRunEvent as CodeRunEventSchema, PermissionAsk } from "./code-mode.js";
import z from "zod";
const BaseRunEvent = z.object({
@ -110,6 +111,23 @@ export const ToolPermissionResponseEvent = BaseRunEvent.extend({
scope: z.enum(["once", "session", "always"]).optional(),
});
// A structured item from a code_agent_run coding turn (tool call, diff, plan,
// message chunk, resolved permission). Fire-and-forget — rendered live.
export const CodeRunStreamEvent = BaseRunEvent.extend({
type: z.literal("code-run-event"),
toolCallId: z.string(),
event: CodeRunEventSchema,
});
// The coding agent is asking for permission mid-turn and the run is BLOCKED until
// the user answers via `codeRun:resolvePermission` (keyed by requestId).
export const CodeRunPermissionRequestEvent = BaseRunEvent.extend({
type: z.literal("code-run-permission-request"),
toolCallId: z.string(),
requestId: z.string(),
ask: PermissionAsk,
});
export const RunErrorEvent = BaseRunEvent.extend({
type: z.literal("error"),
error: z.string(),
@ -134,6 +152,8 @@ export const RunEvent = z.union([
AskHumanResponseEvent,
ToolPermissionRequestEvent,
ToolPermissionResponseEvent,
CodeRunStreamEvent,
CodeRunPermissionRequestEvent,
RunErrorEvent,
RunStoppedEvent,
]);