fix(core,docs): window-derived output caps and compaction threshold, slower retry ladder, retry-all-but-auth (#235)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-08-07 19:14:26 +08:00
committed by GitHub
parent 169c7d5006
commit 864261eade
20 changed files with 846 additions and 251 deletions
+2 -2
View File
@@ -103,7 +103,7 @@ Goal mode is the exception because its objective is re-injected as text every ro
## Automatic reconnect
Every LLM-side failure except `auth` triggers an in-run reconnect — `timeout` (network timeouts, transport disconnects, rate limits, 5xx, transient provider quota errors), `malformed` (truncated streams, JSON parse failures), and **`failed` as well**. `failed` retries even though the classifier judged it non-transient: that judgement is an allowlist of known codes, statuses and message vocabulary, so a gateway phrasing a transient fault its own way (`Upstream HTTP/2 stream failed`, say) lands there and used to kill the turn. Retrying a genuinely permanent error costs the ladder and ends the same way; aborting a transient one destroys the turn. Note this changes the *policy*, not the *taxonomy*: a `failed` request is still recorded as `failed` on its `request_end` and in the Cost center, rather than being relabelled a timeout. On a reconnect the engine re-sends the original input plus a `[turn_retried]` block carrying the previous partial output, so tools are never re-executed. Default limit is 5 reconnects with exponential backoff under a ceiling (base 250ms, cap 30s: 250ms, 500ms, 1s, 2s, 4s ≈ 7.75s of total patience — one shared schedule growing toward the slower retryable classes, with the first steps as fast as a transport blip needs); beyond that the turn settles as `failed`. Each failure's `request_end` announces the planned wait as `retry_in_ms` (same formula as the sleep) and stamps `attempt`, the authoritative 1-based ordinal of the request within its retry run (the CLI and Web App display it verbatim); the Web App renders the wait as a live countdown with "retry now" (skips the remaining wait via `Session.skipReconnectWait` — the attempt counter is unchanged) and "give up" (the ordinary abort; the engine's abort-during-backoff path ends the turn) controls; the CLI prints its own `[retry]` line. All three retryable statuses render identically — a retry the user cannot see is a stalled session with no explanation and no way out. A compaction request is an ordinary LLM request and by default retries on the same cap and ladder (an unusable summary draws on the same budget — see "Context compaction"); a compaction that gives up keeps the original context and tries again at the next trigger. Authentication errors are classified before any retry heuristic and never retry: the request ends with its own terminal status `auth` (only the model reference is fixed at Session creation — credentials are read from the current Project config when the Session loads), and the Web App disables that Session's composer until the model's credential is updated (which auto-unlocks it) or the notice is dismissed for a retry. Tool errors are never retried — they are fed back to the model as `tool_call_output` and the model decides what to do next.
Every LLM-side failure except `auth` triggers an in-run reconnect — `timeout` (transport-shaped errors: network timeouts, transport disconnects, rate limits, 5xx), `malformed` (truncated streams, JSON parse failures), and **`failed` as well** — every provider rejection that isn't an explicit credential failure, bare 403s and quota/subscription errors included. The statuses are taxonomy, not policy: the classifier only picks the label, and a gateway phrasing a transient fault its own way (`Upstream HTTP/2 stream failed`, say) or a quota that refills mid-ladder retries exactly like a network drop. Retrying a genuinely permanent error costs the ladder and ends the same way; aborting a transient one destroys the turn. Note this changes the *policy*, not the *taxonomy*: a `failed` request is still recorded as `failed` on its `request_end` and in the Cost center, rather than being relabelled a timeout. On a reconnect the engine re-sends the original input plus a `[turn_retried]` block carrying the previous partial output, so tools are never re-executed. Default limit is 5 reconnects with exponential backoff under a ceiling (base 2s, cap 30s: 2s, 4s, 8s, 16s, 30s ≈ 60s of total patience — one shared schedule for every retryable class, sized so transient provider failures such as restarts and rate limits get a real recovery window instead of five retries burning out in about a second, and so every planned wait clears the Web App's 2s countdown floor and stays visible); beyond that the turn settles as `failed`. Each failure's `request_end` announces the planned wait as `retry_in_ms` (same formula as the sleep) and stamps `attempt`, the authoritative 1-based ordinal of the request within its retry run (the CLI and Web App display it verbatim); the Web App renders the wait as a live countdown with "retry now" (skips the remaining wait via `Session.skipReconnectWait` — the attempt counter is unchanged) and "give up" (the ordinary abort; the engine's abort-during-backoff path ends the turn) controls; the CLI prints its own `[retry]` line. All three retryable statuses render identically — a retry the user cannot see is a stalled session with no explanation and no way out. A compaction request is an ordinary LLM request and by default retries on the same cap and ladder (an unusable summary draws on the same budget — see "Context compaction"); a compaction that gives up keeps the original context and tries again at the next trigger. Authentication errors are classified before any retry heuristic and never retry: the request ends with its own terminal status `auth` (only the model reference is fixed at Session creation — credentials are read from the current Project config when the Session loads), and the Web App disables that Session's composer until the model's credential is updated (which auto-unlocks it) or the notice is dismissed for a retry. Tool errors are never retried — they are fed back to the model as `tool_call_output` and the model decides what to do next.
## Compaction
@@ -122,7 +122,7 @@ Three triggers (`compaction_begin.reason`):
| reason | Condition |
| --- | --- |
| `context` | last turn's `token_usage.request.total` ≥ `maxContextLength` (default 128000) |
| `context` | last turn's `token_usage.request.total` ≥ `maxContextLength` (default 128000; the effective threshold is capped at the model's `context_window` − 2048, so a small-window model — a 32k local vLLM, say — compacts at ~30.7k instead of overflowing the window first; an entry without `context_window` derives from the assumed 128000 default) |
| `turns` | Session turn count ≥ `maxSessionTurns` (default -1 = unlimited) |
| `manual` | the user runs `/compact` or calls `session.compact()` |
+2 -2
View File
@@ -100,7 +100,7 @@ Task 运行期间,宿主可通过 `session.steer(input)` 排队一条用户消
## 自动重连
除 `auth` 外,LLM 侧的所有失败都会触发引擎内自动重连——`timeout`(网络超时、传输层断连、限流、5xx、瞬时的供应商额度错误)、`malformed`(流截断、JSON 解析失败),**以及 `failed`**。`failed` 也重试,尽管分类器判定它不是瞬时错误:那个判定本质是一张允许清单(已知错误码、状态码与消息措辞),所以用自己说法描述瞬时故障的网关(例如 `Upstream HTTP/2 stream failed`)会落到这一档,此前会直接终止本轮。重试一个真正的永久错误,代价是走完退避梯度后以同样的方式收场;而把瞬时错误直接中断,则毁掉这一轮。注意改的是**策略**而非**分类**:`failed` 请求在 `request_end` 与成本中心里仍然记为 `failed`,不会被改标成超时。重连时同一次 `run` 内重发原始输入,并附加 `[turn_retried]` 块携带上一次的部分输出,避免工具重复执行。默认最多重连 5 次,指数退避并设上限(基数 250ms、上限 30s:250ms、500ms、1s、2s、4s,总耐心约 7.75s——所有可重试类别共用一张时间表,向较慢的类别递增,头几步仍与传输层抖动所需的一样快);超限后该轮以 `failed` 收场。每次失败的 `request_end` 会以 `retry_in_ms` 宣告计划中的等待(与实际休眠同一公式)、以 `attempt` 标注这是本轮第几次尝试(权威序号,CLI 与 Web 的重试行直接显示它),Web App 据此实时倒计时,并提供「立即重试」(经 `Session.skipReconnectWait` 跳过剩余等待——重试计数不变)与「放弃」(普通中断;引擎的退避中中断路径结束本轮)两个内联按钮,CLI 则打印自己的 `[重试]` 行。三种可重试终态的渲染完全一致——用户看不见的重试,等于一次没有任何解释、也无从退出的卡顿。压缩请求是一次普通的 LLM 请求,默认沿用同一重连上限与退避阶梯(无效摘要也计入同一预算,见「上下文压缩」一节);压缩放弃后保留原上下文、等下一次触发再试。鉴权错误在任何重试启发式之前判定、从不重试:请求以专属终态 `auth` 收场(Session 锁定的只是模型引用,凭据在会话装载时取自当前 Project 配置),Web App 据此禁用该 Session 的输入框,直到该模型的凭据被更新(更新后自动解锁)或用户点击「重试」。工具错误从不重试——它们作为 `tool_call_output` 反馈给模型,由模型决定下一步。
除 `auth` 外,LLM 侧的所有失败都会触发引擎内自动重连——`timeout`(传输形态的错误:网络超时、传输层断连、限流、5xx)、`malformed`(流截断、JSON 解析失败),**以及 `failed`**——凡不是明确凭据错误的供应商拒绝都在此列,普通 403 与配额/订阅错误也一样重试。终态只是分类而非策略:分类器只挑标签,用自己说法描述瞬时故障的网关(例如 `Upstream HTTP/2 stream failed`)或阶梯中途恢复的配额,都与断网一样走满同一阶梯。重试一个真正的永久错误,代价是走完退避梯度后以同样的方式收场;而把瞬时错误直接中断,则毁掉这一轮。注意改的是**策略**而非**分类**:`failed` 请求在 `request_end` 与成本中心里仍然记为 `failed`,不会被改标成超时。重连时同一次 `run` 内重发原始输入,并附加 `[turn_retried]` 块携带上一次的部分输出,避免工具重复执行。默认最多重连 5 次,指数退避并设上限(基数 2s、上限 30s:2s、4s、8s、16s、30s,总耐心约 60s——所有可重试类别共用一张时间表,按较慢的类别定基数:供应商重启、限流这类瞬时故障需要以秒计的恢复时间,旧的 250ms 基数约 7.75s 就烧完整个阶梯;每次计划等待也都达到 Web App 2s 的倒计时下限,重试始终可见);超限后该轮以 `failed` 收场。每次失败的 `request_end` 会以 `retry_in_ms` 宣告计划中的等待(与实际休眠同一公式)、以 `attempt` 标注这是本轮第几次尝试(权威序号,CLI 与 Web 的重试行直接显示它),Web App 据此实时倒计时,并提供「立即重试」(经 `Session.skipReconnectWait` 跳过剩余等待——重试计数不变)与「放弃」(普通中断;引擎的退避中中断路径结束本轮)两个内联按钮,CLI 则打印自己的 `[重试]` 行。三种可重试终态的渲染完全一致——用户看不见的重试,等于一次没有任何解释、也无从退出的卡顿。压缩请求是一次普通的 LLM 请求,默认沿用同一重连上限与退避阶梯(无效摘要也计入同一预算,见「上下文压缩」一节);压缩放弃后保留原上下文、等下一次触发再试。鉴权错误在任何重试启发式之前判定、从不重试:请求以专属终态 `auth` 收场(Session 锁定的只是模型引用,凭据在会话装载时取自当前 Project 配置),Web App 据此禁用该 Session 的输入框,直到该模型的凭据被更新(更新后自动解锁)或用户点击「重试」。工具错误从不重试——它们作为 `tool_call_output` 反馈给模型,由模型决定下一步。
## 上下文压缩(Compaction)
@@ -119,7 +119,7 @@ interface CompactionSettings {
| reason | 触发条件 |
| --- | --- |
| `context` | 上一轮 `token_usage.request.total` ≥ `maxContextLength`(默认 128000) |
| `context` | 上一轮 `token_usage.request.total` ≥ `maxContextLength`(默认 128000;生效阈值不超过模型 `context_window` − 2048,小窗口模型——如 32k 的本地 vLLM——约在 30.7k 处压缩,而不是先撞上窗口硬限制;条目未配置 `context_window` 时按 128000 的假定默认值推导) |
| `turns` | Session 轮数 ≥ `maxSessionTurns`(默认 -1,即不限) |
| `manual` | 用户执行 `/compact` 或调用 `session.compact()` |
+2 -2
View File
@@ -99,10 +99,10 @@ Edit this file via the CLI (`penguin config model …`) or the Web Models page
| `version` | `1` | Agent State version (a natural number), incremented on each successful optimization |
| `system_prompt` | built-in template | Required; the only template with placeholder substitution |
| `max_turns` | `-1` | Maximum LLM turns per Task (`-1` = unlimited; a positive integer caps the Task) |
| `model.max_tokens` | `32000` | Output Token limit per Request (-1 = no cap, provider default) |
| `model.max_tokens` | `32000` | Output Token ceiling per Request (-1 = no cap, provider default); each request clamps the effective value to the model's `context_window` minus the estimated input, so a small-window model never gets asked for more than fits |
| `model.thinking_level` | `medium` | `none` / `low` / `medium` / `high` / `xhigh`; the session default, overridable per-Task |
| `model.timeoutMs` | `120000` | Per-Request timeout (milliseconds) |
| `compaction.max_context_length` | `128000` | Context Token threshold that triggers compaction |
| `compaction.max_context_length` | `128000` | Context Token threshold that triggers compaction; the effective threshold is capped at the model's `context_window` − 2048 so compaction fires before a small window overflows |
| `compaction.max_session_turns` | `-1` | Cumulative Session turn threshold (`-1` = unlimited) |
| `compaction.mode` | `summarize` | `summarize` / `discard` |
| `compaction.prompt` | built-in template | Prompt used for summarize compaction |
+2 -2
View File
@@ -99,10 +99,10 @@ output = 0.857143
| `version` | `1` | Agent State 版本号(自然数),每次成功优化自增 |
| `system_prompt` | 内置模板 | 必填;唯一进行占位符替换的模板 |
| `max_turns` | `-1` | 单个 Task 的最大 LLM 轮数(`-1` 不限制,正整数为上限) |
| `model.max_tokens` | `32000` | 单次输出 Token 上限(-1 不设上限,用服务商默认) |
| `model.max_tokens` | `32000` | 单次输出 Token 天花板(-1 不设上限,用服务商默认);每次请求会把实际值收敛到模型 `context_window` 减估算输入以内,小窗口模型不会被索要放不下的输出 |
| `model.thinking_level` | `medium` | `none` / `low` / `medium` / `high` / `xhigh`;作为会话默认档位,可被逐轮 Task 参数覆盖 |
| `model.timeoutMs` | `120000` | 单次 Request 超时(毫秒) |
| `compaction.max_context_length` | `128000` | 触发压缩的上下文 Token 阈值 |
| `compaction.max_context_length` | `128000` | 触发压缩的上下文 Token 阈值;生效阈值不超过模型 `context_window` − 2048,压缩在小窗口溢出之前触发 |
| `compaction.max_session_turns` | `-1` | Session 累计轮数阈值(`-1` 不限制) |
| `compaction.mode` | `summarize` | `summarize` / `discard` |
| `compaction.prompt` | 内置模板 | summarize 压缩使用的 Prompt |
+1 -1
View File
@@ -60,7 +60,7 @@ interface LLMOutcome {
| status | Meaning | Engine reaction |
| --- | --- | --- |
| `completed` | finished normally (token_usage already emitted) | proceed |
| `timeout` | timeout / transport disconnect / transient provider quota error | auto-reconnect within the run |
| `timeout` | timeout / transport disconnect | auto-reconnect within the run |
| `malformed` | response parse failure | auto-reconnect within the run |
| `failed` | an error the classifier did not judge transient (params, …) | auto-reconnect within the run as well — the status is still reported as `failed` |
| `aborted` | user interrupt | stop, hand back to the user |
+1 -1
View File
@@ -60,7 +60,7 @@ interface LLMOutcome {
| status | 含义 | 引擎的反应 |
| --- | --- | --- |
| `completed` | 正常完成(已产出 token_usage) | 继续下一步 |
| `timeout` | 超时/传输层断连/瞬时的供应商额度错误 | 同一 run 内自动重连 |
| `timeout` | 超时/传输层断连 | 同一 run 内自动重连 |
| `malformed` | 响应解析失败 | 同一 run 内自动重连 |
| `failed` | 分类器未判定为瞬时的错误(参数等) | 同样在同一 run 内自动重连——状态本身仍如实上报为 `failed` |
| `aborted` | 用户中断 | 停止交还用户 |
+9 -2
View File
@@ -21,8 +21,8 @@ Each Project's available models are recorded in the hidden `.project_config.toml
| --- | --- |
| `provider` | Config group name; paired with `model_id` it forms the unique key |
| `model_id` | Upstream request id |
| `context_window` | Context window |
| `max_tokens` | Optional per-model output cap (max output tokens per request). When set it overrides the Agent's `model.max_tokens`; unset inherits it. Lower it for small-context models: the per-Agent default (32000) cannot fit into e.g. a 32k window together with any prompt. Omitting the field on a Web full-table save clears it |
| `context_window` | Context window (tokens). Load-bearing, not just display: each request's effective output cap and the compaction threshold are derived from it, so requests never ask for more output than the window still fits. Unset (or implausibly small, under 4096): the output clamp turns off and compaction derives from an assumed 128000 — set the real value for models with smaller windows |
| `max_tokens` | Optional per-model output cap (max output tokens per request). When set it overrides the Agent's `model.max_tokens`; unset inherits it. The cap is a ceiling, not the literal wire value: each request sends `min(max_tokens, context_window − estimated input − safety margin)`, so small-window models work without hand-tuning it. Omitting the field on a Web full-table save clears it |
| `client_type` | Protocol hint (e.g. `openai`); inferred by AgentHub from the model id when omitted |
| `display_name` | Display name |
| `vision` | Whether image input is supported, default true |
@@ -77,6 +77,13 @@ The preset catalog also carries OpenRouter's free tier: `:free` model variants (
Some models in the preset catalog: deepseek-v4-pro / deepseek-v4-flash, gemini-3.1-pro-preview, claude-opus-4-8 / claude-sonnet-4-6, gpt-5.5, glm-5.2, kimi-k2.6, qwen3.8-max (not exhaustive).
## Local / self-hosted OpenAI-compatible endpoints (e.g. vLLM)
A local inference server is just a `custom` entry: `client_type = "openai"`, `base_url` pointing at the server (e.g. `http://127.0.0.1:8000/v1`), and the served model name as `model_id`. Two settings make it run smoothly:
- **Enable tool calling server-side.** For vLLM, start the server with `--enable-auto-tool-choice` and the `--tool-call-parser` matching your model (e.g. `hermes` for Qwen, `llama3_json` for Llama 3.x); without them tool calls arrive as plain text and the agent loop cannot execute anything.
- **Set the entry's `context_window` to the server's real window** — for vLLM, the `--max-model-len` value (e.g. `32768`). The per-request output cap and the compaction threshold both derive from this window automatically: requests clamp `max_tokens` to what the window still fits, and compaction fires before the window overflows, so no hand-tuned `max_tokens` is needed. Left unset, the per-request output clamp is off and compaction assumes a 128000 window, so a smaller real window will reject requests.
## Thinking levels
Five levels: `none | low | medium | high | xhigh`, configured per Agent as `model.thinking_level` in `system_config.yaml`, default medium. The Web pickers offer `low` and above only (many models cannot disable thinking; `none` stays a valid stored value and still displays). The chat draft view offers a quick picker next to the model selector: a picked level is written back to the selected Agent's setting immediately (the switched-to level becomes that Agent's new default and applies from the next session). Inside an active session the thinking level is a **per-turn parameter**: the composer's picker lists only the levels and starts out showing the Agent config's level — while the user hasn't picked one it auto-follows the config (sends omit the level, so config edits keep taking effect); once picked, the level sticks for that session and rides on every subsequent send (it applies to that session's subsequent Tasks only and never writes back to the Agent config). See [Configuration](/configuration).
+9 -2
View File
@@ -21,8 +21,8 @@ description: 经 AgentHub 单一网关接入模型,以 (provider, model_id)
| --- | --- |
| `provider` | 配置分组名,与 `model_id` 成对构成唯一键 |
| `model_id` | 上游请求 id |
| `context_window` | 上下文窗口 |
| `max_tokens` | 可选的按模型输出上限(单次请求最大输出 Token 数)。设置后覆盖 Agent 的 `model.max_tokens`,缺省沿用;小上下文模型建议调低——按 Agent 的缺省值(32000)加上任意 Prompt 无法放进如 32k 的窗口。Web 整表保存时省略该字段即清除 |
| `context_window` | 上下文窗口(Token 数)。不只用于展示:每次请求的实际输出上限与压缩阈值都由它推导,请求不会索要超出窗口剩余空间的输出。缺省(或小于 4096 的非常规值)时输出收敛关闭、压缩按 128000 假定推导——窗口更小的模型务必填真实值 |
| `max_tokens` | 可选的按模型输出上限(单次请求最大输出 Token 数)。设置后覆盖 Agent 的 `model.max_tokens`,缺省沿用。该值是天花板而非逐字上线值:每次请求实际发送 `min(max_tokens, context_window − 估算输入 − 安全余量)`,小窗口模型无需手工调低。Web 整表保存时省略该字段即清除 |
| `client_type` | 协议提示(如 `openai`);缺省由 AgentHub 按 model id 推断 |
| `display_name` | 显示名 |
| `vision` | 是否支持图像输入,默认 true |
@@ -77,6 +77,13 @@ api_key = "sk-..."
预置目录中的部分模型:deepseek-v4-pro / deepseek-v4-flash、gemini-3.1-pro-preview、claude-opus-4-8 / claude-sonnet-4-6、gpt-5.5、glm-5.2、kimi-k2.6、qwen3.8-max 等(非完整清单)。
## 本地 / 自建 OpenAI 兼容端点(如 vLLM)
本地推理服务就是一条 `custom` 条目:`client_type = "openai"`、`base_url` 指向服务地址(如 `http://127.0.0.1:8000/v1`)、`model_id` 填服务端的模型名。两处设置决定运行是否顺畅:
- **服务端要开启工具调用。** vLLM 需以 `--enable-auto-tool-choice` 启动,并按模型选择对应的 `--tool-call-parser`(如 Qwen 用 `hermes`、Llama 3.x 用 `llama3_json`);不开启时工具调用会以纯文本返回,Agent 循环无法执行任何工具。
- **条目的 `context_window` 填服务端的真实窗口**——vLLM 即 `--max-model-len` 的值(如 `32768`)。每次请求的输出上限与压缩阈值都会由该窗口自动推导:请求把 `max_tokens` 收敛到窗口剩余空间以内,压缩也会在撞上窗口硬限制之前触发,无需手工调低 `max_tokens`。不填时不做逐请求输出收敛、压缩按 128000 假定,真实窗口更小会导致请求被拒。
## 思考等级
思考等级共五档:`none | low | medium | high | xhigh`,按 Agent 在 `system_config.yaml` 的 `model.thinking_level` 配置,默认 medium。Web 拾取器只提供 `low` 及以上档位(多数模型不支持关闭思考;`none` 仍是合法的已存值,能正常显示)。对话草稿页在模型选择器旁提供快捷拾取器:选定档位立即写回所选 Agent 的该项配置(切换后的档位即成为该 Agent 的新默认,自下一个 Session 生效)。进行中的会话里,思考等级是**逐轮参数**:输入区拾取器只列出各档位,初始即显示 Agent 配置的档位——用户未手动选择时自动跟随配置下发(请求不携带档位,配置的修改持续生效);选定某档后即固定为该会话的档位,随之后每次发送携带(仅作用于该会话的后续 Task,不写回 Agent 配置)。见 [配置参考](/configuration)。
+2 -2
View File
@@ -196,7 +196,7 @@ interface RequestEndPayload {
error_message?: string; // error detail (LLMOutcome.errorMessage internally — one
// name across the stack), non-completed only: the real
// reason behind a retried/failed Request (e.g. a provider
// quota code) — read by the Cost center's errors panel
// error code) — read by the Cost center's errors panel
attempt?: number; // 1-based ordinal of this request within its retry run (the
// authoritative retry count): stamped on failures and on a
// completion that needed retries; absent on a clean first try
@@ -264,7 +264,7 @@ type StopReason = "completed" | "failed" | "aborted" | "timeout" | "malformed" |
| --- | --- | --- |
| `completed` | finished normally | continue |
| `aborted` | user interrupt | stop, hand back to the user |
| `timeout` | LLM timeout / transport disconnect / transient provider quota error | LLM side only: auto-reconnect within the run |
| `timeout` | LLM timeout / transport disconnect | LLM side only: auto-reconnect within the run |
| `malformed` | parse failure / truncated stream | LLM side only: auto-reconnect within the run |
| `failed` | an error the classifier did not judge transient (LLM); a tool error (Environment) | LLM side: auto-reconnect within the run as well — the status is still reported as `failed`. Environment side: the error is fed back to the model, never retried |
| `auth` | the provider rejected the credentials | stop, hand back to the user — the one LLM status that never retries; hosts gate input until the model's API key is updated (credentials come from the current Project config) |
+2 -2
View File
@@ -193,7 +193,7 @@ interface RequestEndPayload {
// 不受影响;compaction_end 复用同一块——attempt 为最终尝试序号,error_message 为失败详情)
error_message?: string; // 错误详情(内部 LLMOutcome.errorMessage,内外同名),仅非
// completed 携带:被重试/失败的 Request 背后的真实原因
// (如供应商额度码),供成本中心错误面板读取
// (如供应商错误码),供成本中心错误面板读取
attempt?: number; // 本次重试序列内的第几次请求(1 起,权威计数):失败请求
// 与经重试后成功的请求携带;首发即成功不携带
retry_in_ms?: number; // 计划中的重连等待(毫秒),仅当引擎将在本轮内重试时携带,
@@ -260,7 +260,7 @@ type StopReason = "completed" | "failed" | "aborted" | "timeout" | "malformed" |
| --- | --- | --- |
| `completed` | 正常完成 | 继续 |
| `aborted` | 用户中断 | 停止并交还用户 |
| `timeout` | LLM 超时/传输层断连/瞬时的供应商额度错误 | 仅 LLM 侧:同一 run 内自动重连 |
| `timeout` | LLM 超时/传输层断连 | 仅 LLM 侧:同一 run 内自动重连 |
| `malformed` | 响应解析失败/流截断 | 仅 LLM 侧:同一 run 内自动重连 |
| `failed` | 分类器未判定为瞬时的错误(LLM 侧);工具执行出错(Environment 侧) | LLM 侧:同样在同一 run 内自动重连——该状态本身仍如实上报为 `failed`。Environment 侧:错误回灌给模型,从不重试 |
| `auth` | 供应商拒绝了凭据 | 停止并交还用户——唯一从不重试的 LLM 终态;宿主据此禁用输入,直到该模型的 API key 被更新(凭据取自当前 Project 配置) |