pi-subagents 0.27.0 → 0.29.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 +27 -0
- package/README.md +16 -15
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +3 -6
- package/src/agents/agent-management.ts +10 -6
- package/src/agents/agent-selection.ts +2 -0
- package/src/agents/agents.ts +303 -6
- package/src/agents/chain-serializer.ts +4 -9
- package/src/extension/doctor.ts +4 -3
- package/src/extension/fanout-child.ts +0 -1
- package/src/extension/index.ts +1 -4
- package/src/extension/schemas.ts +31 -28
- package/src/intercom/intercom-bridge.ts +11 -1
- package/src/runs/background/async-execution.ts +20 -7
- package/src/runs/background/run-status.ts +1 -7
- package/src/runs/background/subagent-runner.ts +73 -146
- package/src/runs/foreground/chain-execution.ts +61 -13
- package/src/runs/foreground/execution.ts +28 -172
- package/src/runs/foreground/subagent-executor.ts +25 -40
- package/src/runs/shared/acceptance.ts +605 -22
- package/src/runs/shared/completion-guard.ts +3 -26
- package/src/runs/shared/model-fallback.ts +38 -0
- package/src/runs/shared/parallel-utils.ts +6 -8
- package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
- package/src/shared/atomic-json.ts +68 -11
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +8 -32
- package/src/shared/utils.ts +2 -1
- package/src/tui/render.ts +1 -11
- package/src/runs/shared/acceptance-contract.ts +0 -291
- package/src/runs/shared/acceptance-evaluation.ts +0 -221
- package/src/runs/shared/acceptance-finalization.ts +0 -161
- package/src/runs/shared/acceptance-reports.ts +0 -127
|
@@ -4,6 +4,7 @@ import { parseFrontmatter } from "./frontmatter.ts";
|
|
|
4
4
|
import { ChainOutputValidationError, validateChainOutputBindings } from "../runs/shared/chain-outputs.ts";
|
|
5
5
|
import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
|
|
6
6
|
import type { ChainStep } from "../shared/settings.ts";
|
|
7
|
+
import type { AgentSource } from "./agents.ts";
|
|
7
8
|
|
|
8
9
|
function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
9
10
|
const lines = sectionBody.split("\n");
|
|
@@ -83,7 +84,7 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
|
83
84
|
return step;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
export function parseChain(content: string, source:
|
|
87
|
+
export function parseChain(content: string, source: AgentSource, filePath: string): ChainConfig {
|
|
87
88
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
88
89
|
if (!frontmatter.name || !frontmatter.description) {
|
|
89
90
|
throw new Error("Chain frontmatter must include name and description");
|
|
@@ -124,7 +125,7 @@ export function parseChain(content: string, source: "user" | "project", filePath
|
|
|
124
125
|
};
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
export function parseJsonChain(content: string, source:
|
|
128
|
+
export function parseJsonChain(content: string, source: AgentSource, filePath: string): ChainConfig {
|
|
128
129
|
let parsed: unknown;
|
|
129
130
|
try {
|
|
130
131
|
parsed = JSON.parse(content);
|
|
@@ -151,17 +152,11 @@ export function parseJsonChain(content: string, source: "user" | "project", file
|
|
|
151
152
|
throw new Error(`JSON chain '${filePath}' step ${i + 1} must be an object.`);
|
|
152
153
|
}
|
|
153
154
|
const stepRecord = step as Record<string, unknown>;
|
|
154
|
-
const parallel = stepRecord.parallel;
|
|
155
|
-
if (Array.isArray(parallel) && Object.hasOwn(stepRecord, "acceptance")) {
|
|
156
|
-
throw new Error(`Invalid JSON chain '${filePath}': step ${i + 1} acceptance is not supported on static parallel groups; set acceptance on each parallel task.`);
|
|
157
|
-
}
|
|
158
|
-
if (parallel && typeof parallel === "object" && !Array.isArray(parallel) && Object.hasOwn(stepRecord, "acceptance")) {
|
|
159
|
-
throw new Error(`Invalid JSON chain '${filePath}': step ${i + 1} acceptance is not supported on dynamic fanout groups; set acceptance on the dynamic template.`);
|
|
160
|
-
}
|
|
161
155
|
const acceptanceErrors = validateAcceptanceInput(stepRecord.acceptance, `step ${i + 1} acceptance`);
|
|
162
156
|
if (acceptanceErrors.length > 0) {
|
|
163
157
|
throw new Error(`Invalid JSON chain '${filePath}': ${acceptanceErrors.join(" ")}`);
|
|
164
158
|
}
|
|
159
|
+
const parallel = stepRecord.parallel;
|
|
165
160
|
if (Array.isArray(parallel)) {
|
|
166
161
|
for (let taskIndex = 0; taskIndex < parallel.length; taskIndex++) {
|
|
167
162
|
const task = parallel[taskIndex];
|
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,7 +156,6 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
|
|
|
156
156
|
label: "Subagent",
|
|
157
157
|
description: [
|
|
158
158
|
"Delegate to subagents from child-safe fanout mode.",
|
|
159
|
-
"For goal-style requests such as /goal, goal, active goal, or work until evidence says done, use explicit acceptance on the delegated run: criteria for the target, evidence/verify for proof, stopRules for constraints, and maxFinalizationTurns for the bounded loop.",
|
|
160
159
|
"Allowed management/control actions: list, get, status, interrupt, resume, doctor.",
|
|
161
160
|
"Agent config mutation actions create, update, and delete are blocked in this mode.",
|
|
162
161
|
].join("\n"),
|
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>;
|
|
@@ -395,7 +393,6 @@ EXECUTION (use exactly ONE mode):
|
|
|
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
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" })
|
|
398
|
-
• Goal-style requests: when the user says “/goal”, “goal”, “active goal”, “work until evidence says done”, or “verify against a goal”, model that as explicit acceptance. Use acceptance.criteria for the target, acceptance.evidence/verify for proof, acceptance.stopRules for constraints, and acceptance.maxFinalizationTurns for the bounded loop.
|
|
399
396
|
|
|
400
397
|
CHAIN TEMPLATE VARIABLES (use in task strings):
|
|
401
398
|
• {task} - The original task/request from the user
|
package/src/extension/schemas.ts
CHANGED
|
@@ -78,39 +78,42 @@ const AcceptanceReviewGateSchema = Type.Object({
|
|
|
78
78
|
}, { additionalProperties: false });
|
|
79
79
|
|
|
80
80
|
const AcceptanceOverride = Type.Unsafe({
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
anyOf: [
|
|
82
|
+
{ type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
|
|
83
|
+
{ const: false },
|
|
84
|
+
{
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
level: { type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
|
|
88
|
+
criteria: {
|
|
89
|
+
type: "array",
|
|
90
|
+
items: {
|
|
91
|
+
anyOf: [
|
|
92
|
+
{ type: "string" },
|
|
93
|
+
AcceptanceGateSchema,
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
evidence: { type: "array", items: AcceptanceEvidenceKind },
|
|
98
|
+
verify: { type: "array", items: AcceptanceVerifyCommandSchema },
|
|
99
|
+
review: {
|
|
100
|
+
anyOf: [
|
|
101
|
+
{ const: false },
|
|
102
|
+
AcceptanceReviewGateSchema,
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
stopRules: { type: "array", items: { type: "string" } },
|
|
106
|
+
reason: { type: "string" },
|
|
90
107
|
},
|
|
108
|
+
additionalProperties: false,
|
|
91
109
|
},
|
|
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
|
-
allOf: [{
|
|
100
|
-
anyOf: [
|
|
101
|
-
{ required: ["criteria"] },
|
|
102
|
-
{ required: ["evidence"] },
|
|
103
|
-
{ required: ["verify"] },
|
|
104
|
-
{ required: ["review"] },
|
|
105
|
-
{ required: ["stopRules"] },
|
|
106
|
-
],
|
|
107
|
-
}],
|
|
108
|
-
description: "Optional acceptance contract. Use this for goal-style requests such as /goal, goal, active goal, or work until evidence says done: criteria define the target, evidence/verify define proof, stopRules define constraints, and maxFinalizationTurns defines the bounded loop. When present, the child must complete a same-session self-review/repair loop before acceptance is evaluated.",
|
|
110
|
+
],
|
|
111
|
+
description: "Optional acceptance policy. Omitted means auto-inferred; verified requires configured runtime commands.",
|
|
109
112
|
});
|
|
110
113
|
|
|
111
114
|
const TaskItem = Type.Object({
|
|
112
|
-
agent: Type.String(),
|
|
113
|
-
task: Type.String(),
|
|
115
|
+
agent: Type.String(),
|
|
116
|
+
task: Type.String(),
|
|
114
117
|
cwd: Type.Optional(Type.String()),
|
|
115
118
|
count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
|
|
116
119
|
output: Type.Optional(OutputOverride),
|
|
@@ -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
|
}
|
|
@@ -17,7 +17,7 @@ import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
|
17
17
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
18
18
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
19
19
|
import { resolveChildCwd } from "../../shared/utils.ts";
|
|
20
|
-
import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "../shared/model-fallback.ts";
|
|
20
|
+
import { buildModelCandidates, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
|
|
21
21
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
22
22
|
import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
|
|
23
23
|
import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
|
|
@@ -95,6 +95,7 @@ interface AsyncExecutionContext {
|
|
|
95
95
|
cwd: string;
|
|
96
96
|
currentSessionId: string;
|
|
97
97
|
currentModelProvider?: string;
|
|
98
|
+
currentModel?: ParentModel;
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
interface AsyncChainParams {
|
|
@@ -342,7 +343,8 @@ export function executeAsyncChain(
|
|
|
342
343
|
taskTemplate = taskTemplate.replace(/\{chain_dir\}/g, runnerCwd);
|
|
343
344
|
const task = injectSingleOutputInstruction(`${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`, outputPath);
|
|
344
345
|
|
|
345
|
-
const
|
|
346
|
+
const requestedModel = behavior.model ?? a.model;
|
|
347
|
+
const primaryModel = resolveSubagentModelOverride(requestedModel, ctx.currentModel, availableModels, ctx.currentModelProvider);
|
|
346
348
|
const model = applyThinkingSuffix(primaryModel, a.thinking);
|
|
347
349
|
return {
|
|
348
350
|
agent: s.agent,
|
|
@@ -354,7 +356,7 @@ export function executeAsyncChain(
|
|
|
354
356
|
cwd: stepCwd,
|
|
355
357
|
model,
|
|
356
358
|
thinking: resolveEffectiveThinking(model, a.thinking),
|
|
357
|
-
modelCandidates: buildModelCandidates(
|
|
359
|
+
modelCandidates: buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
|
|
358
360
|
applyThinkingSuffix(candidate, a.thinking),
|
|
359
361
|
),
|
|
360
362
|
tools: a.tools,
|
|
@@ -436,6 +438,14 @@ export function executeAsyncChain(
|
|
|
436
438
|
failFast: s.failFast,
|
|
437
439
|
phase: s.phase,
|
|
438
440
|
label: s.label,
|
|
441
|
+
effectiveAcceptance: resolveEffectiveAcceptance({
|
|
442
|
+
explicit: s.acceptance,
|
|
443
|
+
agentName: s.parallel.agent,
|
|
444
|
+
task: s.parallel.task,
|
|
445
|
+
mode: resultMode,
|
|
446
|
+
async: true,
|
|
447
|
+
dynamicGroup: true,
|
|
448
|
+
}),
|
|
439
449
|
};
|
|
440
450
|
}
|
|
441
451
|
return buildSeqStep(s as SequentialStep, nextSessionFile());
|
|
@@ -657,10 +667,13 @@ export function executeAsyncSingle(
|
|
|
657
667
|
const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
|
|
658
668
|
if (validationError) return formatAsyncStartError("single", validationError);
|
|
659
669
|
const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath);
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
670
|
+
const primaryModel = resolveSubagentModelOverride(
|
|
671
|
+
params.modelOverride ?? agentConfig.model,
|
|
672
|
+
ctx.currentModel,
|
|
673
|
+
availableModels,
|
|
674
|
+
ctx.currentModelProvider,
|
|
663
675
|
);
|
|
676
|
+
const model = applyThinkingSuffix(primaryModel, agentConfig.thinking);
|
|
664
677
|
let spawnResult: { pid?: number; error?: string } = {};
|
|
665
678
|
try {
|
|
666
679
|
spawnResult = spawnRunner(
|
|
@@ -673,7 +686,7 @@ export function executeAsyncSingle(
|
|
|
673
686
|
cwd: runnerCwd,
|
|
674
687
|
model,
|
|
675
688
|
thinking: resolveEffectiveThinking(model, agentConfig.thinking),
|
|
676
|
-
modelCandidates: buildModelCandidates(
|
|
689
|
+
modelCandidates: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
|
|
677
690
|
applyThinkingSuffix(candidate, agentConfig.thinking),
|
|
678
691
|
),
|
|
679
692
|
tools: agentConfig.tools,
|
|
@@ -49,11 +49,6 @@ function formatResumeGuidance(runId: string | undefined, children: Array<{ agent
|
|
|
49
49
|
return "Resume: unavailable; no child session file was persisted.";
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function formatAcceptanceFinalizationSummary(finalization: NonNullable<NonNullable<AsyncStatus["steps"]>[number]["acceptance"]>["finalization"] | undefined): string {
|
|
53
|
-
if (!finalization) return "";
|
|
54
|
-
return `, finalization: ${finalization.status} after ${finalization.turns.length}/${finalization.maxTurns} turns`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
52
|
function stepLineLabel(status: AsyncStatus, index: number): string {
|
|
58
53
|
const steps = status.steps ?? [];
|
|
59
54
|
if (status.mode === "parallel") return `Agent ${index + 1}/${steps.length || 1}`;
|
|
@@ -222,8 +217,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
|
|
|
222
217
|
const modelThinking = formatModelThinking(step.model, step.thinking);
|
|
223
218
|
const modelText = modelThinking ? ` (${modelThinking})` : "";
|
|
224
219
|
const errorText = step.error ? `, error: ${step.error}` : "";
|
|
225
|
-
const
|
|
226
|
-
const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}${finalizationText}` : "";
|
|
220
|
+
const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}` : "";
|
|
227
221
|
const display = step.label ? `${step.label} (${step.agent})` : step.agent;
|
|
228
222
|
const phase = step.phase ? `[${step.phase}] ` : "";
|
|
229
223
|
lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${acceptanceText}${errorText}`);
|
|
@@ -8,8 +8,6 @@ import { appendJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
|
|
|
8
8
|
import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
|
|
9
9
|
import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
|
|
10
10
|
import {
|
|
11
|
-
type AcceptanceFinalizationTurn,
|
|
12
|
-
type AcceptanceLedger,
|
|
13
11
|
type ActivityState,
|
|
14
12
|
type ArtifactConfig,
|
|
15
13
|
type ArtifactPaths,
|
|
@@ -20,7 +18,6 @@ import {
|
|
|
20
18
|
type NestedRouteInfo,
|
|
21
19
|
type ResolvedControlConfig,
|
|
22
20
|
type SubagentRunMode,
|
|
23
|
-
type TokenUsage,
|
|
24
21
|
type Usage,
|
|
25
22
|
type WorkflowGraphSnapshot,
|
|
26
23
|
DEFAULT_MAX_OUTPUT,
|
|
@@ -54,7 +51,7 @@ import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested
|
|
|
54
51
|
import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
|
|
55
52
|
import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
|
|
56
53
|
import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
|
|
57
|
-
import { evaluateCompletionMutationGuard
|
|
54
|
+
import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
|
|
58
55
|
import {
|
|
59
56
|
createMutatingFailureState,
|
|
60
57
|
didMutatingToolFail,
|
|
@@ -67,6 +64,7 @@ import {
|
|
|
67
64
|
summarizeRecentMutatingFailures,
|
|
68
65
|
} from "../shared/long-running-guard.ts";
|
|
69
66
|
import { parseSessionTokens } from "../../shared/session-tokens.ts";
|
|
67
|
+
import type { TokenUsage } from "../../shared/types.ts";
|
|
70
68
|
import {
|
|
71
69
|
cleanupWorktrees,
|
|
72
70
|
createWorktrees,
|
|
@@ -79,19 +77,7 @@ import {
|
|
|
79
77
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
80
78
|
import { writeInitialProgressFile } from "../../shared/settings.ts";
|
|
81
79
|
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
82
|
-
import {
|
|
83
|
-
acceptanceFailureMessage,
|
|
84
|
-
acceptanceSelfReviewConfig,
|
|
85
|
-
attachFinalizationToLedger,
|
|
86
|
-
buildFinalizationProcessFailureLedger,
|
|
87
|
-
createFinalizationProcessFailureTurn,
|
|
88
|
-
createFinalizationTurn,
|
|
89
|
-
evaluateAcceptance,
|
|
90
|
-
formatAcceptanceFinalizationPrompt,
|
|
91
|
-
formatAcceptancePrompt,
|
|
92
|
-
shouldRunAcceptanceFinalization,
|
|
93
|
-
stripAcceptanceReport,
|
|
94
|
-
} from "../shared/acceptance.ts";
|
|
80
|
+
import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, formatAcceptancePrompt, stripAcceptanceReport } from "../shared/acceptance.ts";
|
|
95
81
|
|
|
96
82
|
interface SubagentRunConfig {
|
|
97
83
|
id: string;
|
|
@@ -139,7 +125,7 @@ interface StepResult {
|
|
|
139
125
|
structuredOutput?: unknown;
|
|
140
126
|
structuredOutputPath?: string;
|
|
141
127
|
structuredOutputSchemaPath?: string;
|
|
142
|
-
acceptance?: AcceptanceLedger;
|
|
128
|
+
acceptance?: import("../../shared/types.ts").AcceptanceLedger;
|
|
143
129
|
}
|
|
144
130
|
|
|
145
131
|
const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
|
|
@@ -613,7 +599,7 @@ async function runSingleStep(
|
|
|
613
599
|
structuredOutput?: unknown;
|
|
614
600
|
structuredOutputPath?: string;
|
|
615
601
|
structuredOutputSchemaPath?: string;
|
|
616
|
-
acceptance?: AcceptanceLedger;
|
|
602
|
+
acceptance?: import("../../shared/types.ts").AcceptanceLedger;
|
|
617
603
|
}> {
|
|
618
604
|
const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
|
|
619
605
|
? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"))
|
|
@@ -716,15 +702,7 @@ async function runSingleStep(
|
|
|
716
702
|
if (structured.error) structuredError = structured.error;
|
|
717
703
|
else structuredOutput = structured.value;
|
|
718
704
|
}
|
|
719
|
-
const
|
|
720
|
-
agent: step.agent,
|
|
721
|
-
task: taskForCompletionGuard,
|
|
722
|
-
completionGuardEnabled: step.completionGuard !== false,
|
|
723
|
-
usesAcceptanceContract: step.effectiveAcceptance?.explicit === true,
|
|
724
|
-
tools: step.tools,
|
|
725
|
-
mcpDirectTools: step.mcpDirectTools,
|
|
726
|
-
});
|
|
727
|
-
const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && completionPolicy === "mutation-guard"
|
|
705
|
+
const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && step.completionGuard !== false
|
|
728
706
|
? evaluateCompletionMutationGuard({
|
|
729
707
|
agent: step.agent,
|
|
730
708
|
task: taskForCompletionGuard,
|
|
@@ -737,7 +715,7 @@ async function runSingleStep(
|
|
|
737
715
|
const completionGuardError = completionGuardTriggered
|
|
738
716
|
? "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes."
|
|
739
717
|
: undefined;
|
|
740
|
-
const effectiveExitCode =
|
|
718
|
+
const effectiveExitCode = completionGuardTriggered
|
|
741
719
|
? 1
|
|
742
720
|
: structuredError
|
|
743
721
|
? 1
|
|
@@ -765,7 +743,7 @@ async function runSingleStep(
|
|
|
765
743
|
completionGuardTriggeredFinal = completionGuardTriggered;
|
|
766
744
|
finalOutputSnapshot = outputSnapshot;
|
|
767
745
|
finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
|
|
768
|
-
if (attempt.success ||
|
|
746
|
+
if (attempt.success || completionGuardTriggered) break;
|
|
769
747
|
if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
|
|
770
748
|
attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
|
|
771
749
|
}
|
|
@@ -778,12 +756,12 @@ async function runSingleStep(
|
|
|
778
756
|
const output = resolvedOutput.fullOutput;
|
|
779
757
|
const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
|
|
780
758
|
let outputForSummary = output;
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
759
|
+
if (attemptNotes.length > 0) {
|
|
760
|
+
outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
|
|
761
|
+
}
|
|
784
762
|
const outputForAcceptance = rawOutput;
|
|
785
|
-
|
|
786
|
-
|
|
763
|
+
const finalizedOutput = finalizeSingleOutput({
|
|
764
|
+
fullOutput: outputForSummary,
|
|
787
765
|
outputPath: step.outputPath,
|
|
788
766
|
outputMode: step.outputMode,
|
|
789
767
|
exitCode: finalResult?.exitCode ?? 1,
|
|
@@ -792,115 +770,13 @@ async function runSingleStep(
|
|
|
792
770
|
saveError: resolvedOutput.saveError,
|
|
793
771
|
});
|
|
794
772
|
outputForSummary = finalizedOutput.displayOutput;
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
output: outputForAcceptance,
|
|
802
|
-
cwd: step.cwd ?? ctx.cwd,
|
|
803
|
-
})
|
|
773
|
+
const acceptance = step.effectiveAcceptance
|
|
774
|
+
? await evaluateAcceptance({
|
|
775
|
+
acceptance: step.effectiveAcceptance,
|
|
776
|
+
output: outputForAcceptance,
|
|
777
|
+
cwd: step.cwd ?? ctx.cwd,
|
|
778
|
+
})
|
|
804
779
|
: undefined;
|
|
805
|
-
if (acceptance && step.effectiveAcceptance && shouldRunAcceptanceFinalization(step.effectiveAcceptance) && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted) {
|
|
806
|
-
const sessionFile = step.sessionFile ?? (sessionDir ? findLatestSessionFile(sessionDir) ?? undefined : undefined);
|
|
807
|
-
const maxTurns = step.effectiveAcceptance.finalization.maxTurns;
|
|
808
|
-
const turns: AcceptanceFinalizationTurn[] = [];
|
|
809
|
-
if (!sessionFile) {
|
|
810
|
-
const message = "Acceptance finalization requires a session file for same-session continuation.";
|
|
811
|
-
turns.push(createFinalizationProcessFailureTurn({ turn: 1, prompt: "", message }));
|
|
812
|
-
acceptance = buildFinalizationProcessFailureLedger({ initialLedger: acceptance, turns, maxTurns, message });
|
|
813
|
-
} else {
|
|
814
|
-
const selfReviewAcceptance = acceptanceSelfReviewConfig(step.effectiveAcceptance);
|
|
815
|
-
let previousFailure = acceptanceFailureMessage(acceptance);
|
|
816
|
-
let authoritativeLedger = acceptance;
|
|
817
|
-
for (let turn = 1; turn <= maxTurns; turn++) {
|
|
818
|
-
const prompt = formatAcceptanceFinalizationPrompt({
|
|
819
|
-
acceptance: step.effectiveAcceptance,
|
|
820
|
-
initialOutput: outputForAcceptance,
|
|
821
|
-
initialLedger: acceptance,
|
|
822
|
-
turn,
|
|
823
|
-
maxTurns,
|
|
824
|
-
...(previousFailure ? { previousFailure } : {}),
|
|
825
|
-
});
|
|
826
|
-
const { args, env, tempDir } = buildPiArgs({
|
|
827
|
-
baseArgs: ["--mode", "json", "-p"],
|
|
828
|
-
task: prompt,
|
|
829
|
-
sessionEnabled: true,
|
|
830
|
-
sessionFile,
|
|
831
|
-
model: finalResult?.model ?? step.model,
|
|
832
|
-
thinking: step.thinking,
|
|
833
|
-
inheritProjectContext: step.inheritProjectContext,
|
|
834
|
-
inheritSkills: step.inheritSkills,
|
|
835
|
-
tools: step.tools,
|
|
836
|
-
extensions: step.extensions,
|
|
837
|
-
systemPrompt: step.systemPrompt,
|
|
838
|
-
systemPromptMode: step.systemPromptMode,
|
|
839
|
-
mcpDirectTools: step.mcpDirectTools,
|
|
840
|
-
cwd: step.cwd ?? ctx.cwd,
|
|
841
|
-
promptFileStem: `${step.agent}-acceptance-finalization`,
|
|
842
|
-
intercomSessionName: ctx.childIntercomTarget,
|
|
843
|
-
orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
|
|
844
|
-
runId: ctx.id,
|
|
845
|
-
childAgentName: step.agent,
|
|
846
|
-
childIndex: ctx.flatIndex,
|
|
847
|
-
parentEventSink: ctx.nestedRoute?.eventSink,
|
|
848
|
-
parentControlInbox: ctx.nestedRoute?.controlInbox,
|
|
849
|
-
parentRootRunId: ctx.nestedRoute?.rootRunId,
|
|
850
|
-
parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
|
|
851
|
-
});
|
|
852
|
-
ctx.onAttemptStart?.({ model: finalResult?.model ?? step.model, thinking: resolveEffectiveThinking(finalResult?.model ?? step.model, step.thinking) });
|
|
853
|
-
const finalizationRun = await runPiStreaming(
|
|
854
|
-
args,
|
|
855
|
-
step.cwd ?? ctx.cwd,
|
|
856
|
-
`${ctx.outputFile}.finalization-${turn}.log`,
|
|
857
|
-
env,
|
|
858
|
-
ctx.piPackageRoot,
|
|
859
|
-
ctx.piArgv1,
|
|
860
|
-
step.maxSubagentDepth,
|
|
861
|
-
{ eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
|
|
862
|
-
ctx.registerInterrupt,
|
|
863
|
-
ctx.onChildEvent,
|
|
864
|
-
);
|
|
865
|
-
cleanupTempDir(tempDir);
|
|
866
|
-
modelAttempts.push({
|
|
867
|
-
model: finalResult?.model ?? finalizationRun.model ?? step.model ?? "default",
|
|
868
|
-
success: finalizationRun.exitCode === 0 && !finalizationRun.error,
|
|
869
|
-
exitCode: finalizationRun.exitCode,
|
|
870
|
-
error: finalizationRun.error,
|
|
871
|
-
usage: finalizationRun.usage,
|
|
872
|
-
});
|
|
873
|
-
const finalizationOutput = finalizationRun.finalOutput;
|
|
874
|
-
if (finalizationRun.exitCode !== 0 || finalizationRun.error || finalizationRun.interrupted) {
|
|
875
|
-
const message = finalizationRun.error ?? "Acceptance finalization turn did not complete successfully.";
|
|
876
|
-
turns.push(createFinalizationProcessFailureTurn({ turn, prompt, rawOutput: finalizationOutput, message }));
|
|
877
|
-
acceptance = buildFinalizationProcessFailureLedger({ initialLedger: acceptance, turns, maxTurns, message });
|
|
878
|
-
break;
|
|
879
|
-
}
|
|
880
|
-
const selfReviewLedger = await evaluateAcceptance({
|
|
881
|
-
acceptance: selfReviewAcceptance,
|
|
882
|
-
output: finalizationOutput,
|
|
883
|
-
cwd: step.cwd ?? ctx.cwd,
|
|
884
|
-
});
|
|
885
|
-
authoritativeLedger = selfReviewLedger;
|
|
886
|
-
turns.push(createFinalizationTurn({ turn, prompt, rawOutput: finalizationOutput, ledger: selfReviewLedger }));
|
|
887
|
-
const failure = acceptanceFailureMessage(selfReviewLedger);
|
|
888
|
-
if (!failure) {
|
|
889
|
-
authoritativeLedger = step.effectiveAcceptance === selfReviewAcceptance
|
|
890
|
-
? selfReviewLedger
|
|
891
|
-
: await evaluateAcceptance({
|
|
892
|
-
acceptance: step.effectiveAcceptance,
|
|
893
|
-
output: finalizationOutput,
|
|
894
|
-
cwd: step.cwd ?? ctx.cwd,
|
|
895
|
-
});
|
|
896
|
-
acceptance = attachFinalizationToLedger({ initialLedger: acceptance, authoritativeLedger, turns, status: "completed", maxTurns });
|
|
897
|
-
break;
|
|
898
|
-
}
|
|
899
|
-
previousFailure = failure;
|
|
900
|
-
if (turn === maxTurns) acceptance = attachFinalizationToLedger({ initialLedger: acceptance, authoritativeLedger, turns, status: "failed", maxTurns });
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
780
|
const acceptanceFailure = acceptance ? acceptanceFailureMessage(acceptance) : undefined;
|
|
905
781
|
const acceptanceCanFailRun = acceptanceFailure && acceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted;
|
|
906
782
|
const effectiveFinalExitCode = acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
|
|
@@ -1236,7 +1112,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1236
1112
|
writeAtomicJson(statusPath, statusPayload);
|
|
1237
1113
|
emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
|
|
1238
1114
|
};
|
|
1239
|
-
const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running", error?: string, acceptance?: AcceptanceLedger): void => {
|
|
1115
|
+
const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running", error?: string, acceptance?: import("../../shared/types.ts").AcceptanceLedger): void => {
|
|
1240
1116
|
const groupNode = statusPayload.workflowGraph?.nodes.find((node) => node.id === `step-${stepIndex}`);
|
|
1241
1117
|
if (!groupNode) return;
|
|
1242
1118
|
groupNode.status = status;
|
|
@@ -1578,9 +1454,36 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1578
1454
|
placeholder.durationMs = 0;
|
|
1579
1455
|
}
|
|
1580
1456
|
previousOutput = "Dynamic fanout produced 0 results.";
|
|
1457
|
+
const groupAcceptance = step.effectiveAcceptance?.explicit
|
|
1458
|
+
? await evaluateAcceptance({
|
|
1459
|
+
acceptance: step.effectiveAcceptance,
|
|
1460
|
+
output: "",
|
|
1461
|
+
report: aggregateAcceptanceReport({
|
|
1462
|
+
results: [],
|
|
1463
|
+
notes: "Dynamic fanout produced 0 results.",
|
|
1464
|
+
}),
|
|
1465
|
+
cwd,
|
|
1466
|
+
})
|
|
1467
|
+
: undefined;
|
|
1468
|
+
if (placeholder && groupAcceptance) placeholder.acceptance = groupAcceptance;
|
|
1469
|
+
const groupAcceptanceFailure = groupAcceptance ? acceptanceFailureMessage(groupAcceptance) : undefined;
|
|
1470
|
+
if (groupAcceptanceFailure) {
|
|
1471
|
+
statusPayload.state = "failed";
|
|
1472
|
+
statusPayload.error = groupAcceptanceFailure;
|
|
1473
|
+
if (placeholder) {
|
|
1474
|
+
placeholder.status = "failed";
|
|
1475
|
+
placeholder.error = groupAcceptanceFailure;
|
|
1476
|
+
placeholder.exitCode = 1;
|
|
1477
|
+
}
|
|
1478
|
+
markDynamicGraphGroup(stepIndex, "failed", groupAcceptanceFailure, groupAcceptance);
|
|
1479
|
+
statusPayload.lastUpdate = now;
|
|
1480
|
+
writeStatusPayload();
|
|
1481
|
+
results.push({ agent: step.parallel.agent, output: groupAcceptanceFailure, error: groupAcceptanceFailure, success: false, exitCode: 1, acceptance: groupAcceptance });
|
|
1482
|
+
break;
|
|
1483
|
+
}
|
|
1581
1484
|
flatIndex++;
|
|
1582
1485
|
statusPayload.lastUpdate = now;
|
|
1583
|
-
markDynamicGraphGroup(stepIndex, "completed");
|
|
1486
|
+
markDynamicGraphGroup(stepIndex, "completed", undefined, groupAcceptance);
|
|
1584
1487
|
writeStatusPayload();
|
|
1585
1488
|
continue;
|
|
1586
1489
|
}
|
|
@@ -1756,7 +1659,31 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
1756
1659
|
stepIndex,
|
|
1757
1660
|
};
|
|
1758
1661
|
statusPayload.outputs = outputs;
|
|
1759
|
-
|
|
1662
|
+
const groupAcceptance = step.effectiveAcceptance
|
|
1663
|
+
? await evaluateAcceptance({
|
|
1664
|
+
acceptance: step.effectiveAcceptance,
|
|
1665
|
+
output: "",
|
|
1666
|
+
report: aggregateAcceptanceReport({
|
|
1667
|
+
results: parallelResults,
|
|
1668
|
+
notes: `Dynamic fanout collected ${collection.length} result(s) into ${step.collect.as}.`,
|
|
1669
|
+
}),
|
|
1670
|
+
cwd,
|
|
1671
|
+
})
|
|
1672
|
+
: undefined;
|
|
1673
|
+
const groupAcceptanceFailure = groupAcceptance ? acceptanceFailureMessage(groupAcceptance) : undefined;
|
|
1674
|
+
markDynamicGraphGroup(stepIndex, groupAcceptanceFailure ? "failed" : "completed", groupAcceptanceFailure, groupAcceptance);
|
|
1675
|
+
if (groupAcceptanceFailure) {
|
|
1676
|
+
results.push({
|
|
1677
|
+
agent: step.parallel.agent,
|
|
1678
|
+
output: groupAcceptanceFailure,
|
|
1679
|
+
error: groupAcceptanceFailure,
|
|
1680
|
+
success: false,
|
|
1681
|
+
exitCode: 1,
|
|
1682
|
+
structuredOutput: collection,
|
|
1683
|
+
acceptance: groupAcceptance,
|
|
1684
|
+
});
|
|
1685
|
+
statusPayload.error = groupAcceptanceFailure;
|
|
1686
|
+
}
|
|
1760
1687
|
} catch (error) {
|
|
1761
1688
|
const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
|
|
1762
1689
|
results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
|