minimal-agent 0.12.0 → 0.13.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 (74) hide show
  1. package/package.json +3 -2
  2. package/plugins/workflow-runner/src/expressions.js +29 -8
  3. package/plugins/workflow-runner/src/expressions.ts +28 -10
  4. package/plugins/workflow-runner/src/index.js +45 -17
  5. package/plugins/workflow-runner/src/index.ts +44 -18
  6. package/plugins/workflow-runner/src/recovery.js +9 -2
  7. package/plugins/workflow-runner/src/recovery.ts +9 -2
  8. package/plugins/workflow-runner/src/runner.js +31 -5
  9. package/plugins/workflow-runner/src/runner.ts +33 -5
  10. package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
  11. package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -1
  12. package/plugins/workflow-runner/src/stepExecutors/tool.js +29 -10
  13. package/plugins/workflow-runner/src/stepExecutors/tool.ts +29 -11
  14. package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
  15. package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
  16. package/plugins/workflow-runner/src/taskIdentity.js +197 -21
  17. package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
  18. package/plugins/workflow-runner/src/types.js +113 -7
  19. package/plugins/workflow-runner/src/types.ts +111 -7
  20. package/plugins/workflow-runner/src/workflowState.js +455 -54
  21. package/plugins/workflow-runner/src/workflowState.ts +508 -63
  22. package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
  23. package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
  24. package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
  25. package/plugins/workflow-runner/test/expressions.test.ts +42 -0
  26. package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
  27. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
  28. package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
  29. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
  30. package/plugins/workflow-runner/test/research.load.test.ts +328 -0
  31. package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
  32. package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
  33. package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
  34. package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
  35. package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
  36. package/plugins/workflow-runner/test/word.load.test.ts +9 -9
  37. package/skills/skill-creator/scripts/run_eval.py +3 -2
  38. package/src/cli/print.js +17 -0
  39. package/src/cli/print.ts +25 -0
  40. package/src/config/configService.js +24 -0
  41. package/src/config/configService.ts +23 -0
  42. package/src/infra/verification.js +25 -18
  43. package/src/infra/verification.ts +50 -18
  44. package/src/plugins/pluginRunner.js +18 -7
  45. package/src/plugins/pluginRunner.ts +20 -6
  46. package/src/prompts/policyRegistry.js +28 -1
  47. package/src/prompts/policyRegistry.ts +28 -1
  48. package/src/tools/advisor/advisor.js +76 -9
  49. package/src/tools/advisor/advisor.ts +79 -9
  50. package/src/tools/bash/bash.js +51 -6
  51. package/src/tools/bash/bash.ts +56 -7
  52. package/src/tools/powershell/powershell.js +18 -1
  53. package/src/tools/powershell/powershell.ts +18 -2
  54. package/src/tools/read/read.js +9 -4
  55. package/src/tools/read/read.ts +9 -4
  56. package/src/tools/shared/permanentDeleteGuard.js +34 -3
  57. package/src/tools/shared/permanentDeleteGuard.ts +43 -6
  58. package/src/tools/webbrowser/browserControl.js +5 -1
  59. package/src/tools/webbrowser/browserControl.ts +7 -1
  60. package/src/tools/webbrowser/webbrowser.js +8 -4
  61. package/src/tools/webbrowser/webbrowser.ts +8 -4
  62. package/src/types.ts +10 -4
  63. package/workflows/adaptive-research.yaml +5 -5
  64. package/workflows/book-review-short.yaml +5 -5
  65. package/workflows/e2e-write-greet.yaml +2 -2
  66. package/workflows/illustrate.yaml +12 -12
  67. package/workflows/plan.yaml +18 -18
  68. package/workflows/ppt.yaml +201 -201
  69. package/workflows/ralph-loop.yaml +3 -1
  70. package/workflows/research.md +449 -0
  71. package/workflows/research.yaml +873 -0
  72. package/workflows/webppt.yaml +47 -47
  73. package/workflows/word.yaml +80 -80
  74. package/workflows/youtube-shorts.yaml +25 -25
@@ -191,7 +191,24 @@ async function call(input, signal, context) {
191
191
  }
