统一 Session 协作模型
状态:implemented 类型:architecture Owner:backend/yuxi/modules/agents/services/cooperation.py
问题
协作需要多个拥有独立上下文的会话共同完成工作。现有 SubAgent 专用身份、后端、配置目录和调用限制,使同一套会话执行能力出现两套使用方式。目标是统一会话的创建、输入、执行和观察,以会话之间的委派关系表达协作。
本文记录统一会话的概念、接口和生命周期取舍;当前边界由 架构说明与下述源码 Owner 拥有。原调研仅作背景。
决策
顶层创建、跨树操作范围与消息来源导航由会话来源与用户范围协作补充;树级执行名额和整树停止继续保持本记录的边界。
精确任务结果、轻量观察、恢复隔离与多视图运行状态归属由任务观察决策补充。
概念与身份
协作 middleware 从实际 Run 归属读取并注入当前 Session 的名称、路径、父子关系与执行 ID;每个 Run 加载一次固定身份,动态状态仍由协作工具读取。完整继承配置保留能力一致性,同时明确子会话优先交付分配的任务,避免把继承的总编排指令误当成自身职责。相比要求模型自行查询并猜测身份,运行时注入使每次模型调用都有明确的自身定位;提示规则不替代工具执行处的授权校验。真实模型对身份提示的遵循程度仍需独立 provider 验证。
| 概念 | 职责 |
|---|---|
| Agent | 可复用的模型、指令与能力配置 |
| Session | 独立上下文及输入队列的会话实例;内部继续复用 Thread |
| Input / Turn / Run | 持久接收的输入 / 一轮工作 / 一轮中的执行段,沿用现有归属与结果规则 |
| 协作树 | Session 的创建关系;创建关系与树级执行容量;同用户同 APP 的会话可互发消息、追加输入、取消指定 Turn |
移除 is_subagent、SubAgentBackend、子智能体专用目录和白名单配置入口。新会话默认使用派发方的 Agent 配置,继承其实际模型、能力配置与授权约束。Agent 目录与显式配置选择由协作中的 Agent 发现与选择补充。角色与交付要求写入任务描述。新会话只接收显式描述,不复制父历史、checkpoint 或附件上下文,不提供 fork;共享目录中的文件仍可按权限读取。
使用稳定 session_id 做持久身份、路由和授权校验;委派关系保存父 Session、局部唯一名称及创建来源 Run。创建时只接收局部 name,默认子会话的父节点绑定实际派发 Run 所属的 Session,路径由系统按父路径与名称生成;/root 创建 aaa 得到 /root/aaa,只有 aaa 自己创建 bbb 才得到 /root/aaa/bbb。工具参数 schema 和服务均拒绝把完整路径或多段路径作为名称,避免创建关系与调用者自定路径形成两份结构。名称路径如 /root/reviewer 在当前树内解析到 ID,不能单凭路径授权。名称创建后固定、显示标题可修改,第一版不支持改挂父节点。创建关系为树,通信与控制允许跨兄弟节点及层级,包括向根 Session 提交输入。
协作接口
借鉴 OpenAI Responses multi-agent 的协作方式,工具名称与 Yuxi 概念对齐,不新增 Task 实体或复用 Background Job 状态。
| 工具 | 行为与返回边界 |
|---|---|
create_session | 创建关联会话并提交首个 Input,立即返回会话身份、名称路径与输入回执;有 Turn 时返回其 ID |
send_message | 向同用户同 APP 会话投递协作消息;运行中在安全边界读取,空闲时留待下次执行,不创建 Turn 或自动唤醒 |
submit_input | 向同用户同 APP 会话提交新工作;空闲时调度,忙碌时按 FIFO 排队,等待用户回答或审批时明确拒绝 |
cancel_turn | 按 Session ID 与 Turn ID 取消指定工作;保留历史与既有副作用,不暂停、不承诺断点恢复 |
wait_sessions | 等待指定其他成员的消息或工作状态更新,任一有更新即返回;空列表和自身拒绝,携带消费位置与明确超时结果 |
list_sessions | 默认读取本项目目录,历史查询分页与工具字段见会话目录决定;完整队列与 Run 状态由面板摘要提供 |
wait_inputs | 按精确 Input 等待任务终态,支持排队输入与明确超时 |
get_result | 按 Input 或 Turn 读取其 result Run 对应的精确结果 |
状态面板显示协作会话总数和需关注的状态汇总;有未结束的轮次或未暂停的排队输入时显示停止全部图标;点击汇总在右侧打开可关闭的协作列表标签,完整展示父子层级、任务标题和当前状态,并保留需要时的队列继续入口。用户从列表在右侧标签页打开子 Session,使用相同输入、steer、回答与审批能力;子会话不出现在左侧会话列表,不再嵌套工作区工具栏。分栏时状态入口位于主工作区;最大化后主会话成为第一项不可关闭标签,状态入口固定在外层右上角,始终查看主会话状态。状态面板在侧栏未最大化且消息右侧留白足够时嵌入该区域;空间不足或侧栏最大化时在可见状态按钮下方悬浮。两态均保留按钮和输入框可操作;点击外部只关闭悬浮态,失活会话保留各自面板状态。各会话滚动区域相互独立。新工作由 submit_input 发起;运行中方向修正沿用现有 steer 语义。完成、失败、取消和等待用户均形成可观察更新,绑定来源 Session 与对应 Turn/Run。父 Turn 正常结束或失败不终止子 Turn;完成通知持久保存,默认不唤醒已结束的父任务。普通 cancel_turn 只影响指定 Turn;用户显式“停止全部”才沿协作树取消在途工作并停止消费待处理队列。队列沿用现有显式继续入口重新调度;继续队列不恢复已取消的 Turn,不引入任务暂停/恢复工具。
会话工作区按会话独立保活,缓存容器在进入非保活页面时仍保持挂载;新建页与正式路由的缓存身份交接由发送布局连续性决策拥有。sessionRuntime 按线程共享消息历史、运行状态与 SSE;各 SessionWorkspace 独立拥有会话身份、侧栏标签、草稿、模型选择和滚动状态;共享目录只同步资源详情及前台导航投影。AgentView 通过路由 props 接收本实例身份,仅在前台且路由归属一致时协调选择或回写路由,失活使在途协调失效。返回已加载会话只恢复观察,不重置侧栏或滚动;分页缺席时定向读取会话详情。后台实例不自动标记已读。
调度、消息与共享环境
允许递归创建会话;根与所有后代共用 4 个执行名额,按整树原子领取,不能按每父节点分别计数。排队、等待协作更新和等待用户不占执行名额;恢复可执行状态时重新领取。协作等待持久记录唤醒条件、消费位置和超时,并释放实际 worker 执行槽,避免仅释放逻辑名额却让子会话无法执行。这是内部调度等待,不提供用户可操作的暂停/恢复功能。
PostgreSQL 拥有消息、回执、等待条件和终态事实;事务提交后才发布 Redis/ARQ 通知。创建、提交、消息和取消使用来源 Turn 与 tool call ID 组成的幂等键,同一 Turn 经新的 Run 恢复重放也不重复副作用;首次来源 Run 保留为审计事实。消息保留发送者、接收者、来源执行与处理位置,区分已接收和已处理;协作文本不提升为用户授权。结果按目标 Turn 的 result_run_id 读取,不取会话的“最新输出”猜测结果。执行失败有明确终态,唤醒失败可从持久事实补投。
同树默认共享派发方的实际沙盒与 Project Workdir,包括已安装软件、后台进程和端口;各 Session 独立拥有上下文、checkpoint 和 Run lease。任务终态发布与共享沙盒清理解耦,某个 Run 结束或取消不能销毁其他成员使用的环境。取消操作只停止目标执行拥有的活动,不以销毁整个沙盒替代取消;已经分离的共享后台服务不承诺随单个 Turn 取消而回滚。
共享沙盒空闲一段时间后释放,工作目录保留;下一次执行可重建环境,但不承诺恢复后台进程和临时环境。空闲判定与新执行领取须串行协调,活动工具不能被误回收。空闲超时固定为五分钟,由 leases 服务拥有,不增加配置体系。共享文件的并行写入由任务分工协调,第一版不引入自动合并、文件锁平台或 worktree 隔离。
树成员必须处于相同用户、APP 与授权作用域,委派不扩大权限。工具执行、审批和知识库可见性走统一后端边界;共享沙盒不承担不同信任身份之间的隔离。用户直接干预成员会话时,向父 Session 投递变更通知,通知本身不自动启动工作。
实现方案
| 语义 Owner | 改造职责 |
|---|---|
| agents 的 models / repositories / schema | 统一会话身份,持久化委派关系、协作消息及等待事实,保证树内命名、幂等与作用域约束 |
| agents 的 services / scheduler / runner | 复用 Input → Turn → Run 主链路;实现树级并发、等待释放与唤醒、精确取消和终态通知 |
| agents 的 runtime / middlewares | 用统一后端装配八个协作工具,去除专用 SubAgent 后端与身份型能力限制 |
| runtime/sandbox 与 workspace | 分离共享沙盒归属和 Session 执行归属,协调空闲释放与重新使用,保留持久目录边界 |
| API 与前端 sessions / agents | 复用会话输入和观察入口,展示协作树、成员产物与审批,移除专用子智能体配置入口 |
主链路为:工具或用户输入 → 授权后的会话服务 → 持久回执与队列 → 提交后调度 → 领取树级名额执行 → 持久输出和通知。恢复与事件重放读取各自事实 Owner,前端只投影已授权状态。树级观察可聚合更新,完整消息和产物始终归属于产生它们的 Session/Turn/Run。
遵循当前仓库的 fresh-only Schema 契约:只为全新数据库建立统一结构,不新增旧库升级路径或兼容身份。验证使用全新的隔离数据库。核对公开 API/事件和前端消费者并一并更新;现有审批等待和 steer 所需的 Run 接续继续保留。
替代方案
- 保留特殊 SubAgent 并补工具:仍需维护两套配置与能力规则,不满足统一会话的目标。
- 仅统一后端、保留子智能体目录:能减少代码重复,但用户和模型仍需理解特殊身份与白名单。
- 统一 Session 并以委派关系协作:采用;复用执行底座,新增必要的消息与调度语义。
- 直接托管到 Responses API:不采用为本方案前提;Yuxi 继续拥有多模型接入、持久状态与共享沙盒。
非目标:上下文继承或 fork、用户暂停/断点恢复、跨用户或跨 APP 协作、改挂父节点、新 Task 状态机、自动写冲突解决,以及全局 Thread 重命名。
验证
直接证据在全新的隔离 PostgreSQL、真实 API、ARQ worker、checkpoint、provisioner 与浏览器中执行。固定模型重放只替代外部模型响应,持久事实、工具产物和页面由独立 oracle 回读;不以 mock 投递或文档构建替代组装链路验证。
| 验收主张 | 失败面 | 语义 Owner | 直接证据 / 命令 | 负向案例 | 当前结果 |
|---|---|---|---|---|---|
| 统一会话、独立上下文、递归创建 | 仍走特殊后端、复制父历史或任意指定路径 | runtime、cooperation 服务 | 工具名称 schema 用例;test_session_cooperation.py 回读真实父节点、名称路径、创建 Run 和拒绝后的持久记录;test_session_cooperation_e2e.py 回读模型请求、Session、Input 和 Run | 旧工具 schema 的路径参数用例失败;测试进程移除服务名称校验后,六种路径输入均以未拒绝失败;子模型请求包含父历史即拒绝;孙代仍计入根容量 | Passed |
| 整树最多四槽,协作等待释放执行 Owner | 超额或等待阻塞子执行 | cooperation repository、runner | 真实 PG 并发领取;E2E 父等待时四名子成员真实执行,第五名排队,同 Turn 建立 resume Run | 第五个领取拒绝;重复恢复不新增 Run | Passed |
| 幂等与消息不创建工作,目标结果不串轮 | 重复创建、无输入唤醒、输出归属错误 | inputs、cooperation、Turn.result_run_id | PG 回读事件和输入数量;HTTP state 恢复;确定性父子 E2E | 跨 resume Run 重放不重复副作用;改变名称/输入/消息或目标拒绝,新 Turn 可复用 call ID;输出不混入父消息;恢复旧的按接收方查询会使三种跨目标重放断言失败 | Passed |
| 用户与 APP 授权、FIFO 与精确取消 | 跨用户/APP、绕过审批、父取消杀子 | scoped repositories、cooperation / turns | PG 与相关真实 HTTP;取消父 Turn 后回读独立子 Turn;兄弟消息/输入/取消;整树停止与继续 | 跨用户/APP 无事件写入(跨树范围见用户范围协作记录);审批等待返回 409;旧停止扫描不能重新暂停已继续的队列;停止意图先落库时模型边界按取消收敛;已释放 Owner 的重试停止后不能以过期 lease 失败收尾或被继续队列复活 | Passed |
| 共享实际沙盒与安全空闲释放 | 独立环境、错误回收、丢失持久文件 | sandbox、leases、workspace | 父子 E2E:子读取父 /tmp 标记;释放后目录文件保留、临时文件消失;PG 活跃树回收负向 | 活跃 Run 存在时不得调用释放器 | Passed |
| 页面观察自动恢复与普通成员入口 | waiting 误断 SSE、恢复后不能 steer、分页缺席清空成员、审批失败不能重试 | SessionWorkspace、useAgentRunStream、chatThreads、CooperationTree | SSE 协议与分页回归;真实浏览器接收追加输入后的父自动恢复输出;验证状态汇总、独立协作列表、标签复用/关闭、独立草稿/滚动、最大化/还原和浅深色窄屏;拦截 503 验证审批与问答重试 | waiting 后追加输入 abort 订阅即失败;首页移除成员后仍须定向加载原会话;审批失败保留已选决定与答案,解除处理中状态 | Passed |
| 状态面板嵌入与悬浮 | 面板压住消息/入口/输入框、悬浮无法关闭、后台面板被误关闭 | statePanelLayout、SessionWorkspace、useOutsidePointerdown | 6 项布局 unit;真实浏览器验证 2200/1280/375px、分栏/最大化、内部/外部点击、入口开合与路由保活 | 测试进程恢复始终悬浮后,两项嵌入用例以模式错误失败 | Passed |
| 路由会话独立保活 | 状态串路由、缓存新建页覆写正式草稿、后台吞未读 | AppLayout、AgentView、SessionWorkspace、thread_draft | 协调器与观察 unit;真实页面验证不同实例、侧栏/子标签/草稿/非零滚动与最大化恢复;延迟历史响应验证后台不标记已读;首 Input 与草稿交接使用浏览器协议 fixture,刷新后回读本地草稿 | 共享实例的浏览器基线暴露串路由;仅移除草稿归属交接后,返回新建页使正式草稿丢失 | Passed |
| 旧能力移除,全新 Schema 初始化 | 新库失败、重复索引、残留管理入口 | schema、API、frontend | 全新 Schema 用例;生产源码负向搜索;Docker unit、frontend unit/build、工程约束与 docs build | 直接写错 tree/runtime 归属由 FK 拒绝;同键重复物理索引 catalog 检查拒绝 | Passed |
验证命令由测试规范拥有。直接相关命令为 pytest test/integration/services/test_session_cooperation.py、pytest test/e2e/test_session_cooperation_e2e.py 和 pytest test/integration/services/test_database_access_paths.py test/integration/services/test_schema_migration_version.py;完整执行范围与数量记录于 PR。确定性协作 E2E 整套通过。真实 DeepSeek 模型的本地浏览器验收覆盖根会话派发 writer、等待后自动恢复、子会话继承模型并写入共享文件,结果通过数据库、工具调用归属与文件预览回读验证。局域网普通 HTTP 登录、成员入口及整树停止/继续也已验证。该单次演示不替代模型自主协作质量评估。
旧能力不存在:生产装配、配置 schema、管理界面和文档入口不再包含 is_subagent、SubAgentBackend、专用工具/白名单和子智能体目录,不增加旧库升级或特殊身份兼容路径。重新引入条件:存在统一会话配置与授权无法表达的真实产品需求,并另立决策论证。
后果
- 共享沙盒允许成员互相影响文件、进程和端口;这是已接受的协作边界,不能作为互不信任执行的隔离方案。
- 树内任意成员可追加输入或取消工作,可能发生相互取消或持续派工;操作记录发起者、明确目标,并可由用户停止整树。4 个并发名额只限制同时执行,不限制累计成本。
- 等待唤醒、并发领取、取消与沙盒回收跨事务和 worker,必须验证崩溃与竞态。内部等待持久化不得演变成另一套公开暂停状态机。
- Schema 遵循 fresh-only;事件消费者和审批恢复路径仍须同步更新。消息消费位置随 checkpoint 保存,空闲回收初始采用五分钟。