@henryqw/pi-subagent 11.0.1 → 11.0.4
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/README.md +67 -165
- package/dist/display-text.d.ts +5 -0
- package/dist/display-text.js +9 -0
- package/dist/ephemeral.js +2 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +10 -2
- package/extensions/delegate-flow.ts +1 -2
- package/extensions/subagent.ts +1 -2
- package/extensions/task-name.ts +4 -6
- package/extensions/workflow.ts +3 -2
- package/package.json +1 -1
- package/extensions/delegation.ts +0 -9
package/README.md
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
# `@henryqw/pi-subagent`
|
|
2
2
|
|
|
3
|
-
Main
|
|
3
|
+
Main, the parent Pi session, can delegate bounded single, parallel, and chained tasks to isolated child Roles. It also provides a fixed Git Flow for independent implementation work.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
7
|
## Why
|
|
8
8
|
|
|
9
9
|
- **Created for**: Delegate bounded work to isolated child Pi processes without losing Main's context.
|
|
10
|
-
- **Advantage**: Generic
|
|
10
|
+
- **Advantage**: Generic delegation and Git Flow use the same Role settings.
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
pi install npm:@henryqw/pi-task-models
|
|
16
|
-
pi install npm:@henryqw/pi-multi-codex
|
|
17
16
|
pi install npm:@henryqw/pi-subagent
|
|
18
17
|
```
|
|
19
18
|
|
|
@@ -21,228 +20,131 @@ pi install npm:@henryqw/pi-subagent
|
|
|
21
20
|
|
|
22
21
|
| Package | Why |
|
|
23
22
|
| --- | --- |
|
|
24
|
-
| `@henryqw/pi-
|
|
25
|
-
| `@henryqw/pi-task-models` | Required. Shared `fast` / `balanced` / `frontier` / `fav` routes. |
|
|
23
|
+
| `@henryqw/pi-task-models` | Required. Supplies `fast`, `balanced`, `frontier`, and `fav` model routes. |
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
The local config is optional and uses defaults quietly when missing. If shared task-model config is missing, pi-subagent warns once at session start because delegation needs a route.
|
|
25
|
+
Routes come from `~/.pi/agent/config/pi-task-models/config.json`. It stores explicit task overrides. An omitted class uses pi-subagent's `pi-subagent/delegateTask` default, `fast`. Missing shared model config warns once because delegation needs a route.
|
|
30
26
|
|
|
31
27
|
## Use
|
|
32
28
|
|
|
33
|
-
|
|
|
34
|
-
| --- | --- |
|
|
35
|
-
| `delegate_task` |
|
|
36
|
-
| `delegate_flow` |
|
|
37
|
-
| `delegate_flow_continue` |
|
|
38
|
-
|
|
39
|
-
All three delegation tools use Pi's built-in renderer. Its tool-execution block shows live updates and Pi's configured expansion hint; expanding shows the call and result content.
|
|
40
|
-
|
|
41
|
-
### `delegate_task`
|
|
29
|
+
| Tool | Purpose |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `delegate_task` | Run one bounded task, independent tasks in parallel, or dependent tasks in a chain. |
|
|
32
|
+
| `delegate_flow` | Implement and integrate 1–8 independent Git units. |
|
|
33
|
+
| `delegate_flow_continue` | Repair the one blocked Flow unit once. |
|
|
42
34
|
|
|
43
|
-
Select exactly one shape:
|
|
35
|
+
Pi's built-in tool block shows each call and result. Select exactly one `delegate_task` shape:
|
|
44
36
|
|
|
45
37
|
```text
|
|
46
38
|
// Single
|
|
47
39
|
{ role, name, task, model?, modelClass?, background? }
|
|
48
40
|
|
|
49
|
-
// Parallel: 1–8 independent
|
|
41
|
+
// Parallel: 1–8 independent tasks
|
|
50
42
|
{ tasks: [{ role, name, task, model?, modelClass? }], background? }
|
|
51
43
|
|
|
52
|
-
// Chain: 1–8 dependent
|
|
44
|
+
// Chain: 1–8 dependent tasks
|
|
53
45
|
{ chain: [{ role, name, task, model?, modelClass? }], background? }
|
|
54
46
|
```
|
|
55
47
|
|
|
56
|
-
Main supplies
|
|
57
|
-
|
|
58
|
-
#### Model routing
|
|
48
|
+
Main supplies each `name`. It must be a short description, about five words and fewer than 30 characters, with no C0/C1 control characters such as newlines or terminal escapes. `role` and an explicit `model` also reject those controls.
|
|
59
49
|
|
|
60
|
-
|
|
61
|
-
2. An omitted class uses pi-subagent's `pi-subagent/delegateTask` declaration. Its default is `fast`.
|
|
62
|
-
3. The route supplies the model and exact thinking level.
|
|
63
|
-
4. An explicit `model` is `provider/modelId`. It replaces only the route model and must support its thinking level.
|
|
50
|
+
`modelClass` selects `fast`, `balanced`, `frontier`, or `fav`. The route sets the model and exact thinking level. 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.
|
|
64
51
|
|
|
65
|
-
`
|
|
52
|
+
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. Foreground failures throw after keeping bounded sibling and recovery evidence.
|
|
66
53
|
|
|
67
|
-
|
|
54
|
+
One call has one aggregate 50 KiB cap for Main-visible text. Background work belongs to its launching session; shutdown or reload aborts it and may leave only recoverable-work evidence or no follow-up message.
|
|
68
55
|
|
|
69
|
-
|
|
56
|
+
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.
|
|
70
57
|
|
|
71
|
-
|
|
72
|
-
- Chain mode is sequential and fail-fast. Each literal `{previous}` receives only the immediately preceding successful assistant output.
|
|
73
|
-
- Foreground failures throw after retaining bounded sibling and recovery evidence.
|
|
74
|
-
- One tool call has one aggregate 50 KiB Main-visible transport cap. It is not 50 KiB per child.
|
|
75
|
-
- Background workflows are session-scoped. Session shutdown or reload aborts them and may deliver only recoverable-work evidence or no follow-up message.
|
|
76
|
-
- Each delegation resolves its own Role, resources, route, and optional worktree request.
|
|
77
|
-
- When available, `isolation: worktree` gives each entry a deterministic separate worktree. Non-Git or unborn-`HEAD` contexts may use Main's cwd.
|
|
78
|
-
- Siblings and chain steps never implicitly share one created worktree.
|
|
58
|
+
See the [orchestration guide](./docs/orchestration.md) for full delegation, transport, isolation, and UI behavior.
|
|
79
59
|
|
|
80
|
-
|
|
60
|
+
### Flow
|
|
81
61
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
Use Flow only for independent, commuting Git changes. Commuting changes can integrate in any order.
|
|
85
|
-
|
|
86
|
-
Flow accepts 1–8 uniquely identified units. Each unit has a required Main-supplied short `name`, bounded task, optional `modelClass`, direct command/argument validation gate, and optional non-empty `review` judgment criterion. Names must not contain C0/C1 control characters, including newlines and terminal escape characters.
|
|
62
|
+
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.
|
|
87
63
|
|
|
88
64
|
```text
|
|
89
65
|
delegate_flow({ units: [{ id, name, task, modelClass?, validation: [{ command, args }], review? }] })
|
|
90
66
|
delegate_flow_continue({ guidance, modelClass? })
|
|
91
67
|
```
|
|
92
68
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
Objective verification is authoritative. Flow always inspects committed Git state and runs declared validation.
|
|
96
|
-
|
|
97
|
-
A unit without `review` skips review evidence and Reviewer launch. It fast-forwards its exact validated tip through the existing guarded `git merge --ff-only` path.
|
|
98
|
-
|
|
99
|
-
Add `review` only for judgment that automation cannot establish. That unit keeps the exact `{base, tip, patchPath}` protocol and requires exact `PASS` before the same integration path.
|
|
69
|
+
A Flow has 1–8 units with unique non-empty IDs. It is memory-only and allows one active Flow. It freezes the effective Implementer at start and freezes the Reviewer only when a unit requests `review`.
|
|
100
70
|
|
|
101
|
-
|
|
71
|
+
- Each unit gets one worktree. Implementers run in parallel. Flow handles units in declared order.
|
|
72
|
+
- Flow runs each declared command with its arguments. That validation is authoritative for objective checks.
|
|
73
|
+
- Without `review`, Flow fast-forwards the exact validated tip.
|
|
74
|
+
- 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.
|
|
102
75
|
|
|
103
|
-
|
|
76
|
+
One `delegate_flow_continue` can repair an Implementer, validation, or review block in the same worktree. Omitting its class keeps the unit's class; supplying one changes that repair only. A second block is terminal. Rebase and infrastructure failures are terminal. Flow has no dependency graph, saved recovery, automatic retry, aggregate review, or post-merge validation.
|
|
104
77
|
|
|
105
|
-
Flow
|
|
106
|
-
|
|
107
|
-
After integration, it removes the worktree and branch non-forcibly. A refusal is a completion warning with the retained worktree path and/or branch.
|
|
108
|
-
|
|
109
|
-
A rebase that drops all unit commits is a no-op. Flow validates it, skips Reviewer and merge, then cleans up ordinarily.
|
|
110
|
-
|
|
111
|
-
Implementer, validation, or review blocks can be repaired once through `delegate_flow_continue` in the same worktree. An omitted continuation `modelClass` retains the blocked unit's current class. A supplied class replaces it for that one repair.
|
|
112
|
-
|
|
113
|
-
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.
|
|
114
|
-
|
|
115
|
-
Otherwise it is terminal and retains the affected worktree. Flow has no graph, saved recovery, automatic retry, aggregate review, or post-merge gate.
|
|
116
|
-
|
|
117
|
-
`delegate_task` keeps its generic isolation behavior. Flow uses the package-shipped Implementer by default and the package-shipped Reviewer only when a unit requests review; the built-in Scout is not part of Flow. Same-named user Roles remain supported overrides.
|
|
118
|
-
|
|
119
|
-
### Delegate UI summary
|
|
120
|
-
|
|
121
|
-
The transient status widget shows status glyph, role, status label, task summary, activity, and metrics for each child. Activity is `thinking…` or the active tool with elapsed time and path basename. Metrics are completed turns, started tools, model, thinking level, tokens, and total duration.
|
|
122
|
-
|
|
123
|
-
| Aspect | Behavior |
|
|
124
|
-
| --- | --- |
|
|
125
|
-
| Tool-execution block | Pi's built-in renderer: live updates and expandable call/result content |
|
|
126
|
-
| Widget rows | One line per child: glyph, `[role initial]`, short name, activity, metrics |
|
|
127
|
-
| Ordering | Active-first stable (working first, then insertion order) |
|
|
128
|
-
| Line cap | 6 physical lines max (≤6 items: all child rows; >6 items: 5 rows + 1 status-aware overflow) |
|
|
129
|
-
| Terminal retention | Active rows persist; terminal rows clear on next user input |
|
|
130
|
-
|
|
131
|
-
- Rows are active-first: working items first, then stable insertion order.
|
|
132
|
-
- The hard six-physical-line maximum shows all child rows for six or fewer items.
|
|
133
|
-
- Above six, it shows five child rows and one status-aware overflow line: `… N more · X working · Y complete · Z failed · W stopped`.
|
|
134
|
-
- Terminal rows clear on the next real user input. Active rows persist until the child settles.
|
|
135
|
-
- The final `delegate_task` block is deliberately minimal. It has bounded final summaries and role attribution for parallel and chain work. It shows only retained-worktree recovery paths.
|
|
78
|
+
Flow never force-deletes recoverable work. Failed or uncertain units, and cleanup refusals after integration, retain their worktree path or branch for recovery. See [Flow mechanics and recovery](./docs/orchestration.md#delegate_flow).
|
|
136
79
|
|
|
137
80
|
## Config
|
|
138
81
|
|
|
139
|
-
pi-subagent owns
|
|
140
|
-
|
|
141
|
-
One Markdown file belongs to each Role (see [Roles](#roles)). The other is its own optional JSON file below.
|
|
82
|
+
pi-subagent owns `~/.pi/agent/config/pi-subagent/config.json`. It is optional. A missing file uses these defaults without a warning.
|
|
142
83
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
-
| `
|
|
148
|
-
| `
|
|
149
|
-
| `timeout.idleMinutes` | No | Positive number of minutes where minutes × 60 000 ms ≤ 2,147,483,647 | `10` |
|
|
150
|
-
| `timeout.maxMinutes` | No | Positive number within the same ms cap that must be greater than `timeout.idleMinutes`, otherwise the whole `timeout` object falls back to defaults | `30` |
|
|
151
|
-
|
|
152
|
-
- Excess children wait FIFO without consuming child timeout.
|
|
153
|
-
- A terminal response on turn 50 succeeds.
|
|
154
|
-
- An attempted continuation starts no model work and rejects with `turn_limit`.
|
|
155
|
-
- Before a continuing turn, the child gets one warning when completed turns reach 80%.
|
|
156
|
-
- It gets another warning when elapsed time reaches 80% of the maximum runtime.
|
|
157
|
-
- If both thresholds are first reached together, the child gets one combined warning.
|
|
158
|
-
- `PI_SUBAGENT_MAX_SUBAGENTS` overrides `maxSubagents` for the session. It must be a positive integer. An invalid value prevents the extension from loading and leaves `delegate_task` unavailable.
|
|
159
|
-
|
|
160
|
-
This JSON is read leniently. Malformed JSON, a non-object root, unknown keys, or invalid values are collected into one warning.
|
|
161
|
-
|
|
162
|
-
The affected settings fall back to defaults. The file is never rewritten.
|
|
163
|
-
|
|
164
|
-
### Role frontmatter
|
|
84
|
+
| Field | Valid value | Default |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| `maxSubagents` | Safe integer ≥ 1 | `5` |
|
|
87
|
+
| `maxTurns` | Safe integer ≥ 1 | `50` |
|
|
88
|
+
| `timeout.idleMinutes` | Positive minutes; minutes × 60,000 ≤ 2,147,483,647 ms | `10` |
|
|
89
|
+
| `timeout.maxMinutes` | Positive minutes greater than `idleMinutes`; minutes × 60,000 ≤ 2,147,483,647 ms | `30` |
|
|
165
90
|
|
|
166
|
-
|
|
91
|
+
Excess children wait FIFO without using a child timeout. A terminal response on turn 50 succeeds; an attempted continuation rejects with `turn_limit`.
|
|
167
92
|
|
|
168
|
-
|
|
169
|
-
| --- | --- | --- | --- |
|
|
170
|
-
| `name` | Yes | Non-empty text; unique across roles | — |
|
|
171
|
-
| `description` | Yes | Non-empty text | — |
|
|
172
|
-
| `tools` | Yes | YAML array of non-empty tool names | `[]` activates no base built-ins; trusted extension tools and caller additions still activate |
|
|
173
|
-
| `isolation` | No | `worktree` | None |
|
|
174
|
-
| `extensions` | Yes | YAML array of absolute paths, `~/…`, `file://`, or package sources (`npm:`, `git:`, `github:`, `https?:`, `ssh:`) | `[]` selects no Role extension bundle |
|
|
175
|
-
| `skills` | Yes | YAML array of non-empty Skill names | `[]` selects no separately named Role Skills; trusted extension Skills still load |
|
|
176
|
-
| body | Yes | System-prompt Markdown after the frontmatter | — |
|
|
93
|
+
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.
|
|
177
94
|
|
|
178
|
-
|
|
95
|
+
`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.
|
|
179
96
|
|
|
180
97
|
## Roles
|
|
181
98
|
|
|
182
|
-
|
|
99
|
+
Role Markdown files live beside the config file. They require frontmatter and a Markdown system prompt body.
|
|
183
100
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
101
|
+
| Field | Requirement |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| `name` | Required unique, non-empty text without C0/C1 controls. |
|
|
104
|
+
| `description` | Required non-empty text without C0/C1 controls. |
|
|
105
|
+
| `tools` | Required YAML array of non-empty tool names. `[]` selects no base built-ins. |
|
|
106
|
+
| `isolation` | Optional; only `worktree`. |
|
|
107
|
+
| `extensions` | Required YAML array. Entries are absolute paths, `~/…`, `file://`, or `npm:`, `git:`, `github:`, `https?:`, or `ssh:` sources. |
|
|
108
|
+
| `skills` | Required YAML array of non-empty Skill names. |
|
|
109
|
+
| body | Required Markdown system prompt after the frontmatter. |
|
|
110
|
+
|
|
111
|
+
An unreadable or invalid Role fails loading fast. Duplicate Role names are rejected. A same-named user file overrides a built-in Role.
|
|
187
112
|
|
|
188
|
-
|
|
113
|
+
The package always provides these built-in Roles:
|
|
189
114
|
|
|
190
|
-
|
|
115
|
+
| Role | Purpose | Isolation/use |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| `implementer` | Make and validate one focused change. | Requests a worktree; commits scoped work locally. Never pushes or opens a PR without permission. |
|
|
118
|
+
| `reviewer` | Review supplied plans or files for correctness. | Read-only. For Flow review, reads the exact packet in the Unit Worktree; never edits or commits. |
|
|
119
|
+
| `scout` | Map code and evidence for one bounded task. | Read-only and generic `delegate_task` only; never changes files. |
|
|
191
120
|
|
|
192
|
-
|
|
121
|
+
Flow uses the effective Implementer and, only when requested, Reviewer. The Scout is not part of Flow.
|
|
193
122
|
|
|
194
|
-
|
|
123
|
+
[`synthesizer`](./examples/roles/synthesizer.md) is an optional sample for reconciling supplied reports. It does nothing until you copy it:
|
|
195
124
|
|
|
196
125
|
```bash
|
|
197
126
|
mkdir -p ~/.pi/agent/config/pi-subagent
|
|
198
127
|
cp <package-install-dir>/examples/roles/synthesizer.md ~/.pi/agent/config/pi-subagent/
|
|
199
128
|
```
|
|
200
129
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
The package never installs or writes Role configuration. After copying, edit or replace `synthesizer.md` as your own Role.
|
|
204
|
-
|
|
205
|
-
## Skill
|
|
206
|
-
|
|
207
|
-
The bundled [`pi-subagent-delegated-development`](./skills/pi-subagent-delegated-development/SKILL.md) Skill is Main-side planner and orchestrator policy only. `delegate_flow` owns its fixed Git mechanics and validation authority.
|
|
208
|
-
|
|
209
|
-
The Skill adds no runtime code, configuration, or Role installation. Generic orchestration remains outside the executor under [ADR 001](./docs/adr/001-composable-ephemeral-execution.md).
|
|
210
|
-
|
|
211
|
-
## Role, Skill, and resource trust
|
|
212
|
-
|
|
213
|
-
### Role resources
|
|
214
|
-
|
|
215
|
-
A Role explicitly owns base tools, extensions, named Skills, instructions, and optional `isolation: worktree`. Every launch installs its Role tool policy.
|
|
130
|
+
Use `npm root` in a project to find the package install directory. The package never installs or changes this file; after copying, it is yours.
|
|
216
131
|
|
|
217
|
-
|
|
132
|
+
## Role resources and trust
|
|
218
133
|
|
|
219
|
-
|
|
134
|
+
A Role selects base tools, extensions, named Skills, instructions, and optional worktree isolation. Named Skills resolve from Main's effective Pi registry; unavailable ones warn and skip.
|
|
220
135
|
|
|
221
|
-
|
|
136
|
+
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.
|
|
222
137
|
|
|
223
|
-
|
|
138
|
+
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.
|
|
224
139
|
|
|
225
|
-
|
|
140
|
+
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.
|
|
226
141
|
|
|
227
|
-
|
|
142
|
+
## Skill
|
|
228
143
|
|
|
229
|
-
|
|
230
|
-
- `skills: []` selects no separately named Role Skills. Trusted selected extension Skills still load.
|
|
231
|
-
- `extensions: []` selects no Role extension bundle.
|
|
232
|
-
- Parent-only recursive orchestration tools and interactive `ask_question` are excluded from children.
|
|
233
|
-
- Explicit Role or caller tool names are verified against the child’s final filtered active registry after provider extensions finish `session_start`.
|
|
234
|
-
- Unavailable tool names fail before the first model turn with provider-extension guidance. Unavailable named Skills warn and skip.
|
|
144
|
+
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.
|
|
235
145
|
|
|
236
146
|
## Library API
|
|
237
147
|
|
|
238
|
-
The package root exports
|
|
239
|
-
|
|
240
|
-
The executor is for code already running inside active Pi. It does not provide standalone Node.js Pi discovery or launch support.
|
|
241
|
-
|
|
242
|
-
It defaults to a hard limit of 50 turns. An attempted continuation rejects with `turn_limit` while preserving accumulated usage and bounded output.
|
|
243
|
-
|
|
244
|
-
After Pi exits, the executor drains inherited stdout and stderr normally. It destroys streams held by escaped descendants after a short inactivity deadline or one-second hard deadline, so they cannot retain a pool permit.
|
|
245
|
-
|
|
246
|
-
Use [`docs/orchestration.md`](./docs/orchestration.md#public-role-and-executor-api) for exact API behavior.
|
|
148
|
+
The package root exports `loadRoles`, `resolveRoleSkills`, `resolveRoleLaunch`, `createRoleLaunch`, `createEphemeralSubagentExecutor`, and worktree helpers. The executor works only inside the active Pi process; it does not discover or start a standalone Node.js Pi installation.
|
|
247
149
|
|
|
248
|
-
|
|
150
|
+
See the [public Role and executor API](./docs/orchestration.md#public-role-and-executor-api) for contracts and a `prepare` example.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const C0_C1_CONTROL_RANGES = "\\u0000-\\u001F\\u007F-\\u009F";
|
|
2
|
+
export const DISPLAY_TEXT_CONTRACT = {
|
|
3
|
+
controlRanges: C0_C1_CONTROL_RANGES,
|
|
4
|
+
pattern: `^(?![\\s\\S]*[${C0_C1_CONTROL_RANGES}])[\\s\\S]+$`,
|
|
5
|
+
};
|
|
6
|
+
const DISPLAY_TEXT_CONTROL_PATTERN = new RegExp(`[${DISPLAY_TEXT_CONTRACT.controlRanges}]`, "u");
|
|
7
|
+
export function hasDisplayControlCharacters(value) {
|
|
8
|
+
return DISPLAY_TEXT_CONTROL_PATTERN.test(value);
|
|
9
|
+
}
|
package/dist/ephemeral.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { basename } from "node:path";
|
|
4
4
|
import { StringDecoder } from "node:string_decoder";
|
|
5
|
+
import { hasDisplayControlCharacters } from "./display-text.js";
|
|
5
6
|
const MAX_OUTPUT_BYTES = 50 * 1024;
|
|
6
7
|
const MAX_JSON_EVENT_BYTES = 1024 * 1024;
|
|
7
8
|
export const DEFAULT_MAX_TURNS = 50;
|
|
@@ -249,10 +250,7 @@ function activityTooLong(value) {
|
|
|
249
250
|
return typeof value === "string" && Buffer.byteLength(value, "utf8") > MAX_ACTIVITY_TEXT_BYTES;
|
|
250
251
|
}
|
|
251
252
|
function hasTerminalControlChars(text) {
|
|
252
|
-
return
|
|
253
|
-
const code = character.codePointAt(0);
|
|
254
|
-
return code <= 0x1f || code >= 0x7f && code <= 0x9f || code === 0x2028 || code === 0x2029;
|
|
255
|
-
});
|
|
253
|
+
return hasDisplayControlCharacters(text) || /[\u2028\u2029]/u.test(text);
|
|
256
254
|
}
|
|
257
255
|
function activityText(value) {
|
|
258
256
|
return typeof value === "string" && value.trim().length > 0 && !activityTooLong(value) && !hasTerminalControlChars(value);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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
|
+
export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.ts";
|
|
3
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
5
|
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, worktreeContextNote, type WorktreeDirtyInspection, type WorktreeInfo, type WorktreePayload, } from "./worktree.ts";
|
|
5
6
|
export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, type PreparedReviewEvidence, type PrepareExactReviewEvidenceInput, } from "./review-evidence.ts";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,9 @@ import { isAbsolute, join } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { extensionConfigDir } from "@henryqw/pi-config-store";
|
|
6
|
+
import { hasDisplayControlCharacters } from "./display-text.js";
|
|
6
7
|
import { loadTaskModelsConfig, modelReference, orderedProfileRoutes, resolveConfiguredTaskRoute, resolveTaskModelRoute, } from "@henryqw/pi-task-models";
|
|
8
|
+
export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.js";
|
|
7
9
|
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, DEFAULT_MAX_TURNS, EphemeralSubagentError, EXECUTION_BUDGET_ENV, formatDuration, } from "./ephemeral.js";
|
|
8
10
|
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, worktreeContextNote, } from "./worktree.js";
|
|
9
11
|
export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, } from "./review-evidence.js";
|
|
@@ -26,6 +28,12 @@ const cleanText = (value, field, source) => {
|
|
|
26
28
|
}
|
|
27
29
|
return value.trim();
|
|
28
30
|
};
|
|
31
|
+
const cleanDisplayText = (value, field, source) => {
|
|
32
|
+
if (typeof value === "string" && hasDisplayControlCharacters(value)) {
|
|
33
|
+
throw new Error(`${source}: ${field} must not contain C0/C1 control characters.`);
|
|
34
|
+
}
|
|
35
|
+
return cleanText(value, field, source);
|
|
36
|
+
};
|
|
29
37
|
const stringList = (value, field, source) => {
|
|
30
38
|
if (value === undefined)
|
|
31
39
|
throw new Error(`${source}: ${field} is required.`);
|
|
@@ -62,8 +70,8 @@ function parseRoleFile(file, raw) {
|
|
|
62
70
|
if (isolation !== undefined && isolation !== "worktree")
|
|
63
71
|
throw new Error(`${file}: isolation must be "worktree".`);
|
|
64
72
|
return {
|
|
65
|
-
name:
|
|
66
|
-
description:
|
|
73
|
+
name: cleanDisplayText(frontmatter.name, "name", file),
|
|
74
|
+
description: cleanDisplayText(frontmatter.description, "description", file),
|
|
67
75
|
tools: stringList(frontmatter.tools, "tools", file),
|
|
68
76
|
isolation,
|
|
69
77
|
extensions: extensionList(frontmatter.extensions, file),
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
} from "@henryqw/pi-subagent";
|
|
20
20
|
import { Type, type Static } from "typebox";
|
|
21
21
|
import { Check } from "typebox/value";
|
|
22
|
-
import { runDelegation } from "./delegation.ts";
|
|
23
22
|
import { MODEL_CLASS_GUIDANCE } from "./model-class-policy.ts";
|
|
24
23
|
import { TASK_NAME_CONTRACT, TaskNameSchema, normalizeTaskName } from "./task-name.ts";
|
|
25
24
|
|
|
@@ -356,7 +355,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
356
355
|
let started = false;
|
|
357
356
|
try {
|
|
358
357
|
assertCurrent(flow);
|
|
359
|
-
const result = await
|
|
358
|
+
const result = await runtime.executor.run({
|
|
360
359
|
signal,
|
|
361
360
|
onTokens: (tokens) => runtime.updateWidgetTokens(widgetId, tokens),
|
|
362
361
|
onActivity: (event) => runtime.updateWidgetActivity(widgetId, event),
|
package/extensions/subagent.ts
CHANGED
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
} from "@henryqw/pi-subagent";
|
|
35
35
|
import { DEFAULT_TIMEOUT_CONFIG, readSubagentConfig, type SubagentTimeoutConfig } from "./config.ts";
|
|
36
36
|
import { registerDelegateFlow } from "./delegate-flow.ts";
|
|
37
|
-
import { runDelegation } from "./delegation.ts";
|
|
38
37
|
import { MODEL_CLASS_GUIDANCE } from "./model-class-policy.ts";
|
|
39
38
|
import {
|
|
40
39
|
formatBackgroundWorkflowResult,
|
|
@@ -641,7 +640,7 @@ export default function subagentExtension(
|
|
|
641
640
|
: { ...base, status: nextStatus, assistantOutput: nextText });
|
|
642
641
|
};
|
|
643
642
|
try {
|
|
644
|
-
child = await
|
|
643
|
+
child = await executor.run({
|
|
645
644
|
signal: workflowSignal,
|
|
646
645
|
onUpdate: (output) => {
|
|
647
646
|
setState("running", output);
|
package/extensions/task-name.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "@henryqw/pi-subagent";
|
|
1
2
|
import { Type, type Static } from "typebox";
|
|
2
3
|
|
|
3
4
|
const TASK_NAME_MAX_LENGTH = 29 as const;
|
|
4
|
-
const TASK_NAME_CONTROL_RANGES = "\\u0000-\\u001F\\u007F-\\u009F" as const;
|
|
5
5
|
const TASK_NAME_LIMIT_WORDING = `about five words and fewer than ${TASK_NAME_MAX_LENGTH + 1} characters`;
|
|
6
6
|
const TASK_NAME_WORDING = `short descriptive name of ${TASK_NAME_LIMIT_WORDING}`;
|
|
7
7
|
|
|
@@ -10,8 +10,8 @@ export const TASK_NAME_CONTRACT = {
|
|
|
10
10
|
maxLength: TASK_NAME_MAX_LENGTH,
|
|
11
11
|
description: `Short descriptive task name, ${TASK_NAME_LIMIT_WORDING}; C0/C1 control characters are rejected.`,
|
|
12
12
|
promptGuidance: `Use a ${TASK_NAME_WORDING} without C0/C1 control characters.`,
|
|
13
|
-
controlRanges:
|
|
14
|
-
pattern:
|
|
13
|
+
controlRanges: DISPLAY_TEXT_CONTRACT.controlRanges,
|
|
14
|
+
pattern: DISPLAY_TEXT_CONTRACT.pattern,
|
|
15
15
|
} as const;
|
|
16
16
|
|
|
17
17
|
export const TaskNameSchema = Type.String({
|
|
@@ -23,10 +23,8 @@ export const TaskNameSchema = Type.String({
|
|
|
23
23
|
|
|
24
24
|
export type TaskName = Static<typeof TaskNameSchema>;
|
|
25
25
|
|
|
26
|
-
const TASK_NAME_CONTROL_PATTERN = new RegExp(`[${TASK_NAME_CONTRACT.controlRanges}]`, "u");
|
|
27
|
-
|
|
28
26
|
export function normalizeTaskName(value: TaskName, path: string): TaskName {
|
|
29
|
-
if (
|
|
27
|
+
if (hasDisplayControlCharacters(value)) throw new Error(`${path} must not contain C0/C1 control characters.`);
|
|
30
28
|
const normalized = value.trim();
|
|
31
29
|
if (!normalized) throw new Error(`${path} must be non-empty text.`);
|
|
32
30
|
return normalized;
|
package/extensions/workflow.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { DISPLAY_TEXT_CONTRACT } from "@henryqw/pi-subagent";
|
|
2
3
|
import { PROFILE_NAMES } from "@henryqw/pi-task-models";
|
|
3
4
|
import { Type, type Static } from "typebox";
|
|
4
5
|
import { Check } from "typebox/value";
|
|
@@ -6,9 +7,9 @@ import { TaskNameSchema, normalizeTaskName } from "./task-name.ts";
|
|
|
6
7
|
|
|
7
8
|
export const MAX_WORKFLOW_ENTRIES = 8;
|
|
8
9
|
|
|
9
|
-
const RoleSchema = Type.String({ minLength: 1, description: "Configured Subagent role name" });
|
|
10
|
+
const RoleSchema = Type.String({ minLength: 1, pattern: DISPLAY_TEXT_CONTRACT.pattern, description: "Configured Subagent role name" });
|
|
10
11
|
const TaskSchema = Type.String({ minLength: 1, description: "Bounded task packet" });
|
|
11
|
-
const ModelSchema = Type.String({ minLength: 1, description: "Designated model as provider/modelId; replaces the selected route model" });
|
|
12
|
+
const ModelSchema = Type.String({ minLength: 1, pattern: DISPLAY_TEXT_CONTRACT.pattern, description: "Designated model as provider/modelId; replaces the selected route model" });
|
|
12
13
|
const ModelClassSchema = StringEnum(PROFILE_NAMES, { description: "Task model profile" });
|
|
13
14
|
|
|
14
15
|
export const DelegationSchema = Type.Object({
|
package/package.json
CHANGED
package/extensions/delegation.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { EphemeralSubagentExecutor, EphemeralSubagentResult, EphemeralSubagentRunInput } from "@henryqw/pi-subagent";
|
|
2
|
-
|
|
3
|
-
/** Runs one caller-prepared child launch without making lifecycle decisions. */
|
|
4
|
-
export function runDelegation(
|
|
5
|
-
executor: EphemeralSubagentExecutor,
|
|
6
|
-
input: EphemeralSubagentRunInput,
|
|
7
|
-
): Promise<EphemeralSubagentResult> {
|
|
8
|
-
return executor.run(input);
|
|
9
|
-
}
|