focus/AGENTS.md

20 KiB
Raw Blame History

AGENTS.md — focus 项目工程规范

本文件定义 focus 项目的工程规范。所有协作者(人类或 AI agent在修改本仓库代码前必须先阅读并遵守本文件。

参考实现解读见 docs/architecture/

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:

//! 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.rsmain.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 是参考 piTypeScript 原版)和 pi_agent_rustRust 移植)实现的 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 tokiofeatures 按需,见下)
TLS rustls 生态 rustlstokio-rustlsrustls-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 traitlet-elselet-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 解析器

  • APIJsonValue 枚举 + 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 实现

  • 内容:具体 providerAnthropicProviderOpenAiProvider)实现 focus-core::StreamProvider,把 provider 特定的 HTTP/SSE 格式翻译成统一的 StreamEvent
  • 依赖focus-transport 发请求,focus-core 的类型。
  • 测试:用 mock transport录制/回放 HTTP 响应)测试 SSE → StreamEvent 的翻译。禁止真实网络测试(除非显式标记 #[ignore] 的集成测试)。

focus-tools — 文件工具

  • 内容:实现 focus-core::Tool 的具体工具:ReadToolWriteToolEditToolBashTool
  • I/Ostd::fstokio::fs 做文件操作;tokio::process::Command 执行 bash。
  • 测试用临时目录测试文件工具bash 工具测试受控命令。

focus-harness — 基础设施层

  • 内容会话持久化JSONL 追加写、树结构、上下文压缩token 估算 + 摘要生成)、系统提示模板。
  • 会话存储:参考 pi 的设计——树结构(每条 entry 有 id + parentIdJSONL 追加写。
  • 测试:会话树的构建/查询/分支、压缩的 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_CASEcrate 名 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::CancellationTokentokio::select! 实现取消。参考 pi 的 AbortSignal 设计。
  • 超时:所有网络调用必须可超时(tokio::time::timeout)。
  • 避免 .await 持有锁:跨 .await 持有 std::sync::Mutex 会死锁,用 tokio::sync::Mutex 或重构。

4.4 类型设计(对照 pi

参考 docs/architecture/01-types-and-protocol.md,领域类型设计要点:

  • Messagerole 标签的 enumUser / Assistant / ToolResult(对应 pi 的 LLM Message
  • ContentBlocktype 标签的 enumText / Thinking / Image / ToolCall
  • StopReason enumStop / Length / ToolUse / Error / Aborted
  • 手写 JSON 编解码:每个领域类型实现 TryFrom<JsonValue>From<&Self> for JsonValue
  • content vs details 分离ToolResultcontent(模型可见)和 detailsUI/日志可见)分开。

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
  • 当测试需要触及内部实现时,二选一:
    1. 通过公开 API 测试(优先):如 provider 的请求体翻译用 mock transport 录制请求后断言其 JSON而不是直接测私有 builder 函数;
    2. 提升为 pub:确有必要公开的内部组件(如 ChunkedDecoderdays_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 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 验证命令(提交前必跑)

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 可选>
  • typefeat / fix / test / refactor / docs / chore / build
  • scopecrate 名(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. 依赖变更纪律

引入新依赖是重大决策,必须谨慎:

  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-coreuse 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.rsSSE 解析)、src/agent_cx.rs(取消/预算)
  • 架构解读:见 docs/architecture/ 三份文档

学习路径:先读 docs/architecture/01-types-and-protocol.md02-agent-loop.md → 再读对应 crate 的源码。