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.
- package/package.json +3 -2
- package/plugins/workflow-runner/src/expressions.js +29 -8
- package/plugins/workflow-runner/src/expressions.ts +28 -10
- package/plugins/workflow-runner/src/index.js +45 -17
- package/plugins/workflow-runner/src/index.ts +44 -18
- package/plugins/workflow-runner/src/recovery.js +9 -2
- package/plugins/workflow-runner/src/recovery.ts +9 -2
- package/plugins/workflow-runner/src/runner.js +31 -5
- package/plugins/workflow-runner/src/runner.ts +33 -5
- package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -1
- package/plugins/workflow-runner/src/stepExecutors/tool.js +29 -10
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +29 -11
- package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
- package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
- package/plugins/workflow-runner/src/taskIdentity.js +197 -21
- package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
- package/plugins/workflow-runner/src/types.js +113 -7
- package/plugins/workflow-runner/src/types.ts +111 -7
- package/plugins/workflow-runner/src/workflowState.js +455 -54
- package/plugins/workflow-runner/src/workflowState.ts +508 -63
- package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
- package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
- package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
- package/plugins/workflow-runner/test/expressions.test.ts +42 -0
- package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
- package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
- package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
- package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
- package/plugins/workflow-runner/test/research.load.test.ts +328 -0
- package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
- package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
- package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
- package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
- package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
- package/plugins/workflow-runner/test/word.load.test.ts +9 -9
- package/skills/skill-creator/scripts/run_eval.py +3 -2
- package/src/cli/print.js +17 -0
- package/src/cli/print.ts +25 -0
- package/src/config/configService.js +24 -0
- package/src/config/configService.ts +23 -0
- package/src/infra/verification.js +25 -18
- package/src/infra/verification.ts +50 -18
- package/src/plugins/pluginRunner.js +18 -7
- package/src/plugins/pluginRunner.ts +20 -6
- package/src/prompts/policyRegistry.js +28 -1
- package/src/prompts/policyRegistry.ts +28 -1
- package/src/tools/advisor/advisor.js +76 -9
- package/src/tools/advisor/advisor.ts +79 -9
- package/src/tools/bash/bash.js +51 -6
- package/src/tools/bash/bash.ts +56 -7
- package/src/tools/powershell/powershell.js +18 -1
- package/src/tools/powershell/powershell.ts +18 -2
- package/src/tools/read/read.js +9 -4
- package/src/tools/read/read.ts +9 -4
- package/src/tools/shared/permanentDeleteGuard.js +34 -3
- package/src/tools/shared/permanentDeleteGuard.ts +43 -6
- package/src/tools/webbrowser/browserControl.js +5 -1
- package/src/tools/webbrowser/browserControl.ts +7 -1
- package/src/tools/webbrowser/webbrowser.js +8 -4
- package/src/tools/webbrowser/webbrowser.ts +8 -4
- package/src/types.ts +10 -4
- package/workflows/adaptive-research.yaml +5 -5
- package/workflows/book-review-short.yaml +5 -5
- package/workflows/e2e-write-greet.yaml +2 -2
- package/workflows/illustrate.yaml +12 -12
- package/workflows/plan.yaml +18 -18
- package/workflows/ppt.yaml +201 -201
- package/workflows/ralph-loop.yaml +3 -1
- package/workflows/research.md +449 -0
- package/workflows/research.yaml +873 -0
- package/workflows/webppt.yaml +47 -47
- package/workflows/word.yaml +80 -80
- 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:
|
|
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
|
-
|
|
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,
|
package/src/tools/read/read.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
})
|
|
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
|
|
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
|
-
|
|
367
|
+
canvas: canvas.getContext('2d'),
|
|
363
368
|
viewport,
|
|
364
369
|
}).promise;
|
|
365
370
|
const target = join(outputDir, `${outputPrefix}-page-${pageNumber}.png`);
|
package/src/tools/read/read.ts
CHANGED
|
@@ -219,10 +219,15 @@ async function call(
|
|
|
219
219
|
};
|
|
220
220
|
}
|
|
221
221
|
const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
|
|
222
|
-
|
|
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
|
-
})
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 =
|
|
14
|
-
|
|
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
|
-
/**
|
|
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:
|
|
51
|
+
shell: ShellKind,
|
|
15
52
|
): string | undefined {
|
|
16
|
-
let hit = (
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (!hit
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
889
|
-
* - 'max_turns'
|
|
890
|
-
* - 'compact_failed'
|
|
891
|
-
* - '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,写到
|
|
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('
|
|
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
|
|
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
|
|
50
|
-
onFail: "调研笔记
|
|
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: "
|
|
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=
|
|
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: "
|
|
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:
|
|
124
|
-
- 'file_exists:
|
|
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/${
|
|
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/${
|
|
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
|
-
- "
|
|
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: "
|
|
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('
|
|
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: ["
|
|
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('
|
|
189
|
+
when: "fileExists('out/illustrate/${session}/build/illustrations/cand_' + u_idx + '.img')"
|
|
190
190
|
tool: Write
|
|
191
191
|
args:
|
|
192
|
-
file_path: "
|
|
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} 张配图,写
|
|
201
|
+
你是配图验收员。**唯一任务**:从已打分的候选里选出最合适的 ${inputs.count} 张配图,写 `out/illustrate/${session}/build/illustrations.json`。不要做别的。
|
|
202
202
|
|
|
203
|
-
1. 用 Glob 列出
|
|
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 写
|
|
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('
|
|
214
|
+
done_when: "fileExists('out/illustrate/${session}/build/illustrations.json')"
|
|
215
215
|
onError: halt
|
|
216
216
|
max_retries: 1
|
|
217
217
|
artifacts:
|
|
218
|
-
- "
|
|
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
|
-
选中的图片清单见
|
|
226
|
+
选中的图片清单见 out/illustrate/${session}/build/illustrations.json(每条含 path/url/engine/分数)。
|
|
227
227
|
下游 PPT/文章工作流可读 illustrations.json 取用这 ${inputs.count} 张配图。
|