9.8 KiB
Agent GUI 架构收敛方案(历史记录)
状态:架构重构完成(2026-07-11 复核)。本文只保留这次重构的背景、关键决策、迁移切片和退出标准,不再充当现行架构规范。原始规模快照、逐文件归属表和详细迁移清单可从 Git 历史读取。
现行规范:
- Agent GUI Node:界面、会话引擎、宿主边界
- Agent Extensions:扩展目标、运行时安装、setup 生命周期
- Provider-native Subagents:child session 实体关系
packages/agent/gui/AGENTS.md:包内编辑路由和硬规则
1. 为什么重构
原实现没有单个致命错误,而是多个局部合理选择累积成系统性偏移:
- 数据模型没有独立 turn,session 同时承载会话、执行和交互状态
- 工作区级编排依附 React 面板生命周期
- daemon、桌面桥、多个 store 和组件各持有一份乐观状态
- provider 差异以名称分支渗入界面
- Agent GUI 以横向巨石文件组织,修改一个流程会横跨整棵组件树
- dock、消息中心、通知等消费方各自推导 session 状态
- 架构约束主要是散文禁令,缺少类型和自动检查
文件过长只是职责、状态所有权和时序边界不清的可见症状。
2. 决策
2.1 实体边界
活动域分为三个实体:
| 实体 | 所有内容 | 不应包含 |
|---|---|---|
| Session | 目标、工作目录、标题、设置、当前 turn 引用 | turn 结果、待处理审批 |
| Turn | 一次提交的阶段、结果、错误、文件变化 | 后续 turn 的状态 |
| Interaction | 审批、提问、计划确认及处理状态 | session 级展示状态 |
派生值不作为第二份事实保存:
- 展示状态由 turn 和 interaction 推导
- 提交可用性由 selector 推导
- session 只引用 active turn,不复制其 phase/outcome
- interaction 是否待处理由其自身状态表达,不使用三态
null补丁协议
2.2 状态所有权
判定问题只有一个:关闭所有 Agent GUI 面板后,该状态是否仍应存在并继续工作?
- 是:属于 daemon 或工作区级会话引擎
- 否:属于组件本地状态
队列推进、乐观提交、运行时事件对账、当前 session/turn 投影属于引擎;滚动位置、输入焦点、临时展开状态属于组件。会话 Rail 的滚动、section 折叠和“显示更多”按 workspaceId + agentTargetId/all 记忆;搜索 query 只建立瞬时导航状态,不扩张永久 scope map。query controller 只报告当前请求 scope 是否已精确 resolved,不拥有 DOM 或滚动位置。activeConversationId 只表达选中事实;外部打开与新建会话通过显式 reveal intent 请求滚动,Rail 点击与 provider 恢复不发滚动命令。
2.3 单向数据流
flowchart LR
UI[Agent GUI / dock / 消息中心] -->|intent| Engine[工作区会话引擎]
Engine -->|command| Transport[桌面传输壳]
Transport -->|HTTP / event| Daemon[tuttid]
Daemon -->|canonical activity| Transport
Transport -->|runtime event| Engine
Engine -->|selectors| UI
约束:
- daemon 是业务规则和持久状态权威
- 引擎是客户端时序、乐观状态和对账的唯一所有者
- 桌面层只做传输和宿主集成,不形成第二业务核心
- React 读取快照、派发意图、渲染;不以 effect 编排业务生命周期
- 所有消费方共享 selector,不自行重建状态词汇表
2.4 Provider 差异
传输协议不是稳定边界,归一化活动契约才是。
- provider 身份、运行时策略、能力和目标元数据由 descriptor/target 下发
- UI 按 capability 渲染,不按 provider 名称猜行为
- 标准 ACP、专属协议和 SDK 边车都投影为同一活动契约
- 新 provider 不得要求在 Agent GUI 内新增行为分支
2.5 类型与协议
- OpenAPI 是跨 daemon 边界传输类型的事实源
- Go/TypeScript 传输类型从契约生成
- 内部域类型允许存在,但只能通过显式投影跨层
- 时间、状态和身份字段使用单一表示
- 未知枚举必须有显式处理路径,不能用宽字符串绕开检查
3. 目标模块
packages/agent/
├── activity-core/ # 规范化活动状态、reducer、selectors
└── gui/
├── agent-gui/ # 功能模块装配
├── shared/ # 有明确领域名的复用能力
└── host/ # 最小宿主接口
apps/desktop/
└── workspace-agent/ # 传输适配、桌面集成、工作区实例装配
services/tuttid/
├── service/agent/ # 会话、turn、interaction 业务规则
└── api/ # OpenAPI 投影和路由
Agent GUI 纵向拆分为:
- session list:选择、分组、固定、未读
- timeline:活动投影、消息记录、文件变化
- composer:输入、附件、提交能力
- interactions:审批、提问、计划确认
- readiness/setup:目标就绪状态和安装入口
模块可共享域模型和 selector;不得共享“万能 controller”“万能 utils”或扁平大 view model。
4. 迁移切片
迁移按可独立验证的切片完成,不做一次性重写。
切片 1:建立机械护栏
- 记录文件尺寸、effect/ref、provider 分支和公共 props 基线
- 加入 renderer/provider/activity 边界检查
- 规则使用“只降不升”的过渡预算
退出标准:新增代码不能继续扩大已知偏移。
切片 2:生成契约成为唯一跨层类型源
- 先改 OpenAPI,再生成 Go/TypeScript 类型
- 删除手写传输镜像
- 保留内部域类型与生成类型间的显式投影
退出标准:跨边界字段改动由生成检查发现遗漏。
切片 3:Session、Turn、Interaction 实体化
- turn 与 interaction 成为持久实体
- session 只保留 active turn 引用
- daemon 启动时将未结束且不可恢复的 turn 收敛为 interrupted
- 取消操作归 turn 且保持幂等
退出标准:重启后不靠 session 字符串猜 turn 状态。
切片 4:工作区会话引擎
- 用户 intent 与运行时 event 进入同一 reducer
- 乐观提交声明确认、拒绝和超时条件
- 所有对账逻辑集中到一处
- selectors 服务 Agent GUI、dock、通知、消息中心
退出标准:面板卸载不影响队列、运行状态或对账。
切片 5:桌面桥变薄
- 移除桌面层的独立乐观状态和业务判断
- 只保留客户端调用、事件订阅、生命周期装配
退出标准:相同规则不在 desktop 与 engine 各实现一次。
切片 6:Agent GUI 纵向拆分
- 按 list/timeline/composer/interactions/readiness 拆模块
- controller 只装配模块
- 组件不持有工作区业务状态
退出标准:业务文件不超过仓库 800 行限制;新增功能只触及所属纵向模块。
切片 7:清理过渡代码
- 删除旧 store、双写、兼容 reader 和旧 selector
- 删除已归零的过渡预算
- 将仍有效的规则合并进耐久架构文档
退出标准:生产路径只有一个事实源和一个对账路径。
5. 完成快照
2026-07-11 实施快照:
- 协议 P1-P3、provider descriptor、引擎骨架、切片 1 已完成
- activation/submit/queue owner 已进入 engine,旧 overlay 符号扫描为零
- 旧
AgentHostWorkspaceAgentSession/Message/Timeline生产镜像已删除 - generated-file mention provider 已通过 canonical selector 读取
- 原巨石已拆到 800 行以内,会话列表旧 store 已退役
- grouped props/view-model、纵向 view modules、渲染预算与低于 800 行的薄 controller 已落地
- 切片 7 的镜像、过渡导出、旧事件与终值基线清理已完成
- 唯一延后项是按客户端覆盖窗口删除私有持久化/launch migration reader;兼容读取不得重新进入公共写路径
6. 关键状态机
6.1 Turn
submitted -> running -> waiting -> running -> settling -> settled
\------------------------------/
规则:
pendingIntents是提交乐观消息、接受、确认、结果未知、失败与到期的单一事实源- controller 不把命令完成还原为 Promise 工作流
- 激活与普通提交共用同一 prompt envelope
- 乐观消息准入同时认文本与可渲染的结构化
content - 合成计划决策走 tuttid 语义 API 与 durable
plan_decisionsaga - provider 原生 exit-plan 继续走 durable
interactive_response - session read 契约不再暴露旧
status/turnLifecycle/submitAvailability/lastError/runtimeContext - 进程丢失后的未结算 turn 由 daemon 启动时统一收敛为
settled/interrupted
6.2 Interaction
pending -> answered
-> dismissed
-> superseded
规则:
- 每个 interaction 有独立 ID、类型、状态和所属 turn
- 回答操作幂等,重复回答返回已处理状态
- 新 turn 开始时按规则取代不再有效的 interaction
- UI 只显示 selector 给出的 pending interaction
6.3 Readiness / Setup
unavailable -> needs_setup -> setting_up -> ready
\-> failed
规则:
- built-in provider 的 managed-environment wizard 只服务其拥有的内建 provider
- Agent Extension setup 属于 Agent Target 生命周期,由 daemon 持久安装/setup 状态驱动
- React 只读取 host API 快照、发起显式用户动作、渲染状态
- provider 名称不得成为 readiness/setup 分支条件
7. 静态护栏
保留以下只降不升预算:
- 文件长度
- React effect/ref 数量
- provider 名称分支
- view props 面积
- 组件内 store 创建
预算下降时可更新 baseline;预算上升必须先修源头,不能通过抬 baseline 合并。