@signalridge/pi-subagents 1.11.0 → 1.12.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/CHANGELOG.md +40 -0
- package/README.md +76 -8
- package/package.json +8 -8
- package/src/agent-color.ts +1 -1
- package/src/agent-manager.ts +27 -2
- package/src/agent-runner.ts +142 -56
- package/src/agent-types.ts +6 -0
- package/src/custom-agents.ts +12 -4
- package/src/enabled-models.ts +12 -5
- package/src/extension-resource-names.ts +105 -0
- package/src/gate.ts +0 -0
- package/src/index.ts +177 -67
- package/src/model-scope.ts +4 -1
- package/src/nested-tools.ts +10 -4
- package/src/parent-questions.ts +453 -0
- package/src/project-trust.ts +146 -0
- package/src/question-origin.ts +24 -0
- package/src/settings.ts +7 -4
- package/src/skill-loader.ts +22 -10
- package/src/supervisor.ts +28 -8
- package/src/ui/agent-display.ts +2 -1
- package/src/ui/conversation-viewer.ts +49 -18
- package/src/ui/fleet-list.ts +125 -58
- package/src/ui/owner-group.ts +102 -0
- package/src/ui/schedule-menu.ts +14 -7
package/src/index.ts
CHANGED
|
@@ -165,6 +165,9 @@ import {
|
|
|
165
165
|
streamToOutputFile,
|
|
166
166
|
writeInitialEntry,
|
|
167
167
|
} from "./output-file.js";
|
|
168
|
+
import { registerParentQuestions } from "./parent-questions.js";
|
|
169
|
+
import { captureProjectTrust, isProjectResource } from "./project-trust.js";
|
|
170
|
+
import { captureQuestionOrigin } from "./question-origin.js";
|
|
168
171
|
import { SubagentScheduler } from "./schedule.js";
|
|
169
172
|
import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
|
|
170
173
|
import {
|
|
@@ -582,16 +585,18 @@ export default function (pi: ExtensionAPI): void {
|
|
|
582
585
|
if (inert) return;
|
|
583
586
|
if (!rootRuntime) {
|
|
584
587
|
try {
|
|
585
|
-
const
|
|
588
|
+
const trust = captureProjectTrust(ctx);
|
|
589
|
+
const startupSettings = loadSettings(trust.cwd, trust.trusted);
|
|
586
590
|
const startupAgents = loadCustomAgents(
|
|
587
|
-
|
|
591
|
+
trust.cwd,
|
|
588
592
|
startupSettings.strictAgentFiles === true,
|
|
593
|
+
trust.trusted,
|
|
589
594
|
);
|
|
590
595
|
rootRuntime = activateRootRuntime(
|
|
591
596
|
pi,
|
|
592
597
|
startupSettings,
|
|
593
598
|
startupAgents,
|
|
594
|
-
ctx
|
|
599
|
+
ctx,
|
|
595
600
|
unsubscribeRootContext,
|
|
596
601
|
);
|
|
597
602
|
} catch (error: unknown) {
|
|
@@ -611,10 +616,12 @@ function activateRootRuntime(
|
|
|
611
616
|
pi: ExtensionAPI,
|
|
612
617
|
startupSettings: SubagentsSettings,
|
|
613
618
|
startupAgents: Map<string, AgentConfig>,
|
|
614
|
-
|
|
619
|
+
initialContext: ExtensionContext,
|
|
615
620
|
unsubscribeRootContext: () => void,
|
|
616
621
|
): RootRuntime {
|
|
617
|
-
let
|
|
622
|
+
let configurationOwner = initialContext;
|
|
623
|
+
let activeTrust = captureProjectTrust(configurationOwner);
|
|
624
|
+
let sessionCwd = activeTrust.cwd;
|
|
618
625
|
let strictAgentFiles = startupSettings.strictAgentFiles === true;
|
|
619
626
|
|
|
620
627
|
// ---- Register custom notification renderer ----
|
|
@@ -668,7 +675,12 @@ function activateRootRuntime(
|
|
|
668
675
|
|
|
669
676
|
/** Reload agents from project/global custom agent dirs and merge with defaults (called on init and each Agent invocation). */
|
|
670
677
|
const reloadCustomAgents = (strict = false) => {
|
|
671
|
-
const
|
|
678
|
+
const trust = captureProjectTrust(configurationOwner);
|
|
679
|
+
const changed = trust.cwd !== activeTrust.cwd || trust.trusted !== activeTrust.trusted;
|
|
680
|
+
activeTrust = trust;
|
|
681
|
+
sessionCwd = trust.cwd;
|
|
682
|
+
if (changed) applyRuntimeSettings(loadSettings(trust.cwd, trust.trusted));
|
|
683
|
+
const userAgents = loadCustomAgents(trust.cwd, strict, trust.trusted);
|
|
672
684
|
registerAgents(userAgents);
|
|
673
685
|
};
|
|
674
686
|
|
|
@@ -1184,6 +1196,28 @@ function activateRootRuntime(
|
|
|
1184
1196
|
// same session. Invalidate pending off-screen mention turns only when the
|
|
1185
1197
|
// replacement commits: before-switch/tree handlers can still cancel it.
|
|
1186
1198
|
let sessionGeneration = 0;
|
|
1199
|
+
const parentQuestions = registerParentQuestions(pi, () => currentCtx);
|
|
1200
|
+
manager.setParentQuestionFactory((record, ctx) => {
|
|
1201
|
+
const generation = sessionGeneration;
|
|
1202
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
1203
|
+
const originCurrent = captureQuestionOrigin(ctx);
|
|
1204
|
+
return (question, choices, signal) => {
|
|
1205
|
+
// A retained channel can resume after ordinary native appends. Each
|
|
1206
|
+
// question also owns the LIVE complete prefix at its own invocation.
|
|
1207
|
+
const questionOriginCurrent = currentCtx ? captureQuestionOrigin(currentCtx) : () => false;
|
|
1208
|
+
const controller = record.abortController;
|
|
1209
|
+
const owner = JSON.stringify(record.owner);
|
|
1210
|
+
const isCurrent = () => currentCtx !== undefined &&
|
|
1211
|
+
sessionGeneration === generation && boundSessionId === sessionId &&
|
|
1212
|
+
originCurrent(currentCtx) && questionOriginCurrent(currentCtx) &&
|
|
1213
|
+
manager.getRecordMutable(record.id) === record && !record.detached &&
|
|
1214
|
+
record.isBackground === true && record.status === "running" &&
|
|
1215
|
+
record.abortController === controller && !controller?.signal.aborted &&
|
|
1216
|
+
JSON.stringify(record.owner) === owner;
|
|
1217
|
+
return parentQuestions.broker.request({ agentId: record.id, isCurrent }, question, choices,
|
|
1218
|
+
controller ? AbortSignal.any([controller.signal, ...(signal ? [signal] : [])]) : signal);
|
|
1219
|
+
};
|
|
1220
|
+
});
|
|
1187
1221
|
// Public synchronous bus preflight works in either extension load order. A
|
|
1188
1222
|
// filtered-out activation has no bound context and must not veto navigation.
|
|
1189
1223
|
pi.events.on("pi:navigation-preflight", (raw: unknown) => {
|
|
@@ -1254,8 +1288,17 @@ function activateRootRuntime(
|
|
|
1254
1288
|
manager.detachForBranchChange();
|
|
1255
1289
|
manager.resetManagedSpawns();
|
|
1256
1290
|
}
|
|
1291
|
+
parentQuestions.retire();
|
|
1257
1292
|
sessionGeneration++;
|
|
1258
|
-
|
|
1293
|
+
const replacingConfiguration = currentCtx !== undefined;
|
|
1294
|
+
configurationOwner = ctx;
|
|
1295
|
+
const trust = captureProjectTrust(ctx);
|
|
1296
|
+
sessionCwd = trust.cwd;
|
|
1297
|
+
if (replacingConfiguration) {
|
|
1298
|
+
activeTrust = trust;
|
|
1299
|
+
applyRuntimeSettings(loadSettings(trust.cwd, trust.trusted));
|
|
1300
|
+
}
|
|
1301
|
+
reloadCustomAgents();
|
|
1259
1302
|
boundSessionId = ctx.sessionManager?.getSessionId?.();
|
|
1260
1303
|
currentCtx = ctx;
|
|
1261
1304
|
await claimRpcRegistry(ctx, event.reason === "reload");
|
|
@@ -1370,6 +1413,7 @@ function activateRootRuntime(
|
|
|
1370
1413
|
const detachCommittedTree = (event: unknown): void => {
|
|
1371
1414
|
if (!currentCtx || detachedTreeEvent === event) return;
|
|
1372
1415
|
detachedTreeEvent = event;
|
|
1416
|
+
parentQuestions.retire();
|
|
1373
1417
|
manager.detachForBranchChange();
|
|
1374
1418
|
};
|
|
1375
1419
|
// If workflows' committed-tree handler runs first, it must detach our old
|
|
@@ -1386,6 +1430,8 @@ function activateRootRuntime(
|
|
|
1386
1430
|
// even when subagents' session_tree handler runs first.
|
|
1387
1431
|
pi.events.emit("subagents:session_tree_committed", { event });
|
|
1388
1432
|
currentCtx = ctx;
|
|
1433
|
+
configurationOwner = ctx;
|
|
1434
|
+
reloadCustomAgents();
|
|
1389
1435
|
scheduler.stop();
|
|
1390
1436
|
manager.clearCompleted(true);
|
|
1391
1437
|
manager.resetManagedSpawns();
|
|
@@ -1426,6 +1472,7 @@ function activateRootRuntime(
|
|
|
1426
1472
|
},
|
|
1427
1473
|
});
|
|
1428
1474
|
scheduler.stop();
|
|
1475
|
+
parentQuestions.retire();
|
|
1429
1476
|
currentCtx = undefined; // reject new spawns; owned cleanup RPC remains bound
|
|
1430
1477
|
if (pending.length > 0) {
|
|
1431
1478
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1860,8 +1907,8 @@ function activateRootRuntime(
|
|
|
1860
1907
|
|
|
1861
1908
|
// ---- Agent tool description mode ----
|
|
1862
1909
|
// "full" (default) keeps the rich Claude Code-style description; "compact"
|
|
1863
|
-
// swaps in a ~75% smaller one for small/local models (#91).
|
|
1864
|
-
//
|
|
1910
|
+
// swaps in a ~75% smaller one for small/local models (#91). The description
|
|
1911
|
+
// getter rechecks configuration authority so replacements cannot retain denied text.
|
|
1865
1912
|
let toolDescriptionMode: ToolDescriptionMode = "full";
|
|
1866
1913
|
function getToolDescriptionMode(): ToolDescriptionMode {
|
|
1867
1914
|
return toolDescriptionMode;
|
|
@@ -1961,40 +2008,47 @@ function activateRootRuntime(
|
|
|
1961
2008
|
|
|
1962
2009
|
// Apply the already-validated session settings and emit the same lifecycle
|
|
1963
2010
|
// event that the normal startup path exposes. Do not re-read process.cwd().
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2011
|
+
const baselineSettings: SubagentsSettings = { ...snapshotSettings(), strictAgentFiles: false, agentTiers: {} };
|
|
2012
|
+
function applyRuntimeSettings(settings: SubagentsSettings): void {
|
|
2013
|
+
// Clear optional policies as well: absence must not retain the old project's values.
|
|
2014
|
+
setDefaultModel(settings.defaultModel ?? baselineSettings.defaultModel);
|
|
2015
|
+
setFallbackSubagent(settings.fallbackSubagent ?? baselineSettings.fallbackSubagent);
|
|
2016
|
+
applySettings({ ...baselineSettings, ...settings }, {
|
|
2017
|
+
setMaxConcurrent: (n) => manager.setMaxConcurrent(n),
|
|
2018
|
+
setDefaultMaxTurns,
|
|
2019
|
+
setGraceTurns,
|
|
2020
|
+
setDefaultMaxTokens,
|
|
2021
|
+
setDefaultMaxToolCalls,
|
|
2022
|
+
setDefaultToolTimeout: (ms) => setDefaultToolTimeoutMs(ms),
|
|
2023
|
+
setDefaultJoinMode,
|
|
2024
|
+
setDefaultModel,
|
|
2025
|
+
setSchedulingEnabled,
|
|
2026
|
+
setScopeModels: setScopeModelsEnabled,
|
|
2027
|
+
setStrictAgentFiles: (enabled) => {
|
|
2028
|
+
strictAgentFiles = enabled;
|
|
2029
|
+
},
|
|
2030
|
+
setDisableDefaultAgents,
|
|
2031
|
+
setToolDescriptionMode,
|
|
2032
|
+
setFleetView: setFleetViewEnabled,
|
|
2033
|
+
setOutputTranscript: setOutputTranscriptDefault,
|
|
2034
|
+
setRememberAgents: (enabled) => manager.setRememberAgents(enabled),
|
|
2035
|
+
setAgentMentions: (enabled) => {
|
|
2036
|
+
agentMentionMode = enabled;
|
|
2037
|
+
},
|
|
2038
|
+
setSupervisorQuestions: (enabled) => {
|
|
2039
|
+
supervisorQuestionsEnabled = enabled;
|
|
2040
|
+
manager.setSupervisorQuestions(enabled);
|
|
2041
|
+
},
|
|
2042
|
+
setMaxSubagentDepth,
|
|
2043
|
+
setMaxSubagentSpawnsPerBranch: (n) =>
|
|
2044
|
+
manager.setMaxSubagentSpawnsPerBranch(n),
|
|
2045
|
+
setFallbackSubagent,
|
|
2046
|
+
setWorktreeIsolation: setWorktreeIsolationEnabled,
|
|
2047
|
+
setAgentTiers: setAgentTiersSettings,
|
|
2048
|
+
});
|
|
2049
|
+
pi.events.emit("subagents:settings_loaded", { settings });
|
|
2050
|
+
}
|
|
2051
|
+
applyRuntimeSettings(startupSettings);
|
|
1998
2052
|
|
|
1999
2053
|
// A tier that nothing defines is a typo, and the resolver would only reach it
|
|
2000
2054
|
// on the first spawn that needs it — possibly minutes in, mid-task. Report it
|
|
@@ -2023,7 +2077,7 @@ function activateRootRuntime(
|
|
|
2023
2077
|
* prose working. A delivered mention costs no main-model turn; the answer
|
|
2024
2078
|
* arrives through the ordinary completion notification either way.
|
|
2025
2079
|
*/
|
|
2026
|
-
pi.on("input", async (event, ctx) => {
|
|
2080
|
+
pi.on("input", parentQuestions.observeHandledInput(async (event, ctx) => {
|
|
2027
2081
|
// Never hijack text the extension layer itself submitted (pi.sendMessage,
|
|
2028
2082
|
// scheduled prompts) — only something a person typed can be a mention.
|
|
2029
2083
|
if (event.source === "extension" || !isAgentMentionsEnabled())
|
|
@@ -2201,7 +2255,7 @@ function activateRootRuntime(
|
|
|
2201
2255
|
// In `model` mode an off-screen mention-only turn rewrites the typed task
|
|
2202
2256
|
// without copying parent history or its request-local context hooks.
|
|
2203
2257
|
// Nothing reaches the chat, and it starts an ordinary top-level agent.
|
|
2204
|
-
const registeredAgentTool = agentToolRef;
|
|
2258
|
+
const registeredAgentTool = agentToolRef ? freshAgentToolDefinition() : undefined;
|
|
2205
2259
|
if (getAgentMentionMode() === "model" && registeredAgentTool) {
|
|
2206
2260
|
// The hidden turn is itself a provider call. Refuse before saying
|
|
2207
2261
|
// "Starting" or spending that call when its inherited parent model is
|
|
@@ -2262,7 +2316,7 @@ function activateRootRuntime(
|
|
|
2262
2316
|
);
|
|
2263
2317
|
}
|
|
2264
2318
|
return { action: "handled" };
|
|
2265
|
-
});
|
|
2319
|
+
}));
|
|
2266
2320
|
|
|
2267
2321
|
// ---- Agent tool ----
|
|
2268
2322
|
|
|
@@ -2294,15 +2348,13 @@ function activateRootRuntime(
|
|
|
2294
2348
|
// The catalogue is injected into the description rather than left to a lookup
|
|
2295
2349
|
// tool: the host has to pick a tier on its first call, and a tool it must
|
|
2296
2350
|
// remember to call first is a tool it will skip.
|
|
2297
|
-
const
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
? `\n\n${compactTierListText}`
|
|
2302
|
-
: "";
|
|
2351
|
+
const tierSection = (compact = false): string => {
|
|
2352
|
+
const text = compact ? buildCompactAgentTierListText() : buildAgentTierListText();
|
|
2353
|
+
return text ? `\n\n${text}` : "";
|
|
2354
|
+
};
|
|
2303
2355
|
|
|
2304
|
-
const compactAgentToolDescription = `Launch an autonomous agent for complex, multi-step tasks. Agent types:
|
|
2305
|
-
${buildCompactTypeListText()}${
|
|
2356
|
+
const compactAgentToolDescription = () => `Launch an autonomous agent for complex, multi-step tasks. Agent types:
|
|
2357
|
+
${buildCompactTypeListText()}${tierSection(true)}
|
|
2306
2358
|
|
|
2307
2359
|
Custom agents: .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global).
|
|
2308
2360
|
|
|
@@ -2314,12 +2366,12 @@ Notes:
|
|
|
2314
2366
|
- isolation: "worktree" runs the agent in an isolated git worktree; changes land on a branch.
|
|
2315
2367
|
- Raw inherit_context is unavailable; put only an explicitly sanitized summary in the task prompt.`;
|
|
2316
2368
|
|
|
2317
|
-
const fullAgentToolDescription = `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
|
|
2369
|
+
const fullAgentToolDescription = () => `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
|
|
2318
2370
|
|
|
2319
2371
|
Available agent types and the tools they have access to:
|
|
2320
2372
|
${buildTypeListText()}
|
|
2321
2373
|
|
|
2322
|
-
Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.${tierSection}
|
|
2374
|
+
Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.${tierSection()}
|
|
2323
2375
|
|
|
2324
2376
|
When using the Agent tool, specify a subagent_type parameter to select which agent type to use.
|
|
2325
2377
|
|
|
@@ -2367,8 +2419,8 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2367
2419
|
// Both carry their own leading blank line, so a template that drops the
|
|
2368
2420
|
// placeholder inline renders byte-identically to the built-in description
|
|
2369
2421
|
// whether or not any tier is configured.
|
|
2370
|
-
tierList: () => tierSection,
|
|
2371
|
-
compactTierList: () =>
|
|
2422
|
+
tierList: () => tierSection(),
|
|
2423
|
+
compactTierList: () => tierSection(true),
|
|
2372
2424
|
defaultTier: getDefaultAgentTierText,
|
|
2373
2425
|
agentDir: getAgentDir,
|
|
2374
2426
|
scheduleGuideline: () => scheduleGuideline,
|
|
@@ -2384,10 +2436,12 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2384
2436
|
};
|
|
2385
2437
|
|
|
2386
2438
|
const loadCustomToolDescription = (): string | undefined => {
|
|
2439
|
+
const trust = captureProjectTrust(configurationOwner);
|
|
2387
2440
|
for (const path of [
|
|
2388
|
-
join(
|
|
2441
|
+
...(trust.trusted ? [join(trust.cwd, ".pi", "agent-tool-description.md")] : []),
|
|
2389
2442
|
join(getAgentDir(), "agent-tool-description.md"),
|
|
2390
2443
|
]) {
|
|
2444
|
+
if (!trust.trusted && isProjectResource(path, trust.cwd)) continue;
|
|
2391
2445
|
try {
|
|
2392
2446
|
if (!existsSync(path)) continue;
|
|
2393
2447
|
const text = readFileSync(path, "utf-8").trim();
|
|
@@ -2402,9 +2456,11 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2402
2456
|
return undefined;
|
|
2403
2457
|
};
|
|
2404
2458
|
|
|
2405
|
-
const agentToolDescription = (
|
|
2459
|
+
const agentToolDescription = () => {
|
|
2460
|
+
const trust = captureProjectTrust(configurationOwner);
|
|
2461
|
+
if (trust.cwd !== activeTrust.cwd || trust.trusted !== activeTrust.trusted) reloadCustomAgents();
|
|
2406
2462
|
const mode = getToolDescriptionMode();
|
|
2407
|
-
if (mode === "compact") return compactAgentToolDescription;
|
|
2463
|
+
if (mode === "compact") return compactAgentToolDescription();
|
|
2408
2464
|
if (mode === "custom") {
|
|
2409
2465
|
const custom = loadCustomToolDescription();
|
|
2410
2466
|
if (custom) return custom;
|
|
@@ -2412,16 +2468,18 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2412
2468
|
'[pi-subagents] toolDescriptionMode is "custom" but no agent-tool-description.md found — using "full"',
|
|
2413
2469
|
);
|
|
2414
2470
|
}
|
|
2415
|
-
return fullAgentToolDescription;
|
|
2416
|
-
}
|
|
2471
|
+
return fullAgentToolDescription();
|
|
2472
|
+
};
|
|
2417
2473
|
|
|
2418
2474
|
// Captured so the mention clone can hand the copy the REAL registered tool:
|
|
2419
2475
|
// its handler closes over this activation, which is what makes a clone-driven
|
|
2420
2476
|
// spawn an ordinary top-level agent rather than something the fork owns.
|
|
2477
|
+
const agentTypeParameterDescription = () =>
|
|
2478
|
+
`The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available.`;
|
|
2421
2479
|
const agentTool = defineTool({
|
|
2422
2480
|
name: SUBAGENT_TOOL_NAMES.AGENT,
|
|
2423
2481
|
label: "Agent",
|
|
2424
|
-
description
|
|
2482
|
+
get description() { return agentToolDescription(); },
|
|
2425
2483
|
promptSnippet: "Launch autonomous sub-agents for complex multi-step tasks",
|
|
2426
2484
|
promptGuidelines: [
|
|
2427
2485
|
"Use Agent with specialized agents when the task matches an agent type's description. Subagents are valuable for parallelizing independent queries or for protecting the main context window from excessive results, but should not be used excessively when not needed. Importantly, avoid duplicating work that subagents are already doing — if you delegate research to a subagent, do not also perform the same searches yourself.",
|
|
@@ -2438,7 +2496,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2438
2496
|
"A short (3-5 word) description of the task (shown in UI).",
|
|
2439
2497
|
}),
|
|
2440
2498
|
subagent_type: Type.String({
|
|
2441
|
-
description:
|
|
2499
|
+
description: agentTypeParameterDescription(),
|
|
2442
2500
|
}),
|
|
2443
2501
|
tier: Type.Optional(
|
|
2444
2502
|
Type.String({
|
|
@@ -2458,6 +2516,9 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2458
2516
|
"Set to true to run in background. Returns agent ID immediately. You will be notified on completion.",
|
|
2459
2517
|
}),
|
|
2460
2518
|
),
|
|
2519
|
+
supervisor_questions: Type.Optional(Type.Union([Type.Literal("human"), Type.Literal("parent"), Type.Literal("off")], {
|
|
2520
|
+
description: 'Explicit question routing, default human. "parent" is ordinary clarification only (never permission/approval), supported only for fresh non-isolated top-level background calls. No schedules or mode-changing resumes.',
|
|
2521
|
+
})),
|
|
2461
2522
|
resume: Type.Optional(
|
|
2462
2523
|
Type.String({
|
|
2463
2524
|
description:
|
|
@@ -2692,6 +2753,16 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2692
2753
|
...params,
|
|
2693
2754
|
agentTiers: getAgentTiersSettings(),
|
|
2694
2755
|
});
|
|
2756
|
+
if (params.supervisor_questions === "parent" &&
|
|
2757
|
+
(!resolvedConfig.runInBackground || resolvedConfig.isolated || params.schedule || params.resume || !isSupervisorQuestionsEnabled())) {
|
|
2758
|
+
throw new Error('supervisor_questions: "parent" requires a fresh non-isolated background Agent. Set run_in_background: true, omit schedule/resume, and enable Supervisor questions. Foreground calls cannot answer while blocked.');
|
|
2759
|
+
}
|
|
2760
|
+
if (params.schedule && params.supervisor_questions !== undefined) {
|
|
2761
|
+
throw new Error('Scheduled calls do not support per-call question routing; omit supervisor_questions.');
|
|
2762
|
+
}
|
|
2763
|
+
if (params.resume && params.supervisor_questions !== undefined) {
|
|
2764
|
+
throw new Error('Resume preserves the child question mode; omit supervisor_questions and use run_in_background: true for a parent-mode child.');
|
|
2765
|
+
}
|
|
2695
2766
|
if (!params.resume && !params.schedule && resolvedConfig.inheritContext) {
|
|
2696
2767
|
return textResult(INHERIT_CONTEXT_UNAVAILABLE);
|
|
2697
2768
|
}
|
|
@@ -2720,7 +2791,8 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2720
2791
|
if (!resolvedConfig.agentTierSelected) {
|
|
2721
2792
|
const scopeVerdict = checkModelScope({
|
|
2722
2793
|
model,
|
|
2723
|
-
cwd:
|
|
2794
|
+
cwd: activeTrust.cwd,
|
|
2795
|
+
authority: activeTrust,
|
|
2724
2796
|
modelRegistry: ctx.modelRegistry,
|
|
2725
2797
|
callerSupplied: resolvedConfig.modelFromParams,
|
|
2726
2798
|
agentLabel: customConfig?.displayName ?? subagentType,
|
|
@@ -3009,6 +3081,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3009
3081
|
: thinking,
|
|
3010
3082
|
agentTier: resolvedConfig.requestedAgentTier,
|
|
3011
3083
|
isBackground: true,
|
|
3084
|
+
supervisorQuestions: params.supervisor_questions === "parent" ? "parent" : params.supervisor_questions === "off" ? false : undefined,
|
|
3012
3085
|
isolation,
|
|
3013
3086
|
invocation: agentInvocation,
|
|
3014
3087
|
rootSessionId: ctx.sessionManager.getSessionId(),
|
|
@@ -3166,6 +3239,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3166
3239
|
: thinking,
|
|
3167
3240
|
agentTier: resolvedConfig.requestedAgentTier,
|
|
3168
3241
|
isolation,
|
|
3242
|
+
supervisorQuestions: params.supervisor_questions === "off" ? false : undefined,
|
|
3169
3243
|
invocation: agentInvocation,
|
|
3170
3244
|
signal,
|
|
3171
3245
|
rootSessionId: ctx.sessionManager.getSessionId(),
|
|
@@ -3217,6 +3291,42 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
3217
3291
|
pi.registerTool(agentTool);
|
|
3218
3292
|
agentToolRef = agentTool;
|
|
3219
3293
|
|
|
3294
|
+
// Pi snapshots description and parameters when wrapping a registered tool.
|
|
3295
|
+
// Trust changes alone emit no host event. Re-register before each new prompt,
|
|
3296
|
+
// ahead of Pi's effective loadout snapshot, using its public post-bind refresh.
|
|
3297
|
+
// Turn end also covers provider continuations after tools revoke authority.
|
|
3298
|
+
// Keep the same exposure/execution policy and retain the active set.
|
|
3299
|
+
function freshAgentToolDefinition() {
|
|
3300
|
+
reloadCustomAgents();
|
|
3301
|
+
return {
|
|
3302
|
+
...agentTool,
|
|
3303
|
+
description: agentToolDescription(),
|
|
3304
|
+
parameters: {
|
|
3305
|
+
...agentTool.parameters,
|
|
3306
|
+
properties: {
|
|
3307
|
+
...agentTool.parameters.properties,
|
|
3308
|
+
subagent_type: { ...agentTool.parameters.properties.subagent_type, description: agentTypeParameterDescription() },
|
|
3309
|
+
tier: { ...agentTool.parameters.properties.tier, description: buildAgentTierParameterDescription() },
|
|
3310
|
+
},
|
|
3311
|
+
},
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
const refreshAgentDeclaration = () => {
|
|
3315
|
+
const refreshed = freshAgentToolDefinition();
|
|
3316
|
+
const activeTools = pi.getActiveTools();
|
|
3317
|
+
pi.registerTool(refreshed);
|
|
3318
|
+
// Pi's explicit --tools/SDK tools selector can re-activate every matching
|
|
3319
|
+
// name during registry refresh. A metadata refresh must not undo a later
|
|
3320
|
+
// activation/exposure decision by the host or another extension.
|
|
3321
|
+
const refreshedActive = pi.getActiveTools();
|
|
3322
|
+
if (activeTools.length !== refreshedActive.length || activeTools.some((name, i) => name !== refreshedActive[i])) {
|
|
3323
|
+
pi.setActiveTools(activeTools);
|
|
3324
|
+
}
|
|
3325
|
+
agentToolRef = refreshed;
|
|
3326
|
+
};
|
|
3327
|
+
pi.on("before_agent_start", refreshAgentDeclaration);
|
|
3328
|
+
pi.on("turn_end", refreshAgentDeclaration);
|
|
3329
|
+
|
|
3220
3330
|
// ---- get_subagent_result tool ----
|
|
3221
3331
|
|
|
3222
3332
|
pi.registerTool(
|
|
@@ -4834,7 +4944,7 @@ Do not wrap the response in a markdown code fence. Return only the file contents
|
|
|
4834
4944
|
// scoped set would fail the scope check at spawn time.
|
|
4835
4945
|
if (isScopeModelsEnabled()) {
|
|
4836
4946
|
const enabled = resolveEnabledModels(
|
|
4837
|
-
readEnabledModels(sessionCwd),
|
|
4947
|
+
readEnabledModels(sessionCwd, captureProjectTrust(configurationOwner)),
|
|
4838
4948
|
ctx.modelRegistry,
|
|
4839
4949
|
sessionCwd,
|
|
4840
4950
|
);
|
package/src/model-scope.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { isModelInScope, type ModelRegistryRef, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
|
|
11
|
+
import type { ProjectTrust } from "./project-trust.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* When enabled, subagent model choices are validated against `enabledModels`
|
|
@@ -41,6 +42,8 @@ export type ModelScopeVerdict =
|
|
|
41
42
|
export function checkModelScope(args: {
|
|
42
43
|
model: { provider: string; id: string } | undefined;
|
|
43
44
|
cwd: string;
|
|
45
|
+
/** Captured config root/trust and retained ancestor denials; omit only for standalone helpers. */
|
|
46
|
+
authority?: ProjectTrust;
|
|
44
47
|
modelRegistry: ModelRegistryRef;
|
|
45
48
|
/** True when the model came from the tool call rather than frontmatter. */
|
|
46
49
|
callerSupplied: boolean;
|
|
@@ -52,7 +55,7 @@ export function checkModelScope(args: {
|
|
|
52
55
|
const { model, cwd, modelRegistry, callerSupplied, agentLabel, modelInput } = args;
|
|
53
56
|
if (!scopeModelsEnabled || !model) return { kind: "ok" };
|
|
54
57
|
|
|
55
|
-
const allowed = resolveEnabledModels(readEnabledModels(cwd), modelRegistry, cwd);
|
|
58
|
+
const allowed = resolveEnabledModels(readEnabledModels(cwd, args.authority), modelRegistry, cwd);
|
|
56
59
|
if (!allowed || isModelInScope(model, allowed)) return { kind: "ok" };
|
|
57
60
|
|
|
58
61
|
// A tier whose profile says `inherit` names no model of its own, so fall back
|
package/src/nested-tools.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
streamToOutputFile,
|
|
28
28
|
writeInitialEntry,
|
|
29
29
|
} from "./output-file.js";
|
|
30
|
+
import { configurationContext, type ProjectTrust } from "./project-trust.js";
|
|
30
31
|
import { getForegroundOutcomeNote, getStatusNote, partialOutputSuffix } from "./status-note.js";
|
|
31
32
|
import type {
|
|
32
33
|
AgentConfig,
|
|
@@ -104,6 +105,8 @@ export interface NestedToolContext {
|
|
|
104
105
|
allowedSubagents: "all" | string[];
|
|
105
106
|
/** Root used for agent/config discovery; may differ from the agent's working directory. */
|
|
106
107
|
configCwd: string;
|
|
108
|
+
/** Immutable configuration authority captured by the runner, including ancestor denials. */
|
|
109
|
+
projectTrust: ProjectTrust;
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
function textResult(text: string, isError = false) {
|
|
@@ -152,7 +155,9 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
|
|
|
152
155
|
// Agents resolve from a registry built for THIS branch's config root (under
|
|
153
156
|
// worktree isolation, the copy). Never via registerAgents — that is
|
|
154
157
|
// process-global state shared with the main session and every other agent.
|
|
155
|
-
const loadRegistry = () => buildAgentRegistry(loadCustomAgents(
|
|
158
|
+
const loadRegistry = () => buildAgentRegistry(loadCustomAgents(
|
|
159
|
+
context.configCwd, false, context.projectTrust.trusted, context.projectTrust.deniedRoots,
|
|
160
|
+
));
|
|
156
161
|
const allowedTypesIn = (registry: Map<string, AgentConfig>): Set<string> | undefined =>
|
|
157
162
|
context.allowedSubagents === "all"
|
|
158
163
|
? undefined
|
|
@@ -250,6 +255,7 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
|
|
|
250
255
|
const scopeVerdict = checkModelScope({
|
|
251
256
|
model,
|
|
252
257
|
cwd: context.configCwd,
|
|
258
|
+
authority: context.projectTrust,
|
|
253
259
|
modelRegistry: ctx.modelRegistry,
|
|
254
260
|
callerSupplied: invocation.modelFromParams,
|
|
255
261
|
agentLabel: config?.displayName ?? resolvedType,
|
|
@@ -339,7 +345,7 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
|
|
|
339
345
|
}
|
|
340
346
|
};
|
|
341
347
|
|
|
342
|
-
// `ctx` is forwarded
|
|
348
|
+
// `ctx` is forwarded with configuration-root provenance, never captured at tool-build
|
|
343
349
|
// time: each AgentSession builds its own ExtensionRunner from that session's
|
|
344
350
|
// cwd/sessionManager/modelRegistry, so this is the CHILD's context. Capturing
|
|
345
351
|
// one earlier would silently give a grandchild the wrong worktree base, the
|
|
@@ -350,7 +356,7 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
|
|
|
350
356
|
// letting it escape into the child's turn.
|
|
351
357
|
try {
|
|
352
358
|
if (invocation.runInBackground) {
|
|
353
|
-
const id = context.manager.spawn(context.pi, ctx, resolvedType, params.prompt, {
|
|
359
|
+
const id = context.manager.spawn(context.pi, configurationContext(ctx, context.configCwd, context.projectTrust), resolvedType, params.prompt, {
|
|
354
360
|
...options,
|
|
355
361
|
isBackground: true,
|
|
356
362
|
});
|
|
@@ -362,7 +368,7 @@ export function createNestedSubagentTools(context: NestedToolContext): ToolDefin
|
|
|
362
368
|
|
|
363
369
|
const { record } = await context.manager.spawnAndWait(
|
|
364
370
|
context.pi,
|
|
365
|
-
ctx,
|
|
371
|
+
configurationContext(ctx, context.configCwd, context.projectTrust),
|
|
366
372
|
resolvedType,
|
|
367
373
|
params.prompt,
|
|
368
374
|
{ ...options, signal },
|