refactor(video): share async task status helpers

This commit is contained in:
Peter Steinberger 2026-04-06 01:18:09 +01:00
parent 527215c343
commit 5a42355d54
No known key found for this signature in database
16 changed files with 442 additions and 259 deletions

View File

@ -0,0 +1,46 @@
import { describe, expect, it, vi } from "vitest";
const videoGenerationTaskStatusMocks = vi.hoisted(() => ({
buildActiveVideoGenerationTaskPromptContextForSession: vi.fn(),
}));
vi.mock("../../video-generation-task-status.js", () => videoGenerationTaskStatusMocks);
import { resolveAttemptPrependSystemContext } from "./attempt.prompt-helpers.js";
describe("resolveAttemptPrependSystemContext", () => {
it("prepends active video task guidance ahead of hook system context", () => {
videoGenerationTaskStatusMocks.buildActiveVideoGenerationTaskPromptContextForSession.mockReturnValue(
"Active task hint",
);
const result = resolveAttemptPrependSystemContext({
sessionKey: "agent:main:discord:direct:123",
trigger: "user",
hookPrependSystemContext: "Hook system context",
});
expect(
videoGenerationTaskStatusMocks.buildActiveVideoGenerationTaskPromptContextForSession,
).toHaveBeenCalledWith("agent:main:discord:direct:123");
expect(result).toBe("Active task hint\n\nHook system context");
});
it("skips active video task guidance for non-user triggers", () => {
videoGenerationTaskStatusMocks.buildActiveVideoGenerationTaskPromptContextForSession.mockReset();
videoGenerationTaskStatusMocks.buildActiveVideoGenerationTaskPromptContextForSession.mockReturnValue(
"Should not be used",
);
const result = resolveAttemptPrependSystemContext({
sessionKey: "agent:main:discord:direct:123",
trigger: "heartbeat",
hookPrependSystemContext: "Hook system context",
});
expect(
videoGenerationTaskStatusMocks.buildActiveVideoGenerationTaskPromptContextForSession,
).not.toHaveBeenCalled();
expect(result).toBe("Hook system context");
});
});

View File

