说明:主要是人工写作完成的,但部分由 AI(Claude Code + DeepSeek + Waza Skills)完成的,持续更新(通常这句话是落空的)。
学习如何开发 Agent?我还是持续学习的一个过程,从理论到代码,但是缺少的是具体实践,希望自己从这个小文开始慢慢可以学着开发出一个 Agent 来,但是有多慢是不是会弃坑,回顾过去我总是这样的,但是希望这次真的不会,担心的一点由于足够慢,自己写的东西可能很快落后了,但是落后也比啥都没有强。
Agent 定义和架构
什么是 Agent?
一个 AI Coding Agent,本质上是在「用户输入 → LLM 推理 → 工具调用 → 结果反馈 → LLM 再推理」这个循环里跑的程序。拆开来看就四块:
- Agent Loop(主循环):驱动整个对话流转,管理 turn 的边界
- LLM API(模型调用):对接不同大模型,发送消息、接收响应
- Tool System(工具系统):让 LLM 能执行实际操作(读文件、执行命令、编辑代码)
- Context/Memory Management(上下文与记忆管理):管理对话历史、项目上下文、Skills 等
pi-mono 的分层设计
pi-mono 把 Agent 拆成了两层,这个设计思路很值得借鉴:
pi-coding-agent (Harness 层:CLI、资源加载、Session 管理、工具定义)
↓ 依赖
pi-agent-core (Runtime 层:Agent Loop、AgentHarness、状态管理)
↓ 依赖
pi-ai (API 层:多 Provider 统一接口、stream、类型定义)
核心思想:pi-agent-core 是「笨」的执行器,pi-coding-agent 是「聪明」的编排器。所有跟产品体验相关的内容(项目上下文、AGENTS.md、Skills、工具定义)都由 coding-agent 解析好,以纯字符串和工具对象的形式注入 agent-core。agent-core 只管跑 loop、调工具、管理状态,不关心这些内容从哪来。
这样分层后,如果想基于 pi-agent-core 做一个完全不同的 Agent(比如数据分析的、客服的),替换 coding-agent 这一层就够了,core 不用动。
Agent Loop 的运作方式
Agent Loop 是 Agent 的心跳。pi-mono 的 Agent Loop(packages/agent/src/agent-loop.ts)是一个双重循环:
外层循环(Follow-up),处理 follow-up 消息队列。Agent 完成一轮工作准备停下时,队列里如果有 follow-up 消息,就继续跑。
内层循环(Turn),单次对话轮次。每个 turn 包含:
- 发送消息给 LLM,流式接收响应
- 解析响应中的 tool_calls
- 执行工具(并行或串行),获取结果
- 将工具结果追加到上下文
- 检查 steering 消息队列,有新消息就注入
- 判断是否继续(还有 tool calls?有 steering 消息?)
- 继续的话回到第 1 步
下面是 Agent Loop 的简化伪代码:
while (有 follow-up 消息 或 有 tool calls 待处理) {
// 1. 注入待处理消息(steering 或 follow-up)
if (pendingMessages.length > 0) {
context.messages.push(...pendingMessages)
}
// 2. 调用 LLM
messages = convertToLlm(context.messages) // AgentMessage[] → LLM Message[]
response = await streamFn(model, { systemPrompt, messages, tools })
// 3. 执行工具调用
toolCalls = response.content.filter(c => c.type === "toolCall")
toolResults = await executeTools(toolCalls) // 支持并行/串行
context.messages.push(...toolResults)
// 4. 检查是否继续
pollingMessages = await getSteeringMessages()
}
Agent 类(agent.ts)是对 Loop 的有状态封装,管理着当前对话记录(messages)、可用工具(tools)、系统提示词(systemPrompt)、思考级别(thinkingLevel),以及两个消息队列:steering(注入当前对话流)和 followUp(Agent 空闲后触发)。Agent 通过 subscribe() 暴露事件系统,外部监听 agent_start、turn_start、message_start、message_update、message_end、tool_execution_start/end、agent_end 等生命周期事件来驱动 UI 更新。
开源 Agents 实现的分析小结
坚持把分析 pi-mono 和 bub 这个事情坚持下去,同时结合最近不同人对于 Calude Code 源码分析文章(分析源码的实践做法,参考这个:# 如何阅读一份源代码?(2020年版),下面是简单的摘录)。
跑起来;明确目的;区分主线和支线剧情;纵向(单模块)和横向(模块之间的依赖);情景分析;利用好测试用例;厘清核心数据结构之间的关系;多问自己几个问题;写自己的代码阅读笔记。
以上是我简单总结的一些阅读源码时候的手段和注意方法,大体而言有那么几点吧:
- 只有更好的输出才能更好的消化知识,所谓的搭建调试环境、情景分析、多问自己问题、写代码阅读笔记等都是围绕输出来展开的。总而言之,不能像一条死鱼一样指望着光靠看代码就能完全理解它的原理,需要想办法跟它互动起来。
- 写作是人的基础硬实力之一,不仅锻炼自己表达能力,还能帮助整理自己的思路。对程序员而言锻炼写作能力的手段之一就是写博客,越早开始锻炼越好。
最后,如同任何可以习得的技能一般,阅读代码这种能力也需要长时间、大量的反复练习,下一次就从自己感兴趣的项目开始锻炼自己的这种技能吧。
分析 pi-mono 目的是啥?1)如何实现加载 harness 相关的内容?2)如何管理记忆(状态)?3)如何管理 skills 和按需加载?4)如何管理工具调用?
分析 Bub 目的是啥?1)如何实现相关的理念的?2)如何对接 telegram?3)如何实现 tape 系统?
分析 Claude Code 目的是啥?
还有一个可能的参考项目,Agent-SDK without CLI dependencies, as an alternative to claude-agent-sdk, completely open source
pi-mono
项目地址:https://github.com/badlogic/pi-mono
不同 packages 的介绍:
- pi-ai: "Unified multi-provider LLM API (OpenAI, Anthropic, Google, etc.)" 通过 API 对接各种不同大模型
- pi-agent-core: "Agent runtime with tool calling and state management" 带工具调用和状态管理的 agent
- pi-coding-agent: "Interactive coding agent CLI" 实现了 Coding Agent CLI,依赖 agent-code 和 ai
- 其他的包括 pi-mom,pi-tui,pi-web-ui,pi-pods,不做分析
如何运行
基于 readme,npm install/run build/run check 完成后,以及 ./test.sh 之后,运行 ./pi-test.sh,启动 agent,然后执行 /login 选择 LLM provider,我选了 Github Copilot,基于提示完成登录后执行 /model 选择相应的模型,如:Opus 4.6,完成就可以用这个测试的 coding-agent 了。
pi-ai
主要问题:如何对接不同大模型?
pi-ai 是 pi-mono 的模型接入层,不在这次分析的重点范围。它通过统一接口 streamSimple(model, context, options) 屏蔽了不同 Provider(Anthropic、OpenAI、Google、GitHub Copilot 等)的差异,上层代码不关心具体是哪个 Provider,传 model 对象和消息上下文就行。agent-core 和 coding-agent 可以自由切换模型,核心逻辑不动。
pi-agent-core
Agent Core 的定位,以及其中的概念?
什么是 Core?为什么需要 Core?如何实现和使用 Core?
Stateful agent with tool execution and event streaming. - pi-agent-core
pi-agent-core 定位为一个「有状态的 Agent 运行时」,提供三个核心能力:
Agent 类(agent.ts),对 Agent Loop 的有状态封装。持有当前的 messages(对话记录)、tools(工具列表)、systemPrompt、model、thinkingLevel。通过 prompt() 启动一轮对话,通过 continue() 从当前状态继续,通过 subscribe() 暴露事件系统,外部监听 Agent 生命周期中的每个节点。
AgentHarness 类(harness/agent-harness.ts),在 Agent 之上再加一层抽象,负责:
- Session 持久化:每次对话自动写入 session 文件
- 事件钩子系统:
on() 注册特定事件的钩子(tool_call、before_agent_start、before_provider_request 等),subscribe() 监听所有事件 - 消息队列管理:steering(中途插入的消息)、followUp(Agent 空闲后继续的消息)、nextTurn(下个 turn 的消息)
- 资源管理:skills、promptTemplates 的管理和注入
- 状态协调:模型切换、thinking level 切换、工具列表变更时同步持久化
AgentHarness 几个设计细节:
- 不直接调用 LLM,而是通过
createStreamFn 创建闭包,每次 LLM 调用前动态解析 API key、headers、sessionId。OAuth token 可能过期,所以每次调用都重新获取。 prepareNextTurn 钩子:每个 turn 结束后重建 turnState(最新 context、model、thinkingLevel),下个 turn 用最新状态。用户可以在对话中途切换模型或思考级别。pendingSessionWrites 机制:Agent 运行期间,session 写入先缓存起来,turn 结束时批量刷入,不用每次 event 都触发磁盘 I/O。
Session 系统(harness/session/),管理对话历史的持久化存储,后面细说。
pi-agent-core 本身不定义任何具体工具,也不加载任何具体资源——它只提供运行 Agent 的「引擎」。所有具体内容(工具定义、Skills、系统提示词)都由上层(coding-agent)注入。
AgentState 状态
如何调用工具?
pi-mono 的工具系统设计和 Anthropic 的 tool use 协议对齐,在上层做了工程化封装。工具调用的完整流程:
工具定义(AgentTool 接口,types.ts)
interface AgentTool<TParameters, TDetails> extends Tool<TParameters> {
label: string; // 人类可读标签
prepareArguments?: (args: unknown) => Static<TParameters>; // 参数预处理(兼容老旧模型输出)
execute: (toolCallId, params, signal?, onUpdate?) => Promise<AgentToolResult<TDetails>>; // 执行
executionMode?: "sequential" | "parallel"; // 执行模式
}
prepareArguments 是一个兼容性钩子:有些模型输出可能不符合 schema 的 tool call 参数,这个函数在 schema 验证之前运行,可以修正参数格式。
2. 工具调用流程(agent-loop.ts)
prepareToolCall:
1. 查找工具定义
2. 运行 prepareArguments(如果有)
3. schema 验证参数
4. 调用 beforeToolCall 钩子(外部可以 block 某个工具调用)
5. 返回 prepared(准备就绪)或 immediate(立即错误)
executePreparedToolCall:
1. 调用 tool.execute()
2. 支持 onUpdate 回调,工具可以流式输出执行进度
3. 异常被捕获,转换为 error 结果(不抛异常,不中断 loop)
finalizeExecutedToolCall:
1. 调用 afterToolCall 钩子(外部可以修改工具结果、标记错误、设置 terminate)
2. terminate = true 表示这个工具希望 Agent 停止(当一批工具中所有工具都 set terminate,loop 提前退出)
并行 vs 串行执行,工具执行支持两种模式:
parallel(默认):先串行 prepare(因为 prepare 可能依赖前一个工具的结果),然后所有「已准备好」的工具并发执行。执行完成的顺序可能与工具在 assistant message 中出现的顺序不同,但最终 tool_result 消息按原始顺序排列。sequential:逐个执行,每个工具的 prepare → execute → finalize 完成后才开始下一个。适用于有副作用的操作(如文件编辑),避免竞态条件。
内置工具,pi-coding-agent 提供 7 个核心工具(core/tools/index.ts):
| 工具 | 功能 |
|---|
read | 读取文件,支持分页、截断、图片渲染 |
bash | 执行 shell 命令,支持超时、stdin、后台运行 |
edit | 基于精确字符串替换的文件编辑 |
write | 创建或覆写文件 |
grep | 基于 ripgrep 的内容搜索 |
find | 基于 fd 的文件搜索 |
ls | 列出目录内容 |
工具分两组:createCodingTools(read + bash + edit + write,完整读写权限)和 createReadOnlyTools(read + grep + find + ls,只读)。用户可以通过 --no-tools 或 --tools 控制启用的工具。
扩展工具,Extension 系统允许第三方通过 extensions 目录注册自定义工具。Extension 用 TypeScript/JavaScript 编写,可注册:自定义 tool(带 schema 和 execute 函数)、自定义 slash command、自定义 CLI flag。Extension 之间如果有命名冲突,以 load order 先到先得,冲突会报告为诊断警告但不阻止加载。
如何构建 system prompt,并调用 LLM API?
System Prompt 的构建过程
System Prompt 在 core/system-prompt.ts 的 buildSystemPrompt() 中构建,支持两种模式:
- 自定义 Prompt 模式(用户通过
--system-prompt 或 .pi/SYSTEM.md 指定):以用户提供的 prompt 为基础,追加:project context files(AGENTS.md/CLAUDE.md 从根目录到当前目录的所有文件)→ skills 列表(仅元数据,不加载完整内容)→ 日期和当前工作目录 默认 Prompt 模式:pi 内置了一套完整的默认 prompt,包括:
- 角色设定:「你是一个运行在 pi coding agent harness 里的专家编程助手」
- 工具列表和一句话描述
- 使用指南:优先用 grep/find/ls 而不是 bash 做文件探索、保持简洁、标注文件路径
- 项目上下文(AGENTS.md/CLAUDE.md)
- Skills 元数据(
<available_skills> XML 块,符合 agentskills.io 规范) - 日期和工作目录
Skills 的完整内容不在 system prompt 里,system prompt 只放 skills 的 name、description、filePath(以 XML 格式嵌入)。LLM 判断任务匹配某个 skill 时,调用 read 工具读取 SKILL.md 获取完整指令。这样 system prompt 不会膨胀,skills 在需要时又能拿到。
LLM API 调用过程,实际调用 LLM API 的链路:
AgentHarness.createStreamFn() → 闭包包装
↓ 每次调用前
动态获取 API key(处理 OAuth token 过期)
↓ 触发钩子
emitHook("before_provider_request") → 扩展可以修改 stream options(headers、timeout 等)
↓
streamSimple(model, context, options) → pi-ai 层的统一接口
↓ 触发钩子
emitHook("before_provider_payload") → 扩展可以修改请求 payload
↓ 发送 HTTP 请求
收到响应 → emit("after_provider_response")
↓ 返回 EventStream<AgentEvent>
AgentLoop 消费 stream 事件,更新 state
几个要点:
- 每次 LLM 调用前重新获取 API key,支撑短生命周期的 OAuth token
convertToLlm 负责将内部 AgentMessage 转换为 LLM 兼容的 Message 格式(过滤 custom message、notification 等内部消息类型)transformContext 在 convertToLlm 之前运行,可用于上下文裁剪(pruning)- 通过
before_provider_request 和 before_provider_payload 钩子,Extension 可以在请求发送前修改参数
pi-coding-agent
主要问题:
- 如何加载 harness 相关的内容?
- 如何管理记忆(状态)?
- 如何管理 skills 和按需加载?
- 如何调用工具?
- 如何构建 system prompt,并调用 LLM API?
session 设计
https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/session.md
pi 的 Session 设计:
Session = 一个有树状结构的持久化对话记录。 不是线性聊天记录,而是一棵树,每个节点有 id 和 parentId,天然支持 fork、分支、回退。
存储格式,JSONL 文件(session-manager.ts)。每个 Entry 是一行 JSON,类型包括:
| Entry 类型 | 说明 |
|---|
message | 用户/助手/工具结果消息 |
thinking_level_change | 用户切换思考级别 |
model_change | 用户切换模型 |
compaction | 上下文压缩记录 |
branch_summary | 分支切换时的摘要 |
custom | Extension 的自定义持久化数据 |
custom_message | Extension 注入上下文的自定义消息 |
label | 节点标签(用户命名分支) |
session_info | Session 名称 |
树状结构带来的能力:
- Fork,用户回到对话的任意节点,分叉一个新的对话方向。实现方式是复制 session 文件,
parentId 指向 fork 点的 entry - Branch navigation,用户跳转到树中任意节点。目标节点不在当前分支时,系统生成
branch_summary(调 LLM 总结两个分支间的差异),然后 moveTo 到目标节点 - Compaction,上下文过大时,系统将旧对话发给 LLM 做摘要,生成
compaction entry,保留摘要 + 最近 N 条消息,释放 context window 空间
SessionManager(core/session-manager.ts)封装了对 Session 文件的 CRUD 操作:
create(cwd) — 创建新 sessionopen(path) — 打开已有 sessionforkFrom(sourcePath, cwd) — fork 一个 sessionlist(cwd) — 列出项目中的所有 sessioncontinueRecent(cwd) — 恢复最近的 sessionbuildSessionContext() — 重建当前分支的上下文(从 leaf 走到 root)
buildSessionContext() 的逻辑:从当前 leaf 节点出发,沿着 parentId 链走到 root。如果路径上有 compaction entry,只保留 compaction 之后的消息。如果路径上有 branch_summary entry,将其作为上下文注入。最终返回一个 AgentMessage[],可以直接喂给 Agent。
如何加载 harness 相关的内容?
how pi-agent-core load harness related content? "pi-agent-core is a dumb executor. All harness-specific content (project context, AGENTS.md, skills, tool definitions) is resolved by the coding-agent's ResourceLoader and AgentSession, then injected into the agent as plain strings and tool objects. "
coding-agent 相关的代码,如何加载 harness 相关内容:
- Resource Loading (src/core/resource-loader.ts):
- System Prompt Assembly (src/core/system-prompt.ts):
- AgentSession (src/core/agent-session.ts):
ResourceLoader(core/resource-loader.ts)是 harness 内容加载的中心,它的 reload() 方法按以下顺序加载所有内容:
1. 设置加载,SettingsManager.reload() 加载全局设置(~/.pi/agent/settings.json)和项目设置(.pi/settings.json),合并优先级。
2. 资源路径解析,PackageManager.resolve() 从多个来源收集资源路径:
- 自动目录扫描:
~/.pi/agent/skills/、.pi/skills/、.agents/skills/(从 cwd 向上到文件系统根目录) - settings.json 中的配置
- Package 声明(npm 包中的
pi.skills 字段) - CLI 参数:
--skill、--extension - 每个路径标记了来源(source)和作用域(scope:user/project/temporary)
3. Extensions 加载,loadExtensions(extensionPaths) 加载并初始化所有扩展。Extension 编译为 JS 模块运行在受限 runtime 中,可以注册 tools、commands、flags。
4. Skills 发现,loadSkills() 扫描 skill 目录,解析 SKILL.md 文件(提取 frontmatter 中的 name、description、disable-model-invocation),只加载元数据,不加载完整内容。
5. Prompt Templates 加载,loadPromptTemplates() 类似逻辑,解析 prompt template 文件。
6. Themes 加载,loadThemeFromPath() 加载主题 JSON 文件。
7. Context Files 加载,loadProjectContextFiles() 从三个位置读取:
~/.pi/agent/ 下的 AGENTS.md 或 CLAUDE.md(全局级别)- cwd 向上到根目录的每个目录中的 AGENTS.md 或 CLAUDE.md(项目级别)
- 离 cwd 越近的文件越排在后面(优先级越高,后加载覆盖前加载)
8. System Prompt 文件发现,.pi/SYSTEM.md(项目级别)或 ~/.pi/agent/SYSTEM.md(全局级别)
9. Append System Prompt 文件发现,.pi/APPEND_SYSTEM.md(项目级别)或 ~/.pi/agent/APPEND_SYSTEM.md(全局级别)
配置有什么?如何加载?以下是 Github Copilot GPT5.4 给出的一个时序图(mermaid 格式):
sequenceDiagram
autonumber
actor User
participant CLI as main.ts
participant Args as parseArgs()
participant Session as createSessionManager()
participant Runtime as createRuntime()
participant Services as createAgentSessionServices()
participant Loader as DefaultResourceLoader
participant Settings as SettingsManager
participant Models as ModelRegistry
participant Agent as createAgentSessionFromServices()
participant SessionObj as AgentSession
participant Prompt as buildSystemPrompt()
participant Mode as Interactive/Print/RPC
participant LLM as Model Provider
User->>CLI: Start coding-agent CLI
CLI->>Args: parse args
Args-->>CLI: parsed flags, messages, resource options
alt early command
CLI->>CLI: handlePackageCommand() / handleConfigCommand()
CLI-->>User: exit early
else normal startup
CLI->>CLI: resolve app mode
CLI->>CLI: run migrations
CLI->>Settings: create startup settings manager
CLI->>Session: createSessionManager(parsed, cwd, sessionDir)
Session-->>CLI: SessionManager for effective session cwd
CLI->>Runtime: createAgentSessionRuntime(createRuntime, { cwd, agentDir, sessionManager })
Runtime->>Services: createAgentSessionServices({ cwd, agentDir, authStorage, resourceLoaderOptions })
Services->>Settings: create(cwd, agentDir)
Services->>Models: create(authStorage, models.json)
Services->>Loader: new DefaultResourceLoader({ cwd, agentDir, settingsManager, ...resourceLoaderOptions })
Services->>Loader: reload()
Loader->>Loader: resolve extensions, skills, prompts, themes
Loader->>Loader: load AGENTS.md / CLAUDE.md from cwd ancestors
Loader->>Loader: discover .pi/SYSTEM.md or agent SYSTEM.md
Loader->>Loader: discover .pi/APPEND_SYSTEM.md or agent APPEND_SYSTEM.md
Loader-->>Services: loaded resources + context files + prompt sources
Services->>Models: register providers from extensions
Services-->>Runtime: services + diagnostics
Runtime->>Runtime: resolve scoped models
Runtime->>Runtime: build session options from CLI + settings + scope
Runtime->>Agent: createAgentSessionFromServices({ services, sessionManager, model, thinkingLevel, tools })
Agent->>SessionObj: create AgentSession
SessionObj->>Loader: getSystemPrompt()
SessionObj->>Loader: getAppendSystemPrompt()
SessionObj->>Loader: getSkills()
SessionObj->>Loader: getAgentsFiles()
SessionObj->>Prompt: buildSystemPrompt({ customPrompt, appendSystemPrompt, skills, contextFiles, tools })
Prompt-->>SessionObj: final system prompt
Agent-->>Runtime: session + modelFallbackMessage
Runtime-->>CLI: runtime
CLI->>CLI: read piped stdin if not RPC
CLI->>CLI: prepareInitialMessage()
CLI->>CLI: initTheme()
CLI->>CLI: report diagnostics
alt interactive mode
CLI->>Mode: new InteractiveMode(runtime, initial input)
Mode->>SessionObj: run()
else print/json mode
CLI->>Mode: runPrintMode(runtime, initial input)
Mode->>SessionObj: prompt(initial message)
else rpc mode
CLI->>Mode: runRpcMode(runtime)
end
SessionObj->>Loader: prompt templates available for /template expansion
SessionObj->>SessionObj: expand /skill and /template commands
SessionObj->>LLM: send messages with current system prompt
LLM-->>SessionObj: response/events
SessionObj-->>User: output in selected mode
end
如何管理记忆(memory)?
什么是记忆(memory)?为什么需要?
- 记忆(memory):持久态,可以重新加载的上下文,跨 session
什么是 session?为什么需要?
A session in pi is the durable record of one agent conversation, including its tree structure, not just the current in-memory turn state.
Concretely, sessions are stored as JSONL files and each entry has an id and parentId, so pi can preserve branches, rewinds, forks, compactions, model changes, and message history over time. That is described in pi-mono/packages/coding-agent/README.md and implemented in pi-mono/packages/coding-agent/src/core/session-manager.ts.
pi 的 Memory 管理几个关键设计:
1. 树状结构而非线性历史
普通聊天应用是线性的(一条消息接一条消息),pi 用树状结构管理对话,每条 entry 有 id 和 parentId。这带来的能力:
- Fork,回到某个节点,从那里开始新的对话方向。底层是复制 JSONL 文件,设置新的 parentId
- Branch navigation,用户在 TUI 中浏览对话树,跳转到任意节点。切换分支时自动生成
branch_summary,让 LLM 知道上下文发生了什么变化 - Rewind,回到之前的某个点,丢弃之后的内容(本质上是切换到那个节点的分支)
2. Compaction(上下文压缩)
对话历史太长、超出模型的 context window 时,pi 自动触发 compaction:
prepareCompaction() 分析当前分支的 entries,算出哪些消息可以摘要、哪些必须保留(保留最近 N tokens,剩余发给 LLM 做摘要)compact() 调用 LLM 生成摘要- 生成
compaction entry(包含 summary + firstKeptEntryId + tokensBefore),追加到 session buildSessionContext() 重建时,遇到 compaction entry 从摘要开始,只加载 firstKeptEntryId 之后的消息- Compaction 也支持 hooks:
session_before_compact / session_compact,Extension 可以干预摘要过程
compaction entry 像是对话树中的「压缩节点」,重建时自动解压,摘要本身可以被后续 compaction 再次摘要(递归压缩)。
3. Custom entries 支持 Extension 持久化
Extension 可以向 session 写入两种自定义数据:
custom entry,持久化 Extension 的私有状态(如文件编辑历史),不发送给 LLMcustom_message entry,持久化并注入 LLM 上下文(如外部系统的事件通知)
Agent 运行期间,pendingSessionWrites 缓存写入,turn 结束时批量刷入,不用每次 event 都触发磁盘 I/O。
借助 Github Copilot GPT5.4 给出的时序图(mermaid 格式)如下,概括来说:
sequenceDiagram
autonumber
actor User
participant UI as Interactive/Print/RPC Mode
participant Session as AgentSession
participant Manager as SessionManager
participant Store as Session JSONL File
participant Builder as buildSessionContext()
participant Compress as Compaction / Branch Summarization
participant Msg as convertToLlm()
participant LLM as Model Provider
participant Ext as Extensions
User->>UI: Send prompt
UI->>Session: prompt(text, options)
Session->>Session: expand /skill and /template
Session->>Session: flush pending bash messages
Session->>Session: inject pending next-turn messages
opt extension pre-processing
Session->>Ext: emitBeforeAgentStart(text, images, baseSystemPrompt)
Ext-->>Session: extra custom messages and/or prompt override
end
Session->>LLM: agent.prompt(messages, systemPrompt)
LLM-->>Session: assistant response / tool calls / usage
Session->>Manager: appendMessage(user)
Session->>Manager: appendMessage(assistant/tool/custom)
Manager->>Store: append JSONL entries
Note over Manager,Builder: Persistent memory is append-only session history
opt session reload / startup / branch switch
Session->>Manager: buildSessionContext()
Manager->>Builder: rebuild current branch path
Builder->>Builder: walk leaf to root
Builder->>Builder: resolve model + thinking level
alt compaction entry exists on path
Builder->>Builder: emit compaction summary first
Builder->>Builder: keep messages from firstKeptEntryId onward
Builder->>Builder: append post-compaction messages
else no compaction
Builder->>Builder: emit full branch messages
end
Builder-->>Session: reconstructed AgentMessage[]
Session->>Session: agent.state.messages = reconstructed messages
end
opt extension state persistence
Ext->>Manager: appendCustomEntry(customType, data)
Note over Manager,Store: Stored for extension restore only, not sent to LLM
end
opt extension context injection
Ext->>Manager: appendCustomMessageEntry(customType, content, display)
Note over Manager,Builder: Rebuilt as custom messages and included in LLM context
end
opt branch navigation
Session->>Compress: generateBranchSummary(old branch)
Compress-->>Manager: branch_summary entry
Manager->>Store: append branch_summary
Note over Builder: branch_summary becomes a synthetic user-context message
end
opt context grows too large
Session->>Session: check compaction threshold / overflow
Session->>Compress: compact(branch entries)
Compress->>Msg: convertToLlm(messages selected for summary)
Msg-->>Compress: LLM-compatible summary input
Compress->>LLM: summarize older context
LLM-->>Compress: summary text
Compress-->>Manager: appendCompaction(summary, firstKeptEntryId, tokensBefore)
Manager->>Store: append compaction entry
Session->>Manager: buildSessionContext()
Manager->>Builder: rebuild compacted context
Builder-->>Session: summary + kept messages + newer messages
end
Note over Msg,LLM: Messages sent to the model are transformed view-memory, not raw session entries
Session->>Msg: convertToLlm(agent.state.messages)
Msg->>LLM: final model input for next turn
LLM-->>User: next response
如何管理 skills 和按需加载?
这个问题是第一个如何加载 harness 相关内容相关,是不是可以放入一起?
How skills are managed?
At startup, it discovers skills from multiple sources:
- Global dirs: ~/.pi/agent/skills/, ~/.agents/skills/
- Project dirs: .pi/skills/, .agents/skills/ (cwd up to repo/filesystem root)
- Packages: skills/ folders or pi.skills in package package.json
- settings.json: skills array paths
- CLI: --skill (repeatable)
Control flags/settings:
- --no-skills disables auto discovery
- --skill ... still adds explicitly even with --no-skills
- enableSkillCommands controls /skill:name commands
- /reload refreshes discovered skills during a session
How skills are loaded "as required"
- On startup, pi reads only each skill's metadata (name, description).
- It puts the available skill list into the system prompt (XML per Agent Skills spec).
- During a task, if a skill matches, the model should call read on that skill's SKILL.md.
- Full skill instructions are then used only for that task.
So: descriptions are always in context; full skill content is loaded on demand.
Forcing load when needed
- Use /skill: to explicitly load and run it.
- You can pass args: /skill:pdf-tools extract
(args are appended as User: to the skill input). - If a skill has disable-model-invocation: true, it is hidden from auto-selection and must be invoked via /skill:name.
Skills 系统设计的几个原则:
1. 元数据在 prompt 里,完整内容按需加载
每个 skill 是一个目录,包含 SKILL.md(或其他 .md 文件)。SKILL.md 的 YAML frontmatter 定义 name、description、disable-model-invocation。loadSkills()(harness/skills.ts)只解析 frontmatter 和 body,构造 { name, description, content, filePath } 对象。只有 name 和 description 进入 system prompt(格式化为 <available_skills> XML 块,符合 agentskills.io 规范)。
LLM 看到 skill 的描述,判断是否匹配当前任务,匹配就调用 read 工具读取 SKILL.md 的完整路径。system prompt 不会因为大量 skills 而膨胀。
2. 多来源加载与优先级
Skills 来源发现由 PackageManager.resolve() 统一处理。自动发现的目录(.pi/skills/、.agents/skills/)、settings.json 配置、npm package 声明、CLI 参数——这些路径被合并、去重,按优先级排序。每个 skill 标记了 sourceInfo,记录来源(source、scope、origin),用于调试和诊断。
3. Ignore 文件支持
Skill 目录支持 .gitignore、.ignore、.fdignore 规则,遍历时自动跳过被 ignore 的文件和目录。实验性的 skills 可以放在被 gitignore 的目录中,仍然会被加载。
4. 显式调用 Skills
用户可以通过 /skill:name args 显式调用 skill,系统把 skill 内容包装在 <skill> XML 标签中,作为 user message 发送。如果 skill 设置了 disable-model-invocation: true,它不会出现在 <available_skills> 中,只能通过 /skill:name 显式调用——适合危险操作或实验性功能。
5. Agent Skills 规范
pi 的 skills 格式遵循 agentskills.io 规范。Skill 文件的 frontmatter 验证规则:
- name:必须匹配父目录名,小写字母+数字+连字符,不超过 64 字符
- description:必填,不超过 1024 字符
- disable-model-invocation:可选布尔值
sequenceDiagram
autonumber
actor U as User
participant CLI as CLI Parser (args.ts)
participant Main as main.ts
participant SM as SettingsManager
participant PM as DefaultPackageManager
participant RL as DefaultResourceLoader
participant SK as skills.ts (loadSkills)
participant FS as Filesystem
participant AS as AgentSession
participant SP as system-prompt.ts
participant LLM as Model
participant RT as read tool
U->>CLI: pi [--skill ...] [--no-skills]
CLI-->>Main: parsed.skills, parsed.noSkills
Main->>RL: create with additionalSkillPaths + noSkills
RL->>SM: reload settings (global + project)
RL->>PM: resolve resources (auto dirs + settings + packages + CLI)
PM->>FS: scan ~/.pi/agent/skills, ~/.agents/skills
PM->>FS: scan .pi/skills + ancestor .agents/skills
PM-->>RL: resolved skill paths (with precedence/enabled flags)
RL->>SK: loadSkills({skillPaths, includeDefaults:false})
SK->>FS: read SKILL.md / root .md per discovery rules
SK-->>RL: skills[] + diagnostics
RL-->>AS: resourceLoader.getSkills()
AS->>SP: buildSystemPrompt({skills,...})
SP-->>AS: prompt + <available_skills> metadata (if read tool enabled)
AS->>LLM: send system prompt + user message
alt Auto skill load (on-demand)
LLM->>RT: read(<skill location from available_skills>)
RT->>FS: read SKILL.md
FS-->>RT: full skill content
RT-->>LLM: skill instructions
LLM->>AS: continue task using skill guidance
else Explicit command (/skill:name args)
U->>AS: /skill:name args
AS->>FS: read SKILL.md (expand command)
FS-->>AS: file content
AS->>AS: strip frontmatter, wrap <skill ...>, append args
AS->>LLM: send expanded skill block as user message
end
Bub
(待补充:分析 Bub 的 Agent 实现,特别是 tape 系统和 Telegram 集成)
Claude Code
(待补充:分析 Claude Code 的 harness 设计,参考 ccunpacked.dev 和 ZaynHao 的文章)
对于目前技术方向,自己的理解和一点思考
分析完 pi-mono 的实现,对「如何开发一个 Agent」有了更具体的认识:
1. Agent 的核心是 Loop + Context,不是模型
注意力容易放在「用什么模型」上,但 Agent 的工程挑战在模型之外。模型只是 loop 中的一个环节——输入消息,输出响应。真正的复杂度在:上下文怎么不爆炸(compaction)、对话中怎么注入正确的信息(system prompt + AGENTS.md + skills 按需加载)、工具调用失败怎么降级、对话状态怎么持久化和恢复。
pi-mono 花了大量代码在 session 管理、compaction、resource loading 上——这些才是 Agent 的骨架。模型升级了换配置就行,基础设施设计不好,后面改起来很痛。
2. 分层设计让 Agent 可扩展
pi-mono 的 core / harness / cli 三层分离是精心设计的。Agent core 不包含任何「产品」逻辑,只提供运行 Agent 的运行时。产品相关的决策(加载什么资源、提供什么工具、构建什么 system prompt)都在 harness 层。基于同一个 core 可以做 coding agent、客服 agent、数据分析 agent——换 harness 层就行。
3. Session 的树状结构是 Agent 的「时间旅行」
普通聊天应用的线性历史在 Agent 的多轮协作场景下不够用。用户可能想回到之前的某个决策点,换个方向。pi 的树状 session + fork + branch summary 解决了这个问题。特别是 branch summary,不是机械拼接历史消息,而是让 LLM 总结两个分支的差异,切换分支后模型能快速理解上下文变化。
4. Skills 的按需加载是上下文管理的典型做法
把 skills 的元数据放在 system prompt 里,完整内容通过 read 工具按需加载——它把「告诉模型有什么能力」和「告诉模型怎么用这个能力」分开了。元数据始终在上下文(成本低),完整指令只在相关任务时加载(按需付费)。
5. Extension 系统让 Agent 从「工具」变成「平台」
pi 的 extension 系统支持注册自定义工具、命令、flag,通过 hooks(before_agent_start、before_provider_request、tool_call、tool_result 等)介入 Agent 各环节。这让 pi 从「一个 coding agent 工具」变成了「一个可以构建各种 Agent 应用的平台」。
什么是未来
阅读 pi-mono 的代码,同时参考 Mitchel Hashimoto 的 My AI Adoption Journey 和 Anthropic 的 Harness design for long-running application development,对 Agent 的未来方向有几条判断:
1. Harness Engineering 会成为独立领域
模型能力在快速提升,但 Agent 的 harness(系统提示词、上下文管理、工具设计、session 管理、扩展系统)才决定实际使用体验。Harness Engineering 会像 DevOps、SRE 一样,逐渐成为一个被认可的工程方向。
2. Agent 的「记忆」会越来越重要
目前的 Agent 记忆主要靠「把历史消息塞进 context window」。随着 Agent 处理越来越长期的任务(跨天、跨周的项目开发),需要更智能的记忆机制——不只是 compaction,而是结构化的知识提取、任务状态追踪、决策理由记录。pi 的树状 session + custom entries 是这个方向的早期探索。
3. Multi-Agent 协作
pi-mono 目前是单 Agent 架构,但它还有一个 pi-pods 包没有深入分析,可能跟多 Agent 相关。未来多个 Agent 各司其职(一个写代码、一个 review、一个测试),通过结构化通信协议协作——这个方向值得关注。
4. Agent 的「操作系统化」
Claude Code 被描述为「运行在终端里的 AI 操作系统」。Agent 不再只是「对话机器人」,而是一个协调工具、管理状态、持久化记忆、执行后台任务的操作系统级抽象。shell 是它的内核,tools 是它的驱动,skills 是它的应用。
自己开发一个 Agent,需要如何做
基于对 pi-mono 的分析,自己开发一个 Coding Agent,可以按这个顺序来:
第一步:跑通最小 loop,对接一个 LLM API(Anthropic / OpenAI 或兼容接口),实现基本的 prompt → response → display 流程。这是「Hello World」,验证你能和模型对话。
第二步:加入工具调用,先加一个最简单的工具(read 读文件),实现 tool call 的解析、执行、结果回传,验证 LLM 能正确使用工具。然后逐步加更多工具(bash、edit、write)。
第三步:上下文管理,实现对话历史的管理(线性数组开始就行),上下文压缩(messages 太多时,取最近 N 条 + 对旧消息做摘要),加载项目上下文(读取 AGENTS.md 或类似的项目说明文件)。
第四步:System Prompt 设计,角色设定、工具使用指南、项目上下文,持续迭代调优。这是最需要「手感」的部分。
第五步:Session 持久化,保存对话历史到文件(JSONL 是个好选择),支持恢复历史会话,fork 可选但很有用。
第六步:Skills 系统,支持加载外部 skill 文件,元数据进 system prompt,完整内容按需加载,支持 /skill:name 显式调用。
第七步:TUI / 交互体验,命令行交互界面、流式显示 AI 输出、工具调用的可视化反馈。
核心思路:先让 Agent 能跑起来,哪怕只有最基础的功能,然后基于实际使用逐步迭代。 不要一开始就设计复杂架构——pi-mono 的架构也是迭代出来的(commit 历史可以作证)。
参考
关于共通的一些话题
关于 pi-mono
关于 Bub
关于 Harness
分析 Claude Code 的代码
关于 ironcode - codedump
关于 Agent 学习的一些参考