DSH / Atlas
2026-08-10implementedfeature

Durable workflow runs in Chat

Chat 中的持久工作流运行

The ordinary workflow tool row owns the model call and final tool result, but those two records do not explain which members actually started, how they were grouped, whether each member completed, failed, or was cancelled, or what remained unfinished when a process stopped. Live `workflow/*` events expose those facts only inside the current process, so a refresh or later Session open loses the run history. The Web Cl

English

Problem

The ordinary workflow tool row owns the model call and final tool result, but those two records do not explain which members actually started, how they were grouped, whether each member completed, failed, or was cancelled, or what remained unfinished when a process stopped. Live workflow/* events expose those facts only inside the current process, so a refresh or later Session open loses the run history.

The Web Client already assembles business-owned Conversation Nodes from durable Session events. Workflow history therefore needs a producer that can correlate one accepted run with its calling Session, a minimal durable protocol that remains meaningful as a prefix, and an independent renderer that does not take ownership away from the existing tool card.

Decision

dsh-tool-workflow projects every top-level accepted run into the calling Agent's Session. tool-workflow/run-start records the stable runId and validated name; matching workflow member events record the member sequence, exact label, optional exact phase, child Session id, and outcome; tool-workflow/run-end records the stop reason only after the result exists and run.dispose() has reached quiescence. Nested transport executions run normally but write no workflow record because they do not own an independent Chat row.

Recording is observational. The first failed Session append disables all later writes for that run, logs one warning, and never changes cancellation, result mapping, or disposal. Each possible failure leaves either no record or a legal continuous prefix: a started run may lack later members or its ending, and a started member may lack its ending. The package invariant rejects duplicate run starts, invalid or reused positive member sequences, unpaired or repeated member endings, a run ending while members remain open, and every update after a run ending on both cold load and live append.

The workflow package exposes browser-safe run and observation vocabulary through @deepseek-ai/dsh-workflow/types; live Agent requests and control handles remain Host-only. @deepseek-ai/dsh-tool-workflow/types owns the four Session events. Client code imports only these type faces, so the Host and Client TypeScript programs share the durable contract without merging Host Cordis context.

ui-workflow-run registers one workflow-run Conversation Definition and one keyed Chat renderer. Every event independently yields the same runId; run-start initializes State, later events update it in log order, and an update-only history tail remains pending until prepend supplies the unique start. The final node keeps the engine-owned key and anchors at run-start, placing it after the original tool call while preserving one React parent from running through terminal state.

The renderer gives each level a distinct visual responsibility. The run uses a 32-pixel module-platform background row with persistent right/down chevrons and an inline state dot plus status text, without a badge. Phases use 32-pixel disclosure rows with title and member count in the flexible main area and a fixed precise aggregate-status tail, without another dot. Members use a 16-pixel dot slot, a truncating name area, and a fixed 64-pixel status column. Phases exist only when a member actually starts and group by the exact phase string; an omitted phase and the empty string retain distinct identities and localized names. Member settlement changes status without removing or reordering the member. A closed Turn or Step turns missing run or member endings into interrupted presentation; a durable ending remains authoritative when present. Status-driven workflow disclosure owns which run and phase content remains visible as those facts change.

Navigation is derived from two current authorities rather than persisted. A member row is interactive only while its durable member state is running and the current ordinary Session list contains the same id with origin: 'subagent', parentId equal to the displayed parent, and running: true. Underlined member text is the only visible affordance; keyboard focus draws a two-pixel business-primary ring around the name area, and the fixed status label remains the lifecycle word rather than an action instruction. The renderer invokes only the injected ordinary sessions.open(id) callback. Addressed-only, remote, wrong-parent, and terminal members remain visible but static.

The seven-state Figma reference fixes the information hierarchy for running expanded/collapsed, completed history/expanded, failed plus cancelled, interrupted recovery, and dark narrow presentation. Repository DisclosureRow, StateDot, icons, semantic tokens, and keyed-node behavior remain the implementation authority; the reference introduces no runtime field or state owner.

Verification

Package tests cover top-level and nested eligibility, zero-member and concurrent runs, disposal-before-ending order, all four append-failure prefixes, and cold/live invariant rejection. Conversation tests compare complete replace, update-only prepend, and live append; they cover exact phase identity, terminal and interrupted status, disclosure state, list-fact navigation, and HMR removal and re-registration. The shipped Web replay uses the existing workflow parent and child model fixtures to exercise the real worker, spawn provider, Session persistence, browser bundle, running child navigation, terminal retention, original tool-row coexistence, narrow dark tokens, and refresh reconstruction.

Alternatives considered

Append workflow content inside the existing tool card. Rejected because ui-tool and the tool definition own that row's presentation and interaction. A workflow-specific appendix would couple two independently keyed business lifecycles and revive the removed post-tool attachment model.

Persist a server-side projection or add a workflow wire channel. Rejected because Session events already provide persistence, live delivery, pagination, and gap repair. Another service, cache, or transport would duplicate the same facts and create a second lifecycle owner.

Render declared phases or infer a static workflow graph from script text. Rejected because only member-start events prove work happened. meta.phases, phase() narration, branches, and script syntax do not describe one authoritative runtime topology.

Keep terminal child navigation. Rejected because the workflow record proves historical identity, not current accessibility. Cold or remote Session opening needs a separate catalog and authorization contract; this node grants no such promise.

Consequences

Workflow progress survives refresh and process recovery in the same log as its parent conversation, while execution ownership remains with the workflow run holder and the original tool card remains unchanged. The durable protocol adds four small events and one package-owned invariant; first-write failure intentionally sacrifices later observation rather than workflow correctness. Browser State is derived per loaded window, the status-driven disclosure lifecycle keeps review choices local, and navigation can disappear as list facts change. The design shows only actual runtime members and statuses, giving up static graph visualization, outputs, logs, controls, and terminal-member opening.

中文

问题

普通工作流工具行拥有模型调用与最终工具结果,但这两条记录无法说明哪些成员真正开始、如何分组、各成员是完成、失败还是取消,也无法说明进程停止时哪些工作尚未结束。实时 workflow/* 事件只存在于当前进程,因此刷新或稍后重新打开 Session 会丢失运行历史。

Web Client 已经能够从持久 Session 事件组装由业务拥有的 Conversation Node。工作流历史因此需要:能够把一次已接受运行关联到调用 Session 的生产方、作为前缀也始终有意义的最小持久协议,以及不夺走现有工具卡所有权的独立 renderer。

决策

dsh-tool-workflow 把每个已接受的顶层运行投影到调用 Agent 的 Session。tool-workflow/run-start 记录稳定 runId 与已校验名称;匹配的工作流成员事件记录成员序号、精确标签、可选精确阶段、子 Session id 与结果;只有在结果已取得且 run.dispose() 完全停稳后,tool-workflow/run-end 才记录停止原因。嵌套 transport 执行照常运行,但不会写工作流记录,因为它不拥有独立 Chat 行。

记录只供观察。任一次 Session append 首次失败后,本运行会停止所有后续写入、只记录一次告警,并且绝不改变取消、结果映射或 dispose。每种失败位置都留下空记录或合法连续前缀:已开始运行可以缺少后续成员或运行终点,已开始成员也可以缺少成员终点。包 invariant 会在冷加载与实时 append 时拒绝重复运行 start、无效或复用的正成员序号、无配对或重复成员 end、仍有开放成员时结束运行,以及运行结束后的任何更新。

workflow 包通过 @deepseek-ai/dsh-workflow/types 提供浏览器安全的运行与观察词汇;包含活跃 Agent 的请求和控制句柄继续只属于 Host。@deepseek-ai/dsh-tool-workflow/types 拥有四类 Session 事件。Client 只导入这些类型 face,因此 Host 与 Client TypeScript 程序共享持久合同,而不会合并 Host Cordis Context。

ui-workflow-run 注册一个 workflow-run Conversation Definition 和一个 keyed Chat renderer。每条事件都能独立给出同一 runId;run-start 初始化 State,后续事件按日志顺序更新;只有 update 的历史尾页会保持 pending,直到 prepend 补入唯一 start。最终节点保留引擎拥有的 key,并以 run-start 锚定在原工具调用之后,从运行中到终态始终保留同一个 React 父级。

renderer 为每一层分配不同视觉职责。运行使用 32 像素 module-platform 背景行,常驻向右/向下 chevron,并以内联状态点加状态文字表达结局,不使用胶囊。阶段使用 32 像素 disclosure 行,在可伸缩主区显示标题与成员数,在固定尾部精确显示聚合状态且不重复状态点。成员使用 16 像素状态点槽、可省略名称区和固定 64 像素状态列。阶段只在成员真正开始时出现,并按精确阶段字符串分组;字段缺省与空字符串保留不同身份和本地化名称。成员结算只改变状态,不删除或重排成员。所属 Turn 或 Step 关闭时,缺少运行或成员终点会显示为已中断;存在持久终点时仍以它为权威。状态驱动的工作流 disclosure拥有这些事实变化时运行与阶段内容的可见性。

导航从两个当前权威派生,不写入持久记录。只有持久成员状态仍为运行中,且当前普通 Session 列表包含同一 id、origin: 'subagent'parentId 等于当前父 Session、running: true 时,成员行才可交互。带下划线的成员文字是唯一可见提示;键盘聚焦时,名称区显示 2 像素 business-primary 焦点环,固定状态列继续只表达生命周期,而不写动作说明。renderer 只调用注入的普通 sessions.open(id) 回调。仅地址化、远程、父级不符或终态成员继续可见,但保持静态。

七状态 Figma 参考固定运行展开/收起、完成历史/展开、失败与取消、恢复后中断以及暗色窄列的信息层级。仓库的 DisclosureRowStateDot、图标、语义 token 和 keyed-node 行为仍是实现权威;参考稿不引入运行时字段或状态 owner。

验证

包测试覆盖顶层与嵌套准入、零成员与并发运行、先 dispose 后写终点的顺序、四个 append 失败前缀,以及冷/实时 invariant 拒绝。Conversation 测试比较完整 replace、只有 update 的 prepend 和实时 append,并覆盖精确阶段身份、终态与中断状态、disclosure 状态、列表事实导航、HMR 移除与重新注册。shipped Web replay 复用现有工作流父/子模型 fixture,驱动真实 worker、spawn provider、Session 持久化、浏览器 bundle、运行中子级导航、终态保留、原工具行并存、暗色窄列 token 与刷新重建。

曾考虑的替代方案

把工作流内容附加到现有工具卡。 拒绝,因为 ui-tool 与工具定义拥有该行的展示和交互。工作流专属 appendix 会耦合两个独立 keyed 业务生命周期,并恢复已移除的工具后附加模型。

持久化服务端 projection 或新增 workflow wire 通道。 拒绝,因为 Session 事件已经提供持久化、实时传输、分页和 gap repair。另一个 service、cache 或 transport 会复制同一事实并建立第二个生命周期 owner。

展示声明阶段,或从脚本文本推断静态工作流图。 拒绝,因为只有成员 start 事件能证明工作真正发生。meta.phasesphase() 叙述、分支和脚本语法都不是一次运行的权威拓扑。

保留终态子级导航。 拒绝,因为工作流记录证明历史身份,不证明当前可访问性。冷 Session 或远程 Session 的打开需要独立目录与授权合同;本节点不作这种承诺。

后果

工作流进度与父对话保存在同一日志中,能跨刷新与进程恢复;执行所有权仍属于工作流 run holder,原工具卡保持不变。持久协议增加四类小事件和一个包所有的 invariant;首次写入失败会刻意牺牲后续观察,而不是牺牲工作流正确性。浏览器 State 按已加载窗口派生,状态驱动的 disclosure 生命周期把复盘选择留在本地,导航会随列表事实消失。设计只展示真实运行成员与状态,并放弃静态图、输出、日志、控制操作和终态成员打开。