mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-24 20:28:16 +02:00
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:
parent
99ef643c8e
commit
80ed635300
14 changed files with 293 additions and 137 deletions
70
apps/x/packages/shared/src/code-mode.ts
Normal file
70
apps/x/packages/shared/src/code-mode.ts
Normal 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>;
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue