@armadra/agent 0.2.1 → 0.4.0

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 (232) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +142 -49
  3. package/dist/agent/retry.d.ts +1 -1
  4. package/dist/agent/retry.js +2 -1
  5. package/dist/agent/session-cache.js +6 -3
  6. package/dist/agent/session-classifier.d.ts +18 -0
  7. package/dist/agent/session-classifier.js +103 -0
  8. package/dist/agent/session-core.d.ts +13 -0
  9. package/dist/agent/session-state.js +2 -1
  10. package/dist/agent/session-sync.js +9 -2
  11. package/dist/agent/session-tools.js +24 -3
  12. package/dist/agent/session.d.ts +3 -0
  13. package/dist/agent/session.js +20 -10
  14. package/dist/agent/tool-runner.js +15 -2
  15. package/dist/agent/types.d.ts +9 -1
  16. package/dist/ai/apis/anthropic-messages.js +3 -2
  17. package/dist/ai/apis/google-generative-ai.js +3 -2
  18. package/dist/ai/apis/openai-completions.js +3 -2
  19. package/dist/ai/apis/openai-responses.js +5 -3
  20. package/dist/ai/cache/fingerprint.d.ts +1 -1
  21. package/dist/ai/cache/fingerprint.js +1 -1
  22. package/dist/ai/cache/reporting.js +2 -1
  23. package/dist/ai/http.d.ts +28 -6
  24. package/dist/ai/http.js +41 -8
  25. package/dist/ai/providers/channels.d.ts +51 -0
  26. package/dist/ai/providers/channels.js +96 -0
  27. package/dist/ai/providers/enrich.d.ts +34 -0
  28. package/dist/ai/providers/enrich.js +86 -0
  29. package/dist/ai/providers/models-dev-cache.d.ts +53 -0
  30. package/dist/ai/providers/models-dev-cache.js +147 -0
  31. package/dist/ai/providers/models-dev.d.ts +99 -0
  32. package/dist/ai/providers/models-dev.js +315 -0
  33. package/dist/ai/providers/registry.d.ts +42 -4
  34. package/dist/ai/providers/registry.js +195 -34
  35. package/dist/ai/providers/suggest.d.ts +18 -0
  36. package/dist/ai/providers/suggest.js +72 -0
  37. package/dist/ai/sse.d.ts +6 -2
  38. package/dist/ai/sse.js +22 -2
  39. package/dist/ai/types.d.ts +30 -3
  40. package/dist/bundle/ama.cjs +17893 -10698
  41. package/dist/cli/args.d.ts +23 -5
  42. package/dist/cli/args.js +113 -13
  43. package/dist/cli/bootstrap.js +28 -3
  44. package/dist/cli/codemode-notice.d.ts +20 -0
  45. package/dist/cli/codemode-notice.js +55 -0
  46. package/dist/cli/compose-providers.js +3 -0
  47. package/dist/cli/compose-session.d.ts +8 -0
  48. package/dist/cli/compose-session.js +32 -1
  49. package/dist/cli/compose-store.d.ts +1 -1
  50. package/dist/cli/compose-store.js +3 -1
  51. package/dist/cli/compose.d.ts +17 -5
  52. package/dist/cli/compose.js +53 -19
  53. package/dist/cli/default-model.d.ts +38 -1
  54. package/dist/cli/default-model.js +96 -9
  55. package/dist/cli/deps.d.ts +38 -0
  56. package/dist/cli/exit-codes.d.ts +2 -0
  57. package/dist/cli/exit-codes.js +3 -0
  58. package/dist/cli/fake-visibility.d.ts +13 -0
  59. package/dist/cli/fake-visibility.js +26 -0
  60. package/dist/cli/from-prompt.d.ts +18 -0
  61. package/dist/cli/from-prompt.js +49 -0
  62. package/dist/cli/main.d.ts +10 -2
  63. package/dist/cli/main.js +88 -2
  64. package/dist/cli/proxy.d.ts +51 -0
  65. package/dist/cli/proxy.js +135 -0
  66. package/dist/cli/startup-screen.d.ts +29 -0
  67. package/dist/cli/startup-screen.js +47 -0
  68. package/dist/cli/startup-steps.d.ts +1 -1
  69. package/dist/cli/startup-steps.js +20 -13
  70. package/dist/cli/subcommands/config.d.ts +29 -4
  71. package/dist/cli/subcommands/config.js +188 -25
  72. package/dist/cli/subcommands/context.js +5 -1
  73. package/dist/cli/subcommands/doctor.js +12 -1
  74. package/dist/cli/subcommands/init.d.ts +6 -0
  75. package/dist/cli/subcommands/init.js +23 -0
  76. package/dist/cli/subcommands/model-meta.d.ts +16 -0
  77. package/dist/cli/subcommands/model-meta.js +54 -0
  78. package/dist/cli/subcommands/models-cache-probe.js +1 -1
  79. package/dist/cli/subcommands/models-discover.d.ts +18 -6
  80. package/dist/cli/subcommands/models-discover.js +87 -52
  81. package/dist/cli/subcommands/models.js +26 -18
  82. package/dist/cli/subcommands/probe-runner.d.ts +96 -0
  83. package/dist/cli/subcommands/probe-runner.js +264 -0
  84. package/dist/cli/subcommands/providers-list.d.ts +8 -0
  85. package/dist/cli/subcommands/providers-list.js +97 -0
  86. package/dist/cli/subcommands/providers-plan.d.ts +79 -0
  87. package/dist/cli/subcommands/providers-plan.js +215 -0
  88. package/dist/cli/subcommands/providers-probe.d.ts +34 -0
  89. package/dist/cli/subcommands/providers-probe.js +87 -0
  90. package/dist/cli/subcommands/providers.d.ts +28 -0
  91. package/dist/cli/subcommands/providers.js +436 -0
  92. package/dist/cli/subcommands/sessions-export.d.ts +10 -0
  93. package/dist/cli/subcommands/sessions-export.js +59 -0
  94. package/dist/cli/subcommands/sessions-search.d.ts +13 -0
  95. package/dist/cli/subcommands/sessions-search.js +103 -0
  96. package/dist/cli/subcommands/sessions.d.ts +3 -2
  97. package/dist/cli/subcommands/sessions.js +22 -1
  98. package/dist/cli/subcommands/stats.d.ts +17 -0
  99. package/dist/cli/subcommands/stats.js +198 -0
  100. package/dist/cli/system-prompt-arg.d.ts +11 -0
  101. package/dist/cli/system-prompt-arg.js +34 -0
  102. package/dist/codemode/modes.d.ts +4 -11
  103. package/dist/codemode/modes.js +5 -27
  104. package/dist/codemode/tool.d.ts +18 -14
  105. package/dist/codemode/tool.js +71 -28
  106. package/dist/config/init.d.ts +34 -0
  107. package/dist/config/init.js +99 -0
  108. package/dist/config/json-schema.d.ts +15 -0
  109. package/dist/config/json-schema.js +215 -0
  110. package/dist/config/key-docs.d.ts +22 -0
  111. package/dist/config/key-docs.js +105 -0
  112. package/dist/config/merge.d.ts +9 -8
  113. package/dist/config/merge.js +22 -7
  114. package/dist/config/schema.d.ts +1 -1
  115. package/dist/config/schema.js +97 -15
  116. package/dist/config/types.d.ts +57 -11
  117. package/dist/config/types.js +19 -1
  118. package/dist/modes/commands-core.js +6 -5
  119. package/dist/modes/image-input.d.ts +27 -0
  120. package/dist/modes/image-input.js +78 -0
  121. package/dist/modes/interactive/approval-dialog.d.ts +21 -5
  122. package/dist/modes/interactive/approval-dialog.js +106 -27
  123. package/dist/modes/interactive/commands.d.ts +11 -2
  124. package/dist/modes/interactive/commands.js +58 -19
  125. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  126. package/dist/modes/interactive/interactive-mode.js +70 -82
  127. package/dist/modes/interactive/key-dispatch.d.ts +3 -1
  128. package/dist/modes/interactive/key-dispatch.js +5 -6
  129. package/dist/modes/interactive/line/line-mode.d.ts +1 -0
  130. package/dist/modes/interactive/line/line-mode.js +9 -5
  131. package/dist/modes/interactive/line/line-render.d.ts +4 -0
  132. package/dist/modes/interactive/line/line-render.js +28 -6
  133. package/dist/modes/interactive/message-view.d.ts +48 -9
  134. package/dist/modes/interactive/message-view.js +238 -44
  135. package/dist/modes/interactive/panels.d.ts +18 -0
  136. package/dist/modes/interactive/panels.js +143 -0
  137. package/dist/modes/interactive/pickers.d.ts +23 -2
  138. package/dist/modes/interactive/pickers.js +48 -15
  139. package/dist/modes/interactive/run-indicator.d.ts +51 -0
  140. package/dist/modes/interactive/run-indicator.js +189 -0
  141. package/dist/modes/interactive/startup-header.d.ts +40 -0
  142. package/dist/modes/interactive/startup-header.js +169 -0
  143. package/dist/modes/interactive/startup-ui.d.ts +7 -2
  144. package/dist/modes/interactive/startup-ui.js +41 -11
  145. package/dist/modes/interactive/status-bar.d.ts +18 -15
  146. package/dist/modes/interactive/status-bar.js +98 -56
  147. package/dist/modes/interactive/tool-summary.d.ts +46 -0
  148. package/dist/modes/interactive/tool-summary.js +218 -0
  149. package/dist/modes/interactive/tool-view.d.ts +48 -15
  150. package/dist/modes/interactive/tool-view.js +203 -145
  151. package/dist/modes/print/print-mode.d.ts +33 -4
  152. package/dist/modes/print/print-mode.js +128 -8
  153. package/dist/modes/rpc/commands.js +12 -2
  154. package/dist/permissions/auto-safe.d.ts +60 -0
  155. package/dist/permissions/auto-safe.js +529 -0
  156. package/dist/permissions/classifier.d.ts +64 -0
  157. package/dist/permissions/classifier.js +184 -0
  158. package/dist/permissions/dangerous.d.ts +5 -0
  159. package/dist/permissions/dangerous.js +1 -1
  160. package/dist/permissions/modes.d.ts +30 -0
  161. package/dist/permissions/modes.js +78 -0
  162. package/dist/permissions/pipeline.d.ts +31 -4
  163. package/dist/permissions/pipeline.js +196 -6
  164. package/dist/permissions/protected.d.ts +19 -0
  165. package/dist/permissions/protected.js +74 -0
  166. package/dist/permissions/rules.js +3 -0
  167. package/dist/permissions/types.d.ts +50 -3
  168. package/dist/permissions/types.js +3 -0
  169. package/dist/rpc.d.ts +2 -0
  170. package/dist/sdk.d.ts +9 -3
  171. package/dist/sdk.js +10 -2
  172. package/dist/session/export.d.ts +32 -0
  173. package/dist/session/export.js +187 -0
  174. package/dist/session/projection.js +5 -1
  175. package/dist/session/redact.d.ts +15 -0
  176. package/dist/session/redact.js +55 -0
  177. package/dist/session/reuse.d.ts +33 -0
  178. package/dist/session/reuse.js +86 -0
  179. package/dist/session/scan.d.ts +34 -0
  180. package/dist/session/scan.js +140 -0
  181. package/dist/session/search.d.ts +52 -0
  182. package/dist/session/search.js +211 -0
  183. package/dist/session/stats-aggregate.d.ts +63 -0
  184. package/dist/session/stats-aggregate.js +163 -0
  185. package/dist/session/stats-index.d.ts +26 -0
  186. package/dist/session/stats-index.js +91 -0
  187. package/dist/session/stats-scan.d.ts +54 -0
  188. package/dist/session/stats-scan.js +236 -0
  189. package/dist/session/types.d.ts +2 -0
  190. package/dist/tools/image-file.d.ts +31 -0
  191. package/dist/tools/image-file.js +114 -0
  192. package/dist/tools/presets.d.ts +35 -8
  193. package/dist/tools/presets.js +56 -17
  194. package/dist/tools/read.d.ts +4 -8
  195. package/dist/tools/read.js +15 -64
  196. package/dist/tui/component.d.ts +8 -2
  197. package/dist/tui/component.js +3 -1
  198. package/dist/tui/components/box.d.ts +6 -1
  199. package/dist/tui/components/box.js +16 -6
  200. package/dist/tui/components/card.d.ts +23 -0
  201. package/dist/tui/components/card.js +37 -0
  202. package/dist/tui/components/editor-history.d.ts +6 -0
  203. package/dist/tui/components/editor-history.js +45 -0
  204. package/dist/tui/components/editor-paste.d.ts +1 -1
  205. package/dist/tui/components/editor-paste.js +4 -4
  206. package/dist/tui/components/editor.d.ts +11 -5
  207. package/dist/tui/components/editor.js +52 -58
  208. package/dist/tui/components/key-value.d.ts +3 -0
  209. package/dist/tui/components/key-value.js +16 -6
  210. package/dist/tui/components/loader.d.ts +37 -7
  211. package/dist/tui/components/loader.js +84 -21
  212. package/dist/tui/components/markdown.d.ts +5 -1
  213. package/dist/tui/components/markdown.js +45 -21
  214. package/dist/tui/components/meter.d.ts +3 -3
  215. package/dist/tui/components/meter.js +13 -11
  216. package/dist/tui/components/select-list.d.ts +23 -1
  217. package/dist/tui/components/select-list.js +76 -13
  218. package/dist/tui/glyphs.d.ts +68 -0
  219. package/dist/tui/glyphs.js +114 -0
  220. package/dist/tui/theme.d.ts +30 -6
  221. package/dist/tui/theme.js +103 -17
  222. package/dist/tui.d.ts +4 -2
  223. package/dist/tui.js +3 -1
  224. package/docs/codemode.md +23 -9
  225. package/docs/hooks.md +10 -10
  226. package/docs/permissions.md +148 -0
  227. package/docs/providers.md +241 -3
  228. package/docs/rpc.md +38 -38
  229. package/docs/session-format.md +3 -2
  230. package/docs/sessions.md +134 -0
  231. package/docs/tui.md +140 -61
  232. package/package.json +3 -1
@@ -7,6 +7,7 @@
7
7
  import type { WarmingDecisionHandler } from "../ai/cache/types.js";
8
8
  import type { Model, ModelThinkingLevel, ProviderRegistryApi } from "../ai/types.js";
9
9
  import type { HookDispatcherApi, HookEvent, HookEventPayload, HookOutcome } from "../hooks/types.js";
10
+ import type { PermissionClassifier } from "../permissions/classifier.js";
10
11
  import type { ApprovalBroker, PermissionPipelineApi } from "../permissions/types.js";
11
12
  import type { SessionManager } from "../session/manager.js";
12
13
  import type { SessionEntry, SessionEntryInput } from "../session/types.js";
@@ -37,6 +38,14 @@ export interface AgentSessionOptions {
37
38
  brokers?: readonly ApprovalBroker[];
38
39
  /** 无人值守:权限管线把 ask 变 deny。 */
39
40
  unattended?: boolean;
41
+ /**
42
+ * auto 权限模式的模型分类器(§7.4):`model` 为 `provider/model`(config `permission.autoModel`),
43
+ * 缺省用当前会话模型;`timeoutMs` 缺省 10 000。
44
+ */
45
+ permissionClassifier?: {
46
+ model?: string;
47
+ timeoutMs?: number;
48
+ };
40
49
  /** 审批超时,缺省 10 分钟(超时 deny)。 */
41
50
  approvalTimeoutMs?: number;
42
51
  /** 命令式 Hook(B5);缺省无 Hook。 */
@@ -59,6 +68,8 @@ export interface AgentSessionOptions {
59
68
  outputDir?: string;
60
69
  /** task 深度:主会话 0。 */
61
70
  depth?: number;
71
+ /** 模型请求的空闲超时(毫秒,0 关闭);缺省由协议层取 300 000。 */
72
+ idleTimeoutMs?: number;
62
73
  /** 子 Agent 的回合上限(maxTurns)。 */
63
74
  maxTurns?: number;
64
75
  /** 子 Agent 并发上限,缺省 4;`false` 关闭 spawnSubagent。 */
@@ -99,5 +110,7 @@ export interface SessionCore {
99
110
  /** Hook 返回 continue:false 时记下,finishTurn 据此结束 run。 */
100
111
  requestStop(reason: string | undefined): void;
101
112
  spawnSubagent(request: SubagentRequest): Promise<SubagentResult>;
113
+ /** auto 权限模式的分类器(懒建,会话内缓存判定)。 */
114
+ autoClassifier(): PermissionClassifier;
102
115
  log(level: "debug" | "info" | "warn" | "error", message: string): void;
103
116
  }
@@ -5,13 +5,14 @@
5
5
  * 压缩与分支摘要请求的 usage 也计入;`usage` 条目(缓存保温等不进上下文的请求,第三波 §1.7)
6
6
  * 计入 token 与费用但不算消息。上下文 % 用投影感知估算(§9)。
7
7
  */
8
+ import { modelRefOf } from "../ai/providers/channels.js";
8
9
  export function buildSessionState(input) {
9
10
  const { agent, manager, model } = input;
10
11
  return {
11
12
  isStreaming: agent.isRunning,
12
13
  isCompacting: input.isCompacting,
13
14
  isRetrying: input.isRetrying,
14
- model: { provider: model.provider, id: model.id },
15
+ model: modelRefOf(model),
15
16
  thinkingLevel: input.thinkingLevel,
16
17
  permissionMode: input.permissionMode,
17
18
  sessionId: manager.id,
@@ -28,17 +28,24 @@ export function persistMessage(core, message) {
28
28
  export function recordModelState(core, model, thinkingLevel) {
29
29
  let provider;
30
30
  let modelId;
31
+ let channel;
31
32
  let thinking;
32
33
  for (const entry of core.manager.branch()) {
33
34
  if (entry.type === "model_change") {
34
35
  provider = entry.provider;
35
36
  modelId = entry.modelId;
37
+ channel = entry.channel;
36
38
  }
37
39
  else if (entry.type === "thinking_level_change")
38
40
  thinking = entry.thinkingLevel;
39
41
  }
40
- if (provider !== model.provider || modelId !== model.id) {
41
- core.appendEntry({ type: "model_change", provider: model.provider, modelId: model.id });
42
+ if (provider !== model.provider || modelId !== model.id || channel !== model.channel) {
43
+ core.appendEntry({
44
+ type: "model_change",
45
+ provider: model.provider,
46
+ modelId: model.id,
47
+ ...(model.channel !== undefined ? { channel: model.channel } : {}),
48
+ });
42
49
  }
43
50
  if (thinking !== thinkingLevel) {
44
51
  core.appendEntry({ type: "thinking_level_change", thinkingLevel });
@@ -13,6 +13,7 @@
13
13
  import { randomUUID } from "node:crypto";
14
14
  import { DEFAULT_APPROVAL_TIMEOUT_MS } from "../permissions/broker.js";
15
15
  import { previewAction } from "../permissions/preview.js";
16
+ import { classifierRequest } from "./session-classifier.js";
16
17
  import { runSingleToolCall } from "./tool-runner.js";
17
18
  /** 外层是这个工具时,嵌套调用的 Hook 输入带 `viaCodemode: true`。 */
18
19
  const CODEMODE_TOOL_NAME = "codemode";
@@ -36,6 +37,8 @@ export async function requestApproval(core, request, signal) {
36
37
  event.hookReason = request.hookReason;
37
38
  if (request.preview !== undefined)
38
39
  event.preview = request.preview;
40
+ if (request.autoDecision !== undefined)
41
+ event.autoDecision = request.autoDecision;
39
42
  core.emit(event);
40
43
  void core.runHook("Notification", {
41
44
  notification: { kind: "approval", message: `approval requested for ${request.toolName}` },
@@ -117,6 +120,7 @@ export async function gateToolCall(core, call, ctx) {
117
120
  let decision = hookDecision === "ask" ? "ask" : "allow";
118
121
  let approvalReason = "hook";
119
122
  let denyMessage;
123
+ let autoDecision;
120
124
  if (permission !== undefined) {
121
125
  const checkInput = {
122
126
  toolName: call.name,
@@ -132,12 +136,27 @@ export async function gateToolCall(core, call, ctx) {
132
136
  decision = verdict.decision;
133
137
  approvalReason = verdict.approvalReason ?? "mode";
134
138
  denyMessage = verdict.message;
139
+ autoDecision = verdict.auto;
140
+ if (verdict.classify === true) {
141
+ // auto 第 3 层:规则层与静态判定都没决定,问一次独立的模型分类器(只能变 allow)
142
+ const judged = await core
143
+ .autoClassifier()
144
+ .classify(classifierRequest(core, call.name, input, permission.projectRoot), ctx.signal);
145
+ autoDecision = { layer: "classifier", decision: judged.decision, reason: judged.reason };
146
+ if (judged.cached)
147
+ autoDecision.cached = true;
148
+ if (judged.decision === "allow")
149
+ decision = "allow";
150
+ }
151
+ if (autoDecision !== undefined)
152
+ permission.recordAutoDecision?.(call.name, input, autoDecision);
135
153
  }
136
154
  else if (decision === "ask" && core.options.unattended === true) {
137
155
  decision = "deny";
138
156
  }
157
+ const withAuto = (gate) => autoDecision === undefined ? gate : { ...gate, autoDecision };
139
158
  if (decision === "deny") {
140
- return { block: true, reason: denyMessage ?? `Permission denied for ${call.name}` };
159
+ return withAuto({ block: true, reason: denyMessage ?? `Permission denied for ${call.name}` });
141
160
  }
142
161
  if (decision === "ask") {
143
162
  const request = {
@@ -149,16 +168,18 @@ export async function gateToolCall(core, call, ctx) {
149
168
  };
150
169
  if (hookReason !== undefined)
151
170
  request.hookReason = hookReason;
171
+ if (autoDecision !== undefined)
172
+ request.autoDecision = autoDecision;
152
173
  const preview = safePreview(core, request);
153
174
  if (preview !== undefined)
154
175
  request.preview = preview;
155
176
  const answer = await requestApproval(core, request, ctx.signal);
156
177
  if (answer === "deny")
157
- return { block: true, reason: `The user denied ${call.name}` };
178
+ return withAuto({ block: true, reason: `The user denied ${call.name}` });
158
179
  if (answer === "allow_session")
159
180
  permission?.rememberForSession(call.name, input);
160
181
  }
161
- return replaced ? { input } : {};
182
+ return withAuto(replaced ? { input } : {});
162
183
  }
163
184
  export async function afterToolCall(core, call, result, parent) {
164
185
  if (core.options.hooks?.has("PostToolUse", call.name) !== true)
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import type { Model, ModelThinkingLevel } from "../ai/types.js";
14
14
  import type { HookEvent, HookEventPayload, HookOutcome } from "../hooks/types.js";
15
+ import type { PermissionClassifier } from "../permissions/classifier.js";
15
16
  import type { PermissionMode } from "../permissions/types.js";
16
17
  import type { SessionManager } from "../session/manager.js";
17
18
  import type { AgentMessage, BranchSummaryEntry, SessionEntry, SessionEntryInput } from "../session/types.js";
@@ -47,6 +48,7 @@ export declare class AgentSessionImpl implements AgentSession, SessionCore {
47
48
  private systemInput;
48
49
  private readonly subagentPool;
49
50
  private disposed;
51
+ private classifier;
50
52
  /** 已入队、尚未投递的消息(投递时发 queue_update)。 */
51
53
  private readonly queuedMessages;
52
54
  constructor(options: AgentSessionOptions);
@@ -113,5 +115,6 @@ export declare class AgentSessionImpl implements AgentSession, SessionCore {
113
115
  getStats(): SessionStats;
114
116
  dispose(): Promise<void>;
115
117
  childBase(): Pick<AgentSessionOptions, "model" | "thinkingLevel" | "activeTools" | "system">;
118
+ autoClassifier(): PermissionClassifier;
116
119
  spawnSubagent(request: SubagentRequest): Promise<SubagentResult>;
117
120
  }
@@ -10,8 +10,10 @@
10
10
  * - `steer()` / `followUp()`:运行中入队(返回 "queued");空闲时直接以该消息开始一个周期("handled")。
11
11
  * - `abort()`:中断供应商流、工具、重试等待与压缩;回到 idle 后 resolve;不清队列。
12
12
  */
13
+ import { modelRefOf } from "../ai/providers/channels.js";
13
14
  import { join } from "node:path";
14
15
  import { AmaError } from "../errors.js";
16
+ import { createSessionClassifier } from "./session-classifier.js";
15
17
  import { buildProjection } from "../session/projection.js";
16
18
  import { Agent } from "./agent.js";
17
19
  import { queuedText } from "./queue.js";
@@ -52,6 +54,7 @@ export class AgentSessionImpl {
52
54
  systemInput;
53
55
  subagentPool;
54
56
  disposed = false;
57
+ classifier;
55
58
  /** 已入队、尚未投递的消息(投递时发 queue_update)。 */
56
59
  queuedMessages = new WeakSet();
57
60
  constructor(options) {
@@ -102,9 +105,12 @@ export class AgentSessionImpl {
102
105
  getThinkingLevel: () => this.currentThinking,
103
106
  streamOptions: async () => {
104
107
  const apiKey = await this.resolveApiKey();
105
- return apiKey === undefined
106
- ? { sessionId: this.manager.id }
107
- : { apiKey, sessionId: this.manager.id };
108
+ const idle = options.idleTimeoutMs;
109
+ return {
110
+ sessionId: this.manager.id,
111
+ ...(apiKey === undefined ? {} : { apiKey }),
112
+ ...(idle === undefined ? {} : { idleTimeoutMs: idle }),
113
+ };
108
114
  },
109
115
  beforeRequest: async () => {
110
116
  this.syncSystem();
@@ -141,7 +147,8 @@ export class AgentSessionImpl {
141
147
  }
142
148
  async resolveApiKey() {
143
149
  try {
144
- return (await this.options.providers.resolveApiKey(this.currentModel.provider)).apiKey;
150
+ const { provider, channel } = this.currentModel;
151
+ return (await this.options.providers.resolveApiKey(provider, channel)).apiKey;
145
152
  }
146
153
  catch {
147
154
  return undefined;
@@ -358,15 +365,14 @@ export class AgentSessionImpl {
358
365
  }
359
366
  this.currentModel = lookup.model;
360
367
  this.compaction.refresh();
368
+ const next = modelRefOf(lookup.model);
361
369
  this.appendEntry({
362
370
  type: "model_change",
363
- provider: lookup.model.provider,
364
- modelId: lookup.model.id,
365
- });
366
- this.emit({
367
- type: "model_changed",
368
- model: { provider: lookup.model.provider, id: lookup.model.id },
371
+ provider: next.provider,
372
+ modelId: next.id,
373
+ ...(next.channel !== undefined ? { channel: next.channel } : {}),
369
374
  });
375
+ this.emit({ type: "model_changed", model: next });
370
376
  }
371
377
  setThinkingLevel(level) {
372
378
  this.currentThinking = level;
@@ -478,6 +484,10 @@ export class AgentSessionImpl {
478
484
  system: this.systemInput,
479
485
  };
480
486
  }
487
+ autoClassifier() {
488
+ this.classifier ??= createSessionClassifier(this);
489
+ return this.classifier;
490
+ }
481
491
  spawnSubagent(request) {
482
492
  return runSubagent(this, request, this.subagentPool, (options) => new AgentSessionImpl(options));
483
493
  }
@@ -25,6 +25,10 @@ export const DEFAULT_MAX_TOOL_RESULT_CHARS = 30_000;
25
25
  export const DEFAULT_ABORT_GRACE_MS = 3000;
26
26
  /** 嵌套调用结果的截断上限(给脚本,不进上下文)。 */
27
27
  export const NESTED_MAX_RESULT_CHARS = 1024 * 1024;
28
+ /** 门禁给出的 auto 判定,随 tool_execution_end 发出(按调用对象记,调用结束后随之回收)。 */
29
+ const autoDecisions = new WeakMap();
30
+ /** 门禁拒绝(gate.block)而没有执行的调用:tool_execution_end 带 `denied`。 */
31
+ const deniedCalls = new WeakSet();
28
32
  export function errorResult(text) {
29
33
  return { content: text, isError: true };
30
34
  }
@@ -80,9 +84,12 @@ async function prepare(call, assistant, options, signal, parent) {
80
84
  try {
81
85
  const gateContext = parent === undefined ? { signal, assistant, tool } : { signal, assistant, tool, parent };
82
86
  const gate = await options.beforeToolCall(call, gateContext);
87
+ if (gate.autoDecision !== undefined)
88
+ autoDecisions.set(call, gate.autoDecision);
83
89
  if (signal.aborted)
84
90
  return { kind: "immediate", call, result: errorResult(ABORTED_TOOL_TEXT) };
85
91
  if (gate.block === true) {
92
+ deniedCalls.add(call);
86
93
  return {
87
94
  kind: "immediate",
88
95
  call,
@@ -193,13 +200,19 @@ async function execute(prepared, options, signal, emit, parent) {
193
200
  return truncateResult(call, result, options, parent !== undefined);
194
201
  }
195
202
  async function emitEnd(call, result, emit) {
196
- await emit({
203
+ const event = {
197
204
  type: "tool_execution_end",
198
205
  toolCallId: call.id,
199
206
  toolName: call.name,
200
207
  result,
201
208
  isError: result.isError === true,
202
- });
209
+ };
210
+ const auto = autoDecisions.get(call);
211
+ if (auto !== undefined)
212
+ event.autoDecision = auto;
213
+ if (deniedCalls.has(call))
214
+ event.denied = true;
215
+ await emit(event);
203
216
  }
204
217
  async function emitResultMessages(finalized, emit) {
205
218
  const messages = [];
@@ -21,7 +21,7 @@
21
21
  import type { AssistantEvent, AssistantMessage, CacheRetention, ImageBlock, Message, MessageOrigin, Model, ModelRef, ModelThinkingLevel, ToolCallBlock, ToolResultMessage, TranscriptContext, Usage } from "../ai/types.js";
22
22
  import type { CacheMiss, CacheMissReason, CacheReporting, WarmerStatus, WarmingMode } from "../ai/cache/types.js";
23
23
  import type { HookEvent } from "../hooks/types.js";
24
- import type { ActionPreview, ApprovalDecision, ApprovalReason, PermissionMode } from "../permissions/types.js";
24
+ import type { ActionPreview, ApprovalDecision, ApprovalReason, AutoDecision, PermissionMode } from "../permissions/types.js";
25
25
  import type { AgentMessage, SessionEntry } from "../session/types.js";
26
26
  import type { ToolDefinition, ToolResult } from "../tools/types.js";
27
27
  export type { AssistantContentBlock, AssistantMessage, ContentBlock, ImageBlock, Message, StopReason, SystemMessage, TextBlock, ThinkingBlock, ToolCallBlock, ToolResultMessage, Usage, UserMessage, } from "../ai/types.js";
@@ -56,6 +56,8 @@ export interface ToolCallGate {
56
56
  reason?: string;
57
57
  /** Hook 的 updatedInput 替换后的输入。 */
58
58
  input?: unknown;
59
+ /** auto 权限模式的判定(随 tool_execution_end 发出)。 */
60
+ autoDecision?: AutoDecision;
59
61
  }
60
62
  export interface LoopHooks {
61
63
  /** 档一裁剪、Hook 的 additionalContext 注入。 */
@@ -175,6 +177,10 @@ export type SessionEvent = {
175
177
  result: ToolResult;
176
178
  isError: boolean;
177
179
  parentToolCallId?: string;
180
+ /** auto 权限模式下这次调用的判定(层、结论、原因);其它模式缺省。 */
181
+ autoDecision?: AutoDecision;
182
+ /** 被拒(权限管线、PreToolUse Hook、审批或无人值守)而没有执行;原因是 `result` 的文本。 */
183
+ denied?: true;
178
184
  } | {
179
185
  type: "queue_update";
180
186
  steering: string[];
@@ -210,6 +216,8 @@ export type SessionEvent = {
210
216
  timeoutMs: number;
211
217
  /** [W3-C0] 执行前预览(同 `ApprovalRequest.preview`)。 */
212
218
  preview?: ActionPreview;
219
+ /** auto 权限模式下为什么询问(同 `ApprovalRequest.autoDecision`)。 */
220
+ autoDecision?: AutoDecision;
213
221
  } | {
214
222
  type: "permission_resolved";
215
223
  requestId: string;
@@ -12,7 +12,7 @@
12
12
  * 任一出现过(含 0)即 `cacheReported: true`,之后的事件不会把它改回 false。
13
13
  */
14
14
  import { AssistantEventStreamImpl } from "../event-stream.js";
15
- import { USER_AGENT, authHeaders, describeErrorJson, mergeHeaders } from "../http.js";
15
+ import { authHeaders, describeErrorJson, idleTimeoutOf, mergeHeaders, USER_AGENT, } from "../http.js";
16
16
  import { readSseEvents } from "../sse.js";
17
17
  import { postWithCacheFallback } from "./cache-params.js";
18
18
  import { ANTHROPIC_VERSION, anthropicMessagesUrl, buildAnthropicRequest, detectAnthropicCompat, } from "./anthropic-request.js";
@@ -168,11 +168,12 @@ async function run(stream, model, context, options) {
168
168
  body,
169
169
  signal: options.signal,
170
170
  timeoutMs: options.timeoutMs,
171
+ idleTimeoutMs: idleTimeoutOf(options),
171
172
  onResponse: options.onResponse,
172
173
  });
173
174
  stream.push({ type: "start", partial: tracker.output });
174
175
  const state = { blocks: new Map(), stop: undefined, sawMessageStop: false };
175
- for await (const sse of readSseEvents(response.body, options.signal)) {
176
+ for await (const sse of readSseEvents(response.body, options.signal, idleTimeoutOf(options))) {
176
177
  if (sse.event === "ping" || sse.data === "")
177
178
  continue;
178
179
  let data;
@@ -16,7 +16,7 @@
16
16
  * - 错误体 `{error:{code,status,message}}` 重排为 `429 RESOURCE_EXHAUSTED: …`。
17
17
  */
18
18
  import { AssistantEventStreamImpl } from "../event-stream.js";
19
- import { HttpError, USER_AGENT, authHeaders, joinUrl, mergeHeaders, postJson } from "../http.js";
19
+ import { authHeaders, HttpError, idleTimeoutOf, joinUrl, mergeHeaders, postJson, USER_AGENT, } from "../http.js";
20
20
  import { readSseEvents } from "../sse.js";
21
21
  import { GOOGLE_BASE_URL, buildGoogleRequest, detectGoogleCompat } from "./google-request.js";
22
22
  import { BlockTracker, ProviderStopError, STREAM_ENDED_MESSAGE, createOutput, finishDone, finishError, requireApiKey, } from "./shared.js";
@@ -196,6 +196,7 @@ async function run(stream, model, context, options) {
196
196
  body: replaced === undefined ? request.body : replaced,
197
197
  signal: options.signal,
198
198
  timeoutMs: options.timeoutMs,
199
+ idleTimeoutMs: idleTimeoutOf(options),
199
200
  onResponse: options.onResponse,
200
201
  });
201
202
  }
@@ -209,7 +210,7 @@ async function run(stream, model, context, options) {
209
210
  idSeed: Date.now().toString(36),
210
211
  };
211
212
  const body = response.body;
212
- for await (const sse of readSseEvents(body, options.signal)) {
213
+ for await (const sse of readSseEvents(body, options.signal, idleTimeoutOf(options))) {
213
214
  const data = sse.data.trim();
214
215
  if (data === "" || data === "[DONE]")
215
216
  continue;
@@ -14,7 +14,7 @@
14
14
  * 内容推断。
15
15
  */
16
16
  import { AssistantEventStreamImpl } from "../event-stream.js";
17
- import { USER_AGENT, authHeaders, describeErrorJson, joinUrl, mergeHeaders } from "../http.js";
17
+ import { authHeaders, describeErrorJson, idleTimeoutOf, joinUrl, mergeHeaders, USER_AGENT, } from "../http.js";
18
18
  import { readSseEvents } from "../sse.js";
19
19
  import { affinityHeaders, postWithCacheFallback, resolveCacheRetention } from "./cache-params.js";
20
20
  import { detectCompat } from "./openai-compat.js";
@@ -190,6 +190,7 @@ async function run(stream, model, context, options) {
190
190
  body: replaced === undefined ? request.body : replaced,
191
191
  signal: options.signal,
192
192
  timeoutMs: options.timeoutMs,
193
+ idleTimeoutMs: idleTimeoutOf(options),
193
194
  onResponse: options.onResponse,
194
195
  });
195
196
  stream.push({ type: "start", partial: tracker.output });
@@ -203,7 +204,7 @@ async function run(stream, model, context, options) {
203
204
  sawDone: false,
204
205
  };
205
206
  const body = response.body;
206
- for await (const sse of readSseEvents(body, options.signal)) {
207
+ for await (const sse of readSseEvents(body, options.signal, idleTimeoutOf(options))) {
207
208
  const data = sse.data.trim();
208
209
  if (data === "")
209
210
  continue;
@@ -15,7 +15,7 @@
15
15
  * `cacheReported: true`。
16
16
  */
17
17
  import { AssistantEventStreamImpl } from "../event-stream.js";
18
- import { USER_AGENT, authHeaders, describeErrorJson, joinUrl, mergeHeaders } from "../http.js";
18
+ import { authHeaders, describeErrorJson, idleTimeoutOf, joinUrl, mergeHeaders, USER_AGENT, } from "../http.js";
19
19
  import { readSseEvents } from "../sse.js";
20
20
  import { affinityHeaders, postWithCacheFallback, resolveCacheRetention } from "./cache-params.js";
21
21
  import { buildResponsesRequest, detectResponsesCompat, encodeMessageSignature, } from "./openai-responses-request.js";
@@ -50,7 +50,8 @@ export function parseResponsesUsage(raw) {
50
50
  }
51
51
  export function mapResponsesStatus(status, incomplete) {
52
52
  if (status === "incomplete") {
53
- if (incomplete === "max_output_tokens")
53
+ // 中转转发的 DeepSeek 等上游写 `length`(实测 2026-10-02),与 `max_output_tokens` 同义。
54
+ if (incomplete === "max_output_tokens" || incomplete === "length")
54
55
  return { reason: "length" };
55
56
  return {
56
57
  reason: "error",
@@ -248,6 +249,7 @@ async function run(stream, model, context, options) {
248
249
  body: replaced === undefined ? request.body : replaced,
249
250
  signal: options.signal,
250
251
  timeoutMs: options.timeoutMs,
252
+ idleTimeoutMs: idleTimeoutOf(options),
251
253
  onResponse: options.onResponse,
252
254
  });
253
255
  stream.push({ type: "start", partial: tracker.output });
@@ -259,7 +261,7 @@ async function run(stream, model, context, options) {
259
261
  };
260
262
  let finished = false;
261
263
  const body = response.body;
262
- for await (const sse of readSseEvents(body, options.signal)) {
264
+ for await (const sse of readSseEvents(body, options.signal, idleTimeoutOf(options))) {
263
265
  const raw = sse.data.trim();
264
266
  if (raw === "" || raw === "[DONE]")
265
267
  continue;
@@ -11,7 +11,7 @@ import type { Model, ModelRef, TranscriptContext } from "../types.js";
11
11
  import type { PrefixFingerprint } from "./types.js";
12
12
  /** sha256 的前 16 位 hex(64 bit)。 */
13
13
  export declare function hash16(text: string): string;
14
- export declare function modelKey(model: Pick<Model, "provider" | "id"> | ModelRef): string;
14
+ export declare function modelKey(model: Pick<Model, "provider" | "id" | "channel"> | ModelRef): string;
15
15
  export declare function fingerprintContext(context: TranscriptContext, model: Pick<Model, "provider" | "id">): PrefixFingerprint;
16
16
  /** 两个指纹的差异(按归因顺序:system → tools → model);相同返回 undefined。 */
17
17
  export declare function fingerprintChange(prev: PrefixFingerprint, cur: PrefixFingerprint): "system" | "tools" | "model" | undefined;
@@ -14,7 +14,7 @@ export function hash16(text) {
14
14
  return createHash("sha256").update(text).digest("hex").slice(0, 16);
15
15
  }
16
16
  export function modelKey(model) {
17
- return `${model.provider}/${model.id}`;
17
+ return `${model.provider}/${model.id}${model.channel !== undefined ? `@${model.channel}` : ""}`;
18
18
  }
19
19
  function sortedTools(tools) {
20
20
  return [...tools].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
@@ -41,7 +41,8 @@ function hostOf(baseUrl) {
41
41
  }
42
42
  }
43
43
  export function endpointKey(record) {
44
- return `${record.model.provider}|${hostOf(record.baseUrl)}|${record.model.id}`;
44
+ const channel = record.model.channel !== undefined ? `@${record.model.channel}` : "";
45
+ return `${record.model.provider}|${hostOf(record.baseUrl)}|${record.model.id}${channel}`;
45
46
  }
46
47
  function sameFingerprint(a, b) {
47
48
  return a.system === b.system && a.tools === b.tools && a.model === b.model;
package/dist/ai/http.d.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * fetch 包装(设计 §1.2 ai/http.ts):超时、头合并(值为 null 删除)、错误体读取。
3
3
  *
4
- * 代理:Node 的全局 fetch 缺省不读 HTTP(S)_PROXY。Node ≥ 24 设 `NODE_USE_ENV_PROXY=1`
5
- * 即可让 fetch 走环境变量代理;Node 22 需在启动参数里加 `--use-env-proxy`(22.21+)。
6
- * 本模块不自行实现代理,env 原样透传给运行时处理。
4
+ * 代理:Node 的全局 fetch 缺省不读 HTTP(S)_PROXY。CLI 启动时由 cli/proxy.ts 调 Node 内置的
5
+ * `http.setGlobalProxyFromEnv()`(等价于 `NODE_USE_ENV_PROXY=1`);SDK 嵌入方自行决定。
6
+ * 本模块不自行实现代理。
7
+ *
8
+ * 超时:`timeoutMs` 只管到拿到响应头为止;`idleTimeoutMs`(缺省 300 s,0 关闭)既管等响应头,
9
+ * 也管流式读取期间两次收到数据之间的间隔(每收到一块字节即重新计时,见 sse.ts)。空闲超时抛
10
+ * `IdleTimeoutError`,文案含 `idle timeout`,会话层按可重试错误处理。
7
11
  *
8
12
  * 协议层自身**不重试**(重试在会话层,§3.6)。
9
13
  */
@@ -29,6 +33,21 @@ export declare class HttpError extends Error {
29
33
  export declare class RequestTimeoutError extends Error {
30
34
  constructor(timeoutMs: number);
31
35
  }
36
+ /** 流空闲超时缺省值:与 `request.idleTimeoutMs` / `AMA_IDLE_TIMEOUT_MS` 的缺省一致。 */
37
+ export declare const DEFAULT_IDLE_TIMEOUT_MS = 300000;
38
+ /** 请求选项里的空闲超时;未给出用缺省值,0 或负数表示关闭(返回 undefined)。 */
39
+ export declare function idleTimeoutOf(options: {
40
+ idleTimeoutMs?: number | undefined;
41
+ }): number | undefined;
42
+ /**
43
+ * 服务端在 `idleTimeoutMs` 内没有任何字节:`response` = 发出请求后迟迟没有响应头,
44
+ * `stream` = 流读到一半停住。文案含 `idle timeout`(重试分类据此判为可重试)。
45
+ */
46
+ export declare class IdleTimeoutError extends Error {
47
+ readonly idleTimeoutMs: number;
48
+ readonly phase: "response" | "stream";
49
+ constructor(idleTimeoutMs: number, phase: "response" | "stream");
50
+ }
32
51
  /**
33
52
  * 从错误体提取文本:识别 `{error:{type,message,code}}`、`{error:"..."}`、`{message}`、
34
53
  * `{detail}` 等常见形状;不是 JSON 则截断原文。结果形如 `429 rate_limit_error: ...`。
@@ -41,12 +60,15 @@ export interface PostOptions {
41
60
  body: unknown;
42
61
  signal: AbortSignal;
43
62
  timeoutMs?: number | undefined;
63
+ /** 等响应头的空闲上限(`timeoutMs` 未给时生效);undefined 不限。 */
64
+ idleTimeoutMs?: number | undefined;
44
65
  onResponse?: ((status: number, headers: Headers) => void) | undefined;
45
66
  }
46
67
  /**
47
- * POST JSON 并返回 2xx 响应(body 是 SSE 字节流)。非 2xx → HttpError;超时 → RequestTimeoutError;
48
- * 调用方的 signal 中止 → 原样抛 AbortError(调用方据 signal.aborted 判 aborted)。
49
- * 超时只覆盖到拿到响应头为止,流式读取期间由调用方的 signal 控制。
68
+ * POST JSON 并返回 2xx 响应(body 是 SSE 字节流)。非 2xx → HttpError;超时 → RequestTimeoutError
69
+ * (`timeoutMs`)或 IdleTimeoutError(`idleTimeoutMs`);调用方的 signal 中止 → 原样抛 AbortError
70
+ * (调用方据 signal.aborted 判 aborted)。超时只覆盖到拿到响应头为止,流式读取期间的空闲由
71
+ * `readSseEvents` 的 `idleTimeoutMs` 控制。
50
72
  */
51
73
  export declare function postJson(url: string, options: PostOptions): Promise<Response>;
52
74
  /** undici 的 `fetch failed` 把真正原因藏在 cause 里;拼出来便于重试分类与排错。 */
package/dist/ai/http.js CHANGED
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * fetch 包装(设计 §1.2 ai/http.ts):超时、头合并(值为 null 删除)、错误体读取。
3
3
  *
4
- * 代理:Node 的全局 fetch 缺省不读 HTTP(S)_PROXY。Node ≥ 24 设 `NODE_USE_ENV_PROXY=1`
5
- * 即可让 fetch 走环境变量代理;Node 22 需在启动参数里加 `--use-env-proxy`(22.21+)。
6
- * 本模块不自行实现代理,env 原样透传给运行时处理。
4
+ * 代理:Node 的全局 fetch 缺省不读 HTTP(S)_PROXY。CLI 启动时由 cli/proxy.ts 调 Node 内置的
5
+ * `http.setGlobalProxyFromEnv()`(等价于 `NODE_USE_ENV_PROXY=1`);SDK 嵌入方自行决定。
6
+ * 本模块不自行实现代理。
7
+ *
8
+ * 超时:`timeoutMs` 只管到拿到响应头为止;`idleTimeoutMs`(缺省 300 s,0 关闭)既管等响应头,
9
+ * 也管流式读取期间两次收到数据之间的间隔(每收到一块字节即重新计时,见 sse.ts)。空闲超时抛
10
+ * `IdleTimeoutError`,文案含 `idle timeout`,会话层按可重试错误处理。
7
11
  *
8
12
  * 协议层自身**不重试**(重试在会话层,§3.6)。
9
13
  */
@@ -76,6 +80,33 @@ export class RequestTimeoutError extends Error {
76
80
  this.name = "RequestTimeoutError";
77
81
  }
78
82
  }
83
+ /** 流空闲超时缺省值:与 `request.idleTimeoutMs` / `AMA_IDLE_TIMEOUT_MS` 的缺省一致。 */
84
+ export const DEFAULT_IDLE_TIMEOUT_MS = 300_000;
85
+ /** 请求选项里的空闲超时;未给出用缺省值,0 或负数表示关闭(返回 undefined)。 */
86
+ export function idleTimeoutOf(options) {
87
+ const value = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
88
+ return Number.isFinite(value) && value > 0 ? value : undefined;
89
+ }
90
+ function formatDuration(ms) {
91
+ return ms >= 1000 && ms % 1000 === 0 ? `${ms / 1000} s` : `${ms} ms`;
92
+ }
93
+ /**
94
+ * 服务端在 `idleTimeoutMs` 内没有任何字节:`response` = 发出请求后迟迟没有响应头,
95
+ * `stream` = 流读到一半停住。文案含 `idle timeout`(重试分类据此判为可重试)。
96
+ */
97
+ export class IdleTimeoutError extends Error {
98
+ idleTimeoutMs;
99
+ phase;
100
+ constructor(idleTimeoutMs, phase) {
101
+ const what = phase === "response"
102
+ ? `No response from the server within ${formatDuration(idleTimeoutMs)}`
103
+ : `Stream stalled: no data from the server for ${formatDuration(idleTimeoutMs)}`;
104
+ super(`${what} (idle timeout; adjust with request.idleTimeoutMs or AMA_IDLE_TIMEOUT_MS)`);
105
+ this.name = "IdleTimeoutError";
106
+ this.idleTimeoutMs = idleTimeoutMs;
107
+ this.phase = phase;
108
+ }
109
+ }
79
110
  const MAX_ERROR_BODY = 4000;
80
111
  /**
81
112
  * 从错误体提取文本:识别 `{error:{type,message,code}}`、`{error:"..."}`、`{message}`、
@@ -141,12 +172,14 @@ function parseRetryAfter(headers) {
141
172
  return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
142
173
  }
143
174
  /**
144
- * POST JSON 并返回 2xx 响应(body 是 SSE 字节流)。非 2xx → HttpError;超时 → RequestTimeoutError;
145
- * 调用方的 signal 中止 → 原样抛 AbortError(调用方据 signal.aborted 判 aborted)。
146
- * 超时只覆盖到拿到响应头为止,流式读取期间由调用方的 signal 控制。
175
+ * POST JSON 并返回 2xx 响应(body 是 SSE 字节流)。非 2xx → HttpError;超时 → RequestTimeoutError
176
+ * (`timeoutMs`)或 IdleTimeoutError(`idleTimeoutMs`);调用方的 signal 中止 → 原样抛 AbortError
177
+ * (调用方据 signal.aborted 判 aborted)。超时只覆盖到拿到响应头为止,流式读取期间的空闲由
178
+ * `readSseEvents` 的 `idleTimeoutMs` 控制。
147
179
  */
148
180
  export async function postJson(url, options) {
149
- const timeout = options.timeoutMs;
181
+ const explicit = options.timeoutMs !== undefined && options.timeoutMs > 0;
182
+ const timeout = explicit ? options.timeoutMs : options.idleTimeoutMs;
150
183
  const timer = new AbortController();
151
184
  const handle = timeout !== undefined && timeout > 0 ? setTimeout(() => timer.abort(), timeout) : undefined;
152
185
  const signal = AbortSignal.any([options.signal, timer.signal]);
@@ -161,7 +194,7 @@ export async function postJson(url, options) {
161
194
  }
162
195
  catch (error) {
163
196
  if (timer.signal.aborted && !options.signal.aborted && timeout !== undefined) {
164
- throw new RequestTimeoutError(timeout);
197
+ throw explicit ? new RequestTimeoutError(timeout) : new IdleTimeoutError(timeout, "response");
165
198
  }
166
199
  throw normalizeNetworkError(error);
167
200
  }