From 3e912159e1bb72ecc5e5d4a066d7b3f195eeccd3 Mon Sep 17 00:00:00 2001 From: Luis Esteban Acevedo Ladino Date: Fri, 1 May 2026 17:53:57 -0500 Subject: [PATCH] fix: require plan approval before complex edits --- apps/web/src/agent/orchestrator.ts | 34 +++++----- apps/web/src/agent/system-prompt.ts | 18 +++++- .../agent/tools/request-plan-approval.tool.ts | 12 ++-- apps/web/src/agent/tools/schemas.ts | 6 +- apps/web/src/agent/tools/submit-plan.tool.ts | 63 ++++++++++++++++--- .../panels/chat/mode-transition-modal.tsx | 4 +- 6 files changed, 99 insertions(+), 38 deletions(-) diff --git a/apps/web/src/agent/orchestrator.ts b/apps/web/src/agent/orchestrator.ts index 840e1635..df4a800d 100644 --- a/apps/web/src/agent/orchestrator.ts +++ b/apps/web/src/agent/orchestrator.ts @@ -9,6 +9,7 @@ import { toolRegistry } from "@/agent/tools/registry"; import "@/agent/tools"; import { useChatStore } from "@/stores/chat-store"; import { useAgentStore } from "@/stores/agent-store"; +import { usePlanStore } from "@/stores/plan-store"; const MAX_ITERATIONS = 20; @@ -33,12 +34,6 @@ const WRITE_TOOLS = new Set([ "update_keyframe_curve", ]); -const PLAN_ONLY_TOOLS = new Set([ - "submit_plan", - "ask_user", - "request_plan_approval", -]); - const EXECUTE_ONLY_TOOLS = new Set(["update_plan_step"]); interface APIResponse { @@ -221,10 +216,25 @@ async function resolveToolCalls( ): Promise { const agentStore = useAgentStore.getState(); const results: ToolResult[] = []; - const currentMode = useAgentStore.getState().mode; for (const tc of toolCalls) { useAgentStore.getState().setActiveTool(tc.name); + const currentMode = useAgentStore.getState().mode; + const currentPlan = usePlanStore.getState().plan; + + if ( + WRITE_TOOLS.has(tc.name) && + currentPlan?.status === "awaiting_approval" + ) { + results.push({ + toolCallId: tc.id, + name: tc.name, + result: null, + error: + "Cannot edit while a plan is awaiting user approval. Call request_plan_approval and wait for the user to choose Go edit.", + }); + continue; + } if (currentMode === "plan" && WRITE_TOOLS.has(tc.name)) { results.push({ @@ -246,16 +256,6 @@ async function resolveToolCalls( continue; } - if (currentMode === "execute" && PLAN_ONLY_TOOLS.has(tc.name)) { - results.push({ - toolCallId: tc.id, - name: tc.name, - result: null, - error: `Cannot use '${tc.name}' in execute mode. This tool is only available during planning.`, - }); - continue; - } - const currentPermissionMode = useAgentStore.getState().permissionMode; if (currentPermissionMode === "ask" && WRITE_TOOLS.has(tc.name)) { const approved = await requestApproval(tc); diff --git a/apps/web/src/agent/system-prompt.ts b/apps/web/src/agent/system-prompt.ts index 5dd5449e..88f22752 100644 --- a/apps/web/src/agent/system-prompt.ts +++ b/apps/web/src/agent/system-prompt.ts @@ -21,6 +21,7 @@ You are in PLAN mode. You CANNOT make any edits to the timeline. You can only: 2. **Discover**: If the user's request matches a known editing pattern, call list_skills to find relevant techniques. 3. **Clarify**: If anything is ambiguous, use ask_user to get clarification before planning. 4. **Plan**: Once you understand the footage and the goal, call submit_plan with a structured step-by-step plan. +5. **Approval**: Immediately after submit_plan succeeds, call request_plan_approval. Do NOT edit until the user approves. ## PLAN QUALITY RULES: @@ -37,13 +38,24 @@ Submit the plan when: - All clarifying questions have been answered - You have a concrete, actionable plan -After submitting, the user will review and either approve (switching to execute mode) or request changes. +After submitting, you MUST ask for approval through request_plan_approval. The user will choose Keep planning or Go edit. `; const EXECUTE_MODE_INSTRUCTIONS = ` ## CURRENT MODE: EXECUTE -You are in EXECUTE mode. You can read AND write to the timeline. Follow the approved plan step by step. +You are in EXECUTE mode. You can read AND write to the timeline, but complex editing requests still require a plan first. + +## MANDATORY PLANNING GATE + +For any complex editing request that would require multiple timeline edits (cuts, moving media, adding titles/effects, or touching 3+ tools), you MUST: + +1. Analyze the project with read-only tools. +2. Call submit_plan with a concise structured checklist. +3. Call request_plan_approval. +4. Wait for the user to choose Go edit. + +Do NOT perform write tools for complex edits before approval. This is required even if the current mode is EXECUTE. ## EXECUTION RULES: @@ -55,7 +67,7 @@ You are in EXECUTE mode. You can read AND write to the timeline. Follow the appr ## IF NO ACTIVE PLAN: -Execute the user's request directly. For complex edits (3+ tool calls), consider using submit_plan first to get user approval. +Simple one-shot edits may execute directly. Complex edits must go through submit_plan + request_plan_approval first. `; export function buildSystemPrompt( diff --git a/apps/web/src/agent/tools/request-plan-approval.tool.ts b/apps/web/src/agent/tools/request-plan-approval.tool.ts index a75773d5..e00b415c 100644 --- a/apps/web/src/agent/tools/request-plan-approval.tool.ts +++ b/apps/web/src/agent/tools/request-plan-approval.tool.ts @@ -18,12 +18,8 @@ const requestPlanApprovalTool: ToolDefinition = { }; } - const agentStore = useAgentStore.getState(); - if (agentStore.mode !== "plan") { - return { error: "Already in execute mode. No transition needed." }; - } - - planStore.updatePlanStatus("approved"); + useAgentStore.getState().setMode("plan"); + planStore.updatePlanStatus("awaiting_approval"); return new Promise((resolve) => { useAgentStore.getState().setPendingModeTransition({ @@ -32,6 +28,10 @@ const requestPlanApprovalTool: ToolDefinition = { useAgentStore.getState().setPendingModeTransition(null); if (approved) { useAgentStore.getState().setMode("execute"); + usePlanStore.getState().updatePlanStatus("executing"); + } else { + useAgentStore.getState().setMode("plan"); + usePlanStore.getState().updatePlanStatus("awaiting_approval"); } resolve({ approved, diff --git a/apps/web/src/agent/tools/schemas.ts b/apps/web/src/agent/tools/schemas.ts index 69a4825a..175579c9 100644 --- a/apps/web/src/agent/tools/schemas.ts +++ b/apps/web/src/agent/tools/schemas.ts @@ -308,7 +308,7 @@ export const loadSkillSchema: ToolSchema = { export const submitPlanSchema: ToolSchema = { name: "submit_plan", description: - "Submits a structured editing plan with steps. Use ONLY in plan mode after analyzing the footage and understanding the user's request. Each step should describe a specific action with the tools to use. After submission, the user reviews and approves or requests changes before execution begins.", + "Submits a structured editing plan with steps. Use this before complex edits, even from execute mode. Each step should describe a specific action with the tools to use. After submission, call request_plan_approval and wait for the user before any write tools.", parameters: [ { key: "summary", type: "string", required: true }, { key: "steps", type: "array", required: true }, @@ -319,7 +319,7 @@ export const submitPlanSchema: ToolSchema = { export const askUserSchema: ToolSchema = { name: "ask_user", description: - "Asks the user a question during plan mode. Use when you need clarification about their intent, preferences, or content details before finalizing the plan. Optionally provide quick-reply options for common answers. The user can also type a free-form response.", + "Asks the user a question before or during planning. Use when you need clarification about their intent, preferences, or content details before finalizing the plan. Optionally provide quick-reply options for common answers. The user can also type a free-form response.", parameters: [ { key: "question", type: "string", required: true }, { key: "options", type: "array", required: false }, @@ -329,7 +329,7 @@ export const askUserSchema: ToolSchema = { export const requestPlanApprovalSchema: ToolSchema = { name: "request_plan_approval", description: - "Requests user approval to switch from plan mode to execute mode. The user sees a modal with 'Continue planning' and 'Start editing' options. ALWAYS requires explicit user approval — there is no auto-approve. Use this when the plan is ready for execution.", + "Requests user approval to switch from planning to editing. The user sees a modal with Keep planning and Go edit options. ALWAYS requires explicit user approval — there is no auto-approve. Use immediately after submit_plan succeeds.", parameters: [], }; diff --git a/apps/web/src/agent/tools/submit-plan.tool.ts b/apps/web/src/agent/tools/submit-plan.tool.ts index 5926a5e7..b2668936 100644 --- a/apps/web/src/agent/tools/submit-plan.tool.ts +++ b/apps/web/src/agent/tools/submit-plan.tool.ts @@ -2,16 +2,26 @@ import type { AgentContext, ToolDefinition } from "@/agent/types"; import { toolRegistry } from "@/agent/tools/registry"; import { submitPlanSchema } from "@/agent/tools/schemas"; import { usePlanStore, createPlanFromSteps } from "@/stores/plan-store"; +import { useAgentStore } from "@/stores/agent-store"; + +type SubmittedStep = { description: string; tools: string[] }; const submitPlanTool: ToolDefinition = { ...submitPlanSchema, execute: async ( args: Record, _context: AgentContext, - ): Promise<{ planId: string; stepCount: number } | { error: string }> => { + ): Promise< + | { + planId: string; + stepCount: number; + nextAction: "request_plan_approval"; + } + | { error: string } + > => { const { summary, steps, questions } = args as { summary: string; - steps: Array<{ description: string; tools: string[] }>; + steps: unknown; questions?: string[]; }; @@ -19,11 +29,14 @@ const submitPlanTool: ToolDefinition = { return { error: "summary is required and must be a string" }; } - if (!Array.isArray(steps) || steps.length === 0) { - return { error: "steps must be a non-empty array" }; + const normalizedSteps = normalizeSteps(steps); + if (normalizedSteps.length === 0) { + return { + error: "steps must be a non-empty array or a numbered list string", + }; } - for (const [i, step] of steps.entries()) { + for (const [i, step] of normalizedSteps.entries()) { if (!step.description || typeof step.description !== "string") { return { error: `Step ${i + 1} must have a description string` }; } @@ -32,11 +45,47 @@ const submitPlanTool: ToolDefinition = { } } - const plan = createPlanFromSteps(summary, steps, questions); + const plan = createPlanFromSteps(summary, normalizedSteps, questions); usePlanStore.getState().setPlan(plan); + useAgentStore.getState().setMode("plan"); - return { planId: plan.id, stepCount: plan.steps.length }; + return { + planId: plan.id, + stepCount: plan.steps.length, + nextAction: "request_plan_approval", + }; }, }; toolRegistry.register(submitPlanSchema.name, submitPlanTool); + +function normalizeSteps(steps: unknown): SubmittedStep[] { + if (Array.isArray(steps)) { + return steps + .map((step) => { + if (typeof step === "string") { + return { description: step, tools: [] }; + } + if (typeof step !== "object" || step === null) return null; + const raw = step as { description?: unknown; tools?: unknown }; + return { + description: + typeof raw.description === "string" ? raw.description : "", + tools: Array.isArray(raw.tools) + ? raw.tools.filter( + (tool): tool is string => typeof tool === "string", + ) + : [], + }; + }) + .filter((step): step is SubmittedStep => step !== null); + } + + if (typeof steps !== "string") return []; + + return steps + .split("\n") + .map((line) => line.replace(/^\s*\d+[.)-]?\s*/, "").trim()) + .filter(Boolean) + .map((description) => ({ description, tools: [] })); +} diff --git a/apps/web/src/components/editor/panels/chat/mode-transition-modal.tsx b/apps/web/src/components/editor/panels/chat/mode-transition-modal.tsx index 9289938b..1f2c14af 100644 --- a/apps/web/src/components/editor/panels/chat/mode-transition-modal.tsx +++ b/apps/web/src/components/editor/panels/chat/mode-transition-modal.tsx @@ -82,7 +82,7 @@ export function ModeTransitionModal() { className="mr-2 size-4" strokeWidth={1.5} /> - Start editing + Go edit