@armadra/agent 0.7.3 → 0.7.5

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 (179) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/CHANGELOG.zh-CN.md +48 -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 +24 -5
  12. package/dist/agent/session-compaction.js +54 -20
  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 +15 -4
  36. package/dist/ai/apis/cache-params.js +2 -1
  37. package/dist/ai/apis/google-generative-ai.js +4 -2
  38. package/dist/ai/apis/google-request.js +2 -1
  39. package/dist/ai/apis/max-tokens.d.ts +42 -0
  40. package/dist/ai/apis/max-tokens.js +121 -0
  41. package/dist/ai/apis/openai-completions.js +10 -5
  42. package/dist/ai/apis/openai-request.js +4 -2
  43. package/dist/ai/apis/openai-responses-request.js +3 -2
  44. package/dist/ai/apis/openai-responses.js +9 -5
  45. package/dist/ai/apis/shared.d.ts +3 -1
  46. package/dist/ai/apis/shared.js +21 -3
  47. package/dist/ai/cache/fingerprint.d.ts +4 -1
  48. package/dist/ai/cache/fingerprint.js +18 -1
  49. package/dist/ai/cache/miss.d.ts +2 -1
  50. package/dist/ai/cache/miss.js +9 -3
  51. package/dist/ai/cache/reporting.js +5 -1
  52. package/dist/ai/cache/types.d.ts +10 -2
  53. package/dist/ai/cache/warmer.d.ts +6 -1
  54. package/dist/ai/cache/warmer.js +7 -0
  55. package/dist/ai/context.d.ts +12 -4
  56. package/dist/ai/context.js +43 -24
  57. package/dist/ai/fake/fake-provider.d.ts +8 -1
  58. package/dist/ai/fake/fake-provider.js +19 -7
  59. package/dist/ai/fake/fake-script.d.ts +2 -0
  60. package/dist/ai/http.d.ts +17 -9
  61. package/dist/ai/http.js +33 -13
  62. package/dist/ai/image-intern.d.ts +29 -0
  63. package/dist/ai/image-intern.js +58 -0
  64. package/dist/ai/json-body.d.ts +63 -0
  65. package/dist/ai/json-body.js +286 -0
  66. package/dist/ai/overflow.js +5 -0
  67. package/dist/ai/providers/catalog-data.js +13 -13
  68. package/dist/ai/providers/catalog.d.ts +33 -0
  69. package/dist/ai/providers/catalog.js +105 -6
  70. package/dist/ai/providers/enrich.d.ts +13 -3
  71. package/dist/ai/providers/enrich.js +87 -31
  72. package/dist/ai/sse.d.ts +3 -2
  73. package/dist/ai/sse.js +3 -2
  74. package/dist/ai/types.d.ts +9 -2
  75. package/dist/bundle/ama.cjs +3637 -1973
  76. package/dist/cli/bootstrap.js +13 -5
  77. package/dist/cli/compose-agents.js +3 -0
  78. package/dist/cli/compose-extensions.js +2 -0
  79. package/dist/cli/compose-request.d.ts +20 -0
  80. package/dist/cli/compose-request.js +37 -0
  81. package/dist/cli/compose-session.js +2 -2
  82. package/dist/cli/deps.d.ts +1 -1
  83. package/dist/cli/subcommands/model-meta.d.ts +2 -1
  84. package/dist/cli/subcommands/model-meta.js +3 -2
  85. package/dist/codemode/host-side.d.ts +11 -4
  86. package/dist/codemode/host-side.js +25 -6
  87. package/dist/codemode/tool.d.ts +2 -0
  88. package/dist/codemode/tool.js +4 -0
  89. package/dist/config/json-schema.js +9 -2
  90. package/dist/config/key-docs.js +13 -4
  91. package/dist/config/schema-w5.js +9 -1
  92. package/dist/config/schema.d.ts +1 -1
  93. package/dist/config/schema.js +19 -3
  94. package/dist/config/settings-registry.js +6 -0
  95. package/dist/config/types-w5.d.ts +2 -0
  96. package/dist/config/types.d.ts +19 -1
  97. package/dist/config/types.js +4 -0
  98. package/dist/i18n/catalog.d.ts +13 -2
  99. package/dist/i18n/messages/cli.d.ts +2 -0
  100. package/dist/i18n/messages/cli.js +2 -0
  101. package/dist/i18n/messages/config-keys.d.ts +10 -0
  102. package/dist/i18n/messages/config-keys.js +12 -2
  103. package/dist/i18n/messages/config.d.ts +14 -0
  104. package/dist/i18n/messages/config.js +4 -0
  105. package/dist/i18n/messages/errors.d.ts +3 -2
  106. package/dist/i18n/messages/errors.js +15 -2
  107. package/dist/i18n/messages/report.d.ts +2 -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 +30 -18
  114. package/dist/modes/acp/acp-sessions.d.ts +22 -0
  115. package/dist/modes/acp/acp-sessions.js +68 -0
  116. package/dist/modes/context-report.js +3 -2
  117. package/dist/modes/print/json-event.d.ts +12 -1
  118. package/dist/modes/print/json-event.js +48 -1
  119. package/dist/modes/rpc/rpc-mode.d.ts +1 -0
  120. package/dist/modes/rpc/rpc-mode.js +9 -2
  121. package/dist/modes/session-report.d.ts +1 -1
  122. package/dist/modes/session-report.js +12 -6
  123. package/dist/rpc.d.ts +5 -2
  124. package/dist/session/line-reader.d.ts +28 -0
  125. package/dist/session/line-reader.js +82 -0
  126. package/dist/session/list.d.ts +14 -0
  127. package/dist/session/list.js +150 -0
  128. package/dist/session/manager.d.ts +23 -3
  129. package/dist/session/manager.js +91 -62
  130. package/dist/session/migrate.d.ts +2 -1
  131. package/dist/session/migrate.js +2 -1
  132. package/dist/session/offload.d.ts +51 -0
  133. package/dist/session/offload.js +195 -0
  134. package/dist/session/projection.d.ts +16 -3
  135. package/dist/session/projection.js +96 -29
  136. package/dist/session/scan.d.ts +3 -3
  137. package/dist/session/scan.js +9 -18
  138. package/dist/session/store.d.ts +15 -4
  139. package/dist/session/store.js +54 -38
  140. package/dist/session/types.d.ts +7 -2
  141. package/dist/tools/bash.d.ts +1 -1
  142. package/dist/tools/bash.js +5 -4
  143. package/dist/tools/glob.d.ts +2 -2
  144. package/dist/tools/glob.js +2 -2
  145. package/dist/tools/grep.js +4 -6
  146. package/dist/tools/image-file.js +2 -1
  147. package/dist/tools/read-lines.d.ts +71 -0
  148. package/dist/tools/read-lines.js +224 -0
  149. package/dist/tools/read.d.ts +6 -2
  150. package/dist/tools/read.js +47 -27
  151. package/dist/tools/task.d.ts +9 -0
  152. package/dist/tools/task.js +21 -8
  153. package/dist/tools/truncate.d.ts +7 -0
  154. package/dist/tools/truncate.js +11 -0
  155. package/dist/tools/types.d.ts +13 -0
  156. package/dist/trace/build-index.js +13 -1
  157. package/dist/tui/ansi.d.ts +2 -0
  158. package/dist/tui/ansi.js +7 -3
  159. package/dist/tui/components/editor-buffer.js +2 -2
  160. package/dist/tui/components/editor.js +5 -6
  161. package/dist/tui/keys.js +2 -2
  162. package/dist/tui/stdin-buffer.js +3 -3
  163. package/docs/acp.md +8 -1
  164. package/docs/agents.md +42 -6
  165. package/docs/codemode.md +4 -2
  166. package/docs/en/acp.md +3 -2
  167. package/docs/en/host-api.md +1 -1
  168. package/docs/en/permissions.md +2 -2
  169. package/docs/en/providers.md +54 -20
  170. package/docs/en/rpc.md +23 -7
  171. package/docs/en/sessions.md +4 -2
  172. package/docs/en/tui.md +2 -2
  173. package/docs/permissions.md +4 -1
  174. package/docs/providers.md +85 -22
  175. package/docs/rpc.md +22 -6
  176. package/docs/session-format.md +13 -2
  177. package/docs/sessions.md +3 -1
  178. package/docs/tui.md +1 -1
  179. package/package.json +2 -2
