Skip to content

架构总览

Kairox 是一个 local-first 的 AI agent 工作台。两个用户界面(基于 ratatui 的终端应用,以及基于 Tauri 2 + Vue 3 的桌面应用)、headless eval CLI 和可嵌入 SDK 共享同一套 Rust runtime 契约。consumer 之下的所有内容都是一个由小型 crate 组成的 Rust workspace,彼此之间只通过狭窄的 trait 相连。没有服务端、没有云控制面、也没有任何共享的可变单例 —— 整个技术栈都运行在你的机器上,并将状态持久化到本地的 SQLite 数据库。

本页是这套架构的权威地图。它会解释分层示意图、维系分层秩序的依赖规则、每一层中的所有 crate、状态如何以事件的形式在系统中流动,以及背后那些较大设计决策的理由。

分层架构

产品被划分为四个实际层次:用户侧 consumer、runtime/组合层、core 契约层,以及聚焦的领域 crate。

请自上而下地阅读这张图。GUI 中的一次点击或 TUI 中的一次按键会到达一个实现了 AppFacade 的 runtime 对象。agent-core 持有这套接口,并定义描述应用内部行为的"语言"(DomainEventEventPayload、各种类型化 ID、projections)。agent-runtime 实现契约并组合领域 crate。领域 crate 不依赖 UI crate 或 consumer crate。

依赖规则

让这套架构保持稳定的唯一规则是:领域 crate 永远不依赖 consumer 或 runtime

所在层允许依赖不允许依赖
Contractworkspace 内部什么都不依赖runtime、领域 crate、consumer
Domainagent-core 和按顺序排列的其它领域 crateagent-runtime、UI、eval、SDK
Runtime / 组合层contract + 领域 crateUI 前端状态或 view 模块
Consumer Rust crateruntime + 自己需要的领域 crateVue 内部实现或无关 consumer

agent-core 在 workspace 内部零依赖。一些小型领域 crate 也是叶子 crate(agent-lspagent-skillsagent-plugins),因为它们只建模独立的数据或协议概念。任何新增领域 crate 想去调用 UI 表面,都是错的;正确做法是发出一个 event,让 consumer 自己响应。任何新增 UI 功能需要 runtime 行为时,应通过 facade 或由 runtime 支撑的类型化 Tauri command。

在代码层面,这条规则由 Cargo 边保证:没有领域 crate 会 import agent-runtimeagent-tuiagent-gui-tauriagent-evalagent-sdk。在 review 阶段,它体现在 PR 的范围上:一个同时改 agent-runtime 和 GUI 的 feature 是正常的;但一个领域 crate 反过来读取 consumer 状态,就是味道不对。

各层 crate 一览

Facade 层 —— agent-core

agent-core 是被刻意保持精简的。它只包含:

  • AppFacade —— runtime 实现、两个 UI 调用的异步 trait。所有用户可见的操作都要走它的某个方法。
  • 领域事件 —— DomainEventEventPayload(一个囊括了每种 payload 变体的 enum)、MemoryMarkerMemoryScopePermissionDecision
  • 类型化标识符 —— SessionIdWorkspaceIdTaskIdAgentIdMessageIdTrajectoryIdAutonomousTaskId,全部是基于 UUID 的 newtype,并带有 serde 支持。
  • Projections —— TaskSnapshotTaskGraphSnapshotTaskStateAgentRole
  • Review 与 trajectory DTO —— TrajectoryStepTrajectoryOutcomeAdvisorModeAdvisorVerdict,以及 autonomous task snapshot。
  • 构建信息 —— BuildInfo(版本号、git SHA、构建日期),让两个 UI 呈现一致的 banner。

agent-core 导出了一个 specta feature,GUI 的 Tauri 后端会启用它,以便为跨 IPC 边界的类型推导出 specta::Type。这一处是唯一的不对称 —— Rust 内核本身对 Tauri 不持任何立场。

领域层

