@henryqw/pi-subagent 13.0.1 → 14.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 +1 -1
- package/README.md +66 -39
- package/dist/ephemeral.d.ts +1 -0
- package/dist/ephemeral.js +5 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/docs/orchestration.md +10 -2
- package/extensions/config.ts +15 -6
- package/extensions/role-tools.ts +16 -4
- package/package.json +1 -1
package/CONTEXT.md
CHANGED
|
@@ -23,7 +23,7 @@ Provide validated built-in and user Roles, shared task-model Pi launch policy, g
|
|
|
23
23
|
|
|
24
24
|
## Invariants
|
|
25
25
|
|
|
26
|
-
- One Delegated Task creates one ephemeral child process and no saved session. Execution ends at the first hard budget: attempted turn 51 by default (`maxTurns
|
|
26
|
+
- One Delegated Task creates one ephemeral child process and no saved session. Execution ends at the first hard budget: attempted turn 51 by default (`maxTurns` is a safe integer >= 10; default 50) or `deadline = min(last recognized Pi JSON event + idle timeout, child start + maximum runtime)` (recognized Pi events renew; raw bytes do not; max always terminates). A terminal turn 50 succeeds; attempted continuation rejects with `turn_limit`, accumulated usage, and bounded output. Role launches, including `delegate_task` and each `delegate_flow` Implementer or Reviewer launch, reserve the final allowed turn for a response-only handoff: after a continuing penultimate `turn_end` and its tools, they disable every tool and steer one structured final report. Its fixed decision packet is the default, but exact output required by the assigned task or Role takes precedence and is returned alone. A terminal penultimate response gets no handoff. This does not extend the executor limit. Raw `createEphemeralSubagentExecutor` launches do not guarantee a handoff, and timeout, provider, or child-process failures can prevent one. Before the final boundary, the Role tool extension steers the fixed convergence warning once at 80% of completed turns and once at 80% of maximum runtime, combining thresholds first due together and never starting a warning timer or extra turn. After direct Pi exits, inherited stdout/stderr drain until EOF unless an escaped descendant holds them past short inactivity or a one-second hard deadline. Configurable in `~/.pi/agent/config/pi-subagent/config.json` (`maxTurns` default 50 and minimum 10; `timeout.idleMinutes`/`maxMinutes` defaults 10/30).
|
|
27
27
|
- Up to five active ephemeral `delegate_task` children run per Main by default, configurable via `maxSubagents` in `~/.pi/agent/config/pi-subagent/config.json` or the `PI_SUBAGENT_MAX_SUBAGENTS` environment variable; excess calls wait FIFO. Queued calls do not start a child or consume child timeout.
|
|
28
28
|
- 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.
|
|
29
29
|
- 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.
|
package/README.md
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
# `@henryqw/pi-subagent`
|
|
2
2
|
|
|
3
|
-
Delegate bounded work from Main to isolated Pi Roles, or run independent implementation units through a fixed Git Flow.
|
|
3
|
+
Delegate bounded work from Main to isolated Pi Roles, or run independent implementation units through a fixed Git Flow. One Role system covers focused research, review, implementation, parallel work, and ordered chains without loading every detail into Main.
|
|
4
4
|
|
|
5
5
|

|
|
6
|
-
|
|
7
|
-
## Why
|
|
8
|
-
|
|
9
|
-
- **Created for**: Pi users who want focused child work without loading every detail into Main.
|
|
10
|
-
- **Advantage**: One Role system covers research, review, implementation, parallel work, and ordered chains.
|
|
6
|
+

|
|
11
7
|
|
|
12
8
|
## Install
|
|
13
9
|
|
|
@@ -18,13 +14,11 @@ pi install npm:@henryqw/pi-subagent
|
|
|
18
14
|
|
|
19
15
|
Run `/task-models` and configure the `fast` profile before delegating.
|
|
20
16
|
|
|
21
|
-
##
|
|
17
|
+
## Works with
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
| --- | --- |
|
|
25
|
-
| [`@henryqw/pi-task-models`](https://pi.henry.wang/extensions/pi-task-models) | Required. Supplies `fast`, `balanced`, `frontier`, and `fav` model routes. |
|
|
19
|
+
**Required.** [`@henryqw/pi-task-models`](https://pi.henry.wang/extensions/pi-task-models) supplies `fast`, `balanced`, `frontier`, and `fav` model routes.
|
|
26
20
|
|
|
27
|
-
Routes come from `~/.pi/agent/config/pi-task-models/config.json`. It stores explicit task overrides.
|
|
21
|
+
Routes come from `~/.pi/agent/config/pi-task-models/config.json`. It stores explicit task overrides. Missing shared model config warns once because delegation needs a route.
|
|
28
22
|
|
|
29
23
|
## Use
|
|
30
24
|
|
|
@@ -40,13 +34,16 @@ Start with one read-only delegation:
|
|
|
40
34
|
|
|
41
35
|
A separate child returns a bounded report to Main. It creates no saved Pi session and makes model requests through the selected route.
|
|
42
36
|
|
|
43
|
-
|
|
|
44
|
-
| --- | --- |
|
|
45
|
-
| `delegate_task` | Run one bounded task, independent tasks in parallel, or dependent tasks in a chain. |
|
|
46
|
-
| `delegate_flow` | Implement and integrate 1–8 independent Git units. |
|
|
47
|
-
| `delegate_flow_continue` | Repair the one blocked Flow unit once. |
|
|
37
|
+
| Surface | Type | Purpose |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `delegate_task` | tool | Run one bounded task, independent tasks in parallel, or dependent tasks in a chain. |
|
|
40
|
+
| `delegate_flow` | tool | Implement and integrate 1–8 independent Git units. |
|
|
41
|
+
| `delegate_flow_continue` | tool | Repair the one blocked Flow unit once. |
|
|
42
|
+
| `pi-subagent-delegated-development` | skill | Guide Main's planning and orchestration. |
|
|
43
|
+
|
|
44
|
+
Pi's built-in tool block shows each call and result.
|
|
48
45
|
|
|
49
|
-
|
|
46
|
+
Select exactly one `delegate_task` shape:
|
|
50
47
|
|
|
51
48
|
```text
|
|
52
49
|
// Single
|
|
@@ -59,21 +56,27 @@ Pi's built-in tool block shows each call and result. Select exactly one `delegat
|
|
|
59
56
|
{ chain: [{ role, name, task, model?, modelClass? }], background? }
|
|
60
57
|
```
|
|
61
58
|
|
|
62
|
-
Main supplies each `name`. It must be a short description, about five words and fewer than 30 characters
|
|
59
|
+
Main supplies each `name`. It must be a short description, about five words and fewer than 30 characters. Names cannot contain C0/C1 control characters such as newlines or terminal escapes. `role` and an explicit `model` also reject those controls.
|
|
60
|
+
|
|
61
|
+
`modelClass` selects `fast`, `balanced`, `frontier`, or `fav`. An explicit call class wins over a Role class. Without either, the configured task assignment or declared default applies. The route sets the model and exact thinking level.
|
|
62
|
+
|
|
63
|
+
An explicit `model` (`provider/modelId`) replaces only the route model and must support that thinking level. `background` applies to the whole selected mode, never one entry.
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
Parallel tasks start together, settle together, and report in input order. Chains are sequential and fail at the first failure. `{previous}` passes only the immediately preceding successful assistant output.
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
Foreground failures throw after keeping bounded sibling and recovery evidence. One call has one aggregate 50 KiB cap for Main-visible text. Live updates show task names and statuses without opaque IDs. Final results show summaries first and full evidence below.
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
Background work belongs to its launching session. Shutdown or reload aborts it and may leave only recoverable-work evidence or no follow-up message.
|
|
69
70
|
|
|
70
71
|
Each entry resolves its own Role, resources, route, and optional isolation. A Role with `isolation: worktree` gets a separate deterministic worktree when available. Non-Git and unborn-`HEAD` contexts can use Main's directory. Other setup failures, including unsafe submodule layouts, reject instead of falling back. Siblings and chain steps never share a created worktree.
|
|
71
72
|
|
|
72
73
|
See the [orchestration guide](./docs/orchestration.md) for full delegation, transport, isolation, and UI behavior.
|
|
73
74
|
|
|
74
|
-
###
|
|
75
|
+
### Skills
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
The bundled [`pi-subagent-delegated-development`](./skills/pi-subagent-delegated-development/SKILL.md) Skill guides Main's planning and orchestration. It adds no runtime code, config, or Role installation. `delegate_flow` owns its Git mechanics and validation authority.
|
|
78
|
+
|
|
79
|
+
## Flow
|
|
77
80
|
|
|
78
81
|
Flow requires a clean Main worktree on an attached branch with a committed `HEAD`. Use it only for independent Git changes that can merge in any order. Do not split units that overlap files, APIs, schemas, generated output, package metadata, lockfiles, or invariants.
|
|
79
82
|
|
|
@@ -82,16 +85,20 @@ delegate_flow({ units: [{ id, name, task, modelClass?, validation: [{ command, a
|
|
|
82
85
|
delegate_flow_continue({ guidance, modelClass? })
|
|
83
86
|
```
|
|
84
87
|
|
|
85
|
-
A Flow has 1–8 units with unique non-empty IDs
|
|
88
|
+
A Flow has 1–8 units with unique non-empty IDs and allows one active Flow. It freezes the effective Implementer at start. It freezes the Reviewer only when a unit requests `review`.
|
|
86
89
|
|
|
87
90
|
- Each unit gets one worktree. Implementers run in parallel. Flow handles units in declared order.
|
|
88
91
|
- Flow runs each declared command with its arguments. That validation is authoritative for objective checks.
|
|
89
92
|
- Without `review`, Flow fast-forwards the exact validated tip.
|
|
90
93
|
- With `review`, the Reviewer receives the exact `{base, tip, patchPath}` packet and must return exactly `PASS` before the same integration path. Use `review` only for stated judgment that validation cannot decide.
|
|
91
94
|
|
|
92
|
-
An explicit unit `modelClass` overrides both frozen Roles. Without one, each Role uses its own `modelClass
|
|
95
|
+
An explicit unit `modelClass` overrides both frozen Roles. Without one, each Role uses its own `modelClass`, configured `pi-subagent/delegateTask` assignment, or declared default.
|
|
96
|
+
|
|
97
|
+
One `delegate_flow_continue` can repair an Implementer, validation, or review block in the same worktree. Omitting its class keeps the unit's explicit class and frozen Role defaults. Supplying one replaces both Role defaults for that repair and its later Reviewer launch.
|
|
93
98
|
|
|
94
|
-
|
|
99
|
+
A second block is terminal. Rebase and infrastructure failures are terminal.
|
|
100
|
+
|
|
101
|
+
Flow has no dependency graph, saved recovery, automatic retry, aggregate review, or post-merge validation.
|
|
95
102
|
|
|
96
103
|
## Config
|
|
97
104
|
|
|
@@ -100,17 +107,29 @@ pi-subagent owns `~/.pi/agent/config/pi-subagent/config.json`. It is optional. A
|
|
|
100
107
|
| Field | Valid value | Default |
|
|
101
108
|
| --- | --- | --- |
|
|
102
109
|
| `maxSubagents` | Safe integer ≥ 1 | `5` |
|
|
103
|
-
| `maxTurns` | Safe integer ≥
|
|
110
|
+
| `maxTurns` | Safe integer ≥ 10 | `50` |
|
|
104
111
|
| `timeout.idleMinutes` | Positive minutes; minutes × 60,000 ≤ 2,147,483,647 ms | `10` |
|
|
105
112
|
| `timeout.maxMinutes` | Positive minutes greater than `idleMinutes`; minutes × 60,000 ≤ 2,147,483,647 ms | `30` |
|
|
106
113
|
|
|
114
|
+
`maxTurns` defaults to 50. Its minimum is 10.
|
|
115
|
+
|
|
107
116
|
Excess children wait FIFO without using a child timeout. A terminal response on turn 50 succeeds; an attempted continuation rejects with `turn_limit`.
|
|
108
117
|
|
|
118
|
+
### Final response handoff
|
|
119
|
+
|
|
120
|
+
Role launches reserve the final allowed turn for a response-only handoff. This includes `delegate_task` and every Implementer or Reviewer launch within `delegate_flow`.
|
|
121
|
+
|
|
122
|
+
After a continuing penultimate turn, Pi waits for its tools. It then disables all tools and requests a final report. The final allowed provider request has no tools. A terminal penultimate response gets no handoff.
|
|
123
|
+
|
|
124
|
+
The fixed decision packet asks for Status (completed, blocked, or incomplete), one-sentence Outcome, up to three concrete Evidence facts, Blocker, one material Risk, and one Suggested next action. It is the default. Exact output required by the assigned task or Role takes precedence. The child returns only that output, such as a Flow Reviewer's exact `PASS` or caller-required structured output. It reserves a turn within the existing hard limit; it never adds a model turn. Commits, validation, and retained-worktree facts from executor/Flow structured evidence remain authoritative; the model handoff supplies semantic context and a suggested next action.
|
|
125
|
+
|
|
126
|
+
A raw `createEphemeralSubagentExecutor` launch does not guarantee this handoff. A timeout, provider failure, or child-process failure can end a Role launch before handoff.
|
|
127
|
+
|
|
109
128
|
Malformed or unreadable JSON, a non-object root, unknown keys, and invalid values produce one warning. Invalid settings use defaults while valid settings still apply. If the effective maximum is not greater than the idle timeout, both timeout settings use defaults. The file is never rewritten.
|
|
110
129
|
|
|
111
130
|
`PI_SUBAGENT_MAX_SUBAGENTS` overrides `maxSubagents` for the session. It must be a positive integer. An invalid value prevents the extension from loading, so `delegate_task` is unavailable.
|
|
112
131
|
|
|
113
|
-
|
|
132
|
+
### Roles
|
|
114
133
|
|
|
115
134
|
Role Markdown files live beside the config file. They require frontmatter and a Markdown system prompt body.
|
|
116
135
|
|
|
@@ -139,24 +158,32 @@ The package always provides these built-in Roles. Their files leave `modelClass`
|
|
|
139
158
|
|
|
140
159
|
Flow uses the effective Implementer and, only when requested, Reviewer. The Scout is not part of Flow.
|
|
141
160
|
|
|
142
|
-
##
|
|
161
|
+
## API
|
|
143
162
|
|
|
144
|
-
|
|
163
|
+
The package root exports `loadRoles`, `resolveRoleSkills`, `resolveRoleLaunch`, `createRoleLaunch`, `createEphemeralSubagentExecutor`, and worktree helpers.
|
|
145
164
|
|
|
146
|
-
|
|
165
|
+
The executor works only inside the active Pi process. It does not discover or start a standalone Node.js Pi installation.
|
|
147
166
|
|
|
148
|
-
|
|
167
|
+
`finalizeChildWorktree` returns the breaking `WorktreePayload` lifecycle union. `pruned` proves zero commits, a clean tree, and removed worktree and branch. `retained` contains measured `commits` and `dirty` values. `recovery` has an actionable `note` and only completed measurements. An omitted recovery measurement is unknown.
|
|
149
168
|
|
|
150
|
-
|
|
169
|
+
See the [public Role and executor API](./docs/orchestration.md#public-role-and-executor-api) for contracts and a `prepare` example. Pass `modelClass` to `resolveRoleLaunch` to override a Role default.
|
|
151
170
|
|
|
152
|
-
##
|
|
171
|
+
## State and storage
|
|
153
172
|
|
|
154
|
-
|
|
173
|
+
Flow state is memory-only.
|
|
155
174
|
|
|
156
|
-
##
|
|
175
|
+
## Data, cost, and privacy
|
|
157
176
|
|
|
158
|
-
|
|
177
|
+
A Role selects base tools, extensions, named Skills, instructions, and optional worktree isolation. Named Skills resolve from Main's effective Pi registry. Unavailable names warn and skip.
|
|
159
178
|
|
|
160
|
-
|
|
179
|
+
Children disable ambient extension and Skill discovery. `tools: []` adds no base tools, but selected extension tools and caller tools still activate. `extensions: []` adds no Role extension bundle. `skills: []` adds no separately named Role Skills, but selected extension Skills still load.
|
|
161
180
|
|
|
162
|
-
|
|
181
|
+
An explicitly selected extension is trusted, not sandboxed. Its tools, Skills, and executable behavior load together. Select fewer trusted extensions to reduce scope. pi-subagent does not guess or remove undocumented dependencies.
|
|
182
|
+
|
|
183
|
+
Parent-only delegation tools and `ask_question` are always excluded. Requested Role or caller tool names are checked after provider loading. Unavailable tools fail before the first model turn.
|
|
184
|
+
|
|
185
|
+
## Limits and recovery
|
|
186
|
+
|
|
187
|
+
Flow never force-deletes recoverable work. Failed or uncertain units, and cleanup refusals after integration, retain their worktree path or branch for recovery.
|
|
188
|
+
|
|
189
|
+
See [Flow mechanics and recovery](./docs/orchestration.md#delegate_flow) for retained-work recovery.
|
package/dist/ephemeral.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { PiLaunch } from "./index.ts";
|
|
3
|
+
export declare const MIN_MAX_TURNS: 10;
|
|
3
4
|
export declare const DEFAULT_MAX_TURNS = 50;
|
|
4
5
|
export declare const EXECUTION_BUDGET_ENV = "PI_SUBAGENT_EXECUTION_BUDGET";
|
|
5
6
|
export interface EphemeralSubagentTimeout {
|
package/dist/ephemeral.js
CHANGED
|
@@ -5,6 +5,7 @@ import { StringDecoder } from "node:string_decoder";
|
|
|
5
5
|
import { hasDisplayControlCharacters } from "./display-text.js";
|
|
6
6
|
const MAX_OUTPUT_BYTES = 50 * 1024;
|
|
7
7
|
const MAX_JSON_EVENT_BYTES = 1024 * 1024;
|
|
8
|
+
export const MIN_MAX_TURNS = 10;
|
|
8
9
|
export const DEFAULT_MAX_TURNS = 50;
|
|
9
10
|
export const EXECUTION_BUDGET_ENV = "PI_SUBAGENT_EXECUTION_BUDGET";
|
|
10
11
|
const MAX_ACTIVITY_TEXT_BYTES = 4 * 1024;
|
|
@@ -66,9 +67,10 @@ function validateOptions(options) {
|
|
|
66
67
|
if (!Number.isSafeInteger(options.maxConcurrency) || options.maxConcurrency < 1) {
|
|
67
68
|
throw new RangeError("maxConcurrency must be a positive safe integer.");
|
|
68
69
|
}
|
|
69
|
-
const maxTurns = options.maxTurns
|
|
70
|
-
if (!Number.isSafeInteger(maxTurns) || maxTurns <
|
|
71
|
-
throw new RangeError(
|
|
70
|
+
const maxTurns = options.maxTurns === undefined ? DEFAULT_MAX_TURNS : options.maxTurns;
|
|
71
|
+
if (!Number.isSafeInteger(maxTurns) || maxTurns < MIN_MAX_TURNS) {
|
|
72
|
+
throw new RangeError(`maxTurns must be a safe integer >= ${MIN_MAX_TURNS}.`);
|
|
73
|
+
}
|
|
72
74
|
if (!options.timeout || typeof options.timeout !== "object")
|
|
73
75
|
throw new TypeError("timeout is required.");
|
|
74
76
|
const timeout = {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type AvailableModel, type ModelTask, type ProfileName, type ResolvedTaskRoute, type ThinkingLevel } from "@henryqw/pi-task-models";
|
|
3
3
|
export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.ts";
|
|
4
|
-
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, type EphemeralSubagentActivityEvent, type EphemeralSubagentErrorCode, type EphemeralSubagentExecutor, type EphemeralSubagentExecutorOptions, type EphemeralSubagentResult, type EphemeralSubagentRunInput, type EphemeralSubagentTimeout, } from "./ephemeral.ts";
|
|
4
|
+
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, MIN_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, 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";
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { extensionConfigDir } from "@henryqw/pi-config-store";
|
|
|
6
6
|
import { hasDisplayControlCharacters } from "./display-text.js";
|
|
7
7
|
import { loadTaskModelsConfig, modelReference, orderedProfileRoutes, PROFILE_NAMES, resolveConfiguredTaskRoute, resolveTaskModelRoute, } from "@henryqw/pi-task-models";
|
|
8
8
|
export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.js";
|
|
9
|
-
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, } from "./ephemeral.js";
|
|
9
|
+
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, MIN_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, } from "./ephemeral.js";
|
|
10
10
|
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, worktreeContextNote, } from "./worktree.js";
|
|
11
11
|
export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, } from "./review-evidence.js";
|
|
12
12
|
const CODEX_ALIAS = /^openai-codex-(?:[2-9]|[1-9]\d+)$/;
|
package/docs/orchestration.md
CHANGED
|
@@ -142,6 +142,14 @@ Children start with ambient extension and Skill discovery disabled. Only explici
|
|
|
142
142
|
|
|
143
143
|
Role Skill names resolve through Main's effective Pi Skill registry at launch. Missing names are returned in `ResolvedRoleLaunch.missingSkills`; `delegate_task` warns and skips them. Library callers must surface that warning themselves. Missing Skills do not block launch.
|
|
144
144
|
|
|
145
|
+
### Role final-turn handoff
|
|
146
|
+
|
|
147
|
+
Role launches made by `createRoleLaunch` reserve the final allowed turn for a response-only handoff. This includes `delegate_task` and every Implementer or Reviewer launch from `delegate_flow`. A raw `createEphemeralSubagentExecutor` launch does not guarantee a handoff.
|
|
148
|
+
|
|
149
|
+
After a continuing penultimate turn, Pi completes its tools. It then disables every active tool and queues one structured final handoff. The final allowed provider request has no tools. A terminal penultimate response gets no handoff.
|
|
150
|
+
|
|
151
|
+
The handoff requests a fixed Markdown decision packet with Status (`completed`, `blocked`, or `incomplete`), one-sentence Outcome, up to three concrete Evidence facts, Blocker, one material Risk, and one Suggested next action. It is the default. Exact output required by the assigned task or Role takes precedence, and the child replies only with it. This preserves a Flow Reviewer's exact `PASS` and caller-required structured output. It reserves a turn inside the executor's existing hard limit. It never adds a model turn. A timeout, provider failure, or child-process failure can end a Role launch before handoff. Commits, validation, and retained-worktree facts from executor/Flow structured evidence remain authoritative; the model handoff supplies semantic context and a suggested next action.
|
|
152
|
+
|
|
145
153
|
## Public Role and executor API
|
|
146
154
|
|
|
147
155
|
The package root exports the following mechanism-level APIs:
|
|
@@ -175,7 +183,7 @@ const executorOptions = {
|
|
|
175
183
|
};
|
|
176
184
|
```
|
|
177
185
|
|
|
178
|
-
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
|
|
186
|
+
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` must be a safe integer >= 1. `maxTurns` must be a safe integer >= 10. `idleMs` and `maxMs` must be positive. `maxMs` must exceed `idleMs`. Omitted `maxTurns` defaults to 50.
|
|
179
187
|
|
|
180
188
|
The executor is **active-Pi-only**. It reuses the currently running Pi invocation and does not locate or support a standalone Node.js Pi installation. Once direct Pi exits, stdout/stderr drain normally until EOF; an escaped descendant retaining either stream is cut off after short output inactivity or a one-second hard deadline so it cannot retain the FIFO permit.
|
|
181
189
|
|
|
@@ -257,7 +265,7 @@ export function createRunRole(pi) {
|
|
|
257
265
|
}
|
|
258
266
|
```
|
|
259
267
|
|
|
260
|
-
`run` resolves to `EphemeralSubagentResult`. Both outcome variants contain `exitCode`, `output`, `stderr`, and optional `stopReason`, `errorMessage`, and `usage`. A launched child/model failure is a typed `{ outcome: "failure", ... }` result. Abort, timeout, turn-limit, spawn, protocol, preparation, and callback failures reject with `EphemeralSubagentError` and a stable `code`. A terminal response at `maxTurns` succeeds; an attempted continuation rejects with `turn_limit`, accumulated `usage`, and bounded `output`. The executor
|
|
268
|
+
`run` resolves to `EphemeralSubagentResult`. Both outcome variants contain `exitCode`, `output`, `stderr`, and optional `stopReason`, `errorMessage`, and `usage`. A launched child/model failure is a typed `{ outcome: "failure", ... }` result. Abort, timeout, turn-limit, spawn, protocol, preparation, and callback failures reject with `EphemeralSubagentError` and a stable `code`. A terminal response at `maxTurns` succeeds; an attempted continuation rejects with `turn_limit`, accumulated `usage`, and bounded `output`. The executor passes its turn and runtime budget through the child environment. On a Role launch, its tool policy steers the fixed convergence warning before another model turn once at each 80% threshold, combines thresholds first due together, and uses no timer or extra turn. Assistant `output` and `stderr` are bounded, and `usage` contains aggregate child usage when Pi supplies it.
|
|
261
269
|
|
|
262
270
|
### Activity callbacks
|
|
263
271
|
|
package/extensions/config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { createConfigStore, extensionConfigPath } from "@henryqw/pi-config-store";
|
|
3
|
+
import { MIN_MAX_TURNS } from "@henryqw/pi-subagent";
|
|
3
4
|
|
|
4
5
|
export interface SubagentTimeoutConfig {
|
|
5
6
|
/** Minutes a child may stay idle before it is asked to stop. */
|
|
@@ -50,13 +51,21 @@ function parseSubagentConfig(parsed: unknown, path: string): ParsedSubagentConfi
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
config[key] = value;
|
|
54
|
+
const maxSubagents = record.maxSubagents;
|
|
55
|
+
if (maxSubagents !== undefined) {
|
|
56
|
+
if (typeof maxSubagents === "number" && Number.isSafeInteger(maxSubagents) && maxSubagents >= 1) {
|
|
57
|
+
config.maxSubagents = maxSubagents;
|
|
58
58
|
} else {
|
|
59
|
-
problems.push(
|
|
59
|
+
problems.push(`maxSubagents must be a safe integer >= 1, got ${JSON.stringify(maxSubagents)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const maxTurns = record.maxTurns;
|
|
64
|
+
if (maxTurns !== undefined) {
|
|
65
|
+
if (typeof maxTurns === "number" && Number.isSafeInteger(maxTurns) && maxTurns >= MIN_MAX_TURNS) {
|
|
66
|
+
config.maxTurns = maxTurns;
|
|
67
|
+
} else {
|
|
68
|
+
problems.push(`maxTurns must be a safe integer >= ${MIN_MAX_TURNS}, got ${JSON.stringify(maxTurns)}`);
|
|
60
69
|
}
|
|
61
70
|
}
|
|
62
71
|
|
package/extensions/role-tools.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { CHILD_EXCLUDED_TOOL_NAMES, EXECUTION_BUDGET_ENV, ROLE_TOOL_POLICY_FLAG } from "@henryqw/pi-subagent";
|
|
2
|
+
import { CHILD_EXCLUDED_TOOL_NAMES, EXECUTION_BUDGET_ENV, MIN_MAX_TURNS, ROLE_TOOL_POLICY_FLAG } from "@henryqw/pi-subagent";
|
|
3
3
|
|
|
4
4
|
const childExcludedTools: ReadonlySet<string> = new Set(CHILD_EXCLUDED_TOOL_NAMES);
|
|
5
5
|
const WARNING_RATIO = 0.8;
|
|
6
6
|
const WARNING_MESSAGE_TYPE = "pi-subagent-execution-budget";
|
|
7
|
+
const FINAL_HANDOFF_MESSAGE = {
|
|
8
|
+
customType: "pi-subagent-final-handoff",
|
|
9
|
+
content: "**Final handoff required.** Tools are disabled. If your assigned task or Role requires exact output, reply only with that output instead; it takes precedence over this decision packet. Otherwise, reply only with this decision packet:\n\n**Status:** completed | blocked | incomplete\n**Outcome:** one sentence describing what is now true\n**Evidence:** up to three concrete findings, changes, or checks; include an attempted approach only when it prevents Main from repeating failed work\n**Blocker:** none or the exact blocker\n**Risk:** none or one material risk\n**Suggested next:** none or one concrete action",
|
|
10
|
+
display: true,
|
|
11
|
+
};
|
|
7
12
|
|
|
8
13
|
function configuredTools(value: unknown): string[] {
|
|
9
14
|
if (typeof value !== "string") throw new Error(`${ROLE_TOOL_POLICY_FLAG} must be JSON tool names.`);
|
|
@@ -32,7 +37,7 @@ function executionBudget(value: string | undefined): { maxTurns: number; maxMs:
|
|
|
32
37
|
}
|
|
33
38
|
const budget = parsed as Record<string, unknown>;
|
|
34
39
|
if (Object.keys(budget).length !== 3 || !("maxTurns" in budget) || !("maxMs" in budget) || !("startedAt" in budget)
|
|
35
|
-
|| !Number.isSafeInteger(budget.maxTurns) || (budget.maxTurns as number) <
|
|
40
|
+
|| !Number.isSafeInteger(budget.maxTurns) || (budget.maxTurns as number) < MIN_MAX_TURNS
|
|
36
41
|
|| typeof budget.maxMs !== "number" || !Number.isFinite(budget.maxMs) || budget.maxMs <= 0
|
|
37
42
|
|| !Number.isSafeInteger(budget.startedAt) || (budget.startedAt as number) < 0) {
|
|
38
43
|
throw new Error(`${EXECUTION_BUDGET_ENV} must be a JSON execution budget.`);
|
|
@@ -53,6 +58,7 @@ export default function roleTools(pi: ExtensionAPI): void {
|
|
|
53
58
|
description: "Internal Pi Subagent Role tool policy",
|
|
54
59
|
type: "string",
|
|
55
60
|
});
|
|
61
|
+
const budget = executionBudget(process.env[EXECUTION_BUDGET_ENV]);
|
|
56
62
|
pi.on("session_start", () => {
|
|
57
63
|
const selected = configuredTools(pi.getFlag(ROLE_TOOL_POLICY_FLAG));
|
|
58
64
|
const allTools = pi.getAllTools();
|
|
@@ -68,15 +74,21 @@ export default function roleTools(pi: ExtensionAPI): void {
|
|
|
68
74
|
}
|
|
69
75
|
});
|
|
70
76
|
|
|
71
|
-
const budget = executionBudget(process.env[EXECUTION_BUDGET_ENV]);
|
|
72
77
|
if (!budget) return;
|
|
73
78
|
const warningTurn = Math.ceil(budget.maxTurns * WARNING_RATIO);
|
|
74
79
|
let completedTurns = 0;
|
|
80
|
+
let handoffSent = false;
|
|
75
81
|
let turnWarningSent = false;
|
|
76
82
|
let runtimeWarningSent = false;
|
|
77
83
|
pi.on("turn_end", (event) => {
|
|
78
84
|
completedTurns += 1;
|
|
79
|
-
if (!expectsAnotherTurn(event.message)) return;
|
|
85
|
+
if (!expectsAnotherTurn(event.message) || handoffSent) return;
|
|
86
|
+
if (completedTurns === budget.maxTurns - 1) {
|
|
87
|
+
pi.setActiveTools([]);
|
|
88
|
+
pi.sendMessage(FINAL_HANDOFF_MESSAGE, { deliverAs: "steer", triggerTurn: false });
|
|
89
|
+
handoffSent = true;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
80
92
|
const elapsedMs = Math.max(0, Date.now() - budget.startedAt);
|
|
81
93
|
const turnWarningDue = !turnWarningSent && completedTurns >= warningTurn;
|
|
82
94
|
const runtimeWarningDue = !runtimeWarningSent && elapsedMs >= budget.maxMs * WARNING_RATIO;
|