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
|
@@ -21,11 +21,13 @@
|
|
|
21
21
|
* - `{ args.<任意字段>: var_name }` 绑入参的具名字段
|
|
22
22
|
* ============================================================
|
|
23
23
|
*/
|
|
24
|
+
import { randomUUID } from 'node:crypto';
|
|
24
25
|
import { existsSync } from 'node:fs';
|
|
25
26
|
import { resolve } from 'node:path';
|
|
26
27
|
import { executeTool, getToolByName, recordRead, } from '../../../../src/plugin-sdk.js';
|
|
27
28
|
import { interpolateDeep } from '../expressions.js';
|
|
28
29
|
import { buildWorkflowToolContext, effectiveWorkflowCwd, } from '../toolRuntime.js';
|
|
30
|
+
import { safeArtifactPath } from '../types.js';
|
|
29
31
|
export async function* execToolStep(step, vars, ctx) {
|
|
30
32
|
if (!step.tool)
|
|
31
33
|
throw new Error(`step ${step.id}: 缺少 tool 字段`);
|
|
@@ -136,7 +138,7 @@ export async function* execToolStep(step, vars, ctx) {
|
|
|
136
138
|
role: 'assistant',
|
|
137
139
|
content: `[workflow:${step.id}] ✅ ${tool.name} 执行完成:${summary}`,
|
|
138
140
|
timestamp: Date.now(),
|
|
139
|
-
id:
|
|
141
|
+
id: randomUUID(),
|
|
140
142
|
});
|
|
141
143
|
// ⚠️ 工具返回的图片**不能丢**。此前这里只取 content,`visionImages` 被静默丢弃 ——
|
|
142
144
|
// 于是任何「截图/看图」类工作流都拿不到产物,而且不会报错,只是后面的评估步没图可看。
|
|
@@ -160,29 +162,46 @@ export async function* execToolStep(step, vars, ctx) {
|
|
|
160
162
|
};
|
|
161
163
|
}
|
|
162
164
|
/**
|
|
163
|
-
* 写类 tool
|
|
165
|
+
* 写类 tool 步的目标文件若命中【安全的声明产物】且已存在 → pre-Read 解除内核守卫。
|
|
164
166
|
*
|
|
165
167
|
* 路径归一化:artifacts 声明相对 cwd;args.file_path 可能是 ./相对 或 绝对,
|
|
166
168
|
* 双方都 resolve(cwd, …) 后比较(win32 文件系统大小写不敏感 → lowercase 比较;
|
|
167
169
|
* 其余平台保持大小写敏感,避免 Plan.md / plan.md 误判同文件)。
|
|
170
|
+
*
|
|
171
|
+
* 安全守卫(修复路径遍历):
|
|
172
|
+
* · 声明的 artifacts 路径先经 safeArtifactPath 校验【必须落在 cwd 内】,
|
|
173
|
+
* 越界声明直接从命中列表移除——不解守卫,让内核写保护照常生效(fail-closed)。
|
|
174
|
+
* · args.file_path 目标自身也做 cwd 内校验:目标越界(工作流步骤想写 cwd 外)
|
|
175
|
+
* → 同样不解守卫,让写工具在执行时自己报错(通常是权限/文件不存在提示,
|
|
176
|
+
* 而不是静默放行盲写,双重保险)。
|
|
168
177
|
*/
|
|
169
178
|
async function preReadDeclaredArtifact(tool, args, step, ctx) {
|
|
170
179
|
if (tool.isReadOnly !== false)
|
|
171
180
|
return; // 只对写类工具生效
|
|
172
181
|
if (typeof args.file_path !== 'string')
|
|
173
182
|
return;
|
|
183
|
+
const cwd = effectiveWorkflowCwd(ctx);
|
|
174
184
|
const norm = (p) => {
|
|
175
|
-
const abs = resolve(
|
|
185
|
+
const abs = resolve(cwd, p);
|
|
176
186
|
return process.platform === 'win32' ? abs.toLowerCase() : abs;
|
|
177
187
|
};
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
// 1. 目标文件先过 cwd 安全关(越界直接不解守卫)
|
|
189
|
+
const targetRaw = String(args.file_path);
|
|
190
|
+
const targetSafe = safeArtifactPath(cwd, targetRaw);
|
|
191
|
+
if (targetSafe === null)
|
|
192
|
+
return;
|
|
193
|
+
const target = norm(targetSafe);
|
|
194
|
+
// 2. 声明产物过 cwd 安全关(过滤掉越界的声明)
|
|
195
|
+
const rawDeclared = [...(ctx.declaredArtifacts ?? []), ...(step.artifacts ?? [])];
|
|
196
|
+
const declaredAbs = [];
|
|
197
|
+
for (const raw of rawDeclared) {
|
|
198
|
+
const safe = safeArtifactPath(cwd, raw);
|
|
199
|
+
if (safe !== null)
|
|
200
|
+
declaredAbs.push(norm(safe));
|
|
201
|
+
}
|
|
202
|
+
if (!declaredAbs.some((a) => a === target))
|
|
184
203
|
return;
|
|
185
|
-
const abs = resolve(
|
|
204
|
+
const abs = resolve(cwd, targetSafe);
|
|
186
205
|
if (!existsSync(abs))
|
|
187
206
|
return; // 新建文件不需要解守卫
|
|
188
207
|
// 直接登记一次「完整已读」状态解除内核 pre-read 守卫——不真正 Read 内容:
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* ============================================================
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import { randomUUID } from 'node:crypto';
|
|
25
26
|
import { existsSync } from 'node:fs';
|
|
26
27
|
import { resolve } from 'node:path';
|
|
27
28
|
|
|
@@ -37,7 +38,7 @@ import {
|
|
|
37
38
|
buildWorkflowToolContext,
|
|
38
39
|
effectiveWorkflowCwd,
|
|
39
40
|
} from '../toolRuntime.js';
|
|
40
|
-
import type
|
|
41
|
+
import { safeArtifactPath, type RunContext, type StepDef, type StepResult, type VarStack } from '../types.js';
|
|
41
42
|
|
|
42
43
|
export async function* execToolStep(
|
|
43
44
|
step: StepDef,
|
|
@@ -172,7 +173,7 @@ export async function* execToolStep(
|
|
|
172
173
|
role: 'assistant',
|
|
173
174
|
content: `[workflow:${step.id}] ✅ ${tool.name} 执行完成:${summary}`,
|
|
174
175
|
timestamp: Date.now(),
|
|
175
|
-
id:
|
|
176
|
+
id: randomUUID(),
|
|
176
177
|
});
|
|
177
178
|
|
|
178
179
|
// ⚠️ 工具返回的图片**不能丢**。此前这里只取 content,`visionImages` 被静默丢弃 ——
|
|
@@ -199,11 +200,18 @@ export async function* execToolStep(
|
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
/**
|
|
202
|
-
* 写类 tool
|
|
203
|
+
* 写类 tool 步的目标文件若命中【安全的声明产物】且已存在 → pre-Read 解除内核守卫。
|
|
203
204
|
*
|
|
204
205
|
* 路径归一化:artifacts 声明相对 cwd;args.file_path 可能是 ./相对 或 绝对,
|
|
205
206
|
* 双方都 resolve(cwd, …) 后比较(win32 文件系统大小写不敏感 → lowercase 比较;
|
|
206
207
|
* 其余平台保持大小写敏感,避免 Plan.md / plan.md 误判同文件)。
|
|
208
|
+
*
|
|
209
|
+
* 安全守卫(修复路径遍历):
|
|
210
|
+
* · 声明的 artifacts 路径先经 safeArtifactPath 校验【必须落在 cwd 内】,
|
|
211
|
+
* 越界声明直接从命中列表移除——不解守卫,让内核写保护照常生效(fail-closed)。
|
|
212
|
+
* · args.file_path 目标自身也做 cwd 内校验:目标越界(工作流步骤想写 cwd 外)
|
|
213
|
+
* → 同样不解守卫,让写工具在执行时自己报错(通常是权限/文件不存在提示,
|
|
214
|
+
* 而不是静默放行盲写,双重保险)。
|
|
207
215
|
*/
|
|
208
216
|
async function preReadDeclaredArtifact(
|
|
209
217
|
tool: { name: string; isReadOnly?: boolean },
|
|
@@ -214,18 +222,28 @@ async function preReadDeclaredArtifact(
|
|
|
214
222
|
if (tool.isReadOnly !== false) return; // 只对写类工具生效
|
|
215
223
|
if (typeof args.file_path !== 'string') return;
|
|
216
224
|
|
|
225
|
+
const cwd = effectiveWorkflowCwd(ctx);
|
|
217
226
|
const norm = (p: string): string => {
|
|
218
|
-
const abs = resolve(
|
|
227
|
+
const abs = resolve(cwd, p);
|
|
219
228
|
return process.platform === 'win32' ? abs.toLowerCase() : abs;
|
|
220
229
|
};
|
|
221
|
-
const target = norm(args.file_path);
|
|
222
|
-
const declared = [
|
|
223
|
-
...(ctx.declaredArtifacts ?? []),
|
|
224
|
-
...(step.artifacts ?? []),
|
|
225
|
-
];
|
|
226
|
-
if (!declared.some((a) => norm(a) === target)) return;
|
|
227
230
|
|
|
228
|
-
|
|
231
|
+
// 1. 目标文件先过 cwd 安全关(越界直接不解守卫)
|
|
232
|
+
const targetRaw = String(args.file_path);
|
|
233
|
+
const targetSafe = safeArtifactPath(cwd, targetRaw);
|
|
234
|
+
if (targetSafe === null) return;
|
|
235
|
+
const target = norm(targetSafe);
|
|
236
|
+
|
|
237
|
+
// 2. 声明产物过 cwd 安全关(过滤掉越界的声明)
|
|
238
|
+
const rawDeclared = [...(ctx.declaredArtifacts ?? []), ...(step.artifacts ?? [])];
|
|
239
|
+
const declaredAbs: string[] = [];
|
|
240
|
+
for (const raw of rawDeclared) {
|
|
241
|
+
const safe = safeArtifactPath(cwd, raw);
|
|
242
|
+
if (safe !== null) declaredAbs.push(norm(safe));
|
|
243
|
+
}
|
|
244
|
+
if (!declaredAbs.some((a) => a === target)) return;
|
|
245
|
+
|
|
246
|
+
const abs = resolve(cwd, targetSafe);
|
|
229
247
|
if (!existsSync(abs)) return; // 新建文件不需要解守卫
|
|
230
248
|
|
|
231
249
|
// 直接登记一次「完整已读」状态解除内核 pre-read 守卫——不真正 Read 内容:
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { parseCheckString, runVerification } from '../../../../src/plugin-sdk.js';
|
|
14
14
|
import { interpolate } from '../expressions.js';
|
|
15
|
-
|
|
15
|
+
import { effectiveWorkflowCwd } from '../toolRuntime.js';
|
|
16
|
+
export async function execVerifyStep(step, vars, ctx) {
|
|
16
17
|
// 兼容两种格式:YAML 数组 或逗号分隔字符串。
|
|
17
18
|
// 关键:【先插值再按逗号分割】。checks 可能是 "${verify_checks}" 这类模板,插值后才
|
|
18
19
|
// 展开成逗号分隔的多项(如 "shell:bun test,file_exists:x")。若先分割再插值,整段模板
|
|
@@ -43,8 +44,10 @@ export async function execVerifyStep(step, vars) {
|
|
|
43
44
|
}
|
|
44
45
|
checks.push(check);
|
|
45
46
|
}
|
|
46
|
-
//
|
|
47
|
-
|
|
47
|
+
// 执行所有检查。
|
|
48
|
+
// cwd 必须和写入侧同源:会话进了 session worktree 时产物落在 worktree 里,
|
|
49
|
+
// 验收若锚原 workspace 会在空目录找文件,恒判失败并触发无意义的重试。
|
|
50
|
+
const result = await runVerification(checks, effectiveWorkflowCwd(ctx));
|
|
48
51
|
if (!result.passed) {
|
|
49
52
|
const msg = step.onFail
|
|
50
53
|
? interpolate(step.onFail, vars)
|
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
import { parseCheckString, runVerification, type Check } from '../../../../src/plugin-sdk.js';
|
|
15
15
|
import { interpolate } from '../expressions.js';
|
|
16
|
-
import
|
|
16
|
+
import { effectiveWorkflowCwd } from '../toolRuntime.js';
|
|
17
|
+
import type { RunContext, StepDef, StepResult, VarStack } from '../types.js';
|
|
17
18
|
|
|
18
19
|
export async function execVerifyStep(
|
|
19
20
|
step: StepDef,
|
|
20
21
|
vars: VarStack,
|
|
22
|
+
ctx: RunContext,
|
|
21
23
|
): Promise<StepResult> {
|
|
22
24
|
// 兼容两种格式:YAML 数组 或逗号分隔字符串。
|
|
23
25
|
// 关键:【先插值再按逗号分割】。checks 可能是 "${verify_checks}" 这类模板,插值后才
|
|
@@ -51,8 +53,10 @@ export async function execVerifyStep(
|
|
|
51
53
|
checks.push(check);
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
+
// 执行所有检查。
|
|
57
|
+
// cwd 必须和写入侧同源:会话进了 session worktree 时产物落在 worktree 里,
|
|
58
|
+
// 验收若锚原 workspace 会在空目录找文件,恒判失败并触发无意义的重试。
|
|
59
|
+
const result = await runVerification(checks, effectiveWorkflowCwd(ctx));
|
|
56
60
|
|
|
57
61
|
if (!result.passed) {
|
|
58
62
|
const msg = step.onFail
|
|
@@ -1,29 +1,88 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
4
|
import { chat as sdkChat, executeTool as sdkExecuteTool, } from '../../../src/plugin-sdk.js';
|
|
5
5
|
import { truncate } from './agentCommon.js';
|
|
6
6
|
import { extractJsonPayload } from './jsonPayload.js';
|
|
7
7
|
// ============================================================
|
|
8
|
-
// 工作流状态目录布局(
|
|
8
|
+
// 工作流状态目录布局(Phase 1 + 2 + 3 升级)
|
|
9
9
|
// ------------------------------------------------------------
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
10
|
+
// 状态根目录优先级:
|
|
11
|
+
// 1. 环境变量 MINIMAL_AGENT_WORKFLOW_STATE_DIR(全局统一目录)
|
|
12
|
+
// 2. <cwd>/.minimal-agent-workflow/(默认,与历史行为一致)
|
|
13
|
+
//
|
|
14
|
+
// 目录结构(三级:工作流名 / session 实例 / taskId 记录):
|
|
15
|
+
// <stateRoot>/<workflowName>/<sessionId>/ ← 活动任务实例(session 即任务实例)
|
|
16
|
+
// <stateRoot>/<workflowName>/<sessionId>.done/ ← 成功完成的实例归档
|
|
17
|
+
// <stateRoot>/<workflowName>/<sessionId>.trash/ ← 清理回收(A4 归档)
|
|
18
|
+
//
|
|
19
|
+
// Phase 3(session 化):实例目录名 = sessionId(一个 session 同时只跑一个
|
|
20
|
+
// 同名工作流,session 即天然的任务实例划分单元)。taskId(goal 内容 hash)
|
|
21
|
+
// 降级为 current.json 里的【记录字段】——用于同 session 内识别「换主题 →
|
|
22
|
+
// new-task 归档重建 / 同主题 → resume/completed」。不同 session 完全隔离,
|
|
23
|
+
// 同主题也可并行互不干扰。
|
|
24
|
+
//
|
|
25
|
+
// 演进历史:
|
|
26
|
+
// - 最初:<cwd>/.minimal-agent-workflow/ 单例(所有工作流共用)
|
|
27
|
+
// - E6:按 workflow 名隔离,<cwd>/.minimal-agent-workflow/<name>/
|
|
28
|
+
// - Phase 1:状态根目录可配置(环境变量指定全局位置,解决 cwd 切换找不到)
|
|
29
|
+
// - Phase 2:新增 taskId 实例层,同名 workflow 多任务并存(根治新任务清掉旧状态)
|
|
30
|
+
// - Phase 3:实例目录名改为 sessionId,taskId 降级为记录字段(多 session 真正并行)
|
|
17
31
|
// ============================================================
|
|
18
|
-
/**
|
|
32
|
+
/** 工作流状态目录的父目录名(默认落在 cwd 下时使用)。 */
|
|
19
33
|
export const WF_STATE_PARENT = '.minimal-agent-workflow';
|
|
20
|
-
/**
|
|
34
|
+
/** 全局统一状态目录的环境变量名。 */
|
|
35
|
+
export const WF_STATE_DIR_ENV = 'MINIMAL_AGENT_WORKFLOW_STATE_DIR';
|
|
36
|
+
/** 缺省 sessionId(宿主未显式传 session 时的兜底判定值;真正落盘的 session 由 generateDefaultSessionId 自动命名)。 */
|
|
37
|
+
export const DEFAULT_SESSION_ID = 'default';
|
|
38
|
+
/**
|
|
39
|
+
* 工作流引擎的默认 session 自动命名:宿主未显式提供有意义 session(缺失或为
|
|
40
|
+
* DEFAULT_SESSION_ID)时,由引擎自行生成唯一可读的 session 名
|
|
41
|
+
* `YYYYMMDD-HHmmss-xxxxxx`(日期时间 + 6 位随机 hex)。
|
|
42
|
+
*
|
|
43
|
+
* 语义:每次工作流运行(CLI -p 默认 / TUI default 会话)都自动落到独立的
|
|
44
|
+
* `<out>/<workflow>/<session>/` 产物目录与 `<stateRoot>/<workflow>/<session>/`
|
|
45
|
+
* 状态目录——多 session 并行互不覆盖;时间前缀便于人识别与排序,随机后缀
|
|
46
|
+
* 防同一秒并行冲突。宿主显式传 `-s <id>`(非 default)时仍优先使用宿主 id。
|
|
47
|
+
*/
|
|
48
|
+
export function generateDefaultSessionId(now = new Date()) {
|
|
49
|
+
const pad = (n, w = 2) => String(n).padStart(w, '0');
|
|
50
|
+
const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
|
|
51
|
+
const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
52
|
+
const rand = randomBytes(3).toString('hex');
|
|
53
|
+
return `${date}-${time}-${rand}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 解析状态根目录(绝对路径)。
|
|
57
|
+
* 优先级:环境变量 MINIMAL_AGENT_WORKFLOW_STATE_DIR > cwd 下默认目录。
|
|
58
|
+
* 环境变量允许相对路径——基于 process.cwd() 展开(用户可能写 `.wflow` 这类相对值)。
|
|
59
|
+
* 传入 workingDir 是为了 cwd 默认模式的相对路径基准。
|
|
60
|
+
*/
|
|
61
|
+
export function resolveStateRoot(workingDir) {
|
|
62
|
+
const envDir = process.env[WF_STATE_DIR_ENV];
|
|
63
|
+
if (envDir && envDir.trim()) {
|
|
64
|
+
const p = envDir.trim();
|
|
65
|
+
return isAbsolute(p) ? p : resolve(p);
|
|
66
|
+
}
|
|
67
|
+
return join(workingDir, WF_STATE_PARENT);
|
|
68
|
+
}
|
|
69
|
+
/** 把 workflow 名 / sessionId 清洗成安全的目录名(保留字母数字._-,其余替换为 -)。 */
|
|
21
70
|
export function sanitizeWorkflowDirName(name) {
|
|
22
71
|
return name.replace(/[^\w.-]/g, '-');
|
|
23
72
|
}
|
|
24
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* 某 workflow 的状态目录绝对路径(workflow 级目录,下含多个 session 实例)。
|
|
75
|
+
* workflowState / taskIdentity 共用唯一权威实现。
|
|
76
|
+
*/
|
|
25
77
|
export function workflowStateDir(workingDir, workflowName) {
|
|
26
|
-
return join(workingDir,
|
|
78
|
+
return join(resolveStateRoot(workingDir), sanitizeWorkflowDirName(workflowName));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 具体任务实例(= session)的状态目录。
|
|
82
|
+
* 成功后改名为 <sessionId>.done/;清理归档目录为 <sessionId>.trash/。
|
|
83
|
+
*/
|
|
84
|
+
export function taskInstanceDir(workingDir, workflowName, sessionId) {
|
|
85
|
+
return join(workflowStateDir(workingDir, workflowName), sanitizeWorkflowDirName(sessionId));
|
|
27
86
|
}
|
|
28
87
|
/**
|
|
29
88
|
* 计算任务身份 hash。
|
|
@@ -41,8 +100,15 @@ export function workflowStateDir(workingDir, workflowName) {
|
|
|
41
100
|
* - 去除所有标点符号(保留中文、英文、数字、空格)
|
|
42
101
|
* - 去除多余空格(连续空格→单空格,首尾 trim)
|
|
43
102
|
* - 截断到 200 字符(防止超长输入导致不必要的差异)
|
|
103
|
+
*
|
|
104
|
+
* 参数:
|
|
105
|
+
* - workingDir:不传只返回 hash(纯计算,不读盘)。
|
|
106
|
+
* - sessionId:Phase 3 后传入——实例查找限定到「当前 session」的实例目录
|
|
107
|
+
* (<wfDir>/<sessionId>/ 与 <sessionId>.done/),读其 current.json 的 taskId
|
|
108
|
+
* 记录与本次 hash 比较。缺省时退化为旧行为(扫 wfDir 下所有 taskId 实例),
|
|
109
|
+
* 兼容历史调用与测试。
|
|
44
110
|
*/
|
|
45
|
-
export function computeTaskId(workflowName, inputs, workingDir) {
|
|
111
|
+
export function computeTaskId(workflowName, inputs, workingDir, sessionId) {
|
|
46
112
|
// 1. 提取 goal 文本(与 buildGoalAnchor 同策略)
|
|
47
113
|
const rawGoal = extractGoal(inputs);
|
|
48
114
|
// 2. 归一化 goal
|
|
@@ -60,8 +126,23 @@ export function computeTaskId(workflowName, inputs, workingDir) {
|
|
|
60
126
|
reason: 'no_previous',
|
|
61
127
|
};
|
|
62
128
|
}
|
|
63
|
-
// 5.
|
|
64
|
-
|
|
129
|
+
// 5. 找当前任务对应的实例目录,读其 current.json 记录与本次 hash 比较
|
|
130
|
+
// (Phase 3:session 语义——只查当前 session 的实例;taskId 是记录字段)
|
|
131
|
+
const wfDir = workflowStateDir(workingDir, workflowName);
|
|
132
|
+
const candidateDirs = findTaskInstanceDirs(wfDir, contentHash, sessionId);
|
|
133
|
+
if (candidateDirs.length === 0) {
|
|
134
|
+
// 无精确匹配。但若 wfDir 下存在其他任务实例(不同 goal / 已归档),
|
|
135
|
+
// 语义仍是「新任务 + 旧任务在场」——返回 hash_diff 而非 no_previous,
|
|
136
|
+
// 让 workflowState.init 能触发仲裁/清理旧产物(旧布局靠读 wfDir/current.json
|
|
137
|
+
// 天然得到这个信号,新布局必须显式扫描)。
|
|
138
|
+
return {
|
|
139
|
+
hash: contentHash,
|
|
140
|
+
isSameTask: false,
|
|
141
|
+
reason: hasAnyTaskInstanceInDir(wfDir, sessionId) ? 'hash_diff' : 'no_previous',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// 取第一个匹配的(hash 一样就是同一任务,不应有多个;有多个也只取第一个)
|
|
145
|
+
const stateFilePath = join(candidateDirs[0], 'current.json');
|
|
65
146
|
if (!existsSync(stateFilePath)) {
|
|
66
147
|
return {
|
|
67
148
|
hash: contentHash,
|
|
@@ -70,9 +151,9 @@ export function computeTaskId(workflowName, inputs, workingDir) {
|
|
|
70
151
|
};
|
|
71
152
|
}
|
|
72
153
|
try {
|
|
73
|
-
const stateContent = readFileSync(stateFilePath, '
|
|
154
|
+
const stateContent = readFileSync(stateFilePath, 'utf8');
|
|
74
155
|
const prevState = JSON.parse(stateContent);
|
|
75
|
-
// a. 不同 workflow 名 →
|
|
156
|
+
// a. 不同 workflow 名 → 不同任务(理论上不会发生,因为已经按目录隔离了,防御性检查)
|
|
76
157
|
if (prevState.name && prevState.name !== workflowName) {
|
|
77
158
|
return {
|
|
78
159
|
hash: contentHash,
|
|
@@ -104,6 +185,101 @@ export function computeTaskId(workflowName, inputs, workingDir) {
|
|
|
104
185
|
};
|
|
105
186
|
}
|
|
106
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* 在 workflow 级目录下,找到当前任务对应的实例目录。
|
|
190
|
+
* 支持两种布局:
|
|
191
|
+
* - 新布局(Phase 3,传 sessionId):<wfDir>/<sessionId>/current.json(活动)
|
|
192
|
+
* 与 <wfDir>/<sessionId>.done/current.json(已完成)
|
|
193
|
+
* - 旧布局(Phase 2,不传 sessionId):<wfDir>/<taskId>/current.json
|
|
194
|
+
* 与 <wfDir>/<taskId>.done/current.json
|
|
195
|
+
* - 旧布局兼容(无 session 层):<wfDir>/current.json —— 把整个 wfDir 当成一个实例
|
|
196
|
+
* 返回绝对路径数组。
|
|
197
|
+
*/
|
|
198
|
+
function findTaskInstanceDirs(wfDir, taskId, sessionId) {
|
|
199
|
+
const results = [];
|
|
200
|
+
if (sessionId) {
|
|
201
|
+
// Phase 3:session 语义 —— 只找当前 session 的实例(taskId 是记录字段,目录名是 sessionId)
|
|
202
|
+
const activeDir = join(wfDir, sanitizeWorkflowDirName(sessionId));
|
|
203
|
+
const doneDir = join(wfDir, `${sanitizeWorkflowDirName(sessionId)}.done`);
|
|
204
|
+
if (existsSync(activeDir))
|
|
205
|
+
results.push(activeDir);
|
|
206
|
+
if (existsSync(doneDir))
|
|
207
|
+
results.push(doneDir);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// Phase 2 旧行为:<wfDir>/<taskId>/ 和 <wfDir>/<taskId>.done/
|
|
211
|
+
const activeDir = join(wfDir, taskId);
|
|
212
|
+
const doneDir = join(wfDir, `${taskId}.done`);
|
|
213
|
+
if (existsSync(activeDir))
|
|
214
|
+
results.push(activeDir);
|
|
215
|
+
if (existsSync(doneDir))
|
|
216
|
+
results.push(doneDir);
|
|
217
|
+
}
|
|
218
|
+
if (results.length > 0)
|
|
219
|
+
return results;
|
|
220
|
+
// 旧布局兼容:wfDir 本身就是实例目录(current.json 直接在 wfDir 下)
|
|
221
|
+
// 检测方法:wfDir/current.json 存在且 wfDir 下没有子目录是 taskId 格式
|
|
222
|
+
if (existsSync(join(wfDir, 'current.json'))) {
|
|
223
|
+
// 确认这是旧布局(不是某个子实例目录)
|
|
224
|
+
try {
|
|
225
|
+
const entries = readdirSync(wfDir, { withFileTypes: true });
|
|
226
|
+
const hasInstanceSubdir = entries.some((e) => e.isDirectory() && /^[0-9a-f]{16}$/.test(e.name));
|
|
227
|
+
if (!hasInstanceSubdir) {
|
|
228
|
+
results.push(wfDir);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// 读不到就当旧布局
|
|
233
|
+
results.push(wfDir);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return results;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 检查 wfDir 下是否存在任何任务实例(活动态 / .done 归档态 / 旧布局 current.json)。
|
|
240
|
+
* 不含 .trash 回收目录。用于 computeTaskId 在无精确匹配时区分
|
|
241
|
+
* hash_diff(有其他旧任务)与 no_previous(从未跑过)。
|
|
242
|
+
* 传 sessionId 时只统计当前 session 的实例(Phase 3)。
|
|
243
|
+
*/
|
|
244
|
+
function hasAnyTaskInstanceInDir(wfDir, sessionId) {
|
|
245
|
+
if (!existsSync(wfDir))
|
|
246
|
+
return false;
|
|
247
|
+
try {
|
|
248
|
+
if (sessionId) {
|
|
249
|
+
// Phase 3:只看当前 session 的实例
|
|
250
|
+
const active = join(wfDir, sanitizeWorkflowDirName(sessionId));
|
|
251
|
+
const done = join(wfDir, `${sanitizeWorkflowDirName(sessionId)}.done`);
|
|
252
|
+
if (existsSync(active))
|
|
253
|
+
return true;
|
|
254
|
+
if (existsSync(done))
|
|
255
|
+
return true;
|
|
256
|
+
// 旧布局:wfDir 本身就是实例目录
|
|
257
|
+
if (existsSync(join(wfDir, 'current.json')))
|
|
258
|
+
return true;
|
|
259
|
+
if (existsSync(join(wfDir + '.done', 'current.json')))
|
|
260
|
+
return true;
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
const entries = readdirSync(wfDir, { withFileTypes: true });
|
|
264
|
+
for (const e of entries) {
|
|
265
|
+
if (!e.isDirectory())
|
|
266
|
+
continue;
|
|
267
|
+
if (/^[0-9a-f]{16}$/.test(e.name))
|
|
268
|
+
return true; // 活动实例
|
|
269
|
+
if (/^[0-9a-f]{16}\.done$/.test(e.name))
|
|
270
|
+
return true; // 归档实例
|
|
271
|
+
}
|
|
272
|
+
// 旧布局:wfDir 本身就是实例目录
|
|
273
|
+
if (existsSync(join(wfDir, 'current.json')))
|
|
274
|
+
return true;
|
|
275
|
+
if (existsSync(join(wfDir + '.done', 'current.json')))
|
|
276
|
+
return true;
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
107
283
|
/**
|
|
108
284
|
* 从 inputs 中提取 goal 文本
|
|
109
285
|
*
|
|
@@ -225,7 +401,7 @@ async function llmArbitrate(deps, actx) {
|
|
|
225
401
|
for await (const ev of chatFn({
|
|
226
402
|
provider: deps.provider,
|
|
227
403
|
messages: [
|
|
228
|
-
{ role: 'user', content: prompt, timestamp: Date.now(), id:
|
|
404
|
+
{ role: 'user', content: prompt, timestamp: Date.now(), id: randomUUID() },
|
|
229
405
|
],
|
|
230
406
|
tools: [],
|
|
231
407
|
signal: deps.signal,
|