Crate角色关键类型
agent-runtime编排 agent loop、session-actor 执行运行时、context budgets、turn-end 无竞态 compaction、模型切换、可配置的 agent 设置、multi-agent 策略、advisor review、autonomous checkpoint、trajectory capture、MCP 生命周期、permissions。LocalRuntime<S, M>SessionActorSessionExecutionRuntimePlannerAgentWorkerAgentReviewerAgentAgentStrategyDagExecutorTaskGraphMcpServerManager
agent-models模型提供方抽象(OpenAI 兼容、Anthropic、Ollama、Fake),并带有 metadata 与 context-window 注册表。ModelClient trait、ModelRequestModelRouterModelProfileModelRegistry
agent-tools工具注册表、正交的 Approval × Sandbox 策略引擎、内置工具(shell.execfs.readfs.writefs.listpatch.applysearch.ripgrepmonitor.start/list/stopbrowser.actionbrowser.batchcomputer.use),以及 MCP-tool 适配器。ToolRegistryPolicyEngineApprovalPolicySandboxPolicyPolicyDecisionPolicyRiskTool trait、ToolRiskMcpToolAdapterMonitorRegistryBrowserToolComputerUseTool
agent-mcpMCP(Model Context Protocol)客户端、stdio + SSE + Streamable HTTP 传输、server 生命周期、discovery 缓存、marketplace 目录(内置 + 远端来源)。McpClientTransport trait、StdioTransportSseTransportStreamableHttpTransportServerLifecycleMcpServerDefCatalogEntry
agent-lspLSP 和 DAP 客户端实现、JSON-RPC transport、server 生命周期管理,用于代码智能与调试器集成。LspClientDapClientLspServerDefDapServerDefLspServerLifecycleDapServerLifecycleServerStatus
agent-skills原生 skills 系统 —— 可复用的 prompt / tool / workflow 能力、frontmatter 解析、注册表、GUI 设置。SkillRegistrySkillDefSkillFrontmatterSkillScopeSkillSettings
agent-plugins描述插件提供的 skills、tools、hooks、MCP server 的 manifest 与 inventory。PluginManifest,以及若干 inventory 辅助方法。
agent-memory用户、workspace、session 三种作用域下的持久化记忆,基于 tiktoken budget 的 context 装配、multimodal image pruning,以及 prompt compaction。MemoryStore trait、SqliteMemoryStoreContextAssemblerMemoryMarkerContextCompactorImagePruningStrategy
agent-storeappend-only 的 SQLite event store,用于 workspace/session 追踪的 metadata 表,以及 trajectory 持久化。EventStore trait、SqliteEventStoreSessionMetaTrajectoryStoreSqliteTrajectoryStore
agent-configTOML 配置加载、model profile 发现、从 env 中解析 API key、.kairox/ 项目发现、advisor 策略、skills 配置、instructions 配置。ProfileDefAdvisorConfigload_from_strbuild_router

agent-runtime 是组合所有领域 crate 的 composition crate。其余 crate 都保持狭窄:agent-memory 依赖 model metadata,但不依赖 tools 或 storage;agent-tools 依赖 MCP/LSP 协议类型来适配外部工具;agent-config 依赖 model/MCP/LSP 定义来解析 TOML。当一个 runtime 功能同时需要多种能力时,由 runtime 来组合。

UI 层

Crate角色关键类型
agent-tui三栏式的 ratatui 应用(sessions、chat、trace)。带构建信息 banner、permission modal、模型切换 UI。AppChatPanelSessionsPanelTracePanelPermissionModal
agent-guiTauri 2 后端(Rust)+ Vue 3 前端。持久化的 session、任务图、trajectory viewer、autonomous 设置、MCP 与 memory UI、marketplace、模型 / agent / 插件 / hook / instructions / skills 设置。Rust:commands.rsGuiStateevent_forwarder.rsspecta.rs。Vue:stores(sessiontaskGraphagentsautonomousmcpmemorycatalogskills)、组件(ChatPanel.vueTaskSteps.vueTrajectoryViewer.vue、……)

两个 UI 实现的是同一套交互模型 —— 开启 session、发送 prompt、观察 trace、批准 permission —— 走的是同一个 facade。TUI 是最朴素的参考客户端;GUI 则在此基础上增加了持久化、多 session、marketplace 以及设置管理。

事件溯源的状态

在 Kairox 中,状态变更是事件,而不是 mutation。runtime 内部发生的每一件有意义的事 —— 收到一条消息、调用一个工具、做出一次 permission 决策、任务启动或完成、切换模型、提议记忆 —— 都被记录为一个 DomainEvent 并 append 到 event store。UI 是从事件流中渲染出来的,而不是从某个"session manager"持有的可变状态中渲染。

这套设计带来几个自然的特性:

  • 重放是免费的。 重启 GUI 会从 SQLite 重读事件,然后重建任务快照、聊天历史和 trace 时间线。没有什么"从 cache 重建"的路径。
  • UI 是订阅者,而不是所有者。 GUI 的 event_forwarder 调用 LocalRuntime::subscribe_all(),把每一个 DomainEvent 通过 Tauri 的 emit 转发给渲染端。TUI 在同一进程内做同样的事。两者都会按当前聚焦的 SessionId 过滤。
  • 持久化是有边界的。 agent-store 只写 envelope。生产环境下的隐私默认配置(参见 Permissions & Tools 一页)限制了哪些 payload 内容会被持久化。
  • 审计是这套设计的副产品。 因为每个决策都是一个事件,trace 面板根本不需要一份额外的 audit log;它就是那份 audit log。

事件分类本身 —— EventPayload 的每一个变体以及由谁发出 —— 在 Runtime & Sessions 页中有完整说明。

Trait 边界

Trait 边界不是装饰。Kairox 中每一条跨 crate 的依赖都要通过 trait,这样测试时可以替换为 fake,新加适配器(新的 model provider、新的 transport、新的 event store 后端)时也可以即插即用,不必动到 runtime。

