minimal-agent 0.11.0 → 0.12.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 (128) hide show
  1. package/package.json +4 -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/index.js +5 -3
  7. package/plugins/workflow-runner/src/index.ts +31 -13
  8. package/plugins/workflow-runner/src/multimodal.js +2 -1
  9. package/plugins/workflow-runner/src/multimodal.ts +2 -1
  10. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  11. package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
  12. package/plugins/workflow-runner/src/stepExecutors/tool.js +68 -57
  13. package/plugins/workflow-runner/src/stepExecutors/tool.ts +88 -66
  14. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  15. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  16. package/plugins/workflow-runner/src/types.ts +26 -7
  17. package/plugins/workflow-runner/test/context-files.test.ts +47 -16
  18. package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
  19. package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
  20. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  21. package/src/cli/print.js +19 -4
  22. package/src/cli/print.ts +17 -4
  23. package/src/cli/streamJson.js +19 -4
  24. package/src/cli/streamJson.ts +17 -4
  25. package/src/config/configDefaults.js +25 -0
  26. package/src/config/configDefaults.ts +33 -0
  27. package/src/config/configFile.js +2 -21
  28. package/src/config/configFile.ts +11 -24
  29. package/src/config/configManagement.js +1 -1
  30. package/src/config/configManagement.ts +2 -2
  31. package/src/context/compact.js +4 -0
  32. package/src/context/compact.ts +4 -0
  33. package/src/context/contextBundle.js +38 -13
  34. package/src/context/contextBundle.ts +47 -13
  35. package/src/harness/stallGuard.js +19 -4
  36. package/src/harness/stallGuard.ts +30 -7
  37. package/src/loop.js +95 -11
  38. package/src/loop.ts +125 -16
  39. package/src/main.js +8 -1
  40. package/src/main.tsx +7 -1
  41. package/src/memory/constants.js +10 -0
  42. package/src/memory/constants.ts +13 -0
  43. package/src/memory/episodicMemory.js +274 -2
  44. package/src/memory/episodicMemory.ts +306 -1
  45. package/src/memory/fileStore.js +24 -4
  46. package/src/memory/fileStore.ts +22 -2
  47. package/src/memory/memoryService.js +79 -12
  48. package/src/memory/memoryService.ts +108 -9
  49. package/src/memory/repository.js +415 -85
  50. package/src/memory/repository.ts +437 -86
  51. package/src/memory/turnJournal.js +95 -0
  52. package/src/memory/turnJournal.ts +120 -0
  53. package/src/memory/types.ts +16 -0
  54. package/src/plugins/pluginApi.ts +30 -6
  55. package/src/plugins/pluginRunner.js +6 -2
  56. package/src/plugins/pluginRunner.ts +12 -7
  57. package/src/prompts/policyRegistry.js +27 -0
  58. package/src/prompts/policyRegistry.ts +27 -0
  59. package/src/prompts/promptAssembly.js +39 -7
  60. package/src/prompts/promptAssembly.ts +145 -59
  61. package/src/session/runAgentTurn.js +43 -21
  62. package/src/session/runAgentTurn.ts +68 -26
  63. package/src/session/runTurn.js +11 -46
  64. package/src/session/runTurn.ts +12 -46
  65. package/src/session/runtimeStore.js +149 -0
  66. package/src/session/runtimeStore.ts +197 -0
  67. package/src/session/sessionState.js +5 -0
  68. package/src/session/sessionState.ts +14 -0
  69. package/src/tools/agent/agent.js +143 -14
  70. package/src/tools/agent/agent.ts +149 -14
  71. package/src/tools/agent/agentTaskService.js +60 -0
  72. package/src/tools/agent/agentTaskService.ts +113 -0
  73. package/src/tools/agent/agentTypes.js +5 -0
  74. package/src/tools/agent/agentTypes.ts +5 -0
  75. package/src/tools/bash/bash.js +31 -11
  76. package/src/tools/bash/bash.ts +29 -10
  77. package/src/tools/capabilities.js +5 -0
  78. package/src/tools/capabilities.ts +11 -1
  79. package/src/tools/edit/edit.js +7 -4
  80. package/src/tools/edit/edit.ts +7 -4
  81. package/src/tools/edit/multi-edit.js +4 -2
  82. package/src/tools/edit/multi-edit.ts +4 -2
  83. package/src/tools/glob/glob.js +156 -40
  84. package/src/tools/glob/glob.ts +185 -46
  85. package/src/tools/index.js +35 -17
  86. package/src/tools/index.ts +81 -20
  87. package/src/tools/lsp/lsp.js +165 -22
  88. package/src/tools/lsp/lsp.ts +206 -36
  89. package/src/tools/powershell/powershell.js +116 -26
  90. package/src/tools/powershell/powershell.ts +155 -30
  91. package/src/tools/presentresult/presentresult.js +10 -0
  92. package/src/tools/presentresult/presentresult.ts +10 -0
  93. package/src/tools/read/read.js +244 -29
  94. package/src/tools/read/read.ts +391 -59
  95. package/src/tools/shared/diffSummary.js +44 -0
  96. package/src/tools/shared/diffSummary.ts +63 -0
  97. package/src/tools/shared/fileActivity.js +2 -1
  98. package/src/tools/shared/fileActivity.ts +3 -0
  99. package/src/tools/shared/permanentDeleteGuard.js +26 -0
  100. package/src/tools/shared/permanentDeleteGuard.ts +34 -0
  101. package/src/tools/shared/toolOutputStore.js +97 -0
  102. package/src/tools/shared/toolOutputStore.ts +110 -0
  103. package/src/tools/skill/skill.js +86 -0
  104. package/src/tools/skill/skill.ts +94 -0
  105. package/src/tools/snip/snip.js +67 -0
  106. package/src/tools/snip/snip.ts +80 -0
  107. package/src/tools/tasks/backgroundTaskService.js +79 -28
  108. package/src/tools/tasks/backgroundTaskService.ts +165 -49
  109. package/src/tools/tasks/taskManagement.js +163 -0
  110. package/src/tools/tasks/taskManagement.ts +180 -0
  111. package/src/tools/tasks/taskOutput.js +64 -14
  112. package/src/tools/tasks/taskOutput.ts +102 -23
  113. package/src/tools/tasks/taskStop.js +48 -8
  114. package/src/tools/tasks/taskStop.ts +69 -9
  115. package/src/tools/tasks/taskStore.js +46 -0
  116. package/src/tools/tasks/taskStore.ts +77 -0
  117. package/src/tools/terminal/terminalCapture.js +167 -0
  118. package/src/tools/terminal/terminalCapture.ts +179 -0
  119. package/src/tools/trash/trash.js +233 -0
  120. package/src/tools/trash/trash.ts +283 -0
  121. package/src/tools/worktree/sessionWorktree.js +128 -0
  122. package/src/tools/worktree/sessionWorktree.ts +158 -0
  123. package/src/tools/write/write.js +6 -2
  124. package/src/tools/write/write.ts +6 -2
  125. package/src/types.ts +12 -0
  126. package/src/ui/hooks/useChat.js +31 -11
  127. package/src/ui/hooks/useChat.ts +31 -13
  128. package/src/web/protocol.ts +78 -3
