Skip to content

Commit 5a42355

Browse files
committed
refactor(video): share async task status helpers
1 parent 527215c commit 5a42355

16 files changed

Lines changed: 442 additions & 259 deletions
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
const videoGenerationTaskStatusMocks = vi.hoisted(() => ({
4+
buildActiveVideoGenerationTaskPromptContextForSession: vi.fn(),
5+
}));
6+
7+
vi.mock("../../video-generation-task-status.js", () => videoGenerationTaskStatusMocks);
8+
9+
import { resolveAttemptPrependSystemContext } from "./attempt.prompt-helpers.js";
10+
11+
describe("resolveAttemptPrependSystemContext", () => {
12+
it("prepends active video task guidance ahead of hook system context", () => {
13+
videoGenerationTaskStatusMocks.buildActiveVideoGenerationTaskPromptContextForSession.mockReturnValue(
14+
"Active task hint",
15+
);
16+
17+
const result = resolveAttemptPrependSystemContext({
18+
sessionKey: "agent:main:discord:direct:123",
19+
trigger: "user",
20+
hookPrependSystemContext: "Hook system context",
21+
});
22+
23+
expect(
24+
videoGenerationTaskStatusMocks.buildActiveVideoGenerationTaskPromptContextForSession,
25+
).toHaveBeenCalledWith("agent:main:discord:direct:123");
26+
expect(result).toBe("Active task hint\n\nHook system context");
27+
});
28+
29+
it("skips active video task guidance for non-user triggers", () => {
30+
videoGenerationTaskStatusMocks.buildActiveVideoGenerationTaskPromptContextForSession.mockReset();
31+
videoGenerationTaskStatusMocks.buildActiveVideoGenerationTaskPromptContextForSession.mockReturnValue(
32+
"Should not be used",
33+
);
34+
35+
const result = resolveAttemptPrependSystemContext({
36+
sessionKey: "agent:main:discord:direct:123",
37+
trigger: "heartbeat",
38+
hookPrependSystemContext: "Hook system context",
39+
});
40+
41+
expect(
42+
videoGenerationTaskStatusMocks.buildActiveVideoGenerationTaskPromptContextForSession,
43+
).not.toHaveBeenCalled();
44+
expect(result).toBe("Hook system context");
45+
});
46+
});

