@armadra/agent 0.7.2 → 0.7.4

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 (178) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/CHANGELOG.zh-CN.md +41 -0
  3. package/dist/agent/retry.d.ts +14 -5
  4. package/dist/agent/retry.js +30 -8
  5. package/dist/agent/session-cache-key.d.ts +8 -0
  6. package/dist/agent/session-cache-key.js +46 -0
  7. package/dist/agent/session-cache.d.ts +2 -3
  8. package/dist/agent/session-cache.js +19 -37
  9. package/dist/agent/session-classifier.d.ts +3 -2
  10. package/dist/agent/session-classifier.js +36 -10
  11. package/dist/agent/session-compaction.d.ts +30 -5
  12. package/dist/agent/session-compaction.js +69 -29
  13. package/dist/agent/session-core.d.ts +2 -0
  14. package/dist/agent/session-run.d.ts +12 -0
  15. package/dist/agent/session-run.js +29 -10
  16. package/dist/agent/session-subagent.d.ts +8 -1
  17. package/dist/agent/session-subagent.js +37 -14
  18. package/dist/agent/session-sync.d.ts +8 -2
  19. package/dist/agent/session-sync.js +7 -10
  20. package/dist/agent/session-tools.js +3 -2
  21. package/dist/agent/subagent-fork.d.ts +67 -0
  22. package/dist/agent/subagent-fork.js +87 -0
  23. package/dist/agent/subagent-registry.d.ts +2 -2
  24. package/dist/agent/subagent-registry.js +2 -2
  25. package/dist/agent/tool-availability.d.ts +8 -0
  26. package/dist/agent/tool-availability.js +8 -0
  27. package/dist/agent/tool-runner.js +13 -4
  28. package/dist/agent/worktree.d.ts +8 -0
  29. package/dist/agent/worktree.js +51 -4
  30. package/dist/agents/parse.js +6 -1
  31. package/dist/agents/task-record.js +3 -0
  32. package/dist/agents/types.d.ts +2 -0
  33. package/dist/ai/apis/anthropic-messages.js +10 -5
  34. package/dist/ai/apis/anthropic-request.d.ts +2 -1
  35. package/dist/ai/apis/anthropic-request.js +9 -3
  36. package/dist/ai/apis/google-generative-ai.js +4 -2
  37. package/dist/ai/apis/max-tokens.d.ts +42 -0
  38. package/dist/ai/apis/max-tokens.js +121 -0
  39. package/dist/ai/apis/openai-completions.js +10 -5
  40. package/dist/ai/apis/openai-request.js +1 -1
  41. package/dist/ai/apis/openai-responses-request.js +1 -1
  42. package/dist/ai/apis/openai-responses.js +9 -5
  43. package/dist/ai/apis/shared.d.ts +3 -1
  44. package/dist/ai/apis/shared.js +21 -3
  45. package/dist/ai/cache/fingerprint.d.ts +4 -1
  46. package/dist/ai/cache/fingerprint.js +18 -1
  47. package/dist/ai/cache/miss.d.ts +2 -1
  48. package/dist/ai/cache/miss.js +9 -3
  49. package/dist/ai/cache/reporting.js +5 -1
  50. package/dist/ai/cache/types.d.ts +10 -2
  51. package/dist/ai/cache/warmer.d.ts +6 -1
  52. package/dist/ai/cache/warmer.js +7 -0
  53. package/dist/ai/context.d.ts +12 -4
  54. package/dist/ai/context.js +43 -24
  55. package/dist/ai/fake/fake-provider.d.ts +8 -1
  56. package/dist/ai/fake/fake-provider.js +19 -7
  57. package/dist/ai/fake/fake-script.d.ts +2 -0
  58. package/dist/ai/http.d.ts +17 -9
  59. package/dist/ai/http.js +33 -13
  60. package/dist/ai/image-intern.d.ts +29 -0
  61. package/dist/ai/image-intern.js +58 -0
  62. package/dist/ai/json-body.d.ts +31 -0
  63. package/dist/ai/json-body.js +208 -0
  64. package/dist/ai/overflow.js +5 -0
  65. package/dist/ai/providers/catalog-data.js +13 -13
  66. package/dist/ai/providers/catalog.d.ts +33 -0
  67. package/dist/ai/providers/catalog.js +105 -6
  68. package/dist/ai/providers/enrich.d.ts +13 -3
  69. package/dist/ai/providers/enrich.js +87 -31
  70. package/dist/ai/sse.d.ts +3 -2
  71. package/dist/ai/sse.js +3 -2
  72. package/dist/ai/types.d.ts +9 -2
  73. package/dist/bundle/ama.cjs +3142 -1805
  74. package/dist/cli/compose-agents.js +3 -0
  75. package/dist/cli/compose-extensions.js +2 -0
  76. package/dist/cli/compose-request.d.ts +20 -0
  77. package/dist/cli/compose-request.js +37 -0
  78. package/dist/cli/compose-session.js +2 -2
  79. package/dist/cli/subcommands/model-meta.d.ts +2 -1
  80. package/dist/cli/subcommands/model-meta.js +3 -2
  81. package/dist/codemode/host-side.d.ts +11 -4
  82. package/dist/codemode/host-side.js +25 -6
  83. package/dist/codemode/tool.d.ts +2 -0
  84. package/dist/codemode/tool.js +4 -0
  85. package/dist/compaction/prefix-estimate.d.ts +3 -1
  86. package/dist/compaction/prefix-estimate.js +7 -3
  87. package/dist/config/json-schema.js +9 -2
  88. package/dist/config/key-docs.js +13 -4
  89. package/dist/config/schema-w5.js +9 -1
  90. package/dist/config/schema.d.ts +1 -1
  91. package/dist/config/schema.js +19 -3
  92. package/dist/config/settings-registry.js +6 -0
  93. package/dist/config/types-w5.d.ts +2 -0
  94. package/dist/config/types.d.ts +19 -1
  95. package/dist/config/types.js +4 -0
  96. package/dist/i18n/catalog.d.ts +15 -2
  97. package/dist/i18n/messages/cli.d.ts +2 -0
  98. package/dist/i18n/messages/cli.js +2 -0
  99. package/dist/i18n/messages/config-keys.d.ts +10 -0
  100. package/dist/i18n/messages/config-keys.js +12 -2
  101. package/dist/i18n/messages/config.d.ts +14 -0
  102. package/dist/i18n/messages/config.js +4 -0
  103. package/dist/i18n/messages/errors.d.ts +3 -2
  104. package/dist/i18n/messages/errors.js +15 -2
  105. package/dist/i18n/messages/report-context.d.ts +4 -0
  106. package/dist/i18n/messages/report-context.js +4 -0
  107. package/dist/i18n/messages/report.d.ts +6 -0
  108. package/dist/i18n/messages/report.js +2 -0
  109. package/dist/i18n/messages/settings.d.ts +4 -0
  110. package/dist/i18n/messages/settings.js +6 -2
  111. package/dist/i18n/messages/subcommands.d.ts +2 -2
  112. package/dist/i18n/messages/subcommands.js +4 -2
  113. package/dist/modes/acp/acp-server.js +15 -5
  114. package/dist/modes/acp/acp-sessions.d.ts +9 -0
  115. package/dist/modes/acp/acp-sessions.js +25 -0
  116. package/dist/modes/context-report.d.ts +2 -0
  117. package/dist/modes/context-report.js +25 -8
  118. package/dist/modes/print/json-event.d.ts +12 -1
  119. package/dist/modes/print/json-event.js +48 -1
  120. package/dist/modes/rpc/rpc-mode.d.ts +1 -0
  121. package/dist/modes/rpc/rpc-mode.js +9 -2
  122. package/dist/modes/session-report.d.ts +1 -1
  123. package/dist/modes/session-report.js +12 -6
  124. package/dist/rpc.d.ts +5 -2
  125. package/dist/session/line-reader.d.ts +28 -0
  126. package/dist/session/line-reader.js +82 -0
  127. package/dist/session/list.d.ts +14 -0
  128. package/dist/session/list.js +150 -0
  129. package/dist/session/manager.d.ts +7 -3
  130. package/dist/session/manager.js +21 -48
  131. package/dist/session/migrate.d.ts +2 -1
  132. package/dist/session/migrate.js +2 -1
  133. package/dist/session/projection.d.ts +16 -3
  134. package/dist/session/projection.js +96 -29
  135. package/dist/session/scan.d.ts +3 -3
  136. package/dist/session/scan.js +9 -18
  137. package/dist/session/store.d.ts +3 -0
  138. package/dist/session/store.js +37 -31
  139. package/dist/session/types.d.ts +7 -2
  140. package/dist/tools/bash.d.ts +1 -1
  141. package/dist/tools/bash.js +5 -4
  142. package/dist/tools/glob.d.ts +2 -2
  143. package/dist/tools/glob.js +2 -2
  144. package/dist/tools/grep.js +4 -6
  145. package/dist/tools/image-file.js +2 -1
  146. package/dist/tools/read-lines.d.ts +32 -0
  147. package/dist/tools/read-lines.js +147 -0
  148. package/dist/tools/read.d.ts +6 -2
  149. package/dist/tools/read.js +47 -27
  150. package/dist/tools/task.d.ts +9 -0
  151. package/dist/tools/task.js +21 -8
  152. package/dist/tools/truncate.d.ts +7 -0
  153. package/dist/tools/truncate.js +11 -0
  154. package/dist/tools/types.d.ts +13 -0
  155. package/dist/trace/build-index.js +13 -1
  156. package/dist/tui/ansi.d.ts +2 -0
  157. package/dist/tui/ansi.js +7 -3
  158. package/dist/tui/components/editor-buffer.js +2 -2
  159. package/dist/tui/components/editor.js +5 -6
  160. package/dist/tui/keys.js +2 -2
  161. package/dist/tui/stdin-buffer.js +3 -3
  162. package/docs/acp.md +5 -1
  163. package/docs/agents.md +42 -6
  164. package/docs/codemode.md +4 -2
  165. package/docs/en/acp.md +3 -2
  166. package/docs/en/host-api.md +1 -1
  167. package/docs/en/permissions.md +2 -2
  168. package/docs/en/providers.md +54 -20
  169. package/docs/en/rpc.md +23 -7
  170. package/docs/en/sessions.md +4 -2
  171. package/docs/en/tui.md +3 -3
  172. package/docs/permissions.md +4 -1
  173. package/docs/providers.md +85 -22
  174. package/docs/rpc.md +22 -6
  175. package/docs/session-format.md +3 -2
  176. package/docs/sessions.md +3 -1
  177. package/docs/tui.md +2 -2
  178. package/package.json +2 -2