Trait定义于使用方说明
AppFacadeagent-coreagent-runtime、agent-tui、agent-guiUI 与 runtime 之间的集成点。
EventStoreagent-storeagent-runtime,以及 consumer 接线边界SqliteEventStore 实现;测试中使用内存版 SQLite(:memory:)。
MemoryStoreagent-memoryagent-runtime、agent-gui(只读)SqliteMemoryStore 实现。
ModelClientagent-modelsagent-runtime由 OpenAI 兼容、Anthropic、Ollama 以及 Fake 客户端实现;由 router 进行多路复用。
Tool / ToolProvideragent-toolsagent-runtime、agent-mcp(经适配器)内置工具与 MCP 暴露的工具都实现 Tool
Transportagent-mcpagent-mcp 内部StdioTransportSseTransportStreamableHttpTransport 实现。
AgentStrategyagent-runtimeagent-runtimePlanner / Worker / Reviewer 角色。策略之间通过组合搭配,而不是通过继承派生。

如此严格的纪律有一个具体的理由:crates/agent-runtime/tests/full_stack.rs 可以端到端地跑一个真正的 LocalRuntime<SqliteEventStore, FakeModelClient>,不需要 model API key、不需要 MCP server、也不需要 GUI 窗口 —— 因为每一个协作者都是一个 trait,而每一个 fake 都只是一次 cargo test 的事。

Crate 依赖图

上面那条依赖规则,在实际中就是这样一张 DAG。下面这张图正是 cargo 实际强制执行的形状 —— 请把它理解为谁知道谁,而不是运行时谁调用谁

agent-tuiagent-gui-tauriagent-evalagent-sdk 是依赖 agent-runtime 的 consumer crate。领域层没有任何 crate 反过来依赖它们。这种不对称正是关键所在:新增一个 model provider、工具、MCP transport 或 skill 来源时,可以先放在所属领域 crate 中,再由 runtime 组合,而不必让 storage、memory 或协议 crate 知道 UI 状态。

决策记录

有几个较大的决策值得记录在案。它们并不是从一开始就显而易见的,但它们塑造了本站的每一页。

为什么用 facade,而不是直接调用领域

GUI 过去是直接调用 agent-runtime 的。加上 TUI 之后,我们才发现有多少假设是 GUI 形状专属的 —— Tauri 风味的错误类型、GUI 专属的 event 形状。把这些类型上提到 agent-core,并强制两个 UI 都走 AppFacade,让 runtime 可以自由地重构(整个 agent-runtime 的模块布局在 #532 中被彻底拆分)而不会破坏任何一个 UI。代价是每次调用多一次 trait 分发,这对一个被 LLM 阻塞的工作流来说微不足道。

为什么用 event sourcing,而不是一个状态结构体

有三股力量把 runtime 推向了 events:(1) GUI 和 TUI 需要从同一个数据源渲染同一份 trace;(2) 桌面应用在崩溃或重启之后需要干净地恢复状态;(3) 把每个 permission 决策都记成一行,而不是某个方法的返回值,对审计和可观测性来说要干净得多。事件机制就位之后,后续的 compaction 和模型切换功能(参见 #531#533)就变成了"再加几个 event 变体",而不是"把新状态串到每一个消费者里"。

为什么把 agent-runtime 拆成多个聚焦模块

agent-runtime 变得越来越大。把它拆成 agent_loopagentsdag_executorevent_emitterfacade_runtimemcp_managermemory_handlerpermissionsessiontask_graph 之后,每个模块都可以单独测试,模块之间的依赖关系也变得可见。这个模式跟 workspace 的模式是同一个 —— 小模块、窄接口、模块之间互不反向依赖,只是递归地应用到了 crate 内部。这次拆分之后随之而来的"queue-the-actor"重构请见 #532

为什么用 SQLite(而不是自定义文件格式)

SQLite 提供事务性的 append、确定性的恢复、对任务图与 memory 查询的索引读取,而且零外部服务依赖。它通用、经过实战检验。Event store 是 append-only 的,memory store 体量也很小;不存在那种需要不断 schema-migration 的痛苦。

为什么要做两个 UI,而不是一个

TUI 是对 facade 质量的一种刻意的施压机制。如果一个功能能在 TUI 中实现,那 facade 多半设计得正确;如果不能,那说明 facade 里掺杂了 GUI 专属的关切。GUI 是面向最终用户的产品形态;TUI 则是开发者的参考客户端,也是 facade 重构的试金石。同时维护两者的成本并不高,因为它们消费同样的事件。

为什么用 Bun 而不是 npm/pnpm

仓库把 packageManager 锁定到了 Bun,而项目里的 lint-staged、husky、just 等 recipe 都假设使用 bun。Bun 的安装速度以及内置的 bun test 能让 GUI 的开发循环在开发者笔记本上保持流畅。代价 —— 团队工具箱里又多一个包管理器 —— 已经在 安装问题排查与 FAQ 中说明。

本页不涉及的内容

本页是这套系统的地图。它不会展开 runtime 在每一 turn 内的行为、memory 协议、ApprovalPolicy × SandboxPolicy 决策矩阵或扩展面。每一项都在本节中有专门的一页。下一步,从 Runtime & Sessions 开始,看看每个 prompt 背后发生了什么。

基于 Apache-2.0 协议发布。