@armadra/agent 0.7.3 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/CHANGELOG.zh-CN.md +48 -0
  3. package/dist/agent/retry.d.ts +14 -5
  4. package/dist/agent/retry.js +30 -8
  5. package/dist/agent/session-cache-key.d.ts +8 -0
  6. package/dist/agent/session-cache-key.js +46 -0
  7. package/dist/agent/session-cache.d.ts +2 -3
  8. package/dist/agent/session-cache.js +19 -37
  9. package/dist/agent/session-classifier.d.ts +3 -2
  10. package/dist/agent/session-classifier.js +36 -10
  11. package/dist/agent/session-compaction.d.ts +24 -5
  12. package/dist/agent/session-compaction.js +54 -20
  13. package/dist/agent/session-core.d.ts +2 -0
  14. package/dist/agent/session-run.d.ts +12 -0
  15. package/dist/agent/session-run.js +29 -10
  16. package/dist/agent/session-subagent.d.ts +8 -1
  17. package/dist/agent/session-subagent.js +37 -14
  18. package/dist/agent/session-sync.d.ts +8 -2
  19. package/dist/agent/session-sync.js +7 -10
  20. package/dist/agent/session-tools.js +3 -2
  21. package/dist/agent/subagent-fork.d.ts +67 -0
  22. package/dist/agent/subagent-fork.js +87 -0
  23. package/dist/agent/subagent-registry.d.ts +2 -2
  24. package/dist/agent/subagent-registry.js +2 -2
  25. package/dist/agent/tool-availability.d.ts +8 -0
  26. package/dist/agent/tool-availability.js +8 -0
  27. package/dist/agent/tool-runner.js +13 -4
  28. package/dist/agent/worktree.d.ts +8 -0
  29. package/dist/agent/worktree.js +51 -4
  30. package/dist/agents/parse.js +6 -1
  31. package/dist/agents/task-record.js +3 -0
  32. package/dist/agents/types.d.ts +2 -0
  33. package/dist/ai/apis/anthropic-messages.js +10 -5
  34. package/dist/ai/apis/anthropic-request.d.ts +2 -1
  35. package/dist/ai/apis/anthropic-request.js +15 -4
  36. package/dist/ai/apis/cache-params.js +2 -1
  37. package/dist/ai/apis/google-generative-ai.js +4 -2
  38. package/dist/ai/apis/google-request.js +2 -1
  39. package/dist/ai/apis/max-tokens.d.ts +42 -0
  40. package/dist/ai/apis/max-tokens.js +121 -0
  41. package/dist/ai/apis/openai-completions.js +10 -5
  42. package/dist/ai/apis/openai-request.js +4 -2
  43. package/dist/ai/apis/openai-responses-request.js +3 -2
  44. package/dist/ai/apis/openai-responses.js +9 -5
  45. package/dist/ai/apis/shared.d.ts +3 -1
  46. package/dist/ai/apis/shared.js +21 -3
  47. package/dist/ai/cache/fingerprint.d.ts +4 -1
  48. package/dist/ai/cache/fingerprint.js +18 -1
  49. package/dist/ai/cache/miss.d.ts +2 -1
  50. package/dist/ai/cache/miss.js +9 -3
  51. package/dist/ai/cache/reporting.js +5 -1
  52. package/dist/ai/cache/types.d.ts +10 -2
  53. package/dist/ai/cache/warmer.d.ts +6 -1
  54. package/dist/ai/cache/warmer.js +7 -0
  55. package/dist/ai/context.d.ts +12 -4
  56. package/dist/ai/context.js +43 -24
  57. package/dist/ai/fake/fake-provider.d.ts +8 -1
  58. package/dist/ai/fake/fake-provider.js +19 -7
  59. package/dist/ai/fake/fake-script.d.ts +2 -0
  60. package/dist/ai/http.d.ts +17 -9
  61. package/dist/ai/http.js +33 -13
  62. package/dist/ai/image-intern.d.ts +29 -0
  63. package/dist/ai/image-intern.js +58 -0
  64. package/dist/ai/json-body.d.ts +63 -0
  65. package/dist/ai/json-body.js +286 -0
  66. package/dist/ai/overflow.js +5 -0
  67. package/dist/ai/providers/catalog-data.js +13 -13
  68. package/dist/ai/providers/catalog.d.ts +33 -0
  69. package/dist/ai/providers/catalog.js +105 -6
  70. package/dist/ai/providers/enrich.d.ts +13 -3
  71. package/dist/ai/providers/enrich.js +87 -31
  72. package/dist/ai/sse.d.ts +3 -2
  73. package/dist/ai/sse.js +3 -2
  74. package/dist/ai/types.d.ts +9 -2
  75. package/dist/bundle/ama.cjs +3637 -1973
  76. package/dist/cli/bootstrap.js +13 -5
  77. package/dist/cli/compose-agents.js +3 -0
  78. package/dist/cli/compose-extensions.js +2 -0
  79. package/dist/cli/compose-request.d.ts +20 -0
  80. package/dist/cli/compose-request.js +37 -0
  81. package/dist/cli/compose-session.js +2 -2
  82. package/dist/cli/deps.d.ts +1 -1
  83. package/dist/cli/subcommands/model-meta.d.ts +2 -1
  84. package/dist/cli/subcommands/model-meta.js +3 -2
  85. package/dist/codemode/host-side.d.ts +11 -4
  86. package/dist/codemode/host-side.js +25 -6
  87. package/dist/codemode/tool.d.ts +2 -0
  88. package/dist/codemode/tool.js +4 -0
  89. package/dist/config/json-schema.js +9 -2
  90. package/dist/config/key-docs.js +13 -4
  91. package/dist/config/schema-w5.js +9 -1
  92. package/dist/config/schema.d.ts +1 -1
  93. package/dist/config/schema.js +19 -3
  94. package/dist/config/settings-registry.js +6 -0
  95. package/dist/config/types-w5.d.ts +2 -0
  96. package/dist/config/types.d.ts +19 -1
  97. package/dist/config/types.js +4 -0
  98. package/dist/i18n/catalog.d.ts +13 -2
  99. package/dist/i18n/messages/cli.d.ts +2 -0
  100. package/dist/i18n/messages/cli.js +2 -0
  101. package/dist/i18n/messages/config-keys.d.ts +10 -0
  102. package/dist/i18n/messages/config-keys.js +12 -2
  103. package/dist/i18n/messages/config.d.ts +14 -0
  104. package/dist/i18n/messages/config.js +4 -0
  105. package/dist/i18n/messages/errors.d.ts +3 -2
  106. package/dist/i18n/messages/errors.js +15 -2
  107. package/dist/i18n/messages/report.d.ts +2 -0
  108. package/dist/i18n/messages/report.js +2 -0
  109. package/dist/i18n/messages/settings.d.ts +4 -0
  110. package/dist/i18n/messages/settings.js +6 -2
  111. package/dist/i18n/messages/subcommands.d.ts +2 -2
  112. package/dist/i18n/messages/subcommands.js +4 -2
  113. package/dist/modes/acp/acp-server.js +30 -18
  114. package/dist/modes/acp/acp-sessions.d.ts +22 -0
  115. package/dist/modes/acp/acp-sessions.js +68 -0
  116. package/dist/modes/context-report.js +3 -2
  117. package/dist/modes/print/json-event.d.ts +12 -1
  118. package/dist/modes/print/json-event.js +48 -1
  119. package/dist/modes/rpc/rpc-mode.d.ts +1 -0
  120. package/dist/modes/rpc/rpc-mode.js +9 -2
  121. package/dist/modes/session-report.d.ts +1 -1
  122. package/dist/modes/session-report.js +12 -6
  123. package/dist/rpc.d.ts +5 -2
  124. package/dist/session/line-reader.d.ts +28 -0
  125. package/dist/session/line-reader.js +82 -0
  126. package/dist/session/list.d.ts +14 -0
  127. package/dist/session/list.js +150 -0
  128. package/dist/session/manager.d.ts +23 -3
  129. package/dist/session/manager.js +91 -62
  130. package/dist/session/migrate.d.ts +2 -1
  131. package/dist/session/migrate.js +2 -1
  132. package/dist/session/offload.d.ts +51 -0
  133. package/dist/session/offload.js +195 -0
  134. package/dist/session/projection.d.ts +16 -3
  135. package/dist/session/projection.js +96 -29
  136. package/dist/session/scan.d.ts +3 -3
  137. package/dist/session/scan.js +9 -18
  138. package/dist/session/store.d.ts +15 -4
  139. package/dist/session/store.js +54 -38
  140. package/dist/session/types.d.ts +7 -2
  141. package/dist/tools/bash.d.ts +1 -1
  142. package/dist/tools/bash.js +5 -4
  143. package/dist/tools/glob.d.ts +2 -2
  144. package/dist/tools/glob.js +2 -2
  145. package/dist/tools/grep.js +4 -6
  146. package/dist/tools/image-file.js +2 -1
  147. package/dist/tools/read-lines.d.ts +71 -0
  148. package/dist/tools/read-lines.js +224 -0
  149. package/dist/tools/read.d.ts +6 -2
  150. package/dist/tools/read.js +47 -27
  151. package/dist/tools/task.d.ts +9 -0
  152. package/dist/tools/task.js +21 -8
  153. package/dist/tools/truncate.d.ts +7 -0
  154. package/dist/tools/truncate.js +11 -0
  155. package/dist/tools/types.d.ts +13 -0
  156. package/dist/trace/build-index.js +13 -1
  157. package/dist/tui/ansi.d.ts +2 -0
  158. package/dist/tui/ansi.js +7 -3
  159. package/dist/tui/components/editor-buffer.js +2 -2
  160. package/dist/tui/components/editor.js +5 -6
  161. package/dist/tui/keys.js +2 -2
  162. package/dist/tui/stdin-buffer.js +3 -3
  163. package/docs/acp.md +8 -1
  164. package/docs/agents.md +42 -6
  165. package/docs/codemode.md +4 -2
  166. package/docs/en/acp.md +3 -2
  167. package/docs/en/host-api.md +1 -1
  168. package/docs/en/permissions.md +2 -2
  169. package/docs/en/providers.md +54 -20
  170. package/docs/en/rpc.md +23 -7
  171. package/docs/en/sessions.md +4 -2
  172. package/docs/en/tui.md +2 -2
  173. package/docs/permissions.md +4 -1
  174. package/docs/providers.md +85 -22
  175. package/docs/rpc.md +22 -6
  176. package/docs/session-format.md +13 -2
  177. package/docs/sessions.md +3 -1
  178. package/docs/tui.md +1 -1
  179. package/package.json +2 -2