192
192
  export const powerShellTool = {
193
193
  name: 'PowerShell',
194
- description: 'Executes native PowerShell on Windows. Use for Windows-specific administration and scripts; use run_in_background for long builds, tests, or servers, then TaskOutput/TaskStop.',
194
+ description: `Executes native PowerShell on Windows. Use for Windows-specific administration and scripts; use run_in_background for long builds, tests, or servers, then TaskOutput/TaskStop.
195
+
196
+ # Safety - two levels, from strict to lenient
197
+
198
+ ## 1. Blocked outright (fails before execution - do NOT retry with variants)
199
+
200
+ Catastrophic commands (no legitimate use): \`rm -rf /\`-class deletions of root/$HOME/system directories, \`mkfs\`, \`format X:\`, \`dd\` to raw devices, shutdown/reboot, fork bombs, pipe-to-shell from network (\`curl ... | sh\`), \`chmod 777 /\`, full-disk \`del /s\` / \`rmdir /s\`, \`diskpart\`.
201
+
202
+ Permanent deletion (bypasses the Recycle Bin - use the Trash tool instead): \`Remove-Item\`, \`Clear-RecycleBin\`, \`del\` / \`erase\` / \`rd\` / \`rmdir\` / \`rm\` / \`ri\`, and \`.Delete()\` method calls - all fail before execution.
203
+ To delete files or directories use the **Trash** tool (\`action: "move"\` - recoverable); to clean Git untracked files use Trash with \`action: "git-clean"\`. Do NOT try other deletion spellings - they are blocked the same way. If a permanent deletion is truly required, ask the user to run it themselves.
204
+
205
+ ## 2. Requires user confirmation (irreversible)
206
+
207
+ These have legitimate uses but destroy the last copy - execution pauses and asks the user to confirm. In non-interactive contexts they are automatically cancelled.
208
+ - \`git push --force\` / \`-f\` / \`+refspec\`, \`git push --delete <branch>\`
209
+ - \`git filter-branch\` / \`git filter-repo\`, \`git reflog expire --expire=now\` / \`git gc --prune=now\`
210
+ - \`shred -u\` / \`shred --remove\`
211
+ - SQL data destruction: \`DROP DATABASE\`, \`DROP TABLE\`, \`TRUNCATE TABLE\``,
195
212
  inputSchema,
196
213
  parameters: toToolParameters(inputSchema),
197
214
  isReadOnly: false,
@@ -219,8 +219,24 @@ async function call(
219
219
 
220
220
  export const powerShellTool: Tool<PowerShellInput> = {
221
221
  name: 'PowerShell',
222
- description:
223
- 'Executes native PowerShell on Windows. Use for Windows-specific administration and scripts; use run_in_background for long builds, tests, or servers, then TaskOutput/TaskStop.',
222
+ description: `Executes native PowerShell on Windows. Use for Windows-specific administration and scripts; use run_in_background for long builds, tests, or servers, then TaskOutput/TaskStop.
223
+
224
+ # Safety - two levels, from strict to lenient
225
+
226
+ ## 1. Blocked outright (fails before execution - do NOT retry with variants)
227
+
228
+ Catastrophic commands (no legitimate use): \`rm -rf /\`-class deletions of root/$HOME/system directories, \`mkfs\`, \`format X:\`, \`dd\` to raw devices, shutdown/reboot, fork bombs, pipe-to-shell from network (\`curl ... | sh\`), \`chmod 777 /\`, full-disk \`del /s\` / \`rmdir /s\`, \`diskpart\`.
229
+
230
+ Permanent deletion (bypasses the Recycle Bin - use the Trash tool instead): \`Remove-Item\`, \`Clear-RecycleBin\`, \`del\` / \`erase\` / \`rd\` / \`rmdir\` / \`rm\` / \`ri\`, and \`.Delete()\` method calls - all fail before execution.
231
+ To delete files or directories use the **Trash** tool (\`action: "move"\` - recoverable); to clean Git untracked files use Trash with \`action: "git-clean"\`. Do NOT try other deletion spellings - they are blocked the same way. If a permanent deletion is truly required, ask the user to run it themselves.
232
+
233
+ ## 2. Requires user confirmation (irreversible)
234
+
235
+ These have legitimate uses but destroy the last copy - execution pauses and asks the user to confirm. In non-interactive contexts they are automatically cancelled.
236
+ - \`git push --force\` / \`-f\` / \`+refspec\`, \`git push --delete <branch>\`
237
+ - \`git filter-branch\` / \`git filter-repo\`, \`git reflog expire --expire=now\` / \`git gc --prune=now\`
238
+ - \`shred -u\` / \`shred --remove\`
239
+ - SQL data destruction: \`DROP DATABASE\`, \`DROP TABLE\`, \`TRUNCATE TABLE\``,
224
240
  inputSchema,
225
241
  parameters: toToolParameters(inputSchema),
226
242
  isReadOnly: false,
@@ -168,10 +168,15 @@ async function call(input, state, cwd, runtimeIdentity) {
168
168
  };
169
169
  }
170
170
  const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
171
- const document = await pdfjs.getDocument({
171
+ // ⚠️ 释放要调**加载任务**的 destroy,不是文档代理的:pdfjs v6 的
172
+ // PDFDocumentProxy 上没有 destroy(只有 cleanup),写成 document.destroy()
173
+ // 会在 finally 里抛 TypeError,被外层 catch 成「PDF 解析失败」——
174
+ // 于是每一次 PDF 读取都失败,且错误文案完全指不向真因。
175
+ const loadingTask = pdfjs.getDocument({
172
176
  data: new Uint8Array(bytes),
173
177
  useSystemFonts: true,
174
- }).promise;
178
+ });
179
+ const document = await loadingTask.promise;
175
180
  try {
176
181
  const requested = parsePdfPages(input.pages, document.numPages);
177
182
  if (requested.length > 20)
@@ -209,7 +214,7 @@ async function call(input, state, cwd, runtimeIdentity) {
209
214
  };
210
215
  }
211
216
  finally {
212
- await document.destroy().catch(() => undefined);
217
+ await loadingTask.destroy().catch(() => undefined);
213
218
  }
214
219
  }
215
220
  catch (error) {
@@ -359,7 +364,7 @@ async function renderPdfPages(document, filePath, pages, identity) {
359
364
  const viewport = page.getViewport({ scale: 2 });
360
365
  const canvas = createCanvas(Math.ceil(viewport.width), Math.ceil(viewport.height));
361
366
  await page.render({
362
- canvasContext: canvas.getContext('2d'),
367
+ canvas: canvas.getContext('2d'),
363
368
  viewport,
364
369
  }).promise;
365
370
  const target = join(outputDir, `${outputPrefix}-page-${pageNumber}.png`);
@@ -219,10 +219,15 @@ async function call(
219
219
  };
220
220
  }
221
221
  const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
222
- const document = await pdfjs.getDocument({
222
+ // ⚠️ 释放要调**加载任务**的 destroy,不是文档代理的:pdfjs v6 的
223
+ // PDFDocumentProxy 上没有 destroy(只有 cleanup),写成 document.destroy()
224
+ // 会在 finally 里抛 TypeError,被外层 catch 成「PDF 解析失败」——
225
+ // 于是每一次 PDF 读取都失败,且错误文案完全指不向真因。
226
+ const loadingTask = pdfjs.getDocument({
223
227
  data: new Uint8Array(bytes),
224
228
  useSystemFonts: true,
225
- }).promise;
229
+ });
230
+ const document = await loadingTask.promise;
226
231
  try {
227
232
  const requested = parsePdfPages(input.pages, document.numPages);
228
233
  if (requested.length > 20)
@@ -279,7 +284,7 @@ async function call(
279
284
  : {}),
280
285
  };
281
286
  } finally {
282
- await document.destroy().catch(() => undefined);
287
+ await loadingTask.destroy().catch(() => undefined);
283
288
  }
284
289
  } catch (error) {
285
290
  const message = (error as Error).message;
@@ -484,7 +489,7 @@ async function renderPdfPages(
484
489
  Math.ceil(viewport.height),
485
490
  );
486
491
  await page.render({
487
- canvasContext: canvas.getContext('2d') as never,
492
+ canvas: canvas.getContext('2d') as unknown as Parameters<typeof page.render>[0]['canvas'],
488
493
  viewport,
489
494
  }).promise;
490
495
  const target = join(
@@ -1,17 +1,48 @@
1
- /** Minimal recoverability boundary for model-issued shell commands. */
1
+ /**
2
+ * 永久删除守卫:把「绕过回收站的删除」从 shell 里挡回去,逼模型改用可恢复的 Trash。
3
+ *
4
+ * ⚠️ 可靠性边界(与 src/irreversible/rules.ts 同一标准,别高估它):
5
+ * 这是**防模型误删**的护栏,不是对抗性沙盒。判据是对命令文本做正则匹配,
6
+ * 因此下列写法一律漏过,且**有意不追**(越追越像军备竞赛,误伤率先爆):
7
+ * · 绝对路径调用 /bin/rm -rf x
8
+ * · 间接执行 xargs rm / find -exec rm / echo $(rm x)
9
+ * · 换语言 python -c "import shutil; shutil.rmtree('x')"
10
+ * · 跨 shell 转发 cmd 里 powershell -c "Remove-Item x"
11
+ * 真正兜底的是「模型没有恶意」+ Trash 提供了更顺手的替代路径,不是这张正则表。
12
+ *
13
+ * ⚠️ `shell` 必须是**实际执行命令的那个 shell**,不是调用它的工具名。
14
+ * Bash 工具在 win32 找不到 Git Bash 时会回落 cmd.exe(见 tools/bash/bash.ts 的
15
+ * WIN_BASH 探测,且它的 description 会主动告诉模型「这里是 cmd」),那时模型写的是
16
+ * cmd 语法(del / rd),若仍按 'bash' 匹配 —— 三层防护全部落空、文件直接永久丢失。
17
+ */
18
+ /** POSIX shell:删除动词在命令位置(串首或 ; & | 换行之后)。 */
2
19
  const BASH_DELETE = [
3
20
  /(?:^|[;&|\n])\s*(?:sudo\s+)?(?:rm|unlink|rmdir)\b/i,
4
21
  /(?:^|[;&|\n])\s*find\b[^\n;&|]*\s-delete\b/i,
5
22
  ];
23
+ /**
24
+ * cmd.exe:del / erase 删文件,rd / rmdir 删目录,一律不进回收站。
25
+ * 分隔符**必须含 `&`** —— cmd 用 `&` / `&&` 串命令,这正是 POWERSHELL_DELETE
26
+ * 的分隔符类 `[;|\n]` 不能直接复用的原因。
27
+ * `rm` 一并收进来:Git 的 usr\bin 可能在 PATH 上(此时 rm.exe 可用),而 cmd 本身没有 rm。
28
+ */
29
+ const CMD_DELETE = [/(?:^|[;&|\n])\s*(?:del|erase|rd|rmdir|rm)\b/i];
6
30
  const GIT_CLEAN = /(?:^|[;&|\n])\s*git\b[^\n;&|]*\bclean\b([^\n;&|]*)/i;
7
31
  const POWERSHELL_DELETE = [
8
32
  /(?:^|[;|\n])\s*(?:Remove-Item|Clear-RecycleBin)\b/i,
9
33
  /(?:^|[;|\n])\s*(?:del|erase|rd|rmdir|rm|ri)\b/i,
10
34
  /\.Delete\s*\(/i,
11
35
  ];
36
+ const RULES = {
37
+ bash: BASH_DELETE,
38
+ cmd: CMD_DELETE,
39
+ powershell: POWERSHELL_DELETE,
40
+ };
12
41
  export function permanentDeleteReason(command, shell) {
13
- let hit = (shell === 'bash' ? BASH_DELETE : POWERSHELL_DELETE).some((pattern) => pattern.test(command));
14
- if (!hit && shell === 'bash') {
42
+ let hit = RULES[shell].some((pattern) => pattern.test(command));
43
+ // `git clean -f` 三种 shell 里都能跑、都永久删除,所以不按 shell 分支。
44
+ // `-n` / `--dry-run` 是只读预演,必须放行(否则模型连"先看看会删什么"都做不到)。
45
+ if (!hit) {
15
46
  const gitClean = GIT_CLEAN.exec(command);
16
47
  const options = gitClean?.[1] ?? '';
17
48
  const dryRun = /--dry-run\b/.test(options) ||
@@ -1,22 +1,59 @@
1
- /** Minimal recoverability boundary for model-issued shell commands. */
1
+ /**
2
+ * 永久删除守卫:把「绕过回收站的删除」从 shell 里挡回去,逼模型改用可恢复的 Trash。
3
+ *
4
+ * ⚠️ 可靠性边界(与 src/irreversible/rules.ts 同一标准,别高估它):
5
+ * 这是**防模型误删**的护栏,不是对抗性沙盒。判据是对命令文本做正则匹配,
6
+ * 因此下列写法一律漏过,且**有意不追**(越追越像军备竞赛,误伤率先爆):
7
+ * · 绝对路径调用 /bin/rm -rf x
8
+ * · 间接执行 xargs rm / find -exec rm / echo $(rm x)
9
+ * · 换语言 python -c "import shutil; shutil.rmtree('x')"
10
+ * · 跨 shell 转发 cmd 里 powershell -c "Remove-Item x"
11
+ * 真正兜底的是「模型没有恶意」+ Trash 提供了更顺手的替代路径,不是这张正则表。
12
+ *
13
+ * ⚠️ `shell` 必须是**实际执行命令的那个 shell**,不是调用它的工具名。
14
+ * Bash 工具在 win32 找不到 Git Bash 时会回落 cmd.exe(见 tools/bash/bash.ts 的
15
+ * WIN_BASH 探测,且它的 description 会主动告诉模型「这里是 cmd」),那时模型写的是
16
+ * cmd 语法(del / rd),若仍按 'bash' 匹配 —— 三层防护全部落空、文件直接永久丢失。
17
+ */
18
+
19
+ /** POSIX shell:删除动词在命令位置(串首或 ; & | 换行之后)。 */
2
20
  const BASH_DELETE = [
3
21
  /(?:^|[;&|\n])\s*(?:sudo\s+)?(?:rm|unlink|rmdir)\b/i,
4
22
  /(?:^|[;&|\n])\s*find\b[^\n;&|]*\s-delete\b/i,
5
23
  ];
24
+
25
+ /**
26
+ * cmd.exe:del / erase 删文件,rd / rmdir 删目录,一律不进回收站。
27
+ * 分隔符**必须含 `&`** —— cmd 用 `&` / `&&` 串命令,这正是 POWERSHELL_DELETE
28
+ * 的分隔符类 `[;|\n]` 不能直接复用的原因。
29
+ * `rm` 一并收进来:Git 的 usr\bin 可能在 PATH 上(此时 rm.exe 可用),而 cmd 本身没有 rm。
30
+ */
31
+ const CMD_DELETE = [/(?:^|[;&|\n])\s*(?:del|erase|rd|rmdir|rm)\b/i];
32
+
6
33
  const GIT_CLEAN = /(?:^|[;&|\n])\s*git\b[^\n;&|]*\bclean\b([^\n;&|]*)/i;
34
+
7
35
  const POWERSHELL_DELETE = [
8
36
  /(?:^|[;|\n])\s*(?:Remove-Item|Clear-RecycleBin)\b/i,
9
37
  /(?:^|[;|\n])\s*(?:del|erase|rd|rmdir|rm|ri)\b/i,
10
38
  /\.Delete\s*\(/i,
11
39
  ];
40
+
41
+ export type ShellKind = 'bash' | 'cmd' | 'powershell';
42
+
43
+ const RULES: Readonly<Record<ShellKind, RegExp[]>> = {
44
+ bash: BASH_DELETE,
45
+ cmd: CMD_DELETE,
46
+ powershell: POWERSHELL_DELETE,
47
+ };
48
+
12
49
  export function permanentDeleteReason(
13
50
  command: string,
14
- shell: 'bash' | 'powershell',
51
+ shell: ShellKind,
15
52
  ): string | undefined {
16
- let hit = (shell === 'bash' ? BASH_DELETE : POWERSHELL_DELETE).some(
17
- (pattern) => pattern.test(command),
18
- );
19
- if (!hit && shell === 'bash') {
53
+ let hit = RULES[shell].some((pattern) => pattern.test(command));
54
+ // `git clean -f` 三种 shell 里都能跑、都永久删除,所以不按 shell 分支。
55
+ // `-n` / `--dry-run` 是只读预演,必须放行(否则模型连"先看看会删什么"都做不到)。
56
+ if (!hit) {
20
57
  const gitClean = GIT_CLEAN.exec(command);
21
58
  const options = gitClean?.[1] ?? '';
22
59
  const dryRun =
@@ -75,7 +75,11 @@ export class BrowserControlCoordinator {
75
75
  throw new Error('浏览器正在等待另一个任务的人工处理');
76
76
  }
77
77
  if (this.state.status === 'running' && this.state.runId !== input.runId) {
78
- throw new Error('浏览器正在执行另一个会话的原子操作');
78
+ // 带上持锁者身份:这把锁是进程级单例,一旦泄漏,后续每次调用都撞同一句话,
79
+ // 而"另一个会话"是谁、卡在哪个动作上,现场早就没了 —— 排障只能靠猜。
80
+ throw new Error(`浏览器正在执行另一个会话的原子操作(持有者 runId=${this.state.runId ?? '?'}`
81
+ + `${this.state.action ? `, action=${this.state.action}` : ''}`
82
+ + `${this.activeIdentity?.conversationKey ? `, conversation=${this.activeIdentity.conversationKey}` : ''})`);
79
83
  }
80
84
  this.activeIdentity = {
81
85
  ownerId: this.defaultOwnerId,
@@ -106,7 +106,13 @@ export class BrowserControlCoordinator {
106
106
  throw new Error('浏览器正在等待另一个任务的人工处理');
107
107
  }
108
108
  if (this.state.status === 'running' && this.state.runId !== input.runId) {
109
- throw new Error('浏览器正在执行另一个会话的原子操作');
109
+ // 带上持锁者身份:这把锁是进程级单例,一旦泄漏,后续每次调用都撞同一句话,
110
+ // 而"另一个会话"是谁、卡在哪个动作上,现场早就没了 —— 排障只能靠猜。
111
+ throw new Error(
112
+ `浏览器正在执行另一个会话的原子操作(持有者 runId=${this.state.runId ?? '?'}`
113
+ + `${this.state.action ? `, action=${this.state.action}` : ''}`
114
+ + `${this.activeIdentity?.conversationKey ? `, conversation=${this.activeIdentity.conversationKey}` : ''})`,
115
+ );
110
116
  }
111
117
  this.activeIdentity = {
112
118
  ownerId: this.defaultOwnerId,
@@ -1083,12 +1083,16 @@ async function call(input, signal, deps = defaultDeps, ctx = {}) {
1083
1083
  /** 单条命令 = 只有一步的序列。人工等待之后的收尾快照走这个(此时不该再持锁)。 */
1084
1084
  const runAction = (args) => runSequence((run) => run(args));
1085
1085
  stream.beginAgentAction({ runId, conversationKey, action: input.action });
1086
- if (visibleAgentAction) {
1087
- stream.setAgentActing(true, input.action, input.selector ?? input.url);
1088
- ctx.reportProgress?.(describeAction(input)); // C3:每个可见动作发一句人话旁白到对话/状态行
1089
- }
1090
1086
  let pointerShown = false; // C4:本次是否点亮了 AI 指针(finally 淡出用)
1087
+ // ⚠️ 拿到锁之后**必须立刻进 try**:finally 里的 finishAgentAction 是唯一的释放路径。
1088
+ // 下面这段旁白曾经放在 try 之外 —— `ctx.reportProgress` 是宿主注入的回调,它一抛错
1089
+ // 就会跳过整个 try/finally,锁再也不会被释放;此后该进程内**任何**会话调
1090
+ // beginAgent 都会撞上「浏览器正在执行另一个会话的原子操作」,且现场已经没了。
1091
1091
  try {
1092
+ if (visibleAgentAction) {
1093
+ stream.setAgentActing(true, input.action, input.selector ?? input.url);
1094
+ ctx.reportProgress?.(describeAction(input)); // C3:每个可见动作发一句人话旁白到对话/状态行
1095
+ }
1092
1096
  await deps.ensureDaemon(BROWSER_SESSION, { signal });
1093
1097
  // actor 兜底点亮媒体面(幂等):防「直接 navigate 未先 start」「start 那次 connect 竞态失败」,
1094
1098
  // 让面板从 agent 侧被点亮,不依赖面板自启动。
@@ -1156,13 +1156,17 @@ async function call(
1156
1156
  runSequence((run) => run(args));
1157
1157
 
1158
1158
  stream.beginAgentAction({ runId, conversationKey, action: input.action });
1159
- if (visibleAgentAction) {
1160
- stream.setAgentActing(true, input.action, input.selector ?? input.url);
1161
- ctx.reportProgress?.(describeAction(input)); // C3:每个可见动作发一句人话旁白到对话/状态行
1162
- }
1163
1159
  let pointerShown = false; // C4:本次是否点亮了 AI 指针(finally 淡出用)
1164
1160
 
1161
+ // ⚠️ 拿到锁之后**必须立刻进 try**:finally 里的 finishAgentAction 是唯一的释放路径。
1162
+ // 下面这段旁白曾经放在 try 之外 —— `ctx.reportProgress` 是宿主注入的回调,它一抛错
1163
+ // 就会跳过整个 try/finally,锁再也不会被释放;此后该进程内**任何**会话调
1164
+ // beginAgent 都会撞上「浏览器正在执行另一个会话的原子操作」,且现场已经没了。
1165
1165
  try {
1166
+ if (visibleAgentAction) {
1167
+ stream.setAgentActing(true, input.action, input.selector ?? input.url);
1168
+ ctx.reportProgress?.(describeAction(input)); // C3:每个可见动作发一句人话旁白到对话/状态行
1169
+ }
1166
1170
  await deps.ensureDaemon(BROWSER_SESSION, { signal });
1167
1171
  // actor 兜底点亮媒体面(幂等):防「直接 navigate 未先 start」「start 那次 connect 竞态失败」,
1168
1172
  // 让面板从 agent 侧被点亮,不依赖面板自启动。
package/src/types.ts CHANGED
@@ -885,11 +885,17 @@ export type LoopEvent =
885
885
  *
886
886
  * code(v2.x 新增,可选):让 `-p --output-format json` 把终结原因映射成稳定的
887
887
  * stop_reason,也让 print.ts 区分"致命错误(退出)"与"非致命警告(继续)":
888
- * - 'llm_error' LLM 调用失败 → 终结,stop_reason='error'
889
- * - 'max_turns' 撞最大轮数上限 → 终结,stop_reason='max_turns'
890
- * - 'compact_failed' 自动压缩失败但 loop 想继续 → 非致命,print.ts 只 stderr 警告不退出
891
- * - 'aborted' 被中断(signal.aborted)
888
+ * - 'llm_error' LLM 调用失败 → 终结,stop_reason='error'
889
+ * - 'max_turns' 撞最大轮数上限 → 终结,stop_reason='max_turns'
890
+ * - 'compact_failed' 自动压缩失败但 loop 想继续 → 非致命,print.ts 只 stderr 警告不退出
891
+ * - 'aborted' 被中断(signal.aborted)
892
+ * - 'stalled' StallGuard 判定原地打转(同一失败反复 / 整批调用无进展)
893
+ * → 终结,stop_reason='stalled'
894
+ * - 'context_overflow' 即使降级后仍超模型输入上限,主动不发这次请求
895
+ * → 终结,stop_reason='error'
892
896
  * 缺省(旧调用方 / 未分类错误)按致命、stop_reason='error' 处理。
897
+ * ⚠️ 增删 code 时同步改 print.ts 的映射:那里只特判 max_turns / stalled,
898
+ * 其余一律落 'error',新增的终结原因不特判就会被并进通用 error。
893
899
  */
894
900
  | {
895
901
  type: 'error';
@@ -21,11 +21,11 @@ steps:
21
21
  title: "1/3·自主调研写笔记"
22
22
  agent: |
23
23
  调研主题「${inputs.topic}」:用你手上的工具搜集 3-5 个关键要点,
24
- 整理成简洁的中文 markdown,写到 ./out/notes.md(目录不存在就先创建)。
24
+ 整理成简洁的中文 markdown,写到 out/adaptive-research/${session}/notes.md(目录不存在就先创建)。
25
25
  写完后用 Read 自检文件确实写好了再收尾。
26
26
  allowed_tools: [Read, Write, Edit, Bash, Grep, Glob]
27
27
  # 确定性完成条件:模型「宣告要写却没写」时引擎自动提醒续跑(announce-not-do 兜底)
28
- done_when: "fileExists('./out/notes.md')"
28
+ done_when: "fileExists('out/adaptive-research/${session}/notes.md')"
29
29
  capture:
30
30
  # agent 步 raw 只有 text/result(无 content)——曾绑 content 导致 notes 恒 undefined
31
31
  text: notes
@@ -35,7 +35,7 @@ steps:
35
35
  title: "2/3·统计笔记行数"
36
36
  tool: Bash
37
37
  args:
38
- command: "wc -l ./out/notes.md"
38
+ command: "wc -l out/adaptive-research/${session}/notes.md"
39
39
  onError: recover
40
40
  recover_hint: "文件不存在或路径不对,就先用 Glob/Read 定位 notes.md 再统计行数"
41
41
  capture:
@@ -46,5 +46,5 @@ steps:
46
46
  title: "3/3·守门校验"
47
47
  type: verify
48
48
  checks:
49
- - 'file_exists:./out/notes.md'
50
- onFail: "调研笔记 ./out/notes.md 未生成,检查 research 步"
49
+ - 'file_exists:out/adaptive-research/${session}/notes.md'
50
+ onFail: "调研笔记 out/adaptive-research/${session}/notes.md 未生成,检查 research 步"
@@ -56,7 +56,7 @@ steps:
56
56
  title: "3/7·脚本落盘"
57
57
  tool: Write
58
58
  args:
59
- file_path: "videos/${inputs.book}/script.txt"
59
+ file_path: "out/book-review-short/${session}/script.txt"
60
60
  content: "${script_text}"
61
61
 
62
62
  # 4. 生成 3 个钩子标题(loop + llm)—— P1 启用
@@ -97,7 +97,7 @@ steps:
97
97
  - id: gen_thumbnail
98
98
  title: "5/7·生成缩略图(可降级)"
99
99
  skill: image-gen-openrouter
100
- input: 'prompt=${thumb_prompt} output=videos/${inputs.book}/thumbnail.png aspect=9:16'
100
+ input: 'prompt=${thumb_prompt} output=out/book-review-short/${session}/thumbnail.png aspect=9:16'
101
101
  onError: continue
102
102
 
103
103
  # 6. 元数据汇总(确定性工具)
@@ -106,7 +106,7 @@ steps:
106
106
  title: "6/7·汇总 meta.json"
107
107
  tool: Write
108
108
  args:
109
- file_path: "videos/${inputs.book}/meta.json"
109
+ file_path: "out/book-review-short/${session}/meta.json"
110
110
  content: |
111
111
  {
112
112
  "book": ${json(inputs.book)},
@@ -120,6 +120,6 @@ steps:
120
120
  title: "7/7·守门校验"
121
121
  type: verify
122
122
  checks:
123
- - 'file_exists:videos/${inputs.book}/script.txt'
124
- - 'file_exists:videos/${inputs.book}/meta.json'
123
+ - 'file_exists:out/book-review-short/${session}/script.txt'
124
+ - 'file_exists:out/book-review-short/${session}/meta.json'
125
125
  onFail: "脚本或 meta 未写盘,请检查 Write 工具"
@@ -12,7 +12,7 @@ steps:
12
12
  title: "1/3·写入便签"
13
13
  tool: Write
14
14
  args:
15
- file_path: "out/${inputs.topic}/note.txt"
15
+ file_path: "out/e2e-write-greet/${session}/note.txt"
16
16
  content: "topic=${inputs.topic} ok"
17
17
  capture:
18
18
  args.content: note_body
@@ -21,7 +21,7 @@ steps:
21
21
  title: "2/3·校验落盘"
22
22
  type: verify
23
23
  checks:
24
- - 'file_exists:out/${inputs.topic}/note.txt'
24
+ - 'file_exists:out/e2e-write-greet/${session}/note.txt'
25
25
  onFail: "note.txt 未落盘"
26
26
 
27
27
  - id: verify_capture
@@ -8,7 +8,7 @@ version: "0.1"
8
8
 
9
9
  # 最终交付物(new-task 清旧 / completed 报产物 / 完成闸门校验落盘)。中间候选图与分文件不声明(scratch)。
10
10
  artifacts:
11
- - "./build/illustrations.json"
11
+ - "out/illustrate/${session}/build/illustrations.json"
12
12
 
13
13
  # 自愈:浏览器/网络瞬态故障 in-run 重跑。
14
14
  self_heal:
@@ -154,7 +154,7 @@ steps:
154
154
  tool: Download
155
155
  args:
156
156
  url: "${u}"
157
- path: "./build/illustrations/cand_${u_idx}.img"
157
+ path: "out/illustrate/${session}/build/illustrations/cand_${u_idx}.img"
158
158
  timeout: 30000
159
159
  headers:
160
160
  User-Agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"
@@ -170,7 +170,7 @@ steps:
170
170
  do:
171
171
  - id: score_one
172
172
  title: "配图·打分 #${u_idx}"
173
- when: "fileExists('./build/illustrations/cand_' + u_idx + '.img')"
173
+ when: "fileExists('out/illustrate/${session}/build/illustrations/cand_' + u_idx + '.img')"
174
174
  llm: |
175
175
  下方附上一张候选配图(用于主题:「${inputs.topic}」)。请**只看这张图**给出客观评估,输出 JSON。
176
176
  - relevance:0-10,图片内容与主题的契合度(画的东西是否切题)。
@@ -178,7 +178,7 @@ steps:
178
178
  - is_ad:true/false——这是不是**广告/促销/带营销文案或店铺水印/比价图**等不适合当配图的图。
179
179
  - reason:一句话理由(中文)。
180
180
  只输出 JSON 对象本身,不要 markdown 代码块、不要解释。
181
- images: ["./build/illustrations/cand_${u_idx}.img"]
181
+ images: ["out/illustrate/${session}/build/illustrations/cand_${u_idx}.img"]
182
182
  image_detail: high
183
183
  capture:
184
184
  text: score_text
@@ -186,10 +186,10 @@ steps:
186
186
  max_retries: 1
187
187
  - id: write_score
188
188
  title: "配图·记录分数 #${u_idx}"
189
- when: "fileExists('./build/illustrations/cand_' + u_idx + '.img')"
189
+ when: "fileExists('out/illustrate/${session}/build/illustrations/cand_' + u_idx + '.img')"
190
190
  tool: Write
191
191
  args:
192
- file_path: "./build/illustrations/score_${u_idx}.json"
192
+ file_path: "out/illustrate/${session}/build/illustrations/score_${u_idx}.json"
193
193
  content: |
194
194
  {"index": ${u_idx}, "url": ${json(u)}, "engine": ${json(engine_used)}, "path": "build/illustrations/cand_${u_idx}.img", "score": ${score_text}}
195
195
  onError: continue
@@ -198,24 +198,24 @@ steps:
198
198
  - id: select
199
199
  title: "配图·选出最合适的 ${inputs.count} 张"
200
200
  agent: |
201
- 你是配图验收员。**唯一任务**:从已打分的候选里选出最合适的 ${inputs.count} 张配图,写 `./build/illustrations.json`。不要做别的。
201
+ 你是配图验收员。**唯一任务**:从已打分的候选里选出最合适的 ${inputs.count} 张配图,写 `out/illustrate/${session}/build/illustrations.json`。不要做别的。
202
202
 
203
- 1. 用 Glob 列出 `./build/illustrations/score_*.json`,逐个 Read。每个文件形如
203
+ 1. 用 Glob 列出 `out/illustrate/${session}/build/illustrations/score_*.json`,逐个 Read。每个文件形如
204
204
  `{"index","url","engine","path","score":{"relevance","quality","is_ad","reason"}}`(坏文件/缺 score 的跳过)。
205
205
  2. **先剔除 `score.is_ad == true` 的**(广告/促销/水印图绝不入选)。
206
206
  3. 其余按 **relevance×quality 降序**排序,取前 ${inputs.count} 张(不足就有几张给几张)。
207
- 4. 用 Write 写 `./build/illustrations.json`(严格 JSON、无注释、无 ``` 包裹):
207
+ 4. 用 Write 写 `out/illustrate/${session}/build/illustrations.json`(严格 JSON、无注释、无 ``` 包裹):
208
208
  `{"topic": "...", "engine_used": "${engine_used}", "count": <实际入选数>,
209
209
  "picks": [{"path","url","engine","relevance","quality","reason"}, ...]}`
210
210
  path 用 score 文件里的 path(如 build/illustrations/cand_3.img)。
211
211
  完成后简述:扫了几个候选、剔了几张广告、最终选了哪几张(序号 + 一句理由)。
212
212
  allowed_tools: [Glob, Read, Write]
213
213
  maxTurns: 12
214
- done_when: "fileExists('./build/illustrations.json')"
214
+ done_when: "fileExists('out/illustrate/${session}/build/illustrations.json')"
215
215
  onError: halt
216
216
  max_retries: 1
217
217
  artifacts:
218
- - "./build/illustrations.json"
218
+ - "out/illustrate/${session}/build/illustrations.json"
219
219
 
220
220
  # ---------- 阶段7:完成报告(确定性输出,不烧 LLM)----------
221
221
  - id: report
@@ -223,5 +223,5 @@ steps:
223
223
  type: echo
224
224
  message: |
225
225
  ✅ 配图完成(主题:${inputs.topic},来源:${engine_used})。
226
- 选中的图片清单见 ./build/illustrations.json(每条含 path/url/engine/分数)。
226
+ 选中的图片清单见 out/illustrate/${session}/build/illustrations.json(每条含 path/url/engine/分数)。
227
227
  下游 PPT/文章工作流可读 illustrations.json 取用这 ${inputs.count} 张配图。