Files
deepseek-harness/docs/core-data-structures/workflow.md
T
Tianyi Cui 1d43ea3cd5 workflow: dynamic workflows — script-driven multi-agent orchestration
A new capability family at packages/workflow/ in the bash seam shape,
modeled on Claude Code's dynamic workflows: the model writes a JavaScript
orchestration script (export const meta = {...} + plain-JS body), a runtime
executes it, and the script — not the conversation — holds the loop, the
branching, and the intermediate results.

- dsh-workflow (ctx.workflows): abstract WorkflowService + run vocabulary
  (WorkflowRun whose result NEVER rejects) + observe-only workflow/* events
  carrying data snapshots (id + meta, never the live run), per-listener
  contained like subagent/*.
- dsh-workflow-vm: in-process node:vm engine. Meta extraction via a
  string/comment-aware scanner (template interpolation rejected; literal
  evaluated alone in an empty timed context; statement blanked line-
  preservingly so stacks keep script line numbers). Hooks: agent(prompt,
  {label, phase, schema, model}) over ctx.subagents, parallel(), pipeline()
  (no cross-stage barrier), phase(), log(), args. Fatal-vs-null discipline:
  hook misuse (unknown/deferred options, bad arguments, unsupported
  schemas, tripped caps, seam start failures, cancellation) throws fatal
  WorkflowErrors the combinators RE-THROW — never dissolved into the
  per-item null reserved for child failures. Realm boundary: inbound values
  materialized by descriptor walks that never invoke accessors (defineProperty
  copies, __proto__-safe); outbound values rebuilt in-realm via the
  context's own JSON.parse. Determinism bans (Date.now/Math.random/argless
  new Date) kept so future resume support cannot break scripts. Caps and
  timeouts are validated Config. Every hook promise carries a no-op
  rejection consumer (app-boot exits on unhandled rejections).
- dsh-tool-workflow: the model-facing workflow tool, synchronous like
  dsh-tool-subagent (start → await → try/finally dispose; abort bridged;
  non-completed → isError). Generic render card titled by a textual
  meta.name sniff. The tool description carries the authoring contract.

Wired into examples/{coding-agent,acp-agent} with explicit-ask-only
guidance. Coverage at every tier: unit (meta scanner, materializer incl.
counting-getter and __proto__ regressions, combinator semantics,
concurrency ceiling, caps, cancellation, no-unhandled-rejection abandon),
integration over the real spawn stack, with-key e2e (real two-phase run +
the tool through the registry pipeline), and a recorded ACP snapshot
scenario (workflow-run, 1 child session). RFC:
docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md (deferred
work explicitly listed). AGENTS.md budget 1575 → 1590 for the new group's
layout line.
2026-07-05 13:29:35 +08:00

4.7 KiB

Workflow

The workflow seam — an agent running a model-written orchestration SCRIPT that fans out subagents. Like subagent it is one optional capability, not part of the agent-loop spine, so its vocabulary lives here rather than in core.md. Unlike the subagent registry it takes the bash shape: ONE engine implementation per context provides ctx.workflows; there is no named-provider registry (a second engine is a plugin swap, not a co-resident).

Interface: dsh-workflow (ctx.workflows + the vocabulary below). The implementation is dsh-workflow-vm (an in-process node:vm engine); the model-facing consumer is dsh-tool-workflow. The proposal and rationale: the dynamic-workflows RFC.

Source: packages/workflow/workflow/src/types.ts

The start request

What a caller asks for when starting a run. The tool layer builds this from the model's { script, args } plus the calling agent; the engine validates the script's meta block BEFORE the body runs. parent is REQUIRED — every child the script spawns is attributed to it (cwd, lineage, and depth flow through the subagent seam). args must be plain host-realm JSON data; the engine exposes it to the script as the args global.

interface WorkflowStartRequest {
  script: string
  args?: unknown
  parent: Agent
  signal?: AbortSignal
}

The script's identity: WorkflowMeta

The validated export const meta block (Claude Code dynamic-workflows format — a PURE object literal heading the script). phases is progress vocabulary only: phase() calls match titles for observers; no execution structure is implied.

interface WorkflowMeta {
  name: string
  description: string
  whenToUse?: string
  phases?: WorkflowPhase[]
}

The terminal result: WorkflowResult

The outcome of one run, resolved by WorkflowRun.result. value is the script's materialized return value — plain host-realm JSON data (null when the script returned nothing) — meaningful only for completed. stopReason is a CLOSED union (engine-owned; consumers may exhaust it): completed | cancelled | error. A non-completed reason carries the failure in error, and the consumer maps it to an isError tool result rather than reporting partial output as success.

interface WorkflowResult {
  value: unknown
  stopReason: WorkflowStopReason
  error?: string
  agentsStarted: number
}

A live run: WorkflowRun

The handle the consumer holds while a script executes. The consumer awaits result, may cancel mid-flight, and MUST dispose on every path. result does NOT reject — a script failure resolves with stopReason: 'error' — so the consumer maps a non-completed reason to an isError result. dispose() cancels, waits a bounded grace for the script to settle, then abandons it (the engine documents the abandonment semantics); it never hangs on a stuck script.

interface WorkflowRun {
  readonly id: WorkflowRunId
  readonly meta: WorkflowMeta
  readonly result: Promise<WorkflowResult>
  cancel(reason?: string): void
  dispose(): Promise<void>
}

Failure discipline: WorkflowError.fatal

Hook misuse inside a script — bad arguments, unknown/deferred agent() options, a schema outside the structured-output subset, a tripped cap, a seam start failure, cancellation — throws a WorkflowError with fatal: true. The parallel()/pipeline() combinators RE-THROW fatal errors instead of mapping the item to null: a typo'd option must kill the script loudly, never dissolve into something that reads as an ordinary child failure. The per-item null is reserved for child-run failures (a non-completed stop reason) and ordinary in-stage script errors.

Events

The workflow/* events (workflow/start, workflow/phase, workflow/log, workflow/agent-start, workflow/agent-end, workflow/end — see the events catalog) are observe-only emits carrying DATA SNAPSHOTS: every payload starts with WorkflowRunInfo (id + meta), never the live WorkflowRun, so a subscriber cannot gain cancel/dispose, and workflow/end deliberately omits the result value (a listener observing outcomes must not receive a mutable alias of the caller's result). Every emit is per-listener contained — a throwing subscriber is logged, never propagated, and cannot starve the listeners registered after it — mirroring subagent/start/subagent/end.