@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
@@ -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;
@@ -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` 是发给供应商的工具表(按注册顺序),与真实协议请求体的前缀同口径。
@@ -17,6 +20,7 @@ import { appendFileSync } from "node:fs";
17
20
  import { AssistantEventStreamImpl } from "../event-stream.js";
18
21
  import { BlockTracker, ProviderStopError, createOutput, finishDone, finishError, } from "../apis/shared.js";
19
22
  import { contentText, normalizeContext } from "../context.js";
23
+ import { HttpError } from "../http.js";
20
24
  import { describeFakeError, loadFakeScript, parseFakeScript, } from "./fake-script.js";
21
25
  export const FAKE_PROVIDER_ID = "fake";
22
26
  export const FAKE_API_ID = "fake";
@@ -100,6 +104,8 @@ export class FakeProvider {
100
104
  scriptLoader;
101
105
  loaded = false;
102
106
  recordFile;
107
+ keepCalls;
108
+ count = 0;
103
109
  constructor(script, options = {}) {
104
110
  if (typeof script === "function")
105
111
  this.scriptLoader = script;
@@ -107,13 +113,14 @@ export class FakeProvider {
107
113
  this.setScript(script);
108
114
  const record = options.recordFile;
109
115
  this.recordFile = typeof record === "function" ? record : () => record;
116
+ this.keepCalls = options.keepCalls !== false;
110
117
  }
111
118
  api = {
112
119
  id: FAKE_API_ID,
113
120
  stream: (model, context, options) => this.stream(model, context, options),
114
121
  };
115
122
  get callCount() {
116
- return this.calls.length;
123
+ return this.count;
117
124
  }
118
125
  setScript(script) {
119
126
  this.script = parseFakeScript(script);
@@ -126,6 +133,7 @@ export class FakeProvider {
126
133
  }
127
134
  reset() {
128
135
  this.calls.length = 0;
136
+ this.count = 0;
129
137
  }
130
138
  ensureLoaded() {
131
139
  if (this.loaded)
@@ -151,9 +159,11 @@ export class FakeProvider {
151
159
  return undefined;
152
160
  }
153
161
  stream(model, context, options) {
154
- const index = this.calls.length;
155
- const { signal: _s, onPayload: _p, onResponse: _r, ...rest } = options;
156
- this.calls.push({ index, model, context, options: rest });
162
+ const index = this.count++;
163
+ if (this.keepCalls) {
164
+ const { signal: _s, onPayload: _p, onResponse: _r, ...rest } = options;
165
+ this.calls.push({ index, model, context, options: rest });
166
+ }
157
167
  this.record(index, model, context, options);
158
168
  const response = this.responseFor(index) ?? {
159
169
  text: lastUserText(context) || "(no input)",
@@ -198,7 +208,9 @@ export class FakeProvider {
198
208
  const error = response.error ? describeFakeError(response.error) : undefined;
199
209
  if (error && response.error?.kind !== "disconnect") {
200
210
  options.onResponse?.(error.status, new Headers());
201
- throw new Error(error.message);
211
+ if (error.status < 400)
212
+ throw new Error(error.message);
213
+ throw new HttpError(error.status, error.message, "", response.error?.retryAfterMs);
202
214
  }
203
215
  options.onResponse?.(200, new Headers({ "content-type": "text/event-stream" }));
204
216
  if (options.thinkingLevel)
@@ -278,5 +290,5 @@ export class FakeProvider {
278
290
  export const defaultFakeProvider = new FakeProvider(() => {
279
291
  const path = process.env[FAKE_SCRIPT_ENV];
280
292
  return path ? loadFakeScript(path) : undefined;
281
- }, { recordFile: () => process.env[FAKE_RECORD_ENV] || undefined });
293
+ }, { recordFile: () => process.env[FAKE_RECORD_ENV] || undefined, keepCalls: false });
282
294
  export const fakeApi = defaultFakeProvider.api;
@@ -41,6 +41,8 @@ export interface FakeError {
41
41
  kind: FakeErrorKind;
42
42
  message?: string;
43
43
  status?: number;
44
+ /** [ME-C] 模拟 `Retry-After`(毫秒):写进失败消息的 `retryAfterMs`。 */
45
+ retryAfterMs?: number;
44
46
  }
45
47
  export interface FakeResponse {
46
48
  /** 依次产出的块;与 `text` 简写二选一(都给时 text 在后)。 */
package/dist/ai/http.d.ts CHANGED
@@ -5,9 +5,10 @@
5
5
  * `http.setGlobalProxyFromEnv()`(等价于 `NODE_USE_ENV_PROXY=1`);SDK 嵌入方自行决定。
6
6
  * 本模块不自行实现代理。
7
7
  *
8
- * 超时:`timeoutMs` 只管到拿到响应头为止;`idleTimeoutMs`(缺省 300 s,0 关闭)既管等响应头,
9
- * 也管流式读取期间两次收到数据之间的间隔(每收到一块字节即重新计时,见 sse.ts)。空闲超时抛
10
- * `IdleTimeoutError`,文案含 `idle timeout`,会话层按可重试错误处理。
8
+ * 超时:`timeoutMs` 只管到拿到响应头为止;`idleTimeoutMs`(缺省 300 s,0 关闭)管等响应头;
9
+ * `streamIdleTimeoutMs`(缺省 180 s,0 关闭)管流式读取期间两次收到数据之间的间隔(每收到一块字节
10
+ * 即重新计时,见 sse.ts)。[ME-C] D18 空闲超时抛 `IdleTimeoutError`,文案含 `idle timeout`,会话层按
11
+ * 可重试错误处理。
11
12
  *
12
13
  * 协议层自身**不重试**(重试在会话层,§3.6)。
13
14
  */
@@ -33,15 +34,22 @@ export declare class HttpError extends Error {
33
34
  export declare class RequestTimeoutError extends Error {
34
35
  constructor(timeoutMs: number);
35
36
  }
36
- /** 流空闲超时缺省值:与 `request.idleTimeoutMs` / `AMA_IDLE_TIMEOUT_MS` 的缺省一致。 */
37
+ /** 等响应头的空闲超时缺省值:与 `request.idleTimeoutMs` / `AMA_IDLE_TIMEOUT_MS` 的缺省一致。 */
37
38
  export declare const DEFAULT_IDLE_TIMEOUT_MS = 300000;
38
- /** 请求选项里的空闲超时;未给出用缺省值,0 或负数表示关闭(返回 undefined)。 */
39
+ /** 流中两块数据之间的空闲超时缺省值:与 `request.streamIdleTimeoutMs` 的缺省一致。 */
40
+ export declare const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 180000;
41
+ /** 请求选项里等响应头的空闲超时;未给出用缺省值,0 或负数表示关闭(返回 undefined)。 */
39
42
  export declare function idleTimeoutOf(options: {
40
43
  idleTimeoutMs?: number | undefined;
41
44
  }): number | undefined;
45
+ /** 请求选项里流中的空闲超时;未给出用缺省值,0 或负数表示关闭(返回 undefined)。 */
46
+ export declare function streamIdleTimeoutOf(options: {
47
+ streamIdleTimeoutMs?: number | undefined;
48
+ }): number | undefined;
42
49
  /**
43
- * 服务端在 `idleTimeoutMs` 内没有任何字节:`response` = 发出请求后迟迟没有响应头,
44
- * `stream` = 流读到一半停住。文案含 `idle timeout`(重试分类据此判为可重试)。
50
+ * 服务端在空闲上限内没有任何字节:`response` = 发出请求后迟迟没有响应头(`idleTimeoutMs`),
51
+ * `stream` = 流读到一半停住(`streamIdleTimeoutMs`)。文案含 `idle timeout`(重试分类据此判为可重试),
52
+ * 并点名对应的配置键与环境变量。
45
53
  */
46
54
  export declare class IdleTimeoutError extends Error {
47
55
  readonly idleTimeoutMs: number;
@@ -60,7 +68,7 @@ export interface PostOptions {
60
68
  body: unknown;
61
69
  signal: AbortSignal;
62
70
  timeoutMs?: number | undefined;
63
- /** 等响应头的空闲上限(`timeoutMs` 未给时生效);undefined 不限。 */
71
+ /** 等响应头的空闲上限(`timeoutMs` 未给时生效);undefined 不限。流中的空闲见 `readSseEvents`。 */
64
72
  idleTimeoutMs?: number | undefined;
65
73
  onResponse?: ((status: number, headers: Headers) => void) | undefined;
66
74
  }
@@ -68,7 +76,7 @@ export interface PostOptions {
68
76
  * POST JSON 并返回 2xx 响应(body 是 SSE 字节流)。非 2xx → HttpError;超时 → RequestTimeoutError
69
77
  * (`timeoutMs`)或 IdleTimeoutError(`idleTimeoutMs`);调用方的 signal 中止 → 原样抛 AbortError
70
78
  * (调用方据 signal.aborted 判 aborted)。超时只覆盖到拿到响应头为止,流式读取期间的空闲由
71
- * `readSseEvents` 的 `idleTimeoutMs` 控制。
79
+ * `readSseEvents` 的空闲上限(`streamIdleTimeoutOf`)控制。
72
80
  */
73
81
  export declare function postJson(url: string, options: PostOptions): Promise<Response>;
74
82
  /** undici 的 `fetch failed` 把真正原因藏在 cause 里;拼出来便于重试分类与排错。 */
package/dist/ai/http.js CHANGED
@@ -5,13 +5,15 @@
5
5
  * `http.setGlobalProxyFromEnv()`(等价于 `NODE_USE_ENV_PROXY=1`);SDK 嵌入方自行决定。
6
6
  * 本模块不自行实现代理。
7
7
  *
8
- * 超时:`timeoutMs` 只管到拿到响应头为止;`idleTimeoutMs`(缺省 300 s,0 关闭)既管等响应头,
9
- * 也管流式读取期间两次收到数据之间的间隔(每收到一块字节即重新计时,见 sse.ts)。空闲超时抛
10
- * `IdleTimeoutError`,文案含 `idle timeout`,会话层按可重试错误处理。
8
+ * 超时:`timeoutMs` 只管到拿到响应头为止;`idleTimeoutMs`(缺省 300 s,0 关闭)管等响应头;
9
+ * `streamIdleTimeoutMs`(缺省 180 s,0 关闭)管流式读取期间两次收到数据之间的间隔(每收到一块字节
10
+ * 即重新计时,见 sse.ts)。[ME-C] D18 空闲超时抛 `IdleTimeoutError`,文案含 `idle timeout`,会话层按
11
+ * 可重试错误处理。
11
12
  *
12
13
  * 协议层自身**不重试**(重试在会话层,§3.6)。
13
14
  */
14
15
  import { AMA_VERSION } from "../version.js";
16
+ import { jsonFetchBody } from "./json-body.js";
15
17
  /** 大小写不敏感地合并头;后者覆盖前者;值为 null 表示删除该头。键保留最后一次出现的写法。 */
16
18
  export function mergeHeaders(...sources) {
17
19
  const merged = new Map();
@@ -80,19 +82,26 @@ export class RequestTimeoutError extends Error {
80
82
  this.name = "RequestTimeoutError";
81
83
  }
82
84
  }
83
- /** 流空闲超时缺省值:与 `request.idleTimeoutMs` / `AMA_IDLE_TIMEOUT_MS` 的缺省一致。 */
85
+ /** 等响应头的空闲超时缺省值:与 `request.idleTimeoutMs` / `AMA_IDLE_TIMEOUT_MS` 的缺省一致。 */
84
86
  export const DEFAULT_IDLE_TIMEOUT_MS = 300_000;
85
- /** 请求选项里的空闲超时;未给出用缺省值,0 或负数表示关闭(返回 undefined)。 */
87
+ /** 流中两块数据之间的空闲超时缺省值:与 `request.streamIdleTimeoutMs` 的缺省一致。 */
88
+ export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 180_000;
89
+ const positive = (value) => Number.isFinite(value) && value > 0 ? value : undefined;
90
+ /** 请求选项里等响应头的空闲超时;未给出用缺省值,0 或负数表示关闭(返回 undefined)。 */
86
91
  export function idleTimeoutOf(options) {
87
- const value = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
88
- return Number.isFinite(value) && value > 0 ? value : undefined;
92
+ return positive(options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS);
93
+ }
94
+ /** 请求选项里流中的空闲超时;未给出用缺省值,0 或负数表示关闭(返回 undefined)。 */
95
+ export function streamIdleTimeoutOf(options) {
96
+ return positive(options.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS);
89
97
  }
90
98
  function formatDuration(ms) {
91
99
  return ms >= 1000 && ms % 1000 === 0 ? `${ms / 1000} s` : `${ms} ms`;
92
100
  }
93
101
  /**
94
- * 服务端在 `idleTimeoutMs` 内没有任何字节:`response` = 发出请求后迟迟没有响应头,
95
- * `stream` = 流读到一半停住。文案含 `idle timeout`(重试分类据此判为可重试)。
102
+ * 服务端在空闲上限内没有任何字节:`response` = 发出请求后迟迟没有响应头(`idleTimeoutMs`),
103
+ * `stream` = 流读到一半停住(`streamIdleTimeoutMs`)。文案含 `idle timeout`(重试分类据此判为可重试),
104
+ * 并点名对应的配置键与环境变量。
96
105
  */
97
106
  export class IdleTimeoutError extends Error {
98
107
  idleTimeoutMs;
@@ -101,7 +110,10 @@ export class IdleTimeoutError extends Error {
101
110
  const what = phase === "response"
102
111
  ? `No response from the server within ${formatDuration(idleTimeoutMs)}`
103
112
  : `Stream stalled: no data from the server for ${formatDuration(idleTimeoutMs)}`;
104
- super(`${what} (idle timeout; adjust with request.idleTimeoutMs or AMA_IDLE_TIMEOUT_MS)`);
113
+ const knob = phase === "response"
114
+ ? "request.idleTimeoutMs or AMA_IDLE_TIMEOUT_MS"
115
+ : "request.streamIdleTimeoutMs or AMA_STREAM_IDLE_TIMEOUT_MS";
116
+ super(`${what} (idle timeout; adjust with ${knob})`);
105
117
  this.name = "IdleTimeoutError";
106
118
  this.idleTimeoutMs = idleTimeoutMs;
107
119
  this.phase = phase;
@@ -171,11 +183,17 @@ function parseRetryAfter(headers) {
171
183
  const date = Date.parse(value);
172
184
  return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
173
185
  }
186
+ /** 流式请求体须带 `content-length`(不走 chunked,部分中转不收);已有同名头时以算出的字节数为准。 */
187
+ function withContentLength(headers, length) {
188
+ return length === undefined
189
+ ? headers
190
+ : mergeHeaders(headers, { "content-length": String(length) });
191
+ }
174
192
  /**
175
193
  * POST JSON 并返回 2xx 响应(body 是 SSE 字节流)。非 2xx → HttpError;超时 → RequestTimeoutError
176
194
  * (`timeoutMs`)或 IdleTimeoutError(`idleTimeoutMs`);调用方的 signal 中止 → 原样抛 AbortError
177
195
  * (调用方据 signal.aborted 判 aborted)。超时只覆盖到拿到响应头为止,流式读取期间的空闲由
178
- * `readSseEvents` 的 `idleTimeoutMs` 控制。
196
+ * `readSseEvents` 的空闲上限(`streamIdleTimeoutOf`)控制。
179
197
  */
180
198
  export async function postJson(url, options) {
181
199
  const explicit = options.timeoutMs !== undefined && options.timeoutMs > 0;
@@ -185,10 +203,12 @@ export async function postJson(url, options) {
185
203
  const signal = AbortSignal.any([options.signal, timer.signal]);
186
204
  let response;
187
205
  try {
206
+ const payload = jsonFetchBody(options.body);
188
207
  response = await fetch(url, {
189
208
  method: "POST",
190
- headers: options.headers,
191
- body: JSON.stringify(options.body),
209
+ headers: withContentLength(options.headers, payload.contentLength),
210
+ body: payload.body,
211
+ ...(payload.contentLength === undefined ? {} : { duplex: "half" }),
192
212
  signal,
193
213
  });
194
214
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 图片 base64 按内容哈希驻留(docs/memory-plan.md D5、§2.5)。
3
+ *
4
+ * `WeakRef` 的目标不能是字符串,所以驻留单元是 `ImageBlock`:表为 `Map<键, WeakRef<ImageBlock>>`,
5
+ * 键 = `mimeType` + base64 字符串的 sha256;`FinalizationRegistry` 在 block 被回收后清键。命中时返回
6
+ * 已驻留的 block,调用方把它放进内容数组,其中的 base64 字符串随之共享;未命中登记并返回入参。
7
+ * 驻留 block 只在仍被某条消息引用时存活,表本身不增加常驻。block 当作不可变值:没有任何代码就地改它。
8
+ *
9
+ * 哈希一律对 base64 字符串计算(base64 与原始字节一一对应),`read` / 附图与会话恢复的图片才能互相命中。
10
+ */
11
+ import type { ImageBlock } from "./types.js";
12
+ /**
13
+ * 会话条目的最小形状:ai 不 import session(docs/design.md §1.1),`SessionEntry[]` 可直接传入。
14
+ * 只有 `type === "message"` 且 `message.content` 为数组的条目里的 `image` 块参与驻留。
15
+ */
16
+ export interface ImageCarrier {
17
+ readonly type: string;
18
+ readonly message?: object;
19
+ }
20
+ /** 内容哈希(小写 hex);字符串按 UTF-8 计算。 */
21
+ export declare function sha256Hex(data: Buffer | string): string;
22
+ /**
23
+ * 返回与 `block` 内容相同的驻留 block;`hash` 是 `block.data`(base64 字符串)的 sha256,缺省时现算。
24
+ */
25
+ export declare function internImage(block: ImageBlock, hash?: string): ImageBlock;
26
+ /** 就地把会话条目里的图片 block 换成驻留 block,返回替换数(已是驻留对象的不计)。 */
27
+ export declare function internSessionImages(entries: readonly ImageCarrier[]): number;
28
+ /** 驻留表当前的键数(测试与诊断用;包括目标已回收、清理回调尚未运行的键)。 */
29
+ export declare function internedImageCount(): number;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * 图片 base64 按内容哈希驻留(docs/memory-plan.md D5、§2.5)。
3
+ *
4
+ * `WeakRef` 的目标不能是字符串,所以驻留单元是 `ImageBlock`:表为 `Map<键, WeakRef<ImageBlock>>`,
5
+ * 键 = `mimeType` + base64 字符串的 sha256;`FinalizationRegistry` 在 block 被回收后清键。命中时返回
6
+ * 已驻留的 block,调用方把它放进内容数组,其中的 base64 字符串随之共享;未命中登记并返回入参。
7
+ * 驻留 block 只在仍被某条消息引用时存活,表本身不增加常驻。block 当作不可变值:没有任何代码就地改它。
8
+ *
9
+ * 哈希一律对 base64 字符串计算(base64 与原始字节一一对应),`read` / 附图与会话恢复的图片才能互相命中。
10
+ */
11
+ import { createHash } from "node:crypto";
12
+ /** 内容哈希(小写 hex);字符串按 UTF-8 计算。 */
13
+ export function sha256Hex(data) {
14
+ return createHash("sha256").update(data).digest("hex");
15
+ }
16
+ const table = new Map();
17
+ const registry = new FinalizationRegistry((key) => {
18
+ // 同一键可能已换成新 block:只在旧引用确已失效时删
19
+ if (table.get(key)?.deref() === undefined)
20
+ table.delete(key);
21
+ });
22
+ /**
23
+ * 返回与 `block` 内容相同的驻留 block;`hash` 是 `block.data`(base64 字符串)的 sha256,缺省时现算。
24
+ */
25
+ export function internImage(block, hash) {
26
+ const key = `${block.mimeType}:${hash ?? sha256Hex(block.data)}`;
27
+ const existing = table.get(key)?.deref();
28
+ if (existing !== undefined && existing.data.length === block.data.length)
29
+ return existing;
30
+ table.set(key, new WeakRef(block));
31
+ registry.register(block, key);
32
+ return block;
33
+ }
34
+ /** 就地把会话条目里的图片 block 换成驻留 block,返回替换数(已是驻留对象的不计)。 */
35
+ export function internSessionImages(entries) {
36
+ let replaced = 0;
37
+ for (const entry of entries) {
38
+ const message = entry.type === "message" ? entry.message : undefined;
39
+ const content = message?.content;
40
+ if (!Array.isArray(content))
41
+ continue;
42
+ for (let i = 0; i < content.length; i++) {
43
+ const block = content[i];
44
+ if (block?.type !== "image" || typeof block.data !== "string")
45
+ continue;
46
+ const interned = internImage(block);
47
+ if (interned !== block) {
48
+ content[i] = interned;
49
+ replaced++;
50
+ }
51
+ }
52
+ }
53
+ return replaced;
54
+ }
55
+ /** 驻留表当前的键数(测试与诊断用;包括目标已回收、清理回调尚未运行的键)。 */
56
+ export function internedImageCount() {
57
+ return table.size;
58
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 请求体序列化(docs/memory-plan.md D3、§2.3)。
3
+ *
4
+ * 带图请求体里 base64 占绝大部分字节;`JSON.stringify(body)` 会先拼出整份中间字符串,再由 fetch
5
+ * 编码成 UTF-8。这里先廉价扫一遍:没有 ≥ {@link LARGE_STRING_BYTES} 的字符串就照旧走原生;有就只沿
6
+ * 「普通对象 / 数组」结构拼片段——小子树仍交给原生 `JSON.stringify`,大字符串经检查无需转义时原样
7
+ * 作为一个片段。片段两种用法:`serializeJsonBody` 一次分配结果 Buffer 依次写入;`jsonFetchBody`
8
+ * (`postJson` 用)逐片段编码成流、声明总字节数,不在内存里留整份请求体。
9
+ *
10
+ * 结果与 `Buffer.from(JSON.stringify(body), "utf8")` 逐字节相同:自写部分只有结构(`{`、`,`、
11
+ * 键、`[`)与已验证无需转义的大字符串(需要转义的大字符串整个交给原生);`toJSON`、`undefined` /
12
+ * 函数 / symbol 的跳过与 `null`、数字格式、转义全部由原生处理(带 `toJSON` 的值经单键容器序列化,
13
+ * 保证 `toJSON(key)` 收到与原生相同的键)。
14
+ *
15
+ * 图片由转换器包成 {@link LargeString}(`prefix + text`):片段路径直接写转录里的原字符串,不经模板串
16
+ * 拼接后的扁平化;无需转义的结论与字节数按 ImageBlock 缓存,同一张图每进程只扫一次。流式发送时大片段
17
+ * 按 {@link STREAM_CHUNK_CHARS} 切块编码。
18
+ */
19
+ /** 达到这个长度(UTF-16 码元)的字符串才走片段路径。 */
20
+ export declare const LARGE_STRING_BYTES: number;
21
+ /** `isPlain` 的调用次数(测试用:同一块图片第二次序列化不应再扫)。 */
22
+ export declare const plainScans: {
23
+ count: number;
24
+ };
25
+ /**
26
+ * 请求体里的大字符串叶子 `prefix + text`(data URL 或裸 base64),用来代替模板串拼接:拼接得到的
27
+ * cons string 在每次序列化时都会被扁平化成一份新的整图字符串。片段路径直接写 `prefix` 与 `text`
28
+ * (`text` 是转录里的原字符串,不拷贝);`text` 无需转义的结论按 `key`(转录里身份稳定的 ImageBlock)
29
+ * 缓存,同一块每进程只扫一次。约定:同一 `key` 的 `text` 不变。其它遍历者看到的是一个带 `toJSON`
30
+ * 的对象,原生 `JSON.stringify` 得到相同字符串。由 {@link largeString} 构造。
31
+ */
32
+ export declare class LargeString {
33
+ readonly prefix: string;
34
+ readonly key: object;
35
+ readonly text: string;
36
+ constructor(prefix: string, key: object, text: string);
37
+ toJSON(): string;
38
+ }
39
+ /**
40
+ * `prefix + text` 作为请求体的值:`text` 够大且 `prefix` 自身无需转义时给 {@link LargeString},
41
+ * 否则就是拼好的字符串(小图与以前完全相同)。
42
+ */
43
+ export declare function largeString(prefix: string, key: object, text: string): string | LargeString;
44
+ /** 与 `Buffer.from(JSON.stringify(body), "utf8")` 逐字节相同;大字符串不经中间字符串。 */
45
+ export declare function serializeJsonBody(body: unknown): Buffer;
46
+ /** `postJson` 交给 fetch 的请求体。 */
47
+ export interface JsonFetchBody {
48
+ body: string | ReadableStream<Uint8Array>;
49
+ /** 流式请求体的字节数:调用方须作为 `content-length` 头发出(不走 chunked)。 */
50
+ contentLength?: number;
51
+ }
52
+ /**
53
+ * 流式请求体每块最多编码的码元数:大片段(一整张图)按游标切片(V8 切片串不拷贝)逐块编码,
54
+ * 发送期间只多出一块的 UTF-8 字节,而不是一整张图。
55
+ */
56
+ export declare const STREAM_CHUNK_CHARS: number;
57
+ /**
58
+ * 给 fetch 的请求体。不含大字符串时就是 `JSON.stringify(body)`(与以前完全相同);含大字符串时是
59
+ * 逐片段编码的流,并给出总字节数。不交给 fetch 一个 Buffer:Node 的 fetch 会把 BufferSource
60
+ * 请求体再复制两份(实测 mock 300 步带图峰值反而从约 1.0 GB 升到 1.85 GB),流则按片段边读边发,
61
+ * 同一时刻只多出一块(≤ {@link STREAM_CHUNK_CHARS} 码元)的 UTF-8 字节。字节序列与 `serializeJsonBody` 相同。
62
+ */
63
+ export declare function jsonFetchBody(body: unknown): JsonFetchBody;