@henryqw/pi-subagent 18.0.1 → 22.0.1
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 +76 -24
- package/README.md +116 -142
- package/dist/composition.d.ts +11 -0
- package/dist/composition.js +107 -0
- package/dist/direct-herdr.d.ts +17 -0
- package/dist/direct-herdr.js +219 -0
- package/dist/ephemeral.d.ts +2 -2
- package/dist/ephemeral.js +7 -7
- package/dist/git-runtime.d.ts +110 -0
- package/dist/git-runtime.js +472 -0
- package/dist/herdr-runtime.d.ts +137 -0
- package/dist/herdr-runtime.js +1228 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +20 -16
- package/dist/integration-git.d.ts +59 -0
- package/dist/integration-git.js +484 -0
- package/dist/launch-runtime.d.ts +54 -0
- package/dist/launch-runtime.js +195 -0
- package/dist/process.d.ts +9 -0
- package/dist/process.js +11 -0
- package/dist/runner.d.ts +356 -0
- package/dist/runner.js +2710 -0
- package/dist/schema.d.ts +2234 -0
- package/dist/schema.js +1236 -0
- package/dist/store.d.ts +50 -0
- package/dist/store.js +288 -0
- package/dist/working-evidence.d.ts +25 -0
- package/dist/working-evidence.js +261 -0
- package/docs/adr/001-composable-ephemeral-execution.md +3 -3
- package/docs/delegation-routing.html +89 -0
- package/docs/delegation-routing.svg +78 -0
- package/docs/orchestration-architecture.html +238 -0
- package/docs/orchestration-architecture.svg +189 -0
- package/docs/orchestration.md +122 -331
- package/example.png +0 -0
- package/examples/roles/implementer.md +2 -3
- package/extensions/admission.ts +66 -0
- package/extensions/config.ts +51 -37
- package/extensions/isolated.ts +638 -0
- package/extensions/result-transport.ts +13 -103
- package/extensions/role-tools.ts +11 -11
- package/extensions/subagent-command.ts +167 -0
- package/extensions/subagent.ts +264 -540
- package/extensions/task-name.ts +1 -2
- package/extensions/workflow.ts +60 -96
- package/package.json +14 -6
- package/skills/pi-subagent/SKILL.md +35 -0
package/docs/orchestration.md
CHANGED
|
@@ -1,406 +1,197 @@
|
|
|
1
1
|
# Orchestration and package-author API
|
|
2
2
|
|
|
3
|
-
`pi-subagent`
|
|
3
|
+
`pi-subagent` owns both delegation paths behind `delegate_task`:
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
v
|
|
10
|
-
active-Pi ephemeral executor
|
|
6
|
+
┌─ direct ─── read-only Herdr tab in Main's workspace
|
|
7
|
+
request + Role + route ──┤
|
|
8
|
+
└─ isolated ─ Herdr worktree, checked candidate, durable state
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
Both paths resolve Roles and model routes from Pi's effective registries. They share the package's global concurrency, turn/token, timeout, and correction policy. A request cannot supply or refill execution limits.
|
|
14
12
|
|
|
15
|
-
##
|
|
13
|
+
## Direct contract
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
### Single
|
|
15
|
+
Direct mode selects one single, parallel, or chain workflow:
|
|
20
16
|
|
|
21
17
|
```json
|
|
22
18
|
{
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
19
|
+
"mode": "direct",
|
|
20
|
+
"role": "scout",
|
|
21
|
+
"name": "Inspect auth flow",
|
|
22
|
+
"task": "Return the relevant files and risks.",
|
|
23
|
+
"kind": "text",
|
|
24
|
+
"modelClass": "fast"
|
|
28
25
|
}
|
|
29
26
|
```
|
|
30
27
|
|
|
31
|
-
Single mode puts one delegation's fields at the top level.
|
|
32
|
-
|
|
33
|
-
### Parallel
|
|
34
|
-
|
|
35
28
|
```json
|
|
36
29
|
{
|
|
30
|
+
"mode": "direct",
|
|
37
31
|
"tasks": [
|
|
38
|
-
{ "role": "
|
|
39
|
-
{ "role": "
|
|
40
|
-
]
|
|
41
|
-
"background": false
|
|
32
|
+
{ "role": "scout", "name": "Inspect service", "task": "Inspect the service boundary." },
|
|
33
|
+
{ "role": "reviewer", "name": "Inspect tests", "task": "Inspect the test contract." }
|
|
34
|
+
]
|
|
42
35
|
}
|
|
43
36
|
```
|
|
44
37
|
|
|
45
|
-
`tasks` contains 1–8 independent delegations. All entries start concurrently subject to the shared executor cap, all settle even when a sibling fails, and outcomes remain in input order rather than completion order.
|
|
46
|
-
|
|
47
|
-
### Chain
|
|
48
|
-
|
|
49
38
|
```json
|
|
50
39
|
{
|
|
40
|
+
"mode": "direct",
|
|
51
41
|
"chain": [
|
|
52
|
-
{ "role": "
|
|
53
|
-
{ "role": "
|
|
54
|
-
|
|
55
|
-
],
|
|
56
|
-
"background": false
|
|
42
|
+
{ "role": "scout", "name": "Collect evidence", "task": "Collect exact evidence." },
|
|
43
|
+
{ "role": "reviewer", "name": "Review evidence", "task": "Review this report:\n{previous}" }
|
|
44
|
+
]
|
|
57
45
|
}
|
|
58
46
|
```
|
|
59
47
|
|
|
60
|
-
|
|
48
|
+
Unknown properties and mixed shapes are rejected.
|
|
61
49
|
|
|
62
50
|
### Delegation fields
|
|
63
51
|
|
|
52
|
+
Each entry has:
|
|
53
|
+
|
|
64
54
|
| Field | Required | Contract |
|
|
65
55
|
| --- | --- | --- |
|
|
66
|
-
| `role` | yes |
|
|
67
|
-
| `name` | yes |
|
|
68
|
-
| `task` | yes | Non-empty
|
|
69
|
-
| `
|
|
70
|
-
| `modelClass` | no | `fast`, `balanced`, `frontier`, or `fav
|
|
71
|
-
|
|
72
|
-
Those five fields are the complete delegation object. An explicit call `modelClass` wins over the selected Role's optional default. If neither is present, the local Model Task assignment or declared default selects the route. Routes set exact thinking levels. A direct model never changes that level and fails before launch if it cannot support it. `tasks`, `chain`, and `background` cannot be nested. Route fallback occurs only before launch; a started child is never retried by this package.
|
|
73
|
-
|
|
74
|
-
### Background, failures, and transport
|
|
75
|
-
|
|
76
|
-
`background` is top-level policy for the whole selected mode. Foreground blocks until that mode settles. Background returns one acknowledgement and, while its launching session remains active, later delivers the settled workflow outcome; callers cannot background only one entry in an array. Session shutdown or reload aborts unfinished background work, which may deliver only recoverable isolated-work evidence or no follow-up message.
|
|
77
|
-
|
|
78
|
-
Any foreground entry failure makes the tool call throw. Parallel mode first settles every sibling, while chain mode stops immediately. The thrown failure retains available successful sibling output, failed/rejected entry evidence, and worktree recovery reports so useful work remains locatable.
|
|
79
|
-
|
|
80
|
-
All Main-visible text for one tool call shares one aggregate 50 KiB UTF-8 transport cap, including child output, sibling failures, and worktree/recovery evidence. Parallel execution does not multiply the cap by its entry count. Truncation is explicit; internal bookkeeping is not made visible by bypassing the cap.
|
|
81
|
-
|
|
82
|
-
## Per-delegation resources and isolation
|
|
83
|
-
|
|
84
|
-
`delegate_task` first preflights every requested Role name, so an initially unknown Role starts no sibling. Then, after receiving an executor permit, every single entry, parallel sibling, and chain step independently:
|
|
85
|
-
|
|
86
|
-
1. reloads its effective Role by requested name;
|
|
87
|
-
2. resolves its route and named Skills from the latest effective Pi context;
|
|
88
|
-
3. creates its Role launch policy; and
|
|
89
|
-
4. when the reloaded Role requests `isolation: worktree`, creates a worktree identified by the tool call, mode, and input index.
|
|
90
|
-
|
|
91
|
-
Separate deterministic identities produce separate worktree paths and branches. Parallel siblings cannot collide, and a chain does not base one step's worktree on the preceding step's branch. `{previous}` passes text only. There is no implicit shared worktree or hidden workflow state.
|
|
92
|
-
|
|
93
|
-
A worktree starts from Main's current `HEAD`. Clean worktrees with no child commits are pruned; committed, dirty, switched, unmeasurable, or otherwise recoverable work is preserved and reported. For `delegate_task`, non-git directories and repositories with an unborn `HEAD` use Main's working directory. Git submodules reject worktree isolation, and setup failure in a real repository throws rather than silently sharing Main's checkout.
|
|
94
|
-
|
|
95
|
-
If steps must share files, make that an explicit caller decision: use an intentionally shared workspace, merge preserved child commits, or pass state through a caller-owned store. Do not rely on chain order to imply filesystem sharing.
|
|
96
|
-
|
|
97
|
-
## Resource Policy
|
|
98
|
-
|
|
99
|
-
Every Role launch centrally prepends this child identity contract to its system instructions before the Role prompt: the child is a delegated Pi Subagent, not Main; it executes its assigned Role and task directly; Main-only delegation rules do not apply; recursive delegation is unavailable and it must not seek or invoke delegation tools.
|
|
100
|
-
|
|
101
|
-
A Role file requires:
|
|
56
|
+
| `role` | yes | Effective configured Role name |
|
|
57
|
+
| `name` | yes | Short display name without terminal controls |
|
|
58
|
+
| `task` | yes | Non-empty task packet |
|
|
59
|
+
| `kind` | no | `text` only (default); direct `changeset` is rejected |
|
|
60
|
+
| `modelClass` | no | `fast`, `balanced`, `frontier`, or `fav` route |
|
|
61
|
+
| `model` | no | `provider/modelId`; replaces only the route model |
|
|
102
62
|
|
|
103
|
-
|
|
104
|
-
- explicit extension paths or package sources: a YAML array; `extensions: []` selects no Role extension bundle;
|
|
105
|
-
- additional effective Pi Skill names: a YAML array; `skills: []` selects no separately named Role Skills, while trusted selected extension Skills still load;
|
|
106
|
-
- optional exact MCP server names: `mcps: []` or an omitted field denies MCP access, while allowed servers expose all their tools through `pi-mcp-adapter`;
|
|
107
|
-
- system instructions;
|
|
108
|
-
- optional `modelClass` default (`fast`, `balanced`, `frontier`, or `fav`); and
|
|
109
|
-
- optional `isolation: worktree` for the tool layer.
|
|
63
|
+
An explicit model must support the route's thinking level. A launched child is never retried automatically after provider or process failure.
|
|
110
64
|
|
|
111
|
-
|
|
65
|
+
Only Roles with known read-only tools and no extensions or MCP servers may run direct. A direct `changeset` or write-capable Role is rejected; use isolated mode for implementation. Parallel reads run concurrently and report in request order. A chain substitutes only the preceding successful output and stops at its first failure.
|
|
112
66
|
|
|
113
|
-
|
|
67
|
+
The tool returns a handle after the first exact Herdr tab is launched. The remaining tabs are launched asynchronously; their identities are recorded on the session branch. A session-owned observer awaits each exact agent's lifecycle and retrieves its final answer from its Pi session file, not its possibly truncated screen. Idle, blocked, unknown, malformed, or oversized answers fail with recoverable tab/session identity; no whole-run deadline is imposed. `/subagent` shows exact recorded tabs on this session branch, distinguishing locally observed work from unobserved records. A session switch stops observation and carries tab identity into the new branch.
|
|
114
68
|
|
|
115
|
-
|
|
69
|
+
The result is delivered to Main with `triggerTurn: true, deliverAs: "followUp"`. Busy Main finishes its current turn before consuming the result; idle Main starts a turn. Herdr reports worker lifecycle but never prompts Main directly. Read-only workers can observe a concurrently changing checkout; consumers must account for drift rather than treating a tab as file isolation.
|
|
116
70
|
|
|
117
|
-
|
|
71
|
+
## Isolated contract
|
|
118
72
|
|
|
119
|
-
|
|
73
|
+
An isolated call is a checked directed graph:
|
|
120
74
|
|
|
121
|
-
|
|
75
|
+
```text
|
|
76
|
+
{
|
|
77
|
+
mode: "isolated",
|
|
78
|
+
id,
|
|
79
|
+
goal,
|
|
80
|
+
tasks: [1..8],
|
|
81
|
+
finalChecks?,
|
|
82
|
+
finalJudgment?
|
|
83
|
+
}
|
|
84
|
+
```
|
|
122
85
|
|
|
123
|
-
|
|
86
|
+
Every task declares `id`, `kind`, `role`, `modelClass`, `requirements`, `deliverable`, `dependsOn`, and `contextFrom`.
|
|
124
87
|
|
|
125
|
-
|
|
88
|
+
- A `text` task runs in a disposable child worktree and must leave it unchanged.
|
|
89
|
+
- A `changeset` task requires focused checks and owns one worktree, Herdr workspace, worker tab, and retained worker.
|
|
90
|
+
- `dependsOn` controls ready waves; dependent tasks can run from a selected staged snapshot only after Main explicitly advances it.
|
|
91
|
+
- `contextFrom` may reference completed text tasks only and preserves declaration order.
|
|
92
|
+
- A graph containing changesets requires final checks. The repository root must have a `package.json` test script; admission prepends canonical `pnpm test` if absent from final checks. Combined validation rejects a changed root test script.
|
|
126
93
|
|
|
127
|
-
|
|
94
|
+
The runner executes ready worker tasks concurrently, inspects clean committed candidates, runs preliminary task checks and optional preliminary judgment, then records ready candidates for Main. **It does not automatically integrate them.** Main selects exact candidates via `subagent_stage` into an owned integration checkout in explicit order. Each stage is a committed merge of the candidate, not a mutation of Main. Conflicts retain the checkout for Main to resolve; `resolve` verifies the exact two-parent merge instead of replaying an uncertain merge. Main can explicitly run dependent tasks from an exact staged tip with `subagent_integrate advance` and stage their results afterward. Independent candidates may be staged without re-running workers.
|
|
128
95
|
|
|
129
|
-
|
|
96
|
+
Once all selected work is staged, Main calls `subagent_integrate validate` on the exact clean combined tip. This runs every final check, including `pnpm test`, and optional final judgment in the integration checkout before any Main mutation. A failed check/review can be followed by one Main-authored committed correction in that checkout, recorded with `correct`, then another `validate`. `promote` requires exact passing combined evidence and unchanged Main and uses a guarded update to Main. Only proven promotion leads to selected-worker termination and verified cleanup. A changed candidate, integration tip, or Main identity invalidates prior evidence or blocks promotion; review accepts only exact `PASS`.
|
|
130
97
|
|
|
131
|
-
|
|
98
|
+
### Readiness and Main actions
|
|
132
99
|
|
|
133
|
-
|
|
134
|
-
| --- | --- |
|
|
135
|
-
| `loadRoles(agentDir?)` | Validate and load package-shipped built-in and user Role Markdown. |
|
|
136
|
-
| `parseRoleName(value, source?)` / `RoleName` | Normalize one arbitrary Role name. Empty names and C0/C1 controls fail. |
|
|
137
|
-
| `resolveRoleSkills(pi, role)` | Resolve Role Skill names from Pi's effective registry. |
|
|
138
|
-
| `resolveRoleLaunch(pi, ctx, input)` | Resolve a caller-owned Model Task route, applying call-level then Role `modelClass` precedence, and produce `ResolvedRoleLaunch`. |
|
|
139
|
-
| `resolveConfiguredRoleLaunch(pi, ctx, { role, modelClass })` | Reload one configured Role and package resources with a required explicit Model Class; reject missing Role Skills. |
|
|
140
|
-
| `createRoleLaunch(pi, ctx, input)` | Produce the same launch from a caller-supplied resolved route. |
|
|
141
|
-
| `prepareRoleLaunch` / `finalizeRoleLaunch` | Separate the stable Role prompt from argv, expose its immutable tool policy, and reject unavailable Role Skills. |
|
|
142
|
-
| `createEphemeralSubagentExecutor(options)` | Queue and run one prepared no-session child per `run`. |
|
|
143
|
-
| `createChildWorktree` / `finalizeChildWorktree` | Optional caller-managed worktree lifecycle; `createChildWorktree` can prepare exact metadata before allocation. |
|
|
144
|
-
| `prepareExactReviewEvidence` | Validate Git identity and create a bounded private base-to-tip patch with exact `{base, tip, patchPath}` evidence. |
|
|
145
|
-
|
|
146
|
-
`finalizeChildWorktree` returns the breaking `WorktreePayload` lifecycle union:
|
|
147
|
-
|
|
148
|
-
| `outcome` | Fields | Contract |
|
|
149
|
-
| --- | --- | --- |
|
|
150
|
-
| `pruned` | `path`, `branch` | Zero commits and a clean tree were proved. Worktree and branch cleanup completed. |
|
|
151
|
-
| `retained` | `path`, `branch`, `commits`, `dirty` | Work was preserved. Both measurements are known. |
|
|
152
|
-
| `recovery` | `path`, `branch`, `note`, optional `commits`, `dirty` | Recovery needs action. The note tells Main what to inspect. Present measurements completed; omitted values are unknown. |
|
|
100
|
+
The user's request can authorize the declared local graph, checks, review, and Main's later explicit integration actions. After each successful preliminary check batch, the runner drains any follow-up already queued for the same live worker. Each revision must produce a new clean commit and rerun preliminary checks. When the queue is empty, the runner atomically seals the candidate and persists exact readiness evidence without waiting for input.
|
|
153
101
|
|
|
154
|
-
`
|
|
102
|
+
`/subagent` opens a native picker for direct recovery and isolated inspection; locally owned active changesets may queue an optional same-worker revision or drain all still-pending instructions into the native editor and submit one replacement. Cancel after draining leaves them withdrawn; claimed instructions are unaffected. Pi's global Option+Up is unchanged. There is no guaranteed post-completion editing window; late follow-ups fail visibly. After readiness, use `subagent_stage revise` with an exact candidate, generation, current `expectedTip`, and instruction when a same-worker correction remains allowed. Material scope growth, missing authorization, user-owned conflicts, or consequential external actions still require a new decision. pi-subagent never pushes, publishes, deploys, or opens a pull request.
|
|
155
103
|
|
|
156
|
-
`
|
|
104
|
+
`subagent_status` reports ready candidates with `taskId`, `attempt`, and `tip`, plus each generation's number, stage lineage, combined tip, checks, promotion, and retained worktree. Use those exact identities (including branch, HEAD, index, and tree) rather than guessed commits:
|
|
157
105
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
maxTokens: 100_000, // optional; omitted means unlimited
|
|
163
|
-
timeout: { idleMs: 10 * 60_000, maxMs: 30 * 60_000 },
|
|
164
|
-
};
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
Concurrency is FIFO. `run` accepts optional `signal`, `onUpdate(text)`, `onTokens(number)`, and `onActivity(event)` callbacks plus required `prepare()`. A queued run receives its permit before `prepare` executes, so resource and route resolution can use the latest Pi state. Queued time does not consume child timeout. `maxConcurrency`, `maxTurns`, and configured `maxTokens` must be safe integers >= 1. `idleMs` and `maxMs` must be positive. `maxMs` must exceed `idleMs`. Omitted `maxTurns` defaults to 50. Omitted `maxTokens` is unlimited.
|
|
168
|
-
|
|
169
|
-
One executor applies `maxTokens` independently to every `run`; it is not a shared pool. `run` has no token override. The built-in extension reads its global value only from `config/pi-subagent/config.json`.
|
|
106
|
+
- `subagent_stage`: `{ id, action: "stage" | "resolve" | "reject" | "revise", generation, taskId, attempt, candidate, expectedTip, instruction? }`. `candidate` is the ready candidate's `tip`; `expectedTip` is Main's recorded identity for the first stage or the previous stage's exact tip. `resolve` reconciles a pending/conflicted merge; `reject` and `revise` arbitrate a candidate without changing Main.
|
|
107
|
+
- `subagent_integrate`: `{ id, action: "advance" | "validate" | "correct" | "promote" | "reconcile" | "cleanup", generation, expectedTip }`. Use the generation's exact `combinedTip`. `advance` dispatches ready dependents on that snapshot, `correct` records the single clean committed correction after a definitive failed check/review, `reconcile` inspects an interrupted validation or promotion without replaying an uncertain promotion, and `cleanup` completes proved post-promotion cleanup.
|
|
108
|
+
- `subagent_integrate refresh`: `{ id, action: "refresh", generation, expectedTip, expectedMain, newMain }`. If Main advanced to a clean same-branch descendant, this freezes the old generation and allocates a new integration checkout from `newMain`. Restage selected immutable candidates in explicit order, resolve fresh conflicts, then rerun combined validation. Dirty, divergent, or uncertain Main cannot refresh.
|
|
109
|
+
- `subagent_integrate release`: `{ id, action: "release", generation, expectedTip, taskId?, attempt? }`. With no task, release an explicitly superseded checkout using that generation's last staged tip (or integration base if empty). With both `taskId` and `attempt`, release a rejected, terminated worker using its exact candidate tip and the latest generation number (or 1 when none exists); release every generation that staged it first. Cleanup steps are persisted separately, and a retry reconciles already-removed resources by proving absence. Release refuses dirty/conflicted work, changed refs, unproved termination, and still-needed checkouts.
|
|
170
110
|
|
|
171
|
-
|
|
111
|
+
A rejection or revision of a staged candidate supersedes that generation, leaves its checkout read-only, and requires explicit restaging of chosen candidates into a new generation. At most two unreleased owned integration checkouts can coexist per request. Superseded integration checkouts and rejected candidate resources are **retained until Main explicitly releases them**. Inspect exact status first; dirty or unresolved conflicts require manual recovery without force deletion. Rejection invalidates dependent attempts; if the required fresh dependent work cannot run within the bounded attempt/lineage constraints, start a new request. `subagent_abort` cannot discard retained resources, but can finish after all candidates are rejected and all owned checkouts are released.
|
|
172
112
|
|
|
173
|
-
###
|
|
113
|
+
### Recovery and state
|
|
174
114
|
|
|
175
|
-
|
|
115
|
+
State version 5 is stored privately under:
|
|
176
116
|
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
createEphemeralSubagentExecutor,
|
|
180
|
-
finalizeRoleLaunch,
|
|
181
|
-
prepareRoleLaunch,
|
|
182
|
-
} from "@henryqw/pi-subagent";
|
|
183
|
-
import { registerModelTask } from "@henryqw/pi-task-models";
|
|
184
|
-
|
|
185
|
-
const MODEL_TASK = {
|
|
186
|
-
id: "your-package/delegate",
|
|
187
|
-
label: "Package delegation",
|
|
188
|
-
purpose: "Run one package-owned delegated task.",
|
|
189
|
-
defaultProfile: "fast",
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
export function createRunRole(pi) {
|
|
193
|
-
// Register the Model Task at extension load so /task-models discovery works.
|
|
194
|
-
registerModelTask(pi, MODEL_TASK);
|
|
195
|
-
|
|
196
|
-
const executor = createEphemeralSubagentExecutor({
|
|
197
|
-
maxConcurrency: 4,
|
|
198
|
-
maxTurns: 50,
|
|
199
|
-
maxTokens: 100_000,
|
|
200
|
-
timeout: { idleMs: 10 * 60_000, maxMs: 30 * 60_000 },
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
let latestCtx;
|
|
204
|
-
pi.on("session_start", (_event, ctx) => { latestCtx = ctx; });
|
|
205
|
-
pi.on("model_select", (event, ctx) => {
|
|
206
|
-
latestCtx = { ...ctx, model: event.model };
|
|
207
|
-
});
|
|
208
|
-
pi.on("agent_settled", (_event, ctx) => { latestCtx = ctx; });
|
|
209
|
-
|
|
210
|
-
function latestContext() {
|
|
211
|
-
if (!latestCtx) throw new Error("Pi session has not started.");
|
|
212
|
-
return latestCtx;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async function runRole(role, task, options = {}) {
|
|
216
|
-
const {
|
|
217
|
-
signal,
|
|
218
|
-
cwd,
|
|
219
|
-
modelClass,
|
|
220
|
-
extensions = [],
|
|
221
|
-
tools,
|
|
222
|
-
env = {},
|
|
223
|
-
} = options;
|
|
224
|
-
|
|
225
|
-
return executor.run({
|
|
226
|
-
signal,
|
|
227
|
-
prepare: async () => {
|
|
228
|
-
// prepare runs only after this delegation owns a FIFO permit.
|
|
229
|
-
const ctx = latestContext();
|
|
230
|
-
const prepared = prepareRoleLaunch(pi, ctx, {
|
|
231
|
-
role,
|
|
232
|
-
task: MODEL_TASK,
|
|
233
|
-
modelClass,
|
|
234
|
-
extensions,
|
|
235
|
-
tools,
|
|
236
|
-
env,
|
|
237
|
-
});
|
|
238
|
-
return { launch: finalizeRoleLaunch(prepared), task, cwd: cwd ?? ctx.cwd };
|
|
239
|
-
},
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return { runRole };
|
|
244
|
-
}
|
|
117
|
+
```text
|
|
118
|
+
<agent-dir>/config/pi-subagent/state/<repository-hash>/<request-id>.json
|
|
245
119
|
```
|
|
246
120
|
|
|
247
|
-
|
|
121
|
+
Strict parsing rejects unknown properties, malformed evidence, invalid lineage, and old v4 state without migration; preserve old files and recover their work manually. Writes are atomic and repository productive work uses a durable process lease. `delegate_task` and `subagent_resume` acknowledge only after durable state is saved, then continue productive work asynchronously. Completion or attention is sent as a Pi follow-up to the launching session. `subagent_status` is read-only and remains the recovery authority if a follow-up is missed. `subagent_resume` accepts only the continuation reported by state. `subagent_abort` performs exact worker termination and teardown.
|
|
248
122
|
|
|
249
|
-
|
|
123
|
+
Productive execution has no whole-run wall-clock deadline. Long productive work and later resumes remain valid. The request retains its original policy snapshot and correction count; a resume cannot refill them. Current configuration may tighten the correction allowance. Child idle/hard runtime, subprocess I/O, status inspection, termination, cleanup, and outer abort remain bounded independently.
|
|
250
124
|
|
|
251
|
-
|
|
125
|
+
Ambiguous prompt submission or merge/promotion is never replayed automatically. Unknown allocation, failed checks, review findings, Main drift, conflicts, interrupted promotion, unproved termination, or cleanup failure requires Main attention and preserves exact evidence. A definitive failed combined check retains its generation for one correction. Proven promotion is never rolled back; recovery finishes termination and cleanup. Only selected workers and the promoted integration checkout are automatically cleaned after proven promotion; rejected workers and superseded generations require the explicit `release` action.
|
|
252
126
|
|
|
253
|
-
|
|
127
|
+
## Role and resource policy
|
|
254
128
|
|
|
255
|
-
|
|
129
|
+
A Role declares tools, trusted extension sources, Skill names, optional MCP names, instructions, and an optional model-class default. Roles do not declare isolation.
|
|
256
130
|
|
|
257
|
-
|
|
258
|
-
| --- | --- |
|
|
259
|
-
| `tool_execution_start` | `toolCallId: string`, `toolName: string`, `path?: string` |
|
|
260
|
-
| `tool_execution_end` | `toolCallId: string`, `toolName: string` |
|
|
261
|
-
| `message_end` | none |
|
|
131
|
+
Every child launch:
|
|
262
132
|
|
|
263
|
-
|
|
133
|
+
1. disables ambient extension and Skill discovery;
|
|
134
|
+
2. loads only declared resources plus required internal adapters;
|
|
135
|
+
3. resolves Skill and model names from Pi's effective registries;
|
|
136
|
+
4. installs an exact tool policy;
|
|
137
|
+
5. excludes Main-only delegation/recovery tools and `ask_question`; and
|
|
138
|
+
6. prepends the delegated-subagent identity contract.
|
|
264
139
|
|
|
265
|
-
|
|
140
|
+
An empty `tools` list adds no base tools, though selected extension tools can still activate. Omitted or empty `mcps` denies MCP access. Loading `pi-mcp-adapter` directly is rejected because it bypasses the allowlist.
|
|
266
141
|
|
|
267
|
-
|
|
142
|
+
Isolated Herdr Role launches receive the same turn, token, and child-runtime budget metadata as ephemeral launches. Retained workers therefore keep one non-refilling session budget across follow-ups rather than receiving a new request budget.
|
|
268
143
|
|
|
269
|
-
|
|
144
|
+
## Ephemeral executor API
|
|
270
145
|
|
|
271
146
|
```js
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
A small caller-owned failure policy keeps the examples readable:
|
|
282
|
-
|
|
283
|
-
```js
|
|
284
|
-
async function successfulOutput(run) {
|
|
285
|
-
const result = await run;
|
|
286
|
-
if (result.outcome === "failure") {
|
|
287
|
-
const error = new Error(
|
|
288
|
-
result.errorMessage || result.stderr || result.output ||
|
|
289
|
-
`Child exited ${result.exitCode}`,
|
|
290
|
-
);
|
|
291
|
-
error.result = result;
|
|
292
|
-
throw error;
|
|
293
|
-
}
|
|
294
|
-
return result.output;
|
|
295
|
-
}
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### Single
|
|
147
|
+
const executor = createEphemeralSubagentExecutor({
|
|
148
|
+
maxConcurrency: 4,
|
|
149
|
+
maxTurns: 50,
|
|
150
|
+
maxTokens: 100_000, // optional
|
|
151
|
+
timeout: {
|
|
152
|
+
idleMs: 10 * 60_000,
|
|
153
|
+
maxMs: 30 * 60_000,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
299
156
|
|
|
300
|
-
|
|
301
|
-
const result = await runRole(selectedRole, boundedTask, {
|
|
157
|
+
const result = await executor.run({
|
|
302
158
|
signal,
|
|
303
|
-
|
|
159
|
+
onUpdate(text) {},
|
|
160
|
+
onTokens(total) {},
|
|
161
|
+
onActivity(event) {},
|
|
162
|
+
prepare: async () => ({ launch, task, cwd }),
|
|
304
163
|
});
|
|
305
164
|
```
|
|
306
165
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
```js
|
|
310
|
-
const settled = await Promise.allSettled(
|
|
311
|
-
taskPackets.map((task) =>
|
|
312
|
-
successfulOutput(runRole(selectedRole, task, { signal, cwd: sharedWorkspace }))),
|
|
313
|
-
);
|
|
314
|
-
// Promise.allSettled preserves taskPackets order; child failure outcomes reject here,
|
|
315
|
-
// and the executor caps active children.
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
Unlike the tool's fixed maximum of eight entries, a library caller owns its collection bound. Do not pass an unbounded producer merely because active execution is capped.
|
|
319
|
-
|
|
320
|
-
### Chain
|
|
321
|
-
|
|
322
|
-
```js
|
|
323
|
-
let previous = "";
|
|
324
|
-
for (const step of steps) {
|
|
325
|
-
const task = step.task.replaceAll("{previous}", () => previous);
|
|
326
|
-
previous = await successfulOutput(
|
|
327
|
-
runRole(step.role, task, { signal, cwd: sharedWorkspace }),
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
The loop is fail-fast, and only the immediate successful output becomes `previous`.
|
|
333
|
-
|
|
334
|
-
### Fan-out / fan-in
|
|
335
|
-
|
|
336
|
-
```js
|
|
337
|
-
const reports = await Promise.all(
|
|
338
|
-
partitions.map((packet) =>
|
|
339
|
-
successfulOutput(runRole(analysisRole, packet, { signal, cwd: sharedWorkspace }))),
|
|
340
|
-
);
|
|
341
|
-
|
|
342
|
-
const synthesis = await successfulOutput(runRole(
|
|
343
|
-
synthesisRole,
|
|
344
|
-
`Reconcile these caller-bounded reports:\n${JSON.stringify(reports)}`,
|
|
345
|
-
{ signal, cwd: sharedWorkspace },
|
|
346
|
-
));
|
|
347
|
-
```
|
|
166
|
+
The FIFO permit is acquired before `prepare`, so queued work can resolve fresh Role and route state without consuming timeout. Each child gets independent turn/token and idle/hard-runtime limits. Output, stderr, protocol events, callback draining, and descendant stream draining are bounded.
|
|
348
167
|
|
|
349
|
-
|
|
168
|
+
A result contains `outcome`, `exitCode`, bounded `output` and `stderr`, truncation state, optional stop reason/error, and aggregate usage. Abort, timeout, turn/token limit, spawn, protocol, preparation, and callback failures reject with typed `EphemeralSubagentError` codes.
|
|
350
169
|
|
|
351
|
-
|
|
170
|
+
Token accounting adds each completed assistant response once. A terminal response that crosses the configured token value succeeds. A continuing crossing turn completes tools and receives one response-only handoff; further continuation fails. Role tools provide a convergence warning near configured turn/token/runtime limits.
|
|
352
171
|
|
|
353
|
-
|
|
354
|
-
function parseReviewVerdict(text) {
|
|
355
|
-
const value = JSON.parse(text);
|
|
356
|
-
if (
|
|
357
|
-
!value || typeof value !== "object" ||
|
|
358
|
-
typeof value.approved !== "boolean" ||
|
|
359
|
-
!Array.isArray(value.findings) ||
|
|
360
|
-
value.findings.some((finding) => typeof finding !== "string")
|
|
361
|
-
) {
|
|
362
|
-
throw new Error("Invalid review verdict.");
|
|
363
|
-
}
|
|
364
|
-
return value;
|
|
365
|
-
}
|
|
172
|
+
## Role launch API
|
|
366
173
|
|
|
367
|
-
|
|
368
|
-
let findings = [];
|
|
369
|
-
let approved = false;
|
|
370
|
-
|
|
371
|
-
for (let round = 1; round <= maxReviewRounds; round += 1) {
|
|
372
|
-
await successfulOutput(runRole(
|
|
373
|
-
changeRole,
|
|
374
|
-
`Apply round ${round}. Address: ${JSON.stringify(findings)}`,
|
|
375
|
-
{ signal, cwd: sharedWorkspace },
|
|
376
|
-
));
|
|
377
|
-
|
|
378
|
-
const review = await successfulOutput(runRole(
|
|
379
|
-
reviewRole,
|
|
380
|
-
'Return JSON only: {"approved":boolean,"findings":string[]}',
|
|
381
|
-
{ signal, cwd: sharedWorkspace },
|
|
382
|
-
));
|
|
383
|
-
({ approved, findings } = parseReviewVerdict(review));
|
|
384
|
-
if (approved) break;
|
|
385
|
-
}
|
|
174
|
+
The package exports:
|
|
386
175
|
|
|
387
|
-
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
The verdict schema, parser, round state, shared workspace, and terminal decision all belong to the caller. Add a richer protocol only when the workflow requires one; do not encode it as a recursive package workflow definition.
|
|
391
|
-
|
|
392
|
-
## Built-in Roles
|
|
393
|
-
|
|
394
|
-
The package ships three working built-in Roles, validated by the same parser as user roles and always present even with no `config/pi-subagent` directory. Their files leave `modelClass` unset, so they use the local Model Task route unless a caller overrides it:
|
|
395
|
-
|
|
396
|
-
| Built-in | Behavior |
|
|
176
|
+
| API | Responsibility |
|
|
397
177
|
| --- | --- |
|
|
398
|
-
| `
|
|
399
|
-
| `
|
|
400
|
-
| `
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
`
|
|
405
|
-
|
|
406
|
-
|
|
178
|
+
| `loadRoles` | Load built-in and user Role Markdown with strict validation |
|
|
179
|
+
| `parseRoleName` | Normalize arbitrary Role names and reject display controls |
|
|
180
|
+
| `resolveRoleSkills` | Resolve named Skills from Pi's effective registry |
|
|
181
|
+
| `resolveRoleLaunch` | Resolve Role, caller-owned Model Task, resources, and route |
|
|
182
|
+
| `resolveConfiguredRoleLaunch` | Resolve a named Role with an explicit model class |
|
|
183
|
+
| `prepareRoleLaunch` / `finalizeRoleLaunch` | Separate stable prompt data from final argv and tool policy |
|
|
184
|
+
| `createEphemeralSubagentExecutor` | Run bounded no-session Pi children through one FIFO pool |
|
|
185
|
+
| `createChildWorktree` / `finalizeChildWorktree` | Create and conservatively finalize optional child worktrees |
|
|
186
|
+
| `prepareExactReviewEvidence` | Produce bounded private base-to-tip evidence |
|
|
187
|
+
| checked runner exports | Compose strict schemas, state, Git, Herdr, and launch runtimes |
|
|
188
|
+
|
|
189
|
+
Package callers must register their Model Task once with `@henryqw/pi-task-models`. `resolveRoleLaunch` applies call-level model class, then Role default, then the registered task route. `resolveConfiguredRoleLaunch` requires the class explicitly. Missing Skills reject before launch.
|
|
190
|
+
|
|
191
|
+
Worktree finalization returns one of:
|
|
192
|
+
|
|
193
|
+
- `pruned`: no commits and clean files were proved, then branch/worktree removal completed;
|
|
194
|
+
- `retained`: exact commit and dirty measurements are known and work remains; or
|
|
195
|
+
- `recovery`: cleanup or measurement needs deliberate action and reports only proved fields.
|
|
196
|
+
|
|
197
|
+
No cleanup path uses `git clean`, force deletion, hidden fallback to Main, or deletion of unproved work.
|
package/example.png
ADDED
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: implementer
|
|
3
|
-
description: Implements and validates one bounded change
|
|
3
|
+
description: Implements and validates one bounded change in the checkout selected by Main
|
|
4
4
|
tools:
|
|
5
5
|
- read
|
|
6
6
|
- bash
|
|
@@ -12,7 +12,6 @@ tools:
|
|
|
12
12
|
extensions: []
|
|
13
13
|
skills: []
|
|
14
14
|
mcps: []
|
|
15
|
-
isolation: worktree
|
|
16
15
|
---
|
|
17
16
|
|
|
18
17
|
Implement the bounded outcome, not a preassigned file list. Work in the assigned cwd. Read applicable repository instructions and domain context; inspect the relevant flow, callers, and tests before editing. Preserve unrelated work. Fix the root cause with the smallest complete diff using existing patterns and dependencies. Add no speculative work. Stop when complete or blocked.
|
|
@@ -21,6 +20,6 @@ Run focused checks required by the task. Before reporting, remove only task-crea
|
|
|
21
20
|
|
|
22
21
|
Access credentials or the network, create extra artifacts, or broaden scope only when the task requires it. Never invoke external LLM APIs, SDKs, agent harnesses, or model CLIs.
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
Follow the task's commit policy. Direct delegation leaves changes uncommitted; isolated delegation commits completed scoped changes. Do not create or manage another worktree. Leave the assigned checkout and branch intact. Never push or open a pull request without explicit authorization.
|
|
25
24
|
|
|
26
25
|
Report briefly: outcome, commit, checks, and remaining risks.
|