Files
deepseek-harness/packages/workflow/tool-workflow
imccyu b5f618bcfb workflow: swap the engine's internals to node:worker_threads
In-place port of dsh-workflow-vm from the in-process node:vm execution
to one worker thread per run (the workflow-workerthread engine of
PR #215, adopted as THE engine): the script's vm context moves inside
the worker, agent() bridges to ctx.subagents over the message port
(host.ts/protocol.ts/session.ts/worker.ts are new; runtime.ts loses the
abandon channel — the host's grace timer force-settles and TERMINATES
instead), start() pre-parses the body host-side to keep the seam's
synchronous SCRIPT_PARSE throw, and a ready→go handshake keeps a run
cancelled before start from ever executing the body. start() no longer
blocks the host, termination is real, and the value boundary is
serialization by construction. The package keeps its name until the
follow-up rename commit; scripts see the identical hook surface, and
the seam-contract tests hardened ahead of this swap pass unchanged.

The run and child-RPC surfaces are class-shaped rather than literal
bundles: WorkerRun IMPLEMENTS the seam's WorkflowRun (id/meta are its
own clone, separate from event payloads') and start() returns the
instance directly — interface parity with the seam is compiler-checked;
worker-side, ChildRpcBridge (implements ChildPort; callId allocation +
pending book-keeping settled by onChild* entry points) and
RpcChildHandle (every member an RPC keyed by its callId) carry names in
stacks. ChildPort's method is startAgent — it names what it starts,
matching the script-side agent() hook and the agentsStarted /
workflow/agent-* vocabulary; the Child* type names deliberately stay
(the worker side is cordis- and subagent-free; these are reduced JSON
projections, not the seam's types).

Review findings from the reference PR are folded in rather than
re-introduced:
- cancel() drives BOTH child-cancel channels host-side: the request
  signal aborts AND each registered child's explicit cancel() is
  called — a worker wedged in a synchronous spin cannot relay its own
  ChildCancel RPCs (regression: cancel-only provider + wedged worker).
- All host warn paths render through the total renderThrown; a child
  dispose() rejecting a value whose coercion throws still acks
  ChildDisposed instead of wedging the script's finally (regression).
- built-worker.e2e.ts is wired into builtBinSmokeGate and the AGENTS.md
  CI sequence — the built lib/worker.js resolution contract now runs in
  an automated gate.
- workflow/end payload pinned on the worker-death path (with the
  cancelled and grace-force-settle pins riding the ported spec).
- Real-Worker scripted timing budgets widened (50-300ms → 150-1000ms)
  for starved CI hosts.

Workspace plumbing: the "./worker" subpath export sanctions the second
runtime bundle (check-workspace-constraints), tsdown builds two
single-entry passes, tsx becomes a devDependency for the unbuilt worker
spawn.
2026-07-09 19:31:14 +08:00
..

@deepseek-ai/dsh-tool-workflow

The model-facing workflow tool: run a JavaScript orchestration script that fans out subagents, and return the script's final value. Pure schema + lifecycle shaping over ctx.workflows — script parsing, execution, caps, and cancellation live behind the seam, so a hardened engine swaps in without touching what the model sees.

What the model sees

Two parameters: script (required — the full export const meta = {...} + body text; the tool DESCRIPTION carries the complete authoring contract: hooks, semantics, the supported schema subset) and args (optional JSON object exposed to the script as the args global; a bare list is wrapped as a field, a deliberate deviation from Claude Code's any-JSON args so the wire schema stays honest). The plugin also contributes a tool:<toolName> system-prompt section carrying the usage policy — use the tool only on an explicit user ask for a workflow / large orchestration; prefer plain subagent calls for one or two delegations — per the convention that tool guidance ships with the tool plugin, never in the deployment persona.

Lifecycle

Collection is SYNCHRONOUS this cut (like dsh-tool-subagent): execute starts a run and awaits run.result inside a try/finally that always disposes the run, so the script and its children reach quiescence on every path. exec.signal is bridged to run.cancel() (including the already-aborted-before-start case). A non-completed stop reason maps to an isError result reporting the reason — never partial output as success; a parse/meta failure thrown synchronously by start() becomes an isError the model can correct from. The completed result renders the meta name, the agent count, and the return value as JSON, truncated at maxResultChars with an explicit notice.

Render intent

Decided up front (per the render-intent RFC): a generic card titled workflow: <meta.name>, the name sniffed TEXTUALLY from args.script (presentation must be a pure function of args, so it cannot ask the engine to parse); the script text rides as rawInput. The result keeps the generic card.

Config

Key Default Meaning
toolName workflow The model-facing tool name to register.
maxResultChars 50000 Rendered-result ceiling; longer JSON is truncated with a notice.