@armadra/agent 0.3.0 → 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 (198) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +93 -44
  3. package/dist/agent/retry.d.ts +1 -1
  4. package/dist/agent/retry.js +2 -1
  5. package/dist/agent/session-cache.js +1 -1
  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-tools.js +24 -3
  10. package/dist/agent/session.d.ts +3 -0
  11. package/dist/agent/session.js +12 -3
  12. package/dist/agent/tool-runner.js +15 -2
  13. package/dist/agent/types.d.ts +9 -1
  14. package/dist/ai/apis/anthropic-messages.js +3 -2
  15. package/dist/ai/apis/google-generative-ai.js +3 -2
  16. package/dist/ai/apis/openai-completions.js +3 -2
  17. package/dist/ai/apis/openai-responses.js +3 -2
  18. package/dist/ai/http.d.ts +28 -6
  19. package/dist/ai/http.js +41 -8
  20. package/dist/ai/providers/registry.d.ts +8 -1
  21. package/dist/ai/providers/registry.js +30 -22
  22. package/dist/ai/providers/suggest.d.ts +18 -0
  23. package/dist/ai/providers/suggest.js +72 -0
  24. package/dist/ai/sse.d.ts +6 -2
  25. package/dist/ai/sse.js +22 -2
  26. package/dist/ai/types.d.ts +9 -2
  27. package/dist/bundle/ama.cjs +15279 -10151
  28. package/dist/cli/args.d.ts +19 -4
  29. package/dist/cli/args.js +83 -10
  30. package/dist/cli/bootstrap.js +27 -3
  31. package/dist/cli/codemode-notice.d.ts +20 -0
  32. package/dist/cli/codemode-notice.js +55 -0
  33. package/dist/cli/compose-session.d.ts +5 -0
  34. package/dist/cli/compose-session.js +32 -1
  35. package/dist/cli/compose-store.d.ts +1 -1
  36. package/dist/cli/compose-store.js +3 -1
  37. package/dist/cli/compose.d.ts +14 -4
  38. package/dist/cli/compose.js +29 -10
  39. package/dist/cli/default-model.d.ts +38 -1
  40. package/dist/cli/default-model.js +96 -9
  41. package/dist/cli/deps.d.ts +36 -0
  42. package/dist/cli/exit-codes.d.ts +2 -0
  43. package/dist/cli/exit-codes.js +3 -0
  44. package/dist/cli/fake-visibility.d.ts +13 -0
  45. package/dist/cli/fake-visibility.js +26 -0
  46. package/dist/cli/from-prompt.d.ts +18 -0
  47. package/dist/cli/from-prompt.js +49 -0
  48. package/dist/cli/main.d.ts +9 -2
  49. package/dist/cli/main.js +77 -3
  50. package/dist/cli/proxy.d.ts +51 -0
  51. package/dist/cli/proxy.js +135 -0
  52. package/dist/cli/startup-screen.d.ts +29 -0
  53. package/dist/cli/startup-screen.js +47 -0
  54. package/dist/cli/startup-steps.d.ts +1 -1
  55. package/dist/cli/startup-steps.js +14 -12
  56. package/dist/cli/subcommands/config.d.ts +19 -3
  57. package/dist/cli/subcommands/config.js +108 -21
  58. package/dist/cli/subcommands/context.js +4 -1
  59. package/dist/cli/subcommands/doctor.js +12 -1
  60. package/dist/cli/subcommands/init.js +2 -1
  61. package/dist/cli/subcommands/models-discover.d.ts +15 -9
  62. package/dist/cli/subcommands/models-discover.js +53 -46
  63. package/dist/cli/subcommands/probe-runner.d.ts +96 -0
  64. package/dist/cli/subcommands/probe-runner.js +264 -0
  65. package/dist/cli/subcommands/providers-probe.d.ts +34 -0
  66. package/dist/cli/subcommands/providers-probe.js +87 -0
  67. package/dist/cli/subcommands/providers.d.ts +3 -2
  68. package/dist/cli/subcommands/providers.js +48 -45
  69. package/dist/cli/subcommands/sessions-export.d.ts +10 -0
  70. package/dist/cli/subcommands/sessions-export.js +59 -0
  71. package/dist/cli/subcommands/sessions-search.d.ts +13 -0
  72. package/dist/cli/subcommands/sessions-search.js +103 -0
  73. package/dist/cli/subcommands/sessions.d.ts +3 -2
  74. package/dist/cli/subcommands/sessions.js +22 -1
  75. package/dist/cli/subcommands/stats.d.ts +17 -0
  76. package/dist/cli/subcommands/stats.js +198 -0
  77. package/dist/cli/system-prompt-arg.d.ts +11 -0
  78. package/dist/cli/system-prompt-arg.js +34 -0
  79. package/dist/codemode/modes.d.ts +4 -11
  80. package/dist/codemode/modes.js +5 -27
  81. package/dist/codemode/tool.d.ts +18 -14
  82. package/dist/codemode/tool.js +71 -28
  83. package/dist/config/init.d.ts +6 -2
  84. package/dist/config/init.js +12 -5
  85. package/dist/config/json-schema.d.ts +1 -0
  86. package/dist/config/json-schema.js +36 -3
  87. package/dist/config/key-docs.d.ts +22 -0
  88. package/dist/config/key-docs.js +105 -0
  89. package/dist/config/merge.d.ts +9 -8
  90. package/dist/config/merge.js +22 -7
  91. package/dist/config/schema.d.ts +1 -1
  92. package/dist/config/schema.js +24 -5
  93. package/dist/config/types.d.ts +34 -6
  94. package/dist/config/types.js +17 -1
  95. package/dist/modes/commands-core.js +6 -5
  96. package/dist/modes/interactive/approval-dialog.d.ts +21 -5
  97. package/dist/modes/interactive/approval-dialog.js +106 -27
  98. package/dist/modes/interactive/commands.d.ts +11 -2
  99. package/dist/modes/interactive/commands.js +56 -16
  100. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  101. package/dist/modes/interactive/interactive-mode.js +65 -81
  102. package/dist/modes/interactive/key-dispatch.d.ts +3 -1
  103. package/dist/modes/interactive/key-dispatch.js +5 -6
  104. package/dist/modes/interactive/line/line-mode.d.ts +1 -0
  105. package/dist/modes/interactive/line/line-mode.js +6 -4
  106. package/dist/modes/interactive/line/line-render.d.ts +4 -0
  107. package/dist/modes/interactive/line/line-render.js +28 -6
  108. package/dist/modes/interactive/message-view.d.ts +48 -9
  109. package/dist/modes/interactive/message-view.js +238 -44
  110. package/dist/modes/interactive/panels.d.ts +18 -0
  111. package/dist/modes/interactive/panels.js +143 -0
  112. package/dist/modes/interactive/pickers.d.ts +23 -2
  113. package/dist/modes/interactive/pickers.js +48 -15
  114. package/dist/modes/interactive/run-indicator.d.ts +51 -0
  115. package/dist/modes/interactive/run-indicator.js +189 -0
  116. package/dist/modes/interactive/startup-header.d.ts +40 -0
  117. package/dist/modes/interactive/startup-header.js +169 -0
  118. package/dist/modes/interactive/status-bar.d.ts +18 -15
  119. package/dist/modes/interactive/status-bar.js +98 -56
  120. package/dist/modes/interactive/tool-summary.d.ts +46 -0
  121. package/dist/modes/interactive/tool-summary.js +218 -0
  122. package/dist/modes/interactive/tool-view.d.ts +48 -15
  123. package/dist/modes/interactive/tool-view.js +203 -145
  124. package/dist/modes/print/print-mode.d.ts +31 -4
  125. package/dist/modes/print/print-mode.js +116 -7
  126. package/dist/modes/rpc/commands.js +5 -0
  127. package/dist/permissions/auto-safe.d.ts +60 -0
  128. package/dist/permissions/auto-safe.js +529 -0
  129. package/dist/permissions/classifier.d.ts +64 -0
  130. package/dist/permissions/classifier.js +184 -0
  131. package/dist/permissions/dangerous.d.ts +5 -0
  132. package/dist/permissions/dangerous.js +1 -1
  133. package/dist/permissions/modes.d.ts +30 -0
  134. package/dist/permissions/modes.js +78 -0
  135. package/dist/permissions/pipeline.d.ts +31 -4
  136. package/dist/permissions/pipeline.js +196 -6
  137. package/dist/permissions/protected.d.ts +19 -0
  138. package/dist/permissions/protected.js +74 -0
  139. package/dist/permissions/rules.js +3 -0
  140. package/dist/permissions/types.d.ts +50 -3
  141. package/dist/permissions/types.js +3 -0
  142. package/dist/sdk.d.ts +9 -3
  143. package/dist/sdk.js +10 -2
  144. package/dist/session/export.d.ts +32 -0
  145. package/dist/session/export.js +187 -0
  146. package/dist/session/redact.d.ts +15 -0
  147. package/dist/session/redact.js +55 -0
  148. package/dist/session/reuse.d.ts +33 -0
  149. package/dist/session/reuse.js +86 -0
  150. package/dist/session/scan.d.ts +34 -0
  151. package/dist/session/scan.js +140 -0
  152. package/dist/session/search.d.ts +52 -0
  153. package/dist/session/search.js +211 -0
  154. package/dist/session/stats-aggregate.d.ts +63 -0
  155. package/dist/session/stats-aggregate.js +163 -0
  156. package/dist/session/stats-index.d.ts +26 -0
  157. package/dist/session/stats-index.js +91 -0
  158. package/dist/session/stats-scan.d.ts +54 -0
  159. package/dist/session/stats-scan.js +236 -0
  160. package/dist/tools/presets.d.ts +35 -8
  161. package/dist/tools/presets.js +56 -17
  162. package/dist/tui/component.d.ts +8 -2
  163. package/dist/tui/component.js +3 -1
  164. package/dist/tui/components/box.d.ts +6 -1
  165. package/dist/tui/components/box.js +16 -6
  166. package/dist/tui/components/card.d.ts +23 -0
  167. package/dist/tui/components/card.js +37 -0
  168. package/dist/tui/components/editor-history.d.ts +6 -0
  169. package/dist/tui/components/editor-history.js +45 -0
  170. package/dist/tui/components/editor-paste.d.ts +1 -1
  171. package/dist/tui/components/editor-paste.js +4 -4
  172. package/dist/tui/components/editor.d.ts +11 -5
  173. package/dist/tui/components/editor.js +52 -58
  174. package/dist/tui/components/key-value.d.ts +3 -0
  175. package/dist/tui/components/key-value.js +16 -6
  176. package/dist/tui/components/loader.d.ts +37 -7
  177. package/dist/tui/components/loader.js +84 -21
  178. package/dist/tui/components/markdown.d.ts +5 -1
  179. package/dist/tui/components/markdown.js +45 -21
  180. package/dist/tui/components/meter.d.ts +3 -3
  181. package/dist/tui/components/meter.js +13 -11
  182. package/dist/tui/components/select-list.d.ts +23 -1
  183. package/dist/tui/components/select-list.js +76 -13
  184. package/dist/tui/glyphs.d.ts +68 -0
  185. package/dist/tui/glyphs.js +114 -0
  186. package/dist/tui/theme.d.ts +30 -6
  187. package/dist/tui/theme.js +103 -17
  188. package/dist/tui.d.ts +4 -2
  189. package/dist/tui.js +3 -1
  190. package/docs/codemode.md +23 -9
  191. package/docs/hooks.md +10 -10
  192. package/docs/permissions.md +148 -0
  193. package/docs/providers.md +33 -10
  194. package/docs/rpc.md +36 -36
  195. package/docs/session-format.md +2 -1
  196. package/docs/sessions.md +134 -0
  197. package/docs/tui.md +139 -62
  198. package/package.json +3 -1
