DSH / Atlas
2026-07-05implementedfeaturearchived 2026-09-30

Dynamic workflows — a script-driven multi-agent orchestration seam

动态工作流——脚本驱动的多 agent 编排 seam

The harness can delegate ONE task to ONE child (`dsh-tool-subagent`), but work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — forces the model to orchestrate turn by turn: every intermediate result lands in the parent context, the plan lives nowhere durable, and coordination costs a model round-trip per step. Claude Co

English

Problem

The harness can delegate ONE task to ONE child (dsh-tool-subagent), but work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — forces the model to orchestrate turn by turn: every intermediate result lands in the parent context, the plan lives nowhere durable, and coordination costs a model round-trip per step. Claude Code ships this capability as dynamic workflows: the model writes a JavaScript orchestration script, a runtime executes it, and the script — not the conversation — holds the loop, the branching, and the intermediate results.

Decision

A workflow capability family at packages/workflow/ in the bash seam shape (Service Definition / Service Provider / Consumer), plus the structured-output foundation it needs on the subagent seam.

The script contract (Claude Code-compatible)

A workflow call contains JSON meta (name, description, and optional whenToUse/phases) and a JavaScript script body with top-level await that returns a JSON value. Metadata is validated as data and never evaluated. The body receives agent(prompt, options), parallel(thunks), pipeline(items, ...stages), phase(title), log(message), and args. Pipeline stages receive (prev, item, index) with no cross-stage barrier; failed children and ordinary stage errors resolve the affected item to null and skip its remaining stages. Claude Code's determinism restrictions are deferred with journaling, so compatible bodies may use clock and randomness after moving their meta header into the parameter.

One deliberate strictness DIVERGENCE from CC: hook misuse — unknown or deferred options (effort/isolation/agentType), malformed arguments, schemas outside the supported subset, tripped caps, seam start failures — throws a WorkflowError with fatal: true, and the combinators RE-THROW fatal errors instead of nulling the item. Without this, a typo'd option dissolves into a null indistinguishable from a child failure — the accepted-then-ignored failure mode this repo bans. One addition: the tool's args parameter is a JSON OBJECT (a bare list is wrapped as a field) so the wire schema stays honest.

The seam (dsh-workflow)

ctx.workflowEngine is an abstract WorkflowEngine in the bash shape — one engine per context, no named-provider registry (engines are deployment swaps, not co-residents). start(request) throws synchronously for a script that cannot begin; a returned WorkflowRun's result NEVER rejects (failures resolve as stopReason: 'error' | 'cancelled'). The workflow/* events are observe-only emits carrying DATA SNAPSHOTS (id + meta; workflow/end omits the result value), per-listener contained, mirroring subagent/start/subagent/end — control stays with the run's holder. Vocabulary details: subsystems/workflow.md.

The engine (dsh-workflow-ptc): shared Node process execution

The workflow sandbox reuse decision supersedes the worker-thread execution and trust realization. The engine retains the VM and helpers inside a sandboxed PTC Node process. The VM defines the script API; OS file policy and managed process cleanup belong to the shared execution provider.

The host validates metadata and parses the body before publication. Host bindings connect the guest to subagents and workflow observers. Pending starts and published child records share a cancellation signal; the agent-scope runtime-design Agent Note owns their lifecycle rules.

Meta is data: the host never evaluates a metadata literal. Values are lossless JSON: guest-side realm materialization rejects unsupported values before PTC transport. Getters run inside the confined process, and hook errors retain their stable name and code fields across realms. Cooperative helper caps and the initial synchronous-slice timeout remain; no overall workflow elapsed timer is added.

The Consumer (dsh-tool-workflow)

A workflow tool mirroring dsh-tool-subagent's synchronous shape: start, await, try/finally dispose, abort-bridge exec.signal, non-completed → isError. Render intent: a generic card titled by the call's meta.name parameter (presentation is a pure function of args). The tool description IS the model-facing authoring spec. The usage policy ships with the tool as its own tool:<toolName> prompt section (explicit-ask-only guidance — tool guidance lives in tool plugins, never in the deployment persona); the harness has no ultracode-style effort gate.

