Skip to content

[3/3] Error cause in ops and bindings - #293

Merged
eric8810 merged 6 commits into
masterfrom
cause/3-error-cause-in-ops-and-bindings
Oct 22, 2026
Merged

eric8810 merged 6 commits into
masterfrom
cause/3-error-cause-in-ops-and-bindings

Conversation

@cunninghamcard-bit

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

Copy link
Copy Markdown
Contributor

Requested by li · project thread

Summary

RFC-0047(#286)实现的第 3 步,也是最后一步:ops 错误信封的四个域都带上 cause,每种语言把它接成本语言原生的 cause。Node 的 error.cause、Python 的 __cause__、Java 的 getCause()、Go 的 errors.Unwrap 现在能一直走到 Connection refused;重试错误的 cause 是最后一次尝试(AI SDK 的 lastError);C++ 包装改抛带错误码和 JSON 的 aimux::NativeError。错误码、名字和消息不变。

叠在 #290(第 2 步:字符串变体)上,#290 叠在 #289 上。原型 #280 已关闭。

跨语言错误体系概览

基于 #290,整条栈在 master e115e723 上。同一个错误在 ops 边界投影成信封一次,各语言从信封还原,行号是开 PR 时的 master 42208472(#268 合并后):

aimux_call / aimux_stream                       aimux-ffi/src/abi.rs:138
├ params                                        abi.rs:61    参数 JSON 解析失败 → BindingError::InvalidWireJson { message: 文字 }
└ dispatch::call                                aimux-ops/src/dispatch.rs:298
  ├ decode                                      dispatch.rs:219   参数不合 schema → ProtocolError::InvalidParams(文字)
  ├ 结果、事件序列化失败                        dispatch.rs:368 等 → BindingError::ResultSerialization(文字)
  └ OpsError::envelope                          aimux-ops/src/envelope.rs:116
      { kind, code, message = Display, error = payload, retryAfterMs }
      ├ aimux 域       serde_json::to_value(AiMuxError)   #289、#290 之后带 cause
      ├ recording 域   recording_payload        envelope.rs:204   io::Error → "source": 文字
      ├ binding 域     BindingError::payload    envelope.rs:222
      └ protocol 域    ProtocolError::payload   envelope.rs:248
finish                                          abi.rs:35   → aimux_error_t*
├ aimux_error_code / _message / _json           abi.rs:364 / 371 / 380
└ 各语言还原
  ├ Node     AimuxError(error.ts:5)、rethrow(error.ts:21);napi 路径 throwable(bindings/node/src/error.rs:36)
  ├ Python   from_envelope → _payload_exception(_errors.py:97 / 84),按生成的 _NESTED_ERROR_FIELDS 还原嵌套错误
  ├ Java     NativeException(envelope)(NativeException.java:12),Kotlin 跟随
  ├ Go       decodeError → *Error(error.go:24)
  ├ Swift    AimuxError(Aimux.swift:5)
  ├ Flutter  OpsException(errors.dart:4)
  └ C++      aimux::check → std::runtime_error(aimux_raii.hpp:22),只有消息

#289、#290 之后 aimux 域的 payload 里已经有 cause,但其他三个域在构造时就把底层错误变成了文字,各语言也都不读 cause:Python 只把 Retry、ToolCallRepair 的嵌套错误还原成异常,Java、Go、Node 的 cause 链是空的,C++ 连错误码都丢了。

本 PR 的改动

ops 信封,aimux-ops/src/envelope.rs:

OpsError::envelope
├ recording 域   recording_payload
│   Init / OpenFile / Spawn:"source": 文字 → "cause": SourceError::new(io::Error)
├ binding 域
│ ├ InvalidWireJson { argument, message, + cause: Option<SourceError> }        abi.rs、Node、Python 的参数解析带上 serde 错误
│ └ ResultSerialization(String) → { message, cause: Option<SourceError> }
│     BindingError::result_serialization(&error)   所有序列化失败处改用它
└ protocol 域
  └ InvalidParams(String) → { message, cause: Option<SourceError> }
      dispatch::decode 带上 serde 错误;其余检查用 ProtocolError::invalid_params(..),没有 cause
payload 里有 cause 才输出(with_cause);Display、错误码不变
impl Error for BindingError / ProtocolError   source() 返回同一个 cause,Rust 的 source() 链不在域错误处断开

各语言(概览里最后一层):

Node     AimuxError 构造和 rethrow 都调 attachCause:payload.cause → 标准 error.cause(不可枚举,同 new Error(msg, { cause }))
         嵌套 aimux 错误 → AimuxCauseError(name、error、stack),外部错误 → AimuxSourceError,逐层递归
         客户端 JSON.stringify 失败的 InvalidWireJson:payload 带 cause,error.cause 是原来的 TypeError
Python   生成器 contains_error 把 ErrorCause 算作错误类型(tools/aimux-codegen/src/emit/python.rs)
         → 每个带 cause 的变体都进 _NESTED_ERROR_FIELDS,嵌套 aimux 错误还原成对应异常类
         _payload_exception:没有 name 的 cause 还原成 aimux.SourceError 链;__cause__ 指向它;四个域都适用
Java     NativeException 构造时 initCause(ErrorCause.of(payload));ErrorCause 链逐层 initCause,getErrorName() 区分嵌套错误
Go       (*Error).Unwrap() 返回 *aimux.Cause,(*Cause).Unwrap() 继续往下,errors.Unwrap / errors.As 可用
Swift    AimuxError.cause: JSONValue?
Flutter  OpsException.cause: Map<String, dynamic>?
重试     AI_RetryError 没有 cause 字段:Node、Python(__cause__ = last_error)、Java、Go、Swift、Flutter 都取 errors 的最后一个
C++      aimux::check 抛 aimux::NativeError : std::runtime_error,带 code() 和 json()
C ABI    不加函数,cause 只在 aimux_error_json 里

调用方看到的(Python,bindings/python/tests/test_ops_errors.py):

APICallError: rate limited
└ __cause__: SourceError("error sending request")
  └ __cause__: SourceError("tcp connect error")
    └ __cause__: SourceError("Connection refused (os error 111)")
RecordingError: canonical message                ← recording 域同样
└ __cause__: SourceError("permission denied")

黄金样例:recording-init、recording-open-file、recording-spawn 的 source 改为 cause,binding-invalid-wire-json 带 cause;新测试检查序列化失败的 cause 就是 serde 错误、没有底层错误时不输出 cause、binding 和 protocol 域的 source() 走到 serde 错误;Node、Python、Java、Go、Swift、Flutter 各有重试取最后一次尝试的测试,Node 另有嵌套 aimux 错误逐层还原成错误类的测试。

文档:CHANGELOG Breaking,docs/error-model.md 的「错误原因」加上各语言入口、四个域和重试,各绑定 README 写明 cause 的入口。DTO 向量数(238)由 #289、#290 更新,本 PR 不再改。

Type of change

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

Breaking:recording 域 payload 的 source 改名 cause 且变为对象(RFC §10 问题 2,已采纳);BindingError::ResultSerialization、ProtocolError::InvalidParams 改为结构体变体,InvalidWireJson 多一个字段;各语言的原生 cause 从空变为有值;C++ 抛出的类型变为 aimux::NativeError(仍继承 std::runtime_error)。

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

本地还跑了生成器 --check、Python 和 Node 的 DTO 向量(238 个)、Node facade 测试和 tsc、Python 错误测试、Java ErrorCauseTest、Go 的 cause 测试、provider 边界检查;Swift、Flutter 的绑定测试由 CI 跑。

Notes for reviewers

  • 设计见 RFC-0047(RFC-0047: 错误体系与错误原因(cause) #286)§5、§6、§7。recording 域改名和 binding / protocol 域带 cause 是 RFC §10 问题 2、3,review 后已标为采纳。
  • refactor!: drop retryAfterMs from the error envelope #349(信封去掉 retryAfterMs)也改 aimux-ops/src/envelope.rs,后合并的一个解决冲突(RFC §11.1)。
  • serde 的 data 类错误文字可能引用调用方的值,这些文字原来就在消息里,cause 没有新增暴露;要不要加大小和脱敏边界是 RFC §10 问题 5。
  • 宿主回调抛出的异常(CallbackFailure)仍不跨 ABI,RFC §5.3。

cunninghamcard-bit added a commit that referenced this pull request Oct 20, 2026
…entation

Line numbers now refer to master 4220847 (#268 merged). The text follows
what #289-#293 implement: TypeValidation is the cause of schema failures,
transport messages drop the URL, and section 12 names the three PRs.
The open questions in section 10 are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S2GJxE3fH7rQvAwXLg2f1C
cunninghamcard-bit added a commit that referenced this pull request Oct 20, 2026
…s in #276

The findings from implementing #289-#293 (TypeValidation, transport
messages without the URL, the call-site count, the download error type)
move to issue #276. Only the baseline and line numbers stay updated to
master 4220847.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S2GJxE3fH7rQvAwXLg2f1C
cunninghamcard-bit added a commit that referenced this pull request Oct 20, 2026
The RFC is still open, so what #289-#293 found belongs in its text:
TypeValidation as the cause of schema failures, transport and download
messages without the URL, the 117 call sites, and reqwest::Error as the
download error. Baseline and line numbers stay at master 4220847.
Section 10's open questions are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S2GJxE3fH7rQvAwXLg2f1C
@cunninghamcard-bit
cunninghamcard-bit marked this pull request as ready for review October 20, 2026 10:41
@cunninghamcard-bit
cunninghamcard-bit force-pushed the cause/3-error-cause-in-ops-and-bindings branch from f64b656 to 096c31f Compare October 20, 2026 10:48
cunninghamcard-bit added a commit that referenced this pull request Oct 20, 2026
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
@cunninghamcard-bit
cunninghamcard-bit force-pushed the cause/2-error-cause-in-string-variants branch 2 times, most recently from 095464c to e3a861b Compare October 20, 2026 10:51
@cunninghamcard-bit
cunninghamcard-bit force-pushed the cause/3-error-cause-in-ops-and-bindings branch from 096c31f to 2f4ccb4 Compare October 20, 2026 10:51

@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

envelope 四个域、C ABI、C++ NativeError、recording source→cause 都保持一致,未发现阻塞项。建议修:

  • 重要 bindings/node/src/error.ts:28:嵌套 cause 直接复制 JSON,AI_JSONParseError/AI_TypeValidationError 不是错误类,instanceof 和 stack 都不成立,与 RFC-0047 §7 以及 Python/Java 的做法不一致。建议递归还原为对应错误类,或明确文档并补嵌套 AiMux 测试。
  • 次要 aimux-ops/src/envelope.rs:362:BindingError/ProtocolError 没有实现 source(),Rust 的 source 链在 domain error 处断开,与 docs/error-model.md 的承诺不符。
  • 次要 bindings/node/src/index.ts:58:客户端 JSON.stringify 失败构造的 InvalidWireJson 没有 cause。
  • 提示 Node README 没补 error.cause;serde data 错误可能带调用方值,建议在 decode 处只保留类型/位置或做脱敏。

@cunninghamcard-bit
cunninghamcard-bit force-pushed the cause/2-error-cause-in-string-variants branch from e3a861b to b570ada Compare October 20, 2026 14:43
@cunninghamcard-bit
cunninghamcard-bit force-pushed the cause/3-error-cause-in-ops-and-bindings branch from 2f4ccb4 to 87fbf65 Compare October 20, 2026 14:43
cunninghamcard-bit added a commit that referenced this pull request Oct 20, 2026
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

Copy link
Copy Markdown
Contributor Author

回复 @eric8810 的 review(左列是 eric 的原话):

eric 的意见 处理 位置
「重要 bindings/node/src/error.ts:28:嵌套 cause 直接复制 JSON……建议递归还原为对应错误类,或明确文档并补嵌套 AiMux 测试」 递归还原:嵌套 aimux 错误是 AimuxCauseError(继承 Error,name 是 AI SDK 名字,error 是整个 payload,有 stack),外部错误是 AimuxSourceError,每层有自己的 cause。新测试覆盖嵌套 AI_JSONParseError 下面再接外部错误 e9bf2a8 bindings/node/src/error.ts、__test__/facade.test.ts
「次要 envelope.rs:362:BindingError/ProtocolError 没有实现 source()」 两个都实现了,source() 返回 payload 里同一个 cause;新测试从 OpsError 沿 source() 走到 serde 错误 6344d0d aimux-ops/src/envelope.rs,aimux-ops/tests/error_envelope_golden.rs binding_and_protocol_sources_are_their_cause
「次要 bindings/node/src/index.ts:58:客户端 JSON.stringify 失败构造的 InvalidWireJson 没有 cause」 payload 带 cause: { message },和原生解码一致;error.cause 是原来的 TypeError,保留 stack;补了测试 e9bf2a8 bindings/node/src/index.ts
「提示 Node README 没补 error.cause」 补上,写明两种 cause 类和重试取最后一次尝试 e9bf2a8 bindings/node/README.md
「serde data 错误可能带调用方值,建议在 decode 处只保留类型/位置或做脱敏」 先不改文字:这些 serde 文字在本栈之前就在消息里(master 的 dispatch.rs:221、abi.rs:63 都是 error.to_string()),cause 复制的是同一段文字,没有新增暴露。RFC §9 已改成如实描述,大小和脱敏边界作为 §10 问题 5 请你定,定了在这里实现 #286 rfc/0047-error-cause.md §9、§10
(#286 的 review)「Retry 的 lastError 没有接到原生 cause 链」 每个绑定从 errors 的最后一个合成原生 cause:Node error.cause、Python __cause__(同 last_error)、Java getCause()、Go Unwrap()、Swift / Flutter cause,各有测试 e9bf2a8、227f3625、ca359a7b

本地:fmt、clippy -D warnings、cargo test --workspace、生成器 --check、DTO 向量、Node facade 测试和 tsc、Python 错误测试、Java ErrorCauseTest、Go 的 cause 测试都通过。整条栈已 rebase 到 master e115e723。


Generated by Claude Code

eric8810 pushed a commit that referenced this pull request Oct 21, 2026
* docs(rfc): RFC-0047 error system and error cause

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

* docs(rfc): answer the RFC-0047 review

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@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.

复审通过。Node 递归 cause(AimuxCauseError/AimuxSourceError)、BindingError/ProtocolError 的 source()、Node 参数序列化 cause、README,以及各绑定从 errors.last() 合成 Retry cause 都已落实。

合并前同样需要把栈 rebase 到当前 master 并 retarget 到 master:#289/#290 的 squash 合并会改变 base。

@cunninghamcard-bit
cunninghamcard-bit force-pushed the cause/3-error-cause-in-ops-and-bindings branch from 87fbf65 to fb67bbb Compare October 21, 2026 03:30

@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.

评审:[3/3] Error cause in ops and bindings —— 批准

实现完整对齐 RFC-0047 §5/§7 与 §10 已采纳的第 2、3 项:recording 域 source→cause 对象化、binding/protocol 域带 serde 错误、四域同形且码与 Display 不变、Rust source() 链贯通(golden 实测);八绑定原生 cause 各按 §7 入口表落地(Node 不可枚举 attachCause + 递归、Python SourceError 链 + __cause__、Java 手写 ErrorCause 与生成 DTO 的分工、Go 链式 Unwrap + retry lastError、Swift/Flutter cause 属性),Retry 一律取最后尝试;C ABI 冻结(§5.2 ✓)、C++ 改抛 NativeError(code+JSON 断言)。

生成物只有 python.rs 的 contains_error 与 _error_payloads.py 需要同步且已同步,manifest 类型未变无需动向量——分层判断正确。

两点非阻塞:1) base 在 #290 的合并提交之前,#290 重做合并后请 rebase 本 PR;2) RFC §10 问题 5(错误 JSON 大小/脱敏边界)按 RFC 留待后续,PR 已如实登记。

@cunninghamcard-bit
cunninghamcard-bit force-pushed the cause/3-error-cause-in-ops-and-bindings branch from fb67bbb to 1476f87 Compare October 21, 2026 04:01
cunninghamcard-bit and others added 3 commits October 22, 2026 14:03
recording errors replace the `source` string with a SourceError chain,
and BindingError::InvalidWireJson, ResultSerialization and
ProtocolError::InvalidParams keep the serde error as the cause. The
payload has a cause only when there is one; messages and error codes
are unchanged.

Part 3 of RFC-0047.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S2GJxE3fH7rQvAwXLg2f1C
aimux::check used to throw a std::runtime_error with only the message.
NativeError still derives from it. The C ABI getters are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S2GJxE3fH7rQvAwXLg2f1C
Node sets the standard error.cause; Python sets __cause__, restoring
nested aimux errors as their exception classes and other causes as an
aimux.SourceError chain.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S2GJxE3fH7rQvAwXLg2f1C
cunninghamcard-bit and others added 2 commits October 22, 2026 14:03
Java and Kotlin chain ErrorCause through getCause(); Go's Error and
Cause implement Unwrap().

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S2GJxE3fH7rQvAwXLg2f1C
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 cause/3-error-cause-in-ops-and-bindings branch from 1476f87 to 7690cae Compare October 22, 2026 06:18
@cunninghamcard-bit
cunninghamcard-bit changed the base branch from cause/2-error-cause-in-string-variants to master October 22, 2026 06:18
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 cause/3-error-cause-in-ops-and-bindings branch from 7690cae to 7f5de6f Compare October 22, 2026 06:33

@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.

复核 rebase 后的 head 7f5de6f:相对 master 仍是 63 个文件的 #293 改动,之前指出的 Node cause、source()、参数序列化 cause、README 和 Retry lastError 都已保留;22 项检查通过,merge state clean。Approve。

@eric8810
eric8810 merged commit b69137e into master Oct 22, 2026
22 checks passed
cunninghamcard-bit added a commit that referenced this pull request Oct 24, 2026
Brings in the error-cause series (#289, #290, #293). Conflicts: ops
tests take master's `InvalidParams { .. }` without `unprojected()`;
envelope.rs keeps `BindingError::result_serialization` without a clock;
binding READMEs and Node error.ts keep the cause text without
retryAfterMs. Bindings regenerated with aimux-codegen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5j9hYv7hDeYgMvvRu2F9T
cunninghamcard-bit added a commit that referenced this pull request Oct 24, 2026
…h-the-ai-sdk

master's error-cause change (#289/#290/#293) made InvalidResponseData a struct variant; closed_before_finishing now builds it with AiMuxError::invalid_response_data.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoxcKU4zBD64Lri3e4Lrq5
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