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.
Files changed (181) hide show
  1. package/package.json +5 -1
  2. package/plugins/workflow-runner/plugin.js +27 -15
  3. package/plugins/workflow-runner/plugin.ts +34 -20
  4. package/plugins/workflow-runner/src/agentCommon.js +13 -12
  5. package/plugins/workflow-runner/src/agentCommon.ts +22 -13
  6. package/plugins/workflow-runner/src/expressions.js +29 -8
  7. package/plugins/workflow-runner/src/expressions.ts +28 -10
  8. package/plugins/workflow-runner/src/index.js +50 -20
  9. package/plugins/workflow-runner/src/index.ts +75 -31
  10. package/plugins/workflow-runner/src/multimodal.js +2 -1
  11. package/plugins/workflow-runner/src/multimodal.ts +2 -1
  12. package/plugins/workflow-runner/src/recovery.js +9 -2
  13. package/plugins/workflow-runner/src/recovery.ts +9 -2
  14. package/plugins/workflow-runner/src/runner.js +31 -5
  15. package/plugins/workflow-runner/src/runner.ts +33 -5
  16. package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
  17. package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -1
  18. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  19. package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
  20. package/plugins/workflow-runner/src/stepExecutors/tool.js +91 -61
  21. package/plugins/workflow-runner/src/stepExecutors/tool.ts +111 -71
  22. package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
  23. package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
  24. package/plugins/workflow-runner/src/taskIdentity.js +197 -21
  25. package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
  26. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  27. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  28. package/plugins/workflow-runner/src/types.js +113 -7
  29. package/plugins/workflow-runner/src/types.ts +137 -14
  30. package/plugins/workflow-runner/src/workflowState.js +455 -54
  31. package/plugins/workflow-runner/src/workflowState.ts +508 -63
  32. package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
  33. package/plugins/workflow-runner/test/context-files.test.ts +47 -16
  34. package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
  35. package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
  36. package/plugins/workflow-runner/test/expressions.test.ts +42 -0
  37. package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
  38. package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
  39. package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
  40. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
  41. package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
  42. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
  43. package/plugins/workflow-runner/test/research.load.test.ts +328 -0
  44. package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
  45. package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
  46. package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
  47. package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
  48. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  49. package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
  50. package/plugins/workflow-runner/test/word.load.test.ts +9 -9
  51. package/skills/skill-creator/scripts/run_eval.py +3 -2
  52. package/src/cli/print.js +36 -4
  53. package/src/cli/print.ts +42 -4
  54. package/src/cli/streamJson.js +19 -4
  55. package/src/cli/streamJson.ts +17 -4
  56. package/src/config/configDefaults.js +25 -0
  57. package/src/config/configDefaults.ts +33 -0
  58. package/src/config/configFile.js +2 -21
  59. package/src/config/configFile.ts +11 -24
  60. package/src/config/configManagement.js +1 -1
  61. package/src/config/configManagement.ts +2 -2
  62. package/src/config/configService.js +24 -0
  63. package/src/config/configService.ts +23 -0
  64. package/src/context/compact.js +4 -0
  65. package/src/context/compact.ts +4 -0
  66. package/src/context/contextBundle.js +38 -13
  67. package/src/context/contextBundle.ts +47 -13
  68. package/src/harness/stallGuard.js +19 -4
  69. package/src/harness/stallGuard.ts +30 -7
  70. package/src/infra/verification.js +25 -18
  71. package/src/infra/verification.ts +50 -18
  72. package/src/loop.js +95 -11
  73. package/src/loop.ts +125 -16
  74. package/src/main.js +8 -1
  75. package/src/main.tsx +7 -1
  76. package/src/memory/constants.js +10 -0
  77. package/src/memory/constants.ts +13 -0
  78. package/src/memory/episodicMemory.js +274 -2
  79. package/src/memory/episodicMemory.ts +306 -1
  80. package/src/memory/fileStore.js +24 -4
  81. package/src/memory/fileStore.ts +22 -2
  82. package/src/memory/memoryService.js +79 -12
  83. package/src/memory/memoryService.ts +108 -9
  84. package/src/memory/repository.js +415 -85
  85. package/src/memory/repository.ts +437 -86
  86. package/src/memory/turnJournal.js +95 -0
  87. package/src/memory/turnJournal.ts +120 -0
  88. package/src/memory/types.ts +16 -0
  89. package/src/plugins/pluginApi.ts +30 -6
  90. package/src/plugins/pluginRunner.js +23 -8
  91. package/src/plugins/pluginRunner.ts +32 -13
  92. package/src/prompts/policyRegistry.js +55 -1
  93. package/src/prompts/policyRegistry.ts +55 -1
  94. package/src/prompts/promptAssembly.js +39 -7
  95. package/src/prompts/promptAssembly.ts +145 -59
  96. package/src/session/runAgentTurn.js +43 -21
  97. package/src/session/runAgentTurn.ts +68 -26
  98. package/src/session/runTurn.js +11 -46
  99. package/src/session/runTurn.ts +12 -46
  100. package/src/session/runtimeStore.js +149 -0
  101. package/src/session/runtimeStore.ts +197 -0
  102. package/src/session/sessionState.js +5 -0
  103. package/src/session/sessionState.ts +14 -0
  104. package/src/tools/advisor/advisor.js +76 -9
  105. package/src/tools/advisor/advisor.ts +79 -9
  106. package/src/tools/agent/agent.js +143 -14
  107. package/src/tools/agent/agent.ts +149 -14
  108. package/src/tools/agent/agentTaskService.js +60 -0
  109. package/src/tools/agent/agentTaskService.ts +113 -0
  110. package/src/tools/agent/agentTypes.js +5 -0
  111. package/src/tools/agent/agentTypes.ts +5 -0
  112. package/src/tools/bash/bash.js +80 -15
  113. package/src/tools/bash/bash.ts +83 -15
  114. package/src/tools/capabilities.js +5 -0
  115. package/src/tools/capabilities.ts +11 -1
  116. package/src/tools/edit/edit.js +7 -4
  117. package/src/tools/edit/edit.ts +7 -4
  118. package/src/tools/edit/multi-edit.js +4 -2
  119. package/src/tools/edit/multi-edit.ts +4 -2
  120. package/src/tools/glob/glob.js +156 -40
  121. package/src/tools/glob/glob.ts +185 -46
  122. package/src/tools/index.js +35 -17
  123. package/src/tools/index.ts +81 -20
  124. package/src/tools/lsp/lsp.js +165 -22
  125. package/src/tools/lsp/lsp.ts +206 -36
  126. package/src/tools/powershell/powershell.js +134 -27
  127. package/src/tools/powershell/powershell.ts +171 -30
  128. package/src/tools/presentresult/presentresult.js +10 -0
  129. package/src/tools/presentresult/presentresult.ts +10 -0
  130. package/src/tools/read/read.js +249 -29
  131. package/src/tools/read/read.ts +396 -59
  132. package/src/tools/shared/diffSummary.js +44 -0
  133. package/src/tools/shared/diffSummary.ts +63 -0
  134. package/src/tools/shared/fileActivity.js +2 -1
  135. package/src/tools/shared/fileActivity.ts +3 -0
  136. package/src/tools/shared/permanentDeleteGuard.js +57 -0
  137. package/src/tools/shared/permanentDeleteGuard.ts +71 -0
  138. package/src/tools/shared/toolOutputStore.js +97 -0
  139. package/src/tools/shared/toolOutputStore.ts +110 -0
  140. package/src/tools/skill/skill.js +86 -0
  141. package/src/tools/skill/skill.ts +94 -0
  142. package/src/tools/snip/snip.js +67 -0
  143. package/src/tools/snip/snip.ts +80 -0
  144. package/src/tools/tasks/backgroundTaskService.js +79 -28
  145. package/src/tools/tasks/backgroundTaskService.ts +165 -49
  146. package/src/tools/tasks/taskManagement.js +163 -0
  147. package/src/tools/tasks/taskManagement.ts +180 -0
  148. package/src/tools/tasks/taskOutput.js +64 -14
  149. package/src/tools/tasks/taskOutput.ts +102 -23
  150. package/src/tools/tasks/taskStop.js +48 -8
  151. package/src/tools/tasks/taskStop.ts +69 -9
  152. package/src/tools/tasks/taskStore.js +46 -0
  153. package/src/tools/tasks/taskStore.ts +77 -0
  154. package/src/tools/terminal/terminalCapture.js +167 -0
  155. package/src/tools/terminal/terminalCapture.ts +179 -0
  156. package/src/tools/trash/trash.js +233 -0
  157. package/src/tools/trash/trash.ts +283 -0
  158. package/src/tools/webbrowser/browserControl.js +5 -1
  159. package/src/tools/webbrowser/browserControl.ts +7 -1
  160. package/src/tools/webbrowser/webbrowser.js +8 -4
  161. package/src/tools/webbrowser/webbrowser.ts +8 -4
  162. package/src/tools/worktree/sessionWorktree.js +128 -0
  163. package/src/tools/worktree/sessionWorktree.ts +158 -0
  164. package/src/tools/write/write.js +6 -2
  165. package/src/tools/write/write.ts +6 -2
  166. package/src/types.ts +22 -4
  167. package/src/ui/hooks/useChat.js +31 -11
  168. package/src/ui/hooks/useChat.ts +31 -13
  169. package/src/web/protocol.ts +78 -3
  170. package/workflows/adaptive-research.yaml +5 -5
  171. package/workflows/book-review-short.yaml +5 -5
  172. package/workflows/e2e-write-greet.yaml +2 -2
  173. package/workflows/illustrate.yaml +12 -12
  174. package/workflows/plan.yaml +18 -18
  175. package/workflows/ppt.yaml +201 -201
  176. package/workflows/ralph-loop.yaml +3 -1
  177. package/workflows/research.md +449 -0
  178. package/workflows/research.yaml +873 -0
  179. package/workflows/webppt.yaml +47 -47
  180. package/workflows/word.yaml +80 -80
  181. package/workflows/youtube-shorts.yaml +25 -25
