Skip to content

RFC-0047: 错误体系与错误原因(cause) - #286

Merged
eric8810 merged 2 commits into
masterfrom
rfc/0047-error-system-and-cause
Oct 21, 2026
Merged

eric8810 merged 2 commits into
masterfrom
rfc/0047-error-system-and-cause

Conversation

@cunninghamcard-bit

@cunninghamcard-bit cunninghamcard-bit commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Requested by li · project thread

Summary

本 RFC 写清楚 aimux 的错误体系和 AI SDK 哪里不同、为什么不同,并设计 cause:AI SDK 哪个错误类接受 cause,aimux 对应变体就带一个可序列化的 cause 字段;它就是 Rust 的 source(),经过 ops 信封、#268 生成的 DTO,到每种语言的原生 cause。下面先是错误体系的概览,再是本 PR 设计的改动细节。

错误体系概览

非流式调用(以 OpenAI 为例),行号是 master 42208472(#268 合并后):

generate_text                                   aimux-core/src/generate.rs:954
└ retries.retry(..)                             generate.rs:1133          重试边界
  │ └ retry_with_exponential_backoff            aimux-core/src/retry.rs:103
  │   ├ 可重试:AiMuxError::is_retryable        aimux-core/src/error.rs:691   + 响应头提示 retry_header_delay(retry.rs:225)
  │   └ 次数用完或不可重试 → AiMuxError::Retry(RetryError { reason, errors })   retry.rs:163 / 192
  └ do_generate_with_logging                    generate.rs:2266
    └ OpenAIChatLanguageModel::do_generate      aimux-providers/src/openai/model.rs:166
      └ execute_generate                        openai/model.rs:221
        └ post_json_to_api                      aimux-provider-utils/src/post_to_api.rs:17
          └ execute_exchange                    post_to_api.rs:144
            ├ 传输失败                          http.rs:360 → fetch.rs:430 → ✗ map_reqwest_error(fetch.rs:445)只留第一层文字
            │ └ ✗ fetch_error_to_ai_mux_error   handle_fetch_error.rs:29   → ApiCall,无状态码
            ├ 非 2xx                            post_to_api.rs:166
            │ └ failed_response_handler         openai_failed_response_handler(openai/mod.rs:64)
            │   │                                → create_json_error_response_handler(response_handler.rs:220)
            │   └ ✗ handler 自己失败 → ApiCall { status_code, is_retryable: is_retryable_status }   post_to_api.rs:172
            └ 2xx                               post_to_api.rs:188
              └ ✗ successful_response_handler 失败 → ApiCall “Failed to process successful response: {error}”   post_to_api.rs:193
失败后录制
└ record_outcome(OutcomeRecord::from_error)     generate.rs:1144 → recording.rs:484   存 Display 文字和整个 AiMuxError

流式调用:重试只包住拿到第一个事件之前,之后的错误作为事件交付。

stream_text                                     aimux-core/src/generate.rs:1541
└ retries.retry(..)                             generate.rs:1661          只到第一个事件
  └ do_stream → execute_stream                  openai/model.rs:184 / 376
    └ create_event_source_response_handler      aimux-provider-utils/src/response_handler.rs:358
      ├ 流里的 error 帧 → openai_stream_error   openai/mod.rs:91   → ApiCall
      └ ✗ 传输中断 SseError → ApiCall           response_handler.rs:409   error.to_string()
└ TextStreamPart::Error { error }               generate.rs:1887-1890     作为事件交付

跨语言:同一个错误在 ops 边界投影成信封一次,各语言从信封还原。

aimux_call / aimux_stream                       aimux-ffi/src/abi.rs:138
└ dispatch::call                                aimux-ops/src/dispatch.rs:298
  └ OpsError::project_with_clock                dispatch.rs:338
    └ OpsError::envelope                        aimux-ops/src/envelope.rs:116
        { kind, code, message = Display, error = payload, retryAfterMs }
        ✗ recording / binding / protocol 的底层错误早已是字符串(envelope.rs:204、abi.rs:63、dispatch.rs:221)
└ finish                                        abi.rs:35   → aimux_error_t*
  ├ aimux_error_code / _message / _json         abi.rs:364 / 371 / 380
  └ ✗ Node error.rs:37、Python _errors.py:97、Java AimuxResult.decode、Go decodeError、Swift takeError、Dart OpsException.fromJson、C++ aimux::check
      都不接 cause;C++ 连错误码都丢了

✗ 是丢掉底层错误的地方。分类只看结构化字段(status_code、is_retryable、response_headers),这一点不变。每个 ✗ 的逐段调用栈和上游同一位置的写法见 RFC §2.1–§2.8。

本 PR 的改动

本 PR 只加 rfc/0047-error-cause.md,不改代码。下面是 RFC 设计的改动落在哪条路径、改成什么样(RFC §4–§7)。

Rust 类型(RFC §4.1–§4.2):

AiMuxError                                      aimux-core/src/error.rs
├ ApiCall(ApiCallError)        + cause: Option<ErrorCause>      .caused_by(..);source() 返回它
├ JsonParse / InvalidResponseData / InvalidArgument / InvalidPrompt / NoOutputGenerated / Other
│                              (String) → { message, cause: Option<ErrorCause> }   构造函数 json_parse(..) 等
├ InvalidToolInput             cause: String → ErrorCause(JsonParse 或 TypeValidation 错误对象)
├ + TypeValidation             { message, value, cause }   AI SDK TypeValidationError;只作为 cause 出现,错误码与 InvalidResponseData 共用 3
├ ToolCallRepair               已有 cause,接上 source()
└ Retry                        不加字段;source() = 最后一次尝试(AI SDK lastError)
ErrorCause  = AiMux(Box<AiMuxError>) | Source(SourceError)
SourceError = { message, cause: Option<Box<SourceError>> }      外部错误(reqwest、serde、io)的 source() 链快照

网络层,概览里前四个 ✗(RFC §4.3):

execute_exchange                                post_to_api.rs:144
├ 传输失败
│ ├ map_reqwest_error                           fetch.rs:445
│ │   error.without_url() → FetchError::Connect / Io / Other(SourceError::new(&error))   整条链保留,URL 去掉
│ └ fetch_error_to_ai_mux_error                 handle_fetch_error.rs:29
│     ApiCallError { cause: Some(ErrorCause::source(&fetch_error)), .. }   消息只少了 URL
├ 非 2xx,handler 自己失败                       post_to_api.rs:172   原错误 → ApiCallError.cause
├ 2xx,successful handler 失败                   post_to_api.rs:193   原错误 → ApiCallError.cause
└ 流式:SseError、WebSocket 错误                response_handler.rs:409、ws.rs   原错误 → ApiCallError.cause
调用方看到:
API call error: Cannot connect to API: tcp connect error      ← Display,不变
└ cause: tcp connect error                                    ← source()
  └ cause: Connection refused (os error 111)

ops 信封,概览里跨语言的两个 ✗(RFC §5、§7):

OpsError::envelope                              aimux-ops/src/envelope.rs:116
├ aimux 域       serde_json::to_value(AiMuxError)      cause 随错误一起序列化
├ recording 域   recording_payload                     io::Error → cause: SourceError(§10 问题 2)
├ binding 域     BindingError::payload                 InvalidWireJson / ResultSerialization 保存 serde 错误(abi.rs:63)
└ protocol 域    ProtocolError::payload                InvalidParams 保存 serde 错误(dispatch.rs:221)
aimux_error_code / _message / _json             abi.rs:364 / 371 / 380   不加 C 函数,cause 只在 JSON 里
各语言还原
├ Node error.cause         ├ Python __cause__         ├ Java / Kotlin getCause()
├ Go Unwrap()              ├ Swift / Flutter cause    └ C++ aimux::NativeError::json()
└ Retry 没有 cause 字段:各语言取 errors 的最后一个(AI SDK lastError)   RFC §7

DTO(RFC §6,#268 的生成器):

cargo run -p aimux-codegen                      tools/aimux-codegen/src/main.rs:32
└ ErrorCause、SourceError 派生 JsonSchema,进 manifest typeDefs
  └ ErrorCause 标 "x-aimux-codec": "ordered-untagged"   先试 AiMuxError(有 name),再试 SourceError
    └ Python _NESTED_ERROR_FIELDS 把 ErrorCause 算作错误字段   emit/python.rs:391
contract-tests/dto/AiMuxError                   新增带传输链、嵌套 aimux 错误、无 cause 三类向量

实现分三个代码 PR,一个叠一个,最底下一个基于 #268 合并后的 master,用它的生成器:#289 核心类型和网络层,#290 字符串变体(含 TypeValidation 和下载错误去 URL),#293 ops 和绑定。#280 是原型实现,拆分后关闭。实现时发现的几处已写进正文(§2.6、§2.7、§4.2、§11–§13)。eric 10-20 的 review 已答进正文:§7 重试的 lastError 作为各语言原生 cause、Node 和 Java 的 cause 类,§9 安全说明改准确,§11 列全破坏性变更,§11.1 与 RFC-0044、RFC-0048、#349 的关系,§6 登记 Swift WireIndirect;§10 问题 2、3 标为已采纳,新增问题 5(大小和脱敏边界),问题 1、4、5 待讨论。执行清单在 #327。

Type of change

  • Bug fix (non-breaking)
  • New feature (non-breaking)
  • Breaking change
  • New provider
  • New / updated binding
  • Docs / RFC

Checklist

  • cargo fmt --all -- --check passes
  • cargo clippy --workspace --all-targets -- -D warnings passes
  • cargo test --workspace passes (cassette playback — no network/keys)
  • Added or updated cassette tests / contract fixtures for behavior changes
  • Updated relevant docs (README.md, docs/, rfc/, bindings/README.md)
  • Commit messages follow Conventional Commits

只改 rfc/,不涉及代码检查。

Notes for reviewers

cunninghamcard-bit commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

作为贡献者,我(li)对 RFC §10 四个待讨论问题的意见是都按推荐,供维护者参考:

问题 li 的意见
1. cause 链要不要设深度上限 不设。网络错误一般三四层,重试只有几次尝试,整条链也就几十行文字
2. 录制错误的 source 字符串要不要改名 改为 cause: SourceError,和其他错误同一个形状;recording-*.json 黄金样例随之改变
3. binding / protocol 域要不要带 cause 带。规则统一为“手上有外部错误就放进 cause”:InvalidWireJson、ResultSerialization、InvalidParams 保存 serde 错误
4. C++ 要不要提供解析 cause 的辅助函数 不提供,不引入 JSON 依赖,调用方解析 NativeError::json()

Generated by Claude Code

cunninghamcard-bit commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor Author

正文已更新:实现 #289、#290、#293 时发现的四处写进了 RFC 正文,新增 TypeValidation(§2.6、§4.2)、传输和下载错误的消息去掉 URL(§11、§13)、117 处和下载错误是 reqwest::Error(§2.7、§12);基线和行号是 master 42208472(#268 合并后)。§10 四个问题仍是待讨论。执行清单在 #327。


Generated by Claude Code

@eric8810 eric8810 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

方向合理,但契约与实现/其他 RFC 还没对齐,暂不建议接受:

  • 重要 Retry 的 lastError 没有接到原生 cause 链:wire payload 没有 cause,Node/Python/Java/Go 的 cause 都为空。RFC §7 需要明确各绑定如何从 errors.last() 合成 cause,并补测试。
  • 重要 Node 嵌套 cause 是普通对象,不是错误类;Java 实际多了一个手写 ai.arcships.aimux.ErrorCause 包装类,RFC 没说明它和生成的 types.ErrorCause 的分工。
  • 重要 §9 的安全声明不准确:serde_json 的 data 错误会引用违规值,TypeValidation.value 也没有大小/脱敏上限;建议增加大小和脱敏边界。
  • 重要 §11 低估破坏性:InvalidToolInput.cause 字符串→对象、recording source→cause、传输消息去 URL,都不只是“新增可选字段”。
  • 重要(跨 RFC) 与 RFC-0048 冲突:RFC-0047 说 schema 2 旧文件可读,RFC-0048 要删除 schema 1/2 读取;ErrorCause 放 core 也与 RFC-0044 的 aimux-provider 归属不一致。
  • 次要 §10 Q2/Q3 已在 #293 实现,建议标为已采纳;Swift 的 SourceError.cause 需要 #289 的 WireIndirect 处理,RFC 未登记。

cunninghamcard-bit and others added 2 commits October 20, 2026 14:30
Describes how aimux's error system differs from the AI SDK's, traces
today's error paths from the public entry functions to the lines that
drop the underlying error, and designs `cause` across the core, the
transport layer, the ops envelope, the generated DTOs and every binding.
The open questions in section 10 stay open. Line numbers refer to
master 4220847, and the text includes what implementing #289, #290
and #293 found.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S2GJxE3fH7rQvAwXLg2f1C
Retry's lastError as every binding's native cause, the Node and Java
cause classes, an accurate security section with a size and redaction
question, the full list of breaking changes, Swift's WireIndirect, and
how RFC-0044, RFC-0048 and #349 meet this RFC. Q2 and Q3 are marked
adopted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S2GJxE3fH7rQvAwXLg2f1C
@cunninghamcard-bit
cunninghamcard-bit force-pushed the rfc/0047-error-system-and-cause branch from 52f2b63 to 0d5d7b2 Compare October 20, 2026 14:43

Copy link
Copy Markdown
Contributor Author

回复 @eric8810 的 review(左列是 eric 的原话),正文改动在 0d5d7b2:

eric 的意见 处理 位置
「Retry 的 lastError 没有接到原生 cause 链……RFC §7 需要明确各绑定如何从 errors.last() 合成 cause,并补测试」 §7 写明:wire 上没有 cause 字段,各绑定取 errors 的最后一个作为原生 cause,errors 为空时没有;嵌套的 Retry 同样处理。#293 实现了 Node、Python、Java、Go、Swift、Flutter,各有测试 §7;#293
「Node 嵌套 cause 是普通对象……Java 实际多了一个手写 ai.arcships.aimux.ErrorCause 包装类,RFC 没说明它和生成的 types.ErrorCause 的分工」 §7 的 Node 一行改为 AimuxCauseError / AimuxSourceError(#293 已改);新增一段说明 Java 两个类型的分工:types.ErrorCause 是生成的 DTO,用在数据字段;ai.arcships.aimux.ErrorCause 是手写的 RuntimeException,只挂在 getCause() 上,包住同一段 JSON,不重新解码 §7
「§9 的安全声明不准确:serde_json 的 data 错误会引用违规值,TypeValidation.value 也没有大小/脱敏上限」 §9 重写成实际情况:serde data 错误会引用值(文字原来就在消息里)、value 没有上限、信封没有大小上限和值脱敏、录制只按键名脱敏。是否加边界作为 §10 问题 5,倾向只限大小(每层 message 1 KiB、value 16 KiB),另一种是 serde data 错误只留类别和位置 §9、§10 问题 5
「§11 低估破坏性:InvalidToolInput.cause 字符串→对象、recording source→cause、传输消息去 URL,都不只是“新增可选字段”」 §11 重写:这三条和 Rust API、各绑定的 cause 从空变为有值、C++ 抛 NativeError 逐条列出,都写入 CHANGELOG Breaking §11
「与 RFC-0048 冲突:RFC-0047 说 schema 2 旧文件可读,RFC-0048 要删除 schema 1/2 读取」 §8 改为“旧文件可读”只适用于 RFC-0048 之前;schema 3 的 errorValue 仍带 cause、只在 payload 策略下存;回放引擎的 FetchError::Other 载荷随 §4.3 变为 SourceError §8、§11.1
「ErrorCause 放 core 也与 RFC-0044 的 aimux-provider 归属不一致」 不冲突:ErrorCause、SourceError 和 AiMuxError 在同一个文件,随 RFC-0044 L7 的共享错误枚举一起搬到 aimux-provider;本文写 core 是因为基线还没拆。已写进新的 §11.1 §11.1
「§10 Q2/Q3 已在 #293 实现,建议标为已采纳」 已标为采纳;问题 1、4、5 待讨论 §10
「Swift 的 SourceError.cause 需要 #289 的 WireIndirect 处理,RFC 未登记」 §6 第 6 条登记 §6

另外 §11.1 记下 #349(信封去掉 retryAfterMs)和 #293 都改 aimux-ops/src/envelope.rs,后合并的一个解决冲突。分支也 rebase 到了 master e115e723。


Generated by Claude Code

cunninghamcard-bit pushed a commit that referenced this pull request Oct 20, 2026
Publish order puts aimux before providers so C7 can land before C6; observers
ride on DiagnosticScope instead of OperationSettings, with core's Recorder as
the C3-C5 observer; C4's download takes connection, user-agent and error-mapping
parameters; RFC-0047 cited as #286; line references refreshed at 5743a40.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

@eric8810 eric8810 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审通过。Retry 最后一次尝试的原生 cause、Node/Java 的 cause 类型分工、§9 安全边界、§11 破坏性清单、与 RFC-0048 的 schema 2 关系,以及 Q2/Q3 的采纳状态都已对齐实现。Approve。

@eric8810
eric8810 merged commit a2c3a2f into master Oct 21, 2026
14 checks passed
eric8810 pushed a commit that referenced this pull request Oct 21, 2026
* docs(rfc): RFC-0044 crate 拆分、依赖反转与 provider 目录布局

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

* docs(rfc): RFC-0044 录制在 Fetch 层留观察点,上下文传法交给观测 RFC

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

* RFC-0044: fold aimux-stream into aimux-provider-utils (L9, C0)

aimux-stream is a ~120-line adapter over sse-stream plus an unused
extract_lines; its only caller is provider-utils. Upstream keeps SSE
decoding in provider-utils over eventsource-parser. Registers the change
to alignment doc D17.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

* RFC-0044: move provider layout and test placement to RFC-0046

RFC-0044 keeps the crate split and dependency inversion; the per-vendor
directory, test placement and cross-vendor reuse rules move to a
separate RFC-0046.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

* RFC-0044: correct the provider count (48 vendor implementations, 282 presets)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

* RFC-0044: apply review corrections

Fix the recording, session, download and trace call stacks against
master; assign the unassigned modules (decision, files and skills
models, math, util, prelude, replay mocks, rebuild_decision_provider);
name the three upward edges C2 must cut; add test ownership rules;
register the alignment-doc conflicts and list the open questions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

* RFC-0044: keep the aimux-core crate name (contributor opinion)

Record li's opinion that aimux-core keeps its name instead of the D17
rename to aimux; register the difference for the maintainer to decide.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

* Revert "RFC-0044: keep the aimux-core crate name (contributor opinion)"

Contributor opinions go to PR comments, not the RFC text.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

* docs(rfc): RFC-0044 review fixes: transport call scope in C3, unify observation wording, register devtools differences

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

* docs(rfc): RFC-0044 links RFC-0046 draft PR

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

* docs(rfc): RFC-0044 unit tests in foo/tests.rs, public-API tests in tests/it

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

* docs(rfc): put tests in one tests/ directory per module

Follows RFC-0046 section 5: each module directory has a tests/ child,
one Rust file per upstream test file; crate tests/ keeps only
cross-module scenarios.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

* docs(rfc): drop the draft note on RFC-0046

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

* docs(rfc): answer the review of RFC-0044

Keep ProviderDiscovery beside Provider and turn into_text_stream_part
into a free function in aimux. Spell out what DiagnosticScope carries
(call id, seq/attempt/parent, exchange counter, observers) and the
download parity C4 keeps. Let the C7 rename follow C4 instead of the
devtools steps, note that #268 removed the generated aimux_core paths,
and plan a final aimux-stream release before C0 deletes it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

* docs(rfc-0044): address the second review round

Publish order puts aimux before providers so C7 can land before C6; observers
ride on DiagnosticScope instead of OperationSettings, with core's Recorder as
the C3-C5 observer; C4's download takes connection, user-agent and error-mapping
parameters; RFC-0047 cited as #286; line references refreshed at 5743a40.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116HPMoqaLfFr1TyqDq1hrW

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants