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,543 @@
1
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2
+
3
+ import { getEnv } from "@/lib/env";
4
+ import { getWorkspaceDir } from "@/lib/workspace";
5
+ import type { AskUserQuestion } from "@/lib/a2a/types";
6
+ import { waitForAnswer } from "@/lib/agent/answer-store";
7
+ import {
8
+ BACKGROUND_TASK_LIST_TOOL_NAME,
9
+ BACKGROUND_TASK_TOOL_NAME,
10
+ createBackgroundTaskServer,
11
+ } from "@/lib/agent/background-tool";
12
+ import { enqueueBackgroundTask } from "@/lib/agent/background";
13
+
14
+ type TokenEvent = {
15
+ type: "token";
16
+ text: string;
17
+ };
18
+
19
+ type ToolCallEvent = {
20
+ type: "toolCall";
21
+ toolName: string;
22
+ toolId: string;
23
+ input: unknown;
24
+ status: "started" | "completed";
25
+ output?: unknown;
26
+ /** 递增序号,用于前端匹配 started/completed 事件 */
27
+ toolSeq?: number;
28
+ };
29
+
30
+ type FinalEvent = {
31
+ type: "final";
32
+ fullText: string;
33
+ totalCostUsd?: number;
34
+ };
35
+
36
+ type AskUserQuestionEvent = {
37
+ type: "askUserQuestion";
38
+ requestId: string;
39
+ taskId: string;
40
+ questions: AskUserQuestion[];
41
+ };
42
+
43
+ type SubagentEvent = {
44
+ type: "subagent";
45
+ agentId: string;
46
+ agentType: string;
47
+ status: "started" | "stopped";
48
+ };
49
+
50
+ export type ChatStreamEvent =
51
+ | TokenEvent
52
+ | ToolCallEvent
53
+ | FinalEvent
54
+ | AskUserQuestionEvent
55
+ | SubagentEvent;
56
+
57
+ function extractTextDelta(event: unknown): string | null {
58
+ if (!event || typeof event !== "object") return null;
59
+ const rec = event as Record<string, unknown>;
60
+ if (rec.type !== "content_block_delta") return null;
61
+ const delta = rec.delta;
62
+ if (!delta || typeof delta !== "object") return null;
63
+ const deltaRec = delta as Record<string, unknown>;
64
+ const text = deltaRec.text;
65
+ return typeof text === "string" ? text : null;
66
+ }
67
+
68
+ function extractSdkSessionId(message: unknown): string | undefined {
69
+ if (!message || typeof message !== "object") return undefined;
70
+ const rec = message as Record<string, unknown>;
71
+ const sid = rec.session_id;
72
+ return typeof sid === "string" && sid.length > 0 ? sid : undefined;
73
+ }
74
+
75
+ async function* singleTurnPrompt(text: string) {
76
+ yield {
77
+ type: "user" as const,
78
+ message: {
79
+ role: "user" as const,
80
+ content: text,
81
+ },
82
+ parent_tool_use_id: null,
83
+ session_id: "",
84
+ };
85
+ }
86
+
87
+ type ToolCallPayload = {
88
+ type: "toolCall";
89
+ toolName: string;
90
+ toolId: string;
91
+ input: unknown;
92
+ status: "started" | "completed";
93
+ output?: unknown;
94
+ /** 递增序号,用于前端匹配 started/completed 事件(解决 toolId 不一致问题) */
95
+ toolSeq?: number;
96
+ };
97
+
98
+ type AskUserQuestionPayload = {
99
+ type: "askUserQuestion";
100
+ requestId: string;
101
+ taskId: string;
102
+ questions: AskUserQuestion[];
103
+ };
104
+
105
+ type QuestionDismissedPayload = {
106
+ type: "questionDismissed";
107
+ requestId: string;
108
+ taskId: string;
109
+ reason: "timeout" | "cancelled";
110
+ };
111
+
112
+ type SubagentPayload = {
113
+ type: "subagent";
114
+ agentId: string;
115
+ agentType: string;
116
+ status: "started" | "stopped";
117
+ };
118
+
119
+ /**
120
+ * 工具调用状态同步策略:
121
+ *
122
+ * 1. 使用 onToolEvent callback 立即推送 SSE(不依赖 generator yield 时序)
123
+ * 2. 用递增序号 (toolSeq) 作为备用标识,解决 toolId 不一致问题
124
+ * 3. 前端用 toolName + 顺序匹配来关联 started/completed 事件
125
+ * 4. 已通过 callback 发送的事件不再通过 generator yield(避免重复)
126
+ */
127
+ export async function* streamChatReply(input: {
128
+ userText: string;
129
+ taskId: string;
130
+ contextId?: string;
131
+ resumeSessionId?: string;
132
+ abortController?: AbortController;
133
+ onSessionId?: (sessionId: string) => void;
134
+ onToolEvent?: (ev: ToolCallPayload) => void;
135
+ onAskUserQuestion?: (ev: AskUserQuestionPayload) => void;
136
+ onQuestionDismissed?: (ev: QuestionDismissedPayload) => void;
137
+ onSubagentEvent?: (ev: SubagentPayload) => void;
138
+ }): AsyncGenerator<ChatStreamEvent, void> {
139
+ const env = getEnv();
140
+
141
+ let fullText = "";
142
+ let totalCostUsd: number | undefined;
143
+ let stderrBuf = "";
144
+
145
+ let emittedSessionId: string | undefined;
146
+
147
+ const { onToolEvent, onAskUserQuestion, onQuestionDismissed, onSubagentEvent, taskId } = input;
148
+
149
+ // 仅当没有 callback 时才通过 generator yield(兜底路径)
150
+ const toolEventsForYield: ToolCallPayload[] = [];
151
+ const subagentEventsForYield: SubagentPayload[] = [];
152
+
153
+ // 递增序号,用于前端匹配 started/completed 事件
154
+ let toolSeq = 0;
155
+ // 记录每个工具调用的序号,用于 PostToolUse 时查找对应的 started 事件
156
+ const toolIdToSeq = new Map<string, number>();
157
+
158
+ const pushTool = (ev: ToolCallPayload & { toolSeq?: number }) => {
159
+ // 为 started 事件分配序号,completed 事件复用同一序号
160
+ if (ev.status === "started") {
161
+ toolSeq++;
162
+ ev.toolSeq = toolSeq;
163
+ if (ev.toolId) {
164
+ toolIdToSeq.set(ev.toolId, toolSeq);
165
+ }
166
+ } else if (ev.status === "completed") {
167
+ // 尝试从 toolId 找到对应序号
168
+ const seq = ev.toolId ? toolIdToSeq.get(ev.toolId) : undefined;
169
+ ev.toolSeq = seq ?? toolSeq; // 如果找不到,用最新序号(兜底)
170
+ }
171
+
172
+ if (onToolEvent) {
173
+ // 有 callback 时直接推送,不存入 yield 队列
174
+ onToolEvent(ev);
175
+ } else {
176
+ // 无 callback 时存入队列等待 yield
177
+ toolEventsForYield.push(ev);
178
+ }
179
+ };
180
+
181
+ const pushSubagent = (ev: SubagentPayload) => {
182
+ if (onSubagentEvent) {
183
+ onSubagentEvent(ev);
184
+ } else {
185
+ subagentEventsForYield.push(ev);
186
+ }
187
+ };
188
+
189
+ const isBackgroundTaskInput = (
190
+ toolName: string,
191
+ toolInput: unknown,
192
+ ): boolean => {
193
+ if (toolName !== "Task" || !toolInput || typeof toolInput !== "object") return false;
194
+ const rec = toolInput as Record<string, unknown>;
195
+ return (
196
+ rec.run_in_background === true ||
197
+ rec.background === true ||
198
+ rec.runInBackground === true
199
+ );
200
+ };
201
+
202
+ // canUseTool 回调处理 AskUserQuestion
203
+ const canUseTool = async (
204
+ toolName: string,
205
+ toolInput: Record<string, unknown>,
206
+ ): Promise<
207
+ | { behavior: "allow"; updatedInput?: Record<string, unknown> }
208
+ | { behavior: "deny"; message: string; interrupt?: boolean }
209
+ > => {
210
+ if (toolName === BACKGROUND_TASK_TOOL_NAME) {
211
+ return { behavior: "allow", updatedInput: toolInput };
212
+ }
213
+
214
+ if (toolName === "Task") {
215
+ const runInBackground =
216
+ toolInput.run_in_background === true ||
217
+ toolInput.background === true ||
218
+ toolInput.runInBackground === true;
219
+
220
+ if (runInBackground) {
221
+ const prompt = typeof toolInput.prompt === "string" ? toolInput.prompt : "";
222
+ const description =
223
+ typeof toolInput.description === "string" ? toolInput.description : undefined;
224
+ const subagentType =
225
+ typeof toolInput.subagent_type === "string" ? toolInput.subagent_type : undefined;
226
+
227
+ if (!input.contextId) {
228
+ return {
229
+ behavior: "deny",
230
+ message: "Background tasks require an active contextId. Please retry after the task is created.",
231
+ };
232
+ }
233
+
234
+ if (!prompt.trim()) {
235
+ return {
236
+ behavior: "deny",
237
+ message: "Background tasks require a prompt. Use mcp__background_tasks__start instead.",
238
+ };
239
+ }
240
+
241
+ const job = enqueueBackgroundTask({
242
+ contextId: input.contextId,
243
+ parentTaskId: taskId,
244
+ description,
245
+ prompt,
246
+ subagentType,
247
+ });
248
+
249
+ const syntheticToolId = `background-${job.jobId}`;
250
+ const toolInputPayload = {
251
+ description,
252
+ prompt,
253
+ subagent_type: subagentType,
254
+ };
255
+
256
+ pushTool({
257
+ type: "toolCall",
258
+ toolName: BACKGROUND_TASK_TOOL_NAME,
259
+ toolId: syntheticToolId,
260
+ input: toolInputPayload,
261
+ status: "started",
262
+ });
263
+
264
+ pushTool({
265
+ type: "toolCall",
266
+ toolName: BACKGROUND_TASK_TOOL_NAME,
267
+ toolId: syntheticToolId,
268
+ input: toolInputPayload,
269
+ status: "completed",
270
+ output: { jobId: job.jobId, status: job.status },
271
+ });
272
+
273
+ return {
274
+ behavior: "deny",
275
+ message:
276
+ "Background task queued. Use mcp__background_tasks__start for background execution instead of Task.",
277
+ };
278
+ }
279
+ }
280
+
281
+ if (toolName === "AskUserQuestion") {
282
+ const questions = (toolInput.questions ?? []) as AskUserQuestion[];
283
+ const requestId = `${taskId}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
284
+
285
+ // 通知前端显示问题
286
+ onAskUserQuestion?.({
287
+ type: "askUserQuestion",
288
+ requestId,
289
+ taskId,
290
+ questions,
291
+ });
292
+
293
+ try {
294
+ // 等待用户回答(最多 60 秒)
295
+ const answers = await waitForAnswer({
296
+ requestId,
297
+ taskId,
298
+ questions,
299
+ });
300
+
301
+ // 返回用户的回答
302
+ return {
303
+ behavior: "allow",
304
+ updatedInput: {
305
+ questions,
306
+ answers,
307
+ },
308
+ };
309
+ } catch (err) {
310
+ // 超时或取消 - 通知前端清除问题UI
311
+ const reason = err instanceof Error && err.message.includes("timeout")
312
+ ? "timeout" as const
313
+ : "cancelled" as const;
314
+ onQuestionDismissed?.({
315
+ type: "questionDismissed",
316
+ requestId,
317
+ taskId,
318
+ reason,
319
+ });
320
+ return {
321
+ behavior: "deny",
322
+ message: err instanceof Error ? err.message : "User did not respond",
323
+ };
324
+ }
325
+ }
326
+
327
+ // 其他工具自动允许(bypassPermissions 模式)
328
+ return { behavior: "allow", updatedInput: toolInput };
329
+ };
330
+
331
+ const hooks = {
332
+ PreToolUse: [
333
+ {
334
+ matcher: undefined,
335
+ hooks: [
336
+ async (hookInput: unknown) => {
337
+ const i = hookInput as Record<string, unknown>;
338
+ const toolName = String(i.tool_name || "unknown");
339
+ if (isBackgroundTaskInput(toolName, i.tool_input)) {
340
+ return { continue: true };
341
+ }
342
+ pushTool({
343
+ type: "toolCall",
344
+ toolName,
345
+ toolId: String(i.tool_use_id || ""),
346
+ input: i.tool_input,
347
+ status: "started",
348
+ });
349
+ return { continue: true };
350
+ },
351
+ ],
352
+ },
353
+ ],
354
+ PostToolUse: [
355
+ {
356
+ matcher: undefined,
357
+ hooks: [
358
+ async (hookInput: unknown) => {
359
+ const i = hookInput as Record<string, unknown>;
360
+ const toolName = String(i.tool_name || "unknown");
361
+ if (isBackgroundTaskInput(toolName, i.tool_input)) {
362
+ return { continue: true };
363
+ }
364
+ pushTool({
365
+ type: "toolCall",
366
+ toolName,
367
+ toolId: String(i.tool_use_id || ""),
368
+ input: null,
369
+ status: "completed",
370
+ output: i.tool_response,
371
+ });
372
+ return { continue: true };
373
+ },
374
+ ],
375
+ },
376
+ ],
377
+ // 子代理开始
378
+ SubagentStart: [
379
+ {
380
+ matcher: undefined,
381
+ hooks: [
382
+ async (hookInput: unknown) => {
383
+ const i = hookInput as Record<string, unknown>;
384
+ pushSubagent({
385
+ type: "subagent",
386
+ agentId: String(i.agent_id || ""),
387
+ agentType: String(i.agent_type || "unknown"),
388
+ status: "started",
389
+ });
390
+ return { continue: true };
391
+ },
392
+ ],
393
+ },
394
+ ],
395
+ // 子代理结束
396
+ SubagentStop: [
397
+ {
398
+ matcher: undefined,
399
+ hooks: [
400
+ async (hookInput: unknown) => {
401
+ const i = hookInput as Record<string, unknown>;
402
+ pushSubagent({
403
+ type: "subagent",
404
+ agentId: String(i.agent_id || ""),
405
+ agentType: String(i.agent_type || "unknown"),
406
+ status: "stopped",
407
+ });
408
+ return { continue: true };
409
+ },
410
+ ],
411
+ },
412
+ ],
413
+ // 停止时触发(可用于清理资源)
414
+ Stop: [
415
+ {
416
+ matcher: undefined,
417
+ hooks: [
418
+ async (hookInput: unknown) => {
419
+ const i = hookInput as Record<string, unknown>;
420
+ const stopHookActive = Boolean(i.stop_hook_active);
421
+ // 可以在这里执行清理逻辑
422
+ console.log("[Stop Hook] Agent stopping, stop_hook_active:", stopHookActive);
423
+ return { continue: true };
424
+ },
425
+ ],
426
+ },
427
+ ],
428
+ };
429
+
430
+ const backgroundTaskServer =
431
+ input.contextId
432
+ ? createBackgroundTaskServer({ contextId: input.contextId, parentTaskId: taskId })
433
+ : null;
434
+
435
+ const q = query({
436
+ prompt: singleTurnPrompt(input.userText),
437
+ options: {
438
+ abortController: input.abortController,
439
+ model: env.anthropicModel,
440
+ agent: "assistant",
441
+ ...(input.resumeSessionId
442
+ ? { resume: input.resumeSessionId, forkSession: false }
443
+ : {}),
444
+ includePartialMessages: true,
445
+ maxThinkingTokens: 10000,
446
+ allowedTools: [
447
+ "WebSearch",
448
+ "WebFetch",
449
+ "Bash",
450
+ "BashOutput",
451
+ "KillBash",
452
+ "Read",
453
+ "Write",
454
+ "Edit",
455
+ "Glob",
456
+ "Grep",
457
+ "AskUserQuestion",
458
+ "Skill",
459
+ "Task",
460
+ "TodoWrite",
461
+ BACKGROUND_TASK_TOOL_NAME,
462
+ BACKGROUND_TASK_LIST_TOOL_NAME,
463
+ ],
464
+ settingSources: ["project"],
465
+ permissionMode: "bypassPermissions",
466
+ cwd: getWorkspaceDir(),
467
+ ...(backgroundTaskServer ? { mcpServers: { background_tasks: backgroundTaskServer } } : {}),
468
+ canUseTool,
469
+ hooks,
470
+ stderr: (data) => {
471
+ stderrBuf += data;
472
+ if (stderrBuf.length > 20000) stderrBuf = stderrBuf.slice(-20000);
473
+ },
474
+ env: {
475
+ ...process.env,
476
+ ...(env.anthropicApiKey
477
+ ? { ANTHROPIC_API_KEY: env.anthropicApiKey }
478
+ : {}),
479
+ ...(env.anthropicBaseUrl
480
+ ? { ANTHROPIC_BASE_URL: env.anthropicBaseUrl }
481
+ : {}),
482
+ ...(env.anthropicDefaultHaikuModel
483
+ ? { ANTHROPIC_DEFAULT_HAIKU_MODEL: env.anthropicDefaultHaikuModel }
484
+ : {}),
485
+ ...(env.anthropicModel ? { ANTHROPIC_MODEL: env.anthropicModel } : {}),
486
+ },
487
+ },
488
+ });
489
+
490
+ try {
491
+ for await (const msg of q) {
492
+ // 仅当没有 callback 时才通过 yield 发送事件(兜底路径)
493
+ while (toolEventsForYield.length > 0) {
494
+ yield toolEventsForYield.shift()!;
495
+ }
496
+ while (subagentEventsForYield.length > 0) {
497
+ yield subagentEventsForYield.shift()!;
498
+ }
499
+
500
+ if (!emittedSessionId) {
501
+ const sid = extractSdkSessionId(msg);
502
+ if (sid) {
503
+ emittedSessionId = sid;
504
+ input.onSessionId?.(sid);
505
+ }
506
+ }
507
+
508
+ if (msg.type === "stream_event") {
509
+ const delta = extractTextDelta(msg.event);
510
+ if (delta) {
511
+ fullText += delta;
512
+ yield { type: "token", text: delta };
513
+ }
514
+ continue;
515
+ }
516
+
517
+ if (msg.type === "result") {
518
+ totalCostUsd = msg.total_cost_usd;
519
+ }
520
+ }
521
+ } catch (e) {
522
+ const base = e instanceof Error ? e.message : "Unknown error";
523
+ const stderr = stderrBuf.trim();
524
+ const hint =
525
+ !env.anthropicApiKey && stderr.length === 0
526
+ ? "Missing ANTHROPIC_API_KEY (set apps/web/.env.local from .env.example) or authenticate Claude Code via `claude`."
527
+ : "";
528
+ const message = [base, stderr, hint]
529
+ .filter((s) => s && s.length > 0)
530
+ .join("\n");
531
+ throw new Error(message);
532
+ }
533
+
534
+ // 清空剩余的兜底队列
535
+ while (toolEventsForYield.length > 0) {
536
+ yield toolEventsForYield.shift()!;
537
+ }
538
+ while (subagentEventsForYield.length > 0) {
539
+ yield subagentEventsForYield.shift()!;
540
+ }
541
+
542
+ yield { type: "final", fullText, totalCostUsd };
543
+ }
@@ -0,0 +1,26 @@
1
+ type SessionRecord = {
2
+ sessionId: string;
3
+ updatedAtMs: number;
4
+ };
5
+
6
+ declare global {
7
+ var __AGENT_SESSION_BY_CONTEXT__: Map<string, SessionRecord> | undefined;
8
+ }
9
+
10
+ function getStore(): Map<string, SessionRecord> {
11
+ if (!globalThis.__AGENT_SESSION_BY_CONTEXT__) {
12
+ globalThis.__AGENT_SESSION_BY_CONTEXT__ = new Map();
13
+ }
14
+ return globalThis.__AGENT_SESSION_BY_CONTEXT__;
15
+ }
16
+
17
+ export function getSessionIdForContext(contextId: string): string | undefined {
18
+ return getStore().get(contextId)?.sessionId;
19
+ }
20
+
21
+ export function setSessionIdForContext(input: { contextId: string; sessionId: string }): void {
22
+ getStore().set(input.contextId, {
23
+ sessionId: input.sessionId,
24
+ updatedAtMs: Date.now(),
25
+ });
26
+ }
@@ -0,0 +1,44 @@
1
+ import type { AskUserQuestion as A2aAskUserQuestion, A2aTodoItem } from "@/lib/a2a/types";
2
+
3
+ export type AgentTextBlock = { kind: "text"; text: string };
4
+ export type AgentToolBlock = {
5
+ kind: "tool";
6
+ toolId: string;
7
+ toolName: string;
8
+ input: unknown;
9
+ status: "started" | "completed";
10
+ output?: unknown;
11
+ /** 递增序号,用于匹配 started/completed 事件 */
12
+ toolSeq?: number;
13
+ };
14
+ export type AgentBlock = AgentTextBlock | AgentToolBlock;
15
+
16
+ export type ChatItem =
17
+ | { id: string; role: "user"; text: string }
18
+ | {
19
+ id: string;
20
+ role: "agent";
21
+ taskId?: string;
22
+ jobId?: string;
23
+ blocks: AgentBlock[];
24
+ };
25
+
26
+ export type PendingQuestion = {
27
+ requestId: string;
28
+ taskId: string;
29
+ questions: A2aAskUserQuestion[];
30
+ };
31
+
32
+ export type SubagentActivity = {
33
+ agentId: string;
34
+ agentType: string;
35
+ status: "running" | "stopped";
36
+ startedAt: number;
37
+ stoppedAt?: number;
38
+ isBackground?: boolean;
39
+ prompt?: string;
40
+ description?: string;
41
+ result?: string;
42
+ todos?: A2aTodoItem[];
43
+ todoUpdatedAt?: number;
44
+ };
@@ -0,0 +1,31 @@
1
+ export type Env = {
2
+ anthropicApiKey?: string;
3
+ anthropicBaseUrl?: string;
4
+ anthropicDefaultHaikuModel?: string;
5
+ anthropicModel: string;
6
+ a2aVersion: string;
7
+ a2aAgentName: string;
8
+ a2aAgentDescription: string;
9
+ nextPublicA2aBaseUrl: string;
10
+ };
11
+
12
+ function getEnvString(key: string): string | undefined {
13
+ const v = process.env[key];
14
+ return v && v.trim().length > 0 ? v.trim() : undefined;
15
+ }
16
+
17
+ export function getEnv(): Env {
18
+ return {
19
+ anthropicApiKey: getEnvString("ANTHROPIC_API_KEY"),
20
+ anthropicBaseUrl: getEnvString("ANTHROPIC_BASE_URL"),
21
+ anthropicDefaultHaikuModel: getEnvString("ANTHROPIC_DEFAULT_HAIKU_MODEL"),
22
+ anthropicModel: getEnvString("ANTHROPIC_MODEL") ?? "claude-3-5-sonnet-latest",
23
+ a2aVersion: getEnvString("A2A_VERSION") ?? "0.3.0",
24
+ a2aAgentName: getEnvString("A2A_AGENT_NAME") ?? "Remotion Demo Agent (MVP)",
25
+ a2aAgentDescription:
26
+ getEnvString("A2A_AGENT_DESCRIPTION") ??
27
+ "Local A2A agent for chat + future demo video generation",
28
+ nextPublicA2aBaseUrl:
29
+ getEnvString("NEXT_PUBLIC_A2A_BASE_URL") ?? "http://localhost:3000",
30
+ };
31
+ }