pi-subagents 0.53.0 → 0.54.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 +31 -4
- package/README.md +1 -1
- package/docs/configuration.md +3 -3
- package/docs/models.md +17 -3
- package/index.ts +10 -1
- package/package.json +1 -1
- package/prompts/council.md +10 -7
- package/skills/council-mode/SKILL.md +17 -16
- package/skills/pi-subagents/SKILL.md +5 -1
- package/skills/pi-subagents/references/execution-controls.md +2 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +7 -0
- package/src/agents/agent-management.ts +85 -5
- package/src/agents/agents.ts +67 -24
- package/src/api/preflight.ts +7 -2
- package/src/extension/doctor.ts +10 -0
- package/src/extension/fanout-child.ts +3 -2
- package/src/extension/index.ts +4 -3
- package/src/extension/public-execution.ts +1 -1
- package/src/extension/tool-result.ts +19 -0
- package/src/inspectors/herdr/client.ts +3 -3
- package/src/runs/background/async-execution.ts +10 -5
- package/src/runs/background/async-retention.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +5 -0
- package/src/runs/background/result-watcher.ts +4 -3
- package/src/runs/background/stale-run-reconciler.ts +3 -0
- package/src/runs/background/subagent-runner.ts +26 -2
- package/src/runs/background/wait-completions.ts +2 -0
- package/src/runs/background/wait-tool.ts +4 -3
- package/src/runs/foreground/execution.ts +29 -1
- package/src/runs/foreground/subagent-executor.ts +14 -6
- package/src/runs/shared/model-fallback.ts +36 -15
- package/src/runs/shared/model-scope.ts +106 -39
- package/src/runs/shared/pi-args.ts +34 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +36 -9
- package/src/shared/types.ts +13 -3
- package/src/watchdog/permission-arbiter.ts +59 -51
package/src/agents/agents.ts
CHANGED
|
@@ -110,6 +110,9 @@ export interface AgentConfig {
|
|
|
110
110
|
runner?: AgentRunnerConfig;
|
|
111
111
|
localName?: string;
|
|
112
112
|
packageName?: string;
|
|
113
|
+
packageSourceName?: string;
|
|
114
|
+
packageSourceVersion?: string;
|
|
115
|
+
packageSourceRoot?: string;
|
|
113
116
|
description: string;
|
|
114
117
|
aliases?: string[];
|
|
115
118
|
tools?: string[];
|
|
@@ -255,8 +258,15 @@ function getUserChainDir(): string {
|
|
|
255
258
|
return path.join(getAgentDir(), "chains");
|
|
256
259
|
}
|
|
257
260
|
|
|
261
|
+
interface PackageSubagentPath {
|
|
262
|
+
dir: string;
|
|
263
|
+
packageName?: string;
|
|
264
|
+
packageVersion?: string;
|
|
265
|
+
packageRoot: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
258
268
|
interface PackageSubagentPaths {
|
|
259
|
-
agents:
|
|
269
|
+
agents: PackageSubagentPath[];
|
|
260
270
|
chains: string[];
|
|
261
271
|
}
|
|
262
272
|
|
|
@@ -391,18 +401,30 @@ function stringArray(value: unknown): string[] {
|
|
|
391
401
|
return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0);
|
|
392
402
|
}
|
|
393
403
|
|
|
404
|
+
function packageMetadata(pkg: Record<string, unknown>, packageRoot: string): Omit<PackageSubagentPath, "dir"> {
|
|
405
|
+
const name = typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : undefined;
|
|
406
|
+
const version = typeof pkg.version === "string" && pkg.version.trim() ? pkg.version.trim() : undefined;
|
|
407
|
+
return {
|
|
408
|
+
packageRoot,
|
|
409
|
+
...(name ? { packageName: name } : {}),
|
|
410
|
+
...(version ? { packageVersion: version } : {}),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
394
414
|
function extractSubagentPathsFromPackageRoot(packageRoot: string): PackageSubagentPaths {
|
|
395
415
|
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
396
416
|
const pkg = readJsonFileBestEffort(packageJsonPath);
|
|
397
417
|
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return { agents: [], chains: [] };
|
|
418
|
+
const pkgRecord = pkg as Record<string, unknown>;
|
|
419
|
+
const metadata = packageMetadata(pkgRecord, packageRoot);
|
|
398
420
|
|
|
399
421
|
const roots: Record<string, unknown>[] = [];
|
|
400
|
-
const piSubagents =
|
|
422
|
+
const piSubagents = pkgRecord["pi-subagents"];
|
|
401
423
|
if (piSubagents && typeof piSubagents === "object" && !Array.isArray(piSubagents)) {
|
|
402
424
|
roots.push(piSubagents as Record<string, unknown>);
|
|
403
425
|
}
|
|
404
426
|
|
|
405
|
-
const pi =
|
|
427
|
+
const pi = pkgRecord.pi;
|
|
406
428
|
if (pi && typeof pi === "object" && !Array.isArray(pi)) {
|
|
407
429
|
const subagents = (pi as { subagents?: unknown }).subagents;
|
|
408
430
|
if (subagents && typeof subagents === "object" && !Array.isArray(subagents)) {
|
|
@@ -410,10 +432,10 @@ function extractSubagentPathsFromPackageRoot(packageRoot: string): PackageSubage
|
|
|
410
432
|
}
|
|
411
433
|
}
|
|
412
434
|
|
|
413
|
-
const agents:
|
|
435
|
+
const agents: PackageSubagentPath[] = [];
|
|
414
436
|
const chains: string[] = [];
|
|
415
437
|
for (const root of roots) {
|
|
416
|
-
for (const entry of stringArray(root.agents)) agents.push(path.resolve(packageRoot, entry));
|
|
438
|
+
for (const entry of stringArray(root.agents)) agents.push({ dir: path.resolve(packageRoot, entry), ...metadata });
|
|
417
439
|
for (const entry of stringArray(root.chains)) chains.push(path.resolve(packageRoot, entry));
|
|
418
440
|
}
|
|
419
441
|
return { agents, chains };
|
|
@@ -505,17 +527,17 @@ function collectPackageSubagentPaths(cwd: string, options: { includeUser: boolea
|
|
|
505
527
|
const seenRoots = new Set<string>();
|
|
506
528
|
const seenAgents = new Set<string>();
|
|
507
529
|
const seenChains = new Set<string>();
|
|
508
|
-
const agents:
|
|
530
|
+
const agents: PackageSubagentPath[] = [];
|
|
509
531
|
const chains: string[] = [];
|
|
510
532
|
for (const packageRoot of packageRoots) {
|
|
511
533
|
const resolvedRoot = path.resolve(packageRoot);
|
|
512
534
|
if (seenRoots.has(resolvedRoot)) continue;
|
|
513
535
|
seenRoots.add(resolvedRoot);
|
|
514
536
|
const paths = extractSubagentPathsFromPackageRoot(resolvedRoot);
|
|
515
|
-
for (const
|
|
516
|
-
if (seenAgents.has(
|
|
517
|
-
seenAgents.add(
|
|
518
|
-
agents.push(
|
|
537
|
+
for (const agentPath of paths.agents) {
|
|
538
|
+
if (seenAgents.has(agentPath.dir)) continue;
|
|
539
|
+
seenAgents.add(agentPath.dir);
|
|
540
|
+
agents.push(agentPath);
|
|
519
541
|
}
|
|
520
542
|
for (const chainDir of paths.chains) {
|
|
521
543
|
if (seenChains.has(chainDir)) continue;
|
|
@@ -1233,7 +1255,18 @@ function applyCustomAgentOverride(
|
|
|
1233
1255
|
if (override.acceptanceRole !== undefined) {
|
|
1234
1256
|
fill("acceptanceRole", ["acceptanceRole"], override.acceptanceRole === false ? undefined : override.acceptanceRole);
|
|
1235
1257
|
}
|
|
1236
|
-
if (override.disabled !== undefined
|
|
1258
|
+
if (override.disabled !== undefined) {
|
|
1259
|
+
// Unconditional, matching applyBuiltinOverride: custom agents have no
|
|
1260
|
+
// frontmatter concept of `disabled` to protect (unlike model/thinking/
|
|
1261
|
+
// etc., which use `fill()` + agentHasFrontmatterField to defer to the
|
|
1262
|
+
// agent's own file). A guard here previously read `agent.disabled ===
|
|
1263
|
+
// undefined`, which was a no-op before user+project layering existed
|
|
1264
|
+
// (this function only ever ran once, against the pristine base agent,
|
|
1265
|
+
// whose `.disabled` is always undefined for custom agents) but became a
|
|
1266
|
+
// real bug once a user-scope override could run first: the guard then
|
|
1267
|
+
// silently blocked a later project-scope override from ever changing
|
|
1268
|
+
// `disabled`, breaking this PR's own "project wins" precedence for this
|
|
1269
|
+
// one field.
|
|
1237
1270
|
mutable().disabled = override.disabled;
|
|
1238
1271
|
anyFilled = true;
|
|
1239
1272
|
}
|
|
@@ -1275,18 +1308,25 @@ function applyCustomAgentOverrides(
|
|
|
1275
1308
|
userSettingsPath: string,
|
|
1276
1309
|
projectSettingsPath: string | null,
|
|
1277
1310
|
): AgentConfig[] {
|
|
1311
|
+
// Both scopes are applied, user first then project, so a project override that
|
|
1312
|
+
// only sets a subset of fields (e.g. just `extensions`) layers on top of the
|
|
1313
|
+
// user's override instead of silently discarding it (#1341 / issue writeup:
|
|
1314
|
+
// "agentOverrides project override drops user-only fields for custom agents").
|
|
1315
|
+
// Per-field precedence still favors project over user: applyCustomAgentOverride
|
|
1316
|
+
// only fills fields present in the override it's given, and project is applied
|
|
1317
|
+
// last, so any field set at both scopes ends up with the project's value.
|
|
1278
1318
|
return agents.map((agent) => {
|
|
1319
|
+
const userOverride = userSettings.overrides[agent.name];
|
|
1320
|
+
const withUserOverride = userOverride
|
|
1321
|
+
? applyCustomAgentOverride(agent, userOverride, { scope: "user", path: userSettingsPath })
|
|
1322
|
+
: agent;
|
|
1323
|
+
|
|
1279
1324
|
const projectOverride = projectSettings.overrides[agent.name];
|
|
1280
1325
|
if (projectOverride && projectSettingsPath) {
|
|
1281
|
-
return applyCustomAgentOverride(
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
const userOverride = userSettings.overrides[agent.name];
|
|
1285
|
-
if (userOverride) {
|
|
1286
|
-
return applyCustomAgentOverride(agent, userOverride, { scope: "user", path: userSettingsPath });
|
|
1326
|
+
return applyCustomAgentOverride(withUserOverride, projectOverride, { scope: "project", path: projectSettingsPath });
|
|
1287
1327
|
}
|
|
1288
1328
|
|
|
1289
|
-
return
|
|
1329
|
+
return withUserOverride;
|
|
1290
1330
|
});
|
|
1291
1331
|
}
|
|
1292
1332
|
|
|
@@ -1615,7 +1655,7 @@ function resolveAgentRelativeExtensionPaths(paths: string[] | undefined, agentFi
|
|
|
1615
1655
|
});
|
|
1616
1656
|
}
|
|
1617
1657
|
|
|
1618
|
-
function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: AgentSource, discoveryPriority?: number): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
|
|
1658
|
+
function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir">): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
|
|
1619
1659
|
const agents: AgentConfig[] = [];
|
|
1620
1660
|
const diagnostics: AgentDiscoveryDiagnostic[] = [];
|
|
1621
1661
|
|
|
@@ -1750,6 +1790,9 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
|
|
|
1750
1790
|
...(runner !== undefined ? { runner } : {}),
|
|
1751
1791
|
localName,
|
|
1752
1792
|
...(packageName !== undefined ? { packageName } : {}),
|
|
1793
|
+
...(packageSource?.packageName ? { packageSourceName: packageSource.packageName } : {}),
|
|
1794
|
+
...(packageSource?.packageVersion ? { packageSourceVersion: packageSource.packageVersion } : {}),
|
|
1795
|
+
...(packageSource?.packageRoot ? { packageSourceRoot: packageSource.packageRoot } : {}),
|
|
1753
1796
|
description: frontmatter.description,
|
|
1754
1797
|
...(aliases !== undefined ? { aliases } : {}),
|
|
1755
1798
|
...(rawTools !== undefined ? { tools } : {}),
|
|
@@ -1797,8 +1840,8 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
|
|
|
1797
1840
|
return { agents, diagnostics };
|
|
1798
1841
|
}
|
|
1799
1842
|
|
|
1800
|
-
function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
|
|
1801
|
-
return loadAgentsFromDefinitionFiles(readAgentDefinitionFiles(dir), source, discoveryPriority);
|
|
1843
|
+
function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir">): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
|
|
1844
|
+
return loadAgentsFromDefinitionFiles(readAgentDefinitionFiles(dir), source, discoveryPriority, packageSource);
|
|
1802
1845
|
}
|
|
1803
1846
|
|
|
1804
1847
|
function loadChainsFromDir(dir: string, source: AgentSource): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
|
|
@@ -1924,7 +1967,7 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
1924
1967
|
userSettingsPath,
|
|
1925
1968
|
projectSettingsPath,
|
|
1926
1969
|
);
|
|
1927
|
-
const packageLoaded = packageSubagentPaths.agents.map((
|
|
1970
|
+
const packageLoaded = packageSubagentPaths.agents.map((entry, index) => loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry));
|
|
1928
1971
|
const packageMap = new Map<string, AgentConfig>();
|
|
1929
1972
|
for (const loaded of packageLoaded) {
|
|
1930
1973
|
for (const agent of loaded.agents) {
|
|
@@ -1998,8 +2041,8 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
1998
2041
|
);
|
|
1999
2042
|
const packageMap = new Map<string, AgentConfig>();
|
|
2000
2043
|
const packageAgentDiagnostics: AgentDiscoveryDiagnostic[] = [];
|
|
2001
|
-
for (const [index,
|
|
2002
|
-
const loaded = loadAgentsFromDir(dir, "package", packageSubagentPaths.agents.length - index);
|
|
2044
|
+
for (const [index, entry] of packageSubagentPaths.agents.entries()) {
|
|
2045
|
+
const loaded = loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry);
|
|
2003
2046
|
packageAgentDiagnostics.push(...loaded.diagnostics);
|
|
2004
2047
|
for (const agent of loaded.agents) {
|
|
2005
2048
|
if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
|
package/src/api/preflight.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { resolveExecutionAgentScope } from "../agents/agent-scope.ts";
|
|
|
7
7
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts";
|
|
8
8
|
import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
|
|
9
9
|
import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, type AvailableModelInfo, type ParentModel } from "../runs/shared/model-fallback.ts";
|
|
10
|
+
import { resolveModelScopesForAgent } from "../runs/shared/model-scope.ts";
|
|
10
11
|
import { applyThinkingSuffix, resolvePiLaunchToolPlan, type PiLaunchToolPlan } from "../runs/shared/pi-args.ts";
|
|
11
12
|
import { injectOutputPathSystemPrompt, normalizeSingleOutputOverride, resolveSingleOutputPath } from "../runs/shared/single-output.ts";
|
|
12
13
|
import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
|
|
@@ -14,6 +15,7 @@ import { resolveEffectiveThinking } from "../shared/model-info.ts";
|
|
|
14
15
|
import { SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, type ArtifactDirPreference, type ArtifactPaths, type JsonSchemaObject, type OutputMode } from "../shared/types.ts";
|
|
15
16
|
import { capabilityCeilingAgentRestrictionMessage, intersectSubagentCapabilityCeilings, type ResolvedSubagentCapabilityCeiling, type SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
|
|
16
17
|
import { appendTurnBudgetSystemPrompt } from "../runs/shared/turn-budget.ts";
|
|
18
|
+
import { resolvePermissionRules } from "../runs/shared/permissions.ts";
|
|
17
19
|
import type { ResolvedTurnBudget } from "../shared/types.ts";
|
|
18
20
|
import type { ResolvedMcpDirectToolSelection } from "../runs/shared/mcp-direct-tool-allowlist.ts";
|
|
19
21
|
import { resolveStepBehavior } from "../shared/settings.ts";
|
|
@@ -288,19 +290,21 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
288
290
|
const externalRunner = agent.runner?.type === "external-cli" || agent.runner?.type === "external-job";
|
|
289
291
|
const availableModels = normalizeAvailableModels(input.availableModels);
|
|
290
292
|
const preferredProvider = input.preferredProvider ?? input.parentModel?.provider;
|
|
293
|
+
const modelScopes = resolveModelScopesForAgent(discovered.modelScope, agent.name, input.parentModel);
|
|
291
294
|
const primaryModel = externalRunner
|
|
292
295
|
? undefined
|
|
293
|
-
: resolveEffectiveSubagentModel(input.model, agent.model, input.parentModel, availableModels, preferredProvider, { scope:
|
|
296
|
+
: resolveEffectiveSubagentModel(input.model, agent.model, input.parentModel, availableModels, preferredProvider, { scope: modelScopes });
|
|
294
297
|
const effectiveThinkingConfig = input.thinking !== undefined ? input.thinking : agent.thinking;
|
|
295
298
|
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinkingConfig, input.thinking !== undefined);
|
|
296
299
|
const modelCandidates = externalRunner
|
|
297
300
|
? []
|
|
298
301
|
: buildModelCandidates(primaryModel, agent.fallbackModels, availableModels, preferredProvider, {
|
|
299
|
-
scope:
|
|
302
|
+
scope: modelScopes,
|
|
300
303
|
primaryModelFromParent: inheritsParentModel(input.model, agent.model, input.parentModel),
|
|
301
304
|
})
|
|
302
305
|
.map((candidate) => applyThinkingSuffix(candidate, effectiveThinkingConfig, input.thinking !== undefined) ?? candidate);
|
|
303
306
|
let toolPlan: PiLaunchToolPlan;
|
|
307
|
+
const permissionRules = resolvePermissionRules(loadConfig().permissions, agent.permissions);
|
|
304
308
|
try {
|
|
305
309
|
toolPlan = resolvePiLaunchToolPlan({
|
|
306
310
|
tools: agent.tools,
|
|
@@ -312,6 +316,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
312
316
|
structuredOutput: Boolean(input.outputSchema),
|
|
313
317
|
capabilityCeiling: effectiveCapabilityCeiling,
|
|
314
318
|
agentName: agent.name,
|
|
319
|
+
permissionRules,
|
|
315
320
|
});
|
|
316
321
|
} catch (error) {
|
|
317
322
|
const message = error instanceof Error ? error.message : String(error);
|
package/src/extension/doctor.ts
CHANGED
|
@@ -224,6 +224,13 @@ function formatPermissionSystemSection(): string[] {
|
|
|
224
224
|
return lines;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
function formatWorkflowScriptSection(): string[] {
|
|
228
|
+
return [
|
|
229
|
+
"- helpers: runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console",
|
|
230
|
+
"- recovery: if runs.all is missing, reload or update pi-subagents; await Promise.all([runs.run(...)]) is also supported",
|
|
231
|
+
];
|
|
232
|
+
}
|
|
233
|
+
|
|
227
234
|
export function buildDoctorReport(input: DoctorReportInput): string {
|
|
228
235
|
const paths = input.paths ?? defaultPaths();
|
|
229
236
|
const deps = { ...DEFAULT_DEPS, ...input.deps };
|
|
@@ -253,6 +260,9 @@ export function buildDoctorReport(input: DoctorReportInput): string {
|
|
|
253
260
|
"Active async capacity",
|
|
254
261
|
...formatActiveAsyncCapacitySection(input),
|
|
255
262
|
"",
|
|
263
|
+
"Workflow script",
|
|
264
|
+
...formatWorkflowScriptSection(),
|
|
265
|
+
"",
|
|
256
266
|
"Permission system",
|
|
257
267
|
...formatPermissionSystemSection(),
|
|
258
268
|
"",
|
|
@@ -11,6 +11,7 @@ import { readNestedControlRequests, resolveNestedRouteFromEnv, type NestedRoute,
|
|
|
11
11
|
import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts";
|
|
12
12
|
import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
|
|
13
13
|
import { createSubagentParamsSchema } from "./schemas.ts";
|
|
14
|
+
import { finalizeToolResult } from "./tool-result.ts";
|
|
14
15
|
import { loadConfig, resolveAsyncByDefault } from "./config.ts";
|
|
15
16
|
import { type Details, type SubagentState } from "../shared/types.ts";
|
|
16
17
|
|
|
@@ -181,8 +182,8 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
|
|
|
181
182
|
"Mutating management actions (create, update, delete, eject, disable, enable, reset, grant-spawn-budget) are blocked in this mode.",
|
|
182
183
|
].join("\n"),
|
|
183
184
|
parameters: params,
|
|
184
|
-
execute(id, params, signal, onUpdate, ctx) {
|
|
185
|
-
return executor.executePublic(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx);
|
|
185
|
+
async execute(id, params, signal, onUpdate, ctx) {
|
|
186
|
+
return finalizeToolResult(await executor.executePublic(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx));
|
|
186
187
|
},
|
|
187
188
|
};
|
|
188
189
|
|
package/src/extension/index.ts
CHANGED
|
@@ -57,6 +57,7 @@ import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capabili
|
|
|
57
57
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
58
58
|
import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
|
|
59
59
|
import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.ts";
|
|
60
|
+
import { finalizeToolResult } from "./tool-result.ts";
|
|
60
61
|
import { collectGoalContinuationNotices } from "../missions/goal-driver.ts";
|
|
61
62
|
import { restoreForegroundRunHistory } from "../runs/foreground/foreground-history.ts";
|
|
62
63
|
import { resolveMissionStoreLocation } from "../missions/store.ts";
|
|
@@ -517,7 +518,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
517
518
|
observedCompletionRunIds: () => scheduledRunManager.observedCompletionRunIds(),
|
|
518
519
|
hasDeliveryDemand: hasResultDeliveryDemand,
|
|
519
520
|
deliverIntercomResults: config.intercomBridge?.resultDelivery === true,
|
|
520
|
-
resultScanLogging: config.resultScanLogging
|
|
521
|
+
resultScanLogging: config.resultScanLogging,
|
|
521
522
|
},
|
|
522
523
|
);
|
|
523
524
|
const { startResultWatcher, primeExistingResults, stopResultWatcher } = resultWatcher;
|
|
@@ -658,8 +659,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
658
659
|
...buildSubagentToolPromptMetadata(config),
|
|
659
660
|
parameters,
|
|
660
661
|
|
|
661
|
-
execute(id, params, signal, onUpdate, ctx) {
|
|
662
|
-
return executeSubagentCollapsed(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx);
|
|
662
|
+
async execute(id, params, signal, onUpdate, ctx) {
|
|
663
|
+
return finalizeToolResult(await executeSubagentCollapsed(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx));
|
|
663
664
|
},
|
|
664
665
|
|
|
665
666
|
renderCall(args, theme) {
|
|
@@ -122,7 +122,7 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
|
|
|
122
122
|
params: {
|
|
123
123
|
...workflowDefaults,
|
|
124
124
|
...(params.async === undefined && options.asyncByDefault === false ? { async: false } : {}),
|
|
125
|
-
workflowScript: `
|
|
125
|
+
workflowScript: `return runs.run("main", ${JSON.stringify(child)})`,
|
|
126
126
|
} as T,
|
|
127
127
|
};
|
|
128
128
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert pi-subagents' internal logical-error result into the rejection Pi's
|
|
5
|
+
* public tool boundary uses to emit a canonical errored ToolResult.
|
|
6
|
+
*
|
|
7
|
+
* Keep this at registered tool boundaries. Internal workflows intentionally
|
|
8
|
+
* retain their return-based error handling.
|
|
9
|
+
*/
|
|
10
|
+
export function finalizeToolResult<T>(result: AgentToolResult<T>): AgentToolResult<T> {
|
|
11
|
+
if (result.isError !== true) return result;
|
|
12
|
+
|
|
13
|
+
const message = result.content
|
|
14
|
+
.flatMap((item) => item.type === "text" && typeof item.text === "string" ? [item.text] : [])
|
|
15
|
+
.join("\n")
|
|
16
|
+
.trim();
|
|
17
|
+
|
|
18
|
+
throw new Error(message || "pi-subagents reported a logical tool failure.");
|
|
19
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
|
|
3
3
|
export type HerdrErrorCode =
|
|
4
4
|
| "HERDR_UNAVAILABLE"
|
|
@@ -16,7 +16,7 @@ export interface HerdrClient {
|
|
|
16
16
|
run<T = unknown>(args: string[], options?: { timeoutMs?: number; signal?: AbortSignal; textOk?: boolean }): Promise<HerdrResult<T>>;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
type SpawnHerdr = (command: string, args: readonly string[], options: { shell: false; windowsHide: true; env: NodeJS.ProcessEnv }) =>
|
|
19
|
+
type SpawnHerdr = (command: string, args: readonly string[], options: { shell: false; windowsHide: true; env: NodeJS.ProcessEnv }) => ReturnType<typeof spawn>;
|
|
20
20
|
|
|
21
21
|
function error(code: HerdrErrorCode, message: string, details?: unknown): HerdrResult<never> {
|
|
22
22
|
return { ok: false, error: { code, message, ...(details !== undefined ? { details } : {}) } };
|
|
@@ -46,7 +46,7 @@ export function createHerdrClient(options: { bin?: string; spawn?: SpawnHerdr }
|
|
|
46
46
|
return {
|
|
47
47
|
run<T>(args: string[], runOptions: { timeoutMs?: number; signal?: AbortSignal; textOk?: boolean } = {}): Promise<HerdrResult<T>> {
|
|
48
48
|
return new Promise((resolve) => {
|
|
49
|
-
let child:
|
|
49
|
+
let child: ReturnType<typeof spawn>;
|
|
50
50
|
try {
|
|
51
51
|
child = spawnImpl(bin, args, { shell: false, windowsHide: true, env: process.env });
|
|
52
52
|
} catch (cause) {
|
|
@@ -26,7 +26,7 @@ import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
|
26
26
|
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
|
|
27
27
|
import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
|
|
28
28
|
import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
|
|
29
|
-
import type
|
|
29
|
+
import { resolveModelScopesForAgent, type ModelScopeConfig } from "../shared/model-scope.ts";
|
|
30
30
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
31
31
|
import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
|
|
32
32
|
import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
|
|
@@ -791,18 +791,20 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
791
791
|
const taskText = `${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`;
|
|
792
792
|
const task = namespaceOutputPath ? taskText : injectSingleOutputInstruction(taskText, outputPath, a);
|
|
793
793
|
|
|
794
|
+
const modelScopes = resolveModelScopesForAgent(ctx.modelScope, a.name, ctx.currentModel);
|
|
794
795
|
const primaryModel = externalRunner ? undefined : resolveEffectiveSubagentModel(
|
|
795
796
|
s.model,
|
|
796
797
|
a.model,
|
|
797
798
|
ctx.currentModel,
|
|
798
799
|
availableModels,
|
|
799
800
|
ctx.currentModelProvider,
|
|
800
|
-
{ scope:
|
|
801
|
+
{ scope: modelScopes },
|
|
801
802
|
);
|
|
802
803
|
const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
|
|
803
804
|
const effectiveThinking = externalRunner ? undefined : thinkingOverride ?? a.thinking;
|
|
804
805
|
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
|
|
805
806
|
const agentContract = s.agentContract ?? params.agentContract;
|
|
807
|
+
const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
|
|
806
808
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
807
809
|
tools: a.tools,
|
|
808
810
|
extensions: a.extensions,
|
|
@@ -814,9 +816,9 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
814
816
|
capabilityCeiling: params.capabilityCeiling,
|
|
815
817
|
inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
|
|
816
818
|
agentName: a.name,
|
|
819
|
+
permissionRules,
|
|
817
820
|
});
|
|
818
821
|
const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
|
|
819
|
-
const permissionRules = resolvePermissionRules(ctx.permissions, a.permissions);
|
|
820
822
|
if (externalRunner && permissionRules) {
|
|
821
823
|
throw new AsyncStartValidationError(`Agent '${a.name}' uses runner.type='${externalRunnerType}', which cannot enforce native Pi child permission rules.`);
|
|
822
824
|
}
|
|
@@ -839,7 +841,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
839
841
|
thinking: resolveEffectiveThinking(model, effectiveThinking),
|
|
840
842
|
launchResolvedExtensions,
|
|
841
843
|
modelCandidates: externalRunner ? undefined : buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider, {
|
|
842
|
-
scope:
|
|
844
|
+
scope: modelScopes,
|
|
843
845
|
primaryModelFromParent: inheritsParentModel(s.model, a.model, ctx.currentModel),
|
|
844
846
|
}).map((candidate) =>
|
|
845
847
|
applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined),
|
|
@@ -1418,6 +1420,7 @@ export function executeAsyncSingle(
|
|
|
1418
1420
|
? `[Read from: ${readPaths.join(", ")}]\n\n`
|
|
1419
1421
|
: "";
|
|
1420
1422
|
const taskText = readsInstruction + taskWithOutputInstruction;
|
|
1423
|
+
const modelScopes = resolveModelScopesForAgent(ctx.modelScope, agentConfig.name, ctx.currentModel);
|
|
1421
1424
|
const primaryModel = externalRunner ? undefined : params.modelOverrideFromParent
|
|
1422
1425
|
? params.modelOverride
|
|
1423
1426
|
: resolveSubagentModelOverride(
|
|
@@ -1425,6 +1428,7 @@ export function executeAsyncSingle(
|
|
|
1425
1428
|
ctx.currentModel,
|
|
1426
1429
|
availableModels,
|
|
1427
1430
|
ctx.currentModelProvider,
|
|
1431
|
+
{ scope: modelScopes },
|
|
1428
1432
|
);
|
|
1429
1433
|
const effectiveThinking = externalRunner ? undefined : params.thinkingOverride ?? agentConfig.thinking;
|
|
1430
1434
|
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
|
|
@@ -1453,7 +1457,7 @@ export function executeAsyncSingle(
|
|
|
1453
1457
|
const modelCandidates = externalRunner
|
|
1454
1458
|
? []
|
|
1455
1459
|
: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider, {
|
|
1456
|
-
scope:
|
|
1460
|
+
scope: modelScopes,
|
|
1457
1461
|
primaryModelFromParent: params.modelOverrideFromParent,
|
|
1458
1462
|
})
|
|
1459
1463
|
.flatMap((candidate) => {
|
|
@@ -1472,6 +1476,7 @@ export function executeAsyncSingle(
|
|
|
1472
1476
|
capabilityCeiling,
|
|
1473
1477
|
inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
|
|
1474
1478
|
agentName: agentConfig.name,
|
|
1479
|
+
permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
|
|
1475
1480
|
});
|
|
1476
1481
|
const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
|
|
1477
1482
|
const launchContractDigest = launchBindingDigest({
|
|
@@ -600,7 +600,7 @@ function runRetentionDiscovery(input: {
|
|
|
600
600
|
};
|
|
601
601
|
const onAbort = (): void => fail(new RetentionCancelledError("Retention discovery was cancelled."));
|
|
602
602
|
input.signal?.addEventListener("abort", onAbort, { once: true });
|
|
603
|
-
worker.once("error", (error) => fail(error));
|
|
603
|
+
worker.once("error", (error) => fail(error instanceof Error ? error : new Error(String(error))));
|
|
604
604
|
worker.once("exit", (code) => {
|
|
605
605
|
if (!settled) fail(new Error(`Retention discovery worker exited before replying (${code}).`));
|
|
606
606
|
});
|
|
@@ -21,6 +21,7 @@ export interface ImportedAsyncRootResult {
|
|
|
21
21
|
model?: string;
|
|
22
22
|
attemptedModels?: string[];
|
|
23
23
|
modelAttempts?: ModelAttempt[];
|
|
24
|
+
contextOverflow?: boolean;
|
|
24
25
|
totalCost?: CostSummary;
|
|
25
26
|
structuredOutput?: unknown;
|
|
26
27
|
structuredOutputPath?: string;
|
|
@@ -49,6 +50,7 @@ interface AsyncResultFile {
|
|
|
49
50
|
model?: string;
|
|
50
51
|
attemptedModels?: string[];
|
|
51
52
|
modelAttempts?: ModelAttempt[];
|
|
53
|
+
contextOverflow?: boolean;
|
|
52
54
|
totalCost?: CostSummary;
|
|
53
55
|
structuredOutput?: unknown;
|
|
54
56
|
structuredOutputPath?: string;
|
|
@@ -110,6 +112,7 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
|
|
|
110
112
|
...(step?.model ? { model: step.model } : {}),
|
|
111
113
|
...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
112
114
|
...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
|
|
115
|
+
...(step?.contextOverflow ? { contextOverflow: true } : {}),
|
|
113
116
|
...(step?.totalCost ? { totalCost: step.totalCost } : {}),
|
|
114
117
|
...(step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
|
|
115
118
|
...(step?.structuredOutputPath ? { structuredOutputPath: step.structuredOutputPath } : {}),
|
|
@@ -131,6 +134,7 @@ function outputFromTimeout(root: ImportedAsyncRoot, status: AsyncStatus | null,
|
|
|
131
134
|
...(step?.model ? { model: step.model } : {}),
|
|
132
135
|
...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
|
|
133
136
|
...(step?.modelAttempts ? { modelAttempts: step.modelAttempts } : {}),
|
|
137
|
+
...(step?.contextOverflow ? { contextOverflow: true } : {}),
|
|
134
138
|
...(step?.totalCost ? { totalCost: step.totalCost } : {}),
|
|
135
139
|
};
|
|
136
140
|
}
|
|
@@ -158,6 +162,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
|
|
|
158
162
|
...(child?.model ?? step?.model ? { model: child?.model ?? step?.model } : {}),
|
|
159
163
|
...(child?.attemptedModels ?? step?.attemptedModels ? { attemptedModels: child?.attemptedModels ?? step?.attemptedModels } : {}),
|
|
160
164
|
...(child?.modelAttempts ?? step?.modelAttempts ? { modelAttempts: child?.modelAttempts ?? step?.modelAttempts } : {}),
|
|
165
|
+
...(child?.contextOverflow || step?.contextOverflow ? { contextOverflow: true } : {}),
|
|
161
166
|
...(child?.totalCost ?? step?.totalCost ? { totalCost: child?.totalCost ?? step?.totalCost } : {}),
|
|
162
167
|
...(child?.structuredOutput !== undefined ? { structuredOutput: child.structuredOutput } : step?.structuredOutput !== undefined ? { structuredOutput: step.structuredOutput } : {}),
|
|
163
168
|
...(child?.structuredOutputPath ?? step?.structuredOutputPath ? { structuredOutputPath: child?.structuredOutputPath ?? step?.structuredOutputPath } : {}),
|
|
@@ -57,7 +57,7 @@ type ResultWatcherDeps = {
|
|
|
57
57
|
coalesceDelayMs?: number;
|
|
58
58
|
/** Returns true while a durable completion source needs periodic delivery checks. */
|
|
59
59
|
hasDeliveryDemand?: () => boolean;
|
|
60
|
-
/** Control how slow result-index scans are logged. Defaults to \"
|
|
60
|
+
/** Control how slow result-index scans are logged. Defaults to \"activity\". */
|
|
61
61
|
resultScanLogging?: "all" | "activity" | "off";
|
|
62
62
|
platform?: NodeJS.Platform;
|
|
63
63
|
};
|
|
@@ -606,12 +606,13 @@ export function createResultWatcher(
|
|
|
606
606
|
const logScanStats = (stats: ResultScanStats) => {
|
|
607
607
|
const elapsed = Date.now() - stats.startedAt;
|
|
608
608
|
if (elapsed < SLOW_RESULT_SCAN_MS) return;
|
|
609
|
-
|
|
609
|
+
const resultScanLogging = deps.resultScanLogging ?? "activity";
|
|
610
|
+
if (resultScanLogging === "off") return;
|
|
610
611
|
// A scan that inspected and scheduled nothing is a quiet no-op (e.g. the
|
|
611
612
|
// healthy periodic rescan while no async runs are pending). Under
|
|
612
613
|
// "activity", skip it so empty scans do not burn context tokens in the
|
|
613
614
|
// session transcript.
|
|
614
|
-
if (
|
|
615
|
+
if (resultScanLogging === "activity" && stats.files === 0 && stats.scheduled === 0) return;
|
|
615
616
|
console.error(`Subagent result scan inspected ${stats.files} indexed result file(s), scheduled ${stats.scheduled} in ${elapsed}ms (${resultsDir}).`);
|
|
616
617
|
};
|
|
617
618
|
const indexedResultCandidates = (observed: ReadonlySet<string>): string[] => {
|
|
@@ -97,6 +97,7 @@ interface ResultChildOutcome {
|
|
|
97
97
|
thinking?: string;
|
|
98
98
|
attemptedModels?: string[];
|
|
99
99
|
modelAttempts?: NonNullable<AsyncStatus["steps"]>[number]["modelAttempts"];
|
|
100
|
+
contextOverflow?: boolean;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
interface ResultRepairData {
|
|
@@ -163,6 +164,7 @@ function terminalStatusFromResult(status: AsyncStatus, resultPath: string, now:
|
|
|
163
164
|
thinking,
|
|
164
165
|
attemptedModels: child?.attemptedModels ?? step.attemptedModels,
|
|
165
166
|
modelAttempts: child?.modelAttempts ?? step.modelAttempts,
|
|
167
|
+
contextOverflow: child?.contextOverflow ?? step.contextOverflow,
|
|
166
168
|
};
|
|
167
169
|
});
|
|
168
170
|
const terminalStatus: AsyncStatus = {
|
|
@@ -258,6 +260,7 @@ function buildFailedRepair(status: AsyncStatus, asyncDir: string, now: number, r
|
|
|
258
260
|
model: step.model,
|
|
259
261
|
attemptedModels: step.attemptedModels,
|
|
260
262
|
modelAttempts: step.modelAttempts,
|
|
263
|
+
contextOverflow: step.contextOverflow,
|
|
261
264
|
sessionFile: step.sessionFile,
|
|
262
265
|
})),
|
|
263
266
|
exitCode: 1,
|