@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
@@ -14,9 +14,10 @@
14
14
  * 内容推断。
15
15
  */
16
16
  import { AssistantEventStreamImpl } from "../event-stream.js";
17
- import { authHeaders, describeErrorJson, idleTimeoutOf, joinUrl, mergeHeaders, USER_AGENT, } from "../http.js";
17
+ import { authHeaders, describeErrorJson, idleTimeoutOf, streamIdleTimeoutOf, joinUrl, mergeHeaders, USER_AGENT, } from "../http.js";
18
18
  import { readSseEvents } from "../sse.js";
19
- import { affinityHeaders, postWithCacheFallback, resolveCacheRetention } from "./cache-params.js";
19
+ import { affinityHeaders, resolveCacheRetention } from "./cache-params.js";
20
+ import { clampRequestMaxTokens, postWithMaxTokensFallback } from "./max-tokens.js";
20
21
  import { detectCompat } from "./openai-compat.js";
21
22
  import { REASONING_FIELDS, buildOpenAIRequest } from "./openai-request.js";
22
23
  import { BlockTracker, STREAM_ENDED_MESSAGE, ProviderStopError, createOutput, finishDone, finishError, requireApiKey, } from "./shared.js";
@@ -183,16 +184,20 @@ async function run(stream, model, context, options) {
183
184
  if (request.providerThinkingLevel !== undefined) {
184
185
  tracker.output.providerThinkingLevel = request.providerThinkingLevel;
185
186
  }
187
+ const field = request.compat.maxTokensField;
188
+ clampRequestMaxTokens(request.body, field, model.contextWindow);
186
189
  const replaced = options.onPayload?.(request.body);
187
190
  const baseUrl = model.baseUrl ?? "https://api.openai.com/v1";
188
- const response = await postWithCacheFallback(model, joinUrl(baseUrl, "/chat/completions"), {
191
+ const url = joinUrl(baseUrl, "/chat/completions");
192
+ const post = {
189
193
  headers: buildHeaders(model, options),
190
194
  body: replaced === undefined ? request.body : replaced,
191
195
  signal: options.signal,
192
196
  timeoutMs: options.timeoutMs,
193
197
  idleTimeoutMs: idleTimeoutOf(options),
194
198
  onResponse: options.onResponse,
195
- });
199
+ };
200
+ const response = await postWithMaxTokensFallback(model, url, post, field);
196
201
  stream.push({ type: "start", partial: tracker.output });
197
202
  const state = {
198
203
  text: undefined,
@@ -204,7 +209,7 @@ async function run(stream, model, context, options) {
204
209
  sawDone: false,
205
210
  };
206
211
  const body = response.body;
207
- for await (const sse of readSseEvents(body, options.signal, idleTimeoutOf(options))) {
212
+ for await (const sse of readSseEvents(body, options.signal, streamIdleTimeoutOf(options))) {
208
213
  const data = sse.data.trim();
209
214
  if (data === "")
210
215
  continue;
@@ -67,7 +67,7 @@ function convertAssistant(model, message, compat) {
67
67
  out["tool_calls"] = calls.map((call) => ({
68
68
  id: normalizeCallId(model, call.id),
69
69
  type: "function",
70
- function: { name: call.name, arguments: JSON.stringify(call.arguments) },
70
+ function: { name: call.name, arguments: call.rawArguments ?? JSON.stringify(call.arguments) },
71
71
  }));
72
72
  }
73
73
  if (out["content"] === null && calls.length === 0)
@@ -162,7 +162,7 @@ function convertAssistant(model, message) {
162
162
  ...(sameModel && itemId?.startsWith("fc") ? { id: itemId } : {}),
163
163
  call_id: block.id,
164
164
  name: block.name,
165
- arguments: JSON.stringify(block.arguments),
165
+ arguments: block.rawArguments ?? JSON.stringify(block.arguments),
166
166
  });
167
167
  }
168
168
  }
@@ -17,9 +17,10 @@
17
17
  * 错误映射与订阅用量见 chatgpt-backend.ts。
18
18
  */
19
19
  import { AssistantEventStreamImpl } from "../event-stream.js";
20
- import { HttpError, authHeaders, describeErrorJson, idleTimeoutOf, joinUrl, mergeHeaders, USER_AGENT, } from "../http.js";
20
+ import { HttpError, authHeaders, describeErrorJson, idleTimeoutOf, streamIdleTimeoutOf, joinUrl, mergeHeaders, USER_AGENT, } from "../http.js";
21
21
  import { readSseEvents } from "../sse.js";
22
- import { affinityHeaders, postWithCacheFallback, resolveCacheRetention } from "./cache-params.js";
22
+ import { affinityHeaders, resolveCacheRetention } from "./cache-params.js";
23
+ import { clampRequestMaxTokens, postWithMaxTokensFallback } from "./max-tokens.js";
23
24
  import { chatgptHeaders, chatgptPrecheck, handleChatGptEvent, subscriptionModel, quotaOnResponse, recoverChatGptError, } from "./chatgpt-backend.js";
24
25
  import { buildResponsesRequest, detectResponsesCompat, encodeMessageSignature, } from "./openai-responses-request.js";
25
26
  import { BlockTracker, ProviderStopError, STREAM_ENDED_MESSAGE, createOutput, finishDone, finishError, requireApiKey, } from "./shared.js";
@@ -251,17 +252,20 @@ async function post(model, context, initial, tracker) {
251
252
  const chatgpt = compat.chatgptBackend !== undefined;
252
253
  if (chatgpt)
253
254
  chatgptPrecheck(model, compat, options.apiKey);
255
+ clampRequestMaxTokens(request.body, "max_output_tokens", model.contextWindow);
254
256
  const replaced = options.onPayload?.(request.body);
255
257
  const baseUrl = model.baseUrl ?? "https://api.openai.com/v1";
256
258
  try {
257
- const response = await postWithCacheFallback(model, joinUrl(baseUrl, "/responses"), {
259
+ const url = joinUrl(baseUrl, "/responses");
260
+ const post = {
258
261
  headers: buildHeaders(model, options, compat),
259
262
  body: replaced === undefined ? request.body : replaced,
260
263
  signal: options.signal,
261
264
  timeoutMs: options.timeoutMs,
262
265
  idleTimeoutMs: idleTimeoutOf(options),
263
266
  onResponse: chatgpt ? quotaOnResponse(compat, options) : options.onResponse,
264
- });
267
+ };
268
+ const response = await postWithMaxTokensFallback(model, url, post, "max_output_tokens");
265
269
  return { response, options, compat };
266
270
  }
267
271
  catch (error) {
@@ -292,7 +296,7 @@ async function run(stream, model, context, options) {
292
296
  };
293
297
  let finished = false;
294
298
  const body = response.body;
295
- for await (const sse of readSseEvents(body, options.signal, idleTimeoutOf(options))) {
299
+ for await (const sse of readSseEvents(body, options.signal, streamIdleTimeoutOf(options))) {
296
300
  const raw = sse.data.trim();
297
301
  if (raw === "" || raw === "[DONE]")
298
302
  continue;
@@ -5,7 +5,9 @@
5
5
  * - 每个 `*_start` 恰好对应一个 `*_end`;终止事件之前所有打开的块都会被关闭(出错 / 中止时也
6
6
  * 先关块再发 error,消费者不必处理悬空块);
7
7
  * - `toolcall_end` 时 `toolCall.arguments` 一定是对象(容错解析,坏 JSON 退化为 `{}`);
8
- * - 流式期间 `arguments` 随增量更新为「到目前为止」的部分对象。
8
+ * - 流式期间 `arguments` 随增量更新为「到目前为止」的部分对象;
9
+ * - [ME-C] 拼接出的参数串能严格 `JSON.parse` 成对象时原样记进 `rawArguments`,同协议回放时逐字节发回;
10
+ * - [ME-C] 失败响应带 `Retry-After` 时写 `retryAfterMs`(会话层退避取两者较大值)。
9
11
  */
10
12
  import type { AssistantEventStreamImpl } from "../event-stream.js";
11
13
  import type { AssistantMessage, Model, StopReason, StreamOptions, TextBlock, ThinkingBlock, ToolCallBlock } from "../types.js";
@@ -5,11 +5,13 @@
5
5
  * - 每个 `*_start` 恰好对应一个 `*_end`;终止事件之前所有打开的块都会被关闭(出错 / 中止时也
6
6
  * 先关块再发 error,消费者不必处理悬空块);
7
7
  * - `toolcall_end` 时 `toolCall.arguments` 一定是对象(容错解析,坏 JSON 退化为 `{}`);
8
- * - 流式期间 `arguments` 随增量更新为「到目前为止」的部分对象。
8
+ * - 流式期间 `arguments` 随增量更新为「到目前为止」的部分对象;
9
+ * - [ME-C] 拼接出的参数串能严格 `JSON.parse` 成对象时原样记进 `rawArguments`,同协议回放时逐字节发回;
10
+ * - [ME-C] 失败响应带 `Retry-After` 时写 `retryAfterMs`(会话层退避取两者较大值)。
9
11
  */
10
12
  import { AmaError } from "../../errors.js";
11
13
  import { finalizeUsage, emptyUsage } from "../cost.js";
12
- import { errorText, hasAuthHeader } from "../http.js";
14
+ import { HttpError, errorText, hasAuthHeader } from "../http.js";
13
15
  import { parseToolArguments } from "../json-partial.js";
14
16
  export function createOutput(model) {
15
17
  return {
@@ -128,8 +130,11 @@ export class BlockTracker {
128
130
  this.stream.push({ type: "thinking_end", contentIndex: index, partial: this.output });
129
131
  }
130
132
  else {
131
- if (entry.json.length > 0)
133
+ if (entry.json.length > 0) {
132
134
  entry.block.arguments = parseToolArguments(entry.json);
135
+ if (isStrictObject(entry.json))
136
+ entry.block.rawArguments = entry.json;
137
+ }
133
138
  this.stream.push({
134
139
  type: "toolcall_end",
135
140
  contentIndex: index,
@@ -150,6 +155,16 @@ export class BlockTracker {
150
155
  return this.output.content.length - 1;
151
156
  }
152
157
  }
158
+ /** 严格 JSON 且是对象(数组 / 标量容错解析后是 `{}`,回放原串会与执行的参数不一致)。 */
159
+ function isStrictObject(json) {
160
+ try {
161
+ const value = JSON.parse(json);
162
+ return typeof value === "object" && value !== null && !Array.isArray(value);
163
+ }
164
+ catch {
165
+ return false;
166
+ }
167
+ }
153
168
  export const ABORTED_MESSAGE = "Request was aborted";
154
169
  export const STREAM_ENDED_MESSAGE = "Stream ended before completion";
155
170
  /** 正常结束:关块、算 usage / cost、发 done。 */
@@ -167,6 +182,9 @@ export function finishError(stream, tracker, model, error, signal) {
167
182
  const aborted = signal.aborted;
168
183
  output.stopReason = aborted ? "aborted" : "error";
169
184
  output.errorMessage = aborted ? ABORTED_MESSAGE : errorText(error);
185
+ if (!aborted && error instanceof HttpError && error.retryAfterMs !== undefined) {
186
+ output.retryAfterMs = error.retryAfterMs;
187
+ }
170
188
  finalizeUsage(model, output.usage);
171
189
  stream.push({ type: "error", reason: aborted ? "aborted" : "error", message: output });
172
190
  }
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * - `system` = sha256(normalizeContext().systemPrompt) 前 64 bit;
5
5
  * - `tools` = sha256(JSON.stringify(工具表按名排序)) 前 64 bit;
6
- * - `model` = `${provider}/${id}` 原文。
6
+ * - `model` = `${provider}/${id}` 原文;
7
+ * - `sections` = 折叠后每个非空节的 hash16([ME-B] D15:归因时说出哪一节变了)。
7
8
  * 纯函数、零请求;每次真实请求算一次(system + tools 通常 < 20 KB)。未命中归因时与上一条
8
9
  * 记录比对,说出「变了什么」。
9
10
  */
@@ -13,5 +14,7 @@ import type { PrefixFingerprint } from "./types.js";
13
14
  export declare function hash16(text: string): string;
14
15
  export declare function modelKey(model: Pick<Model, "provider" | "id" | "channel"> | ModelRef): string;
15
16
  export declare function fingerprintContext(context: TranscriptContext, model: Pick<Model, "provider" | "id">): PrefixFingerprint;
17
+ /** 变化的节名:先按 `cur` 的节顺序列新增 / 改动的,再列 `prev` 里消失的;任一方没有按节指纹 → []。 */
18
+ export declare function changedSections(prev: PrefixFingerprint, cur: PrefixFingerprint): string[];
16
19
  /** 两个指纹的差异(按归因顺序:system → tools → model);相同返回 undefined。 */
17
20
  export declare function fingerprintChange(prev: PrefixFingerprint, cur: PrefixFingerprint): "system" | "tools" | "model" | undefined;
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * - `system` = sha256(normalizeContext().systemPrompt) 前 64 bit;
5
5
  * - `tools` = sha256(JSON.stringify(工具表按名排序)) 前 64 bit;
6
- * - `model` = `${provider}/${id}` 原文。
6
+ * - `model` = `${provider}/${id}` 原文;
7
+ * - `sections` = 折叠后每个非空节的 hash16([ME-B] D15:归因时说出哪一节变了)。
7
8
  * 纯函数、零请求;每次真实请求算一次(system + tools 通常 < 20 KB)。未命中归因时与上一条
8
9
  * 记录比对,说出「变了什么」。
9
10
  */
@@ -21,12 +22,28 @@ function sortedTools(tools) {
21
22
  }
22
23
  export function fingerprintContext(context, model) {
23
24
  const normalized = normalizeContext(context);
25
+ const sections = {};
26
+ for (const section of normalized.systemSections)
27
+ sections[section.name] = hash16(section.text);
24
28
  return {
25
29
  system: hash16(normalized.systemPrompt),
26
30
  tools: hash16(JSON.stringify(sortedTools(normalized.tools))),
27
31
  model: modelKey(model),
32
+ sections,
28
33
  };
29
34
  }
35
+ /** 变化的节名:先按 `cur` 的节顺序列新增 / 改动的,再列 `prev` 里消失的;任一方没有按节指纹 → []。 */
36
+ export function changedSections(prev, cur) {
37
+ if (prev.sections === undefined || cur.sections === undefined)
38
+ return [];
39
+ const before = prev.sections;
40
+ const after = cur.sections;
41
+ const changed = Object.keys(after).filter((name) => before[name] !== after[name]);
42
+ for (const name of Object.keys(before))
43
+ if (!(name in after))
44
+ changed.push(name);
45
+ return changed;
46
+ }
30
47
  /** 两个指纹的差异(按归因顺序:system → tools → model);相同返回 undefined。 */
31
48
  export function fingerprintChange(prev, cur) {
32
49
  if (prev.system !== cur.system)
@@ -10,7 +10,8 @@
10
10
  * 4. 成本:用本条实付反推——`(cost.input + cost.cacheWrite) / (input + cacheWrite)` 减去读价
11
11
  * (本条有读用实付,否则用目录价);无价格 → `missedCost` 缺省。
12
12
  * 5. 重置点(压缩 / 分支摘要 / 档一裁剪之后的首个请求)由调用方清空 prev;模型切换不豁免。
13
- * 6. 归因:指纹 system / tools 变 → `prefix_changed`;模型变 → `model_changed`;
13
+ * 6. 归因:指纹 system / tools 变 → `prefix_changed`(system 变时 `detail` 带变化的节名,如
14
+ * `system:hooks,memory`,[ME-B] D15);模型变 → `model_changed`;
14
15
  * `idleMs > ttl`(无承诺 TTL 的隐式缓存按 600 s)→ `idle`;task 运行占间隔 ≥ 80% →
15
16
  * `subtask`;其余 → `evicted`。
16
17
  */
@@ -10,11 +10,12 @@
10
10
  * 4. 成本:用本条实付反推——`(cost.input + cost.cacheWrite) / (input + cacheWrite)` 减去读价
11
11
  * (本条有读用实付,否则用目录价);无价格 → `missedCost` 缺省。
12
12
  * 5. 重置点(压缩 / 分支摘要 / 档一裁剪之后的首个请求)由调用方清空 prev;模型切换不豁免。
13
- * 6. 归因:指纹 system / tools 变 → `prefix_changed`;模型变 → `model_changed`;
13
+ * 6. 归因:指纹 system / tools 变 → `prefix_changed`(system 变时 `detail` 带变化的节名,如
14
+ * `system:hooks,memory`,[ME-B] D15);模型变 → `model_changed`;
14
15
  * `idleMs > ttl`(无承诺 TTL 的隐式缓存按 600 s)→ `idle`;task 运行占间隔 ≥ 80% →
15
16
  * `subtask`;其余 → `evicted`。
16
17
  */
17
- import { fingerprintChange } from "./fingerprint.js";
18
+ import { changedSections, fingerprintChange } from "./fingerprint.js";
18
19
  /** 无承诺 TTL 的端点(隐式缓存)按 10 分钟估。 */
19
20
  export const IMPLICIT_CACHE_TTL_MS = 600_000;
20
21
  /** 没有目录 `minTokens` 时的最小可缓存长度与噪声下限。 */
@@ -76,10 +77,15 @@ export function detectMiss(prev, cur, ttlMs, options = {}) {
76
77
  if (cost !== undefined)
77
78
  miss.missedCost = cost;
78
79
  const change = fingerprintChange(prev.fingerprint, cur.fingerprint);
79
- if (change === "system" || change === "tools") {
80
+ if (change === "tools") {
80
81
  miss.reason = "prefix_changed";
81
82
  miss.detail = change;
82
83
  }
84
+ else if (change === "system") {
85
+ miss.reason = "prefix_changed";
86
+ const names = changedSections(prev.fingerprint, cur.fingerprint);
87
+ miss.detail = names.length > 0 ? `system:${names.join(",")}` : "system";
88
+ }
83
89
  else if (change === "model")
84
90
  miss.reason = "model_changed";
85
91
  else if (idleMs > (ttlMs ?? IMPLICIT_CACHE_TTL_MS))
@@ -64,7 +64,11 @@ export class CacheReportingTracker {
64
64
  };
65
65
  this.endpoints.set(key, entry);
66
66
  const last = entry.last;
67
- entry.last = record;
67
+ entry.last = {
68
+ at: record.at,
69
+ promptTokens: record.promptTokens,
70
+ fingerprint: record.fingerprint,
71
+ };
68
72
  const { cacheRead, cacheWrite, cacheReported } = record.usage;
69
73
  if (cacheRead > 0 && Number.isInteger(cacheRead)) {
70
74
  entry.divisor = gcd(entry.divisor, cacheRead);
@@ -18,6 +18,8 @@ export interface PrefixFingerprint {
18
18
  tools: string;
19
19
  /** `${provider}/${id}`(原文,不哈希)。 */
20
20
  model: string;
21
+ /** 节名 → hash16,归因时说出哪一节变了。 */
22
+ sections?: Record<string, string>;
21
23
  }
22
24
  /** 每次真实请求在会话层记一条(内存,不落盘)。 */
23
25
  export interface RequestRecord {
@@ -36,6 +38,12 @@ export interface RequestRecord {
36
38
  contextRef: TranscriptContext;
37
39
  options: Omit<StreamOptions, "signal">;
38
40
  }
41
+ /** [M-A] 端点上一条可比请求的摘要;只存比较需要的字段,不持有转录与回调。 */
42
+ export interface LastRequest {
43
+ at: number;
44
+ promptTokens: number;
45
+ fingerprint: PrefixFingerprint;
46
+ }
39
47
  /** 「供应商不报缓存」三态(§1.6);按 `(provider, baseUrl 主机名, model)` 维护。 */
40
48
  export type CacheReporting = "unknown" | "reported" | "silent";
41
49
  /** 未命中原因(§1.5 第 6 步的归因顺序)。 */
@@ -47,8 +55,8 @@ export interface CacheMiss {
47
55
  /** 重计费金额(美元);模型无价格时缺省(显示 `$?`)。 */
48
56
  missedCost?: number;
49
57
  reason: CacheMissReason;
50
- /** `prefix_changed` 时哪一段变了。 */
51
- detail?: "system" | "tools";
58
+ /** `prefix_changed` 时哪一段变了;[ME-B] system 变时带节名:`system:hooks,memory`。 */
59
+ detail?: "system" | "tools" | `system:${string}`;
52
60
  /** 与上一条可比请求的间隔。 */
53
61
  idleMs: number;
54
62
  }
@@ -14,7 +14,7 @@
14
14
  * - `pause()`:真实请求发出时静默挂起(新请求本身就刷新了缓存);`cancel()` 同时丢弃记录。
15
15
  * 只有已排期之后的异常停止(截止、上限、零命中、过期、否决、失败)回调 `onStopped`。
16
16
  */
17
- import type { AssistantMessage, Model, StreamOptions } from "../types.js";
17
+ import type { Api, AssistantMessage, Model, StreamOptions } from "../types.js";
18
18
  import type { CacheReporting, RequestRecord, WarmDecision, WarmerStatus, WarmingMode, WarmingPhase } from "./types.js";
19
19
  export declare const WARM_MIN_TTL_MS = 10000;
20
20
  export declare const WARM_STREAMING_CAP_MS: number;
@@ -42,6 +42,11 @@ export interface WarmerDeps {
42
42
  onScheduled?(nextWarmAt: number): void;
43
43
  onStopped(reason: string): void;
44
44
  }
45
+ /**
46
+ * 保温重放的输出上限([ME-B] D17):Responses 的 `max_output_tokens` 下限是 16,其余协议 1。
47
+ * 官方下限无法实测,按文档取值;中转同样适用。
48
+ */
49
+ export declare function warmReplayMaxTokens(api: Api): number;
45
50
  export declare function warmDelay(ttlMs: number): number;
46
51
  export interface ReplayCheck {
47
52
  model: Pick<Model, "api" | "reasoning" | "compat">;
@@ -30,6 +30,13 @@ export const REAL_TIMERS = {
30
30
  clearTimeout(handle);
31
31
  },
32
32
  };
33
+ /**
34
+ * 保温重放的输出上限([ME-B] D17):Responses 的 `max_output_tokens` 下限是 16,其余协议 1。
35
+ * 官方下限无法实测,按文档取值;中转同样适用。
36
+ */
37
+ export function warmReplayMaxTokens(api) {
38
+ return api === "openai-responses" ? 16 : 1;
39
+ }
33
40
  export function warmDelay(ttlMs) {
34
41
  return Math.max(1000, Math.floor(Math.min(0.9 * ttlMs, ttlMs - 10_000)));
35
42
  }
@@ -6,8 +6,9 @@
6
6
  * 得到 `systemSections`(保持节首次出现的顺序)与拼好的 `systemPrompt`(节间空一行);
7
7
  * - 中途节补丁(设计 §9.1「会话中途变化的上下文只追加」):对话开始之后的 system 补丁不改写开头,
8
8
  * 渲染成 `<system-reminder>` 包裹的 user 消息按位置插回,开头与之前的消息逐字节不变;
9
- * 例外是补丁移除了工具——工具表本身已变、前缀必然失效,这时全部补丁照旧折回开头;
10
- * - 工具表:按顺序重放 `toolsRemoved`(先)与 `toolsAdded`(后),同名后者覆盖;
9
+ * - 工具表([ME-B] D5):对话开始前按顺序重放 `toolsRemoved`(先)与 `toolsAdded`(后),同名后者覆盖;
10
+ * 开始之后声明冻结——移除不删声明、只在尾部提醒(执行层拒绝调用),已有名字不覆盖(加回只提醒
11
+ * 「available again」),新名字追加在末尾。开头与工具表在会话内只写一次;
11
12
  * - 模态过滤:模型不收图片时,用户消息与工具结果里的图片块换成文字占位。
12
13
  *
13
14
  * 支持「中途 system 消息」的协议(compat.supportsMidConvoSystemMessages)用
@@ -20,11 +21,18 @@ export interface NormalizeOptions {
20
21
  model?: Pick<Model, "input"> | undefined;
21
22
  }
22
23
  export declare const IMAGE_OMITTED_TEXT = "[image omitted: the model does not accept image input]";
24
+ /** 对话开始后移除工具:声明保留,尾部提醒(固定英文;执行层拒绝文案见 agent/tool-availability.ts)。 */
25
+ export declare const toolRemovedReminder: (name: string) => string;
26
+ /** 移除后又加回(声明沿用首次版本)。 */
27
+ export declare const toolRestoredReminder: (name: string) => string;
23
28
  export declare function normalizeContext(context: TranscriptContext, options?: NormalizeOptions): NormalizedContext;
24
29
  /** 中途节补丁作为尾部上下文消息送达时的文本(固定英文,与界面语言无关)。 */
25
30
  export declare function systemReminderText(updates: readonly string[]): string;
26
- /** 渲染一条中途 system 补丁的文本(节变更;工具表变化由请求的工具列表体现)。 */
27
- export declare function renderSystemUpdate(message: SystemMessage): string;
31
+ /**
32
+ * 渲染一条中途 system 补丁的文本:节变更全文;移除的工具各一句提醒;`removed` 里的名字被加回时
33
+ * 一句「available again」(新工具的声明由请求工具表末尾体现,不另写)。`removed` 随之更新。
34
+ */
35
+ export declare function renderSystemUpdate(message: SystemMessage, removed: Set<string>): string;
28
36
  export interface InlineSystemUpdate {
29
37
  /** 插在 `messages[beforeIndex]` 之前;等于 messages.length 表示放在末尾。 */
30
38
  beforeIndex: number;
@@ -6,8 +6,9 @@
6
6
  * 得到 `systemSections`(保持节首次出现的顺序)与拼好的 `systemPrompt`(节间空一行);
7
7
  * - 中途节补丁(设计 §9.1「会话中途变化的上下文只追加」):对话开始之后的 system 补丁不改写开头,
8
8
  * 渲染成 `<system-reminder>` 包裹的 user 消息按位置插回,开头与之前的消息逐字节不变;
9
- * 例外是补丁移除了工具——工具表本身已变、前缀必然失效,这时全部补丁照旧折回开头;
10
- * - 工具表:按顺序重放 `toolsRemoved`(先)与 `toolsAdded`(后),同名后者覆盖;
9
+ * - 工具表([ME-B] D5):对话开始前按顺序重放 `toolsRemoved`(先)与 `toolsAdded`(后),同名后者覆盖;
10
+ * 开始之后声明冻结——移除不删声明、只在尾部提醒(执行层拒绝调用),已有名字不覆盖(加回只提醒
11
+ * 「available again」),新名字追加在末尾。开头与工具表在会话内只写一次;
11
12
  * - 模态过滤:模型不收图片时,用户消息与工具结果里的图片块换成文字占位。
12
13
  *
13
14
  * 支持「中途 system 消息」的协议(compat.supportsMidConvoSystemMessages)用
@@ -15,16 +16,27 @@
15
16
  * 渲染为文本、按位置插回对话。
16
17
  */
17
18
  export const IMAGE_OMITTED_TEXT = "[image omitted: the model does not accept image input]";
19
+ /** 对话开始后移除工具:声明保留,尾部提醒(固定英文;执行层拒绝文案见 agent/tool-availability.ts)。 */
20
+ export const toolRemovedReminder = (name) => `Tool "${name}" is no longer available in this session; calls to it are rejected.`;
21
+ /** 移除后又加回(声明沿用首次版本)。 */
22
+ export const toolRestoredReminder = (name) => `Tool "${name}" is available again.`;
18
23
  class SystemState {
19
24
  sections = new Map();
20
25
  tools = new Map();
21
- apply(message) {
26
+ /** `started`:对话已开始——工具声明冻结(移除不删、已有名字不覆盖、新名字追加)。 */
27
+ apply(message, started = false) {
22
28
  for (const [name, value] of Object.entries(message.sections)) {
23
29
  if (value === null)
24
30
  this.sections.delete(name);
25
31
  else
26
32
  this.sections.set(name, value);
27
33
  }
34
+ if (started) {
35
+ for (const tool of message.toolsAdded ?? [])
36
+ if (!this.tools.has(tool.name))
37
+ this.tools.set(tool.name, tool);
38
+ return;
39
+ }
28
40
  for (const name of message.toolsRemoved ?? [])
29
41
  this.tools.delete(name);
30
42
  for (const tool of message.toolsAdded ?? [])
@@ -62,7 +74,7 @@ export function normalizeContext(context, options) {
62
74
  const tools = new SystemState();
63
75
  const messages = [];
64
76
  const allowImages = allowsImages(options);
65
- const fold = removesToolsMidway(context.messages);
77
+ const removed = new Set();
66
78
  let pending = [];
67
79
  const flush = () => {
68
80
  if (pending.length === 0)
@@ -76,42 +88,47 @@ export function normalizeContext(context, options) {
76
88
  messages.push(filterMessage(message, allowImages));
77
89
  continue;
78
90
  }
79
- tools.apply(message);
80
- if (fold || messages.length === 0) {
91
+ const started = messages.length > 0;
92
+ tools.apply(message, started);
93
+ if (!started) {
81
94
  head.apply(message);
82
95
  continue;
83
96
  }
84
- const text = renderSystemUpdate(message);
97
+ const text = renderSystemUpdate(message, removed);
85
98
  if (text.length > 0)
86
99
  pending.push(text);
87
100
  }
88
101
  flush();
89
102
  return { ...head.snapshot(), tools: tools.snapshot().tools, messages };
90
103
  }
91
- /** 对话开始之后有补丁移除了工具(这时整段前缀必然失效,补丁全部折回开头)。 */
92
- function removesToolsMidway(transcript) {
93
- let started = false;
94
- for (const message of transcript) {
95
- if (message.role !== "system")
96
- started = true;
97
- else if (started && (message.toolsRemoved?.length ?? 0) > 0)
98
- return true;
99
- }
100
- return false;
101
- }
102
104
  /** 中途节补丁作为尾部上下文消息送达时的文本(固定英文,与界面语言无关)。 */
103
105
  export function systemReminderText(updates) {
104
106
  return (`<system-reminder>\n${updates.join("\n\n")}\n\n` +
105
- "These updates replace the earlier versions of those system prompt sections.\n</system-reminder>");
107
+ "These updates replace the earlier versions of those system prompt sections; " +
108
+ "tool availability notes above are current.\n</system-reminder>");
106
109
  }
107
- /** 渲染一条中途 system 补丁的文本(节变更;工具表变化由请求的工具列表体现)。 */
108
- export function renderSystemUpdate(message) {
110
+ /**
111
+ * 渲染一条中途 system 补丁的文本:节变更全文;移除的工具各一句提醒;`removed` 里的名字被加回时
112
+ * 一句「available again」(新工具的声明由请求工具表末尾体现,不另写)。`removed` 随之更新。
113
+ */
114
+ export function renderSystemUpdate(message, removed) {
109
115
  const parts = [];
110
116
  for (const [name, value] of Object.entries(message.sections)) {
111
117
  parts.push(value === null
112
118
  ? `System prompt section "${name}" was removed.`
113
119
  : `System prompt section "${name}" was updated:\n\n${value}`);
114
120
  }
121
+ for (const tool of message.toolsAdded ?? []) {
122
+ if (!removed.delete(tool.name))
123
+ continue;
124
+ parts.push(toolRestoredReminder(tool.name));
125
+ }
126
+ for (const name of message.toolsRemoved ?? []) {
127
+ if (removed.has(name))
128
+ continue;
129
+ removed.add(name);
130
+ parts.push(toolRemovedReminder(name));
131
+ }
115
132
  return parts.join("\n\n");
116
133
  }
117
134
  export function normalizeContextInline(context, options) {
@@ -120,17 +137,19 @@ export function normalizeContextInline(context, options) {
120
137
  const messages = [];
121
138
  const systemUpdates = [];
122
139
  const allowImages = allowsImages(options);
140
+ const removed = new Set();
123
141
  for (const message of context.messages) {
124
142
  if (message.role !== "system") {
125
143
  messages.push(filterMessage(message, allowImages));
126
144
  continue;
127
145
  }
128
- tools.apply(message);
129
- if (messages.length === 0) {
146
+ const started = messages.length > 0;
147
+ tools.apply(message, started);
148
+ if (!started) {
130
149
  head.apply(message);
131
150
  continue;
132
151
  }
133
- const text = renderSystemUpdate(message);
152
+ const text = renderSystemUpdate(message, removed);
134
153
  if (text.length > 0)
135
154
  systemUpdates.push({ beforeIndex: messages.length, text });
136
155
  }
@@ -4,11 +4,14 @@
4
4
  * `responses[n]` 产出文本 / 思考 / 工具调用 / 429 / 溢出 / 断流 / 延迟。
5
5
  *
6
6
  * 事件形状与真实协议完全一致(同一个 BlockTracker),契约测试对两条协议与 fake 跑同一套断言。
7
+ * [ME-C] 工具参数按 JSON 串分块喂给 BlockTracker,所以消息同样带 `rawArguments`;start 之前的错误
8
+ * 以 `HttpError` 抛出(状态 ≥ 400 时),脚本的 `error.retryAfterMs` 进失败消息的 `retryAfterMs`。
7
9
  *
8
10
  * 用法:
9
11
  * - 测试:`const fake = new FakeProvider([...])`,把 `fake.api` 注册进 ApiRegistry(或直接
10
12
  * `fake.api.stream(...)`),`fake.calls` 记录每次调用的上下文与选项;
11
- * - CLI:`--provider fake`;`AMA_FAKE_SCRIPT=<path.json>` 指定脚本(首次调用时读取)。
13
+ * - CLI:`--provider fake`;`AMA_FAKE_SCRIPT=<path.json>` 指定脚本(首次调用时读取)。缺省实例
14
+ * `keepCalls: false`:只计数、不留 `calls`(否则整个进程的转录都被它引用,内存测量多出几十 MB)。
12
15
  * - 录制(第三波 §2.4,bundle 级缓存测试):`AMA_FAKE_RECORD=<file>` 时每次请求向文件追加一行
13
16
  * JSON `{ index, purpose, model, system, tools, messagesCount }`——`system` 是折叠后的系统提示
14
17
  * 原文,`tools` 是发给供应商的工具表(按注册顺序),与真实协议请求体的前缀同口径。
@@ -31,6 +34,8 @@ export interface FakeRecordLine {
31
34
  export interface FakeProviderOptions {
32
35
  /** 录制文件路径(每次请求取一次;undefined = 不录)。 */
33
36
  recordFile?: string | (() => string | undefined);
37
+ /** false:只计数,不往 `calls` 里留上下文与选项(CLI 缺省实例);缺省 true。 */
38
+ keepCalls?: boolean;
34
39
  }
35
40
  export declare const FAKE_MODELS: readonly Model[];
36
41
  export declare function fakeProviderData(): ProviderData;
@@ -47,6 +52,8 @@ export declare class FakeProvider {
47
52
  private readonly scriptLoader;
48
53
  private loaded;
49
54
  private readonly recordFile;
55
+ private readonly keepCalls;
56
+ private count;
50
57
  constructor(script?: FakeScript | FakeResponse[] | (() => FakeScript | undefined), options?: FakeProviderOptions);
51
58
  readonly api: ApiImplementation;
52
59
  get callCount(): number;