ProxyMind 客户端架构
本文是 ProxyMind 客户端的架构权威,只定义客户端内部职责、包边界、依赖方向、状态所有权、 生命周期和稳定不变量。需求、修复和重构必须先符合本文,再进入实现。
Mind、AppServer 与 Fabric 的跨系统职责、Authority 和集成边界以
ARCHITECTURE_SYSTEM.md 为准;本文受其约束,不重新定义跨系统事实。
本文不复制以下契约:
- 线上
mind.chat字段、端点、事件和错误以服务端正式契约及protocol/schema/、protocol/client/为准; - 编码代理的工作方式、代码质量和验证流程以
AGENTS.md为准; - 功能参数、操作说明和领域细节由对应代码、测试及
docs/文档维护; backend/是独立打包服务,不属于客户端运行时。
架构原则
- 每类状态只有一个权威所有者,缓存和展示均可从事实重建。
- 领域规则、应用用例、运行编排、外部实现和前端展示分层维护。
- 模型、工具、审批、持久化、传输和前端通过具名端口组合。
- Command、Run、外部 Effect 和线上游标具有稳定身份;未知结果进入显式对账。
- 路径、Shell、子进程和信号默认兼容 Windows、Linux、macOS,平台差异集中在 adapter。
- 结构化事件用于观测,不从日志文本、异常文案或 UI 状态反推业务事实。
系统边界
mind.py
-> composition.py
-> agent
-> protocol
-> infrastructure
-> observability
-> frontends
| 边界 | 职责 | 不得拥有 |
|---|---|---|
mind.py |
稳定进程入口和具体工厂选择 | 领域规则、前端状态、隐式服务定位 |
composition.py |
组装应用宿主、运行资源和公开能力 | 业务分支、协议解析、UI 逻辑 |
agent/ |
本地代理的领域、用例、编排、端口和持久事实 | 具体 UI、配置路径、HTTP 实现 |
protocol/ |
可供多前端复用的 mind.chat wire SDK |
本地运行生命周期、UI 和配置策略 |
frontends/ |
CLI、TUI、stdio MCP、Subscription 和终端适配 | Run/Effect 权威状态、具体能力组装 |
infrastructure/ |
配置、平台、持久化、服务、工作区和外部实现 | 前端状态、线上 Turn 权威状态 |
sidecars/ |
随客户端发布的隔离子进程入口和固化资产 | 运行编排、审批决定、线上协议语义 |
observability/ |
结构化日志、报告和异常观测 | 业务状态机、用户交互策略 |
metadata/ |
产品名称、版本、编码和展示元数据 | 配置读取、运行状态 |
npm/ |
npm launcher、平台包和发布流程 | Python 运行时、本地执行状态 |
backend/ |
独立打包服务 | 对客户端包的反向依赖 |
agent 分层
agent/
├── protocol/ # 进程内 Command、Event、Item 和能力值
├── domain/ # 纯规则、身份、状态转换、权限和补丁模型
├── ports/ # 跨职责的最小能力契约
├── application/ # 用例、执行上下文、稳定结果和展示投影
├── harness/ # Session、Run、Agent、Hook、Tool 的生命周期编排
├── stores/ # 本地权威事实和幂等边界
├── capabilities/ # 本机能力实现
├── adapters/ # 线上协议和外部代理映射
└── composition.py # 只组合 agent 内部能力
各层职责如下:
agent.protocol保存与传输无关的本地类型化契约,不是 HTTP、SSE 或 WebSocket schema。agent.domain只依据显式输入计算规则,不读取环境、文件、数据库或前端状态。agent.ports描述真实可替换能力和生命周期,不充当 Service Locator。agent.application协调领域规则与端口,不拥有事件循环、连接、进程或 UI 控件。agent.harness拥有 Session、Run、Agent、Hook、Tool、MCP 和 Subscription 的并发与关闭。agent.stores拥有本地事实、CAS 和幂等边界;Store 接收已解析路径或连接。agent.capabilities实现本机能力,agent.adapters隔离线上协议和外部代理变化。
跨层契约使用 Protocol、ABC、dataclass 或具名结果。创建方负责关闭生命周期;可空依赖
只表示真实可选能力,不作为未注入时的兼容回退。第三方对象必须在 adapter 边界转换,不能
穿透到 application 或 domain。
依赖方向
domain / agent.protocol
^
ports
^
application
^
harness
stores / capabilities / adapters / infrastructure / frontends
-> 通过上述契约接入
依赖规则:
domain与agent.protocol不依赖 application、harness、stores、capabilities、 adapters、infrastructure 或 frontends。application不依赖 harness、stores、capabilities、infrastructure 或 frontends。harness通过 application、domain、protocol 和 ports 组织生命周期,不导入前端。agent.composition只组装 agent 内部对象,不导入 infrastructure。infrastructure可以实现 agent ports 或消费 application 契约,但不反向控制运行生命周期。frontends只调用 application、ports、protocol 或显式注入的 infrastructure adapter。- 顶层
protocol不依赖 agent、frontends、infrastructure、server 或 backend。 backend只依赖自身、标准库和第三方库;客户端代码不导入 backend。sidecars不导入 Python 业务包;只有infrastructure.sidecars可以驱动其私有 IPC。
tests/test_package_architecture.py 与 tests/architecture/ 中的专题审计持续检查这些边界;根文件
保持稳定入口,专题审计共享单一源码清单与 AST 缓存。
组合与生命周期
mind.py 和根 composition.py 是唯一具体组合边界:
mind.py选择应用布局、平台实现、能力工厂和前端入口。agent.composition.create_runtime_services创建与 UI 无关的运行服务。- 根
composition.py组装应用宿主、Session、执行资源、持久化和服务 owner。 - CLI、TUI、MCP 和 Subscription 接收已组装的 host、factory 或 port。
ProcessResourceOwner依序关闭前台 Turn、后台任务、MCP、Sidecar、服务、Sandbox、 Store 和观测资源;单项清理失败不得跳过后续资源。
跨前端传递的完整组合对象统一命名为 host。进入 feature 或 application 后继续收窄为
实际消费的端口,不把应用宿主当作通用服务定位器。业务模块不得通过全局变量、Controller、
前端对象或产品名反射发现能力。
Command、Session 与 Run
身份与入口
| 范围 | 稳定身份 | 序号或幂等键 |
|---|---|---|
| 本地运行时 | session_id、run_id |
command_id、idempotency_key、Run sequence |
| 线上协议 | cid、sid、turn_id、attempt、item_id |
request_id、client_message_id、event_seq |
| Subscription | 订阅 session_id、任务 call_id |
message_id、seq、last_acked_seq |
这些身份和序号不得互相赋值或比较。Adapter 显式保存映射,不依赖字符串碰巧相等。
所有主动执行入口最终映射为同一应用命令链:
frontend adapter
-> immutable Command
-> Session command queue
-> Run actor
-> model / tool / approval ports
-> durable event and projection
-> frontend adapter
Command 入队前必须冻结完整语义和 exec_env。重试与安全 redispatch 复用原快照;相同幂等键
和相同语义返回既有结果,不同语义产生冲突。
Review 使用独立 SubmitReviewCommand,在本地 Run 入账前冻结 target、Git workspace、execution、
environment、request_id 和远端 Turn 坐标。确认登记后只观察既有 Turn;冷恢复对已登记项执行
attach/replay,对尚未开始网络操作的 queued Review 以原 Command 安全 redispatch,不把它恢复
成普通 message 或重新打开旧菜单。
单写者与终态
每个 Session 只有一个状态写入者。调用方提交 Command,不直接修改 Session、Run、计划、工具 或审批状态。并发输入、取消、审批和恢复均经命令队列或具名 mailbox 协调。
Run 终态不可离开。连接关闭、展示完成、HTTP 回执、任务取消、SSE EOF 或异常文本都不能替代 逻辑终态事实。
Durable Queue
Durable Queue 与 TUI 当前 Turn 的普通 pending input 是两个入口:
- pending input 由 TUI Session actor 持有,中断后恢复编辑器;只有显式
/queue才持久排队; - 服务端 snapshot/receipt 是队列成员、顺序、版本及 Queue 到 Turn 转换的唯一权威;
- 客户端在网络操作前保存冻结命令、输入、工具、权限、环境和幂等身份,但不维护第二套队列;
- add/start 结果未知时复用原
request_id;明确拒绝后才允许生成新的请求身份; - start 成功后只通过 attach/replay 观察已有 Turn,不得再次调用
/mind-chat; - 本地冻结快照缺失时可以展示队列项目,但不得按当前配置猜测并执行。
直接提交的 Durable Run 也必须在首次网络操作前持久化完整模型请求。恢复时 status 只确定 待观察 Turn 和 replay 水位;本地持有冻结请求时,必须完成 attach 并持久化缺失正文及唯一终态, 才能解除执行门。历史残留若既无冻结请求也无远端 Turn 身份,只能记录失败的恢复决议并解除 本地门禁,不得猜测远端结果或自动重投原输入。
线上协议
protocol/ 是独立 wire SDK:
protocol/schema # 严格字段、判别联合、身份和值约束
protocol/transport # 认证、端点、可靠请求、SSE 和报告传输
protocol/client # chat、review、turn control、tool、effect、fork、compact 等用例
前端可以复用协议 SDK,但不要求共享 Python UI。协议层不拥有本地 Session、Run、工具执行器、
配置或前端生命周期;agent.protocol 的本地事件也不得暴露为远程 SDK。
线上 event_seq 在 cid + sid 范围内跨 Turn 单调。客户端只在完整处理后推进确认游标,并以
turn.completed 作为唯一逻辑终态。其 status 区分 completed、interrupted、failed 和
cancelled;服务端内部存储标志不得成为第二个公开终态。
Item 与前端展示
正式展示事件先由 CanonicalItemReducer 归约:
- active 视图只包含未被 retry 或
presentation.superseded替代的 revision; - audit 视图保留所有展示 attempt;
- 普通 assistant 正文只从 active text item 派生;结构化 Review 正文只从 active review item 的严格
output派生; - approval snapshot 只裁决旧审批,不推进确认游标;
- gap 等控制信号不创建展示 item。
Application 只产出与 UI 工具包无关的展示值。前端负责交互、布局和渲染,不从原始 provider 载荷、异常文本或日志重建业务语义。
终端能力
TUI 读取输入前创建一次不可变终端能力快照。终端身份、颜色、默认前景背景和输出能力是独立 事实;探测只发生在 platform adapter,renderer 不重新读取环境或平台状态。组件消费语义样式, 由唯一 resolver 适配 TrueColor、ANSI 256、ANSI 16 和无色模式。终端身份或颜色能力不得被 当作所有终端特性的总开关。
Turn 表面
每个 Turn 创建带稳定 scope 的 OutputSession,并区分四类出口:
OutputControlPort:流式输出资源;ContentSink:正文事实;PresentationSink:稳定展示单元;OutputActivityPort:等待、工具、审批、重试和恢复等活动事实。
四者共享 scope,但不读取彼此状态或替代彼此生命周期。活动事实只通过
TurnActivityProjector -> reduce_turn_surface() -> TuiTurnSurfaceCoordinator 形成一个前景投影。
Reducer 是纯状态转换;Coordinator 独占 timer、lease、replay 抑制和画面提交。
Turn 表面遵循以下不变量:
lifecycle表示 Turn 是否运行,status_requested表示是否请求状态行,两者相互独立;- 正文、审批和 replay 可以临时隐藏状态行,但不能结束 Turn;
- 工具开始立即请求状态行,工具完成只释放工具 lease;
- 同一因果交接以原子批次归约,只提交最终投影,不产生中间空帧;
- 终端可见内容未变化时只推进 revision,不重建组件或重置 elapsed time;
- 正文实际进入画布后才产生
AssistantVisible,状态行撤下与正文提交属于同一视觉事务; - commentary 正文完成后只在正文流已经 idle 且 Turn 仍运行时恢复状态行;final answer 和 phase 未声明的正文不自行恢复,后续状态只能由明确活动事实请求;
- transport retry 可以暂时覆盖已显示正文,但不释放、替换或重复提交正文;
- retry、supersede 和 replay 的迟到旧事件不得重新取得画面;
- 只有唯一 Turn 终态可以清空 timer、retry、工具和审批 lease 并释放输入边界。
中断和输入必须保持以下语义:
/turn/interrupt的204只确认中断已登记,不表示 Turn 已结束;创建竞态中的 404 只能在 有界窗口内复用同一request_id重试;- 第一次
Ctrl+C保留执行门并等待权威终态;确认窗口内再次Ctrl+C可以退出本地进程, 但不得启动下一 Turn、重复提交输入或自动重投未知输入; - 活动 Turn 中的
Esc可以冻结当前 pending steer,并在中断终态后按 FIFO 形成一个新 Turn;Ctrl+C不设置这一自动提交意图; - 空闲普通文本的 Tab 与 Enter 均提交 Turn;活动 Turn 中 Enter 尝试 steer,Tab 排入下一 Turn; 排队文本只在出队后解析 slash 或 Shell 语义;
- 本地存在冻结请求时,冷恢复必须 attach 到目标水位并补写正文和终态后再解除执行门。
attach/replay 只归约历史事实,不启动瞬时 timer。退出 replay 前必须按 call_id 对账客户端工具:
已收到结果的调用只收束投影,仅仍等待结果的调用可由当前进程接管;不确定状态保持 recovery
gate,不得重放副作用。
OutputSession.close() 先停止输出资源,再关闭 activity scope;两步均幂等,任一步失败仍继续
另一项。无活动画面的前端使用明确的被动实现,不创建伪状态。
工具、审批与 Effect
model intent
-> application tool contract
-> execution policy
-> approval when required
-> durable effect preparation
-> capability / infrastructure execution
-> typed result
-> protocol delivery and reconciliation
核心规则:
- 本地工具、provider 内置工具和 hosted tool 是不同边界,不通过字段猜测互换。
- 参数在执行前校验,第三方结果在 adapter 边界转换。
- Approval、Tool Call 和 Effect 各有稳定身份;
request_id只承担传输幂等。 - 审批只决定当前动作,不成为平台或服务端的全局安全策略。
- MCP 调用必须校验完整调用身份,并消费正式 Effect identity;缺失时不得本地合成权威事实。
- Session grant 只在相同 Session、Environment、server、connector 和 tool 范围内复用。
- 交互审批只拥有待决请求和选择;决定提交后即锁定,终态由结构化事实投影。
- 外部效果成功但本地提交未知时进入 reconciliation,不伪装成失败或自动重放。
- 不可重放效果不得由接管 actor 自动重试;可重试效果必须有明确幂等保证。
/tool-result只发送正式协议字段,不携带工作区、Sidecar 或 UI 私有状态。
持久化与恢复
| 事实 | 所有者 | 恢复原则 |
|---|---|---|
| Run、Command、Event | agent.stores.runs |
幂等写入、单调事件、终态不可离开 |
| Remote Turn request | agent.stores.runs |
网络前冻结,按原坐标 attach/replay |
| Agent graph、mailbox | agent.stores.agents |
活动投递可恢复,消息身份稳定 |
| Session cursor | agent.stores.sessions |
只保存本地会话索引和分支事实 |
| Transcript | agent.domain.transcripts + infrastructure.persistence |
值契约与 IO 分离,损坏可观测 |
| Approval | agent.stores.approvals |
首个决定权威,重复请求幂等 |
| Effect | agent.stores.effects |
prepared、committed、unknown 等事实可对账 |
恢复只从持久事实和安全点开始。Redis、当前连接、前端缓存和日志都不是 authority。已提交事件 不得丢失,未确认效果不得重复执行,无法确定的结果必须显式对账。
基础设施
infrastructure/ 按外部变化来源分组:
config/:配置 schema、分层、偏好、信任和路径契约;platform/:进程、Sandbox、Shell、信号、编码和平台差异;workspace/:工作区命令、补丁、diff 和运行资源;mcp/:MCP session、目录、调用和结果适配;persistence/:Transcript 和会话索引的具体存储;services/:服务 owner、健康、Helix、Turn 环境和配置宿主;skills/、hooks/、sidecars/、update/:对应外部资源的适配与生命周期。
本地进程输出以字节进入 infrastructure.platform 的统一解码生命周期;stdout 与 stderr
分别持有增量状态,系统读取块不构成字符边界。只有完整字符或 EOF 收束后的文本才能进入
workspace 和 frontend,展示层不得再次猜测进程输出编码。
MIND_HOME 是配置根,拥有 config.toml、用户规则和 Hook 配置。MIND_STATE_HOME 是运行
状态根,拥有 history、sessions、reports、Helix 和本地 SQLite;未设置时才默认使用
MIND_HOME。配置文件只要求可读,状态根必须可创建、可写并支持 SQLite 文件锁。显式状态根
失败时必须中止启动,不得回退到其他账本。
内置配置宿主只负责配置 HTTP 页面、路由和进程内生命周期;配置事实仍由
infrastructure.config 所有。npm/ 只负责 launcher、平台包、产物同步和发布,不参与 Python
运行时或协议。两者都不得取得本地执行状态所有权。
JavaScript Sidecar
JavaScript REPL 是本地执行能力,稳定工具名为 js_repl 和 js_repl_reset。Application 负责
参数、授权和结果投影;Harness 负责会话生命周期;infrastructure.sidecars.javascript 负责
进程、私有 IPC 和固化资产;sidecars/js_repl 只保存随产品发布的运行时文件。
Sidecar 必须满足:
- 执行端口与会话生命周期端口分离,由组合根注入;
- Provider 按
session_id隔离会话,并冻结工作目录和sandbox_mode; - 每个安全信封拥有独立 Node 进程,同一会话内执行按 FIFO 串行;
- 权限信封变化、超时、取消、reset、close、EOF 或协议错误均关闭原进程;
- 私有 JSONL 帧在边界完成类型、字段、身份、关联和大小校验;
- 嵌套工具提案必须重新经过工具可见性、schema、审批和 Effect 链;
- Sidecar 私有状态不得进入线上协议、Transcript 元数据或前端状态;
- 固化的 kernel 与 vendor 资产不得重写,构建和测试以固定散列验证完整性;
- wheel、源码分发和独立可执行包均包含完整资产,路径不依赖当前工作目录。
可观测性
业务代码只调用 observability 的结构化入口,不直接创建标准库 logger,不吞异常,也不以日志
文本作为协议。事件按可用范围携带 session、run、线上坐标、command/effect identity、环节、
结果类别和异常来源;正文、凭据、完整工具输出及未筛选载荷不得进入日志。
可观测性只投影事实,不拥有重试、取消、审批或终态决定。
扩展与验收
新增或替换能力时:
- 先确定状态所有者、生命周期和依赖方向。
- 判断职责属于 domain、application、harness、adapter 还是 frontend。
- 优先复用现有端口;只有出现真实可替换边界时才新增端口。
- 由组合边界注入实现,并删除动态发现、旧字段、旧路径和备用回退。
- 同步受影响的正式协议、稳定文档和契约测试。
禁止创建无明确所有者的聚合 core、common、shared 或 utils 层;禁止前端复制协议
reducer、Run 状态机或 Effect 对账;禁止测试替身扩大生产公开面;禁止让 Sidecar、配置宿主或
npm workspace 取得运行状态所有权。
跨边界变更的准出条件:
- 职责、依赖、状态所有权和关闭顺序可由代码直接确认;
- 新路径覆盖完整用例,被替代路径和回退已经删除;
- Command、Event、Effect、Approval 和线上身份保持稳定;
- 受影响的 CLI、TUI、MCP 和 Subscription 入口使用同一应用语义;
- 协议变更同步更新服务端正式契约、schema、client 和契约测试;
- 持久事实可以驱动恢复,未知 Effect 有明确对账路径;
- 包边界审计、定向行为测试和发布检查按
AGENTS.md通过。