@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.
Files changed (81) hide show
  1. package/README.md +313 -53
  2. package/package.json +11 -8
  3. package/src/adaptive-scheduler.ts +196 -0
  4. package/src/admission-benchmark.ts +95 -0
  5. package/src/admission-policy.ts +78 -0
  6. package/src/agent-projection.ts +53 -0
  7. package/src/agents.ts +58 -1
  8. package/src/auto-transport.ts +114 -0
  9. package/src/blocking-status.ts +63 -0
  10. package/src/capabilities.ts +145 -0
  11. package/src/capability-grant.ts +115 -0
  12. package/src/capability-router.ts +107 -0
  13. package/src/completion-delivery.ts +257 -0
  14. package/src/config-status.ts +221 -0
  15. package/src/config-ui.ts +215 -236
  16. package/src/consult-resources.ts +4 -27
  17. package/src/consult.ts +9 -1
  18. package/src/create-stateful-transport.ts +55 -0
  19. package/src/delegation-contract.ts +417 -0
  20. package/src/execution-plan.ts +322 -0
  21. package/src/execution-profiles.ts +95 -0
  22. package/src/execution-ui.ts +320 -0
  23. package/src/execution.ts +848 -158
  24. package/src/in-process-transport.ts +269 -25
  25. package/src/inspect-render.ts +101 -1
  26. package/src/inspect.ts +296 -3
  27. package/src/integration-controller.ts +98 -0
  28. package/src/limits.ts +3 -0
  29. package/src/orchestration-metrics.ts +78 -0
  30. package/src/outcome.ts +61 -0
  31. package/src/panel-child-group.ts +35 -0
  32. package/src/panel-contract.ts +343 -0
  33. package/src/panel-evidence.ts +59 -0
  34. package/src/panel-execution.ts +772 -0
  35. package/src/panel-failure.ts +56 -0
  36. package/src/panel-planning.ts +175 -0
  37. package/src/panel-prompts.ts +132 -0
  38. package/src/panel-reconciliation.ts +57 -0
  39. package/src/panel-render.ts +103 -0
  40. package/src/parallel-limit-ui.ts +112 -0
  41. package/src/params.ts +172 -3
  42. package/src/persistence.ts +182 -32
  43. package/src/prompt-resources.ts +38 -0
  44. package/src/registry-types.ts +175 -0
  45. package/src/registry.ts +466 -143
  46. package/src/render.ts +72 -6
  47. package/src/result-contract.ts +416 -0
  48. package/src/retained-semantic-state.ts +100 -0
  49. package/src/rpc-timeout-finalization.ts +207 -0
  50. package/src/rpc-transport-metadata.ts +65 -0
  51. package/src/rpc-transport.ts +990 -0
  52. package/src/rpc-turn-capture.ts +142 -0
  53. package/src/runner-result.ts +55 -0
  54. package/src/runner-usage.ts +48 -0
  55. package/src/runner.ts +325 -73
  56. package/src/semantic-snapshot.ts +214 -0
  57. package/src/settings.ts +254 -35
  58. package/src/spawn-idempotency.ts +61 -0
  59. package/src/stateful-config.ts +13 -0
  60. package/src/stateful-guidance.ts +1 -0
  61. package/src/stateful-lifecycle.ts +45 -2
  62. package/src/stateful-limit-ui.ts +246 -0
  63. package/src/stateful-limits.ts +96 -0
  64. package/src/stateful-prompt.ts +11 -2
  65. package/src/stateful-render.ts +48 -3
  66. package/src/stateful.ts +467 -357
  67. package/src/subagents.ts +114 -46
  68. package/src/subprocess-transport.ts +64 -5
  69. package/src/supervision.ts +103 -0
  70. package/src/timeout-checkpoint.ts +305 -0
  71. package/src/timeout-finalization.ts +75 -0
  72. package/src/transport-types.ts +68 -0
  73. package/src/transport-ui.ts +169 -0
  74. package/src/transport.ts +16 -4
  75. package/src/turn-budget.ts +109 -0
  76. package/src/verification-policy.ts +17 -0
  77. package/src/work-item-ledger.ts +682 -0
  78. package/src/work-item-persistence.ts +218 -0
  79. package/src/workflow-planning.ts +150 -0
  80. package/src/workflow-ui.ts +61 -0
  81. 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
+ }