@tranhoangnguyen0310/pi-flow-external 4.0.0-external.0 → 5.0.0-external.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/CHANGELOG.md +16 -0
- package/README.md +24 -26
- package/docs/README.md +3 -2
- package/docs/migration-agent-to-external-delegate.md +3 -17
- package/docs/migration-workflow-to-external-workflow.md +48 -0
- package/docs/public-contract.md +3 -3
- package/docs/supervision-and-runtime.md +64 -20
- package/docs/troubleshooting.md +2 -2
- package/docs/workflows.md +36 -4
- package/package.json +2 -1
- package/src/contract/envelope.ts +1 -1
- package/src/contract/workflow.ts +2 -1
- package/src/core/concurrency.ts +2 -2
- package/src/core/spawn.ts +3 -3
- package/src/core/subagent-render.ts +2 -2
- package/src/external-help.ts +17 -13
- package/src/pi-subagent.ts +1 -1
- package/src/profile-creator.ts +2 -2
- package/src/profiles.ts +1 -1
- package/src/prompts.ts +15 -7
- package/src/tool-names.ts +1 -0
- package/src/types.ts +3 -3
- package/src/workflow/script-validation.ts +3 -0
- package/src/workflow/source.ts +2 -1
- package/src/workflow/tool.ts +8 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-flow external are documented here.
|
|
4
4
|
|
|
5
|
+
## [5.0.0-external.0] - 2026-10-15
|
|
6
|
+
|
|
7
|
+
### Breaking
|
|
8
|
+
- Renamed the orchestration tool **`workflow` → `external_workflow`**, following PR #86's direct-tool rename precedent. There is no alias or automatic translation. Update `tools.workflow(...)` → `tools.external_workflow(...)`, `describeTool("workflow")` → `describeTool("external_workflow")`, exact-name hooks, tool allow-lists, and receipt checks. A stale call may reach another extension's `workflow` tool rather than fail.
|
|
9
|
+
- New workflow receipts report `tool: "external_workflow"`. Envelope `contractVersion: 1`, `action: "run"`, run `kind: "workflow"`, `wf_...` IDs, saved scripts, and script helpers such as `agent()` remain unchanged. Settings stay at version 5; no conversion or saved-run rewrite is required. Help topic `workflow`, extension option `{ workflow: false }`, and E2E `--workflow` are unchanged.
|
|
10
|
+
- Update Flow's `pi-cc-extensions` `excludeRenderers` entry from `"workflow"` to `"external_workflow"` (alongside `"external_delegate"` and `"external_runs"`), preserve unrelated entries, and restart Pi. See the [workflow tool migration guide](docs/migration-workflow-to-external-workflow.md).
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Parent guidance requires a plain-language pre-launch briefing covering purpose, assignments, parallel/sequential order, allowed changes, and deliverable, with the same roadmap in `meta.phases` and actual transitions marked by `phase()`. This follows the user's existing approval policy; it is not a runtime-verified chat-message gate.
|
|
14
|
+
- Workflow cards now display existing planned-phase details, including future stages. Help and examples explain the roadmap, actual progress, and explicit launch inspection for retrieving recorded plans during background runs and after completion. Phases remain optional for existing saved scripts; declared intent is not proof of execution.
|
|
15
|
+
|
|
16
|
+
- Foreground/background guidance now puts the selection rule and ownership up front: foreground also supports parallel work; background is for independent work or an explicit request, has no automatic completion delivery, and requires collection in the owning session. The runtime guide includes outcome-aware, paged collection instead of teaching immediate background-then-wait. Permission descriptions distinguish native tool restrictions from OS sandboxing.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Phase explanations use the existing secret redaction before display, and supplied phase details must be strings rather than silently rendering malformed values.
|
|
20
|
+
|
|
5
21
|
## [4.0.0-external.0] - 2026-10-13
|
|
6
22
|
|
|
7
23
|
### Breaking
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ You ask Pi to delegate work in plain chat. Every delegation combines three eleme
|
|
|
24
24
|
|
|
25
25
|
> ⚠️ **Security & Defaults:**
|
|
26
26
|
> Roles describe **intent**; `permission` defines **authority**. Saying "read-only" in prompt text does not restrict an agent.
|
|
27
|
-
> Out of the box, the default harness is `agy` and the default permission is `danger` (full host access). Antigravity rejects `readonly` and `edit`. For
|
|
27
|
+
> Out of the box, the default harness is `agy` and the default permission is `danger` (full host access). Antigravity rejects `readonly` and `edit`. For restricted access, request a supported permission tier explicitly. Claude uses native tool restrictions; Codex uses an OS sandbox. See the permission matrix below.
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
@@ -71,7 +71,7 @@ Pi recognizes the request and delegates:
|
|
|
71
71
|
|
|
72
72
|
- **Role:** `explorer`
|
|
73
73
|
- **Harness:** `claude`
|
|
74
|
-
- **Permission:** `readonly` (
|
|
74
|
+
- **Permission:** `readonly` (native tool restrictions: Claude cannot edit files or run shell commands; not an OS sandbox)
|
|
75
75
|
|
|
76
76
|
*(Programmatic / Codemode equivalent):*
|
|
77
77
|
|
|
@@ -140,34 +140,27 @@ await tools.external_delegate({
|
|
|
140
140
|
});
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
### 2.
|
|
143
|
+
### 2. Foreground or Background?
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
**Use foreground when you need the result next.** Use background when useful independent work can proceed, or you explicitly want to collect the result later. A long task alone does not need background mode.
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
Both `external_delegate` and `external_workflow` support these modes:
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
| | Foreground (default) | Background (`background: true`) |
|
|
150
|
+
|---|---|---|
|
|
151
|
+
| Original call returns | When the work finishes or fails | After registration, with a run ID; work may already have finished |
|
|
152
|
+
| Progress | Live execution card | A static launch receipt; inspect with `/external runs` or ask Pi for status |
|
|
153
|
+
| Next step | Pi checks the returned outcome | Pi keeps the run ID, does independent work, then collects and checks the outcome |
|
|
150
154
|
|
|
151
|
-
|
|
152
|
-
- Or ask Pi: *"How is that background security audit going?"*
|
|
155
|
+
> **You:** "Have Claude audit the API in the background while we review the release checklist. Then bring me the findings."
|
|
153
156
|
|
|
154
|
-
|
|
157
|
+
- **No automatic completion message:** background work does not notify or wake the parent. Pi owns collection unless you explicitly ask it to defer; you should not have to manage every run yourself.
|
|
158
|
+
- **Keep Pi open:** background work belongs to its originating session, not a persistent service. Orderly shutdown requests cancellation with bounded cleanup; restart does not adopt unfinished work.
|
|
159
|
+
- **Stopping is different from waiting:** interrupting the original foreground call cancels it. Interrupting `external_runs wait` only stops waiting; use `/external runs` or `external_runs cancel` to stop background work.
|
|
155
160
|
|
|
156
|
-
|
|
157
|
-
const receipt = await tools.external_delegate({
|
|
158
|
-
description: "Audit security-sensitive endpoints",
|
|
159
|
-
role: "reviewer",
|
|
160
|
-
harness: "claude",
|
|
161
|
-
permission: "readonly",
|
|
162
|
-
prompt: "Audit security-sensitive endpoints.",
|
|
163
|
-
background: true,
|
|
164
|
-
});
|
|
161
|
+
Both modes allow parallel workflow children under the same concurrency limit. Background does not change permissions, enforce new budget limits, or make work inherently faster or cheaper. Prompt instructions such as “do not edit” express intent; the supported `permission` tier defines enforced restrictions.
|
|
165
162
|
|
|
166
|
-
|
|
167
|
-
const runId = receipt.data.run.runId;
|
|
168
|
-
await tools.external_runs({ action: "wait", runIds: [runId], mode: "all" });
|
|
169
|
-
}
|
|
170
|
-
```
|
|
163
|
+
See [launch → independent work → collect](docs/supervision-and-runtime.md#background-example-launch--independent-work--collect) for the operational example, result handling, and cancellation details.
|
|
171
164
|
|
|
172
165
|
### 3. Multi-Agent Workflows (Parallel Delegation)
|
|
173
166
|
|
|
@@ -175,13 +168,17 @@ For tasks that benefit from multiple perspectives at once:
|
|
|
175
168
|
|
|
176
169
|
> **You:** "Run a parallel review: have Claude check for architectural issues while Codex audits test coverage."
|
|
177
170
|
|
|
178
|
-
Pi
|
|
171
|
+
Pi first explains who will do what, what runs in parallel, what may change, and the expected result. It then uses `external_workflow` to run both subagents under a shared concurrency limiter. The live card shows the roadmap and actual child activity; `/external runs` provides inspection and cancellation.
|
|
179
172
|
|
|
180
173
|
*(Workflow script):*
|
|
181
174
|
|
|
182
175
|
```js
|
|
183
|
-
export const meta = {
|
|
176
|
+
export const meta = {
|
|
177
|
+
apiVersion: 1, name: "code-review", description: "Parallel review",
|
|
178
|
+
phases: [{ title: "Review", detail: "Claude checks architecture while Codex checks test coverage; both use readonly access." }],
|
|
179
|
+
};
|
|
184
180
|
|
|
181
|
+
phase("Review");
|
|
185
182
|
const [arch, coverage] = await parallel([
|
|
186
183
|
() => agent("Review architectural integrity read-only", { role: "reviewer", harness: "claude", permission: "readonly" }),
|
|
187
184
|
() => agent("Audit test coverage read-only", { role: "qa", harness: "codex", permission: "readonly" }),
|
|
@@ -227,6 +224,7 @@ The direct delegation tool `Agent` is now `external_delegate`, with no alias. Up
|
|
|
227
224
|
- **[Harness Reference](docs/harness-reference.md):** Deep CLI flags, OpenCode 2 standalone server details, Muse session resume, Grok sandbox notes, and Pi in-process presets.
|
|
228
225
|
- **[Configuration Reference](docs/configuration-reference.md):** Settings v5 JSON schema, sparse inheritance, role overrides, and full command reference.
|
|
229
226
|
- **[Supervision & Runtime](docs/supervision-and-runtime.md):** `external_runs` actions, cursor pagination, batch inspect, and byte budgets.
|
|
230
|
-
- **[Workflows](docs/workflows.md):**
|
|
227
|
+
- **[Workflows](docs/workflows.md):** `external_workflow`, plain-language plans, live phases, error handling (`ChildRunError`), and replay cache.
|
|
228
|
+
- **[Workflow Tool Rename](docs/migration-workflow-to-external-workflow.md):** `workflow` → `external_workflow`, with no alias.
|
|
231
229
|
- **[Public Contract](docs/public-contract.md):** Contract envelope definitions for Pi codemode consumers.
|
|
232
230
|
- **[Troubleshooting](docs/troubleshooting.md):** Common errors, authentication checks, and `pi-cc-extensions` renderer compatibility.
|
package/docs/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Core reference guides for configuring and using the extension with Pi:
|
|
|
18
18
|
| **[Troubleshooting](troubleshooting.md)** | Diagnostic recipes, authentication checks, and `pi-cc-extensions` renderer compatibility. |
|
|
19
19
|
| **[Migration Guide (v4 → v5)](migration-v4-to-v5.md)** | Upgrading from older configurations, backups, command mappings, and safe file purging. |
|
|
20
20
|
| **[Tool Rename Migration (`Agent` → `external_delegate`)](migration-agent-to-external-delegate.md)** | Updating codemode calls, hooks, receipt checks, and renderer exclusions after the direct tool rename. |
|
|
21
|
+
| **[Workflow Tool Rename (`workflow` → `external_workflow`)](migration-workflow-to-external-workflow.md)** | Updating workflow callers, hooks, receipt checks, and renderer exclusions; script helpers are unchanged. |
|
|
21
22
|
|
|
22
23
|
---
|
|
23
24
|
|
|
@@ -26,7 +27,7 @@ Specifications for programmatic subagent delegation and tool outputs:
|
|
|
26
27
|
|
|
27
28
|
| Document | Status | Purpose |
|
|
28
29
|
|---|---|---|
|
|
29
|
-
| **[Public Contract v1](public-contract.md)** | `Normative Contract` | Schema definitions, envelope shapes (`{ ok, data, error }`), and delivery invariants for `external_delegate`, `
|
|
30
|
+
| **[Public Contract v1](public-contract.md)** | `Normative Contract` | Schema definitions, envelope shapes (`{ ok, data, error }`), and delivery invariants for `external_delegate`, `external_workflow`, and `external_runs`. |
|
|
30
31
|
|
|
31
32
|
---
|
|
32
33
|
|
|
@@ -55,6 +56,6 @@ Non-normative proposals, design investigations, and dated implementation records
|
|
|
55
56
|
|
|
56
57
|
## Document Status Legend
|
|
57
58
|
|
|
58
|
-
- **`Normative Guide` / `Normative Contract`:** Authoritative documentation reflecting current
|
|
59
|
+
- **`Normative Guide` / `Normative Contract`:** Authoritative documentation reflecting the current implementation.
|
|
59
60
|
- **`Design Direction`:** Approved future UX/architectural targets; does not claim current implementation satisfies it.
|
|
60
61
|
- **`Historical Record` / `Archive`:** Preserved investigation notes and dated planning documents. Superseded where newer contracts exist.
|
|
@@ -44,26 +44,12 @@ return receipt.data.run;
|
|
|
44
44
|
- **Envelope:** `contractVersion` is still `1`. `action` is still `"delegate"`, and runs still report `kind: "agent"`. `ok`, `data`, `error`, and `warnings` keep their meaning. See [Public Contract v1](public-contract.md).
|
|
45
45
|
- **Settings and saved runs:** settings stay at version 5. No settings conversion or saved-run rewrite is needed.
|
|
46
46
|
- **Workflows:** the workflow `agent()` function keeps its name. It exists only inside workflow scripts and never appears in a tool list.
|
|
47
|
-
- **Other tools:** `workflow`, `external_runs`, and `external_help`
|
|
47
|
+
- **Other tools at the time of this rename:** `workflow`, `external_runs`, and `external_help` kept their names. In the subsequent release, `workflow` became [`external_workflow`](migration-workflow-to-external-workflow.md); use that name for current callers.
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
51
51
|
## Background Runs Still Need Collecting
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
Foreground is the default, including parallel workflows. Use background for useful independent work or an explicit user request—not routinely followed by an immediate wait. Background completion does not notify or wake the parent. Retain the ID and collect/check the outcome unless the user explicitly defers; keep the owning session open.
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
const receipt = await tools.external_delegate({
|
|
57
|
-
description: "Audit endpoints",
|
|
58
|
-
role: "reviewer",
|
|
59
|
-
prompt: "Audit the endpoints read-only.",
|
|
60
|
-
background: true,
|
|
61
|
-
});
|
|
62
|
-
if (!receipt.ok) return receipt.error;
|
|
63
|
-
const runId = receipt.data.run.runId;
|
|
64
|
-
|
|
65
|
-
await tools.external_runs({ action: "wait", runIds: [runId], mode: "all" });
|
|
66
|
-
const final = await tools.external_runs({ action: "inspect", runIds: [runId], view: "final" });
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
`final` holds only a verified successful answer. For a failed run, read the outcome from `wait` and the details with `view: "diagnostics"`.
|
|
55
|
+
Use inline successful output when available; otherwise inspect `final` and follow pagination. Failed runs need outcomes and `diagnostics`, not a successful final answer. See the [launch → independent work → collect example](supervision-and-runtime.md#background-example-launch--independent-work--collect) for the complete sequence and interruption rules.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Migration Guide: `workflow` Tool Renamed to `external_workflow`
|
|
2
|
+
|
|
3
|
+
The orchestration tool formerly named `workflow` is now `external_workflow`, alongside `external_delegate`, `external_runs`, and `external_help`. This follows the direct tool rename in [PR #86](https://github.com/tranhoangnguyen03/pi-flow-external/pull/86).
|
|
4
|
+
|
|
5
|
+
## No Alias
|
|
6
|
+
|
|
7
|
+
Flow no longer registers `workflow`. Old calls are not translated. A stale call fails, or reaches another extension's `workflow` tool if one is registered. Update every caller, then reload or restart Pi.
|
|
8
|
+
|
|
9
|
+
## What to Update
|
|
10
|
+
|
|
11
|
+
| Where | Before | After |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Codemode calls | `tools.workflow({...})` | `tools.external_workflow({...})` |
|
|
14
|
+
| Codemode discovery | `describeTool("workflow")` | `describeTool("external_workflow")` |
|
|
15
|
+
| Exact-name hooks (`tool_call`, `tool_result`, `event.toolName`), handlers, and tool allow-lists such as `--tools` | `"workflow"` | `"external_workflow"` |
|
|
16
|
+
| Result discriminator: envelope `tool` field | `tool: "workflow"` | `tool: "external_workflow"` |
|
|
17
|
+
| `pi-cc-extensions` `excludeRenderers` | `"workflow"` | `"external_workflow"`, then restart Pi. Keep `"workflow"` only if another extension needs it. |
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
const receipt = await tools.external_workflow({
|
|
21
|
+
script: `export const meta = {
|
|
22
|
+
apiVersion: 1, name: "review", description: "Review the change without edits",
|
|
23
|
+
phases: [{ title: "Review", detail: "Claude reviews the change with readonly access." }],
|
|
24
|
+
};
|
|
25
|
+
phase("Review");
|
|
26
|
+
return await agent("Review the change without editing files", {
|
|
27
|
+
description: "Review change", role: "reviewer", harness: "claude", permission: "readonly",
|
|
28
|
+
});`,
|
|
29
|
+
});
|
|
30
|
+
if (!receipt.ok) return receipt.error;
|
|
31
|
+
return receipt.data.run;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What Does Not Change
|
|
35
|
+
|
|
36
|
+
- Inputs: `script`, `name`, `scriptPath`, `args`, `background`, and `resumeFromRunId` are unchanged.
|
|
37
|
+
- Script API: `meta.apiVersion: 1`, `agent()`, `parallel()`, `pipeline()`, `phase()`, and `log()` keep their names. Existing saved scripts do not need rewriting.
|
|
38
|
+
- Envelope: `contractVersion: 1`, `action: "run"`, run `kind: "workflow"`, and `wf_...` run IDs are unchanged.
|
|
39
|
+
- Settings stay at version 5. No settings conversion or saved-run rewrite is needed. The extension option `{ workflow: false }`, help topic `workflow`, workflow directories, and E2E `--workflow` flag are unchanged.
|
|
40
|
+
- `external_delegate`, `external_runs`, and `external_help` keep their names. No bridge-compatibility fix is claimed by this rename.
|
|
41
|
+
|
|
42
|
+
## Explain Work Before Launch
|
|
43
|
+
|
|
44
|
+
Parent guidance now requires a short plain-language briefing: purpose, assignments, parallel/sequential order, allowed changes, and deliverable. Put the roadmap in the existing optional `meta.phases` and use `phase()` at actual transitions. Phase details now display on the live card.
|
|
45
|
+
|
|
46
|
+
This is an instruction to the agent, not a runtime check that a chat message was sent. Planned phases describe intent; child states and activity describe observed execution. Existing saved scripts without phases remain valid. Follow the user's existing approval policy rather than treating every briefing as a new approval gate.
|
|
47
|
+
|
|
48
|
+
Background work still needs collecting through `external_runs`; its launch receipt is not a live monitor. Inspect `view: "launch"` to retrieve the recorded purpose and phase plan during or after execution. That explicit view also contains sensitive script/argument evidence. See [Workflows](workflows.md) for details.
|
package/docs/public-contract.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Public contract v1: external_delegate,
|
|
1
|
+
# Public contract v1: external_delegate, external_workflow and external_runs
|
|
2
2
|
|
|
3
|
-
`external_delegate`, `
|
|
3
|
+
`external_delegate`, `external_workflow` and `external_runs` declare `outputSchema` and return matching `structuredContent`. Human `content` and renderer `details` stay separate. Native Pi codemode receives the structured result, including failures. **Check `ok`**, not just whether the call threw.
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
6
|
const receipt = await tools.external_delegate({ role: "worker", description: "Check build", prompt: "Run the build and report." });
|
|
@@ -23,7 +23,7 @@ Every result carries:
|
|
|
23
23
|
| Tool | `action` | Success `data` | Failure `data` |
|
|
24
24
|
|---|---|---|---|
|
|
25
25
|
| `external_delegate` | `delegate` | `{run}` | `{run}`, or `{run: null}` before registration |
|
|
26
|
-
| `
|
|
26
|
+
| `external_workflow` | `run` | `{run}` | `{run}`, or `{run: null}` before registration |
|
|
27
27
|
| `external_runs` | the requested action | the action's payload (below) | `null` |
|
|
28
28
|
|
|
29
29
|
A failure that has a run keeps that run's evidence. Read-only supervision never returns partial data on failure.
|
|
@@ -31,35 +31,78 @@ Workspace /path/to/project
|
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
34
|
-
## Background
|
|
34
|
+
## Foreground, Background & Lifecycle
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
**Foreground is the default:** omit `background` or set it to `false`. The original `external_delegate` or `external_workflow` call waits for settlement and returns the result or failure. Its live card carries progress while the parent waits.
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
**Background returns a handle:** set `background: true` when the parent has useful independent work before collection, or the user explicitly requests background execution. The receipt confirms registration, not completion; a fast run may already be settled. The launch receipt is not a live monitor. Use `/external runs` to browse, or `external_runs` to inspect, wait, or cancel.
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
Use foreground when the result is needed next—even for a long workflow or parallel children. Do not routinely launch background then immediately wait. Both modes support `parallel()` under the same concurrency limiter; neither changes permission boundaries or budget enforcement. Prompt instructions express intent, not an enforced restriction.
|
|
41
|
+
|
|
42
|
+
### Background Example: Launch → Independent Work → Collect
|
|
43
|
+
|
|
44
|
+
Before launch, tell the user: “Claude will audit the API with readonly permission. While it runs, I’ll read the release checklist, then compare the findings.” For a workflow, also explain assignments and ordering as described in [Workflows](workflows.md#explain-the-plan-before-launch).
|
|
45
|
+
|
|
46
|
+
This codemode example assumes both paths exist; replace them with your project's absolute paths. It uses `external_delegate`, but collection is the same for `external_workflow` and its `wf_...` ID.
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
const launched = await tools.external_delegate({
|
|
50
|
+
description: "Audit API routes",
|
|
51
|
+
role: "reviewer", harness: "claude", permission: "readonly",
|
|
52
|
+
prompt: "Audit /absolute/project/api for security defects. Do not edit files.",
|
|
47
53
|
background: true,
|
|
48
54
|
});
|
|
55
|
+
if (!launched.ok) throw new Error(launched.error.message);
|
|
56
|
+
let run = launched.data.run; // Retain this handle and its runId.
|
|
57
|
+
|
|
58
|
+
// Useful independent work, not an immediate wait:
|
|
59
|
+
const checklist = await tools.read({ path: "/absolute/project/RELEASE.md" });
|
|
60
|
+
|
|
61
|
+
if (!run.state.outcome) {
|
|
62
|
+
const waited = await tools.external_runs({
|
|
63
|
+
action: "wait", runIds: [run.runId], mode: "all",
|
|
64
|
+
});
|
|
65
|
+
if (!waited.ok) throw new Error(waited.error.message);
|
|
66
|
+
const settled = waited.data.completed.find(item => item.runId === run.runId);
|
|
67
|
+
if (!settled) return { pending: waited.data.pending, checklist };
|
|
68
|
+
run = settled;
|
|
69
|
+
}
|
|
49
70
|
|
|
50
|
-
if (
|
|
51
|
-
|
|
71
|
+
if (run.state.outcome === "succeeded" && run.output.delivery === "inline") {
|
|
72
|
+
return { run, result: run.output.value, checklist }; // null/false/0 are valid.
|
|
52
73
|
}
|
|
53
74
|
|
|
54
|
-
const
|
|
75
|
+
const view = run.state.outcome === "succeeded" ? "final" : "diagnostics";
|
|
76
|
+
let cursor;
|
|
77
|
+
let text = "";
|
|
78
|
+
let encoding;
|
|
79
|
+
do {
|
|
80
|
+
const inspected = await tools.external_runs({
|
|
81
|
+
action: "inspect", runIds: [run.runId], view, ...(cursor ? { cursor } : {}),
|
|
82
|
+
});
|
|
83
|
+
if (!inspected.ok) throw new Error(inspected.error.message);
|
|
84
|
+
if (view === "final" && !inspected.data.finalAvailable) {
|
|
85
|
+
throw new Error("No verified final answer is available");
|
|
86
|
+
}
|
|
87
|
+
text += inspected.data.page.text;
|
|
88
|
+
encoding = inspected.data.page.encoding;
|
|
89
|
+
cursor = inspected.data.page.nextCursor;
|
|
90
|
+
} while (cursor);
|
|
91
|
+
return { run, view, encoding, text, checklist };
|
|
55
92
|
```
|
|
56
93
|
|
|
57
|
-
|
|
94
|
+
- **Check the run, not just `ok`.** `wait.ok` means supervision worked, even if a child failed. Read each `completed[i].state.outcome` and all `pending` IDs. With multiple targets, a failed workflow can return early even in `mode: "all"`; pending work continues. Keep collecting it unless the user defers, or cancel it explicitly.
|
|
95
|
+
- **Avoid redundant calls.** If settlement is already known, inspect directly rather than wait again. Successful inline output needs no second fetch. A successful workflow can still have `children.failed` when the script handled child errors; check the returned reports.
|
|
96
|
+
- **Read every page.** Otherwise use `final` for verified success or `diagnostics`/`output` for failure. `final` is empty without verified success. Follow `page.nextCursor`, concatenate text, and only then parse if `encoding` is `json`. Workflow final JSON wraps the value in `{ runId, result, finalAvailable }`; agent finals are text.
|
|
97
|
+
- **Keep ownership.** The parent retains IDs and collects/checks results unless the user explicitly defers collection. A script's local variables do not survive a separate codemode call; pass the retained handle or ID explicitly. If independent work or collection fails, report any still-running IDs rather than abandoning them silently. Never retry a child automatically.
|
|
98
|
+
|
|
99
|
+
### Interruption, Cancellation & Session Ownership
|
|
58
100
|
|
|
59
|
-
|
|
60
|
-
- **
|
|
61
|
-
- **
|
|
62
|
-
- **
|
|
101
|
+
- **Foreground interruption:** interrupting the original blocking call cancels that work.
|
|
102
|
+
- **Wait interruption:** interrupting `external_runs wait` only stops waiting; it does not cancel the selected runs. Use `external_runs cancel` to stop background work. Cancellation requests are not proof of completed cleanup; check settlement.
|
|
103
|
+
- **Workflow cancellation:** cancelling the root stops its active children. Cancelling one child throws a catchable child error; a script that handles it may continue.
|
|
104
|
+
- **No automatic delivery:** background completion does not notify, wake, or resume the parent. Ending a turn is not the same as closing the session; work can continue between turns in the owning session.
|
|
105
|
+
- **Not a persistent service:** keep the owning Pi session open. Orderly shutdown requests cancellation and waits for bounded cleanup. Crashes or force-kills leave unfinished evidence `interrupted_or_uncertain`; restarting Pi never adopts live orphan work.
|
|
63
106
|
|
|
64
107
|
---
|
|
65
108
|
|
|
@@ -110,10 +153,11 @@ Block until one or more targets reach terminal state:
|
|
|
110
153
|
const waitResult = await external_runs({
|
|
111
154
|
action: "wait",
|
|
112
155
|
runIds: [runId1, runId2],
|
|
113
|
-
mode: "all", //
|
|
156
|
+
mode: "all", // A failed workflow can return early; always check data.pending.
|
|
114
157
|
limitBytes: 32768,
|
|
115
158
|
});
|
|
116
159
|
```
|
|
160
|
+
- **Wait Modes:** `any` returns after a selected target settles; `all` waits for all, except an unsuccessful workflow returns early. Check `completed` outcomes and `pending` IDs; `ok: true` does not mean every run succeeded.
|
|
117
161
|
- **Live Progress Heartbeat:** While waiting, the tool emits bounded progress updates (watched targets, completed/pending counts, recent activity) through Pi's update channel.
|
|
118
162
|
- **Shared Budget:** The `limitBytes` cap is spent across settled results in requested order. If a result exceeds remaining space, it returns `resultTruncated: true` with references to inspect the rest.
|
|
119
163
|
|
|
@@ -131,7 +175,7 @@ await external_runs({
|
|
|
131
175
|
|
|
132
176
|
## Operational Evidence & Run Storage
|
|
133
177
|
|
|
134
|
-
|
|
178
|
+
Normal runs write private, best-effort evidence to disk; unavailable storage does not discard a settled in-memory result:
|
|
135
179
|
|
|
136
180
|
```text
|
|
137
181
|
~/.pi/agent/pi-flow-external/runs/<run-id>/
|
package/docs/troubleshooting.md
CHANGED
|
@@ -59,14 +59,14 @@ Solutions to common issues, authentication questions, and environment quirks whe
|
|
|
59
59
|
|
|
60
60
|
## Third-Party UI Compatibility: `pi-cc` / `ccstyle`
|
|
61
61
|
|
|
62
|
-
If using [`pi-cc-extensions`](https://www.npmjs.com/package/pi-cc-extensions), its default rendering overrides can cause `
|
|
62
|
+
If using [`pi-cc-extensions`](https://www.npmjs.com/package/pi-cc-extensions), its default rendering overrides can cause `external_workflow` and `external_runs` wait progress cards to show a generic `Pending…` message.
|
|
63
63
|
|
|
64
64
|
### Resolution
|
|
65
65
|
Add `pi-flow-external` tool names to `excludeRenderers` in `~/.pi/agent/pi-cc-extensions.json`:
|
|
66
66
|
|
|
67
67
|
```json
|
|
68
68
|
{
|
|
69
|
-
"excludeRenderers": ["external_delegate", "
|
|
69
|
+
"excludeRenderers": ["external_delegate", "external_workflow", "external_runs"]
|
|
70
70
|
}
|
|
71
71
|
```
|
|
72
72
|
|
package/docs/workflows.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Workflows
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The `external_workflow` tool provides trusted JavaScript orchestration across external AI harnesses and in-process Pi models, enabling parallel fan-out, multi-stage pipelines, and structured failure handling. The former `workflow` tool name is not an alias; see the [rename migration guide](migration-workflow-to-external-workflow.md).
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -13,8 +13,13 @@ export const meta = {
|
|
|
13
13
|
apiVersion: 1,
|
|
14
14
|
name: "code-review",
|
|
15
15
|
description: "Parallel architecture and QA review",
|
|
16
|
+
phases: [
|
|
17
|
+
{ title: "Review", detail: "Claude checks architecture while Codex checks tests; both use readonly access." },
|
|
18
|
+
{ title: "Collect", detail: "Return both reports for the parent to compare." },
|
|
19
|
+
],
|
|
16
20
|
};
|
|
17
21
|
|
|
22
|
+
phase("Review");
|
|
18
23
|
// Orchestrate subagents:
|
|
19
24
|
const [arch, qa] = await parallel([
|
|
20
25
|
() => agent("Review architecture read-only", {
|
|
@@ -29,11 +34,38 @@ const [arch, qa] = await parallel([
|
|
|
29
34
|
}),
|
|
30
35
|
]);
|
|
31
36
|
|
|
37
|
+
phase("Collect");
|
|
32
38
|
return { arch, qa };
|
|
33
39
|
```
|
|
34
40
|
|
|
35
41
|
---
|
|
36
42
|
|
|
43
|
+
## Explain the Plan Before Launch
|
|
44
|
+
|
|
45
|
+
Before calling `external_workflow`, the parent agent must tell the user, in plain language:
|
|
46
|
+
|
|
47
|
+
- The purpose and which agent/harness handles each assignment.
|
|
48
|
+
- What runs in parallel and what waits for earlier results.
|
|
49
|
+
- What may change and what deliverable the user will receive.
|
|
50
|
+
|
|
51
|
+
For example: “Claude will review the architecture while Codex checks test coverage, both with readonly access. Then I’ll compare their reports and return one prioritized list.” Follow the user's existing approval policy; a briefing is not an extra approval gate. Explain material changes to the plan.
|
|
52
|
+
|
|
53
|
+
Put the same roadmap in `meta.phases`, an optional ordered array of `{ title, detail? }`, and call `phase(title)` when each stage actually begins. Short phase details explain the work on the live card; future stages appear as planned. Existing saved scripts without phases remain valid.
|
|
54
|
+
|
|
55
|
+
**A plan is intent, not proof of execution.** The briefing is an agent instruction, not a runtime-verified chat message. Runtime validation checks phase metadata, not whether the script follows its prose. Child states and activity are the source of truth for observed execution. Do not invent completion estimates or call a quiet agent healthy merely because a spinner moves.
|
|
56
|
+
|
|
57
|
+
Choose foreground by default, including parallel workflows. Use background only for useful independent work or an explicit user request; it does not make children more parallel. While a foreground call runs, the live card carries progress; the parent cannot reliably send chat updates from inside that call. A background receipt is not a live monitor, and completion does not wake the parent. Keep the owning session open and collect/check results unless the user explicitly defers. See [execution and collection](supervision-and-runtime.md#foreground-background--lifecycle) for the complete lifecycle and interruption rules.
|
|
58
|
+
|
|
59
|
+
The recorded purpose and phase details remain accessible in explicit launch inspection, during execution and after completion:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
await tools.external_runs({ action: "inspect", runIds: [runId], view: "launch" });
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Launch evidence also includes the script and arguments and may contain sensitive information. Follow `nextCursor` and concatenate pages when the response is paged.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
37
69
|
## Global Functions & Environment
|
|
38
70
|
|
|
39
71
|
The workflow sandbox exposes these helpers:
|
|
@@ -41,7 +73,7 @@ The workflow sandbox exposes these helpers:
|
|
|
41
73
|
- **`agent(prompt, options)`:** Delegates one subagent task.
|
|
42
74
|
- `options.role`: Role name (`explorer`, `reviewer`, `implementer`, etc.).
|
|
43
75
|
- `options.harness`: Optional harness override (`claude`, `codex`, `agy`, `grok`, `muse`, `opencode`, or named `pi-*`).
|
|
44
|
-
- `options.permission`:
|
|
76
|
+
- `options.permission`: Permission tier (`readonly`, `edit`, or `danger`).
|
|
45
77
|
- `options.context`: Context sharing (`{ mode: "recent", turns: 5 }` or `{ mode: "full" }`).
|
|
46
78
|
- `options.description`: User-facing task label in UI.
|
|
47
79
|
- `options.schema`: Optional JSON / TypeBox schema for structured return values (supported on Pi subagents and native CLI schemas).
|
|
@@ -50,7 +82,7 @@ The workflow sandbox exposes these helpers:
|
|
|
50
82
|
- **`pipeline(items, ...stages)`:** Feeds items sequentially through transformation stages.
|
|
51
83
|
- **`phase(title)`:** Marks milestone boundaries in live progress presentation.
|
|
52
84
|
- **`log(message)`:** Appends diagnostic notes to the workflow journal.
|
|
53
|
-
- **`args`:** Input JSON arguments passed via `
|
|
85
|
+
- **`args`:** Input JSON arguments passed via `external_workflow({ args: { ... } })`.
|
|
54
86
|
- **`cwd`:** Current working directory of the Pi session.
|
|
55
87
|
|
|
56
88
|
---
|
|
@@ -96,7 +128,7 @@ When a workflow begins, it freezes an immutable snapshot of all settings, role i
|
|
|
96
128
|
When executing a saved workflow file from disk, you can pass `resumeFromRunId` to replay:
|
|
97
129
|
|
|
98
130
|
```ts
|
|
99
|
-
await
|
|
131
|
+
await tools.external_workflow({
|
|
100
132
|
scriptPath: "/path/to/workflow.js",
|
|
101
133
|
resumeFromRunId: "wf_previous_run_id",
|
|
102
134
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tranhoangnguyen0310/pi-flow-external",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0-external.0",
|
|
4
4
|
"description": "External Claude Code, Codex CLI, Antigravity, Grok Build CLI, Muse Code, and OpenCode delegation for pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"docs/troubleshooting.md",
|
|
22
22
|
"docs/migration-v4-to-v5.md",
|
|
23
23
|
"docs/migration-agent-to-external-delegate.md",
|
|
24
|
+
"docs/migration-workflow-to-external-workflow.md",
|
|
24
25
|
"docs/public-contract.md"
|
|
25
26
|
],
|
|
26
27
|
"keywords": [
|
package/src/contract/envelope.ts
CHANGED
|
@@ -25,7 +25,7 @@ export function contractEnvelope<S extends TSchema, F extends TSchema>(tool: str
|
|
|
25
25
|
return Type.Union([successVariant(tool, action, success), failureVariant(tool, action, failure)]);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/** Builds one envelope as plain JSON. `redact` re-redacts the whole envelope (
|
|
28
|
+
/** Builds one envelope as plain JSON. `redact` re-redacts the whole envelope (external_delegate and external_workflow receipts). */
|
|
29
29
|
export function envelope({ tool, action, data, warnings = [], error, redact = false }: {
|
|
30
30
|
tool: string;
|
|
31
31
|
action: string;
|
package/src/contract/workflow.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
+
import { EXTERNAL_WORKFLOW_TOOL } from "../tool-names.ts";
|
|
2
3
|
import type { RegisteredRunEntry } from "../core/run-registry.ts";
|
|
3
4
|
import { contractEnvelope, INLINE_RESULT_BYTES, type PublicError } from "./envelope.ts";
|
|
4
5
|
import { liveWorkflowRun, workflowRunSchema, type EvidenceIntegrity, type PublicRun } from "./run.ts";
|
|
5
6
|
|
|
6
7
|
const workflowData = Type.Object({ run: Type.Union([Type.Null(), workflowRunSchema]) });
|
|
7
8
|
/** `ok` describes the workflow operation: a background launch is accepted, a foreground one settled successfully. */
|
|
8
|
-
export const workflowOutputSchema = contractEnvelope(
|
|
9
|
+
export const workflowOutputSchema = contractEnvelope(EXTERNAL_WORKFLOW_TOOL, Type.Literal("run"), workflowData, workflowData);
|
|
9
10
|
|
|
10
11
|
export const HANDLED_CHILD_FAILURES = "handled_child_failures";
|
|
11
12
|
|
package/src/core/concurrency.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A shared in-flight concurrency limiter (semaphore + FIFO wait-queue).
|
|
3
3
|
*
|
|
4
|
-
* One instance is shared across the `
|
|
4
|
+
* One instance is shared across the `external_delegate` tool and the `external_workflow` tool so the
|
|
5
5
|
* global cap bounds the total number of concurrent subagents across both entry
|
|
6
6
|
* points — not per-tool.
|
|
7
7
|
*
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - `tryAcquire()` — synchronous, non-blocking. Returns a release fn if a slot
|
|
10
10
|
* is free, else `null`. Kept for direct semaphore-style callers/tests.
|
|
11
11
|
* - `acquire(signal?)` — async. Resolves with a release fn when a slot frees.
|
|
12
|
-
* Used by both the `
|
|
12
|
+
* Used by both the `external_delegate` tool and workflow `agent()` calls so excess
|
|
13
13
|
* subagents queue and drain under the shared cap. Honors an AbortSignal.
|
|
14
14
|
*
|
|
15
15
|
* Correctness / no over-subscription: `active` is incremented only on the fast
|
package/src/core/spawn.ts
CHANGED
|
@@ -45,18 +45,18 @@ import { runRecordsDirectory } from "./retention.ts";
|
|
|
45
45
|
import { createTimeoutSignal, markSubagentTimedOut } from "./timeout.ts";
|
|
46
46
|
import type { PermissionResolution } from "./permissions.ts";
|
|
47
47
|
import { effectivePiResourcePreset, isValidThinkingLevel, VALID_THINKING_LEVELS } from "../harnesses.ts";
|
|
48
|
-
import { EXTERNAL_DELEGATE_TOOL } from "../tool-names.ts";
|
|
48
|
+
import { EXTERNAL_DELEGATE_TOOL, EXTERNAL_WORKFLOW_TOOL } from "../tool-names.ts";
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* The delegation tools a spawned child must never receive, so subagents cannot
|
|
52
52
|
* recursively fan out. Owned by the spawn core and used as the default exclude
|
|
53
53
|
* list so no caller can accidentally under-specify the nesting block.
|
|
54
54
|
*/
|
|
55
|
-
export const CHILD_EXCLUDED_TOOLS: readonly string[] = [EXTERNAL_DELEGATE_TOOL,
|
|
55
|
+
export const CHILD_EXCLUDED_TOOLS: readonly string[] = [EXTERNAL_DELEGATE_TOOL, EXTERNAL_WORKFLOW_TOOL];
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Parameters for a single subagent run. This is the shared spawn primitive used
|
|
59
|
-
* by both the `
|
|
59
|
+
* by both the `external_delegate` tool and the `external_workflow` tool's `agent()` global.
|
|
60
60
|
* Concurrency accounting lives in the callers, not here; callers acquire a slot
|
|
61
61
|
* before invoking spawnSubagent, so the runtime timeout below excludes queue time.
|
|
62
62
|
*/
|
|
@@ -17,8 +17,8 @@ export const RICH_SUBAGENT_ACTIVE_LIMIT = 4;
|
|
|
17
17
|
* initializes (`initTheme()`); a headless/RPC render, an uninitialized test
|
|
18
18
|
* harness, or any other caller without that startup step must still produce
|
|
19
19
|
* a readable row instead of crashing the tool card. This is the single
|
|
20
|
-
* shared "render a result body" primitive for `
|
|
21
|
-
* `
|
|
20
|
+
* shared "render a result body" primitive for `external_delegate` (subagent-render.ts),
|
|
21
|
+
* `external_workflow` (its own final-output block), and `external_runs`'s output/
|
|
22
22
|
* final views — the last mile of the shared-rendering fix for issue #52's
|
|
23
23
|
* "still plain Text for output rather than rendered Markdown" gap.
|
|
24
24
|
*/
|
package/src/external-help.ts
CHANGED
|
@@ -90,33 +90,37 @@ function validateHarnessFilter(harness: string | undefined, configuredPiHarnesse
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
function workflowHelp(workflowsEnabled: boolean, workflows: ReturnType<typeof listSavedWorkflows>): string {
|
|
93
|
-
const availability = workflowsEnabled ? "The
|
|
93
|
+
const availability = workflowsEnabled ? "The external_workflow tool is enabled." : "The external_workflow tool is disabled for this extension instance.";
|
|
94
94
|
return `${availability} It runs trusted JavaScript, not a sandbox.
|
|
95
95
|
|
|
96
96
|
Provide exactly one workflow source: name, scriptPath, or script. Every script starts with the literal current declaration export const meta = { apiVersion: 1, name, description }, calls agent() at least once, awaits every started call, and returns JSON-serializable data. Missing/unsupported API versions fail before any child launches. Project .pi/workflows are visible only when the project is trusted.
|
|
97
97
|
|
|
98
|
+
Before calling external_workflow, explain the purpose, assignments, parallel/sequential order, allowed changes, and deliverable in plain language. Add the same roadmap as meta.phases: [{ title: "Review", detail: "Who checks what and whether files may change" }, ...], and call phase("Review") when that stage actually starts. Planned phases and details remain visible on the live card; they describe intent, while child states/activity show observed execution. Explain material plan changes and follow the user's existing approval policy. The briefing is an agent instruction, not a runtime-verified chat message; phases remain optional for existing saved scripts. Use external_runs inspect view launch to retrieve the recorded purpose and plan (also includes sensitive script/args); background receipts are not live monitors.
|
|
99
|
+
|
|
98
100
|
APIs: agent(prompt, { description, label, role, harness, subagent_type, permission, max_budget_usd, resume, context, schema, phase }); parallel(thunks); pipeline(items, ...stages); phase(title); log(message). Globals: args and cwd. Agent options other than the profile selector are optional. "description" is the common task-name option shared with the direct external_delegate tool; "label" is a compatible alias — set only one, or both to the same value. Choose role/harness or legacy exact subagent_type, never both. Use unique descriptions/labels and clear task prompts. An agent() succeeds with its value or throws ChildRunError { runId, outcome, message, outputRef, diagnosticsRef }; catch optional failures explicitly. Uncaught failures terminate the workflow and drain siblings. Helpers never convert failures to null. Context options: {mode:"none"} (default), {mode:"recent",turns:N} (positive integer, includes current user turn), or {mode:"full"} (available context after compaction). All children share invocation-time context/settings; earlier child results must still be passed explicitly. Context excludes thinking/system instructions and pending calls; images and snapshots over 1 MiB fail without truncation. Context sharing cannot be combined with resume. Use schema for results that control branching or aggregation. Imports, filesystem globals, Date APIs, and Math.random() are unavailable.
|
|
99
101
|
|
|
100
|
-
|
|
102
|
+
Execution mode: foreground is blocking by default, including parallel workflows. Use background:true only for useful independent work before collection or an explicit user request; do not routinely launch background then immediately wait. Both modes support parallel([() => agent(...), ...]) under the same maxConcurrentSubagents cap; sequential awaits stay serial. Background changes neither permissions nor budget enforcement. Its stable handle means accepted, not completed; the receipt is not a live monitor. Retain the ID and own collection unless the user explicitly defers it. No completion notification wakes this session. Keep the session open: orderly shutdown requests cancellation with bounded cleanup; crashes leave uncertain evidence and restart does not adopt work. Topic usage contains the worked collection guidance.
|
|
103
|
+
|
|
104
|
+
Supervision: external_runs selects targets with runIds. list takes optional workflowRunId, cursor, workflowCursor, and limit. inspect with one id reads summary/output/diagnostics/final/launch and pages with cursor and limitBytes; several ids (up to 20) batch summary only. wait takes up to 100 runIds in mode any or all; check ok for the operation, then each completed run's outcome and pending IDs. It never cancels pending work and can return early on a failed workflow even in all mode. If settlement is already known, skip wait. Use inline successful output when available; otherwise inspect final and follow nextCursor. Failures need diagnostics/output, not final. Follow nextWorkflowCursor for workflow listings too. Foreground-call interruption cancels that call; interrupting wait only stops waiting. Use cancel with one id and an optional reason to stop background work. Cancelling a workflow stops active children; cancelling one child is a catchable workflow error. There is no live steering.
|
|
101
105
|
|
|
102
106
|
Replay: resumeFromRunId works only with persisted scriptPath and starts an explicit new attempt. It reuses the longest unchanged prefix of successful child calls; the first changed/failed/cancelled/timed-out call and its suffix run again. Script recomposition is cheap, but child reruns can cost money or repeat side effects. There is no automatic repaired-script replay.
|
|
103
107
|
|
|
104
|
-
Background is
|
|
108
|
+
Background is an external_workflow tool call parameter ({ script, background: true }), never a meta field — meta.background is silently ignored and does not run the workflow in the background.
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
export const meta = {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
export const meta = { apiVersion: 1, name: "review", description: "Map and review a repository" };
|
|
110
|
+
Example (blocking by default; pass background:true on the external_workflow call only when the parent has other work):
|
|
111
|
+
export const meta = {
|
|
112
|
+
apiVersion: 1, name: "review", description: "Map and review a repository",
|
|
113
|
+
phases: [
|
|
114
|
+
{ title: "Check", detail: "The default harness maps the repository while Codex reviews it; neither is assigned edits." },
|
|
115
|
+
{ title: "Collect", detail: "Return both reports for the parent to compare." },
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
phase("Check");
|
|
116
119
|
const [map, review] = await parallel([
|
|
117
120
|
() => agent("Map /absolute/repo read-only.", { label: "map", role: "explorer" }),
|
|
118
121
|
() => agent("Review /absolute/repo read-only.", { label: "review", role: "reviewer", harness: "codex" }),
|
|
119
122
|
]);
|
|
123
|
+
phase("Collect");
|
|
120
124
|
return { map, review };
|
|
121
125
|
|
|
122
126
|
${formatSavedWorkflows(workflows)}`;
|
package/src/pi-subagent.ts
CHANGED
|
@@ -76,7 +76,7 @@ export const agentToolParameters = Type.Object({
|
|
|
76
76
|
description: "The task briefing to send to the subagent.",
|
|
77
77
|
}),
|
|
78
78
|
background: Type.Optional(Type.Boolean({
|
|
79
|
-
description: "
|
|
79
|
+
description: "Defaults to false: wait for settlement. True returns a stable session-owned run handle after registration; work may already be settled. Use for independent work or an explicit background request. Retain the ID and collect with external_runs; no completion notification. Keep the owning session open.",
|
|
80
80
|
})),
|
|
81
81
|
role: Type.Optional(Type.String({
|
|
82
82
|
minLength: 1,
|
package/src/profile-creator.ts
CHANGED
|
@@ -36,13 +36,13 @@ A role is shared authoring only: one markdown file under pi-flow-external/roles/
|
|
|
36
36
|
|
|
37
37
|
Describe read-only or editing intent in the instructions. Do not ask for a permission tier: a role does not grant authority. The caller passes permission, or the global defaultPermission applies. Do not ask about backend, model, or thinking: shared roles never pin those, and backend-specific execution settings belong in /external config role set <role> --harness <name>; instruction replacements use /external config role edit <role> --harness <name>. When ready, summarize once and call ${ROLE_TOOL_NAME}.
|
|
38
38
|
|
|
39
|
-
Do not write files yourself and do not run external_delegate or
|
|
39
|
+
Do not write files yourself and do not run external_delegate or external_workflow. The tool will show what will be created for review, request confirmation, and write it offline without a backend smoke test. A role is not an authenticated connection; readiness smoke testing belongs to harness registration.`;
|
|
40
40
|
|
|
41
41
|
export const HARNESS_INTERVIEW_PROMPT = `Help me register one named Pi harness configuration through an AI-assisted interview.
|
|
42
42
|
|
|
43
43
|
Ask for a "pi-<label>" name, a provider/model id (validated live against the model registry), an optional thinking level (off/minimal/low/medium/high/xhigh; default off), and a resource preset. The preset is a simple choice of minimal or skills. minimal is the default and loads no skills. skills loads installed skills; project skills load only when the project is trusted. The six canonical roles (explorer, planner, implementer, reviewer, qa, worker) become automatically available on this harness the moment it is registered — no per-role file needed. When ready, summarize once and call ${HARNESS_TOOL_NAME}.
|
|
44
44
|
|
|
45
|
-
Do not write files yourself and do not run external_delegate or
|
|
45
|
+
Do not write files yourself and do not run external_delegate or external_workflow in this branch. The tool will show what will be registered for review, request confirmation, stage it, run a real in-process pi smoke test, and either install it or roll it back.`;
|
|
46
46
|
|
|
47
47
|
const roleParameters = Type.Object({
|
|
48
48
|
name: Type.String({ description: "Lowercase shared role name, such as security-reviewer. No backend prefix; reviewer replaces the built-in reviewer." }),
|
package/src/profiles.ts
CHANGED
|
@@ -264,7 +264,7 @@ export function loadExternalCatalog(agentDir = getAgentDir(), options: { legacyI
|
|
|
264
264
|
const { frontmatter } = parseFrontmatter<Record<string, unknown>>(content);
|
|
265
265
|
const obsolete = ["permission", "capabilitySet"].filter((key) => Object.prototype.hasOwnProperty.call(frontmatter, key));
|
|
266
266
|
if (obsolete.length) {
|
|
267
|
-
throw new Error(`obsolete metadata ${obsolete.join(", ")} does not grant authority. Remove it. A role describes intent; pass permission on the external_delegate or
|
|
267
|
+
throw new Error(`obsolete metadata ${obsolete.join(", ")} does not grant authority. Remove it. A role describes intent; pass permission on the external_delegate or external_workflow call, or set defaultPermission. Pi skills follow the harness preset.`);
|
|
268
268
|
}
|
|
269
269
|
if (Object.keys(frontmatter).some((key) => key !== "description")) throw new Error("shared roles support description only; use an exact override for execution settings");
|
|
270
270
|
} else {
|
package/src/prompts.ts
CHANGED
|
@@ -92,13 +92,15 @@ export function buildCoordinatorPrompt(
|
|
|
92
92
|
|
|
93
93
|
${formatExternalRoleCatalog(profiles, defaultHarness, configuredHarnessNames)}
|
|
94
94
|
|
|
95
|
-
Own the result and verify it yourself. Do the work directly unless a separate harness, a fresh context, or a bounded parallel review earns the overhead. A role is the job. A harness (${EXTERNAL_HARNESSES.join(", ")}, or a registered pi-* name) is the environment. Prefer the configured default harness unless the user asks for another or the task needs a capability the default lacks. Select both through external_delegate or
|
|
95
|
+
Own the result and verify it yourself. Do the work directly unless a separate harness, a fresh context, or a bounded parallel review earns the overhead. A role is the job. A harness (${EXTERNAL_HARNESSES.join(", ")}, or a registered pi-* name) is the environment. Prefer the configured default harness unless the user asks for another or the task needs a capability the default lacks. Select both through external_delegate or external_workflow. Follow the user's own approval policy before widening a delegation. This extension has no fixed multi-agent approval threshold.
|
|
96
|
+
|
|
97
|
+
Before external_workflow, explain the purpose, who does what, which tasks run in parallel or in sequence, allowed changes, and the expected deliverable in plain language. Put the same roadmap in meta.phases with short titles and details, and use phase() to mark actual transitions. Explain material plan changes; follow the user's existing approval policy. Planned phases describe intent, not proof of execution: actual child states and activity are authoritative.
|
|
96
98
|
|
|
97
99
|
Give every child an objective, the context it needs, the changes it may make, a deliverable, and the check you will run. Use absolute paths and say whether the task is read-only or may edit. Permission is the call's tier, otherwise defaultPermission (danger unless changed). Omit it when the child should keep that default, including a reviewer that may use a shell and still must not edit. A danger tier still has to stay inside the authorization the user gave you. agy accepts only danger and rejects readonly and edit.
|
|
98
100
|
|
|
99
101
|
Choose a fresh child or resume on purpose. Omit context for independent work. context {mode:"recent", turns:N} shares the last N user turns, including the current turn; {mode:"full"} shares the available post-compaction conversation. Snapshots drop thinking, system instructions, and pending calls, reject images and payloads over 1 MiB, and are sent to that harness. resume continues the same CLI child and cannot be combined with sharing. A named pi-* harness has no persisted session and no enforced budget. Prefer resume for the same task and a new child for an independent review.
|
|
100
102
|
|
|
101
|
-
|
|
103
|
+
Default to foreground (blocking), including parallel workflows. Use background:true only for useful independent work before collection or an explicit user request; do not routinely background then immediately wait. Retain the run ID and own collection unless the user explicitly defers it. Foreground has a live card; a background receipt is not a live monitor and completion never notifies or wakes this session. Use external_runs wait only when needed; check each run's outcome and pending IDs. Read inline successful output or inspect view final; inspect failures with diagnostics. Interrupting foreground cancels that call; interrupting wait only stops waiting. Explicit cancel stops background work. Keep the owning session open: orderly shutdown requests cancellation; restart does not adopt work. Prefer one foreground external_workflow with parallel() for coordinated parallel tasks; all children share maxConcurrentSubagents. Sequential awaits stay serial. Report milestones. Retry a failed run, or switch its model or harness, only when the user authorized that step. agy alone may make one disclosed infrastructure retry. Nested agents may start in another workspace.
|
|
102
104
|
|
|
103
105
|
Catalog availability reflects enabled harnesses and built-in/authored roles, not CLI installation or authentication. Disabled harnesses reject calls without fallback; manage them through /external config. Harness and role lifecycle commands live under /external config harness and /external config role. Role selection never falls back. external_help topic usage is the playbook; roles, permissions, and workflow are the references.`;
|
|
104
106
|
}
|
|
@@ -187,10 +189,12 @@ export function usageWorkflowScript(): string {
|
|
|
187
189
|
}
|
|
188
190
|
}`);
|
|
189
191
|
return [
|
|
190
|
-
'export const meta = { apiVersion: 1, name: "parallel-review", description: "Review in parallel and keep a sibling failure" };',
|
|
192
|
+
'export const meta = { apiVersion: 1, name: "parallel-review", description: "Review in parallel and keep a sibling failure", phases: [{ title: "Review", detail: "Claude maps the repository while Codex reviews concurrency; neither is assigned to edit files." }, { title: "Collect", detail: "Return both findings, including any failed check." }] };',
|
|
193
|
+
'phase("Review");',
|
|
191
194
|
"const settled = await parallel([",
|
|
192
195
|
branches.join(",\n"),
|
|
193
196
|
"]);",
|
|
197
|
+
'phase("Collect");',
|
|
194
198
|
"return { settled };",
|
|
195
199
|
"",
|
|
196
200
|
].join("\n");
|
|
@@ -203,7 +207,7 @@ function jsonBlock(value: unknown): string {
|
|
|
203
207
|
/** Worked playbook served by external_help topic "usage". Examples are the exported constants. */
|
|
204
208
|
export function formatUsagePlaybook(): string {
|
|
205
209
|
const script = usageWorkflowScript();
|
|
206
|
-
return `Practical delegation playbook. A role is the work. A harness is the environment: ${EXTERNAL_HARNESSES.join(", ")}, or a registered pi-* name. external_delegate and
|
|
210
|
+
return `Practical delegation playbook. A role is the work. A harness is the environment: ${EXTERNAL_HARNESSES.join(", ")}, or a registered pi-* name. external_delegate and external_workflow select every one of those harnesses the same way. Prefer the configured default harness. Name another when the user asks for it or the task needs a capability the default lacks. Do the task directly unless a separate environment, an independent context, or bounded parallel work is worth the coordination. You still own the outcome: read the result and check the deliverable. Follow the user's approval policy before widening a delegation. There is no fixed team-size threshold here.
|
|
207
211
|
|
|
208
212
|
A named pi-* harness runs in-process on the SDK's built-in tools. Preset minimal leaves skills unloaded. Preset skills loads installed skills, and project skills only when the project is trusted. Those presets select skill loading. They are not this extension's tools and they are not web search. Resume does not continue a pi-* child, and a budget there is recorded without enforcement.
|
|
209
213
|
|
|
@@ -211,7 +215,9 @@ Tell each child the objective, the context it needs, what it may change, the del
|
|
|
211
215
|
|
|
212
216
|
Choose fresh context or resume deliberately. Omit context, or pass {"mode":"none"}, for independent work such as a review. {"mode":"recent","turns":N} and {"mode":"full"} share parent conversation and cannot be combined with resume. resume continues one prior CLI run that recorded a session id, on the same backend: ${EXTERNAL_HARNESSES.join(", ")}. A named pi-* harness has no persisted session, so resume does not continue it. Prefer resume for the same task and a new child when you want an independent judgment.
|
|
213
217
|
|
|
214
|
-
|
|
218
|
+
Choose foreground by default, even for a long or parallel workflow: the original call waits for settlement and its live card carries progress. Use background:true for useful independent work or an explicit user request, not routinely followed by an immediate wait. The returned handle means accepted, not completed; the run may already be settled. Keep its ID and collect/check the outcome yourself unless the user explicitly defers collection. No completion notification wakes the session. Keep Pi open: orderly shutdown requests cancellation with bounded cleanup; crashes leave uncertain evidence and restart does not adopt work.
|
|
219
|
+
|
|
220
|
+
After independent work, use external_runs wait if settlement is not already known. Check ok for the operation, then each completed run's state.outcome and pending IDs; a failed workflow can return wait early even in mode all. Use successful output when delivery is inline (null/false/0 are valid); otherwise inspect view final, following cursor pages. For failures inspect diagnostics or output; final is empty without verified success. A successful workflow may still have children.failed when its script caught child errors; read the reports. Interrupting the original foreground call cancels its work. Interrupting wait only stops waiting; cancel explicitly to stop background work. Cancelling a workflow stops active children; cancelling one child throws a catchable child error. Both modes share permissions, budget enforcement, and concurrency limits. A failed or aborted run stays failed. Retry it, or switch model or harness, only when the user authorized that step. agy may make one disclosed infrastructure retry; that exception is not yours to extend.
|
|
215
221
|
|
|
216
222
|
One external_delegate call. Permission stays omitted; the prompt carries the no-edit intent:
|
|
217
223
|
${jsonBlock(USAGE_ONE_AGENT)}
|
|
@@ -219,11 +225,13 @@ ${jsonBlock(USAGE_ONE_AGENT)}
|
|
|
219
225
|
Independent review. Fresh context, a reviewer role, the default tier, no resume:
|
|
220
226
|
${jsonBlock(USAGE_INDEPENDENT_REVIEW)}
|
|
221
227
|
|
|
222
|
-
Background
|
|
228
|
+
Background launch. Save data.run.runId from a successful receipt, then do useful independent work before collection:
|
|
229
|
+
|
|
223
230
|
${jsonBlock(USAGE_BACKGROUND_AGENT)}
|
|
231
|
+
Later, wait if needed and check the outcome. If succeeded, use inline output; otherwise the following is the final-inspection shape (substitute the retained ID and follow nextCursor). For a failed run choose diagnostics instead:
|
|
224
232
|
${jsonBlock(USAGE_RUNS_FINAL)}
|
|
225
233
|
|
|
226
|
-
Parallel workflow. Catch the child error inside each branch so one failure does not drain its sibling. Pass this string as the
|
|
234
|
+
Parallel workflow. First explain the plan in plain language: purpose, assignments, order, allowed changes, and deliverable. Put the same roadmap in meta.phases and mark transitions with phase(). The live card carries progress while a blocking call runs; background receipts are not live monitors, so collect and inspect background work with external_runs. Catch the child error inside each branch so one failure does not drain its sibling. Pass this string as the external_workflow tool's script:
|
|
227
235
|
${script}
|
|
228
236
|
Resume, only for the same task on a CLI harness that stored a session id. Do not combine it with context:
|
|
229
237
|
${jsonBlock(USAGE_RESUME_AGENT)}
|
package/src/tool-names.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -61,18 +61,18 @@ export interface SubagentExtensionOptions {
|
|
|
61
61
|
* Maximum number of subagents allowed to run concurrently across the whole
|
|
62
62
|
* agent run (a global in-flight cap, not a per-level fan-out width). A slot is
|
|
63
63
|
* taken when a subagent launches and released when it completes, fails, or is
|
|
64
|
-
* aborted. The cap is shared by the `
|
|
64
|
+
* aborted. The cap is shared by the `external_delegate` tool and the `external_workflow` tool.
|
|
65
65
|
*/
|
|
66
66
|
maxConcurrentSubagents?: number;
|
|
67
67
|
/**
|
|
68
68
|
* Maximum wall-clock runtime for each launched subagent, in milliseconds.
|
|
69
69
|
* Defaults to a generous global guardrail. Set to 0 to disable. The limit is
|
|
70
|
-
* shared by direct `
|
|
70
|
+
* shared by direct `external_delegate` calls and workflow `agent()` calls, and can also
|
|
71
71
|
* be overridden with `--subagent-timeout-ms`.
|
|
72
72
|
*/
|
|
73
73
|
subagentTimeoutMs?: number;
|
|
74
74
|
/**
|
|
75
|
-
* Register the dynamic `
|
|
75
|
+
* Register the dynamic `external_workflow` tool alongside `external_delegate`. Defaults to true:
|
|
76
76
|
* one product, two entry points. Set to false for a subagents-only surface.
|
|
77
77
|
*/
|
|
78
78
|
workflow?: boolean;
|
|
@@ -231,6 +231,9 @@ function validateMeta(meta: unknown): asserts meta is WorkflowMeta {
|
|
|
231
231
|
if (!phase || typeof phase !== "object" || typeof (phase as WorkflowMetaPhase).title !== "string") {
|
|
232
232
|
throw new Error("each meta phase must have a title string");
|
|
233
233
|
}
|
|
234
|
+
if (phase.detail !== undefined && typeof phase.detail !== "string") {
|
|
235
|
+
throw new Error("meta phase detail must be a string");
|
|
236
|
+
}
|
|
234
237
|
}
|
|
235
238
|
}
|
|
236
239
|
}
|
package/src/workflow/source.ts
CHANGED
|
@@ -20,13 +20,14 @@ import type { WorkflowCachedAgentResult, WorkflowMetaPhase } from "./types.ts";
|
|
|
20
20
|
|
|
21
21
|
export const workflowToolParameters = Type.Object({
|
|
22
22
|
background: Type.Optional(Type.Boolean({
|
|
23
|
-
description: "
|
|
23
|
+
description: "Defaults to false: wait for settlement. True returns a stable session-owned workflow handle after registration; work may already be settled. Use for independent work or an explicit background request. Retain the ID and collect with external_runs; no completion notification. Keep the owning session open.",
|
|
24
24
|
})),
|
|
25
25
|
script: Type.Optional(
|
|
26
26
|
Type.String({
|
|
27
27
|
description: [
|
|
28
28
|
"Raw JavaScript workflow script (no Markdown fences) for an ad-hoc workflow.",
|
|
29
29
|
"First statement: export const meta = { apiVersion: 1, name: 'short_name', description: 'non-empty' }.",
|
|
30
|
+
"Explain the plan in plain language before launch. Add meta.phases: [{ title, detail }] for the roadmap and call phase(title) at actual transitions.",
|
|
30
31
|
"Use agent(prompt, opts), parallel(thunks), pipeline(items, ...stages), phase(title), log(message), args, cwd. Must call agent() at least once and return a JSON-serializable value. Results are canonicalized to JSON; non-plain objects are rejected.",
|
|
31
32
|
"Provide exactly one of `script`, `name`, or `scriptPath`.",
|
|
32
33
|
].join(" "),
|
package/src/workflow/tool.ts
CHANGED
|
@@ -20,8 +20,9 @@ import { runRecordsDirectory } from "../core/retention.ts";
|
|
|
20
20
|
import { captureParentContext } from "../core/parent-context.ts";
|
|
21
21
|
import { RunRegistry, type RegisteredRunHandle } from "../core/run-registry.ts";
|
|
22
22
|
import { ExpectedFlowError, type FlowErrorCode } from "../core/errors.ts";
|
|
23
|
-
import { withContract } from "../contract/envelope.ts";
|
|
23
|
+
import { redactedText, withContract } from "../contract/envelope.ts";
|
|
24
24
|
import { workflowOutputSchema, workflowReceipt } from "../contract/workflow.ts";
|
|
25
|
+
import { EXTERNAL_WORKFLOW_TOOL } from "../tool-names.ts";
|
|
25
26
|
import { loadWorkflowJournal } from "./journal.ts";
|
|
26
27
|
import { OUTPUT_PREVIEW_CHARS } from "../core/progress.ts";
|
|
27
28
|
import { loadExternalCatalog, resolveExternalProfile, selectorHarness, unknownExternalProfileMessage } from "../profiles.ts";
|
|
@@ -135,14 +136,14 @@ export function createWorkflowTool(
|
|
|
135
136
|
options: CreateWorkflowToolOptions,
|
|
136
137
|
): ToolDefinition<typeof workflowToolParameters, WorkflowToolDetails> {
|
|
137
138
|
return defineTool({
|
|
138
|
-
name:
|
|
139
|
-
label:
|
|
140
|
-
description: "Run a saved, persisted, or ad-hoc trusted JavaScript workflow that orchestrates external roles.",
|
|
139
|
+
name: EXTERNAL_WORKFLOW_TOOL,
|
|
140
|
+
label: EXTERNAL_WORKFLOW_TOOL,
|
|
141
|
+
description: "Run a saved, persisted, or ad-hoc trusted JavaScript workflow that orchestrates external roles. Before launch, explain purpose, assignments, parallel/sequential order, allowed changes, and deliverable in plain language.",
|
|
141
142
|
promptSnippet: WORKFLOW_PROMPT_SNIPPET,
|
|
142
143
|
parameters: workflowToolParameters,
|
|
143
144
|
outputSchema: workflowOutputSchema,
|
|
144
145
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
145
|
-
return await withContract<WorkflowToolDetails>({ tool:
|
|
146
|
+
return await withContract<WorkflowToolDetails>({ tool: EXTERNAL_WORKFLOW_TOOL, action: "run", failureData: { run: null }, redact: true }, async () => {
|
|
146
147
|
const parentMessages = captureParentContext(ctx.sessionManager);
|
|
147
148
|
const sessionId = ctx.sessionManager?.getSessionId?.() ?? `unpersisted:${toolCallId}`;
|
|
148
149
|
const sessionVersion = options.registry.sessionVersion(sessionId);
|
|
@@ -783,6 +784,8 @@ function renderPhaseTree(container: Container, details: WorkflowToolDetails, the
|
|
|
783
784
|
0,
|
|
784
785
|
),
|
|
785
786
|
);
|
|
787
|
+
const detail = details.plannedPhases?.find((planned) => planned.title === phase)?.detail;
|
|
788
|
+
if (detail) container.addChild(new Text(` ${theme.fg("muted", redactedText(detail))}`, 0, 0));
|
|
786
789
|
const shown = selectAgentsForRender(agents);
|
|
787
790
|
for (const agent of shown) {
|
|
788
791
|
container.addChild(renderSubagentNode(agent, theme, frame, runningCount, " ", Date.now(), expanded, false));
|