remotion-claude-agent-demo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +160 -0
  2. package/apps/web/README.md +36 -0
  3. package/apps/web/env.example +20 -0
  4. package/apps/web/eslint.config.mjs +18 -0
  5. package/apps/web/next.config.ts +7 -0
  6. package/apps/web/package-lock.json +10348 -0
  7. package/apps/web/package.json +35 -0
  8. package/apps/web/postcss.config.mjs +7 -0
  9. package/apps/web/public/file.svg +1 -0
  10. package/apps/web/public/globe.svg +1 -0
  11. package/apps/web/public/next.svg +1 -0
  12. package/apps/web/public/vercel.svg +1 -0
  13. package/apps/web/public/window.svg +1 -0
  14. package/apps/web/src/app/.well-known/agent-card.json/route.ts +50 -0
  15. package/apps/web/src/app/background-tasks/[jobId]/cancel/route.ts +29 -0
  16. package/apps/web/src/app/events/stream/route.ts +58 -0
  17. package/apps/web/src/app/favicon.ico +0 -0
  18. package/apps/web/src/app/globals.css +174 -0
  19. package/apps/web/src/app/layout.tsx +34 -0
  20. package/apps/web/src/app/messages/answer/route.ts +57 -0
  21. package/apps/web/src/app/messages/stream/route.ts +381 -0
  22. package/apps/web/src/app/page.tsx +358 -0
  23. package/apps/web/src/app/tasks/[taskId]/cancel/route.ts +24 -0
  24. package/apps/web/src/app/tasks/[taskId]/route.ts +24 -0
  25. package/apps/web/src/app/tasks/route.ts +13 -0
  26. package/apps/web/src/components/chat/agent-blocks.tsx +111 -0
  27. package/apps/web/src/components/chat/ask-user-question-panel.tsx +172 -0
  28. package/apps/web/src/components/chat/session-sidebar.tsx +222 -0
  29. package/apps/web/src/components/chat/subagent-activity-sidebar.tsx +248 -0
  30. package/apps/web/src/components/chat/tool-blocks.tsx +550 -0
  31. package/apps/web/src/lib/a2a/activity-store.ts +150 -0
  32. package/apps/web/src/lib/a2a/client.ts +357 -0
  33. package/apps/web/src/lib/a2a/sse.ts +19 -0
  34. package/apps/web/src/lib/a2a/task-store.ts +111 -0
  35. package/apps/web/src/lib/a2a/types.ts +216 -0
  36. package/apps/web/src/lib/agent/answer-store.ts +109 -0
  37. package/apps/web/src/lib/agent/background-delivery.ts +343 -0
  38. package/apps/web/src/lib/agent/background-tool.ts +78 -0
  39. package/apps/web/src/lib/agent/background.ts +452 -0
  40. package/apps/web/src/lib/agent/chat.ts +543 -0
  41. package/apps/web/src/lib/agent/session-store.ts +26 -0
  42. package/apps/web/src/lib/chat/types.ts +44 -0
  43. package/apps/web/src/lib/env.ts +31 -0
  44. package/apps/web/src/lib/hooks/useA2AChat.ts +863 -0
  45. package/apps/web/src/lib/state/chat-atoms.ts +52 -0
  46. package/apps/web/src/lib/workspace.ts +9 -0
  47. package/apps/web/tsconfig.json +35 -0
  48. package/bin/remotion-agent.js +451 -0
  49. package/package.json +34 -0
  50. package/templates/.claude/CLAUDE.md +95 -0
  51. package/templates/.claude/README.md +129 -0
  52. package/templates/.claude/agents/composer-agent.md +188 -0
  53. package/templates/.claude/agents/crafter.md +181 -0
  54. package/templates/.claude/agents/creator.md +134 -0
  55. package/templates/.claude/agents/perceiver.md +92 -0
  56. package/templates/.claude/settings.json +36 -0
  57. package/templates/.claude/settings.local.json +39 -0
  58. package/templates/.claude/skills/agent-browser/SKILL.md +349 -0
  59. package/templates/.claude/skills/agent-browser/references/authentication.md +188 -0
  60. package/templates/.claude/skills/agent-browser/references/proxy-support.md +175 -0
  61. package/templates/.claude/skills/agent-browser/references/session-management.md +181 -0
  62. package/templates/.claude/skills/agent-browser/references/snapshot-refs.md +186 -0
  63. package/templates/.claude/skills/agent-browser/references/video-recording.md +162 -0
  64. package/templates/.claude/skills/agent-browser/templates/authenticated-session.sh +91 -0
  65. package/templates/.claude/skills/agent-browser/templates/capture-workflow.sh +68 -0
  66. package/templates/.claude/skills/agent-browser/templates/form-automation.sh +64 -0
  67. package/templates/.claude/skills/algorithmic-art/LICENSE.txt +202 -0
  68. package/templates/.claude/skills/algorithmic-art/SKILL.md +405 -0
  69. package/templates/.claude/skills/algorithmic-art/templates/generator_template.js +223 -0
  70. package/templates/.claude/skills/algorithmic-art/templates/viewer.html +599 -0
  71. package/templates/.claude/skills/asset-validator/SKILL.md +376 -0
  72. package/templates/.claude/skills/audio-video-sync/SKILL.md +219 -0
  73. package/templates/.claude/skills/bgm-manager/SKILL.md +334 -0
  74. package/templates/.claude/skills/remotion-best-practices/SKILL.md +45 -0
  75. package/templates/.claude/skills/remotion-best-practices/rules/3d.md +86 -0
  76. package/templates/.claude/skills/remotion-best-practices/rules/animations.md +29 -0
  77. package/templates/.claude/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
  78. package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
  79. package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
  80. package/templates/.claude/skills/remotion-best-practices/rules/assets.md +78 -0
  81. package/templates/.claude/skills/remotion-best-practices/rules/audio.md +172 -0
  82. package/templates/.claude/skills/remotion-best-practices/rules/calculate-metadata.md +104 -0
  83. package/templates/.claude/skills/remotion-best-practices/rules/can-decode.md +75 -0
  84. package/templates/.claude/skills/remotion-best-practices/rules/charts.md +58 -0
  85. package/templates/.claude/skills/remotion-best-practices/rules/compositions.md +141 -0
  86. package/templates/.claude/skills/remotion-best-practices/rules/display-captions.md +126 -0
  87. package/templates/.claude/skills/remotion-best-practices/rules/extract-frames.md +229 -0
  88. package/templates/.claude/skills/remotion-best-practices/rules/fonts.md +152 -0
  89. package/templates/.claude/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
  90. package/templates/.claude/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
  91. package/templates/.claude/skills/remotion-best-practices/rules/get-video-duration.md +58 -0
  92. package/templates/.claude/skills/remotion-best-practices/rules/gifs.md +138 -0
  93. package/templates/.claude/skills/remotion-best-practices/rules/images.md +130 -0
  94. package/templates/.claude/skills/remotion-best-practices/rules/import-srt-captions.md +67 -0
  95. package/templates/.claude/skills/remotion-best-practices/rules/lottie.md +68 -0
  96. package/templates/.claude/skills/remotion-best-practices/rules/maps.md +403 -0
  97. package/templates/.claude/skills/remotion-best-practices/rules/measuring-dom-nodes.md +35 -0
  98. package/templates/.claude/skills/remotion-best-practices/rules/measuring-text.md +143 -0
  99. package/templates/.claude/skills/remotion-best-practices/rules/parameters.md +98 -0
  100. package/templates/.claude/skills/remotion-best-practices/rules/sequencing.md +118 -0
  101. package/templates/.claude/skills/remotion-best-practices/rules/tailwind.md +11 -0
  102. package/templates/.claude/skills/remotion-best-practices/rules/text-animations.md +20 -0
  103. package/templates/.claude/skills/remotion-best-practices/rules/timing.md +179 -0
  104. package/templates/.claude/skills/remotion-best-practices/rules/transcribe-captions.md +19 -0
  105. package/templates/.claude/skills/remotion-best-practices/rules/transitions.md +122 -0
  106. package/templates/.claude/skills/remotion-best-practices/rules/trimming.md +53 -0
  107. package/templates/.claude/skills/remotion-best-practices/rules/videos.md +171 -0
  108. package/templates/.claude/skills/remotion-components/SKILL.md +453 -0
  109. package/templates/.claude/skills/render-config/SKILL.md +290 -0
  110. package/templates/.claude/skills/script-writer/SKILL.md +59 -0
  111. package/templates/.claude/skills/style-director/script-writer/SKILL.md +82 -0
  112. package/templates/.claude/skills/style-director/style-director/SKILL.md +287 -0
  113. package/templates/.claude/skills/style-director/style-director/references/audience-and-scenarios.md +43 -0
  114. package/templates/.claude/skills/style-director/style-director/references/interaction-innovation.md +26 -0
  115. package/templates/.claude/skills/style-director/style-director/references/motion-grammar.md +66 -0
  116. package/templates/.claude/skills/style-director/style-director/references/quality-checklist.md +29 -0
  117. package/templates/.claude/skills/style-director/style-director/references/scene-recipes.md +38 -0
  118. package/templates/.claude/skills/style-director/style-director/references/visual-style-system.md +148 -0
  119. package/templates/.claude/skills/subtitle-composer/SKILL.md +304 -0
  120. package/templates/.claude/skills/subtitle-processor/SKILL.md +308 -0
  121. package/templates/.claude/skills/timeline-generator/SKILL.md +253 -0
  122. package/templates/.claude/skills/video-preflight-check/SKILL.md +353 -0
  123. package/templates/.claude/skills/voice-synthesizer/SKILL.md +296 -0
  124. package/templates/.claude/skills/voice-synthesizer/scripts/synthesize_voice.py +315 -0
  125. package/templates/.claude/skills/voice-synthesizer/scripts/tts_cli.py +142 -0
  126. package/templates/.claude/skills/web-design-guidelines/SKILL.md +36 -0
  127. package/templates/.claude/skills/youtube-downloader/SKILL.md +99 -0
  128. package/templates/.claude/skills/youtube-downloader/scripts/download_video.py +145 -0
