fix: require plan approval before complex edits
This commit is contained in:
parent
6d313cfdc4
commit
3e912159e1
|
|
@ -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<ToolResult[]> {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>,
|
||||
_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: [] }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export function ModeTransitionModal() {
|
|||
className="mr-2 size-4"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
Start editing
|
||||
Go edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -95,7 +95,7 @@ export function ModeTransitionModal() {
|
|||
className="mr-2 size-4"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
Continue planning
|
||||
Keep planning
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue