跳转到内容

18. 工程质量、测试与可迁移结论

Craft Agents 0.11.2 已经不是原型规模:约 1,496 个 TypeScript/TSX 文件、34.1 万行、373 个 TypeScript 测试入口(含5个独立进程测试),跨 Electron、headless server、WebUI、Viewer、CLI、两套 Agent backend、三种消息平台和多个子进程。

这一章不再按功能走链,而是回答三个问题:哪些工程选择支撑了这种复杂度,哪些地方已经成为结构债,以及如果在另一个项目复用思想,应该保留什么、舍弃什么。

以下为 0.11.2 快照中 packages/apps/ 的 TS/TSX 统计,不含依赖和生成产物:

包/应用 TS/TSX 文件 约行数 测试文件
apps/electron 617 131,287 90
packages/shared 434 115,553 162
packages/ui 171 30,059 33
packages/server-core 105 27,488 30
packages/messaging-gateway 47 14,028 25
packages/session-tools-core 51 8,575 16
packages/pi-agent-server 26 5,033 10
apps/cli 6 3,703 3
packages/messaging-whatsapp-worker 8 1,788 3
packages/core 10 966 0
apps/webui 11 878 0
apps/viewer 7 650 0
packages/session-mcp-server 1 576 0
packages/server 2 542 1
合计 1,496 341,126 373

这张表揭示两个事实:

  1. shared + electron 占约72%的代码,是主要维护面;
  2. 测试数量不少,但分布很不均匀,WebUI/Viewer/core/session-mcp-server几乎依赖间接覆盖。
文件 约行数 为什么会大
SessionManager.ts 9,005 会话生命周期、运行时、队列、持久化、工作流、browser wiring
AppShell.tsx 3,911 桌面导航、布局、筛选、panel、settings入口
browser-pane-manager.ts 3,613 WebContentsView与整套浏览器自动化
TurnCard.tsx 3,279 多类消息/tool/activity渲染
claude-agent.ts 3,180 SDK适配、permission、event、恢复、工具
config/storage.ts 3,044 跨版本配置/迁移/路径
pi-agent.ts 2,691 子进程桥、协议、event adapter
FreeFormInput.tsx 2,537 composer完整状态机
ChatDisplay.tsx 2,383 turn组织、滚动、搜索与交互
unified-network-interceptor.ts 2,265 多 transport网络策略

热点并不都应立即拆分。browser-pane-manager 大但围绕一个真实资源 owner;SessionManager 则横跨多个可独立变化的子领域,更值得优先解耦。

18.3 测试重点是行为边界,不只是纯函数

Section titled “18.3 测试重点是行为边界,不只是纯函数”

测试集中在最容易分叉的状态机与失败窗口:

  • permission mode与 pre-tool safety矩阵;
  • Claude/Pi event parity与 tool name normalization;
  • session分支、SDK id更新、fork cwd失效回退;
  • 用户消息先落盘、发送失败耐久性;
  • mid-stream queue/steer与 replay timestamp;
  • JSONL anchor/compaction/branch rollback;
  • Source config watcher、credential/OAuth;
  • RemoteBrowser capability与 screenshot wire conversion;
  • Task DAG、引用、budget、repair;
  • Automation condition/security;
  • Messaging binding/access/button幂等;
  • Electron browser permission、notification routing;
  • renderer纯 event handlers和 race regression。

这是成熟 Agent产品应有的测试形状:最危险的 bug通常不是“函数算错”,而是“中断发生在落盘之后、广播之前”“第二次点击落到已结束 runtime”“重连只恢复一半状态”。

18.4 测试缝来自端口,而不是全局 mock

Section titled “18.4 测试缝来自端口,而不是全局 mock”

代码中反复使用小接口/callback:

AgentBackend
ISessionManager
ConductorSessionHost
IBrowserPaneManager
PlatformAdapter
EventSinkFn
CredentialManager
session self-management callbacks

因此测试可用内存 host/adapter/event sink验证调度与状态,不必启动 Electron、真实模型或 Telegram。

几个好例子:

  • TaskRunner 只依赖 create/send/status/completion等 host方法;
  • RemoteBrowserPaneManager 用假的 capability invoker验证 wire;
  • Renderer 用带 capability的 fake adapter测试三种响应模式;
  • processEvent() 是无副作用纯函数;
  • evaluateBindingAccess() 直接穷举安全矩阵。

可迁移原则:测试友好性通常是架构端口清晰的副产品,不是后置 mock技巧。

test 脚本先运行 bun test,再单独查找所有 *.isolated.ts逐个执行。当前隔离测试覆盖:

  • Electron notification routing;
  • branch rollback;
  • session annotations;
  • pre-tool checks;
  • prerequisite manager。

这些测试通常会改 module/global/env/mock状态,放进同一 Bun进程可能互相污染。独立进程是务实隔离,但也提示对应代码对进程级单例或 import-time状态依赖较重。长期可把可变全局收进显式 runtime container,减少必须隔离的测试。

package.json 定义:

test → bun test + isolated tests
validate:dev → typecheck:all + selective shared tests + Python doc-tool smoke
validate:ci → validate:dev + i18n parity/sorted/coverage

.github/workflows/validate.yml 只运行 bun run validate:ci,其中没有根 bun test,也没有根 lint。因此当前 OSS快照中的373个 TypeScript测试入口并未由这个必跑 workflow完整执行。

另一个手动 workflow validate-server.yml 在 Linux/macOS/Windows跑真实 CLI server validation,但只在 workflow_dispatch 触发。

建议最少改为:

required PR jobs
├── typecheck all workspaces
├── unit/integration tests + isolated tests
├── lint + protocol/i18n guards
├── build electron/webui/viewer/server workers
└── platform smoke matrix (可按 nightly/label分层)

当前脚本包含 core/shared/server-core/server/session-tools/pi-agent-server/electron/ui,但未包含:

  • messaging-gateway
  • messaging-whatsapp-worker
  • apps/cli
  • apps/webui
  • apps/viewer
  • session-mcp-server

这些 package大多有自己的 typecheck script,但没有被根验证串起来。命名为 typecheck:all 会给维护者错误信心,建议从 workspace清单自动发现 script,或显式补齐并加测试保证新 workspace不会漏掉。

lint 引用了:

scripts/check-raw-sends.sh
scripts/check-task-tool-checks.sh

但这两个文件不在当前 craft-agents-oss-0.11.2 快照中;.husky 也未随快照提供,而 prepare仍指向 husky。因此直接运行完整 lint会先因脚本缺失失败。

这可能是 OSS同步/发布过滤导致,不一定代表内部仓库缺失,但对开源消费者而言就是可复现性问题。发布过程应在净导出目录执行:

bun install --frozen-lockfile
bun run typecheck:all
bun test
bun run lint
bun run build

任何内部才存在的路径都应在发布前暴露。

大量状态使用 tag:

  • AgentEvent.type
  • message/content block;
  • permission/credential request;
  • automation action/condition;
  • Task node/run state;
  • messaging runtime/access verdict;
  • Pi subprocess command/event。

配合 exhaustive switch,新增事件能在多个 adapter/UI handler编译时报漏分支。

server-core接口、RPC DTO与 UI domain类型基本分开;remote wire还会把 Buffer显式转 Uint8Array。这比把 SDK对象直接穿过 IPC/WS稳健。

旧 binding config、legacy messages、provider tool input、路径与来源配置会先 normalize再进入主链。兼容逻辑集中在入口,内部代码不必每处判断版本。

TypeScript只在同一构建时成立,跨子进程/网络后输入是 unknown。Craft已经在部分 wire上加入 v: 1 和 runtime validation,但仍有可加强处:

  • parent Pi adapter与 subprocess各自维护协议相关类型;
  • Electron channel map/API类型跨多个包同步;
  • shared session JSONL演进主要靠宽松 parser;
  • Task schema接受的节点多于 executor能力;
  • Viewer对 Claude/Pi tool字段做本地 fallback。

建议从单一 schema生成:

runtime validator
TypeScript types
JSON Schema/docs
protocol compatibility tests

并在 handshake中明确 min/max protocol version与capability,而不是只假设 client/server来自同一版本。

Session主链有明确耐久性纪律:

accept user intent
→ assign stable id/timestamp
→ persist user message
→ update in-memory state
→ broadcast/reply
→ run provider

分支/迁移失败有 rollback;JSONL anchor保留 provider恢复语义;queued message在 interruption/restart后有可识别状态;分享上传只读 snapshot而非暴露工作目录。

文件系统事实源还带来:

  • 可检查/可复制/可版本迁移;
  • headless与Electron共享;
  • 不需要内嵌数据库服务;
  • Task/Source/Automation可用普通 YAML/JSON。

不同子系统各自实现 JSON/YAML读写:

  • session JSONL与metadata;
  • config/storage/migration;
  • Sources/credentials;
  • messaging config/bindings/pending/topics;
  • Task spec/run;
  • Automation config/history。

有些使用 temp+rename/队列,有些直接覆盖;有些失败抛出,有些重置为空;有些先写后 event,有些 callback时序不同。

应抽一个小而严格的 storage toolkit:

readVersioned<T>(path, schema, migrations)
writeJsonAtomic(path, value)
appendJsonlDurable(path, record)
withFileLock/serialQueue(key, fn)
quarantineCorrupt(path)

目标不是建 ORM,而是统一 crash窗口、corruption策略和 observability。

代码中多处有“按资源串行”:

  • SessionManager per-session send queue;
  • Source runtime refresh coalescing;
  • TopicRegistry同 name find-or-create序列化;
  • WhatsApp pending command id;
  • plan/permission token一次 claim;
  • TaskRunner max parallel + node状态;
  • event replay buffer sequence;
  • atom transaction更新 metadata。

共同点是没有用一个全局 mutex,而是选择正确 key:session、source、topic、request、binding、run。这是高并发 Agent应用中最值得迁移的模式之一。

优点:

  • messaging logger支持 child context和结构字段;
  • transport connection保留 attempt、close code、last error;
  • Agent错误以 exception上报Sentry;
  • provider/subprocess stderr与机器 stdout分开; -许多关键日志带 workspace/session/binding/event;
  • Task run、automation history和session JSONL本身就是审计记录。

仍需统一:

  • trace/run/turn/tool/request id贯穿 UI→RPC→SessionManager→backend→subprocess;
  • log字段 schema与敏感字段 redaction;
  • model/API/MCP/browser/消息平台的耗时与失败分类;
  • dropped event、replay gap、queue depth、outbox lag等指标;
  • 用户可导出的诊断包与隐私预览。

当前许多 console.* 与结构 logger并存,跨进程关联一次失败仍较费力。

18.15 构建矩阵本身就是架构成本

Section titled “18.15 构建矩阵本身就是架构成本”

产物包括:

  • Electron main/preload/renderer/resources;
  • macOS/Windows/Linux安装包;
  • headless server多平台二进制;
  • Pi agent server;
  • session MCP server;
  • WhatsApp worker;
  • WebUI;
  • Viewer;
  • Python document tools与资源;
  • browser/native依赖。

因此“本地 TypeScript能跑”远远不够。子进程 entry、resources path、Electron ASAR unpack、native module ABI、Python env和平台文件名都可能只在打包后失败。

建议把构建 smoke当测试维度:每个产物至少启动、握手、执行一个无网络命令再退出。

  1. Session Actor:运行资源、队列、持久状态、事件出口以 session为一致性边界。
  2. Backend abstraction:共享外壳但保留 provider-native session/branch/compaction语义。
  3. 统一 RPC:本地 Electron与远程 server使用同一 channel contract。
  4. Reverse capability:远端 Agent可借用在线客户端浏览器,而不把 GUI塞进 server。
  5. Durable-first message:用户输入先落盘,降低确认后丢失。
  6. Pure event projection:流式协议到 UI状态有单一、可测试转移。
  7. Central pre-tool gate:provider变化不绕过权限、安全与prerequisite。
  8. File-native domain:workspace/session/source/task可搬运、可诊断。
  9. Workflow reuse:Task/Automation/Messaging都复用 SessionManager,不复制执行器。
  10. Capability-aware degradation:Web/Viewer/platform adapter明确能力不足时的降级。

SessionManager 同时是:

  • repository;
  • actor registry;
  • backend factory;
  • turn coordinator;
  • permission broker;
  • browser host resolver;
  • source refresher;
  • task/automation host;
  • branch/share/transfer service;
  • event publisher。

问题不是9,000行本身,而是这些职责的变化原因不同。新增 provider、修改 persistence、改 browser host选择都碰同一类,回归半径过大。

flowchart TD
F["SessionFacade / ISessionManager"]
R["SessionRepository"]
A["ManagedSessionRegistry"]
T["TurnCoordinator"]
B["BackendRuntimeFactory"]
P["PermissionBroker"]
C["ContextAndSourceController"]
E["SessionEventPublisher"]
W["WorkflowHost"]
F --> R
F --> A
F --> T
T --> B
T --> P
T --> C
T --> E
F --> W

推荐顺序:

  1. 先抽纯 repository与event publisher;
  2. 再抽 backend runtime factory;
  3. 把 send/queue/steer/abort变成 TurnCoordinator状态机;
  4. 最后把 Task/Automation callbacks移到独立 host adapter;
  5. 外部仍保留 ISessionManager facade,避免大爆炸迁移。

18.18 第二大债:shared 已变成平台内核

Section titled “18.18 第二大债:shared 已变成平台内核”

packages/shared 约11.6万行,包含:

  • Agent实现;
  • config/storage/credentials;
  • prompts/tools/modes;
  • sources/MCP/API;
  • sessions JSONL;
  • tasks/automations;
  • protocol/i18n;
  • browser runtime;
  • network interceptor。

它不是普通“shared utils”,而是多个内聚域的合集。包名掩盖依赖方向,任何 consumer很容易从内部任意路径import。

可逐步形成:

agent-runtime
agent-protocol
session-domain
workspace-config
source-runtime
workflow-domain
security-policy

不必一次拆 npm package;先建立 public barrel与eslint boundary,再按编译/部署需求物理拆包。

当前 AgentBackend 隔离得不错,但 provider差异仍渗到:

  • stored metadata与branch anchors;
  • Viewer tool payload字段;
  • model/thinking配置;
  • context/compaction行为;
  • permission event细节。

不要追求把所有 provider压成最低公分母。更好的模型是:

interface AgentBackend {
common lifecycle...
capabilities: {
nativeFork,
nativeResume,
nativeCompaction,
thinkingLevels,
toolProtocolVersion
}
}

上层以 capability选择语义;持久层保存 provider-neutral transcript + provider-specific resume envelope,两者边界明确。

本地 IBrowserPaneManager 历史上有同步查询,远程 WS调用天然异步,于是 RemoteBrowserPaneManager 对部分 sync方法只能返回空/降级,并另加 async版本。

这说明接口抽象建立在“本地对象”假设上。既然产品已支持远端,跨边界端口应全部 Promise化;本地实现立即 resolve即可。否则调用者可能在本地正确、远端静默得到空列表。

Task schema能 round-trip route/map/loop/approval/...,当前 Runner只执行 session DAG子集。这有利于前向兼容,但会产生“解析成功=支持执行”的错觉。

应在 schema之外显式发布 capability/version:

{
taskSchemaVersion: 1,
executorVersion: 1,
supportedNodeKinds: ['session'],
supportedFeatures: ['depends_on','inputs','retry','verify']
}

Editor保存/运行前根据 executor capability校验,而不是只看 parser。

第16章列出多个2,000–4,000行组件。最需要避免的是“为了减行数把 JSX搬到子文件”,但状态/副作用仍集中在 parent。

真正应抽的是应用层控制器:

  • event subscription + reconcile;
  • route/filter projection;
  • composer intent state machine;
  • panel/navigation model;
  • overlay resolver;
  • platform capability service。

纯 UI组件应拿明确 view model与commands,减少直接访问 window.electronAPI 的位置。

若按风险而非易改程度排:

  1. 远程 TLS信任:移除默认 tlsRejectUnauthorized:false,支持证书 pin/trust UI;
  2. Browser权限细分:Safe模式不应把 click/fill与snapshot一概视作只读;
  3. Shell/path validator跨 macOS/Linux/Windows一致性与fuzz;
  4. Source OAuth/API credential日志与错误 redaction;
  5. Messaging所有 callback/附件/pairing端到端攻击测试;
  6. HTML/Markdown/URL artifact跨 Electron/Web/Viewer同一 sanitization policy;
  7. Remote client capability invocation绑定 workspace/session/host身份;
  8. 分享 snapshot脱敏与撤销后的缓存策略;
  9. 自动化默认 allow-all的可见风险与 sandbox profile;
  10. 文件写入atomic/corruption quarantine。

安全策略应按“动作效果”分类,而不是按工具名或 provider名分类。一个 browser_tool click、MCP send_email 与 shell curl -X POST 都可能产生外部副作用。

对关键时间点注入崩溃:

after user-message persist
after runtime create
mid text stream
after tool side effect before result persist
mid compaction
mid branch copy
mid source credential refresh

重启后验证无用户消息丢失、无重复 tool执行、状态可解释。

  • client/server版本差一档;
  • Pi/WhatsApp JSONL随机分块、粘包、坏帧、超大帧;
  • event sequence gap/replay overflow;
  • unknown event/tool content兼容;
  • capability消失/host切换。
  • shell/PowerShell validator;
  • path boundary/symlink;
  • browser command tokenizer;
  • task引用与DAG cycle;
  • Markdown/Lark/Telegram formatter;
  • session JSONL migration parser。
  • 两窗口同时看/改 session;
  • 一个窗口离线后回放;
  • split view未读;
  • permission在桌面与Telegram竞争;
  • remote browser host断线重选。
flowchart TD
A["1. 修 CI: full tests/typecheck/lint/build"]
B["2. 固化 protocol schemas + compatibility tests"]
C["3. 统一 atomic storage toolkit"]
D["4. 抽 SessionRepository/EventPublisher"]
E["5. 抽 TurnCoordinator/RuntimeFactory"]
F["6. UI application controllers"]
G["7. 拆 shared public domains"]
A --> B --> C --> D --> E --> F --> G

先修验证网,再重构核心。否则拆分只会把现有隐含行为变成无法发现的回归。

  • 实现 AgentBackend lifecycle与event mapping;
  • 声明 native resume/fork/compact/thinking capability;
  • 复用统一 pre-tool permission pipeline;
  • 定义 provider resume envelope与失效回退;
  • 测 message/tool/permission/error/usage parity;
  • 验证中断、queue、branch、restart。
  • config schema与 migration;
  • credential storage,不把 secret写普通文件;
  • OAuth callback/relay与state校验;
  • tool naming/description normalization;
  • runtime refresh串行与在途 turn策略;
  • request/response大小、重试、redaction;
  • source mutation如何影响 active session。
  • canonical type与默认值;
  • metadata/full session是否都需要;
  • JSONL/legacy migration;
  • RPC DTO与event;
  • atom/event processor/UI projection;
  • branch/transfer/share是否携带;
  • 多窗口与重连测试。
  • provider schema/alias;
  • canonical name;
  • mode分类与风险等级;
  • pre-tool path/command/network/source checks;
  • permission描述是否可理解;
  • output truncation/artifact;
  • UI parser与generic fallback;
  • background/cancel/restart语义。

不要从 34万行开始。保留骨架即可:

1. Workspace + file-backed SessionRepository
2. SessionActor(send/abort/queue)
3. 单一 AgentBackend
4. canonical event protocol
5. WebSocket RPC
6. pure UI event reducer
7. central pre-tool policy
8. one Source/MCP path
9. crash/reconnect tests

等这些不变量稳定后,再依次加:

second backend
→ branch/compaction
→ remote client capability
→ Tasks/Automations
→ Messaging
→ share/viewer

Craft最复杂的地方都来自功能组合,不来自单功能本身。增量顺序决定架构是否仍可理解。

模式 A:Actor + durable log + event projection

Section titled “模式 A:Actor + durable log + event projection”
command → actor serializes → durable state → canonical event → UI projection

适合任何长运行、可中断、可恢复 Agent。

模式 B:Provider-neutral core + provider resume envelope

Section titled “模式 B:Provider-neutral core + provider resume envelope”

共享产品语义,但不伪造完全统一的底层会话能力。

SessionManager提供 host接口,Task/Messaging/Browser实现方从外部注入;比 service locator或互相 import清晰。

短断线靠 event buffer低成本追平,长断线按 active/processing范围拉 snapshot。

同一 UI/renderer面对 Electron、Web、Viewer、Telegram、WhatsApp时,明确声明 edit/file/browser/approval等能力。

模式 F:稳定上下文与易变上下文分层

Section titled “模式 F:稳定上下文与易变上下文分层”

System prompt缓存稳定语义,turn context承载时间、workspace、sources、协作状态,降低缓存失效。

  1. 把所有领域都堆进一个 shared 包;
  2. 让单一 manager成为 repository、runtime、workflow和transport总入口;
  3. 直接覆盖小 JSON文件而没有统一 atomic writer;
  4. 用“schema可解析”代表“executor可运行”;
  5. 为本地对象设计同步接口,再勉强远程代理;
  6. 让 Web adapter假装所有 Electron方法都存在却 no-op;
  7. 把 browser整类工具在 Safe模式一概自动允许;
  8. 依赖非必跑/手动 CI验证关键集成;
  9. 在 OSS导出中保留指向未发布脚本的命令;
  10. 在每个 UI surface重复 provider-specific tool字段 fallback。

理解或修改 Craft时,可以用这十条做回归审查:

  1. 用户确认已发送的消息最终必须可从磁盘恢复。
  2. 同一 session同时只有一个协调者决定 turn/queue/abort顺序。
  3. Provider差异不能绕过统一权限和安全检查。
  4. 所有客户端消费同一 canonical session event语义。
  5. Event丢失时必须有权威 snapshot修复路径。
  6. Secret只进入 credential boundary,不进入普通 config/prompt/log。
  7. Workspace/session/source/browser/message binding都必须有明确 ownership key。
  8. 任何跨进程消息都按不可信 unknown 验证。
  9. 外部副作用的批准要绑定精确 request并且一次生效。
  10. Schema、runtime capability与 UI承诺必须描述同一个版本边界。

Craft Agents 0.11.2 最有价值的地方,是它已经把“Agent聊天”推到了真实个人工作系统的复杂边界:多模型、长会话、文件事实源、工具安全、OAuth/MCP、远程客户端、浏览器、工作流、外部消息和跨端 UI。

它证明了一条可行路线:

Session 是核心 Actor
文件系统是可恢复事实源
Backend 是模型运行端口
Event 是所有表面的共同语言
RPC/capability 是部署边界
Policy 是所有工具的共同门卫

同时,0.11.2 也显示了平台成长后的典型压力:中心类和 shared包膨胀、UI应用层过厚、协议同步成本、文件存储策略不一、构建/CI覆盖落后于功能面。

如果只取一个结论:不要从“支持多少模型和工具”衡量 Agent架构,而要从中断、失败、重连、迁移和多入口同时发生时,系统是否仍只有一个可解释事实来衡量。 Craft已经在很多关键链路上做到了这一点;下一阶段的工程重点,是把这些正确不变量从大文件里的经验,提炼成更小、更明确、可自动验证的边界。