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 "@/agent/tools";
|
||||||
import { useChatStore } from "@/stores/chat-store";
|
import { useChatStore } from "@/stores/chat-store";
|
||||||
import { useAgentStore } from "@/stores/agent-store";
|
import { useAgentStore } from "@/stores/agent-store";
|
||||||
|
import { usePlanStore } from "@/stores/plan-store";
|
||||||
|
|
||||||
const MAX_ITERATIONS = 20;
|
const MAX_ITERATIONS = 20;
|
||||||
|
|
||||||
|
|
@ -33,12 +34,6 @@ const WRITE_TOOLS = new Set([
|
||||||
"update_keyframe_curve",
|
"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"]);
|
const EXECUTE_ONLY_TOOLS = new Set(["update_plan_step"]);
|
||||||
|
|
||||||
interface APIResponse {
|
interface APIResponse {
|
||||||
|
|
@ -221,10 +216,25 @@ async function resolveToolCalls(
|
||||||
): Promise<ToolResult[]> {
|
): Promise<ToolResult[]> {
|
||||||
const agentStore = useAgentStore.getState();
|
const agentStore = useAgentStore.getState();
|
||||||
const results: ToolResult[] = [];
|
const results: ToolResult[] = [];
|
||||||
const currentMode = useAgentStore.getState().mode;
|
|
||||||
|
|
||||||
for (const tc of toolCalls) {
|
for (const tc of toolCalls) {
|
||||||
useAgentStore.getState().setActiveTool(tc.name);
|
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)) {
|
if (currentMode === "plan" && WRITE_TOOLS.has(tc.name)) {
|
||||||
results.push({
|
results.push({
|
||||||
|
|
@ -246,16 +256,6 @@ async function resolveToolCalls(
|
||||||
continue;
|
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;
|
const currentPermissionMode = useAgentStore.getState().permissionMode;
|
||||||
if (currentPermissionMode === "ask" && WRITE_TOOLS.has(tc.name)) {
|
if (currentPermissionMode === "ask" && WRITE_TOOLS.has(tc.name)) {
|
||||||
const approved = await requestApproval(tc);
|
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.
|
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.
|
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.
|
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:
|
## PLAN QUALITY RULES:
|
||||||
|
|
||||||
|
|
@ -37,13 +38,24 @@ Submit the plan when:
|
||||||
- All clarifying questions have been answered
|
- All clarifying questions have been answered
|
||||||
- You have a concrete, actionable plan
|
- 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 = `
|
const EXECUTE_MODE_INSTRUCTIONS = `
|
||||||
## CURRENT MODE: EXECUTE
|
## 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:
|
## 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:
|
## 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(
|
export function buildSystemPrompt(
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,8 @@ const requestPlanApprovalTool: ToolDefinition = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const agentStore = useAgentStore.getState();
|
useAgentStore.getState().setMode("plan");
|
||||||
if (agentStore.mode !== "plan") {
|
planStore.updatePlanStatus("awaiting_approval");
|
||||||
return { error: "Already in execute mode. No transition needed." };
|
|
||||||
}
|
|
||||||
|
|
||||||
planStore.updatePlanStatus("approved");
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
useAgentStore.getState().setPendingModeTransition({
|
useAgentStore.getState().setPendingModeTransition({
|
||||||
|
|
@ -32,6 +28,10 @@ const requestPlanApprovalTool: ToolDefinition = {
|
||||||
useAgentStore.getState().setPendingModeTransition(null);
|
useAgentStore.getState().setPendingModeTransition(null);
|
||||||
if (approved) {
|
if (approved) {
|
||||||
useAgentStore.getState().setMode("execute");
|
useAgentStore.getState().setMode("execute");
|
||||||
|
usePlanStore.getState().updatePlanStatus("executing");
|
||||||
|
} else {
|
||||||
|
useAgentStore.getState().setMode("plan");
|
||||||
|
usePlanStore.getState().updatePlanStatus("awaiting_approval");
|
||||||
}
|
}
|
||||||
resolve({
|
resolve({
|
||||||
approved,
|
approved,
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ export const loadSkillSchema: ToolSchema = {
|
||||||
export const submitPlanSchema: ToolSchema = {
|
export const submitPlanSchema: ToolSchema = {
|
||||||
name: "submit_plan",
|
name: "submit_plan",
|
||||||
description:
|
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: [
|
parameters: [
|
||||||
{ key: "summary", type: "string", required: true },
|
{ key: "summary", type: "string", required: true },
|
||||||
{ key: "steps", type: "array", required: true },
|
{ key: "steps", type: "array", required: true },
|
||||||
|
|
@ -319,7 +319,7 @@ export const submitPlanSchema: ToolSchema = {
|
||||||
export const askUserSchema: ToolSchema = {
|
export const askUserSchema: ToolSchema = {
|
||||||
name: "ask_user",
|
name: "ask_user",
|
||||||
description:
|
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: [
|
parameters: [
|
||||||
{ key: "question", type: "string", required: true },
|
{ key: "question", type: "string", required: true },
|
||||||
{ key: "options", type: "array", required: false },
|
{ key: "options", type: "array", required: false },
|
||||||
|
|
@ -329,7 +329,7 @@ export const askUserSchema: ToolSchema = {
|
||||||
export const requestPlanApprovalSchema: ToolSchema = {
|
export const requestPlanApprovalSchema: ToolSchema = {
|
||||||
name: "request_plan_approval",
|
name: "request_plan_approval",
|
||||||
description:
|
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: [],
|
parameters: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,26 @@ import type { AgentContext, ToolDefinition } from "@/agent/types";
|
||||||
import { toolRegistry } from "@/agent/tools/registry";
|
import { toolRegistry } from "@/agent/tools/registry";
|
||||||
import { submitPlanSchema } from "@/agent/tools/schemas";
|
import { submitPlanSchema } from "@/agent/tools/schemas";
|
||||||
import { usePlanStore, createPlanFromSteps } from "@/stores/plan-store";
|
import { usePlanStore, createPlanFromSteps } from "@/stores/plan-store";
|
||||||
|
import { useAgentStore } from "@/stores/agent-store";
|
||||||
|
|
||||||
|
type SubmittedStep = { description: string; tools: string[] };
|
||||||
|
|
||||||
const submitPlanTool: ToolDefinition = {
|
const submitPlanTool: ToolDefinition = {
|
||||||
...submitPlanSchema,
|
...submitPlanSchema,
|
||||||
execute: async (
|
execute: async (
|
||||||
args: Record<string, unknown>,
|
args: Record<string, unknown>,
|
||||||
_context: AgentContext,
|
_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 {
|
const { summary, steps, questions } = args as {
|
||||||
summary: string;
|
summary: string;
|
||||||
steps: Array<{ description: string; tools: string[] }>;
|
steps: unknown;
|
||||||
questions?: string[];
|
questions?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -19,11 +29,14 @@ const submitPlanTool: ToolDefinition = {
|
||||||
return { error: "summary is required and must be a string" };
|
return { error: "summary is required and must be a string" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(steps) || steps.length === 0) {
|
const normalizedSteps = normalizeSteps(steps);
|
||||||
return { error: "steps must be a non-empty array" };
|
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") {
|
if (!step.description || typeof step.description !== "string") {
|
||||||
return { error: `Step ${i + 1} must have a 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);
|
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);
|
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"
|
className="mr-2 size-4"
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
/>
|
/>
|
||||||
Start editing
|
Go edit
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -95,7 +95,7 @@ export function ModeTransitionModal() {
|
||||||
className="mr-2 size-4"
|
className="mr-2 size-4"
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
/>
|
/>
|
||||||
Continue planning
|
Keep planning
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue