# AGENTS.md — focus 项目工程规范 > 本文件定义 `focus` 项目的工程规范。所有协作者(人类或 AI agent)在修改本仓库代码前**必须**先阅读并遵守本文件。 > > 参考实现解读见 [`docs/architecture/`](./docs/architecture/): > - [`00-pi-architecture-overview.md`](./docs/architecture/00-pi-architecture-overview.md) — 全景总览 > - [`01-types-and-protocol.md`](./docs/architecture/01-types-and-protocol.md) — 类型契约精读 > - [`02-agent-loop.md`](./docs/architecture/02-agent-loop.md) — 核心循环精读 ## 0. 代码注释规范(最高优先级 / Top-Priority Comment Rule) > 本节规则凌驾于其它一切约定之上。任何新增或修改的源代码(`.rs` 文件)**必须**同时带有**中文**与**英文**注释;缺少任一语言即视为不合规。 > The rule in this section overrides every other convention below. All newly added or modified source code (`.rs` files) **MUST** carry **both Chinese and English** comments; missing either language is a violation. ### 0.1 双语注释要求 / Bilingual Comment Requirement - **所有**模块级文档注释(`//!`)、项级文档注释(`///`)以及行内/块注释(`//`)都必须**中英双语并存**。 - 推荐写法:先写中文,再写英文(或反之),两者表达同一含义即可,不要求逐字对译。 - **All** module-level doc comments (`//!`), item-level doc comments (`///`), and inline/block comments (`//`) **MUST** appear in **both Chinese and English**. - Recommended style: write Chinese first, then English (or vice versa). They must convey the same meaning; verbatim word-for-word translation is not required. ### 0.2 示例 / Examples 模块文档 / Module doc: ```rust //! Agent 核心循环:实现 pi 风格的双层循环(steering + follow-up)。 //! Core agent loop: implements the pi-style double loop (steering + follow-up). ``` 项文档 / Item doc: ```rust /// 从流中拉取下一个事件;返回 `None` 表示流结束。 /// Pull the next event from the stream; `None` means end-of-stream. pub fn next_event(&mut self) -> Option { ... } ``` 行内注释 / Inline comment: ```rust // 先尝试从缓冲区读取,避免无谓的 syscall。 // Try the buffer first to avoid a pointless syscall. let b = self.buf.pop_front(); ``` ### 0.3 适用范围 / Scope - ✅ 适用:所有 `crates/**/src/**/*.rs`(含 `lib.rs`、`main.rs`)、`tests/**/*.rs` 中的公开项与关键私有项。 - ❌ 不适用:自动生成的代码、`build.rs` 中的纯样板、纯粹的格式占位空行。 - ✅ In scope: all public items and key private items in `crates/**/src/**/*.rs` (including `lib.rs`, `main.rs`) and `tests/**/*.rs`. - ❌ Out of scope: auto-generated code, pure boilerplate in `build.rs`, empty placeholder lines. ## 1. 项目定位 `focus` 是参考 [pi](https://github.com/earendil-works/pi)(TypeScript 原版)和 [pi_agent_rust](https://github.com/Dicklesworthstone/pi_agent_rust)(Rust 移植)实现的 LLM agent 框架。 **核心目标**:以最小的依赖面实现一个分层清晰、可独立测试的 agent 框架,重点学习 pi 的核心设计(agent 循环、消息类型、工具抽象、流式协议)。 **设计哲学**: - **依赖极简**:除 tokio 生态和 rustls 外,不引入任何运行时/序列化/HTTP 框架。能用纯 Rust std 解决的,绝不加依赖。 - **分层隔离**:agent 核心层是纯逻辑,不碰网络/文件系统;I/O 通过 trait 边界注入。 - **类型安全**:手写 JSON 编解码(不依赖 serde/serde_json),所有领域类型都强类型化。 - **可测试优先**:核心循环用 mock provider 驱动测试,不依赖真实 LLM。 ## 2. 技术栈与依赖策略 ### 2.1 允许的外部依赖(白名单) **整个 workspace 只允许以下外部 crate**(除各 crate 的 `std`): | 用途 | crate | 允许使用的 crate | |---|---|---| | 异步运行时 | `tokio` | `tokio`(features 按需,见下) | | TLS | rustls 生态 | `rustls`、`tokio-rustls`、`rustls-native-certs`(或 `webpki-roots`) | | TUI 渲染 | `ratatui` + `crossterm` | 仅 `focus-tui` 可用。终端交互(原始模式、增量重绘、滚动、鼠标、键盘事件)属于纯 std 无法合理解决的**真实需求**,故特批;`ratatui` 的传递依赖(`crossterm`、`unicode-width` 等)随其引入。其余 crate 一律禁止引用 | **这就是全部。** 以下 crate **明确禁止**: - ❌ `serde` / `serde_json` — JSON 用自研 `focus-json` crate - ❌ `reqwest` / `hyper` / `ureq` — HTTP 用 `focus-transport` 自行实现(基于 tokio + rustls) - ❌ `anyhow` / `thiserror` — 错误类型用自研 `focus-core::error`(或各 crate 自定义) - ❌ `async-trait` — 使用 Rust 1.75+ 原生 `async fn in trait` - ❌ `chrono` / `time` — 时间用 `std::time` - ❌ `regex` — 用 `std` 字符串方法或自研简单匹配 - ❌ `clap` — CLI 参数解析自行实现(第一版参数简单) **例外审批**:如遇上述清单无法覆盖的真实需求(如 HTTP/2 支持需 h2 库),必须在 PR/commit 中说明理由并更新本文件的白名单。默认拒绝。 ### 2.2 tokio features 规范 每个 crate 只启用它实际需要的 tokio feature,**禁止用 `full`**: | crate | 推荐 tokio features | |---|---| | `focus-core` | (无 tokio —— 核心层为纯同步逻辑) | | `focus-transport` | `net`, `io-util`, `time`(TCP/TLS、超时) | | `focus-providers` | `rt`, `time`(后台 current-thread 运行时) | | `focus-tools` | (无 tokio —— 工具为同步 `std` I/O) | | `focus-harness` | (无 tokio —— 会话/压缩为纯逻辑 + 同步 `std` I/O) | > workspace 级 `tokio` 声明不带 features;由各成员按上表自行声明。 ### 2.3 Rust 版本 - 使用**最新 stable** Rust。 - 仓库根放 `rust-toolchain.toml` 锁定 channel = stable。 - 允许使用 stable 已稳定的所有语言特性(包括 `async fn in trait`、`let-else`、`let-chains` 等)。 - **禁止** nightly-only 特性。 ## 3. Workspace 结构 细粒度 7-crate 拆分,每个 crate 职责单一、边界清晰、可独立测试。 (`focus-cli` 已删除,由 `focus-tui` 替代。) ```text focus/ ├── AGENTS.md # 本文件 ├── Cargo.toml # [workspace] + 共享依赖版本 ├── LICENSE # MIT ├── README.md # 项目总览与使用说明 ├── rust-toolchain.toml # channel = "stable" ├── crates/ │ ├── focus-json/ # 极简 JSON 解析器(零外部依赖) │ ├── focus-core/ # agent 层:类型 + 循环 + Tool trait │ ├── focus-transport/ # Transport trait + HTTP/1.1 + TLS + SSE 解析 │ ├── focus-providers/ # provider 实现(Anthropic / OpenAI) │ ├── focus-tools/ # 文件工具(read / write / edit / shell) │ ├── focus-harness/ # 会话持久化 + 上下文压缩 + 系统提示模板 │ └── focus-tui/ # 终端交互层(ratatui + crossterm) └── docs/ └── architecture/ # 架构解读文档 ``` ### 3.1 依赖图(强制单向,禁止循环) ```text focus-json ← (零外部依赖,纯 std) focus-core ← focus-json(纯同步逻辑,不依赖 tokio) focus-transport ← focus-json, tokio, rustls, tokio-rustls, rustls-native-certs focus-providers ← focus-core, focus-transport, focus-json, tokio focus-tools ← focus-core, focus-json focus-harness ← focus-core, focus-json focus-tui ← focus-core, focus-providers, focus-tools, focus-harness, focus-json, ratatui, crossterm ``` **规则**: - 依赖只能**向下**流(向叶子 crate)。 - `focus-core` **绝对禁止**依赖 `focus-transport` / `focus-providers` / `focus-tools` / `focus-harness`。核心层必须对 I/O 无感知。 - 禁止循环依赖(Cargo 本身会拒绝,但设计上也不允许语义循环)。 ### 3.2 各 crate 职责 #### `focus-json` — 极简 JSON 解析器 - **API**:`JsonValue` 枚举 + `parse()` / `to_string()` + 各类型手写 `TryFrom` / `From<&T> for JsonValue`。 - **支持范围**:`Null` / `Bool` / `Number(f64)` / `String` / `Array` / `Object`。Object 用 `Vec<(String, JsonValue)>` 保持键序。 - **禁止**:JSON Schema 验证、JSONC 注释、trailing comma、流式解析(第一版只支持完整解析)。 - **测试**:边界用例全覆盖(嵌套、转义、unicode、超大数字、畸形输入)。 #### `focus-core` — agent 核心层(灵魂) - **内容**:领域类型(`Message` / `ContentBlock` / `ToolCall` / `Usage` / `StopReason` / `StreamEvent` / `AgentEvent`)、`Tool` trait、`StreamProvider` trait、agent 循环、状态管理、事件流。 - **I/O 边界**:`StreamProvider` 是 trait,核心层只依赖 trait,不依赖任何具体 provider 实现。 - **测试**:mock provider(实现 `StreamProvider`)驱动 agent 循环的完整测试。**核心层测试绝不能依赖真实网络**。 - **禁止**:任何 `tokio::net` / `tokio::fs` / `tokio::process` / TLS 调用。 #### `focus-transport` — 传输层 - **内容**:`Transport` trait、基于 tokio + rustls 的 HTTPS 客户端、HTTP/1.1 请求/响应编解码、SSE(Server-Sent Events)流式解析器。 - **TLS**:用 `rustls-native-certs`(或 `webpki-roots`)加载根证书,`tokio-rustls` 建立 TLS 连接。 - **SSE 解析**:纯增量解析,能处理跨 chunk 的事件边界。参考 pi 的 `src/sse.rs`。 - **测试**:用内存 buffer 模拟 TLS 流,测试 HTTP 编解码和 SSE 解析。 #### `focus-providers` — provider 实现 - **内容**:具体 provider(`AnthropicProvider`、`OpenAiProvider`)实现 `focus-core::StreamProvider`,把 provider 特定的 HTTP/SSE 格式翻译成统一的 `StreamEvent`。 - **依赖**:`focus-transport` 发请求,`focus-core` 的类型。 - **测试**:用 mock transport(录制/回放 HTTP 响应)测试 SSE → StreamEvent 的翻译。**禁止真实网络测试**(除非显式标记 `#[ignore]` 的集成测试)。 #### `focus-tools` — 文件工具 - **内容**:实现 `focus-core::Tool` 的具体工具:`ReadTool`、`WriteTool`、`EditTool`、`BashTool`。 - **I/O**:`std::fs` 或 `tokio::fs` 做文件操作;`tokio::process::Command` 执行 bash。 - **测试**:用临时目录测试文件工具;bash 工具测试受控命令。 #### `focus-harness` — 基础设施层 - **内容**:会话持久化(JSONL 追加写、树结构)、上下文压缩(token 估算 + 摘要生成)、系统提示模板。 - **会话存储**:参考 pi 的设计——树结构(每条 entry 有 `id` + `parentId`),JSONL 追加写。 - **测试**:会话树的构建/查询/分支、压缩的 token 估算与切点算法。 #### `focus-tui` — 终端交互层(组装职责) - **内容**:provider + tools + harness 的装配、base_url / api_key / model 等配置 (`~/.focus/config.json`,TUI 内 `/config` 编辑;provider 构造时传入)、动态系统 提示注入(方案 E 的 Usage/上下文占用)、自动上下文压缩的执行(harness 产出方案 → 调 provider 摘要 → `Agent::replace_messages` 写回)、会话持久化(`~/.focus/sessions/`)。 - **交互**:多行输入、Markdown 渲染、流式显示、Esc 中止、折叠的思考/工具块 (显示耗时与内容摘要,点击或 Tab 两级展开——先预览后全量)、工具请求与结果合并 为一块、命令(`/new` `/sessions` `/config` `/compact`)。 - **禁止**:把业务逻辑放这里。这里只做**组装与呈现**。 ## 4. 代码规范 ### 4.1 风格 - **遵循 `rustfmt` 默认配置**。CI 用 `cargo fmt --check` 强制。 - **遵循 `clippy`**。CI 用 `cargo clippy -- -D warnings` 强制。零 warning。 - **命名**:类型 `UpperCamelCase`,函数/变量 `snake_case`,常量 `SCREAMING_SNAKE_CASE`,crate 名 `kebab-case`。 - **模块组织**:每个类型/模块一个清晰的职责。文件超过 ~500 行考虑拆分。 - **文档注释**:所有 `pub` 项必须有 `///` 文档注释。crate 根有 `//!` 模块说明。 - **注释**:解释**为什么**,不解释**是什么**(代码本身能读出来)。复杂逻辑加注释。 ### 4.2 错误处理 - **不用 `anyhow` / `thiserror`**。 - 每个 crate 定义自己的错误类型(通常在 `error.rs`),实现 `std::error::Error` + `Display`。 - 可恢复错误用 `Result`,**禁止用 `unwrap()` / `expect()` 处理可预期错误**(如文件不存在、JSON 畸形、网络失败)。 - `unwrap()` / `expect()` 仅允许在两种场景:① 测试代码;② 不变量违反(程序逻辑错误,不可能发生,崩溃可接受)——且必须 `expect("说明为什么这是不变量")`。 - 错误消息要**可操作**:告诉调用方什么错了、怎么修。 ### 4.3 异步规范 - **统一用 tokio 运行时**。禁止混用 `std::thread::spawn` 做异步工作(CPU 密集任务可用 `tokio::task::spawn_blocking`)。 - **`async fn in trait`**:用 Rust 原生(1.75+),**禁止** `async-trait` crate。 - **取消**:用 `tokio::sync::CancellationToken` 或 `tokio::select!` 实现取消。参考 pi 的 `AbortSignal` 设计。 - **超时**:所有网络调用必须可超时(`tokio::time::timeout`)。 - **避免 `.await` 持有锁**:跨 `.await` 持有 `std::sync::Mutex` 会死锁,用 `tokio::sync::Mutex` 或重构。 ### 4.4 类型设计(对照 pi) 参考 [`docs/architecture/01-types-and-protocol.md`](./docs/architecture/01-types-and-protocol.md),领域类型设计要点: - `Message` 是 `role` 标签的 enum:`User` / `Assistant` / `ToolResult`(对应 pi 的 LLM Message)。 - `ContentBlock` 是 `type` 标签的 enum:`Text` / `Thinking` / `Image` / `ToolCall`。 - `StopReason` enum:`Stop` / `Length` / `ToolUse` / `Error` / `Aborted`。 - 手写 JSON 编解码:每个领域类型实现 `TryFrom` 和 `From<&Self> for JsonValue`。 - **content vs details 分离**:`ToolResult` 的 `content`(模型可见)和 `details`(UI/日志可见)分开。 ### 4.5 agent 循环设计(对照 pi) 参考 [`docs/architecture/02-agent-loop.md`](./docs/architecture/02-agent-loop.md),循环实现要点: - **双层循环**:外层 follow-up,内层 steering + 工具调用。 - **错误编码而非抛出**:`StreamProvider` 绝不在 trait 方法里返回 Err 中断循环——错误编码进 `StreamEvent::Error`。 - **流式消息活性**:流式 assistant 消息在流过程中就保持在消息列表末尾,delta 到达时原地更新。 - **工具执行**:支持 sequential / parallel 两种模式。 ## 5. 测试规范(TDD) ### 5.1 测试金字塔 | 层级 | 位置 | 内容 | |---|---|---| | **单元测试** | `crates//tests/_tests.rs`(按被测模块拆分文件) | 每个公开函数/类型的边界用例 | | **集成测试** | `crates//tests/*.rs` | 跨模块的端到端(如 agent 循环跑完一轮) | | **mock 驱动** | `crates/focus-core/tests/agent_loop.rs` 内联 `MockProvider` | 用预设的 `StreamEvent` 序列驱动循环;仅当前单一测试文件使用时内联,多文件共享时抽为独立测试工具 crate | #### 5.1.1 测试位置硬性规则(所有 crate 统一遵守) - **所有测试用例**(单元 + 集成)都必须放在 `crates//tests/` 下,**禁止**在 `src/**` 里写 `#[cfg(test)] mod tests` 内联测试模块。`tests/` 下的文件是独立 crate, 只能访问被测 crate 的**公开 API**。 - 当测试需要触及**内部实现**时,二选一: 1. **通过公开 API 测试**(优先):如 provider 的请求体翻译用 mock transport 录制请求后断言其 JSON,而不是直接测私有 builder 函数; 2. **提升为 `pub`**:确有必要公开的内部组件(如 `ChunkedDecoder`、 `days_to_ymd`)提升为 `pub` 并写完整文档注释,测试放到 `tests/` 中。 - **唯一例外**:`#[cfg(test)]` 的**测试基础设施**(非测试用例本身,如 `focus-transport/src/tls.rs` 中仅测试用的放行式 TLS 配置)允许留在 `src/`。 #### 5.1.1 Rule: where tests must live (applies to every crate) - **All test cases** (unit + integration) must live under `crates//tests/`; inline `#[cfg(test)] mod tests` inside `src/**` is **forbidden**. Files under `tests/` are separate crates and can only see the tested crate's **public API**. - When a test must touch **internals**, pick one of: 1. **Test through the public API** (preferred): e.g. provider request-body translation is asserted on the request recorded by a mock transport, not on a private builder function; 2. **Promote to `pub`**: genuinely useful internals (e.g. `ChunkedDecoder`, `days_to_ymd`) become `pub` with full doc comments, tested from `tests/`. - **Only exception**: `#[cfg(test)]` **test infrastructure** (not test cases — e.g. the test-only permissive TLS config in `focus-transport/src/tls.rs`) may stay in `src/`. ### 5.2 TDD 流程 实现任何功能/修复前: 1. **先写失败测试**(Red)——描述期望行为。 2. **写最小实现让测试通过**(Green)。 3. **重构**(Refactor)——保持测试绿。 ### 5.3 硬性规则 - **核心层(focus-core)测试零网络依赖**。`StreamProvider` 必须可 mock。 - **provider 测试用录制/回放**,不打真实 API。需要真实 API 的测试用 `#[ignore]` 标记,手动运行。 - **CI 必须全绿**:`cargo test --workspace` 全部通过。 - **禁止 flaky 测试**:测试必须确定性的。涉及时间的用注入的 clock。 - 每个公开 API 至少有一个测试用例。 ### 5.4 验证命令(提交前必跑) ```bash cargo fmt --all --check # 格式 cargo clippy --workspace -- -D warnings # lint,零 warning cargo test --workspace # 全部测试 cargo build --workspace # 全部编译 ``` ## 6. Git 与提交规范 ### 6.1 分支 - `main` 是稳定主干,始终保持可编译、测试全绿。 - 功能开发从 `main` 切分支:`feat/-`(如 `feat/core-agent-loop`)。 - 修复分支:`fix/-`。 ### 6.2 提交信息 Conventional Commits 风格: ```text (): ``` - **type**:`feat` / `fix` / `test` / `refactor` / `docs` / `chore` / `build` - **scope**:crate 名(`json` / `core` / `transport` / `providers` / `tools` / `harness` / `tui`)或 `workspace` - **subject**:祈使句,小写开头,不加句号 示例: ```text feat(core): implement agent loop with sequential tool execution test(json): add unicode escape edge cases fix(transport): handle SSE events split across chunks docs(architecture): add types.ts line-by-line reading ``` ### 6.3 PR 检查清单 提交 PR 前自检: - [ ] `cargo fmt --check` 通过 - [ ] `cargo clippy -- -D warnings` 通过 - [ ] `cargo test --workspace` 通过 - [ ] 新增公开 API 有文档注释 - [ ] 新增功能有测试覆盖 - [ ] 没有引入白名单外的依赖(若引入,已更新 §2.1 并说明理由) - [ ] 提交信息符合 Conventional Commits ## 7. 依赖变更纪律 **引入新依赖是重大决策**,必须谨慎: 1. **先问"能不能用 std / 现有 crate 解决"**。如果能,不加。 2. **审计依赖的传递依赖**。`cargo tree -p ` 检查。rustls/tokio 的传递依赖可接受,其他需评估。 3. **更新 §2.1 白名单**。任何新依赖必须在 AGENTS.md §2.1 登记,并在 PR 说明理由。 4. **禁止vendoring 大段第三方代码**作为"伪依赖"。 ## 8. 文档规范 - 架构决策记录在 `docs/architecture/`(已存在三份)。 - 新的架构决策文档用 `NN-.md` 编号。 - crate 级文档在 `src/lib.rs` 的 `//!` 注释。 - 复杂算法/协议在源码注释里解释**为什么**。 ## 9. 常见反模式(禁止) | 反模式 | 为什么禁止 | |---|---| | 在 `focus-core` 里 `use tokio::net::*` | 核心层必须 I/O 无关 | | 用 `serde::Serialize` derive | 违反手写 JSON 原则 | | 用 `.unwrap()` 处理用户/网络输入 | 应返回 `Result` | | `async fn` 跨 `.await` 持有 `std::sync::Mutex` | 死锁风险 | | 在 `focus-core` 测试里发真实 HTTP 请求 | 核心层测试零网络 | | 用 `tokio = { features = ["full"] }` | 必须按需启用 feature | | 一个文件塞多个职责 | 违反单一职责 | | 提交带 `dbg!()` / `println!()` 的代码到 main | 用 `tracing` 或测试断言 | ## 10. 参考资料索引 - **pi 原版(TS)**:参考 `pi-main/packages/agent/src/types.ts`(类型)、`agent-loop.ts`(循环) - **pi Rust 移植**:参考 `pi_agent_rust-main/src/model.rs`(类型映射)、`src/agent.rs:1453`(循环)、`src/sse.rs`(SSE 解析)、`src/agent_cx.rs`(取消/预算) - **架构解读**:见 `docs/architecture/` 三份文档 **学习路径**:先读 `docs/architecture/01-types-and-protocol.md` → `02-agent-loop.md` → 再读对应 crate 的源码。