Files
deepseek-harness/packages/workflow/workflow
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
..

@deepseek-ai/dsh-workflow

The workflow seam (ctx.workflows): an abstract service defining WHAT a workflow engine does — execute a model-written orchestration script that fans out subagents — without saying HOW. The bash-shaped third of the workflow family: implementations subclass WorkflowService and register as the workflows service (one per context); dsh-workflow-vm is the first, and dsh-tool-workflow is the model-facing consumer.

Service: WorkflowService (abstract)

start(request: WorkflowStartRequest): WorkflowRun — parse and execute a script. Throws synchronously (SCRIPT_PARSE/META_INVALID) for a script that cannot begin; once a run is returned, its result NEVER rejects — every failure resolves with stopReason: 'error' (or 'cancelled'). dispose() must reach quiescence within a bounded grace (cancel → wait → abandon), never hanging its caller.

The protected emitWorkflowEvent helper dispatches the workflow/* events with PER-LISTENER containment (a throwing subscriber is logged, never propagated, and cannot starve later listeners) — the same guarantee as the subagent seam's lifecycle emits.

Vocabulary

  • WorkflowStartRequest{ script, args?, parent: Agent, signal? }. parent is REQUIRED: every child the script spawns is attributed to it. args must be plain host-realm JSON data.
  • WorkflowMeta / WorkflowPhase — the script's validated export const meta block (Claude Code format: required name/description, optional whenToUse/phases).
  • WorkflowRun{ id, meta, result, cancel(reason?), dispose() }; the consumer awaits result and MUST dispose on every path.
  • WorkflowResult{ value, stopReason: 'completed'|'cancelled'|'error', error?, agentsStarted }; value is the script's materialized return (plain JSON data; null for no return).
  • WorkflowErrorHarnessError with a WorkflowErrorCode and a fatal flag driving the combinator discipline: a fatal error (bad hook arguments, unsupported options/schemas, tripped caps, seam start failures, cancellation) always propagates through parallel()/pipeline() instead of dissolving into a per-item null. isFatalWorkflowError(error) is the catch-site predicate.

Events

All observe-only emits carrying DATA SNAPSHOTS (WorkflowRunInfo = id + meta) — never the live WorkflowRun, so a listener cannot gain cancel/dispose; control stays with the start() caller:

  • workflow/start(info) / workflow/end(info, resultInfo) — run lifecycle; resultInfo deliberately omits the value.
  • workflow/phase(info, title) / workflow/log(info, message) — script narration.
  • workflow/agent-start(info, agent) / workflow/agent-end(info, agent + outcome) — one pair per agent() call, correlated by seq.

Non-goals (this cut)

Background collection, journaling/resume, saved workflows, nested workflow(), token budgets — see the RFC's deferred section.