Skip to content

[14/14] feat!: ops layer, C ABI and bindings - #265

Merged
eric8810 merged 493 commits into
masterfrom
v4/14-ops-layer-c-abi-and-bindings
Oct 17, 2026
Merged

eric8810 merged 493 commits into
masterfrom
v4/14-ops-layer-c-abi-and-bindings

Conversation

@cunninghamcard-bit

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

Copy link
Copy Markdown
Contributor

Requested by li

Summary

Before: every language binding reached Rust through about 130 provider-specific C exports and per-field error getters (or, for Node and Python, through their own direct calls), so each new capability had to be wired by hand eight times, and stream termination, cancellation and error shape differed per language.

After: RFC-0039 is implemented as one shared layer. The new aimux-ops crate owns a handle registry isolated per owner (C process, Node env, Python interpreter), the op table (the 70 ops of RFC-0039 §3.2 plus decision.decide and decision.capabilities), call/stream dispatch with exactly one end frame after acceptance, the realtime transcription Operation with byte and item budgets, and one error envelope. aimux-ffi exports exactly the 14 RFC primitives (130 → 14 symbols); Node and Python call the shared dispatch directly; Go, Java, Kotlin, Swift and Flutter go through the new C ABI. aimux-codegen generates the C header, the control-object JSON Schemas and every language's op constants from the manifest, with a --check gate.

This is a breaking, one-shot switch: the old exports, headers and provider-specific constructors are gone, with no aliases or shims.

How: the work was built in parallel against a fixed interface contract, then integrated in order (core, op families, C ABI with the old-export deletion and codegen, bindings), with a red-team review of the contract before integration. Of two competing core implementations, the one whose stream frames are independently constructible was kept.

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 (Rust 1.99)
  • cargo test --workspace passes (cassette playback, no network/keys): 3528 passed, 3 failed, 8 ignored on macOS. jev_test::official_provider_failures_use_standard_retry_rules_and_preserve_codes and fetch::tests::the_default_transport_can_be_replaced also fail on this PR's base on the same Mac; post_to_api::tests::slow_response_trips_the_exchange_timeout_as_retryable fails only when the suite runs in parallel and passes with --test-threads=1.
  • Added or updated cassette tests / contract fixtures for behavior changes
  • Updated relevant docs: design doc, coverage ledger, conformance matrix and upgrade guide under docs/, plus one registered-difference line per item in docs/aisdk-architecture-alignment.md
  • Commit messages follow Conventional Commits

Notes for reviewers

  • Position: base master. PRs 1–13 of the stack have landed; eric's merge of master into this branch (c346d18) leaves the tree identical to 133b0f1. The next layer is [15/15] feat!: generate binding DTOs from the Rust types #268. The RFC-0039 §6.1 example still shows the earlier envelope shape (error.type, retryable) and needs correcting to the implemented one (error.name, no retryable).
  • Commits: one per direction (aimux-ops; the C ABI with codegen and the old-export deletion; one per binding; the ops list), then follow-ups: removal of code the ops layer made redundant (per area), CI fixes, and the docs.
  • Removed as redundant: unused helpers and duplicated test scaffolding in aimux-ops; C ABI glue in aimux-ffi; the legacy Node bench package and its dev dependencies; the Java hand-written mock provider server; the unused Python wrapper module; redundant Go stream-bridge and type code. The Flutter FFI header is now generated by aimux-codegen. Of two competing core implementations, the one whose stream frames are independently constructible was kept, because terminal frames could then be tested without a live stream.
  • Rust 1.99: stable moved to 1.99, which deprecates AtomicU64::fetch_update and adds clippy::double_must_use. The fix touches aimux-provider-utils and aimux-providers, so lower PRs in the stack will need the same change on their next run.
  • CI changes: the Flutter job now builds the ABI test fixtures and the Dart bridge and runs the real-library tests; before this, those tests never ran in CI. The Go binding links the C library statically on Linux again.
  • Gates run locally (macOS arm64): aimux-codegen -- --check clean; rustdoc with -D warnings clean; C11/C++17 ABI tests 4/4 pass, also under ASan; aimux-ops gates ops_matrix, manifest_contract, stream_terminal, handle_lifecycle, error_envelope_golden.
  • Binding suites run against the real library: Go ABI 47 and native 36 pass (-race, go vet clean); Flutter dart test 104 passed, 6 skipped; Java 61/61 and Kotlin 35/35 through the offline JUnit runners; Python 33 passed, 37 skipped against the real runtime, plus 43 passed, 27 skipped in the isolated native-contract runner; Node 42/42 including the 11 shared vectors. Not run locally: Gradle itself (dependencies not cached offline) and Swift (the local Swift toolchain crashes), so CI is the judge for those two.
  • No hand-written protocol values: every binding's facade, examples, tests and C fixtures use the generated op, ABI, ops-version and digest constants; Python's constants module moved into the package (aimux/ops.py) so the wrapper can import it. Node's realm no longer keeps a copied list of handle-creating ops: a call finishing after teardown releases the owner's handles, and an operation is recognised by its registry kind. check_symbols.sh derives its required symbols from the generated header and the Dart transport's exports.

Registered differences from RFC-0039

  • D1 Error envelope: error carries the AI SDK error JSON (name, isRetryable) as RFC-0042 requires, with no top-level retryable or type.
  • D2 Decision: HandleKind::Decision with decision.decide / decision.capabilities, because the baseline already exposes Decision (72 ops, nine model kinds).
  • D3 Error codes: protocol codes 300–306; existing codes 1–20, 100–105 and 200–206 keep their meaning; code 4 stays reserved.
  • D4 aimux_handle_finalizer(void *): a non-blocking finalizer with the pointer signature GC finalizers need.
  • D5 Limits (64/256 items, 8 MiB queues, 1 MiB per item) are proposals until RFC-0038 measurement freezes them.
  • D6 (resolved) operation.next is a poll op, as RFC-0039 §3.2 lists it.
  • D7 operation.push admission is synchronous and returns ResourceLimit when full, in C, Python and Node alike.
  • D8 Foreign-owner handles look like unknown ones; a foreign drop succeeds without effect.
  • D9 One retry-hint parser lives in aimux-core and is re-exported by ops.
  • D10 Stdio, host callbacks and other deferred capabilities are not exposed.
  • D11 Accepted by the maintainer: D8 (generated business DTOs) lands after this switch; ts-rs stays as the transitional chain until then (RFC-0039 §0.1.7 amended).
  • D12 Accepted by the maintainer: schema-2 recording read/write stays until the last PR of this series (RFC-0039 §7.1 amended).

Registered gaps (not in this PR)

  • G-a Recorder, session store and session inference stay process-wide.
  • G-b Recording writes schema 2 and reads 1 and 2; schema 3 (RFC-0041) and removal of old-schema reading are the last step of this series.
  • G-c Credential construction and replay transport capabilities return UnsupportedFunctionality (scope: future).
  • G-d Business-type JSON Schemas and language DTO generation belong to RFC-0042; scripts/gen_ts_types.py stays.
  • G-e Installing and exercising every release asset is not yet shown.
  • G-f Package provenance, default methods and named vendor methods are incomplete; provider.invoke accepts only listModels.
  • G-g Admission limits do not bound every provider allocation or the core text pump.
  • G-h Per-business-type revision checks are incomplete; every binding pins ABI, ops version and manifest digest exactly.
  • G-i The full eight-language and platform execution matrix is not yet established; Dart streaming on Windows is unsupported.
  • G-k The old offline mock model has no direct replacement for text generation.

Size

Artifact (release, macOS arm64) base #261 this PR Δ
libaimux_ffi.dylib 7,848,704 B 8,077,056 B +228,352 B (+2.9%)
libaimux_ffi.a 165,739,328 B 171,658,688 B +5,919,360 B (+3.6%)
exported aimux_* symbols 130 14 −116

claude added 30 commits October 14, 2026 02:46
…DeepSeek

The round-trip cases built empty nested maps as untyped literals, which
Dart infers as Map<dynamic, dynamic>, so the parsers' Map<String, dynamic>
casts failed. The stream test's mock model was Groq, which reads stream
usage from x_groq only (upstream groq-chat-language-model.ts:438), so the
finish part had no input tokens; the mock is DeepSeek again, as in [7/8].

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…3-add-the-native-rust-porting-skills

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

