20 KiB
AGENTS.md — focus 项目工程规范
本文件定义
focus项目的工程规范。所有协作者(人类或 AI agent)在修改本仓库代码前必须先阅读并遵守本文件。参考实现解读见
docs/architecture/:
00-pi-architecture-overview.md— 全景总览01-types-and-protocol.md— 类型契约精读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 (
.rsfiles) 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:
//! Agent 核心循环:实现 pi 风格的双层循环(steering + follow-up)。
//! Core agent loop: implements the pi-style double loop (steering + follow-up).
项文档 / Item doc:
/// 从流中拉取下一个事件;返回 `None` 表示流结束。
/// Pull the next event from the stream; `None` means end-of-stream.
pub fn next_event(&mut self) -> Option<StreamEvent> { ... }
行内注释 / Inline comment:
// 先尝试从缓冲区读取,避免无谓的 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(includinglib.rs,main.rs) andtests/**/*.rs. -
❌ Out of scope: auto-generated code, pure boilerplate in
build.rs, empty placeholder lines.
1. 项目定位
focus 是参考 pi(TypeScript 原版)和
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-jsoncrate - ❌
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 加入,届时再更新本图。)
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 依赖图(强制单向,禁止循环)
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)、Tooltrait、StreamProvidertrait、agent 循环、状态管理、事件流。 - I/O 边界:
StreamProvider是 trait,核心层只依赖 trait,不依赖任何具体 provider 实现。 - 测试:mock provider(实现
StreamProvider)驱动 agent 循环的完整测试。核心层测试绝不能依赖真实网络。 - 禁止:任何
tokio::net/tokio::fs/tokio::process/ TLS 调用。
focus-transport — 传输层
- 内容:
Transporttrait、基于 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-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-traitcrate。- 取消:用
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,领域类型设计要点:
Message是role标签的 enum:User/Assistant/ToolResult(对应 pi 的 LLM Message)。ContentBlock是type标签的 enum:Text/Thinking/Image/ToolCall。StopReasonenum: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,循环实现要点:
- 双层循环:外层 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.1.1 测试位置硬性规则(所有 crate 统一遵守)
- 所有测试用例(单元 + 集成)都必须放在
crates/<name>/tests/下,禁止在src/**里写#[cfg(test)] mod tests内联测试模块。tests/下的文件是独立 crate, 只能访问被测 crate 的公开 API。 - 当测试需要触及内部实现时,二选一:
- 通过公开 API 测试(优先):如 provider 的请求体翻译用 mock transport 录制请求后断言其 JSON,而不是直接测私有 builder 函数;
- 提升为
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/<name>/tests/; inline#[cfg(test)] mod testsinsidesrc/**is forbidden. Files undertests/are separate crates and can only see the tested crate's public API. - When a test must touch internals, pick one of:
- 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;
- Promote to
pub: genuinely useful internals (e.g.ChunkedDecoder,days_to_ymd) becomepubwith full doc comments, tested fromtests/.
- Only exception:
#[cfg(test)]test infrastructure (not test cases — e.g. the test-only permissive TLS config infocus-transport/src/tls.rs) may stay insrc/.
5.2 TDD 流程
实现任何功能/修复前:
- 先写失败测试(Red)——描述期望行为。
- 写最小实现让测试通过(Green)。
- 重构(Refactor)——保持测试绿。
5.3 硬性规则
- 核心层(focus-core)测试零网络依赖。
StreamProvider必须可 mock。 - provider 测试用录制/回放,不打真实 API。需要真实 API 的测试用
#[ignore]标记,手动运行。 - CI 必须全绿:
cargo test --workspace全部通过。 - 禁止 flaky 测试:测试必须确定性的。涉及时间的用注入的 clock。
- 每个公开 API 至少有一个测试用例。
5.4 验证命令(提交前必跑)
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 风格:
<type>(<scope>): <subject>
<body 可选>
- type:
feat/fix/test/refactor/docs/chore/build - scope:crate 名(
json/core/transport/providers/tools/harness/tui)或workspace - subject:祈使句,小写开头,不加句号
示例:
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. 依赖变更纪律
引入新依赖是重大决策,必须谨慎:
- 先问"能不能用 std / 现有 crate 解决"。如果能,不加。
- 审计依赖的传递依赖。
cargo tree -p <crate>检查。rustls/tokio 的传递依赖可接受,其他需评估。 - 更新 §2.1 白名单。任何新依赖必须在 AGENTS.md §2.1 登记,并在 PR 说明理由。
- 禁止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 的源码。