pi-better-subagents 0.17.0 → 0.19.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/README.md +15 -2
- package/config.ts +33 -6
- package/delegation.ts +1 -1
- package/docs/usage.md +1 -0
- package/effective-settings.ts +63 -0
- package/index.ts +40 -13
- package/package.json +1 -1
- package/shared-execution-policy.ts +19 -0
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/delegation.ts
CHANGED
|
@@ -15,7 +15,7 @@ export function normalizeDelegationMode(value: unknown): DelegationMode {
|
|
|
15
15
|
export function delegationPrompt(mode: DelegationMode): string {
|
|
16
16
|
switch (mode) {
|
|
17
17
|
case "manual":
|
|
18
|
-
return "Delegation mode: manual. Do not proactively delegate work. Use subagents only when the user explicitly asks for delegation or an active workflow explicitly requires it. A structured plan or plan mode does not override this restriction
|
|
18
|
+
return "Delegation mode: manual. Do not proactively delegate work to subagents. Use subagents only when the user explicitly asks for delegation or an active workflow explicitly requires it. A structured plan or plan mode does not override this restriction. This restricts subagent delegation, not execution concurrency: independent foreground tools and background tasks still follow the Harness execution policy.";
|
|
19
19
|
case "coordinator":
|
|
20
20
|
return "Delegation mode: coordinator. The foreground coordinates, integrates, and verifies. Call agents_catalog to discover current available roles and inspect their descriptions. Pass a role to subagent_spawn by its short name, as listed (role: \"developer\"). Delegate every nontrivial task covered by an available role according to its current description; do not assume bundled roles are unchanged. Keep orchestration, cross-role decisions, unowned or ambiguous work, integration, and final verification in the foreground. Inspect results and failures before concluding; do not poll for running children.";
|
|
21
21
|
case "adaptive":
|
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,8 @@ 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";
|
|
58
|
+
import { appendExecutionPolicy } from "./shared-execution-policy.ts";
|
|
57
59
|
import { resolveExtensions, extensionArgs } from "./extensions.ts";
|
|
58
60
|
import { prepareTaskRuntime } from "./task-policy.ts";
|
|
59
61
|
import { canonicalizePath, takeRecoverySnapshot } from "./shared-sandbox-core.ts";
|
|
@@ -185,6 +187,7 @@ const SUBAGENT_TOOLS = [
|
|
|
185
187
|
"subagent_spawn",
|
|
186
188
|
"subagent_spawn_batch",
|
|
187
189
|
"subagent_list",
|
|
190
|
+
"subagent_settings",
|
|
188
191
|
"subagent_output",
|
|
189
192
|
"subagent_stop",
|
|
190
193
|
"subagent_result",
|
|
@@ -203,6 +206,7 @@ const CATALOG_GUIDELINES = [
|
|
|
203
206
|
];
|
|
204
207
|
|
|
205
208
|
const SUBAGENT_ORCHESTRATION_GUIDELINES = [
|
|
209
|
+
"Follow the Harness execution policy for concurrency; the active delegation mode determines whether to use subagents, not whether independent background commands can overlap.",
|
|
206
210
|
"When a structured plan is active, follow the current delegation mode. Track delegated and foreground deliverables only when delegation is permitted and actually underway; continue unblocked foreground work and integrate results before marking work complete.",
|
|
207
211
|
"Do not treat launching a subagent as completion of the parent milestone; relevant terminal results must be inspected and integrated before verification or completion.",
|
|
208
212
|
];
|
|
@@ -1520,6 +1524,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1520
1524
|
pi.appendEntry("pi-better-subagents-delegation", { version: 1, mode: null });
|
|
1521
1525
|
};
|
|
1522
1526
|
const activeDelegationMode = (): DelegationMode => delegationOverride ?? normalizeDelegationMode(loadConfig().delegationMode);
|
|
1527
|
+
const countRunning = () =>
|
|
1528
|
+
listActiveMetasForParent(process.pid).filter((meta) => effectiveStatus(meta) === "running").length;
|
|
1529
|
+
const effectiveSettings = () => effectiveSubagentSettings({
|
|
1530
|
+
delegationMode: activeDelegationMode(), maxConcurrent: activeConcurrencyCap(),
|
|
1531
|
+
sources: { delegationMode: delegationOverride ? "session" : "config",
|
|
1532
|
+
maxConcurrent: capOverride !== undefined ? "session" : "config" },
|
|
1533
|
+
}, countRunning(), getSharedCapacityGate(countRunning).pending);
|
|
1523
1534
|
const saveSettings = (): { ok: boolean; message: string } => {
|
|
1524
1535
|
try {
|
|
1525
1536
|
writeSubagentSettings({ delegationMode: activeDelegationMode(), maxConcurrent: activeConcurrencyCap() });
|
|
@@ -1844,6 +1855,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1844
1855
|
|
|
1845
1856
|
let spawned: ReturnType<typeof spawnDetached>;
|
|
1846
1857
|
try {
|
|
1858
|
+
// Reservations prevent competing admissions, but do not grandfather
|
|
1859
|
+
// unstarted work after a cap reduction. No await separates this live
|
|
1860
|
+
// occupancy check, child creation, and the running metadata below.
|
|
1861
|
+
const maxConcurrent = activeConcurrencyCap();
|
|
1862
|
+
if (countRunning() >= maxConcurrent) {
|
|
1863
|
+
throw new Error(`Max concurrent subagents (${maxConcurrent}) reached. Stop or let some finish first.`);
|
|
1864
|
+
}
|
|
1847
1865
|
spawned = spawnDetached({ file: cmd.file, fileArgs: cmd.fileArgs, cwd, logPath: logPathFor(id),
|
|
1848
1866
|
env: { [STEER_FILE_ENV]: steerPathFor(id) } });
|
|
1849
1867
|
} catch (error) {
|
|
@@ -1954,8 +1972,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1954
1972
|
if (blankSelector) throw new Error(blankSelector);
|
|
1955
1973
|
|
|
1956
1974
|
const maxConcurrent = activeConcurrencyCap();
|
|
1957
|
-
const countRunning = () =>
|
|
1958
|
-
listActiveMetasForParent(process.pid).filter((m) => effectiveStatus(m) === "running").length;
|
|
1959
1975
|
// Shared with batch-spawn: reserve before any async work so an interleaved
|
|
1960
1976
|
// batch cannot oversubscribe after this check and before writeMeta.
|
|
1961
1977
|
const gate = getSharedCapacityGate(countRunning);
|
|
@@ -2091,8 +2107,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
2091
2107
|
};
|
|
2092
2108
|
|
|
2093
2109
|
const cfg = loadConfig();
|
|
2094
|
-
const countRunning = () =>
|
|
2095
|
-
listActiveMetasForParent(process.pid).filter((m) => effectiveStatus(m) === "running").length;
|
|
2096
2110
|
const launchAvailable = p.onCapacity === "launch-available";
|
|
2097
2111
|
// Shared with single-spawn. Reservations count against maxConcurrent so a
|
|
2098
2112
|
// concurrent single spawn cannot take a slot the batch already admitted.
|
|
@@ -2246,6 +2260,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2246
2260
|
getActiveOrigin: (ctx?: unknown) => activeCallbackOrigin ?? verifiedOriginFromContext(ctx as ExtensionContext | undefined),
|
|
2247
2261
|
};
|
|
2248
2262
|
pi.registerTool(subagentListTool(Type, toolSession));
|
|
2263
|
+
pi.registerTool(subagentSettingsTool(Type, effectiveSettings));
|
|
2249
2264
|
pi.registerTool(subagentOutputTool(Type, toolSession));
|
|
2250
2265
|
const acceptanceResultTool = subagentResultTool(Type, toolSession);
|
|
2251
2266
|
pi.registerTool(acceptanceResultTool);
|
|
@@ -2286,13 +2301,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
2286
2301
|
const openSettings = async (ctx: ExtensionCommandContext): Promise<void> => {
|
|
2287
2302
|
if (ctx.mode === "tui" && ctx.hasUI && typeof ctx.ui.custom === "function") {
|
|
2288
2303
|
await ctx.ui.custom<void>((tui, theme, _kb, done) => createSubagentSettingsPage(theme, {
|
|
2289
|
-
get: () =>
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2304
|
+
get: () => {
|
|
2305
|
+
const settings = effectiveSettings();
|
|
2306
|
+
return {
|
|
2307
|
+
mode: settings.delegationMode, maxConcurrent: settings.maxConcurrent,
|
|
2308
|
+
modeSource: settings.sources.delegationMode,
|
|
2309
|
+
capSource: settings.sources.maxConcurrent,
|
|
2310
|
+
defaultMode: normalizeDelegationMode(loadConfig().delegationMode),
|
|
2311
|
+
defaultCap: normalizeConcurrencyCap(loadConfig().maxConcurrent),
|
|
2312
|
+
};
|
|
2313
|
+
},
|
|
2296
2314
|
changeMode, changeCap, save: saveSettings, reset: resetSettings,
|
|
2297
2315
|
}, () => { refreshBackgroundWorkNavigator(ctx); tui.requestRender(); }, () => done()));
|
|
2298
2316
|
} else {
|
|
@@ -2352,9 +2370,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
2352
2370
|
pi.registerTool(discoveryTool as Parameters<ExtensionAPI["registerTool"]>[0]);
|
|
2353
2371
|
|
|
2354
2372
|
pi.on("before_agent_start", (event) => ({
|
|
2355
|
-
systemPrompt: `${event.systemPrompt}\n\
|
|
2373
|
+
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.`,
|
|
2356
2374
|
}));
|
|
2357
2375
|
|
|
2376
|
+
pi.on("context", (event) => {
|
|
2377
|
+
const settings = effectiveSettings();
|
|
2378
|
+
return { messages: [
|
|
2379
|
+
...event.messages.filter(message => message.role !== "custom" || message.customType !== SUBAGENT_SETTINGS_CONTEXT),
|
|
2380
|
+
{ role: "custom" as const, customType: SUBAGENT_SETTINGS_CONTEXT,
|
|
2381
|
+
content: formatSubagentSettings(settings), display: false, details: settings, timestamp: Date.now() },
|
|
2382
|
+
] };
|
|
2383
|
+
});
|
|
2384
|
+
|
|
2358
2385
|
// ---- live-status lifecycle -----------------------------------------
|
|
2359
2386
|
pi.on("agent_start", async (_event, ctx) => {
|
|
2360
2387
|
setCallbackBatchContext(pi, ctx);
|
package/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Generated from packages/pi-better-harness/extensions/execution-policy.ts. Do not edit directly.
|
|
2
|
+
/** Harness-wide scheduling guidance, vendored so standalone extensions share it. */
|
|
3
|
+
const EXECUTION_POLICY = [
|
|
4
|
+
"Harness execution policy: concurrency is separate from subagent delegation.",
|
|
5
|
+
"Before multi-unit work, identify ready, authorized, independent units, actual prerequisites, shared mutable resources, and capacity or availability constraints. Represent independently verifiable deliverables (such as each host migration) separately in the plan; do not hide them in one serial rollout step or create dependencies merely from list order.",
|
|
6
|
+
"Start all ready, authorized, non-conflicting units concurrently within known resource limits before waiting for any one to finish. Reassess ready work after launches and results; continue unblocked foreground work instead of polling running tasks. Do not equate one background launch with scheduling all ready work.",
|
|
7
|
+
"Serialize only for a concrete dependency, conflicting writes or ownership, resource or concurrency limit, documented rollout gate, or explicit availability requirement. Record the reason in the plan or explain it to the user. Do not invent a canary or host-by-host availability requirement.",
|
|
8
|
+
"If concurrency raises an unresolved material operational trade-off (for example, draining all hosts could leave no available runners), ask the user once before the affected operations. Continue independent preparation meanwhile. Existing authorization, safety checks, workflow prerequisites, and human-approved rollout constraints remain binding; urgency does not waive them.",
|
|
9
|
+
"Choose the execution mechanism after deciding what can run together: foreground tools for small, tightly coupled, or interactive work; subagents for bounded independent reasoning or implementation when the active delegation mode permits; background tasks for independent long-running commands or waits. Manual delegation forbids proactive subagents, not concurrent foreground tools or background commands. Use available batch or parallel tool interfaces where appropriate and inspect every launch receipt for failures or skipped work.",
|
|
10
|
+
"A unit waiting or failing does not block other ready independent units unless the evidence shows a shared blocker. Preserve each unit's guards and rollback boundary. Before completing verification, the plan, or the goal, inspect and integrate every relevant terminal result or failure; launch is not completion.",
|
|
11
|
+
].join("\n");
|
|
12
|
+
|
|
13
|
+
export function appendExecutionPolicy(systemPrompt: string): string {
|
|
14
|
+
// Inspect the prompt, not module-global state: vendored copies have separate
|
|
15
|
+
// module identities, and every fresh turn or reload needs the policy again.
|
|
16
|
+
return systemPrompt.includes(EXECUTION_POLICY)
|
|
17
|
+
? systemPrompt
|
|
18
|
+
: `${systemPrompt}\n\n${EXECUTION_POLICY}`;
|
|
19
|
+
}
|