From 17626613099ac24a00aafcd9e4e2cfe721fd80c1 Mon Sep 17 00:00:00 2001 From: Luis Esteban Acevedo Ladino Date: Fri, 24 Apr 2026 21:04:42 -0500 Subject: [PATCH] feat: add agent delete tool --- .../src/agent/__tests__/orchestrator.test.ts | 42 ++++++++++- apps/web/src/agent/context.ts | 55 +++++++++++++++ apps/web/src/agent/orchestrator.ts | 10 ++- .../agent/providers/__tests__/gemini.test.ts | 3 + .../__tests__/openai-compatible.test.ts | 55 ++++++++++++--- apps/web/src/agent/providers/gemini.ts | 3 + .../src/agent/providers/openai-compatible.ts | 30 ++++---- .../delete-timeline-elements.test.ts | 69 +++++++++++++++++++ .../tools/delete-timeline-elements.tool.ts | 44 ++++++++++++ .../api/agent/chat/__tests__/route.test.ts | 3 +- apps/web/src/app/api/agent/chat/route.ts | 6 ++ .../editor/panels/chat/tool-formatters.ts | 25 +++++++ docs/agent-tool-specs.md | 51 +++++++++++--- docs/agent-tools.md | 19 ++--- propuesta_tecnica.md | 9 +-- 15 files changed, 378 insertions(+), 46 deletions(-) create mode 100644 apps/web/src/agent/tools/__tests__/delete-timeline-elements.test.ts create mode 100644 apps/web/src/agent/tools/delete-timeline-elements.tool.ts diff --git a/apps/web/src/agent/__tests__/orchestrator.test.ts b/apps/web/src/agent/__tests__/orchestrator.test.ts index d4511d1e..0392e15c 100644 --- a/apps/web/src/agent/__tests__/orchestrator.test.ts +++ b/apps/web/src/agent/__tests__/orchestrator.test.ts @@ -63,6 +63,13 @@ const testTool: ToolDefinition = { execute: async (args) => ({ echoed: args.input, flag: args.flag }), }; +const stringArrayTool: ToolDefinition = { + name: "_test_string_array_tool", + description: "A test tool with string array params", + parameters: [{ key: "ids", type: "string[]", required: true }], + execute: async (args) => ({ ids: args.ids }), +}; + /** Tool that intentionally throws during execution — used to prove the * orchestrator's resolveToolCalls catch block works at runtime. */ const throwingTool: ToolDefinition = { @@ -78,6 +85,7 @@ const throwingTool: ToolDefinition = { // Register once (registry is a singleton — re-registering overwrites) toolRegistry.register("_test_tool", testTool); +toolRegistry.register("_test_string_array_tool", stringArrayTool); toolRegistry.register("_test_throwing_tool", throwingTool); // --------------------------------------------------------------------------- @@ -221,8 +229,8 @@ describe("orchestrator", () => { const chat = useChatStore.getState(); const agent = useAgentStore.getState(); - // 8 iterations × (1 assistant + 1 tool_result) + 1 cap message = 17 - expect(chat.messages).toHaveLength(17); + // 20 iterations × (1 assistant + 1 tool_result) + 1 cap message = 41 + expect(chat.messages).toHaveLength(41); // Last message is the cap message const lastMsg = chat.messages[chat.messages.length - 1]; @@ -360,6 +368,36 @@ describe("orchestrator", () => { expect(toolResult.content).toContain("must be a boolean"); }); + test("validates string array arguments", async () => { + let callCount = 0; + setMockFetch(() => { + callCount++; + if (callCount === 1) { + return mockFetchResponse({ + content: "Running tool", + toolCalls: [ + { + id: "tc_string_array", + name: "_test_string_array_tool", + args: { ids: ["a", 2] }, + }, + ], + }); + } + return mockFetchResponse({ content: "Done" }); + }); + + await run( + [{ id: "1", role: "user", content: "Test", timestamp: Date.now() }], + MOCK_CONTEXT, + ); + + const toolResult = useChatStore.getState().messages[1]; + expect(toolResult.role).toBe("tool_result"); + expect(toolResult.content).toContain("ids"); + expect(toolResult.content).toContain("must be an array of strings"); + }); + // ----------------------------------------------------------------------- // Error paths // ----------------------------------------------------------------------- diff --git a/apps/web/src/agent/context.ts b/apps/web/src/agent/context.ts index 8b4710a0..1c61921e 100644 --- a/apps/web/src/agent/context.ts +++ b/apps/web/src/agent/context.ts @@ -72,6 +72,39 @@ export const EditorContextAdapter = { }); return { success: true, affectedElements }; }, + + deleteTimelineElements({ + elementIds, + }: { + elementIds: string[]; + }): { success: boolean; deletedElements: string[] } | { error: string } { + const core = EditorCore.getInstance(); + const activeScene = core.scenes.getActiveSceneOrNull(); + if (!activeScene) { + return { error: "No active timeline" }; + } + + if (!hasTimelineContent(activeScene.tracks)) { + return { error: "No timeline content" }; + } + + const requestedIds = [...new Set(elementIds)]; + const elements = findTimelineElementsByIds({ + tracks: activeScene.tracks, + elementIds: requestedIds, + }); + const foundIds = new Set(elements.map((element) => element.elementId)); + const missingIds = requestedIds.filter( + (elementId) => !foundIds.has(elementId), + ); + + if (missingIds.length > 0) { + return { error: `Timeline elements not found: ${missingIds.join(", ")}` }; + } + + core.timeline.deleteElements({ elements }); + return { success: true, deletedElements: requestedIds }; + }, }; function secondsToTicks(seconds: number): number { @@ -86,4 +119,26 @@ function hasTimelineContent(tracks: SceneTracks): boolean { ); } +function findTimelineElementsByIds({ + tracks, + elementIds, +}: { + tracks: SceneTracks; + elementIds: string[]; +}): Array<{ trackId: string; elementId: string }> { + const requestedIds = new Set(elementIds); + const result: Array<{ trackId: string; elementId: string }> = []; + const allTracks = [tracks.main, ...tracks.overlay, ...tracks.audio]; + + for (const track of allTracks) { + for (const element of track.elements) { + if (requestedIds.has(element.id)) { + result.push({ trackId: track.id, elementId: element.id }); + } + } + } + + return result; +} + export { buildSystemPrompt } from "@/agent/system-prompt"; diff --git a/apps/web/src/agent/orchestrator.ts b/apps/web/src/agent/orchestrator.ts index a3164e82..6b2065a9 100644 --- a/apps/web/src/agent/orchestrator.ts +++ b/apps/web/src/agent/orchestrator.ts @@ -10,10 +10,11 @@ import "@/agent/tools/load-context.tool"; import "@/agent/tools/list-project-assets.tool"; import "@/agent/tools/list-timeline.tool"; import "@/agent/tools/split.tool"; +import "@/agent/tools/delete-timeline-elements.tool"; import { useChatStore } from "@/stores/chat-store"; import { useAgentStore } from "@/stores/agent-store"; -const MAX_ITERATIONS = 8; +const MAX_ITERATIONS = 20; interface APIResponse { content: string; @@ -167,6 +168,13 @@ function validateToolArgs( ) { return `Argument "${param.key}" must be an array of numbers`; } + if ( + param.type === "string[]" && + (!Array.isArray(value) || + !value.every((item) => typeof item === "string")) + ) { + return `Argument "${param.key}" must be an array of strings`; + } } return null; diff --git a/apps/web/src/agent/providers/__tests__/gemini.test.ts b/apps/web/src/agent/providers/__tests__/gemini.test.ts index 2b4e30e3..45cc44aa 100644 --- a/apps/web/src/agent/providers/__tests__/gemini.test.ts +++ b/apps/web/src/agent/providers/__tests__/gemini.test.ts @@ -310,6 +310,7 @@ describe("toGeminiTools", () => { { key: "name", type: "string", required: true }, { key: "count", type: "number", required: false }, { key: "times", type: "number[]", required: false }, + { key: "elementIds", type: "string[]", required: false }, { key: "active", type: "boolean", required: false }, { key: "meta", type: "object", required: false }, ], @@ -326,6 +327,8 @@ describe("toGeminiTools", () => { expect(props.count.type).toBe(SchemaType.NUMBER); expect(props.times.type).toBe(SchemaType.ARRAY); expect(props.times.items?.type).toBe(SchemaType.NUMBER); + expect(props.elementIds.type).toBe(SchemaType.ARRAY); + expect(props.elementIds.items?.type).toBe(SchemaType.STRING); expect(props.active.type).toBe(SchemaType.BOOLEAN); expect(props.meta.type).toBe(SchemaType.OBJECT); }); diff --git a/apps/web/src/agent/providers/__tests__/openai-compatible.test.ts b/apps/web/src/agent/providers/__tests__/openai-compatible.test.ts index 0139ffb0..258f5ab4 100644 --- a/apps/web/src/agent/providers/__tests__/openai-compatible.test.ts +++ b/apps/web/src/agent/providers/__tests__/openai-compatible.test.ts @@ -35,9 +35,7 @@ const SIMPLE_TOOLS: ToolDefinition[] = [ { name: "test_tool", description: "A test tool", - parameters: [ - { key: "input", type: "string", required: true }, - ], + parameters: [{ key: "input", type: "string", required: true }], execute: async () => ({}), }, ]; @@ -117,6 +115,49 @@ describe("OpenAICompatibleAdapter.chat()", () => { }); }); + test("maps array parameter schemas to OpenAI format", async () => { + mockCreate.mockResolvedValueOnce(makeOpenAIResponse({ content: "ok" })); + + const adapter = new OpenAICompatibleAdapter(TEST_CONFIG); + await adapter.chat({ + messages: [{ id: "1", role: "user", content: "Run tool", timestamp: 0 }], + systemPrompt: "System", + tools: [ + { + name: "delete_timeline_elements", + description: "Delete elements", + parameters: [ + { key: "elementIds", type: "string[]", required: true }, + { key: "times", type: "number[]", required: false }, + ], + }, + ], + }); + + const callArgs = mockCreate.mock.calls[0][0] as { + tools: Array<{ + function: { + parameters: { + properties: Record< + string, + { type: string; items?: { type: string } } + >; + }; + }; + }>; + }; + + const props = callArgs.tools[0].function.parameters.properties; + expect(props.elementIds).toEqual({ + type: "array", + items: { type: "string" }, + }); + expect(props.times).toEqual({ + type: "array", + items: { type: "number" }, + }); + }); + test("prepends system prompt as first message", async () => { mockCreate.mockResolvedValueOnce(makeOpenAIResponse({ content: "ok" })); @@ -141,9 +182,7 @@ describe("OpenAICompatibleAdapter.chat()", () => { const adapter = new OpenAICompatibleAdapter(TEST_CONFIG); await adapter.chat({ - messages: [ - { id: "1", role: "user", content: "Hello", timestamp: 0 }, - ], + messages: [{ id: "1", role: "user", content: "Hello", timestamp: 0 }], systemPrompt: "System", tools: [], }); @@ -168,9 +207,7 @@ describe("OpenAICompatibleAdapter.chat()", () => { id: "1", role: "assistant", content: "Let me check.", - toolCalls: [ - { id: "tc_1", name: "test_tool", args: { input: "x" } }, - ], + toolCalls: [{ id: "tc_1", name: "test_tool", args: { input: "x" } }], timestamp: 0, }, ], diff --git a/apps/web/src/agent/providers/gemini.ts b/apps/web/src/agent/providers/gemini.ts index a4a7006e..c31877d0 100644 --- a/apps/web/src/agent/providers/gemini.ts +++ b/apps/web/src/agent/providers/gemini.ts @@ -258,6 +258,9 @@ function toGeminiParameterSchema(type: string): Schema { if (type === "number[]") { return { type: SchemaType.ARRAY, items: { type: SchemaType.NUMBER } }; } + if (type === "string[]") { + return { type: SchemaType.ARRAY, items: { type: SchemaType.STRING } }; + } return { type: SchemaType.STRING }; } diff --git a/apps/web/src/agent/providers/openai-compatible.ts b/apps/web/src/agent/providers/openai-compatible.ts index 7a2423b6..03b79491 100644 --- a/apps/web/src/agent/providers/openai-compatible.ts +++ b/apps/web/src/agent/providers/openai-compatible.ts @@ -17,7 +17,7 @@ interface OpenAIFunctionTool { description: string; parameters: { type: "object"; - properties: Record; + properties: Record; required?: string[]; }; }; @@ -83,18 +83,25 @@ function toOpenAIFunctions(tools: ToolSchema[]): OpenAIFunctionTool[] { parameters: { type: "object" as const, properties: Object.fromEntries( - tool.parameters.map((p) => [p.key, { type: p.type }]), + tool.parameters.map((p) => [p.key, toOpenAIParameterSchema(p.type)]), ), ...(tool.parameters.some((p) => p.required) && { - required: tool.parameters - .filter((p) => p.required) - .map((p) => p.key), + required: tool.parameters.filter((p) => p.required).map((p) => p.key), }), }, }, })); } +function toOpenAIParameterSchema(type: string): { + type: string; + items?: { type: string }; +} { + if (type === "number[]") return { type: "array", items: { type: "number" } }; + if (type === "string[]") return { type: "array", items: { type: "string" } }; + return { type }; +} + // --------------------------------------------------------------------------- // Adapter implementation // --------------------------------------------------------------------------- @@ -138,8 +145,7 @@ export class OpenAICompatibleAdapter implements ProviderAdapter { requestParams.tools = openaiTools; } - const response = - await this.client.chat.completions.create(requestParams); + const response = await this.client.chat.completions.create(requestParams); const choice = response.choices[0]; if (!choice) { @@ -149,12 +155,12 @@ export class OpenAICompatibleAdapter implements ProviderAdapter { const content = choice.message.content ?? ""; let toolCalls: ToolCall[] | undefined; - if ( - choice.message.tool_calls && - choice.message.tool_calls.length > 0 - ) { + if (choice.message.tool_calls && choice.message.tool_calls.length > 0) { toolCalls = choice.message.tool_calls - .filter((tc): tc is Extract => tc.type === "function") + .filter( + (tc): tc is Extract => + tc.type === "function", + ) .map((tc) => ({ id: tc.id, name: tc.function.name, diff --git a/apps/web/src/agent/tools/__tests__/delete-timeline-elements.test.ts b/apps/web/src/agent/tools/__tests__/delete-timeline-elements.test.ts new file mode 100644 index 00000000..e92c5e5e --- /dev/null +++ b/apps/web/src/agent/tools/__tests__/delete-timeline-elements.test.ts @@ -0,0 +1,69 @@ +import { beforeEach, describe, expect, mock, test } from "bun:test"; +import type { AgentContext } from "@/agent/types"; +import { toolRegistry } from "@/agent/tools/registry"; + +const mockDeleteTimelineElements = mock((_args: { elementIds: string[] }) => ({ + success: true, + deletedElements: ["clip-1", "text-1"], +})); + +mock.module("@/agent/context", () => ({ + EditorContextAdapter: { + deleteTimelineElements: mockDeleteTimelineElements, + }, +})); + +await import("@/agent/tools/delete-timeline-elements.tool"); + +const context: AgentContext = { + projectId: "proj-1", + activeSceneId: "scene-A", + mediaAssets: [], + playbackTimeMs: 0, +}; + +describe("delete_timeline_elements tool", () => { + beforeEach(() => { + mockDeleteTimelineElements.mockClear(); + }); + + test("is registered in the tool registry", () => { + expect(toolRegistry.has("delete_timeline_elements")).toBe(true); + }); + + test("deletes timeline elements through the editor adapter", async () => { + const tool = toolRegistry.get("delete_timeline_elements"); + const result = await tool.execute( + { elementIds: ["clip-1", "text-1"] }, + context, + ); + + expect(mockDeleteTimelineElements).toHaveBeenCalledWith({ + elementIds: ["clip-1", "text-1"], + }); + expect(result).toEqual({ + success: true, + deletedElements: ["clip-1", "text-1"], + }); + }); + + test("validates element ids", async () => { + const tool = toolRegistry.get("delete_timeline_elements"); + + expect(await tool.execute({ elementIds: [] }, context)).toEqual({ + error: "Invalid element ids", + }); + expect(await tool.execute({ elementIds: ["clip-1", ""] }, context)).toEqual( + { + error: "Invalid element ids", + }, + ); + expect(await tool.execute({ elementIds: ["clip-1", 2] }, context)).toEqual({ + error: "Invalid element ids", + }); + expect(await tool.execute({ elementIds: "clip-1" }, context)).toEqual({ + error: "Invalid element ids", + }); + expect(mockDeleteTimelineElements).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/agent/tools/delete-timeline-elements.tool.ts b/apps/web/src/agent/tools/delete-timeline-elements.tool.ts new file mode 100644 index 00000000..13922a3e --- /dev/null +++ b/apps/web/src/agent/tools/delete-timeline-elements.tool.ts @@ -0,0 +1,44 @@ +import { EditorContextAdapter } from "@/agent/context"; +import type { AgentContext, ToolDefinition } from "@/agent/types"; +import { toolRegistry } from "@/agent/tools/registry"; + +export type DeleteTimelineElementsArgs = { + elementIds: string[]; +}; + +export type DeleteTimelineElementsResult = { + success: boolean; + deletedElements: string[]; +}; + +const deleteTimelineElementsTool: ToolDefinition = { + name: "delete_timeline_elements", + description: + "Deletes one or more timeline elements by elementId. Use list_timeline first to discover exact elementIds. To delete a time range, split at the range boundaries first, then delete the isolated elementIds.", + parameters: [{ key: "elementIds", type: "string[]", required: true }], + execute: async ( + args: Record, + _context: AgentContext, + ): Promise => { + const elementIds = args.elementIds; + + if (!isValidElementIds(elementIds)) { + return { error: "Invalid element ids" }; + } + + return EditorContextAdapter.deleteTimelineElements({ elementIds }); + }, +}; + +function isValidElementIds(elementIds: unknown): elementIds is string[] { + return ( + Array.isArray(elementIds) && + elementIds.length > 0 && + elementIds.every( + (elementId) => + typeof elementId === "string" && elementId.trim().length > 0, + ) + ); +} + +toolRegistry.register("delete_timeline_elements", deleteTimelineElementsTool); diff --git a/apps/web/src/app/api/agent/chat/__tests__/route.test.ts b/apps/web/src/app/api/agent/chat/__tests__/route.test.ts index f9ed25b1..dd339a8a 100644 --- a/apps/web/src/app/api/agent/chat/__tests__/route.test.ts +++ b/apps/web/src/app/api/agent/chat/__tests__/route.test.ts @@ -152,7 +152,7 @@ describe("POST /api/agent/chat", () => { expect(callArgs.messages).toHaveLength(1); expect(callArgs.systemPrompt).toContain("NeuralCut"); - expect(callArgs.tools).toHaveLength(4); + expect(callArgs.tools).toHaveLength(5); expect( callArgs.tools.map((tool) => (tool as { name: string }).name), ).toEqual([ @@ -160,6 +160,7 @@ describe("POST /api/agent/chat", () => { "list_project_assets", "list_timeline", "split", + "delete_timeline_elements", ]); }); diff --git a/apps/web/src/app/api/agent/chat/route.ts b/apps/web/src/app/api/agent/chat/route.ts index cba16b5b..81fca83d 100644 --- a/apps/web/src/app/api/agent/chat/route.ts +++ b/apps/web/src/app/api/agent/chat/route.ts @@ -43,6 +43,12 @@ const providerToolSchemas: ToolSchema[] = [ "Splits timeline elements at one or more requested timeline times in seconds without deleting, trimming, or moving content. Use one time for a single cut, or multiple times to isolate ranges before separate edit/delete operations.", parameters: [{ key: "times", type: "number[]", required: true }], }, + { + name: "delete_timeline_elements", + description: + "Deletes one or more timeline elements by elementId. Use list_timeline first to discover exact elementIds. To delete a time range, split at the range boundaries first, then delete the isolated elementIds.", + parameters: [{ key: "elementIds", type: "string[]", required: true }], + }, ]; // --------------------------------------------------------------------------- diff --git a/apps/web/src/components/editor/panels/chat/tool-formatters.ts b/apps/web/src/components/editor/panels/chat/tool-formatters.ts index 6811cbfb..7dab6717 100644 --- a/apps/web/src/components/editor/panels/chat/tool-formatters.ts +++ b/apps/web/src/components/editor/panels/chat/tool-formatters.ts @@ -77,6 +77,17 @@ const TOOL_CALL_FORMATTERS: Record< description: `at ${formatted}`, }; }, + delete_timeline_elements: (args) => { + const elementIds = args.elementIds as string[] | undefined; + const count = elementIds?.length ?? 0; + return { + label: "Delete", + description: + count > 0 + ? `${count} element${count !== 1 ? "s" : ""}` + : "No elements specified", + }; + }, load_context: (args) => { const targetType = String(args.targetType ?? "unknown"); const id = args.id ?? args.assetId ?? args.elementId; @@ -134,6 +145,20 @@ const TOOL_RESULT_FORMATTERS: Record< : "Failed", }; }, + delete_timeline_elements: (parsed) => { + const data = parsed as { + success?: boolean; + deletedElements?: string[]; + } | null; + if (!data) return null; + const count = data.deletedElements?.length ?? 0; + return { + label: "Deleted", + description: data.success + ? `${count} element${count !== 1 ? "s" : ""}` + : "Failed", + }; + }, load_context: (parsed) => { const data = parsed as { status?: string; diff --git a/docs/agent-tool-specs.md b/docs/agent-tool-specs.md index 4789c909..dacd1aca 100644 --- a/docs/agent-tool-specs.md +++ b/docs/agent-tool-specs.md @@ -182,7 +182,42 @@ Dividir elementos del timeline en uno o más puntos de tiempo, sin borrar conten --- -## 5. `add_text` +## 5. `delete_timeline_elements` + +### Propósito +Eliminar uno o más elementos concretos del timeline por `elementId`. Para eliminar un rango de tiempo, el agente debe primero usar `split({ times: [start, end] })` y luego borrar los elementos aislados. + +### Input +```ts +{ + elementIds: string[]; +} +``` + +### Output +```ts +{ + success: boolean; + deletedElements: string[]; +} +``` + +### Requirements +- MUST validate `elementIds` contains at least one non-empty string. +- MUST use `list_timeline` first when exact `elementId` values are unknown. +- MUST resolve `elementId` values against the active timeline before mutating. +- MUST fail without mutating if any requested element is missing. +- MUST remove only the requested elements. +- MUST preserve undo/redo behavior if supported. + +### Errors +- Invalid ids: `{ error: "Invalid element ids" }`. +- Missing element: `{ error: "Timeline elements not found: " }`. +- Empty timeline: `{ error: "No timeline content" }`. + +--- + +## 6. `add_text` ### Propósito Agregar texto visual al timeline. Esta primitive cubre títulos, hooks, labels y subtítulos básicos. @@ -220,7 +255,7 @@ Agregar texto visual al timeline. Esta primitive cubre títulos, hooks, labels y --- -## 6. `update_text` +## 7. `update_text` ### Propósito Editar un texto existente en el timeline. @@ -258,7 +293,7 @@ Editar un texto existente en el timeline. --- -## 7. `add_media_to_timeline` +## 8. `add_media_to_timeline` ### Propósito Agregar un asset existente al timeline. @@ -292,10 +327,10 @@ Agregar un asset existente al timeline. --- -## 8. `delete_element` +## 9. `delete_element` *(deprecated in favor of `delete_timeline_elements`)* ### Propósito -Eliminar un elemento específico del timeline. +Eliminar un elemento específico del timeline. La implementación actual debe preferir `delete_timeline_elements` porque soporta borrado en lote y permite componer rangos después de `split`. ### Input ```ts @@ -322,7 +357,7 @@ Eliminar un elemento específico del timeline. --- -## 9. `set_volume` +## 10. `set_volume` ### Propósito Ajustar volumen de un elemento de audio o video. @@ -355,7 +390,7 @@ Ajustar volumen de un elemento de audio o video. --- -## 10. `add_sticker` +## 11. `add_sticker` ### Propósito Agregar un sticker existente al timeline. @@ -390,7 +425,7 @@ Agregar un sticker existente al timeline. --- -## 11. `apply_effect` +## 12. `apply_effect` ### Propósito Aplicar un efecto existente a un clip. En el estado actual del repo, el efecto real disponible parece ser `blur`. diff --git a/docs/agent-tools.md b/docs/agent-tools.md index 83020e1b..bec0bbf3 100644 --- a/docs/agent-tools.md +++ b/docs/agent-tools.md @@ -106,7 +106,7 @@ Uso: - separar intro/outro sin eliminar nada, - dejarle al agente una primitive composable para flujos más grandes. -> `split` NO elimina material. Si el usuario pide “eliminá esta parte”, el agente debe componer `split` con una tool de borrado cuando exista. +> `split` NO elimina material. Si el usuario pide “eliminá esta parte”, el agente debe componer `split` con `delete_timeline_elements`. --- @@ -130,14 +130,13 @@ Uso: --- -### `delete_element` +### `delete_timeline_elements` -Elimina un elemento específico del timeline. +Elimina uno o más elementos específicos del timeline. ```ts { - trackId: string; - elementId: string; + elementIds: string[]; } ``` @@ -147,6 +146,8 @@ Uso: - borrar stickers, - borrar efectos standalone si aplica. +> Para borrar un rango, el agente debe hacer `split({ times: [start, end] })`, volver a listar/identificar los elementos aislados si hace falta, y luego llamar `delete_timeline_elements`. + --- ### `add_text` @@ -295,7 +296,7 @@ Descartada porque `analysisType` introduce categorías artificiales. Gemini debe ### `remove_silences` -Diferida/no prioritaria porque se puede expresar como flujo con `split` + tool de borrado. +Diferida/no prioritaria porque se puede expresar como flujo con `split` + `delete_timeline_elements`. ### `generate_subtitles` @@ -313,9 +314,9 @@ Diferida porque el repo actual no parece tener corrección de color/LUTs impleme 2. `list_timeline` 3. `load_asset_context` 4. `split` -5. `add_text` -6. `add_media_to_timeline` -7. `delete_element` +5. `delete_timeline_elements` +6. `add_text` +7. `add_media_to_timeline` 8. `set_volume` 9. `add_sticker` 10. `apply_effect` diff --git a/propuesta_tecnica.md b/propuesta_tecnica.md index 32a68039..1b8fff8c 100644 --- a/propuesta_tecnica.md +++ b/propuesta_tecnica.md @@ -50,7 +50,7 @@ Decisiones vigentes: Roadmap vigente de alto nivel: 1. **Contexto y percepción:** `list_project_assets`, `list_timeline`, `load_asset_context`. -2. **Edición básica:** `split`, `add_text`, `add_media_to_timeline`, `delete_element`. +2. **Edición básica:** `split`, `delete_timeline_elements`, `add_text`, `add_media_to_timeline`. 3. **Ajustes simples:** `set_volume`, `add_sticker`, `apply_effect`. 4. **UX avanzada:** referencias `@asset`, progreso de uploads/procesamiento, previews/aprobaciones. @@ -422,10 +422,11 @@ Estas son las tools que el equipo debe priorizar. Son suficientemente pequeñas | `list_timeline` | Devuelve resumen estructurado del timeline con `trackId`/`elementId` | Prioridad alta | | `load_asset_context` | Carga un asset en el contexto multimodal de Gemini y cachea la referencia | Prioridad crítica / killer feature | | `split` | Hace cortes puntuales en uno o más timestamps sin borrar contenido | Prioridad alta | +| `delete_timeline_elements` | Borra uno o más elementos concretos del timeline por `elementId` | Prioridad alta | | `add_text` | Agrega texto visual: hooks, títulos, labels, subtítulos básicos | Prioridad alta | | `update_text` | Modifica texto existente | Prioridad media | | `add_media_to_timeline` | Inserta video/audio/imagen existente al timeline | Prioridad alta | -| `delete_element` | Borra un elemento específico del timeline | Prioridad alta | +| `delete_element` | Reemplazada por `delete_timeline_elements` para soportar borrado en lote | Baja | | `set_volume` | Ajusta volumen de audio/video | Prioridad media | | `add_sticker` | Inserta sticker existente | Prioridad media | | `apply_effect` | Aplica efectos existentes; por ahora principalmente `blur` | Prioridad baja/media | @@ -716,7 +717,7 @@ Usuario: "quita la parte donde me equivoco" Agente: watch_video("where does the person mess up?") → identifica timestamp 1:23-1:31 → pide confirmación - → split({ times: [start, end] }) + tool de borrado + → split({ times: [start, end] }) + delete_timeline_elements ``` --- @@ -840,7 +841,7 @@ Esto no reemplaza la primera feature de producto; la **habilita**. **Should-have actualizado:** - Como usuario, puedo insertar assets existentes al timeline con `add_media_to_timeline`. -- Como usuario, puedo borrar elementos específicos con `delete_element`. +- Como usuario, puedo borrar elementos específicos con `delete_timeline_elements`. - Como usuario, puedo ajustar volumen con `set_volume`. - Como usuario, puedo agregar stickers con `add_sticker`. - Como usuario, puedo aplicar efectos existentes con `apply_effect` (inicialmente `blur`).