pi-better-subagents 0.18.0 → 0.19.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/README.md +15 -2
- package/config.ts +33 -6
- package/docs/usage.md +1 -0
- package/effective-settings.ts +63 -0
- package/index.ts +53 -15
- package/package.json +1 -1
- package/shared-navigator.ts +3 -2
package/README.md
CHANGED
|
@@ -42,8 +42,21 @@ this session and persist across reloads; Ctrl+S saves both as defaults, and
|
|
|
42
42
|
Reset to defaults clears
|
|
43
43
|
session overrides. `/subagents mode manual|adaptive|coordinator` and
|
|
44
44
|
`/subagents cap <number>` are command equivalents; `/subagents save` persists
|
|
45
|
-
both. Lowering the cap blocks new single and batch launches
|
|
46
|
-
|
|
45
|
+
both. Lowering the cap blocks new single and batch launches, including launches
|
|
46
|
+
still preparing, without stopping running work. Saved configuration changes are
|
|
47
|
+
picked up on subsequent reads; explicit session overrides still take precedence.
|
|
48
|
+
Before every model request, including continuations after tool calls,
|
|
49
|
+
the foreground receives one temporary settings block with the current mode, cap,
|
|
50
|
+
setting sources, revision, and running/reserved/available slots. The latest block
|
|
51
|
+
replaces older snapshots and delegation guidance; it is not saved as conversation
|
|
52
|
+
history. Changes reach the model at its next request, not during an in-flight
|
|
53
|
+
response, and are not broadcast to running children. The cap is a ceiling, not a
|
|
54
|
+
target number of agents. `subagent_settings` inspects the same effective state
|
|
55
|
+
without changing it. Its revision is a stable fingerprint of mode, cap, and
|
|
56
|
+
sources; occupancy changes do not change the revision. Admission rechecks capacity
|
|
57
|
+
when spawning because the snapshot can become stale.
|
|
58
|
+
|
|
59
|
+
The navigator's `main` row retains its mode-only controls:
|
|
47
60
|
`m` cycles the session mode, `ctrl+s` saves that default, and `x`
|
|
48
61
|
does not stop it. Manual delegates only on explicit user or workflow request, even with a
|
|
49
62
|
plan. Adaptive delegates substantial independent work when useful. Coordinator
|
package/config.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `defaultTools` absent → the built-in SAFE_DEFAULT_TOOLS.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
import { homedir } from "node:os";
|
|
@@ -89,19 +89,23 @@ export const SAFE_DEFAULT_TOOLS = "read, bash, edit, write, web_search, web_fetc
|
|
|
89
89
|
export const SAFE_CLEAN_TOOLS = "read, bash";
|
|
90
90
|
|
|
91
91
|
let cached: SubagentConfig | undefined;
|
|
92
|
+
let cachedIdentity: string | undefined;
|
|
93
|
+
let configOverrideForTests = false;
|
|
92
94
|
let configPathForTests: string | undefined;
|
|
93
95
|
let bundledConfigPathForTests: string | undefined;
|
|
94
96
|
|
|
95
97
|
/** Test seam. Pass undefined to reload config.json on the next loadConfig call. */
|
|
96
98
|
export function setConfigForTests(next: SubagentConfig | undefined): void {
|
|
97
99
|
cached = next;
|
|
100
|
+
cachedIdentity = undefined;
|
|
101
|
+
configOverrideForTests = next !== undefined;
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
/** Test seam. Isolate user config, optionally supplying a package fallback. */
|
|
101
105
|
export function setConfigPathForTests(path: string | undefined, bundledPath?: string): void {
|
|
102
106
|
configPathForTests = path;
|
|
103
107
|
bundledConfigPathForTests = bundledPath;
|
|
104
|
-
|
|
108
|
+
setConfigForTests(undefined);
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
export function configPath(): string {
|
|
@@ -128,8 +132,12 @@ function savedConfig(): SubagentConfig {
|
|
|
128
132
|
return user === undefined ? {} : parseConfig(user);
|
|
129
133
|
}
|
|
130
134
|
|
|
135
|
+
function bundledConfigPath(): string | undefined {
|
|
136
|
+
return bundledConfigPathForTests ?? (configPathForTests ? undefined : join(selfDir(), "config.json"));
|
|
137
|
+
}
|
|
138
|
+
|
|
131
139
|
function bundledConfig(): SubagentConfig {
|
|
132
|
-
const path =
|
|
140
|
+
const path = bundledConfigPath();
|
|
133
141
|
if (!path) return {};
|
|
134
142
|
try {
|
|
135
143
|
return readConfig(path);
|
|
@@ -138,12 +146,31 @@ function bundledConfig(): SubagentConfig {
|
|
|
138
146
|
}
|
|
139
147
|
}
|
|
140
148
|
|
|
149
|
+
function configIdentity(): string {
|
|
150
|
+
const paths = [configPath(), bundledConfigPath(),
|
|
151
|
+
configPathForTests ? undefined : join(piAgentDir(), "extensions", "pi-better-subagents-config.json")];
|
|
152
|
+
return JSON.stringify(paths.map(path => {
|
|
153
|
+
if (!path) return null;
|
|
154
|
+
try {
|
|
155
|
+
const stat = statSync(path, { bigint: true });
|
|
156
|
+
return [path, stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].map(String);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return [path, (error as NodeJS.ErrnoException).code];
|
|
159
|
+
}
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
|
|
141
163
|
/** Saved user choices take precedence; missing/invalid files contribute no keys. */
|
|
142
164
|
export function loadConfig(): SubagentConfig {
|
|
143
|
-
if (
|
|
165
|
+
if (configOverrideForTests) return cached!;
|
|
166
|
+
const identity = configIdentity();
|
|
167
|
+
if (cached && cachedIdentity === identity) return cached;
|
|
144
168
|
let user: SubagentConfig = {};
|
|
145
169
|
try { user = savedConfig(); } catch { /* retain package fallback */ }
|
|
146
170
|
cached = { ...bundledConfig(), ...user };
|
|
171
|
+
// Keep the pre-read identity: migration or a concurrent replacement must
|
|
172
|
+
// trigger another read rather than label an older value with a newer stat.
|
|
173
|
+
cachedIdentity = identity;
|
|
147
174
|
return cached;
|
|
148
175
|
}
|
|
149
176
|
|
|
@@ -162,10 +189,10 @@ export function writeSubagentSettings(settings: { delegationMode: DelegationMode
|
|
|
162
189
|
|
|
163
190
|
function writeConfigSettings(settings: Partial<SubagentConfig>, path: string): void {
|
|
164
191
|
if (path === configPath()) savedConfig();
|
|
165
|
-
|
|
192
|
+
updateHarnessSetting("subagents", previous => ({
|
|
166
193
|
...(previous === undefined ? {} : parseConfig(previous)), ...settings,
|
|
167
194
|
}), { agentDir: () => dirname(path) });
|
|
168
|
-
if (path === configPath())
|
|
195
|
+
if (path === configPath()) setConfigForTests(undefined);
|
|
169
196
|
}
|
|
170
197
|
|
|
171
198
|
/** Normalize a comma/space tool list to pi's bare comma form: "a, b" → "a,b". */
|
package/docs/usage.md
CHANGED
|
@@ -97,6 +97,7 @@ thresholds continue to take precedence for subagent health.
|
|
|
97
97
|
|------|---------|--------------|
|
|
98
98
|
| `subagent_spawn` | never | Launch a task in a background subagent; returns a run id at once. Params: `prompt`, `name`, `model`, `tools` (allowlist), `exclude_tools`, `sandbox`, `sandbox_dir`, `callback`, `clean`, `cwd`, `git_clone_workspace`, `approve`, `allow_nested`, and the [timing](#run-timing-deadline-ceiling-stuck) overrides `deadline_minutes`, `grace_minutes`, `max_minutes`, `stuck_minutes`. |
|
|
99
99
|
| `subagent_spawn_batch` | never | Launch several independent subagents at once. Each job becomes a normal run. Params: `batchName`, `shared` (options applied to every job), `jobs[]` (each needs `prompt`; same optional params as `subagent_spawn`), `onCapacity` (`reject` or `launch-available`). |
|
|
100
|
+
| `subagent_settings` | never | Read-only effective settings and capacity snapshot. No parameters. Returns `delegationMode`, `maxConcurrent`, per-setting `sources` (`session` or `config`), a stable settings `revision`, `runningCount`, `pendingCount`, `occupiedSlots`, and `availableSlots`. The limit is a ceiling, not a target; spawn admission rechecks live capacity. |
|
|
100
101
|
| `subagent_list` | never | Compact current-session list (default 10 rows / 1 KiB page). Params: `all` (machine-global / foreign session), `limit` (default 10, max 100), `cursor`, `max_bytes` (max 4 KiB), `status` (`running`, `completed`, `failed`, `killed`, `exited`, durable `orphaned`, `lost`). Incident counts are compact; no spend/tool histories. |
|
|
101
102
|
| `subagent_output` | never | Bounded current-session excerpt (default 1 KiB / 10 lines). Params: `id`, `lines` (`tail_lines` is a deprecated alias), `cursor` (a returned `nextCursor`, `statusCursor`, or `incidentCursor`), `max_bytes` (max 4 KiB; raw 16 KiB default, 64 KiB max), `mode` (`raw` pages retained log bytes), `include` (`["cost"]`, `["tools"]`, or both: opt-in spend and tool-count lines), `all` (foreign-session id). Missing/unreadable logs are gaps, not empty healthy output. |
|
|
102
103
|
| `subagent_result` | never | Finished-run answer for the current session (2048-byte page). Params: `id`, `cursor` (a returned `nextCursor` continues the answer; `statusCursor` / `incidentCursor` also accepted), `max_bytes` (maximum 8192 bytes), `lines` (optional line cap per answer page), `mode` (`raw`), `include` (`["cost"]`, `["tools"]`, or both), `all` (foreign-session id). Failures and exceptional lifecycle facts come before progress; no tool-name histories. TUI folding is display-only. |
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { delegationPrompt, type DelegationMode } from "./delegation.ts";
|
|
3
|
+
|
|
4
|
+
export const SUBAGENT_SETTINGS_CONTEXT = "pi-better-subagents-settings";
|
|
5
|
+
|
|
6
|
+
export interface EffectiveSubagentSettings {
|
|
7
|
+
revision: string;
|
|
8
|
+
delegationMode: DelegationMode;
|
|
9
|
+
maxConcurrent: number;
|
|
10
|
+
sources: { delegationMode: "session" | "config"; maxConcurrent: "session" | "config" };
|
|
11
|
+
runningCount: number;
|
|
12
|
+
pendingCount: number;
|
|
13
|
+
occupiedSlots: number;
|
|
14
|
+
availableSlots: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function effectiveSubagentSettings(
|
|
18
|
+
settings: Pick<EffectiveSubagentSettings, "delegationMode" | "maxConcurrent" | "sources">,
|
|
19
|
+
runningCount: number,
|
|
20
|
+
pendingCount: number,
|
|
21
|
+
): EffectiveSubagentSettings {
|
|
22
|
+
const revision = createHash("sha256").update(JSON.stringify([
|
|
23
|
+
settings.delegationMode, settings.maxConcurrent, settings.sources.delegationMode, settings.sources.maxConcurrent,
|
|
24
|
+
])).digest("hex").slice(0, 12);
|
|
25
|
+
const occupiedSlots = runningCount + pendingCount;
|
|
26
|
+
return { ...settings, revision, runningCount, pendingCount, occupiedSlots,
|
|
27
|
+
availableSlots: Math.max(0, settings.maxConcurrent - occupiedSlots) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function formatSubagentSettings(settings: EffectiveSubagentSettings): string {
|
|
31
|
+
return [
|
|
32
|
+
`Subagent settings: revision ${settings.revision}`,
|
|
33
|
+
`Delegation: ${settings.delegationMode} (${settings.sources.delegationMode})`,
|
|
34
|
+
`Concurrency limit: ${settings.maxConcurrent} (${settings.sources.maxConcurrent})`,
|
|
35
|
+
`Running: ${settings.runningCount}; reserved: ${settings.pendingCount}`,
|
|
36
|
+
`Occupied slots: ${settings.occupiedSlots}`,
|
|
37
|
+
`Available slots: ${settings.availableSlots}`,
|
|
38
|
+
"The limit is a ceiling, not a target. Launch ready, independent, useful work within available capacity. " +
|
|
39
|
+
"This is a point-in-time snapshot; spawn admission rechecks capacity. Lowering the limit does not stop existing runs.",
|
|
40
|
+
delegationPrompt(settings.delegationMode),
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function subagentSettingsTool(Type: any, read: () => EffectiveSubagentSettings) {
|
|
45
|
+
return {
|
|
46
|
+
name: "subagent_settings",
|
|
47
|
+
label: "Subagent Settings",
|
|
48
|
+
description: "Inspect effective subagent delegation mode, concurrency limit, setting sources, revision, and running/reserved/available slots. Read-only; does not change settings or launch work. The limit is a ceiling, not a target.",
|
|
49
|
+
parameters: Type.Object({}),
|
|
50
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
51
|
+
outputSchema: Type.Object({
|
|
52
|
+
revision: Type.String(), delegationMode: Type.String(), maxConcurrent: Type.Number({ minimum: 1, multipleOf: 1 }),
|
|
53
|
+
sources: Type.Object({ delegationMode: Type.String(), maxConcurrent: Type.String() }),
|
|
54
|
+
runningCount: Type.Number({ minimum: 0, multipleOf: 1 }), pendingCount: Type.Number({ minimum: 0, multipleOf: 1 }),
|
|
55
|
+
occupiedSlots: Type.Number({ minimum: 0, multipleOf: 1 }), availableSlots: Type.Number({ minimum: 0, multipleOf: 1 }),
|
|
56
|
+
}),
|
|
57
|
+
async execute() {
|
|
58
|
+
const settings = read();
|
|
59
|
+
return { content: [{ type: "text" as const, text: formatSubagentSettings(settings) }],
|
|
60
|
+
details: settings, structuredContent: settings };
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
package/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { parseRun, readRunTranscript, resetParseRunCursor, type Usage } from "./
|
|
|
37
37
|
import { finalizeRun as finalizeRunCore } from "./finalization.ts";
|
|
38
38
|
import { loadConfig, writeDelegationMode, writeSubagentSettings, isConcurrencyCap, normalizeConcurrencyCap, normalizeTools, resolveExtensionPath, selfDir, SAFE_DEFAULT_TOOLS, SAFE_CLEAN_TOOLS } from "./config.ts";
|
|
39
39
|
import { createSubagentSettingsPage } from "./settings-page.ts";
|
|
40
|
+
import { effectiveSubagentSettings, formatSubagentSettings, subagentSettingsTool, SUBAGENT_SETTINGS_CONTEXT } from "./effective-settings.ts";
|
|
40
41
|
import { STEER_FILE_ENV, readSteerReceipt } from "./child-steer.ts";
|
|
41
42
|
import {
|
|
42
43
|
decideTiming,
|
|
@@ -53,7 +54,7 @@ import {
|
|
|
53
54
|
type TimingStopReason,
|
|
54
55
|
} from "./timing.ts";
|
|
55
56
|
import { readAppendedLines, type LogCursor } from "./log-cursor.ts";
|
|
56
|
-
import { DELEGATION_MODES, DELEGATION_MODE_REQUEST,
|
|
57
|
+
import { DELEGATION_MODES, DELEGATION_MODE_REQUEST, isDelegationMode, normalizeDelegationMode, type DelegationMode } from "./delegation.ts";
|
|
57
58
|
import { appendExecutionPolicy } from "./shared-execution-policy.ts";
|
|
58
59
|
import { resolveExtensions, extensionArgs } from "./extensions.ts";
|
|
59
60
|
import { prepareTaskRuntime } from "./task-policy.ts";
|
|
@@ -186,6 +187,7 @@ const SUBAGENT_TOOLS = [
|
|
|
186
187
|
"subagent_spawn",
|
|
187
188
|
"subagent_spawn_batch",
|
|
188
189
|
"subagent_list",
|
|
190
|
+
"subagent_settings",
|
|
189
191
|
"subagent_output",
|
|
190
192
|
"subagent_stop",
|
|
191
193
|
"subagent_result",
|
|
@@ -570,7 +572,15 @@ function syncWidgetNavSelection(running: RunMeta[]): void {
|
|
|
570
572
|
*/
|
|
571
573
|
function renderWidget(): void {
|
|
572
574
|
const ctx = uiCtx;
|
|
573
|
-
if (!ctx
|
|
575
|
+
if (!ctx) return;
|
|
576
|
+
// A run can exit after Pi replaced the session that captured this ctx; Pi then throws on any
|
|
577
|
+
// property read. That session's UI is gone, so drop the ctx instead of failing the finalizer.
|
|
578
|
+
try {
|
|
579
|
+
if (!ctx.hasUI) return;
|
|
580
|
+
} catch {
|
|
581
|
+
if (uiCtx === ctx) uiCtx = undefined;
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
574
584
|
updateNavigatorFooter(ctx);
|
|
575
585
|
try { ctx.ui.setWidget("subagents", WIDGET_CLEAR); } catch { /* ignore */ }
|
|
576
586
|
spendCache.clear();
|
|
@@ -1522,6 +1532,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1522
1532
|
pi.appendEntry("pi-better-subagents-delegation", { version: 1, mode: null });
|
|
1523
1533
|
};
|
|
1524
1534
|
const activeDelegationMode = (): DelegationMode => delegationOverride ?? normalizeDelegationMode(loadConfig().delegationMode);
|
|
1535
|
+
const countRunning = () =>
|
|
1536
|
+
listActiveMetasForParent(process.pid).filter((meta) => effectiveStatus(meta) === "running").length;
|
|
1537
|
+
const effectiveSettings = () => effectiveSubagentSettings({
|
|
1538
|
+
delegationMode: activeDelegationMode(), maxConcurrent: activeConcurrencyCap(),
|
|
1539
|
+
sources: { delegationMode: delegationOverride ? "session" : "config",
|
|
1540
|
+
maxConcurrent: capOverride !== undefined ? "session" : "config" },
|
|
1541
|
+
}, countRunning(), getSharedCapacityGate(countRunning).pending);
|
|
1525
1542
|
const saveSettings = (): { ok: boolean; message: string } => {
|
|
1526
1543
|
try {
|
|
1527
1544
|
writeSubagentSettings({ delegationMode: activeDelegationMode(), maxConcurrent: activeConcurrencyCap() });
|
|
@@ -1838,7 +1855,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1838
1855
|
const snapshot = taskRuntime ? takeRecoverySnapshot(taskRuntime.policy.permissions) : undefined;
|
|
1839
1856
|
if (snapshot?.started) {
|
|
1840
1857
|
void snapshot.done.then((outcome) => {
|
|
1841
|
-
if (
|
|
1858
|
+
if (outcome.ok) return;
|
|
1859
|
+
// The snapshot can finish after the session was replaced and ctx went stale.
|
|
1860
|
+
try { ctx.ui?.notify?.(`Subagent ${id}: recovery snapshot failed (run continues): ${outcome.detail}`, "warning"); } catch { /* ignore */ }
|
|
1842
1861
|
});
|
|
1843
1862
|
}
|
|
1844
1863
|
const cmd = taskRuntime ? { file: taskRuntime.file, fileArgs: [...taskRuntime.fileArgs, ...args] } : { file: piBin, fileArgs: args };
|
|
@@ -1846,6 +1865,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1846
1865
|
|
|
1847
1866
|
let spawned: ReturnType<typeof spawnDetached>;
|
|
1848
1867
|
try {
|
|
1868
|
+
// Reservations prevent competing admissions, but do not grandfather
|
|
1869
|
+
// unstarted work after a cap reduction. No await separates this live
|
|
1870
|
+
// occupancy check, child creation, and the running metadata below.
|
|
1871
|
+
const maxConcurrent = activeConcurrencyCap();
|
|
1872
|
+
if (countRunning() >= maxConcurrent) {
|
|
1873
|
+
throw new Error(`Max concurrent subagents (${maxConcurrent}) reached. Stop or let some finish first.`);
|
|
1874
|
+
}
|
|
1849
1875
|
spawned = spawnDetached({ file: cmd.file, fileArgs: cmd.fileArgs, cwd, logPath: logPathFor(id),
|
|
1850
1876
|
env: { [STEER_FILE_ENV]: steerPathFor(id) } });
|
|
1851
1877
|
} catch (error) {
|
|
@@ -1956,8 +1982,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1956
1982
|
if (blankSelector) throw new Error(blankSelector);
|
|
1957
1983
|
|
|
1958
1984
|
const maxConcurrent = activeConcurrencyCap();
|
|
1959
|
-
const countRunning = () =>
|
|
1960
|
-
listActiveMetasForParent(process.pid).filter((m) => effectiveStatus(m) === "running").length;
|
|
1961
1985
|
// Shared with batch-spawn: reserve before any async work so an interleaved
|
|
1962
1986
|
// batch cannot oversubscribe after this check and before writeMeta.
|
|
1963
1987
|
const gate = getSharedCapacityGate(countRunning);
|
|
@@ -2093,8 +2117,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
2093
2117
|
};
|
|
2094
2118
|
|
|
2095
2119
|
const cfg = loadConfig();
|
|
2096
|
-
const countRunning = () =>
|
|
2097
|
-
listActiveMetasForParent(process.pid).filter((m) => effectiveStatus(m) === "running").length;
|
|
2098
2120
|
const launchAvailable = p.onCapacity === "launch-available";
|
|
2099
2121
|
// Shared with single-spawn. Reservations count against maxConcurrent so a
|
|
2100
2122
|
// concurrent single spawn cannot take a slot the batch already admitted.
|
|
@@ -2248,6 +2270,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2248
2270
|
getActiveOrigin: (ctx?: unknown) => activeCallbackOrigin ?? verifiedOriginFromContext(ctx as ExtensionContext | undefined),
|
|
2249
2271
|
};
|
|
2250
2272
|
pi.registerTool(subagentListTool(Type, toolSession));
|
|
2273
|
+
pi.registerTool(subagentSettingsTool(Type, effectiveSettings));
|
|
2251
2274
|
pi.registerTool(subagentOutputTool(Type, toolSession));
|
|
2252
2275
|
const acceptanceResultTool = subagentResultTool(Type, toolSession);
|
|
2253
2276
|
pi.registerTool(acceptanceResultTool);
|
|
@@ -2288,13 +2311,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
2288
2311
|
const openSettings = async (ctx: ExtensionCommandContext): Promise<void> => {
|
|
2289
2312
|
if (ctx.mode === "tui" && ctx.hasUI && typeof ctx.ui.custom === "function") {
|
|
2290
2313
|
await ctx.ui.custom<void>((tui, theme, _kb, done) => createSubagentSettingsPage(theme, {
|
|
2291
|
-
get: () =>
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2314
|
+
get: () => {
|
|
2315
|
+
const settings = effectiveSettings();
|
|
2316
|
+
return {
|
|
2317
|
+
mode: settings.delegationMode, maxConcurrent: settings.maxConcurrent,
|
|
2318
|
+
modeSource: settings.sources.delegationMode,
|
|
2319
|
+
capSource: settings.sources.maxConcurrent,
|
|
2320
|
+
defaultMode: normalizeDelegationMode(loadConfig().delegationMode),
|
|
2321
|
+
defaultCap: normalizeConcurrencyCap(loadConfig().maxConcurrent),
|
|
2322
|
+
};
|
|
2323
|
+
},
|
|
2298
2324
|
changeMode, changeCap, save: saveSettings, reset: resetSettings,
|
|
2299
2325
|
}, () => { refreshBackgroundWorkNavigator(ctx); tui.requestRender(); }, () => done()));
|
|
2300
2326
|
} else {
|
|
@@ -2354,9 +2380,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
2354
2380
|
pi.registerTool(discoveryTool as Parameters<ExtensionAPI["registerTool"]>[0]);
|
|
2355
2381
|
|
|
2356
2382
|
pi.on("before_agent_start", (event) => ({
|
|
2357
|
-
systemPrompt: `${appendExecutionPolicy(event.systemPrompt)}\n\
|
|
2383
|
+
systemPrompt: `${appendExecutionPolicy(event.systemPrompt)}\n\nUse the latest Subagent settings context block as the authoritative delegation mode and capacity snapshot. Its delegation guidance replaces earlier mode-specific guidance, including guidance in a plan. Inspect subagent_settings when needed; never treat the concurrency limit as a target number of agents.`,
|
|
2358
2384
|
}));
|
|
2359
2385
|
|
|
2386
|
+
pi.on("context", (event) => {
|
|
2387
|
+
const settings = effectiveSettings();
|
|
2388
|
+
return { messages: [
|
|
2389
|
+
...event.messages.filter(message => message.role !== "custom" || message.customType !== SUBAGENT_SETTINGS_CONTEXT),
|
|
2390
|
+
{ role: "custom" as const, customType: SUBAGENT_SETTINGS_CONTEXT,
|
|
2391
|
+
content: formatSubagentSettings(settings), display: false, details: settings, timestamp: Date.now() },
|
|
2392
|
+
] };
|
|
2393
|
+
});
|
|
2394
|
+
|
|
2360
2395
|
// ---- live-status lifecycle -----------------------------------------
|
|
2361
2396
|
pi.on("agent_start", async (_event, ctx) => {
|
|
2362
2397
|
setCallbackBatchContext(pi, ctx);
|
|
@@ -2472,6 +2507,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2472
2507
|
// running/orphaned children before dropping the session origin, so they
|
|
2473
2508
|
// cannot become live process groups with no coordinator.
|
|
2474
2509
|
stopCurrentSessionSubagents(ctx);
|
|
2510
|
+
// The stopped children exit after this handler returns; their finalizers
|
|
2511
|
+
// must not reach for a ctx that Pi invalidates once the session is replaced.
|
|
2512
|
+
uiCtx = undefined;
|
|
2475
2513
|
agentSessionSettings.clear();
|
|
2476
2514
|
catalogModelRegistry = undefined;
|
|
2477
2515
|
activeCallbackOrigin = undefined;
|
package/package.json
CHANGED
package/shared-navigator.ts
CHANGED
|
@@ -174,8 +174,9 @@ export function isNavigatorUiAvailable(ctx: ExtensionContext | undefined): boole
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
export function navigatorFooterHint(
|
|
178
|
-
|
|
177
|
+
export function navigatorFooterHint(_count: number): string | null {
|
|
178
|
+
// Retain the compatibility helper so refresh/reload clears any old badge.
|
|
179
|
+
return null;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
export function applyNavigatorFooter(ui: { setStatus(key: string, value: string | undefined): void }, count: number): string | null {
|