@@ -25,6 +25,7 @@ import { existsSync } from 'node:fs';
25
25
  import { resolve } from 'node:path';
26
26
  import { executeTool, getToolByName, recordRead, } from '../../../../src/plugin-sdk.js';
27
27
  import { interpolateDeep } from '../expressions.js';
28
+ import { buildWorkflowToolContext, effectiveWorkflowCwd, } from '../toolRuntime.js';
28
29
  export async function* execToolStep(step, vars, ctx) {
29
30
  if (!step.tool)
30
31
  throw new Error(`step ${step.id}: 缺少 tool 字段`);
@@ -56,8 +57,8 @@ export async function* execToolStep(step, vars, ctx) {
56
57
  argsFriendly: friendlyToolDesc(tool.name, args),
57
58
  };
58
59
  // ════ 声明产物覆盖判定(解 Write/Edit 的 pre-read 守卫) ════
59
- // 内核写类工具有「文件已存在必须先 Read」守卫(fileState 进程级 Map,
60
- // 跨进程 / resume / 残留旧产物时必空)。确定性 tool 步的 args 是 yaml 作者
60
+ // 内核写类工具有「文件已存在必须先 Read」守卫(fileState 按会话隔离,
61
+ // 跨进程 / resume / 残留旧产物时可能为空)。确定性 tool 步的 args 是 yaml 作者
61
62
  // 写死的,模型没有机会先 Read —— 目标文件若是工作流**声明的产物**
62
63
  // (def 级 + step 级 artifacts:,= 工作流自己拥有、本就要覆盖的文件),
63
64
  // 这里自动 pre-Read 一次解除守卫(管线动作,不 yield 事件、结果丢弃)。
@@ -65,16 +66,60 @@ export async function* execToolStep(step, vars, ctx) {
65
66
  await preReadDeclaredArtifact(tool, args, step, ctx);
66
67
  // 宿主能力按 `uses:` 切片后转发。`executeTool` 的第四参此前从未传过 ——
67
68
  // 这正是「workflow 里 ComputerUse 恒失败」的末端表现。
68
- const hostSlice = hostContextFor(step, ctx);
69
- if ('error' in hostSlice)
70
- throw new Error(`step ${step.id}: ${hostSlice.error}`);
71
- const result = await executeTool(tool.name, argsJson, ctx.signal, hostSlice.ctx);
69
+ const pendingProgress = [];
70
+ let wakeProgress;
71
+ const toolContext = buildWorkflowToolContext(step, ctx, toolCallId, (message, detail) => {
72
+ pendingProgress.push({
73
+ type: 'tool_progress',
74
+ toolName: tool.name,
75
+ toolCallId,
76
+ message,
77
+ detail,
78
+ });
79
+ wakeProgress?.();
80
+ });
81
+ if ('error' in toolContext) {
82
+ throw new Error(`step ${step.id}: ${toolContext.error}`);
83
+ }
84
+ const dispatch = ctx.executeTool ?? executeTool;
85
+ let settled = false;
86
+ let result;
87
+ void dispatch(tool.name, argsJson, ctx.signal, toolContext)
88
+ .then((value) => {
89
+ result = value;
90
+ })
91
+ .catch((error) => {
92
+ result = {
93
+ ok: false,
94
+ error: error instanceof Error ? error.message : String(error),
95
+ };
96
+ })
97
+ .finally(() => {
98
+ settled = true;
99
+ wakeProgress?.();
100
+ });
101
+ while (!settled || pendingProgress.length > 0) {
102
+ const progress = pendingProgress.shift();
103
+ if (progress) {
104
+ yield progress;
105
+ continue;
106
+ }
107
+ await new Promise((resolveWake) => {
108
+ wakeProgress = resolveWake;
109
+ if (settled || pendingProgress.length > 0)
110
+ resolveWake();
111
+ });
112
+ wakeProgress = undefined;
113
+ }
114
+ if (!result)
115
+ throw new Error(`step ${step.id}: 工具执行未返回结果`);
72
116
  yield {
73
117
  type: 'tool_end',
74
118
  toolName: tool.name,
75
119
  toolCallId,
76
120
  ok: result.ok,
77
121
  content: result.ok ? result.content : result.error,
122
+ metadata: result.metadata,
78
123
  };
79
124
  if (!result.ok) {
80
125
  throw new Error(result.error);
@@ -101,56 +146,19 @@ export async function* execToolStep(step, vars, ctx) {
101
146
  .map((image) => (typeof image.source === 'string' ? image.source : ''))
102
147
  .filter((path) => path.length > 0);
103
148
  return {
104
- raw: { content, result: content, args, images },
149
+ raw: {
150
+ content,
151
+ result: content,
152
+ args,
153
+ images,
154
+ metadata: result.metadata,
155
+ recovery: result.recovery,
156
+ sensitivity: result.sensitivity,
157
+ visionNote: result.visionNote,
158
+ },
105
159
  preview: content.length > 200 ? `${content.slice(0, 200)}...` : content,
106
160
  };
107
161
  }
108
- /**
109
- * 按 `step.uses` 从宿主上下文里切出这一步该拿到的那片。
110
- *
111
- * 三条语义,缺一条这个机制就退化:
112
- * · 未声明 `uses` → 返回**空上下文**(不是 undefined,`toolGate` 这类与能力无关的
113
- * 宿主策略仍要生效)。工具因此走自己的清晰报错,不会静默降级;
114
- * · 声明了但宿主没提供 → **响亮报错**。yaml 说了要用电脑,而这次运行根本没开
115
- * `--computer-use`,那是配置错误,应该在这一步就停,不是跑到一半才发现;
116
- * · 只转发声明过的那一片 —— 声明 `[computer]` 的步拿不到 `browserRun`。
117
- *
118
- * 单一出处:切片逻辑只在这里,runner / 其它 executor 都不许自己拼。
119
- */
120
- function hostContextFor(step, ctx) {
121
- // toolGate 是宿主的通用策略缝(与具体能力无关),任何步都该受它约束。
122
- const base = ctx.host?.toolGate
123
- ? { toolGate: ctx.host.toolGate }
124
- : {};
125
- const uses = step.uses ?? [];
126
- if (uses.length === 0)
127
- return { ctx: base };
128
- const out = { ...base };
129
- for (const capability of uses) {
130
- if (capability === 'computer') {
131
- if (!ctx.host?.computerRun) {
132
- return {
133
- error: 'uses:[computer] 声明了需要电脑操控能力,但本次运行的宿主没有提供 —— '
134
- + '请用 `--computer-use interactive` 启动,或去掉该步的 uses 声明。',
135
- };
136
- }
137
- out.computerRun = ctx.host.computerRun;
138
- // 人工接管通道与能力同进退:没有它,遇到验证/密码只能硬失败。
139
- if (ctx.host.computerIntervention)
140
- out.computerIntervention = ctx.host.computerIntervention;
141
- }
142
- else if (capability === 'browser') {
143
- if (!ctx.host?.browserRun) {
144
- return {
145
- error: 'uses:[browser] 声明了需要浏览器授权上下文,但本次运行的宿主没有提供。'
146
- + '(基础浏览操作不声明 uses 也能用;upload/submit 这类才需要它。)',
147
- };
148
- }
149
- out.browserRun = ctx.host.browserRun;
150
- }
151
- }
152
- return { ctx: out };
153
- }
154
162
  /**
155
163
  * 写类 tool 步的目标文件若命中声明产物且已存在 → pre-Read 解除内核守卫。
156
164
  *
@@ -164,14 +172,17 @@ async function preReadDeclaredArtifact(tool, args, step, ctx) {
164
172
  if (typeof args.file_path !== 'string')
165
173
  return;
166
174
  const norm = (p) => {
167
- const abs = resolve(ctx.cwd, p);
175
+ const abs = resolve(effectiveWorkflowCwd(ctx), p);
168
176
  return process.platform === 'win32' ? abs.toLowerCase() : abs;
169
177
  };
170
178
  const target = norm(args.file_path);
171
- const declared = [...(ctx.declaredArtifacts ?? []), ...(step.artifacts ?? [])];
179
+ const declared = [
180
+ ...(ctx.declaredArtifacts ?? []),
181
+ ...(step.artifacts ?? []),
182
+ ];
172
183
  if (!declared.some((a) => norm(a) === target))
173
184
  return;
174
- const abs = resolve(ctx.cwd, args.file_path);
185
+ const abs = resolve(effectiveWorkflowCwd(ctx), args.file_path);
175
186
  if (!existsSync(abs))
176
187
  return; // 新建文件不需要解守卫
177
188
  // 直接登记一次「完整已读」状态解除内核 pre-read 守卫——不真正 Read 内容:
@@ -182,7 +193,7 @@ async function preReadDeclaredArtifact(tool, args, step, ctx) {
182
193
  // (首轮新建 PLAN.md 不存在直接放行,审批反馈后回写已存在的大 PLAN.md 被挡死)。
183
194
  // 两道守卫(never-read + truncated)必须一并解除,声明产物的整体覆盖才算真正授权。
184
195
  // recordRead 内部自带 statSync 失败兜底(返回 1、不抛),无需再包 try/catch。
185
- recordRead(abs, { truncated: false });
196
+ recordRead(abs, { truncated: false }, ctx.sessionState?.fileReads);
186
197
  }
187
198
  /**
188
199
  * 给 tool_start 生成人话描述(LiveArea spinner 行用)。
@@ -26,13 +26,17 @@ import { existsSync } from 'node:fs';
26
26
  import { resolve } from 'node:path';
27
27
 
28
28
  import {
29
+ type LoopEvent,
30
+ type ToolResult,
29
31
  executeTool,
30
32
  getToolByName,
31
33
  recordRead,
32
- type LoopEvent,
33
- type ToolCallContext,
34
34
  } from '../../../../src/plugin-sdk.js';
35
35
  import { interpolateDeep } from '../expressions.js';
36
+ import {
37
+ buildWorkflowToolContext,
38
+ effectiveWorkflowCwd,
39
+ } from '../toolRuntime.js';
36
40
  import type { RunContext, StepDef, StepResult, VarStack } from '../types.js';
37
41
 
38
42
  export async function* execToolStep(
@@ -74,19 +78,73 @@ export async function* execToolStep(
74
78
  };
75
79
 
76
80
  // ════ 声明产物覆盖判定(解 Write/Edit 的 pre-read 守卫) ════
77
- // 内核写类工具有「文件已存在必须先 Read」守卫(fileState 进程级 Map,
78
- // 跨进程 / resume / 残留旧产物时必空)。确定性 tool 步的 args 是 yaml 作者
81
+ // 内核写类工具有「文件已存在必须先 Read」守卫(fileState 按会话隔离,
82
+ // 跨进程 / resume / 残留旧产物时可能为空)。确定性 tool 步的 args 是 yaml 作者
79
83
  // 写死的,模型没有机会先 Read —— 目标文件若是工作流**声明的产物**
80
84
  // (def 级 + step 级 artifacts:,= 工作流自己拥有、本就要覆盖的文件),
81
85
  // 这里自动 pre-Read 一次解除守卫(管线动作,不 yield 事件、结果丢弃)。
82
86
  // 非声明产物的盲写保护原样生效;pre-Read 失败也不拦,让写工具自己报错。
83
- await preReadDeclaredArtifact(tool, args as Record<string, unknown>, step, ctx);
87
+ await preReadDeclaredArtifact(
88
+ tool,
89
+ args as Record<string, unknown>,
90
+ step,
91
+ ctx,
92
+ );
84
93
 
85
94
  // 宿主能力按 `uses:` 切片后转发。`executeTool` 的第四参此前从未传过 ——
86
95
  // 这正是「workflow 里 ComputerUse 恒失败」的末端表现。
87
- const hostSlice = hostContextFor(step, ctx);
88
- if ('error' in hostSlice) throw new Error(`step ${step.id}: ${hostSlice.error}`);
89
- const result = await executeTool(tool.name, argsJson, ctx.signal, hostSlice.ctx);
96
+ const pendingProgress: LoopEvent[] = [];
97
+ let wakeProgress: (() => void) | undefined;
98
+ const toolContext = buildWorkflowToolContext(
99
+ step,
100
+ ctx,
101
+ toolCallId,
102
+ (message, detail) => {
103
+ pendingProgress.push({
104
+ type: 'tool_progress',
105
+ toolName: tool.name,
106
+ toolCallId,
107
+ message,
108
+ detail,
109
+ });
110
+ wakeProgress?.();
111
+ },
112
+ );
113
+ if ('error' in toolContext) {
114
+ throw new Error(`step ${step.id}: ${toolContext.error}`);
115
+ }
116
+
117
+ const dispatch = ctx.executeTool ?? executeTool;
118
+ let settled = false;
119
+ let result: ToolResult | undefined;
120
+ void dispatch(tool.name, argsJson, ctx.signal, toolContext)
121
+ .then((value) => {
122
+ result = value;
123
+ })
124
+ .catch((error: unknown) => {
125
+ result = {
126
+ ok: false,
127
+ error: error instanceof Error ? error.message : String(error),
128
+ };
129
+ })
130
+ .finally(() => {
131
+ settled = true;
132
+ wakeProgress?.();
133
+ });
134
+
135
+ while (!settled || pendingProgress.length > 0) {
136
+ const progress = pendingProgress.shift();
137
+ if (progress) {
138
+ yield progress;
139
+ continue;
140
+ }
141
+ await new Promise<void>((resolveWake) => {
142
+ wakeProgress = resolveWake;
143
+ if (settled || pendingProgress.length > 0) resolveWake();
144
+ });
145
+ wakeProgress = undefined;
146
+ }
147
+ if (!result) throw new Error(`step ${step.id}: 工具执行未返回结果`);
90
148
 
91
149
  yield {
92
150
  type: 'tool_end',
@@ -94,6 +152,7 @@ export async function* execToolStep(
94
152
  toolCallId,
95
153
  ok: result.ok,
96
154
  content: result.ok ? result.content : result.error,
155
+ metadata: result.metadata,
97
156
  };
98
157
 
99
158
  if (!result.ok) {
@@ -105,9 +164,10 @@ export async function* execToolStep(
105
164
  // 后续 agent/llm/skill 步骤走 runQuery() 时,LLM 能在上下文中看到
106
165
  // 这步做了什么(与正常 TAOR 循环中 tool_call → tool_result 的记录方式一致)。
107
166
  // 只记录精简摘要(不把完整工具回执塞入 history,避免 token 浪费)。
108
- const summary = content.length > 500
109
- ? `${content.slice(0, 500)}…(已截断,共 ${content.length} 字符)`
110
- : content;
167
+ const summary =
168
+ content.length > 500
169
+ ? `${content.slice(0, 500)}…(已截断,共 ${content.length} 字符)`
170
+ : content;
111
171
  ctx.history.push({
112
172
  role: 'assistant',
113
173
  content: `[workflow:${step.id}] ✅ ${tool.name} 执行完成:${summary}`,
@@ -124,61 +184,20 @@ export async function* execToolStep(
124
184
  .filter((path) => path.length > 0);
125
185
 
126
186
  return {
127
- raw: { content, result: content, args, images },
187
+ raw: {
188
+ content,
189
+ result: content,
190
+ args,
191
+ images,
192
+ metadata: result.metadata,
193
+ recovery: result.recovery,
194
+ sensitivity: result.sensitivity,
195
+ visionNote: result.visionNote,
196
+ },
128
197
  preview: content.length > 200 ? `${content.slice(0, 200)}...` : content,
129
198
  };
130
199
  }
131
200
 
132
- /**
133
- * 按 `step.uses` 从宿主上下文里切出这一步该拿到的那片。
134
- *
135
- * 三条语义,缺一条这个机制就退化:
136
- * · 未声明 `uses` → 返回**空上下文**(不是 undefined,`toolGate` 这类与能力无关的
137
- * 宿主策略仍要生效)。工具因此走自己的清晰报错,不会静默降级;
138
- * · 声明了但宿主没提供 → **响亮报错**。yaml 说了要用电脑,而这次运行根本没开
139
- * `--computer-use`,那是配置错误,应该在这一步就停,不是跑到一半才发现;
140
- * · 只转发声明过的那一片 —— 声明 `[computer]` 的步拿不到 `browserRun`。
141
- *
142
- * 单一出处:切片逻辑只在这里,runner / 其它 executor 都不许自己拼。
143
- */
144
- function hostContextFor(
145
- step: StepDef,
146
- ctx: RunContext,
147
- ): { ctx: ToolCallContext } | { error: string } {
148
- // toolGate 是宿主的通用策略缝(与具体能力无关),任何步都该受它约束。
149
- const base: ToolCallContext = ctx.host?.toolGate
150
- ? { toolGate: ctx.host.toolGate }
151
- : {};
152
- const uses = step.uses ?? [];
153
- if (uses.length === 0) return { ctx: base };
154
-
155
- const out: ToolCallContext = { ...base };
156
- for (const capability of uses) {
157
- if (capability === 'computer') {
158
- if (!ctx.host?.computerRun) {
159
- return {
160
- error:
161
- 'uses:[computer] 声明了需要电脑操控能力,但本次运行的宿主没有提供 —— '
162
- + '请用 `--computer-use interactive` 启动,或去掉该步的 uses 声明。',
163
- };
164
- }
165
- out.computerRun = ctx.host.computerRun;
166
- // 人工接管通道与能力同进退:没有它,遇到验证/密码只能硬失败。
167
- if (ctx.host.computerIntervention) out.computerIntervention = ctx.host.computerIntervention;
168
- } else if (capability === 'browser') {
169
- if (!ctx.host?.browserRun) {
170
- return {
171
- error:
172
- 'uses:[browser] 声明了需要浏览器授权上下文,但本次运行的宿主没有提供。'
173
- + '(基础浏览操作不声明 uses 也能用;upload/submit 这类才需要它。)',
174
- };
175
- }
176
- out.browserRun = ctx.host.browserRun;
177
- }
178
- }
179
- return { ctx: out };
180
- }
181
-
182
201
  /**
183
202
  * 写类 tool 步的目标文件若命中声明产物且已存在 → pre-Read 解除内核守卫。
184
203
  *
@@ -196,14 +215,17 @@ async function preReadDeclaredArtifact(
196
215
  if (typeof args.file_path !== 'string') return;
197
216
 
198
217
  const norm = (p: string): string => {
199
- const abs = resolve(ctx.cwd, p);
218
+ const abs = resolve(effectiveWorkflowCwd(ctx), p);
200
219
  return process.platform === 'win32' ? abs.toLowerCase() : abs;
201
220
  };
202
221
  const target = norm(args.file_path);
203
- const declared = [...(ctx.declaredArtifacts ?? []), ...(step.artifacts ?? [])];
222
+ const declared = [
223
+ ...(ctx.declaredArtifacts ?? []),
224
+ ...(step.artifacts ?? []),
225
+ ];
204
226
  if (!declared.some((a) => norm(a) === target)) return;
205
227
 
206
- const abs = resolve(ctx.cwd, args.file_path);
228
+ const abs = resolve(effectiveWorkflowCwd(ctx), args.file_path);
207
229
  if (!existsSync(abs)) return; // 新建文件不需要解守卫
208
230
 
209
231
  // 直接登记一次「完整已读」状态解除内核 pre-read 守卫——不真正 Read 内容:
@@ -214,7 +236,7 @@ async function preReadDeclaredArtifact(
214
236
  // (首轮新建 PLAN.md 不存在直接放行,审批反馈后回写已存在的大 PLAN.md 被挡死)。
215
237
  // 两道守卫(never-read + truncated)必须一并解除,声明产物的整体覆盖才算真正授权。
216
238
  // recordRead 内部自带 statSync 失败兜底(返回 1、不抛),无需再包 try/catch。
217
- recordRead(abs, { truncated: false });
239
+ recordRead(abs, { truncated: false }, ctx.sessionState?.fileReads);
218
240
  }
219
241
 
220
242
  /**
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The workflow identity stays anchored to the registered workspace, while file
3
+ * and process tools follow a session worktree as soon as EnterWorktree creates
4
+ * one. Keeping this decision in one helper prevents executors from drifting.
5
+ */
6
+ export function effectiveWorkflowCwd(ctx) {
7
+ return ctx.sessionState?.activeWorktree?.path ?? ctx.cwd;
8
+ }
9
+ export function buildWorkflowToolContext(step, ctx, toolCallId, reportProgress) {
10
+ const context = {
11
+ ...ctx.toolContext,
12
+ history: ctx.history,
13
+ sessionState: ctx.sessionState,
14
+ provider: ctx.provider,
15
+ toolGate: ctx.host?.toolGate ?? ctx.toolContext?.toolGate,
16
+ promptCache: ctx.promptCache ?? ctx.toolContext?.promptCache,
17
+ workspaceId: ctx.runtimeContext?.workspaceId ?? ctx.toolContext?.workspaceId,
18
+ sessionId: ctx.runtimeContext?.sessionId ?? ctx.toolContext?.sessionId,
19
+ memoryAccess: ctx.runtimeContext?.memoryAccess ?? ctx.toolContext?.memoryAccess,
20
+ cwd: effectiveWorkflowCwd(ctx),
21
+ toolCallId,
22
+ reportProgress,
23
+ };
24
+ for (const capability of step.uses ?? []) {
25
+ if (capability === 'computer') {
26
+ if (!ctx.host?.computerRun) {
27
+ return {
28
+ error: 'uses:[computer] 声明了需要电脑操控能力,但本次运行的宿主没有提供 —— ' +
29
+ '请在支持 Computer Use 的宿主启动,或去掉该步的 uses 声明。',
30
+ };
31
+ }
32
+ context.computerRun = ctx.host.computerRun;
33
+ if (ctx.host.computerIntervention) {
34
+ context.computerIntervention = ctx.host.computerIntervention;
35
+ }
36
+ continue;
37
+ }
38
+ if (capability === 'browser') {
39
+ if (!ctx.host?.browserRun) {
40
+ return {
41
+ error: 'uses:[browser] 声明了需要浏览器授权上下文,但本次运行的宿主没有提供。',
42
+ };
43
+ }
44
+ context.browserRun = ctx.host.browserRun;
45
+ }
46
+ }
47
+ return context;
48
+ }
@@ -0,0 +1,64 @@
1
+ import type { ToolCallContext } from '../../../src/plugin-sdk.js';
2
+ import type { RunContext, StepDef } from './types.js';
3
+
4
+ /**
5
+ * The workflow identity stays anchored to the registered workspace, while file
6
+ * and process tools follow a session worktree as soon as EnterWorktree creates
7
+ * one. Keeping this decision in one helper prevents executors from drifting.
8
+ */
9
+ export function effectiveWorkflowCwd(ctx: RunContext): string {
10
+ return ctx.sessionState?.activeWorktree?.path ?? ctx.cwd;
11
+ }
12
+
13
+ export function buildWorkflowToolContext(
14
+ step: StepDef,
15
+ ctx: RunContext,
16
+ toolCallId: string,
17
+ reportProgress: NonNullable<ToolCallContext['reportProgress']>,
18
+ ): ToolCallContext | { error: string } {
19
+ const context: ToolCallContext = {
20
+ ...ctx.toolContext,
21
+ history: ctx.history,
22
+ sessionState: ctx.sessionState,
23
+ provider: ctx.provider,
24
+ toolGate: ctx.host?.toolGate ?? ctx.toolContext?.toolGate,
25
+ promptCache: ctx.promptCache ?? ctx.toolContext?.promptCache,
26
+ workspaceId:
27
+ ctx.runtimeContext?.workspaceId ?? ctx.toolContext?.workspaceId,
28
+ sessionId: ctx.runtimeContext?.sessionId ?? ctx.toolContext?.sessionId,
29
+ memoryAccess:
30
+ ctx.runtimeContext?.memoryAccess ?? ctx.toolContext?.memoryAccess,
31
+ cwd: effectiveWorkflowCwd(ctx),
32
+ toolCallId,
33
+ reportProgress,
34
+ };
35
+
36
+ for (const capability of step.uses ?? []) {
37
+ if (capability === 'computer') {
38
+ if (!ctx.host?.computerRun) {
39
+ return {
40
+ error:
41
+ 'uses:[computer] 声明了需要电脑操控能力,但本次运行的宿主没有提供 —— ' +
42
+ '请在支持 Computer Use 的宿主启动,或去掉该步的 uses 声明。',
43
+ };
44
+ }
45
+ context.computerRun = ctx.host.computerRun;
46
+ if (ctx.host.computerIntervention) {
47
+ context.computerIntervention = ctx.host.computerIntervention;
48
+ }
49
+ continue;
50
+ }
51
+
52
+ if (capability === 'browser') {
53
+ if (!ctx.host?.browserRun) {
54
+ return {
55
+ error:
56
+ 'uses:[browser] 声明了需要浏览器授权上下文,但本次运行的宿主没有提供。',
57
+ };
58
+ }
59
+ context.browserRun = ctx.host.browserRun;
60
+ }
61
+ }
62
+
63
+ return context;
64
+ }
@@ -16,9 +16,11 @@
16
16
  import type {
17
17
  ImageDetail,
18
18
  Message,
19
+ PluginContext,
19
20
  Provider,
20
21
  SessionState,
21
22
  Tool,
23
+ ToolCallContext,
22
24
  } from '../../../src/plugin-sdk.js';
23
25
 
24
26
  // ---------------- 1. Workflow / Step 定义 ----------------
@@ -237,7 +239,7 @@ export interface StepDef {
237
239
  /** ADR-05: 仅 llm 节点声明的上下文文件清单(skill 节点 loader 拒绝);runner 拼 path+hint 进 prompt 并开放 Read 子循环,不 inline 内容。 */
238
240
  context_files?: Array<{ path: string; hint?: string }>;
239
241
 
240
- /** ADR-05: agent 节点的工具白名单;未声明时默认全量 ALL_TOOLS。 */
242
+ /** ADR-05: agent 节点的工具白名单;未声明时继承宿主过滤后的有效工具集。 */
241
243
  allowed_tools?: string[];
242
244
 
243
245
  /**
@@ -437,10 +439,9 @@ export interface RunContext {
437
439
  cwd: string;
438
440
  /**
439
441
  * 可用工具列表。
440
- * v0.9 重构后:agent/llm/skill 步委托 runQuery(TAOR) 执行,
441
- * runQuery 内部使用自己的 ALL_TOOLS 全量工具列表。
442
- * 此字段保留给 runner 内部确定性步(tool 步)的 executeTool 调度使用,
443
- * 以及测试兼容。生产路径由 plugin.ts 注入 ALL_TOOLS。
442
+ * agent/llm/skill 与确定性 tool 步都以此列表为宿主授权上限。
443
+ * plugin.ts 先按 runtime 和父宿主白名单过滤,再把同一列表交给提示词、
444
+ * Schema 和真实执行,任何工作流节点都不能从 ALL_TOOLS 恢复隐藏能力。
444
445
  */
445
446
  tools: Tool[];
446
447
  transientContext?: string;
@@ -494,11 +495,29 @@ export interface RunContext {
494
495
  tools: import('../../../src/plugin-sdk.js').Tool[];
495
496
  signal?: AbortSignal;
496
497
  promptCache?: import('../../../src/plugin-sdk.js').PromptCacheContext;
497
- }) => AsyncGenerator<import('../../../src/plugin-sdk.js').LlmStreamEvent, void, undefined>;
498
+ }) => AsyncGenerator<
499
+ import('../../../src/plugin-sdk.js').LlmStreamEvent,
500
+ void,
501
+ undefined
502
+ >;
498
503
  /**
499
504
  * DI:executeTool 实现(仅测试用)。同上。
500
505
  */
501
- executeTool?: (name: string, rawArgsJson: string, signal?: AbortSignal) => Promise<import('../../../src/plugin-sdk.js').ToolResult>;
506
+ executeTool?: PluginContext['executeTool'];
507
+ /**
508
+ * Canonical non-model-visible context shared by deterministic tool nodes.
509
+ * Browser/computer capabilities are deliberately excluded here and are only
510
+ * added by the step-level `uses` slicer.
511
+ */
512
+ toolContext?: Omit<
513
+ ToolCallContext,
514
+ | 'browserRun'
515
+ | 'computerRun'
516
+ | 'computerIntervention'
517
+ | 'reportProgress'
518
+ | 'toolCallId'
519
+ | 'cwd'
520
+ >;
502
521
  /**
503
522
  * 会话状态(驱动 microCompact / reactiveCompact)。
504
523
  *