oh-my-pi 全景代码解析(三):pi-agent-core 运行时与状态机

📑 目录

oh-my-pi 全景代码解析(三):pi-agent-core 运行时与状态机

系列导读:本文深入 Pi 架构的心脏——pi-agent-core 包。这个包实现了 Agent 的核心请求-响应循环(Request-Response Loop),负责管理 Agent 的状态、工具调用生命周期、权限决策、以及与上下层的交互。我们将从核心循环的伪代码出发,逐步剖析状态机设计、权限门机制、工具注册系统、事件总线、以及无子 Agent 的设计哲学。


一、核心循环:Agent 的"心跳"

Agent 的核心循环是其全部行为的源泉。这个循环不是简单的"发送请求-接收响应",而是一个复杂的状态机驱动的异步生成器,需要处理流式输出、工具调用、用户中断、权限确认、上下文压缩等多种并发事件。

1.1 循环的整体结构

graph TD
    subgraph "Agent Loop Phases"
        A[Start Turn] --> B[Build Context]
        B --> C[Check Compaction]
        C -->|If needed| D[Compact Context]
        C -->|No need| E[Call LLM]
        D --> E
        E --> F[Process Stream]
        F --> G{Tool Calls?}
        G -->|Yes| H[Check Permission]
        H -->|Denied| I[Inject Denial]
        H -->|Ask| J[Wait User Approval]
        J -->|Approved| K[Execute Tool]
        J -->|Rejected| I
        H -->|Allowed| K
        K --> L[Inject Result]
        L --> M{More Tools?}
        M -->|Yes| H
        M -->|No| N[Turn Complete]
        G -->|No| N
        N --> O{User Input?}
        O -->|Message| P[Append to Context]
        O -->|Steering| Q[Interrupt + Append]
        O -->|Follow-up| R[Queue for Next Turn]
        O -->|None| S[End Session]
        P --> A
        Q --> A
        R --> A
    end

核心循环使用异步生成器实现双向通信:

async function* runAgentLoop(config, context) {
    while (!state.isInterrupted) {
        const turnContext = await buildTurnContext(context);
        const stream = await streamSimple(model, turnContext);

        for await (const event of stream) {
            yield event;  // To UI layer
            // Receive user input via yield return
        }
    }
}

1.2 五阶段结构

循环被明确分为五个阶段,每个阶段都有明确的 yield 事件:

Turn=BuildContextCallLLMProcessStreamExecuteToolsTurnComplete\text{Turn} = \text{BuildContext} \rightarrow \text{CallLLM} \rightarrow \text{ProcessStream} \rightarrow \text{ExecuteTools} \rightarrow \text{TurnComplete}

sequenceDiagram
    participant User
    participant UI as pi-tui
    participant Agent as pi-agent-core
    participant AI as pi-ai
    participant Ext as Extension

    User->>UI: Send message
    UI->>Agent: runAgentLoop()

    Agent->>Ext: onBeforeTurn()
    Ext-->>Agent: Injected memories

    Agent->>AI: streamSimple()
    AI-->>Agent: text_delta
    Agent-->>UI: yield text_delta
    UI-->>User: Display text

    AI-->>Agent: tool_call_start
    Agent->>Ext: onCheckPermission()
    Ext-->>Agent: ask

    Agent-->>UI: yield permission_request
    UI-->>User: Show confirm dialog
    User-->>UI: Approve
    UI-->>Agent: return approval

    Agent->>Ext: onBeforeToolUse()
    Agent->>Tool: execute()
    Tool-->>Agent: result
    Agent->>Ext: onAfterToolUse()

    Agent-->>UI: yield tool_result
    UI-->>User: Display result

    AI-->>Agent: done
    Agent-->>UI: yield turn_complete

二、状态机:Agent 的"记忆"

Agent 的状态不是简单的"当前消息列表",而是一个包含丰富元数据的状态机。

2.1 AgentState 数据结构

classDiagram
    class AgentState {
        +Context context
        +number turnCount
        +number totalCost
        +number totalTokens
        +Map pendingToolCalls
        +boolean isInterrupted
        +Model currentModel
        +string sessionPath
        +string currentNodeId
        +string[] branchStack
        +Map lastToolResults
        +Map fileReadCache
        +Map extensionStates
    }

    class Context {
        +string systemPrompt
        +Message[] messages
        +number temperature
        +number maxTokens
    }

    class SessionNode {
        +string id
        +string parentId
        +NodeType type
        +string role
        +string content
        +NodeMetadata metadata
        +string[] children
        +string[] bookmarks
    }

    AgentState --> Context
    AgentState --> SessionNode

2.2 状态转换图

stateDiagram-v2
    [*] --> Idle
    Idle --> BuildingContext: User message
    BuildingContext --> CallingLLM: Context ready
    CallingLLM --> ProcessingStream: Stream started
    ProcessingStream --> CheckingPermission: Tool call
    ProcessingStream --> WaitingForInput: No tools
    CheckingPermission --> WaitingForApproval: Ask
    CheckingPermission --> ExecutingTool: Allow
    CheckingPermission --> ProcessingStream: Deny
    WaitingForApproval --> ExecutingTool: Approve
    WaitingForApproval --> ProcessingStream: Reject
    ExecutingTool --> ProcessingStream: More tools
    ExecutingTool --> TurnComplete: All done
    TurnComplete --> Idle: Follow-up
    TurnComplete --> Idle: Steering
    TurnComplete --> [*]: End

三、权限门:安全与自由的平衡

Pi 的权限系统是一个经典的默认开放、可选收紧设计。默认 YOLO 模式——所有工具调用自动执行。

3.1 权限决策流程

graph TD
    A[Tool Call Request] --> B{YOLO Mode?}
    B -->|Yes| C[Allow]
    B -->|No| D{Whitelist?}
    D -->|Not in list| E[Deny]
    D -->|In list| F{Extension Rules?}
    F -->|Deny| E
    F -->|Ask| G[Ask User]
    F -->|Allow| H{Tool Rules?}
    H -->|Match| I[Apply Rule Action]
    H -->|No match| J[Default Policy]
    J -->|Allow| C
    J -->|Ask| G
    J -->|Deny| E
    G -->|Approve| C
    G -->|Reject| E

核心代码:

async function checkToolPermission(
    toolName: string,
    args: Record<string, unknown>,
    config: AgentConfig,
    extensions: Extension[]
): Promise<"allow" | "deny" | "ask"> {

    if (config.yoloMode) return "allow";

    if (config.allowedTools && !config.allowedTools.includes(toolName)) {
        return "deny";
    }

    for (const ext of extensions) {
        const decision = await ext.onCheckPermission?.({ toolName, args, config });
        if (decision === "deny") return "deny";
        if (decision === "ask") return "ask";
    }

    return config.defaultPermission || "ask";
}

3.2 为什么默认 YOLO?

Mario Zechner 的哲学:

"Pi 默认不询问权限,因为编码 Agent 的核心价值在于流畅性。如果每次文件读取、每次 grep 搜索都需要确认,Agent 的实用性会大幅下降。"

这与 Unix 的设计一致:默认给予用户完全的自由,但提供强大的机制让用户按需收紧


四、工具注册系统:从声明到执行

Pi 的工具系统不是静态的函数列表,而是一个动态的、可扩展的注册表

4.1 工具注册表架构

graph TB
    subgraph "Tool Registry"
        R[Map string, ToolDefinition]
    end

    subgraph "Built-in Tools"
        T1[read]
        T2[write]
        T3[edit]
        T4[bash]
    end

    subgraph "Optional Tools (Rust)"
        T5[grep]
        T6[find]
        T7[ls]
    end

    subgraph "Extension Tools"
        T8[deploy]
        T9[test]
        T10[lint]
    end

    T1 -->|register| R
    T2 -->|register| R
    T3 -->|register| R
    T4 -->|register| R
    T5 -->|register| R
    T6 -->|register| R
    T7 -->|register| R
    T8 -->|register| R
    T9 -->|register| R
    T10 -->|register| R

    R -->|toLLMTools| LLM[LLM Context]
    R -->|execute| Exec[Tool Execution]

4.2 工具定义接口

interface ToolDefinition {
    name: string;
    description: string;
    parameters: JSONSchema;
    execute: (args, ctx) => Promise<unknown>;
    checkPermission?: (args, ctx) => Promise<"allow" | "deny" | "ask">;
    postProcess?: (result, ctx) => Promise<unknown>;
}

五、事件总线:扩展的神经系统

Agent 核心循环通过事件总线与扩展通信。这是一个有序、可拦截、可修改的事件处理系统。

5.1 事件类型全景

graph LR
    subgraph "Agent Events"
        E1[phase]
        E2[text_delta]
        E3[thinking_delta]
        E4[tool_call_start]
        E5[tool_call_delta]
        E6[tool_call_complete]
        E7[usage]
        E8[error]
        E9[tool_executing]
        E10[tool_result]
        E11[tool_error]
        E12[tool_denied]
        E13[permission_request]
        E14[compaction]
        E15[turn_complete]
        E16[session_end]
        E17[interrupted]
        E18[retry]
        E19[wait_for_input]
    end

5.2 扩展事件处理顺序

sequenceDiagram
    participant Agent
    participant Ext1 as Extension 1
    participant Ext2 as Extension 2
    participant Ext3 as Extension 3

    Agent->>Ext1: onBeforeTurn()
    Ext1-->>Agent: messages
    Agent->>Ext2: onBeforeTurn()
    Ext2-->>Agent: messages
    Agent->>Ext3: onBeforeTurn()
    Ext3-->>Agent: messages

    Agent->>Ext1: onCheckPermission()
    Ext1-->>Agent: allow
    Agent->>Ext2: onCheckPermission()
    Ext2-->>Agent: ask
    Note over Agent: Stop at first ask/deny

六、无子 Agent 的设计哲学

Pi 最引人注目的架构决策之一是不内置子 Agent 工具

6.1 为什么不要内置子 Agent?

Mario Zechner 的解释:

"子 Agent 是黑盒。当 Claude Code 启动一个子 Agent 时,你对其内部行为零可见性。它读取了哪些文件?它做了什么决策?它犯了什么错误?如果子 Agent 失败,调试是痛苦的。"

6.2 替代方案:bash 启动新进程

graph TB
    subgraph "Claude Code Approach"
        A1[Task Tool] --> A2[Sub-Agent 1]
        A1 --> A3[Sub-Agent 2]
        A1 --> A4[Sub-Agent 3]
        A2 --> A5[Black Box Execution]
        A3 --> A5
        A4 --> A5
    end

    subgraph "Pi Approach"
        B1[bash Tool] --> B2[spawn pi -p "task"]
        B2 --> B3[New Process in tmux]
        B3 --> B4[Fully Observable]
        B3 --> B5[Interactable]
        B3 --> B6[Debuggable]
    end

6.3 上下文收集模式

graph LR
    subgraph "Session 1: Context Gathering"
        S1A[Analyze codebase] --> S1B[Save CONTEXT.md]
    end

    subgraph "Session 2: Implementation"
        S2A[Read CONTEXT.md] --> S2B[Implement feature]
    end

    S1B -->|Artifact| S2A

七、错误处理与恢复策略

Agent 的错误处理不是简单的 try-catch,而是一个分层的、可恢复的策略系统

7.1 错误分类与恢复策略

graph TD
    A[Error Occurred] --> B{Classify Error}

    B -->|LLM Error| C{Retryable?}
    C -->|Yes| D[Exponential Backoff Retry]
    C -->|No| E[Fallback Model]

    B -->|Context Error| F{Context Limit?}
    F -->|Yes| G[Compact Context]
    G --> H[Retry]

    B -->|Tool Error| I{Tool-specific?}
    I -->|Yes| J[Tool PostProcess]
    I -->|No| K[Inject Error to Context]

    B -->|Permission Error| L[Inject Denial]

    B -->|Config Error| M[Ask User]

    D -->|Max retries| E
    E -->|No fallback| N[Abort Session]
    H -->|Still fails| N
    J -->|Fixed| H
    K --> H
    L --> H
    M --> H

八、性能优化:Agent 循环的微观优化

8.1 消息历史的增量更新

使用链表结构而非数组,追加操作是 O(1):

interface MessageNode {
    message: Message;
    next: MessageNode | null;
    prev: MessageNode | null;
}

function appendMessage(list, message) {
    const newNode = { message, next: null, prev: list.tail };
    if (list.tail) list.tail.next = newNode;
    else list.head = newNode;
    list.tail = newNode;
    list.length++;
}

8.2 Token 计数缓存

function estimateTokens(context) {
    let total = countTokens(context.systemPrompt);
    for (const msg of context.messages) {
        if (msg._tokenCount && msg._tokenCountModel === context.model.id) {
            total += msg._tokenCount;  // Cache hit
        } else {
            const count = countTokens(msg.content);
            msg._tokenCount = count;
            msg._tokenCountModel = context.model.id;
            total += count;
        }
    }
    return total;
}

九、设计原则总结

mindmap
  root((pi-agent-core 原则))
    生成器优先
      双向通信
      yield 事件
      yield return 输入
    阶段透明
      每阶段 yield
      UI 可显示状态
      扩展可拦截
    默认开放
      YOLO 模式
      权限扩展 opt-in
    无子 Agent
      bash 启动新进程
      tmux 可观测
      上下文收集模式
    持久化状态
      显式可序列化
      可恢复
    错误可恢复
      分层策略
      自动重试
      降级
      压缩
      用户介入

pi-agent-core 是 Agent"思考"和"行动"的引擎室。它的设计体现了生成器优先、阶段透明、默认开放、无子 Agent、持久化状态、错误可恢复六大核心原则。

在下一篇中,我们将深入 pi-tui,剖析这个不依赖任何现有终端 UI 库的差分渲染系统。


本系列基于 oh-my-pi v2026.07 和 Pi Agent v0.66+ 的公开源码与文档编写。