@@ -11,16 +11,17 @@
11
11
  * ============================================================
12
12
  */
13
13
 
14
- import { findWorkflowByName, listWorkflows } from './loader.js';
15
- import { runWorkflow } from './runner.js';
16
- import type { InputDef, RunContext, WorkflowEvent } from './types.js';
17
14
  import {
18
- buildBaseSystemPrompt,
19
15
  type LoopEvent,
20
16
  type Message,
17
+ type PluginContext,
21
18
  type Provider,
22
19
  type Tool,
20
+ buildBaseSystemPrompt,
23
21
  } from '../../../src/plugin-sdk.js';
22
+ import { findWorkflowByName, listWorkflows } from './loader.js';
23
+ import { runWorkflow } from './runner.js';
24
+ import type { InputDef, RunContext, WorkflowEvent } from './types.js';
24
25
 
25
26
  export interface RunWorkflowFromCommandOptions {
26
27
  provider: Provider;
@@ -31,11 +32,13 @@ export interface RunWorkflowFromCommandOptions {
31
32
  sessionState?: import('../../../src/plugin-sdk.js').SessionState;
32
33
  /** 当前工作目录(默认 process.cwd()) */
33
34
  cwd?: string;
34
- /** 可用工具列表(默认空数组,由 runner 注入 ALL_TOOLS) */
35
+ /** 宿主过滤后的可用工具列表;缺省为空,绝不在 runner 内恢复全量工具。 */
35
36
  tools?: Tool[];
36
37
  transientContext?: string;
37
38
  transientBundle?: import('../../../src/plugin-sdk.js').ContextBundle;
38
39
  promptCache?: import('../../../src/plugin-sdk.js').PromptCacheContext;
40
+ executeTool?: PluginContext['executeTool'];
41
+ toolContext?: RunContext['toolContext'];
39
42
  /**
40
43
  * 宿主授权上下文(浏览器 / 电脑操控)。
41
44
  *
@@ -88,29 +91,43 @@ export function parseWorkflowArgs(rawArgs: string): {
88
91
  return { name, inputs, positional };
89
92
  }
90
93
 
91
- /** 简单 token 化:双引号包裹的整体当一个 token;其它按空白切。 */
94
+ /**
95
+ * shell 风格 token 化:空白切分 token;引号(' 或 ")在 token **任意位置**(不只开头)
96
+ * 出现即进入引号模式——引号内的空白不切分、引号字符本身被剥离。
97
+ *
98
+ * 修复:`--input topic="带空格的值"`(引号在 `=` 后,token 开头是 `t`)曾被按空白
99
+ * 硬切 → 值截断 + 引号残留(实测 inputs.topic 带 `"` 尾,positional 里散落被拆的字)。
100
+ * 现在 token 内任意位置的引号都正确分组。
101
+ *
102
+ * 边界语义(被 parseWorkflowArgs.test.ts 冻结):
103
+ * - 未闭合引号:剩余部分并入当前 token(不报错)
104
+ * - 双引号内的单引号是字面量(不触发嵌套)
105
+ * - 反斜杠不转义(`a\"b` 中 `"` 仍开启引号模式,行为变化但测试用 toBeDefined 容错)
106
+ * - 连续空白合并、`""` 产生空 token
107
+ */
92
108
  function tokenizeArgs(s: string): string[] {
93
109
  const out: string[] = [];
94
110
  let i = 0;
95
111
  while (i < s.length) {
96
112
  while (i < s.length && /\s/.test(s[i])) i++;
97
113
  if (i >= s.length) break;
98
- if (s[i] === '"' || s[i] === "'") {
99
- const q = s[i];
100
- i++;
101
- let acc = '';
102
- while (i < s.length && s[i] !== q) {
103
- acc += s[i++];
104
- }
105
- if (i < s.length) i++;
106
- out.push(acc);
107
- } else {
108
- let acc = '';
109
- while (i < s.length && !/\s/.test(s[i])) {
110
- acc += s[i++];
114
+ let acc = '';
115
+ while (i < s.length) {
116
+ const c = s[i];
117
+ if (c === '"' || c === "'") {
118
+ // 引号模式:收集到闭合引号,引号内空白保留
119
+ const q = c;
120
+ i++;
121
+ while (i < s.length && s[i] !== q) acc += s[i++];
122
+ if (i < s.length) i++; // 跳过闭合引号
123
+ } else if (/\s/.test(c)) {
124
+ break;
125
+ } else {
126
+ acc += c;
127
+ i++;
111
128
  }
112
- out.push(acc);
113
129
  }
130
+ out.push(acc);
114
131
  }
115
132
  return out;
116
133
  }
@@ -138,8 +155,9 @@ function coerceInputs(
138
155
  continue;
139
156
  }
140
157
  if (def.type === 'number') {
141
- const n = parseFloat(rawVal);
142
- if (Number.isNaN(n)) throw new Error(`input ${def.name} 必须是数字,收到 "${rawVal}"`);
158
+ const n = Number.parseFloat(rawVal);
159
+ if (Number.isNaN(n))
160
+ throw new Error(`input ${def.name} 必须是数字,收到 "${rawVal}"`);
143
161
  out[def.name] = n;
144
162
  } else if (def.type === 'enum') {
145
163
  if (!def.values?.includes(rawVal)) {
@@ -167,7 +185,9 @@ async function refreshWorkflowSystemPrompt(
167
185
  // 只刷稳定层:workflow 一跑就是几十步,system 消息若含日期/记忆等易变内容,
168
186
  // 每步都会重写 history[0]、把 provider 前缀缓存打散(见 prompts/system.ts)。
169
187
  const content = await buildBaseSystemPrompt(cwd, tools);
170
- const systemIndex = history.findIndex((m) => m.role === 'system' && !m._ephemeral);
188
+ const systemIndex = history.findIndex(
189
+ (m) => m.role === 'system' && !m._ephemeral,
190
+ );
171
191
  const fresh: Message = { role: 'system', content };
172
192
 
173
193
  if (systemIndex === 0) {
@@ -213,13 +233,18 @@ export async function* runWorkflowFromCommand(
213
233
  // - 默认:严格按"第 i 个位置参数 ↔ def.inputs[i]"对应。
214
234
  // --input k=v 显式优先:已显式的 input 不被位置参数覆盖(对应位置参数被丢弃,会 warn)。
215
235
  const overriddenByKV: string[] = [];
236
+ const greedyNotes: string[] = [];
216
237
  const greedyFirst = def.inputs?.[0]?.greedy === true;
217
238
  if (greedyFirst && def.inputs && positional.length > 0) {
218
239
  const greedyName = def.inputs[0].name;
219
240
  if (greedyName in rawInputs) {
220
- // 首参已被 --input 占位 → 整串位置参数被忽略
221
- overriddenByKV.push(
222
- `${greedyName}=${JSON.stringify(rawInputs[greedyName])} (位置参数 ${JSON.stringify(positional.join(' '))} 被忽略)`,
241
+ // 首参已由 --input 提供时,位置参数仍按贪婪语义【拼回】首参末尾--
242
+ // 含空格的值未加引号时会被 shell 拆开(--input topic=A B C 只有 A 进 KV,
243
+ // B C 散落成位置参数),静默丢弃曾把整段中文调研主题截成半句、
244
+ // 连锁触发任务仲裁误判 new-task 清空全部产物。
245
+ rawInputs[greedyName] = [rawInputs[greedyName], ...positional].join(' ');
246
+ greedyNotes.push(
247
+ `${greedyName}:--input 值未加引号被拆成 ${positional.length} 段位置参数,已自动拼回(下次请写 --input "${greedyName}=完整值")`,
223
248
  );
224
249
  } else {
225
250
  // 贪婪吸收:整句进首参(空格 join 还原用户原话,不逐位拆映射)
@@ -250,6 +275,13 @@ export async function* runWorkflowFromCommand(
250
275
  message: `⚠ 忽略了 ${dropped.length} 个多余位置参数: ${dropped.map((d) => JSON.stringify(d)).join(' ')}(workflow "${name}" 只声明了 ${def.inputs.length} 个 input)`,
251
276
  };
252
277
  }
278
+ // warn:贪婪首参的 --input 值被 shell 拆开后自动拼回
279
+ if (greedyNotes.length > 0) {
280
+ yield {
281
+ type: 'warning',
282
+ message: `⚠ ${greedyNotes.join(';')}`,
283
+ };
284
+ }
253
285
  // warn:位置参数被 --input 覆盖
254
286
  if (overriddenByKV.length > 0) {
255
287
  yield {
@@ -278,6 +310,8 @@ export async function* runWorkflowFromCommand(
278
310
  transientContext: opts.transientContext,
279
311
  transientBundle: opts.transientBundle,
280
312
  promptCache: opts.promptCache,
313
+ executeTool: opts.executeTool,
314
+ toolContext: opts.toolContext,
281
315
  };
282
316
 
283
317
  // 透传 runWorkflow 的事件,并观察是否出现终结事件(error / interrupted = 失败或中断,
@@ -295,7 +329,11 @@ export async function* runWorkflowFromCommand(
295
329
  }
296
330
 
297
331
  /** /workflows —— 列出可用 workflow。yield 一条 text 事件让 UI 显示。 */
298
- export async function* runWorkflowsList(): AsyncGenerator<LoopEvent, void, void> {
332
+ export async function* runWorkflowsList(): AsyncGenerator<
333
+ LoopEvent,
334
+ void,
335
+ void
336
+ > {
299
337
  const list = await listWorkflows();
300
338
  if (list.length === 0) {
301
339
  yield {
@@ -308,13 +346,19 @@ export async function* runWorkflowsList(): AsyncGenerator<LoopEvent, void, void>
308
346
  }
309
347
  const lines = ['可用工作流:'];
310
348
  for (const w of list) {
311
- lines.push(` /workflow ${w.name} — ${w.description.split('\n')[0].trim()}`);
349
+ lines.push(
350
+ ` /workflow ${w.name} — ${w.description.split('\n')[0].trim()}`,
351
+ );
312
352
  }
313
353
  lines.push('');
314
354
  lines.push('运行示例:');
315
- lines.push(' /workflow <name> <val> 位置参数(按 inputs 声明顺序,第一个 input 直接写值)');
355
+ lines.push(
356
+ ' /workflow <name> <val> 位置参数(按 inputs 声明顺序,第一个 input 直接写值)',
357
+ );
316
358
  lines.push(' /workflow <name>.yaml <val> .yaml 后缀可省');
317
- lines.push(' /workflow <name> --input k=v 显式 KV(多 input 或想跳过中间 input 时用)');
318
- yield { type: 'text', delta: lines.join('\n') + '\n' };
359
+ lines.push(
360
+ ' /workflow <name> --input k=v 显式 KV(多 input 或想跳过中间 input 时用)',
361
+ );
362
+ yield { type: 'text', delta: `${lines.join('\n')}\n` };
319
363
  yield { type: 'turn_done' };
320
364
  }
@@ -1,5 +1,6 @@
1
1
  import { isAbsolute, resolve } from 'node:path';
2
2
  import { interpolate } from './expressions.js';
3
+ import { effectiveWorkflowCwd } from './toolRuntime.js';
3
4
  export function resolveWorkflowMultimodalOptions(step, vars, ctx, onMissing) {
4
5
  const imageDetail = step.image_detail;
5
6
  const imageMaxLongSidePixel = step.image_max_long_side_pixel;
@@ -33,5 +34,5 @@ function resolveWorkflowImageSource(source, ctx) {
33
34
  isAbsolute(clean)) {
34
35
  return clean;
35
36
  }
36
- return resolve(ctx.cwd, clean);
37
+ return resolve(effectiveWorkflowCwd(ctx), clean);
37
38
  }
@@ -2,6 +2,7 @@ import { isAbsolute, resolve } from 'node:path';
2
2
 
3
3
  import type { ImageDetail, ImageInputSpec } from '../../../src/plugin-sdk.js';
4
4
  import { interpolate } from './expressions.js';
5
+ import { effectiveWorkflowCwd } from './toolRuntime.js';
5
6
  import type { RunContext, StepDef, VarStack } from './types.js';
6
7
 
7
8
  export interface WorkflowMultimodalOptions {
@@ -59,5 +60,5 @@ function resolveWorkflowImageSource(source: string, ctx: RunContext): string {
59
60
  ) {
60
61
  return clean;
61
62
  }
62
- return resolve(ctx.cwd, clean);
63
+ return resolve(effectiveWorkflowCwd(ctx), clean);
63
64
  }
@@ -81,8 +81,15 @@ export async function* runRecovery(step, vars, ctx, errorMsg) {
81
81
  : recoverPrompt + schemaHint;
82
82
  try {
83
83
  // 委托 TAOR 引擎执行(共享消费层 runStepQuery)——事件透传给 UI(显示恢复进度)。
84
- // 注意剥掉 step 的 allowed_tools 白名单:原步收敛工具是为弱模型聚焦,
85
- // 恢复 agent 的职责正是「随机应变换路径」,要全工具箱(保持 v0.9 行为)。
84
+ // 注意剥掉 step 的 allowed_tools 白名单:【allowed_tools 本身不是安全边界,是弱模型聚焦优化】
85
+ // (原步限定一小撮工具给能力弱的模型,避免它瞎试跑遍所有工具)。
86
+ // 恢复 agent 的职责正是「随机应变换路径」,需要它能调用原步没考虑过的替代工具,
87
+ // 所以这里解除聚焦限制。真正的安全边界在**另外两层**,此处没有动:
88
+ // 1. `StepDef.uses: [browser|computer]` —— 宿主能力切片(buildWorkflowToolContext 仍生效),
89
+ // 原步没声明 computer 的话,恢复 agent 也调不通 ComputerUse。
90
+ // 2. `ctx.tools` —— 插件层按 runtime + 宿主白名单裁剪后的有效工具集合,
91
+ // 无论 allowed_tools 写什么都不能超出它。
92
+ // 保持 v0.9 行为(放开 allowed_tools),但明确安全层次不发生变化。
86
93
  const { rawText, sawLlmError, sawMaxTurns, llmErrorDetail } = yield* runStepQuery(fullPrompt, { ...step, allowed_tools: undefined }, ctx, { maxTurns: step.maxTurns ?? 25 });
87
94
  // 恢复是兜底路径:无论 provider 失败还是模型空答,都按「没救回」返回
88
95
  // ok:false(不抛重试——retry 在进入 recover 前已用尽);只是归因更诚实。
@@ -104,8 +104,15 @@ export async function* runRecovery(
104
104
 
105
105
  try {
106
106
  // 委托 TAOR 引擎执行(共享消费层 runStepQuery)——事件透传给 UI(显示恢复进度)。
107
- // 注意剥掉 step 的 allowed_tools 白名单:原步收敛工具是为弱模型聚焦,
108
- // 恢复 agent 的职责正是「随机应变换路径」,要全工具箱(保持 v0.9 行为)。
107
+ // 注意剥掉 step 的 allowed_tools 白名单:【allowed_tools 本身不是安全边界,是弱模型聚焦优化】
108
+ // (原步限定一小撮工具给能力弱的模型,避免它瞎试跑遍所有工具)。
109
+ // 恢复 agent 的职责正是「随机应变换路径」,需要它能调用原步没考虑过的替代工具,
110
+ // 所以这里解除聚焦限制。真正的安全边界在**另外两层**,此处没有动:
111
+ // 1. `StepDef.uses: [browser|computer]` —— 宿主能力切片(buildWorkflowToolContext 仍生效),
112
+ // 原步没声明 computer 的话,恢复 agent 也调不通 ComputerUse。
113
+ // 2. `ctx.tools` —— 插件层按 runtime + 宿主白名单裁剪后的有效工具集合,
114
+ // 无论 allowed_tools 写什么都不能超出它。
115
+ // 保持 v0.9 行为(放开 allowed_tools),但明确安全层次不发生变化。
109
116
  const { rawText, sawLlmError, sawMaxTurns, llmErrorDetail } =
110
117
  yield* runStepQuery(
111
118
  fullPrompt,
@@ -25,7 +25,8 @@ import { existsSync } from 'node:fs';
25
25
  import { resolve } from 'node:path';
26
26
  import { dlog, getWorkingDir } from '../../../src/plugin-sdk.js';
27
27
  import { evalExpr, interpolate } from './expressions.js';
28
- import { buildTaskArbitrator, isContinuationPhrase } from './taskIdentity.js';
28
+ import { effectiveWorkflowCwd } from './toolRuntime.js';
29
+ import { buildTaskArbitrator, DEFAULT_SESSION_ID, generateDefaultSessionId, isContinuationPhrase, sanitizeWorkflowDirName } from './taskIdentity.js';
29
30
  import { execAgentStep } from './stepExecutors/agent.js';
30
31
  import { execAssertStep } from './stepExecutors/assert.js';
31
32
  import { execVerifyStep } from './stepExecutors/verify.js';
@@ -153,7 +154,7 @@ async function* dispatchActionStep(step, vars, ctx) {
153
154
  if (step.type === 'assert')
154
155
  return execAssertStep(step, vars, ctx);
155
156
  if (step.type === 'verify')
156
- return await execVerifyStep(step, vars);
157
+ return await execVerifyStep(step, vars, ctx);
157
158
  // set / echo 从 runSteps 的内联块搬进执行器体系:它们不影响迭代、只产出 StepResult。
158
159
  // 搬过来的直接收益是 `capture:` 自然生效(此前它们从不调 bindCapture —— 静默失效)。
159
160
  if (step.type === 'set')
@@ -340,6 +341,20 @@ function collectDeclaredArtifacts(def) {
340
341
  export async function* runWorkflow(def, inputs, ctx) {
341
342
  const vars = new VarStack();
342
343
  vars.setGlobal('inputs', inputs);
344
+ // 让 fileExists / readJson(done_when、when、over 都靠它们)与写入侧锚同一个目录。
345
+ // 传函数而非当前值:worktree 是运行途中才可能进入的,锚点必须每次求值时重算。
346
+ vars.resolveCwd = () => effectiveWorkflowCwd(ctx);
347
+ // Phase 3:session 即任务实例。优先用宿主显式提供的 sessionId(TUI 当前会话 /
348
+ // -p 的 --session);缺失或为缺省 'default' 时,由工作流引擎自动命名一个唯一
349
+ // session(generateDefaultSessionId,见 taskIdentity.ts)——保证每次工作流运行都
350
+ // 有独立目录,多 session 并行互不覆盖;宿主显式指定时仍可复用/续跑该 session。
351
+ // 注入全局变量 `session`(sanitize 后的目录安全名),供 yaml 产物路径用
352
+ // ${session} 占位符(如 out/research/${session}/01-goal.md)。
353
+ const explicitSessionId = ctx.runtimeContext?.sessionId;
354
+ const sessionId = explicitSessionId && explicitSessionId !== DEFAULT_SESSION_ID
355
+ ? explicitSessionId
356
+ : generateDefaultSessionId();
357
+ vars.setGlobal('session', sanitizeWorkflowDirName(sessionId));
343
358
  // onError:'recover' 用:把工作流总目标(description)透传给恢复 agent,
344
359
  // 让它知道「这条工作流到底想干什么」。已有 goal 则不覆盖。
345
360
  const runCtx = { ...ctx, goal: ctx.goal ?? def.description };
@@ -353,13 +368,17 @@ export async function* runWorkflow(def, inputs, ctx) {
353
368
  runCtx.maxTurnsRetryDelayMs = def.self_heal?.max_turns_retry_delay_ms;
354
369
  // 声明产物清单:state.init 用于 new-task 清理/completed 报产物;
355
370
  // 挂到 runCtx 供 tool 步覆盖判定(声明产物自动 pre-Read 解 Write 守卫)。
356
- const declaredArtifacts = collectDeclaredArtifacts(def);
371
+ // Phase 3:产物路径可能含 ${session} 占位符,收集后按 vars 展开为实际路径
372
+ //(new-task 清理 / completed 报告都以展开后的真实路径为准)。
373
+ const declaredArtifacts = collectDeclaredArtifacts(def)
374
+ .map((a) => interpolate(a, vars))
375
+ .filter((a) => a.length > 0);
357
376
  runCtx.declaredArtifacts = declaredArtifacts;
358
377
  // 任务身份语义仲裁器(hash 不等 + 旧任务未完成时的二审,见 taskIdentity.ts):
359
378
  // 短语快路径 → LLM 单轮仲裁 → AskUserQuestion → 兜底 new-task。
360
379
  // init 非 generator,仲裁过程的人话记录收进 notes,init 返回后统一 yield。
361
380
  const arbitrationNotes = [];
362
- const state = new WorkflowState(getWorkingDir(), def.name);
381
+ const state = new WorkflowState(getWorkingDir(), def.name, sessionId);
363
382
  const resume = await state.init(def.name, inputs, def.steps, def.self_heal?.backoff, declaredArtifacts, buildTaskArbitrator({
364
383
  provider: runCtx.provider,
365
384
  chat: runCtx.chat,
@@ -396,9 +415,16 @@ export async function* runWorkflow(def, inputs, ctx) {
396
415
  message: '♻ 工作流定义已变更(步骤结构与旧断点不再匹配),旧执行现场已作废,本次从头开始执行。',
397
416
  };
398
417
  }
399
- // fresh/new-task 启动清理可见化:全新任务开始前已删掉的残留产物(上次任务
418
+ // fresh/new-task 启动清理可见化:全新任务开始前清理的残留产物(上次任务
400
419
  // 的声明产物 + /new 后无主孤儿)。用户约定:全新 plan 一律从干净状态开始;
401
420
  // 删除范围仅限本工作流声明的产物,同任务 resume 不清理。
421
+ // 清理一律先归档到 .trash 回收目录再删残留:仲裁误判 new-task 时成果可找回。
422
+ if (resume.archivedTo) {
423
+ yield {
424
+ type: 'warning',
425
+ message: `全新任务:上次残留的旧产物已归档到 ${resume.archivedTo}/(确认不需要后可删除该目录),从干净状态开始。`,
426
+ };
427
+ }
402
428
  if (resume.cleanedArtifacts?.length) {
403
429
  yield {
404
430
  type: 'warning',
@@ -28,7 +28,8 @@ import { dlog, executeTool, getWorkingDir, type LoopEvent } from '../../../src/p
28
28
  import { isMaxTurnsCutoff, truncate } from './agentCommon.js';
29
29
  import { evalExpr, interpolate, interpolateDeep, interpolateTyped } from './expressions.js';
30
30
  import { runRecovery } from './recovery.js';
31
- import { buildTaskArbitrator, isContinuationPhrase } from './taskIdentity.js';
31
+ import { effectiveWorkflowCwd } from './toolRuntime.js';
32
+ import { buildTaskArbitrator, DEFAULT_SESSION_ID, generateDefaultSessionId, isContinuationPhrase, sanitizeWorkflowDirName } from './taskIdentity.js';
32
33
  import { execAgentStep } from './stepExecutors/agent.js';
33
34
  import { execAssertStep } from './stepExecutors/assert.js';
34
35
  import { execVerifyStep } from './stepExecutors/verify.js';
@@ -206,7 +207,7 @@ async function* dispatchActionStep(
206
207
  ctx: RunContext,
207
208
  ): AsyncGenerator<LoopEvent, StepResult, void> {
208
209
  if (step.type === 'assert') return execAssertStep(step, vars, ctx);
209
- if (step.type === 'verify') return await execVerifyStep(step, vars);
210
+ if (step.type === 'verify') return await execVerifyStep(step, vars, ctx);
210
211
  // set / echo 从 runSteps 的内联块搬进执行器体系:它们不影响迭代、只产出 StepResult。
211
212
  // 搬过来的直接收益是 `capture:` 自然生效(此前它们从不调 bindCapture —— 静默失效)。
212
213
  if (step.type === 'set') return yield* execSetStep(step, vars);
@@ -395,6 +396,22 @@ export async function* runWorkflow(
395
396
  ): AsyncGenerator<WorkflowEvent | LoopEvent, void, void> {
396
397
  const vars = new VarStack();
397
398
  vars.setGlobal('inputs', inputs);
399
+ // 让 fileExists / readJson(done_when、when、over 都靠它们)与写入侧锚同一个目录。
400
+ // 传函数而非当前值:worktree 是运行途中才可能进入的,锚点必须每次求值时重算。
401
+ vars.resolveCwd = () => effectiveWorkflowCwd(ctx);
402
+
403
+ // Phase 3:session 即任务实例。优先用宿主显式提供的 sessionId(TUI 当前会话 /
404
+ // -p 的 --session);缺失或为缺省 'default' 时,由工作流引擎自动命名一个唯一
405
+ // session(generateDefaultSessionId,见 taskIdentity.ts)——保证每次工作流运行都
406
+ // 有独立目录,多 session 并行互不覆盖;宿主显式指定时仍可复用/续跑该 session。
407
+ // 注入全局变量 `session`(sanitize 后的目录安全名),供 yaml 产物路径用
408
+ // ${session} 占位符(如 out/research/${session}/01-goal.md)。
409
+ const explicitSessionId = ctx.runtimeContext?.sessionId;
410
+ const sessionId =
411
+ explicitSessionId && explicitSessionId !== DEFAULT_SESSION_ID
412
+ ? explicitSessionId
413
+ : generateDefaultSessionId();
414
+ vars.setGlobal('session', sanitizeWorkflowDirName(sessionId));
398
415
 
399
416
  // onError:'recover' 用:把工作流总目标(description)透传给恢复 agent,
400
417
  // 让它知道「这条工作流到底想干什么」。已有 goal 则不覆盖。
@@ -412,14 +429,18 @@ export async function* runWorkflow(
412
429
 
413
430
  // 声明产物清单:state.init 用于 new-task 清理/completed 报产物;
414
431
  // 挂到 runCtx 供 tool 步覆盖判定(声明产物自动 pre-Read 解 Write 守卫)。
415
- const declaredArtifacts = collectDeclaredArtifacts(def);
432
+ // Phase 3:产物路径可能含 ${session} 占位符,收集后按 vars 展开为实际路径
433
+ //(new-task 清理 / completed 报告都以展开后的真实路径为准)。
434
+ const declaredArtifacts = collectDeclaredArtifacts(def)
435
+ .map((a) => interpolate(a, vars))
436
+ .filter((a) => a.length > 0);
416
437
  runCtx.declaredArtifacts = declaredArtifacts;
417
438
 
418
439
  // 任务身份语义仲裁器(hash 不等 + 旧任务未完成时的二审,见 taskIdentity.ts):
419
440
  // 短语快路径 → LLM 单轮仲裁 → AskUserQuestion → 兜底 new-task。
420
441
  // init 非 generator,仲裁过程的人话记录收进 notes,init 返回后统一 yield。
421
442
  const arbitrationNotes: string[] = [];
422
- const state = new WorkflowState(getWorkingDir(), def.name);
443
+ const state = new WorkflowState(getWorkingDir(), def.name, sessionId);
423
444
  const resume = await state.init(
424
445
  def.name,
425
446
  inputs,
@@ -469,9 +490,16 @@ export async function* runWorkflow(
469
490
  };
470
491
  }
471
492
 
472
- // fresh/new-task 启动清理可见化:全新任务开始前已删掉的残留产物(上次任务
493
+ // fresh/new-task 启动清理可见化:全新任务开始前清理的残留产物(上次任务
473
494
  // 的声明产物 + /new 后无主孤儿)。用户约定:全新 plan 一律从干净状态开始;
474
495
  // 删除范围仅限本工作流声明的产物,同任务 resume 不清理。
496
+ // 清理一律先归档到 .trash 回收目录再删残留:仲裁误判 new-task 时成果可找回。
497
+ if (resume.archivedTo) {
498
+ yield {
499
+ type: 'warning',
500
+ message: `全新任务:上次残留的旧产物已归档到 ${resume.archivedTo}/(确认不需要后可删除该目录),从干净状态开始。`,
501
+ };
502
+ }
475
503
  if (resume.cleanedArtifacts?.length) {
476
504
  yield {
477
505
  type: 'warning',
@@ -8,6 +8,7 @@
8
8
  * 让后续 LLM 步骤能看到验证通过的信息(如"文件已存在""变量值符合预期"等)。
9
9
  * ============================================================
10
10
  */
11
+ import { randomUUID } from 'node:crypto';
11
12
  import { evalExpr, interpolate } from '../expressions.js';
12
13
  export function execAssertStep(step, vars, ctx) {
13
14
  if (typeof step.condition !== 'string') {
@@ -34,7 +35,7 @@ export function execAssertStep(step, vars, ctx) {
34
35
  role: 'assistant',
35
36
  content: `[workflow:${step.id}] ✅ 断言通过:${step.condition}`,
36
37
  timestamp: Date.now(),
37
- id: crypto.randomUUID(),
38
+ id: randomUUID(),
38
39
  });
39
40
  }
40
41
  return { raw: { ok: true }, preview: 'assert passed' };
@@ -9,6 +9,8 @@
9
9
  * ============================================================
10
10
  */
11
11
 
12
+ import { randomUUID } from 'node:crypto';
13
+
12
14
  import { evalExpr, interpolate } from '../expressions.js';
13
15
  import type { RunContext, StepDef, StepResult, VarStack } from '../types.js';
14
16
 
@@ -42,7 +44,7 @@ export function execAssertStep(
42
44
  role: 'assistant',
43
45
  content: `[workflow:${step.id}] ✅ 断言通过:${step.condition}`,
44
46
  timestamp: Date.now(),
45
- id: crypto.randomUUID(),
47
+ id: randomUUID(),
46
48
  });
47
49
  }
48
50
 
@@ -15,11 +15,12 @@
15
15
  * capture:{ text: var_name } / { result: var_name }
16
16
  * ============================================================
17
17
  */
18
- import { readdir, readFile } from 'node:fs/promises';
18
+ import { readFile, readdir } from 'node:fs/promises';
19
19
  import { join } from 'node:path';
20
- import { getResourceSearchPaths } from '../../../../src/plugin-sdk.js';
20
+ import { getResourceSearchPaths, } from '../../../../src/plugin-sdk.js';
21
21
  import { describeLlmFailure, describeMaxTurnsCutoff, effectiveGoalAnchor, runStepQuery, } from '../agentCommon.js';
22
22
  import { interpolate } from '../expressions.js';
23
+ import { effectiveWorkflowCwd } from '../toolRuntime.js';
23
24
  function stripFrontmatter(content) {
24
25
  return content.replace(/^---\n[\s\S]*?\n---\n?/, '');
25
26
  }
@@ -53,9 +54,7 @@ async function findSkillFile(skillName, cwd) {
53
54
  if (parseFrontmatterName(md) === skillName)
54
55
  return skillFile;
55
56
  }
56
- catch {
57
- continue;
58
- }
57
+ catch { }
59
58
  }
60
59
  }
61
60
  return null;
@@ -64,7 +63,7 @@ export async function* execSkillStep(step, vars, ctx) {
64
63
  if (!step.skill)
65
64
  throw new Error(`step ${step.id}: 缺少 skill 字段`);
66
65
  const skillName = interpolate(step.skill, vars);
67
- const skillFile = await findSkillFile(skillName, ctx.cwd);
66
+ const skillFile = await findSkillFile(skillName, effectiveWorkflowCwd(ctx));
68
67
  if (!skillFile) {
69
68
  throw new Error(`step ${step.id}: 未找到 skill "${skillName}"(cwd/skills/ 与 packageRoot/skills/ 双源均未命中)`);
70
69
  }
@@ -16,10 +16,13 @@
16
16
  * ============================================================
17
17
  */
18
18
 
19
- import { readdir, readFile } from 'node:fs/promises';
19
+ import { readFile, readdir } from 'node:fs/promises';
20
20
  import { join } from 'node:path';
21
21
 
22
- import { getResourceSearchPaths, type LoopEvent } from '../../../../src/plugin-sdk.js';
22
+ import {
23
+ type LoopEvent,
24
+ getResourceSearchPaths,
25
+ } from '../../../../src/plugin-sdk.js';
23
26
  import {
24
27
  describeLlmFailure,
25
28
  describeMaxTurnsCutoff,
@@ -27,6 +30,7 @@ import {
27
30
  runStepQuery,
28
31
  } from '../agentCommon.js';
29
32
  import { interpolate } from '../expressions.js';
33
+ import { effectiveWorkflowCwd } from '../toolRuntime.js';
30
34
  import type { RunContext, StepDef, StepResult, VarStack } from '../types.js';
31
35
 
32
36
  function stripFrontmatter(content: string): string {
@@ -41,7 +45,10 @@ function parseFrontmatterName(content: string): string | null {
41
45
  return nm[1].trim().replace(/^["']|["']$/g, '');
42
46
  }
43
47
 
44
- async function findSkillFile(skillName: string, cwd: string): Promise<string | null> {
48
+ async function findSkillFile(
49
+ skillName: string,
50
+ cwd: string,
51
+ ): Promise<string | null> {
45
52
  const roots = getResourceSearchPaths('skills', import.meta.url, cwd);
46
53
 
47
54
  for (const root of roots) {
@@ -58,9 +65,7 @@ async function findSkillFile(skillName: string, cwd: string): Promise<string | n
58
65
  try {
59
66
  const md = await readFile(skillFile, 'utf8');
60
67
  if (parseFrontmatterName(md) === skillName) return skillFile;
61
- } catch {
62
- continue;
63
- }
68
+ } catch {}
64
69
  }
65
70
  }
66
71
  return null;
@@ -74,7 +79,7 @@ export async function* execSkillStep(
74
79
  if (!step.skill) throw new Error(`step ${step.id}: 缺少 skill 字段`);
75
80
  const skillName = interpolate(step.skill, vars);
76
81
 
77
- const skillFile = await findSkillFile(skillName, ctx.cwd);
82
+ const skillFile = await findSkillFile(skillName, effectiveWorkflowCwd(ctx));
78
83
  if (!skillFile) {
79
84
  throw new Error(
80
85
  `step ${step.id}: 未找到 skill "${skillName}"(cwd/skills/ 与 packageRoot/skills/ 双源均未命中)`,