pi-subagents 0.38.0 → 0.40.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 +38 -0
- package/README.md +85 -13
- package/agents/worker.md +1 -0
- package/package.json +1 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +4 -3
- package/skills/pi-subagents/references/execution-controls.md +22 -3
- package/skills/pi-subagents/references/management-authoring-rpc.md +5 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +17 -3
- package/src/agents/agent-management.ts +71 -30
- package/src/agents/agent-serializer.ts +4 -0
- package/src/agents/agents.ts +60 -1
- package/src/api/preflight.ts +16 -12
- package/src/extension/chain-validation.ts +17 -1
- package/src/extension/rpc.ts +2 -0
- package/src/extension/schemas.ts +16 -3
- package/src/extension/tool-description.ts +6 -5
- package/src/intercom/result-intercom.ts +33 -4
- package/src/runs/background/async-execution.ts +63 -16
- package/src/runs/background/async-job-tracker.ts +5 -1
- package/src/runs/background/async-resume.ts +1 -0
- package/src/runs/background/async-status.ts +18 -2
- package/src/runs/background/chain-append.ts +48 -5
- package/src/runs/background/control-channel.ts +60 -0
- package/src/runs/background/notify.ts +26 -4
- package/src/runs/background/result-watcher.ts +20 -3
- package/src/runs/background/run-status.ts +29 -3
- package/src/runs/background/subagent-runner.ts +302 -18
- package/src/runs/foreground/async-stop-action.ts +65 -0
- package/src/runs/foreground/chain-execution.ts +143 -43
- package/src/runs/foreground/execution.ts +384 -202
- package/src/runs/foreground/foreground-control.ts +30 -0
- package/src/runs/foreground/subagent-executor.ts +314 -77
- package/src/runs/shared/capability-ceiling.ts +51 -19
- package/src/runs/shared/chain-outputs.ts +3 -1
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/nested-events.ts +97 -20
- package/src/runs/shared/parallel-utils.ts +43 -12
- package/src/runs/shared/pi-args.ts +45 -3
- package/src/runs/shared/process-signal.ts +19 -0
- package/src/runs/shared/run-history.ts +45 -9
- package/src/runs/shared/runtime-acknowledged-extensions.ts +71 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +30 -0
- package/src/runs/shared/usage-budget.ts +65 -0
- package/src/runs/shared/workflow-graph.ts +26 -1
- package/src/shared/settings.ts +17 -1
- package/src/shared/types.ts +124 -8
- package/src/shared/utils.ts +12 -1
- package/src/slash/slash-commands.ts +43 -1
- package/src/slash/slash-live-state.ts +5 -3
- package/src/tui/fleet-status.ts +12 -2
- package/src/tui/fleet.ts +181 -8
- package/src/tui/render.ts +4 -2
- package/src/watchdog/register-main.ts +14 -7
- package/src/watchdog/review.ts +4 -3
- package/src/watchdog/runtime.ts +170 -17
- package/src/watchdog/scope.ts +62 -0
- package/src/watchdog/settings.ts +41 -1
- package/src/watchdog/types.ts +10 -0
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
mergeBuiltinAgentOverride,
|
|
20
20
|
removeBuiltinAgentOverride,
|
|
21
21
|
removeBuiltinAgentOverrideFields,
|
|
22
|
+
resolveAgentName,
|
|
22
23
|
} from "./agents.ts";
|
|
23
24
|
import { serializeAgent } from "./agent-serializer.ts";
|
|
24
25
|
import { mergeAgentsForScope } from "./agent-selection.ts";
|
|
@@ -35,10 +36,11 @@ import { resolveTurnBudgetConfig } from "../runs/shared/turn-budget.ts";
|
|
|
35
36
|
import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
|
|
36
37
|
import type { AcceptanceInput, Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
|
|
37
38
|
import { getProjectConfigDir } from "../shared/utils.ts";
|
|
39
|
+
import { capabilityCeilingAgentRestrictionSources, isAgentAllowedByCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
|
|
38
40
|
|
|
39
41
|
type ManagementAction = "list" | "get" | "models" | "create" | "update" | "delete" | "eject" | "disable" | "enable" | "reset";
|
|
40
42
|
type ManagementScope = "user" | "project";
|
|
41
|
-
type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig };
|
|
43
|
+
type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig; currentSessionId?: string };
|
|
42
44
|
|
|
43
45
|
interface ManagementParams {
|
|
44
46
|
action?: string;
|
|
@@ -113,8 +115,13 @@ function findAgents(name: string, cwd: string, scope: AgentScope = "both"): Agen
|
|
|
113
115
|
const d = discoverAgentsAll(cwd);
|
|
114
116
|
const raw = name.trim();
|
|
115
117
|
const sanitized = sanitizeName(raw);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
const scoped = mergeAgentsForScope(scope, d.user, d.project, d.builtin, d.package);
|
|
119
|
+
let resolved = resolveAgentName(raw, scoped);
|
|
120
|
+
if (!resolved.agent && !resolved.error && sanitized !== raw) resolved = resolveAgentName(sanitized, scoped);
|
|
121
|
+
if (resolved.agent) return scoped.filter((agent) => agent.name === resolved.agent!.name).sort((a, b) => a.source.localeCompare(b.source));
|
|
122
|
+
return scoped
|
|
123
|
+
.filter((agent) => Boolean(resolveAgentName(raw, [agent]).agent)
|
|
124
|
+
|| (sanitized !== raw && Boolean(resolveAgentName(sanitized, [agent]).agent)))
|
|
118
125
|
.sort((a, b) => a.source.localeCompare(b.source));
|
|
119
126
|
}
|
|
120
127
|
|
|
@@ -128,15 +135,20 @@ function findChains(name: string, cwd: string, scope: AgentScope = "both"): Chai
|
|
|
128
135
|
|
|
129
136
|
const AGENT_SOURCE_PRECEDENCE: Record<AgentSource, number> = { builtin: 0, package: 1, user: 2, project: 3 };
|
|
130
137
|
|
|
131
|
-
// Returns the highest-precedence
|
|
132
|
-
// matching mergeAgentsForScope for "both"
|
|
133
|
-
|
|
134
|
-
function pickEffectiveAgent(d: ReturnType<typeof discoverAgentsAll>, name: string): AgentConfig | undefined {
|
|
138
|
+
// Returns the highest-precedence definition for a resolved canonical name (project > user > package > builtin),
|
|
139
|
+
// matching mergeAgentsForScope for "both", including disabled agents so disable/enable can locate hidden targets.
|
|
140
|
+
function resolveEffectiveAgent(d: ReturnType<typeof discoverAgentsAll>, name: string): { agent?: AgentConfig; error?: string } {
|
|
135
141
|
const raw = name.trim();
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
142
|
+
const candidates = allAgents(d);
|
|
143
|
+
let resolved = resolveAgentName(raw, candidates);
|
|
144
|
+
if (!resolved.agent && !resolved.error) {
|
|
145
|
+
const sanitized = sanitizeName(raw);
|
|
146
|
+
if (sanitized !== raw) resolved = resolveAgentName(sanitized, candidates);
|
|
147
|
+
}
|
|
148
|
+
if (resolved.error) return { error: resolved.error };
|
|
149
|
+
if (!resolved.agent) return {};
|
|
150
|
+
const matches = candidates.filter((agent) => agent.name === resolved.agent!.name);
|
|
151
|
+
return { agent: matches.reduce((best, agent) => (AGENT_SOURCE_PRECEDENCE[agent.source] > AGENT_SOURCE_PRECEDENCE[best.source] ? agent : best)) };
|
|
140
152
|
}
|
|
141
153
|
|
|
142
154
|
function nameExistsInScope(cwd: string, scope: ManagementScope, name: string, excludePath?: string): boolean {
|
|
@@ -156,8 +168,9 @@ function isMutableSource(source: AgentSource): source is ManagementScope {
|
|
|
156
168
|
|
|
157
169
|
function unknownChainAgents(cwd: string, steps: ChainStepConfig[]): string[] {
|
|
158
170
|
const d = discoverAgentsAll(cwd);
|
|
159
|
-
const
|
|
160
|
-
return [...new Set(steps.map((s) => s.agent).filter((
|
|
171
|
+
const agents = allAgents(d);
|
|
172
|
+
return [...new Set(steps.map((s) => s.agent).filter((agentName): agentName is string => typeof agentName === "string" && !resolveAgentName(agentName, agents).agent))]
|
|
173
|
+
.sort((a, b) => a.localeCompare(b));
|
|
161
174
|
}
|
|
162
175
|
|
|
163
176
|
function chainStepWarnings(ctx: ManagementContext, steps: ChainStepConfig[]): string[] {
|
|
@@ -251,6 +264,7 @@ export function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<
|
|
|
251
264
|
if (hasKey(cfg, "name")) changed("name");
|
|
252
265
|
if (hasKey(cfg, "package")) changed("package");
|
|
253
266
|
if (hasKey(cfg, "description")) changed("description");
|
|
267
|
+
if (hasKey(cfg, "aliases")) changed("alias", "aliases");
|
|
254
268
|
if (hasKey(cfg, "systemPrompt")) changed("systemPrompt");
|
|
255
269
|
if (hasKey(cfg, "model")) changed("model");
|
|
256
270
|
if (hasKey(cfg, "fallbackModels")) changed("fallbackModels");
|
|
@@ -370,6 +384,16 @@ function parseTools(raw: string): { tools?: string[]; mcpDirectTools?: string[]
|
|
|
370
384
|
}
|
|
371
385
|
|
|
372
386
|
function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): string | undefined {
|
|
387
|
+
if (hasKey(cfg, "aliases")) {
|
|
388
|
+
if (cfg.aliases === false || cfg.aliases === "") target.aliases = undefined;
|
|
389
|
+
else if (typeof cfg.aliases === "string") {
|
|
390
|
+
const aliases = parseCsv(cfg.aliases).filter((alias) => alias !== target.name);
|
|
391
|
+
target.aliases = aliases.length ? aliases : undefined;
|
|
392
|
+
} else if (Array.isArray(cfg.aliases) && cfg.aliases.every((entry) => typeof entry === "string")) {
|
|
393
|
+
const aliases = [...new Set(cfg.aliases.map((entry) => entry.trim()).filter(Boolean).filter((alias) => alias !== target.name))];
|
|
394
|
+
target.aliases = aliases.length ? aliases : undefined;
|
|
395
|
+
} else return "config.aliases must be a comma-separated string, string array, or false when provided.";
|
|
396
|
+
}
|
|
373
397
|
if (hasKey(cfg, "systemPrompt")) {
|
|
374
398
|
if (cfg.systemPrompt === false || cfg.systemPrompt === "") target.systemPrompt = "";
|
|
375
399
|
else if (typeof cfg.systemPrompt === "string") target.systemPrompt = cfg.systemPrompt;
|
|
@@ -513,13 +537,17 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
|
|
|
513
537
|
return undefined;
|
|
514
538
|
}
|
|
515
539
|
|
|
516
|
-
function resolveTarget<T extends { source: AgentSource; filePath: string }>(
|
|
540
|
+
function resolveTarget<T extends { name: string; source: AgentSource; filePath: string }>(
|
|
517
541
|
kind: "agent" | "chain",
|
|
518
542
|
name: string,
|
|
519
543
|
matches: T[],
|
|
520
544
|
cwd: string,
|
|
521
545
|
scopeHint?: string,
|
|
522
546
|
): T | AgentToolResult<Details> {
|
|
547
|
+
const distinctNames = [...new Set(matches.map((m) => m.name))];
|
|
548
|
+
if (distinctNames.length > 1) {
|
|
549
|
+
return result(`Ambiguous ${kind} alias or name '${name}': ${distinctNames.sort((a, b) => a.localeCompare(b)).join(", ")}`, true);
|
|
550
|
+
}
|
|
523
551
|
const mutable = matches.filter((m): m is T & { source: ManagementScope } => isMutableSource(m.source));
|
|
524
552
|
if (mutable.length === 0) {
|
|
525
553
|
if (matches.length > 0) {
|
|
@@ -564,6 +592,7 @@ function formatAgentDetail(agent: AgentConfig): string {
|
|
|
564
592
|
lines.push(`Local name: ${frontmatterNameForConfig(agent)}`);
|
|
565
593
|
lines.push(`Package: ${agent.packageName}`);
|
|
566
594
|
}
|
|
595
|
+
if (agent.aliases?.length) lines.push(`Aliases: ${agent.aliases.join(", ")}`);
|
|
567
596
|
if (agent.model) lines.push(`Model: ${agent.model}`);
|
|
568
597
|
if (agent.fallbackModels?.length) lines.push(`Fallback models: ${agent.fallbackModels.join(", ")}`);
|
|
569
598
|
if (tools.length) lines.push(`Tools: ${tools.join(", ")}`);
|
|
@@ -648,7 +677,11 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
|
|
|
648
677
|
const d = discoverAgentsAll(ctx.cwd);
|
|
649
678
|
const scopedAgents = mergeAgentsForScope(scope, d.user, d.project, d.builtin, d.package)
|
|
650
679
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
651
|
-
const
|
|
680
|
+
const capabilityCeiling = resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId);
|
|
681
|
+
const visibleAgents = scopedAgents.filter((a) => !a.disabled);
|
|
682
|
+
const agents = visibleAgents.filter((a) => isAgentAllowedByCapabilityCeiling(a.name, capabilityCeiling));
|
|
683
|
+
const restrictedAgents = visibleAgents.filter((a) => !isAgentAllowedByCapabilityCeiling(a.name, capabilityCeiling));
|
|
684
|
+
const restrictedSources = capabilityCeilingAgentRestrictionSources(capabilityCeiling);
|
|
652
685
|
const chains = d.chains.filter((c) => scope === "both" || c.source === "package" || c.source === scope).sort((a, b) => a.name.localeCompare(b.name));
|
|
653
686
|
const diagnostics = d.chainDiagnostics.filter((entry) => scope === "both" || entry.source === scope);
|
|
654
687
|
const proactiveSuggestions = buildProactiveSkillSubagentRecommendationLines({
|
|
@@ -660,8 +693,13 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
|
|
|
660
693
|
const lines = [
|
|
661
694
|
"Executable agents:",
|
|
662
695
|
...(agents.length
|
|
663
|
-
? agents.map((a) => `- ${a.name} (${a.source}${a.defaultContext ? `, context: ${a.defaultContext}` : ""}): ${a.description}`)
|
|
696
|
+
? agents.map((a) => `- ${a.name} (${a.source}${a.defaultContext ? `, context: ${a.defaultContext}` : ""}${a.aliases?.length ? `, aliases: ${a.aliases.join(", ")}` : ""}): ${a.description}`)
|
|
664
697
|
: ["- (none)"]),
|
|
698
|
+
...(restrictedAgents.length ? [
|
|
699
|
+
"",
|
|
700
|
+
`Restricted agents (not executable in this session${restrictedSources?.length ? `; capability ceiling: ${restrictedSources.join(", ")}` : ""}):`,
|
|
701
|
+
...restrictedAgents.map((a) => `- ${a.name} (${a.source}${a.aliases?.length ? `, aliases: ${a.aliases.join(", ")}` : ""}): ${a.description}`),
|
|
702
|
+
] : []),
|
|
665
703
|
"",
|
|
666
704
|
"Chains:",
|
|
667
705
|
...(chains.length ? chains.map((c) => `- ${c.name} (${c.source}): ${c.description}`) : ["- (none)"]),
|
|
@@ -760,12 +798,13 @@ function handleGet(params: ManagementParams, ctx: ManagementContext): AgentToolR
|
|
|
760
798
|
const blocks: string[] = [];
|
|
761
799
|
let anyFound = false;
|
|
762
800
|
if (params.agent) {
|
|
763
|
-
const
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
801
|
+
const matches = findAgents(params.agent, ctx.cwd, scope);
|
|
802
|
+
const distinctNames = [...new Set(matches.map((agent) => agent.name))];
|
|
803
|
+
if (distinctNames.length > 1) {
|
|
804
|
+
const msg = `Ambiguous agent alias or name '${params.agent}': ${distinctNames.sort((a, b) => a.localeCompare(b)).join(", ")}`;
|
|
805
|
+
if (!hasBoth) return result(msg, true);
|
|
806
|
+
blocks.push(msg);
|
|
807
|
+
} else if (!matches.length) {
|
|
769
808
|
const msg = `Agent '${params.agent}' not found. Available: ${availableNames(ctx.cwd, "agent").join(", ") || "none"}.`;
|
|
770
809
|
if (!hasBoth) return result(msg, true);
|
|
771
810
|
blocks.push(msg);
|
|
@@ -1027,13 +1066,14 @@ function handleDisable(params: ManagementParams, ctx: ManagementContext): AgentT
|
|
|
1027
1066
|
if (scope === "project" && d.projectSettingsPath === null) {
|
|
1028
1067
|
return result("Project override is not available here: no project config root (.pi or .agents) was found above the cwd. Use agentScope: 'user' or run from inside a project.", true);
|
|
1029
1068
|
}
|
|
1030
|
-
const effective =
|
|
1031
|
-
if (
|
|
1069
|
+
const effective = resolveEffectiveAgent(d, raw);
|
|
1070
|
+
if (effective.error) return result(effective.error, true);
|
|
1071
|
+
if (!effective.agent) {
|
|
1032
1072
|
return result(`Agent '${raw}' not found. Available: ${availableNames(ctx.cwd, "agent").join(", ") || "none"}.`, true);
|
|
1033
1073
|
}
|
|
1034
|
-
const runtimeName = effective.name;
|
|
1074
|
+
const runtimeName = effective.agent.name;
|
|
1035
1075
|
const settingsPath = mergeBuiltinAgentOverride(ctx.cwd, runtimeName, scope, { disabled: true });
|
|
1036
|
-
const after =
|
|
1076
|
+
const after = resolveEffectiveAgent(discoverAgentsAll(ctx.cwd), raw).agent;
|
|
1037
1077
|
if (after?.disabled === true) {
|
|
1038
1078
|
return result(`Disabled agent '${runtimeName}' via ${scope} settings override at ${settingsPath}. It is now hidden from runtime discovery and { action: "list" }.`);
|
|
1039
1079
|
}
|
|
@@ -1050,13 +1090,14 @@ function handleEnable(params: ManagementParams, ctx: ManagementContext): AgentTo
|
|
|
1050
1090
|
if (scope === "project" && d.projectSettingsPath === null) {
|
|
1051
1091
|
return result("Project override is not available here: no project config root (.pi or .agents) was found above the cwd. Use agentScope: 'user' or run from inside a project.", true);
|
|
1052
1092
|
}
|
|
1053
|
-
const effective =
|
|
1054
|
-
if (
|
|
1093
|
+
const effective = resolveEffectiveAgent(d, raw);
|
|
1094
|
+
if (effective.error) return result(effective.error, true);
|
|
1095
|
+
if (!effective.agent) {
|
|
1055
1096
|
return result(`Agent '${raw}' not found. Available: ${availableNames(ctx.cwd, "agent").join(", ") || "none"}.`, true);
|
|
1056
1097
|
}
|
|
1057
|
-
const runtimeName = effective.name;
|
|
1098
|
+
const runtimeName = effective.agent.name;
|
|
1058
1099
|
const { path: settingsPath, removed } = removeBuiltinAgentOverrideFields(ctx.cwd, runtimeName, scope, ["disabled"]);
|
|
1059
|
-
const after =
|
|
1100
|
+
const after = resolveEffectiveAgent(discoverAgentsAll(ctx.cwd), raw).agent;
|
|
1060
1101
|
if (after && after.disabled !== true) {
|
|
1061
1102
|
if (removed) return result(`Enabled agent '${runtimeName}' (removed disabled override at ${settingsPath}).`);
|
|
1062
1103
|
return result(`Agent '${runtimeName}' is already enabled.`);
|
|
@@ -5,6 +5,8 @@ export const KNOWN_FIELDS = new Set([
|
|
|
5
5
|
"name",
|
|
6
6
|
"package",
|
|
7
7
|
"description",
|
|
8
|
+
"alias",
|
|
9
|
+
"aliases",
|
|
8
10
|
"tools",
|
|
9
11
|
"model",
|
|
10
12
|
"fallbackModels",
|
|
@@ -50,6 +52,8 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
|
|
|
50
52
|
lines.push(`name: ${frontmatterNameForConfig(config)}`);
|
|
51
53
|
if (config.packageName) lines.push(`package: ${config.packageName}`);
|
|
52
54
|
lines.push(`description: ${config.description}`);
|
|
55
|
+
const aliasesValue = joinComma(config.aliases);
|
|
56
|
+
if (aliasesValue || preserve("alias", "aliases")) lines.push(`aliases: ${aliasesValue ?? ""}`);
|
|
53
57
|
|
|
54
58
|
const tools = [
|
|
55
59
|
...(config.tools ?? []),
|
package/src/agents/agents.ts
CHANGED
|
@@ -59,6 +59,7 @@ export function defaultInheritSkills(): boolean {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export interface BuiltinAgentOverrideBase {
|
|
62
|
+
description?: string;
|
|
62
63
|
model?: string;
|
|
63
64
|
fallbackModels?: string[];
|
|
64
65
|
thinking?: string | false;
|
|
@@ -80,6 +81,7 @@ export interface BuiltinAgentOverrideBase {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
interface BuiltinAgentOverrideConfig {
|
|
84
|
+
description?: string;
|
|
83
85
|
model?: string | false;
|
|
84
86
|
fallbackModels?: string[] | false;
|
|
85
87
|
thinking?: string | false;
|
|
@@ -116,6 +118,7 @@ export interface AgentConfig {
|
|
|
116
118
|
localName?: string;
|
|
117
119
|
packageName?: string;
|
|
118
120
|
description: string;
|
|
121
|
+
aliases?: string[];
|
|
119
122
|
tools?: string[];
|
|
120
123
|
mcpDirectTools?: string[];
|
|
121
124
|
model?: string;
|
|
@@ -473,6 +476,41 @@ function collectPackageSubagentPaths(cwd: string, options: { includeUser: boolea
|
|
|
473
476
|
return { agents, chains };
|
|
474
477
|
}
|
|
475
478
|
|
|
479
|
+
function normalizeAgentAliases(rawAliases: string[] | undefined, agentName: string): string[] | undefined {
|
|
480
|
+
const aliases = [...new Set((rawAliases ?? []).map((alias) => alias.trim()).filter(Boolean))]
|
|
481
|
+
.filter((alias) => alias !== agentName);
|
|
482
|
+
return aliases.length > 0 ? aliases : undefined;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function effectiveAgentMatch(matches: AgentConfig[]): { agent?: AgentConfig; error?: string } {
|
|
486
|
+
const distinctNames = [...new Set(matches.map((agent) => agent.name))];
|
|
487
|
+
if (distinctNames.length === 1) {
|
|
488
|
+
const sourceRank = new Map<AgentConfig["source"], number>([["builtin", 0], ["package", 1], ["user", 2], ["project", 3]]);
|
|
489
|
+
return { agent: [...matches].sort((a, b) => (sourceRank.get(b.source) ?? 0) - (sourceRank.get(a.source) ?? 0))[0] };
|
|
490
|
+
}
|
|
491
|
+
return {};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function resolveAgentName(name: string, agents: AgentConfig[]): { agent?: AgentConfig; error?: string } {
|
|
495
|
+
const raw = name.trim();
|
|
496
|
+
const exact = agents.filter((agent) => agent.name === raw || agent.localName === raw);
|
|
497
|
+
if (exact.length === 1) return { agent: exact[0] };
|
|
498
|
+
if (exact.length > 1) {
|
|
499
|
+
const effective = effectiveAgentMatch(exact);
|
|
500
|
+
if (effective.agent) return effective;
|
|
501
|
+
return { error: `Ambiguous agent name '${name}': ${exact.map((agent) => agent.name).join(", ")}` };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const aliases = agents.filter((agent) => agent.aliases?.includes(raw));
|
|
505
|
+
if (aliases.length === 1) return { agent: aliases[0] };
|
|
506
|
+
if (aliases.length > 1) {
|
|
507
|
+
const effective = effectiveAgentMatch(aliases);
|
|
508
|
+
if (effective.agent) return effective;
|
|
509
|
+
return { error: `Ambiguous agent alias '${name}': ${aliases.map((agent) => agent.name).join(", ")}` };
|
|
510
|
+
}
|
|
511
|
+
return {};
|
|
512
|
+
}
|
|
513
|
+
|
|
476
514
|
function splitToolList(rawTools: string[] | undefined): { tools?: string[]; mcpDirectTools?: string[] } {
|
|
477
515
|
const mcpDirectTools: string[] = [];
|
|
478
516
|
const tools: string[] = [];
|
|
@@ -509,6 +547,7 @@ function arraysEqual(a: string[] | undefined, b: string[] | undefined): boolean
|
|
|
509
547
|
|
|
510
548
|
function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
511
549
|
return {
|
|
550
|
+
description: agent.description,
|
|
512
551
|
model: agent.model,
|
|
513
552
|
fallbackModels: agent.fallbackModels ? [...agent.fallbackModels] : undefined,
|
|
514
553
|
thinking: agent.thinking,
|
|
@@ -532,6 +571,7 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
|
532
571
|
|
|
533
572
|
function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentOverrideConfig {
|
|
534
573
|
return {
|
|
574
|
+
...(override.description !== undefined ? { description: override.description } : {}),
|
|
535
575
|
...(override.model !== undefined ? { model: override.model } : {}),
|
|
536
576
|
...(override.fallbackModels !== undefined
|
|
537
577
|
? { fallbackModels: override.fallbackModels === false ? false : [...override.fallbackModels] }
|
|
@@ -684,6 +724,14 @@ function parseBuiltinOverrideEntry(
|
|
|
684
724
|
const input = value as Record<string, unknown>;
|
|
685
725
|
const override: BuiltinAgentOverrideConfig = {};
|
|
686
726
|
|
|
727
|
+
if ("description" in input) {
|
|
728
|
+
if (typeof input.description === "string" && input.description.trim()) {
|
|
729
|
+
override.description = input.description.trim();
|
|
730
|
+
} else {
|
|
731
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'description'; expected a non-empty string.`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
687
735
|
if ("model" in input) {
|
|
688
736
|
if (typeof input.model === "string" || input.model === false) override.model = input.model;
|
|
689
737
|
else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'model'; expected a string or false.`);
|
|
@@ -931,6 +979,7 @@ function applyBuiltinOverride(
|
|
|
931
979
|
override: { ...meta, base: cloneOverrideBase(agent) },
|
|
932
980
|
};
|
|
933
981
|
|
|
982
|
+
if (override.description !== undefined) next.description = override.description;
|
|
934
983
|
if (override.model !== undefined) next.model = override.model === false ? undefined : override.model;
|
|
935
984
|
if (override.fallbackModels !== undefined) {
|
|
936
985
|
next.fallbackModels = override.fallbackModels === false ? undefined : [...override.fallbackModels];
|
|
@@ -1051,6 +1100,10 @@ function applyCustomAgentOverride(
|
|
|
1051
1100
|
anyFilled = true;
|
|
1052
1101
|
};
|
|
1053
1102
|
|
|
1103
|
+
if (override.description !== undefined) {
|
|
1104
|
+
mutable().description = override.description;
|
|
1105
|
+
anyFilled = true;
|
|
1106
|
+
}
|
|
1054
1107
|
if (override.model !== undefined) {
|
|
1055
1108
|
fill("model", ["model"], override.model === false ? undefined : override.model);
|
|
1056
1109
|
}
|
|
@@ -1141,10 +1194,14 @@ function applyCustomAgentOverrides(
|
|
|
1141
1194
|
|
|
1142
1195
|
export function buildBuiltinOverrideConfig(
|
|
1143
1196
|
base: BuiltinAgentOverrideBase,
|
|
1144
|
-
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "completionGuard" | "toolBudget"
|
|
1197
|
+
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "completionGuard" | "toolBudget"> & Partial<Pick<AgentConfig, "description">>,
|
|
1145
1198
|
): BuiltinAgentOverrideConfig | undefined {
|
|
1146
1199
|
const override: BuiltinAgentOverrideConfig = {};
|
|
1147
1200
|
|
|
1201
|
+
if (draft.description !== undefined) {
|
|
1202
|
+
const description = draft.description.trim();
|
|
1203
|
+
if (description && description !== base.description) override.description = description;
|
|
1204
|
+
}
|
|
1148
1205
|
if (draft.model !== base.model) override.model = draft.model ?? false;
|
|
1149
1206
|
if (!arraysEqual(draft.fallbackModels, base.fallbackModels)) override.fallbackModels = draft.fallbackModels ? [...draft.fallbackModels] : false;
|
|
1150
1207
|
if (draft.thinking !== base.thinking) override.thinking = draft.thinking ?? false;
|
|
@@ -1385,6 +1442,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
1385
1442
|
const tools = parsedTools.tools ?? [];
|
|
1386
1443
|
const mcpDirectTools = parsedTools.mcpDirectTools ?? [];
|
|
1387
1444
|
const defaultReads = parseFrontmatterList(frontmatter.defaultReads);
|
|
1445
|
+
const aliases = normalizeAgentAliases(parseFrontmatterList(frontmatter.aliases ?? frontmatter.alias), runtimeName);
|
|
1388
1446
|
const skillStr = frontmatter.skill || frontmatter.skills;
|
|
1389
1447
|
const skills = parseFrontmatterList(skillStr);
|
|
1390
1448
|
const skillPath = parseFrontmatterList(frontmatter.skillPath);
|
|
@@ -1465,6 +1523,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
1465
1523
|
localName,
|
|
1466
1524
|
packageName,
|
|
1467
1525
|
description: frontmatter.description,
|
|
1526
|
+
aliases,
|
|
1468
1527
|
tools: rawTools !== undefined ? tools : undefined,
|
|
1469
1528
|
mcpDirectTools: mcpDirectTools.length > 0 ? mcpDirectTools : undefined,
|
|
1470
1529
|
model: frontmatter.model,
|
package/src/api/preflight.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { discoverAgents, discoverAgentsAll, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
|
|
5
|
+
import { discoverAgents, discoverAgentsAll, resolveAgentName, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
|
|
6
6
|
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";
|
|
@@ -12,7 +12,7 @@ import { injectOutputPathSystemPrompt, normalizeSingleOutputOverride, resolveSin
|
|
|
12
12
|
import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
|
|
13
13
|
import { resolveEffectiveThinking } from "../shared/model-info.ts";
|
|
14
14
|
import { SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, type ArtifactDirPreference, type ArtifactPaths, type JsonSchemaObject, type OutputMode } from "../shared/types.ts";
|
|
15
|
-
import type
|
|
15
|
+
import { capabilityCeilingAgentRestrictionMessage, intersectSubagentCapabilityCeilings, type ResolvedSubagentCapabilityCeiling, type SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
|
|
16
16
|
import { appendTurnBudgetSystemPrompt } from "../runs/shared/turn-budget.ts";
|
|
17
17
|
import type { ResolvedTurnBudget } from "../shared/types.ts";
|
|
18
18
|
import type { ResolvedMcpDirectToolSelection } from "../runs/shared/mcp-direct-tool-allowlist.ts";
|
|
@@ -31,7 +31,8 @@ export type SubagentLaunchContractReasonCode =
|
|
|
31
31
|
| "denied_required_tool"
|
|
32
32
|
| "invalid_artifact_dir"
|
|
33
33
|
| "invalid_cwd"
|
|
34
|
-
| "unsupported_mode"
|
|
34
|
+
| "unsupported_mode"
|
|
35
|
+
| "restricted_agent";
|
|
35
36
|
|
|
36
37
|
export interface SubagentLaunchContractDiagnostic {
|
|
37
38
|
code: SubagentLaunchContractReasonCode | "host_required" | "snapshot_warning";
|
|
@@ -191,7 +192,7 @@ function normalizeAvailableModels(models: SubagentLaunchContractInput["available
|
|
|
191
192
|
function candidateList(inputAgent: string, selected: AgentConfig | undefined, cwd: string): SubagentLaunchContractAgentCandidate[] {
|
|
192
193
|
const all = discoverAgentsAll(cwd);
|
|
193
194
|
return [...all.builtin, ...all.package, ...all.user, ...all.project]
|
|
194
|
-
.filter((agent) =>
|
|
195
|
+
.filter((agent) => Boolean(resolveAgentName(inputAgent, [agent]).agent))
|
|
195
196
|
.map((agent) => ({
|
|
196
197
|
name: agent.name,
|
|
197
198
|
...(agent.localName ? { localName: agent.localName } : {}),
|
|
@@ -225,14 +226,17 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
225
226
|
}
|
|
226
227
|
const scope = resolveExecutionAgentScope(input.agentScope);
|
|
227
228
|
const discovered = discoverAgents(effectiveCwd, scope);
|
|
228
|
-
const
|
|
229
|
-
if (
|
|
230
|
-
return { ok: false, code: "
|
|
229
|
+
const resolvedAgent = resolveAgentName(input.agent, discovered.agents);
|
|
230
|
+
if (resolvedAgent.error) {
|
|
231
|
+
return { ok: false, code: "ambiguous_agent", message: resolvedAgent.error, diagnostics };
|
|
231
232
|
}
|
|
232
|
-
if (
|
|
233
|
-
return { ok: false, code: "
|
|
233
|
+
if (!resolvedAgent.agent) {
|
|
234
|
+
return { ok: false, code: "missing_agent", message: `Unknown agent: ${input.agent}`, diagnostics };
|
|
234
235
|
}
|
|
235
|
-
const agent =
|
|
236
|
+
const agent = resolvedAgent.agent;
|
|
237
|
+
const effectiveCapabilityCeiling = intersectSubagentCapabilityCeilings(input.capabilityCeiling, input.inheritedCapabilityCeiling);
|
|
238
|
+
const restrictionMessage = capabilityCeilingAgentRestrictionMessage(agent.name, effectiveCapabilityCeiling);
|
|
239
|
+
if (restrictionMessage) return { ok: false, code: "restricted_agent", message: restrictionMessage, diagnostics };
|
|
236
240
|
const runId = input.runId ?? "preflight";
|
|
237
241
|
const skillInput = normalizeSkillInput(input.skill);
|
|
238
242
|
const outputOverride = normalizeSingleOutputOverride(input.output, agent.output);
|
|
@@ -272,8 +276,8 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
272
276
|
cwd: effectiveCwd,
|
|
273
277
|
requireReadTool: resolvedSkills.resolved.length > 0,
|
|
274
278
|
structuredOutput: Boolean(input.outputSchema),
|
|
275
|
-
capabilityCeiling:
|
|
276
|
-
|
|
279
|
+
capabilityCeiling: effectiveCapabilityCeiling,
|
|
280
|
+
agentName: agent.name,
|
|
277
281
|
});
|
|
278
282
|
} catch (error) {
|
|
279
283
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -43,7 +43,7 @@ export const EXPAND_KEYS = allowedKeysOf(DynamicExpandSchema);
|
|
|
43
43
|
export const EXPAND_FROM_KEYS = allowedKeysOf(ExpandFromSchema);
|
|
44
44
|
export const COLLECT_KEYS = allowedKeysOf(DynamicCollectSchema);
|
|
45
45
|
|
|
46
|
-
const CHAIN_STEP_EXAMPLE = '{"agent": "worker", "task": "do X"}';
|
|
46
|
+
const CHAIN_STEP_EXAMPLE = '{"agent": "worker", "task": "do X"} or {"checkpoint": "review", "message": "Approve before implementation?"}';
|
|
47
47
|
const PARALLEL_TASK_EXAMPLE = '{"agent": "worker", "task": "do X"}';
|
|
48
48
|
const DYNAMIC_TEMPLATE_EXAMPLE = '{"agent": "worker", "task": "Review {item.path}"}';
|
|
49
49
|
const EXPAND_EXAMPLE = '{"from": {"output": "targets", "path": "/items"}, "maxItems": 4}';
|
|
@@ -58,6 +58,21 @@ function disallowedKeys(value: Record<string, unknown>, allowed: readonly string
|
|
|
58
58
|
return Object.keys(value).filter((key) => !allowed.includes(key));
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function validateCheckpointShape(step: Record<string, unknown>, path: string): void {
|
|
62
|
+
if (step.checkpoint === undefined) return;
|
|
63
|
+
const allowed = new Set(["checkpoint", "message", "phase", "label"]);
|
|
64
|
+
const mixed = Object.keys(step).filter((key) => !allowed.has(key));
|
|
65
|
+
if (mixed.length > 0) {
|
|
66
|
+
throw new Error(`subagent ${path}: checkpoint steps cannot be mixed with child-launch properties ${quoteList(mixed)}.\n\nExample: {"checkpoint": "review", "message": "Approve before implementation?"}`);
|
|
67
|
+
}
|
|
68
|
+
if (typeof step.checkpoint !== "string" || !step.checkpoint.trim()) {
|
|
69
|
+
throw new Error(`subagent ${path}.checkpoint: expected a non-empty string.`);
|
|
70
|
+
}
|
|
71
|
+
if (step.message !== undefined && typeof step.message !== "string") {
|
|
72
|
+
throw new Error(`subagent ${path}.message: expected a string.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
61
76
|
function typeName(value: unknown): string {
|
|
62
77
|
if (Array.isArray(value)) return "array";
|
|
63
78
|
return typeof value;
|
|
@@ -120,6 +135,7 @@ export function validateChainInput(args: unknown): void {
|
|
|
120
135
|
const stepPath = `chain step validation failed at chain[${index}]`;
|
|
121
136
|
expectObject(step, stepPath, CHAIN_STEP_KEYS, CHAIN_STEP_EXAMPLE);
|
|
122
137
|
checkNoExtraKeys(step, stepPath, ChainItem, CHAIN_STEP_KEYS, CHAIN_STEP_EXAMPLE);
|
|
138
|
+
validateCheckpointShape(step, stepPath);
|
|
123
139
|
|
|
124
140
|
if (step.expand !== undefined) {
|
|
125
141
|
const expandPath = `${stepPath}.expand`;
|
package/src/extension/rpc.ts
CHANGED
|
@@ -386,6 +386,8 @@ function pingData(ctx: ExtensionContext | null) {
|
|
|
386
386
|
interrupt: true,
|
|
387
387
|
stop: true,
|
|
388
388
|
resume: true,
|
|
389
|
+
launchResolvedExtensions: { version: 1, source: "launch-resolved" },
|
|
390
|
+
runtimeAcknowledgedExtensions: { version: 1, source: "child-runtime", event: "subagent:acknowledge-extension" },
|
|
389
391
|
processTerminalProof: { version: 1, lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION },
|
|
390
392
|
},
|
|
391
393
|
events: {
|
package/src/extension/schemas.ts
CHANGED
|
@@ -119,6 +119,16 @@ const ToolBudgetOverride = Type.Object({
|
|
|
119
119
|
block: Type.Optional(ToolBudgetBlock),
|
|
120
120
|
}, { additionalProperties: false, description: "Optional child tool-call budget. soft nudges the child; after hard, block tools (default read/grep/find/ls, or '*' for all tools) are blocked so the child can finalize." });
|
|
121
121
|
|
|
122
|
+
const UsageBudgetLimitOverride = Type.Object({
|
|
123
|
+
soft: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
|
|
124
|
+
hard: Type.Number({ exclusiveMinimum: 0 }),
|
|
125
|
+
}, { additionalProperties: false });
|
|
126
|
+
|
|
127
|
+
const UsageBudgetOverride = Type.Object({
|
|
128
|
+
tokens: Type.Optional(UsageBudgetLimitOverride),
|
|
129
|
+
costUsd: Type.Optional(UsageBudgetLimitOverride),
|
|
130
|
+
}, { additionalProperties: false, description: "Optional root-only reported-usage budget. Hard limits prevent future child launches; running children are not stopped." });
|
|
131
|
+
|
|
122
132
|
const TaskItem = Type.Object({
|
|
123
133
|
agent: Type.String(),
|
|
124
134
|
task: Type.String(),
|
|
@@ -195,6 +205,8 @@ export const DynamicCollectSchema = Type.Object({
|
|
|
195
205
|
|
|
196
206
|
// Flattened so chain steps do not need an object-shape anyOf/oneOf union.
|
|
197
207
|
export const ChainItem = Type.Object({
|
|
208
|
+
checkpoint: Type.Optional(Type.String({ description: "Approval checkpoint name. Pauses the chain without launching a child until approve-checkpoint or reject-checkpoint is called." })),
|
|
209
|
+
message: Type.Optional(Type.String({ description: "Optional approval message shown while the checkpoint is paused." })),
|
|
198
210
|
agent: Type.Optional(Type.String({ description: "Sequential step agent name" })),
|
|
199
211
|
task: Type.Optional(Type.String({
|
|
200
212
|
description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder, {outputs.name}=prior named output. Required for first step, defaults to '{previous}' for subsequent steps."
|
|
@@ -229,7 +241,7 @@ export const ChainItem = Type.Object({
|
|
|
229
241
|
description: "Create isolated git worktrees for each parallel task."
|
|
230
242
|
})),
|
|
231
243
|
}, {
|
|
232
|
-
description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution,
|
|
244
|
+
description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, {expand, parallel: {...}, collect} for dynamic fanout, or {checkpoint: name, message?} for an approval pause.",
|
|
233
245
|
additionalProperties: false,
|
|
234
246
|
});
|
|
235
247
|
|
|
@@ -256,10 +268,10 @@ const SubagentParamsSchema = Type.Object({
|
|
|
256
268
|
description: "Optional management/control action. Omit this field entirely for execution/delegation ({agent, task}, {tasks}, or {chain}); use it only for management/control actions."
|
|
257
269
|
})),
|
|
258
270
|
id: Type.Optional(Type.String({
|
|
259
|
-
description: "Run id or prefix for action='status', action='interrupt', action='stop', action='resume', action='steer',
|
|
271
|
+
description: "Run id or prefix for action='status', action='interrupt', action='stop', action='resume', action='steer', action='append-step', action='approve-checkpoint', or action='reject-checkpoint'."
|
|
260
272
|
})),
|
|
261
273
|
runId: Type.Optional(Type.String({
|
|
262
|
-
description: "Target run ID for action='interrupt', action='stop', action='resume', action='steer',
|
|
274
|
+
description: "Target run ID for action='interrupt', action='stop', action='resume', action='steer', action='append-step', action='approve-checkpoint', or action='reject-checkpoint'. Prefer id for new calls."
|
|
263
275
|
})),
|
|
264
276
|
dir: Type.Optional(Type.String({
|
|
265
277
|
description: "Async run directory for action='status', action='stop', action='resume', or action='steer'."
|
|
@@ -306,6 +318,7 @@ const SubagentParamsSchema = Type.Object({
|
|
|
306
318
|
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs for foreground and async/background runs; foreground defaults to 30m absent call/agent." })),
|
|
307
319
|
turnBudget: Type.Optional(TurnBudgetOverride),
|
|
308
320
|
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
321
|
+
usageBudget: Type.Optional(UsageBudgetOverride),
|
|
309
322
|
agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
|
|
310
323
|
cwd: Type.Optional(Type.String()),
|
|
311
324
|
artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
|
|
@@ -8,7 +8,7 @@ const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
|
|
|
8
8
|
|
|
9
9
|
export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
10
10
|
• Use { action: "list" } before execution and only run executable/non-disabled agents or chains.
|
|
11
|
-
• Keep execution and management separate: omit action for SINGLE/PARALLEL/CHAIN execution; use action only for list/get/models/create/update/delete/status/grant-spawn-budget/interrupt/stop/resume/append-step/doctor.
|
|
11
|
+
• Keep execution and management separate: omit action for SINGLE/PARALLEL/CHAIN execution; use action only for list/get/models/create/update/delete/status/grant-spawn-budget/interrupt/stop/resume/steer/append-step/approve-checkpoint/reject-checkpoint/doctor.
|
|
12
12
|
• Async/background runs: launch with async:true only when work can proceed independently. Do not sleep or poll status just to wait. In an interactive session, normally return control and let Pi wake you; do not call subagent_wait merely to wait. Override that default and call subagent_wait when the current request is run-to-completion — for example, the user asked you to report results back before continuing or a skill must finish in one turn. Headless sessions auto-drain current-session work at agent_end; use subagent_wait when this turn must receive results before it ends.
|
|
13
13
|
• Child-safety boundary: ordinary child subagents are not orchestrators and must not run subagents. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
|
|
14
14
|
• Writing/review safety: keep one writer for the same cwd/worktree. Use fresh-context read-only reviewers/validators for independent review, then have the parent synthesize and apply fixes as the sole writer unless an isolated worktree was intentionally requested.
|
|
@@ -19,7 +19,7 @@ export const FULL_SUBAGENT_TOOL_DESCRIPTION = `To delegate work, call with { age
|
|
|
19
19
|
EXECUTION (use exactly ONE mode):
|
|
20
20
|
• Before executing, use { action: "list" } to inspect configured agents/chains. Only execute agents listed as executable/non-disabled.
|
|
21
21
|
• SINGLE: { agent, task? } - one task; omit task for self-contained agents
|
|
22
|
-
• CHAIN: { chain: [{agent:"agent-a"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional parallel fan-out
|
|
22
|
+
• CHAIN: { chain: [{agent:"agent-a"}, {checkpoint:"review"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional approval checkpoints and parallel fan-out
|
|
23
23
|
• PARALLEL: { tasks: [{agent,task,count?,output?,reads?,progress?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
|
|
24
24
|
• Optional context: { context: "fresh" | "fork" } (explicit value overrides every child; when omitted, each requested agent uses its own defaultContext, otherwise "fresh"; inspect agent defaults via { action: "list" })
|
|
25
25
|
• Fork thinking: model strings accept a thinking suffix (provider/model:off|minimal|low|medium|high|xhigh|max). Forking over a parent transcript that carries signed Anthropic thinking blocks forces thinking off only when a child's effective primary or fallback model resolves to the Anthropic provider or anthropic-messages API; unresolved models are treated conservatively. The result notes affected children, including on failures. Use fresh context when an Anthropic child needs thinking.
|
|
@@ -35,7 +35,7 @@ CHAIN TEMPLATE VARIABLES (use in task strings):
|
|
|
35
35
|
CHAIN EXAMPLES (quick reference for the nested schema):
|
|
36
36
|
• Sequential: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {agent:"agent-b", task:"Plan based on {previous}"}] }
|
|
37
37
|
• Parallel fan-out: { chain: [{parallel: [{agent:"agent-a", task:"Check part of {task}", count: 3}]}] }
|
|
38
|
-
• Mixed: { chain: [{agent:"agent-a", task:"Research {task}"}, {parallel: [{agent:"agent-b", task:"Review {previous}", count: 2}]}, {agent:"agent-c", task:"Summarize {previous}"}] }
|
|
38
|
+
• Mixed: { chain: [{agent:"agent-a", task:"Research {task}"}, {checkpoint:"review", message:"Approve implementation?"}, {parallel: [{agent:"agent-b", task:"Review {previous}", count: 2}]}, {agent:"agent-c", task:"Summarize {previous}"}] }
|
|
39
39
|
|
|
40
40
|
MANAGEMENT (use action field, omit agent/task/chain/tasks):
|
|
41
41
|
• { action: "list" } - discover executable agents/chains
|
|
@@ -63,6 +63,7 @@ CONTROL:
|
|
|
63
63
|
• { action: "resume", id: "...", message: "...", index?: 0 } - revive a paused, completed, or failed async/foreground child from its session; stopped runs are non-resumable; routed nested runs may accept live follow-ups; use steer for a live top-level async child
|
|
64
64
|
• { action: "steer", id: "...", message: "...", index?: 0 } - await correlated child-Pi input acceptance for up to 3 seconds; returns delivered, scheduled, pending, partial, recovered, or failed with a request id. Only top-level single runs may recover after a further 15-second pause/revival bound; chain, parallel, and nested runs never auto-interrupt.
|
|
65
65
|
• { action: "append-step", id: "...", chain: [{agent:"agent-c", task:"Use {previous}"}] } - append one step to the tail of a running async chain
|
|
66
|
+
• { action: "approve-checkpoint", id: "..." } / { action: "reject-checkpoint", id: "..." } - decide a paused current-session async chain checkpoint
|
|
66
67
|
|
|
67
68
|
SCHEDULE (opt-in; requires { "scheduledRuns": { "enabled": true } } in config.json):
|
|
68
69
|
• { action: "schedule", agent, task?, schedule: "+10m" | "2030-01-01T09:00:00Z", scheduleName? } - defer a subagent launch until a future time. Also accepts tasks[] or chain[]. Scheduled runs always launch async with fresh context; they become normal tracked async runs once they fire. Only schedule explicit delayed runs the user asked for.
|
|
@@ -79,7 +80,7 @@ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `To delegate work, call with {
|
|
|
79
80
|
|
|
80
81
|
EXECUTE:
|
|
81
82
|
• Before execution, call { action: "list" }; run only executable/non-disabled configured agents/chains.
|
|
82
|
-
• SINGLE {agent, task?}; PARALLEL {tasks:[{agent,task,count?,output?,reads?,progress?}], concurrency?, worktree?}; CHAIN {chain:[{agent,task?},{parallel:[...]}]}.
|
|
83
|
+
• SINGLE {agent, task?}; PARALLEL {tasks:[{agent,task,count?,output?,reads?,progress?}], concurrency?, worktree?}; CHAIN {chain:[{agent,task?},{checkpoint:"review"},{parallel:[...]}]}.
|
|
83
84
|
• context can be "fresh" or "fork"; omitted uses each agent defaultContext, otherwise fresh. timeoutMs/maxRuntimeMs apply to foreground and async/background runs; foreground defaults to 30 minutes only when neither value nor an agent timeout is provided.
|
|
84
85
|
• Omit acceptance for reviewer/read-only calls. Evidence levels end at verified; use acceptance.review.required for independent writer review. reviewed is an achieved status, never an explicit input.
|
|
85
86
|
• Chain templates may use {task}, {previous}, {chain_dir}, and named outputs. Parallel worktree isolation requires a clean git repo.
|
|
@@ -89,7 +90,7 @@ EXECUTE:
|
|
|
89
90
|
MANAGE / CONTROL:
|
|
90
91
|
• Use action without execution fields: list, get, models, create, update, delete, eject, disable, enable, reset, grant-spawn-budget, doctor, watchdog.status, watchdog.check, watchdog.recommend-model, watchdog.configure.
|
|
91
92
|
• Agent acceptanceRole (read-only or writer) affects inferred acceptance only, never tools. Explicit task intent wins; omission keeps name heuristics. Update with false or an empty string to clear it.
|
|
92
|
-
• Async control actions: status, interrupt, stop, resume, steer, append-step. Use stop with an id for current-session top-level async runs. Use status view:"fleet" for active-run overview, view:"transcript" to tail child output, steer for acknowledged top-level live async guidance, and resume for paused/completed/failed revival or a routed nested follow-up. Stopped runs are non-resumable. Steering delivery means Pi accepted the correlated user input, not model compliance; use index for a specific child.
|
|
93
|
+
• Async control actions: status, interrupt, stop, resume, steer, append-step, approve-checkpoint, reject-checkpoint. Use stop with an id for current-session top-level async runs. Use status view:"fleet" for active-run overview, view:"transcript" to tail child output, steer for acknowledged top-level live async guidance, and resume for paused/completed/failed revival or a routed nested follow-up. Stopped runs are non-resumable. Steering delivery means Pi accepted the correlated user input, not model compliance; use index for a specific child.
|
|
93
94
|
• Opt-in schedule actions: schedule, schedule-list, schedule-status, schedule-cancel. Schedule only explicit delayed runs the user asked for.
|
|
94
95
|
|
|
95
96
|
ASYNC / WAIT:
|