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
253 lines
9.8 KiB
Markdown
253 lines
9.8 KiB
Markdown
# Agent GUI 架构收敛方案(历史记录)
|
||
|
||
状态:架构重构完成(2026-07-11 复核)。本文只保留这次重构的背景、关键决策、迁移切片和退出标准,不再充当现行架构规范。原始规模快照、逐文件归属表和详细迁移清单可从 Git 历史读取。
|
||
|
||
现行规范:
|
||
|
||
- [Agent GUI Node](./agent-gui-node.md):界面、会话引擎、宿主边界
|
||
- [Agent Extensions](./agent-extensions.md):扩展目标、运行时安装、setup 生命周期
|
||
- [Provider-native Subagents](../specs/2026-07-15-provider-native-subagents.md):child session 实体关系
|
||
- `packages/agent/gui/AGENTS.md`:包内编辑路由和硬规则
|
||
|
||
## 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 单向数据流
|
||
|
||
```mermaid
|
||
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. 目标模块
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
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
|
||
|
||
```text
|
||
pending -> answered
|
||
-> dismissed
|
||
-> superseded
|
||
```
|
||
|
||
规则:
|
||
|
||
- 每个 interaction 有独立 ID、类型、状态和所属 turn
|
||
- 回答操作幂等,重复回答返回已处理状态
|
||
- 新 turn 开始时按规则取代不再有效的 interaction
|
||
- UI 只显示 selector 给出的 pending interaction
|
||
|
||
### 6.3 Readiness / Setup
|
||
|
||
```text
|
||
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 合并。
|