@@ -0,0 +1,87 @@
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 { DEFAULT_COMPACTION_SETTINGS } from "./session-compaction.js";
14
+ /** 父上一次请求的输入 token 超过(子模型窗口 − reserveTokens)的这个比例时回落 fresh。 */
15
+ export const FORK_MAX_CONTEXT_RATIO = 0.5;
16
+ const TASK_CUSTOM_TYPE = "ama.task";
17
+ /** 请求的上下文模式:调用参数 > 类型 frontmatter > `fresh`。 */
18
+ export function requestedContext(request, agent) {
19
+ return request.context ?? agent.context ?? "fresh";
20
+ }
21
+ /** 父分支里发出 `parentToolCallId` 的 assistant 之前一条条目的 id;找不到(或它是首条)返回 undefined。 */
22
+ export function forkPoint(branch, parentToolCallId) {
23
+ const index = branch.findIndex((entry) => entry.type === "message" &&
24
+ entry.message.role === "assistant" &&
25
+ entry.message.content.some((block) => block.type === "toolCall" && block.id === parentToolCallId));
26
+ return index > 0 ? branch[index - 1]?.id : undefined;
27
+ }
28
+ /**
29
+ * D3 的回落条件依次检查,全部通过才复制分支(复制即落盘:父有文件时子会话文件立即写出)。
30
+ * `model` / `thinking` 是子会话按类型与参数解析出的值。
31
+ */
32
+ export function forkPlan(parent, spec, model, thinking) {
33
+ const base = parent.childBase();
34
+ if (model.provider !== base.model.provider || model.id !== base.model.id)
35
+ return { fallback: `model ${model.provider}/${model.id} differs from the parent` };
36
+ if (thinking !== (base.thinkingLevel ?? "off"))
37
+ return { fallback: `thinking level ${thinking} differs from the parent` };
38
+ const last = parent.cache?.lastTurn;
39
+ if (last === undefined)
40
+ return { fallback: "the parent has not sent a request yet" };
41
+ const reserve = parent.options.compaction?.reserveTokens ?? DEFAULT_COMPACTION_SETTINGS.reserveTokens;
42
+ const window = model.contextWindow;
43
+ const limit = window === undefined ? Infinity : FORK_MAX_CONTEXT_RATIO * Math.max(0, window - reserve);
44
+ if (last.promptTokens > limit)
45
+ return { fallback: `parent context ${last.promptTokens} tokens exceeds ${Math.floor(limit)}` };
46
+ const forkedFrom = forkPoint(parent.manager.branch(), spec.request.parentToolCallId);
47
+ if (forkedFrom === undefined)
48
+ return { fallback: "the task call was not found in the parent transcript" };
49
+ const parentSession = parent.manager.file();
50
+ const manager = parent.manager.fork(forkedFrom, {
51
+ cwd: spec.cwd,
52
+ head: {
53
+ type: "custom",
54
+ customType: TASK_CUSTOM_TYPE,
55
+ data: {
56
+ taskId: spec.taskId,
57
+ agent: spec.agent.name,
58
+ parentToolCallId: spec.request.parentToolCallId,
59
+ description: spec.request.description,
60
+ parentSession,
61
+ context: "fork",
62
+ forkedFrom,
63
+ },
64
+ },
65
+ });
66
+ return { manager, forkedFrom };
67
+ }
68
+ /** fork 子会话的首条 user 消息(固定英文,发给模型)。 */
69
+ export function forkBrief(input) {
70
+ const lines = [
71
+ "<task>",
72
+ "You are a sub-agent forked from the conversation above at this point. The main agent cannot " +
73
+ "see your work, only your final reply; do not delegate further. Requests above were for the " +
74
+ "main agent: do only this task. Instructions in this block take precedence over earlier " +
75
+ "plans or reminders above.",
76
+ ];
77
+ if (input.worktree !== undefined)
78
+ lines.push(`Working directory for this task: ${input.worktree.cwd}. Relative paths in the ` +
79
+ `conversation above refer to ${input.worktree.parentCwd}.`);
80
+ if (input.unavailable.length > 0)
81
+ lines.push(`Tools not available to you: ${input.unavailable.join(", ")}. Calls to them are rejected.`);
82
+ const role = input.role.trim();
83
+ if (role !== "")
84
+ lines.push(`<role>\n${role}\n</role>`);
85
+ lines.push(`<instructions>\n${input.prompt}\n</instructions>`, "Complete the task, then end with a concise report: what you did, key findings, files changed (if any).", "</task>");
86
+ return lines.join("\n");
87
+ }
@@ -7,7 +7,7 @@
7
7
  * 宿主,经 `env.runners`);同一 `task(agent=…)` 入口;