@@ -0,0 +1,121 @@
1
+ /**
2
+ * `max_tokens` 的主动收紧与被动修正(docs/model-efficiency-plan.md §1.9、D9)。[ME-C]
3
+ *
4
+ * - 主动收紧:窗口已知时 `max_tokens = min(请求值, max(MIN_OUTPUT_TOKENS, 窗口 − 估算输入 − 余量))`,
5
+ * 输入按请求体字符 / 4 估算(本模块自带,不依赖 compaction/)。预算型思考(`fixed`)不收紧:
6
+ * 预算由 max_tokens 推导,改它会让消息缓存失效。
7
+ * - 被动修正:端点以 400 拒收并说出上限(「max_tokens 范围」类文案)时,记入进程级 `maxTokensCaps`、
8
+ * 以上限重发一次;之后同一模型的请求直接用上限。Anthropic「输入 + max_tokens 超出上下文」只按这次
9
+ * 的输入算出可用值重发、不记上限;可用值不足 `MIN_OUTPUT_TOKENS` 判为溢出(overflow.ts 认得该文案)。
10
+ * 重发只发生在 `start` 之前,流契约不变(与 `postWithCacheFallback` 同模式,包在它外层)。
11
+ */
12
+ import { HttpError, errorText } from "../http.js";
13
+ import { postWithCacheFallback } from "./cache-params.js";
14
+ /** 主动收紧的下限:再小就不如让请求原样发出、由被动修正或溢出处理接手。 */
15
+ export const MIN_OUTPUT_TOKENS = 1024;
16
+ /** 窗口里给估算误差留的余量。 */
17
+ export const OUTPUT_HEADROOM_TOKENS = 2048;
18
+ /** `${provider}/${model}` → 已知上限(被动修正时记入,之后的请求直接用)。 */
19
+ export const maxTokensCaps = new Map();
20
+ /** 主动收紧后的 max_tokens;`window` 未知或 `fixed`(预算型思考)时返回 requested。 */
21
+ export function clampMaxTokens(requested, window, estimatedInput, fixed) {
22
+ if (fixed || window === undefined || !Number.isFinite(window) || window <= 0)
23
+ return requested;
24
+ const room = Math.floor(window - estimatedInput - OUTPUT_HEADROOM_TOKENS);
25
+ return Math.min(requested, Math.max(MIN_OUTPUT_TOKENS, room));
26
+ }
27
+ /** 请求体的输入 token 估算:JSON 字符数 / 4(CJK 偏低,由被动修正兜底)。 */
28
+ export function estimateInputTokens(body) {
29
+ try {
30
+ return Math.ceil(JSON.stringify(body).length / 4);
31
+ }
32
+ catch {
33
+ return 0;
34
+ }
35
+ }
36
+ /**
37
+ * 就地收紧 `holder[field]`(缺省 holder = body;Google 的 `generationConfig.maxOutputTokens`
38
+ * 传 holder)。字段不是数字时不动。
39
+ */
40
+ export function clampRequestMaxTokens(body, field, window, fixed = false, holder = body) {
41
+ const requested = holder[field];
42
+ if (typeof requested !== "number" || fixed || window === undefined)
43
+ return;
44
+ holder[field] = clampMaxTokens(requested, window, estimateInputTokens(body), false);
45
+ }
46
+ /** Google `generationConfig.maxOutputTokens`;带正的思考预算时不收紧(预算按回答上限留白算出)。 */
47
+ export function clampGoogleMaxTokens(body, window) {
48
+ const config = body["generationConfig"];
49
+ if (typeof config !== "object" || config === null)
50
+ return;
51
+ const thinking = config["thinkingConfig"];
52
+ const budget = typeof thinking === "object" && thinking !== null
53
+ ? thinking["thinkingBudget"]
54
+ : undefined;
55
+ const fixed = typeof budget === "number" && budget > 0;
56
+ clampRequestMaxTokens(body, "maxOutputTokens", window, fixed, config);
57
+ }
58
+ const NUM = "`?(\\d[\\d,_]*)`?";
59
+ const toInt = (text) => Number((text ?? "").replace(/[,_]/g, ""));
60
+ const RANGE = new RegExp(`range of \`?max_\\w*tokens\`?\\s*(?:should be|is|must be)?\\s*[[(]\\s*\\d+\\s*,\\s*${NUM}\\s*[\\])]`, "i");
61
+ const LIMIT = new RegExp(`max_\\w*tokens\`?[^\\n]{0,200}?(?:must be|should be|less than or equal to|at most|<=)\\s*${NUM}`, "i");
62
+ const CONTEXT_LIMIT = /input length and `?max_tokens`? exceed context limit:?\s*(\d+)\s*\+\s*(\d+)\s*>\s*(\d+)/i;
63
+ function classify(error) {
64
+ if (error instanceof HttpError && error.status !== 400 && error.status !== 422)
65
+ return undefined;
66
+ const text = errorText(error);
67
+ const context = CONTEXT_LIMIT.exec(text);
68
+ if (context) {
69
+ const cap = toInt(context[3]) - toInt(context[1]);
70
+ return cap < MIN_OUTPUT_TOKENS ? { overflow: true } : { cap, transient: true };
71
+ }
72
+ const match = RANGE.exec(text) ?? LIMIT.exec(text);
73
+ if (!match)
74
+ return undefined;
75
+ const cap = toInt(match[1]);
76
+ return Number.isFinite(cap) && cap >= 1 ? { cap, transient: false } : undefined;
77
+ }
78
+ /** 400 文案里的上限;`{ cap }` 可重发,`{ overflow: true }` 判溢出,undefined 不是本类错误。 */
79
+ export function parseMaxTokensRejection(error) {
80
+ const rejection = classify(error);
81
+ return rejection === undefined || "overflow" in rejection ? rejection : { cap: rejection.cap };
82
+ }
83
+ /** body[field] 超过 cap 时返回改小后的浅拷贝,否则原样返回。 */
84
+ function withCap(body, field, cap) {
85
+ if (cap === undefined || typeof body !== "object" || body === null)
86
+ return body;
87
+ const value = body[field];
88
+ return typeof value === "number" && value > cap ? { ...body, [field]: cap } : body;
89
+ }
90
+ /** Anthropic 预算型思考:budget_tokens 必须小于 max_tokens,上限压不下时不重发。 */
91
+ function budgetFits(body, cap) {
92
+ const thinking = body["thinking"];
93
+ if (typeof thinking !== "object" || thinking === null)
94
+ return true;
95
+ const budget = thinking["budget_tokens"];
96
+ return typeof budget !== "number" || budget < cap;
97
+ }
98
+ /** `postWithCacheFallback` 之外再包一层:max_tokens 超上限的 400 以收紧值重发一次(只在 `start` 之前)。 */
99
+ export async function postWithMaxTokensFallback(model, url, options, field) {
100
+ const key = `${model.provider}/${model.id}`;
101
+ const body = withCap(options.body, field, maxTokensCaps.get(key));
102
+ try {
103
+ return await postWithCacheFallback(model, url, { ...options, body });
104
+ }
105
+ catch (error) {
106
+ if (options.signal.aborted || typeof body !== "object" || body === null)
107
+ throw error;
108
+ const rejection = classify(error);
109
+ const sent = body[field];
110
+ if (rejection === undefined || !("cap" in rejection) || typeof sent !== "number")
111
+ throw error;
112
+ if (rejection.cap >= sent || !budgetFits(body, rejection.cap))
113
+ throw error;
114
+ if (!rejection.transient)
115
+ maxTokensCaps.set(key, rejection.cap);
116
+ return postWithCacheFallback(model, url, {
117
+ ...options,
118
+ body: withCap(body, field, rejection.cap),
119
+ });
120
+ }
121
+ }
@@ -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;
@@ -19,12 +19,14 @@ import { contentText, normalizeContext, normalizeContextInline, sanitizeText, }
19
19
  import { DEFAULT_THINKING_BUDGETS, clampBudgetToAnswerRoom, clampThinkingLevel, mappedThinkingValue, } from "../thinking.js";