@@ -18,12 +18,16 @@
18
18
  * (只统计,不写分支、不改请求);附档一 / 档二阈值。压缩调度本身仍只看 `estimate()`。
19
19
  * [W3-C1b] 阈值 / 手动压缩与分支摘要优先走会话前缀续写(缓存控制器给前缀),溢出恢复不走
20
20
  * (前缀本身已超窗口);续写失败回落独立请求并记 warning。
21
+ * [ME-B] D6 档一 / 档二边界:先干跑档一;裁完仍超预算且续写可用(同模型、前缀成立、缓存未冷)→
22
+ * 不裁、直接续写摘要(被裁内容本来就进摘要,续写比全价独立请求便宜一个数量级);裁完够 → 只裁;
23
+ * 缓存已冷 → 照旧先裁,摘要走独立请求。D16 软窗口:`compaction.contextBudget` 设了时档一 / 档二 /
24
+ * 熔断 / context_pressure 的窗口取 min(模型窗口, 它)。
21
25
  */
22
26
  import { AmaError } from "../errors.js";
23
27
  import { CompactionBreaker } from "../compaction/breaker.js";
24
28
  import { estimateMessageTokens, estimateProjectedTokens, } from "../compaction/estimate.js";
25
- import { estimatePrefixTokens, withPrefixBaseline } from "../compaction/prefix-estimate.js";
26
- import { planPrune, prunePolicy } from "../compaction/prune-tier.js";
29
+ import { prefixSystemMessage, withPrefixBaseline } from "../compaction/prefix-estimate.js";
30
+ import { planPrune, prunePolicy, } from "../compaction/prune-tier.js";
27
31
  import { createProtection, skillLocations } from "../compaction/protect.js";
28
32
  import { buildPostCompactBlock } from "../compaction/post-compact.js";
29
33
  import { prepareCompaction, prepareCompactionAt, runCompaction, } from "../compaction/summarize-tier.js";
@@ -37,6 +41,16 @@ export const DEFAULT_COMPACTION_SETTINGS = {
37
41
  };
38
42
  /** 保留区不超过 (窗口 − 预留) 的 40%,否则小窗口模型上摘要后仍然溢出。 */
39
43
  export const KEEP_RECENT_MAX_RATIO = 0.4;
44
+ /**
45
+ * [ME-B] D16 软窗口:模型窗口与 `compaction.contextBudget` 取小;窗口未知仍 undefined(不压缩)。
46
+ * `compaction` 是会话选项里展开的 config 段(`contextBudget` 不在 CompactionSettings 类型上)。
47
+ */
48
+ export function effectiveWindow(contextWindow, compaction) {
49
+ const budget = compaction?.contextBudget;
50
+ if (contextWindow === undefined || budget === undefined)
51
+ return contextWindow;
52
+ return Math.min(contextWindow, budget);
53
+ }
40
54
  /** Hook additionalContext 的 custom 类型(与 UserPromptSubmit 的相同)。 */
41
55
  const HOOK_CONTEXT_CUSTOM_TYPE = "ama.hook_context";
