00. 阅读指南与全景地图
本章先给一张不会迷路的地图。Craft Agents 的目录很多,但真正决定行为的是少数几条链;只要先认清“命令从哪里进入、状态由谁拥有、事件从哪里出去”,后续模块就能挂到同一棵树上。
0.1 我们在拆什么
Section titled “0.1 我们在拆什么”分析对象不是单一 Agent SDK,而是一个完整产品栈:
- Electron 桌面壳与 React/Jotai UI;
- 可独立运行的 headless server;
- WebSocket RPC 客户端/服务端;
- Claude Agent SDK 后端;
- Pi SDK 子进程后端;
- 会话、工作区、项目、数据源、技能和任务的文件系统存储;
- MCP/API 数据源、OAuth 与本地加密凭据;
- 浏览器宿主、自动化、消息网关与公开 Viewer。
这意味着它至少有四种“状态”:
| 状态 | 例子 | 主要所有者 |
|---|---|---|
| 持久领域状态 | session 名称、消息、source、project、task run | 文件系统存储层 |
| 会话运行时状态 | agent 实例、正在处理、队列、权限请求、MCP pool | SessionManager.ManagedSession |
| 传输状态 | client id、capability、seq、event replay buffer | WsRpcServer / WsRpcClient |
| 展示状态 | streaming message、面板、选中会话、toast | Electron renderer atoms/event processor |
阅读时最常见的误区,是把其中某一层当作全部事实源。比如 renderer 里有 isProcessing,但真正决定能否插入新消息、是否 queue/steer 的是服务端 SessionManager;UI 只是投影。
0.2 一张总图
Section titled “0.2 一张总图”flowchart LR subgraph Clients["客户端表面"] E["Electron Renderer"] C["CLI"] W["WebUI"] M["Telegram / WhatsApp / Lark"] end
subgraph Host["服务进程"] RPC["WsRpcServer + handlers"] SM["SessionManager\n会话 Actor 容器"] FS["Workspace / Session JSONL\nSources / Skills / Tasks"] AUTO["AutomationSystem / TaskRunner"] GATE["MessagingGateway"] end
subgraph Runtime["Agent runtime"] F["Backend Factory"] CA["ClaudeAgent\nin-process SDK"] PA["PiAgent\nJSONL bridge"] PS["pi-agent-server\nsubprocess"] MCP["McpClientPool"] end
E -->|"RPC / push"| RPC C -->|"WebSocket"| RPC W -->|"同端口 HTTP + WS"| RPC M --> GATE GATE --> SM RPC --> SM SM <--> FS AUTO --> SM SM --> F F --> CA F --> PA PA <--> PS CA --> MCP PA --> MCP SM -. "browser capability invoke" .-> E图里最重要的箭头不是 UI 到 RPC,而是 SessionManager → Backend Factory。它说明会话是上层稳定抽象,Claude/Pi 只是可替换运行时;另一方面 SessionManager -.-> Electron 的反向 capability 调用说明远程模式并非纯粹请求/响应,而是双向协作。
0.3 五条主链
Section titled “0.3 五条主链”主链一:启动
Section titled “主链一:启动”Electron main 或 packages/server → bootstrapServer() → PlatformServices → SessionManager → WsRpcServer.listen() → registerAllRpcHandlers() → setSessionEventSink() → SessionManager.initialize()源码入口:
packages/server/src/index.tspackages/server-core/src/bootstrap/headless-start.tsapps/electron/src/main/index.ts
主链二:发送消息
Section titled “主链二:发送消息”renderer/CLI/gateway → sessions:send RPC → SessionManager.sendMessage() → 先创建并 flush 用户消息 → getOrCreateAgent() → backend.chat(...): AsyncGenerator<AgentEvent> → SessionManager.processEvent() → session_event push → renderer pure event processor源码入口:
主链三:工具调用
Section titled “主链三:工具调用”provider tool request → provider adapter → centralized runPreToolUseChecks() → prerequisite / permission / path / command checks → built-in / session-scoped / MCP proxy tool → tool_result AgentEvent → persisted ToolMessage + UI cardClaude 由 SDK hook/canUseTool 接入;Pi 则把 pre-tool-use 和 tool execution 变成 JSONL request/response。两条路最终都复用中央检查器和 MCP pool。
主链四:持久化与恢复
Section titled “主链四:持久化与恢复”ManagedSession 变更 → PersistenceQueue.enqueue() 500ms debounce → merge 外部 metadata 变化 → temp file + atomic rename → session.jsonl line 1 = SessionHeader line 2+ = StoredMessage → 启动时只读 header 列表 → 打开/执行时懒加载 messages这里的“外部 metadata 变化”很重要:配置文件可以被 Agent 自己编辑,watcher 也可能同时触发更新,持久化队列不能简单覆盖磁盘最新值。
主链五:远程浏览器
Section titled “主链五:远程浏览器”remote SessionManager → RemoteBrowserPaneManager → WsRpcServer.invokeClient(clientId, client:browser:invoke) → Electron preload capability dispatcher → BrowserPaneManager / WebContentsView → result 回到服务端工具调用这条链是理解“薄客户端”的关键:LLM 和工具在远端跑,但 GUI 浏览器只能在本机 Electron 里存在,于是服务端需要选择具有 capability 的客户端并反向 RPC。
0.4 关键不变量
Section titled “0.4 关键不变量”后续章节会反复用到这些不变量:
- 会话 ID 是产品级身份,SDK session ID 是 provider 级恢复句柄。 两者不能混用。
sdkCwd创建后固定,workingDirectory可变。 前者帮助 provider 找回 transcript,后者决定当前工具工作目录。- LLM connection 在第一次创建 agent 后锁定。 因为它决定 backend/provider/credential 和恢复协议。
- 持久化消息是事实源,流式 delta 是短暂投影。
text_complete才生成权威 assistant message。 - 权限模式只有
safe | ask | allow-all。 UI 展示名可以变化,持久语义不能漂移。 - queue 与 steer 语义不同。 queue 不应把当前 turn 标为 interrupted;只有实际中断/转向才标。
- Source 连接由主进程 pool 集中拥有。 provider 后端拿 proxy tool,不直接各建一套 MCP 生命周期。
- 本地能力不得误路由到远端。 窗口、对话框、shell 集成、浏览器 pane 等由 routing 表明确归类。
- 用户消息确认前必须持久化。 RPC ack 不应先于 durable write。
- 分支是 provider-native 硬截止,不只是复制一段 UI 消息。 无法满足时必须走显式 fallback 或失败。
0.5 如何验证文档中的说法
Section titled “0.5 如何验证文档中的说法”每个章节都提供三类证据:
- 类型证据:接口/联合类型说明系统允许哪些状态。
- 控制流证据:入口函数和关键分支说明状态如何变化。
- 测试证据:测试名说明维护者特别防哪些回归。
例如要验证“排队消息先落盘再确认”,不要只看 DTO;应该同时看:
SendMessageOptions与 user message 状态事件;SessionManager.sendMessage()的 midstream 分支;sendmessage-durability.test.ts与midstream-queue.test.ts。
这种三角验证比看注释可靠。
0.6 阅读顺序为什么不按目录
Section titled “0.6 阅读顺序为什么不按目录”目录结构反映构建边界,运行链反映行为边界。比如一次 Source 工具调用会穿过:
shared/sources→ shared/mcp→ shared/agent→ server-core/sessions→ core/types→ electron/renderer/event-processor如果按目录逐个读,很容易记住文件却不知道它们何时协作。因此正文先讲运行链,再回头解释包边界。
0.7 本章小结
Section titled “0.7 本章小结”把 Craft Agents 压缩成一句结构描述:
多个客户端通过统一 RPC 操作服务端的会话 Actor;会话 Actor 把持久领域状态、provider runtime、工具资源和事件流绑在一起,再把结果投影回不同 UI 与消息渠道。
下一章先解释为什么产品会自然长成这个结构,而不是立刻钻进 9,000 行的 SessionManager。