minimal-agent 0.11.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 +5 -1
- package/plugins/workflow-runner/plugin.js +27 -15
- package/plugins/workflow-runner/plugin.ts +34 -20
- package/plugins/workflow-runner/src/agentCommon.js +13 -12
- package/plugins/workflow-runner/src/agentCommon.ts +22 -13
- 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 +50 -20
- package/plugins/workflow-runner/src/index.ts +75 -31
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +2 -1
- 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/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
- package/plugins/workflow-runner/src/stepExecutors/tool.js +91 -61
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +111 -71
- 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/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.js +113 -7
- package/plugins/workflow-runner/src/types.ts +137 -14
- 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/context-files.test.ts +47 -16
- 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/hostCapability.test.ts +9 -3
- package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
- package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
- 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/toolRuntimeIntegration.test.ts +168 -0
- 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 +36 -4
- package/src/cli/print.ts +42 -4
- package/src/cli/streamJson.js +19 -4
- package/src/cli/streamJson.ts +17 -4
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configFile.js +2 -21
- package/src/config/configFile.ts +11 -24
- package/src/config/configManagement.js +1 -1
- package/src/config/configManagement.ts +2 -2
- package/src/config/configService.js +24 -0
- package/src/config/configService.ts +23 -0
- package/src/context/compact.js +4 -0
- package/src/context/compact.ts +4 -0
- package/src/context/contextBundle.js +38 -13
- package/src/context/contextBundle.ts +47 -13
- package/src/harness/stallGuard.js +19 -4
- package/src/harness/stallGuard.ts +30 -7
- package/src/infra/verification.js +25 -18
- package/src/infra/verification.ts +50 -18
- package/src/loop.js +95 -11
- package/src/loop.ts +125 -16
- package/src/main.js +8 -1
- package/src/main.tsx +7 -1
- package/src/memory/constants.js +10 -0
- package/src/memory/constants.ts +13 -0
- package/src/memory/episodicMemory.js +274 -2
- package/src/memory/episodicMemory.ts +306 -1
- package/src/memory/fileStore.js +24 -4
- package/src/memory/fileStore.ts +22 -2
- package/src/memory/memoryService.js +79 -12
- package/src/memory/memoryService.ts +108 -9
- package/src/memory/repository.js +415 -85
- package/src/memory/repository.ts +437 -86
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.ts +16 -0
- package/src/plugins/pluginApi.ts +30 -6
- package/src/plugins/pluginRunner.js +23 -8
- package/src/plugins/pluginRunner.ts +32 -13
- package/src/prompts/policyRegistry.js +55 -1
- package/src/prompts/policyRegistry.ts +55 -1
- package/src/prompts/promptAssembly.js +39 -7
- package/src/prompts/promptAssembly.ts +145 -59
- package/src/session/runAgentTurn.js +43 -21
- package/src/session/runAgentTurn.ts +68 -26
- package/src/session/runTurn.js +11 -46
- package/src/session/runTurn.ts +12 -46
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +5 -0
- package/src/session/sessionState.ts +14 -0
- package/src/tools/advisor/advisor.js +76 -9
- package/src/tools/advisor/advisor.ts +79 -9
- package/src/tools/agent/agent.js +143 -14
- package/src/tools/agent/agent.ts +149 -14
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +5 -0
- package/src/tools/agent/agentTypes.ts +5 -0
- package/src/tools/bash/bash.js +80 -15
- package/src/tools/bash/bash.ts +83 -15
- package/src/tools/capabilities.js +5 -0
- package/src/tools/capabilities.ts +11 -1
- package/src/tools/edit/edit.js +7 -4
- package/src/tools/edit/edit.ts +7 -4
- package/src/tools/edit/multi-edit.js +4 -2
- package/src/tools/edit/multi-edit.ts +4 -2
- package/src/tools/glob/glob.js +156 -40
- package/src/tools/glob/glob.ts +185 -46
- package/src/tools/index.js +35 -17
- package/src/tools/index.ts +81 -20
- package/src/tools/lsp/lsp.js +165 -22
- package/src/tools/lsp/lsp.ts +206 -36
- package/src/tools/powershell/powershell.js +134 -27
- package/src/tools/powershell/powershell.ts +171 -30
- package/src/tools/presentresult/presentresult.js +10 -0
- package/src/tools/presentresult/presentresult.ts +10 -0
- package/src/tools/read/read.js +249 -29
- package/src/tools/read/read.ts +396 -59
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +2 -1
- package/src/tools/shared/fileActivity.ts +3 -0
- package/src/tools/shared/permanentDeleteGuard.js +57 -0
- package/src/tools/shared/permanentDeleteGuard.ts +71 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +79 -28
- package/src/tools/tasks/backgroundTaskService.ts +165 -49
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +64 -14
- package/src/tools/tasks/taskOutput.ts +102 -23
- package/src/tools/tasks/taskStop.js +48 -8
- package/src/tools/tasks/taskStop.ts +69 -9
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- 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/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +6 -2
- package/src/tools/write/write.ts +6 -2
- package/src/types.ts +22 -4
- package/src/ui/hooks/useChat.js +31 -11
- package/src/ui/hooks/useChat.ts +31 -13
- package/src/web/protocol.ts +78 -3
- 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
|
@@ -29,10 +29,12 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { createHash } from 'node:crypto';
|
|
31
31
|
import { mkdir, writeFile, appendFile, rm, rename, readFile, readdir } from 'node:fs/promises';
|
|
32
|
-
import { existsSync } from 'node:fs';
|
|
33
|
-
import { join } from 'node:path';
|
|
34
|
-
import { computeTaskId, extractGoal,
|
|
32
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
33
|
+
import { join, dirname, relative } from 'node:path';
|
|
34
|
+
import { computeTaskId, extractGoal, sanitizeWorkflowDirName, workflowStateDir } from './taskIdentity.js';
|
|
35
|
+
import { safeArtifactPath } from './types.js';
|
|
35
36
|
const DONE_SUFFIX = '.done';
|
|
37
|
+
const TRASH_SUFFIX = '.trash';
|
|
36
38
|
const CHECKPOINTS_DIR = 'checkpoints';
|
|
37
39
|
const SUPPLEMENTS_FILE = 'supplements.json';
|
|
38
40
|
/**
|
|
@@ -44,6 +46,17 @@ export const LOOP_CP_SEP = '@@';
|
|
|
44
46
|
export function loopCheckpointName(loopId, iteration, stepId) {
|
|
45
47
|
return `${loopId}${LOOP_CP_SEP}${iteration}${LOOP_CP_SEP}${stepId}`;
|
|
46
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* glob 段(路径中的一节,不含分隔符)转正则源:`*` -> `[^/]*`、`?` -> `[^/]`,
|
|
51
|
+
* 其余字符全部转义(`[` `]` 按字面处理,不支持字符类--声明产物用 `*`/`?` 足够,
|
|
52
|
+
* 少一种语法就少一类转义事故面)。
|
|
53
|
+
*/
|
|
54
|
+
function artifactSegToPattern(seg) {
|
|
55
|
+
return seg
|
|
56
|
+
.replace(/[.+^${}()|[\]\\!,$^-]/g, '\\$&')
|
|
57
|
+
.replace(/\*/g, '[^/]*')
|
|
58
|
+
.replace(/\?/g, '[^/]');
|
|
59
|
+
}
|
|
47
60
|
/** 解析复合 checkpoint 文件名;非复合名(普通顶层步)返回 null。 */
|
|
48
61
|
function parseLoopCheckpointName(name) {
|
|
49
62
|
const parts = name.split(LOOP_CP_SEP);
|
|
@@ -77,12 +90,23 @@ export function computeDefFingerprint(steps) {
|
|
|
77
90
|
.slice(0, 16);
|
|
78
91
|
}
|
|
79
92
|
export class WorkflowState {
|
|
93
|
+
/** 任务实例目录(绝对路径),init 阶段确定 session 后设置 = <wfDir>/<sessionId>/ */
|
|
80
94
|
dir;
|
|
95
|
+
/** workflow 级目录(下含多个 session 实例) */
|
|
96
|
+
wfDir;
|
|
81
97
|
workingDir;
|
|
82
|
-
|
|
98
|
+
workflowName;
|
|
99
|
+
/** 当前 session 的实例标识(一个 session 同时只跑一个同名工作流,即任务实例) */
|
|
100
|
+
sessionId;
|
|
101
|
+
constructor(workingDir, workflowName, sessionId = 'default') {
|
|
83
102
|
this.workingDir = workingDir;
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
this.workflowName = workflowName;
|
|
104
|
+
// Phase 3:session 即任务实例——实例目录 = <wfDir>/<sessionId>/
|
|
105
|
+
this.sessionId = sanitizeWorkflowDirName(sessionId);
|
|
106
|
+
// workflow 级目录(与 taskIdentity.computeTaskId 读的是同一权威路径)
|
|
107
|
+
this.wfDir = workflowStateDir(workingDir, workflowName);
|
|
108
|
+
// 实例目录暂空占位,init 中确定后赋值
|
|
109
|
+
this.dir = '';
|
|
86
110
|
}
|
|
87
111
|
/**
|
|
88
112
|
* 初始化工作流状态 —— 核心入口,返回恢复信息。
|
|
@@ -106,45 +130,108 @@ export class WorkflowState {
|
|
|
106
130
|
arbitrate) {
|
|
107
131
|
// 1. 计算当前任务的 identity(goal 内容 hash 是唯一权威信号)
|
|
108
132
|
// 与本次定义的结构指纹(resume 兼容性信号:yaml 结构变了旧断点即作废)
|
|
109
|
-
|
|
133
|
+
// Phase 3:传 sessionId,实例查找限定到当前 session(taskId 降级为记录字段)
|
|
134
|
+
const identity = computeTaskId(name, inputs, this.workingDir, this.sessionId);
|
|
110
135
|
const fp = computeDefFingerprint(steps);
|
|
111
|
-
// 2.
|
|
136
|
+
// 2. 确定本任务的实例目录(Phase 3:session 即实例目录 = <wfDir>/<sessionId>/)
|
|
137
|
+
// resume/completed:沿用当前 session 的实例目录;fresh/new-task:建/重建它
|
|
138
|
+
this.dir = this.resolveTaskDir(identity.hash, identity.isSameTask);
|
|
139
|
+
// 3. 读取活动态 current.json
|
|
112
140
|
const oldState = await this.readCurrentState();
|
|
113
|
-
if (
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
if (oldState) {
|
|
142
|
+
// ---- 有活动状态 ----
|
|
143
|
+
if (identity.isSameTask) {
|
|
144
|
+
// 同任务 → resume / recovery(下面有公共分支处理)
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
// 不同任务 + 旧任务未完成 → new-task(下面 oldState 非空分支处理)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// ---- 活动态为空 ----
|
|
152
|
+
// 先看是否有同 taskId 的归档(同一个任务之前成功完成了)
|
|
153
|
+
const archived = identity.isSameTask
|
|
154
|
+
? await this.findArchivedStateByTaskId(identity.hash)
|
|
155
|
+
: null;
|
|
156
|
+
if (archived && archived.taskId === identity.hash) {
|
|
157
|
+
// 模式 3: completed —— 同任务此前已成功
|
|
158
|
+
// 把 this.dir 指向归档目录(后续读产物等基于它)
|
|
159
|
+
this.dir = join(this.wfDir, `${this.sessionId}${DONE_SUFFIX}`);
|
|
160
|
+
return {
|
|
161
|
+
mode: 'completed',
|
|
162
|
+
artifacts: await this.existingArtifactsAsync(archived.artifacts ?? declaredArtifacts),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// 有其他任务实例(不同 goal)→ 先仲裁二审,再决定 new-task / resume
|
|
166
|
+
if (this.hasAnyTaskInstance()) {
|
|
167
|
+
let arbitratedResume = false;
|
|
168
|
+
if (arbitrate &&
|
|
169
|
+
identity.reason === 'hash_diff' &&
|
|
170
|
+
identity.isSameTask === false) {
|
|
171
|
+
const latestActive = await this.findLatestActiveState();
|
|
172
|
+
if (latestActive &&
|
|
173
|
+
latestActive.state.name === name &&
|
|
174
|
+
latestActive.state.status !== 'done' &&
|
|
175
|
+
latestActive.state.defFingerprint === fp) {
|
|
176
|
+
// 临时切换 this.dir 到旧任务目录,以便 scanCheckpoints / readInputs 工作
|
|
177
|
+
const prevDir = this.dir;
|
|
178
|
+
this.dir = latestActive.dir;
|
|
179
|
+
try {
|
|
180
|
+
const prevGoal = extractGoal((await this.readInputs()) ?? {});
|
|
181
|
+
const newGoal = extractGoal(inputs);
|
|
182
|
+
let verdict = 'new';
|
|
183
|
+
try {
|
|
184
|
+
verdict = await arbitrate({
|
|
185
|
+
workflowName: name,
|
|
186
|
+
prevGoal,
|
|
187
|
+
newGoal,
|
|
188
|
+
completedStepIds: await this.scanCheckpoints(steps),
|
|
189
|
+
totalSteps: steps.length,
|
|
190
|
+
artifacts: latestActive.state.artifacts ?? [],
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
verdict = 'new';
|
|
195
|
+
}
|
|
196
|
+
if (verdict === 'resume') {
|
|
197
|
+
// 先置位再 return:finally 不再把 this.dir 改回新任务目录,
|
|
198
|
+
// 延续旧实例目录。否则后续 readSupplements / recordCheckpoint
|
|
199
|
+
// 全部写到错误位置,resume 的断点续跑静默失效
|
|
200
|
+
// (实测:supplements 读空、checkpoint 写丢、current.json 停在旧值)。
|
|
201
|
+
arbitratedResume = true;
|
|
202
|
+
const info = await this.buildResumeInfo(latestActive.state, steps, selfHealBackoff);
|
|
203
|
+
info.prevGoal = prevGoal;
|
|
204
|
+
info.supplementaryGoal = newGoal;
|
|
205
|
+
return info;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
// 仲裁判 new → 恢复 this.dir 为新任务目录(除非 resume 已 return)
|
|
210
|
+
if (!arbitratedResume)
|
|
211
|
+
this.dir = prevDir;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
123
214
|
}
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
215
|
+
// 模式 2: new-task —— 不同任务,清旧声明产物 + 旧状态后全新开始
|
|
216
|
+
const oldArtifacts = await this.collectAllInstanceArtifacts(name, declaredArtifacts);
|
|
217
|
+
const sweep = await this.archiveThenClean(oldArtifacts.length > 0 ? oldArtifacts : declaredArtifacts, `${Date.now()}-stale`);
|
|
218
|
+
const sweepOrphan = await this.archiveThenClean(declaredArtifacts, `${Date.now()}-orphan-${identity.hash.slice(0, 8)}`);
|
|
128
219
|
await this.cleanSlate(name, inputs, identity.hash, declaredArtifacts, fp);
|
|
129
220
|
return {
|
|
130
221
|
mode: 'new-task',
|
|
131
222
|
startFrom: 0,
|
|
132
|
-
cleanedArtifacts: [
|
|
133
|
-
|
|
134
|
-
...(await this.cleanupStaleDeclared(declaredArtifacts)),
|
|
135
|
-
],
|
|
223
|
+
cleanedArtifacts: [...sweep.cleaned, ...sweepOrphan.cleaned],
|
|
224
|
+
archivedTo: sweep.archivedTo ?? sweepOrphan.archivedTo,
|
|
136
225
|
};
|
|
137
226
|
}
|
|
138
|
-
// 模式 1: fresh ——
|
|
139
|
-
|
|
140
|
-
// artifacts 清单随 current.json 一起被删,引擎无从知道它属于谁)。
|
|
141
|
-
// 用户约定:全新任务开始时清理残留与中间文件——删除范围**仅限本工作流
|
|
142
|
-
// 声明的产物**(与运行自身对它们的覆盖写权限一致,不碰用户其他文件)。
|
|
227
|
+
// 模式 1: fresh —— 完全没有任何任务实例,首次运行
|
|
228
|
+
const sweepF = await this.archiveThenClean(declaredArtifacts, `${Date.now()}-orphan`);
|
|
143
229
|
await this.cleanSlate(name, inputs, identity.hash, declaredArtifacts, fp);
|
|
144
230
|
return {
|
|
145
231
|
mode: 'fresh',
|
|
146
232
|
startFrom: 0,
|
|
147
|
-
cleanedArtifacts:
|
|
233
|
+
cleanedArtifacts: sweepF.cleaned,
|
|
234
|
+
archivedTo: sweepF.archivedTo,
|
|
148
235
|
};
|
|
149
236
|
}
|
|
150
237
|
// 3. 有活动状态(上次中断 / 失败 / 进行中,未归档)—— 用 hash 判同/异任务
|
|
@@ -188,22 +275,21 @@ export class WorkflowState {
|
|
|
188
275
|
// 模式 2: new-task —— 不同任务,清旧声明产物 + 旧状态后全新开始。
|
|
189
276
|
// legacy 状态文件无 artifacts 字段时:仅当同名 workflow 用本次声明清单兜底
|
|
190
277
|
//(不同名 workflow 的产物不是我们的,不能殃及)。
|
|
191
|
-
const
|
|
278
|
+
const sweep = await this.archiveThenClean(oldState.artifacts ?? (oldState.name === name ? declaredArtifacts : []), `${Date.now()}-${oldState.taskId.slice(0, 8)}`);
|
|
279
|
+
const sweepStale = await this.archiveThenClean(declaredArtifacts, `${Date.now()}-orphan-${identity.hash.slice(0, 8)}`);
|
|
192
280
|
await this.cleanSlate(name, inputs, identity.hash, declaredArtifacts, fp);
|
|
193
281
|
return {
|
|
194
282
|
mode: 'new-task',
|
|
195
283
|
startFrom: 0,
|
|
196
|
-
cleanedArtifacts: [
|
|
197
|
-
|
|
198
|
-
...(await this.cleanupStaleDeclared(declaredArtifacts)),
|
|
199
|
-
],
|
|
284
|
+
cleanedArtifacts: [...sweep.cleaned, ...sweepStale.cleaned],
|
|
285
|
+
archivedTo: sweep.archivedTo ?? sweepStale.archivedTo,
|
|
200
286
|
};
|
|
201
287
|
}
|
|
202
288
|
// 4. 同一任务 —— 检查完成状态(兜底:正常成功会 archive,故活动目录 done 少见)
|
|
203
289
|
if (oldState.status === 'done') {
|
|
204
290
|
return {
|
|
205
291
|
mode: 'completed',
|
|
206
|
-
artifacts: this.
|
|
292
|
+
artifacts: await this.existingArtifactsAsync(oldState.artifacts ?? declaredArtifacts),
|
|
207
293
|
};
|
|
208
294
|
}
|
|
209
295
|
// 4.5 定义结构变更守卫:同任务但 yaml 结构改过(增删步/换 id/调嵌套)——
|
|
@@ -213,16 +299,15 @@ export class WorkflowState {
|
|
|
213
299
|
// runner 经 defChanged 旗标向用户明示。legacy 状态(无 defFingerprint
|
|
214
300
|
// 字段)无从验证兼容性,同样按变更处理(一次性升级成本,安全优先)。
|
|
215
301
|
if (oldState.defFingerprint !== fp) {
|
|
216
|
-
const
|
|
302
|
+
const sweep = await this.archiveThenClean(oldState.artifacts ?? declaredArtifacts, `${Date.now()}-${oldState.taskId.slice(0, 8)}`);
|
|
303
|
+
const sweepStale = await this.archiveThenClean(declaredArtifacts, `${Date.now()}-orphan-${identity.hash.slice(0, 8)}`);
|
|
217
304
|
await this.cleanSlate(name, inputs, identity.hash, declaredArtifacts, fp);
|
|
218
305
|
return {
|
|
219
306
|
mode: 'new-task',
|
|
220
307
|
startFrom: 0,
|
|
221
308
|
defChanged: true,
|
|
222
|
-
cleanedArtifacts: [
|
|
223
|
-
|
|
224
|
-
...(await this.cleanupStaleDeclared(declaredArtifacts)),
|
|
225
|
-
],
|
|
309
|
+
cleanedArtifacts: [...sweep.cleaned, ...sweepStale.cleaned],
|
|
310
|
+
archivedTo: sweep.archivedTo ?? sweepStale.archivedTo,
|
|
226
311
|
};
|
|
227
312
|
}
|
|
228
313
|
// 5. 模式 4: resume —— 同一任务未完成,扫描 checkpoint 决定恢复点。
|
|
@@ -462,23 +547,337 @@ export class WorkflowState {
|
|
|
462
547
|
return result;
|
|
463
548
|
}
|
|
464
549
|
// ---- 私有方法:声明式产物生命周期 ----
|
|
550
|
+
/**
|
|
551
|
+
* 把声明产物里的 glob 模式(`research/04-deep-dive-*.md`)展开成 cwd 下实际
|
|
552
|
+
* 匹配的文件相对路径列表;非 glob 路径原样保留。越界路径被过滤(fail-closed)。
|
|
553
|
+
* 支持段内 `*` / `?`(不跨目录分隔符);声明里写 glob 却一个文件都不匹配时
|
|
554
|
+
* 该声明被丢弃(与旧行为一致:不存在的产物不参与清理/报告)。
|
|
555
|
+
*/
|
|
556
|
+
async expandDeclaredArtifacts(paths) {
|
|
557
|
+
const out = [];
|
|
558
|
+
for (const p of paths) {
|
|
559
|
+
const safe = safeArtifactPath(this.workingDir, p);
|
|
560
|
+
if (safe === null)
|
|
561
|
+
continue;
|
|
562
|
+
if (!/[*?]/.test(safe)) {
|
|
563
|
+
out.push(safe);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const norm = safe.replace(/\\/g, '/');
|
|
567
|
+
const segs = norm.split('/');
|
|
568
|
+
const fullRe = new RegExp(`^${segs.map(artifactSegToPattern).join('/')}$`);
|
|
569
|
+
out.push(...(await this.walkGlob(this.workingDir, segs, fullRe, '')));
|
|
570
|
+
}
|
|
571
|
+
return out;
|
|
572
|
+
}
|
|
573
|
+
/** 逐段匹配目录树收集文件;dir 读失败(不存在/无权限)返回空。 */
|
|
574
|
+
async walkGlob(absDir, segs, fullRe, relPrefix) {
|
|
575
|
+
const [head, ...rest] = segs;
|
|
576
|
+
if (!head)
|
|
577
|
+
return [];
|
|
578
|
+
let entries;
|
|
579
|
+
try {
|
|
580
|
+
entries = await readdir(absDir, { withFileTypes: true });
|
|
581
|
+
}
|
|
582
|
+
catch {
|
|
583
|
+
return [];
|
|
584
|
+
}
|
|
585
|
+
const headRe = new RegExp(`^${artifactSegToPattern(head)}$`);
|
|
586
|
+
const results = [];
|
|
587
|
+
for (const e of entries) {
|
|
588
|
+
if (!headRe.test(e.name))
|
|
589
|
+
continue;
|
|
590
|
+
const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
|
|
591
|
+
if (rest.length === 0) {
|
|
592
|
+
if (e.isFile() && fullRe.test(rel))
|
|
593
|
+
results.push(rel);
|
|
594
|
+
}
|
|
595
|
+
else if (e.isDirectory()) {
|
|
596
|
+
results.push(...(await this.walkGlob(join(absDir, e.name), rest, fullRe, rel)));
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return results;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* 清理前把【实际存在】的声明产物移入回收目录 `<状态目录>.trash/<stamp>/`(保留
|
|
603
|
+
* 相对路径结构),返回回收目录(相对 cwd 表示,便于 UI 展示);无可归档文件或
|
|
604
|
+
* 一个都没归档成功时返回 undefined(此时后续 rm 兜底直接删,不能谎报归档位置--
|
|
605
|
+
* 用户会按提示去回收站找文件)。单个 move 失败不阻断(残留交给 rm 兜底)。
|
|
606
|
+
* 回收目录是状态目录的 sibling:cleanSlate/cleanup 只重建 `<name>/`,
|
|
607
|
+
* `.trash/` 跨任务留存,直到用户手动清理(如保留最近 N 次的淘汰策略,暂未实现)。
|
|
608
|
+
*/
|
|
609
|
+
async archiveDeclaredArtifacts(existingRels, stamp) {
|
|
610
|
+
if (existingRels.length === 0)
|
|
611
|
+
return undefined;
|
|
612
|
+
const trashRoot = join(this.dir + TRASH_SUFFIX, stamp);
|
|
613
|
+
let archived = 0;
|
|
614
|
+
for (const rel of existingRels) {
|
|
615
|
+
const dest = join(trashRoot, rel);
|
|
616
|
+
try {
|
|
617
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
618
|
+
await rename(join(this.workingDir, rel), dest);
|
|
619
|
+
archived++;
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
// 单个归档失败不阻断,残留交给后续 rm 兜底
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (archived === 0)
|
|
626
|
+
return undefined;
|
|
627
|
+
const display = relative(this.workingDir, trashRoot);
|
|
628
|
+
return display.startsWith('..') ? trashRoot : display;
|
|
629
|
+
}
|
|
465
630
|
/** 删除工作流【声明】的产物文件(相对 cwd),用于 new-task 清理旧任务残留(如旧 PLAN.md)。
|
|
466
|
-
*
|
|
631
|
+
* 仅删工作流自己声明的产物(glob 声明先展开为实际文件)、仅 force-rm 文件,
|
|
632
|
+
* **并强制路径在 cwd 内(防 ../ 跳出删用户家目录)**。
|
|
633
|
+
* 返回真正执行清理的路径列表(展开后的实际文件相对路径;越界路径被过滤掉,fail-closed)。 */
|
|
467
634
|
async cleanupDeclaredArtifacts(paths) {
|
|
468
|
-
|
|
469
|
-
|
|
635
|
+
const done = [];
|
|
636
|
+
for (const rel of await this.expandDeclaredArtifacts(paths)) {
|
|
637
|
+
const abs = join(this.workingDir, rel);
|
|
638
|
+
if (!existsSync(abs))
|
|
639
|
+
continue;
|
|
640
|
+
try {
|
|
641
|
+
await rm(abs, { force: true });
|
|
642
|
+
done.push(rel);
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
// 单个删除失败不影响整体(与原实现 .catch(() => {}) 一致)
|
|
646
|
+
}
|
|
470
647
|
}
|
|
471
|
-
return
|
|
648
|
+
return done;
|
|
472
649
|
}
|
|
473
|
-
/** 从声明产物路径里过滤出 cwd
|
|
474
|
-
|
|
475
|
-
|
|
650
|
+
/** 从声明产物路径里过滤出 cwd 下【实际存在且路径安全落在 cwd 内】的文件
|
|
651
|
+
* (completed 报真实产物用;glob 声明先展开;越界声明不报告,视为不存在)。 */
|
|
652
|
+
async existingArtifactsAsync(paths) {
|
|
653
|
+
const expanded = await this.expandDeclaredArtifacts(paths);
|
|
654
|
+
return expanded.filter((rel) => existsSync(join(this.workingDir, rel)));
|
|
476
655
|
}
|
|
477
|
-
/**
|
|
478
|
-
|
|
479
|
-
|
|
656
|
+
/**
|
|
657
|
+
* 「先归档再清理」组合:把【实际存在】的声明产物移入 .trash 回收目录,
|
|
658
|
+
* 归档后残留(rename 失败者)由 cleanup 兜底 rm。返回清理列表与回收目录。
|
|
659
|
+
* fresh/new-task 启动清理统一走这里--直接 rm 旧产物曾在实战中造成不可找回的损失
|
|
660
|
+
* (LLM 仲裁误判 new-task,同主题调研的全部报告被静默删除)。
|
|
661
|
+
*/
|
|
662
|
+
async archiveThenClean(paths, stamp) {
|
|
663
|
+
const existing = await this.existingArtifactsAsync(paths);
|
|
664
|
+
const archivedTo = await this.archiveDeclaredArtifacts(existing, stamp);
|
|
665
|
+
const cleaned = await this.cleanupDeclaredArtifacts(paths);
|
|
666
|
+
return { cleaned, archivedTo };
|
|
480
667
|
}
|
|
481
668
|
// ---- 公共方法:归档与清理(保持原有签名兼容) ----
|
|
669
|
+
/**
|
|
670
|
+
* 确定当前任务的实例目录路径。
|
|
671
|
+
* Phase 3:实例目录 = <wfDir>/<sessionId>/(session 即任务实例)。
|
|
672
|
+
* isSameTask=true 时若旧布局(wfDir 本身)存在则沿用(兼容升级前状态)。
|
|
673
|
+
*/
|
|
674
|
+
resolveTaskDir(_taskId, isSameTask) {
|
|
675
|
+
if (isSameTask) {
|
|
676
|
+
const existing = this.findExistingTaskDir(_taskId);
|
|
677
|
+
if (existing)
|
|
678
|
+
return existing;
|
|
679
|
+
}
|
|
680
|
+
return join(this.wfDir, this.sessionId);
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* 找当前 session 对应的现存活动实例目录(含旧布局兼容)。
|
|
684
|
+
* 找不到返回 null。
|
|
685
|
+
*/
|
|
686
|
+
findExistingTaskDir(_taskId) {
|
|
687
|
+
// Phase 3:当前 session 的实例目录
|
|
688
|
+
const sessionDir = join(this.wfDir, this.sessionId);
|
|
689
|
+
if (existsSync(sessionDir))
|
|
690
|
+
return sessionDir;
|
|
691
|
+
// 旧布局兼容:wfDir 本身就是实例目录
|
|
692
|
+
if (existsSync(join(this.wfDir, 'current.json'))) {
|
|
693
|
+
try {
|
|
694
|
+
const entries = readdirSync(this.wfDir, { withFileTypes: true });
|
|
695
|
+
const hasInstanceSubdir = entries.some((e) => e.isDirectory() && /^[0-9a-f]{16}$/.test(e.name));
|
|
696
|
+
if (!hasInstanceSubdir)
|
|
697
|
+
return this.wfDir;
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
return this.wfDir;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* 扫描当前 session 是否有任何任务实例(活动态或 .done 归档态,不包括 .trash)。
|
|
707
|
+
* 用于 isSameTask=false 时区分 fresh(从未跑过)和 new-task(有其他旧任务)。
|
|
708
|
+
*/
|
|
709
|
+
hasAnyTaskInstance() {
|
|
710
|
+
if (!existsSync(this.wfDir))
|
|
711
|
+
return false;
|
|
712
|
+
// Phase 3:当前 session 的实例(活动 / .done)
|
|
713
|
+
if (existsSync(join(this.wfDir, this.sessionId)))
|
|
714
|
+
return true;
|
|
715
|
+
if (existsSync(join(this.wfDir, `${this.sessionId}${DONE_SUFFIX}`)))
|
|
716
|
+
return true;
|
|
717
|
+
// 旧布局:wfDir 本身就是实例(current.json 直接在 wfDir 下)
|
|
718
|
+
if (existsSync(join(this.wfDir, 'current.json')))
|
|
719
|
+
return true;
|
|
720
|
+
if (existsSync(join(this.wfDir + DONE_SUFFIX, 'current.json')))
|
|
721
|
+
return true;
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* 扫描 wfDir 下所有 .done 归档,返回第一个(也是最新的,按修改时间排序)的 current.json。
|
|
726
|
+
* 用于 completed 检测的兜底——当 hash 不匹配但 wfDir 下有归档时,判断是否是同名 workflow 的旧归档。
|
|
727
|
+
*/
|
|
728
|
+
findLatestArchivedState() {
|
|
729
|
+
return this.findArchivedState(() => true);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* 在 wfDir 下找当前 session 对应的归档目录(.done)里的 current.json。
|
|
733
|
+
* 支持新布局(wfDir/<sessionId>.done/)和旧布局(wfDir.done/)。
|
|
734
|
+
*/
|
|
735
|
+
findArchivedStateByTaskId(_taskId) {
|
|
736
|
+
return this.findArchivedState((name, isLegacy) => isLegacy || name === `${this.sessionId}${DONE_SUFFIX}`);
|
|
737
|
+
}
|
|
738
|
+
async findArchivedState(predicate) {
|
|
739
|
+
// 旧布局:wfDir.done/current.json
|
|
740
|
+
const legacyDone = this.wfDir + DONE_SUFFIX;
|
|
741
|
+
if (existsSync(join(legacyDone, 'current.json')) && predicate('', true)) {
|
|
742
|
+
try {
|
|
743
|
+
const raw = await readFile(join(legacyDone, 'current.json'), 'utf-8');
|
|
744
|
+
return JSON.parse(raw);
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
// fall through
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
// 新布局:wfDir/<taskId>.done/current.json
|
|
751
|
+
if (!existsSync(this.wfDir))
|
|
752
|
+
return null;
|
|
753
|
+
try {
|
|
754
|
+
const entries = readdirSync(this.wfDir, { withFileTypes: true });
|
|
755
|
+
const doneDirs = entries
|
|
756
|
+
.filter((e) => e.isDirectory() && e.name.endsWith(DONE_SUFFIX))
|
|
757
|
+
.map((e) => e.name)
|
|
758
|
+
.filter((name) => predicate(name, false))
|
|
759
|
+
.sort();
|
|
760
|
+
for (const name of doneDirs) {
|
|
761
|
+
try {
|
|
762
|
+
const raw = await readFile(join(this.wfDir, name, 'current.json'), 'utf-8');
|
|
763
|
+
return JSON.parse(raw);
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
// ignore
|
|
772
|
+
}
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* 收集【当前 session】的任务实例(活动态 + .done 归档态)的声明产物路径,
|
|
777
|
+
* 用于 new-task 时归档+清理旧任务的产物。去重后返回。
|
|
778
|
+
* Phase 3:只收集当前 session 的实例产物——不同 session 的产物各归各,
|
|
779
|
+
* new-task 绝不殃及其他 session 的产出。
|
|
780
|
+
*/
|
|
781
|
+
async collectAllInstanceArtifacts(workflowName, fallback) {
|
|
782
|
+
const seen = new Set();
|
|
783
|
+
const allArtifacts = [];
|
|
784
|
+
const pushAll = (arr) => {
|
|
785
|
+
for (const a of arr ?? []) {
|
|
786
|
+
if (!seen.has(a)) {
|
|
787
|
+
seen.add(a);
|
|
788
|
+
allArtifacts.push(a);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
// Phase 3:当前 session 的活动实例与 .done 归档
|
|
793
|
+
for (const dirName of [this.sessionId, `${this.sessionId}${DONE_SUFFIX}`]) {
|
|
794
|
+
const dir = join(this.wfDir, dirName);
|
|
795
|
+
if (!existsSync(dir))
|
|
796
|
+
continue;
|
|
797
|
+
try {
|
|
798
|
+
const raw = await readFile(join(dir, 'current.json'), 'utf-8');
|
|
799
|
+
const s = JSON.parse(raw);
|
|
800
|
+
if (s.name === workflowName)
|
|
801
|
+
pushAll(s.artifacts);
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
// ignore
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
// 旧布局兼容:wfDir 本身有 current.json(且无子实例目录)或 wfDir.done/
|
|
808
|
+
if (!existsSync(this.wfDir)) {
|
|
809
|
+
const legacyDone = this.wfDir + DONE_SUFFIX;
|
|
810
|
+
try {
|
|
811
|
+
const raw = await readFile(join(legacyDone, 'current.json'), 'utf-8');
|
|
812
|
+
const s = JSON.parse(raw);
|
|
813
|
+
if (s.name === workflowName)
|
|
814
|
+
pushAll(s.artifacts);
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
// ignore
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
try {
|
|
822
|
+
const entries = readdirSync(this.wfDir, { withFileTypes: true });
|
|
823
|
+
const hasInstanceSubdir = entries.some((e) => e.isDirectory() && /^[0-9a-f]{16}/.test(e.name));
|
|
824
|
+
if (!hasInstanceSubdir && existsSync(join(this.wfDir, 'current.json'))) {
|
|
825
|
+
try {
|
|
826
|
+
const raw = await readFile(join(this.wfDir, 'current.json'), 'utf-8');
|
|
827
|
+
const s = JSON.parse(raw);
|
|
828
|
+
if (s.name === workflowName)
|
|
829
|
+
pushAll(s.artifacts);
|
|
830
|
+
}
|
|
831
|
+
catch {
|
|
832
|
+
// ignore
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
// ignore
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return allArtifacts.length > 0 ? allArtifacts : fallback;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* 找当前 session 的活动态实例(未完成),返回其目录 + current.json。
|
|
844
|
+
* 用于 isSameTask=false 时的仲裁——拿到旧任务的 state 做二审。
|
|
845
|
+
* 找不到返回 null。
|
|
846
|
+
*/
|
|
847
|
+
async findLatestActiveState() {
|
|
848
|
+
// Phase 3:当前 session 的活动实例 <wfDir>/<sessionId>/
|
|
849
|
+
const sessionDir = join(this.wfDir, this.sessionId);
|
|
850
|
+
if (existsSync(sessionDir)) {
|
|
851
|
+
try {
|
|
852
|
+
const raw = await readFile(join(sessionDir, 'current.json'), 'utf-8');
|
|
853
|
+
const state = JSON.parse(raw);
|
|
854
|
+
if (state.status !== 'done') {
|
|
855
|
+
return { dir: sessionDir, state };
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
// fall through
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
// 旧布局兼容:wfDir 本身是实例目录(current.json 直接在其下)
|
|
863
|
+
if (existsSync(join(this.wfDir, 'current.json'))) {
|
|
864
|
+
try {
|
|
865
|
+
const entries = readdirSync(this.wfDir, { withFileTypes: true });
|
|
866
|
+
const hasInstanceSubdir = entries.some((e) => e.isDirectory() && /^[0-9a-f]{16}/.test(e.name));
|
|
867
|
+
if (!hasInstanceSubdir) {
|
|
868
|
+
const raw = await readFile(join(this.wfDir, 'current.json'), 'utf-8');
|
|
869
|
+
const state = JSON.parse(raw);
|
|
870
|
+
if (state.status !== 'done') {
|
|
871
|
+
return { dir: this.wfDir, state };
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
// fall through
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
482
881
|
/**
|
|
483
882
|
* 归档(成功完成时调用):重命名状态目录为 .done 后缀,保留所有中间产物。
|
|
484
883
|
* vars.json / progress.md / inputs.json / checkpoints/ 全部保留——供后续对话 Read 回溯。
|
|
@@ -498,7 +897,9 @@ export class WorkflowState {
|
|
|
498
897
|
// rename 失败时静默(某些文件系统/权限问题),保留原目录
|
|
499
898
|
}
|
|
500
899
|
}
|
|
501
|
-
/** 强制清理(/new 或新任务 init
|
|
900
|
+
/** 强制清理(/new 或新任务 init 时调用):删除当前任务实例目录(含 .done 归档)。
|
|
901
|
+
* 注意:.trash 回收目录不在这里删——它是产物清理前的归档安全网,
|
|
902
|
+
* 跨任务保留,由用户或后续淘汰策略管理。 */
|
|
502
903
|
async cleanup() {
|
|
503
904
|
await rm(this.dir, { recursive: true, force: true }).catch(() => { });
|
|
504
905
|
await rm(this.dir + DONE_SUFFIX, { recursive: true, force: true }).catch(() => { });
|