@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
@@ -12,6 +12,8 @@
12
12
  * - **parallel**:同一回复里的多个 task 真并行(会话注册表的池限流,缺省 4);与 sequential 工具
13
13
  * (edit 等)同批时仍按 tool-runner 的规则整批串行。
14
14
  * - 深度 ≤ 1:子会话的工具表里保留 task(与父字节一致,缓存前缀可复用),运行时在这里拒绝。
15
+ * - [ME-A] `context: "fork"`:子会话继承本会话已完成的回合(同模型、同前缀);`details.context` 是实际
16
+ * 模式(不满足条件时回落 `fresh`)。
15
17
  * - 结果 = 子 Agent 的最终报告(> 50 KB 截头尾、全文落 outputs/,见 agents/result.ts)+ details。
16
18
  */
17
19
  import { AgentCatalog } from "../agents/catalog.js";
@@ -26,15 +28,20 @@ const THINKING = [
26
28
  "high",
27
29
  "xhigh",
28
30
  ];
31
+ /**
32
+ * [ME-A] 两版描述共用的开头:fresh / fork 两种上下文。default+task 与 codemode-only 的预算余量都只有
33
+ * 几十 token,加 `context` 参数时整段改写、字符数基本不增(`agent` 参数的「Default general」也去掉,
34
+ * 清单里 general 已标 default)。
35
+ */
36
+ export const TASK_CONTEXT_LEAD = "Delegate to a sub-agent (fresh context: give full instructions; context fork: inherits " +
37
+ "this conversation).";
29
38
  /** 缺省前台(`-p`、`subagents.background: never`)时的描述。 */
30
- export const FOREGROUND_DESCRIPTION = "Delegate to a sub-agent (fresh context; give full instructions). Returns its final report. " +
31
- "Tasks in one reply run in parallel; writers should use isolation worktree. background: " +
32
- "returns a taskId now (see task_ctl); taskId: continue that task.";
39
+ export const FOREGROUND_DESCRIPTION = `${TASK_CONTEXT_LEAD} Returns its final report. Parallel in one reply; writers use isolation ` +
40
+ "worktree. background: returns a taskId (see task_ctl); taskId: continue it.";
33
41
  /** [W7-B1] 缺省后台(交互 / RPC / ACP,docs/agents-concurrency-plan.md §2.6)时的描述。 */
34
- export const BACKGROUND_DESCRIPTION = "Delegate to a sub-agent (fresh context; give full instructions). Runs in the background by " +
35
- "default: returns a taskId; a <task-notification> follows when done, so keep working. " +
36
- "background:false if your next step needs the result. taskId: continue a task. Tasks in one " +
37
- "reply run in parallel; writers use isolation worktree.";
42
+ export const BACKGROUND_DESCRIPTION = `${TASK_CONTEXT_LEAD} Runs in the background by default: returns a taskId; a ` +
43
+ "<task-notification> follows; keep working. background:false waits for the result. " +
44
+ "taskId: continue it. Parallel in one reply; writers use isolation worktree.";
38
45
  /** 描述里类型清单的标题(清单本身另有 400 token 预算,见 agents/catalog.ts)。 */
39
46
  export const TASK_AGENTS_HEADING = "\nAgents:\n";
40
47
  const catalogs = new WeakMap();
