diff --git a/src/agents/media-generation-task-status-shared.test.ts b/src/agents/media-generation-task-status-shared.test.ts new file mode 100644 index 000000000000..3f5b9a498037 --- /dev/null +++ b/src/agents/media-generation-task-status-shared.test.ts @@ -0,0 +1,126 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { TaskRecord } from "../tasks/task-registry.types.js"; +import { + buildActiveMediaGenerationTaskPromptContextForSession, + findActiveMediaGenerationTaskForSession, + findDuplicateGuardMediaGenerationTaskForSession, + listActiveMediaGenerationTasksForSession, + MEDIA_GENERATION_DELIVERING_COMPLETION_PROGRESS, + resetRecentMediaGenerationDuplicateGuardsForTests, +} from "./media-generation-task-status-shared.js"; + +const taskRuntimeInternalMocks = vi.hoisted(() => ({ + listFreshTasksForOwnerKey: vi.fn(), +})); + +vi.mock("../tasks/runtime-internal.js", () => taskRuntimeInternalMocks); + +function makeTask(overrides: Partial = {}): TaskRecord { + const now = Date.now(); + return { + taskId: "task-1", + runtime: "cli", + taskKind: "video-generate", + sourceId: "video-generate:byteplus", + requesterSessionKey: "session/A", + ownerKey: "session/A", + scopeKind: "session", + runId: "run-1", + task: "generate clip 01", + status: "running", + deliveryStatus: "not_applicable", + notifyPolicy: "silent", + createdAt: now, + startedAt: now, + lastEventAt: now, + ...overrides, + }; +} + +beforeEach(() => { + resetRecentMediaGenerationDuplicateGuardsForTests(); + taskRuntimeInternalMocks.listFreshTasksForOwnerKey.mockReset(); +}); + +describe("media generation delivery-phase prompt guard", () => { + it("does not warn about a task waiting only for completion delivery", () => { + taskRuntimeInternalMocks.listFreshTasksForOwnerKey.mockReturnValue([ + makeTask({ progressSummary: MEDIA_GENERATION_DELIVERING_COMPLETION_PROGRESS }), + ]); + + expect( + buildActiveMediaGenerationTaskPromptContextForSession({ + sessionKey: "session/A", + taskKind: "video-generate", + sourcePrefix: "video-generate", + nounLabel: "video", + toolName: "video_generate", + completionLabel: "video", + }), + ).toBeUndefined(); + }); + + it("still warns while media generation is running", () => { + taskRuntimeInternalMocks.listFreshTasksForOwnerKey.mockReturnValue([ + makeTask({ progressSummary: "Generating video" }), + ]); + + expect( + buildActiveMediaGenerationTaskPromptContextForSession({ + sessionKey: "session/A", + taskKind: "video-generate", + sourcePrefix: "video-generate", + nounLabel: "video", + toolName: "video_generate", + completionLabel: "video", + }), + ).toContain("Do not call `video_generate` again for the same request"); + }); + + it("keeps delivery-phase tasks available to duplicate/status lookups", () => { + const task = makeTask({ progressSummary: MEDIA_GENERATION_DELIVERING_COMPLETION_PROGRESS }); + taskRuntimeInternalMocks.listFreshTasksForOwnerKey.mockReturnValue([task]); + + expect( + listActiveMediaGenerationTasksForSession({ + sessionKey: "session/A", + taskKind: "video-generate", + sourcePrefix: "video-generate", + }), + ).toEqual([task]); + expect( + findActiveMediaGenerationTaskForSession({ + sessionKey: "session/A", + taskKind: "video-generate", + sourcePrefix: "video-generate", + }), + ).toEqual(task); + }); + + it("blocks the same prompt while allowing a distinct prompt", () => { + const task = makeTask({ + task: "generate clip 01", + progressSummary: MEDIA_GENERATION_DELIVERING_COMPLETION_PROGRESS, + }); + taskRuntimeInternalMocks.listFreshTasksForOwnerKey.mockReturnValue([task]); + + expect( + findDuplicateGuardMediaGenerationTaskForSession({ + sessionKey: "session/A", + taskKind: "video-generate", + sourcePrefix: "video-generate", + taskLabel: "generate clip 01", + maxAgeMs: 120_000, + }), + ).toEqual(task); + expect( + findDuplicateGuardMediaGenerationTaskForSession({ + sessionKey: "session/A", + taskKind: "video-generate", + sourcePrefix: "video-generate", + taskLabel: "generate clip 02", + maxAgeMs: 120_000, + }), + ).toBeUndefined(); + }); +}); diff --git a/src/agents/media-generation-task-status-shared.ts b/src/agents/media-generation-task-status-shared.ts index da3d03804456..51a18f6d6dde 100644 --- a/src/agents/media-generation-task-status-shared.ts +++ b/src/agents/media-generation-task-status-shared.ts @@ -14,6 +14,10 @@ import type { TaskRecord } from "../tasks/task-registry.types.js"; import { buildSessionAsyncTaskStatusDetails } from "./session-async-task-status.js"; import { stableStringify } from "./stable-stringify.js"; +/** Marks media as ready while requester delivery is still being confirmed. */ +export const MEDIA_GENERATION_DELIVERING_COMPLETION_PROGRESS = + "Generated media; delivering completion"; + type RecentMediaGenerationTaskStart = { task: TaskRecord; requestKey?: string; @@ -299,6 +303,7 @@ export function findActiveMediaGenerationTaskForSession(params: { taskKind: string; sourcePrefix: string; taskLabel?: string; + excludeDeliveringCompletion?: boolean; }): TaskRecord | undefined { return listActiveMediaGenerationTasksForSession(params)[0]; } @@ -309,6 +314,7 @@ export function listActiveMediaGenerationTasksForSession(params: { taskKind: string; sourcePrefix: string; taskLabel?: string; + excludeDeliveringCompletion?: boolean; }): TaskRecord[] { const sessionKey = normalizeOptionalString(params.sessionKey); if (!sessionKey) { @@ -331,6 +337,12 @@ export function listActiveMediaGenerationTasksForSession(params: { if (taskLabel && !mediaGenerationTaskLabelMatches(task, taskLabel)) { return false; } + if ( + params.excludeDeliveringCompletion && + task.progressSummary === MEDIA_GENERATION_DELIVERING_COMPLETION_PROGRESS + ) { + return false; + } return true; }); return [ @@ -456,6 +468,7 @@ export function buildActiveMediaGenerationTaskPromptContextForSession(params: { sessionKey: params.sessionKey, taskKind: params.taskKind, sourcePrefix: params.sourcePrefix, + excludeDeliveringCompletion: true, }); if (!task) { return undefined; diff --git a/src/agents/tools/media-generate-background-shared.ts b/src/agents/tools/media-generate-background-shared.ts index 162e2d7828d2..1c32aaf7e515 100644 --- a/src/agents/tools/media-generate-background-shared.ts +++ b/src/agents/tools/media-generate-background-shared.ts @@ -34,6 +34,7 @@ import { type AgentGeneratedAttachment, } from "../generated-attachments.js"; import { formatAgentInternalEventsForPrompt, type AgentInternalEvent } from "../internal-events.js"; +import { MEDIA_GENERATION_DELIVERING_COMPLETION_PROGRESS } from "../media-generation-task-status-shared.js"; import { deliverSubagentAnnouncement, loadRequesterSessionEntry, @@ -447,7 +448,7 @@ export function scheduleMediaGenerationTaskCompletion< try { params.lifecycle.recordTaskProgress({ handle: params.handle, - progressSummary: "Generated media; delivering completion", + progressSummary: MEDIA_GENERATION_DELIVERING_COMPLETION_PROGRESS, }); } catch (error) { params.onWakeFailure(`${params.toolName} completion progress update failed`, { diff --git a/src/agents/tools/music-generate-tool.ts b/src/agents/tools/music-generate-tool.ts index c154d45d1e00..e634a44b3220 100644 --- a/src/agents/tools/music-generate-tool.ts +++ b/src/agents/tools/music-generate-tool.ts @@ -646,6 +646,7 @@ export function createMusicGenerateTool(options?: { const activeDuplicateGuardResult = createMusicGenerateDuplicateGuardResult( options?.agentSessionKey, + { prompt }, ); if (activeDuplicateGuardResult) { return activeDuplicateGuardResult; @@ -703,7 +704,7 @@ export function createMusicGenerateTool(options?: { }); const duplicateGuardResult = createMusicGenerateDuplicateGuardResult( options?.agentSessionKey, - { requestKey }, + { prompt, requestKey }, ); if (duplicateGuardResult) { return duplicateGuardResult; diff --git a/src/agents/tools/video-generate-tool.ts b/src/agents/tools/video-generate-tool.ts index 138ea74abf34..f97534f34268 100644 --- a/src/agents/tools/video-generate-tool.ts +++ b/src/agents/tools/video-generate-tool.ts @@ -1005,6 +1005,7 @@ export function createVideoGenerateTool(options?: { const activeDuplicateGuardResult = createVideoGenerateDuplicateGuardResult( options?.agentSessionKey, + { prompt }, ); if (activeDuplicateGuardResult) { return activeDuplicateGuardResult; @@ -1113,7 +1114,7 @@ export function createVideoGenerateTool(options?: { }); const duplicateGuardResult = createVideoGenerateDuplicateGuardResult( options?.agentSessionKey, - { requestKey }, + { prompt, requestKey }, ); if (duplicateGuardResult) { return duplicateGuardResult;