focus/docs/architecture/00-pi-architecture-overview.md

16 KiB
Raw Permalink Blame History

Pi Agent 架构总览

本文档是对两个参考项目的架构解读,作为后续实现 focus(纯 Rust std + workspaces 版)的参考蓝图。

阅读建议:配合 01-types-and-protocol.md(类型逐行精读)与 02-agent-loop.md(循环逐行精读)一起看。

一、分层架构

Pi 是一个严格的分层架构。agent 层是纯逻辑,不依赖任何 I/O —— 这是整个设计的核心洞察。

┌─────────────────────────────────────────────────────────┐
│  coding-agent 层  (CLI / TUI / 文件工具)                  │  ← 产品层
│    pi-main: packages/coding-agent                         │
│    rust:    src/main.rs + src/interactive.rs + src/cli.rs │
├─────────────────────────────────────────────────────────┤
│  harness 层  (会话持久化 / 上下文压缩 / 系统提示 / 工具注册)  │  ← 基础设施
│    pi-main: packages/agent/src/harness/                   │
│    rust:    src/session.rs + src/compaction.rs            │
├─────────────────────────────────────────────────────────┤
│  agent 层    (消息类型 / agent 循环 / 工具执行 / 状态)       │  ← 纯逻辑(灵魂)
│    pi-main: packages/agent/src/agent-loop.ts + types.ts   │
│    rust:    src/agent.rs核心循环+ src/model.rs         │
├─────────────────────────────────────────────────────────┤
│  ai 层       (协议类型 / 流式接口 / provider HTTP 适配)      │  ← 网络
│    pi-main: packages/ai/                                  │
│    rust:    src/provider.rs + src/providers/ + src/sse.rs │
└─────────────────────────────────────────────────────────┘
职责 纯 std 可行性
ai 层 多 provider 统一流式 API、SSE 解析、HTTP/TLS TLS 是硬墙;可抽 Transport trait
agent 层 消息类型、agent 循环、工具 trait、状态管理、事件 ⚠️ 大部分可行,需手写 JSON
harness 层 会话树持久化(JSONL)、上下文压缩、系统提示模板 完全可行
coding-agent 层 CLI、文件工具(read/write/edit/bash/grep) 完全可行std::fs + std::process

