@henryqw/pi-subagent 6.1.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +2 -2
- package/README.md +18 -6
- package/dist/index.d.ts +8 -2
- package/dist/index.js +7 -2
- package/docs/orchestration.md +74 -49
- package/extensions/delegate-flow.ts +39 -11
- package/extensions/subagent.ts +52 -22
- package/package.json +2 -2
- package/skills/pi-subagent-delegated-development/SKILL.md +2 -0
package/CONTEXT.md
CHANGED
|
@@ -28,10 +28,10 @@ Provide validated built-in and user Roles, shared task-model Pi launch policy, g
|
|
|
28
28
|
- Up to five active ephemeral `delegate_task` children run per Main by default, configurable via `maxSubagents` in `~/.pi/agent/config/pi-subagent/pi-subagent.json` or the `PI_SUBAGENT_MAX_SUBAGENTS` environment variable; excess calls wait FIFO. Queued calls do not start a child or consume child timeout. Managed Herdr workers are unaffected.
|
|
29
29
|
- Ambient child extensions and Skills stay disabled. Every Role requires `tools`, `extensions`, and `skills` YAML arrays, and every launch installs the Role tool policy. `tools: []` activates no base built-ins but does activate all tools from explicitly selected trusted extension bundles and explicit caller tool additions; `skills: []` selects no separately named Role Skills but trusted selected extension Skills still load; `extensions: []` selects no Role extension bundle. A Role/caller explicitly selected extension is a trusted atomic capability bundle: all tools it registers and all Skills supplied through its Pi package metadata or dynamic `resources_discover` load alongside separately named Role Skills. This intentionally includes the extension's executable lifecycle/prompt behavior; pi-subagent does not infer or externally narrow undocumented dependencies, and loading an extension is not sandboxing. Scope children by selecting fewer trusted extensions; finer granularity requires separate entry points/configuration or an upstream split. Explicit Role/caller tool names still verify against the final filtered registry, while parent-only recursive orchestration tools remain excluded.
|
|
30
30
|
- Role Skill names resolve through Main's effective Pi Skill registry; unavailable names warn and skip without blocking delegation. Explicit Role/caller tool names verify against the final filtered child registry after explicit provider `session_start` handlers, and unavailable names fail before the first turn.
|
|
31
|
-
- Main policy populates direct `model` and `thinking` only for explicit user overrides; otherwise it selects only `modelClass` (`fast` normally, `balanced` upfront for obvious complexity). This has no provenance tracking or runtime enforcement. An omitted class uses
|
|
31
|
+
- Main policy populates direct `model` and `thinking` only for explicit user overrides; otherwise it selects only `modelClass` (`fast` normally, `balanced` upfront for obvious complexity). This has no provenance tracking or runtime enforcement. An omitted class uses pi-subagent's local `pi-subagent/delegateTask` Model Task declaration (default `fast`); library callers select a Role plus their own Model Task declaration.
|
|
32
32
|
- The selected profile resolves primary then fallback only before launch when a route, model, or thinking level is unavailable. If neither route is usable, launch rejects with `Run /task-models`; a started child is never retried by this package.
|
|
33
33
|
- User Role Markdown files and Subagent JSON config live only in the user `config/pi-subagent` directory; model routes live in shared `config/pi-task-models.json`. Package-shipped built-in Roles (`implementer`, `reviewer`) resolve from the package's own `examples/roles/` Markdown through the same parser; a same-named user file explicitly overrides a built-in for `delegate_task` and `delegate_flow`.
|
|
34
|
-
- `delegate_flow` accepts 1–8 independent units with direct validation commands, optional `modelClass`, and optional non-empty `review` judgment text. Omitted classes use
|
|
34
|
+
- `delegate_flow` accepts 1–8 independent units with direct validation commands, optional `modelClass`, and optional non-empty `review` judgment text. Omitted classes use pi-subagent's local `pi-subagent/delegateTask` declaration (default `fast`); each unit's current class resolves through its shared profile route for its Implementer and, when `review` exists, Reviewer. At Flow start it always resolves/freezes the effective Implementer and resolves/freezes a Reviewer only when at least one requested unit has `review`. One active Flow creates every Unit Worktree before launching Implementers in parallel, then processes settled units in declared order. For each unit Flow rebases in place when earlier units advanced Main, inspects committed Git state, and runs declared validation. Validation is authoritative for objective verification: units without `review` fast-forward their exact validated tip; units with `review` send the exact Review Packet to the Reviewer in the same worktree and require exact `PASS` before the same guarded `git merge --ff-only` path.
|
|
35
35
|
- Flow is memory-only. Only a post-rebase commit drop produces a no-op (`base === tip`); it validates, skips Reviewer and merge, then cleans up ordinarily. Initial zero-commit implementations block. Implementer, validation, or reviewer blocks allow one `delegate_flow_continue({ guidance, modelClass? })` repair in the same worktree; omission retains the Unit's current class and presence replaces it for that repair. A second block is terminal. Rebase and evidence/Reviewer/infrastructure failures retain worktrees. A reported fast-forward failure retains its worktree unless Main is clean at the exact integrated tip, which completes with the merge diagnostic as a warning. Cleanup uses non-forced worktree removal and branch deletion; cleanup refusal is a completion warning.
|
|
36
36
|
- Flow has no graph, saved recovery, automatic retry, aggregate review, or post-merge validation. It never changes generic `delegate_task` Role resolution, isolation, non-Git fallback, or ordinary direct plan/file review.
|
|
37
37
|
- Numbered Codex routes prefer Main's active account slot and explicitly load the multi-Codex child extension.
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ pi install npm:@henryqw/pi-subagent
|
|
|
30
30
|
| `delegate_flow` | tool | Package-owned parallel implementation and declared-order Git integration for 1–8 independent units. |
|
|
31
31
|
| `delegate_flow_continue` | tool | Repair the blocked Flow unit once in its existing worktree. |
|
|
32
32
|
|
|
33
|
-
All three delegation tool blocks use
|
|
33
|
+
All three delegation tool blocks use Pi's default boxed shell and background. Their compact custom content is an immutable call label, foreground aggregate partial-result status, and bounded final summaries—no expanded view.
|
|
34
34
|
|
|
35
35
|
### `delegate_task`
|
|
36
36
|
|
|
@@ -47,13 +47,13 @@ Select exactly one shape:
|
|
|
47
47
|
{ chain: [{ role, task, model?, modelClass?, thinking? }], background? }
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
`model` is `provider/modelId` and overrides `modelClass`. Main populates `model` and `thinking` only for an explicit user override; otherwise it chooses only `modelClass`—`fast` normally, or `balanced` upfront for obviously complex work. This is Main policy only: the runtime records no provenance and does not enforce it. `modelClass` is `fast`, `balanced`, `frontier`, or `fav`; omission uses
|
|
50
|
+
`model` is `provider/modelId` and overrides `modelClass`. Main populates `model` and `thinking` only for an explicit user override; otherwise it chooses only `modelClass`—`fast` normally, or `balanced` upfront for obviously complex work. This is Main policy only: the runtime records no provenance and does not enforce it. `modelClass` is `fast`, `balanced`, `frontier`, or `fav`; omission uses pi-subagent's local `pi-subagent/delegateTask` Model Task declaration (default `fast`), which shared config can explicitly override. `background` applies to the entire selected mode and is never a per-delegation field.
|
|
51
51
|
|
|
52
52
|
Parallel mode starts entries concurrently, waits for every entry, and reports them in input order. Chain mode is sequential and fail-fast; every literal `{previous}` receives only the immediately preceding successful assistant output. Foreground failures throw after retaining bounded sibling and recovery evidence. One tool call has one aggregate 50 KiB Main-visible transport cap, not 50 KiB per child.
|
|
53
53
|
|
|
54
54
|
Background workflows are session-scoped. Session shutdown or reload aborts them and may deliver only recoverable-work evidence or no follow-up message.
|
|
55
55
|
|
|
56
|
-
The transient
|
|
56
|
+
The transient status widget renders one line per child with: status glyph, role, status label, task summary, activity (thinking… or active tool with elapsed time and path basename), and metrics (completed turns, started tools, model, thinking level, tokens, total duration). Rows are ordered active-first (working items first, stable insertion order for the rest). A hard six-physical-line maximum applies: when total items are six or fewer, all child rows render; above six, five child rows plus one status-aware overflow line render (`… N more · X working · Y complete · Z failed · W stopped`). Terminal rows clear on the next real user input; active rows persist until the child settles. The final `delegate_task` block is deliberately minimal: bounded final summaries with role attribution for parallel/chain, and only retained-worktree recovery paths. It has no expanded view.
|
|
57
57
|
|
|
58
58
|
Each delegation resolves its own Role, resources, route, and optional worktree request. When available, `isolation: worktree` gives each entry a deterministic separate worktree; non-Git or unborn-`HEAD` contexts may use Main's cwd. Siblings and chain steps never implicitly share one created worktree.
|
|
59
59
|
|
|
@@ -70,15 +70,27 @@ delegate_flow_continue({ guidance, modelClass? })
|
|
|
70
70
|
|
|
71
71
|
Objective verification is authoritative. Flow always inspects committed Git state and runs declared validation. A unit without `review` skips review evidence and Reviewer launch, then fast-forwards its exact validated tip through the existing guarded `git merge --ff-only` path. Add `review` only for an explicit judgment that automation cannot establish; that unit retains the exact `{base, tip, patchPath}` protocol and requires exact `PASS` before the same integration path.
|
|
72
72
|
|
|
73
|
-
One memory-only Flow may be active. At start it resolves/freezes the effective `implementer` Role, including a same-named user override, and resolves/freezes the effective `reviewer` only if at least one requested unit has `review`. Omitted `modelClass` uses
|
|
73
|
+
One memory-only Flow may be active. At start it resolves/freezes the effective `implementer` Role, including a same-named user override, and resolves/freezes the effective `reviewer` only if at least one requested unit has `review`. Omitted `modelClass` uses pi-subagent's local `pi-subagent/delegateTask` declaration (default `fast`); a selected class resolves through its shared profile model-and-thinking route for the unit's Implementer and, when applicable, Reviewer. It creates one Unit Worktree per unit, runs Implementers in parallel, then processes settled results in declared order. It removes the worktree and branch non-forcibly after integration; a refusal is a completion warning with the retained worktree path and/or branch.
|
|
74
74
|
|
|
75
75
|
A rebase that drops all unit commits is a no-op: Flow validates it, skips Reviewer and merge, then cleans up ordinarily. Implementer, validation, or review blocks can be repaired once through `delegate_flow_continue` in the same worktree. Omitted continuation `modelClass` retains the blocked unit's current class; a supplied class replaces it for that one repair. Rebase and infrastructure failures are terminal. A reported fast-forward failure completes with its diagnostic as a warning only when Git left Main clean at the exact integrated tip; otherwise it is terminal and retains the affected worktree. Flow has no graph, saved recovery, automatic retry, aggregate review, or post-merge gate.
|
|
76
76
|
|
|
77
77
|
`delegate_task` remains generic with its ordinary isolation behavior. Flow uses the package-shipped Implementer by default and the package-shipped Reviewer only when a unit requests review; same-named user Roles remain supported overrides.
|
|
78
78
|
|
|
79
|
+
### Delegate UI summary
|
|
80
|
+
|
|
81
|
+
| Aspect | Behavior |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| Call label | `delegate_task · single/parallel/chain · N task(s)`; `delegate_flow · parallel→serial · N unit(s)`; `delegate_flow_continue · repair continuation` |
|
|
84
|
+
| Partial progress | `delegate_task`: aggregate counts (running/pending/complete/failed/skipped); `delegate_flow`: phase transitions (setup → implement → verify/integrate → review → repair) |
|
|
85
|
+
| Widget rows | One line per child: glyph, role, status, task, activity, metrics |
|
|
86
|
+
| Ordering | Active-first stable (working first, then insertion order) |
|
|
87
|
+
| Line cap | 6 physical lines max (≤6 items: all child rows; >6 items: 5 rows + 1 status-aware overflow) |
|
|
88
|
+
| Terminal retention | Active rows persist; terminal rows clear on next user input |
|
|
89
|
+
| Final result | Bounded summaries with recovery paths; no expanded view |
|
|
90
|
+
|
|
79
91
|
## Config
|
|
80
92
|
|
|
81
|
-
pi-subagent owns the extension-named config directory `~/.pi/agent/config/pi-subagent/`, which holds two kinds of user-owned configuration: one Markdown file per Role (see [Roles](#roles)) and its own optional JSON file below. Model routing is *not* configured here; children resolve routes through the shared `@henryqw/pi-task-models` config at `~/.pi/agent/config/pi-task-models.json
|
|
93
|
+
pi-subagent owns the extension-named config directory `~/.pi/agent/config/pi-subagent/`, which holds two kinds of user-owned configuration: one Markdown file per Role (see [Roles](#roles)) and its own optional JSON file below. Model routing is *not* configured here; children resolve routes through the shared `@henryqw/pi-task-models` config at `~/.pi/agent/config/pi-task-models.json`, which stores only explicit task overrides. The local `pi-subagent/delegateTask` declaration supplies the omitted-class default.
|
|
82
94
|
|
|
83
95
|
`~/.pi/agent/config/pi-subagent/pi-subagent.json` controls the ephemeral child pool and timeouts. All fields are optional; a missing file uses defaults.
|
|
84
96
|
|
|
@@ -143,4 +155,4 @@ A Role explicitly owns base tools, extensions, named Skills, instructions, and o
|
|
|
143
155
|
|
|
144
156
|
The package root exports Role loading and launch resolution, `createEphemeralSubagentExecutor`, worktree helpers, and generic managed Herdr lifecycle helpers. The ephemeral executor is for code already running inside active Pi; it does not provide standalone Node.js Pi discovery or launch support. After Pi itself exits, it drains inherited stdout/stderr normally but destroys streams still held by escaped descendants after a short inactivity deadline or one-second hard deadline, so they cannot retain a pool permit.
|
|
145
157
|
|
|
146
|
-
Use [`docs/orchestration.md`](./docs/orchestration.md#public-role-and-executor-api) for exact API behavior and a post-permit `prepare` example using `resolveRoleLaunch` against the latest Pi context.
|
|
158
|
+
Use [`docs/orchestration.md`](./docs/orchestration.md#public-role-and-executor-api) for exact API behavior and a post-permit `prepare` example using `resolveRoleLaunch` with a caller-owned Model Task declaration against the latest Pi context.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type HerdrExecutor } from "@henryqw/pi-herdr";
|
|
3
|
-
import { type AvailableModel, type ProfileName, type ResolvedTaskRoute, type ThinkingLevel } from "@henryqw/pi-task-models";
|
|
3
|
+
import { type AvailableModel, type ModelTask, type ProfileName, type ResolvedTaskRoute, type ThinkingLevel } from "@henryqw/pi-task-models";
|
|
4
4
|
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, EphemeralSubagentError, formatDuration, type EphemeralSubagentActivityEvent, type EphemeralSubagentErrorCode, type EphemeralSubagentExecutor, type EphemeralSubagentExecutorOptions, type EphemeralSubagentResult, type EphemeralSubagentRunInput, type EphemeralSubagentTimeout, } from "./ephemeral.ts";
|
|
5
5
|
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, worktreeContextNote, type WorktreeDirtyInspection, type WorktreeInfo, type WorktreePayload, } from "./worktree.ts";
|
|
6
6
|
export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, type PreparedReviewEvidence, type PrepareExactReviewEvidenceInput, } from "./review-evidence.ts";
|
|
7
7
|
export declare const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
|
|
8
8
|
export declare const CHILD_EXCLUDED_TOOL_NAMES: readonly ["delegate_task", "delegate_flow", "delegate_flow_continue", "ask_question"];
|
|
9
9
|
export declare const CHILD_EXCLUDED_TOOLS: string;
|
|
10
|
+
export declare const DELEGATE_TASK: {
|
|
11
|
+
readonly id: "pi-subagent/delegateTask";
|
|
12
|
+
readonly label: "Subagent delegation";
|
|
13
|
+
readonly purpose: "Launch an isolated Pi subagent.";
|
|
14
|
+
readonly defaultProfile: "fast";
|
|
15
|
+
};
|
|
10
16
|
export interface Role {
|
|
11
17
|
name: string;
|
|
12
18
|
description: string;
|
|
@@ -33,7 +39,7 @@ export interface CreateRoleLaunchInput {
|
|
|
33
39
|
env?: Readonly<Record<string, string>>;
|
|
34
40
|
}
|
|
35
41
|
export interface ResolveRoleLaunchInput extends Omit<CreateRoleLaunchInput, "route"> {
|
|
36
|
-
|
|
42
|
+
task: ModelTask;
|
|
37
43
|
agentDir?: string;
|
|
38
44
|
}
|
|
39
45
|
export interface ResolvedRoleSkills {
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,12 @@ export const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
|
|
|
15
15
|
export const CHILD_EXCLUDED_TOOL_NAMES = ["delegate_task", "delegate_flow", "delegate_flow_continue", "ask_question"];
|
|
16
16
|
export const CHILD_EXCLUDED_TOOLS = CHILD_EXCLUDED_TOOL_NAMES.join(",");
|
|
17
17
|
const CHILD_IDENTITY_POLICY = "You are a delegated Pi Subagent, not Main. Execute the assigned Role and task directly. Main-only delegation rules do not apply. Recursive delegation is unavailable; do not seek or invoke delegation tools.";
|
|
18
|
+
export const DELEGATE_TASK = {
|
|
19
|
+
id: "pi-subagent/delegateTask",
|
|
20
|
+
label: "Subagent delegation",
|
|
21
|
+
purpose: "Launch an isolated Pi subagent.",
|
|
22
|
+
defaultProfile: "fast",
|
|
23
|
+
};
|
|
18
24
|
const cleanText = (value, field, source) => {
|
|
19
25
|
if (typeof value !== "string" || !value.trim() || value.includes("\0")) {
|
|
20
26
|
throw new Error(`${source}: ${field} must be non-empty text.`);
|
|
@@ -186,10 +192,9 @@ export function createRoleLaunch(pi, ctx, input) {
|
|
|
186
192
|
};
|
|
187
193
|
}
|
|
188
194
|
export function resolveRoleLaunch(pi, ctx, input) {
|
|
189
|
-
const taskId = cleanText(input.taskId, "task ID", "Role launch");
|
|
190
195
|
return createRoleLaunch(pi, ctx, {
|
|
191
196
|
...input,
|
|
192
|
-
route: resolveConfiguredTaskRoute(ctx,
|
|
197
|
+
route: resolveConfiguredTaskRoute(ctx, input.task, input.agentDir),
|
|
193
198
|
});
|
|
194
199
|
}
|
|
195
200
|
function launchEnvironmentArgs(launch) {
|
package/docs/orchestration.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
```text
|
|
6
6
|
Role (built-in or user override) + latest Pi registries ── resolveRoleLaunch ──> PiLaunch
|
|
7
7
|
│
|
|
8
|
-
caller-owned
|
|
8
|
+
caller-owned Model Task declaration, cwd, signal ────────┤
|
|
9
9
|
v
|
|
10
10
|
active-Pi ephemeral executor
|
|
11
11
|
```
|
|
@@ -68,7 +68,7 @@ Single mode puts one delegation's fields at the top level.
|
|
|
68
68
|
| `role` | yes | Name of a Role in the user's effective `config/pi-subagent` directory or a package-shipped built-in (`implementer`, `reviewer`); a same-named user file overrides the built-in. |
|
|
69
69
|
| `task` | yes | Non-empty bounded task packet. |
|
|
70
70
|
| `model` | no | Designated `provider/modelId`; takes precedence over `modelClass`, and Main supplies it only for an explicit user override. |
|
|
71
|
-
| `modelClass` | no | `fast`, `balanced`, `frontier`, or `fav`; Main normally chooses `fast`, may choose `balanced` upfront for obvious complexity, and omission uses
|
|
71
|
+
| `modelClass` | no | `fast`, `balanced`, `frontier`, or `fav`; Main normally chooses `fast`, may choose `balanced` upfront for obvious complexity, and omission uses pi-subagent's local Model Task declaration. |
|
|
72
72
|
| `thinking` | no | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`; Main supplies it only for an explicit user override. Route selection skips models that cannot honor it. |
|
|
73
73
|
|
|
74
74
|
Those five fields are the complete delegation object. The direct-model/thinking rule is Main-facing policy only: the runtime adds no provenance tracking or enforcement. `tasks`, `chain`, and `background` cannot be nested. Route fallback occurs only before launch; a started child is never retried by this package.
|
|
@@ -85,7 +85,7 @@ All Main-visible text for one tool call shares one aggregate 50 KiB UTF-8 transp
|
|
|
85
85
|
|
|
86
86
|
`delegate_flow({ units })` accepts 1–8 units with unique non-empty `id` and `task`, one or more direct `{command, args}` validation commands, optional `modelClass`, and optional non-empty `review` text. `delegate_flow_continue({ guidance, modelClass? })` is available only for the one blocked unit of the active Flow.
|
|
87
87
|
|
|
88
|
-
A Flow is memory-only and permits one active Flow. At start it always resolves/freezes the effective `implementer` Role, including a same-named user override. It resolves/freezes the effective `reviewer` only if at least one requested unit declares `review`. Omitted unit classes use
|
|
88
|
+
A Flow is memory-only and permits one active Flow. At start it always resolves/freezes the effective `implementer` Role, including a same-named user override. It resolves/freezes the effective `reviewer` only if at least one requested unit declares `review`. Omitted unit classes use pi-subagent's local `pi-subagent/delegateTask` declaration (default `fast`); a selected class resolves through its existing `pi-task-models` profile model-and-thinking route for the unit's Implementer and, when applicable, Reviewer. It requires clean committed Git Main and creates every Unit Worktree before launching work; setup failure launches no Implementer. Each unit gets exactly one worktree and one Implementer. Implementers run in parallel and all settle. Flow then processes units in declared order:
|
|
89
89
|
|
|
90
90
|
```text
|
|
91
91
|
Implementers (parallel, one Unit Worktree each)
|
|
@@ -149,12 +149,12 @@ The package root exports the following mechanism-level APIs:
|
|
|
149
149
|
| --- | --- |
|
|
150
150
|
| `loadRoles(agentDir?)` | Validate and load package-shipped built-in and user Role Markdown. |
|
|
151
151
|
| `resolveRoleSkills(pi, role)` | Resolve Role Skill names from Pi's effective registry. |
|
|
152
|
-
| `resolveRoleLaunch(pi, ctx, input)` | Resolve a
|
|
152
|
+
| `resolveRoleLaunch(pi, ctx, input)` | Resolve a caller-owned Model Task route and produce `ResolvedRoleLaunch`. |
|
|
153
153
|
| `createRoleLaunch(pi, ctx, input)` | Produce the same launch from a caller-supplied resolved route. |
|
|
154
154
|
| `createEphemeralSubagentExecutor(options)` | Queue and run one prepared no-session child per `run`. |
|
|
155
155
|
| `createChildWorktree` / `finalizeChildWorktree` | Optional caller-managed worktree lifecycle. |
|
|
156
156
|
|
|
157
|
-
A loaded `Role` contains `name`, `description`, required normalized `tools`, `extensions`, and `skills` arrays, optional `isolation`, and `systemPrompt`. `resolveRoleLaunch` accepts `role`, `
|
|
157
|
+
A loaded `Role` contains `name`, `description`, required normalized `tools`, `extensions`, and `skills` arrays, optional `isolation`, and `systemPrompt`. `resolveRoleLaunch` accepts `role`, a caller-owned `task` Model Task declaration, and optional caller `agentDir`, `extensions`, `tools`, and `env`. At extension load, callers invoke `registerModelTask(pi, task)` from `@henryqw/pi-task-models` once to expose that declaration in the shared control plane. Its result is a `PiLaunch` (`{ env, args }`) plus the selected `model`, `thinkingLevel`, and `missingSkills`.
|
|
158
158
|
|
|
159
159
|
`createEphemeralSubagentExecutor` requires:
|
|
160
160
|
|
|
@@ -178,54 +178,69 @@ import {
|
|
|
178
178
|
createEphemeralSubagentExecutor,
|
|
179
179
|
resolveRoleLaunch,
|
|
180
180
|
} from "@henryqw/pi-subagent";
|
|
181
|
+
import { registerModelTask } from "@henryqw/pi-task-models";
|
|
181
182
|
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
const MODEL_TASK = {
|
|
184
|
+
id: "your-package/delegate",
|
|
185
|
+
label: "Package delegation",
|
|
186
|
+
purpose: "Run one package-owned delegated task.",
|
|
187
|
+
defaultProfile: "fast",
|
|
188
|
+
};
|
|
186
189
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
pi
|
|
190
|
-
latestCtx = { ...ctx, model: event.model };
|
|
191
|
-
});
|
|
192
|
-
pi.on("agent_settled", (_event, ctx) => { latestCtx = ctx; });
|
|
190
|
+
export function createRunRole(pi) {
|
|
191
|
+
// Register the Model Task at extension load so /task-models discovery works.
|
|
192
|
+
registerModelTask(pi, MODEL_TASK);
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
194
|
+
const executor = createEphemeralSubagentExecutor({
|
|
195
|
+
maxConcurrency: 4,
|
|
196
|
+
timeout: { idleMs: 10 * 60_000, maxMs: 30 * 60_000 },
|
|
197
|
+
});
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
extensions = [],
|
|
204
|
-
tools,
|
|
205
|
-
env = {},
|
|
206
|
-
} = options;
|
|
207
|
-
|
|
208
|
-
return executor.run({
|
|
209
|
-
signal,
|
|
210
|
-
prepare: async () => {
|
|
211
|
-
// prepare runs only after this delegation owns a FIFO permit.
|
|
212
|
-
const ctx = latestContext();
|
|
213
|
-
const launch = resolveRoleLaunch(pi, ctx, {
|
|
214
|
-
role,
|
|
215
|
-
taskId: "your-package/delegate",
|
|
216
|
-
extensions,
|
|
217
|
-
tools,
|
|
218
|
-
env,
|
|
219
|
-
});
|
|
220
|
-
if (launch.missingSkills.length && ctx.hasUI) {
|
|
221
|
-
ctx.ui.notify(
|
|
222
|
-
`Skipped unavailable Skills: ${launch.missingSkills.join(", ")}`,
|
|
223
|
-
"warning",
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
return { launch, task, cwd: cwd ?? ctx.cwd };
|
|
227
|
-
},
|
|
199
|
+
let latestCtx;
|
|
200
|
+
pi.on("session_start", (_event, ctx) => { latestCtx = ctx; });
|
|
201
|
+
pi.on("model_select", (event, ctx) => {
|
|
202
|
+
latestCtx = { ...ctx, model: event.model };
|
|
228
203
|
});
|
|
204
|
+
pi.on("agent_settled", (_event, ctx) => { latestCtx = ctx; });
|
|
205
|
+
|
|
206
|
+
function latestContext() {
|
|
207
|
+
if (!latestCtx) throw new Error("Pi session has not started.");
|
|
208
|
+
return latestCtx;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function runRole(role, task, options = {}) {
|
|
212
|
+
const {
|
|
213
|
+
signal,
|
|
214
|
+
cwd,
|
|
215
|
+
extensions = [],
|
|
216
|
+
tools,
|
|
217
|
+
env = {},
|
|
218
|
+
} = options;
|
|
219
|
+
|
|
220
|
+
return executor.run({
|
|
221
|
+
signal,
|
|
222
|
+
prepare: async () => {
|
|
223
|
+
// prepare runs only after this delegation owns a FIFO permit.
|
|
224
|
+
const ctx = latestContext();
|
|
225
|
+
const launch = resolveRoleLaunch(pi, ctx, {
|
|
226
|
+
role,
|
|
227
|
+
task: MODEL_TASK,
|
|
228
|
+
extensions,
|
|
229
|
+
tools,
|
|
230
|
+
env,
|
|
231
|
+
});
|
|
232
|
+
if (launch.missingSkills.length && ctx.hasUI) {
|
|
233
|
+
ctx.ui.notify(
|
|
234
|
+
`Skipped unavailable Skills: ${launch.missingSkills.join(", ")}`,
|
|
235
|
+
"warning",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return { launch, task, cwd: cwd ?? ctx.cwd };
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { runRole };
|
|
229
244
|
}
|
|
230
245
|
```
|
|
231
246
|
|
|
@@ -249,7 +264,17 @@ Generic managed Herdr exports (`managedSubagentWorkspaceId`, reconciliation help
|
|
|
249
264
|
|
|
250
265
|
## JavaScript composition
|
|
251
266
|
|
|
252
|
-
The examples below use caller-selected `Role` objects and the `runRole`
|
|
267
|
+
The examples below use caller-selected `Role` objects and the `runRole` function returned by the package's initializer. A consuming Pi extension calls the initializer once at startup:
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
import { createRunRole } from "your-package";
|
|
271
|
+
|
|
272
|
+
export default function yourExtension(pi) {
|
|
273
|
+
const { runRole } = createRunRole(pi);
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Variable names such as `reviewRole` are local bindings, not reserved Role names. The executor's `maxConcurrency` bounds launches; callers must also bound collections and loops.
|
|
253
278
|
|
|
254
279
|
A small caller-owned failure policy keeps the examples readable:
|
|
255
280
|
|
|
@@ -108,6 +108,7 @@ type FlowState = {
|
|
|
108
108
|
};
|
|
109
109
|
|
|
110
110
|
type UsageMeter = { usage?: Usage };
|
|
111
|
+
type FlowProgress = { line: string };
|
|
111
112
|
|
|
112
113
|
type CommandResult = {
|
|
113
114
|
stdout: string;
|
|
@@ -176,9 +177,18 @@ export function parseDelegateFlowContinue(value: unknown): Static<typeof Delegat
|
|
|
176
177
|
};
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
function unitCount(count: number): string {
|
|
181
|
+
return `${count} unit${count === 1 ? "" : "s"}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
179
184
|
function flowCallLabel(args: { units?: unknown }): string {
|
|
180
185
|
const count = Array.isArray(args.units) ? args.units.length : 0;
|
|
181
|
-
return `delegate_flow ·
|
|
186
|
+
return `delegate_flow · parallel→serial · ${unitCount(count)}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isFlowProgress(value: unknown): value is FlowProgress {
|
|
190
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
191
|
+
&& typeof (value as { line?: unknown }).line === "string";
|
|
182
192
|
}
|
|
183
193
|
|
|
184
194
|
function flowResultLines(text: string): string[] {
|
|
@@ -609,6 +619,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
609
619
|
signal: AbortSignal | undefined,
|
|
610
620
|
ctx: ExtensionContext,
|
|
611
621
|
meter: UsageMeter,
|
|
622
|
+
emitProgress: (line: string) => void,
|
|
612
623
|
) => {
|
|
613
624
|
assertCurrent(flow);
|
|
614
625
|
const main = flow.main!;
|
|
@@ -624,6 +635,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
624
635
|
|
|
625
636
|
const implementationFailure = settlementFailure(unit.implementation!);
|
|
626
637
|
if (implementationFailure) return block(flow, unit, "implementer", implementationFailure, meter);
|
|
638
|
+
emitProgress(`verify/integrate · unit ${flow.index + 1}/${flow.units.length}`);
|
|
627
639
|
|
|
628
640
|
let inspected = await inspectUnit(unit, false, signal);
|
|
629
641
|
assertCurrent(flow);
|
|
@@ -690,6 +702,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
690
702
|
if (reviewCriterion !== undefined) {
|
|
691
703
|
const reviewer = flow.reviewer;
|
|
692
704
|
if (!reviewer) return terminal(flow, "infrastructure", "Flow Reviewer was not resolved for a unit that requires review.", meter);
|
|
705
|
+
emitProgress(`review · unit ${flow.index + 1}/${flow.units.length}`);
|
|
693
706
|
let evidence;
|
|
694
707
|
try {
|
|
695
708
|
evidence = await prepareExactReviewEvidence({ base: main.expectedHead, tip, worktree: unit.worktree.path }, signal);
|
|
@@ -787,16 +800,16 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
787
800
|
"If a Flow blocks, inspect its classification and call delegate_flow_continue once with explicit repair guidance; modelClass may replace that one repair's current class.",
|
|
788
801
|
],
|
|
789
802
|
parameters: DelegateFlowSchema,
|
|
790
|
-
renderShell: "self",
|
|
791
803
|
renderCall(args, theme, _context) {
|
|
792
804
|
return renderToolLines([theme.fg("toolTitle", flowCallLabel(args))], theme);
|
|
793
805
|
},
|
|
794
|
-
renderResult(result,
|
|
806
|
+
renderResult(result, { isPartial }, theme, _context) {
|
|
807
|
+
if (isPartial) return renderToolLines(isFlowProgress(result.details) ? [theme.fg("muted", result.details.line)] : [], theme);
|
|
795
808
|
const text = result.content.find((part) => part.type === "text")?.text ?? "(no output)";
|
|
796
809
|
return renderToolLines(flowResultLines(text), theme);
|
|
797
810
|
},
|
|
798
811
|
prepareArguments: parseDelegateFlow,
|
|
799
|
-
async execute(toolCallId, params, signal,
|
|
812
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
800
813
|
const request = parseDelegateFlow(params);
|
|
801
814
|
if (active) throw new Error("delegate_flow rejected because another Flow is active.");
|
|
802
815
|
const roles = runtime.loadRoles();
|
|
@@ -805,6 +818,10 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
805
818
|
const reviewer = needsReviewer ? roles.find(({ name }) => name === "reviewer") : undefined;
|
|
806
819
|
if (!implementer) throw new Error("delegate_flow requires an implementer Role.");
|
|
807
820
|
if (needsReviewer && !reviewer) throw new Error("delegate_flow requires a reviewer Role when a unit declares review.");
|
|
821
|
+
const emitProgress = (line: string) => {
|
|
822
|
+
const progress: FlowProgress = { line };
|
|
823
|
+
onUpdate?.({ content: [{ type: "text", text: progress.line }], details: progress });
|
|
824
|
+
};
|
|
808
825
|
const flow: FlowState = {
|
|
809
826
|
phase: "running",
|
|
810
827
|
generation: runtime.getSessionGeneration(),
|
|
@@ -818,6 +835,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
818
835
|
warnings: [],
|
|
819
836
|
};
|
|
820
837
|
active = flow;
|
|
838
|
+
emitProgress(`setup · ${unitCount(request.units.length)}`);
|
|
821
839
|
const operationSignal = bindSignal(flow, signal);
|
|
822
840
|
const meter: UsageMeter = {};
|
|
823
841
|
let setupComplete = false;
|
|
@@ -855,6 +873,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
855
873
|
await checkMain(flow.main, operationSignal);
|
|
856
874
|
assertCurrent(flow);
|
|
857
875
|
setupComplete = true;
|
|
876
|
+
let completedImplementers = 0;
|
|
858
877
|
const settlements = await Promise.all(flow.units.map((unit, index) => runChild(
|
|
859
878
|
flow,
|
|
860
879
|
flow.implementer,
|
|
@@ -866,11 +885,15 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
866
885
|
operationSignal,
|
|
867
886
|
ctx,
|
|
868
887
|
meter,
|
|
869
|
-
))
|
|
888
|
+
).then((settlement) => {
|
|
889
|
+
completedImplementers += 1;
|
|
890
|
+
emitProgress(`implement · ${completedImplementers}/${flow.units.length} complete`);
|
|
891
|
+
return settlement;
|
|
892
|
+
})));
|
|
870
893
|
for (const [index, settlement] of settlements.entries()) flow.units[index]!.implementation = settlement;
|
|
871
894
|
assertCurrent(flow);
|
|
872
895
|
if (operationSignal.aborted) operationSignal.throwIfAborted();
|
|
873
|
-
return await processFlow(flow, toolCallId, operationSignal, ctx, meter);
|
|
896
|
+
return await processFlow(flow, toolCallId, operationSignal, ctx, meter, emitProgress);
|
|
874
897
|
} catch (error) {
|
|
875
898
|
if (!setupComplete) {
|
|
876
899
|
for (const unit of [...flow.units].reverse()) {
|
|
@@ -892,16 +915,16 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
892
915
|
promptSnippet: "Repair and continue the blocked deterministic Flow",
|
|
893
916
|
promptGuidelines: ["Call delegate_flow_continue only after delegate_flow reports a repairable block, with explicit guidance addressing that block."],
|
|
894
917
|
parameters: DelegateFlowContinueSchema,
|
|
895
|
-
renderShell: "self",
|
|
896
918
|
renderCall(_args, theme, _context) {
|
|
897
|
-
return renderToolLines([theme.fg("toolTitle", "delegate_flow_continue ·
|
|
919
|
+
return renderToolLines([theme.fg("toolTitle", "delegate_flow_continue · repair continuation")], theme);
|
|
898
920
|
},
|
|
899
|
-
renderResult(result,
|
|
921
|
+
renderResult(result, { isPartial }, theme, _context) {
|
|
922
|
+
if (isPartial) return renderToolLines(isFlowProgress(result.details) ? [theme.fg("muted", result.details.line)] : [], theme);
|
|
900
923
|
const text = result.content.find((part) => part.type === "text")?.text ?? "(no output)";
|
|
901
924
|
return renderToolLines(flowResultLines(text), theme);
|
|
902
925
|
},
|
|
903
926
|
prepareArguments: parseDelegateFlowContinue,
|
|
904
|
-
async execute(toolCallId, params, signal,
|
|
927
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
905
928
|
const { guidance, modelClass } = parseDelegateFlowContinue(params);
|
|
906
929
|
const flow = active;
|
|
907
930
|
if (!flow) throw new Error("delegate_flow_continue requires an active blocked Flow.");
|
|
@@ -916,7 +939,12 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
916
939
|
if (modelClass !== undefined) unit.modelClass = modelClass;
|
|
917
940
|
const operationSignal = bindSignal(flow, signal);
|
|
918
941
|
const meter: UsageMeter = {};
|
|
942
|
+
const emitProgress = (line: string) => {
|
|
943
|
+
const progress: FlowProgress = { line };
|
|
944
|
+
onUpdate?.({ content: [{ type: "text", text: progress.line }], details: progress });
|
|
945
|
+
};
|
|
919
946
|
try {
|
|
947
|
+
emitProgress(`repair · unit ${flow.index + 1}/${flow.units.length}`);
|
|
920
948
|
unit.implementation = await runChild(
|
|
921
949
|
flow,
|
|
922
950
|
flow.implementer,
|
|
@@ -931,7 +959,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
931
959
|
);
|
|
932
960
|
assertCurrent(flow);
|
|
933
961
|
if (operationSignal.aborted) operationSignal.throwIfAborted();
|
|
934
|
-
return await processFlow(flow, toolCallId, operationSignal, ctx, meter);
|
|
962
|
+
return await processFlow(flow, toolCallId, operationSignal, ctx, meter, emitProgress);
|
|
935
963
|
} catch (error) {
|
|
936
964
|
return terminal(flow, "infrastructure", errorText(error), meter);
|
|
937
965
|
}
|
package/extensions/subagent.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
availableTaskModels,
|
|
7
7
|
type ThinkingLevel,
|
|
8
8
|
modelReference,
|
|
9
|
+
registerModelTask,
|
|
9
10
|
resolveAvailableModel,
|
|
10
11
|
resolveConfiguredTaskRoute,
|
|
11
12
|
type ResolvedTaskRoute,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
capEphemeralSubagentOutput as capOutput,
|
|
16
17
|
createChildWorktree,
|
|
17
18
|
createEphemeralSubagentExecutor,
|
|
19
|
+
DELEGATE_TASK,
|
|
18
20
|
createRoleLaunch,
|
|
19
21
|
EphemeralSubagentError,
|
|
20
22
|
finalizeChildWorktree,
|
|
@@ -54,10 +56,10 @@ import {
|
|
|
54
56
|
type WorkflowEntry,
|
|
55
57
|
} from "./workflow.ts";
|
|
56
58
|
|
|
57
|
-
const SUBAGENT_TASK = "pi-subagent/delegateTask";
|
|
58
59
|
const WIDGET_KEY = "subagent-status";
|
|
59
60
|
const WIDGET_INTERVAL_MS = 80;
|
|
60
|
-
const
|
|
61
|
+
const MAX_WIDGET_ITEMS = 8;
|
|
62
|
+
const MAX_WIDGET_LINES = 6;
|
|
61
63
|
export const MAX_WIDGET_ACTIVE_TOOLS = 8;
|
|
62
64
|
const DEFAULT_TIMEOUT_POLICY = {
|
|
63
65
|
idleMs: DEFAULT_TIMEOUT_CONFIG.idleMinutes * 60_000,
|
|
@@ -178,9 +180,30 @@ function isWorkflowTransportDetails(value: unknown): value is WorkflowTransportD
|
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
function workflowCallLabel(args: { tasks?: unknown; chain?: unknown }): string {
|
|
181
|
-
if (Array.isArray(args.chain)) return `delegate_task ·
|
|
182
|
-
if (Array.isArray(args.tasks)) return `delegate_task ·
|
|
183
|
-
return "delegate_task ·
|
|
183
|
+
if (Array.isArray(args.chain)) return `delegate_task · chain · ${args.chain.length} task${args.chain.length === 1 ? "" : "s"}`;
|
|
184
|
+
if (Array.isArray(args.tasks)) return `delegate_task · parallel · ${args.tasks.length} task${args.tasks.length === 1 ? "" : "s"}`;
|
|
185
|
+
return "delegate_task · single · 1 task";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function workflowProgressLine(details: WorkflowTransportDetails): string {
|
|
189
|
+
let running = 0;
|
|
190
|
+
let pending = 0;
|
|
191
|
+
let complete = 0;
|
|
192
|
+
let failed = 0;
|
|
193
|
+
let skipped = 0;
|
|
194
|
+
for (const { status } of details.entries) {
|
|
195
|
+
switch (status) {
|
|
196
|
+
case "running": running += 1; break;
|
|
197
|
+
case "pending": pending += 1; break;
|
|
198
|
+
case "succeeded": complete += 1; break;
|
|
199
|
+
case "failed":
|
|
200
|
+
case "rejected": failed += 1; break;
|
|
201
|
+
case "skipped": skipped += 1; break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return [[running, "running"], [pending, "pending"], [complete, "complete"], [failed, "failed"], [skipped, "skipped"]]
|
|
205
|
+
.flatMap(([count, label]) => count ? [`${count} ${label}`] : [])
|
|
206
|
+
.join(" · ");
|
|
184
207
|
}
|
|
185
208
|
|
|
186
209
|
function workflowResultLines(details: WorkflowTransportDetails, theme: Theme): string[] {
|
|
@@ -215,18 +238,23 @@ function renderWidgetRows(
|
|
|
215
238
|
spinnerIndex: number,
|
|
216
239
|
theme: Theme,
|
|
217
240
|
): string[] {
|
|
218
|
-
const
|
|
241
|
+
const ordered = [...items.filter(({ status }) => status === "working"), ...items.filter(({ status }) => status !== "working")];
|
|
242
|
+
const visible = ordered.slice(0, ordered.length > MAX_WIDGET_LINES ? MAX_WIDGET_LINES - 1 : MAX_WIDGET_LINES);
|
|
219
243
|
if (!visible.length) return [];
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
244
|
+
const hidden = ordered.slice(visible.length);
|
|
245
|
+
const lines = visible.map((item) => truncateToWidth(
|
|
246
|
+
`${statusGlyph(item.status, spinnerIndex, theme)} ${theme.fg("accent", item.role)} · ${statusLabel(item.status)} · ${theme.fg("text", item.task)} · ${theme.fg("text", activityLabel(item, now))} · ${theme.fg("muted", activityMetrics(item, now))}`,
|
|
247
|
+
width,
|
|
248
|
+
));
|
|
249
|
+
if (hidden.length) {
|
|
250
|
+
const counts: Record<WidgetStatus, number> = { working: 0, success: 0, failure: 0, aborted: 0 };
|
|
251
|
+
for (const { status } of hidden) counts[status] += 1;
|
|
252
|
+
lines.push(truncateToWidth(theme.fg("muted", [
|
|
253
|
+
`… ${hidden.length} more`,
|
|
254
|
+
...(["working", "success", "failure", "aborted"] as const).flatMap((status) =>
|
|
255
|
+
counts[status] ? [`${counts[status]} ${statusLabel(status)}`] : []),
|
|
256
|
+
].join(" · ")), width));
|
|
257
|
+
}
|
|
230
258
|
return lines;
|
|
231
259
|
}
|
|
232
260
|
|
|
@@ -277,6 +305,7 @@ export default function subagentExtension(
|
|
|
277
305
|
pi: ExtensionAPI,
|
|
278
306
|
overrideTimeoutPolicy?: TimeoutPolicy,
|
|
279
307
|
): void {
|
|
308
|
+
registerModelTask(pi, DELEGATE_TASK);
|
|
280
309
|
const widgetItems = new Map<string, WidgetItem>();
|
|
281
310
|
// Each child is a full Pi process issuing its own model calls; cap parallel
|
|
282
311
|
// spend. Precedence: PI_SUBAGENT_MAX_SUBAGENTS env > config/pi-subagent/pi-subagent.json
|
|
@@ -357,11 +386,11 @@ export default function subagentExtension(
|
|
|
357
386
|
) => {
|
|
358
387
|
if (!ctx.hasUI) return;
|
|
359
388
|
ensureWidget(ctx);
|
|
360
|
-
if (!widgetItems.has(id) && widgetItems.size >=
|
|
389
|
+
if (!widgetItems.has(id) && widgetItems.size >= MAX_WIDGET_ITEMS) {
|
|
361
390
|
for (const [oldestId, item] of widgetItems) {
|
|
362
391
|
if (item.status === "working") continue;
|
|
363
392
|
widgetItems.delete(oldestId);
|
|
364
|
-
if (widgetItems.size <
|
|
393
|
+
if (widgetItems.size < MAX_WIDGET_ITEMS) break;
|
|
365
394
|
}
|
|
366
395
|
}
|
|
367
396
|
widgetItems.set(id, {
|
|
@@ -557,7 +586,7 @@ export default function subagentExtension(
|
|
|
557
586
|
return createRoleLaunch(pi, launchCtx, {
|
|
558
587
|
role,
|
|
559
588
|
route: modelClass === undefined
|
|
560
|
-
? resolveConfiguredTaskRoute(launchCtx,
|
|
589
|
+
? resolveConfiguredTaskRoute(launchCtx, DELEGATE_TASK)
|
|
561
590
|
: resolveTaskRoute(launchCtx, modelClass),
|
|
562
591
|
});
|
|
563
592
|
},
|
|
@@ -580,13 +609,14 @@ export default function subagentExtension(
|
|
|
580
609
|
"delegate_task background applies to the whole selected workflow and returns before results exist; use it only when the user explicitly asks for non-blocking work.",
|
|
581
610
|
],
|
|
582
611
|
parameters: WorkflowSchema,
|
|
583
|
-
renderShell: "self",
|
|
584
612
|
renderCall(args, theme, _context) {
|
|
585
613
|
return renderToolLines([theme.fg("toolTitle", workflowCallLabel(args))], theme);
|
|
586
614
|
},
|
|
587
615
|
renderResult(result, { isPartial }, theme, _context) {
|
|
588
|
-
if (isPartial) return renderToolLines([], theme);
|
|
589
616
|
const details = result.details;
|
|
617
|
+
if (isPartial) return renderToolLines(isWorkflowTransportDetails(details)
|
|
618
|
+
? [theme.fg("muted", workflowProgressLine(details))]
|
|
619
|
+
: [], theme);
|
|
590
620
|
if (isWorkflowTransportDetails(details)) return renderToolLines(workflowResultLines(details, theme), theme);
|
|
591
621
|
if (typeof details === "object" && details !== null && (details as { background?: unknown }).background === true) {
|
|
592
622
|
return renderToolLines([theme.fg("muted", "Background workflow accepted.")], theme);
|
|
@@ -633,7 +663,7 @@ export default function subagentExtension(
|
|
|
633
663
|
route: delegation.model !== undefined
|
|
634
664
|
? resolveDesignatedRoute(launchCtx(), delegation.model, delegation.thinking)
|
|
635
665
|
: delegation.modelClass === undefined
|
|
636
|
-
? resolveConfiguredTaskRoute(launchCtx(),
|
|
666
|
+
? resolveConfiguredTaskRoute(launchCtx(), DELEGATE_TASK, undefined, delegation.thinking)
|
|
637
667
|
: resolveTaskRoute(launchCtx(), delegation.modelClass, undefined, delegation.thinking),
|
|
638
668
|
});
|
|
639
669
|
const notifyMissingSkills = (role: Role, launch: ReturnType<typeof resolveLaunch>) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-subagent",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Delegate bounded single, parallel, or chained tasks to isolated Pi roles.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -66,6 +66,6 @@
|
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@henryqw/pi-herdr": "^0.4.0",
|
|
68
68
|
"@henryqw/pi-multi-codex": "^0.3.8",
|
|
69
|
-
"@henryqw/pi-task-models": "^
|
|
69
|
+
"@henryqw/pi-task-models": "^3.0.0"
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -9,6 +9,8 @@ You are Main, the planner/orchestrator: slice work and call `delegate_flow`. Do
|
|
|
9
9
|
|
|
10
10
|
## Slice
|
|
11
11
|
|
|
12
|
+
Before slicing, identify applicable repository prohibitions. If the request or plan conflicts with them, stop and resolve the conflict before delegation. Copy them into every affected task and into `review` when automated validation cannot establish compliance; never replace repository policy with generic preservation or migration assumptions. When compatibility is disallowed, require deletion of replaced paths and forbid legacy readers, aliases, adapters, dual schemas, deprecation paths, and compatibility fallbacks.
|
|
13
|
+
|
|
12
14
|
Use the fewest cohesive units. `delegate_flow` is for independent units expected to commute; combine or sequence work that overlaps files, APIs, schemas, generated output, package metadata, lockfiles, or invariants. Dependent work remains outside Flow, in one task or ordinary caller-controlled sequencing.
|
|
13
15
|
|
|
14
16
|
Give every unit a bounded objective, owned scope and exclusions, and its direct validation command/argument array. Do not pass the parent request unchanged. Use `modelClass: "fast"` normally; use `"balanced"` upfront only for obviously complex work. Add non-empty `review` only for an explicit judgment that automated validation cannot establish. Call `delegate_flow` with 1–8 units; the runtime always supplies the effective Implementer and supplies the Reviewer only when a unit needs review.
|