Six verified A-findings from the code-stage review, each with a regression test: - parallel()/pipeline() resolved to HOST arrays inside the vm realm, exposing host Array.prototype to scripts; combinator results are now realm-built (in-realm Array.from bound at context setup). - materializeFromRealm ran proxy traps (ownKeys/getOwnPropertyDescriptor/ getPrototypeOf) during the descriptor walk — realm code on the host stack, outside the vm timeout, escaping as raw errors; proxies (root, nested, and in the prototype position) are now rejected trap-free via util.types.isProxy before any inspection. - an already-aborted signal or an immediate cancel() no longer reports 'completed' for a hook-free script: drive() checks cancellation before running the body and again when the script settles. - dispose() now waits (bounded by disposeGraceMs) for stray agent() children to FINISH disposing, not just for the script to settle: every agent() call is tracked and quiesce() drains the in-flight set. - workflow/* event payloads were live mutable aliases shared across emissions; emitWorkflowEvent now hands each listener its own structural clone. - the structured-output turn-continuation veto is now prepend: true, so an earlier-registered force-continue listener cannot short-circuit it. Docs updated in the same change (READMEs, core-data-structures/workflow.md, the dynamic-workflows RFC, regenerated cordis catalogs).
3.3 KiB
@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 for the script to settle and its children to finish disposing → abandon), never hanging its caller.
The protected emitWorkflowEvent helper dispatches the workflow/* events with PER-LISTENER containment and PER-LISTENER payload snapshots (a throwing subscriber is logged, never propagated, and cannot starve later listeners; each subscriber gets its own clone of the payload, so mutating it corrupts neither the engine nor other listeners) — the same containment guarantee as the subagent seam's lifecycle emits.
Vocabulary
WorkflowStartRequest—{ script, args?, parent: Agent, signal? }.parentis REQUIRED: every child the script spawns is attributed to it.argsmust be plain host-realm JSON data.WorkflowMeta/WorkflowPhase— the script's validatedexport const metablock (Claude Code format: requiredname/description, optionalwhenToUse/phases).WorkflowRun—{ id, meta, result, cancel(reason?), dispose() }; the consumer awaitsresultand MUSTdisposeon every path.WorkflowResult—{ value, stopReason: 'completed'|'cancelled'|'error', error?, agentsStarted };valueis the script's materialized return (plain JSON data;nullfor no return).WorkflowError—HarnessErrorwith aWorkflowErrorCodeand afatalflag driving the combinator discipline: a fatal error (bad hook arguments, unsupported options/schemas, tripped caps, seam start failures, cancellation) always propagates throughparallel()/pipeline()instead of dissolving into a per-itemnull.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;resultInfodeliberately 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 peragent()call, correlated byseq.
Non-goals (this cut)
Background collection, journaling/resume, saved workflows, nested workflow(), token budgets — see the RFC's deferred section.