@@ -0,0 +1,452 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2
+
3
+ import { appendBackgroundTaskEvent, appendTodoUpdateEvent } from "@/lib/a2a/activity-store";
4
+ import type { A2aTodoItem } from "@/lib/a2a/types";
5
+ import { enqueueBackgroundDelivery } from "@/lib/agent/background-delivery";
6
+ import { getEnv } from "@/lib/env";
7
+ import { getWorkspaceDir } from "@/lib/workspace";
8
+
9
+ const DEFAULT_CONCURRENCY = 2;
10
+ const PROGRESS_INTERVAL_MS = 1200;
11
+ const PROGRESS_CHARS = 400;
12
+
13
+ export type BackgroundTaskInput = {
14
+ contextId: string;
15
+ description?: string;
16
+ prompt: string;
17
+ subagentType?: string;
18
+ parentTaskId?: string;
19
+ };
20
+
21
+ export type BackgroundTaskStatus = "queued" | "running" | "completed" | "failed" | "cancelled";
22
+
23
+ export type BackgroundTaskRecord = {
24
+ jobId: string;
25
+ contextId: string;
26
+ status: BackgroundTaskStatus;
27
+ description?: string;
28
+ prompt: string;
29
+ subagentType?: string;
30
+ parentTaskId?: string;
31
+ createdAt: number;
32
+ startedAt?: number;
33
+ finishedAt?: number;
34
+ result?: string;
35
+ error?: string;
36
+ sessionId?: string;
37
+ todos?: A2aTodoItem[];
38
+ todoUpdatedAt?: number;
39
+ cancelRequested?: boolean;
40
+ abortController?: AbortController;
41
+ };
42
+
43
+ export type BackgroundTaskSummary = {
44
+ jobId: string;
45
+ contextId: string;
46
+ status: BackgroundTaskStatus;
47
+ description?: string;
48
+ subagentType?: string;
49
+ createdAt: number;
50
+ startedAt?: number;
51
+ finishedAt?: number;
52
+ result?: string;
53
+ error?: string;
54
+ todos?: A2aTodoItem[];
55
+ todoUpdatedAt?: number;
56
+ };
57
+
58
+ type BackgroundQueue = {
59
+ running: number;
60
+ queue: string[];
61
+ jobs: Map<string, BackgroundTaskRecord>;
62
+ };
63
+
64
+ declare global {
65
+ var __A2A_BACKGROUND_QUEUE__: BackgroundQueue | undefined;
66
+ }
67
+
68
+ function getQueue(): BackgroundQueue {
69
+ if (!globalThis.__A2A_BACKGROUND_QUEUE__) {
70
+ globalThis.__A2A_BACKGROUND_QUEUE__ = {
71
+ running: 0,
72
+ queue: [],
73
+ jobs: new Map(),
74
+ };
75
+ }
76
+ return globalThis.__A2A_BACKGROUND_QUEUE__;
77
+ }
78
+
79
+ function getConcurrency(): number {
80
+ const raw = process.env.BACKGROUND_TASK_CONCURRENCY;
81
+ const value = raw ? Number(raw) : DEFAULT_CONCURRENCY;
82
+ if (!Number.isFinite(value) || value <= 0) return DEFAULT_CONCURRENCY;
83
+ return Math.floor(value);
84
+ }
85
+
86
+ function makeJobPrompt(input: BackgroundTaskInput): string {
87
+ if (input.description) {
88
+ return [`Task: ${input.description}`, "", input.prompt].join("\n");
89
+ }
90
+ return input.prompt;
91
+ }
92
+
93
+ function publishEvent(job: BackgroundTaskRecord, status: BackgroundTaskStatus) {
94
+ appendBackgroundTaskEvent({
95
+ contextId: job.contextId,
96
+ data: {
97
+ jobId: job.jobId,
98
+ status,
99
+ description: job.description,
100
+ prompt: job.prompt,
101
+ subagentType: job.subagentType,
102
+ createdAt: job.createdAt,
103
+ startedAt: job.startedAt,
104
+ finishedAt: job.finishedAt,
105
+ result: job.result,
106
+ error: job.error,
107
+ parentTaskId: job.parentTaskId,
108
+ sessionId: job.sessionId,
109
+ },
110
+ });
111
+ }
112
+
113
+ export function listBackgroundTaskSummaries(input?: {
114
+ contextId?: string;
115
+ }): BackgroundTaskSummary[] {
116
+ const queue = getQueue();
117
+ const tasks = Array.from(queue.jobs.values());
118
+ const filtered = input?.contextId
119
+ ? tasks.filter((job) => job.contextId === input.contextId)
120
+ : tasks;
121
+ return filtered.map((job) => ({
122
+ jobId: job.jobId,
123
+ contextId: job.contextId,
124
+ status: job.status,
125
+ description: job.description,
126
+ subagentType: job.subagentType,
127
+ createdAt: job.createdAt,
128
+ startedAt: job.startedAt,
129
+ finishedAt: job.finishedAt,
130
+ result: job.result ? job.result.slice(0, 2000) : undefined,
131
+ error: job.error ? job.error.slice(0, 2000) : undefined,
132
+ todos: job.todos,
133
+ todoUpdatedAt: job.todoUpdatedAt,
134
+ }));
135
+ }
136
+
137
+ export function enqueueBackgroundTask(input: BackgroundTaskInput): BackgroundTaskRecord {
138
+ const queue = getQueue();
139
+ const jobId = globalThis.crypto.randomUUID();
140
+ const now = Date.now();
141
+ const job: BackgroundTaskRecord = {
142
+ jobId,
143
+ contextId: input.contextId,
144
+ status: "queued",
145
+ description: input.description,
146
+ prompt: input.prompt,
147
+ subagentType: input.subagentType,
148
+ parentTaskId: input.parentTaskId,
149
+ createdAt: now,
150
+ };
151
+
152
+ queue.jobs.set(jobId, job);
153
+ queue.queue.push(jobId);
154
+ publishEvent(job, "queued");
155
+ setTimeout(() => {
156
+ void drainQueue();
157
+ }, 0);
158
+ return job;
159
+ }
160
+
161
+ export function cancelBackgroundTasksForParent(parentTaskId: string): number {
162
+ const queue = getQueue();
163
+ let cancelled = 0;
164
+
165
+ for (const job of queue.jobs.values()) {
166
+ if (job.parentTaskId !== parentTaskId) continue;
167
+ if (job.status === "completed" || job.status === "failed" || job.status === "cancelled") {
168
+ continue;
169
+ }
170
+
171
+ job.cancelRequested = true;
172
+ if (job.status === "queued") {
173
+ queue.queue = queue.queue.filter((id) => id !== job.jobId);
174
+ job.status = "cancelled";
175
+ job.finishedAt = Date.now();
176
+ job.error = "cancelled";
177
+ publishEvent(job, "cancelled");
178
+ cancelled += 1;
179
+ continue;
180
+ }
181
+
182
+ job.abortController?.abort();
183
+ cancelled += 1;
184
+ }
185
+
186
+ return cancelled;
187
+ }
188
+
189
+ export function cancelBackgroundTask(jobId: string): boolean {
190
+ const queue = getQueue();
191
+ const job = queue.jobs.get(jobId);
192
+ if (!job) return false;
193
+ if (job.status === "completed" || job.status === "failed" || job.status === "cancelled") {
194
+ return false;
195
+ }
196
+
197
+ job.cancelRequested = true;
198
+ if (job.status === "queued") {
199
+ queue.queue = queue.queue.filter((id) => id !== job.jobId);
200
+ job.status = "cancelled";
201
+ job.finishedAt = Date.now();
202
+ job.error = "cancelled";
203
+ publishEvent(job, "cancelled");
204
+ return true;
205
+ }
206
+
207
+ job.abortController?.abort();
208
+ return true;
209
+ }
210
+
211
+ async function drainQueue() {
212
+ const queue = getQueue();
213
+ const concurrency = getConcurrency();
214
+
215
+ while (queue.running < concurrency && queue.queue.length > 0) {
216
+ const jobId = queue.queue.shift();
217
+ if (!jobId) break;
218
+ const job = queue.jobs.get(jobId);
219
+ if (!job || job.status !== "queued") continue;
220
+ queue.running += 1;
221
+ void runJob(job)
222
+ .catch((err) => {
223
+ console.warn("Background task failed", err);
224
+ })
225
+ .finally(() => {
226
+ queue.running -= 1;
227
+ void drainQueue();
228
+ });
229
+ }
230
+ }
231
+
232
+ function extractTextDelta(event: unknown): string | null {
233
+ if (!event || typeof event !== "object") return null;
234
+ const rec = event as Record<string, unknown>;
235
+ if (rec.type !== "content_block_delta") return null;
236
+ const delta = rec.delta;
237
+ if (!delta || typeof delta !== "object") return null;
238
+ const deltaRec = delta as Record<string, unknown>;
239
+ const text = deltaRec.text;
240
+ return typeof text === "string" ? text : null;
241
+ }
242
+
243
+ function extractSdkSessionId(message: unknown): string | undefined {
244
+ if (!message || typeof message !== "object") return undefined;
245
+ const rec = message as Record<string, unknown>;
246
+ const sid = rec.session_id;
247
+ return typeof sid === "string" && sid.length > 0 ? sid : undefined;
248
+ }
249
+
250
+ async function* singleTurnPrompt(text: string) {
251
+ yield {
252
+ type: "user" as const,
253
+ message: {
254
+ role: "user" as const,
255
+ content: text,
256
+ },
257
+ parent_tool_use_id: null,
258
+ session_id: "",
259
+ };
260
+ }
261
+
262
+ async function runJob(job: BackgroundTaskRecord): Promise<void> {
263
+ await new Promise((resolve) => setTimeout(resolve, 0));
264
+ const env = getEnv();
265
+ job.status = "running";
266
+ job.startedAt = Date.now();
267
+ publishEvent(job, "running");
268
+
269
+ let output = "";
270
+ let stderrBuf = "";
271
+ let lastProgressAt = 0;
272
+ let lastProgressLen = 0;
273
+
274
+ const abortController = new AbortController();
275
+ job.abortController = abortController;
276
+
277
+ const q = query({
278
+ prompt: singleTurnPrompt(
279
+ makeJobPrompt({
280
+ contextId: job.contextId,
281
+ description: job.description,
282
+ prompt: job.prompt,
283
+ subagentType: job.subagentType,
284
+ parentTaskId: job.parentTaskId,
285
+ }),
286
+ ),
287
+ options: {
288
+ abortController,
289
+ model: env.anthropicModel,
290
+ agent: job.subagentType ?? "assistant",
291
+ includePartialMessages: true,
292
+ maxThinkingTokens: 10000,
293
+ allowedTools: [
294
+ "WebSearch",
295
+ "WebFetch",
296
+ "Bash",
297
+ "BashOutput",
298
+ "KillBash",
299
+ "Read",
300
+ "Write",
301
+ "Edit",
302
+ "Glob",
303
+ "Grep",
304
+ "Skill",
305
+ "Task",
306
+ "TodoWrite",
307
+ ],
308
+ settingSources: ["project"],
309
+ permissionMode: "bypassPermissions",
310
+ cwd: getWorkspaceDir(),
311
+ hooks: {
312
+ PreToolUse: [
313
+ {
314
+ matcher: undefined,
315
+ hooks: [
316
+ async (hookInput: unknown) => {
317
+ const i = hookInput as Record<string, unknown>;
318
+ if (i.tool_name === "TodoWrite") {
319
+ const todos = extractTodos(i.tool_input);
320
+ if (todos) {
321
+ job.todos = todos;
322
+ job.todoUpdatedAt = Date.now();
323
+ appendTodoUpdateEvent({
324
+ contextId: job.contextId,
325
+ data: {
326
+ jobId: job.jobId,
327
+ todos,
328
+ updatedAt: job.todoUpdatedAt,
329
+ },
330
+ });
331
+ }
332
+ }
333
+ return { continue: true };
334
+ },
335
+ ],
336
+ },
337
+ ],
338
+ },
339
+ stderr: (data) => {
340
+ stderrBuf += data;
341
+ if (stderrBuf.length > 20000) stderrBuf = stderrBuf.slice(-20000);
342
+ },
343
+ env: {
344
+ ...process.env,
345
+ ...(env.anthropicApiKey
346
+ ? { ANTHROPIC_API_KEY: env.anthropicApiKey }
347
+ : {}),
348
+ ...(env.anthropicBaseUrl
349
+ ? { ANTHROPIC_BASE_URL: env.anthropicBaseUrl }
350
+ : {}),
351
+ ...(env.anthropicDefaultHaikuModel
352
+ ? { ANTHROPIC_DEFAULT_HAIKU_MODEL: env.anthropicDefaultHaikuModel }
353
+ : {}),
354
+ ...(env.anthropicModel ? { ANTHROPIC_MODEL: env.anthropicModel } : {}),
355
+ },
356
+ },
357
+ });
358
+
359
+ try {
360
+ for await (const msg of q) {
361
+ if (!job.sessionId) {
362
+ const sid = extractSdkSessionId(msg);
363
+ if (sid) job.sessionId = sid;
364
+ }
365
+ if (msg.type === "stream_event") {
366
+ const delta = extractTextDelta(msg.event);
367
+ if (delta) {
368
+ output += delta;
369
+ const now = Date.now();
370
+ if (
371
+ now - lastProgressAt >= PROGRESS_INTERVAL_MS &&
372
+ output.length - lastProgressLen >= PROGRESS_CHARS
373
+ ) {
374
+ lastProgressAt = now;
375
+ lastProgressLen = output.length;
376
+ job.result = output;
377
+ publishEvent(job, "running");
378
+ }
379
+ }
380
+ }
381
+ }
382
+
383
+ if (job.cancelRequested || abortController.signal.aborted) {
384
+ job.status = "cancelled";
385
+ job.finishedAt = Date.now();
386
+ job.error = "cancelled";
387
+ publishEvent(job, "cancelled");
388
+ return;
389
+ }
390
+
391
+ job.status = "completed";
392
+ job.finishedAt = Date.now();
393
+ job.result = output;
394
+ publishEvent(job, "completed");
395
+ enqueueBackgroundDelivery({
396
+ contextId: job.contextId,
397
+ jobId: job.jobId,
398
+ description: job.description,
399
+ subagentType: job.subagentType,
400
+ status: "completed",
401
+ result: job.result,
402
+ finishedAt: job.finishedAt,
403
+ });
404
+ } catch (err) {
405
+ const base = err instanceof Error ? err.message : "Unknown error";
406
+ const stderr = stderrBuf.trim();
407
+ if (job.cancelRequested || abortController.signal.aborted) {
408
+ job.status = "cancelled";
409
+ job.finishedAt = Date.now();
410
+ job.error = "cancelled";
411
+ publishEvent(job, "cancelled");
412
+ return;
413
+ }
414
+
415
+ job.status = "failed";
416
+ job.finishedAt = Date.now();
417
+ job.error = [base, stderr].filter((s) => s && s.length > 0).join("\n");
418
+ publishEvent(job, "failed");
419
+ enqueueBackgroundDelivery({
420
+ contextId: job.contextId,
421
+ jobId: job.jobId,
422
+ description: job.description,
423
+ subagentType: job.subagentType,
424
+ status: "failed",
425
+ result: job.result,
426
+ error: job.error,
427
+ finishedAt: job.finishedAt,
428
+ });
429
+ }
430
+ }
431
+
432
+ function extractTodos(input: unknown): A2aTodoItem[] | null {
433
+ if (!input || typeof input !== "object") return null;
434
+ const rec = input as Record<string, unknown>;
435
+ if (!Array.isArray(rec.todos)) return null;
436
+ const todos = rec.todos
437
+ .map((todo): A2aTodoItem | null => {
438
+ if (!todo || typeof todo !== "object") return null;
439
+ const t = todo as Record<string, unknown>;
440
+ const content = typeof t.content === "string" ? t.content : null;
441
+ const status =
442
+ t.status === "pending" || t.status === "in_progress" || t.status === "completed"
443
+ ? (t.status as A2aTodoItem["status"])
444
+ : null;
445
+ if (!content || !status) return null;
446
+ const item: A2aTodoItem = { content, status };
447
+ if (typeof t.activeForm === "string") item.activeForm = t.activeForm;
448
+ return item;
449
+ })
450
+ .filter((t): t is A2aTodoItem => t !== null);
451
+ return todos.length > 0 ? todos : null;
452
+ }