pi-subagents 0.28.0 → 0.30.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 -0
- package/README.md +26 -62
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +29 -35
- package/src/agents/agent-management.ts +29 -22
- package/src/agents/agent-selection.ts +2 -0
- package/src/agents/agent-serializer.ts +5 -10
- package/src/agents/agents.ts +339 -47
- package/src/agents/chain-serializer.ts +4 -9
- package/src/agents/proactive-skills.ts +191 -0
- package/src/extension/doctor.ts +4 -3
- package/src/extension/fanout-child.ts +1 -3
- package/src/extension/index.ts +6 -9
- package/src/extension/schemas.ts +63 -26
- package/src/intercom/intercom-bridge.ts +11 -1
- package/src/intercom/result-intercom.ts +0 -5
- package/src/runs/background/async-execution.ts +186 -74
- package/src/runs/background/async-resume.ts +53 -5
- package/src/runs/background/async-status.ts +4 -1
- package/src/runs/background/chain-append.ts +282 -0
- package/src/runs/background/chain-root-attachment.ts +161 -0
- package/src/runs/background/run-status.ts +2 -7
- package/src/runs/background/subagent-runner.ts +160 -219
- package/src/runs/foreground/chain-execution.ts +62 -58
- package/src/runs/foreground/execution.ts +39 -343
- package/src/runs/foreground/subagent-executor.ts +316 -111
- package/src/runs/shared/acceptance.ts +605 -22
- package/src/runs/shared/chain-outputs.ts +23 -8
- package/src/runs/shared/completion-guard.ts +3 -26
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +38 -0
- package/src/runs/shared/parallel-utils.ts +13 -10
- package/src/runs/shared/pi-args.ts +3 -2
- package/src/runs/shared/subagent-control.ts +8 -11
- package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
- package/src/runs/shared/workflow-graph.ts +2 -6
- package/src/shared/atomic-json.ts +68 -11
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +20 -49
- package/src/shared/utils.ts +2 -8
- package/src/slash/slash-bridge.ts +3 -1
- package/src/slash/slash-commands.ts +1 -1
- package/src/tui/render.ts +14 -29
- package/src/runs/shared/acceptance-contract.ts +0 -318
- package/src/runs/shared/acceptance-evaluation.ts +0 -221
- package/src/runs/shared/acceptance-finalization.ts +0 -173
- package/src/runs/shared/acceptance-reports.ts +0 -127
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { AgentConfig, ChainConfig, ChainStepConfig } from "./agents.ts";
|
|
2
|
+
import type { ProactiveSkillSubagentsConfig } from "../shared/types.ts";
|
|
3
|
+
|
|
4
|
+
const SUBAGENT_ORCHESTRATION_SKILL = "pi-subagents";
|
|
5
|
+
const DEFAULT_MIN_REFERENCES = 2;
|
|
6
|
+
const DEFAULT_MAX_RECOMMENDATIONS = 3;
|
|
7
|
+
const DEFAULT_PREFERRED_AGENT = "reviewer";
|
|
8
|
+
const FALLBACK_AGENT_ORDER = ["reviewer", "context-builder", "delegate"];
|
|
9
|
+
const MAX_RECOMMENDATION_CAP = 5;
|
|
10
|
+
|
|
11
|
+
export interface ResolvedProactiveSkillSubagentsConfig {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
minReferences: number;
|
|
14
|
+
maxRecommendations: number;
|
|
15
|
+
preferredAgent: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ProactiveSkillSubagentRecommendation {
|
|
19
|
+
skill: string;
|
|
20
|
+
agent: string;
|
|
21
|
+
references: number;
|
|
22
|
+
sources: string[];
|
|
23
|
+
description?: string;
|
|
24
|
+
reason: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AvailableSkill {
|
|
28
|
+
name: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function positiveInteger(value: unknown): number | undefined {
|
|
33
|
+
if (typeof value !== "number") return undefined;
|
|
34
|
+
if (!Number.isInteger(value) || !Number.isFinite(value) || value < 1) return undefined;
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveProactiveSkillSubagentsConfig(
|
|
39
|
+
config?: ProactiveSkillSubagentsConfig | false,
|
|
40
|
+
): ResolvedProactiveSkillSubagentsConfig {
|
|
41
|
+
if (config === false) {
|
|
42
|
+
return {
|
|
43
|
+
enabled: false,
|
|
44
|
+
minReferences: DEFAULT_MIN_REFERENCES,
|
|
45
|
+
maxRecommendations: DEFAULT_MAX_RECOMMENDATIONS,
|
|
46
|
+
preferredAgent: DEFAULT_PREFERRED_AGENT,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const maxRecommendations = positiveInteger(config?.maxRecommendations) ?? DEFAULT_MAX_RECOMMENDATIONS;
|
|
51
|
+
return {
|
|
52
|
+
enabled: config?.enabled ?? true,
|
|
53
|
+
minReferences: positiveInteger(config?.minReferences) ?? DEFAULT_MIN_REFERENCES,
|
|
54
|
+
maxRecommendations: Math.min(maxRecommendations, MAX_RECOMMENDATION_CAP),
|
|
55
|
+
preferredAgent: typeof config?.preferredAgent === "string" && config.preferredAgent.trim()
|
|
56
|
+
? config.preferredAgent.trim()
|
|
57
|
+
: DEFAULT_PREFERRED_AGENT,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeSkillNames(value: unknown): string[] {
|
|
62
|
+
if (value === false || value === true || value === undefined || value === null) return [];
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
return [...new Set(value.filter((entry): entry is string => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean))];
|
|
65
|
+
}
|
|
66
|
+
if (typeof value === "string") {
|
|
67
|
+
return [...new Set(value.split(",").map((entry) => entry.trim()).filter(Boolean))];
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function collectStepSkills(step: ChainStepConfig, out: Set<string>): void {
|
|
73
|
+
for (const skill of normalizeSkillNames(step.skills ?? (step as { skill?: unknown }).skill)) {
|
|
74
|
+
out.add(skill);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const parallel = step.parallel;
|
|
78
|
+
if (!parallel) return;
|
|
79
|
+
if (Array.isArray(parallel)) {
|
|
80
|
+
for (const child of parallel) {
|
|
81
|
+
if (child && typeof child === "object" && !Array.isArray(child)) {
|
|
82
|
+
collectStepSkills(child as ChainStepConfig, out);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (typeof parallel === "object") {
|
|
88
|
+
collectStepSkills(parallel as ChainStepConfig, out);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function chooseRecommendationAgent(agents: AgentConfig[], preferredAgent: string): string | undefined {
|
|
93
|
+
const enabled = agents.filter((agent) => !agent.disabled);
|
|
94
|
+
if (enabled.some((agent) => agent.name === preferredAgent)) return preferredAgent;
|
|
95
|
+
for (const name of FALLBACK_AGENT_ORDER) {
|
|
96
|
+
if (enabled.some((agent) => agent.name === name)) return name;
|
|
97
|
+
}
|
|
98
|
+
return enabled[0]?.name;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function addSource(counts: Map<string, Set<string>>, skill: string, source: string): void {
|
|
102
|
+
if (skill === SUBAGENT_ORCHESTRATION_SKILL) return;
|
|
103
|
+
const sources = counts.get(skill) ?? new Set<string>();
|
|
104
|
+
sources.add(source);
|
|
105
|
+
counts.set(skill, sources);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function recommendProactiveSkillSubagents(input: {
|
|
109
|
+
agents: AgentConfig[];
|
|
110
|
+
chains?: ChainConfig[];
|
|
111
|
+
availableSkills?: AvailableSkill[];
|
|
112
|
+
config?: ProactiveSkillSubagentsConfig | false;
|
|
113
|
+
}): ProactiveSkillSubagentRecommendation[] {
|
|
114
|
+
const config = resolveProactiveSkillSubagentsConfig(input.config);
|
|
115
|
+
if (!config.enabled) return [];
|
|
116
|
+
|
|
117
|
+
const agent = chooseRecommendationAgent(input.agents, config.preferredAgent);
|
|
118
|
+
if (!agent) return [];
|
|
119
|
+
|
|
120
|
+
const availableByName = input.availableSkills
|
|
121
|
+
? new Map(input.availableSkills.map((skill) => [skill.name, skill]))
|
|
122
|
+
: undefined;
|
|
123
|
+
const counts = new Map<string, Set<string>>();
|
|
124
|
+
|
|
125
|
+
for (const candidate of input.agents) {
|
|
126
|
+
if (candidate.disabled) continue;
|
|
127
|
+
for (const skill of candidate.skills ?? []) {
|
|
128
|
+
addSource(counts, skill, `agent:${candidate.name}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const chain of input.chains ?? []) {
|
|
133
|
+
const chainSkills = new Set<string>();
|
|
134
|
+
for (const step of chain.steps) {
|
|
135
|
+
collectStepSkills(step, chainSkills);
|
|
136
|
+
}
|
|
137
|
+
for (const skill of chainSkills) {
|
|
138
|
+
addSource(counts, skill, `chain:${chain.name}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return [...counts.entries()]
|
|
143
|
+
.filter(([skill, sources]) => sources.size >= config.minReferences && (!availableByName || availableByName.has(skill)))
|
|
144
|
+
.map(([skill, sources]) => ({
|
|
145
|
+
skill,
|
|
146
|
+
agent,
|
|
147
|
+
references: sources.size,
|
|
148
|
+
sources: [...sources].sort((a, b) => a.localeCompare(b)),
|
|
149
|
+
description: availableByName?.get(skill)?.description,
|
|
150
|
+
reason: `referenced by ${sources.size} configured agents/chains`,
|
|
151
|
+
}))
|
|
152
|
+
.sort((a, b) => b.references - a.references || a.skill.localeCompare(b.skill))
|
|
153
|
+
.slice(0, config.maxRecommendations);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function formatProactiveSkillSubagentRecommendations(
|
|
157
|
+
recommendations: ProactiveSkillSubagentRecommendation[],
|
|
158
|
+
): string[] {
|
|
159
|
+
if (recommendations.length === 0) return [];
|
|
160
|
+
return [
|
|
161
|
+
"Proactive skill subagent suggestions:",
|
|
162
|
+
...recommendations.map((recommendation) => {
|
|
163
|
+
const sampleSources = recommendation.sources.slice(0, 3).join(", ");
|
|
164
|
+
const extra = recommendation.sources.length > 3 ? `, +${recommendation.sources.length - 3} more` : "";
|
|
165
|
+
const description = recommendation.description ? ` - ${recommendation.description}` : "";
|
|
166
|
+
return `- ${recommendation.skill} via ${recommendation.agent} (${recommendation.reason}; ${sampleSources}${extra})${description}`;
|
|
167
|
+
}),
|
|
168
|
+
"Guardrails: use these for broad tasks where a skill-specialist pass is useful; keep fanout small, use fresh context unless private/session context is explicitly needed, and skip when the user asks for a direct answer.",
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function buildProactiveSkillSubagentRecommendationLines(input: {
|
|
173
|
+
agents: AgentConfig[];
|
|
174
|
+
chains?: ChainConfig[];
|
|
175
|
+
config?: ProactiveSkillSubagentsConfig | false;
|
|
176
|
+
discoverAvailableSkills: () => AvailableSkill[];
|
|
177
|
+
}): string[] {
|
|
178
|
+
if (!resolveProactiveSkillSubagentsConfig(input.config).enabled) return [];
|
|
179
|
+
let availableSkills: AvailableSkill[];
|
|
180
|
+
try {
|
|
181
|
+
availableSkills = input.discoverAvailableSkills();
|
|
182
|
+
} catch {
|
|
183
|
+
availableSkills = [];
|
|
184
|
+
}
|
|
185
|
+
return formatProactiveSkillSubagentRecommendations(recommendProactiveSkillSubagents({
|
|
186
|
+
agents: input.agents,
|
|
187
|
+
chains: input.chains,
|
|
188
|
+
availableSkills,
|
|
189
|
+
config: input.config,
|
|
190
|
+
}));
|
|
191
|
+
}
|
package/src/extension/doctor.ts
CHANGED
|
@@ -81,7 +81,7 @@ function formatExistingDirectory(label: string, dirPath: string): string {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function formatSourceCounts(counts: Record<AgentSource, number>): string {
|
|
84
|
-
return `builtin ${counts.builtin}, user ${counts.user}, project ${counts.project}`;
|
|
84
|
+
return `builtin ${counts.builtin}, package ${counts.package}, user ${counts.user}, project ${counts.project}`;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
function formatSkillSourceCounts(skills: Array<{ source: SkillSource }>): string {
|
|
@@ -132,15 +132,16 @@ function formatDiscovery(input: DoctorReportInput, deps: DoctorDeps): string[] {
|
|
|
132
132
|
const discovered = deps.discoverAgentsAll(input.cwd);
|
|
133
133
|
const agentCounts = {
|
|
134
134
|
builtin: discovered.builtin.length,
|
|
135
|
+
package: discovered.package?.length ?? 0,
|
|
135
136
|
user: discovered.user.length,
|
|
136
137
|
project: discovered.project.length,
|
|
137
138
|
};
|
|
138
139
|
const chainCounts = discovered.chains.reduce<Record<AgentSource, number>>((counts, chain) => {
|
|
139
140
|
counts[chain.source] += 1;
|
|
140
141
|
return counts;
|
|
141
|
-
}, { builtin: 0, user: 0, project: 0 });
|
|
142
|
+
}, { builtin: 0, package: 0, user: 0, project: 0 });
|
|
142
143
|
return [
|
|
143
|
-
`- agents: total ${agentCounts.builtin + agentCounts.user + agentCounts.project} (${formatSourceCounts(agentCounts)})`,
|
|
144
|
+
`- agents: total ${agentCounts.builtin + agentCounts.package + agentCounts.user + agentCounts.project} (${formatSourceCounts(agentCounts)})`,
|
|
144
145
|
`- chains: total ${discovered.chains.length} (${formatSourceCounts(chainCounts)})`,
|
|
145
146
|
].join("\n");
|
|
146
147
|
}),
|
|
@@ -156,9 +156,7 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
|
|
|
156
156
|
label: "Subagent",
|
|
157
157
|
description: [
|
|
158
158
|
"Delegate to subagents from child-safe fanout mode.",
|
|
159
|
-
"
|
|
160
|
-
"For implementation handoffs from a plan, PRD, spec, issue, or broad fix, put implementation instructions and plan paths in task, and put the definition of done, evidence, verification commands, constraints, and loop cap in acceptance.",
|
|
161
|
-
"Allowed management/control actions: list, get, status, interrupt, resume, doctor.",
|
|
159
|
+
"Allowed management/control actions: list, get, status, interrupt, resume, append-step, doctor.",
|
|
162
160
|
"Agent config mutation actions create, update, and delete are blocked in this mode.",
|
|
163
161
|
].join("\n"),
|
|
164
162
|
parameters: SubagentParams,
|
package/src/extension/index.ts
CHANGED
|
@@ -33,8 +33,7 @@ import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
|
|
|
33
33
|
import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
|
|
34
34
|
import { inspectSubagentStatus } from "../runs/background/run-status.ts";
|
|
35
35
|
import registerSubagentNotify, { type SubagentNotifyDetails } from "../runs/background/notify.ts";
|
|
36
|
-
import { SUBAGENT_CHILD_ENV
|
|
37
|
-
import registerFanoutChildSubagentExtension from "./fanout-child.ts";
|
|
36
|
+
import { SUBAGENT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
38
37
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
39
38
|
import { loadConfig } from "./config.ts";
|
|
40
39
|
import {
|
|
@@ -209,7 +208,6 @@ class SubagentControlNoticeComponent implements Component {
|
|
|
209
208
|
|
|
210
209
|
export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
211
210
|
if (process.env[SUBAGENT_CHILD_ENV] === "1") {
|
|
212
|
-
if (process.env[SUBAGENT_FANOUT_CHILD_ENV] === "1") registerFanoutChildSubagentExtension(pi);
|
|
213
211
|
return;
|
|
214
212
|
}
|
|
215
213
|
const globalStore = globalThis as Record<string, unknown>;
|
|
@@ -394,10 +392,8 @@ EXECUTION (use exactly ONE mode):
|
|
|
394
392
|
• SINGLE: { agent, task? } - one task; omit task for self-contained agents
|
|
395
393
|
• CHAIN: { chain: [{agent:"agent-a"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional parallel fan-out
|
|
396
394
|
• PARALLEL: { tasks: [{agent,task,count?,output?,reads?,progress?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
|
|
397
|
-
• Foreground timeout: { timeoutMs } or { maxRuntimeMs } - wall-clock limit for foreground single, parallel, and chain runs. Timed-out children return timedOut:true with completed sibling/prior results preserved. Not for async/background runs.
|
|
398
395
|
• Optional context: { context: "fresh" | "fork" } (default: if any requested agent has defaultContext: "fork", the whole invocation uses fork; otherwise "fresh"; inspect agent defaults via { action: "list" })
|
|
399
|
-
•
|
|
400
|
-
• Plan/spec implementation handoffs: when delegating a plan, PRD, spec, issue, or broad fix to an editing-capable child, prefer structured acceptance instead of burying validation requirements in task prose. Put the implementation instructions and plan paths in task; put the definition of done, evidence, verification commands, constraints, and loop cap in acceptance.
|
|
396
|
+
• If { action: "list" } shows proactive skill subagent suggestions, consider a small fresh-context fanout for broad tasks where one of those skills would materially help
|
|
401
397
|
|
|
402
398
|
CHAIN TEMPLATE VARIABLES (use in task strings):
|
|
403
399
|
• {task} - The original task/request from the user
|
|
@@ -409,15 +405,16 @@ Example: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {agent:"agent-b", t
|
|
|
409
405
|
MANAGEMENT (use action field, omit agent/task/chain/tasks):
|
|
410
406
|
• { action: "list" } - discover executable agents/chains
|
|
411
407
|
• { action: "get", agent: "name" } - full detail; packaged agents use dotted runtime names like "package.agent"
|
|
412
|
-
• { action: "create", config: { name: "custom-agent", package: "code-analysis", systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext,
|
|
413
|
-
• { action: "update", agent: "code-analysis.custom-agent", config: { package: "analysis",
|
|
408
|
+
• { action: "create", config: { name: "custom-agent", package: "code-analysis", systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, ... } }
|
|
409
|
+
• { action: "update", agent: "code-analysis.custom-agent", config: { package: "analysis", ... } } - merge
|
|
414
410
|
• { action: "delete", agent: "code-analysis.custom-agent" }
|
|
415
411
|
• Use chainName for chain operations; packaged chains also use dotted runtime names
|
|
416
412
|
|
|
417
413
|
CONTROL:
|
|
418
414
|
• { action: "status", id: "..." } - inspect an async/background run by id or prefix
|
|
419
415
|
• { action: "interrupt", id?: "..." } - soft-interrupt the current child turn and leave the run paused
|
|
420
|
-
• { action: "resume", id: "...", message: "...", index?: 0 } - follow up with a live async child or revive a completed async/foreground child from its session
|
|
416
|
+
• { action: "resume", id: "...", message: "...", index?: 0 } - interrupt then follow up with a live async child, or revive a completed async/foreground child from its session
|
|
417
|
+
• { action: "append-step", id: "...", chain: [{agent:"agent-c", task:"Use {previous}"}] } - append one step to the tail of a running async chain
|
|
421
418
|
|
|
422
419
|
DIAGNOSTICS:
|
|
423
420
|
• { action: "doctor" } - read-only report for runtime paths, discovery, sessions, and intercom`,
|
package/src/extension/schemas.ts
CHANGED
|
@@ -5,6 +5,31 @@
|
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
import { SUBAGENT_ACTIONS } from "../shared/types.ts";
|
|
7
7
|
|
|
8
|
+
function keepTopLevelParameterDescriptions<T>(schema: T): T {
|
|
9
|
+
return pruneNestedDescriptions(schema, []) as T;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function pruneNestedDescriptions(value: unknown, path: string[]): unknown {
|
|
13
|
+
if (!value || typeof value !== "object") return value;
|
|
14
|
+
|
|
15
|
+
const result = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
|
|
16
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
17
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
18
|
+
if (!descriptor) continue;
|
|
19
|
+
if (key === "description" && !isTopLevelParameterDescription(path)) continue;
|
|
20
|
+
if ("value" in descriptor) {
|
|
21
|
+
const nextPath = typeof key === "string" ? [...path, key] : path;
|
|
22
|
+
descriptor.value = pruneNestedDescriptions(descriptor.value, nextPath);
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(result, key, descriptor);
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isTopLevelParameterDescription(path: string[]): boolean {
|
|
30
|
+
return path.length === 2 && path[0] === "properties";
|
|
31
|
+
}
|
|
32
|
+
|
|
8
33
|
const SkillOverride = Type.Unsafe({
|
|
9
34
|
anyOf: [
|
|
10
35
|
{ type: "array", items: { type: "string" } },
|
|
@@ -78,30 +103,42 @@ const AcceptanceReviewGateSchema = Type.Object({
|
|
|
78
103
|
}, { additionalProperties: false });
|
|
79
104
|
|
|
80
105
|
const AcceptanceOverride = Type.Unsafe({
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
anyOf: [
|
|
107
|
+
{ type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
|
|
108
|
+
{ const: false },
|
|
109
|
+
{
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
level: { type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
|
|
113
|
+
criteria: {
|
|
114
|
+
type: "array",
|
|
115
|
+
items: {
|
|
116
|
+
anyOf: [
|
|
117
|
+
{ type: "string" },
|
|
118
|
+
AcceptanceGateSchema,
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
evidence: { type: "array", items: AcceptanceEvidenceKind },
|
|
123
|
+
verify: { type: "array", items: AcceptanceVerifyCommandSchema },
|
|
124
|
+
review: {
|
|
125
|
+
anyOf: [
|
|
126
|
+
{ const: false },
|
|
127
|
+
AcceptanceReviewGateSchema,
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
stopRules: { type: "array", items: { type: "string" } },
|
|
131
|
+
reason: { type: "string" },
|
|
90
132
|
},
|
|
133
|
+
additionalProperties: false,
|
|
91
134
|
},
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
review: AcceptanceReviewGateSchema,
|
|
95
|
-
stopRules: { type: "array", items: { type: "string" } },
|
|
96
|
-
maxFinalizationTurns: { type: "integer", minimum: 1, maximum: 10 },
|
|
97
|
-
},
|
|
98
|
-
additionalProperties: false,
|
|
99
|
-
description: "Optional acceptance contract. Use this for goal-style requests and for implementation handoffs from plans, PRDs, specs, issues, or broad fixes. Put implementation instructions and plan paths in task; put the definition of done in criteria, proof in evidence/verify, constraints in stopRules, and the bounded loop budget in maxFinalizationTurns. Runtime validation still requires at least one of criteria, evidence, verify, review, or stopRules. When present, the child must complete a same-session self-review/repair loop before acceptance is evaluated.",
|
|
135
|
+
],
|
|
136
|
+
description: "Optional acceptance policy. Omitted means auto-inferred; verified requires configured runtime commands.",
|
|
100
137
|
});
|
|
101
138
|
|
|
102
139
|
const TaskItem = Type.Object({
|
|
103
|
-
agent: Type.String(),
|
|
104
|
-
task: Type.String(),
|
|
140
|
+
agent: Type.String(),
|
|
141
|
+
task: Type.String(),
|
|
105
142
|
cwd: Type.Optional(Type.String()),
|
|
106
143
|
count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
|
|
107
144
|
output: Type.Optional(OutputOverride),
|
|
@@ -221,7 +258,7 @@ const ControlOverrides = Type.Object({
|
|
|
221
258
|
})),
|
|
222
259
|
});
|
|
223
260
|
|
|
224
|
-
|
|
261
|
+
const SubagentParamsSchema = Type.Object({
|
|
225
262
|
agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode) or target for management get/update/delete" })),
|
|
226
263
|
task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
|
|
227
264
|
// Management action (when present, tool operates in management mode)
|
|
@@ -230,10 +267,10 @@ export const SubagentParams = Type.Object({
|
|
|
230
267
|
description: "Management/control action. Omit for execution mode."
|
|
231
268
|
})),
|
|
232
269
|
id: Type.Optional(Type.String({
|
|
233
|
-
description: "Run id or prefix for action='status', action='interrupt', or action='
|
|
270
|
+
description: "Run id or prefix for action='status', action='interrupt', action='resume', or action='append-step'."
|
|
234
271
|
})),
|
|
235
272
|
runId: Type.Optional(Type.String({
|
|
236
|
-
description: "Target run ID for action='interrupt' or action='
|
|
273
|
+
description: "Target run ID for action='interrupt', action='resume', or action='append-step'. Defaults to the most recently active controllable run for interrupt. Prefer id for new calls."
|
|
237
274
|
})),
|
|
238
275
|
dir: Type.Optional(Type.String({
|
|
239
276
|
description: "Async run directory for action='status' or action='resume'."
|
|
@@ -250,18 +287,16 @@ export const SubagentParams = Type.Object({
|
|
|
250
287
|
{ type: "object", additionalProperties: true },
|
|
251
288
|
{ type: "string" },
|
|
252
289
|
],
|
|
253
|
-
description: "Agent or chain config for create/update. Agent: name, package (optional namespace; runtime name becomes package.name), description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth
|
|
290
|
+
description: "Agent or chain config for create/update. Agent: name, package (optional namespace; runtime name becomes package.name), description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), subagentOnlyExtensions (comma-separated child-only extension paths), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, package, description, scope, steps (array of {agent, task?, output?, outputMode?, reads?, model?, skill?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
|
|
254
291
|
})),
|
|
255
292
|
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?, output?, outputMode?, reads?, progress?}, ...]" })),
|
|
256
293
|
concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Top-level PARALLEL mode only: max concurrent tasks. Defaults to config.parallel.concurrency or 4." })),
|
|
257
|
-
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Foreground execution wall-clock timeout in milliseconds. When it expires, running children are soft-interrupted and timed-out results are returned. Foreground only; async/background runs ignore this field." })),
|
|
258
|
-
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias for timeoutMs. Use only one unless both values are identical." })),
|
|
259
294
|
worktree: Type.Optional(Type.Boolean({
|
|
260
295
|
description: "Create isolated git worktrees for each parallel task. " +
|
|
261
296
|
"Prevents filesystem conflicts. Requires clean git state. " +
|
|
262
297
|
"Per-worktree diffs included in output."
|
|
263
298
|
})),
|
|
264
|
-
chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: sequential pipeline where each step's response becomes {previous} for the next.
|
|
299
|
+
chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: sequential pipeline where each step's response becomes {previous} for the next. With action='append-step', provide exactly one step to append to an active async chain; it can use {previous}, {chain_dir}, and existing {outputs.name} references." })),
|
|
265
300
|
context: Type.Optional(Type.String({
|
|
266
301
|
enum: ["fresh", "fork"],
|
|
267
302
|
description: "'fresh' or 'fork' to branch from parent session. If omitted, any requested agent with defaultContext: 'fork' makes the whole invocation forked; otherwise the default is 'fresh'.",
|
|
@@ -292,3 +327,5 @@ export const SubagentParams = Type.Object({
|
|
|
292
327
|
model: Type.Optional(Type.String({ description: "Override model for single agent (e.g. 'anthropic/claude-sonnet-4')" })),
|
|
293
328
|
acceptance: Type.Optional(AcceptanceOverride),
|
|
294
329
|
});
|
|
330
|
+
|
|
331
|
+
export const SubagentParams = keepTopLevelParameterDescriptions(SubagentParamsSchema);
|
|
@@ -17,6 +17,16 @@ function defaultIntercomExtensionDir(agentDir = defaultAgentDir()): string {
|
|
|
17
17
|
return path.join(agentDir, "extensions", PI_INTERCOM_PACKAGE_NAME);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export const INTERCOM_EXTENSION_DIR_ENV = "PI_INTERCOM_EXTENSION_DIR";
|
|
21
|
+
|
|
22
|
+
// Launcher-provided override for the pi-intercom package directory. Lets a hermetic
|
|
23
|
+
// wrapper point the subagent intercom bridge at a read-only install (e.g. a Nix-store
|
|
24
|
+
// path) instead of seeding the package into the writable agent dir.
|
|
25
|
+
function envIntercomExtensionDir(): string | undefined {
|
|
26
|
+
const dir = process.env[INTERCOM_EXTENSION_DIR_ENV]?.trim();
|
|
27
|
+
return dir ? dir : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
function defaultIntercomConfigPath(agentDir = defaultAgentDir()): string {
|
|
21
31
|
return path.join(agentDir, "intercom", "config.json");
|
|
22
32
|
}
|
|
@@ -224,7 +234,7 @@ function configuredPiIntercomPackageDir(input: ResolveIntercomBridgeInput, agent
|
|
|
224
234
|
}
|
|
225
235
|
|
|
226
236
|
function resolveIntercomExtensionDir(input: ResolveIntercomBridgeInput, agentDir: string): string {
|
|
227
|
-
const legacyDir = path.resolve(input.extensionDir ?? defaultIntercomExtensionDir(agentDir));
|
|
237
|
+
const legacyDir = path.resolve(input.extensionDir ?? envIntercomExtensionDir() ?? defaultIntercomExtensionDir(agentDir));
|
|
228
238
|
if (fs.existsSync(legacyDir)) return legacyDir;
|
|
229
239
|
return configuredPiIntercomPackageDir(input, agentDir) ?? legacyDir;
|
|
230
240
|
}
|
|
@@ -20,10 +20,8 @@ export function resolveSubagentResultStatus(input: {
|
|
|
20
20
|
state?: string;
|
|
21
21
|
interrupted?: boolean;
|
|
22
22
|
detached?: boolean;
|
|
23
|
-
timedOut?: boolean;
|
|
24
23
|
}): SubagentResultStatus {
|
|
25
24
|
if (input.detached) return "detached";
|
|
26
|
-
if (input.timedOut || input.state === "timed-out") return "timed-out";
|
|
27
25
|
if (input.interrupted || input.state === "paused") return "paused";
|
|
28
26
|
if (typeof input.success === "boolean") return input.success ? "completed" : "failed";
|
|
29
27
|
if (input.state === "complete") return "completed";
|
|
@@ -38,7 +36,6 @@ function countStatuses(children: SubagentResultIntercomChild[]): Record<Subagent
|
|
|
38
36
|
failed: 0,
|
|
39
37
|
paused: 0,
|
|
40
38
|
detached: 0,
|
|
41
|
-
"timed-out": 0,
|
|
42
39
|
};
|
|
43
40
|
for (const child of children) {
|
|
44
41
|
counts[child.status] += 1;
|
|
@@ -52,7 +49,6 @@ function formatStatusCounts(counts: Record<SubagentResultStatus, number>): strin
|
|
|
52
49
|
counts.failed ? `${counts.failed} failed` : undefined,
|
|
53
50
|
counts.paused ? `${counts.paused} paused` : undefined,
|
|
54
51
|
counts.detached ? `${counts.detached} detached` : undefined,
|
|
55
|
-
counts["timed-out"] ? `${counts["timed-out"]} timed out` : undefined,
|
|
56
52
|
].filter((part): part is string => Boolean(part));
|
|
57
53
|
return parts.length ? parts.join(", ") : "0 results";
|
|
58
54
|
}
|
|
@@ -60,7 +56,6 @@ function formatStatusCounts(counts: Record<SubagentResultStatus, number>): strin
|
|
|
60
56
|
function resolveGroupedStatus(children: SubagentResultIntercomChild[]): SubagentResultStatus {
|
|
61
57
|
const counts = countStatuses(children);
|
|
62
58
|
if (counts.failed > 0) return "failed";
|
|
63
|
-
if (counts["timed-out"] > 0) return "timed-out";
|
|
64
59
|
if (counts.paused > 0) return "paused";
|
|
65
60
|
if (counts.completed > 0) return "completed";
|
|
66
61
|
if (counts.detached > 0) return "detached";
|