focus/AGENTS.md

347 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<StreamEvent> { ... }
```
行内注释 / 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` |
**这就是全部。** 以下 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` | `rt`, `sync`, `macros` |
| `focus-transport` | `rt`, `net`, `io-util`, `sync` |
| `focus-providers` | `rt`, `sync`, `macros` |
| `focus-tools` | (无 tokio —— 工具为同步 `std` I/O |
| `focus-harness` | (无 tokio —— 会话/压缩为纯逻辑 + 同步 `std` I/O |
### 2.3 Rust 版本
- 使用**最新 stable** Rust。
- 仓库根放 `rust-toolchain.toml` 锁定 channel = stable。
- 允许使用 stable 已稳定的所有语言特性(包括 `async fn in trait`、`let-else`、`let-chains` 等)。
- **禁止** nightly-only 特性。
## 3. Workspace 结构
细粒度 6-crate 拆分,每个 crate 职责单一、边界清晰、可独立测试。
`focus-cli` 已删除TUI 交互层将在后续里程碑以新 crate 加入,届时再更新本图。)
```text
focus/
├── AGENTS.md # 本文件
├── Cargo.toml # [workspace] + 共享依赖版本
├── 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 / bash
│ └── focus-harness/ # 会话持久化 + 上下文压缩 + 系统提示模板
├── docs/
│ └── architecture/ # 架构解读文档(已存在)
└── tests/ # workspace 级集成测试(可选)
```
### 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-tools ← focus-core, tokio
focus-harness ← focus-core, focus-json
```
**规则**
- 依赖只能**向下**流(向叶子 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<JsonValue>` / `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 请求/响应编解码、SSEServer-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-cli` — 已删除(由 TUI 替代)
- `focus-cli` 已彻底删除(不再作为 workspace 成员)。后续里程碑将新增 TUI crate
(届时命名,如 `focus-tui`),承担**组装**职责provider + tools + harness 的装配、
base_url / api_key / model 等配置provider 构造时传入)、动态系统提示注入、
自动上下文压缩的执行harness 产出方案 → 调 provider 摘要 → `Agent::replace_messages` 写回)。
## 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<JsonValue>``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/<name>/tests/<module>_tests.rs`(按被测模块拆分文件) | 每个公开函数/类型的边界用例 |
| **集成测试** | `crates/<name>/tests/*.rs` | 跨模块的端到端(如 agent 循环跑完一轮) |
| **mock 驱动** | `crates/focus-core/tests/agent_loop.rs` 内联 `MockProvider` | 用预设的 `StreamEvent` 序列驱动循环;仅当前单一测试文件使用时内联,多文件共享时抽为独立测试工具 crate |
### 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/<crate>-<topic>`(如 `feat/core-agent-loop`)。
- 修复分支:`fix/<crate>-<topic>`。
### 6.2 提交信息
Conventional Commits 风格:
```text
<type>(<scope>): <subject>
<body 可选>
```
- **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 <crate>` 检查。rustls/tokio 的传递依赖可接受,其他需评估。
3. **更新 §2.1 白名单**。任何新依赖必须在 AGENTS.md §2.1 登记,并在 PR 说明理由。
4. **禁止vendoring 大段第三方代码**作为"伪依赖"。
## 8. 文档规范
- 架构决策记录在 `docs/architecture/`(已存在三份)。
- 新的架构决策文档用 `NN-<topic>.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 的源码。