hermes-taskflow 0.3.0-beta.1.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -11,23 +11,23 @@
|
|
|
11
11
|
|
|
12
12
|
**English** · [简体中文](https://github.com/heggria/taskflow/blob/main/README.zh-CN.md)
|
|
13
13
|
|
|
14
|
-
[0
|
|
14
|
+
[1.0 overview](#taskflow-10-declarative-coding-agent-workflows) · [Quickstart](#quickstart) · [Docs](https://heggria.github.io/taskflow/en/docs) · [Examples](https://github.com/heggria/taskflow/blob/main/examples) · [Changelog](https://github.com/heggria/taskflow/blob/main/CHANGELOG.md)
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
# taskflow 0
|
|
20
|
+
# taskflow 1.0: declarative coding-agent workflows
|
|
21
21
|
|
|
22
|
-
**taskflow is a declarative runtime for coding-agent workflows.** It turns a graph into a verifiable execution contract, runs phases in isolation, and keeps intermediate work out of the host conversation. In
|
|
22
|
+
**taskflow is a declarative runtime for coding-agent workflows.** It turns a graph into a verifiable execution contract, runs phases in isolation, and keeps intermediate work out of the host conversation. In Taskflow 1.0, the contract also describes the effects a phase is allowed to propose.
|
|
23
23
|
|
|
24
|
-
> **
|
|
24
|
+
> **Taskflow 1.0.0** includes the DAG runtime, trusted filesystem effects and complete Control Plane (CLI/MCP/WebUI). [GitHub Releases](https://github.com/heggria/taskflow/releases) and npm establish publication; see the [acceptance scoreboard](https://github.com/heggria/taskflow/blob/main/docs/internal/1.0.0-ga-scoreboard.md) for verified scope and limits.
|
|
25
25
|
|
|
26
|
-
## The
|
|
26
|
+
## The declared-effect contract
|
|
27
27
|
|
|
28
28
|
An agent can propose content. It should not become the mutation authority merely because it can run a command.
|
|
29
29
|
|
|
30
|
-
For admitted, declared filesystem-write targets, taskflow 0
|
|
30
|
+
For admitted, declared filesystem-write targets, taskflow 1.0 makes the path explicit and routes the final mutation through the resources transaction:
|
|
31
31
|
|
|
32
32
|
```text
|
|
33
33
|
flow / .tf.ts
|
|
@@ -47,39 +47,40 @@ flow / .tf.ts
|
|
|
47
47
|
|
|
48
48
|
This is **not** an OS sandbox. Resolve-only hosts cannot prevent every write to an undeclared path. Secret and service references are typed and fail closed in this cut; they do not imply a vault or network backend.
|
|
49
49
|
|
|
50
|
-
##
|
|
50
|
+
## The 1.0 target and current implementation
|
|
51
51
|
|
|
52
|
-
| Layer | What it does |
|
|
52
|
+
| Layer | What it does | Support boundary |
|
|
53
53
|
|---|---|---|
|
|
54
|
-
| **Taskflow runtime** | Declarative DAGs, 12 phase types, budgets, retries, approvals, isolation, resume, replay, trace, and recompute |
|
|
55
|
-
| **Trusted Effects** | Closed `EffectIR`, `PathRef` / `SecretRef` / `ServiceRef`, confidentiality/integrity labels, effect validation, overlap checks, and ledger-backed `why-*` explainers |
|
|
56
|
-
| **Resource transaction** | Snapshot → lease → durable intent/permit → stage → commit, or restore and reject |
|
|
54
|
+
| **Taskflow runtime** | Declarative DAGs, 12 phase types, budgets, retries, approvals, isolation, resume, replay, trace, and recompute | Stable runtime contract |
|
|
55
|
+
| **Trusted Effects** | Closed `EffectIR`, `PathRef` / `SecretRef` / `ServiceRef`, confidentiality/integrity labels, effect validation, overlap checks, and ledger-backed `why-*` explainers | Declared-target contract |
|
|
56
|
+
| **Resource transaction** | Snapshot → lease → durable intent/permit → stage → commit, or restore and reject | Declared-target contract |
|
|
57
57
|
| **Host adapters** | Pi, Codex, Claude Code, OpenCode, Grok Build, and Hermes Agent use the same flow contract | Existing host surface; support remains host-specific |
|
|
58
|
-
| **Control Plane** |
|
|
59
|
-
| **WebUI** | Runs,
|
|
58
|
+
| **Control Plane** | Authenticated multi-project admission, global concurrency, durable replay, approvals/CAS, Receipts and operator CLI/MCP | Bundled in public `taskflow-mcp-core`; Unix UDS, Windows pipes non-GA |
|
|
59
|
+
| **WebUI** | Runs, approval edits, Receipts and evidence browsing | Local token login, live authorization and project isolation |
|
|
60
60
|
|
|
61
|
-
The
|
|
61
|
+
The [1.0 release plan](https://github.com/heggria/taskflow/blob/main/docs/internal/1.0.0-release-plan.md) defines the stable scope and acceptance gates. The filesystem-transaction details are in [`docs/internal/0.3.0-trusted-effects-mvp.md`](https://github.com/heggria/taskflow/blob/main/docs/internal/0.3.0-trusted-effects-mvp.md). The 0.3-C Control Plane plan is [`docs/internal/0.3-c-control-plane-plan.md`](https://github.com/heggria/taskflow/blob/main/docs/internal/0.3-c-control-plane-plan.md).
|
|
62
62
|
|
|
63
63
|
## Quickstart
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
Use Node.js **≥ 22.19.0**. To run from the 1.0 source checkout:
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
git clone https://github.com/heggria/taskflow.git
|
|
69
69
|
cd taskflow
|
|
70
|
-
git checkout
|
|
70
|
+
git checkout v1.0.0
|
|
71
71
|
pnpm install
|
|
72
72
|
pnpm run typecheck
|
|
73
73
|
pnpm test
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
Install the matching 1.0.0 package set after confirming the release is available:
|
|
77
|
+
|
|
77
78
|
```bash
|
|
78
|
-
npm install --global pi-taskflow@
|
|
79
|
-
npm install --global codex-taskflow@
|
|
79
|
+
npm install --global pi-taskflow@1.0.0
|
|
80
|
+
npm install --global codex-taskflow@1.0.0
|
|
80
81
|
```
|
|
81
82
|
|
|
82
|
-
The host-specific plugin and MCP commands remain in the [host guides](https://heggria.github.io/taskflow/en/docs/guides/).
|
|
83
|
+
The host-specific plugin and MCP commands remain in the [host guides](https://heggria.github.io/taskflow/en/docs/guides/).
|
|
83
84
|
|
|
84
85
|
Run the no-LLM Trusted Effects vertical-slice fixture:
|
|
85
86
|
|
|
@@ -88,7 +89,15 @@ pnpm exec node --conditions=development --experimental-strip-types --test \
|
|
|
88
89
|
packages/taskflow-core/test/effects-e2e-fixture.test.ts
|
|
89
90
|
```
|
|
90
91
|
|
|
91
|
-
This exercises the checked-in `examples/trusted-effects-write.json` path without a live LLM. For an interactive run, use the host guide for the adapter you already run. The
|
|
92
|
+
This exercises the checked-in `examples/trusted-effects-write.json` path without a live LLM. For an interactive run, use the host guide for the adapter you already run. The [release guide](https://github.com/heggria/taskflow/blob/main/RELEASE.md) covers packed-consumer validation and release gates.
|
|
93
|
+
|
|
94
|
+
### Local Control Console
|
|
95
|
+
|
|
96
|
+
In Pi, run `/tf web` to start the bundled local Control Console and open the default browser. If browser opening fails, the command shows a manual loopback URL. Use the one-use token in the reported private (0600) handoff file to log in; tokens are never included in the URL or command notifications.
|
|
97
|
+
|
|
98
|
+
The console shows Control Plane runs, approvals, Receipts and existing evidence. Legacy Pi run transcripts are outside this view. Repeating the command in the same project reuses its service; changing projects or leaving the Pi session stops only the console process started by that session. Unix local transport is required. If another host already owns the Control home, stop that owner explicitly before launching this console.
|
|
99
|
+
|
|
100
|
+
See [Pi compatibility](https://github.com/heggria/taskflow/blob/main/docs/pi-compatibility.md) for the tested SDK matrix and the distinction between real-process fixtures and live-provider acceptance.
|
|
92
101
|
|
|
93
102
|
## Declare an effect
|
|
94
103
|
|
|
@@ -191,7 +200,7 @@ Host support is not a blanket security guarantee. Read the [host support baselin
|
|
|
191
200
|
- Writes to undeclared paths remain host-policy dependent under resolve-only execution.
|
|
192
201
|
- `SecretRef` and `ServiceRef` are typed handles only; no vault or live service adapter ships in this cut.
|
|
193
202
|
- There is no FileBroker or full OS sandbox claim in 0.3 MVP.
|
|
194
|
-
- Control Plane
|
|
203
|
+
- Control Plane admission, authorized replay, durable approvals, Receipts, global coordination and WebUI are implemented. Public `taskflow-mcp-core` provides the control binaries; operator actions require an explicitly provisioned separate credential. Request audit fields never grant authority.
|
|
195
204
|
|
|
196
205
|
## Development
|
|
197
206
|
|
|
@@ -210,13 +219,13 @@ The monorepo contains the host-neutral `taskflow-core`, Trusted Effects and reso
|
|
|
210
219
|
|
|
211
220
|
| Start here | Use it for |
|
|
212
221
|
|---|---|
|
|
213
|
-
| [0
|
|
222
|
+
| [1.0 overview](https://heggria.github.io/taskflow/en/docs) | 1.0 scope, support, and the security boundary |
|
|
214
223
|
| [Getting Started](https://heggria.github.io/taskflow/en/docs/getting-started) | First flow and host setup |
|
|
215
224
|
| [Core Concepts](https://heggria.github.io/taskflow/en/docs/concepts/) | DAGs, isolation, verification, resume, and evidence |
|
|
216
225
|
| [Compiler & Runtime](https://heggria.github.io/taskflow/en/docs/compiler-runtime/) | JSON, TypeScript DSL, FlowIR, replay, and recompute |
|
|
217
226
|
| [Host Guides](https://heggria.github.io/taskflow/en/docs/guides/) | Pi, Codex, Claude Code, OpenCode, Grok, and Hermes |
|
|
218
227
|
| [Examples](https://github.com/heggria/taskflow/blob/main/examples) | Runnable flow definitions, including Trusted Effects |
|
|
219
|
-
| [Changelog](https://github.com/heggria/taskflow/blob/main/CHANGELOG.md) | Release history and
|
|
228
|
+
| [Changelog](https://github.com/heggria/taskflow/blob/main/CHANGELOG.md) | Release history and version notes |
|
|
220
229
|
|
|
221
230
|
## License
|
|
222
231
|
|
|
@@ -226,6 +235,6 @@ The monorepo contains the host-neutral `taskflow-core`, Trusted Effects and reso
|
|
|
226
235
|
|
|
227
236
|
**Declare the effect. Verify the path. Commit through one authority.**
|
|
228
237
|
|
|
229
|
-
[Read the docs](https://heggria.github.io/taskflow/en/docs) · [
|
|
238
|
+
[Read the docs](https://heggria.github.io/taskflow/en/docs) · [Install Taskflow 1.0](#quickstart) · [View releases](https://github.com/heggria/taskflow/releases)
|
|
230
239
|
|
|
231
240
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermes-taskflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Run taskflow on Hermes Agent: a Hermes subagent runner plus an MCP server (and a config scaffold) that exposes the taskflow_* tools to Hermes users.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hermes",
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"taskflow-core": "0.
|
|
54
|
-
"taskflow-hosts": "0.
|
|
55
|
-
"taskflow-mcp-core": "0.
|
|
53
|
+
"taskflow-core": "1.0.0",
|
|
54
|
+
"taskflow-hosts": "1.0.0",
|
|
55
|
+
"taskflow-mcp-core": "1.0.0"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/copy-readme.mjs hermes-taskflow"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
taskflow:
|
|
16
16
|
command: "npx"
|
|
17
|
-
args: ["-y", "-p", "hermes-taskflow@0.
|
|
17
|
+
args: ["-y", "-p", "hermes-taskflow@1.0.0", "hermes-taskflow-mcp"]
|
|
18
18
|
env:
|
|
19
19
|
# Uncomment for mutating agent phases (terminal / file write).
|
|
20
20
|
# Leave unset for verify/plan/script-only flows.
|
|
@@ -147,9 +147,9 @@ For a non-trivial flow you'll iterate on, **write the definition to a file**
|
|
|
147
147
|
(typically in the OS tmp dir) and point every call at it with `defineFile`:
|
|
148
148
|
|
|
149
149
|
```jsonc
|
|
150
|
-
// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[
|
|
150
|
+
// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[...]} object)
|
|
151
151
|
// 2. verify, iterate, run — all reference the SAME file by path:
|
|
152
|
-
{ "name": "taskflow_plan", "arguments": { "defineFile": "/tmp/audit.json", "args": {
|
|
152
|
+
{ "name": "taskflow_plan", "arguments": { "defineFile": "/tmp/audit.json", "args": { ... } } } // zero tokens: bind + plan + budget bound
|
|
153
153
|
{ "name": "taskflow_verify", "arguments": { "defineFile": "/tmp/audit.json" } } // zero tokens
|
|
154
154
|
{ "name": "taskflow_compile", "arguments": { "defineFile": "/tmp/audit.json" } } // diagram
|
|
155
155
|
{ "name": "taskflow_lint", "arguments": { "defineFile": "/tmp/audit.json" } } // script-lint + custom verifiers
|
|
@@ -572,7 +572,7 @@ Each effect:
|
|
|
572
572
|
**Phase output is the payload.** With one declared `fs.write`, the phase's
|
|
573
573
|
output becomes the staged file content (see the example: `process.stdout.write`
|
|
574
574
|
= the report). With several `fs.write` effects, the phase must emit JSON
|
|
575
|
-
mapping each effect id to its content (`{ "report": "
|
|
575
|
+
mapping each effect id to its content (`{ "report": "...", "backup": "..." }`).
|
|
576
576
|
Commit promotes each file atomically; a later failure restores every admitted
|
|
577
577
|
file to its durable pre-state, and a direct write by the agent/script to a
|
|
578
578
|
**declared final path** is detected and restored — only the resource
|
|
@@ -624,8 +624,8 @@ more than comparing every approach.
|
|
|
624
624
|
{
|
|
625
625
|
"id": "quick", "type": "race",
|
|
626
626
|
"branches": [
|
|
627
|
-
{ "task": "Answer with a short heuristic
|
|
628
|
-
{ "task": "Answer with a thorough search
|
|
627
|
+
{ "task": "Answer with a short heuristic...", "agent": "executor" },
|
|
628
|
+
{ "task": "Answer with a thorough search...", "agent": "researcher" }
|
|
629
629
|
],
|
|
630
630
|
"final": true
|
|
631
631
|
}
|
|
@@ -49,7 +49,7 @@ Top-level keys of the taskflow definition object.
|
|
|
49
49
|
| `agentScope` | `user`\|`project`\|`both` | `user` | Which agent dirs to load. See §6. |
|
|
50
50
|
| `scriptCwd` | `invocation`\|`flow` | `invocation` | Default cwd policy for `script` phases. `flow` requires trusted saved-flow/`defineFile` provenance; explicit phase `cwd` wins, and inherited cwd-bridge boundaries still constrain the resolved source directory. `taskFile` is the same class of load-time trusted source (see §2). |
|
|
51
51
|
| `args` | record | `{}` | Declared invocation arguments. See §3. |
|
|
52
|
-
| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command",
|
|
52
|
+
| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", ...}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). |
|
|
53
53
|
| `phases` | array | — | **Required.** The phase DAG. See §2. |
|
|
54
54
|
| `version` | number | `1` | Informational metadata in 0.2.x; it does not select runtime semantics or migrate a flow. |
|
|
55
55
|
|
|
@@ -66,11 +66,11 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s.
|
|
|
66
66
|
"id": "audit", // required, unique — referenced via {steps.audit.output}
|
|
67
67
|
"type": "map", // agent | parallel | map | gate | reduce | approval | flow | loop | tournament | script | race | expand (default: agent)
|
|
68
68
|
"agent": "analyst", // agent name to run this phase
|
|
69
|
-
"task": "Audit {item.route}
|
|
69
|
+
"task": "Audit {item.route}...",
|
|
70
70
|
"dependsOn": ["discover"],// DAG edges
|
|
71
71
|
"over": "{steps.discover.json}", // [map] array to fan out over
|
|
72
72
|
"as": "item", // [map] loop var name (default: item)
|
|
73
|
-
"branches": [ /*
|
|
73
|
+
"branches": [ /* ... */ ], // [parallel|race] static task list
|
|
74
74
|
"from": ["audit"], // [reduce] phase ids to aggregate
|
|
75
75
|
"def": "{steps.plan.json}", // [expand|flow] inline fragment / dynamic sub-flow
|
|
76
76
|
"expandMode": "nested", // [expand] nested | graft
|
|
@@ -86,7 +86,7 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s.
|
|
|
86
86
|
|
|
87
87
|
| Key | Applies to | Default | Notes |
|
|
88
88
|
|-----|-----------|---------|-------|
|
|
89
|
-
| `id` | all | — | **Required, unique.** Used in `{steps.<id
|
|
89
|
+
| `id` | all | — | **Required, unique.** Used in `{steps.<id>...}`. |
|
|
90
90
|
| `type` | all | `agent` | One of the **12** phase types (agent, parallel, map, gate, reduce, approval, flow, loop, tournament, script, **race**, **expand**). |
|
|
91
91
|
| `agent` | all | first available | Agent name; resolved from the scoped pool. |
|
|
92
92
|
| `task` | agent, gate, map, reduce | — | Prompt; supports interpolation. Required for these types unless `taskFile` is set. |
|
|
@@ -268,8 +268,8 @@ There are **two independent concurrency limits**:
|
|
|
268
268
|
"concurrency": 6, // ≤6 sibling phases run at once
|
|
269
269
|
"phases": [
|
|
270
270
|
{ "id": "scan", "type": "map", "over": "{steps.list.json}",
|
|
271
|
-
"concurrency": 3, //
|
|
272
|
-
"task": "
|
|
271
|
+
"concurrency": 3, // ...but this map only fans out 3 at a time
|
|
272
|
+
"task": "...", "dependsOn": ["list"] }
|
|
273
273
|
]
|
|
274
274
|
}
|
|
275
275
|
```
|
|
@@ -435,7 +435,7 @@ fingerprint folds **“did the world change?”** signals into that key, so an
|
|
|
435
435
|
external change becomes a cache **miss** even when the task text is identical.
|
|
436
436
|
Each entry is one of:
|
|
437
437
|
|
|
438
|
-
| Entry | Becomes a miss when
|
|
438
|
+
| Entry | Becomes a miss when... | Resolves to |
|
|
439
439
|
|-------|----------------------|-------------|
|
|
440
440
|
| `git:HEAD` / `git:<ref>` | the commit moves | the resolved SHA (30s timeout → `<timeout>`; no git → `<no-git>`) |
|
|
441
441
|
| `glob:<pattern>` | the **set of matching paths** or their metadata changes | sorted path list with size + mtime (content-hashed globs use `glob!:` instead, which is mtime-independent) |
|
|
@@ -497,7 +497,7 @@ Each entry is one of:
|
|
|
497
497
|
```jsonc
|
|
498
498
|
{ "id": "review", "type": "gate", "agent": "reviewer",
|
|
499
499
|
"model": "claude-opus-4", "thinking": "high",
|
|
500
|
-
"task": "
|
|
500
|
+
"task": "...\nVERDICT:", "dependsOn": ["audit"] }
|
|
501
501
|
```
|
|
502
502
|
|
|
503
503
|
**Sandbox a phase to read-only in a subdirectory:**
|
|
@@ -516,7 +516,7 @@ Each entry is one of:
|
|
|
516
516
|
|
|
517
517
|
**Project-only agents:**
|
|
518
518
|
```jsonc
|
|
519
|
-
{ "name": "ci-audit", "agentScope": "project", "phases": [ /*
|
|
519
|
+
{ "name": "ci-audit", "agentScope": "project", "phases": [ /* ... */ ] }
|
|
520
520
|
```
|
|
521
521
|
|
|
522
522
|
---
|
|
@@ -555,22 +555,22 @@ symlinks, and commit atomically; `--emit both` preflights both destinations.
|
|
|
555
555
|
|
|
556
556
|
**Authoring notes (kinds ↔ runes)**
|
|
557
557
|
|
|
558
|
-
Import: `import { flow, agent, map,
|
|
558
|
+
Import: `import { flow, agent, map, ... } from "taskflow-dsl"`. Runes erase to Taskflow
|
|
559
559
|
JSON kinds (single source: `PHASE_TYPES` in core + `erase/kinds/*` registry).
|
|
560
560
|
|
|
561
561
|
| JSON `type` | DSL rune(s) | Notes |
|
|
562
562
|
|-------------|-------------|--------|
|
|
563
563
|
| `agent` | `agent(task, opts?)` | templates → `{steps.*}` / `{item.*}` |
|
|
564
|
-
| `parallel` | `parallel([agent
|
|
565
|
-
| `map` | `map(source, item => agent
|
|
564
|
+
| `parallel` | `parallel([agent...])` | waits for all branches |
|
|
565
|
+
| `map` | `map(source, item => agent...)` | `over` + `as` |
|
|
566
566
|
| `gate` | `gate(up, opts?, task?)` · `gate.automated` · `gate.scored` | sugar → `eval` / `score` |
|
|
567
|
-
| `reduce` | `reduce([
|
|
567
|
+
| `reduce` | `reduce([...], () => agent...)` | `from` |
|
|
568
568
|
| `approval` | `approval({ request })` | |
|
|
569
569
|
| `flow` | `subflow("name")` · `subflow.def(plan)` | use vs def |
|
|
570
|
-
| `loop` | `loop({ task, until?,
|
|
571
|
-
| `tournament` | `tournament({ branches/variants, judge,
|
|
570
|
+
| `loop` | `loop({ task, until?, ... })` | |
|
|
571
|
+
| `tournament` | `tournament({ branches/variants, judge, ... })` | |
|
|
572
572
|
| `script` | `script(run, opts?)` | string or argv array |
|
|
573
|
-
| `race` | `race([agent
|
|
573
|
+
| `race` | `race([agent...], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted |
|
|
574
574
|
| `expand` | `expand` / `expand.nested` / `expand.graft` | `def` + `expandMode` |
|
|
575
575
|
|
|
576
576
|
- `const [a,b] = parallel([agent(...), agent(...)])` desugars to **two real agent phases** (`a`, `b`) that run concurrently (no `dependsOn` between them). Prefer this when you need `{steps.a.output}`.
|