8
8
  * - 并发池(`subagents.maxConcurrent`,缺省 4)+ 排队上限(`maxPending`,缺省 16,超出报错不要重试);
9
9
  * - 前台(等结果)/ 后台(立即返回 taskId,完成后 `<task-notification>` 走父会话 followUp 队列);
10
- * - `taskId` 续聊:句柄保留 16 个(LRU,释放的只是内存,JSONL 永在),被释放或 resume 后按会话
10
+ * - `taskId` 续聊:句柄保留 4 个(`subagents.retainSessions`,LRU,释放的只是内存,JSONL 永在),被释放或 resume 后按会话
11
11
  * 文件 / 外部会话 id 重新接上;
12
12
  * - 结果 > 50 KB 保留头尾并全文落 `outputs/`;轮数耗尽加说明;worktree 隔离;
13
13
  * - 事件 `subagent_start / update / end`;父会话 `custom{ama.task}` 记任务快照(带 `status`),
@@ -26,7 +26,7 @@ import { type DirectReply } from "./subagent-direct.js";
26
26
  import { type BackgroundReason } from "./subagent-background.js";
27
27
  export declare const DEFAULT_SUBAGENT_CONCURRENCY = 4;
28
28
  export declare const DEFAULT_MAX_PENDING = 16;
29
- export declare const DEFAULT_RETAINED = 16;
29
+ export declare const DEFAULT_RETAINED = 4;
30
30
  export { SubagentPool, TASK_CUSTOM_TYPE, type AmaRunnerSpec, type TaskHandle };
31
31
  export type { BackgroundReason };
32
32
  export type AmaRunnerFactory = (spec: AmaRunnerSpec) => SubagentRunner;
@@ -7,7 +7,7 @@
7
7
  * 宿主,经 `env.runners`);同一 `task(agent=…)` 入口;
8
8
  * - 并发池(`subagents.maxConcurrent`,缺省 4)+ 排队上限(`maxPending`,缺省 16,超出报错不要重试);
9
9
  * - 前台(等结果)/ 后台(立即返回 taskId,完成后 `<task-notification>` 走父会话 followUp 队列);
10
- * - `taskId` 续聊:句柄保留 16 个(LRU,释放的只是内存,JSONL 永在),被释放或 resume 后按会话
10
+ * - `taskId` 续聊:句柄保留 4 个(`subagents.retainSessions`,LRU,释放的只是内存,JSONL 永在),被释放或 resume 后按会话
11
11
  * 文件 / 外部会话 id 重新接上;
12
12
  * - 结果 > 50 KB 保留头尾并全文落 `outputs/`;轮数耗尽加说明;worktree 隔离;
13
13
  * - 事件 `subagent_start / update / end`;父会话 `custom{ama.task}` 记任务快照(带 `status`),
@@ -30,7 +30,7 @@ import { directMessage, drainDirect, liveOf } from "./subagent-direct.js";
30
30
  import { TaskNotifier, WaitDetach, backgroundRecords, blockingTasks, foregroundWaiter, settleTasks, startedResult, waitForTask, } from "./subagent-background.js";
31
31
  export const DEFAULT_SUBAGENT_CONCURRENCY = 4;
32
32
  export const DEFAULT_MAX_PENDING = 16;
33
- export const DEFAULT_RETAINED = 16;
33
+ export const DEFAULT_RETAINED = 4;
34
34
  export { SubagentPool, TASK_CUSTOM_TYPE };
35
35
  const registries = new Map();