src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { isCronSessionKey, isSubagentSessionKey } from "../../../routing/session
88
import { joinPresentTextSegments } from "../../../shared/text/join-segments.js";
99
import { prependSystemPromptAdditionAfterCacheBoundary } from "../../system-prompt-cache-boundary.js";
1010
import { resolveEffectiveToolFsWorkspaceOnly } from "../../tool-fs-policy.js";
11+
import { buildActiveVideoGenerationTaskPromptContextForSession } from "../../video-generation-task-status.js";
1112
import type { CompactEmbeddedPiSessionParams } from "../compact.js";
1213
import { buildEmbeddedCompactionRuntimeContext } from "../compaction-runtime-context.js";
1314
import { log } from "../logger.js";
@@ -119,6 +120,18 @@ export function prependSystemPromptAddition(params: {
119120
return prependSystemPromptAdditionAfterCacheBoundary(params);
120121
}
121122

123+
export function resolveAttemptPrependSystemContext(params: {
124+
sessionKey?: string;
125+
trigger?: EmbeddedRunAttemptParams["trigger"];
126+
hookPrependSystemContext?: string;
127+
}): string | undefined {
128+
const activeVideoTaskPromptContext =
129+
params.trigger === "user" || params.trigger === "manual"
130+
? buildActiveVideoGenerationTaskPromptContextForSession(params.sessionKey)
131+
: undefined;
132+
return joinPresentTextSegments([activeVideoTaskPromptContext, params.hookPrependSystemContext]);
133+
}
134+
122135
/** Build runtime context passed into context-engine afterTurn hooks. */
123136
export function buildAfterTurnRuntimeContext(params: {
124137
attempt: Pick<

src/agents/pi-embedded-runner/run/attempt.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
2424
import { resolveToolCallArgumentsEncoding } from "../../../plugins/provider-model-compat.js";
2525
import { resolveProviderSystemPromptContribution } from "../../../plugins/provider-runtime.js";
2626
import { isSubagentSessionKey } from "../../../routing/session-key.js";
27-
import { joinPresentTextSegments } from "../../../shared/text/join-segments.js";
2827
import { buildTtsSystemPromptHint } from "../../../tts/tts.js";
2928
import { resolveUserPath } from "../../../utils.js";
3029
import { normalizeMessageChannel } from "../../../utils/message-channel.js";
@@ -94,7 +93,6 @@ import { buildSystemPromptParams } from "../../system-prompt-params.js";
9493
import { buildSystemPromptReport } from "../../system-prompt-report.js";
9594
import { sanitizeToolCallIdsForCloudCodeAssist } from "../../tool-call-id.js";
9695
import { resolveTranscriptPolicy } from "../../transcript-policy.js";
97-
import { buildActiveVideoGenerationTaskPromptContextForSession } from "../../video-generation-task-status.js";
9896
import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js";
9997
import { isRunnerAbortError } from "../abort.js";
10098
import { isCacheTtlEligibleProvider } from "../cache-ttl.js";
@@ -155,6 +153,7 @@ import {
155153
buildAfterTurnRuntimeContext,
156154
prependSystemPromptAddition,
157155
resolveAttemptFsWorkspaceOnly,
156+
resolveAttemptPrependSystemContext,
158157
resolvePromptBuildHookResult,
159158
resolvePromptModeForSession,
160159
shouldWarnOnOrphanedUserRepair,
@@ -210,6 +209,7 @@ export {
210209
buildAfterTurnRuntimeContext,
211210
prependSystemPromptAddition,
212211
resolveAttemptFsWorkspaceOnly,
212+
resolveAttemptPrependSystemContext,
213213
resolvePromptBuildHookResult,
214214
resolvePromptModeForSession,
215215
shouldWarnOnOrphanedUserRepair,
@@ -1523,10 +1523,6 @@ export async function runEmbeddedAttempt(
15231523
hookRunner,
15241524
legacyBeforeAgentStartResult: params.legacyBeforeAgentStartResult,
15251525
});
1526-
const activeVideoTaskPromptContext =
1527-
params.trigger === "user" || params.trigger === "manual"
1528-
? buildActiveVideoGenerationTaskPromptContextForSession(params.sessionKey)
1529-
: undefined;
15301526
{
15311527
if (hookResult?.prependContext) {
15321528
effectivePrompt = `${hookResult.prependContext}\n\n${effectivePrompt}`;
@@ -1543,10 +1539,11 @@ export async function runEmbeddedAttempt(
15431539
}
15441540
const prependedOrAppendedSystemPrompt = composeSystemPromptWithHookContext({
15451541
baseSystemPrompt: systemPromptText,
1546-
prependSystemContext: joinPresentTextSegments([
1547-
activeVideoTaskPromptContext,
1548-
hookResult?.prependSystemContext,
1549-
]),
1542+
prependSystemContext: resolveAttemptPrependSystemContext({
1543+
sessionKey: params.sessionKey,
1544+
trigger: params.trigger,
1545+
hookPrependSystemContext: hookResult?.prependSystemContext,
1546+
}),
15501547
appendSystemContext: hookResult?.appendSystemContext,
15511548
});
15521549
if (prependedOrAppendedSystemPrompt) {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { listTasksForOwnerKey } from "../tasks/runtime-internal.js";
2+
import type { TaskRecord, TaskRuntime, TaskStatus } from "../tasks/task-registry.types.js";
3+
4+
const DEFAULT_ACTIVE_STATUSES = new Set<TaskStatus>(["queued", "running"]);
5+
6+
export function findActiveSessionTask(params: {
7+
sessionKey?: string;
8+
runtime?: TaskRuntime;
9+
taskKind?: string;
10+
statuses?: ReadonlySet<TaskStatus>;
11+
sourceIdPrefix?: string;
12+
}): TaskRecord | null {
13+
const normalizedSessionKey = params.sessionKey?.trim();
14+
if (!normalizedSessionKey) {
15+
return null;
16+
}
17+
const statuses = params.statuses ?? DEFAULT_ACTIVE_STATUSES;
18+
const taskKind = params.taskKind?.trim();
19+
const sourceIdPrefix = params.sourceIdPrefix?.trim();
20+
const matches = listTasksForOwnerKey(normalizedSessionKey).filter((task) => {
21+
if (task.scopeKind !== "session") {
22+
return false;
23+
}
24+
if (params.runtime && task.runtime !== params.runtime) {
25+
return false;
26+
}
27+
if (!statuses.has(task.status)) {
28+
return false;
29+
}
30+
if (taskKind && task.taskKind !== taskKind) {
31+
return false;
32+
}
33+
if (sourceIdPrefix) {
34+
const sourceId = task.sourceId?.trim() ?? "";
35+
if (sourceId !== sourceIdPrefix && !sourceId.startsWith(`${sourceIdPrefix}:`)) {
36+
return false;
37+
}
38+
}
39+
return true;
40+
});
41+
if (matches.length === 0) {
42+
return null;
43+
}
44+
return matches.find((task) => task.status === "running") ?? matches[0] ?? null;
45+
}
46+
47+
export function buildSessionAsyncTaskStatusDetails(task: TaskRecord): Record<string, unknown> {
48+
return {
49+
async: true,
50+
active: true,
51+
existingTask: true,
52+
status: task.status,
53+
task: {
54+
taskId: task.taskId,
55+
...(task.runId ? { runId: task.runId } : {}),
56+
},
57+
...(task.taskKind ? { taskKind: task.taskKind } : {}),
58+
...(task.progressSummary ? { progressSummary: task.progressSummary } : {}),
59+
...(task.sourceId ? { sourceId: task.sourceId } : {}),
60+
};
61+
}

src/agents/tools/video-generate-background.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { VIDEO_GENERATION_TASK_KIND } from "../video-generation-task-status.js";
23
import {
34
createVideoGenerationTaskRun,
45
recordVideoGenerationTaskProgress,
@@ -48,6 +49,7 @@ describe("video generate background helpers", () => {
4849
});
4950
expect(taskExecutorMocks.createRunningTaskRun).toHaveBeenCalledWith(
5051
expect.objectContaining({
52+
taskKind: VIDEO_GENERATION_TASK_KIND,
5153
sourceId: "video_generate:openai",
5254
progressSummary: "Queued video generation",
5355
}),

src/agents/tools/video-generate-background.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { DeliveryContext } from "../../utils/delivery-context.js";
1010
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
1111
import { formatAgentInternalEventsForPrompt, type AgentInternalEvent } from "../internal-events.js";
1212
import { deliverSubagentAnnouncement } from "../subagent-announce-delivery.js";
13+
import { VIDEO_GENERATION_TASK_KIND } from "../video-generation-task-status.js";
1314

1415
const log = createSubsystemLogger("agents/tools/video-generate-background");
1516

@@ -35,6 +36,7 @@ export function createVideoGenerationTaskRun(params: {
3536
try {
3637
const task = createRunningTaskRun({
3738
runtime: "cli",
39+
taskKind: VIDEO_GENERATION_TASK_KIND,
3840
sourceId: params.providerId ? `video_generate:${params.providerId}` : "video_generate",
3941
requesterSessionKey: sessionKey,
4042
ownerKey: sessionKey,
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import type { OpenClawConfig } from "../../config/config.js";
2+
import { getProviderEnvVars } from "../../secrets/provider-env-vars.js";
3+
import { listRuntimeVideoGenerationProviders } from "../../video-generation/runtime.js";
4+
import {
5+
buildVideoGenerationTaskStatusDetails,
6+
buildVideoGenerationTaskStatusText,
7+
findActiveVideoGenerationTaskForSession,
8+
} from "../video-generation-task-status.js";
9+
10+
type VideoGenerateActionResult = {
11+
content: Array<{ type: "text"; text: string }>;
12+
details: Record<string, unknown>;
13+
};
14+
15+
function getVideoGenerationProviderAuthEnvVars(providerId: string): string[] {
16+
return getProviderEnvVars(providerId);
17+
}
18+
19+
export function createVideoGenerateListActionResult(
20+
config?: OpenClawConfig,
21+
): VideoGenerateActionResult {
22+
const providers = listRuntimeVideoGenerationProviders({ config });
23+
if (providers.length === 0) {
24+
return {
25+
content: [{ type: "text", text: "No video-generation providers are registered." }],
26+
details: { providers: [] },
27+
};
28+
}
29+
const lines = providers.map((provider) => {
30+
const authHints = getVideoGenerationProviderAuthEnvVars(provider.id);
31+
const capabilities = [
32+
provider.capabilities.maxVideos ? `maxVideos=${provider.capabilities.maxVideos}` : null,
33+
provider.capabilities.maxInputImages
34+
? `maxInputImages=${provider.capabilities.maxInputImages}`
35+
: null,
36+
provider.capabilities.maxInputVideos
37+
? `maxInputVideos=${provider.capabilities.maxInputVideos}`
38+
: null,
39+
provider.capabilities.maxDurationSeconds
40+
? `maxDurationSeconds=${provider.capabilities.maxDurationSeconds}`
41+
: null,
42+
provider.capabilities.supportedDurationSeconds?.length
43+
? `supportedDurationSeconds=${provider.capabilities.supportedDurationSeconds.join("/")}`
44+
: null,
45+
provider.capabilities.supportedDurationSecondsByModel &&
46+
Object.keys(provider.capabilities.supportedDurationSecondsByModel).length > 0
47+
? `supportedDurationSecondsByModel=${Object.entries(
48+
provider.capabilities.supportedDurationSecondsByModel,
49+
)
50+
.map(([modelId, durations]) => `${modelId}:${durations.join("/")}`)
51+
.join("; ")}`
52+
: null,
53+
provider.capabilities.supportsResolution ? "resolution" : null,
54+
provider.capabilities.supportsAspectRatio ? "aspectRatio" : null,
55+
provider.capabilities.supportsSize ? "size" : null,
56+
provider.capabilities.supportsAudio ? "audio" : null,
57+
provider.capabilities.supportsWatermark ? "watermark" : null,
58+
]
59+
.filter((entry): entry is string => Boolean(entry))
60+
.join(", ");
61+
return [
62+
`${provider.id}: default=${provider.defaultModel ?? "none"}`,
63+
provider.models?.length ? `models=${provider.models.join(", ")}` : null,
64+
capabilities ? `capabilities=${capabilities}` : null,
65+
authHints.length > 0 ? `auth=${authHints.join(" / ")}` : null,
66+
]
67+
.filter((entry): entry is string => Boolean(entry))
68+
.join(" | ");
69+
});
70+
return {
71+
content: [{ type: "text", text: lines.join("\n") }],
72+
details: {
73+
providers: providers.map((provider) => ({
74+
id: provider.id,
75+
defaultModel: provider.defaultModel,
76+
models: provider.models ?? [],
77+
authEnvVars: getVideoGenerationProviderAuthEnvVars(provider.id),
78+
capabilities: provider.capabilities,
79+
})),
80+
},
81+
};
82+
}
83+
84+
export function createVideoGenerateStatusActionResult(
85+
sessionKey?: string,
86+
): VideoGenerateActionResult {
87+
const activeTask = findActiveVideoGenerationTaskForSession(sessionKey);
88+
if (!activeTask) {
89+
return {
90+
content: [
91+
{
92+
type: "text",
93+
text: "No active video generation task is currently running for this session.",
94+
},
95+
],
96+
details: {
97+
action: "status",
98+
active: false,
99+
},
100+
};
101+
}
102+
return {
103+
content: [
104+
{
105+
type: "text",
106+
text: buildVideoGenerationTaskStatusText(activeTask),
107+
},
108+
],
109+
details: {
110+
action: "status",
111+
...buildVideoGenerationTaskStatusDetails(activeTask),
112+
},
113+
};
114+
}
115+
116+
export function createVideoGenerateDuplicateGuardResult(
117+
sessionKey?: string,
118+
): VideoGenerateActionResult | null {
119+
const activeTask = findActiveVideoGenerationTaskForSession(sessionKey);
120+
if (!activeTask) {
121+
return null;
122+
}
123+
return {
124+
content: [
125+
{
126+
type: "text",
127+
text: buildVideoGenerationTaskStatusText(activeTask, { duplicateGuard: true }),
128+
},
129+
],
130+
details: {
131+
action: "status",
132+
duplicateGuard: true,
133+
...buildVideoGenerationTaskStatusDetails(activeTask),
134+
},
135+
};
136+
}

0 commit comments

Comments
 (0)