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
@@ -12,12 +12,14 @@ import { createMicroCompactState, } from '../context/microCompactLite.js';
12
12
  import { createReactiveCompactState, } from '../context/reactiveCompact.js';
13
13
  import { createFileReadState } from '../tools/shared/fileState.js';
14
14
  import { createBackgroundTaskState, disposeBackgroundTaskState, } from '../tools/tasks/backgroundTaskService.js';
15
+ import { createAgentTaskState, stopAllAgentTasks } from '../tools/agent/agentTaskService.js';
15
16
  export function createSessionState() {
16
17
  return {
17
18
  reactive: createReactiveCompactState(),
18
19
  microCompact: createMicroCompactState(),
19
20
  fileReads: createFileReadState(),
20
21
  backgroundTasks: createBackgroundTaskState(),
22
+ agentTasks: createAgentTaskState(),
21
23
  };
22
24
  }
23
25
  const persistentStates = new Map();
@@ -56,6 +58,7 @@ export async function disposeSessionState(key) {
56
58
  return;
57
59
  persistentStates.delete(key);
58
60
  await disposeBackgroundTaskState(entry.state.backgroundTasks);
61
+ await stopAllAgentTasks(entry.state.agentTasks);
59
62
  entry.state.fileReads.entries.clear();
60
63
  }
