@henryqw/pi-subagent 27.1.2 → 27.2.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 -0
- package/README.md +16 -5
- package/dist/direct-herdr.js +1 -1
- package/dist/index.d.ts +36 -1
- package/dist/index.js +96 -29
- package/extensions/config.ts +13 -150
- package/extensions/isolated.ts +8 -1
- package/extensions/subagent.ts +45 -14
- package/package.json +3 -3
package/CONTEXT.md
CHANGED
|
@@ -36,6 +36,7 @@ Own Role-configured Pi delegation through one `delegate_task` surface:
|
|
|
36
36
|
### Global limits
|
|
37
37
|
|
|
38
38
|
- `config/pi-subagent/config.json` is the only user execution-policy source. It owns `maxSubagents`, `maxTurns`, optional `maxTokens`, `maxCorrections`, and child idle/hard timeout.
|
|
39
|
+
- The config loads through `@henryqw/pi-config-store` fields. An unknown key or an invalid bounded limit, timeout, or `roleDefaults` key warns at launch and uses its finite default. An invalid `maxTokens`, or a file that cannot be read or parsed, disables delegation with an error, because the token default is unlimited. Role Markdown files keep per-file exclusion; durable state stays strict.
|
|
39
40
|
- Request fields cannot override or replenish policy. Durable requests store the policy snapshot and cumulative correction count; current config may tighten the correction allowance.
|
|
40
41
|
- Productive execution has no whole-run wall-clock deadline. Long productive work and later resume remain valid. Direct Herdr observation also has no whole-task elapsed deadline; worker idleness is bounded.
|
|
41
42
|
- Child idle/hard runtime, turn/token handling, outer abort, process I/O, status inspection, termination, and cleanup remain independently bounded. Direct Herdr workers use idle limits, not ephemeral child hard runtime.
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ Commands are for you. Delegation and state tools and the packaged skill are for
|
|
|
45
45
|
| `subagent_abort` | tool | Abort an isolated request only when no retained candidates or integration worktrees remain; cannot discard them. |
|
|
46
46
|
| `git_read` | tool | For Role-declared children (not Main): inspect bounded local Git evidence without Bash, tests, network access, or writes. |
|
|
47
47
|
| `pi-subagent` | skill | Guide Main through delegation, authorization, checks, integration, and recovery. |
|
|
48
|
-
| Direct status widget (`D`) | ui | For you in the TUI: show direct worker references (such as `D7.1`), status, Role badges, names, route, task elapsed time, measured
|
|
48
|
+
| Direct status widget (`D`) | ui | For you in the TUI: show direct worker references (such as `D7.1`), status, Role badges, names, route, task elapsed time, measured task tokens, pending tools, and the latest tool's recorded time. |
|
|
49
49
|
| Isolated status widget (`I`) | ui | For you in the TUI: show recorded task state, Role/model-class badges, and retained workspaces; readiness does not mean promotion. |
|
|
50
50
|
|
|
51
51
|

