@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
package/src/runner.ts CHANGED
@@ -5,8 +5,12 @@ import * as path from "node:path";
5
5
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
6
6
  import type { Message } from "@earendil-works/pi-ai";
7
7
  import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
8
+ import type { SchedulingDecision } from "./adaptive-scheduler.js";
8
9
  import type { AgentConfig, AgentScope, AgentSource, SubagentThinkingLevel } from "./agents.js";
10
+ import { type CapabilityGrant, revokeCapabilityGrant } from "./capability-grant.js";
9
11
  import type { TargetPolicyAudit } from "./cwd-policy.js";
12
+ import type { DelegationContract } from "./delegation-contract.js";
13
+ import type { ExecutionPlan } from "./execution-plan.js";
10
14
  import {
11
15
  appendBounded,
12
16
  DEFAULT_MAX_CONTEXT_BYTES,
@@ -16,25 +20,50 @@ import {
16
20
  MAX_SUBAGENT_TIMEOUT_MS,
17
21
  truncateUtf8,
18
22
  } from "./limits.js";
23
+ import type { OrchestrationMetrics } from "./orchestration-metrics.js";
24
+ import { type ClassifiedSubagentOutcome, classifyStructuredOutcome } from "./outcome.js";
25
+ import type { PanelSynthesis } from "./panel-contract.js";
26
+ import type { PanelEvidenceArtifact } from "./panel-evidence.js";
27
+ import type { PanelFailure } from "./panel-failure.js";
28
+ import type { PanelPhaseBudgets, PanelPreset } from "./panel-planning.js";
19
29
  import { resolvePiInvocation } from "./pi-invocation.js";
20
30
  import { JsonLineDecoder } from "./protocol.js";
31
+ import {
32
+ type AnyStructuredSubagentResult,
33
+ parseAnyStructuredSubagentResult,
34
+ type SubagentResultFormat,
35
+ } from "./result-contract.js";
36
+ import {
37
+ formatResultFailure as formatBaseResultFailure,
38
+ getResultFinalOutput as getBaseResultFinalOutput,
39
+ getFinalOutput,
40
+ isResultError as isBaseResultError,
41
+ } from "./runner-result.js";
42
+ import {
43
+ addUsageValue,
44
+ mergeUsageStats,
45
+ protocolUsageCost,
46
+ protocolUsageCount,
47
+ type UsageStats,
48
+ } from "./runner-usage.js";
49
+ import {
50
+ formatTimeoutCheckpoint,
51
+ formatTurnTerminationMessage,
52
+ journalMessages,
53
+ TimeoutProgressJournal,
54
+ TURN_TERMINATION_VERSION,
55
+ type TurnTerminationReport,
56
+ } from "./timeout-checkpoint.js";
57
+ import {
58
+ buildTimeoutFinalizationPrompt,
59
+ resolveTimeoutFinalizationMs,
60
+ } from "./timeout-finalization.js";
61
+ import { TurnBudgetMonitor, type TurnBudgetStop, type TurnLimits } from "./turn-budget.js";
62
+ import type { WorkItemLedgerSnapshot } from "./work-item-ledger.js";
21
63
 
22
64
  export const KILL_GRACE_MS = 5000;
23
65
 
24
- export interface UsageStats {
25
- input: number;
26
- output: number;
27
- cacheRead: number;
28
- cacheWrite: number;
29
- cost: number;
30
- costInput?: number;
31
- costOutput?: number;
32
- costCacheRead?: number;
33
- costCacheWrite?: number;
34
- totalTokens?: number;
35
- contextTokens: number;
36
- turns: number;
37
- }
66
+ export type { UsageStats } from "./runner-usage.js";
38
67
  export type RecentActivityItem =
39
68
  | { type: "text"; text: string }
40
69
  | { type: "toolCall"; name: string; args: Record<string, unknown> };
@@ -42,21 +71,6 @@ export type RecentActivityItem =
42
71
  const MAX_RECENT_ACTIVITY_ITEMS = 10;
43
72
  const MAX_RECENT_ACTIVITY_BYTES = 8 * 1024;
44
73
  const MAX_RECENT_ACTIVITY_ARGUMENT_BYTES = 1024;
45
- const MAX_USAGE_VALUE = Number.MAX_SAFE_INTEGER;
46
-
47
- function protocolUsageCount(value: unknown): number {
48
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
49
- }
50
-
51
- function protocolUsageCost(value: unknown): number {
52
- return typeof value === "number" && Number.isFinite(value) && value >= 0
53
- ? Math.min(value, MAX_USAGE_VALUE)
54
- : 0;
55
- }
56
-
57
- function addUsageValue(current: number, addition: number): number {
58
- return Math.min(MAX_USAGE_VALUE, current + addition);
59
- }
60
74
 
61
75
  export interface SingleResult {
62
76
  agent: string;
@@ -76,6 +90,10 @@ export interface SingleResult {
76
90
  errorMessage?: string;
77
91
  step?: number;
78
92
  finalOutput?: string;
93
+ partialOutput?: string;
94
+ timeoutSummary?: string;
95
+ timeoutSummaryError?: string;
96
+ termination?: TurnTerminationReport;
79
97
  timedOut?: boolean;
80
98
  timeoutMs?: number;
81
99
  aborted?: boolean;
@@ -89,51 +107,71 @@ export interface SingleResult {
89
107
  overridden: string[];
90
108
  unsupported: string[];
91
109
  };
110
+ contract?: DelegationContract;
111
+ resultFormat?: SubagentResultFormat;
112
+ structuredResult?: AnyStructuredSubagentResult;
113
+ resultContractInvalid?: boolean;
114
+ outcome?: ClassifiedSubagentOutcome;
115
+ attemptCount?: number;
116
+ hedged?: boolean;
117
+ executionPlan?: ExecutionPlan;
118
+ capabilityGrant?: CapabilityGrant;
119
+ }
120
+
121
+ export interface PanelDetails {
122
+ id: string;
123
+ preset: PanelPreset;
124
+ sharedTaskPreview: string;
125
+ state: "running" | "completed" | "degraded" | "insufficient-panel" | "failed" | "cancelled";
126
+ reviewerIds: string[];
127
+ validReviewCount: number;
128
+ failedReviewCount: number;
129
+ blockingObjectionCount: number;
130
+ dissentCount: number;
131
+ budgets: PanelPhaseBudgets;
132
+ evidence: PanelEvidenceArtifact[];
133
+ failures: PanelFailure[];
134
+ synthesis?: PanelSynthesis;
135
+ synthesizerResult?: SingleResult;
136
+ cleanupComplete: boolean;
92
137
  }
93
138
 
94
139
  export interface SubagentDetails {
95
- mode: "single" | "parallel" | "chain";
140
+ mode: "single" | "parallel" | "chain" | "workflow" | "panel";
96
141
  agentScope: AgentScope;
97
142
  projectAgentsDir: string | null;
98
143
  results: SingleResult[];
99
144
  aggregator?: SingleResult;
145
+ workflow?: WorkItemLedgerSnapshot;
146
+ schedulerDecisions?: SchedulingDecision[];
147
+ metrics?: OrchestrationMetrics;
148
+ panel?: PanelDetails;
100
149
  isError?: boolean;
101
150
  }
102
151
 
103
- function getFinalOutput(messages: Message[]): string {
104
- for (let i = messages.length - 1; i >= 0; i--) {
105
- const msg = messages[i];
106
- if (msg.role === "assistant") {
107
- const text = msg.content
108
- .filter((part) => part.type === "text")
109
- .map((part) => part.text)
110
- .join("\n");
111
- if (text) return text;
112
- }
113
- }
114
- return "";
115
- }
116
-
117
152
  export function getResultFinalOutput(result: SingleResult): string {
118
- return result.finalOutput ?? getFinalOutput(result.messages);
153
+ return getBaseResultFinalOutput(result);
119
154
  }
120
155
 
121
156
  export function isResultError(result: SingleResult): boolean {
122
157
  return (
123
- (result.exitCode !== 0 && result.exitCode !== -1) ||
124
- result.timedOut === true ||
125
- result.stopReason === "timeout" ||
126
- result.stopReason === "error" ||
127
- result.stopReason === "aborted"
158
+ isBaseResultError(result) ||
159
+ result.resultContractInvalid === true ||
160
+ (result.outcome !== undefined &&
161
+ result.outcome.status !== "completed" &&
162
+ result.outcome.status !== "partial")
128
163
  );
129
164
  }
130
165
 
131
166
  export function formatResultFailure(result: SingleResult): string {
132
- const error = result.errorMessage || result.stderr.trim();
133
- const output = getResultFinalOutput(result);
134
- const combined =
135
- error && output ? `${error}\n\nPartial output:\n${output}` : error || output || "(no output)";
136
- return truncateUtf8(combined, DEFAULT_MAX_CONTEXT_BYTES).text;
167
+ const contractError = result.resultContractInvalid
168
+ ? `Subagent returned an invalid ${result.resultFormat ?? "structured"} result contract`
169
+ : result.outcome && !["completed", "partial"].includes(result.outcome.status)
170
+ ? `Subagent outcome ${result.outcome.status}${result.outcome.reasonCode ? ` (${result.outcome.reasonCode})` : ""}; recovery: ${result.outcome.recoveryActions.join(", ") || "none"}`
171
+ : undefined;
172
+ return contractError
173
+ ? formatBaseResultFailure({ ...result, errorMessage: contractError })
174
+ : formatBaseResultFailure(result);
137
175
  }
138
176
 
139
177
  function boundMessageText(
@@ -262,9 +300,11 @@ export function buildFanInContext(
262
300
  const error = result.errorMessage || result.stderr.trim();
263
301
  const resultText = failed
264
302
  ? `${error ? "Error" : output ? "Partial output" : "Error"}:\n${formatResultFailure(result)}`
265
- : output
266
- ? `Output:\n${output}`
267
- : "Output: (no output)";
303
+ : result.structuredResult
304
+ ? `Structured result:\n${JSON.stringify(result.structuredResult)}`
305
+ : output
306
+ ? `Output:\n${output}`
307
+ : "Output: (no output)";
268
308
  return [
269
309
  `## Result ${index + 1}: ${result.agent} (${status})`,
270
310
  `Task: ${result.task}`,
@@ -405,6 +445,30 @@ export interface ChildLaunchPolicy {
405
445
  projectTrust?: boolean;
406
446
  baseSystemPrompt?: string;
407
447
  appendSystemPromptPaths?: string[];
448
+ /** Internal timeout recovery control; omitted means enabled. */
449
+ finalizeOnTimeout?: boolean;
450
+ /** Internal hard deadline for the summary attempt. */
451
+ timeoutFinalizationMs?: number;
452
+ /** Optional stateful result contract retained during timeout finalization. */
453
+ timeoutResultFormat?: SubagentResultFormat;
454
+ /** Optional non-wall-clock limits for this turn. */
455
+ turnLimits?: TurnLimits;
456
+ /** Override the timeout reason when an orchestration deadline caps this child. */
457
+ workTimeoutReason?: "work_timeout" | "orchestration_timeout";
458
+ /** Public limit value reported when the effective child timeout is only the remaining budget. */
459
+ workTimeoutReportLimit?: number;
460
+ /** Absolute blocking-workflow deadline that also caps model finalization. */
461
+ orchestrationDeadlineAt?: number;
462
+ /** Completion contract requested for this turn. */
463
+ resultFormat?: SubagentResultFormat;
464
+ /** Normalized request contract retained in result details. */
465
+ contract?: DelegationContract;
466
+ /** Original task summary shown in result details when the executed prompt has contract metadata. */
467
+ displayTask?: string;
468
+ /** Immutable audit or enforcement decision made before launch. */
469
+ executionPlan?: ExecutionPlan;
470
+ /** Executor-owned authority lifetime bound to the accepted plan generation. */
471
+ capabilityGrant?: CapabilityGrant;
408
472
  }
409
473
 
410
474
  export async function runSingleAgent(
@@ -455,10 +519,11 @@ export async function runSingleAgent(
455
519
  let latestAssistantOutput = "";
456
520
  let terminalAssistantOutput: string | undefined;
457
521
 
522
+ const progressJournal = new TimeoutProgressJournal();
458
523
  const currentResult: SingleResult = {
459
524
  agent: agentName,
460
525
  agentSource: agent.source,
461
- task,
526
+ task: launchPolicy?.displayTask ?? task,
462
527
  exitCode: 0,
463
528
  messages: [],
464
529
  stderr: "",
@@ -475,6 +540,10 @@ export async function runSingleAgent(
475
540
  thinkingLevel,
476
541
  step,
477
542
  timeoutMs,
543
+ contract: launchPolicy?.contract,
544
+ resultFormat: launchPolicy?.resultFormat,
545
+ executionPlan: launchPolicy?.executionPlan,
546
+ capabilityGrant: launchPolicy?.capabilityGrant,
478
547
  };
479
548
  const selectedAssistantOutput = () =>
480
549
  terminalAssistantOutput !== undefined
@@ -579,17 +648,25 @@ export async function runSingleAgent(
579
648
  }
580
649
  let wasAborted = false;
581
650
  let timedOut = false;
651
+ let budgetStop:
652
+ | TurnBudgetStop
653
+ | { reason: "work_timeout" | "orchestration_timeout"; limit: number }
654
+ | undefined;
582
655
 
583
656
  const exitCode = await new Promise<number>((resolve) => {
584
657
  let settled = false;
585
658
  let cleanupTermination: (() => void) | undefined;
586
659
  let timeout: NodeJS.Timeout | undefined;
660
+ let terminationDeadline: NodeJS.Timeout | undefined;
587
661
  let abortHandler: (() => void) | undefined;
662
+ let budgetMonitor: TurnBudgetMonitor | undefined;
588
663
  const finish = (code: number) => {
589
664
  if (settled) return;
590
665
  settled = true;
591
666
  if (timeout) clearTimeout(timeout);
667
+ if (terminationDeadline) clearTimeout(terminationDeadline);
592
668
  cleanupTermination?.();
669
+ budgetMonitor?.dispose();
593
670
  if (signal && abortHandler) signal.removeEventListener("abort", abortHandler);
594
671
  resolve(code);
595
672
  };
@@ -647,6 +724,7 @@ export async function runSingleAgent(
647
724
  const event = raw as { type?: string; message?: Message };
648
725
  if (event.type === "message_end" && event.message) {
649
726
  const msg = event.message;
727
+ journalMessages(progressJournal, [msg]);
650
728
  if (msg.role === "assistant") {
651
729
  const output = truncateUtf8(getFinalOutput([msg]), DEFAULT_MAX_OUTPUT_BYTES);
652
730
  currentResult.truncated ||= output.truncated;
@@ -659,6 +737,10 @@ export async function runSingleAgent(
659
737
  addMessage(msg);
660
738
  if (msg.role === "assistant") {
661
739
  currentResult.usage.turns++;
740
+ budgetMonitor?.recordToolCalls(
741
+ msg.content.filter((part) => part.type === "toolCall").length,
742
+ );
743
+ budgetMonitor?.recordAssistantTurn(msg.stopReason);
662
744
  const usage = msg.usage;
663
745
  if (usage && typeof usage === "object") {
664
746
  const input = protocolUsageCount(usage.input);
@@ -719,6 +801,8 @@ export async function runSingleAgent(
719
801
  }
720
802
  emitUpdate();
721
803
  } else if (event.type === "tool_result_end" && event.message) {
804
+ journalMessages(progressJournal, [event.message]);
805
+ budgetMonitor?.recordActivity();
722
806
  addMessage(event.message);
723
807
  emitUpdate();
724
808
  }
@@ -732,21 +816,47 @@ export async function runSingleAgent(
732
816
  currentResult.truncated = true;
733
817
  },
734
818
  });
735
-
736
- timeout = setTimeout(() => {
737
- timedOut = true;
738
- currentResult.timedOut = true;
739
- currentResult.stopReason = "timeout";
740
- setErrorMessage(`Subagent timed out after ${timeoutMs}ms`);
819
+ const beginTermination = (exitCode: number) => {
820
+ if (cleanupTermination || settled) return;
821
+ cleanupTermination = terminateProcess(proc);
822
+ terminationDeadline = setTimeout(() => {
823
+ decoder.finish();
824
+ proc.stdin?.destroy();
825
+ proc.stdout?.destroy();
826
+ proc.stderr?.destroy();
827
+ finish(exitCode);
828
+ }, KILL_GRACE_MS + 1_000);
829
+ };
830
+ const stopForBudget = (
831
+ stop: TurnBudgetStop | { reason: "work_timeout" | "orchestration_timeout"; limit: number },
832
+ ) => {
833
+ if (budgetStop || settled || wasAborted) return;
834
+ budgetStop = stop;
835
+ timedOut = stop.reason.endsWith("timeout");
836
+ currentResult.timedOut = timedOut || undefined;
837
+ currentResult.stopReason = timedOut ? "timeout" : "limit";
838
+ const message = formatTurnTerminationMessage(stop.reason, stop.limit);
839
+ setErrorMessage(message);
741
840
  const bounded = appendBounded(
742
841
  currentResult.stderr,
743
- `\nSubagent timed out after ${timeoutMs}ms.`,
842
+ `\n${message}.`,
744
843
  DEFAULT_MAX_STDERR_BYTES,
745
844
  );
746
845
  currentResult.stderr = bounded.text;
747
846
  currentResult.truncated ||= bounded.truncated;
748
847
  emitUpdate();
749
- cleanupTermination = terminateProcess(proc);
848
+ beginTermination(124);
849
+ };
850
+ budgetMonitor = new TurnBudgetMonitor({
851
+ ...launchPolicy?.turnLimits,
852
+ onExceeded: stopForBudget,
853
+ });
854
+
855
+ timeout = setTimeout(() => {
856
+ stopForBudget({
857
+ reason: launchPolicy?.workTimeoutReason ?? "work_timeout",
858
+ limit: launchPolicy?.workTimeoutReportLimit ?? timeoutMs,
859
+ });
750
860
  }, timeoutMs);
751
861
  timeout.unref();
752
862
 
@@ -765,10 +875,10 @@ export async function runSingleAgent(
765
875
  });
766
876
  proc.on("close", (code) => {
767
877
  decoder.finish();
768
- finish(timedOut ? 124 : wasAborted ? 130 : (code ?? 0));
878
+ finish(budgetStop ? 124 : wasAborted ? 130 : (code ?? 0));
769
879
  });
770
880
  proc.on("error", (error) => {
771
- currentResult.launchFailed = true;
881
+ currentResult.launchFailed = currentResult.processStarted ? undefined : true;
772
882
  const message = setErrorMessage(error.message);
773
883
  const bounded = appendBounded(
774
884
  currentResult.stderr,
@@ -777,27 +887,162 @@ export async function runSingleAgent(
777
887
  );
778
888
  currentResult.stderr = bounded.text;
779
889
  currentResult.truncated ||= bounded.truncated;
780
- finish(1);
890
+ if (currentResult.processStarted) beginTermination(1);
891
+ else finish(1);
781
892
  });
782
893
 
783
894
  if (signal) {
784
895
  abortHandler = () => {
785
- if (timedOut || settled) return;
896
+ if (budgetStop || settled) return;
786
897
  wasAborted = true;
787
898
  currentResult.aborted = true;
788
899
  currentResult.stopReason = "aborted";
789
900
  setErrorMessage("Subagent was aborted");
790
- cleanupTermination = terminateProcess(proc);
901
+ beginTermination(130);
791
902
  };
792
903
  if (signal.aborted) abortHandler();
793
904
  else signal.addEventListener("abort", abortHandler, { once: true });
794
905
  }
795
906
  });
796
907
 
797
- currentResult.exitCode = exitCode;
908
+ if (signal?.aborted && budgetStop) {
909
+ budgetStop = undefined;
910
+ timedOut = false;
911
+ currentResult.timedOut = undefined;
912
+ currentResult.aborted = true;
913
+ currentResult.stopReason = "aborted";
914
+ setErrorMessage("Subagent was aborted");
915
+ }
916
+ currentResult.exitCode = currentResult.aborted ? 130 : exitCode;
798
917
  const final = truncateUtf8(selectedAssistantOutput(), DEFAULT_MAX_OUTPUT_BYTES);
799
918
  currentResult.finalOutput = final.text;
800
919
  currentResult.truncated ||= final.truncated;
920
+ if (budgetStop) {
921
+ currentResult.partialOutput = currentResult.finalOutput || undefined;
922
+ currentResult.termination = {
923
+ version: TURN_TERMINATION_VERSION,
924
+ reason: budgetStop.reason,
925
+ limit: budgetStop.limit,
926
+ checkpoint: progressJournal.checkpoint(task, currentResult.partialOutput),
927
+ finalization: { attempted: false, status: "skipped", durationMs: 0 },
928
+ };
929
+ }
930
+ const remainingFinalizationMs = launchPolicy?.orchestrationDeadlineAt
931
+ ? Math.floor(launchPolicy.orchestrationDeadlineAt - Date.now())
932
+ : undefined;
933
+ if (
934
+ budgetStop &&
935
+ budgetStop.reason !== "orchestration_timeout" &&
936
+ launchPolicy?.finalizeOnTimeout !== false &&
937
+ !signal?.aborted &&
938
+ (remainingFinalizationMs === undefined || remainingFinalizationMs > 0)
939
+ ) {
940
+ const finalizationStartedAt = Date.now();
941
+ const requestedFinalizationMs = resolveTimeoutFinalizationMs(
942
+ timeoutMs,
943
+ launchPolicy?.timeoutFinalizationMs,
944
+ );
945
+ const finalizationMs = Math.min(
946
+ requestedFinalizationMs,
947
+ remainingFinalizationMs ?? requestedFinalizationMs,
948
+ );
949
+ const summary = await runSingleAgent(
950
+ defaultCwd,
951
+ agents,
952
+ agentName,
953
+ buildTimeoutFinalizationPrompt({
954
+ task,
955
+ partialOutput: currentResult.partialOutput,
956
+ recentActivity: currentResult.recentActivity,
957
+ checkpoint: currentResult.termination?.checkpoint,
958
+ terminationReason: budgetStop.reason,
959
+ resultFormat: launchPolicy?.timeoutResultFormat,
960
+ }),
961
+ cwd,
962
+ step,
963
+ signal,
964
+ thinkingLevel,
965
+ finalizationMs,
966
+ undefined,
967
+ makeDetails,
968
+ invocationOverride,
969
+ {
970
+ ...launchPolicy,
971
+ tools: [],
972
+ disableExtensions: true,
973
+ disableSkills: true,
974
+ disablePromptTemplates: true,
975
+ disableContextFiles: true,
976
+ appendSystemPromptPaths: undefined,
977
+ finalizeOnTimeout: false,
978
+ turnLimits: undefined,
979
+ workTimeoutReason: "work_timeout",
980
+ workTimeoutReportLimit: finalizationMs,
981
+ orchestrationDeadlineAt: undefined,
982
+ },
983
+ );
984
+ mergeUsageStats(currentResult.usage, summary.usage);
985
+ const summaryOutput = getResultFinalOutput(summary).trim();
986
+ if (summary.exitCode === 0 && summaryOutput) {
987
+ currentResult.timeoutSummary = summaryOutput;
988
+ currentResult.finalOutput = summaryOutput;
989
+ if (currentResult.termination) {
990
+ currentResult.termination.finalization = {
991
+ attempted: true,
992
+ status: "completed",
993
+ durationMs: Date.now() - finalizationStartedAt,
994
+ };
995
+ }
996
+ } else {
997
+ currentResult.timeoutSummaryError =
998
+ summary.errorMessage || summary.stderr.trim() || "Summary produced no final text";
999
+ if (currentResult.termination) {
1000
+ currentResult.termination.finalization = {
1001
+ attempted: true,
1002
+ status: summary.timedOut ? "timed_out" : "failed",
1003
+ durationMs: Date.now() - finalizationStartedAt,
1004
+ error: currentResult.timeoutSummaryError,
1005
+ };
1006
+ }
1007
+ }
1008
+ currentResult.truncated ||= summary.truncated;
1009
+ }
1010
+ if (currentResult.termination && !currentResult.finalOutput.trim()) {
1011
+ currentResult.finalOutput = formatTimeoutCheckpoint(currentResult.termination.checkpoint);
1012
+ }
1013
+ if (currentResult.exitCode === 0 && launchPolicy?.resultFormat !== undefined) {
1014
+ currentResult.structuredResult = parseAnyStructuredSubagentResult(
1015
+ currentResult.finalOutput ?? "",
1016
+ launchPolicy.resultFormat,
1017
+ );
1018
+ currentResult.resultContractInvalid =
1019
+ launchPolicy.resultFormat !== "text" && currentResult.structuredResult === undefined;
1020
+ if (
1021
+ currentResult.structuredResult?.version === "pi-subagents:result:v2" &&
1022
+ launchPolicy.executionPlan
1023
+ ) {
1024
+ currentResult.structuredResult.provenance = {
1025
+ ...currentResult.structuredResult.provenance,
1026
+ ...(launchPolicy.executionPlan.taskId
1027
+ ? { taskId: launchPolicy.executionPlan.taskId }
1028
+ : {}),
1029
+ taskGeneration: launchPolicy.executionPlan.taskGeneration,
1030
+ executionPlanId: launchPolicy.executionPlan.id,
1031
+ cancellationLineage: [...launchPolicy.executionPlan.cancellationLineage],
1032
+ };
1033
+ }
1034
+ if (currentResult.structuredResult?.version === "pi-subagents:result:v2") {
1035
+ currentResult.outcome = classifyStructuredOutcome(
1036
+ currentResult.structuredResult.status,
1037
+ currentResult.structuredResult.reasonCode,
1038
+ );
1039
+ } else if (currentResult.resultContractInvalid) {
1040
+ currentResult.outcome = classifyStructuredOutcome(
1041
+ "contract-invalid",
1042
+ "malformed-structured-result",
1043
+ );
1044
+ }
1045
+ }
801
1046
  if (
802
1047
  currentResult.exitCode === 0 &&
803
1048
  currentResult.stopReason !== "error" &&
@@ -807,6 +1052,13 @@ export async function runSingleAgent(
807
1052
  currentResult.stopReason = "error";
808
1053
  setErrorMessage("Subagent completed without final text");
809
1054
  }
1055
+ if (currentResult.capabilityGrant?.state === "active") {
1056
+ currentResult.capabilityGrant = revokeCapabilityGrant(
1057
+ currentResult.capabilityGrant,
1058
+ "turn-settled",
1059
+ Date.now(),
1060
+ );
1061
+ }
810
1062
  currentResult.policy = {
811
1063
  inherited: ["environment"],
812
1064
  overridden: [