跳转到内容

00. 阅读指南与全景地图

本章先给一张不会迷路的地图。Craft Agents 的目录很多,但真正决定行为的是少数几条链;只要先认清“命令从哪里进入、状态由谁拥有、事件从哪里出去”,后续模块就能挂到同一棵树上。

分析对象不是单一 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 只是投影。

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 调用说明远程模式并非纯粹请求/响应,而是双向协作。

Electron main 或 packages/server
→ bootstrapServer()
→ PlatformServices
→ SessionManager
→ WsRpcServer.listen()
→ registerAllRpcHandlers()
→ setSessionEventSink()
→ SessionManager.initialize()

源码入口:

renderer/CLI/gateway
→ sessions:send RPC
→ SessionManager.sendMessage()
→ 先创建并 flush 用户消息
→ getOrCreateAgent()
→ backend.chat(...): AsyncGenerator<AgentEvent>
→ SessionManager.processEvent()
→ session_event push
→ renderer pure event processor

源码入口:

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 card

Claude 由 SDK hook/canUseTool 接入;Pi 则把 pre-tool-use 和 tool execution 变成 JSONL request/response。两条路最终都复用中央检查器和 MCP pool。

ManagedSession 变更
→ PersistenceQueue.enqueue() 500ms debounce
→ merge 外部 metadata 变化
→ temp file + atomic rename
→ session.jsonl
line 1 = SessionHeader
line 2+ = StoredMessage
→ 启动时只读 header 列表
→ 打开/执行时懒加载 messages

这里的“外部 metadata 变化”很重要:配置文件可以被 Agent 自己编辑,watcher 也可能同时触发更新,持久化队列不能简单覆盖磁盘最新值。

remote SessionManager
→ RemoteBrowserPaneManager
→ WsRpcServer.invokeClient(clientId, client:browser:invoke)
→ Electron preload capability dispatcher
→ BrowserPaneManager / WebContentsView
→ result 回到服务端工具调用

这条链是理解“薄客户端”的关键:LLM 和工具在远端跑,但 GUI 浏览器只能在本机 Electron 里存在,于是服务端需要选择具有 capability 的客户端并反向 RPC。

后续章节会反复用到这些不变量:

  1. 会话 ID 是产品级身份,SDK session ID 是 provider 级恢复句柄。 两者不能混用。
  2. sdkCwd 创建后固定,workingDirectory 可变。 前者帮助 provider 找回 transcript,后者决定当前工具工作目录。
  3. LLM connection 在第一次创建 agent 后锁定。 因为它决定 backend/provider/credential 和恢复协议。
  4. 持久化消息是事实源,流式 delta 是短暂投影。 text_complete 才生成权威 assistant message。
  5. 权限模式只有 safe | ask | allow-all UI 展示名可以变化,持久语义不能漂移。
  6. queue 与 steer 语义不同。 queue 不应把当前 turn 标为 interrupted;只有实际中断/转向才标。
  7. Source 连接由主进程 pool 集中拥有。 provider 后端拿 proxy tool,不直接各建一套 MCP 生命周期。
  8. 本地能力不得误路由到远端。 窗口、对话框、shell 集成、浏览器 pane 等由 routing 表明确归类。
  9. 用户消息确认前必须持久化。 RPC ack 不应先于 durable write。
  10. 分支是 provider-native 硬截止,不只是复制一段 UI 消息。 无法满足时必须走显式 fallback 或失败。

每个章节都提供三类证据:

  • 类型证据:接口/联合类型说明系统允许哪些状态。
  • 控制流证据:入口函数和关键分支说明状态如何变化。
  • 测试证据:测试名说明维护者特别防哪些回归。

例如要验证“排队消息先落盘再确认”,不要只看 DTO;应该同时看:

  1. SendMessageOptions 与 user message 状态事件;
  2. SessionManager.sendMessage() 的 midstream 分支;
  3. sendmessage-durability.test.tsmidstream-queue.test.ts

这种三角验证比看注释可靠。

目录结构反映构建边界,运行链反映行为边界。比如一次 Source 工具调用会穿过:

shared/sources
→ shared/mcp
→ shared/agent
→ server-core/sessions
→ core/types
→ electron/renderer/event-processor

如果按目录逐个读,很容易记住文件却不知道它们何时协作。因此正文先讲运行链,再回头解释包边界。

把 Craft Agents 压缩成一句结构描述:

多个客户端通过统一 RPC 操作服务端的会话 Actor;会话 Actor 把持久领域状态、provider runtime、工具资源和事件流绑在一起,再把结果投影回不同 UI 与消息渠道。

下一章先解释为什么产品会自然长成这个结构,而不是立刻钻进 9,000 行的 SessionManager