@@ -68,6 +75,8 @@ export function buildSubagentRequest(input, ctx) {
68
75
  return 'isolation must be "none" or "worktree"';
69
76
  if (input.budgetUsd !== undefined && !(input.budgetUsd > 0))
70
77
  return "budgetUsd must be > 0";
78
+ if (input.context !== undefined && input.context !== "fork" && input.context !== "fresh")
79
+ return 'context must be "fork" or "fresh"';
71
80
  const request = {
72
81
  prompt: input.prompt,
73
82
  parentToolCallId: ctx.toolCallId,
@@ -96,6 +105,8 @@ export function buildSubagentRequest(input, ctx) {
96
105
  request.model = input.model;
97
106
  if (input.thinkingLevel !== undefined)
98
107
  request.thinkingLevel = input.thinkingLevel;
108
+ if (input.context !== undefined)
109
+ request.context = input.context;
99
110
  return request;
100
111
  }
101
112
  export function createTaskTool(_options = {}) {
@@ -111,7 +122,7 @@ export function createTaskTool(_options = {}) {
111
122
  type: "object",
112
123
  properties: {
113
124
  prompt: { type: "string" },
114
- agent: { type: "string", description: "Default general" },
125
+ agent: { type: "string" },
115
126
  description: { type: "string" },
116
127
  background: { type: "boolean" },
117
128
  taskId: { type: "string" },
@@ -121,6 +132,7 @@ export function createTaskTool(_options = {}) {
121
132
  model: { type: "string" },
122
133
  thinkingLevel: { type: "string", enum: [...THINKING] },
123
134
  maxTurns: { type: "integer" },
135
+ context: { type: "string", enum: ["fork", "fresh"] },
124
136
  },
125
137
  required: ["prompt"],
126
138
  additionalProperties: false,
@@ -145,6 +157,7 @@ export function createTaskTool(_options = {}) {
145
157
  ...(result.status !== undefined ? { status: result.status } : {}),
146
158
  ...(result.sessionFile !== undefined ? { sessionFile: result.sessionFile } : {}),
147
159
  ...(result.outputFile !== undefined ? { outputFile: result.outputFile } : {}),
160
+ ...(result.context !== undefined ? { context: result.context } : {}),
148
161
  usage: result.usage,
149
162
  stopReason: result.stopReason,
150
163
  };
@@ -8,6 +8,13 @@
8
8
  */
9
9
  export declare const DEFAULT_MAX_LINES = 2000;
10
10
  export declare const DEFAULT_MAX_BYTES: number;
11
+ /** [ME-D] 自截时给说明行留的余量(字符)。 */
12
+ export declare const RESULT_NOTE_RESERVE = 512;
13
+ /**
14
+ * [ME-D] 工具自截的字节上限(D11):会话的结果上限(`ToolContext.maxResultChars`)减去说明行余量,
15
+ * 不超过 50 KB——一次截到位,不再被会话层二次截中段。字节数 ≤ N 时字符数必然 ≤ N。
16
+ */
17
+ export declare function toolOutputBytes(maxResultChars: number | undefined): number;
11
18
  /** grep 匹配行的字符上限。 */
12
19
  export declare const GREP_MAX_LINE_CHARS = 500;
13
20
  export interface TruncateOptions {
@@ -11,6 +11,17 @@ import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  export const DEFAULT_MAX_LINES = 2000;
13
13
  export const DEFAULT_MAX_BYTES = 50 * 1024;
14
+ /** [ME-D] 自截时给说明行留的余量(字符)。 */
15
+ export const RESULT_NOTE_RESERVE = 512;
16
+ /**
17
+ * [ME-D] 工具自截的字节上限(D11):会话的结果上限(`ToolContext.maxResultChars`)减去说明行余量,
18
+ * 不超过 50 KB——一次截到位,不再被会话层二次截中段。字节数 ≤ N 时字符数必然 ≤ N。
19
+ */
20
+ export function toolOutputBytes(maxResultChars) {
21
+ if (maxResultChars === undefined)
22
+ return DEFAULT_MAX_BYTES;
23
+ return Math.max(1024, Math.min(DEFAULT_MAX_BYTES, maxResultChars - RESULT_NOTE_RESERVE));
24
+ }
14
25
  /** grep 匹配行的字符上限。 */
15
26
  export const GREP_MAX_LINE_CHARS = 500;
16
27
  export function byteLength(text) {
@@ -102,6 +102,8 @@ export interface SubagentRequest {
102
102
  isolation?: "none" | "worktree";
103
103
  /** [W5-C0] 外部 Agent 的美元预算。 */
104
104
  budgetUsd?: number;
105
+ /** [ME-C0] `fork`:继承父会话已完成的回合(同模型、同前缀);缺省 `fresh`。 */
106
+ context?: "fork" | "fresh";
105
107
  }
106
108
  /** [W5-C0] 子 Agent 任务的终态(`subagent_end.status`)。 */
107
109
  export type SubagentStatus = "completed" | "failed" | "aborted" | "max_turns" | "interrupted";
@@ -133,6 +135,8 @@ export interface SubagentResult {
133
135
  /** [W5-C0] 结果全文(> 50 KB 截断时、后台任务的输出文件)。 */
134
136
  outputFile?: string;
135
137
  sessionRef?: SubagentSessionRef;
138
+ /** [ME-A] 请求了 `context: "fork"` 时的实际模式(回落为 `fresh`);未请求 fork 时缺省。 */
139
+ context?: "fork" | "fresh";
136
140
  }
137
141
  /** runner 上报的进度(由注册表转成 `subagent_update` 事件)。 */
138
142
  export type SubagentEvent = {
@@ -173,6 +177,11 @@ export type SubagentEvent = {
173
177
  type: "notice";
174
178
  level: "info" | "warn";
175
179
  text: string;
180
+ }
181
+ /** [ME-A] ama 子会话请求了 `context: "fork"` 时的实际模式(注册表写进 `TaskInfo.context`)。 */
182
+ | {
183
+ type: "context";
184
+ mode: "fork" | "fresh";
176
185
  };
177
186
  export interface SubagentRunRequest {
178
187
  prompt: string;
@@ -225,6 +234,8 @@ export interface TaskInfo {
225
234
  contextWindow?: number;
226
235
  outputFile?: string;
227
236
  sessionRef?: SubagentSessionRef;
237
+ /** [ME-C0] 子会话的实际上下文模式(fork 回落为 fresh 时记 `fresh`);外部 Agent 缺省。 */
238
+ context?: "fork" | "fresh";
228
239
  }
229
240
  /** [W5-C0] 任务注册表的只读视图(W5-G 的 `SubagentRegistry` 提供)。 */
230
241
  export interface TaskRegistryView {
@@ -243,6 +254,8 @@ export interface ToolContext {
243
254
  readonly thinkingLevel?: ModelThinkingLevel;
244
255
  /** 截断全文的落盘目录(`<sessionDir>/outputs`);内存会话为 undefined。 */
245
256
  readonly outputDir?: string;
257
+ /** [ME-C0] 会话层的工具结果上限(字符);工具据此一次截到位。宿主 / SDK 自建上下文可不给。 */
258
+ readonly maxResultChars?: number;
246
259
  onUpdate(partial: string): void;
247
260
  /** 本会话已 read 的绝对路径(write / edit 先读后写检查)。 */
248
261
  readonly readFiles: ReadonlySet<string>;
@@ -33,7 +33,17 @@ export function traceData(entry) {
33
33
  ? data
34
34
  : undefined;
35
35
  }
36
+ /** [ME-A] fork 式子会话从父复制来的条目(首条 `ama.task{context:"fork"}` 之后到 forkedFrom 为止)。 */
37
+ function forkInherited(entries) {
38
+ const head = entries[0];
39
+ const data = head?.type === "custom" && head.customType === TASK_CUSTOM_TYPE ? head.data : {};
40
+ if (!isObject(data) || data["context"] !== "fork" || typeof data["forkedFrom"] !== "string")
41
+ return new Set();
42
+ const end = entries.findIndex((entry) => entry.id === data["forkedFrom"]);
43
+ return new Set(entries.slice(1, end + 1).map((entry) => entry.id));
44
+ }
36
45
  export function buildIndex(entries) {
46
+ const inherited = forkInherited(entries);
37
47
  const index = {
38
48
  stepByAssistant: new Map(),
39
49
  toolTiming: new Map(),
@@ -52,9 +62,11 @@ export function buildIndex(entries) {
52
62
  }
53
63
  if (entry.type === "custom" && entry.customType === TASK_CUSTOM_TYPE) {
54
64
  const data = entry.data;
55
- // 子会话自己的首条没有 status,不是父会话的快照
65
+ // 子会话自己的首条没有 status,不是父会话的快照;fork 继承来的父快照也不算
56
66
  if (!isObject(data) || typeof data["taskId"] !== "string" || data["status"] === undefined)
57
67
  continue;
68
+ if (inherited.has(entry.id))
69
+ continue;
58
70
  const parent = typeof data["parentToolCallId"] === "string" ? data["parentToolCallId"] : "";
59
71
  const byTask = index.tasks.get(parent) ?? new Map();
60
72
  byTask.set(data["taskId"], { data: data, entryId: entry.id });
@@ -7,6 +7,8 @@
7
7
  * - 所有截断 / 切片在保留 SGR 样式的同时,于末尾补 `\x1b[0m`,保证「每行末尾重置样式」。
8
8
  */
9
9
  export declare const SGR_RESET = "\u001B[0m";
10
+ /** 字素切分器,首次用到才构造(-p / RPC / ACP / --version 不付 ICU 断字数据的初始化)。 */
11
+ export declare function graphemeSegmenter(): Intl.Segmenter;
10
12
  /** 单个码点的列宽(0 / 1 / 2)。 */
11
13
  export declare function codePointWidth(cp: number): 0 | 1 | 2;
12
14
  /** 一个字素簇的列宽:首码点决定基础宽度;VS16(U+FE0F)把文本型 emoji 升为 2 列。 */
package/dist/tui/ansi.js CHANGED
@@ -7,7 +7,11 @@
7
7
  * - 所有截断 / 切片在保留 SGR 样式的同时,于末尾补 `\x1b[0m`,保证「每行末尾重置样式」。
8
8
  */
9
9
  export const SGR_RESET = "\x1b[0m";
10
- const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
10
+ let segmenter;
11
+ /** 字素切分器,首次用到才构造(-p / RPC / ACP / --version 不付 ICU 断字数据的初始化)。 */
12
+ export function graphemeSegmenter() {
13
+ return (segmenter ??= new Intl.Segmenter(undefined, { granularity: "grapheme" }));
14
+ }
11
15
  /** 东亚宽(W)与全角(F)区段,含常见 emoji 区块;按起点升序,二分查找。 */
12
16
  const WIDE_RANGES = [
13
17
  [0x1100, 0x115f],
@@ -193,7 +197,7 @@ function textWidth(text) {
193
197
  if (isPlainAscii(text))
194
198
  return text.length;
195
199
  let width = 0;
196
- for (const { segment } of segmenter.segment(text))
200
+ for (const { segment } of graphemeSegmenter().segment(text))
197
201
  width += graphemeWidth(segment);
198
202
  return width;
199
203
  }
@@ -221,7 +225,7 @@ function* pieces(s) {
221
225
  yield { text: ch, width: 1, escape: false };
222
226
  }
223
227
  else {
224
- for (const { segment } of segmenter.segment(run)) {
228
+ for (const { segment } of graphemeSegmenter().segment(run)) {
225
229
  yield { text: segment, width: graphemeWidth(segment), escape: false };
226
230
  }
227
231
  }
@@ -7,10 +7,10 @@
7
7
  * - 撤销:每次编辑前存快照;连续输入同类字符合并为一步;上限 50 步。
8
8
  * - 不做选区(无鼠标、无 kill ring,§12.9)。
9
9
  */
10
- const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
10
+ import { graphemeSegmenter } from "../ansi.js";
11
11
  function graphemeStarts(line) {
12
12
  const starts = [];
13
- for (const { index } of segmenter.segment(line))
13
+ for (const { index } of graphemeSegmenter().segment(line))
14
14
  starts.push(index);
15
15
  starts.push(line.length);
16
16
  return starts;
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { msg } from "../../i18n/index.js";
17
17
  import { CURSOR_MARKER } from "../component.js";
18
- import { truncateToWidth, visibleWidth } from "../ansi.js";
18
+ import { graphemeSegmenter, truncateToWidth, visibleWidth } from "../ansi.js";
19
19
  import { defaultKeybindings } from "../keybindings.js";
20
20
  import { isPasteData, isPrintableText, unwrapPaste } from "../keys.js";
21
21
  import { plainTheme } from "../theme.js";
@@ -24,7 +24,6 @@ import { appendHistoryFile, loadHistoryFile } from "./editor-history.js";
24
24
  import { PasteStore } from "./editor-paste.js";
25
25
  import { SelectList } from "./select-list.js";
26
26
  export { loadHistoryFile } from "./editor-history.js";
27
- const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
28
27
  /** 提示符 `› ` / 续行缩进的宽度。 */
29
28
  const PROMPT_WIDTH = 2;
30
29
  export class Editor {
@@ -290,7 +289,7 @@ export class Editor {
290
289
  const line = this.buffer.getLines()[row.line];
291
290
  let col = row.start;
292
291
  let width = 0;
293
- for (const { segment, index } of segmenter.segment(line.slice(row.start, row.end))) {
292
+ for (const { segment, index } of graphemeSegmenter().segment(line.slice(row.start, row.end))) {
294
293
  const w = visibleWidth(segment);
295
294
  if (width + w > targetWidth)
296
295
  break;
@@ -304,7 +303,7 @@ export class Editor {
304
303
  }
305
304
  prevGrapheme(line, col) {
306
305
  let prev = 0;
307
- for (const { index } of segmenter.segment(line)) {
306
+ for (const { index } of graphemeSegmenter().segment(line)) {
308
307
  if (index >= col)
309
308
  break;
310
309
  prev = index;
@@ -403,7 +402,7 @@ export class Editor {
403
402
  let start = 0;
404
403
  let used = 0;
405
404
  let pos = 0;
406
- const graphemes = [...segmenter.segment(text)];
405
+ const graphemes = [...graphemeSegmenter().segment(text)];
407
406
  let gi = 0;
408
407
  while (gi < graphemes.length) {
409
408
  const g = graphemes[gi];
@@ -521,7 +520,7 @@ export class Editor {
521
520
  }
522
521
  }
523
522
  function nextGrapheme(text, pos) {
524
- const first = segmenter
523
+ const first = graphemeSegmenter()
525
524
  .segment(text.slice(pos, pos + 32))[Symbol.iterator]()
526
525
  .next();
527
526
  return first.done ? " " : first.value.segment;
package/dist/tui/keys.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * KeyId 形如 `ctrl+shift+up`:修饰键按 ctrl → alt → shift 排序,键名小写;
9
9
  * 可打印字符的键名就是字符本身(`a`、`/`、`中`),空格为 `space`。
10
10
  */
11
+ import { graphemeSegmenter } from "./ansi.js";
11
12
  export const PASTE_START = "\x1b[200~";
12
13
  export const PASTE_END = "\x1b[201~";
13
14
  const NO_MODS = { ctrl: false, alt: false, shift: false };
@@ -212,10 +213,9 @@ export function parseKey(data) {
212
213
  return undefined;
213
214
  return withAlt(inner);
214
215
  }
215
- const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
216
216
  function isSingleGrapheme(data) {
217
217
  let count = 0;
218
- for (const _ of graphemeSegmenter.segment(data)) {
218
+ for (const _ of graphemeSegmenter().segment(data)) {
219
219
  count++;
220
220
  if (count > 1)
221
221
  return false;
@@ -8,6 +8,7 @@
8
8
  * - 块末尾的不完整转义序列先挂起;在 `AMA_TUI_ESC_TIMEOUT` 毫秒内没有后续数据就原样发出
9
9
  * (孤立 ESC 即 Escape 键)。缺省:SSH / tmux 下 100 ms,本地 10 ms。
10
10
  */
11
+ import { graphemeSegmenter } from "./ansi.js";
11
12
  import { PASTE_END, PASTE_START } from "./keys.js";
12
13
  /** 读环境:`AMA_TUI_ESC_TIMEOUT` 优先;否则 SSH / tmux 100 ms,本地 10 ms。 */
13
14
  export function defaultEscTimeout(env = process.env) {
@@ -21,7 +22,6 @@ export function defaultEscTimeout(env = process.env) {
21
22
  return 100;
22
23
  return 10;
23
24
  }
24
- const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
25
25
  /** 从 0 开始扫描一个转义序列(buf[0] === ESC)。 */
26
26
  function scanEscape(buf) {
27
27
  if (buf.length === 1)
@@ -74,7 +74,7 @@ function scanEscape(buf) {
74
74
  }
75
75
  // Alt 前缀 + 单个字素
76
76
  const rest = buf.slice(1);
77
- const first = segmenter.segment(rest.slice(0, 64))[Symbol.iterator]().next();
77
+ const first = graphemeSegmenter().segment(rest.slice(0, 64))[Symbol.iterator]().next();
78
78
  const segment = first.done ? rest[0] : first.value.segment;
79
79
  return { kind: "complete", length: 1 + segment.length };
80
80
  }
@@ -180,7 +180,7 @@ export class StdinBuffer {
180
180
  const c = buf.charCodeAt(0);
181
181
  if (c < 0x20 || c === 0x7f)
182
182
  return 1;
183
- const first = segmenter.segment(buf.slice(0, 64))[Symbol.iterator]().next();
183
+ const first = graphemeSegmenter().segment(buf.slice(0, 64))[Symbol.iterator]().next();
184
184
  if (first.done)
185
185
  return 1;
186
186
  // 字素簇不能吞掉后面的 ESC 或控制字符
package/docs/acp.md CHANGED
@@ -47,8 +47,15 @@ ama --mode acp # 与 -p 互斥;其余参数(--model、-
47
47
  「本会话允许」的记忆(切回来要重新允许,与 TUI `/resume` 一致)。
48
48
  - 权限模式按会话记:对前台会话 `set_mode` 立即生效;对其它会话只记下(照样发 `current_mode_update`),轮到它跑时
49
49
  再应用到权限管线,并再发一条 `current_mode_update`。所以对排队中的会话改模式不会影响正在跑的那个。
50
+ - 后台子 Agent 完成后的通知回合不经这个队列,在会话空闲时自己开始;它在跑时,对同一会话的 `session/prompt` 也等它
51
+ 结束再开始(不报 busy),等待期间客户端先收到通知回合的 `session/update`。等待中 `session/cancel` 会中断通知回合,
52
+ 这条提示回 `cancelled`,之后也不会再被发出。这条提示答复里的 `usage` 只算它自己的回合,不含之前的通知回合。
50
53
  - 每个回合结束后发 `session_info_update`(`updatedAt`,标题变了才带 `title`)。
51
- - 关掉一个会话只释放它(跑 SessionEnd Hook);stdin 关闭时排队的提示回 `cancelled`,等在跑的结束,再依次释放全部会话。
54
+ - 关掉一个会话只释放它(跑 SessionEnd Hook),它的转录、图片与子 Agent 句柄随之可被回收,进程启动时建的那个会话
55
+ 也一样。关闭前先让它安静下来:它的排队提示回 `cancelled`、在跑的后台子 Agent 停掉(不再发完成通知)、在跑的回合
56
+ (含通知回合)被中断,已排着的通知也不再开回合;`session/close` 之后该会话不再有 `session/update`。被中断的
57
+ 通知回合与 `session/cancel` 一样在会话文件里留下中断标记。stdin 关闭时排队的提示回 `cancelled`,等在跑的提示结束,
58
+ 通知回合与后台子 Agent 按上面的方式停下,再依次释放全部会话。
52
59
 
53
60
  ### 事件映射
54
61
 
package/docs/agents.md CHANGED
@@ -39,6 +39,7 @@ thinking: low
39
39
  max-turns: 20 # 缺省 30
40
40
  isolation: none # none(缺省)| worktree
41
41
  background: false # 不写 = 按 config subagents.background
42
+ context: fresh # fresh(缺省)| fork:继承父会话已完成的回合,见「fork 模式」
42
43
  runner: ama # ama(缺省)| claude | codex | acp:<程序>
43
44
  ---
44
45
 
@@ -69,6 +70,7 @@ runner: ama # ama(缺省)| claude | codex | acp:<程序>
69
70
  | `taskId` | 续聊:向已有任务的子会话追加一条消息(忽略 `agent` / `tools` / `model`) |
70
71
  | `isolation` | `worktree`:在独立 git worktree 里运行 |
71
72
  | `budgetUsd` | 外部 Agent 的美元预算 |
73
+ | `context` | `fresh`(缺省):子会话从空白开始;`fork`:继承本会话已完成的回合(同模型、同前缀),见「fork 模式」。缺省取类型的 `context` |
72
74
  | `tools` / `model` / `thinkingLevel` / `maxTurns` | 保留的高级参数(描述里不展开) |
73
75
 
74
76
  同一条回复里的多个 `task` **并行**执行,由会话的任务池限流(`subagents.maxConcurrent`,缺省 4);排队超过
@@ -77,7 +79,8 @@ runner: ama # ama(缺省)| claude | codex | acp:<程序>
77
79
 
78
80
  结果是子 Agent 的最终报告,前面带 `[task tN]`。超过 50 KB 时保留开头 70% 与结尾 30%,中间注明省略了多少字节,
79
81
  全文写到会话目录的 `outputs/<会话 id>-<taskId>.md`(内存会话写到系统临时目录)。轮数用尽且最后一步停在工具结果上时,
80
- ama 以「不允许调用工具」再跑一轮要最终报告,结果前加 `[Turn limit reached; …]`,状态 `max_turns`。
82
+ ama 再跑一轮、在消息里要求「不要调用工具、直接给最终报告」,结果前加 `[Turn limit reached; …]`,状态 `max_turns`。这一轮的请求
83
+ 不发 `toolChoice`(改动它会让缓存前缀失效);模型仍调用工具时这一轮就此结束,结果为「(the sub-agent returned no text)」。
81
84
 
82
85
  ### 前台与后台
83
86
 
@@ -127,11 +130,12 @@ ama 以「不允许调用工具」再跑一轮要最终报告,结果前加 `[T
127
130
 
128
131
  ### 续聊、保留与 resume
129
132
 
130
- 每个任务有会话内唯一的 `taskId`(`t1`、`t2` …)。子会话结束后不立即释放,最多保留 16 个(最久未用的先释放内存;
131
- JSONL 一直在)。`task{taskId}` / `task_ctl send` 续聊时,保留中的直接追加消息,已释放的按会话文件重新打开再追加——
133
+ 每个任务有会话内唯一的 `taskId`(`t1`、`t2` …)。子会话结束后不立即释放,最多保留 4 个(`subagents.retainSessions`;
134
+ 最久未用的先释放内存,JSONL 一直在)。`task{taskId}` / `task_ctl send` 续聊时,保留中的直接追加消息,已释放的按会话文件重新打开再追加——
132
135
  同一个任务始终写同一个 JSONL。父会话在任务开始、每次续聊与结束时写一条 `custom{ama.task}`
133
136
  ([session-format.md](session-format.md));`--resume` 时据此重建任务列表,当时还在运行的标 `interrupted`,仍可续聊。
134
- 内存会话(`--no-session`)的任务被释放后不能续聊。
137
+ 内存会话(`--no-session`)的任务被释放后不能续聊。子会话转录常达数十 MB,保留得越多常驻内存越大;频繁轮换续聊
138
+ 5 个以上任务时可调大保留数,代价只是被释放的任务续聊时多一次从磁盘重开。
135
139
 
136
140
  ### worktree 隔离
137
141
 
@@ -140,7 +144,38 @@ JSONL 一直在)。`task{taskId}` / `task_ctl send` 续聊时,保留中的
140
144
  worktree 里与父 cwd 对应的目录。结束时没有改动(工作区干净且没有新提交)就删掉 worktree 与分支;有改动则保留,结果末尾给出
141
145
  分支名、路径与 `git diff --stat`(含未跟踪文件),由你决定是否合并。`.ama/worktrees/` 下自动写一个 `.gitignore`(`*`),
142
146
  父仓库的 `git status` 与 grep / glob 都看不到这些目录。不在 git 仓库里时直接报错,不会退回共享目录。
143
- worktree 里的编辑不记进父会话的检查点。注意:worktree 不共享依赖(`node_modules` 等),不保证能直接构建或运行测试。
147
+ 多个隔离任务同时开始或结束时,同一仓库的 worktree 增删与删分支按顺序执行(它们都会改 `.git/worktrees`);另一个 ama 进程同时操作时遇到的瞬时错误会短暂重试。worktree 里的编辑不记进父会话的检查点。注意:worktree 不共享依赖(`node_modules` 等),不保证能直接构建或运行测试。
148
+
149
+ ### fork 模式
150
+
151
+ `context: "fork"`(调用参数或类型 frontmatter)的子会话**继承父会话到发起这次 `task` 之前的全部对话**:
152
+
153
+ - 子会话文件复制父分支到发起调用的那条 assistant 之前(同一回复里并行的多个 fork 任务共用这个点),首条仍是
154
+ `custom{ama.task}`,data 多 `context: "fork"` 与 `forkedFrom`(复制的最后一条条目 id);`parentSession` 指回父文件。
155
+ - 系统提示与工具表与父**逐字节相同**(不加 `role` 节,`task` / `task_ctl` 仍在表里、按深度拒绝),所以子会话的首个请求
156
+ 就是父上一次请求的前缀加一条 `<task>` 消息,前缀按读价计费;`prompt_cache_key` 沿用父链的根会话 id。
157
+ - 角色说明与任务写在 `<task>` 消息里,声明「上面的请求属于主 Agent、只做这项任务、本块优先于上文的计划与提醒」。子会话
158
+ 看得到父的计划、提醒与读过的文件内容,但 `read` 记录从空开始(编辑前照旧要先读)。
159
+ - 类型声明了 `tools` / `disallowed-tools`(或参数给了 `tools`)时,工具表不变,不可用的工具在执行层拒绝
160
+ (`Tool "X" is not available in this session.`),并在 `<task>` 里列出。只读类型照旧走只读管线。
161
+ - `isolation: "worktree"` 时子会话的 cwd 是 worktree,`<task>` 里说明上文的相对路径指父目录;cwd 节变化以尾部补丁发送,
162
+ 不影响已缓存的前缀。
163
+
164
+ 以下情况回落为 fresh(日志记一条 info,结果 `details.context` 标 `fresh`):调用参数或类型指定了与父不同的模型或思考级别;
165
+ 父会话还没有发出过请求;父上一次请求的输入超过(窗口 − `compaction.reserveTokens`)的一半。续聊(`taskId`)与 resume
166
+ 照原文件重开,不受影响。只在请求了 fork 时 `details.context` 才出现。
167
+
168
+ 何时用 fork:子任务需要你已经读过、讨论过的内容时。fork 的首个请求把父上下文整段带上,按命中价计费;fresh 只发系统提示、
169
+ 工具表与任务说明,但你得在 `prompt` 里写清全部背景,子 Agent 往往还要重新读文件。
170
+
171
+ | 供应商(命中价 / 未命中价) | 8k 父上下文 fork 一次(实测,见 [benchmarks](benchmarks/efficiency-2026-10.md)「F」) | 适合 |
172
+ | ------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------- |
173
+ | DeepSeek(约 1/50) | 命中 77%,与父自己的下一回合相同;折合约 1.9k 全价 token | 需要父上下文时几乎总划算 |
174
+ | Kimi(约 1/4–1/10) | 命中 97.5% | 需要父上下文、且要重读较多时 |
175
+ | OpenAI 系(经中转,不发路由键) | 两次分别 0% 与 85%,取决于中转把请求路由到哪个上游 | 视中转而定 |
176
+ | Anthropic(1/10,写入 1.25×) | 未实测;前缀相同,命中应与父的下一回合相同 | 需要父上下文时 |
177
+
178
+ `explore` 这类只需要定位文件的任务建议保持 fresh;内置类型都缺省 fresh。
144
179
 
145
180
  ### 事件与统计
146
181
 
@@ -154,6 +189,7 @@ RPC `get_tasks` / `get_agents` 返回任务快照与可用类型(来源、定
154
189
  | --------------------------------- | -------------------------------------------------------------------------------- |
155
190
  | `subagents.maxConcurrent` | 同时运行的子 Agent,缺省 4 |
156
191
  | `subagents.maxPending` | 排队上限,缺省 16 |
192
+ | `subagents.retainSessions` | 保留在内存里的已结束子会话数(LRU),缺省 4,0 = 结束即释放;只认用户级 |
157
193
  | `subagents.defaultModel` | 子 Agent 缺省模型,不设继承父会话 |
158
194
  | `subagents.background` | `auto`(缺省)\| `always` \| `never`,见「前台与后台」;用户 / 项目 / 宿主级都认 |
159
195
  | `subagents.autoBackgroundAfterMs` | 前台任务运行超过该毫秒数自动转后台,缺省 0(关闭);用户 / 项目级都认 |
@@ -165,7 +201,7 @@ RPC `get_tasks` / `get_agents` 返回任务快照与可用类型(来源、定
165
201
 
166
202
  - 子 Agent 不能再委派(深度 1);协调多个 Agent 的场景交给宿主(如 Armadra 画布)。
167
203
  - 只读类型的 bash 只放行 plan 模式认可的只读命令,识别不了的一律拒绝,宁可少用。
168
- - 不读取 `.claude/agents`;不支持 fork 模式(继承父对话的子 Agent)。
204
+ - 不读取 `.claude/agents`。fork 模式只用父的模型与思考级别(不同则回落 fresh),外部 Agent 不支持 fork。
169
205
 
170
206
  ## 外部 Agent
171
207
 
package/docs/codemode.md CHANGED
@@ -34,7 +34,7 @@
34
34
  { "version": 1, "tools": { "preset": "codemode-only" }, "permission": { "allow": ["codemode"] } }
35
35
  ```
36
36
 
37
- 其它配置:`codemode.inlineBudget`(`only` 模式在描述里内联声明的预算,估算 token,缺省 3000,超出只列名字;`on` 模式不内联)、`codemode.requireStrict`(见下文沙箱)。项目级配置只能把 `codemode.mode` 设为 `off`。
37
+ 其它配置:`codemode.inlineBudget`(`only` 模式在描述里内联声明的预算,估算 token,缺省 3000,超出只列名字;`on` 模式不内联)、`codemode.requireStrict`(见下文沙箱)、`codemode.maxHeapMb`(脚本子进程的堆上限,见下文沙箱)。项目级配置只能把 `codemode.mode` 设为 `off`。
38
38
 
39
39
  `on` 模式的前缀开销:去重前 `codemode` 描述把已直接暴露的六个工具的声明又内联一遍,其它工具各追加一行提示,系统提示 + 工具表比 `off` 多约 1356 token;现在只多约 390 token(字符 / 4 估算,测试锁定 ≤ 500)。升级后续接的旧会话因为描述字节变化会有一次缓存未命中。
40
40
 
@@ -94,9 +94,11 @@
94
94
  每次执行起一个子进程:
95
95
 
96
96
  ```text
97
- <node> --permission --allow-fs-read=<ama-sandbox.cjs> --disallow-code-generation-from-strings <ama-sandbox.cjs> --ama-codemode-sandbox
97
+ <node> --max-old-space-size=256 --permission --allow-fs-read=<ama-sandbox.cjs> --disallow-code-generation-from-strings <ama-sandbox.cjs> --ama-codemode-sandbox
98
98
  ```
99
99
 
100
+ - 堆上限:`--max-old-space-size` 取 `codemode.maxHeapMb`(缺省 256 MB,0 不加;只认用户级)。脚本超出时子进程 OOM 退出,工具结果是脚本错误 `Script exceeded the codemode memory limit (256 MB); process the data in smaller pieces`,宿主进程内存不受影响;需要一次处理大数据时调大或分片。
101
+
100
102
  - 空环境启动,拿不到密钥、会话文件与环境变量(Windows 上 libuv 会从父进程补入 PATH、SYSTEMROOT、USERPROFILE 等系统变量,不含密钥);不授予文件写、子进程、worker、addon、inspector 权限;Node 22.0–22.12 用 `--experimental-permission`;嵌入 Electron 时设 `ELECTRON_RUN_AS_NODE=1`。
101
103
  - 子进程里用 `node:vm` 建只含 ECMAScript 内建对象的上下文(`codeGeneration: { strings: false, wasm: false }`,沙箱对象空原型);全局函数都在上下文内定义,只经一个宿主函数交换 JSON 字符串;子进程主 realm 也禁止字符串生成代码,经构造器链逃逸拿不到 `Function("return process")`。
102
104
  - `tools.*` 经 stdin / stdout 的 JSON 行协议回调父进程执行。
package/docs/en/acp.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English · [简体中文](../acp.md)
4
4
 
5
- > Translated from the Chinese [docs/acp.md](../acp.md) as of commit `bd37706`. When the two differ, the Chinese version is
5
+ > Translated from the Chinese [docs/acp.md](../acp.md) as of commit `f6419be`. When the two differ, the Chinese version is
6
6
  > authoritative.
7
7
 
8
8
  ama works on both sides of ACP:
@@ -47,8 +47,9 @@ One `ama --mode acp` process can keep several sessions open (Zed's threads share
47
47
  - **One turn runs at a time**: while another session runs, `session/prompt` goes into a FIFO queue and starts when the previous turn ends; it no longer fails busy. `session/new`, `load`, `resume`, `list`, `set_mode`, `set_config_option` and `close` can be called at any time.
48
48
  - When a turn starts, its session becomes the "foreground" session: the host (`HostApi.session.*`), the common fields of hooks and the session seen by tools all switch to it, and "allowed for this session" grants are cleared (switching back means allowing again, as with `/resume` in the TUI).
49
49
  - Permission modes are kept per session: `set_mode` on the foreground session applies at once; on another session it is only recorded (a `current_mode_update` is still sent) and applied to the permission pipeline when that session's turn starts, with another `current_mode_update`. So changing the mode of a queued session does not affect the one that is running.
50
+ - The notification turn that follows a finished background subagent does not go through this queue; it starts on its own once the session is idle. While it runs, `session/prompt` on the same session also waits for it to end (no busy error), and the client sees the notification turn's `session/update` first. `session/cancel` while waiting interrupts the notification turn; the prompt answers `cancelled` and is not sent later either. The `usage` in that prompt's answer covers only its own turn, not the notification turn before it.
50
51
  - After every turn a `session_info_update` is sent (`updatedAt`; `title` only when it changed).
51
- - Closing a session releases only that session (running the SessionEnd hook); when stdin closes, queued prompts answer `cancelled`, the running turn is allowed to finish, then all sessions are released in turn.
52
+ - Closing a session releases only that session (running the SessionEnd hook), and its transcript, images and subagent handles can then be garbage-collected, including the session created at process start. The session is quieted first: its queued prompts answer `cancelled`, running background subagents are stopped (no completion notification), the running turn (notification turns included) is interrupted and notifications already queued no longer start a turn; after `session/close` that session sends no more `session/update`. An interrupted notification turn leaves an interruption marker in the session file, as `session/cancel` does. When stdin closes, queued prompts answer `cancelled`, running prompts are allowed to finish, notification turns and background subagents are stopped as above, then all sessions are released in turn.
52
53
 
53
54
  ### Event mapping
54
55
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  English · [简体中文](../host-api.md)
4
4
 
5
- > Translated from the Chinese [docs/host-api.md](../host-api.md) as of commit `0065cf4`. When the two differ, the Chinese
5
+ > Translated from the Chinese [docs/host-api.md](../host-api.md) as of commit `979608b`. When the two differ, the Chinese
6
6
  > version is authoritative.
7
7
 
8
8
  A host adapter is a local JS module that ama loads at startup and hands a `HostApi`. With it the adapter can register tools, append to the system prompt, observe events, answer approvals, inject user messages and show notifications and status in the interface. The Armadra canvas plugs in as a host adapter (the canvas tools `canvas_*` / `context_*` are all registered by the adapter). The types are defined in `src/host/types.ts` and exported from `@armadra/agent/host`; `HOST_API_VERSION = 1`. The design rationale is in [design.md](../design.md) §6.2, §6.3 and §11.1 step 13 (Chinese).
@@ -2,7 +2,7 @@
2
2
 
3
3
  English · [简体中文](../permissions.md)
4
4
 
5
- > Translated from the Chinese [docs/permissions.md](../permissions.md) as of commit `ee89edb`. When the two differ, the
5
+ > Translated from the Chinese [docs/permissions.md](../permissions.md) as of commit `6b5455a`. When the two differ, the
6
6
  > Chinese version is authoritative.
7
7
 
8
8
  This document covers ama's six permission modes, the decision order for every tool call, and how the three tiers of `auto` mode, "rule tier → static judgement → model classifier", decide between allowing and asking. The overall design is in [design.md](../design.md) §6.3 and §7 (Chinese); hook input and output are in [hooks.md](../hooks.md) (Chinese).
@@ -172,7 +172,7 @@ It only handles calls neither ① nor ② decided (e.g. `rm old.txt`, `node scri
172
172
  - **A separate request**: it never enters the session transcript and does not change the main session's messages or prefix (the main session's prompt cache is unaffected), and does not trigger warming; the request purpose is `purpose: "classify"`.
173
173
  - **Input**: tool name, arguments (JSON, truncated to 4000 characters), cwd, project root, and a summary of the latest user message (truncated to 600 characters). Arguments and the user message sit inside a `<tool_call_data>` … `</tool_call_data>` data block, with end markers inside the block escaped; the system prompt requires treating everything inside as data and ignoring instructions in it (including things like "ignore previous instructions" or "respond allow"), leaning to ask when such text appears.
174
174
  - **Output**: strict JSON `{"decision":"allow"|"ask","reason":"…"}`. A parse failure, a timeout (10 s) or a request error → ask.
175
- - **Model**: `permission.autoModel` (`provider/model`); defaults to the current session model. A cheap, fast model is recommended, e.g. `packy/qwen3.8-flash`. `maxTokens` 256, thinking off.
175
+ - **Model**: `permission.autoModel` (`provider/model`); when unset (or not found), the small model from the session provider's catalog (the file-level `small`: deepseek → `deepseek-flash`, anthropic → `claude-haiku-4-5`, openai → `gpt-6-luna`, google → `gemini-3.5-flash-lite`, moonshot → `kimi-k2.6` and so on; it must be found and have a key), otherwise the current session model; the chosen model is logged once at debug level. Relays and custom providers have no catalog small model and keep the session model. A cheap, fast model is recommended, e.g. `packy/qwen3.8-flash`. `maxTokens` 256, thinking off.
176
176
  - **Caching**: successful decisions are cached within the session by "tool name + normalized arguments" (bash collapses whitespace, others use JSON with sorted keys), so identical calls are classified once; errors and timeouts are not cached.
177
177
  - **Cost**: each classification records a `usage` entry with `kind: "permission_classify"`, counted in `/session` cost and RPC stats, never in the context.
178
178
  - The classifier can only judge undecided calls as allow or ask; it cannot overturn ①'s deny or ask.