diff --git a/.agents/native-rust/PROFILE.md b/.agents/native-rust/PROFILE.md new file mode 100644 index 00000000..173112f6 --- /dev/null +++ b/.agents/native-rust/PROFILE.md @@ -0,0 +1,85 @@ +# aimux 配置(native-rust profile) + +`ts-rust-port` 和 `rust-native-rewrite` 共用。只写不看仓库就不知道、又不会很快过期的事:不写行号、提交号和待办清单,那些放 issue 和 PR。 + +## 上游和映射 + +- vercel/ai monorepo;锁定版本以 `fixtures/aisdk/VERSIONS.json` 为准,不在里面的包用 `docs/aisdk-architecture-alignment.md` 头部的版本,再没有就先问。 +- 源码:`https://raw.githubusercontent.com/vercel/ai//packages/<目录>/src/<文件>`;测试是同目录的 `*.test.ts`。 + +| 上游 | aimux | +|---|---| +| `ai` | `aimux-core/src`(如 `json_repair.rs` ← `util/fix-json.ts` + `parse-partial-json.ts`) | +| `@ai-sdk/provider` | `aimux-core/src` 的 V4 类型 | +| `@ai-sdk/provider-utils` | `aimux-provider-utils/src`,kebab → snake | +| `@ai-sdk/` | `aimux-providers/src//`;`google-vertex` → `vertex/`,`amazon-bedrock` → `bedrock/`;按模块头注释的 `packages//src/.ts` 找 | +| 无上游 | `aimux-ffi`、`bindings/`、`recording.rs`/`replay.rs`、`decision_*`:只能新旧差分 | + +OpenAI-compatible preset 来自 `provider_registry.json` 的运行时 descriptor 表(D12),没有逐厂商代码。 + +## 设计文档 + +`docs/aisdk-architecture-alignment.md`:决策记录 D1–D31,§0.4–0.5,§5(定下来的不像 Rust 的写法),§9.2–9.3(构建检查、行为验收)。碰到消息类型或 wire 再读第二部分。 + +## 不能动的面 + +| 面 | 守护 | +|---|---| +| 公开 JSON:camelCase、kebab-case `type`、base64 字节、缺失省略、null 保留 | 绑定测试、`cargo test --workspace` | +| ts-rs 生成的 d.ts(不新增 `#[derive(TS)]`,要退役) | `python3 scripts/gen_ts_types.py --check` | +| C ABI(`aimux-ffi`,各语言绑定依赖) | `aimux-ffi/tests`、`scripts/local-ci.sh` | +| Node 绑定 | 构建后 `git diff --exit-code -- bindings/node/index.js bindings/node/index.d.ts` | +| 录制格式、cassette 回放 | 回放测试 | +| 错误 JSON(AI SDK 的 `name` 标签) | 原生化不改 | +| `docs/api/providers.md` | `python3 scripts/gen_providers_doc.py --check` | + +## 预言机 + +1. 上游录制:`fixtures/aisdk//*.json`,`aimux-providers/tests/*_factory_test.rs` 回放(URL、headers、body、结果、流事件、错误分类)。 +2. 搬来的上游单元测试,例如 `aimux-core/tests/util_test.rs` 的 fix-json 用例。 +3. node 跑锁定版本上游(Node 22 `--experimental-strip-types`),读写 JSONL。 +4. cassette 回放:`aimux-providers/tests/cassettes/`,不联网。 +5. 新旧差分只在改动期间存在;入库的是它记下的结果(例:`aimux-core/tests/fixtures/json_repair_cases.jsonl`),不是旧实现。跨构建差分的探针不入库,PR 描述写位置和命令。进程级计数器(`source-{n}` 之类)比较前归一化。 + +## 检查(CI 对每个 PR 跑) + +```sh +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps +cargo test --workspace # 会改写 bindings/node/src/types/*.ts,跑完 checkout 回去 +python3 scripts/gen_ts_types.py --check +python3 scripts/gen_providers_doc.py --check +bash scripts/check_provider_boundaries.sh +``` + +碰到 napi、FFI 时加跑 `scripts/local-ci.sh` 对应的节。每个 worktree 用自己的 `CARGO_TARGET_DIR`。 + +## 项目决定 + +- wire 跟 AI SDK 的 JSON 形态;wire 变化单独成 PR,同步所有绑定。 +- 可选方法用 `Option>`(§5、D22);provider 可调用 `call(model_id)`;默认 provider 访问不可失败,显式工厂返回 `Result`。 +- 设置求值时机(工厂、取模型、请求)逐包跟上游(D10);headers 每次请求求值。 +- 不用 provider 名字判断厂商(D8);providerOptions 的键在各包 `options.rs` 只写一次(D9);retry 只在调用层(D14);core 单步,不跑 agent loop(D25)。 +- `do_generate`/`do_stream` 保持 `async_trait`(registry、FFI 要 `dyn`)。 +- 分层单向:`shared/`、`openai_compatible/` 不引用具体厂商模块。跨模块扩展点由共享层定义 trait,厂商用自己的纯数据 struct 实现;包内有限几种情况用包内 enum(`check_provider_boundaries.sh` 守)。 +- 流:每个厂商的流是一个实现 `Transformer` 的具名 struct;工具调用的累积是流 struct 自己的字段,没有共享的 tracker 类型(和上游 provider-utils 结构不同,行为照上游)。 +- 保留 openai-compatible 的 `transformRequestBody`、`metadataExtractor`、`supportsMultiPartToolContent` 和 `specification_version`。 +- 栈内 breaking 直接替换,不留 deprecated 别名,同一 PR 改完调用方和绑定(D31)。 +- 范围只做结构清理、行为对齐和缺陷修复,不新增对外能力(§0.4)。 + +## 陷阱 + +- 数字:上游是 JS number,用量、价格、max_tokens 用整数解析;写回 JSON 的 `f64` 会把 `12` 变成 `12.0`,加 golden 测试。 +- null 和缺失:上游区分,`Option` + `skip_serializing_if` 会合并;wire DTO 保留 null。 +- SSE 和 event-stream 可以在任意字节切开;重写分帧时用同一字节流的所有切法差分。 +- headers:settings → 单次调用 → 鉴权,后层覆盖;`None` 删除;名字小写。 +- JS promise 创建就跑,Rust future 不 poll 不跑;不 await 的 promise 移植时改成 spawn 或删掉。 +- UTF-16:JS 的长度和下标按码元;截断、长度校验用 emoji 和中文对一次上游。 + +## 工作约束 + +- 每个 PR 一个方向:一个模块或一种模式。标题 `[n/m] refactor: …`,分支名和标题一致,开 PR 前定好。 +- 不改写别人已推送的分支;PR 先 Draft,CI 绿了再 Ready。 +- 生产代码不新增 `unwrap`/`expect`/`panic`(§9.2);凭据类型的 `Debug` 脱敏。 +- 参照库:rig(`0xPlaygrounds/rig`)、async-openai(`64bit/async-openai`)。 diff --git a/.agents/native-rust/README.md b/.agents/native-rust/README.md new file mode 100644 index 00000000..beeb9480 --- /dev/null +++ b/.agents/native-rust/README.md @@ -0,0 +1,8 @@ +# Agent notes for native-Rust work + +For coding agents (and people) doing two kinds of work on aimux: + +- aligning a Rust module with its pinned AI SDK upstream: `../skills/ts-rust-port/SKILL.md`; +- rewriting TS-shaped Rust into native Rust without changing behaviour: `../skills/rust-native-rewrite/SKILL.md`. + +`PROFILE.md` holds the repository facts both read. Nothing here is built or run by CI. The skills and profile are written in Chinese. diff --git a/.agents/skills/rust-native-rewrite/SKILL.md b/.agents/skills/rust-native-rewrite/SKILL.md new file mode 100644 index 00000000..da39ada4 --- /dev/null +++ b/.agents/skills/rust-native-rewrite/SKILL.md @@ -0,0 +1,79 @@ +--- +name: rust-native-rewrite +description: Rewrite TS-shaped Rust into native Rust within one repo, keeping behaviour and wire format, proven by an old-vs-new differential (in-process for one module, cross-build for a cross-cutting refactor). +--- + +# 把 TS 味的 Rust 改成原生 Rust + +单仓库,不需要上游源码;行为由新旧差分证明。要和上游本身对比,用 `ts-rust-port`。 +仓库事实在 `.agents/native-rust/PROFILE.md`(下称配置);本文只写流程、纪律和写法对照。 + +用法:给一个文件或模块;或 `pattern <模式>`(跨模块,如“回调改成 trait”);或 `review [ref]`(只审查)。 + +## 纪律 + +1. 只改结构,不改行为;看到的 bug 记下来,另开 PR。 +2. 一个 PR 一种模式或一个模块。配置“项目决定”里的写法不改。 +3. 动手前在基线上跑一遍配置的检查,记下每个测试二进制的通过数;改完逐个比,多出来的只能是新测试,改过的已有测试逐个说明。 +4. 差分报告写分母和覆盖面:“比较 N 个,M 处差异;没比的是 X,因为 Y”。失败一次修一个,到 0 为止;0 之后做变异检查。 +5. 先测稳定性:同一构建跑两次,会变的值(计数器、UUID、时间、签名)先屏蔽。 +6. 改完要比原来短或一样长。为了“原生”多出来的样板(字段写两遍、套一层通用框架)不算改进。 +7. 去掉闭包时不能颠倒依赖:共享层不为了一个闭合 enum 去 import 具体模块;跨模块扩展点用共享层的 trait,具体模块用纯数据 struct 实现。 +8. 跑完看 `git status`;临时拷进去的探针和生成器改写的文件不进提交。不自动推送。 + +## 流程 + +1. **选目标**:对目标跑下面的痕迹扫描,剔除“项目决定”和“不能动的面”挡住的;剩下为空就报告并停下。目标没有测试先补测试,单独提交。 +2. **契约**(commit body 或 PR 描述):改哪些、不能变什么、允许的差异、怎么测、是否 breaking。只有要 breaking、要动“不能动的面”或违背“项目决定”时先等用户回复。 +3. **差分**: + - 一个模块、纯函数或状态机:进程内差分。改动期间把旧实现复制进测试,随机生成器默认 2 万、可放大到 10^6;到 0 后,把有代表性的输入和旧实现的结果录成 fixture 入库,删掉旧实现副本。 + - 跨很多文件(配置、流、请求构造):跨构建差分。新旧两个 worktree 各用自己的 target;同一个探针测试文件放两边,在最底层传输(`fetch`)注入一个按录制回答、记录请求的传输,加一个死代理防联网;每个操作一行 JSONL,脚本逐 key 比较。探针不入库,PR 描述写位置和命令。 +4. **实现**:先把新 API 的调用写成 doc test,再按写法对照改。公开名字不变,除非契约允许 breaking。 +5. **审查**(子 agent,有“本次引入”的问题就回第 4 步): + - BEHAVIOUR:输出、错误、边界情况和旧实现一致。 + - WIRE:“不能动的面”没变。 + - DECISION:没违背“项目决定”和工作约束;没顺手修 bug。 + - LAYERING:没有新增 `shared → 具体模块` 之类的反向依赖。 + - SIZE:行数没有无故增加;没有为编译而加的 clone/unwrap。 + - ORACLE:差分可复现,数字带分母;变异检查做了。 + - SECURITY:Debug 不打印密钥;读环境变量不 panic。 +6. **提交和汇报**:`refactor()[!]: …`,body 写契约、差分 N/M、变异检查。PR 描述最后放一张 Verification 表:基线和改后测试数、测试改动、差分、变异检查、各项检查。 + +## 写法对照 + +| TS 形状 | 原生写法 | +|---|---| +| 一个 `kind`/`role`/`state` 字段,其余字段随它变 | enum,每个变体带自己的数据 | +| 几个 `bool` 组成的状态;`finished` 标志 | enum 变体;终结操作取 `self` | +| `-1`、空字符串哨兵;`isize` 下标;`Vec` 扫描 | `Option`、`usize`、`char_indices()` | +| 和字符串字面量比较的 `kind: String` | enum + serde rename | +| `extra: Value`、`.get("x")` | 有类型字段;开放部分用 `#[serde(flatten)]` | +| `Option>`,缺失和空无区别 | `Vec` + `Default` | +| `expect("never empty")` 维持的不变量 | 让非法状态构造不出来 | +| options 里的 `Box` 钩子 | trait + 给闭包的 blanket impl,`new(closure)` 照常能用 | +| 包内有限几种配置闭包(`url()`、`headers()`) | 包内纯数据 enum,请求时求值 | +| 共享层给各具体模块开的扩展点 | 共享层的 trait,具体模块的纯数据 struct 实现 | +| `new TransformStream({ start, transform, flush })`、靠一堆局部变量维持的流状态 | 实现 `Transformer` 的具名 struct;只读上下文和 `#[derive(Default)]` 的累积状态分开;同步驱动入口方便无 I/O 单测 | +| 外面套一个通用的 tracker/accumulator 对象 | 状态直接作为使用者 struct 的字段,逻辑是它的私有方法 | +| 把 `Transformer` 接到流上的 `async_stream!` + 每帧新 `Vec` | 直接实现 `futures::Stream` 的 struct,复用一个 `VecDeque` | +| 每次调用返回新 `Vec` | 写进调用方给的 `&mut Vec` | +| 小集合配 `HashMap` 索引 | `Vec` 线性扫描 | +| `createX(settings)`,settings 全 `Option` | `X::new(必填)` + consuming setter | +| `Result<_, ()>`、`Other(String)` 错误 | `thiserror` enum,`#[from]` 保留 source | +| 只有一个实现的 trait | struct | +| 插入顺序有意义却用 `HashMap` | `IndexMap` | +| 凭据类型 `derive(Debug)` | 手写脱敏 `Debug` | + +## 痕迹扫描 + +```sh +P=path/to/module.rs # 只扫目标 +rg -n '\w+: bool\b|let mut \w+ = (true|false);|isize|-1\b|Vec' "$P" +rg -n '\.expect\(|\.unwrap\(|unreachable!|panic!' "$P" +rg -n 'Option<(Vec|HashMap)<|(type|kind|role)\w*: (String|&str)|== "\w+"' "$P" +rg -n 'pub \w+: (serde_json::)?Value|as_object\(\)|\.get\("|#\[serde\(untagged\)\]|Other\(String\)' "$P" +rg -n '(Box|Arc)' "$P" +rg -n 'use crate::(vertex|bedrock|google|anthropic|openai|aws_polly)' aimux-providers/src/shared # 反向依赖 +``` + +rig(`0xPlaygrounds/rig`)里可对照的:按角色的消息 enum(`completion/message.rs`)、请求 `new` + setter + `#[non_exhaustive]`(`completion/request.rs`)、纯数据 provider 配置和 `Secret`(`providers/openai/wire.rs`)、流的 `finish(self)`(`streaming/mod.rs`)、钩子 trait(`agent/hook.rs`)。 diff --git a/.agents/skills/ts-rust-port/SKILL.md b/.agents/skills/ts-rust-port/SKILL.md new file mode 100644 index 00000000..606b212b --- /dev/null +++ b/.agents/skills/ts-rust-port/SKILL.md @@ -0,0 +1,47 @@ +--- +name: ts-rust-port +description: Port a TypeScript module to Rust, fix one place where a Rust port disagrees with its pinned TS upstream, or check a Rust module against that upstream, using upstream tests, recorded fixtures and running upstream TS. +--- + +# TS 上游和它的 Rust 移植 + +两个仓库:锁定版本的 TS 上游和 Rust 仓库,目标是 Rust 的行为和上游一致。把移植来的代码改成原生写法是另一件事,用 `rust-native-rewrite`。 +仓库事实在 `.agents/native-rust/PROFILE.md`(下称配置)。 + +| 用法 | 做什么 | +|---|---| +| 上游文件,或还没移植的模块 | 移植 | +| `fix <一处不一致>` | 把一处行为改成和上游一样(最常见) | +| `parity ` | 和上游对比,只报告 | +| `review [ref]` | 只审查 | + +## 纪律 + +1. 移植时结构跟上游走(文件、类型、函数一一对应),不改进、不修 bug;已经移植过的代码只走 `fix` 或 `parity`。 +2. 说“上游是这样”要给锁定版本的文件和行号。 +3. 上游测试逐个用例搬,只改 harness,不改场景。 +4. 报告写分母:“比较 N 个,M 处差异”。失败一次修一个,到 0 为止。 +5. 上游源码下载到新的空目录,当数据用:路径作参数传给 node,不在那个目录里执行东西。 +6. 配置“项目决定”里和上游刻意不同的地方,不改回上游。 +7. 不自动推送。 + +## fix + +1. 找到锁定版本的上游文件和行号;在“项目决定”里的就不改,报告并停下。 +2. 先写一个在当前代码上失败的测试:优先回放上游录制的 fixture,其次搬上游用例,再其次用 node 跑上游拿期望值。 +3. 改到通过,只改这一处;顺带看到的其他差异记下来。 +4. 跑配置的检查,基线和改后逐个测试二进制比。 +5. 提交 `fix(): <行为> matches <包>@<版本>`,body 写上游出处、改前改后、新测试名。同一层的几条可以同一个 PR,每条一个提交。 + +## 移植 + +1. 用配置的映射把目标解析成“Rust 文件 + 上游文件 + 版本”;查清碰到哪些“不能动的面”和“项目决定”。 +2. 契约:上游文件和版本、不能变的、允许的差异、怎么测、是否 breaking。只有要 breaking 或违背配置时先等用户回复。 +3. 实现(子 agent):按上游结构写,只在所有权模型要求时偏离;上游测试逐个搬;循环到 0 失败;再做一次对比上游。常见失败:缺 match 分支、迭代顺序、null 和缺失混同、数字类型、UTF-16 下标、JSON 键顺序、错误文本。 +4. 审查(子 agent,循环):对每个改动的文件读完对应的上游文件,查 FIDELITY(数据流、边界、错误分类和上游一致)、WIRE、DECISION、ORACLE(用例搬全、引用带版本行号)、ERROR(错误名和消息保留,不新增 panic)、SCOPE。 +5. 提交 `feat(): port `,body 写搬了多少用例、对比上游的 N/M。 + +## 对比上游 + +按能用的顺序:上游录制的 fixture 回放;node 跑锁定版本上游、读同一份 JSONL 语料逐条比;搬来的上游测试(最弱)。 +语料覆盖空输入、截断、乱序、重复、深嵌套、错误路径、非 ASCII 和辅助平面字符,以及配置“陷阱”里的每一项。每类差异给一个最小复现,标明是已知的还是新发现的。runner 不入库,commit body 写位置和命令。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 32e07985..01a6a444 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,12 +15,19 @@ aimux/ ├── bindings/ # Node, Python, Swift, Kotlin, Flutter, Go, C — share one Rust core ├── rfc/ # Design docs (RFCs) ├── docs/ # Public product docs (see docs/README.md) +├── .agents/ # Skills for coding agents (see below) └── scripts/ # Code-generation and maintenance scripts ``` `docs/internal/` contains historical research and audit notes; it is not part of the public surface and generally does not need changes. +`.agents/` holds skills for coding agents and their maintainers (porting from +the AI SDK, native-Rust rewrites) and the PROFILE of repository facts they share +(`.agents/native-rust/PROFILE.md`). The skills and profile are written in +Chinese, like the recent RFCs. They hold only what stays true, and change in +the same PR as the code they describe. + ## Prerequisites - **Rust**: stable toolchain (see `rust-toolchain.toml`). The workspace targets