20
20
  import { cacheControlFor } from "./anthropic-request.js";
21
21
  import { effectiveRetention, resolveCacheRetention, resolvePromptCacheCompat, } from "./cache-params.js";
22
+ import { largeString } from "../json-body.js";
22
23
  import { detectCompat } from "./openai-compat.js";
23
24
  /** 思考文本回放用的字段名;thinkingSignature 记录来源字段,回放时原样放回。 */
24
25
  export const REASONING_FIELDS = ["reasoning_content", "reasoning", "reasoning_text"];
25
26
  export const ASSISTANT_BRIDGE_TEXT = "Tool results received.";
26
27
  function imagePart(block) {
27
- return { type: "image_url", image_url: { url: `data:${block.mimeType};base64,${block.data}` } };
28
+ const url = largeString(`data:${block.mimeType};base64,`, block, block.data);
29
+ return { type: "image_url", image_url: { url } };
28
30
  }
29
31
  function convertUser(message) {
30
32
  if (typeof message.content === "string") {
@@ -67,7 +69,7 @@ function convertAssistant(model, message, compat) {
67
69
  out["tool_calls"] = calls.map((call) => ({
68
70
  id: normalizeCallId(model, call.id),
69
71
  type: "function",
70
- function: { name: call.name, arguments: JSON.stringify(call.arguments) },
72
+ function: { name: call.name, arguments: call.rawArguments ?? JSON.stringify(call.arguments) },
71
73
  }));
72
74
  }
73
75
  if (out["content"] === null && calls.length === 0)
@@ -21,6 +21,7 @@
21
21
  * - [W6-O] compat `chatgptBackend`(ChatGPT 订阅后端):最后经 chatgpt-backend.ts 按后端白名单改写。
22
22
  */
23
23
  import { contentText, normalizeContext, sanitizeText } from "../context.js";
24
+ import { largeString } from "../json-body.js";
24
25
  import { clampThinkingLevel, mappedThinkingValue } from "../thinking.js";
25
26
  import { resolveCacheRetention, resolvePromptCacheCompat } from "./cache-params.js";
26
27
  import { transformChatGptBody } from "./chatgpt-backend.js";
@@ -110,7 +111,7 @@ function imagePart(block) {
110
111
  return {
111
112
  type: "input_image",
112
113
  detail: "auto",
113
- image_url: `data:${block.mimeType};base64,${block.data}`,
114
+ image_url: largeString(`data:${block.mimeType};base64,`, block, block.data),
114
115
  };
115
116
  }
116
117
  function convertUser(message) {
@@ -162,7 +163,7 @@ function convertAssistant(model, message) {
162
163
  ...(sameModel && itemId?.startsWith("fc") ? { id: itemId } : {}),
163
164
  call_id: block.id,
164
165
  name: block.name,
165
- arguments: JSON.stringify(block.arguments),
166
+ arguments: block.rawArguments ?? JSON.stringify(block.arguments),
166
167
  });
167
168
  }
168
169
  }
@@ -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;