feat: add agent delete tool
This commit is contained in:
parent
7c19c72058
commit
1762661309
|
|
@ -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
|
||||
// -----------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ interface OpenAIFunctionTool {
|
|||
description: string;
|
||||
parameters: {
|
||||
type: "object";
|
||||
properties: Record<string, { type: string }>;
|
||||
properties: Record<string, { type: string; items?: { type: string } }>;
|
||||
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<typeof tc, { type: "function" }> => tc.type === "function")
|
||||
.filter(
|
||||
(tc): tc is Extract<typeof tc, { type: "function" }> =>
|
||||
tc.type === "function",
|
||||
)
|
||||
.map((tc) => ({
|
||||
id: tc.id,
|
||||
name: tc.function.name,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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<string, unknown>,
|
||||
_context: AgentContext,
|
||||
): Promise<DeleteTimelineElementsResult | { error: string }> => {
|
||||
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);
|
||||
|
|
@ -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",
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }],
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: <ids>" }`.
|
||||
- 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`.
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
|
|
|||
Loading…
Reference in New Issue