36
36
  /** RPC `get_tasks` / `/tasks`:会话 id → 注册表视图(没有任务时 undefined)。 */
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 工具可用性的固定英文文案(docs/model-efficiency-plan.md §1.4、D5):执行层拒绝与尾部提醒共用,
3
+ * 发给模型,两种界面语言下逐字节相同。[ME-C0]
4
+ * [ME-B] 两条提醒由 `ai/context.ts` 渲染(ai 不 import agent),定义移到那里、这里再导出。
5
+ */
6
+ export { toolRemovedReminder, toolRestoredReminder } from "../ai/context.js";
7
+ /** 执行层拒绝:工具不在本会话可用(未注册,或在 `unavailableTools` 里)。 */
8
+ export declare const toolUnavailableText: (name: string) => string;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 工具可用性的固定英文文案(docs/model-efficiency-plan.md §1.4、D5):执行层拒绝与尾部提醒共用,
3
+ * 发给模型,两种界面语言下逐字节相同。[ME-C0]
4
+ * [ME-B] 两条提醒由 `ai/context.ts` 渲染(ai 不 import agent),定义移到那里、这里再导出。
5
+ */
6
+ export { toolRemovedReminder, toolRestoredReminder } from "../ai/context.js";
7
+ /** 执行层拒绝:工具不在本会话可用(未注册,或在 `unavailableTools` 里)。 */
8
+ export const toolUnavailableText = (name) => `Tool "${name}" is not available in this session.`;
@@ -27,6 +27,7 @@ import { formatSchemaErrors, validateSchema } from "./schema.js";
27
27
  import { CODEMODE_TOOL } from "../tools/presets.js";
28
28
  import { executionModeOf } from "../tools/registry.js";
29
29
  import { truncateMiddle } from "../tools/truncate.js";
30
+ import { toolUnavailableText } from "./tool-availability.js";
30
31
  export const ABORTED_TOOL_TEXT = "aborted by user";
31
32
  export const DEFAULT_MAX_TOOL_RESULT_CHARS = 30_000;
32
33
  export const DEFAULT_ABORT_GRACE_MS = 3000;
@@ -63,7 +64,7 @@ function notFoundText(name, options) {
63
64
  if (options.getNestedTool?.(name) !== undefined && options.getTool(CODEMODE_TOOL) !== undefined) {
64
65
  return `Tool ${name} is only callable inside a codemode script: tools.${name}({...})`;
65
66
  }
66
- return `Tool ${name} not found`;
67
+ return toolUnavailableText(name);
67
68
  }
