pi-taskflow 0.0.5 → 0.0.7
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/DESIGN.md +15 -1
- package/README.md +115 -16
- package/examples/conditional-research.json +56 -0
- package/examples/guarded-refactor.json +50 -0
- package/extensions/agents.ts +8 -1
- package/extensions/index.ts +42 -18
- package/extensions/interpolate.ts +232 -1
- package/extensions/render.ts +47 -35
- package/extensions/runner.ts +127 -80
- package/extensions/runtime.ts +480 -54
- package/extensions/schema.ts +218 -6
- package/extensions/store.ts +76 -4
- package/extensions/usage.ts +42 -0
- package/package.json +2 -2
- package/skills/taskflow/SKILL.md +146 -2
- package/skills/taskflow/configuration.md +0 -2
package/DESIGN.md
CHANGED
|
@@ -204,6 +204,19 @@ pi-taskflow/
|
|
|
204
204
|
| `map` | 对上游数组**动态 fan-out**,每项一个 agent | ≤concurrency |
|
|
205
205
|
| `gate` | 质量门 / 对抗 review(可决定是否继续) | 1+ |
|
|
206
206
|
| `reduce` | 把多结果聚合为一(synthesize) | 1 |
|
|
207
|
+
| `approval` | **人在环**:暂停等待 approve / reject / edit | 1 |
|
|
208
|
+
| `flow` | 把一个**已保存的 taskflow** 当作单个 phase 运行(组合复用) | 子流程并发 |
|
|
209
|
+
|
|
210
|
+
### 3.3b 控制流 / 可靠性字段(任意 phase)
|
|
211
|
+
|
|
212
|
+
| 字段 | 语义 |
|
|
213
|
+
|------|------|
|
|
214
|
+
| `when` | 条件守卫:表达式为假则 skip 该 phase。支持 `{refs}`、`== != < > <= >=`、`&& \|\| !`、括号、字符串/数字字面量。解析失败 fail-open(仍运行) |
|
|
215
|
+
| `join` | 依赖 join:`all`(默认,等全部 dep)/ `any`(OR-join,任一 dep 完成即运行) |
|
|
216
|
+
| `retry` | `{max, backoffMs, factor}`:失败重试,延迟 = `backoffMs * factor^attempt` |
|
|
217
|
+
| `use` / `with` | `flow` 子流程的名字与入参(入参字符串值会插值) |
|
|
218
|
+
|
|
219
|
+
顶层 `budget: {maxUSD, maxTokens}`:累计成本/token 超限即停(剩余 phase skip,运行态 `blocked`)。
|
|
207
220
|
|
|
208
221
|
### 3.4 模板插值
|
|
209
222
|
|
|
@@ -293,7 +306,8 @@ export async function runTaskflow(def, args, ctx): Promise<TaskflowResult>
|
|
|
293
306
|
| **v0.1** | DSL + schema + runtime(agent/parallel/map/reduce)+ `taskflow` 工具 + `/tf run` + 内存隔离 + 流式进度 | ✅ 已发布 (npm 0.0.1) |
|
|
294
307
|
| **v0.2** | 保存/动态命令注册 + 跨 session 恢复 + `gate` 真门控 + run 历史交互 TUI | ✅ 已完成 (npm 0.0.3) |
|
|
295
308
|
| **v0.3** | examples + SKILL.md(教 LLM 写定义)+ YAML 支持 + 发布 npm | 🚧 examples/SKILL/npm 已做;YAML 待办 |
|
|
296
|
-
| **v0.
|
|
309
|
+
| **v0.6** | 控制流 & 可靠性:`when` 条件分支 + `join:any` OR-join + 声明式 `retry` + `approval` 人在环 + `flow` 子流程组合 + `budget` 成本上限 | ✅ 已完成 |
|
|
310
|
+
| **v0.7+** | 真·后台执行(detached + 轮询)+ 事件/cron 触发 + 成本**预估** + mermaid DAG 导出 + 内置 `deep-research` 工作流 | ⏳ 待办 |
|
|
297
311
|
|
|
298
312
|
---
|
|
299
313
|
|
package/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
<img src="./assets/hero.png" alt="pi-taskflow — declarative, multi-phase subagent workflows" width="880">
|
|
4
|
+
|
|
5
|
+
<p>
|
|
6
|
+
<a href="https://www.npmjs.com/package/pi-taskflow"><img src="https://img.shields.io/npm/v/pi-taskflow?style=flat-square&color=B692FF&label=npm" alt="npm version"></a>
|
|
7
|
+
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-43D9AD?style=flat-square" alt="MIT license"></a>
|
|
8
|
+
<a href="https://pi.dev"><img src="https://img.shields.io/badge/for-Pi%20coding%20agent-6E8BFF?style=flat-square" alt="for the Pi coding agent"></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
</div>
|
|
6
12
|
|
|
7
13
|
> Lightweight workflow orchestration for the [Pi coding agent](https://pi.dev).
|
|
8
14
|
|
|
@@ -16,9 +22,10 @@ saveable as a one-word `/tf:<name>` command.
|
|
|
16
22
|
pi install npm:pi-taskflow
|
|
17
23
|
```
|
|
18
24
|
|
|
19
|
-
Fan out one subagent per item,
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
Fan out one subagent per item, route on results, retry the flaky ones, pause for
|
|
26
|
+
human approval, cap the spend, and gate the output with an adversarial review —
|
|
27
|
+
all from one declarative definition. Only the final report reaches your
|
|
28
|
+
conversation; every intermediate transcript stays in the runtime.
|
|
22
29
|
|
|
23
30
|
## Why
|
|
24
31
|
|
|
@@ -39,6 +46,11 @@ only the final phase's output.
|
|
|
39
46
|
| Scale | a few tasks | dynamic `map` fan-out |
|
|
40
47
|
| Resumable | no | yes (cross-session, cached phases skip) |
|
|
41
48
|
| Quality gates | no | `gate` phases with `VERDICT: BLOCK / PASS` |
|
|
49
|
+
| Conditional routing | no | `when` guards + `join: any` OR-joins |
|
|
50
|
+
| Fault tolerance | no | per-phase `retry` with backoff |
|
|
51
|
+
| Human-in-the-loop | no | `approval` phases (approve / reject / edit) |
|
|
52
|
+
| Cost control | no | run-wide `budget` (USD / token caps) |
|
|
53
|
+
| Composition | no | `flow` phases run saved sub-flows |
|
|
42
54
|
| Progress visibility | opaque while running | live DAG render with timing + cost |
|
|
43
55
|
| Ergonomics | inline JSON each time | shorthand (`task`/`tasks`/`chain`) or DSL |
|
|
44
56
|
|
|
@@ -131,6 +143,36 @@ only the final report back.
|
|
|
131
143
|
|
|
132
144
|
Save it once → `/tf:summarize-files` forever.
|
|
133
145
|
|
|
146
|
+
### Route, gate, and guard
|
|
147
|
+
|
|
148
|
+
Phases also **branch, retry, pause for a human, and respect a budget** — still
|
|
149
|
+
declaratively, no scripting:
|
|
150
|
+
|
|
151
|
+
```jsonc
|
|
152
|
+
{
|
|
153
|
+
"name": "triage-and-fix",
|
|
154
|
+
"budget": { "maxUSD": 1.5 },
|
|
155
|
+
"phases": [
|
|
156
|
+
{ "id": "triage", "type": "agent", "agent": "analyst", "output": "json",
|
|
157
|
+
"task": "Classify the bug. Output ONLY {\"severity\":\"high\"} or {\"severity\":\"low\"}." },
|
|
158
|
+
{ "id": "deep", "when": "{steps.triage.json.severity} == high", "dependsOn": ["triage"],
|
|
159
|
+
"agent": "executor_code", "task": "Root-cause and patch it.",
|
|
160
|
+
"retry": { "max": 2, "backoffMs": 500 } },
|
|
161
|
+
{ "id": "quick", "when": "{steps.triage.json.severity} == low", "dependsOn": ["triage"],
|
|
162
|
+
"agent": "executor_fast", "task": "Apply the quick fix." },
|
|
163
|
+
{ "id": "approve", "type": "approval", "join": "any", "dependsOn": ["deep", "quick"],
|
|
164
|
+
"task": "Review the fix before it ships." },
|
|
165
|
+
{ "id": "ship", "type": "agent", "dependsOn": ["approve"],
|
|
166
|
+
"task": "Open a PR with the change.", "final": true }
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- **`when`** routes to `deep` *or* `quick` from the triage JSON; the other branch is skipped.
|
|
172
|
+
- **`join: "any"`** lets `approve` run as soon as whichever branch fired completes.
|
|
173
|
+
- **`retry`** re-runs a flaky patch with backoff; **`budget`** halts the whole run if it gets too expensive.
|
|
174
|
+
- **`approval`** pauses for a human (approve / reject / edit) before the final `ship`.
|
|
175
|
+
|
|
134
176
|
## Watch it run
|
|
135
177
|
|
|
136
178
|
This is the live progress render for a real run — the `self-improve` flow that
|
|
@@ -172,9 +214,51 @@ writes and verifies its own test suites, caught here mid-block by a quality gate
|
|
|
172
214
|
| `map` | fan out over an array — one subagent per item, `{item}` bound | `over`, `task` |
|
|
173
215
|
| `gate` | quality/review step that can **halt the flow** | `task` |
|
|
174
216
|
| `reduce` | aggregate `from[]` phase outputs into one | `from`, `task` |
|
|
217
|
+
| `approval` | **human-in-the-loop** pause — approve / reject / edit before continuing | — |
|
|
218
|
+
| `flow` | run a **saved sub-flow** as one phase (composition/reuse) | `use` |
|
|
219
|
+
|
|
220
|
+
### Common phase fields
|
|
175
221
|
|
|
176
|
-
Every phase needs `id
|
|
177
|
-
|
|
222
|
+
Every phase needs a unique `id` and a `type` (defaults to `agent`). On top of the
|
|
223
|
+
per-type fields above:
|
|
224
|
+
|
|
225
|
+
| Field | Meaning |
|
|
226
|
+
|---|---|
|
|
227
|
+
| `agent` | Agent to run (defaults to the first discovered agent) |
|
|
228
|
+
| `dependsOn` | Phase ids this phase waits for — builds the DAG |
|
|
229
|
+
| `join` | `"all"` (default) waits for every dep; `"any"` is an OR-join |
|
|
230
|
+
| `when` | Conditional guard — skip unless the expression is truthy |
|
|
231
|
+
| `retry` | `{ max, backoffMs?, factor? }` — retry a failing subagent |
|
|
232
|
+
| `output` | `"text"` (default) or `"json"` (exposes `{steps.ID.json}`) |
|
|
233
|
+
| `model` / `thinking` / `tools` | Per-phase overrides for the subagent |
|
|
234
|
+
| `cwd` | Working directory for the subagent |
|
|
235
|
+
| `concurrency` | Fan-out cap for `map` / `parallel` (overrides the flow default) |
|
|
236
|
+
| `final` | Marks the result-bearing phase (else the last phase wins) |
|
|
237
|
+
| `optional` | A failure here does **not** abort the run |
|
|
238
|
+
| `use` / `with` | (`flow`) saved sub-flow name + its args |
|
|
239
|
+
|
|
240
|
+
Flow-level keys: `name`, `description`, `args`, `concurrency` (default 8),
|
|
241
|
+
`agentScope`, and `budget: { maxUSD?, maxTokens? }`.
|
|
242
|
+
|
|
243
|
+
### Control flow & reliability
|
|
244
|
+
|
|
245
|
+
- **`when`** — skip a phase unless an expression is truthy. Supports `{refs}`,
|
|
246
|
+
`== != < > <= >=`, `&& || !`, parentheses, and quoted strings/numbers, e.g.
|
|
247
|
+
`"when": "{steps.triage.json.route} == deep"`. Pair with `join: "any"` on the
|
|
248
|
+
merge phase to build real if/else routing. Parse errors **fail open**.
|
|
249
|
+
- **`join: "any"`** — an OR-join: the phase runs as soon as *one* dependency
|
|
250
|
+
completes (default `"all"` waits for every dep).
|
|
251
|
+
- **`retry`** — `{ "max": 2, "backoffMs": 500, "factor": 2 }` retries a failing
|
|
252
|
+
subagent with fixed (`factor:1`) or exponential backoff; usage is summed and
|
|
253
|
+
the attempt count shows as `↻N` in the TUI.
|
|
254
|
+
- **`approval`** — pause for a human (`select`: Approve / Reject / Edit). Reject
|
|
255
|
+
halts the flow; Edit injects the typed note as the phase output for downstream
|
|
256
|
+
steps. Non-interactive runs auto-approve.
|
|
257
|
+
- **`flow`** — `{ "type": "flow", "use": "deep-research", "with": { "topic": "{item}" } }`
|
|
258
|
+
runs a saved flow as a phase (recursion is detected and rejected).
|
|
259
|
+
- **`budget`** — a run-wide `{maxUSD, maxTokens}` ceiling; once exceeded, pending
|
|
260
|
+
phases are skipped (and in-flight fan-out stops spawning) and the run is
|
|
261
|
+
`blocked`.
|
|
178
262
|
|
|
179
263
|
### `output` format
|
|
180
264
|
|
|
@@ -263,9 +347,26 @@ file). Phase-level overrides for `model`, `thinking`, and `tools` are passed as
|
|
|
263
347
|
Settings from `~/.pi/agent/settings.json` (the `subagents.agentOverrides` map)
|
|
264
348
|
are honored, letting you tweak model, thinking, or tools per agent across all flows.
|
|
265
349
|
|
|
350
|
+
## Examples
|
|
351
|
+
|
|
352
|
+
Ready-to-read definitions live in [`examples/`](./examples):
|
|
353
|
+
|
|
354
|
+
| File | Demonstrates |
|
|
355
|
+
|---|---|
|
|
356
|
+
| [`summarize-files.json`](./examples/summarize-files.json) | discover → `map` fan-out → `reduce` |
|
|
357
|
+
| [`conditional-research.json`](./examples/conditional-research.json) | `when` routing + `join: any` + `gate` + `budget` |
|
|
358
|
+
| [`guarded-refactor.json`](./examples/guarded-refactor.json) | `approval` (human-in-the-loop) + `retry` + `gate` |
|
|
359
|
+
|
|
360
|
+
To use one, copy it into `.pi/taskflows/<name>.json` (or
|
|
361
|
+
`~/.pi/agent/taskflows/`) and it registers as `/tf:<name>` — or just point the
|
|
362
|
+
model at the definition.
|
|
363
|
+
|
|
266
364
|
## Status & limits
|
|
267
365
|
|
|
268
|
-
- **v0.0.
|
|
366
|
+
- **v0.0.6** — control flow & reliability: conditional `when` guards, `join: any`
|
|
367
|
+
OR-joins, declarative `retry`/backoff, `approval` (human-in-the-loop) phases,
|
|
368
|
+
`flow` (saved sub-flow composition), and run-wide `budget` caps — on top of the
|
|
369
|
+
DSL + DAG runtime (`agent`/`parallel`/`map`/`gate`/`reduce`),
|
|
269
370
|
inline + saved flows, cross-session resume, live progress, isolated context.
|
|
270
371
|
Default `concurrency` is 8 (set on the flow; per-phase `concurrency` overrides
|
|
271
372
|
for that phase).
|
|
@@ -279,7 +380,8 @@ are honored, letting you tweak model, thinking, or tools per agent across all fl
|
|
|
279
380
|
### What it doesn't do (yet)
|
|
280
381
|
|
|
281
382
|
- **No detached background execution.** A run needs the pi session to stay open.
|
|
282
|
-
True background execution is on the
|
|
383
|
+
True background execution (and event/cron triggers on top of it) is on the
|
|
384
|
+
roadmap.
|
|
283
385
|
- **No `output: "file"`.** Outputs are text/JSON only. Write files via agent
|
|
284
386
|
tool calls if needed.
|
|
285
387
|
- **`map` requires a JSON array.** The `over` field must resolve to
|
|
@@ -292,13 +394,10 @@ are honored, letting you tweak model, thinking, or tools per agent across all fl
|
|
|
292
394
|
```bash
|
|
293
395
|
npm install
|
|
294
396
|
npm run typecheck
|
|
295
|
-
|
|
296
|
-
test/schema.test.ts test/runtime.test.ts test/runner.test.ts \
|
|
297
|
-
test/store.test.ts test/agents.test.ts test/render.test.ts \
|
|
298
|
-
test/desugar.test.ts
|
|
397
|
+
npm test # unit tests — no network, no process spawning
|
|
299
398
|
|
|
300
399
|
# real end-to-end (spawns live subagents; needs model access)
|
|
301
|
-
|
|
400
|
+
npm run test:e2e
|
|
302
401
|
```
|
|
303
402
|
|
|
304
403
|
## Contributing
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "conditional-research",
|
|
3
|
+
"description": "Triage a topic, route to a deep or quick research branch (conditional `when` + OR-join), gate the result, and report.",
|
|
4
|
+
"version": 1,
|
|
5
|
+
"args": {
|
|
6
|
+
"topic": { "description": "The subject to research" }
|
|
7
|
+
},
|
|
8
|
+
"concurrency": 4,
|
|
9
|
+
"agentScope": "user",
|
|
10
|
+
"budget": { "maxUSD": 1.0 },
|
|
11
|
+
"phases": [
|
|
12
|
+
{
|
|
13
|
+
"id": "triage",
|
|
14
|
+
"type": "agent",
|
|
15
|
+
"agent": "analyst",
|
|
16
|
+
"task": "Decide how much research the topic \"{args.topic}\" needs. Output ONLY JSON: {\"route\": \"deep\"} for broad/ambiguous topics, or {\"route\": \"quick\"} for narrow/well-defined ones.",
|
|
17
|
+
"output": "json"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "deep",
|
|
21
|
+
"type": "agent",
|
|
22
|
+
"agent": "explorer",
|
|
23
|
+
"when": "{steps.triage.json.route} == deep",
|
|
24
|
+
"dependsOn": ["triage"],
|
|
25
|
+
"task": "Do a thorough, multi-angle investigation of \"{args.topic}\". Cover background, key players, trade-offs, and open questions.",
|
|
26
|
+
"retry": { "max": 2, "backoffMs": 500, "factor": 2 }
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "quick",
|
|
30
|
+
"type": "agent",
|
|
31
|
+
"agent": "explorer",
|
|
32
|
+
"when": "{steps.triage.json.route} == quick",
|
|
33
|
+
"dependsOn": ["triage"],
|
|
34
|
+
"task": "Give a concise, focused answer on \"{args.topic}\" with the 3 most important points.",
|
|
35
|
+
"retry": { "max": 2, "backoffMs": 500, "factor": 2 }
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "review",
|
|
39
|
+
"type": "gate",
|
|
40
|
+
"agent": "critic",
|
|
41
|
+
"join": "any",
|
|
42
|
+
"from": ["deep", "quick"],
|
|
43
|
+
"dependsOn": ["deep", "quick"],
|
|
44
|
+
"task": "Review the research below for unsupported claims. If it is solid, end with 'VERDICT: PASS'; if it is too thin, end with 'VERDICT: BLOCK' and one reason.\n\n{steps.deep.output}{steps.quick.output}"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "report",
|
|
48
|
+
"type": "reduce",
|
|
49
|
+
"from": ["review"],
|
|
50
|
+
"dependsOn": ["review"],
|
|
51
|
+
"agent": "doc-writer",
|
|
52
|
+
"task": "Write a clean markdown brief on \"{args.topic}\" from the validated research:\n\n{steps.deep.output}{steps.quick.output}",
|
|
53
|
+
"final": true
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "guarded-refactor",
|
|
3
|
+
"description": "Plan a change, pause for human approval before touching code (HITL), then implement with retries and a final review gate. Budget-capped.",
|
|
4
|
+
"version": 1,
|
|
5
|
+
"args": {
|
|
6
|
+
"target": { "default": "src", "description": "Directory or module to refactor" },
|
|
7
|
+
"goal": { "description": "What the refactor should achieve" }
|
|
8
|
+
},
|
|
9
|
+
"concurrency": 4,
|
|
10
|
+
"agentScope": "user",
|
|
11
|
+
"budget": { "maxUSD": 2.0 },
|
|
12
|
+
"phases": [
|
|
13
|
+
{
|
|
14
|
+
"id": "plan",
|
|
15
|
+
"type": "agent",
|
|
16
|
+
"agent": "planner",
|
|
17
|
+
"task": "Produce a concrete, step-by-step refactor plan for {args.target} to achieve: {args.goal}. List the files you would change and the risks."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "approve",
|
|
21
|
+
"type": "approval",
|
|
22
|
+
"dependsOn": ["plan"],
|
|
23
|
+
"task": "Review the refactor plan above. Approve to proceed, reject to abort, or edit to add constraints the implementer must follow."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "implement",
|
|
27
|
+
"type": "agent",
|
|
28
|
+
"agent": "executor_code",
|
|
29
|
+
"dependsOn": ["approve"],
|
|
30
|
+
"task": "Implement the approved plan for {args.target}.\nPlan:\n{steps.plan.output}\nExtra human guidance (if any):\n{steps.approve.output}",
|
|
31
|
+
"retry": { "max": 1, "backoffMs": 1000 }
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "review",
|
|
35
|
+
"type": "gate",
|
|
36
|
+
"agent": "reviewer",
|
|
37
|
+
"dependsOn": ["implement"],
|
|
38
|
+
"task": "Review the implementation report below. If it is correct and complete, end with 'VERDICT: PASS'; otherwise 'VERDICT: BLOCK' with reasons.\n\n{steps.implement.output}"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "summary",
|
|
42
|
+
"type": "reduce",
|
|
43
|
+
"from": ["review"],
|
|
44
|
+
"dependsOn": ["review"],
|
|
45
|
+
"agent": "doc-writer",
|
|
46
|
+
"task": "Write a short changelog entry summarizing what was done:\n\n{steps.implement.output}",
|
|
47
|
+
"final": true
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
package/extensions/agents.ts
CHANGED
|
@@ -55,7 +55,14 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
|
|
|
55
55
|
continue;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const { frontmatter, body } =
|
|
58
|
+
const { frontmatter, body } = (() => {
|
|
59
|
+
try {
|
|
60
|
+
return parseFrontmatter<Record<string, string>>(content);
|
|
61
|
+
} catch {
|
|
62
|
+
// A single malformed agent file must not break discovery for every flow.
|
|
63
|
+
return { frontmatter: {} as Record<string, string>, body: "" };
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
59
66
|
if (!frontmatter.name || !frontmatter.description) continue;
|
|
60
67
|
|
|
61
68
|
const tools = frontmatter.tools
|
package/extensions/index.ts
CHANGED
|
@@ -18,8 +18,8 @@ import { Type } from "typebox";
|
|
|
18
18
|
import { type AgentScope, discoverAgents, readSubagentSettings } from "./agents.ts";
|
|
19
19
|
import { renderRunResult, summarizeRun } from "./render.ts";
|
|
20
20
|
import { RunHistoryComponent, type RunHistoryResult } from "./runs-view.ts";
|
|
21
|
-
import { executeTaskflow, type RuntimeResult } from "./runtime.ts";
|
|
22
|
-
import { finalPhase, type Taskflow, validateTaskflow, desugar, isShorthand } from "./schema.ts";
|
|
21
|
+
import { executeTaskflow, type ApprovalDecision, type ApprovalRequest, type RuntimeResult } from "./runtime.ts";
|
|
22
|
+
import { finalPhase, resolveArgs, type Taskflow, validateTaskflow, desugar, isShorthand } from "./schema.ts";
|
|
23
23
|
import {
|
|
24
24
|
getFlow,
|
|
25
25
|
listFlows,
|
|
@@ -86,17 +86,6 @@ const TaskflowParams = Type.Object({
|
|
|
86
86
|
),
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
function resolveArgs(def: Taskflow, provided: Record<string, unknown> | undefined): Record<string, unknown> {
|
|
90
|
-
const args: Record<string, unknown> = {};
|
|
91
|
-
for (const [key, spec] of Object.entries(def.args ?? {})) {
|
|
92
|
-
if (provided && key in provided) args[key] = provided[key];
|
|
93
|
-
else if (spec.default !== undefined) args[key] = spec.default;
|
|
94
|
-
}
|
|
95
|
-
// also pass through any extra provided args
|
|
96
|
-
if (provided) for (const [k, v] of Object.entries(provided)) if (!(k in args)) args[k] = v;
|
|
97
|
-
return args;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
89
|
function makeRunState(def: Taskflow, args: Record<string, unknown>, cwd: string): RunState {
|
|
101
90
|
return {
|
|
102
91
|
runId: newRunId(def.name),
|
|
@@ -153,6 +142,29 @@ async function runFlow(
|
|
|
153
142
|
(heartbeat as { unref?: () => void }).unref?.();
|
|
154
143
|
}
|
|
155
144
|
|
|
145
|
+
// Human-in-the-loop approver — only when an interactive UI is available.
|
|
146
|
+
const requestApproval = ctx.hasUI
|
|
147
|
+
? async (req: ApprovalRequest): Promise<ApprovalDecision> => {
|
|
148
|
+
if (req.upstream?.trim()) {
|
|
149
|
+
const snip = req.upstream.replace(/\s+/g, " ").trim();
|
|
150
|
+
ctx.ui.notify(`[${def.name}/${req.phaseId}] ${snip.length > 280 ? `${snip.slice(0, 280)}…` : snip}`, "info");
|
|
151
|
+
}
|
|
152
|
+
const choice = await ctx.ui.select(
|
|
153
|
+
`Taskflow approval — ${req.phaseId}: ${req.message}`,
|
|
154
|
+
["Approve", "Reject", "Edit / add guidance"],
|
|
155
|
+
{ signal },
|
|
156
|
+
);
|
|
157
|
+
if (!choice || choice === "Reject") return { decision: "reject" };
|
|
158
|
+
if (choice.startsWith("Edit")) {
|
|
159
|
+
const note = await ctx.ui.input("Guidance passed downstream as this phase's output", "type guidance…", {
|
|
160
|
+
signal,
|
|
161
|
+
});
|
|
162
|
+
return { decision: "edit", note: note ?? "" };
|
|
163
|
+
}
|
|
164
|
+
return { decision: "approve" };
|
|
165
|
+
}
|
|
166
|
+
: undefined;
|
|
167
|
+
|
|
156
168
|
try {
|
|
157
169
|
const result = await executeTaskflow(state, {
|
|
158
170
|
cwd: ctx.cwd,
|
|
@@ -160,6 +172,8 @@ async function runFlow(
|
|
|
160
172
|
globalThinking: settings.globalThinking,
|
|
161
173
|
signal,
|
|
162
174
|
persist: persistThrottled,
|
|
175
|
+
requestApproval,
|
|
176
|
+
loadFlow: (name: string) => getFlow(ctx.cwd, name)?.def,
|
|
163
177
|
});
|
|
164
178
|
return result;
|
|
165
179
|
} finally {
|
|
@@ -199,11 +213,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
199
213
|
label: "Taskflow",
|
|
200
214
|
description: [
|
|
201
215
|
"Orchestrate a multi-phase workflow of subagents from a declarative definition.",
|
|
202
|
-
"Phases (agent, parallel, map, gate, reduce) form a DAG; intermediate outputs stay out of your context — only the final phase output is returned.",
|
|
216
|
+
"Phases (agent, parallel, map, gate, reduce, approval, flow) form a DAG; intermediate outputs stay out of your context — only the final phase output is returned.",
|
|
203
217
|
"Use action=run with an inline `define` (you write the DSL) or a saved `name`.",
|
|
204
218
|
"For simple non-DAG delegations (like the subagent tool) skip the DSL: pass `task` (+optional `agent`) for one task, `tasks:[{task,agent?}]` to run in parallel, or `chain:[{task,agent?}]` to run sequentially (reference the prior step with {previous.output}).",
|
|
205
219
|
"Use action=save to persist a definition as a reusable /tf:<name> command. action=resume continues a paused run. action=list shows saved flows.",
|
|
206
|
-
"DSL: {name, args?, concurrency?, phases:[{id, type, agent, task, dependsOn?, over?(map), as?(map), branches?(parallel), from?(reduce), output?:'json', final?}]}.",
|
|
220
|
+
"DSL: {name, args?, concurrency?, budget?:{maxUSD,maxTokens}, phases:[{id, type, agent, task, dependsOn?, join?:'all'|'any', when?, retry?:{max,backoffMs,factor}, over?(map), as?(map), branches?(parallel), from?(reduce), use?(flow), with?(flow), output?:'json', final?}]}.",
|
|
221
|
+
"Phase types: agent (one subagent), parallel (static branches), map (dynamic fan-out over an array), gate (VERDICT: PASS/BLOCK quality gate), reduce (aggregate from N phases), approval (human-in-the-loop pause), flow (run a saved sub-flow). join:'any' is an OR-join; when is a conditional guard; retry adds backoff; budget caps run cost.",
|
|
207
222
|
"Interpolation: {args.X}, {steps.ID.output}, {steps.ID.json}, {item} (map), {previous.output}.",
|
|
208
223
|
].join(" "),
|
|
209
224
|
parameters: TaskflowParams,
|
|
@@ -286,19 +301,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
286
301
|
);
|
|
287
302
|
},
|
|
288
303
|
});
|
|
304
|
+
const warningText = v.warnings.length ? `\n\nWarnings:\n- ${v.warnings.join("\n- ")}` : "";
|
|
289
305
|
return {
|
|
290
306
|
content: [
|
|
291
|
-
{ type: "text", text: `Saved taskflow '${def.name}' → ${filePath}\nRun it with /tf:${def.name} or action=run
|
|
307
|
+
{ type: "text", text: `Saved taskflow '${def.name}' → ${filePath}\nRun it with /tf:${def.name} or action=run.${warningText}` },
|
|
292
308
|
],
|
|
293
309
|
details: { action, message: filePath } satisfies TaskflowDetails,
|
|
294
310
|
};
|
|
295
311
|
}
|
|
296
312
|
|
|
297
313
|
// run
|
|
298
|
-
const v = validateTaskflow(def);
|
|
299
|
-
if (!v.ok) return errorResult(action, `Invalid taskflow:\n- ${v.errors.join("\n- ")}`);
|
|
300
314
|
const args = resolveArgs(def, params.args);
|
|
315
|
+
const v = validateTaskflow(def, { args, cwd: ctx.cwd });
|
|
316
|
+
if (!v.ok) return errorResult(action, `Invalid taskflow:\n- ${v.errors.join("\n- ")}`);
|
|
317
|
+
for (const w of v.warnings) {
|
|
318
|
+
console.warn(`[taskflow:${def.name}] ${w}`);
|
|
319
|
+
}
|
|
301
320
|
const result = await runFlow(def, args, ctx, signal, onUpdate as any);
|
|
321
|
+
// Surface the validation warnings in the tool result so the model
|
|
322
|
+
// can acknowledge or fix them, and the user sees them in the chat.
|
|
323
|
+
if (v.warnings.length) {
|
|
324
|
+
result.finalOutput = `${result.finalOutput}\n\nWarnings:\n- ${v.warnings.join("\n- ")}`;
|
|
325
|
+
}
|
|
302
326
|
return finalResult(action, result);
|
|
303
327
|
},
|
|
304
328
|
|