Fixes on top of the previous commit:

- node-binding builds with `napi build` without `--release`, so its
  uploaded node-* artifacts are dev-profile builds. The workflow-level
  CARGO_PROFILE_DEV_DEBUG=0 stripped their debug info (Linux artifact
  55.3 MB -> 17.6 MB) and its CARGO_PROFILE_RELEASE_* overrides had no
  effect. Debug info is now dropped in rust-test only, where the test
  binaries are linked, and the dead overrides are gone.
- ffi-build goes back to `cargo build`, as release.yml does. Building
  only the cdylib changes the shipped library (x86_64 Linux .so
  9,446,848 -> 7,637,144 bytes, same 129 exports), so the ffi-*
  artifacts would no longer match the release build or earlier runs.
- rust-cache `cache-all-crates` keeps unused registry crates, it does
  not cache workspace crates; dropped. Caches are saved from master only
  (`save-if`); pull requests restore master's caches instead of each
  writing their own, which is what pushed the repository past 10 GB.
- The two node-binding legs on each runner OS (x64 and arm64 targets)
  shared one cache key: one leg failed to save and the other restored a
  cache built for the wrong target. The key now includes the target.
- CARGO_INCREMENTAL=0 is already set by rust-cache; dropped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS
GenerateTextResult.content is the AI SDK content array, so a text part
is {"type":"text","text":...}; the test still read the old
externally tagged {"Text":{...}} shape. It now reads the part as the
tool-call test below it does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
With every stacked pull request running CI, runs wait far longer for a
runner than they spend building: each run had 6 macOS jobs against a
5-job macOS limit and 18 Linux jobs against a 20-job total limit, and
the macOS jobs of one run sat queued for 15 minutes.

- Java (JDK 8, 11, 17, 21) and Kotlin (JDK 17, 21) run in one job
  that builds the FFI library once and runs each JDK in turn: 6 jobs
  and 6 FFI builds become 1.
- The Go binding is tested in the ffi-build legs on Linux and macOS,
  against the static archive that job already builds: the 2 Go jobs
  and their FFI builds go away.
- The x86_64 macOS Node binding is cross-built on Apple Silicon and
  tested with x64 Node under Rosetta instead of on the Intel runner,
  where its build took 371 of the job's 465 seconds.
- scripts/build-ios-xcframework.sh builds both iOS slices in one cargo
  invocation, so they compile in parallel; the output is unchanged.

A run goes from 24 jobs (6 macOS) to 17 (5 macOS).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS
The base now spells tool choice as `{"type": ...}`, has no
`CallOptions::max_retries`, and adds `user_agent_suffix` to the Open
Responses settings. Without this the probe silently falls back to a
default prompt for every cassette with a tool choice.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…ite-json-repair-in-idiomatic-rust

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…tool-call-repair-is-a-trait

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…config-is-plain-data

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…3-add-the-native-rust-porting-skills

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…streams-are-transformer-structs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
The first-chunk timer keeps starting with the operation, because providers
wait for the first SSE event inside do_stream; starting it with the response
stream would leave a stream that answers 200 and then sends nothing without
a bound. §5 now files it as a product difference, and §9.1 lists it with the
stream retries (streamRetries) among the known gaps.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
[1/8] now stacks on #262, so its diff keeps only the fixture cleanup;
the CI trigger change comes from #262.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
…ite-json-repair-in-idiomatic-rust

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…tool-call-repair-is-a-trait

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…config-is-plain-data

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…3-add-the-native-rust-porting-skills

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…streams-are-transformer-structs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
- subosito/flutter-action saves a 1.5-2.2 GB SDK cache per OS. A pull
  request can only restore caches saved on its own branch or on master,
  so with the action's cache on, every open pull request saved its own
  copy and the 10 GB budget filled up again. The action's `cache` input
  turns restore and save off together, so the two are split: a dry run
  of the action gives the key and path, actions/cache/restore runs on
  every branch, the action installs the SDK (a no-op when the restore
  hit), and actions/cache/save runs on master only, at the end of the
  job so the engine artifacts the build downloads are in the cache. The
  pub cache stays off, as on master. The Gradle cache already saves from
  the default branch only.
- bindings/node and bindings/python are their own Cargo workspaces and
  build into bindings/<name>/target, which rust-cache's default (the
  repository's target directory) never covered: the Node and Python jobs
  rebuilt aimux-core and aimux-providers on every run. `workspaces` now
  points rust-cache at each binding's directory.
- The ffi-build comment said the Go binding links the static archive;
  on macOS ld64 picks the .dylib next to it. Comment only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS
aimux_stream_text runs the whole stream future on the caller's thread
(ffi_block_on). With the test-only override at opt-level 0, one stream
through the C ABI needs about 530 KB of stack on the AI SDK branches
(about 300 KB on master), more than the 512 KB stack of a Swift
cooperative thread: the async Swift tests died with SIGBUS on #255 and
#259 once they carried this workflow. At opt-level 1 the same stream
needs about 110 KB; the shipping profile needs about 160 KB. The
library is also 40% smaller, and the build is still far from the
shipping profile's fat LTO with one codegen unit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS
#262 now builds the test-only FFI libraries at opt-level 1, so the Swift
async stream test no longer overflows its 512 KB thread.

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

Copy link
Copy Markdown
Contributor

预审清单复核确认:结构性问题 1–7 与顺序偏差两项已逐项抽查属实——schema 单一来源(error-envelope.json 等直接引用 manifest 定义)、operation.next 已改 OpMode::Poll、sourceSymbolInventory 已按 RFC 字段名、Java NativeHandle 补 PhantomReference + 非阻塞 finalizer、manifest 加 digest 且七绑定精确比对、push 语义统一为立即 ResourceLimit、Node/Python 回放 11 个共享场景、RFC-0039 §0.1.7/§0.1.9 修订行按维护者 05:11 的决定写好(ts-rs 过渡链登记、schema-2 保留至系列最后一个 PR)。谢谢处理得这么完整。转 ready 后安排正式评审。

eric8810 and others added 8 commits October 17, 2026 09:27
master 带入 #262/#246/#247 的 squash;三个冲突文件取本分支版本
(ci.yml 为已调和 #262 任务表 + web-typecheck + pin 脚本测试的超集),
gen_ts_types.py 自动合并。
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one
commit) through [1/8], whose master merge eric made (2a0b02b). The tree
is unchanged: this branch already had all three.

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one
commit) through [2/8]. The tree is unchanged: this branch already had
all three.

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one
commit) through [3/8]. The tree is unchanged: this branch already had
all three.

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one
commit) through [4/8]. The tree is unchanged: this branch already had
all three.

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one
commit) through [5/8]. The tree is unchanged: this branch already had
all three.

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one
commit) through [6/8]. The tree is unchanged: this branch already had
all three.

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
Brings in master at ec338c9 (#262, #246 and #247, each squashed into one
commit) through [7/8]. The tree is unchanged: this branch already had
all three.

Resolution: no conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
@cunninghamcard-bit
cunninghamcard-bit marked this pull request as ready for review October 17, 2026 11:41
@cunninghamcard-bit

cunninghamcard-bit commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor Author

@eric8810 已转为 ready,当前头 55c54cd(已合入最新 v4/13),CI 22 项全绿,可以安排正式评审了。

claude and others added 6 commits October 17, 2026 11:47
…ite-json-repair-in-idiomatic-rust

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…tool-call-repair-is-a-trait

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…config-is-plain-data

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…streams-are-transformer-structs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…3-add-the-native-rust-porting-skills

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GqcmS2Qe8c4L99HtjtbDD
…s-layer-c-abi-and-bindings

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.

