Files
tutti/docs/architecture/agent-gui-refactor-plan.md
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

9.8 KiB
Raw Permalink Blame History

Agent GUI 架构收敛方案(历史记录)

状态:架构重构完成(2026-07-11 复核)。本文只保留这次重构的背景、关键决策、迁移切片和退出标准,不再充当现行架构规范。原始规模快照、逐文件归属表和详细迁移清单可从 Git 历史读取。

现行规范:

1. 为什么重构

原实现没有单个致命错误,而是多个局部合理选择累积成系统性偏移:

  1. 数据模型没有独立 turn,session 同时承载会话、执行和交互状态
  2. 工作区级编排依附 React 面板生命周期
  3. daemon、桌面桥、多个 store 和组件各持有一份乐观状态
  4. provider 差异以名称分支渗入界面
  5. Agent GUI 以横向巨石文件组织,修改一个流程会横跨整棵组件树
  6. dock、消息中心、通知等消费方各自推导 session 状态
  7. 架构约束主要是散文禁令,缺少类型和自动检查

文件过长只是职责、状态所有权和时序边界不清的可见症状。

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_decision saga
  • 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 合并。