68
69
  async function prepare(call, assistant, options, signal, parent) {
69
70
  if (signal.aborted)
@@ -124,10 +125,14 @@ function resultText(content) {
124
125
  return content;
125
126
  return content.map((block) => (block.type === "text" ? block.text : "")).join("");
126
127
  }
127
- function truncateResult(call, result, options, nested = false) {
128
- const limit = nested
128
+ /** 结果字符上限:嵌套调用 1 MB,否则会话的 `maxToolResultChars`。 */
129
+ function resultLimit(options, nested) {
130
+ return nested
129
131
  ? NESTED_MAX_RESULT_CHARS
130
132
  : (options.maxToolResultChars ?? DEFAULT_MAX_TOOL_RESULT_CHARS);
133
+ }
134
+ function truncateResult(call, result, options, nested = false) {
135
+ const limit = resultLimit(options, nested);
131
136
  const text = resultText(result.content);
132
137
  if (text.length <= limit)
133
138
  return result;
@@ -174,7 +179,11 @@ async function execute(prepared, options, signal, emit, parent) {
174
179
  });
175
180
  let result;
176
181
  try {
177
- const ctx = options.createToolContext(call, signal, onUpdate);
182
+ const created = options.createToolContext(call, signal, onUpdate);
183
+ // [ME-D] 工具按会话结果上限一次截到位(D11);宿主自己给了就不覆盖
184
+ const ctx = created.maxResultChars !== undefined
185
+ ? created
186
+ : { ...created, maxResultChars: resultLimit(options, parent !== undefined) };
178
187
  const run = tool.execute(input, ctx).then((value) => value, (error) => errorResult(signal.aborted
179
188
  ? ABORTED_TOOL_TEXT
180
189
  : error instanceof Error
@@ -8,6 +8,9 @@
8
8
  * - 非 git 目录直接报错,不回落到共享目录;`.ama/worktrees/.gitignore` 写 `*`,父会话的 git 与
9
9
  * grep / glob(读嵌套 .gitignore)都看不到这些目录;
10
10
  * - worktree 不保证可运行:依赖(node_modules 等)不共享。
11
+ * - 并发:同一仓库的 `worktree add / remove` 与删分支会扫描、改写 `.git/worktrees/*`,并行的两个隔离
12
+ * 任务曾互相读到对方建了一半的管理目录(`无法读取 .git/worktrees/<id>/commondir`)。同进程内按仓库的
13
+ * 公共 git 目录串行;跨进程(两个 ama 同时开隔离任务)的同类瞬时错误短暂重试。
11
14
  */
12
15
  export interface Worktree {
13
16
  /** worktree 根目录。 */
@@ -29,6 +32,11 @@ export interface WorktreeOutcome {
29
32
  }
30
33
  export type GitRunner = (args: readonly string[], cwd: string) => Promise<string>;
31
34
  export declare const runGit: GitRunner;
35
+ export declare function serialized<T>(key: string, run: () => Promise<T>): Promise<T>;
36
+ export declare function retryTransient<T>(run: () => Promise<T>, options?: {
37
+ attempts?: number;
38
+ delayMs?: number;
39
+ }): Promise<T>;
32
40
  export declare function worktreeBranch(taskId: string): string;
33
41
  /** 在父 cwd 所在仓库里为任务建 worktree;非 git 目录抛错。 */
34
42
  export declare function createWorktree(cwd: string, taskId: string, git?: GitRunner): Promise<Worktree>;
@@ -8,10 +8,13 @@
8
8
  * - 非 git 目录直接报错,不回落到共享目录;`.ama/worktrees/.gitignore` 写 `*`,父会话的 git 与
9
9
  * grep / glob(读嵌套 .gitignore)都看不到这些目录;
10
10
  * - worktree 不保证可运行:依赖(node_modules 等)不共享。
11
+ * - 并发:同一仓库的 `worktree add / remove` 与删分支会扫描、改写 `.git/worktrees/*`,并行的两个隔离
12
+ * 任务曾互相读到对方建了一半的管理目录(`无法读取 .git/worktrees/<id>/commondir`)。同进程内按仓库的
13
+ * 公共 git 目录串行;跨进程(两个 ama 同时开隔离任务)的同类瞬时错误短暂重试。
11
14
  */
12
15
  import { execFile } from "node:child_process";
13
16
  import { existsSync, mkdirSync, realpathSync, writeFileSync } from "node:fs";
14
- import { join, relative } from "node:path";
17
+ import { join, relative, resolve } from "node:path";
15
18
  const TIMEOUT_MS = 30_000;
16
19
  export const runGit = (args, cwd) => new Promise((resolve, reject) => {
17
20
  execFile("git", [...args], { cwd, timeout: TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => {
@@ -33,6 +36,48 @@ function realPath(path) {
33
36
  return path;
34
37
  }
35
38
  }
39
+ /** 每个仓库(按公共 git 目录)一条队列:worktree 的增删与删分支串行执行。 */
40
+ const queues = new Map();
41
+ export function serialized(key, run) {
42
+ const previous = queues.get(key) ?? Promise.resolve();
43
+ const current = previous.then(run, run);
44
+ const tail = current.then(() => undefined, () => undefined);
45
+ queues.set(key, tail);
46
+ void tail.then(() => {
47
+ if (queues.get(key) === tail)
48
+ queues.delete(key);
49
+ });
50
+ return current;
51
+ }
52
+ /** 另一个进程正在增删 worktree 时 git 报的瞬时错误(读到半建的管理目录、锁文件已存在)。 */
53
+ const TRANSIENT = /commondir|gitdir|could not read|unable to read|无法读取|\.lock'?: File exists|Unable to create '.*\.lock'|无法创建 '.*\.lock'/i;
54
+ export async function retryTransient(run, options = {}) {
55
+ const attempts = options.attempts ?? 5;
56
+ const delayMs = options.delayMs ?? 50;
57
+ for (let n = 1;; n++) {
58
+ try {
59
+ return await run();
60
+ }
61
+ catch (error) {
62
+ if (n >= attempts || !TRANSIENT.test(error instanceof Error ? error.message : String(error)))
63
+ throw error;
64
+ await new Promise((r) => setTimeout(r, delayMs * n));
65
+ }
66
+ }
67
+ }
68
+ /** 仓库的公共 git 目录(所有 worktree 共享),作为串行队列的键。 */
69
+ async function commonDir(repo, git) {
70
+ try {
71
+ return realPath(resolve(repo, (await git(["rev-parse", "--git-common-dir"], repo)).trim()));
72
+ }
73
+ catch {
74
+ return repo;
75
+ }
76
+ }
77
+ /** 串行 + 瞬时错误重试地跑一条会改动 `.git/worktrees` 的 git 命令。 */
78
+ function mutate(key, git, args, cwd) {
79
+ return serialized(key, () => retryTransient(() => git(args, cwd)));
80
+ }
36
81
  export function worktreeBranch(taskId) {
37
82
  return `ama/task-${taskId}`;
38
83
  }
@@ -53,8 +98,9 @@ export async function createWorktree(cwd, taskId, git = runGit) {
53
98
  writeFileSync(ignore, "*\n");
54
99
  const path = join(root, taskId);
55
100
  const branch = worktreeBranch(taskId);
101
+ const key = await commonDir(repo, git);
56
102
  if (!existsSync(path))
57
- await git(["worktree", "add", "-b", branch, path, base], repo);
103
+ await mutate(key, git, ["worktree", "add", "-b", branch, path, base], repo);
58
104
  const sub = relative(repo, realPath(cwd));
59
105
  const childCwd = sub === "" || sub.startsWith("..") ? path : join(path, sub);
60
106
  mkdirSync(childCwd, { recursive: true });
@@ -65,8 +111,9 @@ export async function finishWorktree(tree, git = runGit) {
65
111
  const status = (await git(["status", "--porcelain"], tree.path)).trim();
66
112
  const head = (await git(["rev-parse", "HEAD"], tree.path)).trim();
67
113
  if (status === "" && head === tree.base) {
68
- await git(["worktree", "remove", "--force", tree.path], tree.repo);
69
- await git(["branch", "-D", tree.branch], tree.repo).catch(() => "");
114
+ const key = await commonDir(tree.repo, git);
115
+ await mutate(key, git, ["worktree", "remove", "--force", tree.path], tree.repo);
116
+ await mutate(key, git, ["branch", "-D", tree.branch], tree.repo).catch(() => "");
70
117
  return { branch: tree.branch, changed: false };
71
118
  }
72
119
  const stat = (await git(["diff", "--stat", tree.base], tree.path).catch(() => "")).trim();
@@ -88,8 +88,11 @@ export function parseAgentDefinition(text, filePath, source) {
88
88
  const background = data["background"];
89
89
  if (background !== undefined && typeof background !== "boolean")
90
90
  return fail("background must be true or false");
91
+ const context = data["context"];
92
+ if (context !== undefined && context !== null && context !== "fork" && context !== "fresh")
93
+ return fail("context must be fork or fresh");
91
94
  if (runner !== "ama") {
92
- const ignored = ["tools", "disallowed-tools", "permission-mode"].filter((key) => data[key] !== undefined);
95
+ const ignored = ["tools", "disallowed-tools", "permission-mode", "context"].filter((key) => data[key] !== undefined);
93
96
  if (ignored.length > 0)
94
97
  warnings.push(`${filePath}: ${ignored.join(", ")} ignored for external runner ${runner} (it keeps its own policy)`);
95
98
  }
@@ -113,5 +116,7 @@ export function parseAgentDefinition(text, filePath, source) {
113
116
  agent.disallowedTools = disallowedTools;
114
117
  if (typeof thinking === "string")
115
118
  agent.thinking = thinking;
119
+ if (context === "fork" || context === "fresh")
120
+ agent.context = context;
116
121
  return { agent, warnings };
117
122
  }
@@ -171,6 +171,9 @@ export function applyRunnerEvent(record, event, sink) {
171
171
  case "notice":
172
172
  sink.log(event.level, `[task ${taskId}] ${event.text}`);
173
173
  return;
174
+ case "context":
175
+ record.info.context = event.mode;
176
+ return;
174
177
  case "turn_trace":
175
178
  sink.appendTrace?.({
176
179
  kind: "external_turn",
@@ -31,6 +31,8 @@ export interface AgentDefinition {
31
31
  */
32
32
  background?: boolean;
33
33
  runner: AgentRunnerSpec;
34
+ /** [ME-C0] frontmatter `context:`:`fork` 继承父会话已完成的回合;缺省 `fresh`。 */
35
+ context?: "fork" | "fresh";
34
36
  /** 正文:追加到子会话系统提示末尾的角色说明。 */
35
37
  prompt: string;
36
38
  source: AgentSource;
@@ -12,9 +12,9 @@
12
12
  * 任一出现过(含 0)即 `cacheReported: true`,之后的事件不会把它改回 false。
13
13
  */
14
14
  import { AssistantEventStreamImpl } from "../event-stream.js";
15
- import { authHeaders, describeErrorJson, idleTimeoutOf, mergeHeaders, USER_AGENT, } from "../http.js";
15
+ import { authHeaders, describeErrorJson, idleTimeoutOf, streamIdleTimeoutOf, mergeHeaders, USER_AGENT, } from "../http.js";
16
16
  import { readSseEvents } from "../sse.js";
17
- import { postWithCacheFallback } from "./cache-params.js";
17
+ import { clampRequestMaxTokens, postWithMaxTokensFallback } from "./max-tokens.js";
18
18
  import { ANTHROPIC_VERSION, anthropicMessagesUrl, buildAnthropicRequest, detectAnthropicCompat, } from "./anthropic-request.js";
19
19
  import { BlockTracker, STREAM_ENDED_MESSAGE, ProviderStopError, createOutput, finishDone, finishError, requireApiKey, } from "./shared.js";
20
20
  function num(value) {
@@ -160,20 +160,25 @@ async function run(stream, model, context, options) {
160
160
  if (request.providerThinkingLevel !== undefined) {
161
161
  tracker.output.providerThinkingLevel = request.providerThinkingLevel;
162
162
  }
163
+ // 预算型思考的预算由 max_tokens 推导,收紧会让消息缓存失效:不主动收紧(D9)
164
+ const budgeted = obj(request.body["thinking"])?.["type"] === "enabled";
165
+ clampRequestMaxTokens(request.body, "max_tokens", model.contextWindow, budgeted);
163
166
  const replaced = options.onPayload?.(request.body);
164
167
  const body = replaced === undefined ? request.body : replaced;
165
168
  const baseUrl = model.baseUrl ?? "https://api.anthropic.com";
166
- const response = await postWithCacheFallback(model, anthropicMessagesUrl(baseUrl), {
169
+ const url = anthropicMessagesUrl(baseUrl);
170
+ const post = {
167
171
  headers: buildHeaders(model, options, request.betas),
168
172
  body,
169
173
  signal: options.signal,
170
174
  timeoutMs: options.timeoutMs,
171
175
  idleTimeoutMs: idleTimeoutOf(options),
172
176
  onResponse: options.onResponse,
173
- });
177
+ };
178
+ const response = await postWithMaxTokensFallback(model, url, post, "max_tokens");
174
179
  stream.push({ type: "start", partial: tracker.output });
175
180
  const state = { blocks: new Map(), stop: undefined, sawMessageStop: false };
176
- for await (const sse of readSseEvents(response.body, options.signal, idleTimeoutOf(options))) {
181
+ for await (const sse of readSseEvents(response.body, options.signal, streamIdleTimeoutOf(options))) {
177
182
  if (sse.event === "ping" || sse.data === "")
178
183
  continue;
179
184
  let data;
@@ -7,7 +7,8 @@
7
7
  * `ttl: "1h"`,仅 `supportsLongCacheRetention`——官方端点缺省开,中转缺省关、降为 short;最后做 TTL
8
8
  * 顺序校验,5m 之后出现 1h 则全部降为 5m)按优先级取前
9
9
  * `maxCacheBreakpoints` 个:① 最后一条 user 消息(含工具结果)的最后一个块 ② system 末块
10
- * ③ 最后一个工具定义(`supportsCacheControlOnTools`)。
10
+ * ③ 倒数第二条 user 消息的末块(= 上一次请求的写入点;并行工具结果多时最后一条离上次写入点可能超出
11
+ * 回看窗口)④ 最后一个工具定义(`supportsCacheControlOnTools`)。[ME-C] D7
11
12
  *
12
13
  * thinking:`adaptiveThinking` 模型发 `{type:"adaptive"}` + `output_config.effort`;
13
14
  * 其余推理模型按预算发 `{type:"enabled", budget_tokens}`,预算计入 max_tokens(有工具时带交错思考 beta 头,
@@ -7,7 +7,8 @@
7
7
  * `ttl: "1h"`,仅 `supportsLongCacheRetention`——官方端点缺省开,中转缺省关、降为 short;最后做 TTL
8
8
  * 顺序校验,5m 之后出现 1h 则全部降为 5m)按优先级取前
9
9
  * `maxCacheBreakpoints` 个:① 最后一条 user 消息(含工具结果)的最后一个块 ② system 末块
10
- * ③ 最后一个工具定义(`supportsCacheControlOnTools`)。
10
+ * ③ 倒数第二条 user 消息的末块(= 上一次请求的写入点;并行工具结果多时最后一条离上次写入点可能超出
11
+ * 回看窗口)④ 最后一个工具定义(`supportsCacheControlOnTools`)。[ME-C] D7
11
12
  *
12
13
  * thinking:`adaptiveThinking` 模型发 `{type:"adaptive"}` + `output_config.effort`;
13
14
  * 其余推理模型按预算发 `{type:"enabled", budget_tokens}`,预算计入 max_tokens(有工具时带交错思考 beta 头,
@@ -16,6 +17,7 @@
16
17
  * 除非 `supportsTemperatureWithThinking`。`toolChoice: "none"`(有工具时)→ `tool_choice:{type:"none"}`。
17
18
  */
18
19
  import { contentText, normalizeContext, sanitizeText } from "../context.js";
20
+ import { largeString } from "../json-body.js";
19
21
  import { budgetedMaxTokens, clampThinkingLevel, mappedThinkingValue, thinkingBudget, } from "../thinking.js";
20
22
  import { detectAnthropicCompat } from "./anthropic-compat.js";
21
23
  import { effectiveRetention, resolveCacheRetention, resolvePromptCacheCompat, } from "./cache-params.js";
@@ -75,7 +77,11 @@ function convertBlocks(content) {
75
77
  else {
76
78
  blocks.push({
77
79
  type: "image",
78
- source: { type: "base64", media_type: block.mimeType, data: block.data },
80
+ source: {
81
+ type: "base64",
82
+ media_type: block.mimeType,
83
+ data: largeString("", block, block.data),
84
+ },
79
85
  });
80
86
  }
81
87
  }
@@ -161,12 +167,15 @@ function convertTools(tools) {
161
167
  input_schema: { ...tool.parameters, type: "object" },
162
168
  }));
163
169
  }
164
- /** 在最后一条 user 消息的最后一个块上打断点;返回是否打上。 */
165
- function markLastUser(messages, cacheControl) {
170
+ /** 在从后数第 `skip + 1` 条 user 消息的最后一个块上打断点;返回是否打上。 */
171
+ function markLastUser(messages, cacheControl, skip = 0) {
172
+ let remaining = skip;
166
173
  for (let i = messages.length - 1; i >= 0; i--) {
167
174
  const message = messages[i];
168
175
  if (!message || message["role"] !== "user")
169
176
  continue;
177
+ if (remaining-- > 0)
178
+ continue;
170
179
  const content = message["content"];
171
180
  if (typeof content === "string") {
172
181
  message["content"] = [{ type: "text", text: content, cache_control: cacheControl }];
@@ -240,6 +249,8 @@ export function buildAnthropicRequest(model, context, options) {
240
249
  lastSystem["cache_control"] = cacheControl;
241
250
  budget--;
242
251
  }
252
+ if (cacheControl && budget > 0 && markLastUser(messages, cacheControl, 1))
253
+ budget--;
243
254
  const lastTool = tools[tools.length - 1];
244
255
  if (cacheControl && budget > 0 && lastTool && compat.supportsCacheControlOnTools) {
245
256
  lastTool["cache_control"] = cacheControl;
@@ -14,6 +14,7 @@
14
14
  * 并提示一次(实测:某中转的 Responses 对 DeepSeek 拒收 prompt_cache_retention / _options)。
15
15
  */
16
16
  import { HttpError, postJson } from "../http.js";
17
+ import { LargeString } from "../json-body.js";
17
18
  import { msg } from "../../i18n/index.js";
18
19
  const DEFAULT_BASE_URLS = {
19
20
  "anthropic-messages": "https://api.anthropic.com",
@@ -133,7 +134,7 @@ export function isCacheParamRejection(error) {
133
134
  function stripInto(value, removed) {
134
135
  if (Array.isArray(value))
135
136
  return value.map((item) => stripInto(item, removed));
136
- if (typeof value !== "object" || value === null)
137
+ if (typeof value !== "object" || value === null || value instanceof LargeString)
137
138
  return value;
138
139
  const out = {};
139
140
  for (const [key, inner] of Object.entries(value)) {
@@ -16,9 +16,10 @@
16
16
  * - 错误体 `{error:{code,status,message}}` 重排为 `429 RESOURCE_EXHAUSTED: …`。
17
17
  */
18
18
  import { AssistantEventStreamImpl } from "../event-stream.js";
19
- import { authHeaders, HttpError, idleTimeoutOf, joinUrl, mergeHeaders, postJson, USER_AGENT, } from "../http.js";
19
+ import { authHeaders, HttpError, idleTimeoutOf, streamIdleTimeoutOf, joinUrl, mergeHeaders, postJson, USER_AGENT, } from "../http.js";
20
20
  import { readSseEvents } from "../sse.js";
21
21
  import { GOOGLE_BASE_URL, buildGoogleRequest, detectGoogleCompat } from "./google-request.js";
22
+ import { clampGoogleMaxTokens } from "./max-tokens.js";
22
23
  import { BlockTracker, ProviderStopError, STREAM_ENDED_MESSAGE, createOutput, finishDone, finishError, requireApiKey, } from "./shared.js";
23
24
  function num(value) {
24
25
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
@@ -188,6 +189,7 @@ async function run(stream, model, context, options) {
188
189
  if (request.providerThinkingLevel !== undefined) {
189
190
  tracker.output.providerThinkingLevel = request.providerThinkingLevel;
190
191
  }
192
+ clampGoogleMaxTokens(request.body, model.contextWindow);
191
193
  const replaced = options.onPayload?.(request.body);
192
194
  let response;
193
195
  try {
@@ -210,7 +212,7 @@ async function run(stream, model, context, options) {
210
212
  idSeed: Date.now().toString(36),
211
213
  };
212
214
  const body = response.body;
213
- for await (const sse of readSseEvents(body, options.signal, idleTimeoutOf(options))) {
215
+ for await (const sse of readSseEvents(body, options.signal, streamIdleTimeoutOf(options))) {
214
216
  const data = sse.data.trim();
215
217
  if (data === "" || data === "[DONE]")
216
218
  continue;
@@ -13,6 +13,7 @@
13
13
  * - `toolChoice: "none"`(有工具时)→ `toolConfig.functionCallingConfig.mode: "NONE"`。
14
14
  */
15
15
  import { contentText, normalizeContext, sanitizeText } from "../context.js";
16
+ import { largeString } from "../json-body.js";
16
17
  import { DEFAULT_THINKING_BUDGETS, clampBudgetToAnswerRoom, clampThinkingLevel, discreteThinkingLevel, } from "../thinking.js";
17
18
  export const GOOGLE_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
18
19
  const GOOGLE_COMPAT_KEYS = [
@@ -63,7 +64,7 @@ function validSignature(signature) {
63
64
  BASE64.test(signature));
64
65
  }
65
66
  function inlineData(block) {
66
- return { inlineData: { mimeType: block.mimeType, data: block.data } };
67
+ return { inlineData: { mimeType: block.mimeType, data: largeString("", block, block.data) } };
67
68
  }
68
69
  function convertUser(message) {
69
70
  if (typeof message.content === "string") {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `max_tokens` 的主动收紧与被动修正(docs/model-efficiency-plan.md §1.9、D9)。[ME-C]
3
+ *
4
+ * - 主动收紧:窗口已知时 `max_tokens = min(请求值, max(MIN_OUTPUT_TOKENS, 窗口 − 估算输入 − 余量))`,
5
+ * 输入按请求体字符 / 4 估算(本模块自带,不依赖 compaction/)。预算型思考(`fixed`)不收紧:
6
+ * 预算由 max_tokens 推导,改它会让消息缓存失效。
7
+ * - 被动修正:端点以 400 拒收并说出上限(「max_tokens 范围」类文案)时,记入进程级 `maxTokensCaps`、
8
+ * 以上限重发一次;之后同一模型的请求直接用上限。Anthropic「输入 + max_tokens 超出上下文」只按这次
9
+ * 的输入算出可用值重发、不记上限;可用值不足 `MIN_OUTPUT_TOKENS` 判为溢出(overflow.ts 认得该文案)。
10
+ * 重发只发生在 `start` 之前,流契约不变(与 `postWithCacheFallback` 同模式,包在它外层)。
11
+ */
12
+ import { type PostOptions } from "../http.js";
13
+ import type { Model } from "../types.js";
14
+ /** 主动收紧的下限:再小就不如让请求原样发出、由被动修正或溢出处理接手。 */
15
+ export declare const MIN_OUTPUT_TOKENS = 1024;
16
+ /** 窗口里给估算误差留的余量。 */
17
+ export declare const OUTPUT_HEADROOM_TOKENS = 2048;
18
+ /** 请求体里放 max_tokens 的字段名(三条协议各不相同)。 */
19
+ export type MaxTokensField = "max_tokens" | "max_completion_tokens" | "max_output_tokens";
20
+ /** `${provider}/${model}` → 已知上限(被动修正时记入,之后的请求直接用)。 */
21
+ export declare const maxTokensCaps: Map<string, number>;
22
+ type Json = Record<string, unknown>;
23
+ /** 主动收紧后的 max_tokens;`window` 未知或 `fixed`(预算型思考)时返回 requested。 */
24
+ export declare function clampMaxTokens(requested: number, window: number | undefined, estimatedInput: number, fixed: boolean): number;
25
+ /** 请求体的输入 token 估算:JSON 字符数 / 4(CJK 偏低,由被动修正兜底)。 */
26
+ export declare function estimateInputTokens(body: unknown): number;
27
+ /**
28
+ * 就地收紧 `holder[field]`(缺省 holder = body;Google 的 `generationConfig.maxOutputTokens`
29
+ * 传 holder)。字段不是数字时不动。
30
+ */
31
+ export declare function clampRequestMaxTokens(body: Json, field: string, window: number | undefined, fixed?: boolean, holder?: Json): void;
32
+ /** Google `generationConfig.maxOutputTokens`;带正的思考预算时不收紧(预算按回答上限留白算出)。 */
33
+ export declare function clampGoogleMaxTokens(body: Json, window: number | undefined): void;
34
+ /** 400 文案里的上限;`{ cap }` 可重发,`{ overflow: true }` 判溢出,undefined 不是本类错误。 */
35
+ export declare function parseMaxTokensRejection(error: unknown): {
36
+ cap: number;
37
+ } | {
38
+ overflow: true;
39
+ } | undefined;
40
+ /** `postWithCacheFallback` 之外再包一层:max_tokens 超上限的 400 以收紧值重发一次(只在 `start` 之前)。 */
41
+ export declare function postWithMaxTokensFallback(model: Pick<Model, "provider" | "id">, url: string, options: PostOptions, field: MaxTokensField): Promise<Response>;
42
+ export {};