@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
@@ -19,9 +19,10 @@
19
19
  import { realpathSync } from "node:fs";
20
20
  import { resolve } from "node:path";
21
21
  import { stopReasonOf } from "../../ai/apis/shared.js";
22
- import { createSessionAlongside, currentSession, disposeSessionAlongside, setForegroundSession, } from "../../cli/compose-session.js";
22
+ import { createSessionAlongside, currentSession, setForegroundSession, } from "../../cli/compose-session.js";
23
23
  import { listSessions } from "../../cli/compose-store.js";
24
24
  import { AgentSessionImpl } from "../../agent/session.js";
25
+ import { taskControl } from "../../agents/task-control.js";
25
26
  import { RpcError } from "../../drivers/jsonrpc.js";
26
27
  import { ACP_METHODS, ACP_PROTOCOL_VERSION, RPC_ERRORS, } from "../../drivers/acp/types.js";
27
28
  import { isPermissionMode } from "../../permissions/modes.js";
@@ -31,7 +32,7 @@ import { AcpEventMapper, permissionModes, toolKind, toolLocations, toolTitle, }
31
32
  import { msg } from "../../i18n/index.js";
32
33
  import { CONFIG_IDS, applyConfigOption, availableCommands, buildConfigOptions, prepareConfigOptions, } from "./acp-config.js";
33
34
  import { clientCapabilitiesOf } from "./acp-connection.js";
