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,109 @@
1
+ /**
2
+ * 管理 AskUserQuestion 的等待和回答机制
3
+ * 当 Claude 调用 AskUserQuestion 时,后端会暂停执行等待用户回答
4
+ */
5
+
6
+ import type { AskUserQuestion } from "@/lib/a2a/types";
7
+
8
+ type PendingQuestion = {
9
+ requestId: string;
10
+ taskId: string;
11
+ questions: AskUserQuestion[];
12
+ resolve: (answers: Record<string, string>) => void;
13
+ reject: (reason: Error) => void;
14
+ timeoutId: ReturnType<typeof setTimeout>;
15
+ };
16
+
17
+ // 存储等待中的问题
18
+ const pendingQuestions = new Map<string, PendingQuestion>();
19
+
20
+ // 默认超时时间 60 秒(SDK 要求)
21
+ const DEFAULT_TIMEOUT_MS = 60_000;
22
+
23
+ /**
24
+ * 创建一个等待用户回答的 Promise
25
+ * 在 canUseTool 回调中调用
26
+ */
27
+ export function waitForAnswer(input: {
28
+ requestId: string;
29
+ taskId: string;
30
+ questions: AskUserQuestion[];
31
+ timeoutMs?: number;
32
+ }): Promise<Record<string, string>> {
33
+ const { requestId, taskId, questions, timeoutMs = DEFAULT_TIMEOUT_MS } = input;
34
+
35
+ return new Promise((resolve, reject) => {
36
+ const timeoutId = setTimeout(() => {
37
+ pendingQuestions.delete(requestId);
38
+ reject(new Error(`AskUserQuestion timeout after ${timeoutMs}ms`));
39
+ }, timeoutMs);
40
+
41
+ pendingQuestions.set(requestId, {
42
+ requestId,
43
+ taskId,
44
+ questions,
45
+ resolve,
46
+ reject,
47
+ timeoutId,
48
+ });
49
+ });
50
+ }
51
+
52
+ /**
53
+ * 提交用户的回答
54
+ * 在 answer API 端点中调用
55
+ */
56
+ export function submitAnswer(input: {
57
+ requestId: string;
58
+ answers: Record<string, string>;
59
+ }): boolean {
60
+ const { requestId, answers } = input;
61
+ const pending = pendingQuestions.get(requestId);
62
+
63
+ if (!pending) {
64
+ return false;
65
+ }
66
+
67
+ clearTimeout(pending.timeoutId);
68
+ pendingQuestions.delete(requestId);
69
+ pending.resolve(answers);
70
+ return true;
71
+ }
72
+
73
+ /**
74
+ * 取消等待中的问题
75
+ */
76
+ export function cancelPendingQuestion(requestId: string): boolean {
77
+ const pending = pendingQuestions.get(requestId);
78
+
79
+ if (!pending) {
80
+ return false;
81
+ }
82
+
83
+ clearTimeout(pending.timeoutId);
84
+ pendingQuestions.delete(requestId);
85
+ pending.reject(new Error("Question cancelled"));
86
+ return true;
87
+ }
88
+
89
+ /**
90
+ * 检查是否有等待中的问题
91
+ */
92
+ export function hasPendingQuestion(requestId: string): boolean {
93
+ return pendingQuestions.has(requestId);
94
+ }
95
+
96
+ /**
97
+ * 获取等待中的问题详情
98
+ */
99
+ export function getPendingQuestion(requestId: string): {
100
+ taskId: string;
101
+ questions: AskUserQuestion[];
102
+ } | null {
103
+ const pending = pendingQuestions.get(requestId);
104
+ if (!pending) return null;
105
+ return {
106
+ taskId: pending.taskId,
107
+ questions: pending.questions,
108
+ };
109
+ }
@@ -0,0 +1,343 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2
+
3
+ import { appendDeliveryMessageEvent } from "@/lib/a2a/activity-store";
4
+ import { getSessionIdForContext, setSessionIdForContext } from "@/lib/agent/session-store";
5
+ import { getEnv } from "@/lib/env";
6
+ import { getWorkspaceDir } from "@/lib/workspace";
7
+
8
+ const MAX_DELIVERY_CHARS = 300;
9
+ const MAX_PROMPT_DETAIL_CHARS = 600;
10
+ const MAX_DIRECT_DETAIL_CHARS = 160;
11
+
12
+ export type BackgroundDeliveryItem = {
13
+ contextId: string;
14
+ jobId: string;
15
+ description?: string;
16
+ subagentType?: string;
17
+ status: "completed" | "failed";
18
+ result?: string;
19
+ error?: string;
20
+ finishedAt?: number;
21
+ };
22
+
23
+ type DeliveryQueue = {
24
+ delivering: boolean;
25
+ items: BackgroundDeliveryItem[];
26
+ };
27
+
28
+ type DeliveryStore = {
29
+ queues: Map<string, DeliveryQueue>;
30
+ activeContexts: Set<string>;
31
+ };
32
+
33
+ declare global {
34
+ var __A2A_DELIVERY_STORE__: DeliveryStore | undefined;
35
+ }
36
+
37
+ function getStore(): DeliveryStore {
38
+ if (!globalThis.__A2A_DELIVERY_STORE__) {
39
+ globalThis.__A2A_DELIVERY_STORE__ = {
40
+ queues: new Map(),
41
+ activeContexts: new Set(),
42
+ };
43
+ }
44
+ return globalThis.__A2A_DELIVERY_STORE__;
45
+ }
46
+
47
+ function getQueue(contextId: string): DeliveryQueue {
48
+ const store = getStore();
49
+ const existing = store.queues.get(contextId);
50
+ if (existing) return existing;
51
+ const queue: DeliveryQueue = { delivering: false, items: [] };
52
+ store.queues.set(contextId, queue);
53
+ return queue;
54
+ }
55
+
56
+ export function markContextActive(contextId: string) {
57
+ getStore().activeContexts.add(contextId);
58
+ }
59
+
60
+ export function markContextIdle(contextId: string) {
61
+ getStore().activeContexts.delete(contextId);
62
+ void flushBackgroundDeliveries(contextId);
63
+ }
64
+
65
+ export function enqueueBackgroundDelivery(item: BackgroundDeliveryItem) {
66
+ const queue = getQueue(item.contextId);
67
+ queue.items.push(item);
68
+ if (!getStore().activeContexts.has(item.contextId)) {
69
+ void flushBackgroundDeliveries(item.contextId);
70
+ }
71
+ }
72
+
73
+ function compactText(input?: string): string {
74
+ if (!input) return "";
75
+ return input.replace(/\s+/g, " ").trim();
76
+ }
77
+
78
+ function truncateText(input: string, max: number): string {
79
+ const clean = compactText(input);
80
+ if (clean.length <= max) return clean;
81
+ return `${clean.slice(0, Math.max(0, max - 3)).trimEnd()}...`;
82
+ }
83
+
84
+ function buildTaskLabel(item: BackgroundDeliveryItem): string {
85
+ return item.description || item.subagentType || "后台任务";
86
+ }
87
+
88
+ function buildDirectMessage(item: BackgroundDeliveryItem): string {
89
+ const taskLabel = buildTaskLabel(item);
90
+ const statusLabel = item.status === "completed" ? "已完成" : "已失败";
91
+ const detailSource = item.status === "failed" ? item.error : item.result;
92
+ const detail = detailSource
93
+ ? truncateText(detailSource, MAX_DIRECT_DETAIL_CHARS)
94
+ : "";
95
+ const detailLabel = detail
96
+ ? item.status === "failed"
97
+ ? `错误:${detail}`
98
+ : `结果:${detail}`
99
+ : "";
100
+ return detailLabel
101
+ ? `任务 ${taskLabel}(id:${item.jobId})${statusLabel}。${detailLabel}`
102
+ : `任务 ${taskLabel}(id:${item.jobId})${statusLabel}`;
103
+ }
104
+
105
+ async function* singleTurnPrompt(text: string) {
106
+ yield {
107
+ type: "user" as const,
108
+ message: {
109
+ role: "user" as const,
110
+ content: text,
111
+ },
112
+ parent_tool_use_id: null,
113
+ session_id: "",
114
+ };
115
+ }
116
+
117
+ function extractTextDelta(event: unknown): string | null {
118
+ if (!event || typeof event !== "object") return null;
119
+ const rec = event as Record<string, unknown>;
120
+ if (rec.type !== "content_block_delta") return null;
121
+ const delta = rec.delta;
122
+ if (!delta || typeof delta !== "object") return null;
123
+ const deltaRec = delta as Record<string, unknown>;
124
+ const text = deltaRec.text;
125
+ return typeof text === "string" ? text : null;
126
+ }
127
+
128
+ function extractTextFromContent(content: unknown): string | null {
129
+ if (typeof content === "string") return content;
130
+ if (!Array.isArray(content)) return null;
131
+ let text = "";
132
+ for (const block of content) {
133
+ if (!block || typeof block !== "object") continue;
134
+ const rec = block as Record<string, unknown>;
135
+ if (rec.type === "text" && typeof rec.text === "string") {
136
+ text += rec.text;
137
+ }
138
+ }
139
+ return text ? text : null;
140
+ }
141
+
142
+ function extractAssistantText(message: unknown): string | null {
143
+ if (!message || typeof message !== "object") return null;
144
+ const rec = message as Record<string, unknown>;
145
+ if (rec.type === "assistant") {
146
+ const msg = rec.message;
147
+ if (!msg || typeof msg !== "object") return null;
148
+ const content = (msg as Record<string, unknown>).content;
149
+ return extractTextFromContent(content);
150
+ }
151
+ if (rec.type === "result" && rec.subtype === "success") {
152
+ const result = rec.result;
153
+ if (typeof result === "string" && result.trim().length > 0) return result;
154
+ }
155
+ return null;
156
+ }
157
+
158
+ function extractSdkSessionId(message: unknown): string | undefined {
159
+ if (!message || typeof message !== "object") return undefined;
160
+ const rec = message as Record<string, unknown>;
161
+ const sid = rec.session_id;
162
+ return typeof sid === "string" && sid.length > 0 ? sid : undefined;
163
+ }
164
+
165
+ function buildResumePrompt(item: BackgroundDeliveryItem): string {
166
+ const taskLabel = buildTaskLabel(item);
167
+ const statusLabel = item.status === "completed" ? "已完成" : "已失败";
168
+ const detailSource = item.status === "failed" ? item.error : item.result;
169
+ const detail = detailSource
170
+ ? truncateText(detailSource, MAX_PROMPT_DETAIL_CHARS)
171
+ : "";
172
+ const detailLabel = detail
173
+ ? item.status === "failed"
174
+ ? `错误:${detail}`
175
+ : `结果:${detail}`
176
+ : "";
177
+ const lines = [
178
+ "你是主会话的通知助手,只需输出一条简短通知。",
179
+ "要求:不要调用任何工具;不要提出后续动作;不要重复;不要提问;不使用 Markdown;最多两句话。",
180
+ `任务:${taskLabel}(id:${item.jobId})`,
181
+ `状态:${statusLabel}`,
182
+ ];
183
+ if (detailLabel) lines.push(detailLabel);
184
+ lines.push("请输出通知:");
185
+ return lines.join("\n");
186
+ }
187
+
188
+ async function generateDeliveryMessage(
189
+ item: BackgroundDeliveryItem,
190
+ sessionId: string,
191
+ onUpdate?: (text: string) => void,
192
+ ): Promise<string> {
193
+ const env = getEnv();
194
+ let output = "";
195
+ let stderrBuf = "";
196
+ let finalText: string | null = null;
197
+ let lastUpdateLen = 0;
198
+ let sawTextDelta = false;
199
+ let abortedForLength = false;
200
+ const abortController = new AbortController();
201
+
202
+ const emitProgress = () => {
203
+ if (!onUpdate || output.length === 0) return;
204
+ if (output.length !== lastUpdateLen) {
205
+ lastUpdateLen = output.length;
206
+ onUpdate(output);
207
+ }
208
+ };
209
+
210
+ const q = query({
211
+ prompt: singleTurnPrompt(buildResumePrompt(item)),
212
+ options: {
213
+ abortController,
214
+ model: env.anthropicModel,
215
+ resume: sessionId,
216
+ forkSession: false,
217
+ includePartialMessages: true,
218
+ maxThinkingTokens: 256,
219
+ maxTurns: 1,
220
+ permissionMode: "bypassPermissions",
221
+ allowedTools: [],
222
+ settingSources: ["project"],
223
+ cwd: getWorkspaceDir(),
224
+ stderr: (data) => {
225
+ stderrBuf += data;
226
+ if (stderrBuf.length > 20000) stderrBuf = stderrBuf.slice(-20000);
227
+ },
228
+ env: {
229
+ ...process.env,
230
+ ...(env.anthropicApiKey
231
+ ? { ANTHROPIC_API_KEY: env.anthropicApiKey }
232
+ : {}),
233
+ ...(env.anthropicBaseUrl
234
+ ? { ANTHROPIC_BASE_URL: env.anthropicBaseUrl }
235
+ : {}),
236
+ ...(env.anthropicDefaultHaikuModel
237
+ ? { ANTHROPIC_DEFAULT_HAIKU_MODEL: env.anthropicDefaultHaikuModel }
238
+ : {}),
239
+ ...(env.anthropicModel ? { ANTHROPIC_MODEL: env.anthropicModel } : {}),
240
+ },
241
+ },
242
+ });
243
+
244
+ try {
245
+ for await (const msg of q) {
246
+ const sid = extractSdkSessionId(msg);
247
+ if (sid) setSessionIdForContext({ contextId: item.contextId, sessionId: sid });
248
+
249
+ if (msg.type === "stream_event") {
250
+ const delta = extractTextDelta(msg.event);
251
+ if (delta) {
252
+ output += delta;
253
+ sawTextDelta = true;
254
+ if (output.length >= MAX_DELIVERY_CHARS) {
255
+ abortedForLength = true;
256
+ emitProgress();
257
+ abortController.abort();
258
+ break;
259
+ }
260
+ emitProgress();
261
+ }
262
+ continue;
263
+ }
264
+
265
+ const assistantText = extractAssistantText(msg);
266
+ if (assistantText) finalText = assistantText;
267
+ }
268
+ } catch (err) {
269
+ if (!abortedForLength) {
270
+ throw err;
271
+ }
272
+ }
273
+
274
+ if (abortedForLength && output.trim().length > 0) {
275
+ return truncateText(output, MAX_DELIVERY_CHARS);
276
+ }
277
+
278
+ if (sawTextDelta) return output;
279
+ const trimmed = output.trim();
280
+ if (trimmed) return output;
281
+
282
+ if (finalText && finalText.trim().length > 0) return finalText;
283
+
284
+ const stderr = stderrBuf.trim();
285
+ if (stderr) throw new Error(stderr);
286
+ return buildDirectMessage(item);
287
+ }
288
+
289
+ export async function flushBackgroundDeliveries(contextId: string) {
290
+ const store = getStore();
291
+ if (store.activeContexts.has(contextId)) return;
292
+
293
+ const queue = getQueue(contextId);
294
+ if (queue.delivering) return;
295
+ queue.delivering = true;
296
+
297
+ try {
298
+ while (queue.items.length > 0 && !store.activeContexts.has(contextId)) {
299
+ const item = queue.items.shift();
300
+ if (!item) return;
301
+
302
+ let lastEmittedText = "";
303
+ let didStreamUpdate = false;
304
+ const emitDelivery = (text: string) => {
305
+ if (!text || text === lastEmittedText) return;
306
+ const isDelta = text.startsWith(lastEmittedText);
307
+ const rawText = isDelta ? text.slice(lastEmittedText.length) : text;
308
+ if (rawText.length === 0) return;
309
+ lastEmittedText = text;
310
+ appendDeliveryMessageEvent({
311
+ contextId,
312
+ data: {
313
+ jobId: item.jobId,
314
+ text: rawText,
315
+ createdAt: Date.now(),
316
+ isDelta,
317
+ },
318
+ });
319
+ };
320
+
321
+ const sessionId = getSessionIdForContext(contextId);
322
+ if (!sessionId) {
323
+ emitDelivery(buildDirectMessage(item));
324
+ continue;
325
+ }
326
+
327
+ try {
328
+ const text = await generateDeliveryMessage(item, sessionId, (update) => {
329
+ didStreamUpdate = true;
330
+ emitDelivery(update);
331
+ });
332
+ emitDelivery(text);
333
+ } catch (err) {
334
+ console.warn("Background delivery resume failed", err);
335
+ if (!didStreamUpdate) {
336
+ emitDelivery(buildDirectMessage(item));
337
+ }
338
+ }
339
+ }
340
+ } finally {
341
+ queue.delivering = false;
342
+ }
343
+ }
@@ -0,0 +1,78 @@
1
+ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
2
+ import { z } from "zod";
3
+
4
+ import { enqueueBackgroundTask, listBackgroundTaskSummaries } from "@/lib/agent/background";
5
+
6
+ export const BACKGROUND_TASK_TOOL_NAME = "mcp__background_tasks__start";
7
+ export const BACKGROUND_TASK_LIST_TOOL_NAME = "mcp__background_tasks__list";
8
+
9
+ export function createBackgroundTaskServer(input: {
10
+ contextId: string;
11
+ parentTaskId: string;
12
+ }) {
13
+ return createSdkMcpServer({
14
+ name: "background_tasks",
15
+ version: "1.0.0",
16
+ tools: [
17
+ tool(
18
+ "start",
19
+ "Start a background task and return a jobId immediately. Progress is reported via the activity stream.",
20
+ {
21
+ description: z.string().min(1).describe("Short label shown in the UI"),
22
+ prompt: z.string().min(1).describe("Task instructions"),
23
+ subagent_type: z
24
+ .string()
25
+ .min(1)
26
+ .optional()
27
+ .describe("Subagent name from .claude/agents"),
28
+ },
29
+ async (args) => {
30
+ const job = enqueueBackgroundTask({
31
+ contextId: input.contextId,
32
+ parentTaskId: input.parentTaskId,
33
+ description: args.description,
34
+ prompt: args.prompt,
35
+ subagentType: args.subagent_type,
36
+ });
37
+
38
+ return {
39
+ content: [
40
+ {
41
+ type: "text",
42
+ text: JSON.stringify({ jobId: job.jobId, status: job.status }),
43
+ },
44
+ ],
45
+ };
46
+ },
47
+ ),
48
+ tool(
49
+ "list",
50
+ "List background tasks for the current session with status/todo snapshot.",
51
+ { _: z.optional(z.never()).describe("No arguments") },
52
+ async () => {
53
+ const tasks = listBackgroundTaskSummaries({ contextId: input.contextId }).map(
54
+ (task) => ({
55
+ jobId: task.jobId,
56
+ status: task.status,
57
+ description: task.description,
58
+ subagentType: task.subagentType,
59
+ createdAt: task.createdAt,
60
+ startedAt: task.startedAt,
61
+ finishedAt: task.finishedAt,
62
+ todos: task.todos,
63
+ todoUpdatedAt: task.todoUpdatedAt,
64
+ }),
65
+ );
66
+ return {
67
+ content: [
68
+ {
69
+ type: "text",
70
+ text: JSON.stringify({ tasks }),
71
+ },
72
+ ],
73
+ };
74
+ },
75
+ ),
76
+ ],
77
+ });
78
+ }