二、核心数据流(一次完整 run

用户调用 harness.prompt("做 X")
        │
        ▼
┌─ runAgentLoop ────────────────────────────────────────────┐
│  emit(agent_start)                                        │
│  把 user message 加入 context.messages                    │
│                                                            │
│  ┌─ runLoop双层循环──────────────────────────────────┐  │
│  │  外层 while: follow-up 循环                            │  │
│  │    内层 while: steering + 工具循环                     │  │
│  │      │                                                 │  │
│  │      │  (1) streamAssistantResponse ─── 边界转换 ──┐   │  │
│  │      │      transformContext (AgentMsg→AgentMsg)   │   │  │
│  │      │      convertToLlm    (AgentMsg→Message[])   │   │  │
│  │      │      streamFn(model, llmContext) ───────────┼──▶│  │ provider
│  │      │      for await event: 累积流式 assistant     │   │  │ (SSE→Event)
│  │      │      return finalMessage ◀──────────────────┘   │  │
│  │      │                                                 │  │
│  │      │  (2) 提取 toolCalls                            │  │
│  │      │      validateToolArguments                     │  │
│  │      │      beforeToolCall hook (可 block)            │  │
│  │      │      execute (sequential | parallel)           │  │
│  │      │      afterToolCall hook (可覆盖结果)            │  │
│  │      │      → ToolResultMessage[]                     │  │
│  │      │                                                 │  │
│  │      │  (3) prepareNextTurn (harness 每轮重建上下文)   │  │
│  │      │      shouldStopAfterTurn?                       │  │
│  │      │      getSteeringMessages() 再 poll             │  │
│  │      └─────────────────────────────────────────────┘  │  │
│  │    getFollowUpMessages()                              │  │
│  └───────────────────────────────────────────────────────┘  │
│  emit(agent_end)                                          │
└────────────────────────────────────────────────────────────┘

四个关键设计点(详见 02-agent-loop.md

  1. 边界处转换AgentMessage[] 永远不直接发给 LLMconvertToLlm 是唯一翻译点。
  2. 流式消息是"活"的:流式过程中 assistant 消息就躺在 context.messages 末尾,随 delta 原地修改。
  3. prepareNextTurn 重建harness 每 turn 后从 session 重载消息、重算系统提示、重建工具列表。
  4. Steering vs Follow-upsteering 在 agent 工作时插入(内层 pollfollow-up 在 agent 要停下时检查(外层 poll

三、两个类型宇宙

Pi 区分 LLM Messageprovider 说的语言)和 AgentMessage(可扩展的 transcript

// LLM Messagepi-ai/types.ts—— 三种角色role 做顶层分发
Message = UserMessage | AssistantMessage | ToolResultMessage

// AgentMessageagent/types.ts—— 在 Message 之上扩展自定义角色
type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages]
//                                       ^^^^^^^^^^^^^^^^^^^^
//                        coding-agent 通过 declaration merging 加:
//                        bashExecution / custom / branchSummary / compactionSummary

自定义消息不直接发给 LLMconvertToLlm 在边界转成 Message[]。这让 transcript 能承载 UI 专属信息而不污染 LLM 上下文。

内容块ContentBlocktype 做内部分发

块类型 出现在 说明
TextContent user / assistant / toolResult 普通文本
ThinkingContent assistant only 推理过程(带签名防篡改)
ImageContent user / toolResult非 assistant base64 图片
ToolCall assistant only 工具调用请求

详细类型定义见 01-types-and-protocol.md

四、流式模型

Pi 没有用 EventEmitter 或回调,而是手搓了 EventStream<T, R>

class EventStream<T, R> implements AsyncIterable<T> {
    push(event: T)         // 生产者 API
    end(result?: R)
    result(): Promise<R>   // 独立的"最终结果" promise
    [Symbol.asyncIterator]()  // for await 消费
}

同一个对象既是可迭代流(边收 delta又能 await response.result()(拿完整消息)

流式事件协议 AssistantMessageEvent —— 每个事件都带完整 partial 快照,消费者无需自己累积:

| { type: "start"; partial: AssistantMessage }
| { type: "text_delta";  contentIndex; delta; partial }
| { type: "toolcall_delta"; contentIndex; delta; partial }
| { type: "done";  reason: "stop"|"length"|"toolUse"; message }
| { type: "error"; reason: "aborted"|"error"; error }

五、工具抽象

interface AgentTool<TParameters, TDetails> extends Tool {
    label: string
    prepareArguments?: (args: unknown) => Static<TParameters>   // 预验证 shim
    execute: (
        toolCallId: string,
        params: Static<TParameters>,
        signal?: AbortSignal,
        onUpdate?: (partialResult: AgentToolResult<TDetails>) => void,  // 流式更新
    ) => Promise<AgentToolResult<TDetails>>
    executionMode?: "sequential" | "parallel"   // per-tool 覆盖
}

interface AgentToolResult<T> {
    content: (TextContent | ImageContent)[]   // 模型看到的
    details: T                                 // UI/日志用的(不发给模型)
    addedToolNames?: string[]                  // 延迟加载工具
    terminate?: boolean                        // batch 级停止提示
}

关键设计:content(模型看到)和 detailsUI 看)分离terminate 是 batch 级的——只有当批内所有结果都设 terminate: true 才真正停止(一个"继续"结果可否决停止)。

两种执行模式agent-loop.ts:413-556

  • sequentialprepare→execute→finalize 一个一个来
  • parallel:所有 prepare 顺序完成execute 并发(Promise.alltool_execution_end 按完成顺序发射,但 ToolResultMessage 按 assistant 源顺序发射

Rust 版多了 TS 没有的 ToolEffects 位标志调度tools.rs:37-155每个工具声明 READ|WRITE|NETWORK|PROCESSplan_tool_effect_batches 把兼容工具分批并发。

六、Provider 实现(以 Anthropic 为例)

streamSimplestreamstream 职责:构造 HTTP 请求 → 解析 SSE → 翻译成 AssistantMessageEvent

Anthropic SSE → pi-ai 事件映射anthropic-messages.ts:559-732

Anthropic SSE pi-ai 动作
message_start 种入 responseId + 初始 usage
content_block_start (text/thinking/tool_use) push 对应空块emit *_start
content_block_delta text_delta/thinking_delta 累加到块emit *_delta
content_block_delta input_json_delta partialJson += ...; arguments = parseStreamingJson(partialJson)
content_block_stop toolCall: 解析最终 arguments删除 scratch 字段
message_delta mapStopReasonend_turn→stop, max_tokens→length, tool_use→toolUse

七、Harness 层(可选基础设施)

功能 TS 位置 核心设计
系统提示 harness/system-prompt.ts 每轮重建(可注入 cwd/git 等动态上下文)
上下文压缩 harness/compaction/compaction.ts shouldCompactfindCutPointgenerateSummary滚动总结,不从头重算)
会话持久化 harness/session/ 树结构(非列表)+ JSONL 追加写
工具注册 harness/ ToolRegistry + before/afterToolCall hooks

会话存储的关键设计

  • 是树不是列表:每个 entry 有 id + parentIdgetBranch(leafId) 返回根到叶路径——会话分叉/版本管理的基础
  • JSONL 追加写:每条一行 JSON永不修改appendEntry 就是 appendFile(line + "\n")
  • 目录隔离encodeCwd("/home/foo")--home-foo--

八、TS → Rust 移植对照

设计点 TS (pi-main) Rust (pi_agent_rust)
消息类型 interface + union enum + serde tag字段名/标签完全对应)
流式消息 直接替换 Arc<AssistantMessage> + Arc::make_mut COW
流式事件 单一 AssistantMessageEvent 拆成内部 StreamEvent(不序列化)+ 外部 AssistantMessageEvent(序列化)
工具执行 per-tool executionMode 额外加 ToolEffects 位标志 + 批次调度
异步运行时 Node 事件循环 自研 asupersync(能力化 Cx + 预算 + TLS + HTTP/1.1),刻意避开 tokio/reqwest
取消机制 AbortSignal (DOM) asupersync::sync::Notify + AtomicBool + Cx::checkpoint()

Rust 版最值得注意的是:用自研 asupersync 完全替代 tokio + reqwest + rustls。作者追求极简依赖,但即便如此,serde/serde_json/futures/async-trait 仍结构性无法省掉——这印证了"agent 层是纯逻辑但需要 JSON 序列化"的判断。

九、"纯 std" 可行性总结(针对 focus 实现)

组件 纯 std? 说明
agent 循环逻辑 纯状态机调度,for 循环 + channel
消息类型 ⚠️ 需手写 JSON 编解码(~300 行极简版),或认栽用 serde
工具 trait Rust 1.75+ 原生 async trait无需 async-trait
流式事件 mpsc::Receiver 替代 Stream
会话持久化 JSONL + std::fs
文件工具 std::fs + std::process::Command
上下文压缩 纯算法
provider 网络 TLS 是硬墙 → 抽 Transport trait实现用 curl 子进程

结论focus 的 agent 层 + harness 层 + 工具层可以做到几乎纯 std网络层必须留 trait 边界。建议 workspace 拆分:

focus/
├── Cargo.toml              # [workspace]
├── AGENTS.md               # 工程规范
└── crates/
    ├── focus-json/         # 极简 JSON 解析/序列化(零外部依赖)
    ├── focus-core/         # agent 层:类型、循环、工具 trait纯 std + 手写 JSON无 tokio
    ├── focus-transport/    # 传输层HTTPStokio + rustls、HTTP/1.1、SSE
    ├── focus-providers/    # provider 实现Anthropic / OpenAI
    ├── focus-harness/      # 会话/压缩/系统提示(纯 std
    ├── focus-tools/        # 文件工具read / write / edit / shell
    └── focus-tui/          # 终端交互层ratatui + crossterm

注:早期规划中的 focus-cli 最终由 focus-tui 取代。

十、学习路线

第一阶段:吃透核心(按顺序)

  1. 01-types-and-protocol.mdtypes.ts 逐行精读(建立词汇表)
  2. 02-agent-loop.mdagent-loop.ts 核心循环逐行精读Pi 的心脏)
  3. pi-main/packages/ai/src/utils/event-stream.tsEventStream 实现

第二阶段:真实 provider

  1. pi-main/packages/ai/src/api/anthropic-messages.tsstream 函数的 SSE 翻译

第三阶段harness可选

  1. pi-main/packages/agent/src/harness/agent-harness.tsprepareNextTurn + hook 系统
  2. pi-main/packages/agent/src/harness/compaction/compaction.ts — 上下文压缩
  3. pi-main/packages/agent/src/harness/session/ — 会话树

Rust 版对照阅读(按需 grep不通读

  • src/model.rs — 对照 TS 类型
  • src/agent.rs:1453 run_loop — 对照双层循环
  • src/agent.rs:1960 stream_assistant_response — 对照边界转换
  • src/tools.rs:158-191 Tool trait — 对照 AgentTool
  • src/sse.rs — 可学习的纯 SSE 解析(除 SseStream 的 futures 部分)
  • src/agent_cx.rs — 能力化上下文180 行,可通读)

避开(产品化代码,非核心):extensions*.rs(2MB+ WASM/JS)、main.rs(287KB CLI)、interactive.rs(TUI)