01. 产品问题与设计哲学
读源码前先问一个产品问题:如果目标只是“调用模型并显示流式文本”,为什么需要 34 万行 TypeScript?答案是 Craft Agents 的产品单位不是一次 completion,而是一个可长期存在、可并行、可分支、可外接能力、可远程运行的工作会话。
1.1 它解决的不是聊天,而是 Agent 工作管理
Section titled “1.1 它解决的不是聊天,而是 Agent 工作管理”根 README 把目标描述为多任务、无摩擦连接外部服务、可分享会话和文档中心工作流。落实到代码,产品有四层递进:
- 一次 turn:模型生成文本、调用工具、处理中断。
- 一个 session:保存历史、权限、source、模型、状态、标签、项目归属。
- 一组并行 session:Inbox/Kanban、父子任务、DAG、自动化、外部消息绑定。
- 一个可迁移工作环境:workspace 文件夹、远程 server、CLI/WebUI、bundle/transfer/share。
这解释了为什么 SessionManager 比 provider adapter 大得多:LLM 调用只是生命周期中间的一段。
源码证据:README.md、README.md 功能表。
1.2 产品对象的层级
Section titled “1.2 产品对象的层级”flowchart TD G["Global config\n连接、偏好、最近工作区"] W["Workspace\n可搬运的能力与会话容器"] P["Project\n上下文、资产、MEMORY.md"] S["Session\n长期工作单元"] T["Turn\n一次模型执行"] A["AgentEvent\n流式行为"]
G --> W W --> P W --> S P -. "可选绑定" .-> S S --> T T --> A这里有一个有意的非树关系:Session 属于 Workspace,但只“可选绑定” Project。Project 是上下文与资产分组,不是会话存储父目录。这样项目可以改名或解绑,而 session 的物理地址仍稳定。
1.3 Agent-native 在代码里意味着什么
Section titled “1.3 Agent-native 在代码里意味着什么”“Agent-native”容易变成口号。这个仓库里至少有四个可观察实现:
1.3.1 配置本身是 Agent 可操作的产品表面
Section titled “1.3.1 配置本身是 Agent 可操作的产品表面”Sources、Skills、Statuses、Permissions、Automations 等都以可读文件存在;系统 prompt 告诉 Agent 如何编辑,再提供 config_validate、source_test、skill_validate 等自检工具。
结果是“设置 UI”不是唯一控制面。用户说“接入 Linear”时,Agent 可以写 source 配置、引导凭据、验证连接,并在当前会话热激活。
1.3.2 能力发现晚于产品发布
Section titled “1.3.2 能力发现晚于产品发布”MCP/API source 不是编译时写死的工具列表。McpClientPool.sync() 连接当前选中的 source,拉取 tools,再注册为 mcp__{slug}__{tool} proxy。能力集合因此是 session/runtime 级动态状态。
1.3.3 Agent 可以管理会话系统本身
Section titled “1.3.3 Agent 可以管理会话系统本身”session-scoped tools 包含:
- 改自己的 labels/status;
- 查询 session/background task;
- 创建 task;
- 给另一个 agent session 发消息;
- 提交 plan;
- 触发 OAuth/credential prompt;
- 运行受限数据转换或脚本。
这让 Agent 不只操作用户文件,也能操作产品中的工作单元。
1.3.4 人机交接是协议,不是提示词约定
Section titled “1.3.4 人机交接是协议,不是提示词约定”SubmitPlan 和 auth request 不只是返回一段文本。它们触发 interruptForHandoff(...),结束当前处理态,释放浏览器资源,持久化 handoff 状态,等用户动作后再继续。也就是说“暂停给人审批”被建模成生命周期事件。
1.4 为什么同时支持 Claude 与 Pi
Section titled “1.4 为什么同时支持 Claude 与 Pi”README 明确说两套 SDK 并行使用。代码并没有把它们压成最小公分母,而是做了两层抽象:
- 上层统一:
AgentBackend.chat()、AgentEvent、abort、runtime update、source/tool callback。 - 下层保留:Claude 的 resume/fork、Pi 的 turn anchor/steer/subprocess session manager。
这是一个成熟取舍。如果只暴露通用文本与 tool call,分支、上下文缓存、原生 steer 等重要能力会丢失;如果完全透传 provider API,上层 SessionManager 和 UI 又会分裂。Craft 选择“统一生命周期,允许能力探针和可选方法”。
1.5 三个产品承诺与对应工程成本
Section titled “1.5 三个产品承诺与对应工程成本”承诺一:多会话同时工作
Section titled “承诺一:多会话同时工作”成本不是开多个数组元素,而是每个 session 都可能持有:
- provider runtime 或子进程;
- MCP client pool 和 source 状态;
- processing generation、队列与 abort 状态;
- pending permission/auth/plan;
- background tasks 与浏览器 owner;
- persistence queue 与 UI unread 状态。
因此 ManagedSession 接近一个小型 Actor mailbox + resource scope。
承诺二:本地与远程“同一个产品”
Section titled “承诺二:本地与远程“同一个产品””成本是所有服务端操作都要协议化;同时本机独有的 dialog、window、browser pane 又不能在 headless server 执行。项目引入 channel routing 与 client capabilities,将“谁有能力执行”从静态进程假设变成握手状态。
承诺三:source 改完立即生效
Section titled “承诺三:source 改完立即生效”成本是配置 watcher、credential refresh、MCP pool reconcile、backend tool re-registration 和 current-turn retry 必须协作。简单粗暴重启 agent 会破坏当前上下文;完全原地更新又不是所有字段都安全,所以代码区分 restart signature 与 in-place signature。
1.6 设计原则:文件系统优先
Section titled “1.6 设计原则:文件系统优先”Craft Agents 没有把 workspace 主状态放进数据库。核心目录是可查看、可复制、可版本管理的普通文件:
workspace/├── config.json├── sessions/<id>/session.jsonl├── projects/<slug>/config.json + MEMORY.md + assets/├── sources/<slug>/config.json + guide.md├── skills/<slug>/SKILL.md├── tasks/<slug>/task.yaml + runs/├── statuses/└── labels/收益:
- Agent 可以用普通文件工具修改配置;
- workspace 可移动、备份、diff;
- session bundle/transfer 更直接;
- 用户拥有数据,不被数据库 schema 锁死。
代价:
- 必须处理原子写、损坏、外部并发编辑、路径可移植;
- 列表性能不能每次解析完整 transcript;
- 迁移不再由数据库 transaction 兜底;
- 凭据不能跟普通配置一起明文存储。
JSONL header、persistence queue、portable path 和独立 encrypted credential store,正是这些代价的答案。
1.7 设计原则:用户可控,自动化不越权
Section titled “1.7 设计原则:用户可控,自动化不越权”权限模式固定为:
| 持久值 | UI 语义 | 行为 |
|---|---|---|
safe |
Explore | 允许读取,阻止写入/危险行为 |
ask |
Ask to Edit | 需要用户批准 |
allow-all |
Auto | 自动批准,但仍经过基础安全/结构检查 |
需要注意:权限模式不是 provider 自带字段,而是产品自己的安全策略。Claude 与 Pi 都必须进入同一个 runPreToolUseChecks() 管道,否则同一会话切 provider 会改变安全含义。
自动化和 Task 子会话没有人守着回答 ask,因此 TaskRunner 对缺省模式做了显式无人值守选择,而不是不加思考继承 workspace 默认值。这个选择有风险,但至少风险被写成代码常量与注释,而非隐含行为。
1.8 设计原则:事件投影而非共享内存
Section titled “1.8 设计原则:事件投影而非共享内存”服务端和 renderer 不共享 session 对象。服务端发出 SessionEvent,前端用纯函数归约到 Jotai state:
authoritative mutation → persist if required → push typed event → renderer processEvent(oldState, event) → newState + declarative effects这带来两个好处:
- 同一协议可供 Electron、WebUI、CLI、消息网关消费。
- 前端事件处理可用纯单元测试覆盖,避免到处 setState。
代价是必须认真设计事件幂等、关联 ID、乱序与 reconnect replay。
1.9 设计原则:provider-native 锚点优先于“看起来像分支”
Section titled “1.9 设计原则:provider-native 锚点优先于“看起来像分支””UI 可以轻易复制父 session 的前 N 条消息并显示成新会话,但模型真正的上下文可能仍包含截止点后的内容。Craft 的分支流程要求:
- 同一 backend/provider;
- 父 provider session ID 可用;
- 分支点有 provider-native turn anchor;
- Claude 传
resumeSessionAt,Pi 通过 sidecar anchor 与 session manager branch; - preflight 失败则回滚新 session,而不是假装成功。
这体现一个重要原则:产品语义必须落到 provider 的真实语义,不能只在 UI 造型。
1.10 产品架构中的张力
Section titled “1.10 产品架构中的张力”灵活配置 vs 可预测运行时
Section titled “灵活配置 vs 可预测运行时”文件可随时编辑,但运行中的 agent 不应因任何小变更都重启。解决方式是配置签名分级和串行 refresh lock。
多 provider vs 行为一致
Section titled “多 provider vs 行为一致”统一抽象要保证安全、事件、存储一致;provider-native 能力又不能消失。解决方式是 optional capability 与后端适配器,而不是最低共同接口。
实时 UI vs 耐久性
Section titled “实时 UI vs 耐久性”立即显示很爽,但断电后消息丢失不可接受。解决方式是 optimistic ID + 服务端 flush + accepted/queued/processing 状态事件。
无人值守 vs 审批安全
Section titled “无人值守 vs 审批安全”自动任务不能永远卡在 ask,放开权限又可能过强。代码选择显式 autonomous default,并用 token budget、max parallel、retry bounds、sandbox 等补充边界。
1.11 本章小结
Section titled “1.11 本章小结”Craft Agents 的架构不是偶然堆叠。只要接受“长期、多会话、可远程、可扩展、用户拥有文件、provider 能切换”这些产品约束,Session Actor、RPC、JSONL、动态工具池和统一安全管道几乎都会自然出现。
下一章回到代码目录,解释这些职责为什么被切成 14 个包,以及哪些依赖方向是刻意的架构边界。