@henryqw/pi-subagent 27.1.1 → 27.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +1 -0
- package/README.md +11 -2
- package/dist/ephemeral.d.ts +2 -0
- package/dist/ephemeral.js +2 -1
- package/dist/index.d.ts +36 -1
- package/dist/index.js +96 -29
- package/dist/runner.d.ts +1 -1
- package/dist/runner.js +2 -5
- package/extensions/config.ts +13 -150
- package/extensions/isolated.ts +10 -5
- 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
|
@@ -220,11 +220,19 @@ pi-subagent owns `~/.pi/agent/config/pi-subagent/config.json`. If the file is mi
|
|
|
220
220
|
| `maxCorrections` | Same-worker automatic corrections per isolated request | Safe integer ≥ 0 | `1` |
|
|
221
221
|
| `timeout.idleMinutes` | Direct worker and ephemeral child idle timeout | Positive and within Node's timer range | `10` |
|
|
222
222
|
| `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
|
|
223
|
+
| `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
224
|
|
|
225
225
|
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
226
|
|
|
227
|
-
|
|
227
|
+
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:
|
|
228
|
+
|
|
229
|
+
- An unknown key shows a warning. The extension ignores the key.
|
|
230
|
+
- 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.
|
|
231
|
+
- 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`.
|
|
232
|
+
- An unknown or invalid key in `roleDefaults` shows a warning. The extension ignores only that key, so Roles get fewer default resources.
|
|
233
|
+
- 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.
|
|
234
|
+
|
|
235
|
+
No key is required. The extension preserves the file and never rewrites it automatically.
|
|
228
236
|
|
|
229
237
|
### Roles
|
|
230
238
|
|
|
@@ -274,6 +282,7 @@ The servers named in `mcps` come only from the global `~/.pi/agent/mcp.json`. In
|
|
|
274
282
|
| Surface | Type | Purpose |
|
|
275
283
|
| --- | --- | --- |
|
|
276
284
|
| `loadRoleCatalog`, `findRole`, `parseRoleName`, `parseRoleDefaults` | functions | Load built-in and user Roles with `roleDefaults` and per-file errors, then select one by name. |
|
|
285
|
+
| `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
286
|
| `resolveRoleSkills`, `resolveRoleLaunch`, `resolveConfiguredRoleLaunch` | functions | Resolve effective Pi resources and model routes. |
|
|
278
287
|
| `prepareRoleLaunch`, `finalizeRoleLaunch` | functions | Prepare Role prompts, arguments, and tool policy. |
|
|
279
288
|
| `createEphemeralSubagentExecutor` | function | Run bounded no-session Pi children through a FIFO pool. |
|
package/dist/ephemeral.d.ts
CHANGED
|
@@ -71,6 +71,8 @@ export interface EphemeralSubagentExecutor {
|
|
|
71
71
|
* It reuses the active Pi process invocation; it does not resolve a standalone Pi installation.
|
|
72
72
|
*/
|
|
73
73
|
export declare function createEphemeralSubagentExecutor(options: EphemeralSubagentExecutorOptions): EphemeralSubagentExecutor;
|
|
74
|
+
/** Longest prefix of `text` within `maxBytes` UTF-8 bytes, never splitting a code point. */
|
|
75
|
+
export declare function utf8Prefix(text: string, maxBytes: number): string;
|
|
74
76
|
export declare function capEphemeralSubagentOutput(text: string): string;
|
|
75
77
|
export declare function addUsage(left: Usage | undefined, right: Usage | undefined): Usage | undefined;
|
|
76
78
|
export declare function formatDuration(milliseconds: number): string;
|
package/dist/ephemeral.js
CHANGED
|
@@ -271,7 +271,8 @@ function oversizedToolStart(prefix) {
|
|
|
271
271
|
return;
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
|
-
|
|
274
|
+
/** Longest prefix of `text` within `maxBytes` UTF-8 bytes, never splitting a code point. */
|
|
275
|
+
export function utf8Prefix(text, maxBytes) {
|
|
275
276
|
return new StringDecoder().write(Buffer.from(text).subarray(0, maxBytes));
|
|
276
277
|
}
|
|
277
278
|
function cappedPrefix(text, totalBytes) {
|
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/dist/runner.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type EphemeralSubagentExecutor } from "./ephemeral.ts";
|
|
2
2
|
import type { PruneOwnership } from "./prune.ts";
|
|
3
3
|
import { IntegrationGit } from "./integration-git.ts";
|
|
4
4
|
import { type AllocationKind, type CheckCommand, type ChangesetTaskRequest, type CleanupKind, type ExecuteRequest, type ExecutionPolicySnapshot, type HostAllocationIntent, type HostAllocationPlan, type ModelClass, type ReviewEvidence, type ResumeRequest, type RunState, type TaskAttempt, type TaskRequest, type TaskState, type WorktreeAllocationIntent, type WorktreeAllocationPlan, type WorkspaceIdentity } from "./schema.ts";
|
package/dist/runner.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { utf8Prefix } from "./ephemeral.js";
|
|
2
3
|
import { createChildWorktree, finalizeChildWorktree, inspectWorktreeDirty } from "./worktree.js";
|
|
3
4
|
import { runGit } from "./git-process.js";
|
|
4
5
|
import { IntegrationGit } from "./integration-git.js";
|
|
@@ -162,11 +163,7 @@ class FollowupControl {
|
|
|
162
163
|
function bounded(value, maxBytes = MAX_PERSISTED_RUNTIME_TEXT_BYTES) {
|
|
163
164
|
if (Buffer.byteLength(value, "utf8") <= maxBytes)
|
|
164
165
|
return value;
|
|
165
|
-
|
|
166
|
-
let end = Math.min(value.length, contentBytes);
|
|
167
|
-
while (end > 0 && Buffer.byteLength(value.slice(0, end), "utf8") > contentBytes)
|
|
168
|
-
end -= 1;
|
|
169
|
-
return `${value.slice(0, end)}${TRUNCATION_MARKER}`;
|
|
166
|
+
return `${utf8Prefix(value, maxBytes - Buffer.byteLength(TRUNCATION_MARKER, "utf8"))}${TRUNCATION_MARKER}`;
|
|
170
167
|
}
|
|
171
168
|
function boundedRuntimeText(value, field) {
|
|
172
169
|
if (typeof value !== "string")
|
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
|
@@ -2,7 +2,7 @@ import { basename } from "node:path";
|
|
|
2
2
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
5
|
-
import type
|
|
5
|
+
import { utf8Prefix, type EphemeralSubagentExecutor } from "../dist/ephemeral.js";
|
|
6
6
|
import type { EffectiveExecutionPolicy } from "./config.ts";
|
|
7
7
|
import {
|
|
8
8
|
createCanonicalGitRootResolver,
|
|
@@ -196,9 +196,7 @@ function boundedReport(lines: readonly string[], omitted: string): string {
|
|
|
196
196
|
|
|
197
197
|
function boundedPublicText(value: string, maxBytes = PUBLIC_EVIDENCE_MAX_BYTES): string {
|
|
198
198
|
if (Buffer.byteLength(value, "utf8") <= maxBytes) return value;
|
|
199
|
-
|
|
200
|
-
while (end > 0 && Buffer.byteLength(value.slice(0, end), "utf8") > maxBytes) end -= 1;
|
|
201
|
-
return `${value.slice(0, end)}\n[truncated]`;
|
|
199
|
+
return `${utf8Prefix(value, maxBytes)}\n[truncated]`;
|
|
202
200
|
}
|
|
203
201
|
|
|
204
202
|
function publicFailedCheck(evidence: CheckBatchEvidence | undefined) {
|
|
@@ -484,6 +482,8 @@ export interface RegisterIsolatedOptions {
|
|
|
484
482
|
executor: EphemeralSubagentExecutor;
|
|
485
483
|
policy: EffectiveExecutionPolicy;
|
|
486
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;
|
|
487
487
|
componentsFactory?: CreateIsolatedComponents;
|
|
488
488
|
}
|
|
489
489
|
|
|
@@ -506,7 +506,12 @@ export function registerIsolatedExtension(pi: ExtensionAPI, options: RegisterIso
|
|
|
506
506
|
};
|
|
507
507
|
const getComponents = () => components ??= componentsFactory({
|
|
508
508
|
pi,
|
|
509
|
-
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
|
+
},
|
|
510
515
|
executor: options.executor,
|
|
511
516
|
policy: options.policy,
|
|
512
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.
|
|
3
|
+
"version": "27.2.0",
|
|
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": {
|