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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minimal-agent",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Bun-first 的本地 Agent 内核,支持工具、插件、工作流、上下文压缩、长期记忆与 OpenAI 兼容接口;Tauri 桌面宿主和 Node.js npm 发布可复用同一内核",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Bill Wang <leiwang0359@gmail.com>",
@@ -131,9 +131,12 @@
131
131
  "prepublishOnly": "bun run clean && bun run build"
132
132
  },
133
133
  "dependencies": {
134
+ "ccxt": "^4.5.58",
135
+ "diff": "^8.0.2",
134
136
  "fast-glob": "^3.3.2",
135
137
  "ink": "^5.0.1",
136
138
  "marked": "^18.0.6",
139
+ "pdfjs-dist": "^6.2.108",
137
140
  "react": "^18.3.1",
138
141
  "turndown": "^7.2.4",
139
142
  "vscode-jsonrpc": "^8.2.1",
@@ -148,6 +151,7 @@
148
151
  "typescript": "^5.5.0"
149
152
  },
150
153
  "optionalDependencies": {
154
+ "@napi-rs/canvas": "^0.1.100",
151
155
  "playwright-core": "^1.40.0",
152
156
  "pngjs": "^7.0.0",
153
157
  "pptxgenjs": "^4.0.1"
@@ -12,7 +12,7 @@
12
12
  * 缓存这次 import,所以 cold start 后 dynamic import 只跑一次。
13
13
  * ============================================================
14
14
  */
15
- import { ALL_TOOLS, derivePromptCacheContext, getWorkingDir, createAgentRuntimeContext, filterToolsForRuntime, buildTransientPromptBundle, } from '../../src/plugin-sdk.js';
15
+ import { ALL_TOOLS, buildTransientPromptBundle, createAgentRuntimeContext, derivePromptCacheContext, filterToolsForRuntime, getWorkingDir, } from '../../src/plugin-sdk.js';
16
16
  import { runWorkflowFromCommand, runWorkflowsList } from './src/index.js';
17
17
  const api = {
18
18
  async *runCommand(commandName, args, ctx) {
@@ -43,21 +43,22 @@ const api = {
43
43
  ? ALL_TOOLS.filter((tool) => hostToolNames.has(tool.name))
44
44
  : ALL_TOOLS;
45
45
  const workflowTools = filterToolsForRuntime(hostTools, workflowRuntime);
46
- const transientBundle = ctx.transientBundle ?? await buildTransientPromptBundle({
47
- cwd,
48
- tools: workflowTools,
49
- profile: 'main',
50
- includeProjectInstructions: true,
51
- includePlugins: true,
52
- runtime: {
53
- surface: 'workflow',
46
+ const transientBundle = ctx.transientBundle ??
47
+ (await buildTransientPromptBundle({
54
48
  cwd,
55
- workspaceId: workflowRuntime.workspaceId,
56
- interactiveAsk: workflowRuntime.interactiveAsk,
57
- memoryAccess: workflowRuntime.memoryAccess,
58
- currentUserInput: args,
59
- },
60
- }).catch(() => undefined);
49
+ tools: workflowTools,
50
+ profile: 'main',
51
+ includeProjectInstructions: true,
52
+ includePlugins: true,
53
+ runtime: {
54
+ surface: 'workflow',
55
+ cwd,
56
+ workspaceId: workflowRuntime.workspaceId,
57
+ interactiveAsk: workflowRuntime.interactiveAsk,
58
+ memoryAccess: workflowRuntime.memoryAccess,
59
+ currentUserInput: args,
60
+ },
61
+ }).catch(() => undefined));
61
62
  yield* runWorkflowFromCommand(args, {
62
63
  provider: ctx.provider,
63
64
  history: ctx.history,
@@ -66,6 +67,17 @@ const api = {
66
67
  cwd,
67
68
  tools: workflowTools,
68
69
  runtimeContext: workflowRuntime,
70
+ executeTool: ctx.executeTool,
71
+ toolContext: {
72
+ history: ctx.history,
73
+ sessionState: ctx.sessionState,
74
+ provider: ctx.provider,
75
+ toolGate: ctx.host?.toolGate,
76
+ promptCache: ctx.promptCache,
77
+ workspaceId: workflowRuntime.workspaceId,
78
+ sessionId: workflowRuntime.sessionId,
79
+ memoryAccess: workflowRuntime.memoryAccess,
80
+ },
69
81
  // 宿主授权上下文原样带上;引擎按各步 `uses:` 切片后才转给工具,不整份下发。
70
82
  host: ctx.host,
71
83
  transientContext: ctx.transientContext,
@@ -16,11 +16,11 @@
16
16
  import type { PluginApi } from '../../src/plugin-sdk.js';
17
17
  import {
18
18
  ALL_TOOLS,
19
- derivePromptCacheContext,
20
- getWorkingDir,
19
+ buildTransientPromptBundle,
21
20
  createAgentRuntimeContext,
21
+ derivePromptCacheContext,
22
22
  filterToolsForRuntime,
23
- buildTransientPromptBundle,
23
+ getWorkingDir,
24
24
  } from '../../src/plugin-sdk.js';
25
25
  import { runWorkflowFromCommand, runWorkflowsList } from './src/index.js';
26
26
 
@@ -43,9 +43,10 @@ const api: PluginApi = {
43
43
  cwd,
44
44
  surface: 'workflow',
45
45
  owner: ctx.runtimeContext?.owner ?? 'untrusted',
46
- memoryAccess: ctx.runtimeContext?.memoryAccess === 'denied'
47
- ? 'denied'
48
- : 'read-only',
46
+ memoryAccess:
47
+ ctx.runtimeContext?.memoryAccess === 'denied'
48
+ ? 'denied'
49
+ : 'read-only',
49
50
  });
50
51
  const hostToolNames = ctx.allowedTools
51
52
  ? new Set(ctx.allowedTools)
@@ -54,21 +55,23 @@ const api: PluginApi = {
54
55
  ? ALL_TOOLS.filter((tool) => hostToolNames.has(tool.name))
55
56
  : ALL_TOOLS;
56
57
  const workflowTools = filterToolsForRuntime(hostTools, workflowRuntime);
57
- const transientBundle = ctx.transientBundle ?? await buildTransientPromptBundle({
58
- cwd,
59
- tools: workflowTools,
60
- profile: 'main',
61
- includeProjectInstructions: true,
62
- includePlugins: true,
63
- runtime: {
64
- surface: 'workflow',
58
+ const transientBundle =
59
+ ctx.transientBundle ??
60
+ (await buildTransientPromptBundle({
65
61
  cwd,
66
- workspaceId: workflowRuntime.workspaceId,
67
- interactiveAsk: workflowRuntime.interactiveAsk,
68
- memoryAccess: workflowRuntime.memoryAccess,
69
- currentUserInput: args,
70
- },
71
- }).catch(() => undefined);
62
+ tools: workflowTools,
63
+ profile: 'main',
64
+ includeProjectInstructions: true,
65
+ includePlugins: true,
66
+ runtime: {
67
+ surface: 'workflow',
68
+ cwd,
69
+ workspaceId: workflowRuntime.workspaceId,
70
+ interactiveAsk: workflowRuntime.interactiveAsk,
71
+ memoryAccess: workflowRuntime.memoryAccess,
72
+ currentUserInput: args,
73
+ },
74
+ }).catch(() => undefined));
72
75
  yield* runWorkflowFromCommand(args, {
73
76
  provider: ctx.provider,
74
77
  history: ctx.history,
@@ -77,6 +80,17 @@ const api: PluginApi = {
77
80
  cwd,
78
81
  tools: workflowTools,
79
82
  runtimeContext: workflowRuntime,
83
+ executeTool: ctx.executeTool,
84
+ toolContext: {
85
+ history: ctx.history,
86
+ sessionState: ctx.sessionState,
87
+ provider: ctx.provider,
88
+ toolGate: ctx.host?.toolGate,
89
+ promptCache: ctx.promptCache,
90
+ workspaceId: workflowRuntime.workspaceId,
91
+ sessionId: workflowRuntime.sessionId,
92
+ memoryAccess: workflowRuntime.memoryAccess,
93
+ },
80
94
  // 宿主授权上下文原样带上;引擎按各步 `uses:` 切片后才转给工具,不整份下发。
81
95
  host: ctx.host,
82
96
  transientContext: ctx.transientContext,
@@ -19,7 +19,8 @@
19
19
  * ③ 检测逻辑增加内容长度启发式(reason > 80 字符视为误报)
20
20
  * ============================================================
21
21
  */
22
- import { runQuery, createAgentRuntimeContext, stripThinkBlocks, } from '../../../src/plugin-sdk.js';
22
+ import { createAgentRuntimeContext, runQuery, stripThinkBlocks, } from '../../../src/plugin-sdk.js';
23
+ import { effectiveWorkflowCwd } from './toolRuntime.js';
23
24
  import { normalizeCapture, } from './types.js';
24
25
  /**
25
26
  * agent 显式放弃的唯一标记;recover 与 agent 节点共用。
@@ -59,7 +60,9 @@ export function effectiveGoalAnchor(ctx) {
59
60
  if (supps.length === 0)
60
61
  return base;
61
62
  const lines = supps.map((s) => `> 📌 用户补充要求:${s}`).join('\n');
62
- return base ? `${base}\n${lines}` : `${lines}\n> ⚠️ 以上补充要求与原始目标同等约束所有操作。`;
63
+ return base
64
+ ? `${base}\n${lines}`
65
+ : `${lines}\n> ⚠️ 以上补充要求与原始目标同等约束所有操作。`;
63
66
  }
64
67
  /** 把 llm_error 细节拼进可重试失败的抛错文案(无细节时退化为原样文案)。 */
65
68
  export function describeLlmFailure(stepId, detail) {
@@ -96,14 +99,10 @@ function createStepHistory(step, ctx) {
96
99
  export async function* runStepQuery(prompt, step, ctx, opts) {
97
100
  const activeHistory = createStepHistory(step, ctx);
98
101
  const availableTools = ctx.tools ?? [];
99
- const hostToolNames = availableTools.length > 0
100
- ? new Set(availableTools.map((tool) => tool.name))
101
- : null;
102
- const effectiveStepTools = hostToolNames === null
103
- ? step.allowed_tools
104
- : step.allowed_tools === undefined
105
- ? [...hostToolNames]
106
- : step.allowed_tools.filter((name) => hostToolNames.has(name));
102
+ const hostToolNames = new Set(availableTools.map((tool) => tool.name));
103
+ const effectiveStepTools = step.allowed_tools === undefined
104
+ ? [...hostToolNames]
105
+ : step.allowed_tools.filter((name) => hostToolNames.has(name));
107
106
  // 记录 runQuery 开始前的 history 长度,用于之后提取本步的 assistant 输出
108
107
  const historyLenBefore = activeHistory.length;
109
108
  const runtimeContext = createAgentRuntimeContext({
@@ -112,7 +111,7 @@ export async function* runStepQuery(prompt, step, ctx, opts) {
112
111
  profile: 'main',
113
112
  interactiveAsk: false,
114
113
  }),
115
- cwd: ctx.cwd,
114
+ cwd: effectiveWorkflowCwd(ctx),
116
115
  owner: ctx.runtimeContext?.owner ?? 'untrusted',
117
116
  memoryAccess: ctx.runtimeContext?.memoryAccess ?? 'denied',
118
117
  });
@@ -283,7 +282,9 @@ function isLikelyFalsePositive(reason) {
283
282
  if (reason.length > 80)
284
283
  return true;
285
284
  // 如果 reason 看起来像 markdown 正文(标题/章节),也是误报
286
- if (/^#{1,6}\s/m.test(reason) || /^[-*+]\s/m.test(reason) || /^\d+\.\s/m.test(reason)) {
285
+ if (/^#{1,6}\s/m.test(reason) ||
286
+ /^[-*+]\s/m.test(reason) ||
287
+ /^\d+\.\s/m.test(reason)) {
287
288
  return true;
288
289
  }
289
290
  return false;
@@ -25,10 +25,11 @@ import {
25
25
  type ImageInputSpec,
26
26
  type LoopEvent,
27
27
  type Message,
28
- runQuery,
29
28
  createAgentRuntimeContext,
29
+ runQuery,
30
30
  stripThinkBlocks,
31
31
  } from '../../../src/plugin-sdk.js';
32
+ import { effectiveWorkflowCwd } from './toolRuntime.js';
32
33
  import {
33
34
  type RunContext,
34
35
  type StepDef,
@@ -81,7 +82,9 @@ export function effectiveGoalAnchor(ctx: RunContext): string {
81
82
  const supps = (ctx.goalSupplements ?? []).filter((s) => s.trim());
82
83
  if (supps.length === 0) return base;
83
84
  const lines = supps.map((s) => `> 📌 用户补充要求:${s}`).join('\n');
84
- return base ? `${base}\n${lines}` : `${lines}\n> ⚠️ 以上补充要求与原始目标同等约束所有操作。`;
85
+ return base
86
+ ? `${base}\n${lines}`
87
+ : `${lines}\n> ⚠️ 以上补充要求与原始目标同等约束所有操作。`;
85
88
  }
86
89
 
87
90
  /** runStepQuery 的归纳结果:本步产出文本 + 终结性错误旗标(语义判断留给各执行器)。 */
@@ -164,12 +167,9 @@ export async function* runStepQuery(
164
167
  ): AsyncGenerator<LoopEvent, StepQueryOutcome, void> {
165
168
  const activeHistory = createStepHistory(step, ctx);
166
169
  const availableTools = ctx.tools ?? [];
167
- const hostToolNames = availableTools.length > 0
168
- ? new Set(availableTools.map((tool) => tool.name))
169
- : null;
170
- const effectiveStepTools = hostToolNames === null
171
- ? step.allowed_tools
172
- : step.allowed_tools === undefined
170
+ const hostToolNames = new Set(availableTools.map((tool) => tool.name));
171
+ const effectiveStepTools =
172
+ step.allowed_tools === undefined
173
173
  ? [...hostToolNames]
174
174
  : step.allowed_tools.filter((name) => hostToolNames.has(name));
175
175
  // 记录 runQuery 开始前的 history 长度,用于之后提取本步的 assistant 输出
@@ -180,7 +180,7 @@ export async function* runStepQuery(
180
180
  profile: 'main' as const,
181
181
  interactiveAsk: false,
182
182
  }),
183
- cwd: ctx.cwd,
183
+ cwd: effectiveWorkflowCwd(ctx),
184
184
  owner: ctx.runtimeContext?.owner ?? 'untrusted',
185
185
  memoryAccess: ctx.runtimeContext?.memoryAccess ?? 'denied',
186
186
  });
@@ -200,7 +200,8 @@ export async function* runStepQuery(
200
200
  signal: ctx.signal,
201
201
  maxTurns: opts.maxTurns,
202
202
  allowedTools: effectiveStepTools,
203
- requiredTools: step.allowed_tools === undefined ? undefined : effectiveStepTools,
203
+ requiredTools:
204
+ step.allowed_tools === undefined ? undefined : effectiveStepTools,
204
205
  chat: ctx.chat,
205
206
  executeTool: ctx.executeTool,
206
207
  sessionState: ctx.sessionState,
@@ -296,7 +297,10 @@ export async function* runStepQuery(
296
297
  export const MAX_TURNS_CUTOFF_MARKER = '达到最大轮数';
297
298
 
298
299
  /** maxTurns 截断的可重试失败文案(重试 = 在共享 history 上续作,再给一份轮数预算)。 */
299
- export function describeMaxTurnsCutoff(stepId: string, maxTurns: number): string {
300
+ export function describeMaxTurnsCutoff(
301
+ stepId: string,
302
+ maxTurns: number,
303
+ ): string {
300
304
  return `step ${stepId}: ${MAX_TURNS_CUTOFF_MARKER} ${maxTurns} 被截断(可续跑)`;
301
305
  }
302
306
 
@@ -351,7 +355,11 @@ export function parseGiveup(text: string): string | null {
351
355
  function isLikelyFalsePositive(reason: string): boolean {
352
356
  if (reason.length > 80) return true;
353
357
  // 如果 reason 看起来像 markdown 正文(标题/章节),也是误报
354
- if (/^#{1,6}\s/m.test(reason) || /^[-*+]\s/m.test(reason) || /^\d+\.\s/m.test(reason)) {
358
+ if (
359
+ /^#{1,6}\s/m.test(reason) ||
360
+ /^[-*+]\s/m.test(reason) ||
361
+ /^\d+\.\s/m.test(reason)
362
+ ) {
355
363
  return true;
356
364
  }
357
365
  return false;
@@ -405,7 +413,8 @@ export function interpretMiniReActResult(
405
413
  ok: true,
406
414
  value: {
407
415
  raw,
408
- preview: cleanText.length > 200 ? `${cleanText.slice(0, 200)}…` : cleanText,
416
+ preview:
417
+ cleanText.length > 200 ? `${cleanText.slice(0, 200)}…` : cleanText,
409
418
  },
410
419
  };
411
420
  }
@@ -40,6 +40,7 @@
40
40
  import { existsSync, readFileSync } from 'node:fs';
41
41
  import { join, isAbsolute } from 'node:path';
42
42
  import { getWorkingDir } from '../../../src/plugin-sdk.js';
43
+ import { DANGEROUS_KEYS, isAbsWithinCwd } from './types.js';
43
44
  const PUNCT2 = new Set(['==', '!=', '>=', '<=', '&&', '||']);
44
45
  const PUNCT1 = new Set(['(', ')', '[', ']', ',', '.', '!', '>', '<', '+', '?', ':']);
45
46
  function tokenize(src) {
@@ -143,12 +144,25 @@ function isPunct(c, v) {
143
144
  const t = peek(c);
144
145
  return !!t && t.kind === 'punct' && t.value === v;
145
146
  }
147
+ /**
148
+ * 读磁盘的内置函数(fileExists / readJson)的锚点。
149
+ * 必须和写入侧同源:进了 session worktree 后产物落在 worktree 里,
150
+ * 这里若仍锚原 workspace,done_when 会在空目录里找文件而恒判 false。
151
+ */
152
+ function cwdOf(vars) {
153
+ return vars.resolveCwd?.() ?? getWorkingDir();
154
+ }
146
155
  const BUILTINS = {
147
- fileExists(args) {
156
+ fileExists(args, cwd) {
148
157
  const p = String(args[0] ?? '');
149
158
  if (!p)
150
159
  return false;
151
- const abs = isAbsolute(p) ? p : join(getWorkingDir(), p);
160
+ const abs = isAbsolute(p) ? p : join(cwd, p);
161
+ // 安全守卫:越出 cwd 的路径视为"不存在"(fail-closed,与 readJson 同策略)。
162
+ // 不可信 YAML(模板库/他人分享)可能带 fileExists('/etc/passwd') 做信息刺探,
163
+ // 这里与 cleanupDeclaredArtifacts 保持一致:只允许操作 cwd 下的文件。
164
+ if (!isAbsWithinCwd(cwd, abs))
165
+ return false;
152
166
  return existsSync(abs);
153
167
  },
154
168
  length(args) {
@@ -247,16 +261,19 @@ const BUILTINS = {
247
261
  return 'null';
248
262
  }
249
263
  },
250
- // 读磁盘 JSON 文件并取值:path 相对 getWorkingDir()(绝对路径原样);给了 key
251
- // 则返回顶层 obj[key],否则返回整值。缺文件 / 坏 JSON / 键不存在 → null
264
+ // 读磁盘 JSON 文件并取值:path 相对当前有效 cwd(绝对路径先经 cwd 安全校验,
265
+ // 越界直接 null,与 fileExists 同 fail-closed 策略);给了 key 则返回顶层
266
+ // obj[key],否则返回整值。缺文件 / 坏 JSON / 键不存在 / 越界 → null
252
267
  //(与 match/extract 同「不抛」哲学)。动机:让确定性脚本产出的 manifest
253
268
  //(如 source_assets.json)直接驱动工作流 for-each / branch —— 填补「磁盘
254
269
  // 结构化数据 → 控制流」的空白(与 fileExists 平级的基础能力)。
255
- readJson(args) {
270
+ readJson(args, cwd) {
256
271
  const p = String(args[0] ?? '');
257
272
  if (!p)
258
273
  return null;
259
- const abs = isAbsolute(p) ? p : join(getWorkingDir(), p);
274
+ const abs = isAbsolute(p) ? p : join(cwd, p);
275
+ if (!isAbsWithinCwd(cwd, abs))
276
+ return null; // 越界 → null(不抛,fail-closed)
260
277
  try {
261
278
  // 剥前导 BOM:某些工具/重定向写出的 JSON 带 BOM,JSON.parse 会因此抛错。
262
279
  let raw = readFileSync(abs, 'utf8');
@@ -439,7 +456,7 @@ function parsePrimary(c, vars) {
439
456
  const fn = BUILTINS[t.value];
440
457
  if (!fn)
441
458
  throw new Error(`未知函数 "${t.value}"。白名单:${Object.keys(BUILTINS).join(', ')}`);
442
- return fn(argv);
459
+ return fn(argv, cwdOf(vars));
443
460
  }
444
461
  // 字面常量
445
462
  if (t.value === 'true')
@@ -608,8 +625,12 @@ export function interpolateDeep(value, vars, opts) {
608
625
  if (value && typeof value === 'object') {
609
626
  const out = {};
610
627
  for (const [k, v] of Object.entries(value)) {
611
- // 注意:key 也插值(loop 里 `hook_${idx}` 这种),但只对字符串 key 有意义
628
+ // 注意:key 也插值(loop 里 `hook_${idx}` 这种),但只对字符串 key 有意义;
629
+ // 插值后的 key 若命中危险键(__proto__/constructor/prototype)→ 直接跳过不写,
630
+ // 防止经动态 key 注入原型链属性(与 VarStack 防线保持一致)。
612
631
  const newKey = interpolate(k, vars, opts);
632
+ if (DANGEROUS_KEYS.has(newKey))
633
+ continue;
613
634
  out[newKey] = interpolateDeep(v, vars, opts);
614
635
  }
615
636
  return out;
@@ -42,7 +42,7 @@ import { existsSync, readFileSync } from 'node:fs';
42
42
  import { join, isAbsolute } from 'node:path';
43
43
 
44
44
  import { getWorkingDir } from '../../../src/plugin-sdk.js';
45
- import type { VarStack } from './types.js';
45
+ import { DANGEROUS_KEYS, isAbsWithinCwd, type VarStack } from './types.js';
46
46
 
47
47
  // ---------------- Tokenizer ----------------
48
48
 
@@ -160,11 +160,24 @@ function isPunct(c: Cursor, v: string): boolean {
160
160
  return !!t && t.kind === 'punct' && t.value === v;
161
161
  }
162
162
 
163
- const BUILTINS: Record<string, (args: unknown[]) => unknown> = {
164
- fileExists(args) {
163
+ /**
164
+ * 读磁盘的内置函数(fileExists / readJson)的锚点。
165
+ * 必须和写入侧同源:进了 session worktree 后产物落在 worktree 里,
166
+ * 这里若仍锚原 workspace,done_when 会在空目录里找文件而恒判 false。
167
+ */
168
+ function cwdOf(vars: VarStack): string {
169
+ return vars.resolveCwd?.() ?? getWorkingDir();
170
+ }
171
+
172
+ const BUILTINS: Record<string, (args: unknown[], cwd: string) => unknown> = {
173
+ fileExists(args, cwd) {
165
174
  const p = String(args[0] ?? '');
166
175
  if (!p) return false;
167
- const abs = isAbsolute(p) ? p : join(getWorkingDir(), p);
176
+ const abs = isAbsolute(p) ? p : join(cwd, p);
177
+ // 安全守卫:越出 cwd 的路径视为"不存在"(fail-closed,与 readJson 同策略)。
178
+ // 不可信 YAML(模板库/他人分享)可能带 fileExists('/etc/passwd') 做信息刺探,
179
+ // 这里与 cleanupDeclaredArtifacts 保持一致:只允许操作 cwd 下的文件。
180
+ if (!isAbsWithinCwd(cwd, abs)) return false;
168
181
  return existsSync(abs);
169
182
  },
170
183
  length(args) {
@@ -254,15 +267,17 @@ const BUILTINS: Record<string, (args: unknown[]) => unknown> = {
254
267
  return 'null';
255
268
  }
256
269
  },
257
- // 读磁盘 JSON 文件并取值:path 相对 getWorkingDir()(绝对路径原样);给了 key
258
- // 则返回顶层 obj[key],否则返回整值。缺文件 / 坏 JSON / 键不存在 → null
270
+ // 读磁盘 JSON 文件并取值:path 相对当前有效 cwd(绝对路径先经 cwd 安全校验,
271
+ // 越界直接 null,与 fileExists 同 fail-closed 策略);给了 key 则返回顶层
272
+ // obj[key],否则返回整值。缺文件 / 坏 JSON / 键不存在 / 越界 → null
259
273
  //(与 match/extract 同「不抛」哲学)。动机:让确定性脚本产出的 manifest
260
274
  //(如 source_assets.json)直接驱动工作流 for-each / branch —— 填补「磁盘
261
275
  // 结构化数据 → 控制流」的空白(与 fileExists 平级的基础能力)。
262
- readJson(args) {
276
+ readJson(args, cwd) {
263
277
  const p = String(args[0] ?? '');
264
278
  if (!p) return null;
265
- const abs = isAbsolute(p) ? p : join(getWorkingDir(), p);
279
+ const abs = isAbsolute(p) ? p : join(cwd, p);
280
+ if (!isAbsWithinCwd(cwd, abs)) return null; // 越界 → null(不抛,fail-closed)
266
281
  try {
267
282
  // 剥前导 BOM:某些工具/重定向写出的 JSON 带 BOM,JSON.parse 会因此抛错。
268
283
  let raw = readFileSync(abs, 'utf8');
@@ -436,7 +451,7 @@ function parsePrimary(c: Cursor, vars: VarStack): unknown {
436
451
  eat(c);
437
452
  const fn = BUILTINS[t.value];
438
453
  if (!fn) throw new Error(`未知函数 "${t.value}"。白名单:${Object.keys(BUILTINS).join(', ')}`);
439
- return fn(argv);
454
+ return fn(argv, cwdOf(vars));
440
455
  }
441
456
  // 字面常量
442
457
  if (t.value === 'true') return true;
@@ -612,8 +627,11 @@ export function interpolateDeep<T>(
612
627
  if (value && typeof value === 'object') {
613
628
  const out: Record<string, unknown> = {};
614
629
  for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
615
- // 注意:key 也插值(loop 里 `hook_${idx}` 这种),但只对字符串 key 有意义
630
+ // 注意:key 也插值(loop 里 `hook_${idx}` 这种),但只对字符串 key 有意义;
631
+ // 插值后的 key 若命中危险键(__proto__/constructor/prototype)→ 直接跳过不写,
632
+ // 防止经动态 key 注入原型链属性(与 VarStack 防线保持一致)。
616
633
  const newKey = interpolate(k, vars, opts);
634
+ if (DANGEROUS_KEYS.has(newKey)) continue;
617
635
  out[newKey] = interpolateDeep(v, vars, opts);
618
636
  }
619
637
  return out as unknown as T;
@@ -10,9 +10,9 @@
10
10
  * 不污染默认 T-A-O-R 路径。
11
11
  * ============================================================
12
12
  */
13
+ import { buildBaseSystemPrompt, } from '../../../src/plugin-sdk.js';
13
14
  import { findWorkflowByName, listWorkflows } from './loader.js';
14
15
  import { runWorkflow } from './runner.js';
15
- import { buildBaseSystemPrompt, } from '../../../src/plugin-sdk.js';
16
16
  /**
17
17
  * 把 "/workflow <name> [pos1 pos2 ...] [--input k=v ...]" 拆成 name + 显式 inputs + 位置参数。
18
18
  * 简单解析器(不依赖第三方):
@@ -53,7 +53,20 @@ export function parseWorkflowArgs(rawArgs) {
53
53
  }
54
54
  return { name, inputs, positional };
55
55
  }
56
- /** 简单 token 化:双引号包裹的整体当一个 token;其它按空白切。 */
56
+ /**
57
+ * shell 风格 token 化:空白切分 token;引号(' 或 ")在 token **任意位置**(不只开头)
58
+ * 出现即进入引号模式——引号内的空白不切分、引号字符本身被剥离。
59
+ *
60
+ * 修复:`--input topic="带空格的值"`(引号在 `=` 后,token 开头是 `t`)曾被按空白
61
+ * 硬切 → 值截断 + 引号残留(实测 inputs.topic 带 `"` 尾,positional 里散落被拆的字)。
62
+ * 现在 token 内任意位置的引号都正确分组。
63
+ *
64
+ * 边界语义(被 parseWorkflowArgs.test.ts 冻结):
65
+ * - 未闭合引号:剩余部分并入当前 token(不报错)
66
+ * - 双引号内的单引号是字面量(不触发嵌套)
67
+ * - 反斜杠不转义(`a\"b` 中 `"` 仍开启引号模式,行为变化但测试用 toBeDefined 容错)
68
+ * - 连续空白合并、`""` 产生空 token
69
+ */
57
70
  function tokenizeArgs(s) {
58
71
  const out = [];
59
72
  let i = 0;
@@ -62,24 +75,27 @@ function tokenizeArgs(s) {
62
75
  i++;
63
76
  if (i >= s.length)
64
77
  break;
65
- if (s[i] === '"' || s[i] === "'") {
66
- const q = s[i];
67
- i++;
68
- let acc = '';
69
- while (i < s.length && s[i] !== q) {
70
- acc += s[i++];
78
+ let acc = '';
79
+ while (i < s.length) {
80
+ const c = s[i];
81
+ if (c === '"' || c === "'") {
82
+ // 引号模式:收集到闭合引号,引号内空白保留
83
+ const q = c;
84
+ i++;
85
+ while (i < s.length && s[i] !== q)
86
+ acc += s[i++];
87
+ if (i < s.length)
88
+ i++; // 跳过闭合引号
71
89
  }
72
- if (i < s.length)
90
+ else if (/\s/.test(c)) {
91
+ break;
92
+ }
93
+ else {
94
+ acc += c;
73
95
  i++;
74
- out.push(acc);
75
- }
76
- else {
77
- let acc = '';
78
- while (i < s.length && !/\s/.test(s[i])) {
79
- acc += s[i++];
80
96
  }
81
- out.push(acc);
82
97
  }
98
+ out.push(acc);
83
99
  }
84
100
  return out;
85
101
  }
@@ -103,7 +119,7 @@ function coerceInputs(raw, defs) {
103
119
  continue;
104
120
  }
105
121
  if (def.type === 'number') {
106
- const n = parseFloat(rawVal);
122
+ const n = Number.parseFloat(rawVal);
107
123
  if (Number.isNaN(n))
108
124
  throw new Error(`input ${def.name} 必须是数字,收到 "${rawVal}"`);
109
125
  out[def.name] = n;
@@ -167,12 +183,17 @@ export async function* runWorkflowFromCommand(rawArgs, opts) {
167
183
  // - 默认:严格按"第 i 个位置参数 ↔ def.inputs[i]"对应。
168
184
  // --input k=v 显式优先:已显式的 input 不被位置参数覆盖(对应位置参数被丢弃,会 warn)。
169
185
  const overriddenByKV = [];
186
+ const greedyNotes = [];
170
187
  const greedyFirst = def.inputs?.[0]?.greedy === true;
171
188
  if (greedyFirst && def.inputs && positional.length > 0) {
172
189
  const greedyName = def.inputs[0].name;
173
190
  if (greedyName in rawInputs) {
174
- // 首参已被 --input 占位 → 整串位置参数被忽略
175
- overriddenByKV.push(`${greedyName}=${JSON.stringify(rawInputs[greedyName])} (位置参数 ${JSON.stringify(positional.join(' '))} 被忽略)`);
191
+ // 首参已由 --input 提供时,位置参数仍按贪婪语义【拼回】首参末尾--
192
+ // 含空格的值未加引号时会被 shell 拆开(--input topic=A B C 只有 A 进 KV,
193
+ // B C 散落成位置参数),静默丢弃曾把整段中文调研主题截成半句、
194
+ // 连锁触发任务仲裁误判 new-task 清空全部产物。
195
+ rawInputs[greedyName] = [rawInputs[greedyName], ...positional].join(' ');
196
+ greedyNotes.push(`${greedyName}:--input 值未加引号被拆成 ${positional.length} 段位置参数,已自动拼回(下次请写 --input "${greedyName}=完整值")`);
176
197
  }
177
198
  else {
178
199
  // 贪婪吸收:整句进首参(空格 join 还原用户原话,不逐位拆映射)
@@ -202,6 +223,13 @@ export async function* runWorkflowFromCommand(rawArgs, opts) {
202
223
  message: `⚠ 忽略了 ${dropped.length} 个多余位置参数: ${dropped.map((d) => JSON.stringify(d)).join(' ')}(workflow "${name}" 只声明了 ${def.inputs.length} 个 input)`,
203
224
  };
204
225
  }
226
+ // warn:贪婪首参的 --input 值被 shell 拆开后自动拼回
227
+ if (greedyNotes.length > 0) {
228
+ yield {
229
+ type: 'warning',
230
+ message: `⚠ ${greedyNotes.join(';')}`,
231
+ };
232
+ }
205
233
  // warn:位置参数被 --input 覆盖
206
234
  if (overriddenByKV.length > 0) {
207
235
  yield {
@@ -229,6 +257,8 @@ export async function* runWorkflowFromCommand(rawArgs, opts) {
229
257
  transientContext: opts.transientContext,
230
258
  transientBundle: opts.transientBundle,
231
259
  promptCache: opts.promptCache,
260
+ executeTool: opts.executeTool,
261
+ toolContext: opts.toolContext,
232
262
  };
233
263
  // 透传 runWorkflow 的事件,并观察是否出现终结事件(error / interrupted = 失败或中断,
234
264
  // 与 gateway broker 的 TERMINAL 判定同一套逻辑)。成功(没见到终结事件)则在末尾补一条
@@ -265,6 +295,6 @@ export async function* runWorkflowsList() {
265
295
  lines.push(' /workflow <name> <val> 位置参数(按 inputs 声明顺序,第一个 input 直接写值)');
266
296
  lines.push(' /workflow <name>.yaml <val> .yaml 后缀可省');
267
297
  lines.push(' /workflow <name> --input k=v 显式 KV(多 input 或想跳过中间 input 时用)');
268
- yield { type: 'text', delta: lines.join('\n') + '\n' };
298
+ yield { type: 'text', delta: `${lines.join('\n')}\n` };
269
299
  yield { type: 'turn_done' };
270
300
  }