@ -8,6 +8,7 @@ import { isCronSessionKey, isSubagentSessionKey } from "../../../routing/session
import { joinPresentTextSegments } from "../../../shared/text/join-segments.js";
import { prependSystemPromptAdditionAfterCacheBoundary } from "../../system-prompt-cache-boundary.js";
import { resolveEffectiveToolFsWorkspaceOnly } from "../../tool-fs-policy.js";
import { buildActiveVideoGenerationTaskPromptContextForSession } from "../../video-generation-task-status.js";
import type { CompactEmbeddedPiSessionParams } from "../compact.js";
import { buildEmbeddedCompactionRuntimeContext } from "../compaction-runtime-context.js";
import { log } from "../logger.js";
@ -119,6 +120,18 @@ export function prependSystemPromptAddition(params: {
return prependSystemPromptAdditionAfterCacheBoundary(params);
}
export function resolveAttemptPrependSystemContext(params: {
sessionKey?: string;
trigger?: EmbeddedRunAttemptParams["trigger"];
hookPrependSystemContext?: string;
}): string | undefined {
const activeVideoTaskPromptContext =
params.trigger === "user" || params.trigger === "manual"
? buildActiveVideoGenerationTaskPromptContextForSession(params.sessionKey)
: undefined;
return joinPresentTextSegments([activeVideoTaskPromptContext, params.hookPrependSystemContext]);
}
/** Build runtime context passed into context-engine afterTurn hooks. */
export function buildAfterTurnRuntimeContext(params: {
attempt: Pick<

View File

@ -24,7 +24,6 @@ import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import { resolveToolCallArgumentsEncoding } from "../../../plugins/provider-model-compat.js";
import { resolveProviderSystemPromptContribution } from "../../../plugins/provider-runtime.js";
import { isSubagentSessionKey } from "../../../routing/session-key.js";
import { joinPresentTextSegments } from "../../../shared/text/join-segments.js";
import { buildTtsSystemPromptHint } from "../../../tts/tts.js";
import { resolveUserPath } from "../../../utils.js";
import { normalizeMessageChannel } from "../../../utils/message-channel.js";
@ -94,7 +93,6 @@ import { buildSystemPromptParams } from "../../system-prompt-params.js";
import { buildSystemPromptReport } from "../../system-prompt-report.js";
import { sanitizeToolCallIdsForCloudCodeAssist } from "../../tool-call-id.js";
import { resolveTranscriptPolicy } from "../../transcript-policy.js";
import { buildActiveVideoGenerationTaskPromptContextForSession } from "../../video-generation-task-status.js";
import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js";
import { isRunnerAbortError } from "../abort.js";
import { isCacheTtlEligibleProvider } from "../cache-ttl.js";
@ -155,6 +153,7 @@ import {
buildAfterTurnRuntimeContext,
prependSystemPromptAddition,
resolveAttemptFsWorkspaceOnly,
resolveAttemptPrependSystemContext,
resolvePromptBuildHookResult,
resolvePromptModeForSession,
shouldWarnOnOrphanedUserRepair,
@ -210,6 +209,7 @@ export {
buildAfterTurnRuntimeContext,
prependSystemPromptAddition,
resolveAttemptFsWorkspaceOnly,
resolveAttemptPrependSystemContext,
resolvePromptBuildHookResult,
resolvePromptModeForSession,
shouldWarnOnOrphanedUserRepair,
@ -1523,10 +1523,6 @@ export async function runEmbeddedAttempt(
hookRunner,
legacyBeforeAgentStartResult: params.legacyBeforeAgentStartResult,
});
const activeVideoTaskPromptContext =
params.trigger === "user" || params.trigger === "manual"
? buildActiveVideoGenerationTaskPromptContextForSession(params.sessionKey)
: undefined;
{
if (hookResult?.prependContext) {
effectivePrompt = `${hookResult.prependContext}\n\n${effectivePrompt}`;
@ -1543,10 +1539,11 @@ export async function runEmbeddedAttempt(
}
const prependedOrAppendedSystemPrompt = composeSystemPromptWithHookContext({
baseSystemPrompt: systemPromptText,
prependSystemContext: joinPresentTextSegments([
activeVideoTaskPromptContext,
hookResult?.prependSystemContext,
]),
prependSystemContext: resolveAttemptPrependSystemContext({
sessionKey: params.sessionKey,
trigger: params.trigger,
hookPrependSystemContext: hookResult?.prependSystemContext,
}),
appendSystemContext: hookResult?.appendSystemContext,
});
if (prependedOrAppendedSystemPrompt) {

View File

@ -0,0 +1,61 @@
import { listTasksForOwnerKey } from "../tasks/runtime-internal.js";
import type { TaskRecord, TaskRuntime, TaskStatus } from "../tasks/task-registry.types.js";
const DEFAULT_ACTIVE_STATUSES = new Set<TaskStatus>(["queued", "running"]);
export function findActiveSessionTask(params: {
sessionKey?: string;
runtime?: TaskRuntime;
taskKind?: string;
statuses?: ReadonlySet<TaskStatus>;
sourceIdPrefix?: string;
}): TaskRecord | null {
const normalizedSessionKey = params.sessionKey?.trim();
if (!normalizedSessionKey) {
return null;
}
const statuses = params.statuses ?? DEFAULT_ACTIVE_STATUSES;
const taskKind = params.taskKind?.trim();
const sourceIdPrefix = params.sourceIdPrefix?.trim();
const matches = listTasksForOwnerKey(normalizedSessionKey).filter((task) => {
if (task.scopeKind !== "session") {
return false;
}
if (params.runtime && task.runtime !== params.runtime) {
return false;
}
if (!statuses.has(task.status)) {
return false;
}
if (taskKind && task.taskKind !== taskKind) {
return false;
}
if (sourceIdPrefix) {
const sourceId = task.sourceId?.trim() ?? "";
if (sourceId !== sourceIdPrefix && !sourceId.startsWith(`${sourceIdPrefix}:`)) {
return false;
}
}
return true;
});
if (matches.length === 0) {
return null;
}
return matches.find((task) => task.status === "running") ?? matches[0] ?? null;
}
export function buildSessionAsyncTaskStatusDetails(task: TaskRecord): Record<string, unknown> {
return {
async: true,
active: true,
existingTask: true,
status: task.status,
task: {
taskId: task.taskId,
...(task.runId ? { runId: task.runId } : {}),
},
...(task.taskKind ? { taskKind: task.taskKind } : {}),
...(task.progressSummary ? { progressSummary: task.progressSummary } : {}),
...(task.sourceId ? { sourceId: task.sourceId } : {}),
};
}

View File

@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { VIDEO_GENERATION_TASK_KIND } from "../video-generation-task-status.js";
import {
createVideoGenerationTaskRun,
recordVideoGenerationTaskProgress,
@ -48,6 +49,7 @@ describe("video generate background helpers", () => {
});
expect(taskExecutorMocks.createRunningTaskRun).toHaveBeenCalledWith(
expect.objectContaining({
taskKind: VIDEO_GENERATION_TASK_KIND,
sourceId: "video_generate:openai",
progressSummary: "Queued video generation",
}),

View File

@ -10,6 +10,7 @@ import type { DeliveryContext } from "../../utils/delivery-context.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
import { formatAgentInternalEventsForPrompt, type AgentInternalEvent } from "../internal-events.js";
import { deliverSubagentAnnouncement } from "../subagent-announce-delivery.js";
import { VIDEO_GENERATION_TASK_KIND } from "../video-generation-task-status.js";
const log = createSubsystemLogger("agents/tools/video-generate-background");
@ -35,6 +36,7 @@ export function createVideoGenerationTaskRun(params: {
try {
const task = createRunningTaskRun({
runtime: "cli",
taskKind: VIDEO_GENERATION_TASK_KIND,
sourceId: params.providerId ? `video_generate:${params.providerId}` : "video_generate",
requesterSessionKey: sessionKey,
ownerKey: sessionKey,

View File

@ -0,0 +1,136 @@
import type { OpenClawConfig } from "../../config/config.js";
import { getProviderEnvVars } from "../../secrets/provider-env-vars.js";
import { listRuntimeVideoGenerationProviders } from "../../video-generation/runtime.js";
import {
buildVideoGenerationTaskStatusDetails,
buildVideoGenerationTaskStatusText,
findActiveVideoGenerationTaskForSession,
} from "../video-generation-task-status.js";
type VideoGenerateActionResult = {
content: Array<{ type: "text"; text: string }>;
details: Record<string, unknown>;
};
function getVideoGenerationProviderAuthEnvVars(providerId: string): string[] {
return getProviderEnvVars(providerId);
}
export function createVideoGenerateListActionResult(
config?: OpenClawConfig,
): VideoGenerateActionResult {
const providers = listRuntimeVideoGenerationProviders({ config });
if (providers.length === 0) {
return {
content: [{ type: "text", text: "No video-generation providers are registered." }],
details: { providers: [] },
};
}
const lines = providers.map((provider) => {
const authHints = getVideoGenerationProviderAuthEnvVars(provider.id);
const capabilities = [
provider.capabilities.maxVideos ? `maxVideos=${provider.capabilities.maxVideos}` : null,
provider.capabilities.maxInputImages
? `maxInputImages=${provider.capabilities.maxInputImages}`
: null,
provider.capabilities.maxInputVideos
? `maxInputVideos=${provider.capabilities.maxInputVideos}`
: null,
provider.capabilities.maxDurationSeconds
? `maxDurationSeconds=${provider.capabilities.maxDurationSeconds}`
: null,
provider.capabilities.supportedDurationSeconds?.length
? `supportedDurationSeconds=${provider.capabilities.supportedDurationSeconds.join("/")}`
: null,
provider.capabilities.supportedDurationSecondsByModel &&
Object.keys(provider.capabilities.supportedDurationSecondsByModel).length > 0
? `supportedDurationSecondsByModel=${Object.entries(
provider.capabilities.supportedDurationSecondsByModel,
)
.map(([modelId, durations]) => `${modelId}:${durations.join("/")}`)
.join("; ")}`
: null,
provider.capabilities.supportsResolution ? "resolution" : null,
provider.capabilities.supportsAspectRatio ? "aspectRatio" : null,
provider.capabilities.supportsSize ? "size" : null,
provider.capabilities.supportsAudio ? "audio" : null,
provider.capabilities.supportsWatermark ? "watermark" : null,
]
.filter((entry): entry is string => Boolean(entry))
.join(", ");
return [
`${provider.id}: default=${provider.defaultModel ?? "none"}`,
provider.models?.length ? `models=${provider.models.join(", ")}` : null,
capabilities ? `capabilities=${capabilities}` : null,
authHints.length > 0 ? `auth=${authHints.join(" / ")}` : null,
]
.filter((entry): entry is string => Boolean(entry))
.join(" | ");
});
return {
content: [{ type: "text", text: lines.join("\n") }],
details: {
providers: providers.map((provider) => ({
id: provider.id,
defaultModel: provider.defaultModel,
models: provider.models ?? [],
authEnvVars: getVideoGenerationProviderAuthEnvVars(provider.id),
capabilities: provider.capabilities,
})),
},
};
}
export function createVideoGenerateStatusActionResult(
sessionKey?: string,
): VideoGenerateActionResult {
const activeTask = findActiveVideoGenerationTaskForSession(sessionKey);
if (!activeTask) {
return {
content: [
{
type: "text",
text: "No active video generation task is currently running for this session.",
},
],
details: {
action: "status",
active: false,
},
};
}
return {
content: [
{
type: "text",
text: buildVideoGenerationTaskStatusText(activeTask),
},
],
details: {
action: "status",
...buildVideoGenerationTaskStatusDetails(activeTask),
},
};
}
export function createVideoGenerateDuplicateGuardResult(
sessionKey?: string,
): VideoGenerateActionResult | null {
const activeTask = findActiveVideoGenerationTaskForSession(sessionKey);
if (!activeTask) {
return null;
}
return {
content: [
{
type: "text",
text: buildVideoGenerationTaskStatusText(activeTask, { duplicateGuard: true }),
},
],
details: {
action: "status",
duplicateGuard: true,
...buildVideoGenerationTaskStatusDetails(activeTask),
},
};
}

View File

@ -0,0 +1,106 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as videoGenerationRuntime from "../../video-generation/runtime.js";
import { VIDEO_GENERATION_TASK_KIND } from "../video-generation-task-status.js";
import {
createVideoGenerateDuplicateGuardResult,
createVideoGenerateStatusActionResult,
} from "./video-generate-tool.actions.js";
const taskRuntimeInternalMocks = vi.hoisted(() => ({
listTasksForOwnerKey: vi.fn(),
}));
vi.mock("../../tasks/runtime-internal.js", () => taskRuntimeInternalMocks);
describe("createVideoGenerateTool status actions", () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.spyOn(videoGenerationRuntime, "listRuntimeVideoGenerationProviders").mockReturnValue([]);
taskRuntimeInternalMocks.listTasksForOwnerKey.mockReset();
taskRuntimeInternalMocks.listTasksForOwnerKey.mockReturnValue([]);
});
afterEach(() => {
vi.unstubAllEnvs();
});
it("returns active task status instead of starting a duplicate generation", async () => {
taskRuntimeInternalMocks.listTasksForOwnerKey.mockReturnValue([
{
taskId: "task-active",
runtime: "cli",
taskKind: VIDEO_GENERATION_TASK_KIND,
sourceId: "video_generate:openai",
requesterSessionKey: "agent:main:discord:direct:123",
ownerKey: "agent:main:discord:direct:123",
scopeKind: "session",
runId: "tool:video_generate:active",
task: "friendly lobster surfing",
status: "running",
deliveryStatus: "not_applicable",
notifyPolicy: "silent",
createdAt: Date.now(),
progressSummary: "Generating video",
},
]);
const result = createVideoGenerateDuplicateGuardResult("agent:main:discord:direct:123");
const text = (result?.content?.[0] as { text: string } | undefined)?.text ?? "";
expect(result).not.toBeNull();
expect(text).toContain("Video generation task task-active is already running with openai.");
expect(text).toContain("Do not call video_generate again for this request.");
expect(result?.details).toMatchObject({
action: "status",
duplicateGuard: true,
active: true,
existingTask: true,
status: "running",
taskKind: VIDEO_GENERATION_TASK_KIND,
provider: "openai",
task: {
taskId: "task-active",
runId: "tool:video_generate:active",
},
progressSummary: "Generating video",
});
});
it("reports active task status when action=status is requested", async () => {
taskRuntimeInternalMocks.listTasksForOwnerKey.mockReturnValue([
{
taskId: "task-active",
runtime: "cli",
taskKind: VIDEO_GENERATION_TASK_KIND,
sourceId: "video_generate:google",
requesterSessionKey: "agent:main:discord:direct:123",
ownerKey: "agent:main:discord:direct:123",
scopeKind: "session",
runId: "tool:video_generate:active",
task: "friendly lobster surfing",
status: "queued",
deliveryStatus: "not_applicable",
notifyPolicy: "silent",
createdAt: Date.now(),
progressSummary: "Queued video generation",
},
]);
const result = createVideoGenerateStatusActionResult("agent:main:discord:direct:123");
const text = (result.content?.[0] as { text: string } | undefined)?.text ?? "";
expect(text).toContain("Video generation task task-active is already queued with google.");
expect(result.details).toMatchObject({
action: "status",
active: true,
existingTask: true,
status: "queued",
taskKind: VIDEO_GENERATION_TASK_KIND,
provider: "google",
task: {
taskId: "task-active",
},
progressSummary: "Queued video generation",
});
});
});

View File

@ -222,112 +222,6 @@ describe("createVideoGenerateTool", () => {
);
});
it("returns active task status instead of starting a duplicate generation", async () => {
const generateSpy = vi.spyOn(videoGenerationRuntime, "generateVideo");
taskRuntimeInternalMocks.listTasksForOwnerKey.mockReturnValue([
{
taskId: "task-active",
runtime: "cli",
sourceId: "video_generate:openai",
requesterSessionKey: "agent:main:discord:direct:123",
ownerKey: "agent:main:discord:direct:123",
scopeKind: "session",
runId: "tool:video_generate:active",
task: "friendly lobster surfing",
status: "running",
deliveryStatus: "not_applicable",
notifyPolicy: "silent",
createdAt: Date.now(),
progressSummary: "Generating video",
},
]);
const tool = createVideoGenerateTool({
config: asConfig({
agents: {
defaults: {
videoGenerationModel: { primary: "openai/sora-2" },
},
},
}),
agentSessionKey: "agent:main:discord:direct:123",
});
if (!tool) {
throw new Error("expected video_generate tool");
}
const result = await tool.execute("call-dup", { prompt: "friendly lobster surfing" });
const text = (result.content?.[0] as { text: string } | undefined)?.text ?? "";
expect(text).toContain("Video generation task task-active is already running with openai.");
expect(text).toContain("Do not call video_generate again for this request.");
expect(result.details).toMatchObject({
action: "status",
duplicateGuard: true,
active: true,
existingTask: true,
status: "running",
provider: "openai",
task: {
taskId: "task-active",
runId: "tool:video_generate:active",
},
progressSummary: "Generating video",
});
expect(taskExecutorMocks.createRunningTaskRun).not.toHaveBeenCalled();
expect(generateSpy).not.toHaveBeenCalled();
});
it("reports active task status when action=status is requested", async () => {
taskRuntimeInternalMocks.listTasksForOwnerKey.mockReturnValue([
{
taskId: "task-active",
runtime: "cli",
sourceId: "video_generate:google",
requesterSessionKey: "agent:main:discord:direct:123",
ownerKey: "agent:main:discord:direct:123",
scopeKind: "session",
runId: "tool:video_generate:active",
task: "friendly lobster surfing",
status: "queued",
deliveryStatus: "not_applicable",
notifyPolicy: "silent",
createdAt: Date.now(),
progressSummary: "Queued video generation",
},
]);
const tool = createVideoGenerateTool({
config: asConfig({
agents: {
defaults: {
videoGenerationModel: { primary: "google/veo-3.1-fast-generate-preview" },
},
},
}),
agentSessionKey: "agent:main:discord:direct:123",
});
if (!tool) {
throw new Error("expected video_generate tool");
}
const result = await tool.execute("call-status", { action: "status" });
const text = (result.content?.[0] as { text: string } | undefined)?.text ?? "";
expect(text).toContain("Video generation task task-active is already queued with google.");
expect(result.details).toMatchObject({
action: "status",
active: true,
existingTask: true,
status: "queued",
provider: "google",
task: {
taskId: "task-active",
},
progressSummary: "Queued video generation",
});
});
it("surfaces provider generation failures inline when there is no detached session", async () => {
vi.spyOn(videoGenerationRuntime, "generateVideo").mockRejectedValue(new Error("queue boom"));

View File

@ -5,7 +5,6 @@ import { createSubsystemLogger } from "../../logging/subsystem.js";
import { saveMediaBuffer } from "../../media/store.js";
import { loadWebMedia } from "../../media/web-media.js";
import { readSnakeCaseParamRaw } from "../../param-key.js";
import { getProviderEnvVars } from "../../secrets/provider-env-vars.js";
import { resolveUserPath } from "../../utils.js";
import type { DeliveryContext } from "../../utils/delivery-context.js";
import { resolveVideoGenerationSupportedDurations } from "../../video-generation/duration-support.js";
@ -21,11 +20,6 @@ import type {
VideoGenerationSourceAsset,
} from "../../video-generation/types.js";
import { normalizeProviderId } from "../provider-id.js";
import {
buildVideoGenerationTaskStatusDetails,
buildVideoGenerationTaskStatusText,
findActiveVideoGenerationTaskForSession,
} from "../video-generation-task-status.js";
import {
ToolInputError,
readNumberParam,
@ -60,6 +54,11 @@ import {
type VideoGenerationTaskHandle,
wakeVideoGenerationTaskCompletion,
} from "./video-generate-background.js";
import {
createVideoGenerateDuplicateGuardResult,
createVideoGenerateListActionResult,
createVideoGenerateStatusActionResult,
} from "./video-generate-tool.actions.js";
const log = createSubsystemLogger("agents/tools/video-generate");
const MAX_INPUT_IMAGES = 5;
@ -149,10 +148,6 @@ const VideoGenerateToolSchema = Type.Object({
),
});
function getVideoGenerationProviderAuthEnvVars(providerId: string): string[] {
return getProviderEnvVars(providerId);
}
function resolveVideoGenerationModelCandidates(params: {
cfg?: OpenClawConfig;
agentDir?: string;
@ -755,113 +750,18 @@ export function createVideoGenerateTool(options?: {
applyVideoGenerationModelConfigDefaults(cfg, videoGenerationModelConfig) ?? cfg;
if (action === "list") {
const providers = listRuntimeVideoGenerationProviders({ config: effectiveCfg });
if (providers.length === 0) {
return {
content: [{ type: "text", text: "No video-generation providers are registered." }],
details: { providers: [] },
};
}
const lines = providers.map((provider) => {
const authHints = getVideoGenerationProviderAuthEnvVars(provider.id);
const capabilities = [
provider.capabilities.maxVideos ? `maxVideos=${provider.capabilities.maxVideos}` : null,
provider.capabilities.maxInputImages
? `maxInputImages=${provider.capabilities.maxInputImages}`
: null,
provider.capabilities.maxInputVideos
? `maxInputVideos=${provider.capabilities.maxInputVideos}`
: null,
provider.capabilities.maxDurationSeconds
? `maxDurationSeconds=${provider.capabilities.maxDurationSeconds}`
: null,
provider.capabilities.supportedDurationSeconds?.length
? `supportedDurationSeconds=${provider.capabilities.supportedDurationSeconds.join("/")}`
: null,
provider.capabilities.supportedDurationSecondsByModel &&
Object.keys(provider.capabilities.supportedDurationSecondsByModel).length > 0
? `supportedDurationSecondsByModel=${Object.entries(
provider.capabilities.supportedDurationSecondsByModel,
)
.map(([modelId, durations]) => `${modelId}:${durations.join("/")}`)
.join("; ")}`
: null,
provider.capabilities.supportsResolution ? "resolution" : null,
provider.capabilities.supportsAspectRatio ? "aspectRatio" : null,
provider.capabilities.supportsSize ? "size" : null,
provider.capabilities.supportsAudio ? "audio" : null,
provider.capabilities.supportsWatermark ? "watermark" : null,
]
.filter((entry): entry is string => Boolean(entry))
.join(", ");
return [
`${provider.id}: default=${provider.defaultModel ?? "none"}`,
provider.models?.length ? `models=${provider.models.join(", ")}` : null,
capabilities ? `capabilities=${capabilities}` : null,
authHints.length > 0 ? `auth=${authHints.join(" / ")}` : null,
]
.filter((entry): entry is string => Boolean(entry))
.join(" | ");
});
return {
content: [{ type: "text", text: lines.join("\n") }],
details: {
providers: providers.map((provider) => ({
id: provider.id,
defaultModel: provider.defaultModel,
models: provider.models ?? [],
authEnvVars: getVideoGenerationProviderAuthEnvVars(provider.id),
capabilities: provider.capabilities,
})),
},
};
return createVideoGenerateListActionResult(effectiveCfg);
}
if (action === "status") {
const activeTask = findActiveVideoGenerationTaskForSession(options?.agentSessionKey);
if (!activeTask) {
return {
content: [
{
type: "text",
text: "No active video generation task is currently running for this session.",
},
],
details: {
action: "status",
active: false,
},
};
}
return {
content: [
{
type: "text",
text: buildVideoGenerationTaskStatusText(activeTask),
},
],
details: {
action: "status",
...buildVideoGenerationTaskStatusDetails(activeTask),
},
};
return createVideoGenerateStatusActionResult(options?.agentSessionKey);
}
const activeTask = findActiveVideoGenerationTaskForSession(options?.agentSessionKey);
if (activeTask) {
return {
content: [
{
type: "text",
text: buildVideoGenerationTaskStatusText(activeTask, { duplicateGuard: true }),
},
],
details: {
action: "status",
duplicateGuard: true,
...buildVideoGenerationTaskStatusDetails(activeTask),
},
};
const duplicateGuardResult = createVideoGenerateDuplicateGuardResult(
options?.agentSessionKey,
);
if (duplicateGuardResult) {
return duplicateGuardResult;
}
const prompt = readStringParam(args, "prompt", { required: true });

View File

@ -6,6 +6,7 @@ import {
findActiveVideoGenerationTaskForSession,
getVideoGenerationTaskProviderId,
isActiveVideoGenerationTask,
VIDEO_GENERATION_TASK_KIND,
} from "./video-generation-task-status.js";
const taskRuntimeInternalMocks = vi.hoisted(() => ({
@ -25,6 +26,7 @@ describe("video generation task status", () => {
isActiveVideoGenerationTask({
taskId: "task-1",
runtime: "cli",
taskKind: VIDEO_GENERATION_TASK_KIND,
sourceId: "video_generate:openai",
requesterSessionKey: "agent:main",
ownerKey: "agent:main",
@ -40,6 +42,7 @@ describe("video generation task status", () => {
isActiveVideoGenerationTask({
taskId: "task-2",
runtime: "cron",
taskKind: VIDEO_GENERATION_TASK_KIND,
sourceId: "video_generate:openai",
requesterSessionKey: "agent:main",
ownerKey: "agent:main",
@ -58,6 +61,7 @@ describe("video generation task status", () => {
{
taskId: "task-queued",
runtime: "cli",
taskKind: VIDEO_GENERATION_TASK_KIND,
sourceId: "video_generate:google",
requesterSessionKey: "agent:main",
ownerKey: "agent:main",
@ -71,6 +75,7 @@ describe("video generation task status", () => {
{
taskId: "task-running",
runtime: "cli",
taskKind: VIDEO_GENERATION_TASK_KIND,
sourceId: "video_generate:openai",
requesterSessionKey: "agent:main",
ownerKey: "agent:main",
@ -95,6 +100,7 @@ describe("video generation task status", () => {
active: true,
existingTask: true,
status: "running",
taskKind: VIDEO_GENERATION_TASK_KIND,
provider: "openai",
progressSummary: "Generating video",
});
@ -105,6 +111,7 @@ describe("video generation task status", () => {
{
taskId: "task-running",
runtime: "cli",
taskKind: VIDEO_GENERATION_TASK_KIND,
sourceId: "video_generate:openai",
requesterSessionKey: "agent:main",
ownerKey: "agent:main",

View File

@ -1,21 +1,18 @@
import { listTasksForOwnerKey } from "../tasks/runtime-internal.js";
import type { TaskRecord } from "../tasks/task-registry.types.js";
import {
buildSessionAsyncTaskStatusDetails,
findActiveSessionTask,
} from "./session-async-task-status.js";
const ACTIVE_VIDEO_GENERATION_STATUSES = new Set(["queued", "running"]);
export const VIDEO_GENERATION_TASK_KIND = "video_generation";
const VIDEO_GENERATION_SOURCE_PREFIX = "video_generate";
function isActiveStatus(status: string): boolean {
return ACTIVE_VIDEO_GENERATION_STATUSES.has(status);
}
export function isActiveVideoGenerationTask(task: TaskRecord): boolean {
const sourceId = task.sourceId?.trim() ?? "";
return (
task.runtime === "cli" &&
task.scopeKind === "session" &&
isActiveStatus(task.status) &&
(sourceId === VIDEO_GENERATION_SOURCE_PREFIX ||
sourceId.startsWith(`${VIDEO_GENERATION_SOURCE_PREFIX}:`))
task.taskKind === VIDEO_GENERATION_TASK_KIND &&
(task.status === "queued" || task.status === "running")
);
}
@ -29,32 +26,18 @@ export function getVideoGenerationTaskProviderId(task: TaskRecord): string | und
}
export function findActiveVideoGenerationTaskForSession(sessionKey?: string): TaskRecord | null {
const normalizedSessionKey = sessionKey?.trim();
if (!normalizedSessionKey) {
return null;
}
const activeTasks = listTasksForOwnerKey(normalizedSessionKey).filter(
isActiveVideoGenerationTask,
);
if (activeTasks.length === 0) {
return null;
}
return activeTasks.find((task) => task.status === "running") ?? activeTasks[0] ?? null;
return findActiveSessionTask({
sessionKey,
runtime: "cli",
taskKind: VIDEO_GENERATION_TASK_KIND,
sourceIdPrefix: VIDEO_GENERATION_SOURCE_PREFIX,
});
}
export function buildVideoGenerationTaskStatusDetails(task: TaskRecord): Record<string, unknown> {
const provider = getVideoGenerationTaskProviderId(task);
return {
async: true,
active: true,
existingTask: true,
status: task.status,
task: {
taskId: task.taskId,
...(task.runId ? { runId: task.runId } : {}),
},
...(task.progressSummary ? { progressSummary: task.progressSummary } : {}),
...(task.sourceId ? { sourceId: task.sourceId } : {}),
...buildSessionAsyncTaskStatusDetails(task),
...(provider ? { provider } : {}),
};
}

View File

@ -178,6 +178,33 @@ describe("task-executor", () => {
});
});
it("persists explicit task kind metadata on created runs", async () => {
await withTaskExecutorStateDir(async () => {
const created = createRunningTaskRun({
runtime: "cli",
taskKind: "video_generation",
sourceId: "video_generate:openai",
ownerKey: "agent:main:main",
scopeKind: "session",
childSessionKey: "agent:main:main",
runId: "run-executor-kind",
task: "Generate lobster video",
startedAt: 10,
deliveryStatus: "not_applicable",
});
expect(getTaskById(created.taskId)).toMatchObject({
taskId: created.taskId,
taskKind: "video_generation",
sourceId: "video_generate:openai",
});
expect(findTaskByRunId("run-executor-kind")).toMatchObject({
taskId: created.taskId,
taskKind: "video_generation",
});
});
});
it("auto-creates a one-task flow and keeps it synced with task status", async () => {
await withTaskExecutorStateDir(async () => {
const created = createRunningTaskRun({

View File

@ -84,6 +84,7 @@ function ensureSingleTaskFlow(params: {
export function createQueuedTaskRun(params: {
runtime: TaskRuntime;
taskKind?: string;
sourceId?: string;
requesterSessionKey?: string;
ownerKey?: string;
@ -116,6 +117,7 @@ export function getFlowTaskSummary(flowId: string): TaskRegistrySummary {
export function createRunningTaskRun(params: {
runtime: TaskRuntime;
taskKind?: string;
sourceId?: string;
requesterSessionKey?: string;
ownerKey?: string;

View File

@ -651,6 +651,7 @@ function findExistingTaskForCreate(params: {
function mergeExistingTaskForCreate(
existing: TaskRecord,
params: {
taskKind?: string;
requesterOrigin?: TaskDeliveryState["requesterOrigin"];
sourceId?: string;
parentFlowId?: string;
@ -676,6 +677,9 @@ function mergeExistingTaskForCreate(
if (params.sourceId?.trim() && !existing.sourceId?.trim()) {
patch.sourceId = params.sourceId.trim();
}
if (params.taskKind?.trim() && !existing.taskKind?.trim()) {
patch.taskKind = params.taskKind.trim();
}
if (params.parentFlowId?.trim() && !existing.parentFlowId?.trim()) {
assertParentFlowLinkAllowed({
ownerKey: existing.ownerKey,
@ -1357,6 +1361,7 @@ function ensureListener() {
export function createTaskRecord(params: {
runtime: TaskRuntime;
taskKind?: string;
sourceId?: string;
requesterSessionKey?: string;
ownerKey?: string;
@ -1431,6 +1436,7 @@ export function createTaskRecord(params: {
const record: TaskRecord = {
taskId,
runtime: params.runtime,
taskKind: params.taskKind?.trim() || undefined,
sourceId: params.sourceId?.trim() || undefined,
requesterSessionKey,
ownerKey,

View File

@ -53,6 +53,7 @@ export type TaskDeliveryState = {
export type TaskRecord = {
taskId: string;
runtime: TaskRuntime;
taskKind?: string;
sourceId?: string;
requesterSessionKey: string;
ownerKey: string;