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.
This commit is contained in:
Tianyi Cui
2026-07-05 13:29:35 +08:00
parent dafb81be7b
commit 1d43ea3cd5
52 changed files with 4459 additions and 109 deletions
+62
View File
@@ -313,6 +313,68 @@ Types: [ToolExecution](../core-data-structures/tools.md)
Source: [`packages/core/tools/src/index.ts:76`](../../packages/core/tools/src/index.ts)
## `workflow/*`
### `workflow/agent-end` — emit
One `agent()` call settled (clean result, child failure, or run cancellation). Paired with Events['workflow/agent-start'].
```ts cordis-catalog
'workflow/agent-end'(info: WorkflowRunInfo, agent: WorkflowAgentEndInfo): void
```
Source: [`packages/workflow/workflow/src/index.ts:91`](../../packages/workflow/workflow/src/index.ts)
### `workflow/agent-start` — emit
One `agent()` call started a child run. Paired with Events['workflow/agent-end'] by `agent.seq`.
```ts cordis-catalog
'workflow/agent-start'(info: WorkflowRunInfo, agent: WorkflowAgentInfo): void
```
Source: [`packages/workflow/workflow/src/index.ts:83`](../../packages/workflow/workflow/src/index.ts)
### `workflow/end` — emit
A workflow run settled (any stop reason). Fired when WorkflowRun.result resolves. Paired with Events['workflow/start'].
```ts cordis-catalog
'workflow/end'(info: WorkflowRunInfo, result: WorkflowResultInfo): void
```
Source: [`packages/workflow/workflow/src/index.ts:101`](../../packages/workflow/workflow/src/index.ts)
### `workflow/log` — emit
The script emitted a narration line (a `log(message)` call).
```ts cordis-catalog
'workflow/log'(info: WorkflowRunInfo, message: string): void
```
Source: [`packages/workflow/workflow/src/index.ts:75`](../../packages/workflow/workflow/src/index.ts)
### `workflow/phase` — emit
The script entered a phase (a `phase(title)` call) — progress grouping for observers; no execution semantics.
```ts cordis-catalog
'workflow/phase'(info: WorkflowRunInfo, title: string): void
```
Source: [`packages/workflow/workflow/src/index.ts:68`](../../packages/workflow/workflow/src/index.ts)
### `workflow/start` — emit
A workflow run started — the script's meta block validated, the body about to execute. Paired with Events['workflow/end'].
```ts cordis-catalog
'workflow/start'(info: WorkflowRunInfo): void
```
Source: [`packages/workflow/workflow/src/index.ts:60`](../../packages/workflow/workflow/src/index.ts)
## Inherited events (cordis core + loader/hmr/timer)
The framework events every plugin also sees, beyond the harness vocabulary above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the page is a complete picture of the event bus, without elevating framework internals to the harness tier's prominence.
+16
View File
@@ -227,6 +227,22 @@ async fetch(request: WebFetchRequest, exec?: WebExecContext): Promise<WebFetchRe
Source: [`packages/web/web/src/index.ts:87`](../../packages/web/web/src/index.ts)
## `ctx.workflows` — `WorkflowService` (abstract seam)
Abstract workflow execution service. Subclass, implement start, and load the subclass as a plugin — it registers as `ctx.workflows` (one implementation per context; loading a second throws, cordis' standard duplicate-service behavior).
Semantics every implementation must honor:
- start throws synchronously for a request that cannot begin (an unparseable script, an invalid meta block). Once it returns a WorkflowRun, `result` NEVER rejects — every failure resolves with `stopReason: 'error'` (or `'cancelled'`).
- The `workflow/*` events fire through emitWorkflowEvent (data snapshots, per-listener containment); `workflow/end` fires exactly once per started run, after `result` is settled or as it settles.
- `dispose()` reaches quiescence within a bounded grace: it cancels, waits for the script to settle, and abandons a stuck script rather than hanging its caller (the engine documents what abandonment leaves behind).
```ts cordis-catalog
abstract start(request: WorkflowStartRequest): WorkflowRun
```
Source: [`packages/workflow/workflow/src/index.ts:188`](../../packages/workflow/workflow/src/index.ts)
## Inherited `ctx` members (cordis core + loader/hmr/timer)
The framework `ctx` surface every plugin also sees, beyond the harness services above. This is pinned vendor source ([vendoring policy](../../vendor/README.md)); it is summarized here so the page is a complete picture of what `ctx` offers, without elevating framework internals to the harness tier's prominence.