正式评审:批准(预审 → 修复 → 复核 → 本轮增量核验,四步完成)

  1. 预审(五个方向并行):aimux-ops 核心 / C ABI + codegen / Node + Python / 五个 C ABI 绑定 / 跨切面 + roadmap——无阻断性实现缺陷,7 项结构性问题 + 2 项顺序偏差 + 若干次要建议;
  2. 修复(b4ef67af):全部处理,含 schema 单一来源、operation.next→poll、sourceSymbolInventory、Java PhantomReference 兜底、manifest digest 七绑定精确比对、push 语义统一、Node/Python 回放 11 个共享场景、RFC-0039 §0.1.7/§0.1.9 修订行(D8/ts-rs 过渡与 schema-2 保留,按维护者 2026-10-16 决定登记)、op 常量全量引用、ASan+arm64 入 CI、错误脱敏 canary;
  3. 复核:结构性问题与修订行逐项抽查属实;
  4. 本轮增量(55c54cda→133b0f16):仅栈同步合并 + 22 行文档注解(#252 的 CallOptions→OperationSettings 落点说明),无代码风险。承诺的文档全部在位:docs/rfc-0039-implementation.md、aimux-ffi/ABI-MIGRATION.md(130 行符号账本)、docs/api/ops.md;22 项 CI 全绿。

合并定位不变:基于 v4/13 栈顶,待 [1/8]–[13/13] 全部落地后 retarget master 合并;#268(D8/D11 的绑定 DTO 生成)保持 Draft 随后。批准。

@eric8810
eric8810 changed the base branch from v4/13-add-the-native-rust-porting-skills to master October 17, 2026 15:39
master 带入 [1/8]–[13/13] 的 squash;冲突与自动合并漂移收敛到本层树
(层树为作者维护、CI 全绿的真相)。
@eric8810
eric8810 merged commit 5743a40 into master Oct 17, 2026
22 checks passed
cunninghamcard-bit added a commit that referenced this pull request Oct 17, 2026
…ined

master (5743a40) has the same tree as 133b0f1, which this branch already
merged, so the result keeps this branch's tree unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
eric8810 added a commit that referenced this pull request Oct 20, 2026
* docs(agents): trim the native-Rust skills and profile to what stays true

The profile carried line numbers and commit hashes of one stack head, a
known-bug list and a candidate list that the stack has since worked
through, and a cross-build probe and gate script that CI never compiles
or runs. Keep only stable facts: upstream and module mapping, design-doc
sections, surfaces that must not change, oracles, checks, project
decisions (now including one-way layering and streams without a shared
tool-call tracker), traps and working rules.

The skills keep their process, discipline and rewrite table, shortened.
New rules from the provider-config and stream layers: removing closures
must not reverse module dependencies, a rewrite must not grow the code,
and an in-process differential is recorded as fixtures, not kept as a
copy of the old implementation.

The probe and gate script are dropped from the repository; PR
descriptions say where the probe lives and how to run it.

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

* docs(agents): drop the preset count from the profile

The registry has 282 rows, not 281, and the number changes whenever a
preset is added; the profile only keeps facts that stay true.

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

* ci: pin cargo-ndk and keep pull requests from saving two more caches

cargo-ndk is installed through cargo-binstall, which took whatever was
newest; pin it to 4.1.2, the version CI has been installing.

The contract and Swift jobs were the last two that saved a Rust cache
on every pull request. Like the other jobs they now restore on pull
requests and save only on master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

* feat(ops)!: add the aimux-ops dispatch layer

One typed dispatch for every op in RFC-0039 §3.2 plus decision.decide and decision.capabilities: an owner-isolated handle registry, op descriptors and a manifest, call/stream dispatch with a single end frame, the realtime transcription Operation with byte and item budgets, and the error envelope projected once. The retry-after parser moves to one pure helper in aimux-core.

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

* feat(ffi)!: replace the C exports with the RFC-0039 primitives

aimux-ffi now exports exactly the fourteen primitives (call, stream, operation push/next, abort, drop, manifest, ABI version, error and string release, handle finalizer) over aimux-ops; the 130 provider-specific and field-getter exports, their headers and old-only tests are removed. aimux-codegen generates the header, control schemas and per-language op constants with a --check gate; C/C++ tests and CI jobs run against the generated header.

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

* feat(node)!: move the node binding onto the ops layer

The node binding now goes through the RFC-0039 contract instead of provider-specific entry points.

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

* feat(python)!: move the python binding onto the ops layer

The python binding now goes through the RFC-0039 contract instead of provider-specific entry points.

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

* feat(go)!: move the go binding onto the ops layer

The go binding now goes through the RFC-0039 contract instead of provider-specific entry points.

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

* feat(java)!: move the java binding onto the ops layer

The java binding now goes through the RFC-0039 contract instead of provider-specific entry points.

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

* feat(kotlin)!: move the kotlin binding onto the ops layer

The kotlin binding now goes through the RFC-0039 contract instead of provider-specific entry points.

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

* feat(swift)!: move the swift binding onto the ops layer

The swift binding now goes through the RFC-0039 contract instead of provider-specific entry points.

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

* feat(flutter)!: move the flutter binding onto the ops layer

The flutter binding now goes through the RFC-0039 contract instead of provider-specific entry points.

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

* docs(ops): list the registered operations

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

* chore: delete the web console's generated ProviderName copy

tools/aimux-web/web/src/types/ProviderName.ts was the eighth copy written
by scripts/gen_provider_names.py, which this branch deletes. Nothing
imports it (it is an orphan on master too), and its header still claimed
the deleted generator. RFC-0036 §12.3 A2 requires every Rust, binding
and web copy and the generator to go.

The three quality-audit reports that cite `gen_provider_names.py --check`
as a current gate get a later-change note, as docs/internal/phase4-audit.md
already has.

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

* ci: type-check the web console against the generated types

The console imports the core types from bindings/node/src/types through
@aimux/types. gen_ts_types.py --check only compares the generated files,
so a renamed or removed Rust type regenerates cleanly and leaves a
dangling import that no pull request check catches. RFC-0036 §13.1 A4
asks for every import to be type-checked.

A new "Web console typecheck" job runs `npm ci && npm run typecheck`
(vue-tsc --noEmit) in tools/aimux-web/web. It runs when the shared code
changes (which includes tools/aimux-web and the Rust crates whose types
are generated) or when bindings/node changes, where the generated types
live; documentation-only changes skip it. "CI result" checks it.
Removing bindings/node/src/types/Recording.ts locally makes it fail on
the two files that import it.

RFC-0029 §9.1 gets a later-change note: the console no longer has its
own copies, and TS_RS_EXPORT_DIR no longer points at the web directory.

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

* refactor(ops): remove unused helpers and duplicated test scaffolding

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

* refactor(ffi): trim the C ABI glue and generate the Flutter header from the manifest

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

* refactor(go): remove code the ops layer made redundant

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

* refactor(node): drop the legacy bench package and fix the stale trace assertion

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

* refactor(flutter): generate the FFI bindings from the codegen header

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

* fix(kotlin): expect the abort as the cause of CancellationException

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

* fix(python): align the Responses fixture and remove the unused wrapper

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

* refactor(java): remove the hand-written mock provider server

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

* docs: size and performance gates follow RFC-0043

#208 is merged as RFC-0043, which sets the gates without waiting for
the migration, and master's ROADMAP §4 (#205) lists S1 / S2 / P as near
term. This branch had deferred them until the migration was done and
until #208 was rewritten. S1, S2, S3 and P are no longer marked as
deferred in ROADMAP §2 and §3, the alignment doc §0.7 puts them back in
the unaffected row with a pointer to RFC-0043, and the list of the
maintainer's RFC drafts names RFC-0043 instead of RFC-0038. Each
migration layer's PR still lists artifact sizes before and after; the
0.9 capability matrix stays after the migration.

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

* docs: the alignment doc links back to RFC-0036 §12.2 and names the merged RFCs

#207 merged RFC-0036 §12-§14 and asks that the alignment doc's matching
section be updated with this first layer and link back to the §12.2
replacement table. §0.7 now links RFC-0036 §12.2 in both places that
cited "#207 §12.2", and links the topic RFCs, all of them now merged
(RFC-0033, RFC-0036 §12-§14, RFC-0039 to RFC-0043).

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

* chore: satisfy the Rust 1.99 lints

Rust 1.99 deprecates AtomicU64::fetch_update and adds clippy::double_must_use, which rejects #[must_use] on functions returning an already must-use BoxStream.

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

* fix(ffi): include <cstring> in the C++ RAII test

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

* ci(flutter): build the ABI test fixtures and run the real-library tests

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

* fix(python): make every GIL-release entry reach a blocking native wait

Video now uses inline Vertex bytes so core never downloads from the loopback fixture, recorder close no longer stops a dropped handle during teardown, and catalogue.fetch is skipped with its reason because it refuses non-public sources before any I/O.

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

* fix(swift): qualify the iterator's Failure so Swift 6 compiles BoundedStream

Inside AsyncIterator, an unqualified Failure resolves to the AsyncIteratorProtocol associated type (any Error) under Swift 6.

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

* fix(kotlin): assert on the root cause where coroutine stack-trace recovery copies exceptions

With assertions enabled, kotlinx-coroutines recovers stack traces by copying exceptions across suspension points, so identity holds only for the root cause; the dispatcher test compares threads instead of names, which debug mode suffixes.

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

* fix(node): import the worker module by file URL so the test runs on Windows

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

* fix(go): link the C library statically on Linux again

The ops-layer Go binding dropped -Wl,-Bstatic, so go test on Linux needed libaimux_ffi.so on the loader path.

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

* chore(node): commit the napi-generated declarations

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

* test(flutter): check isolate responsiveness by ordering instead of tick counts

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

* ci: run the AI SDK pin script tests

scripts/fetch-aisdk.test.mjs and scripts/aisdk-fixtures/versions.test.mjs
ran nowhere. Both are offline: the fetch test serves its own registry on
127.0.0.1, and the versions test reads local JSON. They now run in the
generator drift job, which already runs for every code change. The
lock-file test skips until scripts/aisdk-fixtures/package-lock.json
exists and then holds the lock to VERSIONS.json.

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

* docs: note the retired contract fixtures in the RFCs and the changelog

RFC-0011, RFC-0013, RFC-0016 and RFC-0035 still pointed at
contract-tests/fixtures/wire-format.json, tool-call-repair.json and the
binding contract tests this layer deletes. Each gets a 后续变更 line
saying what no longer applies and what pins wire behaviour now, and
the changelog gets a Removed entry for the retired fixtures.

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

* test(flutter): resume the fixture's native sleep after EINTR

On Linux the Dart VM signals its threads, which cut the 180 ms nanosleep short so the slow call returned before the caller's timer ran.

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

* docs(ops): add the RFC-0039 design, coverage ledger, conformance matrix and upgrade guide

The alignment doc gains one registered-difference line per RFC-0039 difference and gap, pointing at the design doc.

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

* docs(web): note that cargo test rewrites the tracked node types

aimux-core's #[ts(export)] derives are not feature-gated, and
TS_RS_EXPORT_DIR points at bindings/node/src/types/, so a plain cargo
test rewrites that tracked directory. scripts/gen_ts_types.py is the
source of truth; the debt goes away when ts-rs retires with D8.

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

* test(openai): tool calls are flushed after an error frame

openai-chat-language-model.ts forwards an error chunk as an error part
(:561-568) and still flushes the tool-call tracker before finish
(:656-670). Nothing pinned that order here: the existing mid-stream
error test streams text only. The new test streams a tool-call start,
an error frame and [DONE], and requires tool-input-start, -delta, error,
tool-input-end, the tool call with its incomplete raw arguments, then a
finish whose reason is error.

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

* refactor(provider-utils): bound the tracker's id suffix search

unique_id searched `generated-1, generated-2, ...` without a bound and
ended in `expect`. The search only runs when `generated` is one of the
n ids in use, so one of `generated-1 ..= generated-n` is free; it now
searches that range, as streaming-tool-call-tracker.ts:468-485 does,
and cannot panic. The ids it returns are unchanged.

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

* test(core): restore the OpenAI output tool-argument tests

cc5446fa deleted the end-to-end tests of the OpenAI-compatible output
(fx4/p2 aimux-core/tests/tool_input_test.rs:401-804) together with the
V3 prompt tests around them. The converter unit tests in
openai_output.rs only feed hand-built parts to the converter, so three
contracts lost their only coverage: a repaired call shows in the
completion while the stream keeps the provider's name and deltas, a
repair that still fails keeps the original name and argument text on
both paths, and the stream's two-chunk shape when input deltas arrive.

They come back as aimux-core/tests/openai_output_tool_input_test.rs,
bodies unchanged; only the mock model moves to the [3/8] result types
(request / response on GenerateResult and StreamResult). One new test
completes the raw-text matrix for a known tool: truncated JSON, a JSON
string, non-JSON text, null and a wrong-typed object with whitespace,
each with and without input deltas, through both entry points.

The OpenAI output is an aimux extension (RFC-0035, RFC-0042 §5 items
7-8), so these tests pin aimux behaviour, not upstream behaviour.

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

* test(core): part metadata replays whole into providerOptions

Response messages copy a part's provider metadata into providerOptions
as one map (ai@7.0.127 to-response-messages.ts), and nothing pinned
that beyond a single known namespace. The new test feeds an unknown
namespace with nested nulls, an empty namespace and a known one through
the text path and the streamed reasoning path (metadata on the end
event) and requires the serialized map back unchanged, key order
included.

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

* test(ffi): restore the composite, frame-error and session-input tests

cc5446fa removed the FFI unit tests that need a mock language model
along with the V3 mocks themselves (fx4/p2 aimux-ffi/src/lib.rs
4178-4515 and 4837-4881). The entry points they cover are unchanged at
this layer, so the tests come back with only the mocks moved to the
current result types (request / response on GenerateResult and
StreamResult):

- a recoverable frame error is a data part on aimux_stream_text and is
  skipped on aimux_stream_text_as_openai, and both streams still end
  with on_done;
- aimux_router_new and aimux_moa_new build their models, reject bad
  JSON, a dead member or aggregator handle and a NULL array, treat an
  empty or null config as defaults, and allow zero MoA references;
- aimux_transcription_session_new rejects bad JSON, a non-transcription
  handle and a bad abort handle, and next_part rejects NULL out-params.

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

* fix(core): a stream that ends with Finish is traced as completed

stream_text's pump stops polling at the terminal part and drops the
provider stream, so TraceRecordingStream never saw end of stream for a
completed call. Its Drop then traced every completed stream_text call as
"stream dropped before completion" and the LCP lookup was skipped.

The wrapper now writes the record when Finish passes, with Finish's
usage. A natural end of stream without Finish and a drop mid-flight are
recorded as before, and a stream that is drained after Finish is still
recorded once.

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

* test(core): restore the TraceLayer suite

cc5446fa cut the TraceLayer half of cache_probe_test.rs only because it
stopped compiling after the Usage and GenerateResult type changes. The
ten tests come back on the current API: generate and stream records with
usage, TTFT and session chain; part errors, transport errors and failed
calls with their error text and counts; an early drop still recorded;
the LCP upper bound in aggregates; scope keys isolating providers that
share a model id; the R-2.2 route-affinity suppression and the C4-2
independent-evidence case.

The TTFT check now reads the value; the deleted test matched
"ttft_ms": in the JSONL, which a null also matches. Four new tests pin
the call id taken from the recording context, body id and headers read
from the result, a file part counted as first output, and the raw usage
object kept on the record.

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

* test(core): a traced call keeps the id of its recording

Under an enabled recorder, generate_text and stream_text through a
TraceLayer trace each call under its recording's id, and replaying that
recording sends the call the model saw.

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

* test(google): restore the inlineData-to-file tests

cc5446fa deleted the three Finding 1 tests when the file variants became
tuple variants, leaving their two Gemini image cassettes unreferenced.
They come back on the current types: one generated PNG with its full
base64 payload, text and image kept in order, and the streamed file part.

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

* test(core): restore the replay_with_model tests

cc5446fa deleted the four in-crate request-replay tests, so
replay_with_model had no test at this layer. They come back on the
current types: a recording is rebuilt and resent, overrides apply, bad
input options fail with a parse error, and differing provider options
miss the exact matcher.

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

* test(core): pin an abort after a finished step and repeated tool-call ids

Upstream's stream-text flush (stream-text.ts:1725-1757) rejects only
when no step was recorded; an abort after a recorded step resolves with
that step and null total usage. consume() does the same, and now says
why next to the Abort arm. stream_text never yields an abort between
FinishStep and Finish, so the test builds that order by replacing the
result's stream.

A repeated tool-call id behaves as upstream on both paths: streaming
forwards both calls and resolves a later provider result to the last
one (stream-language-model-call.ts:432, :705-707, :764); generating
maps both content parts to the first (convert-language-model-content.ts
:74-84). Two tests pin that.

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

* docs(alignment): register recording_context and deferred provider results as known gaps

CallOptions still carries recording_context, which §3.1 excludes from
call options; it leaves with the OperationRuntime and attempt scope.
A deferred provider tool result is accepted when a provider tool of the
same name is passed, wider than upstream's supportsDeferredResults; the
flag comes with the tool definitions. Both now sit in the §9.1 list.

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

* refactor(stream_text): the chunk loop has no arm that cannot run

The loop over model chunks relied on its own order twice:
`StreamStart` was handled by an `if let ... continue` before the match,
so its match arm was `unreachable!("handled above")`; and the fallback
arm converted the remaining parts with `map_payloads(|_| unreachable!(..),
|_| unreachable!(..))`, valid only because the tool call and approval
arms came first.

`StreamPart::split_payload` now splits a part into the variants that
carry a payload (tool call, approval request, reasoning file) and the
rest, whose payload types are `Infallible`. The loop matches on that:
each payload has its own arm, and the fallback widens the rest with
`match never {}`. `StreamStart` is a normal arm; the step-opening
preamble skips it with `matches!`. A new payload-carrying variant now
fails to compile instead of panicking at run time. Behaviour is
unchanged.

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

* test(provider-utils): the default-transport test keeps 127.0.0.1 off its proxy

the_default_transport_can_be_replaced swaps the process-wide default
transport for one that sends everything except localhost through a fake
proxy. Tests running at the same time (download_blob, the post_to_api
timeout test) reach their mock servers on 127.0.0.1 through that default,
so with many test threads their requests landed in the fake proxy and
failed. 127.0.0.1 is now on the swapped transport's no_proxy list too.

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

* docs(web): a blank key at a custom base_url goes out as an empty key

The README and RFC-0029 said such a call goes out without a key. The
key is an explicit empty string, as with the AI SDK's `apiKey: ''`:
no stored or environment key is used, and providers that always send
their credential header (OpenAI's `Authorization: Bearer`) send it
empty.

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

* test(provider-utils): pin the redirect cap and re-signing on a same-origin hop

Two redirect rules had no test: an API call follows at most ten hops
and fails on the eleventh with the redirect's status, and a hop that
stays on the request's origin goes through the signing transport again
and is signed for its own URL, never with the first request's
signature (RFC-0040 §3.2). With the signing clock fixed, the two
signatures can differ only because the path does.

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

* docs(rfc): RFC-0040 §3.2 follows the implemented redirect rule

The merged RFC-0040 says credentialed requests do not follow redirects
by default. API requests follow them, as the AI SDK's postToApi does,
with credentials locked to the request's origin: a hop off it keeps
only User-Agent, signing and token decorators add nothing, a hop back
does not restore them, and the chain stops at ten. §3.2 gets a
revision note saying so and where the code and tests are, and the
alignment doc's §5 registers the difference from both the RFC text and
upstream fetch.

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

* test(providers): a preset's base-URL and optional-key variables

Nothing tested a registry row's base_url_env or its optional key
variable directly. Four serial tests use the ollama row: the base URL
variable is read per request, not when the provider is created; an
explicit base_url wins over it; an unusable value fails the request
with an InvalidArgument naming the variable and sends nothing; the
optional key goes out as Bearer only when set and non-empty. The
existing keyless test becomes serial and clears OLLAMA_API_KEY so it
cannot see a key one of them set.

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

* test(mistral): model_length is a length finish

The mapping follows @ai-sdk/mistral 4.0.54, which has no test for it.
Generate and stream both report Length and keep the raw reason.

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

* test(providers): non-object input of a past tool call

One assistant message carries six past tool calls (null, a string, an
array, a number, a boolean and an object) through each factory. OpenAI
chat sends "{}" for every non-object, Groq and DeepSeek send
JSON.stringify(input), and Anthropic wraps it as rawInvalidInput, as
their upstream converters do.

RFC-0042 §6.2 named only Bedrock for rawInvalidInput; Anthropic's
toAnthropicToolInput applies the same rule.

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

* fix(anthropic): toolset member calls go back as upstream sends them

A computer-toolset member call in the history was converted differently
from @ai-sdk/anthropic 4.0.71 (convert-to-anthropic-prompt.ts:984-1004):

- the `action` was removed from the input; upstream sends the input as
  it is, `action` included;
- a call without a string `action` was dropped with a warning, which left
  its tool_result without a tool_use; upstream warns and still sends the
  call, named after the toolset.

Both now follow upstream. The rest of the toolset path is aligned too:
the toolset name is read from `providerOptions.anthropic.toolsetName`
only (`getAnthropicToolsetName`), a tool_result carries `toolset_name`
before `content`, a member call's metadata is `{ toolsetName, caller }`
in that order, and a streamed member input that secureJsonParse would
refuse (malformed, or with a prototype key) is kept raw rather than
getting the `action` injected.

The computer toolset had no CHANGELOG line; it has one now. New tests in
anthropic_toolset_test.rs cover the request, both response paths and a
full round trip.

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

* test(preset): the registry checks run on any input

The registry schema checks lived in load_registry, which read the
embedded provider_registry.json, so they could only be exercised by
editing that file. parse_registry takes the JSON text instead; the
process-wide table passes the embedded file. New unit tests feed it
rows that break each rule (duplicate or malformed name, non-http base
URL, unknown auth, missing key variable, placeholders without params, a
derived param with env/default, an explicit null, an unknown field) and
one valid pair of rows.

The by-name shim in provider.rs now names the PR that removes it: #265,
where the C ABI and the bindings move to the ops table and stop calling
it.

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

* fix(anthropic): provider tool args are checked as upstream checks them

@ai-sdk/anthropic 4.0.71 prepareTools runs validateTypes on the args of
ten provider-defined tools: the computer toolset, the advisor,
text_editor_20250728 and the three web_fetch and three web_search
versions (anthropic-prepare-tools.ts:197-445). The Anthropic and Vertex
paths checked none of them and sent whatever came in; Bedrock checked
six, in a copy of its own.

The check now lives once, next to the tool definitions, and runs in
prepareTools' place for all three providers: args that do not match
fail the call with InvalidArgument before any request, and the other
tools still pass their args through unchecked. Bedrock keeps dropping
the three tools it does not support before the check, as upstream does.
prepare_tools and prepare_tools_with_provider return a Result.

The advisor regression test now passes the model the cassette recorded,
since an advisor without one is refused. New tests in
anthropic_provider_tool_args_test.rs and bedrock_upstream_e2e_test.rs.

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

* test(provider-utils): say how the botocore SigV4 pins were made

The pinned signatures match botocore's SigV4Auth only when the request
carries X-Amz-Content-SHA256 before signing; SigV4Auth adds that header
on its own for S3 alone. The test's doc comment now says so, so the
pins can be recomputed.

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

* docs(core): a tool result carries only output

The changelog still said a tool result reads the earlier result and
is_error fields; this layer removed that, so the entry now says a tool
result carries only the AI SDK output object. ToolResultPart's upstream
reference points at content-part.ts:218-241, and the reasoning parts
drop an #[allow(unused)] that suppressed nothing: their provider
metadata reaches the response messages and the stream aggregate.

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

* feat(anthropic): decision_model is upstream's evaluationModel

@ai-sdk/anthropic 4.0.71 defines evaluationModel as an
EvaluationLanguageModel over the Messages model, and the registry's
decision lookup falls back to it, so "anthropic:<id>" resolves upstream
while aimux returned NoSuchModel. decision_model now returns the same
DecisionLanguageModel adapter Google uses, with provider
"{name}.evaluation". The alignment doc's §3.1 names all three adapters.

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

* test(google): restore the thought-signature replay tests

[1/8] deleted the node test and the Rust request-body test for issue
#9: a Gemini thinking model rejects the next turn with HTTP 400 unless
the functionCall's thoughtSignature comes back beside it. What remained
checked only the parse direction. Three request-body tests bring the
replay direction back on the V4 prompt shape:

- providerOptions.google.thoughtSignature on a tool call is sent as a
  sibling of functionCall in the same part
  (convert-to-google-messages.ts:404-411);
- an unsigned call before Gemini 3 is sent without the field;
- on Gemini 3 an unsigned call gets skip_thought_signature_validator and
  a signed one keeps its signature (convert-to-google-messages.ts
  :540-552).

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

* test(providers): a failed decision is recorded and replays as the failure

"openai:<id>" with a refusal and "vllm:<id>" with a skipped question are
resolved through the registry under a RingRecorder. Each fails with the
API's message after one request, leaves one recording with outcome
status Error, no decision result and no key, and that recording replays
offline as the same failure rather than an answer.

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

* fix(core): request options reject unknown keys

[8/8] switched the request options to camelCase JSON but left them
lenient, so a key written the aimux 0.5 way (max_retries,
timeout.total_ms, fail_mode) silently fell back to the default instead
of failing. RFC-0042 §7.2 asks for unknown request fields to be
rejected.

GenerateTextOptions, TimeoutConfiguration, the options of the other
operations (embedding, speech, image, transcription, reranking, video
and its poll settings, search, file upload, decide), MoaConfig and the
C ABI's router and transcription-session configs now deny unknown
fields, so the error names the key. CallOptions stays lenient: a
recording keeps the call options and the operation settings in one
object, and replay reads both from it. Replay first drops the
null-valued keys of a recorded timeout, because aimux 0.5 wrote every
unset timeout key as null and such a key carries nothing.

The Node and Python bindings had three bugs of the same kind:
- their router config still read provider_name / model_id, so the JSON
  that names the router through the C ABI was ignored there. It is now
  camelCase and strict like the C ABI's, and so is their transcription
  session config, which still read input_audio_format;
- transcribe filled its placeholder as media_type, so any opts_json
  failed with "missing field `mediaType`". The code and its unit test
  now share one placeholder list;
- test_gil.py passed snake_case LIMITS, so its timeout guard was gone.

The bindings' Rust unit tests still used snake_case keys and never ran
in CI, because the crates are not workspace members. They are fixed,
and the node-binding and python-binding jobs now run cargo test --lib.
The Node test for a per-call body_overrides now expects the rejection,
and the Java SearchModel example uses maxResults.

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

* docs(core): replay's question types and the video status key

MockDecisionReplayModel reports every question type because a recording
holds no capability snapshot; decide rejects an unsupported type before
recording, so replay fails such a request with no matching recording
instead of answering it.

The idempotency key aimux mints goes on do_start only, while do_status
gets the caller's headers, as the AI SDK does.

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

* fix(core): a prompt part with an unknown type names it

Every tagged union already rejects an unknown `type` with "unknown
variant `x`, expected one of ...", as RFC-0042 §7.2 asks. The untagged
wrappers around them lost that error: ModelPrompt, UserContent and
AssistantContent tried each variant in turn and reported only "data did
not match any variant of untagged enum ModelPrompt", which is all a C,
Node or Python host saw for a prompt with a misspelled part type.

FilePartData had a worse case. As an untagged Tagged | String |
Reference, it read {"type": "bogus", ...} or {"type": "URL", ...} as a
provider reference whose provider is "type". The AI SDK never does
that: isProviderReference requires !('type' in data) and
isTaggedFileData takes the object as tagged data.

The four types, and ReasoningFilePartData, now deserialize by JSON
kind: a string is the text or string form, an array is read as the
parts or messages, and an object with a `type` key is read as tagged
file data, so each element keeps its own error. Serialization and the
generated TypeScript types are unchanged; only Deserialize is written
by hand.

New tests pin the "unknown variant" error for every tagged union, for
the wrappers, for file data inside a prompt, and at the C ABI.

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

* docs(core): cite upstream for the snake_case video frame type

VideoFrameType is the one snake_case literal pair left in the public
JSON, and it is upstream's own: @ai-sdk/provider@4.0.22
video-model-v4-frame-image.ts:9 declares 'first_frame' | 'last_frame',
and ai@7.0.127 generate-video.ts:814 compares against it. The doc
comment now says so instead of "Aligned with V4", and a test pins both
values and rejects the camelCase spelling. VideoFrameType.ts is
regenerated for the new comment.

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

* test(anthropic): compare usage JSON with what the AI SDK wrote

The usage shape (camelCase, both detail objects always present, unknown
counts absent, no raw on the total) was pinned only against hand-written
JSON. The recorded fixtures hold the AI SDK's own result.usage, which
in ai@7.0.127 is totalUsage (generate-text.ts:1797-1799), built by
addLanguageModelUsage (types/usage.ts:122-155).

assert_generate_result now sums the one step's usage the same way and
compares the whole JSON with the recorded value, so a drift in key
names, empty detail objects or raw shows up against upstream output.

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

* refactor(types)!: describe the wire types with schemars instead of ts-rs

Every type reachable from the ops manifest derives JsonSchema; codecs a schema cannot express are marked with x-aimux-codec. serde_json::Value stays the one opaque JSON type. ts-rs is removed from aimux-core.

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

* test(aimux-core): replace the in-test legacy json_repair port with recorded cases

The differential test kept a 430-line copy of the line-for-line port of
fix-json.ts / parse-partial-json.ts as its oracle. Record its verdict
instead: 740 inputs (one shortest per outcome/ending bucket out of
300 000 generated ones, plus hand-picked edges) with the fix_json output
and the PartialJson outcome, in fixtures/json_repair_cases.jsonl. The
upstream cases stay in util_test.rs. 14 hand mutations of json_repair.rs
are all caught.

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

* docs(core): say why json_repair stays public

Core's own paths never repair a final tool-call input (RFC-0042 §3.2).
The module stays public because the AI SDK exports parsePartialJson, for
callers that render streamed partial JSON.

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

* refactor(providers): keep vendor endpoints out of shared and openai_compatible

Making provider config plain data left the shared layers importing vendor
modules: shared's EndpointSource named the Vertex, Bedrock and Polly
endpoints and called google::supported_urls, and openai_compatible's
BaseUrl and fetch headers named the Vertex partner types and carried
Alibaba and Moonshot AI usage variants.

The shared layer now defines the extension points and the vendor modules
implement them on their own plain-data structs: ResolveEndpoint (with
HttpEndpoint for the common case) in shared, ResolveBaseUrl and
ResolveFetchHeaders in openai_compatible. The Gemini supported-URL patterns
are passed as a function of the base URL and model id. The Alibaba and
Moonshot AI usage converters move to preset.rs, which owns those registry
rows, and reach the chat model through the existing convert_usage hook, so
UsageDialect is gone.

ChatSettings::error_structure is a capturing Arc<dyn Fn> again, the same
shape as before and as convert_usage next to it.

check_provider_boundaries.sh gains a rule that fails when code in shared/
or openai_compatible/ names a crate module other than the shared layers,
the preset registry and runtime_decision.

Behaviour is unchanged.

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

* test(searxng): the instance URL comes from SEARXNG_URL per request

Covers the BaseUrl::Env path of the config-as-data change: set, the
request goes to that instance; unset, the request fails with LoadSetting
and nothing is sent. Also note in the changelog that PresetDescriptor can
no longer be built with a struct literal outside the crate.

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

* refactor(providers)!: remove the by-name provider shim the bindings no longer use

Bindings create providers through the ops layer, so aimux_providers::provider and its JSON options test have no callers left.

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

* refactor(provider-utils): PipeThrough is a Stream over one reused queue

pipe_through ran the transformer inside an async_stream generator and
moved the controller's queue out with std::mem::take after every call, so
each chunk allocated a fresh Vec. It now returns PipeThrough, a plain struct
that implements futures::Stream: it hands out queued parts from one
VecDeque that is kept for the whole stream, pulls the next input chunk only
when the queue is empty, and calls flush once the input ends unless the
transformer closed the stream. start runs when the stream is built, as the
WHATWG constructor does; nothing it emits changes.

TransformStreamController gains new, drain and is_closed so a transformer
can be driven and checked synchronously, without a stream.

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

* refactor(providers): chat streams share ChatToolCalls, no tracker type

StreamingToolCallTracker lived in aimux-provider-utils behind a builder,
a TypeValidation enum, a TrackerError type and a caller-owned out-vector
that every chat stream drained after each call. Its only users are the five
OpenAI-style chat streams, so it moves next to them as crate::stream's
ChatToolCalls: a Vec of in-progress calls plus the id generator, whose
delta method emits straight into the stream's controller and ends the
stream with InvalidResponseData itself. The vendor's type rule is a plain
invalid_type flag the stream computes from its own wire type (OpenAI:
'function' when present; Groq: 'function' required), so no rule enum is
left. The matching table, the 5.0.55 empty-arguments handling, the
dropped ambiguous deltas, id de-duplication and flush ordering are
unchanged, and so are the parts every stream emits.

The five chunk handlers stay separate Transformers: they differ in error
handling, the order of reasoning, text and tool parts, response-metadata
rules and finish metadata, so a shared one would be a set of flags. What
they do share now is the read-only StreamContext (warnings, raw-chunk
flag, URL, request body, response headers) with start and raw helpers,
and their accumulated state derives Default, so a constructor lists only
what the request decides.

The tracker's upstream test suite and the T3 differential fixture are
ported case for case to drive the streams' transform and flush directly:
untyped cases run on Mistral (optional index, injected ids), 'if-present'
on OpenAI, 'required' on Groq and extractMetadata on the
OpenAI-compatible thought signatures. The argument-state port moves with
its only caller.

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

* refactor(providers): vendor streams share StreamContext and Emit

Every vendor stream struct carried its own copy of the request context
(warnings, the raw-chunk flag, the URL, request body and response headers
an in-stream error reports, and for some the id generator) under
per-vendor names, and every constructor listed each accumulated field a
second time with its starting value. The context is now one StreamContext
field (with start and raw helpers, and the model's generate_id), and the
streams derive Default, so a constructor names only what the request
decides and ends with ..Default::default(). Where a starting value was not
a default (finish reason 'other', a first-chunk flag set to true) the
field becomes an Option or an inverted flag that the flush reads the same
way.

The rest of the repetition goes too: the parts emitted without provider
metadata (text, reasoning and tool-input start/delta/end) are written
through the Emit extension trait on the controller, one line each instead
of a five-line struct literal; Controller names
TransformStreamController<StreamPart>, so the transformer signatures fit
on one line; and FINISH_OTHER / FINISH_ERROR replace the finish-reason
literals without a raw reason, in the streams and the generate paths next
to them.

Google and Vertex keep separate streams. The emitted parts are unchanged.

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

* test(providers): drive chat and Anthropic stream transformers synchronously

Vendor streams are plain Transformer structs now, so their behaviour can be
pinned without HTTP or SSE framing. A small crate-internal helper,
stream::testing::run, calls start, transform per chunk and flush exactly as
pipe_through does (stopping once the controller is closed) and renders each
emitted item as a short line.

The OpenAI chat stream is covered for interleaved tool calls (deltas in
arrival order, end and tool-call parts in index order at flush), an error
frame (error part, then text-end and an Error finish), a flush that closes
open reasoning and text blocks, and a non-function tool type that errors the
stream without a flush. The Anthropic Messages stream is covered for
interleaved text and tool_use blocks ending at message_stop (later events are
not read), a mid-stream overloaded error, and a flush without message_stop,
which emits no finish part.

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

* chore(providers): let shared layers use the stream module in the boundary check

The vendor-reference rule for shared/ and openai_compatible/ predates the
crate-internal stream module that the chat streams now share; stream is a
shared layer, not a vendor.

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

* docs: say where the chat streams keep their tool-call state

RFC-0042 T2 and §4 placed the accumulation state inside provider-utils.
After the tracker removal in this layer it is a crate-private field of
the OpenAI-chat family's stream Transformers in aimux-providers/src/stream/.
Name that location, update the alignment doc rows that still said the
tracker moves into aimux-provider-utils, and register the structural
difference from @ai-sdk/provider-utils in §5.

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

* test(provider-utils): nothing is emitted after a transform stream ended

Every vendor stream relies on the controller dropping what is emitted
after terminate() or error(); cover it directly, and name the AI SDK /
WHATWG counterpart of enqueue_error, error and terminate in their docs.

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

* feat(codegen)!: derive every op type schema from Rust and normalize it into a language IR

Op descriptors reference types through TypeRef, the manifest embeds input and output schema graphs, the hand-written control schemas are gone, and aimux-codegen writes schema/types and a small IR for language emitters. contract-tests/dto holds round-trip vectors seeded from existing fixtures, checked against Rust.

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

* feat(codegen): run registered language emitters over the IR

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

* fix(codegen): let emitters own their files and mark closed objects in the IR

Emitters declare the paths they own, so --check covers binding DTO files.
Structs and tagged variants carry whether the schema forbids unknown keys.
Recordings and trace records become output roots because bindings read them
outside any op; header pairs are described as two-element string arrays.
Scalar root vectors are dropped: they exercised no generated type.

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

* chore(scripts): one vector runner per line

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

* feat(go)!: generate the Go DTOs from the IR

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

* feat(swift)!: generate the Swift DTOs from the IR

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

* feat(dart)!: generate the Dart DTOs from the IR

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

* feat(node)!: generate the TypeScript DTOs from the IR

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

* feat(java)!: generate the Java DTOs from the IR

Kotlin uses the generated Java classes.

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

* feat(python)!: generate the Python DTOs from the IR

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

* refactor(go): generate plain encoding/json structs

Optional fields are pointers or nil-able slices and maps tagged omitzero;
Field[T] remains only for presence-coded fields.

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

* refactor(dart): generate plain immutable classes with native ints

Integers above 2^63-1 do not fit Dart's int; the vector check counts them
as a registered difference.

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

* refactor(python): name the sized integer types once and drop doubled Optional

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

* refactor(java): generate Jackson-annotated classes with native integers

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

* fix(ops): terminate and drop an operation safely after its runtime shut down

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

* test(ops): error envelopes never carry a missing or invalid secret

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

* docs(rfc-0039): record the accepted D8 and schema-2 order, the new amendments and documented semantics

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

* fix(ops)!: one schema per control object, poll mode for operation.next, digest in the manifest

The manifest's control schemas are the only definition; codegen copies them
instead of composing contradicting ones. operation.next is a poll op, the
descriptor field is sourceSymbolInventory (RFC-0039 §2.2), and the manifest
carries its own digest so bindings can pin it.

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

* fix(codegen): write the Dart constants under lib/ so package imports resolve

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

* fix(bindings): pin opsVersion and manifest digest exactly, name ops by constant, reclaim forgotten Java handles

Go, Java, Kotlin, Swift and Flutter require the exact generated OPS_VERSION
and the manifest's digest. Facades name ops through the generated constants.
Java handles a caller never closed are reclaimed through aimux_handle_finalizer
from a phantom-reference queue. Java bounds the callback frame copy like the
other bindings.

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

* fix(node,python): replay the shared vectors, reject a full push queue, pin the manifest exactly

Node and Python replay all eleven shared operation scenarios through their
real dispatch path in their normal test commands. Node's operation.push
rejects with ResourceLimit when the queue is full, as Python and C do. Node
reads ABI, ops version and digest from its generated constants, now under
src/generated so the compiled package keeps its layout.

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

* ci: drop steps that claimed shared-vector checks, one iOS symbol list, add ASan and Linux ARM64 jobs

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

* docs(rfc-0039): every binding now pins the manifest digest

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

* fix(ops,node): one handle-id rule shared by DTO decoding, the registry and Node

Registry and Node parsed handle ids by hand and rejected leading zeros, unlike
HandleId and the manifest schema. All three now use HandleId's FromStr.

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

* docs(rfc-0039): the series' last PR removes schema-2 and older recording reads

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

* fix(core): fix_json steps through UTF-16 code units like upstream

A character outside the BMP is two code units upstream and one char here,
so "[1😀" was repaired to "[1]" where upstream gives "[1😀]". Stepping
such a character twice reproduces upstream's two steps: neither half is a
JSON token, and a mark on either keeps the whole character.

Against the same 1,000,000 inputs run through ai@7.0.127 fix-json.ts, the
63 fix_json differences are gone; fix_json now matches upstream on all of
them.

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

* test(dataforseo): the login comes from DATAFORSEO_LOGIN per request

* feat(core): the recording context has an operation scope

RecordingContext::scope runs a future with a recording context current,
RecordingContext::scope_stream keeps it current on every poll of a stream,
and RecordingContext::current reads it, the same way OperationSettings
carries the retry count, timeout and session id (operation.rs). A None
scope hides an outer context.

This is the mechanism the model call options' recording_context moves to:
the alignment document's call options carry no recording context (§3.1),
and the transport reads its diagnostic scope from the operation (§3.5).

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

* docs(contributing): say what .agents/ holds and in which language

* refactor(core)!: the recording context is an operation value, not a call option

CallOptions and DecisionCallOptions lose recording_context; the V4 call
options carry no recording context (@ai-sdk/provider@4.0.21
language-model-v4-call-options.ts:8-138), and the alignment document's
call options carry none either (§3.1). This was the last item of that kind
in its known-gaps list.

generate_text, stream_text and decide run the model call in
RecordingContext::scope; stream_text's pump task polls the provider stream
through RecordingContext::scope_stream, since a spawned task does not
inherit the scope. The router and MoA scope each child step in the child
context they used to put on the child's options (step_context), so every
exchange keeps its call id, step label and attempt numbers.
HttpRequest::new, the Anthropic request builder and the trace layer read
RecordingContext::current(). The JSON is unchanged: the field was never
serialized.

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

* fix(core)!: a deferred tool result needs a provider tool that supports it

A provider tool result whose call is not in the response is accepted only
for a provider tool of that name that sets supportsDeferredResults, as in
ai@7.0.127 convert-language-model-content.ts:96-103; before, any provider
tool of that name was enough.

ProviderTool gains the AI SDK tool's optional supportsDeferredResults
(provider-utils@5.0.55 src/types/tool.ts:316-326), which upstream's tool
factories set: Anthropic web_search, web_fetch, code_execution 20250825 and
20260120, tool_search and advisor, and openai.programmatic_tool_calling. It
is omitted from the JSON when absent and is not sent to the provider. The
alignment document no longer lists the wider rule as a gap.

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

* docs(core): the replay key and input record no longer name a recording_context field

CallOptions has no recording_context since the recording context became an
operation scope; the comments on canonical_call_key, call_options_match and
InputRecord.options list the runtime fields that remain.

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

* build(core): ts-rs exports run only from the generator

aimux-core's #[ts(export)] attributes were ungated, so every plain
cargo test rewrote bindings/node/src/types through the TS_RS_EXPORT_DIR
in .cargo/config.toml. aimux-core now has the same ts-export feature as
tools/aimux-web (tools/aimux-web/Cargo.toml:21-23), and each of its 158
exports is #[cfg_attr(feature = "ts-export", ts(export))].

scripts/gen_ts_types.py enables the feature for the core target, and
--check also fails on any ts(export attribute outside that gate in either
crate's source, printing file:line. The .cargo/config.toml comment and the
web README now say that cargo test writes nothing and the generator is
the only writer. gen_ts_types.py --check passes: 159 node files and 8 web
files are unchanged.

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

* fix(provider-utils): the tool call tracker generates ids as upstream

StreamingToolCallTracker::new() used "tool-call" as its id generator, so
an OpenAI chat stream without wire ids produced tool-call, tool-call-1,
and so on. Upstream defaults to the random generator
(provider-utils@5.0.55 src/streaming-tool-call-tracker.ts:114) and uses
"tool-call" only when a generator returns a blank id (:461). new() now
defaults to generate_id (provider-utils@5.0.55 src/generate-id.ts), and
FALLBACK_TOOL_CALL_ID is only the blank-output fallback.

generate_id.rs is the same file a later layer of this stack adds, so the
restack has nothing to merge there. The tracker tests that assert ids pass
their generator explicitly; a new test checks that new() gives a call
without a wire id a 16-character id.

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

* feat(bindings): typed provider tools carry supportsDeferredResults

The AI SDK's provider tool has an optional supportsDeferredResults flag
(provider-utils@5.0.55 src/types/tool.ts:316-326). The typed ProviderTool
in Java, Kotlin, Swift, Python and Flutter gains the same optional field,
written as "supportsDeferredResults" and omitted from JSON when absent.
Go has no typed ProviderTool, and the Node type is generated from the
core. A round-trip test in each binding covers the field absent, true and
false.

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

* chore(node): regenerate InputRecord.ts for the corrected options comment

The InputRecord.options comment no longer names a recording_context field
(docs(core) commit before); the generated Node type carries the same text.

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

* refactor(node,python): derive handle bookkeeping from the registry, not an op allowlist

Node's realm kept a hand-copied list of 22 handle-creating ops plus a list of
JSON keys to scan. A call that finishes after cleanup now releases the owner's
handles directly, and an operation's signal is kept when the result is a bare
handle id the registry knows as an Operation. handle.drop is named once in
aimux-ops and shared by dispatch, Node and Python.

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

* refactor(flutter): use generated ABI, ops and symbol names everywhere

The load check compares the ABI major with ops.ABI_VERSION, tests and the
example use ops.OP_* and the header macros, and check_symbols.sh derives its
required list from the generated header and the transport's EXPORTs.

* refactor(bindings): drive Swift contract tests from generated ops constants

* refactor(bindings): use generated Go op and ABI constants

* refactor(bindings): use generated protocol constants in JVM tests

* refactor(bindings): use generated Node operation constants

* refactor(bindings): use generated Python operation constants

* fix(core)!: MoA failMode and the recording operation are kebab-case

MoaFailMode (aimux-core/src/moa.rs) and RecordingOperation
(aimux-core/src/recording.rs) used snake_case values in the
public JSON; the AI SDK spells multi-word literals in kebab-case (for
example type: 'tool-call' at provider@4.0.22
src/language-model/v4/language-model-v4-tool-call.ts:7). failMode is now
"best-effort" | "fail-fast" and the operation is "language-model" |
"decision".

Recording output does not change: the language-model operation is never
written (skip_serializing_if = is_language_model), and no fixture or
recording spells "language_model". The FFI header and docs, the Java doc
and the tests use the new values; new tests check that MoaConfig accepts
"fail-fast" and rejects "fail_fast", and that RecordingOperation
round-trips "language-model". RecordingOperation.ts is regenerated with
scripts/gen_ts_types.py.

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

* refactor(web)!: take the wire types from the generated TypeScript

Console Wire* DTOs live in the aimux-web lib with JsonSchema. The existing
TypeScript emitter writes them to web/src/types/generated/. ts-rs and
scripts/gen_ts_types.py are gone; CI checks DTO vectors per binding job.

* fix(ci): regenerate the providers doc, tolerate Windows addon locks, avoid SIGPIPE in the iOS symbol check

The by-name shim's removal left its comment above `pub mod replay`, which the
providers doc generator read as a section title; the stale comment goes.
Windows keeps the loaded contract addon locked until exit, so its temp folder
may not be removable there. The iOS check grepped awk's output through a
pipe; grep -q exiting early made awk fail under pipefail.

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

* feat(ops): list the provider packages in the manifest and runtime.describe

RFC-0039 §10.1 has bindings take per-provider construction lists from the
manifest. The list is the factory's provider names plus jev, which
provider.create builds itself, so it is exactly what provider…
eric8810 pushed a commit that referenced this pull request Oct 20, 2026
The two jobs #265 added ran on every pull request and were missing from
CI result's needs, so a sanitizer failure did not block a merge. Gate them
on the change map like rust-test and require them in CI result.


Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS

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.

3 participants