34
- import { PromptQueue, cancelledResult, pageSessions, sessionTitle, } from "./acp-sessions.js";
35
+ import { PromptQueue, cancelledResult, disposeQuiet, pageSessions, promptWhenIdle, quiesce, sessionTitle, } from "./acp-sessions.js";
35
36
  export const ACP_AGENT_CAPABILITIES = {
36
37
  loadSession: true,
37
38
  promptCapabilities: { image: true, audio: false, embeddedContext: true },
@@ -157,8 +158,9 @@ export class AcpServer {
157
158
  .abort()
158
159
  .catch(() => undefined);
159
160
  await this.queue.settled();
160
- for (const session of this.sessions())
161
- await session.waitForIdle().catch(() => undefined);
161
+ for (const s of this.sessions())
162
+ if (s instanceof AgentSessionImpl)
163
+ await quiesce(s, taskControl(s.state.sessionId));
162
164
  const foreground = this.session();
163
165
  for (const entry of this.pool.values())
164
166
  entry.unsubscribe();
@@ -167,7 +169,7 @@ export class AcpServer {
167
169
  continue;
168
170
  if (!(foreground instanceof AgentSessionImpl))
169
171
  continue;
170
- await disposeSessionAlongside(this.runtime, session, foreground).catch(() => undefined);
172
+ await disposeQuiet(this.runtime, session, foreground).catch(() => undefined);
171
173
  }
172
174
  this.pool.clear();
173
175
  this.standby = undefined;
@@ -311,12 +313,11 @@ export class AcpServer {
311
313
  return {};
312
314
  this.pool.delete(id);
313
315
  this.queue.cancelQueued(id);
314
- const running = this.queue.running;
315
- if (running?.sessionId === id) {
316
- running.cancelRequested = true;
317
- await entry.session.abort().catch(() => undefined);
318
- await this.queue.settled(id);
319
- }
316
+ // [#167] 通知回合不经队列:不论谁在跑都先安静下来(停后台任务、abort 到空闲)
317
+ if (this.queue.running?.sessionId === id)
318
+ this.queue.running.cancelRequested = true;
319
+ await quiesce(entry.session, taskControl(id));
320
+ await this.queue.settled(id);
320
321
  entry.unsubscribe();
321
322
  let fallback = this.session();
322
323
  if (fallback === entry.session) {
@@ -330,12 +331,13 @@ export class AcpServer {
330
331
  }
331
332
  }
332
333
  if (fallback instanceof AgentSessionImpl)
333
- await disposeSessionAlongside(this.runtime, entry.session, fallback);
334
+ await disposeQuiet(this.runtime, entry.session, fallback, taskControl(id));
334
335
  return {};
335
336
  }
336
337
  async prompt(params, signal) {
337
338
  const entry = this.entryOf(params);
338
339
  const { text, images } = promptOf(params["prompt"]);
340
+ let onAbort;
339
341
  const result = new Promise((resolve, reject) => {
340
342
  const job = {
341
343
  sessionId: entry.id,
@@ -347,13 +349,22 @@ export class AcpServer {
347
349
  };
348
350
  // 客户端以 $/cancel_request 撤回:等价 session/cancel,答复由对等端改成 -32800。
349
351
  // 连接关闭(stdin 结束)也会 abort 这个 signal——那时不算撤回,已开始的运行照常跑完。
350
- signal?.addEventListener("abort", () => {
352
+ onAbort = () => {
351
353
  if (this.peer.isOpen)
352
354
  this.cancelJob(job);
353
- }, { once: true });
355
+ };
356
+ signal?.addEventListener("abort", onAbort, { once: true });
354
357
  this.queue.push(job);
355
358
  });
356
- const answer = await result;
359
+ let answer;
360
+ try {
361
+ answer = await result;
362
+ }
363
+ finally {
364
+ // 有监听的组合信号(AbortSignal.any)被运行时强持有:不摘掉,job → 会话随之常驻(D2 L1)
365
+ if (onAbort !== undefined)
366
+ signal?.removeEventListener("abort", onAbort);
367
+ }
357
368
  if (signal?.aborted === true && this.peer.isOpen)
358
369
  throw new RpcError(RPC_ERRORS.requestCancelled, "session/prompt: request cancelled");
359
370
  return answer;
@@ -367,16 +378,17 @@ export class AcpServer {
367
378
  setForegroundSession(this.runtime, session);
368
379
  if (this.runtime.permission.mode !== entry.mode)
369
380
  session.setPermissionMode(entry.mode);
370
- const before = session.getStats().tokens;
381
+ let before = session.getStats().tokens;
371
382
  let failure;
372
383
  try {
373
- await session.prompt(job.text, job.images.length > 0 ? { images: job.images } : {});
384
+ // [#166] 起点取在真正发起时:等待期间结束的通知回合不计入本次 usage
385
+ await promptWhenIdle(session, job, () => void (before = session.getStats().tokens));
374
386
  }
375
387
  catch (error) {
376
388
  failure = error;
377
389
  }
378
- await this.peer.flush();
379
390
  const after = session.getStats().tokens;
391
+ await this.peer.flush();
380
392
  const usage = {
381
393
  inputTokens: after.input - before.input,
382
394
  outputTokens: after.output - before.output,
@@ -8,6 +8,8 @@
8
8
  */
9
9
  import type { ImageBlock } from "../../ai/types.js";
10
10
  import type { AgentSessionImpl } from "../../agent/session.js";
11
+ import type { TaskControl } from "../../agents/task-control.js";
12
+ import type { Runtime } from "../../cli/runtime.js";
11
13
  import type { AgentMessage, SessionListItem } from "../../session/types.js";
12
14
  import { type AcpPromptResult, type AcpListSessionsResult } from "../../drivers/acp/types.js";
13
15
  import type { PermissionMode } from "../../permissions/types.js";
@@ -78,3 +80,23 @@ export declare function decodeCursor(cursor: unknown): {
78
80
  };
79
81
  /** 一页会话(`cursor` 缺省从头)。 */
80
82
  export declare function pageSessions(items: readonly SessionListItem[], cursor: unknown, pageSize?: number): AcpListSessionsResult;
83
+ /**
84
+ * 跑一个出队的提示(docs/memory-plan.md D4、§2.2,Issue #139)。[M-A]
85
+ *
86
+ * 后台子 Agent 的完成通知以 `followUp` 在会话空闲时开回合,不经 {@link PromptQueue};那一回合在跑时
87
+ * 直接 `prompt` 会报 busy。这里先等它结束;与通知器竞速输了(等到空闲后它抢先开了回合)再等一轮。
88
+ * 不用 `streamingBehavior: "followUp"` 入队:`abort()` 不清队列,等待期间的 `session/cancel`
89
+ * 会让这条提示在之后的周期里冒出来。`job.cancelRequested`(cancel / close / 撤回)→ 不再发,直接返回。
90
+ */
91
+ export declare function promptWhenIdle(session: AgentSessionImpl, job: PromptJob, onStart?: () => void): Promise<void>;
92
+ /**
93
+ * 关会话前把它安静下来(Issue #167):先停在跑的后台任务(被停止的任务不发完成通知),再反复 abort,
94
+ * 直到隔一个宏任务仍然空闲——已完成任务排在投递链上的通知会在上一周期结算后的微任务里开新周期,
95
+ * 一次 abort 不够。超过 `rounds` 轮仍不空闲返回 false。被打断的通知回合与用户 cancel 同样留下中断标记。
96
+ */
97
+ export declare function quiesce(session: AgentSessionImpl, control: TaskControl | undefined, rounds?: number): Promise<boolean>;
98
+ /**
99
+ * `disposeSessionAlongside` 的安静版:撞上 busy(SessionEnd Hook 期间又开了通知回合)→ 再 quiesce 重试;
100
+ * dispose 之后若还有周期(与 dispose 同一时刻开出来的)再 abort 一次。
101
+ */
102
+ export declare function disposeQuiet(runtime: Runtime, session: AgentSessionImpl, fallback: AgentSessionImpl, control?: TaskControl, retries?: number): Promise<void>;
@@ -6,8 +6,10 @@
6
6
  * - 同一时刻只跑一个回合,其它会话的 `session/prompt` 进 {@link PromptQueue}(FIFO),不再回 busy;
7
7
  * - `session/list`:按 `updatedAt` 降序、`sessionId` 次序分页,cursor 是 `updatedAt|sessionId` 的 base64。
8
8
  */
9
+ import { disposeSessionAlongside } from "../../cli/compose-session.js";
9
10
  import { RPC_ERRORS, } from "../../drivers/acp/types.js";
10
11
  import { RpcError } from "../../drivers/jsonrpc.js";
12
+ import { AmaError } from "../../errors.js";
11
13
  import { msg } from "../../i18n/index.js";
12
14
  /** `session/list` 每页条数。 */
13
15
  export const LIST_PAGE_SIZE = 50;
@@ -155,3 +157,69 @@ export function pageSessions(items, cursor, pageSize = LIST_PAGE_SIZE) {
155
157
  ? { sessions, nextCursor: encodeCursor(last.updatedAt, last.sessionId) }
156
158
  : { sessions };
157
159
  }
160
+ /**
161
+ * 跑一个出队的提示(docs/memory-plan.md D4、§2.2,Issue #139)。[M-A]
162
+ *
163
+ * 后台子 Agent 的完成通知以 `followUp` 在会话空闲时开回合,不经 {@link PromptQueue};那一回合在跑时
164
+ * 直接 `prompt` 会报 busy。这里先等它结束;与通知器竞速输了(等到空闲后它抢先开了回合)再等一轮。
165
+ * 不用 `streamingBehavior: "followUp"` 入队:`abort()` 不清队列,等待期间的 `session/cancel`
166
+ * 会让这条提示在之后的周期里冒出来。`job.cancelRequested`(cancel / close / 撤回)→ 不再发,直接返回。
167
+ */
168
+ export async function promptWhenIdle(session, job, onStart) {
169
+ for (;;) {
170
+ // 等周期而不是看 `state.isStreaming`:busy 以周期为准,周期的收尾阶段 isStreaming 已是 false
171
+ await session.waitForIdle();
172
+ if (job.cancelRequested)
173
+ return;
174
+ // [#166] 每次真正发起前回调(busy 重试时再调,最后一次为准):usage 的起点不含之前的通知回合
175
+ onStart?.();
176
+ try {
177
+ await session.prompt(job.text, job.images.length > 0 ? { images: job.images } : {});
178
+ return;
179
+ }
180
+ catch (error) {
181
+ if (!(error instanceof AmaError && error.code === "busy"))
182
+ throw error;
183
+ }
184
+ }
185
+ }
186
+ const BUSY = Symbol("busy");
187
+ /** 当前没有周期(含收尾阶段):`waitForIdle()` 已结算即空闲。 */
188
+ async function idleNow(session) {
189
+ return (await Promise.race([session.waitForIdle(), Promise.resolve(BUSY)])) !== BUSY;
190
+ }
191
+ /**
192
+ * 关会话前把它安静下来(Issue #167):先停在跑的后台任务(被停止的任务不发完成通知),再反复 abort,
193
+ * 直到隔一个宏任务仍然空闲——已完成任务排在投递链上的通知会在上一周期结算后的微任务里开新周期,
194
+ * 一次 abort 不够。超过 `rounds` 轮仍不空闲返回 false。被打断的通知回合与用户 cancel 同样留下中断标记。
195
+ */
196
+ export async function quiesce(session, control, rounds = 50) {
197
+ for (const task of control?.list() ?? [])
198
+ if (task.status === "running")
199
+ await control?.stop(task.taskId).catch(() => undefined);
200
+ for (let i = 0; i < rounds; i++) {
201
+ await session.abort();
202
+ await new Promise((resolve) => setImmediate(resolve));
203
+ if (await idleNow(session))
204
+ return true;
205
+ }
206
+ return false;
207
+ }
208
+ /**
209
+ * `disposeSessionAlongside` 的安静版:撞上 busy(SessionEnd Hook 期间又开了通知回合)→ 再 quiesce 重试;
210
+ * dispose 之后若还有周期(与 dispose 同一时刻开出来的)再 abort 一次。
211
+ */
212
+ export async function disposeQuiet(runtime, session, fallback, control, retries = 3) {
213
+ for (let i = 0;; i++) {
214
+ try {
215
+ await disposeSessionAlongside(runtime, session, fallback);
216
+ break;
217
+ }
218
+ catch (error) {
219
+ if (i >= retries || !(error instanceof AmaError && error.code === "busy"))
220
+ throw error;
221
+ await quiesce(session, control);
222
+ }
223
+ }
224
+ await session.abort();
225
+ }
@@ -10,7 +10,7 @@
10
10
  * 统计只读:取活动分支的投影(与 `getStats().contextTokens` 同一口径),不改请求体、不写条目。
11
11
  */
12
12
  import { AgentSessionImpl } from "../agent/session.js";
13
- import { DEFAULT_COMPACTION_SETTINGS, pendingPrefixMessage } 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";
@@ -48,7 +48,8 @@ export function contextSnapshot(session) {
48
48
  estimate = { ...estimate, tokens: stats.contextTokens };
49
49
  const window = stats.contextWindow;
50
50
  const reserve = compaction.reserveTokens ?? DEFAULT_COMPACTION_SETTINGS.reserveTokens;
51
- 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);
52
53
  const percent = window === undefined || window <= 0
53
54
  ? undefined
54
55
  : Math.min(100, Math.round((estimate.tokens / window) * 1000) / 10);
@@ -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[];