@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
@@ -58,6 +58,7 @@ export declare const en: {
58
58
  "compaction.enabled": string;
59
59
  "compaction.reserveTokens": string;
60
60
  "compaction.keepRecentTokens": string;
61
+ "compaction.contextBudget": string;
61
62
  "compaction.prune.keepResults": string;
62
63
  "compaction.prune.clearAtLeast": string;
63
64
  "cache.warming": string;
@@ -66,6 +67,7 @@ export declare const en: {
66
67
  "cache.missNotices": string;
67
68
  "cache.warmSubagents": string;
68
69
  "request.idleTimeoutMs": string;
70
+ "request.streamIdleTimeoutMs": string;
69
71
  "retry.enabled": string;
70
72
  "retry.maxRetries": string;
71
73
  "retry.baseDelayMs": string;
@@ -237,6 +239,7 @@ export declare const zh: {
237
239
  "compaction.enabled": string;
238
240
  "compaction.reserveTokens": string;
239
241
  "compaction.keepRecentTokens": string;
242
+ "compaction.contextBudget": string;
240
243
  "compaction.prune.keepResults": string;
241
244
  "compaction.prune.clearAtLeast": string;
242
245
  "cache.warming": string;
@@ -245,6 +248,7 @@ export declare const zh: {
245
248
  "cache.missNotices": string;
246
249
  "cache.warmSubagents": string;
247
250
  "request.idleTimeoutMs": string;
251
+ "request.streamIdleTimeoutMs": string;
248
252
  "retry.enabled": string;
249
253
  "retry.maxRetries": string;
250
254
  "retry.baseDelayMs": string;
@@ -58,6 +58,7 @@ export const en = {
58
58
  "compaction.enabled": "Auto compaction",
59
59
  "compaction.reserveTokens": "Reserve tokens",
60
60
  "compaction.keepRecentTokens": "Keep recent tokens",
61
+ "compaction.contextBudget": "Context budget",
61
62
  "compaction.prune.keepResults": "Prune: keep results",
62
63
  "compaction.prune.clearAtLeast": "Prune: clear at least",
63
64
  "cache.warming": "Cache warming",
@@ -65,7 +66,8 @@ export const en = {
65
66
  "cache.minSavingsUsd": "Warming min savings ($)",
66
67
  "cache.missNotices": "Cache miss notices",
67
68
  "cache.warmSubagents": "Warm subagents",
68
- "request.idleTimeoutMs": "Stream idle timeout (ms)",
69
+ "request.idleTimeoutMs": "Response idle timeout (ms)",
70
+ "request.streamIdleTimeoutMs": "In-stream idle timeout (ms)",
69
71
  "retry.enabled": "Auto retry",
70
72
  "retry.maxRetries": "Max retries",
71
73
  "retry.baseDelayMs": "Retry base delay (ms)",
@@ -231,6 +233,7 @@ export const zh = {
231
233
  "compaction.enabled": "自动压缩",
232
234
  "compaction.reserveTokens": "预留 token",
233
235
  "compaction.keepRecentTokens": "保留最近 token",
236
+ "compaction.contextBudget": "上下文预算",
234
237
  "compaction.prune.keepResults": "裁剪:保留结果数",
235
238
  "compaction.prune.clearAtLeast": "裁剪:至少省",
236
239
  "cache.warming": "缓存保温",
@@ -238,7 +241,8 @@ export const zh = {
238
241
  "cache.minSavingsUsd": "保温最低节省(美元)",
239
242
  "cache.missNotices": "未命中提示",
240
243
  "cache.warmSubagents": "子会话保温",
241
- "request.idleTimeoutMs": "流空闲超时(毫秒)",
244
+ "request.idleTimeoutMs": "响应头等待超时(毫秒)",
245
+ "request.streamIdleTimeoutMs": "流中空闲超时(毫秒)",
242
246
  "retry.enabled": "自动重试",
243
247
  "retry.maxRetries": "最多重试次数",
244
248
  "retry.baseDelayMs": "重试首次等待(毫秒)",
@@ -192,7 +192,7 @@ export declare const en: {
192
192
  image: string;
193
193
  channels: (names: readonly string[]) => string;
194
194
  price: string;
195
- sources: (parts: readonly string[], match: string | undefined, noTools: boolean) => string;
195
+ sources: (parts: readonly string[], match: string | undefined, noTools: boolean, catalog?: string) => string;
196
196
  };
197
197
  cacheProbe: {
198
198
  adviceSilent: (provider: string, cacheFieldPresent: boolean) => string;
@@ -478,7 +478,7 @@ export declare const zh: {
478
478
  image: string;
479
479
  channels: (names: readonly string[]) => string;
480
480
  price: string;
481
- sources: (parts: readonly string[], match: string | undefined, noTools: boolean) => string;
481
+ sources: (parts: readonly string[], match: string | undefined, noTools: boolean, catalog: string | undefined) => string;
482
482
  };
483
483
  cacheProbe: {
484
484
  adviceSilent: (provider: string, cacheFieldPresent: boolean) => string;
@@ -196,7 +196,8 @@ export const en = {
196
196
  image: "images",
197
197
  channels: (names) => `channels ${names.join(",")}`,
198
198
  price: "price",
199
- sources: (parts, match, noTools) => `sources ${parts.join(" · ")}` +
199
+ sources: (parts, match, noTools, catalog) => `sources ${parts.join(" · ")}` +
200
+ (catalog !== undefined ? `; catalog ${catalog}` : "") +
200
201
  (match !== undefined ? `; models.dev ${match}` : "") +
201
202
  (noTools ? "; no tool calling" : ""),
202
203
  },
@@ -429,7 +430,8 @@ export const zh = {
429
430
  image: "图片",
430
431
  channels: (names) => `渠道 ${names.join(",")}`,
431
432
  price: "价格",
432
- sources: (parts, match, noTools) => `来源 ${parts.join(" · ")}` +
433
+ sources: (parts, match, noTools, catalog) => `来源 ${parts.join(" · ")}` +
434
+ (catalog !== undefined ? `;目录 ${catalog}` : "") +
433
435
  (match !== undefined ? `;models.dev ${match}` : "") +
434
436
  (noTools ? ";不支持工具调用" : ""),
435
437
  },
@@ -31,7 +31,7 @@ import { AcpEventMapper, permissionModes, toolKind, toolLocations, toolTitle, }
31
31
  import { msg } from "../../i18n/index.js";
32
32
  import { CONFIG_IDS, applyConfigOption, availableCommands, buildConfigOptions, prepareConfigOptions, } from "./acp-config.js";
33
33
  import { clientCapabilitiesOf } from "./acp-connection.js";
34
- import { PromptQueue, cancelledResult, pageSessions, sessionTitle, } from "./acp-sessions.js";
34
+ import { PromptQueue, cancelledResult, pageSessions, promptWhenIdle, sessionTitle, } from "./acp-sessions.js";
35
35
  export const ACP_AGENT_CAPABILITIES = {
36
36
  loadSession: true,
37
37
  promptCapabilities: { image: true, audio: false, embeddedContext: true },
@@ -336,6 +336,7 @@ export class AcpServer {
336
336
  async prompt(params, signal) {
337
337
  const entry = this.entryOf(params);
338
338
  const { text, images } = promptOf(params["prompt"]);
339
+ let onAbort;
339
340
  const result = new Promise((resolve, reject) => {
340
341
  const job = {
341
342
  sessionId: entry.id,
@@ -347,13 +348,22 @@ export class AcpServer {
347
348
  };
348
349
  // 客户端以 $/cancel_request 撤回:等价 session/cancel,答复由对等端改成 -32800。
349
350
  // 连接关闭(stdin 结束)也会 abort 这个 signal——那时不算撤回,已开始的运行照常跑完。
350
- signal?.addEventListener("abort", () => {
351
+ onAbort = () => {
351
352
  if (this.peer.isOpen)
352
353
  this.cancelJob(job);
353
- }, { once: true });
354
+ };
355
+ signal?.addEventListener("abort", onAbort, { once: true });
354
356
  this.queue.push(job);
355
357
  });
356
- const answer = await result;
358
+ let answer;
359
+ try {
360
+ answer = await result;
361
+ }
362
+ finally {
363
+ // 有监听的组合信号(AbortSignal.any)被运行时强持有:不摘掉,job → 会话随之常驻(D2 L1)
364
+ if (onAbort !== undefined)
365
+ signal?.removeEventListener("abort", onAbort);
366
+ }
357
367
  if (signal?.aborted === true && this.peer.isOpen)
358
368
  throw new RpcError(RPC_ERRORS.requestCancelled, "session/prompt: request cancelled");
359
369
  return answer;
@@ -370,7 +380,7 @@ export class AcpServer {
370
380
  const before = session.getStats().tokens;
371
381
  let failure;
372
382
  try {
373
- await session.prompt(job.text, job.images.length > 0 ? { images: job.images } : {});
383
+ await promptWhenIdle(session, job);
374
384
  }
375
385
  catch (error) {
376
386
  failure = error;
@@ -78,3 +78,12 @@ export declare function decodeCursor(cursor: unknown): {
78
78
  };
79
79
  /** 一页会话(`cursor` 缺省从头)。 */
80
80
  export declare function pageSessions(items: readonly SessionListItem[], cursor: unknown, pageSize?: number): AcpListSessionsResult;
81
+ /**
82
+ * 跑一个出队的提示(docs/memory-plan.md D4、§2.2,Issue #139)。[M-A]
83
+ *
84
+ * 后台子 Agent 的完成通知以 `followUp` 在会话空闲时开回合,不经 {@link PromptQueue};那一回合在跑时
85
+ * 直接 `prompt` 会报 busy。这里先等它结束;与通知器竞速输了(等到空闲后它抢先开了回合)再等一轮。
86
+ * 不用 `streamingBehavior: "followUp"` 入队:`abort()` 不清队列,等待期间的 `session/cancel`
87
+ * 会让这条提示在之后的周期里冒出来。`job.cancelRequested`(cancel / close / 撤回)→ 不再发,直接返回。
88
+ */
89
+ export declare function promptWhenIdle(session: AgentSessionImpl, job: PromptJob): Promise<void>;
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { RPC_ERRORS, } from "../../drivers/acp/types.js";
10
10
  import { RpcError } from "../../drivers/jsonrpc.js";
11
+ import { AmaError } from "../../errors.js";
11
12
  import { msg } from "../../i18n/index.js";
12
13
  /** `session/list` 每页条数。 */
13
14
  export const LIST_PAGE_SIZE = 50;
@@ -155,3 +156,27 @@ export function pageSessions(items, cursor, pageSize = LIST_PAGE_SIZE) {
155
156
  ? { sessions, nextCursor: encodeCursor(last.updatedAt, last.sessionId) }
156
157
  : { sessions };
157
158
  }
159
+ /**
160
+ * 跑一个出队的提示(docs/memory-plan.md D4、§2.2,Issue #139)。[M-A]
161
+ *
162
+ * 后台子 Agent 的完成通知以 `followUp` 在会话空闲时开回合,不经 {@link PromptQueue};那一回合在跑时
163
+ * 直接 `prompt` 会报 busy。这里先等它结束;与通知器竞速输了(等到空闲后它抢先开了回合)再等一轮。
164
+ * 不用 `streamingBehavior: "followUp"` 入队:`abort()` 不清队列,等待期间的 `session/cancel`
165
+ * 会让这条提示在之后的周期里冒出来。`job.cancelRequested`(cancel / close / 撤回)→ 不再发,直接返回。
166
+ */
167
+ export async function promptWhenIdle(session, job) {
168
+ for (;;) {
169
+ // 等周期而不是看 `state.isStreaming`:busy 以周期为准,周期的收尾阶段 isStreaming 已是 false
170
+ await session.waitForIdle();
171
+ if (job.cancelRequested)
172
+ return;
173
+ try {
174
+ await session.prompt(job.text, job.images.length > 0 ? { images: job.images } : {});
175
+ return;
176
+ }
177
+ catch (error) {
178
+ if (!(error instanceof AmaError && error.code === "busy"))
179
+ throw error;
180
+ }
181
+ }
182
+ }
@@ -18,6 +18,8 @@ export declare const CONTEXT_TOP_RESULTS = 5;
18
18
  export interface ContextSnapshot {
19
19
  breakdown: ContextBreakdown;
20
20
  estimate: ContextEstimate;
21
+ /** 首次请求前:系统提示与工具声明按将要发送的那一份估算(与状态栏的启动基线同一口径)。 */
22
+ prefixPending: boolean;
21
23
  window: number | undefined;
22
24
  /** 0–100(一位小数,封顶 100);无窗口时 undefined。 */
23
25
  percent: number | undefined;
@@ -10,7 +10,7 @@
10
10
  * 统计只读:取活动分支的投影(与 `getStats().contextTokens` 同一口径),不改请求体、不写条目。
11
11
  */
12
12
  import { AgentSessionImpl } from "../agent/session.js";
13
- import { DEFAULT_COMPACTION_SETTINGS } from "../agent/session-compaction.js";
13
+ import { DEFAULT_COMPACTION_SETTINGS, effectiveWindow, pendingPrefixMessage, } from "../agent/session-compaction.js";
14
14
  import { breakdownMessages, largestToolResults, } from "../compaction/breakdown.js";
15
15
  import { estimateContextTokens, estimateProjectedTokens, } from "../compaction/estimate.js";
16
16
  import { prunePolicy } from "../compaction/prune-tier.js";
@@ -28,24 +28,35 @@ export function contextSnapshot(session) {
28
28
  let messages = session.messages;
29
29
  let estimate;
30
30
  let compaction = {};
31
+ let prefixPending = false;
31
32
  if (session instanceof AgentSessionImpl) {
32
33
  const branch = session.manager.branch();
33
34
  const items = buildProjection(branch).items;
34
35
  messages = items.map((item) => item.message);
35
36
  estimate = estimateProjectedTokens(items, branch);
36
37
  compaction = (session.options.compaction ?? {});
38
+ if (!messages.some((message) => "role" in message && message.role === "system")) {
39
+ prefixPending = true;
40
+ messages = [pendingPrefixMessage(session), ...messages];
41
+ }
37
42
  }
38
43
  else
39
44
  estimate = estimateContextTokens(messages);
40
- const window = session.getStats().contextWindow;
45
+ const stats = session.getStats();
46
+ // 「已用」与状态栏、getStats 同一口径(含启动基线)
47
+ if (stats.contextTokens !== undefined)
48
+ estimate = { ...estimate, tokens: stats.contextTokens };
49
+ const window = stats.contextWindow;
41
50
  const reserve = compaction.reserveTokens ?? DEFAULT_COMPACTION_SETTINGS.reserveTokens;
42
- const budget = window === undefined ? undefined : Math.max(0, window - reserve);
51
+ const soft = effectiveWindow(window, compaction);
52
+ const budget = soft === undefined ? undefined : Math.max(0, soft - reserve);
43
53
  const percent = window === undefined || window <= 0
44
54
  ? undefined
45
55
  : Math.min(100, Math.round((estimate.tokens / window) * 1000) / 10);
46
56
  return {
47
57
  breakdown: breakdownMessages(messages),
48
58
  estimate,
59
+ prefixPending,
49
60
  window,
50
61
  percent,
51
62
  autoCompaction: session.state.autoCompaction,
@@ -69,9 +80,11 @@ function headerRows(snap) {
69
80
  });
70
81
  rows.push({
71
82
  key: m.keySource,
72
- value: estimate.lastUsageIndex === null
73
- ? m.sourceEstimate
74
- : m.sourceUsage(formatTokenCount(estimate.usageTokens), formatTokenCount(estimate.trailingTokens)),
83
+ value: snap.prefixPending
84
+ ? m.sourcePrefix
85
+ : estimate.lastUsageIndex === null
86
+ ? m.sourceEstimate
87
+ : m.sourceUsage(formatTokenCount(estimate.usageTokens), formatTokenCount(estimate.trailingTokens)),
75
88
  });
76
89
  let auto;
77
90
  if (!snap.autoCompaction)
@@ -132,7 +145,7 @@ export function describeContext(session) {
132
145
  const { breakdown } = snap;
133
146
  const lines = [m.title, ...renderRows(headerRows(snap), " "), ""];
134
147
  lines.push(m.breakdown(formatTokenCount(breakdown.total)));
135
- lines.push(` ${breakdown.hasSystem ? m.prefixCounted : m.prefixPending}`);
148
+ lines.push(` ${snap.prefixPending ? m.prefixEstimated : breakdown.hasSystem ? m.prefixCounted : m.prefixPending}`);
136
149
  if (breakdown.total === 0)
137
150
  lines.push(` ${m.empty}`);
138
151
  else
@@ -157,7 +170,11 @@ export function contextPanel(session, theme) {
157
170
  "",
158
171
  theme.bold(m.breakdown(formatTokenCount(breakdown.total))),
159
172
  new Indent(new Stack([
160
- new Text(theme.fg("dim", breakdown.hasSystem ? m.prefixCounted : m.prefixPending)),
173
+ new Text(theme.fg("dim", snap.prefixPending
174
+ ? m.prefixEstimated
175
+ : breakdown.hasSystem
176
+ ? m.prefixCounted
177
+ : m.prefixPending)),
161
178
  breakdown.total === 0
162
179
  ? theme.fg("dim", m.empty)
163
180
  : table(categoryRows(breakdown, theme), true),
@@ -4,13 +4,24 @@
4
4
  * - `message_update` 变成纯增量:去掉累计消息与 `partial`,附最新 `usage`;`done / error`
5
5
  * 保留最终 `message`。`tool_execution_end` 去掉 `result.fileChange`([ACP-C0] 改前 / 改后全文只给
6
6
  * ACP 映射用,不上 RPC / stream-json 线)。其余 `SessionEvent` 原样。
7
+ * - [M-G] `compact`(RPC 客户端声明 `compact_events`,docs/memory-plan.md D9):`compactEvent()` 去掉
8
+ * 重复的大正文——`turn_end.toolResults[]` 每项只留 `{ toolCallId, toolName, isError, timestamp,
9
+ * contentOmitted }`;`message_start` 与 `entry_appended`(`message` 条目)里的 toolResult / 带图片的 user
10
+ * 消息 `content` 换成 `""` 并加 `contentOmitted: true`。`message_end` 与 `tool_execution_end` 保持全量。
11
+ * `stream-json` 不传 compact,输出不变。
7
12
  * - `toJsonLine()`:一行 JSON;Error 序列化为 `{ name, message }`,bigint 转字符串,
8
13
  * U+2028 / U+2029 转义(部分 JSONL 客户端会把它们当换行);图片 base64 不截断。
9
14
  */
10
15
  import type { SessionEvent } from "../../agent/types.js";
11
16
  import type { AssistantEvent } from "../../ai/types.js";
12
17
  import type { RpcEvent, WireAssistantEvent } from "../../rpc.js";
18
+ export interface WireOptions {
19
+ /** [M-G] 客户端声明了 `compact_events`。 */
20
+ compact?: boolean;
21
+ }
13
22
  export declare function toWireAssistantEvent(event: AssistantEvent): WireAssistantEvent;
14
- export declare function toWireEvent(event: SessionEvent): RpcEvent;
23
+ /** [M-G] 精简事件:只改 turn_end / message_start / entry_appended,其余(含 message_end)原样。 */
24
+ export declare function compactEvent(event: SessionEvent): SessionEvent;
25
+ export declare function toWireEvent(input: SessionEvent, options?: WireOptions): RpcEvent;
15
26
  /** 一行 JSON(不含换行符)。 */
16
27
  export declare function toJsonLine(value: unknown): string;
@@ -4,6 +4,11 @@
4
4
  * - `message_update` 变成纯增量:去掉累计消息与 `partial`,附最新 `usage`;`done / error`
5
5
  * 保留最终 `message`。`tool_execution_end` 去掉 `result.fileChange`([ACP-C0] 改前 / 改后全文只给
6
6
  * ACP 映射用,不上 RPC / stream-json 线)。其余 `SessionEvent` 原样。
7
+ * - [M-G] `compact`(RPC 客户端声明 `compact_events`,docs/memory-plan.md D9):`compactEvent()` 去掉
8
+ * 重复的大正文——`turn_end.toolResults[]` 每项只留 `{ toolCallId, toolName, isError, timestamp,
9
+ * contentOmitted }`;`message_start` 与 `entry_appended`(`message` 条目)里的 toolResult / 带图片的 user
10
+ * 消息 `content` 换成 `""` 并加 `contentOmitted: true`。`message_end` 与 `tool_execution_end` 保持全量。
11
+ * `stream-json` 不传 compact,输出不变。
7
12
  * - `toJsonLine()`:一行 JSON;Error 序列化为 `{ name, message }`,bigint 转字符串,
8
13
  * U+2028 / U+2029 转义(部分 JSONL 客户端会把它们当换行);图片 base64 不截断。
9
14
  */
@@ -13,7 +18,49 @@ export function toWireAssistantEvent(event) {
13
18
  const { partial: _partial, ...rest } = event;
14
19
  return rest;
15
20
  }
16
- export function toWireEvent(event) {
21
+ /** toolResult 与带图片的 user 消息:正文换成 `""` 并标 `contentOmitted`;其它消息原样。 */
22
+ function omitContent(message) {
23
+ const omit = message.role === "toolResult" ||
24
+ (message.role === "user" &&
25
+ Array.isArray(message.content) &&
26
+ message.content.some((block) => block.type === "image"));
27
+ return omit ? { ...message, content: "", contentOmitted: true } : message;
28
+ }
29
+ /** [M-G] 精简事件:只改 turn_end / message_start / entry_appended,其余(含 message_end)原样。 */
30
+ export function compactEvent(event) {
31
+ switch (event.type) {
32
+ case "turn_end":
33
+ if (event.toolResults.length === 0)
34
+ return event;
35
+ return {
36
+ ...event,
37
+ // 线上形状(docs/rpc.md)不是完整的 ToolResultMessage:正文与 details 都不带
38
+ toolResults: event.toolResults.map(({ toolCallId, toolName, isError, timestamp }) => ({
39
+ toolCallId,
40
+ toolName,
41
+ isError,
42
+ timestamp,
43
+ contentOmitted: true,
44
+ })),
45
+ };
46
+ case "message_start": {
47
+ const message = omitContent(event.message);
48
+ return message === event.message ? event : { ...event, message };
49
+ }
50
+ case "entry_appended": {
51
+ if (event.entry.type !== "message")
52
+ return event;
53
+ const message = omitContent(event.entry.message);
54
+ return message === event.entry.message
55
+ ? event
56
+ : { ...event, entry: { ...event.entry, message } };
57
+ }
58
+ default:
59
+ return event;
60
+ }
61
+ }
62
+ export function toWireEvent(input, options = {}) {
63
+ const event = options.compact === true ? compactEvent(input) : input;
17
64
  if (event.type === "tool_execution_end" && event.result.fileChange !== undefined) {
18
65
  const { fileChange: _fileChange, ...result } = event.result;
19
66
  return { ...event, result };
@@ -6,6 +6,7 @@
6
6
  * - 审批:客户端 `set_client_capabilities{capabilities:["approvals"]}` 之后才挂 UI broker;
7
7
  * 之前的 ask 无人作答 → deny。`permission_request` 带 `timeoutMs`,超时由会话 deny 并发
8
8
  * `permission_resolved`。
9
+ * - [M-G] 声明 `compact_events` 之后的事件经 `toWireEvent(event, { compact: true })` 精简(json-event.ts)。
9
10
  * - 宿主 `ui.notify` → `{type:"notification", level, message}` 一行(stderr 同时一份)。
10
11
  * - stdin 关闭:撤下审批(之后的 ask 无人作答 → deny)、等在途命令与已开始的运行结束、
11
12
  * 应答写完 → 退出 0(会话由 runCli dispose)。`printf '{…prompt…}' | ama --mode rpc` 因此能拿到
@@ -6,6 +6,7 @@
6
6
  * - 审批:客户端 `set_client_capabilities{capabilities:["approvals"]}` 之后才挂 UI broker;
7
7
  * 之前的 ask 无人作答 → deny。`permission_request` 带 `timeoutMs`,超时由会话 deny 并发
8
8
  * `permission_resolved`。
9
+ * - [M-G] 声明 `compact_events` 之后的事件经 `toWireEvent(event, { compact: true })` 精简(json-event.ts)。
9
10
  * - 宿主 `ui.notify` → `{type:"notification", level, message}` 一行(stderr 同时一份)。
10
11
  * - stdin 关闭:撤下审批(之后的 ask 无人作答 → deny)、等在途命令与已开始的运行结束、
11
12
  * 应答写完 → 退出 0(会话由 runCli dispose)。`printf '{…prompt…}' | ama --mode rpc` 因此能拿到
@@ -24,7 +25,13 @@ import { cachedAgentInfos } from "../../agents/external.js";
24
25
  import { RpcApprovals, handlers } from "./commands.js";
25
26
  import { createLineReader, writeChunked } from "./jsonl.js";
26
27
  import { msg } from "../../i18n/index.js";
27
- export const RPC_CAPABILITIES = ["approvals", "images", "hooks", "plans"];
28
+ export const RPC_CAPABILITIES = [
29
+ "approvals",
30
+ "images",
31
+ "hooks",
32
+ "plans",
33
+ "compact_events",
34
+ ];
28
35
  export async function runRpcMode(runtime, context, options = {}) {
29
36
  const stdin = options.stdin ?? process.stdin;
30
37
  const stdout = options.stdout ?? process.stdout;
@@ -34,7 +41,7 @@ export async function runRpcMode(runtime, context, options = {}) {
34
41
  writing = writing.then(() => writeChunked(stdout, line)).catch(() => undefined);
35
42
  return writing;
36
43
  };
37
- const subscribe = (s) => s.subscribe((event) => void write(toWireEvent(event)));
44
+ const subscribe = (s) => s.subscribe((event) => void write(toWireEvent(event, { compact: ctx.capabilities.has("compact_events") })));
38
45
  let session = currentSession(runtime);
39
46
  let unsubscribe = subscribe(session);
40
47
  const approvals = new RpcApprovals();
@@ -72,5 +72,5 @@ export declare function subscriptionRows(session: AgentSession, now?: number): K
72
72
  export declare function renderRows(rows: readonly KeyValueRow[], indent?: string): string[];
73
73
  export declare function describeCache(session: AgentSession, now?: number): string;
74
74
  export declare function describeSession(session: AgentSession, now?: number): string;
75
- /** `/cache fingerprint`:最近一次真实请求的前缀指纹(system / tools 哈希与模型)。 */
75
+ /** `/cache fingerprint`:最近一次真实请求的前缀指纹(system / tools 哈希与模型;[ME-B] 再逐节列哈希)。 */
76
76
  export declare function describeFingerprint(session: AgentSession): string;
@@ -80,11 +80,13 @@ export function missReasonText(miss) {
80
80
  case "model_changed":
81
81
  return m.missModel;
82
82
  case "prefix_changed":
83
- return miss.detail === "tools"
84
- ? m.missTools
85
- : miss.detail === "system"
86
- ? m.missSystem
87
- : m.missPrefix;
83
+ if (miss.detail === "tools")
84
+ return m.missTools;
85
+ if (miss.detail === "system")
86
+ return m.missSystem;
87
+ if (miss.detail?.startsWith("system:") === true)
88
+ return m.missSystemSections(miss.detail.slice("system:".length).replaceAll(",", ", "));
89
+ return m.missPrefix;
88
90
  case "evicted":
89
91
  return m.missEvicted;
90
92
  }
@@ -325,7 +327,7 @@ export function describeSession(session, now = Date.now()) {
325
327
  ...(external.length > 0 ? [m.externalTitle, ...renderRows(external, " ")] : []),
326
328
  ].join("\n");
327
329
  }
328
- /** `/cache fingerprint`:最近一次真实请求的前缀指纹(system / tools 哈希与模型)。 */
330
+ /** `/cache fingerprint`:最近一次真实请求的前缀指纹(system / tools 哈希与模型;[ME-B] 再逐节列哈希)。 */
329
331
  export function describeFingerprint(session) {
330
332
  const m = msg().report.session;
331
333
  if (!(session instanceof AgentSessionImpl))
@@ -338,6 +340,10 @@ export function describeFingerprint(session) {
338
340
  m.fingerprintTitle,
339
341
  ...renderRows([
340
342
  { key: "system", value: f.system },
343
+ ...Object.entries(f.sections ?? {}).map(([name, hash]) => ({
344
+ key: ` ${name}`,
345
+ value: hash,
346
+ })),
341
347
  { key: "tools", value: f.tools },
342
348
  { key: "model", value: f.model },
343
349
  ], " "),
package/dist/rpc.d.ts CHANGED
@@ -33,8 +33,11 @@ import type { TracePreview } from "./trace/preview.js";
33
33
  import type { Trace, TraceSubagentNode } from "./trace/types.js";
34
34
  export type { SessionEvent } from "./agent/types.js";
35
35
  export declare const RPC_PROTOCOL_VERSION: 1;
36
- /** `plans`(W5-C0):客户端声明后计划审批交给客户端(`plan_proposed` → `plan_response`)。 */
37
- export type RpcCapability = "approvals" | "images" | "hooks" | "plans";
36
+ /**
37
+ * `plans`(W5-C0):客户端声明后计划审批交给客户端(`plan_proposed` → `plan_response`)。
38
+ * `compact_events`(M-G):客户端声明后 turn_end / message_start / entry_appended 不重复带工具结果与图片正文。
39
+ */
40
+ export type RpcCapability = "approvals" | "images" | "hooks" | "plans" | "compact_events";
38
41
  export interface RpcHello {
39
42
  type: "hello";
40
43
  protocolVersion: typeof RPC_PROTOCOL_VERSION;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 会话文件的字节级按行读取(docs/memory-plan.md D6、§2.4)。[M-C0] 实现与单测;调用方由 [M-C] 接入
3
+ * (`store.ts readSessionLines`、`scan.ts forEachLine`、`list.ts`)。
4
+ *
5
+ * - fd + 一块复用的缓冲(缺省 64 KiB)顺序 `readSync`,以 `indexOf(0x0a)` 切行;跨块的残片挪到缓冲
6
+ * 开头续读,一行比缓冲还长时缓冲翻倍——每行不再分配,不生成全文字符串,也不 split 出整个数组;
7
+ * - 回调拿到的 `line` 不含 `\n` 与行尾的一个 `\r`,**只在回调期间有效**(可能是复用块的视图),
8
+ * 需要保留时自行拷贝或解码;空行照样回调(是否跳过由调用方决定);
9
+ * - `index` 是物理行号(0 起,含空行);`byteOffset` 是该行首字节在文件中的偏移(修复半行时
10
+ * 直接 `truncateSync(file, byteOffset)`);`last` 为 true 表示这是**没有以 `\n` 结尾**的末行;
11
+ * 文件以 `\n` 结尾时不会多回调一个空的末行;
12
+ * - 回调返回 `false` 立即停止读盘并关闭 fd。
13
+ */
14
+ /** 缺省块大小。 */
15
+ export declare const LINE_CHUNK_BYTES: number;
16
+ export interface LineVisit {
17
+ (line: Buffer, index: number, last: boolean, byteOffset: number): boolean | void;
18
+ }
19
+ /** 逐行访问 `file`(同步);打不开时抛 `fs` 的原始错误。 */
20
+ export declare function forEachLineSync(file: string, visit: LineVisit, chunkBytes?: number): void;
21
+ /**
22
+ * ama 写的行:不解析、只解码行首就取出 type(与消息的 role),口径同 `scan.ts lineType`;
23
+ * 不是这种形状(或行首不足以判断 role)时 undefined,调用方退回 `JSON.parse`。
24
+ */
25
+ export declare function lineTypeOf(line: Buffer): {
26
+ type: string;
27
+ role?: string;
28
+ } | undefined;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 会话文件的字节级按行读取(docs/memory-plan.md D6、§2.4)。[M-C0] 实现与单测;调用方由 [M-C] 接入
3
+ * (`store.ts readSessionLines`、`scan.ts forEachLine`、`list.ts`)。
4
+ *
5
+ * - fd + 一块复用的缓冲(缺省 64 KiB)顺序 `readSync`,以 `indexOf(0x0a)` 切行;跨块的残片挪到缓冲
6
+ * 开头续读,一行比缓冲还长时缓冲翻倍——每行不再分配,不生成全文字符串,也不 split 出整个数组;
7
+ * - 回调拿到的 `line` 不含 `\n` 与行尾的一个 `\r`,**只在回调期间有效**(可能是复用块的视图),
8
+ * 需要保留时自行拷贝或解码;空行照样回调(是否跳过由调用方决定);
9
+ * - `index` 是物理行号(0 起,含空行);`byteOffset` 是该行首字节在文件中的偏移(修复半行时
10
+ * 直接 `truncateSync(file, byteOffset)`);`last` 为 true 表示这是**没有以 `\n` 结尾**的末行;
11
+ * 文件以 `\n` 结尾时不会多回调一个空的末行;
12
+ * - 回调返回 `false` 立即停止读盘并关闭 fd。
13
+ */
14
+ import { closeSync, openSync, readSync } from "node:fs";
15
+ import { lineType } from "./scan.js";
16
+ /** 缺省块大小。 */
17
+ export const LINE_CHUNK_BYTES = 64 * 1024;
18
+ const LF = 0x0a;
19
+ const CR = 0x0d;
20
+ function stripCr(line) {
21
+ return line.length > 0 && line[line.length - 1] === CR ? line.subarray(0, -1) : line;
22
+ }
23
+ /** 逐行访问 `file`(同步);打不开时抛 `fs` 的原始错误。 */
24
+ export function forEachLineSync(file, visit, chunkBytes = LINE_CHUNK_BYTES) {
25
+ const fd = openSync(file, "r");
26
+ try {
27
+ // 一块复用的缓冲:未成行的残片挪到开头再续读;一行比缓冲还长时才翻倍(之后沿用)
28
+ let buf = Buffer.allocUnsafe(Math.max(1, Math.floor(chunkBytes)));
29
+ let start = 0; // 未成行数据在 buf 中的起点
30
+ let end = 0; // 有效数据的终点
31
+ let index = 0;
32
+ let lineStart = 0; // `start` 处对应的文件偏移
33
+ let position = 0;
34
+ for (;;) {
35
+ if (start > 0) {
36
+ buf.copyWithin(0, start, end);
37
+ end -= start;
38
+ start = 0;
39
+ }
40
+ if (end === buf.length) {
41
+ const grown = Buffer.allocUnsafe(buf.length * 2);
42
+ buf.copy(grown, 0, 0, end);
43
+ buf = grown;
44
+ }
45
+ const scanFrom = end; // 残片里已确认没有 \n
46
+ const read = readSync(fd, buf, end, buf.length - end, position);
47
+ if (read === 0)
48
+ break;
49
+ position += read;
50
+ end += read;
51
+ const view = buf.subarray(0, end);
52
+ for (let nl = view.indexOf(LF, scanFrom); nl >= 0; nl = view.indexOf(LF, start)) {
53
+ const line = view.subarray(start, nl);
54
+ const offset = lineStart;
55
+ lineStart += nl + 1 - start;
56
+ start = nl + 1;
57
+ if (visit(stripCr(line), index++, false, offset) === false)
58
+ return;
59
+ }
60
+ }
61
+ if (end > start)
62
+ visit(stripCr(buf.subarray(start, end)), index, true, lineStart);
63
+ }
64
+ finally {
65
+ closeSync(fd);
66
+ }
67
+ }
68
+ /** 只解码这么多字节来判断行类型(`{"type":"message","message":{"role":"toolResult"` 约 48 字节)。 */
69
+ const TYPE_HEAD_BYTES = 96;
70
+ /**
71
+ * ama 写的行:不解析、只解码行首就取出 type(与消息的 role),口径同 `scan.ts lineType`;
72
+ * 不是这种形状(或行首不足以判断 role)时 undefined,调用方退回 `JSON.parse`。
73
+ */
74
+ export function lineTypeOf(line) {
75
+ if (line.length <= TYPE_HEAD_BYTES)
76
+ return lineType(line.toString("utf8"));
77
+ const found = lineType(line.subarray(0, TYPE_HEAD_BYTES).toString("utf8"));
78
+ // role 可能被截断:宁可让调用方完整解析,也不给出与 `lineType` 不同的答案
79
+ if (found?.type === "message" && found.role === undefined)
80
+ return undefined;
81
+ return found;
82
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 会话列表(`SessionManager.list` 的实现)。[M-C] 按块逐行读(docs/memory-plan.md D6、§2.4):
3
+ * 只解析头、第一条条目、`leaf` / `session_info` 行、要取 `firstPrompt` 的那一条 user 消息与末行;
4
+ * 其余 ama 写的行只看行首的 type / role(`lineTypeOf`),不生成全文字符串、不留条目对象。
5
+ *
6
+ * 口径与「`migrateSessionLines(readSessionLines(file).lines)` 再统计」一致:空行跳过、末尾半行忽略、
7
+ * 头必须是 `type: "session"` / `version === SESSION_FORMAT_VERSION` / id 与 cwd 为字符串、`leaf` 行不算
8
+ * 条目、重复的头与解析失败的中间行让整个文件跳过。差别只有一处:不解析的 ama 行不再逐条检查
9
+ * `id`(ama 写入的条目总有 id);行尾不是 `}` 的(写到一半又被追加的坏行)仍会退回完整解析。
10
+ * 不经 `migrateSessionLines`——格式升到 v2 时要同时改这里(R6,见 migrate.ts)。
11
+ */
12
+ import { type SessionListItem } from "./types.js";
13
+ /** 只读列出目录下的会话(最新在前);损坏的文件跳过。 */
14
+ export declare function listSessionItems(dir: string): SessionListItem[];