42
56
  export class CompactionController {
@@ -56,14 +70,18 @@ export class CompactionController {
56
70
  this.pruneConfig.prune = extra.prune;
57
71
  if (extra.pruneExclude !== undefined)
58
72
  this.pruneConfig.pruneExclude = extra.pruneExclude;
59
- this.breaker = new CompactionBreaker(this.settings.enabled, core.model().contextWindow);
73
+ this.breaker = new CompactionBreaker(this.settings.enabled, this.window());
74
+ }
75
+ /** 档一 / 档二 / 熔断用的窗口(软窗口生效后的值)。 */
76
+ window() {
77
+ return effectiveWindow(this.core.model().contextWindow, this.settings);
60
78
  }
61
79
  get isCompacting() {
62
80
  return this.compacting;
63
81
  }
64
82
  /** 模型或开关变化后调用。 */
65
83
  refresh() {
66
- this.breaker.configure(this.settings.enabled, this.core.model().contextWindow);
84
+ this.breaker.configure(this.settings.enabled, this.window());
67
85
  }
68
86
  setAuto(enabled) {
69
87
  this.settings = { ...this.settings, enabled };
@@ -96,17 +114,10 @@ export class CompactionController {
96
114
  }
97
115
  /** 将要发送的系统提示 + 工具声明的估算(与首条 system 消息同样装配,不落盘)。 */
98
116
  prefixTokens() {
99
- const core = this.core;
100
- const tools = core
101
- .activeToolNames()
102
- .map((name) => core.activeTool(name))
103
- .filter((tool) => tool !== undefined);
104
- const system = core.childBase?.().system ?? core.options.system ?? {};
105
- const sections = definedSections(assembleSections({ ...system, tools, cwd: core.cwd }));
106
- return estimatePrefixTokens(sections, toolDecls(tools));
117
+ return estimateMessageTokens(pendingPrefixMessage(this.core));
107
118
  }
108
119
  budget() {
109
- const window = this.core.model().contextWindow;
120
+ const window = this.window();
110
121
  return window === undefined ? undefined : Math.max(0, window - this.settings.reserveTokens);
111
122
  }
112
123
  keepRecent() {
@@ -115,7 +126,10 @@ export class CompactionController {
115
126
  return this.settings.keepRecentTokens;
116
127
  return Math.max(1, Math.min(this.settings.keepRecentTokens, Math.floor(budget * KEEP_RECENT_MAX_RATIO)));
117
128
  }
118
- /** 摘要请求的公共选项;`continuation` 时附上会话前缀续写的材料。 */
129
+ /**
130
+ * 摘要请求的公共选项;`continuation` 时附上会话前缀续写的材料(传入对象 = 调用方已算好的那份,
131
+ * 避免重算)。
132
+ */
119
133
  async summarizer(signal, instructions, continuation) {
120
134
  const core = this.core;
121
135
  const options = {
@@ -127,7 +141,9 @@ export class CompactionController {
127
141
  onFallback: (reason) => core.log("warn", `summary by prefix continuation failed (${reason}); used a separate request`),
128
142
  onInvalid: (reason) => core.log("warn", `${reason}; kept it after one retry`),
129
143
  };
130
- if (continuation)
144
+ if (typeof continuation === "object")
145
+ options.continuation = continuation;
146
+ else if (continuation)
131
147
  options.continuation = core.cache?.summaryContinuation();
132
148
  return options;
133
149
  }
@@ -136,10 +152,8 @@ export class CompactionController {
136
152
  const budget = this.budget();
137
153
  return budget === undefined ? undefined : prunePolicy(budget, this.pruneConfig.prune);
138
154
  }
139
- /**
140
- * 档一;`need` = 清到目标还要省多少(undefined = 全部候选)。返回省下的 token 估算(0 = 未裁)。
141
- */
142
- prune(policy, need) {
155
+ /** 档一干跑;`need` = 清到目标还要省多少(undefined = 全部候选)。 */
156
+ prunePlan(policy, need) {
143
157
  const core = this.core;
144
158
  const items = buildProjection(core.manager.branch()).items;
145
159
  const isProtected = createProtection({
@@ -148,7 +162,10 @@ export class CompactionController {
148
162
  exclude: this.pruneConfig.pruneExclude ?? [],
149
163
  keepInContext: (name) => core.tool(name)?.annotations?.keepInContext === true,
150
164
  });
151
- const plan = planPrune(items, { policy, need, outputDir: core.outputDir(), isProtected });
165
+ return planPrune(items, { policy, need, outputDir: core.outputDir(), isProtected });
166
+ }
167
+ /** 档一落盘:写 context_edit 并重载消息。返回省下的 token 估算(0 = 未裁)。 */
168
+ applyPrune(plan) {
152
169
  for (const item of plan.items) {
153
170
  this.core.appendEntry({
154
171
  type: "context_edit",
@@ -161,7 +178,10 @@ export class CompactionController {
161
178
  this.core.reloadMessages();
162
179
  return plan.savedTokens;
163
180
  }
164
- /** 阈值检查(档一 → 档二)。失败只记录,不打断 run。 */
181
+ /**
182
+ * 阈值检查(档一 → 档二,[ME-B] D6)。失败只记录,不打断 run。
183
+ * 先干跑档一:裁完仍超预算、熔断允许且续写可用 → 跳过裁剪直接续写摘要;否则照旧先裁。
184
+ */
165
185
  async checkThreshold(signal) {
166
186
  if (!this.breaker.autoEnabled || this.compacting || signal.aborted)
167
187
  return;
@@ -173,17 +193,24 @@ export class CompactionController {
173
193
  let tokens = this.estimate().tokens;
174
194
  // 缓存已冷(C3):前缀反正要重写,未到阈值也把候选一次换掉(仍要可省 ≥ clearAtLeast)
175
195
  const cold = this.core.cache?.isCold() ?? false;
196
+ let continuation;
176
197
  if (tokens > policy.triggerTokens || cold) {
177
- const need = cold ? undefined : tokens - policy.targetTokens;
178
- if (this.prune(policy, need) > 0)
198
+ const plan = this.prunePlan(policy, cold ? undefined : tokens - policy.targetTokens);
199
+ if (!cold && tokens - plan.savedTokens > budget && this.summaryAllowed(budget))
200
+ continuation = this.core.cache?.summaryContinuation();
201
+ if (continuation === undefined && this.applyPrune(plan) > 0)
179
202
  tokens = this.estimate().tokens;
180
203
  }
181
204
  if (tokens <= budget)
182
205
  return;
183
- this.breaker.setPrefixOverflow(this.fixedPrefixTokens() > budget);
184
- if (!this.breaker.canSummarize())
206
+ if (!this.summaryAllowed(budget))
185
207
  return this.warnBlocked();
186
- await this.summarize("threshold", signal);
208
+ await this.summarize("threshold", signal, undefined, undefined, continuation);
209
+ }
210
+ /** 熔断是否允许自动摘要(顺带更新「固定前缀超预算」)。 */
211
+ summaryAllowed(budget) {
212
+ this.breaker.setPrefixOverflow(this.fixedPrefixTokens() > budget);
213
+ return this.breaker.canSummarize();
187
214
  }
188
215
  /** 回注块(todo、计划、已加载 Skill、最近文件、转录与 outputs 路径;不含文件正文)。 */
189
216
  postCompactBlock() {
@@ -256,7 +283,7 @@ export class CompactionController {
256
283
  async recoverOverflow(signal) {
257
284
  if (!this.settings.enabled)
258
285
  return { retry: false, warning: "context overflow: auto-compaction is disabled" };
259
- if (this.core.model().contextWindow === undefined) {
286
+ if (this.window() === undefined) {
260
287
  return {
261
288
  retry: false,
262
289
  warning: "context overflow: model has no contextWindow, auto-compaction is off",
@@ -314,7 +341,7 @@ export class CompactionController {
314
341
  details: draft.details,
315
342
  });
316
343
  }
317
- async summarize(trigger, signal, instructions, cutAt) {
344
+ async summarize(trigger, signal, instructions, cutAt, continuation) {
318
345
  const core = this.core;
319
346
  this.compacting = true;
320
347
  core.emit({ type: "compaction_start", trigger });
@@ -366,7 +393,7 @@ export class CompactionController {
366
393
  if (plan === undefined)
367
394
  return fail("nothing to compact");
368
395
  try {
369
- const options = await this.summarizer(signal, customInstructions, trigger !== "overflow");
396
+ const options = await this.summarizer(signal, customInstructions, trigger === "overflow" ? false : (continuation ?? true));
370
397
  const draft = await runCompaction(plan, options);
371
398
  if (signal.aborted)
372
399
  return fail("aborted", true);
@@ -411,3 +438,16 @@ export class CompactionController {
411
438
  }
412
439
  }
413
440
  }
441
+ /**
442
+ * 首次请求前将要发送的首条 `system` 消息(系统提示 + 工具声明,与真正发送时同样装配)。只用于统计:
443
+ * `getStats` 的启动基线与 `/context` 的分类明细共用它,口径一致;不写分支、不改请求体。
444
+ */
445
+ export function pendingPrefixMessage(core) {
446
+ const tools = core
447
+ .activeToolNames()
448
+ .map((name) => core.activeTool(name))
449
+ .filter((tool) => tool !== undefined);
450
+ const system = core.childBase?.().system ?? core.options.system ?? {};
451
+ const sections = definedSections(assembleSections({ ...system, tools, cwd: core.cwd }));
452
+ return prefixSystemMessage(sections, toolDecls(tools));
453
+ }
@@ -35,6 +35,8 @@ export interface AgentSessionOptions {
35
35
  tools?: readonly ToolDefinition[];
36
36
  /** 活动集;缺省 = tools 全部。 */
37
37
  activeTools?: readonly string[];
38
+ /** [ME-C0] 留在请求工具表里、执行时拒绝的工具(fork 子会话的 tools / disallowed-tools)。 */
39
+ unavailableTools?: readonly string[];
38
40
  /** 权限管线(B3);缺省 = 全部放行。 */
39
41
  permission?: PermissionPipelineApi;
40
42
  /** 审批回答者链:宿主 broker → UI broker;都返回 undefined → deny(无人值守)。 */
@@ -12,6 +12,9 @@
12
12
  * - 最终失败后若 followUp 队列非空,照常投递。
13
13
  * - [W5-H2] 模型回退:可重试错误在 overloaded 时、或重试用尽后,若配置了 `fallbackModel` 就切过去
14
14
  * 重试一次本请求(`model_fallback` 事件),回退模型回复后切回主模型;每个周期最多一次。
15
+ * [ME-C] overloaded 先快速重试一次(1 s 基数 + 抖动,计一次 attempt),仍失败才回退(D8)。
16
+ * - [ME-C] 退避取 `retryDelayMs(attempt, settings, 失败消息的 retryAfterMs)`;限流(rate_limited)的
17
+ * 重试上限 = maxRetries + 2,`auto_retry_start.maxAttempts` 随之。
15
18
  * - [W5-H2] 扩展经 `noteSettleWarning` 登记的 warning(预算到限 `limit_reached`)写进本周期的 agent_settled;
16
19
  * `limit_reached:` 开头的错误回复(预算拦下请求)不重试。
17
20
  * - [W5-H2] run 带 `warning`(重复调用检测 `repeated_tool_call`):不跑 Stop Hook,agent_settled 带该 warning。
@@ -19,6 +22,7 @@
19
22
  import type { AssistantMessage, ImageBlock, UserMessage } from "../ai/types.js";
20
23
  import type { AgentMessage } from "../session/types.js";
21
24
  import type { RunOutcome } from "./loop.js";
25
+ import { type FailureKind } from "./retry.js";
22
26
  import type { SessionCore } from "./session-core.js";
23
27
  import type { CompactionController } from "./session-compaction.js";
24
28
  import type { PromptDisposition, RetrySettings } from "./types.js";
@@ -34,6 +38,12 @@ export type RunDecision = {
34
38
  } | {
35
39
  kind: "retry";
36
40
  errorMessage: string;
41
+ /** [ME-C] 本次失败的种类(决定重试上限)。 */
42
+ failure?: FailureKind;
43
+ /** [ME-C] overloaded 回退前的快速重试:1 s 基数。 */
44
+ quick?: boolean;
45
+ /** [ME-C] 失败响应的 Retry-After(毫秒)。 */
46
+ retryAfterMs?: number;
37
47
  } | {
38
48
  kind: "overflow";
39
49
  } | {
@@ -61,6 +71,8 @@ export interface RunCycleDeps {
61
71
  afterSettled?(): Promise<void>;
62
72
  }
63
73
  export declare function decideAfterRun(deps: RunCycleDeps, outcome: RunOutcome, retryAttempt: number, overflowRecovered: boolean, fallbackUsed?: boolean): RunDecision;
74
+ /** [ME-C] overloaded 快速重试的退避基数(设置的基数更小时取设置)。 */
75
+ export declare const QUICK_RETRY_BASE_MS = 1000;
64
76
  /** 用 context_edit 把失败尝试(助手消息与其后补的结果)从上下文剔除。 */
65
77
  export declare function excludeFailedAttempt(core: SessionCore, failed: AssistantMessage | undefined, reason: "retry" | "overflow"): void;
66
78
  export declare function recordAbort(core: SessionCore): void;
@@ -12,13 +12,16 @@
12
12
  * - 最终失败后若 followUp 队列非空,照常投递。
13
13
  * - [W5-H2] 模型回退:可重试错误在 overloaded 时、或重试用尽后,若配置了 `fallbackModel` 就切过去
14
14
  * 重试一次本请求(`model_fallback` 事件),回退模型回复后切回主模型;每个周期最多一次。
15
+ * [ME-C] overloaded 先快速重试一次(1 s 基数 + 抖动,计一次 attempt),仍失败才回退(D8)。
16
+ * - [ME-C] 退避取 `retryDelayMs(attempt, settings, 失败消息的 retryAfterMs)`;限流(rate_limited)的
17
+ * 重试上限 = maxRetries + 2,`auto_retry_start.maxAttempts` 随之。
15
18
  * - [W5-H2] 扩展经 `noteSettleWarning` 登记的 warning(预算到限 `limit_reached`)写进本周期的 agent_settled;
16
19
  * `limit_reached:` 开头的错误回复(预算拦下请求)不重试。
17
20
  * - [W5-H2] run 带 `warning`(重复调用检测 `repeated_tool_call`):不跑 Stop Hook,agent_settled 带该 warning。
18
21
  */
19
22
  import { AmaError } from "../errors.js";
20
23
  import { formatModelRef, modelRefOf } from "../ai/providers/channels.js";
21
- import { classifyFailure, retryDelayMs, sleep } from "./retry.js";
24
+ import { classifyFailure, isRetryableKind, maxRetriesFor, retryDelayMs, sleep, } from "./retry.js";
22
25
  export const MAX_STOP_HOOK_CONTINUATIONS = 3;
23
26
  export const ABORTED_CUSTOM_TYPE = "ama.aborted";
24
27
  /** [W5-H2] 预算拦下请求时错误回复的前缀(limits.ts):判为最终失败,不重试、不回退。 */
@@ -59,17 +62,34 @@ export function decideAfterRun(deps, outcome, retryAttempt, overflowRecovered, f
59
62
  return recoverable ? { kind: "overflow" } : { kind: "failed", errorMessage };
60
63
  }
61
64
  const settings = deps.retry();
62
- const canFallback = kind === "retryable" && !fallbackUsed && fallbackTarget(deps.core) !== undefined;
63
- if (canFallback && OVERLOADED.test(errorMessage))
64
- return { kind: "fallback", errorMessage };
65
- if (kind === "retryable" && settings.enabled && retryAttempt < settings.maxRetries) {
66
- return { kind: "retry", errorMessage };
65
+ const canFallback = isRetryableKind(kind) && !fallbackUsed && fallbackTarget(deps.core) !== undefined;
66
+ const retry = { kind: "retry", errorMessage, failure: kind };
67
+ if (last.retryAfterMs !== undefined)
68
+ retry.retryAfterMs = last.retryAfterMs;
69
+ if (canFallback && OVERLOADED.test(errorMessage)) {
70
+ return retryAttempt === 0 && settings.enabled
71
+ ? { ...retry, quick: true }
72
+ : { kind: "fallback", errorMessage };
73
+ }
74
+ if (isRetryableKind(kind) && settings.enabled && retryAttempt < maxRetriesFor(kind, settings)) {
75
+ return retry;
67
76
  }
68
77
  if (canFallback)
69
78
  return { kind: "fallback", errorMessage };
70
79
  return { kind: "failed", errorMessage };
71
80
  }
72
81
  const OVERLOADED = /overloaded/i;
82
+ /** [ME-C] overloaded 快速重试的退避基数(设置的基数更小时取设置)。 */
83
+ export const QUICK_RETRY_BASE_MS = 1000;
84
+ /** 本次重试的等待与上限(`auto_retry_start` 的 delayMs / maxAttempts)。 */
85
+ function retryPlan(decision, attempt, settings) {
86
+ const quickBase = Math.min(QUICK_RETRY_BASE_MS, settings.baseDelayMs);
87
+ const base = decision.quick === true ? { ...settings, baseDelayMs: quickBase } : settings;
88
+ return {
89
+ delayMs: retryDelayMs(decision.quick === true ? 1 : attempt, base, decision.retryAfterMs),
90
+ maxAttempts: maxRetriesFor(decision.failure ?? "retryable", settings),
91
+ };
92
+ }
73
93
  /** 配置了与当前模型不同、且能找到的回退模型时返回它的引用。 */
74
94
  function fallbackTarget(core) {
75
95
  const target = core.options.fallbackModel?.trim();
@@ -225,19 +245,18 @@ async function runSettledCycle(deps, prompts, signal) {
225
245
  retryAttempt++;
226
246
  deps.setRetrying(true);
227
247
  excludeFailedAttempt(core, outcome.lastAssistant, "retry");
228
- const settings = deps.retry();
229
- const delayMs = retryDelayMs(retryAttempt, settings);
248
+ const { delayMs, maxAttempts } = retryPlan(current, retryAttempt, deps.retry());
230
249
  core.emit({
231
250
  type: "auto_retry_start",
232
251
  attempt: retryAttempt,
233
- maxAttempts: settings.maxRetries,
252
+ maxAttempts,
234
253
  delayMs,
235
254
  errorMessage: current.errorMessage,
236
255
  });
237
256
  void core.runHook("Notification", {
238
257
  notification: {
239
258
  kind: "retry",
240
- message: `retry ${retryAttempt}/${settings.maxRetries}: ${current.errorMessage}`,
259
+ message: `retry ${retryAttempt}/${maxAttempts}: ${current.errorMessage}`,
241
260
  },
242
261
  });
243
262
  try {
@@ -11,7 +11,11 @@
11
11
  * 声明了 `tools` / `disallowed-tools`(或调用参数给了 `tools`)时工具表才不同。
12
12
  * - 只读类型(`permission-mode: plan`):子会话用一条 plan 模式的权限管线(规则同父),ask 一律转
13
13
  * deny——写类工具与识别不了的 bash 被拒且不弹审批;`inherit` 共用父的管线(不会比父宽)。
14
- * - 轮数耗尽且最后停在工具结果上:以 `toolChoice:"none"` 再跑一轮要最终报告,状态 `max_turns`。
14
+ * - 轮数耗尽且最后停在工具结果上:以 `maxTurns: 1` 再跑一轮要最终报告(只靠提示、不发 `toolChoice`,
15
+ * [ME-A] D13 请求选项不变才能命中缓存;仍调用工具则结果回落为「没有文本」),状态 `max_turns`。
16
+ * - [ME-A] `context: "fork"`(D1–D3,subagent-fork.ts):子会话复制父分支到发起调用的 assistant 之前,
17
+ * 不写 `role` 节、工具表与父相同、类型限制改由 `unavailableTools` 在执行层拒绝;任务以 `<task>` 消息
18
+ * 追加在继承的历史之后。不满足条件时回落 fresh(记 info 日志),结果的 `context` 标实际模式。
15
19
  * - broker 包一层,请求带 `context{depth, parentToolCallId, taskId}`,审批事件转发到父会话;
16
20
  * [RW-B] 父的检查点钩子(worktree 里运行时不记);[W3-C1b] 子会话命中与重计费汇总进父。
17
21
  * - [W6-A] 句柄外露 `observe` / `entries`(子 Agent 视图跟随)与 `message`(视图里直接发的消息,
@@ -23,6 +27,7 @@ import { SessionManager } from "../session/manager.js";
23
27
  import type { AgentMessage } from "../session/types.js";
24
28
  import type { MessageOrigin } from "../ai/types.js";
25
29
  import type { SubagentRequest, SubagentResult, SubagentRunner } from "../tools/types.js";
30
+ import type { RequestRecord } from "../ai/cache/types.js";
26
31
  import type { AgentSessionOptions } from "./session-core.js";
27
32
  import { DEFAULT_SUBAGENT_CONCURRENCY, SubagentPool, TASK_CUSTOM_TYPE, type AmaRunnerSpec, type RegistryHost } from "./subagent-registry.js";
28
33
  import type { SessionEvent, SessionStats } from "./types.js";
@@ -47,6 +52,8 @@ export interface SubagentParent extends RegistryHost {
47
52
  cacheRead: number;
48
53
  prompt: number;
49
54
  }, reBilledTokens: number): void;
55
+ /** [ME-A] 父会话上一次真实请求(fork 的前提与上下文比例)。 */
56
+ readonly lastTurn?: RequestRecord | undefined;
50
57
  };
51
58
  }
52
59
  export interface ChildSession {
@@ -11,7 +11,11 @@
11
11
  * 声明了 `tools` / `disallowed-tools`(或调用参数给了 `tools`)时工具表才不同。
12
12
  * - 只读类型(`permission-mode: plan`):子会话用一条 plan 模式的权限管线(规则同父),ask 一律转
13
13
  * deny——写类工具与识别不了的 bash 被拒且不弹审批;`inherit` 共用父的管线(不会比父宽)。
14
- * - 轮数耗尽且最后停在工具结果上:以 `toolChoice:"none"` 再跑一轮要最终报告,状态 `max_turns`。
14
+ * - 轮数耗尽且最后停在工具结果上:以 `maxTurns: 1` 再跑一轮要最终报告(只靠提示、不发 `toolChoice`,
15
+ * [ME-A] D13 请求选项不变才能命中缓存;仍调用工具则结果回落为「没有文本」),状态 `max_turns`。
16
+ * - [ME-A] `context: "fork"`(D1–D3,subagent-fork.ts):子会话复制父分支到发起调用的 assistant 之前,
17
+ * 不写 `role` 节、工具表与父相同、类型限制改由 `unavailableTools` 在执行层拒绝;任务以 `<task>` 消息
18
+ * 追加在继承的历史之后。不满足条件时回落 fresh(记 info 日志),结果的 `context` 标实际模式。
15
19
  * - broker 包一层,请求带 `context{depth, parentToolCallId, taskId}`,审批事件转发到父会话;
16
20
  * [RW-B] 父的检查点钩子(worktree 里运行时不记);[W3-C1b] 子会话命中与重计费汇总进父。
17
21
  * - [W6-A] 句柄外露 `observe` / `entries`(子 Agent 视图跟随)与 `message`(视图里直接发的消息,
@@ -22,6 +26,7 @@ import { PermissionPipeline } from "../permissions/pipeline.js";
22
26
  import { SessionManager } from "../session/manager.js";
23
27
  import { agentRole } from "../agents/builtin.js";
24
28
  import { ZERO_USAGE } from "./loop.js";
29
+ import { forkBrief, forkPlan, requestedContext } from "./subagent-fork.js";
25
30
  import { DEFAULT_SUBAGENT_CONCURRENCY, SubagentPool, TASK_CUSTOM_TYPE, subagentRegistryFor, } from "./subagent-registry.js";
26
31
  export { DEFAULT_SUBAGENT_CONCURRENCY, SubagentPool, TASK_CUSTOM_TYPE };
27
32
  export const DEFAULT_SUBAGENT_MAX_TURNS = 30;
@@ -130,22 +135,29 @@ async function startAmaChild(parent, spec, createChild, run) {
130
135
  throw new AmaError("model_not_found", `unknown model ${spec.modelRef}`);
131
136
  model = lookup.model;
132
137
  }
133
- const manager = childManager(parent, spec);
138
+ const thinkingLevel = spec.request.thinkingLevel ?? spec.agent.thinking ?? base.thinkingLevel ?? "off";
139
+ const wanted = spec.resumeFile === undefined && requestedContext(spec.request, spec.agent) === "fork";
140
+ const plan = wanted ? forkPlan(parent, spec, model, thinkingLevel) : undefined;
141
+ if (plan !== undefined && "fallback" in plan)
142
+ parent.log("info", `task ${spec.taskId}: fork falls back to fresh (${plan.fallback})`);
143
+ const forked = plan !== undefined && "manager" in plan ? plan.manager : undefined;
144
+ const mode = !wanted ? undefined : forked ? "fork" : "fresh";
145
+ const manager = forked ?? childManager(parent, spec);
134
146
  const checkpointHooks = spec.isolated ? undefined : parent.checkpointHooks?.();
135
- let finalRound = false;
136
- const finalRoundExtension = () => ({
137
- id: "ama.subagent-final-round",
138
- wrapStream: (stream) => (m, context, options) => stream(m, context, finalRound ? { ...options, toolChoice: "none" } : options),
139
- });
140
147
  const maxTurns = spec.request.maxTurns ?? spec.agent.maxTurns ?? DEFAULT_SUBAGENT_MAX_TURNS;
148
+ const allowed = childToolNames(parent, spec, base.activeTools);
149
+ // fork:工具表 = 父活动集(前缀不变),类型限制的工具在执行层拒绝
150
+ const inherited = base.activeTools ?? (parent.options.tools ?? []).map((tool) => tool.name);
151
+ const unavailable = inherited.filter((name) => !allowed.includes(name) && !PARENT_ONLY_TOOLS.includes(name));
141
152
  const options = {
142
153
  ...parent.options,
143
154
  ...base,
144
- system: { ...base.system, role: agentRole(spec.agent) },
155
+ // fork:系统提示与父相同(base.system,不加 role 节)
156
+ ...(forked === undefined ? { system: { ...base.system, role: agentRole(spec.agent) } } : {}),
145
157
  sessionManager: manager,
146
158
  model,
147
- thinkingLevel: spec.request.thinkingLevel ?? spec.agent.thinking ?? base.thinkingLevel ?? "off",
148
- activeTools: childToolNames(parent, spec, base.activeTools),
159
+ thinkingLevel,
160
+ ...(forked === undefined ? { activeTools: allowed } : {}),
149
161
  brokers: brokersForChild(parent.options.brokers, {
150
162
  depth: parent.depth + 1,
151
163
  parentToolCallId: spec.request.parentToolCallId,
@@ -154,8 +166,9 @@ async function startAmaChild(parent, spec, createChild, run) {
154
166
  depth: parent.depth + 1,
155
167
  maxTurns,
156
168
  subagents: false,
157
- extensions: [...(parent.options.extensions ?? []), finalRoundExtension],
158
169
  };
170
+ if (forked !== undefined && unavailable.length > 0)
171
+ options.unavailableTools = [...(parent.options.unavailableTools ?? []), ...unavailable];
159
172
  delete options.checkpointHooks;
160
173
  if (checkpointHooks !== undefined)
161
174
  options.checkpointHooks = checkpointHooks;
@@ -182,6 +195,8 @@ async function startAmaChild(parent, spec, createChild, run) {
182
195
  else if (event.type === "permission_request" || event.type === "permission_resolved")
183
196
  parent.emit(event);
184
197
  });
198
+ if (mode !== undefined)
199
+ run.onEvent({ type: "context", mode });
185
200
  const onAbort = () => void child.abort();
186
201
  run.signal.addEventListener("abort", onAbort, { once: true });
187
202
  let billed = { cacheRead: 0, prompt: 0, reBilled: 0 };
@@ -212,7 +227,6 @@ async function startAmaChild(parent, spec, createChild, run) {
212
227
  active = false;
213
228
  const exhausted = error === undefined && !run.signal.aborted && turns - startTurns >= maxTurns && toolUse;
214
229
  if (exhausted) {
215
- finalRound = true;
216
230
  options.maxTurns = 1;
217
231
  try {
218
232
  await child.prompt(FINAL_REPORT_PROMPT);
@@ -221,18 +235,27 @@ async function startAmaChild(parent, spec, createChild, run) {
221
235
  error = caught instanceof Error ? caught.message : String(caught);
222
236
  }
223
237
  finally {
224
- finalRound = false;
225
238
  options.maxTurns = maxTurns;
226
239
  }
227
240
  }
228
241
  const result = collect(child, error, exhausted, run.signal.aborted);
242
+ if (mode !== undefined)
243
+ result.context = mode;
229
244
  billed = addCache(parent, child.getStats(), billed);
230
245
  const file = manager.file();
231
246
  if (file !== undefined)
232
247
  result.sessionFile = file;
233
248
  return result;
234
249
  };
235
- let current = runOnce(run.prompt, spec.origin);
250
+ const first = forked === undefined
251
+ ? run.prompt
252
+ : forkBrief({
253
+ prompt: run.prompt,
254
+ role: spec.agent.prompt,
255
+ unavailable,
256
+ ...(spec.isolated ? { worktree: { cwd: spec.cwd, parentCwd: parent.cwd } } : {}),
257
+ });
258
+ let current = runOnce(first, spec.origin);
236
259
  const handle = {
237
260
  id: manager.id,
238
261
  model: `${model.provider}/${model.id}`,
@@ -4,7 +4,8 @@
4
4
  * - `persistMessage`:Agent 的 message_end → 会话条目(LLM 消息 → `message`;custom → `custom_message`)。
5
5
  * - `recordModelState`:路径上最近的 model_change / thinking_level_change 与当前不同则补记。
6
6
  * - `syncSystemMessage`:目标系统提示 + 工具表与转录重放的状态比对,落全量(首条)或补丁;
7
- * 中途只追加工具时 tools / rules 节保持已发送的文本(前缀稳定,§9.1)。
7
+ * 对话开始后 tools / rules 节冻结为已发送的文本([ME-B] D5:工具增删只经请求工具表与尾部提醒体现,
8
+ * 开头永不改写,§9.1)。
8
9
  * - `runHookWithEvents`:调 HookDispatcher(公共字段按本会话的 depth / sessionId / sessionFile 覆盖),
9
10
  * 逐条发 hook_executed;Hook 自身出错记 warning、按无决策处理。
10
11
  */
@@ -15,13 +16,18 @@ import type { ToolDefinition } from "../tools/types.js";
15
16
  import type { SessionManager } from "../session/manager.js";
16
17
  import type { SessionCore } from "./session-core.js";
17
18
  import type { SessionEvent } from "./types.js";
18
- import { type SystemPromptInput } from "./system-prompt.js";
19
+ import { currentSystemState, type SystemPromptInput } from "./system-prompt.js";
19
20
  export declare function persistMessage(core: SessionCore, message: AgentMessage): SessionEntry | undefined;
20
21
  export declare function recordModelState(core: SessionCore, model: Model, thinkingLevel: ModelThinkingLevel): void;
21
22
  /** 落一条 model_change,返回模型引用(`setModel` 发 model_changed 用)。 */
22
23
  export declare function appendModelChange(core: SessionCore, model: Model): ModelRef;
23
24
  export declare function findModelOrThrow(providers: ProviderRegistryApi, ref: string): Model;
24
25
  export declare function syncSystemMessage(core: SessionCore, input: Omit<SystemPromptInput, "tools" | "cwd">, tools: readonly ToolDefinition[]): void;
26
+ /**
27
+ * 对话开始后保留已发送的 tools / rules 节:新工具的声明经请求工具表末尾追加,移除的工具保留声明、
28
+ * 尾部提醒「no longer available」,system 文本不变,前缀缓存不失效(设计 §9.1、[ME-B] D5)。
29
+ */
30
+ export declare function keepSectionsAfterStart(current: NonNullable<ReturnType<typeof currentSystemState>>, sections: Record<string, string>): void;
25
31
  export declare function runHookWithEvents(core: Pick<SessionCore, "options" | "emit" | "log" | "depth" | "manager">, event: HookEvent, payload: HookEventPayload, signal?: AbortSignal): Promise<HookOutcome | undefined>;
26
32
  /** session_start 事件(bootstrap / SDK 在会话就绪后发一次)。 */
27
33
  export declare function sessionStartEvent(manager: SessionManager, reason: "startup" | "resume" | "new" | "fork"): SessionEvent;
@@ -4,7 +4,8 @@
4
4
  * - `persistMessage`:Agent 的 message_end → 会话条目(LLM 消息 → `message`;custom → `custom_message`)。
5
5
  * - `recordModelState`:路径上最近的 model_change / thinking_level_change 与当前不同则补记。
6
6
  * - `syncSystemMessage`:目标系统提示 + 工具表与转录重放的状态比对,落全量(首条)或补丁;
7
- * 中途只追加工具时 tools / rules 节保持已发送的文本(前缀稳定,§9.1)。
7
+ * 对话开始后 tools / rules 节冻结为已发送的文本([ME-B] D5:工具增删只经请求工具表与尾部提醒体现,
8
+ * 开头永不改写,§9.1)。
8
9
  * - `runHookWithEvents`:调 HookDispatcher(公共字段按本会话的 depth / sessionId / sessionFile 覆盖),
9
10
  * 逐条发 hook_executed;Hook 自身出错记 warning、按无决策处理。
10
11
  */
@@ -71,8 +72,8 @@ export function syncSystemMessage(core, input, tools) {
71
72
  recordModelState(core, core.model(), core.thinkingLevel());
72
73
  const sections = definedSections(assembleSections({ ...input, tools, cwd: core.cwd }));
73
74
  const current = currentSystemState(core.agent.messages);
74
- if (current !== undefined)
75
- keepSectionsOnToolAppend(current, sections, tools);
75
+ if (current !== undefined && core.agent.messages.some((message) => message.role !== "system"))
76
+ keepSectionsAfterStart(current, sections);
76
77
  const patch = diffSystem(current, sections, toolDecls(tools));
77
78
  if (patch === undefined)
78
79
  return;
@@ -80,14 +81,10 @@ export function syncSystemMessage(core, input, tools) {
80
81
  core.agent.messages.push(patch);
81
82
  }
82
83
  /**
83
- * 会话中途只**追加**工具(宿主晚注册、setActiveTools 加名字)时,保留已发送的 tools / rules 节:
84
- * 新工具的声明经请求工具表末尾追加,system 文本不变,前缀缓存不失效(设计 §9.1)。
85
- * 有工具被移除时照常重写这两节。
84
+ * 对话开始后保留已发送的 tools / rules 节:新工具的声明经请求工具表末尾追加,移除的工具保留声明、
85
+ * 尾部提醒「no longer available」,system 文本不变,前缀缓存不失效(设计 §9.1、[ME-B] D5)。
86
86
  */
87
- function keepSectionsOnToolAppend(current, sections, tools) {
88
- const now = new Set(tools.map((tool) => tool.name));
89
- if (current.tools.some((tool) => !now.has(tool.name)))
90
- return;
87
+ export function keepSectionsAfterStart(current, sections) {
91
88
  for (const name of ["tools", "rules"]) {
92
89
  const sent = current.sections[name];
93
90
  if (sent === undefined)
@@ -16,6 +16,7 @@ import { previewAction } from "../permissions/preview.js";
16
16
  import { PermissionPipeline } from "../permissions/pipeline.js";
17
17
  import { classifierRequest } from "./session-classifier.js";
18
18
  import { runSingleToolCall } from "./tool-runner.js";
19
+ import { toolUnavailableText } from "./tool-availability.js";
19
20
  /** 外层是这个工具时,嵌套调用的 Hook 输入带 `viaCodemode: true`。 */
20
21
  const CODEMODE_TOOL_NAME = "codemode";
21
22
  function textOf(content) {
@@ -128,8 +129,8 @@ function safePreview(core, request) {
128
129
  }
129
130
  export async function gateToolCall(core, call, ctx) {
130
131
  const tool = ctx.tool;
131
- if (tool === undefined)
132
- return { block: true, reason: `Tool ${call.name} not found` };
132
+ if (tool === undefined || core.options.unavailableTools?.includes(call.name) === true)
133
+ return { block: true, reason: toolUnavailableText(call.name) };
133
134
  let input = call.arguments;
134
135
  let replaced = false;
135
136
  let hookDecision;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * fork 式子 Agent(docs/model-efficiency-plan.md §2.1,D1–D3)。[ME-A]
3
+ *
4
+ * - fork 点 = 发出本次 `task` 调用的那条 assistant **之前一条**条目(= 父会话上一次真实请求的转录);
5
+ * 同一条 assistant 里并行的多个 fork 任务共享同一个 fork 点。
6
+ * - `forkPlan`:满足条件时用 `SessionManager.fork(entryId, { head })` 复制分支,`head` 是
7
+ * `custom{ama.task, context:"fork", forkedFrom}`(子会话首条仍是 `ama.task`);否则给出回落原因:
8
+ * 与父不同的模型 / 思考级别、父还没有真实请求、父上一次请求超过 `FORK_MAX_CONTEXT_RATIO` ×
9
+ * (窗口 − reserveTokens)、找不到发起调用的 assistant。续聊与 resume 不经过这里(照原文件重开)。
10
+ * - `forkBrief`:角色说明 + 任务合成一条 user 消息(固定英文),追加在继承的历史之后;子会话不写
11
+ * `role` 节,工具表与父逐字节相同,类型限制的工具在执行层拒绝(`unavailableTools`)。
12
+ */
13
+ import type { Model, ModelThinkingLevel } from "../ai/types.js";
14
+ import type { AgentDefinition } from "../agents/types.js";
15
+ import type { SessionManager } from "../session/manager.js";
16
+ import type { SessionEntry } from "../session/types.js";
17
+ import type { SubagentRequest } from "../tools/types.js";
18
+ import type { AgentSessionOptions } from "./session-core.js";
19
+ /** 父上一次请求的输入 token 超过(子模型窗口 − reserveTokens)的这个比例时回落 fresh。 */
20
+ export declare const FORK_MAX_CONTEXT_RATIO = 0.5;
21
+ export type ContextMode = "fork" | "fresh";
22
+ /** 请求的上下文模式:调用参数 > 类型 frontmatter > `fresh`。 */
23
+ export declare function requestedContext(request: Pick<SubagentRequest, "context">, agent: Pick<AgentDefinition, "context">): ContextMode;
24
+ /** 父分支里发出 `parentToolCallId` 的 assistant 之前一条条目的 id;找不到(或它是首条)返回 undefined。 */
25
+ export declare function forkPoint(branch: readonly SessionEntry[], parentToolCallId: string): string | undefined;
26
+ export interface ForkParent {
27
+ readonly manager: SessionManager;
28
+ readonly options: Pick<AgentSessionOptions, "compaction">;
29
+ readonly cache?: {
30
+ readonly lastTurn?: {
31
+ readonly promptTokens: number;
32
+ } | undefined;
33
+ };
34
+ childBase(): Pick<AgentSessionOptions, "model" | "thinkingLevel">;
35
+ }
36
+ export interface ForkSpec {
37
+ taskId: string;
38
+ agent: Pick<AgentDefinition, "name">;
39
+ request: Pick<SubagentRequest, "parentToolCallId" | "description">;
40
+ /** 子会话 cwd(隔离时是 worktree)。 */
41
+ cwd: string;
42
+ }
43
+ export type ForkPlan = {
44
+ manager: SessionManager;
45
+ forkedFrom: string;
46
+ } | {
47
+ fallback: string;
48
+ };
49
+ /**
50
+ * D3 的回落条件依次检查,全部通过才复制分支(复制即落盘:父有文件时子会话文件立即写出)。
51
+ * `model` / `thinking` 是子会话按类型与参数解析出的值。
52
+ */
53
+ export declare function forkPlan(parent: ForkParent, spec: ForkSpec, model: Model, thinking: ModelThinkingLevel): ForkPlan;
54
+ export interface ForkBriefInput {
55
+ prompt: string;
56
+ /** 类型正文(不含通用前言:这里的首句已说明子 Agent 身份)。 */
57
+ role: string;
58
+ /** 不可用(执行层拒绝)的工具名。 */
59
+ unavailable: readonly string[];
60
+ /** 隔离运行时:子会话 cwd 与父 cwd。 */
61
+ worktree?: {
62
+ cwd: string;
63
+ parentCwd: string;
64
+ };
65
+ }
66
+ /** fork 子会话的首条 user 消息(固定英文,发给模型)。 */
67
+ export declare function forkBrief(input: ForkBriefInput): string;