|
|
@@ -56,7 +56,9 @@ The coordinator is extension code that runs in Main's Pi process. It is not anot
|
|
|
56
56
|
|
|
57
57
|
The TUI has two separate compact status widgets. Each widget has one row for each visible worker or task. `D` marks direct work, and `I` marks isolated work. Badges such as `[S]` and `[I3]` identify the Role initial. Isolated badges also include the model class: `1` fast, `2` balanced, `3` frontier, `*` fav.
|
|
58
58
|
|
|
59
|
-
Each direct row starts with a saved direct reference such as `D7.1`. `D7` is the workflow number in this Pi session, and `.1` is the position of the worker in the original request. `/subagent` uses the same reference and name. The Herdr tab label shows them at launch. Numbers continue after reload, and the session never uses a number again. A session switch can move a workflow into a session that already uses its number. In that case, the workflow gets a new number there. An existing Herdr tab keeps its original launch label. The direct widget shows the launched model, thinking level, elapsed time, and measured
|
|
59
|
+
Each direct row starts with a saved direct reference such as `D7.1`. `D7` is the workflow number in this Pi session, and `.1` is the position of the worker in the original request. `/subagent` uses the same reference and name. The Herdr tab label shows them at launch. Numbers continue after reload, and the session never uses a number again. A session switch can move a workflow into a session that already uses its number. In that case, the workflow gets a new number there. An existing Herdr tab keeps its original launch label. The direct widget shows the launched model, thinking level, elapsed time, and measured task tokens (`— tok` until usage is available). Failed rows show before working and completed rows. When rows overflow, `… N more` includes counts by status. The isolated widget shows the recorded task state and any retained workspace. It does not show reliable live model, thinking, or token readings from Herdr. Attention shows before ordinary work, and overflow uses `+N more · /subagent`. An aborted request with visible tasks has its own `request aborted` row. This row does not change the recorded statuses of the tasks. A committed worker without a checked candidate can show attention, not readiness. Status glyphs use the active TUI theme. The text also names the status without color.
|
|
60
|
+
|
|
61
|
+
For the direct widget and result summaries, measured tokens are the cumulative input and output tokens from completed assistant usage records for the current task turn. Cache-read and cache-write tokens are excluded. A continued worker counts only its new task turn. Missing usage stays unknown. This display count does not change token-limit accounting.
|
|
60
62
|
|
|
61
63
|
A ready isolated candidate is **not promoted** to Main. To reload the menu and the isolated widget from saved state, choose **Refresh** in `/subagent`. Refresh does not recover or change a request. Use `/subagent` or `subagent_status` for exact identities, failures, and recovery.
|
|
62
64
|
|
|
@@ -111,7 +113,7 @@ Tab creation can time out or return an incomplete or mismatched identity. In tha
|
|
|
111
113
|
|
|
112
114
|
Tasks in a write request must return one JSON completion object, such as `{"outcome":"succeeded","answer":"Scoped commit completed."}`. `outcome` must be `succeeded`, `failed`, or `blocked`, and `answer` must be non-empty. A normal final Pi turn is not sufficient. Failure, blocked work, plain text, or a malformed completion stops later dispatch. But admission depends on proved termination, not on the reported outcome. Success is the explicit report of the worker. It is not an independent validation of its changes.
|
|
113
115
|
|
|
114
|
-
The tool returns a task ID and the first Herdr tab after launch, not the answer. The extension observes each worker and sends one result to Main when the workflow finishes. If Main is busy, Pi queues the result after the current turn. If Main is idle, the result starts a turn. The extension does not report a blocked, stalled, unknown, or truncated result as success. In the result, the line of each launched worker shows its model, thinking level, measured
|
|
116
|
+
The tool returns a task ID and the first Herdr tab after launch, not the answer. The extension observes each worker and sends one result to Main when the workflow finishes. If Main is busy, Pi queues the result after the current turn. If Main is idle, the result starts a turn. The extension does not report a blocked, stalled, unknown, or truncated result as success. In the result, the line of each launched worker shows its model, thinking level, measured task tokens, and elapsed time, such as `✓ [1/2] Check · worker · provider/model · thinking high · 1.5k tok · 42s`. If Pi recorded no usage, the line shows `tokens unknown`. Failed and stopped workers show the usage that the extension measured until they ended. A worker that the extension skipped before launch shows no usage. The result details contain the same values in each entry as `model`, `thinkingLevel`, `tokens`, and `elapsedMs`. When the token count is unknown, `tokens` is absent. If a task fails after its worker produced text, the result keeps a bounded excerpt marked as partial output. This excerpt is never success. Every worker result delivered to Main starts with a header. The header states that the text is the output of the worker and carries no user authority. Text that imitates harness framing gets an inserted backslash. Examples are `<system-reminder>`-style tags and lines that start with `Human:` or `Assistant:`. Nothing else changes. Isolated status and follow-up answer previews use the same header. A session switch or shutdown stops observation and preserves tab identities for recovery. To inspect exact tabs, agents, and Pi session files, open `/subagent` on the session branch. This includes tabs launched after the first. The menu has one row for each workflow. A single worker shows its reference, Role, and name (`D7.1 · worker · Search audit`). A multi-worker workflow shows its reference and worker count (`D8 · 3 workers`). When you select it, the menu lists each recorded worker by reference, Role, and name with its exact IDs. References and names help you match rows. The extension never uses them to decide ownership. Older records without saved references show their saved allocation label or exact task ID. A `recorded only` row does not prove that its workers run or finished. The menu labels locally observed work separately.
|
|
115
117
|
|
|
116
118
|
### Continue or stop one direct worker
|
|
117
119
|
|
|
@@ -220,11 +222,19 @@ pi-subagent owns `~/.pi/agent/config/pi-subagent/config.json`. If the file is mi
|
|
|
220
222
|
| `maxCorrections` | Same-worker automatic corrections per isolated request | Safe integer ≥ 0 | `1` |
|
|
221
223
|
| `timeout.idleMinutes` | Direct worker and ephemeral child idle timeout | Positive and within Node's timer range | `10` |
|
|
222
224
|
| `timeout.maxMinutes` | Ephemeral child hard runtime | Greater than idle and within Node's timer range | `30` |
|
|
223
|
-
| `roleDefaults` | Optional `extensions`, `tools`, and `skills` arrays merged into every built-in and user Role, with the same validation as Role fields | Object
|
|
225
|
+
| `roleDefaults` | Optional `extensions`, `tools`, and `skills` arrays merged into every built-in and user Role, with the same validation as Role fields | Object with `extensions`, `tools`, or `skills` | None |
|
|
224
226
|
|
|
225
227
|
In each merged list, `roleDefaults` entries come first, and the entries of the Role follow. The merge removes duplicates and occurs before resource resolution. Use `roleDefaults` for the provider extension that every child needs. Limits come only from this global file. Request fields cannot override or replenish them. Existing durable requests keep their recorded policy, but a lower current correction limit can tighten recovery. Intentionally, there is no whole-run timeout setting.
|
|
226
228
|
|
|
227
|
-
|
|
229
|
+
A bad config never stops the extension. At launch, and again each time it reads the file (a `delegate_task` call, each later worker of a direct chain, each isolated Role launch, `/subagent roles`, or a status inspection), the extension shows each new problem one time:
|
|
230
|
+
|
|
231
|
+
- An unknown key shows a warning. The extension ignores the key.
|
|
232
|
+
- A misspelled key that is near a missing key, such as `maxToken` for `maxTokens`, counts as an invalid value of that key. The warning gives the likely key.
|
|
233
|
+
- An invalid value in `maxSubagents`, `maxTurns`, `maxCorrections`, or `timeout` shows a warning. The extension uses the finite default for that key. If `timeout.maxMinutes` is not greater than `timeout.idleMinutes`, the extension uses the default `timeout`.
|
|
234
|
+
- An unknown or invalid key in `roleDefaults` shows a warning. The extension ignores only that key, so Roles get fewer default resources.
|
|
235
|
+
- An invalid or misspelled `maxTokens`, malformed JSON, invalid UTF-8, a file larger than 64 KiB, or an unreadable file shows an error and blocks delegation. The default token limit is unlimited, so the extension does not use a default in these cases. `/subagent` stays available. Correct the file to enable delegation again. The block also applies to work that already runs: the next worker of a direct workflow, or the next Role launch of an isolated request, fails before it starts. A workflow that runs keeps the limits that it started with. If the file had the problem when the session started, also run `/reload`, because the session took its execution limits from the defaults.
|
|
236
|
+
|
|
237
|
+
No key is required. The extension preserves the file and never rewrites it automatically.
|
|
228
238
|
|
|
229
239
|
### Roles
|
|
230
240
|
|
|
@@ -274,6 +284,7 @@ The servers named in `mcps` come only from the global `~/.pi/agent/mcp.json`. In
|
|
|
274
284
|
| Surface | Type | Purpose |
|
|
275
285
|
| --- | --- | --- |
|
|
276
286
|
| `loadRoleCatalog`, `findRole`, `parseRoleName`, `parseRoleDefaults` | functions | Load built-in and user Roles with `roleDefaults` and per-file errors, then select one by name. |
|
|
287
|
+
| `registerRoleDefaultsReporting(pi, agentDir?)` | function | Reports `roleDefaults` problems at each session start. Returns `report(ctx)`; call it before a lazy `loadRoleCatalog`. Each problem shows one time in each session. |
|
|
277
288
|
| `resolveRoleSkills`, `resolveRoleLaunch`, `resolveConfiguredRoleLaunch` | functions | Resolve effective Pi resources and model routes. |
|
|
278
289
|
| `prepareRoleLaunch`, `finalizeRoleLaunch` | functions | Prepare Role prompts, arguments, and tool policy. |
|
|
279
290
|
| `createEphemeralSubagentExecutor` | function | Run bounded no-session Pi children through a FIFO pool. |
|
package/dist/direct-herdr.js
CHANGED
|
@@ -155,7 +155,7 @@ export function directSessionUsage(jsonl, prompt) {
|
|
|
155
155
|
else if (active && message.role === "assistant") {
|
|
156
156
|
if (message.usage && typeof message.usage === "object") {
|
|
157
157
|
const usage = message.usage;
|
|
158
|
-
const values = [usage.input, usage.output
|
|
158
|
+
const values = [usage.input, usage.output];
|
|
159
159
|
if (values.every((value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0)) {
|
|
160
160
|
total += values.reduce((sum, value) => sum + value, 0);
|
|
161
161
|
observed = true;
|
package/dist/index.d.ts
CHANGED
|
@@ -94,6 +94,40 @@ export interface ResolvedRoleSkills {
|
|
|
94
94
|
export declare function parseRoleName(value: unknown, source?: string): RoleName;
|
|
95
95
|
/** Validate `config.json` `roleDefaults` with the same rules as Role fields; `label` prefixes errors. */
|
|
96
96
|
export declare function parseRoleDefaults(value: unknown, label?: string): RoleDefaults;
|
|
97
|
+
export interface SubagentConfig {
|
|
98
|
+
maxSubagents: number;
|
|
99
|
+
maxTurns: number;
|
|
100
|
+
maxTokens: number | undefined;
|
|
101
|
+
maxCorrections: number;
|
|
102
|
+
timeout: {
|
|
103
|
+
idleMinutes: number;
|
|
104
|
+
maxMinutes: number;
|
|
105
|
+
};
|
|
106
|
+
roleDefaults: RoleDefaults;
|
|
107
|
+
}
|
|
108
|
+
export declare const DEFAULT_SUBAGENT_CONFIG: Readonly<SubagentConfig>;
|
|
109
|
+
/**
|
|
110
|
+
* The user `config.json` store shared by the extension and `loadRoleCatalog`.
|
|
111
|
+
* Invalid bounded limits and `roleDefaults` keys fall back to defaults; an
|
|
112
|
+
* invalid `maxTokens` disables the config because its default is unlimited.
|
|
113
|
+
*/
|
|
114
|
+
export declare function createSubagentConfigStore(agentDir?: string): import("@henryqw/pi-config-store").ConfigStore<import("@henryqw/pi-config-store").FieldsValue<{
|
|
115
|
+
maxSubagents: import("@henryqw/pi-config-store").Field<number>;
|
|
116
|
+
maxTurns: import("@henryqw/pi-config-store").Field<number>;
|
|
117
|
+
maxTokens: import("@henryqw/pi-config-store").Field<number | undefined>;
|
|
118
|
+
maxCorrections: import("@henryqw/pi-config-store").Field<number>;
|
|
119
|
+
timeout: import("@henryqw/pi-config-store").Field<{
|
|
120
|
+
idleMinutes: number;
|
|
121
|
+
maxMinutes: number;
|
|
122
|
+
}>;
|
|
123
|
+
roleDefaults: import("@henryqw/pi-config-store").Field<RoleDefaults>;
|
|
124
|
+
}>>;
|
|
125
|
+
/**
|
|
126
|
+
* Report `roleDefaults` issues at each session start, and return a function that reports them after a lazy
|
|
127
|
+
* Role catalog load, such as `loadRoleCatalog` or `resolveRoleLaunch`. Every pi-config-store reporter shares
|
|
128
|
+
* one record, so each issue shows one time in each session, also with the pi-subagent extension active.
|
|
129
|
+
*/
|
|
130
|
+
export declare function registerRoleDefaultsReporting(pi: Pick<ExtensionAPI, "on">, agentDir?: string): (ctx: Pick<ExtensionContext, "hasUI" | "ui">) => void;
|
|
97
131
|
declare const BUILTIN_ROLE_NAMES: readonly ["implementer", "reviewer", "scout"];
|
|
98
132
|
export type BuiltinRoleName = (typeof BUILTIN_ROLE_NAMES)[number];
|
|
99
133
|
export declare function loadBuiltinRole(name: BuiltinRoleName): Role;
|
|
@@ -101,7 +135,8 @@ export declare function loadBuiltinRole(name: BuiltinRoleName): Role;
|
|
|
101
135
|
* Package-shipped Roles plus user Roles from `config/pi-subagent`, with
|
|
102
136
|
* `roleDefaults` merged in. A user Role overrides a same-named built-in. Each
|
|
103
137
|
* invalid or duplicate user file is reported and makes its claimed name
|
|
104
|
-
* unavailable without hiding valid Roles.
|
|
138
|
+
* unavailable without hiding valid Roles. Invalid `roleDefaults` keys are
|
|
139
|
+
* dropped; a `config.json` that cannot load (for example, malformed JSON) throws.
|
|
105
140
|
*/
|
|
106
141
|
export declare function loadRoleCatalog(agentDir?: string): RoleCatalog;
|
|
107
142
|
/** Select one catalog Role, naming invalid files that claim it. */
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,9 @@ import { readFileSync, readdirSync } from "node:fs";
|
|
|
2
2
|
import { basename, isAbsolute, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { DefaultPackageManager, getAgentDir, parseFrontmatter, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { extensionConfigDir,
|
|
5
|
+
import { createConfigStore, extensionConfigDir, field, formatConfigIssues, parsers, registerConfigReporting } from "@henryqw/pi-config-store";
|
|
6
6
|
import { hasDisplayControlCharacters } from "./display-text.js";
|
|
7
|
+
import { DEFAULT_MAX_TURNS } from "./ephemeral.js";
|
|
7
8
|
import { loadRoleMcpConfig, parseRoleMcpAllowlist } from "./mcp-role.js";
|
|
8
9
|
import { loadTaskModelsConfig, modelReference, orderedProfileRoutes, PROFILE_NAMES, resolveConfiguredTaskRoute, resolveTaskModelRoute, } from "@henryqw/pi-task-models";
|
|
9
10
|
export { DISPLAY_TEXT_CONTRACT, hasDisplayControlCharacters } from "./display-text.js";
|
|
@@ -172,27 +173,97 @@ export function parseRoleDefaults(value, label = "roleDefaults") {
|
|
|
172
173
|
...(record.skills === undefined ? {} : { skills: stringList(record.skills, "skills", label) }),
|
|
173
174
|
};
|
|
174
175
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
throw new Error(`${path} is not valid JSON (${error instanceof Error ? error.message : String(error)}).`);
|
|
176
|
+
/** Per-key `roleDefaults`: an unknown key or invalid list is reported and dropped, so fewer resources load. */
|
|
177
|
+
const roleDefaultsField = (raw, report) => {
|
|
178
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
179
|
+
throw new Error("must be a JSON object");
|
|
180
|
+
const defaults = {};
|
|
181
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
182
|
+
if (!ROLE_DEFAULT_KEYS.includes(key)) {
|
|
183
|
+
report([key], "unknown key", "unknown-key");
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
Object.assign(defaults, parseRoleDefaults({ [key]: value }));
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
report([key], error instanceof Error ? error.message : String(error));
|
|
191
|
+
}
|
|
192
192
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
return defaults;
|
|
194
|
+
};
|
|
195
|
+
export const DEFAULT_SUBAGENT_CONFIG = Object.freeze({
|
|
196
|
+
maxSubagents: 5,
|
|
197
|
+
maxTurns: DEFAULT_MAX_TURNS,
|
|
198
|
+
maxTokens: undefined,
|
|
199
|
+
maxCorrections: 1,
|
|
200
|
+
timeout: Object.freeze({ idleMinutes: 10, maxMinutes: 30 }),
|
|
201
|
+
roleDefaults: Object.freeze({}),
|
|
202
|
+
});
|
|
203
|
+
const timeoutMinutes = parsers.number({ greaterThan: 0, max: 2_147_483_647 / 60_000 });
|
|
204
|
+
const timeoutField = (raw, report) => {
|
|
205
|
+
const timeout = parsers.object({
|
|
206
|
+
idleMinutes: field(timeoutMinutes, DEFAULT_SUBAGENT_CONFIG.timeout.idleMinutes),
|
|
207
|
+
maxMinutes: field(timeoutMinutes, DEFAULT_SUBAGENT_CONFIG.timeout.maxMinutes),
|
|
208
|
+
})(raw, report);
|
|
209
|
+
if (timeout.maxMinutes <= timeout.idleMinutes) {
|
|
210
|
+
throw new Error(`maxMinutes (${timeout.maxMinutes}) must be greater than idleMinutes (${timeout.idleMinutes})`);
|
|
211
|
+
}
|
|
212
|
+
return timeout;
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* The user `config.json` store shared by the extension and `loadRoleCatalog`.
|
|
216
|
+
* Invalid bounded limits and `roleDefaults` keys fall back to defaults; an
|
|
217
|
+
* invalid `maxTokens` disables the config because its default is unlimited.
|
|
218
|
+
*/
|
|
219
|
+
export function createSubagentConfigStore(agentDir = getAgentDir()) {
|
|
220
|
+
return createConfigStore({
|
|
221
|
+
extensionId: "pi-subagent",
|
|
222
|
+
agentDir,
|
|
223
|
+
fields: {
|
|
224
|
+
maxSubagents: field(parsers.integer({ min: 1 }), DEFAULT_SUBAGENT_CONFIG.maxSubagents),
|
|
225
|
+
maxTurns: field(parsers.integer({ min: 1 }), DEFAULT_SUBAGENT_CONFIG.maxTurns),
|
|
226
|
+
maxTokens: field(parsers.integer({ min: 1 }), undefined, { onInvalid: "disable" }),
|
|
227
|
+
maxCorrections: field(parsers.integer({ min: 0 }), DEFAULT_SUBAGENT_CONFIG.maxCorrections),
|
|
228
|
+
timeout: field(timeoutField, DEFAULT_SUBAGENT_CONFIG.timeout),
|
|
229
|
+
roleDefaults: field(roleDefaultsField, DEFAULT_SUBAGENT_CONFIG.roleDefaults),
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Read only `roleDefaults`. The extension reports and enforces the execution limits, so a bad limit does not
|
|
235
|
+
* break the Role catalog of other consumers; other keys are not this reader's issues.
|
|
236
|
+
*/
|
|
237
|
+
function loadRoleDefaults(agentDir) {
|
|
238
|
+
const store = createConfigStore({
|
|
239
|
+
extensionId: "pi-subagent",
|
|
240
|
+
agentDir,
|
|
241
|
+
fields: { roleDefaults: field(roleDefaultsField, DEFAULT_SUBAGENT_CONFIG.roleDefaults) },
|
|
242
|
+
});
|
|
243
|
+
const loaded = store.loadSync();
|
|
244
|
+
const issues = loaded.issues.filter(({ key }) => !key || /^roleDefaults(?![\w$])/.test(key));
|
|
245
|
+
// A file that cannot load has no trusted `roleDefaults`.
|
|
246
|
+
return issues.some(({ key }) => !key)
|
|
247
|
+
? { store, loaded: { status: "disabled", source: loaded.source, issues } }
|
|
248
|
+
: { store, loaded: { ...loaded, issues } };
|
|
249
|
+
}
|
|
250
|
+
function readRoleDefaults(agentDir) {
|
|
251
|
+
const { store, loaded } = loadRoleDefaults(agentDir);
|
|
252
|
+
if (loaded.status !== "ok")
|
|
253
|
+
throw new Error(`${store.path}: ${formatConfigIssues(loaded.issues)}`);
|
|
254
|
+
return loaded.value.roleDefaults;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Report `roleDefaults` issues at each session start, and return a function that reports them after a lazy
|
|
258
|
+
* Role catalog load, such as `loadRoleCatalog` or `resolveRoleLaunch`. Every pi-config-store reporter shares
|
|
259
|
+
* one record, so each issue shows one time in each session, also with the pi-subagent extension active.
|
|
260
|
+
*/
|
|
261
|
+
export function registerRoleDefaultsReporting(pi, agentDir = getAgentDir()) {
|
|
262
|
+
const target = loadRoleDefaults(agentDir).store;
|
|
263
|
+
const reporter = registerConfigReporting(pi, []);
|
|
264
|
+
const report = (ctx) => reporter.report(ctx, target, loadRoleDefaults(agentDir).loaded);
|
|
265
|
+
pi.on("session_start", (_event, ctx) => report(ctx));
|
|
266
|
+
return report;
|
|
196
267
|
}
|
|
197
268
|
const withDefaults = (defaults, values) => [...new Set([...(defaults ?? []), ...values])];
|
|
198
269
|
function applyRoleDefaults(role, defaults) {
|
|
@@ -262,7 +333,8 @@ function claimedRoleName(file) {
|
|
|
262
333
|
* Package-shipped Roles plus user Roles from `config/pi-subagent`, with
|
|
263
334
|
* `roleDefaults` merged in. A user Role overrides a same-named built-in. Each
|
|
264
335
|
* invalid or duplicate user file is reported and makes its claimed name
|
|
265
|
-
* unavailable without hiding valid Roles.
|
|
336
|
+
* unavailable without hiding valid Roles. Invalid `roleDefaults` keys are
|
|
337
|
+
* dropped; a `config.json` that cannot load (for example, malformed JSON) throws.
|
|
266
338
|
*/
|
|
267
339
|
export function loadRoleCatalog(agentDir = getAgentDir()) {
|
|
268
340
|
const defaults = readRoleDefaults(agentDir);
|
|
@@ -326,13 +398,8 @@ export function findRole(catalog, name) {
|
|
|
326
398
|
throw new Error(`Unknown Subagent role: ${name}. Available roles: ${catalog.roles.map((candidate) => candidate.name).join(", ") || "none"}.${catalog.errors.length ? ` ${catalog.errors.length} invalid Role file(s); run /subagent roles.` : ""}`);
|
|
327
399
|
}
|
|
328
400
|
export function resolveTaskRoute(ctx, profileName, agentDir = getAgentDir()) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
config = loadTaskModelsConfig(agentDir).value;
|
|
332
|
-
}
|
|
333
|
-
catch {
|
|
334
|
-
throw new Error("Couldn't read task model config. Run /task-models.");
|
|
335
|
-
}
|
|
401
|
+
// A disabled config throws with the path and the problems.
|
|
402
|
+
const config = loadTaskModelsConfig(agentDir).value;
|
|
336
403
|
return resolveConfiguredRoute(ctx, profileName, config.profiles[profileName]);
|
|
337
404
|
}
|
|
338
405
|
function resolveConfiguredRoute(ctx, profileName, profile) {
|
package/extensions/config.ts
CHANGED
|
@@ -1,158 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createConfigStore, extensionConfigPath } from "@henryqw/pi-config-store";
|
|
1
|
+
import { formatConfigIssues, type ConfigLoad } from "@henryqw/pi-config-store";
|
|
3
2
|
import type { ExecutionPolicySnapshot } from "../dist/schema.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
export interface SubagentTimeoutConfig {
|
|
7
|
-
/** Minutes a child may stay idle before it is asked to stop. */
|
|
8
|
-
idleMinutes?: number;
|
|
9
|
-
/** Hard cap in minutes before a child is killed regardless of activity. */
|
|
10
|
-
maxMinutes?: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface SubagentConfig {
|
|
14
|
-
maxSubagents?: number;
|
|
15
|
-
maxTurns?: number;
|
|
16
|
-
maxTokens?: number;
|
|
17
|
-
maxCorrections?: number;
|
|
18
|
-
timeout?: SubagentTimeoutConfig;
|
|
19
|
-
/** Merged into every Role by `loadRoleCatalog`; validated here so malformed values block delegation. */
|
|
20
|
-
roleDefaults?: RoleDefaults;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface LoadedSubagentConfig {
|
|
24
|
-
source: "file" | "missing";
|
|
25
|
-
config: SubagentConfig;
|
|
26
|
-
/** Human-readable problems; malformed files are preserved and delegation fails closed. */
|
|
27
|
-
error?: string;
|
|
28
|
-
}
|
|
3
|
+
import type { SubagentConfig } from "@henryqw/pi-subagent";
|
|
29
4
|
|
|
30
5
|
export type EffectiveExecutionPolicy = ExecutionPolicySnapshot;
|
|
31
6
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
37
|
-
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
38
|
-
export const DEFAULT_TIMEOUT_CONFIG = { idleMinutes: 10, maxMinutes: 30 } as const;
|
|
39
|
-
export const DEFAULT_EXECUTION_POLICY = {
|
|
40
|
-
maxSubagents: 5,
|
|
41
|
-
maxTurns: 50,
|
|
42
|
-
maxCorrections: 1,
|
|
43
|
-
} as const;
|
|
44
|
-
const INTEGER_FIELDS = ["maxSubagents", "maxTurns", "maxTokens"] as const;
|
|
45
|
-
const POLICY_FIELDS = ["maxCorrections"] as const;
|
|
46
|
-
const TIMEOUT_FIELDS = ["idleMinutes", "maxMinutes"] as const;
|
|
47
|
-
|
|
48
|
-
export const configPath = (agentDir = getAgentDir()): string =>
|
|
49
|
-
extensionConfigPath(EXTENSION_ID, agentDir);
|
|
50
|
-
|
|
51
|
-
function parseSubagentConfig(parsed: unknown, path: string): ParsedSubagentConfig {
|
|
52
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
53
|
-
return { config: {}, error: `${path} must contain a JSON object; delegation is blocked until this file is corrected.` };
|
|
54
|
-
}
|
|
55
|
-
const record = parsed as Record<string, unknown>;
|
|
56
|
-
const problems: string[] = [];
|
|
57
|
-
const config: SubagentConfig = {};
|
|
58
|
-
for (const key of Object.keys(record)) {
|
|
59
|
-
if (![...INTEGER_FIELDS, ...POLICY_FIELDS, "timeout", "roleDefaults"].includes(key as never)) {
|
|
60
|
-
problems.push(`unknown config key ${JSON.stringify(key)}; expected ${[...INTEGER_FIELDS, ...POLICY_FIELDS].join(", ")}, timeout, roleDefaults`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (record.roleDefaults !== undefined) {
|
|
64
|
-
try { config.roleDefaults = parseRoleDefaults(record.roleDefaults); }
|
|
65
|
-
catch (error) { problems.push(error instanceof Error ? error.message : String(error)); }
|
|
7
|
+
/** Resolve one immutable policy. A disabled config blocks delegation. */
|
|
8
|
+
export function resolveExecutionPolicy(path: string, loaded: ConfigLoad<SubagentConfig>): EffectiveExecutionPolicy {
|
|
9
|
+
if (loaded.status === "disabled") {
|
|
10
|
+
throw new Error(`${path}: ${formatConfigIssues(loaded.issues)}; delegation is blocked until this file is corrected.`);
|
|
66
11
|
}
|
|
67
|
-
|
|
68
|
-
const value = record[key];
|
|
69
|
-
if (value === undefined) continue;
|
|
70
|
-
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 1) config[key] = value;
|
|
71
|
-
else problems.push(`${key} must be a safe integer >= 1, got ${JSON.stringify(value)}`);
|
|
72
|
-
}
|
|
73
|
-
if (record.maxCorrections !== undefined) {
|
|
74
|
-
if (!Number.isSafeInteger(record.maxCorrections) || (record.maxCorrections as number) < 0) {
|
|
75
|
-
problems.push(`maxCorrections must be a safe integer >= 0, got ${JSON.stringify(record.maxCorrections)}`);
|
|
76
|
-
} else config.maxCorrections = record.maxCorrections as number;
|
|
77
|
-
}
|
|
78
|
-
if (record.timeout !== undefined) {
|
|
79
|
-
if (!record.timeout || typeof record.timeout !== "object" || Array.isArray(record.timeout)) {
|
|
80
|
-
problems.push(`timeout must be a JSON object, got ${JSON.stringify(record.timeout)}`);
|
|
81
|
-
} else {
|
|
82
|
-
const timeoutRecord = record.timeout as Record<string, unknown>;
|
|
83
|
-
const timeout: SubagentTimeoutConfig = {};
|
|
84
|
-
for (const key of TIMEOUT_FIELDS) {
|
|
85
|
-
const value = timeoutRecord[key];
|
|
86
|
-
if (value === undefined) continue;
|
|
87
|
-
if (!positive(value)) problems.push(`timeout.${key} must be a positive number of minutes, got ${JSON.stringify(value)}`);
|
|
88
|
-
else if (value * 60_000 > MAX_TIMER_DELAY_MS) problems.push(`timeout.${key} exceeds ${MAX_TIMER_DELAY_MS} ms, got ${JSON.stringify(value)} minutes`);
|
|
89
|
-
else timeout[key] = value;
|
|
90
|
-
}
|
|
91
|
-
for (const key of Object.keys(timeoutRecord)) {
|
|
92
|
-
if (!TIMEOUT_FIELDS.includes(key as keyof SubagentTimeoutConfig)) problems.push(`unknown timeout.${key}; expected ${TIMEOUT_FIELDS.join(", ")}`);
|
|
93
|
-
}
|
|
94
|
-
if (Object.keys(timeout).length) config.timeout = timeout;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (config.timeout) {
|
|
98
|
-
const idleMinutes = config.timeout.idleMinutes ?? DEFAULT_TIMEOUT_CONFIG.idleMinutes;
|
|
99
|
-
const maxMinutes = config.timeout.maxMinutes ?? DEFAULT_TIMEOUT_CONFIG.maxMinutes;
|
|
100
|
-
if (maxMinutes <= idleMinutes) {
|
|
101
|
-
problems.push(`timeout.maxMinutes (${maxMinutes}) must be greater than timeout.idleMinutes (${idleMinutes})`);
|
|
102
|
-
delete config.timeout;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
config,
|
|
107
|
-
error: problems.length ? `${path}: ${problems.join("; ")}; delegation is blocked until this file is corrected.` : undefined,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Resolve one immutable policy. Malformed explicit safety settings fail closed. */
|
|
112
|
-
export function resolveExecutionPolicy(loaded: LoadedSubagentConfig): EffectiveExecutionPolicy {
|
|
113
|
-
if (loaded.error) throw new Error(loaded.error);
|
|
114
|
-
const timeout = loaded.config.timeout;
|
|
12
|
+
const { maxSubagents, maxTurns, maxTokens, maxCorrections, timeout } = loaded.value;
|
|
115
13
|
return Object.freeze({
|
|
116
|
-
maxSubagents
|
|
117
|
-
maxTurns
|
|
118
|
-
...(
|
|
119
|
-
childIdleMs:
|
|
120
|
-
childMaxMs:
|
|
121
|
-
maxCorrections
|
|
14
|
+
maxSubagents,
|
|
15
|
+
maxTurns,
|
|
16
|
+
...(maxTokens === undefined ? {} : { maxTokens }),
|
|
17
|
+
childIdleMs: timeout.idleMinutes * 60_000,
|
|
18
|
+
childMaxMs: timeout.maxMinutes * 60_000,
|
|
19
|
+
maxCorrections,
|
|
122
20
|
});
|
|
123
21
|
}
|
|
124
|
-
|
|
125
|
-
/** Read untrusted optional config without rewriting it. */
|
|
126
|
-
export function readSubagentConfig(agentDir = getAgentDir()): LoadedSubagentConfig {
|
|
127
|
-
const path = configPath(agentDir);
|
|
128
|
-
let validationError: string | undefined;
|
|
129
|
-
const store = createConfigStore<SubagentConfig>({
|
|
130
|
-
extensionId: EXTENSION_ID,
|
|
131
|
-
agentDir,
|
|
132
|
-
defaults: () => ({}),
|
|
133
|
-
parse(value) {
|
|
134
|
-
const parsed = parseSubagentConfig(value, path);
|
|
135
|
-
validationError = parsed.error;
|
|
136
|
-
return parsed.config;
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
try {
|
|
140
|
-
const loaded = store.loadSync();
|
|
141
|
-
return {
|
|
142
|
-
source: loaded.source,
|
|
143
|
-
config: loaded.value,
|
|
144
|
-
...(validationError === undefined ? {} : { error: validationError }),
|
|
145
|
-
};
|
|
146
|
-
} catch (error) {
|
|
147
|
-
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
148
|
-
if (code !== undefined && code !== "ERR_ENCODING_INVALID_ENCODED_DATA") throw error;
|
|
149
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
150
|
-
return {
|
|
151
|
-
source: "file",
|
|
152
|
-
config: {},
|
|
153
|
-
error: error instanceof SyntaxError
|
|
154
|
-
? `${path} is not valid JSON (${reason}); delegation is blocked until this file is corrected.`
|
|
155
|
-
: `${path} couldn't be read (${reason}); delegation is blocked until this file is corrected.`,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
}
|
package/extensions/isolated.ts
CHANGED
|
@@ -482,6 +482,8 @@ export interface RegisterIsolatedOptions {
|
|
|
482
482
|
executor: EphemeralSubagentExecutor;
|
|
483
483
|
policy: EffectiveExecutionPolicy;
|
|
484
484
|
currentPolicy(): EffectiveExecutionPolicy;
|
|
485
|
+
/** Report new config issues, and throw when the config now blocks delegation, before each deferred Role launch. */
|
|
486
|
+
beforeRoleLaunch?(ctx: ExtensionContext): void;
|
|
485
487
|
componentsFactory?: CreateIsolatedComponents;
|
|
486
488
|
}
|
|
487
489
|
|
|
@@ -504,7 +506,12 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
|
|
|
504
506
|
};
|
|
505
507
|
const getComponents = () => components ??= componentsFactory({
|
|
506
508
|
pi,
|
|
507
|
-
context
|
|
509
|
+
// The launch runtime reads this context only to resolve a Role launch.
|
|
510
|
+
context: () => {
|
|
511
|
+
const ctx = latestContext();
|
|
512
|
+
options.beforeRoleLaunch?.(ctx);
|
|
513
|
+
return ctx;
|
|
514
|
+
},
|
|
508
515
|
executor: options.executor,
|
|
509
516
|
policy: options.policy,
|
|
510
517
|
currentPolicy: options.currentPolicy,
|
package/extensions/subagent.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { type Component, Text, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
-
import { availableTaskModels, loadTaskModelsConfig, modelReference, type ProfileName, registerModelTask, resolveAvailableModel, resolveConfiguredTaskRoute, type ResolvedTaskRoute, taskThinkingLevels } from "@henryqw/pi-task-models";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { availableTaskModels, loadTaskModelsConfig, modelReference, type ProfileName, registerModelTask, registerTaskModelsConfigReporting, resolveAvailableModel, resolveConfiguredTaskRoute, type ResolvedTaskRoute, taskThinkingLevels } from "@henryqw/pi-task-models";
|
|
5
|
+
import { registerConfigReporting } from "@henryqw/pi-config-store";
|
|
6
|
+
import { capEphemeralSubagentOutput as capOutput, createEphemeralSubagentExecutor, createSubagentConfigStore, DEFAULT_SUBAGENT_CONFIG, DELEGATE_TASK, findRole, formatDuration, loadRoleCatalog, prepareRoleLaunch, resolveRolePackageResources, resolveTaskRoute, ROLE_TOOL_POLICY_FLAG, type Role } from "@henryqw/pi-subagent";
|
|
7
|
+
import { resolveExecutionPolicy, type EffectiveExecutionPolicy } from "./config.ts";
|
|
7
8
|
import { registerCheckoutAdmission } from "./admission.ts";
|
|
8
9
|
import { registerIsolatedExtension } from "./isolated.ts";
|
|
9
10
|
import { directRef, registerSubagentCommand, type DirectDisplay, type DirectTask } from "./subagent-command.ts";
|
|
@@ -180,20 +181,45 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
180
181
|
const widgetItems = new Map<string, WidgetItem>();
|
|
181
182
|
const tokenHandles = new Map<string, DirectHandle>();
|
|
182
183
|
const samplingTokens = new Set<string>();
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
184
|
+
const configStore = createSubagentConfigStore();
|
|
185
|
+
const configReporter = registerConfigReporting(pi, [configStore]);
|
|
186
|
+
const reportTaskModels = registerTaskModelsConfigReporting(pi);
|
|
187
|
+
/** Report new issues before a lazy Role catalog, route, or policy read. */
|
|
188
|
+
const reportConfig = (ctx: Pick<ExtensionContext, "hasUI" | "ui">): void => {
|
|
189
|
+
configReporter.report(ctx, configStore, configStore.loadSync());
|
|
190
|
+
reportTaskModels(ctx);
|
|
191
|
+
};
|
|
192
|
+
const loadedConfig = configStore.loadSync();
|
|
193
|
+
// A disabled config blocks each delegation; the executor still needs finite limits.
|
|
194
|
+
const initialPolicy = resolveExecutionPolicy(configStore.path, loadedConfig.status === "ok" ? loadedConfig
|
|
195
|
+
: { status: "ok", source: loadedConfig.source, value: DEFAULT_SUBAGENT_CONFIG, issues: [] });
|
|
190
196
|
const executor = createEphemeralSubagentExecutor({
|
|
191
197
|
maxConcurrency: initialPolicy.maxSubagents,
|
|
192
198
|
maxTurns: initialPolicy.maxTurns,
|
|
193
199
|
maxTokens: initialPolicy.maxTokens,
|
|
194
200
|
timeout: { idleMs: initialPolicy.childIdleMs, maxMs: initialPolicy.childMaxMs },
|
|
195
201
|
});
|
|
196
|
-
|
|
202
|
+
// The isolated runner reads the policy lazily without a context, so each read reports through the session's.
|
|
203
|
+
let latestCtx: ExtensionContext | undefined;
|
|
204
|
+
const currentPolicy = (): EffectiveExecutionPolicy => {
|
|
205
|
+
const loaded = configStore.loadSync();
|
|
206
|
+
if (latestCtx) configReporter.report(latestCtx, configStore, loaded);
|
|
207
|
+
const policy = resolveExecutionPolicy(configStore.path, loaded);
|
|
208
|
+
// The executor took its limits from the defaults, which can be unlimited, so a repaired file needs a reload.
|
|
209
|
+
if (loadedConfig.status === "disabled") {
|
|
210
|
+
throw new Error(`${configStore.path} was disabled when the session started; run /reload to apply its execution limits.`);
|
|
211
|
+
}
|
|
212
|
+
return policy;
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Before a deferred Role launch: report new issues, then revalidate so a now-disabled config blocks the child.
|
|
216
|
+
* The workflow keeps the limits that it admitted at entry (direct idle timeout and concurrency, or the durable
|
|
217
|
+
* isolated request's recorded policy); a limit change applies to the next workflow.
|
|
218
|
+
*/
|
|
219
|
+
const beforeRoleLaunch = (ctx: ExtensionContext): void => {
|
|
220
|
+
reportConfig(ctx);
|
|
221
|
+
currentPolicy();
|
|
222
|
+
};
|
|
197
223
|
const canWrite = (input: unknown): boolean => {
|
|
198
224
|
try {
|
|
199
225
|
const parsed = parseDelegateTask(input);
|
|
@@ -222,6 +248,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
222
248
|
executor,
|
|
223
249
|
policy: initialPolicy,
|
|
224
250
|
currentPolicy,
|
|
251
|
+
beforeRoleLaunch,
|
|
225
252
|
});
|
|
226
253
|
let directSequence = 0;
|
|
227
254
|
const directTasks = new Map<string, {
|
|
@@ -237,7 +264,6 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
237
264
|
return tab.entryId === saved.entryId && tab.name === saved.name && tab.tabId === saved.tabId
|
|
238
265
|
&& tab.paneId === saved.paneId && tab.sessionFile === saved.sessionFile;
|
|
239
266
|
});
|
|
240
|
-
let latestCtx: ExtensionContext | undefined;
|
|
241
267
|
let sessionEpoch = 0;
|
|
242
268
|
// Shutdown still saves outgoing identities; only a session start defers records to its carry loop.
|
|
243
269
|
let directRecordEpoch = 0;
|
|
@@ -311,6 +337,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
311
337
|
if (!current()) throw new Error("Session or branch changed before status inspection.");
|
|
312
338
|
const snapshot = notices.join("\n");
|
|
313
339
|
if (Buffer.byteLength(snapshot, "utf8") > 32_000) throw new Error("Status exceeds the 32 KiB inspection limit; use subagent_status for exact evidence.");
|
|
340
|
+
reportConfig(ctx);
|
|
314
341
|
const role = findRole(loadRoleCatalog(), "scout");
|
|
315
342
|
const prepared = await prepareDirectLaunch(role, ctx, { modelClass: "fast" });
|
|
316
343
|
const herdr = createDirectHerdr(pi, ctx.cwd, currentPolicy().childIdleMs);
|
|
@@ -354,6 +381,7 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
354
381
|
roles(ctx) {
|
|
355
382
|
const message = (error: unknown) => error instanceof Error ? error.message : String(error);
|
|
356
383
|
let catalog: ReturnType<typeof loadRoleCatalog>;
|
|
384
|
+
reportConfig(ctx);
|
|
357
385
|
try { catalog = loadRoleCatalog(); } catch (error) { return `Role catalog unavailable: ${message(error)}`; }
|
|
358
386
|
const list = (values: readonly string[] | undefined) => values?.length ? values.join(", ") : "none";
|
|
359
387
|
const lines = catalog.roles.flatMap((role) => {
|
|
@@ -507,13 +535,12 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
507
535
|
}
|
|
508
536
|
latestCtx = ctx;
|
|
509
537
|
ensureWidget(ctx);
|
|
510
|
-
if (loadedConfig.error !== undefined) ctx.ui.notify(loadedConfig.error, "warning");
|
|
511
538
|
try {
|
|
512
539
|
if (loadTaskModelsConfig().source === "missing") {
|
|
513
540
|
ctx.ui.notify("Task model config is missing; run /task-models to configure it.", "warning");
|
|
514
541
|
}
|
|
515
542
|
} catch {
|
|
516
|
-
//
|
|
543
|
+
// A disabled shared config throws; the task-models config reporter already shows the path and the problems.
|
|
517
544
|
}
|
|
518
545
|
});
|
|
519
546
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
@@ -620,6 +647,8 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
620
647
|
},
|
|
621
648
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
622
649
|
signal?.throwIfAborted();
|
|
650
|
+
// Isolated requests read the policy in the runner; report it here with the tool's context.
|
|
651
|
+
reportConfig(ctx);
|
|
623
652
|
const parsed = parseDelegateTask(params);
|
|
624
653
|
if (parsed.mode === "isolated") return await isolatedSurface.execute(parsed.request, signal, ctx);
|
|
625
654
|
const workflow = parsed.workflow;
|
|
@@ -719,6 +748,8 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
719
748
|
directTasks.set(taskId, local);
|
|
720
749
|
const launch = async (entry: WorkflowEntry, activeSignal: AbortSignal) => {
|
|
721
750
|
activeSignal.throwIfAborted();
|
|
751
|
+
// A later worker launches after earlier ones finish; report and revalidate config edits made meanwhile.
|
|
752
|
+
beforeRoleLaunch(latestCtx ?? ctx);
|
|
722
753
|
const role = findRole(loadRoleCatalog(), entry.delegation.role);
|
|
723
754
|
const { modelClass, model } = entry.delegation;
|
|
724
755
|
const prepared = await prepareDirectLaunch(role, latestCtx ?? ctx, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-subagent",
|
|
3
|
-
"version": "27.1
|
|
3
|
+
"version": "27.2.1",
|
|
4
4
|
"description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -66,11 +66,11 @@
|
|
|
66
66
|
"image": "https://raw.githubusercontent.com/HenryQW/pi-harness/main/extensions/pi-subagent/example.png"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@henryqw/pi-config-store": "^
|
|
69
|
+
"@henryqw/pi-config-store": "^4.0.0",
|
|
70
70
|
"@henryqw/pi-herdr": "^0.5.0",
|
|
71
71
|
"@henryqw/pi-multi-codex": "^3.0.0",
|
|
72
72
|
"@henryqw/pi-process": "^0.2.0",
|
|
73
|
-
"@henryqw/pi-task-models": "^
|
|
73
|
+
"@henryqw/pi-task-models": "^9.0.0",
|
|
74
74
|
"proper-lockfile": "^4.1.2"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|