For a top-level tool execution, the same consumer also writes the run and actual member lifecycle into the calling parent Session as four log-only tool-workflow/* events. The recording path observes rather than controls execution: its first append failure disables later writes for that run and leaves a legal prefix without changing the tool result. ui-workflow-run rebuilds those facts through the Conversation Node engine as a separate keyed Chat row; the existing generic tool row remains its own presentation owner. The detailed persistence, replay, disclosure, and live-navigation decision lives in durable workflow runs in Chat.

The foundation: structured output on the subagent seam

SubagentStartRequest.outputSchema is implemented by dsh-subagent-in-process-driver for both in-process backends. Each structured child receives its own scoped capture tool, instruction, and enforcement registrations on child.ctx; concurrent children can use different schemas without sharing mutable policy, and disposing the child removes the entire attachment.

An output schema makes a schema-valid committed capture mandatory for successful child completion. The scoped runtime presents the capture tool and instruction, commits only a successful final outcome—including the enclosing run_code outcome for an SDK call—denies later side effects after capture becomes pending, and stops the child without another model step after commit. A validation failure remains a retryable tool error; clean completion without a committed capture settles as an error.

ObjectJsonSchema is the object-rooted consumer view of the unified enforceable raw JSON Schema subset in dsh-tools; unsupported keywords fail loudly because that wire data becomes the capture tool's parameters verbatim. The unified JSON-value schema Agent Note owns the vocabulary and validation semantics, while the agent-scope runtime-design Agent Note owns the assembly, commit, guard, and terminal-stop algorithms.

Testing

Verification belongs to the workflow helper and host-lifecycle tests, the shared Node PTC confinement tests, and source/built workflow execution through the shipped profile. Recorded workflow and opt-in Ralph scenarios own the assembled model transcript; replay configurations using a passthrough sandbox do not establish OS enforcement.

Deferred (documented non-goals)

  • Background collection (start tool → run id → completion notice → collect), designed alongside shell/subagent background unification.
  • Journaling + resume (resumeFromRunId, cached agent() prefixes) — implementing it reintroduces CC's determinism bans as a script-contract tightening (scripts may read the clock).
  • Saved/bundled workflows (a .deepseek/workflows/ registry, slash-command API) and script persistence to a run directory (the tool-call event already records the script durably).
  • Nested workflow(), token budget, and the effort/isolation/agentType agent options (each rejects loud with a message naming it deferred).
  • An overall run wall-clock timeout — workflow lifetime remains caller-controlled; explicit cancellation stops PTC execution and awaits child cleanup.
  • ACP-backend structured output and toolFilter (both still capability-gated false).

Alternatives considered

  • Host-side defenses for VM values (proxy rejection, descriptor walks and cross-realm clones): these cannot enforce OS file authority. VM evaluation and materialization belong inside the confined process; the shared PTC provider owns validation at the process transport.
  • In-process node:vm execution: mechanically simplest — no RPC, no thread — but start() blocks the caller for the script's initial synchronous slice, a synchronous spin past the first await cannot be killed in-process (the vm timeout covers only that first slice), and dispose() could only abandon an unsettling script on the host loop. The PTC process keeps the vm-context script API while unblocking the host and providing managed termination.
  • isolated-vm: adding a separate JavaScript engine brings native dependency and deployment requirements; the shared PTC provider already supplies process confinement.
  • Background execution as the default (CC's shape): deferred; foreground-synchronous matches dsh-tool-subagent's cut, and background semantics should be designed ONCE across shell/subagent/workflow rather than per-tool.
  • Workflow-layer JSON parsing for agent({schema}): duplicating a seam concern at one consumer while the seam's capability flag stayed dishonestly false.
  • Meta embedded in the script as export const meta = {...} (CC's exact format): keeps scripts self-contained and CC scripts drop-in, but obtaining meta requires evaluating model-written text on the host. Even an empty timed vm context cannot bound script-controlled getters when the host reads the resulting object. A JSON parameter removes the scanner, evaluation, and host-spin hole; the cost is that a CC script's meta header must move into the parameter (the body stays drop-in).
  • ValueSchemaSpec as the outputSchema wire type: the author form now has equivalent vocabulary, but a workflow supplies realm-foreign raw JSON Schema data; pretending that runtime data is a trusted author declaration would skip the raw-schema assertion boundary.
  • A schema-object library (zod, or the repo's schemastery) for the structured-output subset: the schema is wire data — plain JSON that crosses the vm realm boundary in agent({schema}) and lands verbatim in the forced tool's parameters — exactly where live schema objects cannot sit; consuming raw JSON Schema at runtime would need a third-party converter on top (zod core only emits JSON Schema, not the reverse), and it would put a second schema language beside schemastery's config role.
  • ajv for value validation: it validates FULL JSON Schema, so the subset gate — the module's actual point, since every accepted keyword must be one the harness enforces — would remain hand-written regardless; it compiles validators through new Function; and it would be dsh-tools' first runtime dependency, all to replace the ~70-line value walker while the path-qualified, every-violation error reporting stays custom either way.
  • Provider JSON mode instead of the capture tool: it guarantees valid JSON, not schema conformance, and its interaction with tool calling is unclear. The capture tool preserves in-turn validation retries. Provider-side strict tool schemas can later narrow the accepted subset without changing this design.

Consequences

Fan-out plans now live in rerunnable scripts, and outputSchema provides authoritative structured child results. Each run pays PTC process startup and binding RPC costs. Host execution stays non-blocking, cancellation stops the managed process, and JSON serialization separates guest values from the host. Invalid options fail rather than degrading to Claude Code's null; consumers retain control through the run handle while observers receive snapshots only. Top-level Web users also receive a durable, replayable workflow record without widening the execution seam or coupling the original tool card to workflow-specific UI.

中文

问题

harness 可以通过 dsh-tool-subagent 将一个任务委派给一个子 agent(智能体),但需要扇出到多个独立部分的工作——跨多文件审计、迁移、多角度调研、对抗式验证——迫使模型逐轮次编排:每个中间结果都落入父上下文,计划无处持久存储,每一步的协调都要消耗一次模型往返。Claude Code 以动态工作流的形式提供了这一能力:模型编写一段 JavaScript 编排脚本,运行时执行它,由脚本(而非对话)持有循环、分支和中间结果。

决策

在 packages/workflow/ 下以 bash seam 的形态(Service Definition/Service Provider/Consumer)提供一组工作流能力,以及它在 subagent seam 上所需的结构化输出基础。

脚本约定(兼容 Claude Code)

一次工作流调用包含 JSON meta(name、description,以及可选的 whenToUse/phases)和一段支持顶层 await 并返回 JSON 值的 JavaScript script 正文。元数据作为数据校验,从不被执行。正文接收 agent(prompt, options)、parallel(thunks)、pipeline(items, ...stages)、phase(title)、log(message) 和 args。流水线各阶段接收 (prev, item, index),阶段之间无屏障;失败的子 agent 和普通阶段错误将受影响的 item 结算为 null 并跳过其剩余阶段。Claude Code 的确定性限制随日志机制一并延后实现,因此兼容的脚本正文在将 meta 头移入参数后可以使用时钟和随机数。

与 CC 有一处刻意的严格性差异:钩子误用——未知或延迟的选项(effort/isolation/agentType)、格式错误的参数、超出支持子集的 schema、触发上限、seam 启动失败——会抛出带 fatal: true 的 WorkflowError,组合器会重新抛出 fatal 错误而非将 item 置为 null。如果不这样做,一个拼错的选项会悄然变成一个与子 agent 失败无法区分的 null——这正是本仓库禁止的「被接受后被忽略」的失败模式。另有一处新增:工具的 args 参数是一个 JSON 对象(裸列表被包装为一个字段),使协议格式(wire format)保持诚实。

seam(dsh-workflow)

ctx.workflowEngine 是 bash 形态的抽象 WorkflowEngine——每个上下文一个引擎,无命名提供方注册表(引擎是部署级替换,不是共存者)。start(request) 对无法启动的脚本同步抛出;返回的 WorkflowRun 的 result 永不 reject(失败时结算为 stopReason: 'error' | 'cancelled')。workflow/* 事件是仅观察的 emit,携带数据快照(id + meta;workflow/end 省略 result 值),按监听器隔离,与 subagent/start/subagent/end 对称——控制权留在 run 的持有者手中。词汇详情见 subsystems/workflow.md。

引擎(dsh-workflow-ptc):共享 Node 进程执行

工作流沙箱复用决策取代 worker-thread 执行与信任实现。引擎在沙箱化 PTC Node 进程中保留 VM 与辅助函数。VM 定义脚本 API;OS 文件策略和受管进程清理由共享执行提供方负责。

宿主在发布前校验元数据并解析正文。Host 绑定将 guest 连接到 subagent 和工作流观察器。待启动与已发布子记录共享取消信号;agent 作用域运行时设计 Agent Note负责其生命周期规则。

Meta 是数据:Host 从不执行元数据字面量。值是无损 JSON:guest 侧 realm 物化在 PTC 传输前拒绝不支持的值。getter 在受限进程内运行,钩子错误跨 realm 保留稳定的 name 与 code 字段。保留协作式辅助函数上限与最初同步片段超时;不增加整体工作流经过时间定时器。

Consumer(dsh-tool-workflow)

一个 workflow 工具,镜像 dsh-tool-subagent 的同步形态:启动、await、try/finally dispose、abort 桥接 exec.signal、非 completed → isError。渲染意图:一张以调用的 meta.name 参数为标题的 generic 卡片(展示是参数的纯函数)。工具描述即面向模型的编写规范。使用策略以工具自身的 tool:<toolName> 提示词段落随工具发布(显式请求才使用的引导——工具引导存在于工具插件中,从不在部署 persona 中);harness 没有 ultracode 风格的 effort 门控。

对于顶层工具执行,同一消费方还会把运行及实际成员生命周期写入调用方父 Session,形成四类 log-only tool-workflow/* 事件。记录路径只观察、不控制执行:第一次 append 失败会禁用本运行后续写入并留下合法前缀,不改变工具结果。ui-workflow-run 通过 Conversation Node 引擎重建这些事实,形成独立 keyed Chat 行;现有 generic 工具行继续拥有自己的展示。持久化、回放、展开/收起与实时导航的详细决策见 Chat 中的持久工作流运行。

基础:subagent seam 上的结构化输出

SubagentStartRequest.outputSchema 由 dsh-subagent-in-process-driver 为两个进程内后端实现。每个结构化子 agent 在 child.ctx 上获得自己的作用域捕获工具、指令和强制注册;并发子 agent 可以使用不同的 schema 而不共享可变策略,dispose 子 agent 时移除整个附件。

输出 schema 使一次 schema 有效的已提交捕获成为子 agent 成功完成的必要条件。作用域运行时呈现捕获工具和指令,仅提交成功的最终结果(包括 SDK 调用时外层 run_code 的结果),在捕获变为 pending 后拒绝后续副作用,并在提交后不再进行模型步骤即停止子 agent。校验失败仍是可重试的工具错误;没有已提交捕获的正常完成以错误结算。

ObjectJsonSchema 是 dsh-tools 统一且可强制执行的原始 JSON Schema 子集所提供的对象根消费方视图;不支持的关键字会明确报错,因为该协议数据会逐字成为捕获工具的 parameters。统一 JSON 值 schema Agent Note定义词汇与校验语义,agent 作用域运行时设计 Agent Note则定义组装、提交、守卫和终止停止算法。

测试

验证由工作流辅助函数和 Host 生命周期测试、共享 Node PTC 约束测试,以及通过已发布 profile 的源码/构建后工作流执行负责。已记录工作流与显式启用的 Ralph 场景负责组装后的模型转录;使用 passthrough 沙箱的回放配置不能证明 OS 强制能力。

延迟(明确的非目标)

  • 后台收集(启动工具 → run id → 完成通知 → 收集),与 shell/subagent 后台统一一起设计。
  • 日志化 + 恢复(resumeFromRunId、缓存的 agent() 前缀):实现它会以脚本约定收紧的形式重新引入 CC 的确定性禁令(脚本可以读取时钟)。
  • 保存/打包的工作流(.deepseek/workflows/ 注册表、斜杠命令 API)和脚本持久化到运行目录(工具调用事件已经持久记录了脚本)。
  • 嵌套 workflow()、token budget,以及 effort/isolation/agentType agent 选项(每个都会明确拒绝,并在消息中注明其已延迟实现)。
  • 整体运行的挂钟超时:工作流生命周期仍由调用方控制;显式取消停止 PTC 执行并等待子 agent 清理。
  • ACP(Agent Client Protocol)后端结构化输出和 toolFilter(两者仍以能力标志 false 门控)。

曾考虑的替代方案

  • VM 值的 Host 侧防护(代理拒绝、描述符遍历和跨 realm 克隆):这些无法强制 OS 文件权限。VM 求值与物化属于受限进程内部;共享 PTC 提供方负责进程传输处的验证。
  • 进程内 node:vm 执行:机械上最简——无 RPC、无线程——但 start() 会在脚本的初始同步切片期间阻塞调用方,第一个 await 之后的同步自旋无法在进程内终止(vm timeout 仅覆盖第一个切片),且 dispose() 只能在宿主循环上放弃一个未 settle 的脚本。PTC 进程保持 vm 上下文脚本 API,同时解除宿主阻塞并提供受管终止。
  • isolated-vm:引入另一套 JavaScript 引擎会增加原生依赖与部署要求;共享 PTC 提供方已提供进程约束。
  • 后台执行作为默认(CC 的形态):延迟。前台同步与 dsh-tool-subagent 的当前形态一致,后台语义应在 bash、subagent 和工作流之间统一设计一次,而非逐工具设计。
  • 工作流层为 agent({schema}) 做 JSON 解析:在一个消费方重复 seam 关注点,而 seam 的能力标志仍不诚实地为 false。
  • Meta 嵌入脚本中作为 export const meta = {...}(CC 的确切格式):保持脚本自包含且 CC 脚本可直接使用,但获取 meta 需要在宿主上执行模型编写的文本。即使一个空的限时 vm 上下文也无法约束脚本控制的 getter(当宿主读取结果对象时)。JSON 参数消除了扫描器、执行和宿主自旋漏洞;代价是 CC 脚本的 meta 头必须移入参数(正文保持可直接使用)。
  • ValueSchemaSpec 作为 outputSchema 协议类型:面向作者的形式如今具有等价词汇,但工作流提供的是来自其他 realm 的原始 JSON Schema 数据;将这类运行时数据假装成可信的作者声明,会跳过原始 schema 断言边界。
  • schema 对象库(zod 或本仓库的 schemastery)用于结构化输出子集:schema 是协议数据——纯 JSON,跨越 agent({schema}) 中的 vm realm 边界并逐字落入强制工具的 parameters——正是活 schema 对象无法存在的位置;在运行时消费原始 JSON Schema 需要在其上加一个第三方转换器(zod core 只输出 JSON Schema,不能反向),且会在 schemastery 的配置角色旁边放置第二种 schema 语言。
  • ajv 用于值校验:它校验完整 JSON Schema,因此子集门控——模块的真正要点,因为每个被接受的关键字都必须是 harness 强制执行的——无论如何仍需手写;它通过 new Function 编译校验器;且它将成为 dsh-tools 的第一个运行时依赖,仅为替换约 70 行的值遍历器,而带路径且逐一报告所有违规的错误报告无论如何都是自定义的。
  • 提供方 JSON 模式代替捕获工具:它保证 JSON 有效,但不保证其符合 schema,且它与工具调用的交互不明确。捕获工具保留了轮次内的校验重试。提供方侧的严格工具 schema 后续可以在不改变本设计的情况下收窄接受的子集。

后果

扇出计划现在存在于可重运行的脚本中,outputSchema 提供权威的结构化子 agent 结果。每次运行付出 PTC 进程启动与绑定 RPC 成本。Host 执行保持非阻塞,取消停止受管进程,JSON 序列化将 guest 值与 Host 分开。无效选项会失败而非退化为 Claude Code 的 null;消费方通过 run handle 保持控制权,观察者仅接收快照。顶层 Web 用户还会得到持久、可回放的工作流记录,同时不扩宽执行 seam,也不把原工具卡耦合到工作流专属 UI。