61
64
  export async function disposeAllSessionStates() {
@@ -63,11 +66,13 @@ export async function disposeAllSessionStates() {
63
66
  persistentStates.clear();
64
67
  await Promise.all(entries.map(async ({ state }) => {
65
68
  await disposeBackgroundTaskState(state.backgroundTasks);
69
+ await stopAllAgentTasks(state.agentTasks);
66
70
  state.fileReads.entries.clear();
67
71
  }));
68
72
  }
69
73
  export async function disposeDetachedSessionState(state) {
70
74
  await disposeBackgroundTaskState(state.backgroundTasks);
75
+ await stopAllAgentTasks(state.agentTasks);
71
76
  state.fileReads.entries.clear();
72
77
  }
73
78
  function prunePersistentStates(now, keepKey) {
@@ -23,12 +23,22 @@ import {
23
23
  disposeBackgroundTaskState,
24
24
  type BackgroundTaskState,
25
25
  } from '../tools/tasks/backgroundTaskService.js';
26
+ import { createAgentTaskState, stopAllAgentTasks, type AgentTaskState } from '../tools/agent/agentTaskService.js';
26
27
 
27
28
  export interface SessionState {
28
29
  reactive: ReactiveCompactState;
29
30
  microCompact: MicroCompactState;
30
31
  fileReads: FileReadState;
31
32
  backgroundTasks: BackgroundTaskState;
33
+ agentTasks: AgentTaskState;
34
+ /** Main-agent worktree changes the effective cwd without changing workspace identity. */
35
+ activeWorktree?: {
36
+ id: string;
37
+ path: string;
38
+ branch: string;
39
+ baseSha: string;
40
+ gitRoot: string;
41
+ };
32
42
  }
33
43
 
34
44
  export function createSessionState(): SessionState {
@@ -37,6 +47,7 @@ export function createSessionState(): SessionState {
37
47
  microCompact: createMicroCompactState(),
38
48
  fileReads: createFileReadState(),
39
49
  backgroundTasks: createBackgroundTaskState(),
50
+ agentTasks: createAgentTaskState(),
40
51
  };
41
52
  }
42
53
 
@@ -84,6 +95,7 @@ export async function disposeSessionState(key: string): Promise<void> {
84
95
  if (!entry) return;
85
96
  persistentStates.delete(key);
86
97
  await disposeBackgroundTaskState(entry.state.backgroundTasks);
98
+ await stopAllAgentTasks(entry.state.agentTasks);
87
99
  entry.state.fileReads.entries.clear();
88
100
  }
89
101
 
@@ -92,12 +104,14 @@ export async function disposeAllSessionStates(): Promise<void> {
92
104
  persistentStates.clear();
93
105
  await Promise.all(entries.map(async ({ state }) => {
94
106
  await disposeBackgroundTaskState(state.backgroundTasks);
107
+ await stopAllAgentTasks(state.agentTasks);
95
108
  state.fileReads.entries.clear();
96
109
  }));
97
110
  }
98
111
 
99
112
  export async function disposeDetachedSessionState(state: SessionState): Promise<void> {
100
113
  await disposeBackgroundTaskState(state.backgroundTasks);
114
+ await stopAllAgentTasks(state.agentTasks);
101
115
  state.fileReads.entries.clear();
102
116
  }
103
117
 
@@ -42,11 +42,23 @@ const inputSchema = z.object({
42
42
  const parameters = toToolParameters(inputSchema);
43
43
  // ---------------- 2. Description(给 LLM 看的工具说明) ----------------
44
44
  const description = `- Consults an independent, separately-configured second LLM ("顾问模型" / advisor) for a second opinion on your current plan, diagnosis, or progress.
45
- - Call with no arguments in most cases — the advisor receives a bounded, relevant view of the current session (the original request, recent discussion, current plan/diagnosis, and key tool results). Only pass \`note\` if something important is not otherwise clear.
46
- - Exception: if your current plan/diagnosis relies on things you saw in images (screenshots, photos, diagrams), summarize the key visual facts in \`note\` in 1-2 sentences (what each image shows and what you concluded from it) — the advisor may not be able to see the images themselves, and any visual judgment you never wrote down as text does not exist for it.
47
- - Call this: (1) after a plan/diagnosis has taken shape but before you commit to it (write code, declare a conclusion); (2) when you're stuck — the same error keeps recurring, your fixes aren't converging, or the user has pushed back and you're reconsidering direction; (3) before declaring a multi-step task complete and reporting back to the user.
45
+ - Call with no arguments in most cases — the advisor receives a bounded, relevant view of the current session (the original request, recent discussion, current plan/diagnosis, and key tool results). Always pass key evidence (original code snippets, raw data, full plan text) in \`note\`, not just your summary — the advisor cannot verify things you never wrote down.
46
+ - Exception: if your current plan/diagnosis relies on things you saw in images (screenshots, photos, diagrams), summarize the key visual facts in \`note\` in 1-2 sentences (what each image shows and what you concluded from it) — the advisor may not be able to see the images themselves.
47
+ - **Call it BEFORE substantive work, not after things go wrong.** The advisor is worth the most before an approach hardens — once you have written the code, the plan is expensive to abandon.
48
+ - Orientation is NOT substantive work: finding files, reading code, running a command to see the current state. Do that first, then consult.
49
+ - Substantive work IS: writing or editing files, committing to an interpretation, building on an assumption, declaring an answer.
50
+ - **MUST call Advisor in all 5 of these situations — do not wait for errors or user prompts:**
51
+ 1. After a plan / development plan is finalized, **before** executing it (get critique first, avoid discovering wrong direction halfway)
52
+ 2. **Before** submitting final results to the user (before PresentResult): code reviews, assessment reports, technical selections, refactoring plans — any time you "deliver a conclusion"
53
+ 3. The same error occurs 2+ times in a row, or fixes are not converging (early sign of infinite loop — switch approach immediately)
54
+ 4. The user gives unsatisfied feedback a **second time** in the same turn (stop spinning in place, get a fresh perspective first)
55
+ 5. Decisions involving security, data loss, or irreversible operations (advisor double-check)
56
+ - **Make your work durable BEFORE calling in situation 2.** Write the file, save the result. This call takes time; if the turn ends during it, a written file survives and an unwritten answer does not.
57
+ - On a long task, consult at least twice: once before the approach is locked in, once before you declare it done. On a short reactive task whose next step is dictated by output you just read, one call is enough — the advisor adds most of its value early.
58
+ - **Take the advice seriously, but do not obey it blindly.** Adapt if you follow a step and it empirically fails, or you hold primary-source evidence contradicting a specific claim (the file says X, the test output says Y). A passing self-test is NOT evidence the advice is wrong — it is evidence your test does not check what the advice checks.
59
+ - **If the advisor contradicts evidence you already gathered, do not silently switch.** Call Advisor once more stating the conflict outright ("I measured X, you advise Y — which constraint breaks the tie?"). The advisor saw your evidence but may have underweighted it; one reconciling call is far cheaper than committing to the wrong branch.
48
60
  - The advisor has no tool access and does a single read-only pass over the transcript — it cannot verify anything itself, only reason about what it's shown.
49
- - If this tool errors (advisor not configured in shared settings, disabled, manual-only, or a call failure), that's not a signal to stop — treat it as "no second opinion available this time" and continue with your own judgment. Do not block or retry in a loop.`;
61
+ - If this tool errors (advisor not configured in shared settings, disabled, manual-only, or a call failure), that's not a signal to stop — treat it as "no second opinion available this time" and continue with your own judgment, but **explicitly note "not cross-verified by advisor" in your final output**. Do not block or retry in a loop.`;
50
62
  // ---------------- 3. 顾问 persona system prompt ----------------
51
63
  const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技术顾问,为正在独立执行任务的另一个 AI 助手提供第二意见评审。
52
64
  你的职责:技术方案评估、代码评审、疑难问题的根因定位与解决方向判断。
@@ -54,6 +66,14 @@ const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技
54
66
  你会看到该助手当前会话中受预算约束的相关记录——包括首要需求、近期交流、它的思考过程、当前方案
55
67
  和关键工具结果与执行结果,以及它现在想让你重点关注的内容(如果有)。
56
68
 
69
+ 重要:你**没有工具访问权,无法自行验证任何事实**。你只是提供一个不同视角的怀疑和推理,
70
+ 不能把自己的推测当成确定结论。每条指出的问题都要明确标注:
71
+ - 【有证据】:基于记录里明确出现的代码/测试结果/事实
72
+ - 【推断】:基于已有信息的合理推断,但未被证实
73
+ - 【存疑】:信息不足,需要执行助手回去核实
74
+ 不要过度自信,不要给出确定性语气的未验证判断——执行助手会对你的意见逐条做验证,
75
+ 夸大其词会被判定为误报。
76
+
57
77
  审视时请做到:
58
78
  - 像资深工程师做方案评审/代码评审一样直接、具体——发现问题就明说,不要无条件附和或说套话。
59
79
  - 优先检查:方案是否遗漏了风险或边界情况、诊断是否真的找到了根因还是只是让症状消失、
@@ -66,7 +86,16 @@ const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技
66
86
  或换个方式绕开,都要点出来。
67
87
  - 结论必须基于你在历史记录里看到的实际证据,不要脱离上下文给泛泛的通用建议;
68
88
  被省略的记录(尤其工具结果原文)所涉及的判断,明说你无法核实。
69
- - 篇幅精炼:只讲最关键的 1-3 点,不要复述历史、不要客套开场白。`;
89
+ - 篇幅精炼:只讲最关键的 1-3 点,不要复述历史、不要客套开场白。
90
+
91
+ 关于「带着反证回来」的那一次咨询(助手被要求这样做,不是在质疑你):
92
+ 当它说「我实测是 X,你上次建议 Y」时,它手里的是一手证据,你手里的是推理——
93
+ 默认它的观测为真,然后只回答一个问题:**哪个约束更硬**。
94
+ - 你上次漏看了它给出的证据 → 直接认,并说清结论怎么变。
95
+ - 它的观测其实没测到你担心的那一层(测了被改的函数,而真实调用路径根本走不到)
96
+ → 指出这个错位,并说要测什么才算数。
97
+ - 两者都成立、只是适用范围不同 → 说清各自的边界,别硬选一个。
98
+ 不要为了显得一致而坚持原判,也不要因为被反驳就整个收回——两种都会让它失去判断依据。`;
70
99
  // ---------------- 4. 消息构造(纯函数,单独导出可测) ----------------
71
100
  /**
72
101
  * 把主循环的 history 转成喂给顾问模型的 messages。
@@ -203,6 +232,13 @@ function fitAdvisorBudget(messages, inputBudget) {
203
232
  selected.push(first);
204
233
  used += countMessagesTokens([first]);
205
234
  }
235
+ // 从最近往前收,**装不下就停**(break,不是 continue)。
236
+ //
237
+ // ⚠️ 曾经是 continue:一条大消息放不下就跳过它、继续往前捡更小的旧消息。
238
+ // 结果是给顾问一段**中间有洞**的对话 —— 而洞恰好开在信息量最大的地方(长工具
239
+ // 结果、完整方案文本),偏偏下面那句省略提示还写着「保留最初需求和最近相关记录」,
240
+ // 顾问于是把两条本不相邻的消息读成前后相接,在残缺的因果链上给出自信结论。
241
+ // 宁可少给,不可给错:连续的尾部 + 如实说明从哪里开始省略。
206
242
  const recent = [];
207
243
  for (let index = body.length - 1; index >= 0; index--) {
208
244
  if (index === firstUserIndex)
@@ -210,7 +246,7 @@ function fitAdvisorBudget(messages, inputBudget) {
210
246
  const message = body[index];
211
247
  const cost = countMessagesTokens([message]);
212
248
  if (used + cost > inputBudget)
213
- continue;
249
+ break;
214
250
  recent.unshift(message);
215
251
  used += cost;
216
252
  }
@@ -287,17 +323,22 @@ export async function runAdvisor(input, signal, ctx, chatFn = chat, runtime = {}
287
323
  return {
288
324
  ok: false,
289
325
  error: '顾问模型未配置。请在设置 → 模型中完成专家顾问配置。' +
290
- '可继续凭自己判断推进,不必等待顾问。',
326
+ '本回合后续不要再次尝试调用 Advisor;可继续凭自己判断推进,不必等待顾问。',
291
327
  };
292
328
  }
293
329
  if (!advisor.enabled) {
294
330
  return {
295
331
  ok: false,
296
- error: '顾问模型当前已关闭(/advisor on 开启)。可继续凭自己判断推进,不必等待顾问。',
332
+ error: '顾问模型当前已关闭(/advisor on 开启)。' +
333
+ '本回合后续不要再次尝试调用 Advisor;可继续凭自己判断推进,不必等待顾问。',
297
334
  };
298
335
  }
299
336
  if (advisor.mode === 'manual' && ctx?.advisorInvocation !== 'manual') {
300
- return { ok: false, error: '顾问模型当前为“仅手动”模式;请使用 /advisor ask 发起咨询。' };
337
+ return {
338
+ ok: false,
339
+ error: '顾问模型当前为"仅手动"模式;请使用 /advisor ask 发起咨询。' +
340
+ '本回合后续不要再次尝试自动调用 Advisor。',
341
+ };
301
342
  }
302
343
  if (ctx?.advisorCalls && ctx.advisorCalls.used >= ctx.advisorCalls.max) {
303
344
  // ⚠️ 必须说明「配额会增长」:配额随本回合工作量上调(loop.ts::advisorQuotaFor),
@@ -435,6 +476,32 @@ export const advisorTool = {
435
476
  isConcurrencySafe: false,
436
477
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
437
478
  interruptBehavior: () => 'cancel',
479
+ /**
480
+ * 动态可用性探测:未配置 / 已关闭 / 仅手动模式 → 工具不暴露给模型。
481
+ * 对齐 ComputerUse / WebBrowser 的模式:关闭时工具直接从 LLM 视野消失,
482
+ * 避免"看到工具但调不了→反复尝试"的流畅度问题。
483
+ *
484
+ * signature 随 enabled/mode/modelId 变化,确保配置改动后工具集签名更新、
485
+ * 模型下一轮拿到的是新工具列表。
486
+ */
487
+ resolveAvailability() {
488
+ // 走同步探测(读内存快照),不触发磁盘 IO;
489
+ // resolveAvailability 是工具集构造阶段的同步调用,不能 await。
490
+ const avail = configService.advisorAvailabilitySync();
491
+ // ⚠️ 安全兜底:如果 snapshot 尚未初始化(极端情况:工具集构造发生在任何
492
+ // configService 异步调用之前),宁可返回 available=true 让工具显示出来
493
+ // (调用时 runAdvisor 会做真正的 fresh reload 校验),也不能误判为
494
+ // "未配置" 导致 Advisor 从工具列表里消失——少一个工具比多一个工具严重得多。
495
+ if (avail.configured && !avail.enabled) {
496
+ return { available: false, signature: 'disabled' };
497
+ }
498
+ if (avail.configured && avail.mode === 'manual') {
499
+ return { available: false, signature: 'manual-only' };
500
+ }
501
+ // 未初始化 / 已配置且可用 → 都显示工具(未配置时调用会有明确错误提示)
502
+ // modelId 进签名:不同顾问模型 = 不同能力 = 工具语义不同。
503
+ return { available: true, signature: avail.modelId ?? 'initializing' };
504
+ },
438
505
  call,
439
506
  callWithContext: runAdvisor,
440
507
  };
@@ -51,11 +51,23 @@ const parameters = toToolParameters(inputSchema);
51
51
 
52
52
  // ---------------- 2. Description(给 LLM 看的工具说明) ----------------
53
53
  const description = `- Consults an independent, separately-configured second LLM ("顾问模型" / advisor) for a second opinion on your current plan, diagnosis, or progress.
54
- - Call with no arguments in most cases — the advisor receives a bounded, relevant view of the current session (the original request, recent discussion, current plan/diagnosis, and key tool results). Only pass \`note\` if something important is not otherwise clear.
55
- - Exception: if your current plan/diagnosis relies on things you saw in images (screenshots, photos, diagrams), summarize the key visual facts in \`note\` in 1-2 sentences (what each image shows and what you concluded from it) — the advisor may not be able to see the images themselves, and any visual judgment you never wrote down as text does not exist for it.
56
- - Call this: (1) after a plan/diagnosis has taken shape but before you commit to it (write code, declare a conclusion); (2) when you're stuck — the same error keeps recurring, your fixes aren't converging, or the user has pushed back and you're reconsidering direction; (3) before declaring a multi-step task complete and reporting back to the user.
54
+ - Call with no arguments in most cases — the advisor receives a bounded, relevant view of the current session (the original request, recent discussion, current plan/diagnosis, and key tool results). Always pass key evidence (original code snippets, raw data, full plan text) in \`note\`, not just your summary — the advisor cannot verify things you never wrote down.
55
+ - Exception: if your current plan/diagnosis relies on things you saw in images (screenshots, photos, diagrams), summarize the key visual facts in \`note\` in 1-2 sentences (what each image shows and what you concluded from it) — the advisor may not be able to see the images themselves.
56
+ - **Call it BEFORE substantive work, not after things go wrong.** The advisor is worth the most before an approach hardens — once you have written the code, the plan is expensive to abandon.
57
+ - Orientation is NOT substantive work: finding files, reading code, running a command to see the current state. Do that first, then consult.
58
+ - Substantive work IS: writing or editing files, committing to an interpretation, building on an assumption, declaring an answer.
59
+ - **MUST call Advisor in all 5 of these situations — do not wait for errors or user prompts:**
60
+ 1. After a plan / development plan is finalized, **before** executing it (get critique first, avoid discovering wrong direction halfway)
61
+ 2. **Before** submitting final results to the user (before PresentResult): code reviews, assessment reports, technical selections, refactoring plans — any time you "deliver a conclusion"
62
+ 3. The same error occurs 2+ times in a row, or fixes are not converging (early sign of infinite loop — switch approach immediately)
63
+ 4. The user gives unsatisfied feedback a **second time** in the same turn (stop spinning in place, get a fresh perspective first)
64
+ 5. Decisions involving security, data loss, or irreversible operations (advisor double-check)
65
+ - **Make your work durable BEFORE calling in situation 2.** Write the file, save the result. This call takes time; if the turn ends during it, a written file survives and an unwritten answer does not.
66
+ - On a long task, consult at least twice: once before the approach is locked in, once before you declare it done. On a short reactive task whose next step is dictated by output you just read, one call is enough — the advisor adds most of its value early.
67
+ - **Take the advice seriously, but do not obey it blindly.** Adapt if you follow a step and it empirically fails, or you hold primary-source evidence contradicting a specific claim (the file says X, the test output says Y). A passing self-test is NOT evidence the advice is wrong — it is evidence your test does not check what the advice checks.
68
+ - **If the advisor contradicts evidence you already gathered, do not silently switch.** Call Advisor once more stating the conflict outright ("I measured X, you advise Y — which constraint breaks the tie?"). The advisor saw your evidence but may have underweighted it; one reconciling call is far cheaper than committing to the wrong branch.
57
69
  - The advisor has no tool access and does a single read-only pass over the transcript — it cannot verify anything itself, only reason about what it's shown.
58
- - If this tool errors (advisor not configured in shared settings, disabled, manual-only, or a call failure), that's not a signal to stop — treat it as "no second opinion available this time" and continue with your own judgment. Do not block or retry in a loop.`;
70
+ - If this tool errors (advisor not configured in shared settings, disabled, manual-only, or a call failure), that's not a signal to stop — treat it as "no second opinion available this time" and continue with your own judgment, but **explicitly note "not cross-verified by advisor" in your final output**. Do not block or retry in a loop.`;
59
71
 
60
72
  // ---------------- 3. 顾问 persona system prompt ----------------
61
73
  const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技术顾问,为正在独立执行任务的另一个 AI 助手提供第二意见评审。
@@ -64,6 +76,14 @@ const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技
64
76
  你会看到该助手当前会话中受预算约束的相关记录——包括首要需求、近期交流、它的思考过程、当前方案
65
77
  和关键工具结果与执行结果,以及它现在想让你重点关注的内容(如果有)。
66
78
 
79
+ 重要:你**没有工具访问权,无法自行验证任何事实**。你只是提供一个不同视角的怀疑和推理,
80
+ 不能把自己的推测当成确定结论。每条指出的问题都要明确标注:
81
+ - 【有证据】:基于记录里明确出现的代码/测试结果/事实
82
+ - 【推断】:基于已有信息的合理推断,但未被证实
83
+ - 【存疑】:信息不足,需要执行助手回去核实
84
+ 不要过度自信,不要给出确定性语气的未验证判断——执行助手会对你的意见逐条做验证,
85
+ 夸大其词会被判定为误报。
86
+
67
87
  审视时请做到:
68
88
  - 像资深工程师做方案评审/代码评审一样直接、具体——发现问题就明说,不要无条件附和或说套话。
69
89
  - 优先检查:方案是否遗漏了风险或边界情况、诊断是否真的找到了根因还是只是让症状消失、
@@ -76,7 +96,16 @@ const ADVISOR_SYSTEM_PROMPT = `你是一位经验丰富的全栈开发专家技
76
96
  或换个方式绕开,都要点出来。
77
97
  - 结论必须基于你在历史记录里看到的实际证据,不要脱离上下文给泛泛的通用建议;
78
98
  被省略的记录(尤其工具结果原文)所涉及的判断,明说你无法核实。
79
- - 篇幅精炼:只讲最关键的 1-3 点,不要复述历史、不要客套开场白。`;
99
+ - 篇幅精炼:只讲最关键的 1-3 点,不要复述历史、不要客套开场白。
100
+
101
+ 关于「带着反证回来」的那一次咨询(助手被要求这样做,不是在质疑你):
102
+ 当它说「我实测是 X,你上次建议 Y」时,它手里的是一手证据,你手里的是推理——
103
+ 默认它的观测为真,然后只回答一个问题:**哪个约束更硬**。
104
+ - 你上次漏看了它给出的证据 → 直接认,并说清结论怎么变。
105
+ - 它的观测其实没测到你担心的那一层(测了被改的函数,而真实调用路径根本走不到)
106
+ → 指出这个错位,并说要测什么才算数。
107
+ - 两者都成立、只是适用范围不同 → 说清各自的边界,别硬选一个。
108
+ 不要为了显得一致而坚持原判,也不要因为被反驳就整个收回——两种都会让它失去判断依据。`;
80
109
 
81
110
  // ---------------- 4. 消息构造(纯函数,单独导出可测) ----------------
82
111
 
@@ -234,12 +263,19 @@ function fitAdvisorBudget(messages: Message[], inputBudget: number): Message[] {
234
263
  selected.push(first);
235
264
  used += countMessagesTokens([first]);
236
265
  }
266
+ // 从最近往前收,**装不下就停**(break,不是 continue)。
267
+ //
268
+ // ⚠️ 曾经是 continue:一条大消息放不下就跳过它、继续往前捡更小的旧消息。
269
+ // 结果是给顾问一段**中间有洞**的对话 —— 而洞恰好开在信息量最大的地方(长工具
270
+ // 结果、完整方案文本),偏偏下面那句省略提示还写着「保留最初需求和最近相关记录」,
271
+ // 顾问于是把两条本不相邻的消息读成前后相接,在残缺的因果链上给出自信结论。
272
+ // 宁可少给,不可给错:连续的尾部 + 如实说明从哪里开始省略。
237
273
  const recent: Message[] = [];
238
274
  for (let index = body.length - 1; index >= 0; index--) {
239
275
  if (index === firstUserIndex) continue;
240
276
  const message = body[index]!;
241
277
  const cost = countMessagesTokens([message]);
242
- if (used + cost > inputBudget) continue;
278
+ if (used + cost > inputBudget) break;
243
279
  recent.unshift(message);
244
280
  used += cost;
245
281
  }
@@ -348,19 +384,26 @@ export async function runAdvisor(
348
384
  ok: false,
349
385
  error:
350
386
  '顾问模型未配置。请在设置 → 模型中完成专家顾问配置。' +
351
- '可继续凭自己判断推进,不必等待顾问。',
387
+ '本回合后续不要再次尝试调用 Advisor;可继续凭自己判断推进,不必等待顾问。',
352
388
  };
353
389
  }
354
390
 
355
391
  if (!advisor.enabled) {
356
392
  return {
357
393
  ok: false,
358
- error: '顾问模型当前已关闭(/advisor on 开启)。可继续凭自己判断推进,不必等待顾问。',
394
+ error:
395
+ '顾问模型当前已关闭(/advisor on 开启)。' +
396
+ '本回合后续不要再次尝试调用 Advisor;可继续凭自己判断推进,不必等待顾问。',
359
397
  };
360
398
  }
361
399
 
362
400
  if (advisor.mode === 'manual' && ctx?.advisorInvocation !== 'manual') {
363
- return { ok: false, error: '顾问模型当前为“仅手动”模式;请使用 /advisor ask 发起咨询。' };
401
+ return {
402
+ ok: false,
403
+ error:
404
+ '顾问模型当前为"仅手动"模式;请使用 /advisor ask 发起咨询。' +
405
+ '本回合后续不要再次尝试自动调用 Advisor。',
406
+ };
364
407
  }
365
408
 
366
409
  if (ctx?.advisorCalls && ctx.advisorCalls.used >= ctx.advisorCalls.max) {
@@ -507,6 +550,33 @@ export const advisorTool: Tool<AdvisorInput> = {
507
550
  isConcurrencySafe: false,
508
551
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
509
552
  interruptBehavior: () => 'cancel',
553
+ /**
554
+ * 动态可用性探测:未配置 / 已关闭 / 仅手动模式 → 工具不暴露给模型。
555
+ * 对齐 ComputerUse / WebBrowser 的模式:关闭时工具直接从 LLM 视野消失,
556
+ * 避免"看到工具但调不了→反复尝试"的流畅度问题。
557
+ *
558
+ * signature 随 enabled/mode/modelId 变化,确保配置改动后工具集签名更新、
559
+ * 模型下一轮拿到的是新工具列表。
560
+ */
561
+ resolveAvailability() {
562
+ // 走同步探测(读内存快照),不触发磁盘 IO;
563
+ // resolveAvailability 是工具集构造阶段的同步调用,不能 await。
564
+ const avail = configService.advisorAvailabilitySync();
565
+
566
+ // ⚠️ 安全兜底:如果 snapshot 尚未初始化(极端情况:工具集构造发生在任何
567
+ // configService 异步调用之前),宁可返回 available=true 让工具显示出来
568
+ // (调用时 runAdvisor 会做真正的 fresh reload 校验),也不能误判为
569
+ // "未配置" 导致 Advisor 从工具列表里消失——少一个工具比多一个工具严重得多。
570
+ if (avail.configured && !avail.enabled) {
571
+ return { available: false, signature: 'disabled' };
572
+ }
573
+ if (avail.configured && avail.mode === 'manual') {
574
+ return { available: false, signature: 'manual-only' };
575
+ }
576
+ // 未初始化 / 已配置且可用 → 都显示工具(未配置时调用会有明确错误提示)
577
+ // modelId 进签名:不同顾问模型 = 不同能力 = 工具语义不同。
578
+ return { available: true, signature: avail.modelId ?? 'initializing' };
579
+ },
510
580
  call,
511
581
  callWithContext: runAdvisor,
512
582
  };
@@ -3,14 +3,12 @@
3
3
  * src/tools/agent/agent.ts —— Agent 工具(fork 子 agent / 多 agent 并行)
4
4
  * ------------------------------------------------------------
5
5
  * 借鉴 Claude Code 官方 AgentTool/ForkAgent 的核心思想,按 minimal-agent
6
- * 内核架构做同步等效实现:
6
+ * 内核架构做可恢复的前台/后台等效实现:
7
7
  * - 官方 fork = 「隔离 context 嵌套跑同一主循环 + 摘要回传(完整历史不进
8
- * 父上下文)」。我们不需要它的后台任务队列/AsyncLocalStorage 身份追踪
9
- * (那是为 Ink UI 后台任务服务的)——本工具同步 await 一个嵌套 runQuery,
10
- * 声明 isConcurrencySafe:true 后,模型在同一轮发出 N 个 Agent 调用时,
11
- * loop.ts 现成的并行 worker 队列自动把它们并发执行。这就是并行的全部来源。
12
- * - 同轮混入任一非并发安全工具(如 Edit)会整轮退化串行——这是想要的安全
13
- * 语义(写操作与并行 fork 不该交错)。
8
+ * 父上下文)」。本工具在隔离 history 中嵌套运行同一主循环,并将私有历史、
9
+ * 进度和结果写入会话运行存储;前台、后台和重启后续接共用同一执行内核。
10
+ * - 同轮多个 Agent 调用仍由 loop.ts 的并行 worker 执行;write scopes 与可选
11
+ * worktree 负责隔离真正可并行的写任务。
14
12
  *
15
13
  * 已知限制(如实声明):
16
14
  * - 子 agent 的 LoopEvent 不外泄:-p / stream-json 宿主只看到本工具的一对
@@ -27,6 +25,8 @@
27
25
  * ============================================================
28
26
  */
29
27
  import { randomUUID } from 'node:crypto';
28
+ import { EventEmitter } from 'node:events';
29
+ import { access } from 'node:fs/promises';
30
30
  import { withAskSource } from '../../askBridge.js';
31
31
  import { isAbsolute, relative, resolve, sep } from 'node:path';
32
32
  import { z } from 'zod';
@@ -41,6 +41,7 @@ import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
41
41
  import { SUBAGENT_EXCLUDED_TOOLS, resolveAgentType } from './agentTypes.js';
42
42
  import { acquireSubagentLease, normalizeWriteScopes } from './coordinator.js';
43
43
  import { commitWorktreeChanges, createAgentWorktree, hasWorktreeChanges, removeAgentWorktree, worktreeChangedFiles, worktreeDiffStat, } from './worktree.js';
44
+ import { appendAgentEvent, loadAgentRecord, saveAgentRecord } from './agentTaskService.js';
44
45
  let corePromise = null;
45
46
  /** 首次调用时才真正加载 loop/tools——此刻模块图早已初始化完毕,环安全。 */
46
47
  function loadCore() {
@@ -79,13 +80,16 @@ const inputSchema = z.object({
79
80
  .max(32)
80
81
  .optional()
81
82
  .describe('general 预计修改的工作目录相对路径。范围互不重叠时可并行;缺省时按整个工作目录串行。explore 不需要。'),
83
+ run_in_background: z.boolean().optional().describe('立即返回 agent_id,子任务在后台运行;用 TaskOutput/TaskStop 管理'),
84
+ resume_agent_id: z.string().regex(/^agent_[0-9a-f-]{36}$/i).optional().describe('继续当前会话中此前完成、中断或失败的 Agent 历史'),
82
85
  });
83
86
  const parameters = toToolParameters(inputSchema);
84
87
  // ---------------- 2. Description(给 LLM 看的工具说明) ----------------
85
88
  // 借鉴官方 AgentTool prompt 文案(任务书自包含 / 同轮并行 / 何时不用)。
86
- const description = `- Launches a stateless sub-agent with a fresh, isolated context to execute one self-contained subtask; returns only its final report text (its full working history never enters this conversation).
89
+ const description = `- Launches an isolated sub-agent for one self-contained task. Every run returns an agent_id; run_in_background=true continues without blocking, and resume_agent_id continues its persisted private history across user turns or an app restart.
87
90
  - The sub-agent CANNOT see this conversation. The \`prompt\` must be fully self-contained: background, exact file paths, what to look for / do, and the expected output format.
88
91
  - **To parallelize, emit multiple Agent calls in ONE assistant message** — they run concurrently. This is the main value of this tool.
92
+ - Background Agent progress/results are read with TaskOutput and stopped with TaskStop. Do not poll: TaskOutput(block=true) waits efficiently.
89
93
  - Use for: exploring/researching several independent areas of a codebase at once, parallel information gathering from multiple sources, independent analyses that will be synthesized afterwards, and (with agent_type:'general') genuinely parallelizable write/execute work.
90
94
  - Do NOT use for: single targeted lookups that one or two direct tool calls would answer (a fork starts cold and re-derives context — slower and costlier), or strictly sequential work. Only general execution agents may ask the user, and only when genuinely blocked.
91
95
  - agent_type: 'explore' (default) = strictly read-only research; 'general' = read-write execution. For direct writes, declare relative write_scopes; disjoint scopes may run concurrently, overlapping or omitted scopes are serialized.
@@ -237,10 +241,10 @@ export async function runAgentTask(input, signal, ctx, deps = {}) {
237
241
  // allowedTools 只裁「下发给模型的清单」;guardedExecute 是真正的硬防线(见下)。
238
242
  const allowedTools = def.resolveAllowedTools(ALL_TOOLS.map((t) => t.name));
239
243
  // ---- worktree 隔离(仅 general 显式要求时) ----
240
- let wt = null;
244
+ let wt = deps.existingWorktree ?? null;
241
245
  const isolation = input.isolation ?? ctx.subagentConfig?.defaultWriteIsolation ?? 'none';
242
- if (def.type === 'general' && isolation === 'worktree') {
243
- const created = await createAgentWorktree(`fork-${randomUUID().slice(0, 8)}`);
246
+ if (def.type === 'general' && isolation === 'worktree' && !wt) {
247
+ const created = await createAgentWorktree(`fork-${randomUUID().slice(0, 8)}`, ctx.cwd);
244
248
  if (!created.ok) {
245
249
  return {
246
250
  ok: false,
@@ -248,8 +252,9 @@ export async function runAgentTask(input, signal, ctx, deps = {}) {
248
252
  };
249
253
  }
250
254
  wt = created;
255
+ await deps.onWorktreeCreated?.(created);
251
256
  }
252
- const cwd = wt?.path ?? getWorkingDir();
257
+ const cwd = wt?.path ?? ctx.cwd ?? getWorkingDir();
253
258
  let writeScopes;
254
259
  try {
255
260
  writeScopes = def.type === 'general'
@@ -288,6 +293,7 @@ export async function runAgentTask(input, signal, ctx, deps = {}) {
288
293
  surface: 'subagent',
289
294
  cwd,
290
295
  workspaceId: ctx.workspaceId,
296
+ sessionId: ctx.sessionId,
291
297
  interactiveAsk: childRuntime.interactiveAsk,
292
298
  memoryAccess: childMemoryAccess,
293
299
  currentUserInput: input.prompt,
@@ -303,7 +309,7 @@ export async function runAgentTask(input, signal, ctx, deps = {}) {
303
309
  ctx.reportProgress?.(line, detail);
304
310
  };
305
311
  // ---- 子 history:一条 system(压缩按 role 定位,必须存在)+ runQuery 自己 push 任务书 ----
306
- const history = [
312
+ const history = deps.initialHistory?.length ? [...deps.initialHistory] : [
307
313
  { role: 'system', content: await buildSubSystemPrompt(cwd, def, subTools) },
308
314
  ];
309
315
  const historyLenBefore = history.length;
@@ -529,6 +535,7 @@ export async function runAgentTask(input, signal, ctx, deps = {}) {
529
535
  finally {
530
536
  stopTimeout();
531
537
  releaseLease?.();
538
+ await deps.captureHistory?.(history);
532
539
  }
533
540
  // ---- 结局映射(直接查 signal 状态,不依赖事件旗标) ----
534
541
  if (ownerCancelController.signal.aborted) {
@@ -625,6 +632,128 @@ export async function runAgentTask(input, signal, ctx, deps = {}) {
625
632
  async function call(input, signal) {
626
633
  return runAgentTask(input, signal, {});
627
634
  }
635
+ export async function runManagedAgentTask(input, signal, ctx, deps = {}) {
636
+ if (!ctx.sessionState)
637
+ return runAgentTask(input, signal, ctx);
638
+ const id = input.resume_agent_id ?? `agent_${randomUUID()}`;
639
+ let initialHistory;
640
+ let resumedWorktree;
641
+ let coreInput = { ...input, run_in_background: undefined, resume_agent_id: undefined };
642
+ if (input.resume_agent_id) {
643
+ const live = ctx.sessionState.agentTasks.tasks.get(id);
644
+ if (live?.status === 'running')
645
+ return { ok: false, error: `Agent ${id} 正在运行,不能并发恢复` };
646
+ const saved = await loadAgentRecord(ctx, id);
647
+ if (!saved)
648
+ return { ok: false, error: `找不到可恢复 Agent:${id}` };
649
+ initialHistory = saved.history;
650
+ resumedWorktree = saved.worktree && await access(saved.worktree.path)
651
+ .then(() => saved.worktree)
652
+ .catch(() => undefined);
653
+ coreInput = {
654
+ ...saved.input,
655
+ prompt: input.prompt,
656
+ description: input.description || saved.description,
657
+ run_in_background: undefined,
658
+ resume_agent_id: undefined,
659
+ };
660
+ }
661
+ const abort = new AbortController();
662
+ const combined = signal ? AbortSignal.any([signal, abort.signal]) : abort.signal;
663
+ let settle;
664
+ const completion = new Promise((resolvePromise) => { settle = resolvePromise; });
665
+ const task = {
666
+ id,
667
+ status: 'running',
668
+ description: coreInput.description,
669
+ input: coreInput,
670
+ createdAt: Date.now(),
671
+ updatedAt: Date.now(),
672
+ abort,
673
+ completion,
674
+ events: new EventEmitter(),
675
+ };
676
+ ctx.sessionState.agentTasks.tasks.set(id, task);
677
+ await saveAgentRecord(ctx, task);
678
+ await appendAgentEvent(ctx, id, { phase: 'started', resumed: Boolean(input.resume_agent_id) });
679
+ const run = async () => {
680
+ let captured = initialHistory;
681
+ let lastPersistedProgressAt = 0;
682
+ const managedContext = {
683
+ ...ctx,
684
+ reportProgress: (line, detail) => {
685
+ ctx.reportProgress?.(line, detail);
686
+ task.progress = line;
687
+ task.events.emit('change');
688
+ if (Date.now() - lastPersistedProgressAt >= 1_000) {
689
+ lastPersistedProgressAt = Date.now();
690
+ void saveAgentRecord(ctx, task).catch(() => undefined);
691
+ void appendAgentEvent(ctx, id, { phase: 'progress', message: line })
692
+ .catch(() => undefined);
693
+ }
694
+ },
695
+ };
696
+ try {
697
+ const result = await runAgentTask(coreInput, combined, managedContext, {
698
+ ...deps,
699
+ initialHistory,
700
+ existingWorktree: resumedWorktree,
701
+ onWorktreeCreated: async (worktree) => {
702
+ task.worktree = worktree;
703
+ await saveAgentRecord(ctx, task);
704
+ },
705
+ captureHistory: (history) => { captured = [...history]; },
706
+ });
707
+ task.result = result;
708
+ task.history = captured;
709
+ task.status = abort.signal.aborted ? 'stopped' : result.ok ? 'completed' : 'failed';
710
+ if (task.worktree && !await access(task.worktree.path).then(() => true).catch(() => false)) {
711
+ task.worktree = undefined;
712
+ }
713
+ task.updatedAt = Date.now();
714
+ await saveAgentRecord(ctx, task);
715
+ await appendAgentEvent(ctx, id, { phase: task.status, ok: result.ok });
716
+ return result;
717
+ }
718
+ catch (error) {
719
+ task.status = abort.signal.aborted ? 'stopped' : 'failed';
720
+ task.result = { ok: false, error: error.message };
721
+ task.history = captured;
722
+ task.updatedAt = Date.now();
723
+ await saveAgentRecord(ctx, task).catch(() => undefined);
724
+ await appendAgentEvent(ctx, id, { phase: task.status, error: error.message })
725
+ .catch(() => undefined);
726
+ return task.result;
727
+ }
728
+ finally {
729
+ task.events.emit('change');
730
+ settle();
731
+ }
732
+ };
733
+ if (input.run_in_background) {
734
+ void run();
735
+ return {
736
+ ok: true,
737
+ content: `后台 Agent 已启动。agent_id=${id}\n使用 TaskOutput 获取进度/结果,TaskStop 停止。`,
738
+ metadata: {
739
+ kind: 'agent', agentId: id, status: 'running',
740
+ agentType: input.agent_type ?? 'explore', description: input.description,
741
+ isolation: input.isolation ?? 'none', turns: 0, toolCalls: 0,
742
+ failedToolCalls: 0, durationMs: 0, changedFiles: [],
743
+ },
744
+ };
745
+ }
746
+ const result = await run();
747
+ if (result.ok && result.metadata?.kind === 'agent') {
748
+ result.metadata.agentId = id;
749
+ result.metadata.status = 'completed';
750
+ result.content = `agent_id=${id}\n${result.content}`;
751
+ }
752
+ else if (!result.ok) {
753
+ result.error = `agent_id=${id}\n${result.error}`;
754
+ }
755
+ return result;
756
+ }
628
757
  // ---------------- 5. 导出 ----------------
629
758
  export const agentTool = {
630
759
  name: 'Agent',
@@ -638,5 +767,5 @@ export const agentTool = {
638
767
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
639
768
  interruptBehavior: () => 'block',
640
769
  call,
641
- callWithContext: runAgentTask,
770
+ callWithContext: runManagedAgentTask,
642
771
  };