@@ -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
  }
@@ -13,6 +13,7 @@
13
13
  import { modelRefOf } from "../ai/providers/channels.js";
14
14
  import { join } from "node:path";
15
15
  import { AmaError } from "../errors.js";
16
+ import { createSessionClassifier } from "./session-classifier.js";
16
17
  import { buildProjection } from "../session/projection.js";
17
18
  import { Agent } from "./agent.js";
18
19
  import { queuedText } from "./queue.js";
@@ -53,6 +54,7 @@ export class AgentSessionImpl {
53
54
  systemInput;
54
55
  subagentPool;
55
56
  disposed = false;
57
+ classifier;
56
58
  /** 已入队、尚未投递的消息(投递时发 queue_update)。 */
57
59
  queuedMessages = new WeakSet();
58
60
  constructor(options) {
@@ -103,9 +105,12 @@ export class AgentSessionImpl {
103
105
  getThinkingLevel: () => this.currentThinking,
104
106
  streamOptions: async () => {
105
107
  const apiKey = await this.resolveApiKey();
106
- return apiKey === undefined
107
- ? { sessionId: this.manager.id }
108
- : { 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
+ };
109
114
  },
110
115
  beforeRequest: async () => {
111
116
  this.syncSystem();
@@ -479,6 +484,10 @@ export class AgentSessionImpl {
479
484
  system: this.systemInput,
480
485
  };
481
486
  }
487
+ autoClassifier() {
488
+ this.classifier ??= createSessionClassifier(this);
489
+ return this.classifier;
490
+ }
482
491
  spawnSubagent(request) {
483
492
  return runSubagent(this, request, this.subagentPool, (options) => new AgentSessionImpl(options));
484
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";
@@ -249,6 +249,7 @@ async function run(stream, model, context, options) {
249
249
  body: replaced === undefined ? request.body : replaced,
250
250
  signal: options.signal,
251
251
  timeoutMs: options.timeoutMs,
252
+ idleTimeoutMs: idleTimeoutOf(options),
252
253
  onResponse: options.onResponse,
253
254
  });
254
255
  stream.push({ type: "start", partial: tracker.output });
@@ -260,7 +261,7 @@ async function run(stream, model, context, options) {
260
261
  };
261
262
  let finished = false;
262
263
  const body = response.body;
263
- for await (const sse of readSseEvents(body, options.signal)) {
264
+ for await (const sse of readSseEvents(body, options.signal, idleTimeoutOf(options))) {
264
265
  const raw = sse.data.trim();
265
266
  if (raw === "" || raw === "[DONE]")
266
267
  continue;
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
  }
@@ -90,10 +90,17 @@ export declare class ProviderRegistry implements ProviderRegistryApi {
90
90
  /** 追加 / 替换模型(例如本地枚举得到的 ollama 模型)。 */
91
91
  addModels(providerId: string, models: readonly Model[], source?: ModelSource): void;
92
92
  private synthesize;
93
- /** 按渠道取模型:渠道不在模型的 channels 里 → 列出可用渠道。 */
93
+ /** 按渠道取模型:渠道不在模型的 channels 里 → channel_not_found,列出可用渠道。 */
94
94
  private withChannel;
95
+ /**
96
+ * `provider/model`、`provider/model@渠道` 或裸模型 id。`@` 先按渠道解析(id 里本来就带 `@` 且已登记的
97
+ * 模型除外),中转供应商的「未登记即合成」不会把 `@渠道` 吞进模型 id。失败的 reason:供应商不存在 →
98
+ * provider_not_found(候选是编辑距离最近的供应商);渠道不存在 → channel_not_found;模型不存在 →
99
+ * not_found(候选是最接近的模型)。
100
+ */
95
101
  findModel(ref: string): ModelLookup;
96
102
  private findPlain;
103
+ /** 最接近的模型(包含关系优先,其次编辑距离),`provider/id` 形式。 */
97
104
  private similar;
98
105
  hasConfiguredKey(providerId: string): boolean;
99
106
  resolveApiKey(providerId: string, channel?: string): Promise<ApiKeyResolution>;
@@ -30,6 +30,7 @@ import { BUILTIN_PROVIDERS, fallbackEnvKey, isRelayedBaseUrl } from "./builtin.j
30
30
  import { applyModelOverride, loadBuiltinCatalog, toModel, withCustomDefaults } from "./catalog.js";
31
31
  import { channelKeyId, modelChannels, parseChannels, splitChannelRef } from "./channels.js";
32
32
  import { catalogMetadata, enrichEntry, lazyIndex, } from "./enrich.js";
33
+ import { closest } from "./suggest.js";
33
34
  /**
34
35
  * 物化:模型补齐供应商级字段(不可变:返回新对象)。给了 `channel` 时协议与地址取渠道的(模型级
35
36
  * `explicit` 覆盖优先),headers / compat 按 供应商 ← 渠道 ← 模型 合并,并记下渠道名。
@@ -326,36 +327,40 @@ export class ProviderRegistry {
326
327
  const channelData = provider.channels?.find((c) => c.name === (channel ?? model.channels?.[0]));
327
328
  return materializeModel(model, provider, channelData);
328
329
  }
329
- /** 按渠道取模型:渠道不在模型的 channels 里 → 列出可用渠道。 */
330
+ /** 按渠道取模型:渠道不在模型的 channels 里 → channel_not_found,列出可用渠道。 */
330
331
  withChannel(provider, model, channel) {
331
332
  const available = model.channels ?? [];
332
- if (!available.includes(channel)) {
333
+ if (!available.includes(channel) || !provider.channels?.some((c) => c.name === channel)) {
333
334
  return {
334
335
  ok: false,
335
- reason: "not_found",
336
- candidates: available.map((c) => `${provider.id}/${model.id}@${c}`),
336
+ reason: "channel_not_found",
337
+ candidates: available.filter(Boolean).map((c) => `${provider.id}/${model.id}@${c}`),
337
338
  };
338
339
  }
339
340
  if (channel === model.channel)
340
341
  return { ok: true, model, provider };
341
342
  return { ok: true, model: this.materialize(provider, model, channel), provider };
342
343
  }
344
+ /**
345
+ * `provider/model`、`provider/model@渠道` 或裸模型 id。`@` 先按渠道解析(id 里本来就带 `@` 且已登记的
346
+ * 模型除外),中转供应商的「未登记即合成」不会把 `@渠道` 吞进模型 id。失败的 reason:供应商不存在 →
347
+ * provider_not_found(候选是编辑距离最近的供应商);渠道不存在 → channel_not_found;模型不存在 →
348
+ * not_found(候选是最接近的模型)。
349
+ */
343
350
  findModel(ref) {
344
351
  const trimmed = ref.trim();
345
- const direct = this.findPlain(trimmed);
346
- if (direct.ok)
347
- return direct;
348
352
  const split = splitChannelRef(trimmed);
349
353
  if (split === undefined)
350
- return direct;
354
+ return this.findPlain(trimmed);
355
+ const exact = this.findPlain(trimmed, undefined, false);
356
+ if (exact.ok)
357
+ return exact;
351
358
  const base = this.findPlain(split.base, split.channel);
352
359
  if (!base.ok)
353
- return base.reason === "ambiguous" ? base : direct;
354
- if (!base.provider.channels?.some((c) => c.name === split.channel))
355
- return direct;
360
+ return base;
356
361
  return this.withChannel(base.provider, base.model, split.channel);
357
362
  }
358
- findPlain(trimmed, channel) {
363
+ findPlain(trimmed, channel, synthesize = true) {
359
364
  const slash = trimmed.indexOf("/");
360
365
  if (slash > 0) {
361
366
  const provider = this.providers.get(trimmed.slice(0, slash));
@@ -364,7 +369,8 @@ export class ProviderRegistry {
364
369
  const model = provider.models.find((m) => m.id === id);
365
370
  if (model)
366
371
  return { ok: true, model, provider };
367
- if ((provider.models.length === 0 || this.relayed.has(provider.id)) && id.length > 0) {
372
+ const relayed = provider.models.length === 0 || this.relayed.has(provider.id);
373
+ if (synthesize && relayed && id.length > 0) {
368
374
  const synthesized = this.synthesize(provider, id);
369
375
  if (channel !== undefined && provider.channels?.some((c) => c.name === channel))
370
376
  synthesized.channels = [...new Set([...(synthesized.channels ?? []), channel])];
@@ -374,6 +380,14 @@ export class ProviderRegistry {
374
380
  }
375
381
  }
376
382
  const matches = this.listModels().filter((entry) => entry.model.id === trimmed);
383
+ if (matches.length === 0 && slash > 0) {
384
+ const ids = [...this.providers.keys()];
385
+ return {
386
+ ok: false,
387
+ reason: "provider_not_found",
388
+ candidates: closest(trimmed.slice(0, slash), ids),
389
+ };
390
+ }
377
391
  if (matches.length === 1 && matches[0]) {
378
392
  return { ok: true, model: matches[0].model, provider: matches[0].provider };
379
393
  }
@@ -390,16 +404,10 @@ export class ProviderRegistry {
390
404
  candidates: (configured.length > 0 ? configured : matches).map((entry) => `${entry.provider.id}/${entry.model.id}`),
391
405
  };
392
406
  }
407
+ /** 最接近的模型(包含关系优先,其次编辑距离),`provider/id` 形式。 */
393
408
  similar(id, providerId) {
394
- const needle = id.toLowerCase();
395
- return this.listModels()
396
- .filter((entry) => providerId === undefined || entry.provider.id === providerId)
397
- .filter((entry) => {
398
- const candidate = entry.model.id.toLowerCase();
399
- return needle.length > 0 && (candidate.includes(needle) || needle.includes(candidate));
400
- })
401
- .slice(0, 10)
402
- .map((entry) => `${entry.provider.id}/${entry.model.id}`);
409
+ const entries = this.listModels().filter((entry) => providerId === undefined || entry.provider.id === providerId);
410
+ return closest(id, entries, 5, (entry) => entry.model.id).map((entry) => `${entry.provider.id}/${entry.model.id}`);
403
411
  }
404
412
  hasConfiguredKey(providerId) {
405
413
  const provider = this.providers.get(providerId);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 模型 / 供应商 / 渠道写错时的候选与报错文案(W4-C)。
3
+ *
4
+ * 候选排序:包含关系(`sonnet` ⊂ `claude-sonnet-5`)优先,其次按编辑距离;距离超过
5
+ * `max(2, ⌈长度 / 3⌉)` 的不算接近。只比较 id 本身(不含 `provider/` 前缀),大小写不敏感。
6
+ */
7
+ import type { ModelLookup } from "../types.js";
8
+ /** Levenshtein 距离(插入 / 删除 / 替换各计 1)。 */
9
+ export declare function editDistance(a: string, b: string): number;
10
+ /**
11
+ * 从 `candidates` 里挑与 `needle` 最接近的至多 `limit` 个。`key` 取参与比较的部分
12
+ * (缺省整串),返回原值。
13
+ */
14
+ export declare function closest<T>(needle: string, candidates: readonly T[], limit?: number, key?: (item: T) => string): T[];
15
+ /** 解析失败的一行说明(不含前缀标签):`渠道不存在:x;该模型可用渠道:…` 等。 */
16
+ export declare function describeLookupFailure(ref: string, failure: Extract<ModelLookup, {
17
+ ok: false;
18
+ }>): string;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 模型 / 供应商 / 渠道写错时的候选与报错文案(W4-C)。
3
+ *
4
+ * 候选排序:包含关系(`sonnet` ⊂ `claude-sonnet-5`)优先,其次按编辑距离;距离超过
5
+ * `max(2, ⌈长度 / 3⌉)` 的不算接近。只比较 id 本身(不含 `provider/` 前缀),大小写不敏感。
6
+ */
7
+ /** Levenshtein 距离(插入 / 删除 / 替换各计 1)。 */
8
+ export function editDistance(a, b) {
9
+ if (a === b)
10
+ return 0;
11
+ if (a.length === 0)
12
+ return b.length;
13
+ if (b.length === 0)
14
+ return a.length;
15
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
16
+ for (let i = 1; i <= a.length; i++) {
17
+ const row = [i];
18
+ for (let j = 1; j <= b.length; j++) {
19
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
20
+ row[j] = Math.min((prev[j] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
21
+ }
22
+ prev = row;
23
+ }
24
+ return prev[b.length] ?? 0;
25
+ }
26
+ /**
27
+ * 从 `candidates` 里挑与 `needle` 最接近的至多 `limit` 个。`key` 取参与比较的部分
28
+ * (缺省整串),返回原值。
29
+ */
30
+ export function closest(needle, candidates, limit = 5, key = String) {
31
+ const target = needle.toLowerCase();
32
+ if (target === "")
33
+ return [];
34
+ const budget = Math.max(2, Math.ceil(target.length / 3));
35
+ const scored = [];
36
+ candidates.forEach((item, index) => {
37
+ const value = key(item).toLowerCase();
38
+ if (value === "")
39
+ return;
40
+ if (value.includes(target) || target.includes(value)) {
41
+ scored.push({ item, score: Math.abs(value.length - target.length) / 1000, index });
42
+ return;
43
+ }
44
+ const distance = editDistance(target, value);
45
+ if (distance <= budget)
46
+ scored.push({ item, score: distance, index });
47
+ });
48
+ return scored
49
+ .sort((a, b) => a.score - b.score || a.index - b.index)
50
+ .slice(0, limit)
51
+ .map((entry) => entry.item);
52
+ }
53
+ /** 解析失败的一行说明(不含前缀标签):`渠道不存在:x;该模型可用渠道:…` 等。 */
54
+ export function describeLookupFailure(ref, failure) {
55
+ const list = failure.candidates.slice(0, 20).join(", ");
56
+ switch (failure.reason) {
57
+ case "channel_not_found":
58
+ return failure.candidates.length > 0
59
+ ? `渠道不存在:${ref};该模型可用渠道:${list}`
60
+ : `渠道不存在:${ref};该模型没有可选渠道(去掉 @ 后缀)`;
61
+ case "provider_not_found":
62
+ return failure.candidates.length > 0
63
+ ? `供应商不存在:${ref};最接近的供应商:${list}`
64
+ : `供应商不存在:${ref}(ama providers list 查看已配置的供应商)`;
65
+ case "ambiguous":
66
+ return `模型名有歧义:${ref};候选:${list}`;
67
+ default:
68
+ return failure.candidates.length > 0
69
+ ? `模型不存在:${ref};最接近的模型:${list}`
70
+ : `模型不存在:${ref}(ama models list 查看可用模型)`;
71
+ }
72
+ }
package/dist/ai/sse.d.ts CHANGED
@@ -28,5 +28,9 @@ export declare class SseParser {
28
28
  private processLine;
29
29
  private dispatch;
30
30
  }
31
- /** 从字节流读 SSE 事件;`signal` 中止时停止读取(调用方据此产出 aborted)。 */
32
- export declare function readSseEvents(body: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncGenerator<SseEvent>;
31
+ /**
32
+ * 从字节流读 SSE 事件;`signal` 中止时停止读取(调用方据此产出 aborted)。
33
+ * `idleTimeoutMs`:等待下一块字节的上限,每收到一块即重新计时(消费者处理事件的时间不计);
34
+ * 超时取消底层流并抛 `IdleTimeoutError`(phase `stream`)。
35
+ */
36
+ export declare function readSseEvents(body: ReadableStream<Uint8Array>, signal?: AbortSignal, idleTimeoutMs?: number): AsyncGenerator<SseEvent>;