@narumitw/pi-subagents 0.49.2 → 0.51.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/README.md +313 -53
- package/package.json +11 -8
- package/src/adaptive-scheduler.ts +196 -0
- package/src/admission-benchmark.ts +95 -0
- package/src/admission-policy.ts +78 -0
- package/src/agent-projection.ts +53 -0
- package/src/agents.ts +58 -1
- package/src/auto-transport.ts +114 -0
- package/src/blocking-status.ts +63 -0
- package/src/capabilities.ts +145 -0
- package/src/capability-grant.ts +115 -0
- package/src/capability-router.ts +107 -0
- package/src/completion-delivery.ts +257 -0
- package/src/config-status.ts +221 -0
- package/src/config-ui.ts +215 -236
- package/src/consult-resources.ts +4 -27
- package/src/consult.ts +9 -1
- package/src/create-stateful-transport.ts +55 -0
- package/src/delegation-contract.ts +417 -0
- package/src/execution-plan.ts +322 -0
- package/src/execution-profiles.ts +95 -0
- package/src/execution-ui.ts +320 -0
- package/src/execution.ts +848 -158
- package/src/in-process-transport.ts +269 -25
- package/src/inspect-render.ts +101 -1
- package/src/inspect.ts +296 -3
- package/src/integration-controller.ts +98 -0
- package/src/limits.ts +3 -0
- package/src/orchestration-metrics.ts +78 -0
- package/src/outcome.ts +61 -0
- package/src/panel-child-group.ts +35 -0
- package/src/panel-contract.ts +343 -0
- package/src/panel-evidence.ts +59 -0
- package/src/panel-execution.ts +772 -0
- package/src/panel-failure.ts +56 -0
- package/src/panel-planning.ts +175 -0
- package/src/panel-prompts.ts +132 -0
- package/src/panel-reconciliation.ts +57 -0
- package/src/panel-render.ts +103 -0
- package/src/parallel-limit-ui.ts +112 -0
- package/src/params.ts +172 -3
- package/src/persistence.ts +182 -32
- package/src/prompt-resources.ts +38 -0
- package/src/registry-types.ts +175 -0
- package/src/registry.ts +466 -143
- package/src/render.ts +72 -6
- package/src/result-contract.ts +416 -0
- package/src/retained-semantic-state.ts +100 -0
- package/src/rpc-timeout-finalization.ts +207 -0
- package/src/rpc-transport-metadata.ts +65 -0
- package/src/rpc-transport.ts +990 -0
- package/src/rpc-turn-capture.ts +142 -0
- package/src/runner-result.ts +55 -0
- package/src/runner-usage.ts +48 -0
- package/src/runner.ts +325 -73
- package/src/semantic-snapshot.ts +214 -0
- package/src/settings.ts +254 -35
- package/src/spawn-idempotency.ts +61 -0
- package/src/stateful-config.ts +13 -0
- package/src/stateful-guidance.ts +1 -0
- package/src/stateful-lifecycle.ts +45 -2
- package/src/stateful-limit-ui.ts +246 -0
- package/src/stateful-limits.ts +96 -0
- package/src/stateful-prompt.ts +11 -2
- package/src/stateful-render.ts +48 -3
- package/src/stateful.ts +467 -357
- package/src/subagents.ts +114 -46
- package/src/subprocess-transport.ts +64 -5
- package/src/supervision.ts +103 -0
- package/src/timeout-checkpoint.ts +305 -0
- package/src/timeout-finalization.ts +75 -0
- package/src/transport-types.ts +68 -0
- package/src/transport-ui.ts +169 -0
- package/src/transport.ts +16 -4
- package/src/turn-budget.ts +109 -0
- package/src/verification-policy.ts +17 -0
- package/src/work-item-ledger.ts +682 -0
- package/src/work-item-persistence.ts +218 -0
- package/src/workflow-planning.ts +150 -0
- package/src/workflow-ui.ts +61 -0
- package/src/workspace.ts +69 -12
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
import type { AgentToolResult, AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { AgentConfig, AgentScope, SubagentThinkingLevel } from "./agents.js";
|
|
4
|
+
import { panelReviewStatus, panelSynthesisStatus, startSubagentStatus } from "./blocking-status.js";
|
|
5
|
+
import { issueCapabilityGrant, revokeCapabilityGrant } from "./capability-grant.js";
|
|
6
|
+
import type { ResolvedSubagentTarget } from "./cwd-policy.js";
|
|
7
|
+
import { targetPolicyAudit } from "./cwd-policy.js";
|
|
8
|
+
import type { DelegationContract } from "./delegation-contract.js";
|
|
9
|
+
import {
|
|
10
|
+
acknowledgeExecutionPlan,
|
|
11
|
+
createExecutionPlan,
|
|
12
|
+
resolveContractTools,
|
|
13
|
+
} from "./execution-plan.js";
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_MAX_OUTPUT_BYTES,
|
|
16
|
+
MAX_BLOCKING_PARALLEL_CONCURRENCY,
|
|
17
|
+
truncateUtf8,
|
|
18
|
+
} from "./limits.js";
|
|
19
|
+
import { calculateOrchestrationMetrics } from "./orchestration-metrics.js";
|
|
20
|
+
import { PanelChildGroup } from "./panel-child-group.js";
|
|
21
|
+
import { type PanelReview, parsePanelReview, parsePanelSynthesis } from "./panel-contract.js";
|
|
22
|
+
import { PanelEvidenceLedger } from "./panel-evidence.js";
|
|
23
|
+
import { classifyPanelFailure, type PanelFailure } from "./panel-failure.js";
|
|
24
|
+
import {
|
|
25
|
+
createPanelWorkLedger,
|
|
26
|
+
type PanelPreset,
|
|
27
|
+
planPanelBudgets,
|
|
28
|
+
validatePanelRequest,
|
|
29
|
+
} from "./panel-planning.js";
|
|
30
|
+
import { buildPanelReviewerPrompt, buildPanelSynthesisPrompt } from "./panel-prompts.js";
|
|
31
|
+
import { reconcilePanel } from "./panel-reconciliation.js";
|
|
32
|
+
import type { SubagentParams } from "./params.js";
|
|
33
|
+
import {
|
|
34
|
+
type ChildLaunchPolicy,
|
|
35
|
+
getResultFinalOutput,
|
|
36
|
+
isResultError,
|
|
37
|
+
mapWithConcurrencyLimit,
|
|
38
|
+
runSingleAgent,
|
|
39
|
+
type SingleResult,
|
|
40
|
+
type SubagentDetails,
|
|
41
|
+
} from "./runner.js";
|
|
42
|
+
import { boundedPrivateText, boundText } from "./safe-text.js";
|
|
43
|
+
import { createSessionWorkItemPersistence } from "./work-item-persistence.js";
|
|
44
|
+
import { assertWorkspaceIsolationReady } from "./workspace.js";
|
|
45
|
+
|
|
46
|
+
export interface PanelExecutionInput {
|
|
47
|
+
toolCallId: string;
|
|
48
|
+
params: SubagentParams;
|
|
49
|
+
panel: NonNullable<SubagentParams["panel"]>;
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
onUpdate?: AgentToolUpdateCallback<SubagentDetails>;
|
|
52
|
+
ctx: ExtensionContext;
|
|
53
|
+
agents: AgentConfig[];
|
|
54
|
+
agentScope: AgentScope;
|
|
55
|
+
projectAgentsDir: string | null;
|
|
56
|
+
maxParallelTasks: number;
|
|
57
|
+
target: ResolvedSubagentTarget;
|
|
58
|
+
resolveThinkingLevel: (
|
|
59
|
+
agentName: string,
|
|
60
|
+
local?: SubagentThinkingLevel,
|
|
61
|
+
) => SubagentThinkingLevel | undefined;
|
|
62
|
+
resolveTimeoutMs: (agentName: string, local?: number) => number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type PanelToolResult = AgentToolResult<SubagentDetails> & { isError?: boolean };
|
|
66
|
+
|
|
67
|
+
const PANEL_READ_ONLY_TOOLS = new Set(["read", "grep", "find", "ls"]);
|
|
68
|
+
|
|
69
|
+
export async function preflightPanelExecution(
|
|
70
|
+
input: Pick<
|
|
71
|
+
PanelExecutionInput,
|
|
72
|
+
"panel" | "agents" | "target" | "resolveThinkingLevel" | "resolveTimeoutMs" | "signal"
|
|
73
|
+
>,
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
assertPanelActive(input.signal);
|
|
76
|
+
let requiresWorktree = false;
|
|
77
|
+
for (const [index, reviewer] of input.panel.reviewers.entries()) {
|
|
78
|
+
assertPanelActive(input.signal);
|
|
79
|
+
const agent = input.agents.find((candidate) => candidate.name === reviewer.agent);
|
|
80
|
+
if (!agent) throw new Error(`Unknown panel agent: ${reviewer.agent}`);
|
|
81
|
+
requiresWorktree ||= !isReadOnlyAgent(agent);
|
|
82
|
+
const policy = launchPolicy(
|
|
83
|
+
input as PanelExecutionInput,
|
|
84
|
+
reviewer.agent,
|
|
85
|
+
input.resolveThinkingLevel(reviewer.agent, reviewer.thinkingLevel),
|
|
86
|
+
input.panel.task,
|
|
87
|
+
input.resolveTimeoutMs(reviewer.agent, reviewer.timeoutMs),
|
|
88
|
+
index + 1,
|
|
89
|
+
isReadOnlyAgent(agent) ? "shared" : "worktree",
|
|
90
|
+
"review",
|
|
91
|
+
);
|
|
92
|
+
if (policy.capabilityGrant) {
|
|
93
|
+
revokeCapabilityGrant(policy.capabilityGrant, "preflight-complete", Date.now());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (requiresWorktree) {
|
|
97
|
+
await assertWorkspaceIsolationReady(input.target.cwd);
|
|
98
|
+
assertPanelActive(input.signal);
|
|
99
|
+
}
|
|
100
|
+
const synthesizer = input.panel.synthesizer;
|
|
101
|
+
const policy = launchPolicy(
|
|
102
|
+
input as PanelExecutionInput,
|
|
103
|
+
synthesizer.agent,
|
|
104
|
+
input.resolveThinkingLevel(synthesizer.agent, synthesizer.thinkingLevel),
|
|
105
|
+
"Panel synthesis",
|
|
106
|
+
input.resolveTimeoutMs(synthesizer.agent, synthesizer.timeoutMs),
|
|
107
|
+
input.panel.reviewers.length + 1,
|
|
108
|
+
"shared",
|
|
109
|
+
"tool-less",
|
|
110
|
+
);
|
|
111
|
+
if (policy.capabilityGrant) {
|
|
112
|
+
revokeCapabilityGrant(policy.capabilityGrant, "preflight-complete", Date.now());
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function executePanel(input: PanelExecutionInput): Promise<PanelToolResult> {
|
|
117
|
+
validatePanelRequest(input.panel, input.maxParallelTasks);
|
|
118
|
+
const panelId = input.panel.id ?? `panel-${input.toolCallId}`;
|
|
119
|
+
const preset: PanelPreset = input.panel.preset ?? "custom";
|
|
120
|
+
const minValidReviews = input.panel.minValidReviews ?? 2;
|
|
121
|
+
const requestedReviewTimeout = Math.max(
|
|
122
|
+
...input.panel.reviewers.map((reviewer) =>
|
|
123
|
+
input.resolveTimeoutMs(reviewer.agent, reviewer.timeoutMs),
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
const requestedSynthesisTimeout = input.resolveTimeoutMs(
|
|
127
|
+
input.panel.synthesizer.agent,
|
|
128
|
+
input.panel.synthesizer.timeoutMs,
|
|
129
|
+
);
|
|
130
|
+
const totalMs =
|
|
131
|
+
input.params.totalTimeoutMs ?? requestedReviewTimeout + requestedSynthesisTimeout + 60_000;
|
|
132
|
+
const budgets = planPanelBudgets(totalMs, input.panel.reviewers.length);
|
|
133
|
+
const panelStartedAt = Date.now();
|
|
134
|
+
const failureMessageLimit = Math.max(128, Math.floor((8 * 1024) / input.panel.reviewers.length));
|
|
135
|
+
const requiredAgents = [
|
|
136
|
+
...input.panel.reviewers.map((reviewer) => reviewer.agent),
|
|
137
|
+
input.panel.synthesizer.agent,
|
|
138
|
+
];
|
|
139
|
+
for (const agentName of requiredAgents) {
|
|
140
|
+
if (!input.agents.some((agent) => agent.name === agentName)) {
|
|
141
|
+
throw new Error(`Unknown panel agent: ${agentName}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const group = new PanelChildGroup(input.signal);
|
|
146
|
+
const ledger = new PanelEvidenceLedger(panelId, input.panel.reviewers.length);
|
|
147
|
+
const workLedger = createPanelWorkLedger(panelId, input.panel, input.agents);
|
|
148
|
+
const persistenceOwner =
|
|
149
|
+
input.ctx.sessionManager.getSessionId?.() ??
|
|
150
|
+
input.ctx.sessionManager.getSessionFile?.() ??
|
|
151
|
+
`ephemeral:${input.ctx.cwd}`;
|
|
152
|
+
const workPersistence = createSessionWorkItemPersistence(persistenceOwner, panelId);
|
|
153
|
+
const persistWork = () => workPersistence.save(workLedger.snapshot());
|
|
154
|
+
const workspaceByReviewer = new Map<string, string>();
|
|
155
|
+
let output: PanelToolResult | undefined;
|
|
156
|
+
const results: SingleResult[] = [];
|
|
157
|
+
const failures: PanelFailure[] = [];
|
|
158
|
+
let panelDetails = createPanelDetails({
|
|
159
|
+
panelId,
|
|
160
|
+
preset,
|
|
161
|
+
reviewerIds: input.panel.reviewers.map((reviewer) => reviewer.id),
|
|
162
|
+
sharedTask: input.panel.task,
|
|
163
|
+
budgets,
|
|
164
|
+
});
|
|
165
|
+
const makeDetails = (currentResults: SingleResult[] = results): SubagentDetails => ({
|
|
166
|
+
mode: "panel",
|
|
167
|
+
agentScope: input.agentScope,
|
|
168
|
+
projectAgentsDir: input.projectAgentsDir,
|
|
169
|
+
results: [...currentResults],
|
|
170
|
+
workflow: workLedger.snapshot(),
|
|
171
|
+
metrics: calculateOrchestrationMetrics(workLedger.snapshot(), currentResults, panelDetails),
|
|
172
|
+
panel: panelDetails,
|
|
173
|
+
});
|
|
174
|
+
const status = startSubagentStatus(
|
|
175
|
+
input.ctx,
|
|
176
|
+
input.toolCallId,
|
|
177
|
+
panelReviewStatus(0, input.panel.reviewers.length, input.panel.reviewers.length),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
await persistWork();
|
|
182
|
+
assertPanelActive(group.signal);
|
|
183
|
+
for (const reviewer of input.panel.reviewers) {
|
|
184
|
+
assertPanelActive(group.signal);
|
|
185
|
+
const agent = input.agents.find(
|
|
186
|
+
(candidate) => candidate.name === reviewer.agent,
|
|
187
|
+
) as AgentConfig;
|
|
188
|
+
if (isReadOnlyAgent(agent)) continue;
|
|
189
|
+
const workspace = await group.createWorkspace(`${panelId}:${reviewer.id}`, input.target.cwd);
|
|
190
|
+
assertPanelActive(group.signal);
|
|
191
|
+
workspaceByReviewer.set(reviewer.id, workspace.path);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let completed = 0;
|
|
195
|
+
const reviewPhaseStartedAt = panelStartedAt;
|
|
196
|
+
const reviewResults = await mapWithConcurrencyLimit(
|
|
197
|
+
input.panel.reviewers,
|
|
198
|
+
Math.min(MAX_BLOCKING_PARALLEL_CONCURRENCY, input.maxParallelTasks),
|
|
199
|
+
async (reviewer, index) => {
|
|
200
|
+
const workItemId = `review:${reviewer.id}`;
|
|
201
|
+
const startedWork = workLedger.start(workItemId, `agent:${reviewer.agent}`);
|
|
202
|
+
const taskGeneration = startedWork.taskGeneration;
|
|
203
|
+
const prompt = buildPanelReviewerPrompt({
|
|
204
|
+
panelId,
|
|
205
|
+
preset,
|
|
206
|
+
task: input.panel.task,
|
|
207
|
+
context: input.panel.context,
|
|
208
|
+
reviewerId: reviewer.id,
|
|
209
|
+
focus: reviewer.focus,
|
|
210
|
+
});
|
|
211
|
+
const phaseRemaining = budgets.reviewMs - (Date.now() - reviewPhaseStartedAt);
|
|
212
|
+
const reviewBudgetAvailable = phaseRemaining >= 1;
|
|
213
|
+
const timeoutMs = Math.min(
|
|
214
|
+
input.resolveTimeoutMs(reviewer.agent, reviewer.timeoutMs),
|
|
215
|
+
phaseRemaining,
|
|
216
|
+
);
|
|
217
|
+
const thinkingLevel = input.resolveThinkingLevel(reviewer.agent, reviewer.thinkingLevel);
|
|
218
|
+
let artifactRevision = 0;
|
|
219
|
+
let latestFingerprint = "";
|
|
220
|
+
let repeatedEvidenceUpdates = 0;
|
|
221
|
+
const reviewerController = new AbortController();
|
|
222
|
+
const reviewerSignal = AbortSignal.any([group.signal, reviewerController.signal]);
|
|
223
|
+
const publishOutput = (candidate: SingleResult): PanelReview | undefined => {
|
|
224
|
+
const parsed = parsePanelReview(getResultFinalOutput(candidate), reviewer.id, {
|
|
225
|
+
agent: reviewer.agent,
|
|
226
|
+
model: candidate.actualModel ?? candidate.model,
|
|
227
|
+
taskGeneration,
|
|
228
|
+
});
|
|
229
|
+
if (!parsed) return undefined;
|
|
230
|
+
const fingerprint = JSON.stringify(parsed);
|
|
231
|
+
if (fingerprint === latestFingerprint) {
|
|
232
|
+
repeatedEvidenceUpdates += 1;
|
|
233
|
+
if (repeatedEvidenceUpdates >= 8 && !reviewerController.signal.aborted) {
|
|
234
|
+
reviewerController.abort("semantic-stall");
|
|
235
|
+
}
|
|
236
|
+
return parsed;
|
|
237
|
+
}
|
|
238
|
+
artifactRevision += 1;
|
|
239
|
+
if (!ledger.publish(parsed, artifactRevision)) {
|
|
240
|
+
artifactRevision -= 1;
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
latestFingerprint = fingerprint;
|
|
244
|
+
repeatedEvidenceUpdates = 0;
|
|
245
|
+
return parsed;
|
|
246
|
+
};
|
|
247
|
+
const runAttempt = async (attempt: number): Promise<SingleResult> => {
|
|
248
|
+
if (!reviewBudgetAvailable) {
|
|
249
|
+
return panelReviewerBudgetExhaustedResult(
|
|
250
|
+
input.agents,
|
|
251
|
+
reviewer.agent,
|
|
252
|
+
input.panel.task,
|
|
253
|
+
thinkingLevel,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const result = await runSingleAgent(
|
|
257
|
+
input.ctx.cwd,
|
|
258
|
+
input.agents,
|
|
259
|
+
reviewer.agent,
|
|
260
|
+
prompt,
|
|
261
|
+
workspaceByReviewer.get(reviewer.id) ?? input.target.cwd,
|
|
262
|
+
index + 1,
|
|
263
|
+
reviewerSignal,
|
|
264
|
+
thinkingLevel,
|
|
265
|
+
timeoutMs,
|
|
266
|
+
(partial) => {
|
|
267
|
+
const partialResult = partial.details?.results[0];
|
|
268
|
+
if (partialResult) publishOutput(partialResult);
|
|
269
|
+
input.onUpdate?.({
|
|
270
|
+
content: partial.content,
|
|
271
|
+
details: makeDetails(),
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
makeDetails,
|
|
275
|
+
undefined,
|
|
276
|
+
launchPolicy(
|
|
277
|
+
input,
|
|
278
|
+
reviewer.agent,
|
|
279
|
+
thinkingLevel,
|
|
280
|
+
`Panel review ${reviewer.id}`,
|
|
281
|
+
timeoutMs,
|
|
282
|
+
taskGeneration,
|
|
283
|
+
workspaceByReviewer.has(reviewer.id) ? "worktree" : "shared",
|
|
284
|
+
"review",
|
|
285
|
+
{
|
|
286
|
+
idleTimeoutMs: reviewer.idleTimeoutMs ?? input.params.idleTimeoutMs,
|
|
287
|
+
maxTurns: reviewer.maxTurns ?? input.params.maxTurns,
|
|
288
|
+
maxToolCalls: reviewer.maxToolCalls ?? input.params.maxToolCalls,
|
|
289
|
+
},
|
|
290
|
+
reviewPhaseStartedAt + budgets.reviewMs + budgets.finalizationMs,
|
|
291
|
+
),
|
|
292
|
+
);
|
|
293
|
+
result.target = targetPolicyAudit(input.target);
|
|
294
|
+
result.attemptCount = attempt;
|
|
295
|
+
if (
|
|
296
|
+
reviewerController.signal.reason === "semantic-stall" &&
|
|
297
|
+
result.aborted &&
|
|
298
|
+
!group.signal.aborted
|
|
299
|
+
) {
|
|
300
|
+
result.aborted = false;
|
|
301
|
+
result.stopReason = "semantic-stall";
|
|
302
|
+
result.errorMessage =
|
|
303
|
+
"Panel reviewer stopped after repeated updates without new valid evidence";
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
let result = await runAttempt(1);
|
|
309
|
+
let parsed = publishOutput(result) ?? ledger.latest(reviewer.id)?.review;
|
|
310
|
+
let classification = classifyPanelFailure(result);
|
|
311
|
+
if (!parsed && classification.retryable && !group.signal.aborted) {
|
|
312
|
+
result = await runAttempt(2);
|
|
313
|
+
parsed = publishOutput(result) ?? ledger.latest(reviewer.id)?.review;
|
|
314
|
+
classification = classifyPanelFailure(result);
|
|
315
|
+
}
|
|
316
|
+
if (!parsed && result.timedOut && reviewBudgetAvailable && !group.signal.aborted) {
|
|
317
|
+
const finalizationPrompt = `${prompt}\n\nYour prior attempt stopped before a valid artifact. Use this bounded checkpoint and return the required panel-review JSON now:\n${boundText(getResultFinalOutput(result), 8 * 1024, 200).text}`;
|
|
318
|
+
const finalized = await runSingleAgent(
|
|
319
|
+
input.ctx.cwd,
|
|
320
|
+
input.agents,
|
|
321
|
+
reviewer.agent,
|
|
322
|
+
finalizationPrompt,
|
|
323
|
+
workspaceByReviewer.get(reviewer.id) ?? input.target.cwd,
|
|
324
|
+
index + 1,
|
|
325
|
+
group.signal,
|
|
326
|
+
thinkingLevel,
|
|
327
|
+
budgets.finalizationMs,
|
|
328
|
+
undefined,
|
|
329
|
+
makeDetails,
|
|
330
|
+
undefined,
|
|
331
|
+
{
|
|
332
|
+
...launchPolicy(
|
|
333
|
+
input,
|
|
334
|
+
reviewer.agent,
|
|
335
|
+
thinkingLevel,
|
|
336
|
+
`Panel review finalization ${reviewer.id}`,
|
|
337
|
+
budgets.finalizationMs,
|
|
338
|
+
taskGeneration,
|
|
339
|
+
workspaceByReviewer.has(reviewer.id) ? "worktree" : "shared",
|
|
340
|
+
"tool-less",
|
|
341
|
+
),
|
|
342
|
+
finalizeOnTimeout: false,
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
finalized.attemptCount = (result.attemptCount ?? 1) + 1;
|
|
346
|
+
result = finalized;
|
|
347
|
+
parsed = publishOutput(result) ?? ledger.latest(reviewer.id)?.review;
|
|
348
|
+
classification = classifyPanelFailure(result);
|
|
349
|
+
}
|
|
350
|
+
if (parsed && isResultError(result)) {
|
|
351
|
+
failures.push({
|
|
352
|
+
reviewerId: reviewer.id,
|
|
353
|
+
...classification,
|
|
354
|
+
message: boundedPrivateText(
|
|
355
|
+
result.errorMessage ??
|
|
356
|
+
(result.stderr.trim() || "Reviewer failed after publishing valid evidence"),
|
|
357
|
+
failureMessageLimit,
|
|
358
|
+
),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
if (!parsed) {
|
|
362
|
+
if (!isResultError(result)) result.resultContractInvalid = true;
|
|
363
|
+
classification = classifyPanelFailure(result);
|
|
364
|
+
failures.push({
|
|
365
|
+
reviewerId: reviewer.id,
|
|
366
|
+
...classification,
|
|
367
|
+
message: boundedPrivateText(
|
|
368
|
+
result.errorMessage ?? (result.stderr.trim() || "No valid panel review artifact"),
|
|
369
|
+
failureMessageLimit,
|
|
370
|
+
),
|
|
371
|
+
});
|
|
372
|
+
workLedger.settle(
|
|
373
|
+
workItemId,
|
|
374
|
+
result.aborted ? "interrupted" : "failed",
|
|
375
|
+
classification.kind,
|
|
376
|
+
);
|
|
377
|
+
} else {
|
|
378
|
+
const artifact = ledger.latest(reviewer.id);
|
|
379
|
+
workLedger.complete(workItemId, {
|
|
380
|
+
taskGeneration,
|
|
381
|
+
executionPlanId: result.executionPlan?.id,
|
|
382
|
+
artifacts: artifact
|
|
383
|
+
? [
|
|
384
|
+
{
|
|
385
|
+
id: `panel-review:${reviewer.id}`,
|
|
386
|
+
kind: "panel-review",
|
|
387
|
+
version: String(artifact.revision),
|
|
388
|
+
verified: false,
|
|
389
|
+
},
|
|
390
|
+
]
|
|
391
|
+
: [],
|
|
392
|
+
verificationAccepted: false,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
completed += 1;
|
|
396
|
+
status.update(
|
|
397
|
+
panelReviewStatus(
|
|
398
|
+
completed,
|
|
399
|
+
input.panel.reviewers.length,
|
|
400
|
+
input.panel.reviewers.length - completed,
|
|
401
|
+
),
|
|
402
|
+
);
|
|
403
|
+
return compactPanelResult(result);
|
|
404
|
+
},
|
|
405
|
+
group.signal,
|
|
406
|
+
(reviewer) => {
|
|
407
|
+
const workItemId = `review:${reviewer.id}`;
|
|
408
|
+
workLedger.settle(workItemId, "interrupted", "parent-aborted-before-launch");
|
|
409
|
+
failures.push({
|
|
410
|
+
reviewerId: reviewer.id,
|
|
411
|
+
kind: "cancelled",
|
|
412
|
+
retryable: false,
|
|
413
|
+
message: "Panel reviewer was not launched because the parent call was cancelled",
|
|
414
|
+
});
|
|
415
|
+
completed += 1;
|
|
416
|
+
return cancelledPanelResult(
|
|
417
|
+
input.agents,
|
|
418
|
+
reviewer.agent,
|
|
419
|
+
input.panel.task,
|
|
420
|
+
input.resolveThinkingLevel(reviewer.agent, reviewer.thinkingLevel),
|
|
421
|
+
);
|
|
422
|
+
},
|
|
423
|
+
);
|
|
424
|
+
results.push(...reviewResults);
|
|
425
|
+
await persistWork();
|
|
426
|
+
const reviews = ledger.snapshot().map((artifact) => artifact.review);
|
|
427
|
+
const reconciliation = reconcilePanel({ reviews, failures, minValidReviews });
|
|
428
|
+
panelDetails = {
|
|
429
|
+
...panelDetails,
|
|
430
|
+
validReviewCount: reviews.length,
|
|
431
|
+
failedReviewCount: failures.length,
|
|
432
|
+
blockingObjectionCount: reconciliation.blockingObjections.length,
|
|
433
|
+
evidence: ledger.snapshot(),
|
|
434
|
+
failures: [...reconciliation.failures],
|
|
435
|
+
};
|
|
436
|
+
if (reconciliation.kind === "insufficient-panel") {
|
|
437
|
+
workLedger.settle(
|
|
438
|
+
"synthesis",
|
|
439
|
+
group.signal.aborted ? "interrupted" : "blocked",
|
|
440
|
+
"insufficient-valid-reviews",
|
|
441
|
+
);
|
|
442
|
+
await persistWork();
|
|
443
|
+
panelDetails.state = group.signal.aborted ? "cancelled" : "insufficient-panel";
|
|
444
|
+
output = {
|
|
445
|
+
content: [
|
|
446
|
+
{
|
|
447
|
+
type: "text",
|
|
448
|
+
text: boundText(
|
|
449
|
+
`Insufficient panel: ${reviews.length}/${minValidReviews} valid reviews. Partial evidence was preserved; synthesis was not run.`,
|
|
450
|
+
DEFAULT_MAX_OUTPUT_BYTES,
|
|
451
|
+
).text,
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
details: { ...makeDetails(), isError: true },
|
|
455
|
+
isError: true,
|
|
456
|
+
};
|
|
457
|
+
} else {
|
|
458
|
+
const synthesisWork = workLedger.start("synthesis", `agent:${input.panel.synthesizer.agent}`);
|
|
459
|
+
await persistWork();
|
|
460
|
+
status.update(panelSynthesisStatus(input.panel.synthesizer.agent));
|
|
461
|
+
const synthesisPrompt = buildPanelSynthesisPrompt({
|
|
462
|
+
panelId,
|
|
463
|
+
task: input.panel.task,
|
|
464
|
+
reviews: reconciliation.reviews,
|
|
465
|
+
failures: reconciliation.failures,
|
|
466
|
+
});
|
|
467
|
+
const synthesisTimeout = Math.min(
|
|
468
|
+
input.resolveTimeoutMs(input.panel.synthesizer.agent, input.panel.synthesizer.timeoutMs),
|
|
469
|
+
budgets.synthesisMs,
|
|
470
|
+
Math.max(0, panelStartedAt + budgets.totalMs - budgets.cleanupMs - Date.now()),
|
|
471
|
+
);
|
|
472
|
+
const synthesisThinkingLevel = input.resolveThinkingLevel(
|
|
473
|
+
input.panel.synthesizer.agent,
|
|
474
|
+
input.panel.synthesizer.thinkingLevel,
|
|
475
|
+
);
|
|
476
|
+
const synthesisResult =
|
|
477
|
+
synthesisTimeout < 1
|
|
478
|
+
? panelBudgetExhaustedResult(
|
|
479
|
+
input.agents,
|
|
480
|
+
input.panel.synthesizer.agent,
|
|
481
|
+
synthesisPrompt,
|
|
482
|
+
synthesisThinkingLevel,
|
|
483
|
+
)
|
|
484
|
+
: await runSingleAgent(
|
|
485
|
+
input.ctx.cwd,
|
|
486
|
+
input.agents,
|
|
487
|
+
input.panel.synthesizer.agent,
|
|
488
|
+
synthesisPrompt,
|
|
489
|
+
input.target.cwd,
|
|
490
|
+
undefined,
|
|
491
|
+
group.signal,
|
|
492
|
+
synthesisThinkingLevel,
|
|
493
|
+
synthesisTimeout,
|
|
494
|
+
undefined,
|
|
495
|
+
makeDetails,
|
|
496
|
+
undefined,
|
|
497
|
+
{
|
|
498
|
+
...launchPolicy(
|
|
499
|
+
input,
|
|
500
|
+
input.panel.synthesizer.agent,
|
|
501
|
+
synthesisThinkingLevel,
|
|
502
|
+
"Panel synthesis",
|
|
503
|
+
synthesisTimeout,
|
|
504
|
+
input.panel.reviewers.length + 1,
|
|
505
|
+
"shared",
|
|
506
|
+
"tool-less",
|
|
507
|
+
{
|
|
508
|
+
idleTimeoutMs:
|
|
509
|
+
input.panel.synthesizer.idleTimeoutMs ?? input.params.idleTimeoutMs,
|
|
510
|
+
maxTurns: input.panel.synthesizer.maxTurns ?? input.params.maxTurns,
|
|
511
|
+
maxToolCalls: input.panel.synthesizer.maxToolCalls ?? input.params.maxToolCalls,
|
|
512
|
+
},
|
|
513
|
+
),
|
|
514
|
+
},
|
|
515
|
+
);
|
|
516
|
+
synthesisResult.target = targetPolicyAudit(input.target);
|
|
517
|
+
const synthesis = parsePanelSynthesis(
|
|
518
|
+
getResultFinalOutput(synthesisResult),
|
|
519
|
+
reconciliation.reviews,
|
|
520
|
+
reconciliation.failures
|
|
521
|
+
.map((failure) => failure.reviewerId)
|
|
522
|
+
.filter((id): id is string => Boolean(id)),
|
|
523
|
+
);
|
|
524
|
+
compactPanelResult(synthesisResult);
|
|
525
|
+
const synthesisErrored = isResultError(synthesisResult);
|
|
526
|
+
panelDetails = {
|
|
527
|
+
...panelDetails,
|
|
528
|
+
synthesizerResult: synthesisResult,
|
|
529
|
+
synthesis,
|
|
530
|
+
dissentCount: synthesis?.disagreements.length ?? 0,
|
|
531
|
+
state: synthesisErrored
|
|
532
|
+
? group.signal.aborted
|
|
533
|
+
? "cancelled"
|
|
534
|
+
: "failed"
|
|
535
|
+
: synthesis
|
|
536
|
+
? failures.length > 0
|
|
537
|
+
? "degraded"
|
|
538
|
+
: "completed"
|
|
539
|
+
: group.signal.aborted
|
|
540
|
+
? "cancelled"
|
|
541
|
+
: "failed",
|
|
542
|
+
};
|
|
543
|
+
const synthesisInvalid = !synthesis || synthesisErrored;
|
|
544
|
+
if (!synthesis && !isResultError(synthesisResult)) {
|
|
545
|
+
synthesisResult.resultContractInvalid = true;
|
|
546
|
+
}
|
|
547
|
+
if (synthesisInvalid) {
|
|
548
|
+
workLedger.settle(
|
|
549
|
+
"synthesis",
|
|
550
|
+
synthesisResult.aborted ? "interrupted" : "failed",
|
|
551
|
+
"invalid-panel-synthesis",
|
|
552
|
+
);
|
|
553
|
+
} else {
|
|
554
|
+
workLedger.complete("synthesis", {
|
|
555
|
+
taskGeneration: synthesisWork.taskGeneration,
|
|
556
|
+
executionPlanId: synthesisResult.executionPlan?.id,
|
|
557
|
+
verificationAccepted: false,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
await persistWork();
|
|
561
|
+
output = {
|
|
562
|
+
content: [
|
|
563
|
+
{
|
|
564
|
+
type: "text",
|
|
565
|
+
text: synthesis
|
|
566
|
+
? boundText(synthesis.summary, DEFAULT_MAX_OUTPUT_BYTES).text
|
|
567
|
+
: "Panel synthesis failed or returned an invalid panel-synthesis contract.",
|
|
568
|
+
},
|
|
569
|
+
],
|
|
570
|
+
details: { ...makeDetails(), isError: synthesisInvalid || undefined },
|
|
571
|
+
isError: synthesisInvalid || undefined,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
} finally {
|
|
575
|
+
status.clear();
|
|
576
|
+
await group.close();
|
|
577
|
+
panelDetails.cleanupComplete = true;
|
|
578
|
+
if (output?.details.panel) output.details.panel.cleanupComplete = true;
|
|
579
|
+
}
|
|
580
|
+
return output as PanelToolResult;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function assertPanelActive(signal?: AbortSignal): void {
|
|
584
|
+
if (!signal?.aborted) return;
|
|
585
|
+
if (signal.reason instanceof Error) throw signal.reason;
|
|
586
|
+
throw new DOMException("Panel execution was cancelled during setup", "AbortError");
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function isReadOnlyAgent(agent: AgentConfig): boolean {
|
|
590
|
+
return agent.capabilityManifest?.authority?.filesystem === "read";
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function launchPolicy(
|
|
594
|
+
input: PanelExecutionInput,
|
|
595
|
+
agentName: string,
|
|
596
|
+
thinkingLevel: SubagentThinkingLevel | undefined,
|
|
597
|
+
displayTask: string,
|
|
598
|
+
timeoutMs: number,
|
|
599
|
+
taskGeneration: number,
|
|
600
|
+
workspaceMode: "shared" | "worktree",
|
|
601
|
+
toolMode: "review" | "tool-less",
|
|
602
|
+
turnLimits?: ChildLaunchPolicy["turnLimits"],
|
|
603
|
+
orchestrationDeadlineAt?: number,
|
|
604
|
+
): ChildLaunchPolicy {
|
|
605
|
+
const agent = input.agents.find((candidate) => candidate.name === agentName);
|
|
606
|
+
if (!agent) throw new Error(`Unknown panel agent: ${agentName}`);
|
|
607
|
+
const resolvedTools = resolveContractTools(agent.tools, undefined);
|
|
608
|
+
const tools =
|
|
609
|
+
toolMode === "tool-less"
|
|
610
|
+
? []
|
|
611
|
+
: workspaceMode === "shared"
|
|
612
|
+
? (resolvedTools?.filter((tool) => PANEL_READ_ONLY_TOOLS.has(tool)) ?? [])
|
|
613
|
+
: resolvedTools;
|
|
614
|
+
const contract: DelegationContract = {
|
|
615
|
+
version: "pi-subagents:delegation:v2",
|
|
616
|
+
level: "minimal",
|
|
617
|
+
taskId: `panel-${taskGeneration}`,
|
|
618
|
+
objective: truncateUtf8(displayTask, 16 * 1024).text,
|
|
619
|
+
nonGoals: [],
|
|
620
|
+
dependencies: [],
|
|
621
|
+
requiredInputs: [],
|
|
622
|
+
acceptanceCriteria: ["Return one valid bounded panel contract"],
|
|
623
|
+
requiredEvidence: [],
|
|
624
|
+
sideEffectPolicy:
|
|
625
|
+
toolMode === "tool-less" || workspaceMode === "shared" ? "read-only" : "mutating",
|
|
626
|
+
enforcement: "audit",
|
|
627
|
+
};
|
|
628
|
+
const executionPlan = createExecutionPlan({
|
|
629
|
+
contract,
|
|
630
|
+
agent,
|
|
631
|
+
effectiveTools: tools,
|
|
632
|
+
target: targetPolicyAudit(input.target),
|
|
633
|
+
workspaceMode,
|
|
634
|
+
transport: "subprocess",
|
|
635
|
+
resultFormat: "text",
|
|
636
|
+
model: agent.model,
|
|
637
|
+
thinkingLevel,
|
|
638
|
+
timeoutMs,
|
|
639
|
+
taskGeneration,
|
|
640
|
+
});
|
|
641
|
+
const acknowledgement = acknowledgeExecutionPlan(executionPlan);
|
|
642
|
+
if (acknowledgement.status === "rejected") {
|
|
643
|
+
throw new Error(`Panel execution plan rejected: ${JSON.stringify(acknowledgement)}`);
|
|
644
|
+
}
|
|
645
|
+
return {
|
|
646
|
+
projectTrust: input.target.trust.projectTrusted,
|
|
647
|
+
tools,
|
|
648
|
+
contract,
|
|
649
|
+
displayTask,
|
|
650
|
+
turnLimits,
|
|
651
|
+
orchestrationDeadlineAt,
|
|
652
|
+
finalizeOnTimeout: false,
|
|
653
|
+
executionPlan,
|
|
654
|
+
capabilityGrant: issueCapabilityGrant(executionPlan, Date.now(), timeoutMs + 60_000),
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function cancelledPanelResult(
|
|
659
|
+
agents: AgentConfig[],
|
|
660
|
+
agentName: string,
|
|
661
|
+
task: string,
|
|
662
|
+
thinkingLevel: SubagentThinkingLevel | undefined,
|
|
663
|
+
): SingleResult {
|
|
664
|
+
const message = "Panel reviewer was not launched because the parent call was cancelled";
|
|
665
|
+
return {
|
|
666
|
+
agent: agentName,
|
|
667
|
+
agentSource: agents.find((agent) => agent.name === agentName)?.source ?? "unknown",
|
|
668
|
+
task,
|
|
669
|
+
exitCode: 130,
|
|
670
|
+
messages: [],
|
|
671
|
+
stderr: message,
|
|
672
|
+
usage: {
|
|
673
|
+
input: 0,
|
|
674
|
+
output: 0,
|
|
675
|
+
cacheRead: 0,
|
|
676
|
+
cacheWrite: 0,
|
|
677
|
+
cost: 0,
|
|
678
|
+
contextTokens: 0,
|
|
679
|
+
turns: 0,
|
|
680
|
+
},
|
|
681
|
+
thinkingLevel,
|
|
682
|
+
finalOutput: "",
|
|
683
|
+
errorMessage: message,
|
|
684
|
+
aborted: true,
|
|
685
|
+
stopReason: "aborted",
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function panelReviewerBudgetExhaustedResult(
|
|
690
|
+
agents: AgentConfig[],
|
|
691
|
+
agentName: string,
|
|
692
|
+
task: string,
|
|
693
|
+
thinkingLevel: SubagentThinkingLevel | undefined,
|
|
694
|
+
): SingleResult {
|
|
695
|
+
const result = panelBudgetExhaustedResult(agents, agentName, task, thinkingLevel);
|
|
696
|
+
result.errorMessage = "Panel review budget was exhausted before launch";
|
|
697
|
+
result.stderr = result.errorMessage;
|
|
698
|
+
return result;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function panelBudgetExhaustedResult(
|
|
702
|
+
agents: AgentConfig[],
|
|
703
|
+
agentName: string,
|
|
704
|
+
task: string,
|
|
705
|
+
thinkingLevel: SubagentThinkingLevel | undefined,
|
|
706
|
+
): SingleResult {
|
|
707
|
+
const message = "Panel synthesis budget was exhausted before launch";
|
|
708
|
+
return {
|
|
709
|
+
agent: agentName,
|
|
710
|
+
agentSource: agents.find((agent) => agent.name === agentName)?.source ?? "unknown",
|
|
711
|
+
task,
|
|
712
|
+
exitCode: 124,
|
|
713
|
+
messages: [],
|
|
714
|
+
stderr: message,
|
|
715
|
+
usage: {
|
|
716
|
+
input: 0,
|
|
717
|
+
output: 0,
|
|
718
|
+
cacheRead: 0,
|
|
719
|
+
cacheWrite: 0,
|
|
720
|
+
cost: 0,
|
|
721
|
+
contextTokens: 0,
|
|
722
|
+
turns: 0,
|
|
723
|
+
},
|
|
724
|
+
thinkingLevel,
|
|
725
|
+
finalOutput: "",
|
|
726
|
+
errorMessage: message,
|
|
727
|
+
timedOut: true,
|
|
728
|
+
stopReason: "timeout",
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function compactPanelResult(result: SingleResult): SingleResult {
|
|
733
|
+
result.messages = [];
|
|
734
|
+
result.recentActivity = undefined;
|
|
735
|
+
result.recentActivityTotal = undefined;
|
|
736
|
+
result.task = boundText(result.task, 512, 20).text;
|
|
737
|
+
result.finalOutput = result.finalOutput
|
|
738
|
+
? boundText(result.finalOutput, 512, 20).text
|
|
739
|
+
: result.finalOutput;
|
|
740
|
+
result.partialOutput = result.partialOutput
|
|
741
|
+
? boundText(result.partialOutput, 512, 20).text
|
|
742
|
+
: result.partialOutput;
|
|
743
|
+
result.stderr = boundText(result.stderr, 512, 20).text;
|
|
744
|
+
result.errorMessage = result.errorMessage
|
|
745
|
+
? boundedPrivateText(result.errorMessage, 512)
|
|
746
|
+
: result.errorMessage;
|
|
747
|
+
return result;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function createPanelDetails(input: {
|
|
751
|
+
panelId: string;
|
|
752
|
+
preset: PanelPreset;
|
|
753
|
+
reviewerIds: string[];
|
|
754
|
+
sharedTask: string;
|
|
755
|
+
budgets: ReturnType<typeof planPanelBudgets>;
|
|
756
|
+
}): NonNullable<SubagentDetails["panel"]> {
|
|
757
|
+
return {
|
|
758
|
+
id: input.panelId,
|
|
759
|
+
preset: input.preset,
|
|
760
|
+
sharedTaskPreview: boundedPrivateText(input.sharedTask, 2 * 1024),
|
|
761
|
+
state: "running",
|
|
762
|
+
reviewerIds: input.reviewerIds,
|
|
763
|
+
validReviewCount: 0,
|
|
764
|
+
failedReviewCount: 0,
|
|
765
|
+
blockingObjectionCount: 0,
|
|
766
|
+
dissentCount: 0,
|
|
767
|
+
budgets: input.budgets,
|
|
768
|
+
evidence: [],
|
|
769
|
+
failures: [],
|
|
770
|
+
cleanupComplete: false,
|
|
771
|
+
};
|
|
772
|
+
}
|