pi-subagents 0.58.0 → 0.60.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 (116) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/docs/agents.md +5 -3
  3. package/docs/configuration.md +4 -4
  4. package/docs/extension-api.md +1 -1
  5. package/docs/models.md +24 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +93 -5
  8. package/docs/workflows.md +94 -3
  9. package/package.json +3 -1
  10. package/prompts/review-loop.md +2 -2
  11. package/skills/council-mode/SKILL.md +48 -243
  12. package/skills/council-mode/references/pass-contracts.md +150 -0
  13. package/skills/pi-subagents/SKILL.md +87 -37
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
  15. package/skills/pi-subagents/references/execution-controls.md +52 -7
  16. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -4
  17. package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  18. package/skills/pi-subagents/references/prompting-and-roles.md +54 -26
  19. package/skills/pi-subagents/references/review-and-validation.md +73 -0
  20. package/src/agents/agent-management.ts +159 -42
  21. package/src/agents/agent-serializer.ts +4 -2
  22. package/src/agents/agents.ts +67 -26
  23. package/src/agents/runtime-agent-registry.ts +9 -16
  24. package/src/api/background-work.ts +5 -1
  25. package/src/api/delegation.ts +0 -7
  26. package/src/api/preflight.ts +6 -9
  27. package/src/api/shared-types.ts +2 -0
  28. package/src/extension/fanout-child.ts +5 -3
  29. package/src/extension/index.ts +51 -23
  30. package/src/extension/public-execution.ts +16 -2
  31. package/src/extension/schemas.ts +36 -10
  32. package/src/extension/tool-description.ts +24 -5
  33. package/src/intercom/result-intercom.ts +2 -0
  34. package/src/profiles/profiles.ts +5 -6
  35. package/src/runs/background/active-async-capacity.ts +2 -2
  36. package/src/runs/background/async-execution.ts +52 -37
  37. package/src/runs/background/async-job-tracker.ts +17 -12
  38. package/src/runs/background/async-resume.ts +23 -25
  39. package/src/runs/background/async-status-snapshot.ts +23 -261
  40. package/src/runs/background/async-status.ts +110 -8
  41. package/src/runs/background/chain-append.ts +6 -3
  42. package/src/runs/background/chain-root-attachment.ts +60 -8
  43. package/src/runs/background/control-channel.ts +3 -2
  44. package/src/runs/background/fleet-view.ts +21 -11
  45. package/src/runs/background/notify.ts +158 -6
  46. package/src/runs/background/result-files.ts +2 -1
  47. package/src/runs/background/result-watcher.ts +2 -0
  48. package/src/runs/background/resume-guidance.ts +1 -1
  49. package/src/runs/background/retained-children.ts +1 -1
  50. package/src/runs/background/run-status.ts +30 -9
  51. package/src/runs/background/scheduled-runs.ts +86 -7
  52. package/src/runs/background/stale-run-reconciler.ts +10 -4
  53. package/src/runs/background/steering.ts +4 -14
  54. package/src/runs/background/subagent-runner.ts +351 -358
  55. package/src/runs/background/subagent-wait.ts +68 -12
  56. package/src/runs/background/terminal-run-index.ts +1 -1
  57. package/src/runs/background/wait-completions.ts +25 -1
  58. package/src/runs/background/wait-config.ts +23 -9
  59. package/src/runs/background/wait-tool.ts +9 -2
  60. package/src/runs/foreground/async-steering-action.ts +2 -2
  61. package/src/runs/foreground/execution.ts +125 -122
  62. package/src/runs/foreground/foreground-control.ts +3 -0
  63. package/src/runs/foreground/foreground-history.ts +1 -0
  64. package/src/runs/foreground/subagent-executor.ts +576 -240
  65. package/src/runs/foreground/workflow-detach-reconcile.ts +99 -200
  66. package/src/runs/shared/abort-recovery.ts +119 -0
  67. package/src/runs/shared/async-status-projection.ts +597 -0
  68. package/src/runs/shared/background-process-options.ts +9 -0
  69. package/src/runs/shared/child-identity.ts +19 -4
  70. package/src/runs/shared/child-launch-plan.ts +151 -0
  71. package/src/runs/shared/completion-evidence.ts +89 -0
  72. package/src/runs/shared/completion-guard.ts +1 -1
  73. package/src/runs/shared/dynamic-fanout.ts +2 -2
  74. package/src/runs/shared/host-step-status.ts +230 -0
  75. package/src/runs/shared/lane-metadata.ts +105 -0
  76. package/src/runs/shared/mcp-config-sources.ts +42 -6
  77. package/src/runs/shared/mcp-direct-tool-allowlist.ts +93 -143
  78. package/src/runs/shared/mcp-direct-tool-grant.ts +194 -0
  79. package/src/runs/shared/model-fallback.ts +9 -2
  80. package/src/runs/shared/mutation-evidence.ts +52 -3
  81. package/src/runs/shared/nested-events.ts +6 -2
  82. package/src/runs/shared/nested-render.ts +7 -3
  83. package/src/runs/shared/parallel-handoff.ts +419 -7
  84. package/src/runs/shared/parallel-utils.ts +7 -0
  85. package/src/runs/shared/pi-args.ts +67 -3
  86. package/src/runs/shared/single-output.ts +72 -22
  87. package/src/runs/shared/subagent-prompt-runtime.ts +33 -6
  88. package/src/runs/shared/workflow-graph.ts +15 -0
  89. package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
  90. package/src/runs/shared/worktree.ts +18 -0
  91. package/src/shared/child-session-name.ts +46 -0
  92. package/src/shared/extension-context.ts +24 -0
  93. package/src/shared/formatters.ts +5 -2
  94. package/src/shared/launch-contract.ts +1 -1
  95. package/src/shared/settings.ts +9 -103
  96. package/src/shared/types.ts +232 -30
  97. package/src/shared/utils.ts +35 -55
  98. package/src/slash/delegation-adapters.ts +1 -8
  99. package/src/slash/delegation-request.ts +0 -4
  100. package/src/slash/slash-bridge.ts +1 -2
  101. package/src/slash/slash-commands.ts +369 -90
  102. package/src/slash/slash-live-state.ts +22 -11
  103. package/src/tui/fleet-status.ts +134 -75
  104. package/src/tui/fleet.ts +11 -5
  105. package/src/tui/render-helpers.ts +31 -0
  106. package/src/tui/render.ts +895 -121
  107. package/src/watchdog/change-signature.ts +40 -1
  108. package/src/watchdog/turn-delta.ts +1 -1
  109. package/src/workflows/chat-progress.ts +6 -3
  110. package/src/workflows/host-command.ts +235 -0
  111. package/src/workflows/scripted-workflow.ts +503 -38
  112. package/src/workflows/workflow-child-summary.ts +9 -5
  113. package/src/workflows/workflow-preflight.ts +270 -0
  114. package/src/workflows/workflow-receipt.ts +43 -4
  115. package/src/workflows/workflow-settlement.ts +246 -0
  116. package/src/runs/shared/turn-budget.ts +0 -98
@@ -0,0 +1,246 @@
1
+ import type {
2
+ AsyncStatus,
3
+ WorkflowRecoveryAction,
4
+ WorkflowTerminalOutcome,
5
+ WorkflowTerminalResolution,
6
+ Usage,
7
+ } from "../shared/types.ts";
8
+ import type { WorkflowReceipt } from "./workflow-receipt.ts";
9
+ import { workflowChildSummary } from "./workflow-child-summary.ts";
10
+
11
+ export const UNSUPPORTED_DETACHED_WORKFLOW_CONTINUATION = "unsupported-continuation: detached workflow child settled, but JavaScript workflow continuation was not persisted. Resume the workflow explicitly instead of treating the completed child as top-level workflow completion.";
12
+ export const INTERRUPTED_DETACHED_CHILD = "Interrupted. Waiting for explicit next action.";
13
+ export const EVIDENCE_PERSISTENCE_FAILED = "evidence-persistence-failed";
14
+
15
+ export type WorkflowStatusStep = NonNullable<AsyncStatus["steps"]>[number] & {
16
+ outputPathMapping?: { requestedPath: string; savedPath: string };
17
+ interrupted?: boolean;
18
+ };
19
+
20
+ export interface WorkflowPublicChild {
21
+ workflowKey?: string;
22
+ agent?: string;
23
+ /** Human-readable display name for the child session, when derived at launch. */
24
+ sessionName?: string;
25
+ runId?: string;
26
+ usage?: Usage;
27
+ sessionFile?: string;
28
+ output: string;
29
+ outputState: "present" | "absent";
30
+ structuredOutput?: unknown;
31
+ success: boolean;
32
+ terminalOutcome?: WorkflowTerminalOutcome;
33
+ outputReference?: string;
34
+ outputPathMapping?: { requestedPath: string; savedPath: string };
35
+ stopped?: boolean;
36
+ interrupted?: boolean;
37
+ detached?: boolean;
38
+ error?: string;
39
+ artifactPaths?: { outputPath: string };
40
+ }
41
+
42
+ export interface WorkflowSettlementPlan {
43
+ status: AsyncStatus;
44
+ publicResult: Record<string, unknown>;
45
+ receipt?: WorkflowReceipt;
46
+ recovery: WorkflowRecoveryAction[];
47
+ completionEvent?: Record<string, unknown>;
48
+ }
49
+
50
+ function cloneWorkflowStatus(status: AsyncStatus): AsyncStatus {
51
+ return {
52
+ ...status,
53
+ steps: status.steps?.map((step) => ({ ...step })),
54
+ workflow: status.workflow ? { ...status.workflow, trace: [...(status.workflow.trace ?? [])] } : status.workflow,
55
+ };
56
+ }
57
+
58
+ function workflowState(status: AsyncStatus): "completed" | "failed" | "paused" | "stopped" {
59
+ return status.state === "complete" ? "completed" : status.state === "paused" ? "paused" : status.state === "stopped" ? "stopped" : "failed";
60
+ }
61
+
62
+ function withWorkflowChildren(status: AsyncStatus): AsyncStatus {
63
+ return {
64
+ ...status,
65
+ workflowChildren: workflowChildSummary({
66
+ parentToolCallId: status.toolCallId ?? status.runId,
67
+ workflowRunId: status.runId,
68
+ workflowState: workflowState(status),
69
+ inventoryComplete: true,
70
+ trace: status.workflow?.trace,
71
+ steps: status.steps,
72
+ }),
73
+ };
74
+ }
75
+
76
+ export function findWorkflowSettlementStep(status: AsyncStatus, childRunId: string, workflowKey?: string, sessionFile?: string): WorkflowStatusStep | undefined {
77
+ const exact = status.steps?.find((candidate) => candidate.runId === childRunId);
78
+ if (exact || !workflowKey || !sessionFile) return exact;
79
+ const candidates = status.steps?.filter((candidate) => candidate.runId === undefined
80
+ && candidate.workflowKey === workflowKey
81
+ && candidate.sessionFile === sessionFile) ?? [];
82
+ return candidates.length === 1 ? candidates[0] : undefined;
83
+ }
84
+
85
+ export function promoteSettledPausedWorkflow(status: AsyncStatus, now = Date.now()): AsyncStatus | undefined {
86
+ if (status.mode !== "workflow" || status.state !== "paused") return undefined;
87
+ const next = cloneWorkflowStatus(status);
88
+ const stillOpen = next.steps?.some((candidate) =>
89
+ candidate.status === "running"
90
+ || (candidate.status === "paused" && candidate.activityState === "needs_attention")
91
+ ) === true;
92
+ if (stillOpen || !next.steps?.length) return undefined;
93
+ const failed = next.steps.some((candidate) => candidate.status === "failed");
94
+ next.lastUpdate = now;
95
+ next.state = "failed";
96
+ next.endedAt = now;
97
+ delete next.activityState;
98
+ if (!failed) next.error = UNSUPPORTED_DETACHED_WORKFLOW_CONTINUATION;
99
+ return withWorkflowChildren(next);
100
+ }
101
+
102
+ export function applyDetachedChildSettlement(
103
+ status: AsyncStatus,
104
+ input: { childRunId: string; result: { exitCode: number | null; error?: string; interrupted?: boolean; sessionFile?: string; sessionName?: string; stopped?: boolean }; workflowKey?: string; now?: number },
105
+ ): AsyncStatus | undefined {
106
+ if (status.mode !== "workflow" || status.state !== "paused") return undefined;
107
+ const next = cloneWorkflowStatus(status);
108
+ const step = findWorkflowSettlementStep(next, input.childRunId, input.workflowKey, input.result.sessionFile);
109
+ if (!step) return undefined;
110
+ step.runId ??= input.childRunId;
111
+ const succeeded = input.result.exitCode === 0 && !input.result.error && !input.result.interrupted;
112
+ const failedSiblingError = next.steps?.find((candidate) => {
113
+ const candidateStep = candidate as WorkflowStatusStep;
114
+ return candidateStep !== step && candidateStep.status === "failed" && !candidateStep.interrupted && candidateStep.error;
115
+ })?.error;
116
+ const interruptedChildError = next.steps?.find((candidate) => {
117
+ const candidateStep = candidate as WorkflowStatusStep;
118
+ return candidateStep.status === "failed" && candidateStep.interrupted && candidateStep.error;
119
+ })?.error;
120
+ const now = input.now ?? Date.now();
121
+ step.status = succeeded ? "completed" : "failed";
122
+ step.endedAt = now;
123
+ delete step.activityState;
124
+ delete step.currentTool;
125
+ delete step.currentToolStartedAt;
126
+ if (input.result.sessionFile) step.sessionFile = input.result.sessionFile;
127
+ if (input.result.sessionName) step.sessionName = input.result.sessionName;
128
+ if (succeeded) {
129
+ delete step.error;
130
+ delete step.interrupted;
131
+ } else if (input.result.interrupted || input.result.stopped) {
132
+ step.error = input.result.error ?? INTERRUPTED_DETACHED_CHILD;
133
+ step.interrupted = true;
134
+ if (input.result.stopped) step.stopped = true;
135
+ } else {
136
+ delete step.interrupted;
137
+ delete step.stopped;
138
+ if (input.result.error) step.error = input.result.error;
139
+ }
140
+ next.lastUpdate = now;
141
+ const promoted = promoteSettledPausedWorkflow(next, now);
142
+ if (promoted?.state === "failed" && failedSiblingError) promoted.error = failedSiblingError;
143
+ else if (promoted?.state === "failed" && (input.result.interrupted || input.result.stopped)) promoted.error = input.result.error ?? INTERRUPTED_DETACHED_CHILD;
144
+ else if (promoted?.state === "failed" && input.result.error) promoted.error = input.result.error;
145
+ else if (promoted?.state === "failed" && interruptedChildError) promoted.error = interruptedChildError;
146
+ return withWorkflowChildren(promoted ?? next);
147
+ }
148
+
149
+ export function classifyWorkflowSettlement(status: AsyncStatus, interrupted = false): WorkflowTerminalResolution | undefined {
150
+ if (status.state !== "complete" && status.state !== "failed") return undefined;
151
+ if (status.steps?.some((step) => {
152
+ const candidate = step as WorkflowStatusStep;
153
+ return candidate.status === "failed" && !candidate.interrupted;
154
+ })) return "failed-child";
155
+ if (interrupted || status.steps?.some((step) => {
156
+ const candidate = step as WorkflowStatusStep;
157
+ return candidate.status === "stopped" || candidate.stopped || candidate.interrupted || candidate.error === INTERRUPTED_DETACHED_CHILD;
158
+ })) return "interrupted-child";
159
+ return "settled-awaiting-resume";
160
+ }
161
+
162
+ export function workflowRecoveryActions(receipt: WorkflowReceipt | undefined): WorkflowRecoveryAction[] {
163
+ if (!receipt) return [];
164
+ return Object.values(receipt.entries).flatMap((entry) => entry.resumability.state === "resumable"
165
+ ? [{ key: entry.key, call: "runs.run" as const, resume: { workflowRunId: receipt.workflowRunId, key: entry.key, latest: true as const }, taskRequired: true as const }]
166
+ : []);
167
+ }
168
+
169
+ export function workflowTerminalOutcomeForResult(result: { timedOut?: boolean; turnBudgetExceeded?: boolean; toolBudgetBlocked?: boolean }): WorkflowTerminalOutcome | undefined {
170
+ if (result.timedOut) return { state: "partial", reason: "timeout" };
171
+ if (result.turnBudgetExceeded || result.toolBudgetBlocked) return { state: "partial", reason: "budget_exhausted" };
172
+ return undefined;
173
+ }
174
+
175
+ export function workflowOutputPathMappingSummary(children: readonly unknown[]): string {
176
+ const mappings = children.flatMap((entry) => {
177
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
178
+ const child = entry as Partial<WorkflowPublicChild>;
179
+ return child.outputPathMapping
180
+ ? [`'${child.workflowKey ?? "child"}': requested ${child.outputPathMapping.requestedPath} -> saved ${child.outputPathMapping.savedPath}`]
181
+ : [];
182
+ });
183
+ return mappings.length > 0 ? ` Output path mappings: ${mappings.join("; ")}.` : "";
184
+ }
185
+
186
+ export function planWorkflowSettlement(input: {
187
+ status: AsyncStatus;
188
+ summary: string;
189
+ children: WorkflowPublicChild[];
190
+ baseResult: Record<string, unknown>;
191
+ receipt?: WorkflowReceipt;
192
+ receiptPath?: string;
193
+ receiptPersistenceError?: string;
194
+ resolution?: WorkflowTerminalResolution;
195
+ terminalOutcome?: WorkflowTerminalOutcome;
196
+ now?: number;
197
+ eventMetadata?: Record<string, unknown>;
198
+ }): WorkflowSettlementPlan {
199
+ const now = input.now ?? Date.now();
200
+ let status = withWorkflowChildren(cloneWorkflowStatus(input.status));
201
+ let summary = input.summary;
202
+ let receipt = input.receipt;
203
+ let resolution = input.resolution;
204
+ if (input.receiptPersistenceError) {
205
+ const diagnostic = `${EVIDENCE_PERSISTENCE_FAILED}: ${input.receiptPersistenceError}`;
206
+ status = withWorkflowChildren({ ...status, state: "failed", error: diagnostic, activityState: undefined, endedAt: now, lastUpdate: now });
207
+ summary = `${diagnostic} Available child evidence was preserved, but workflow completion was not accepted.`;
208
+ receipt = undefined;
209
+ if (!resolution) resolution = classifyWorkflowSettlement(status);
210
+ }
211
+ const recovery = workflowRecoveryActions(receipt);
212
+ if (receipt) {
213
+ receipt = {
214
+ ...receipt,
215
+ state: status.state === "complete" ? "complete" : status.state === "paused" ? "paused" : status.state === "stopped" ? "stopped" : "failed",
216
+ workflowChildren: status.workflowChildren,
217
+ ...(resolution ? { workflowResolution: resolution, recovery } : {}),
218
+ };
219
+ }
220
+ const publicResult: Record<string, unknown> = {
221
+ ...input.baseResult,
222
+ success: status.state === "complete",
223
+ state: status.state,
224
+ summary,
225
+ error: status.state === "complete" ? undefined : status.error ?? summary,
226
+ stopped: status.stopped ? true : undefined,
227
+ activityState: status.activityState,
228
+ workflowChildren: status.workflowChildren,
229
+ results: input.children,
230
+ ...(input.terminalOutcome ? { terminalOutcome: input.terminalOutcome } : {}),
231
+ ...(resolution ? { workflowResolution: resolution, recovery } : {}),
232
+ ...(receipt && input.receiptPath ? { workflowReceipt: { path: input.receiptPath, receipt } } : {}),
233
+ timestamp: now,
234
+ };
235
+ const terminal = status.state === "complete" || status.state === "failed" || status.state === "partial" || status.state === "stopped";
236
+ const completionEvent = terminal ? {
237
+ type: "subagent.workflow.completed",
238
+ state: status.state,
239
+ ...(resolution ? { workflowResolution: resolution } : {}),
240
+ ...(input.terminalOutcome ? { terminalOutcome: input.terminalOutcome } : {}),
241
+ ...(status.error ? { error: status.error } : {}),
242
+ ...(status.activityState ? { activityState: status.activityState } : {}),
243
+ ...input.eventMetadata,
244
+ } : undefined;
245
+ return { status, publicResult, receipt, recovery, completionEvent };
246
+ }
@@ -1,98 +0,0 @@
1
- import type { ResolvedTurnBudget, TurnBudgetState } from "../../shared/types.ts";
2
-
3
- export const DEFAULT_TURN_BUDGET_GRACE_TURNS = 1;
4
-
5
- export function resolveTurnBudgetConfig(
6
- raw: unknown,
7
- label = "turnBudget",
8
- ): { turnBudget?: ResolvedTurnBudget; error?: string } {
9
- if (raw === undefined) return {};
10
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
11
- return { error: `${label} must be an object with maxTurns and optional graceTurns.` };
12
- }
13
- const unknownField = Object.keys(raw).find((key) => key !== "maxTurns" && key !== "graceTurns");
14
- if (unknownField) return { error: `${label}.${unknownField} is not supported.` };
15
- const budget = raw as { maxTurns?: unknown; graceTurns?: unknown };
16
- if (typeof budget.maxTurns !== "number" || !Number.isInteger(budget.maxTurns) || budget.maxTurns < 1) {
17
- return { error: `${label}.maxTurns must be an integer >= 1.` };
18
- }
19
- const graceTurns = budget.graceTurns ?? DEFAULT_TURN_BUDGET_GRACE_TURNS;
20
- if (typeof graceTurns !== "number" || !Number.isInteger(graceTurns) || graceTurns < 0) {
21
- return { error: `${label}.graceTurns must be an integer >= 0.` };
22
- }
23
- return { turnBudget: { maxTurns: budget.maxTurns, graceTurns } };
24
- }
25
-
26
- export function appendTurnBudgetSystemPrompt(systemPrompt: string, budget: { maxTurns: number; graceTurns?: number } | undefined): string {
27
- if (!budget) return systemPrompt;
28
- const graceTurns = budget.graceTurns ?? DEFAULT_TURN_BUDGET_GRACE_TURNS;
29
- const grace = graceTurns === 1 ? "1 additional assistant turn" : `${graceTurns} additional assistant turns`;
30
- const block = [
31
- "## Turn budget",
32
- `This child run has a soft budget of ${budget.maxTurns} assistant turn${budget.maxTurns === 1 ? "" : "s"}.`,
33
- `After that, ${grace} may be allowed only for a final wrap-up.`,
34
- "When you approach or reach the soft budget, stop starting new tool work and return the final answer immediately.",
35
- "This runner uses process-mode execution, so live steering after launch may be unavailable; treat this instruction as the wrap-up request.",
36
- "If you continue past the soft budget plus grace turns, the supervisor may abort the process and return only partial output.",
37
- ].join("\n");
38
- return systemPrompt.trim() ? `${systemPrompt.trim()}\n\n${block}` : block;
39
- }
40
-
41
- export function turnBudgetSoftNote(budget: ResolvedTurnBudget, turnCount: number): string {
42
- return `Turn budget wrap-up was requested after ${turnCount} assistant turn${turnCount === 1 ? "" : "s"} (soft limit ${budget.maxTurns}, grace ${budget.graceTurns}). Process-mode live steering is unavailable, so the child was warned at launch to wrap up by this budget. Output may be partial.`;
43
- }
44
-
45
- export function turnBudgetExceededMessage(budget: ResolvedTurnBudget, turnCount: number): string {
46
- return `Subagent exceeded turn budget after ${turnCount} assistant turn${turnCount === 1 ? "" : "s"} (soft limit ${budget.maxTurns} + grace ${budget.graceTurns}).`;
47
- }
48
-
49
- export function turnBudgetDeferredNote(budget: ResolvedTurnBudget, turnCount: number): string {
50
- return `Turn-budget termination was deferred at ${turnCount} assistant turn${turnCount === 1 ? "" : "s"} (soft limit ${budget.maxTurns} + grace ${budget.graceTurns}) because the assistant started tool work. The run ended before another safe assistant boundary; output may be partial.`;
51
- }
52
-
53
- export function formatTurnBudgetOutput(message: string, output: string): string {
54
- return output.trim()
55
- ? `${message}\n\nPartial output before turn-budget abort:\n${output}`
56
- : message;
57
- }
58
-
59
- export function initialTurnBudgetState(budget: { maxTurns: number; graceTurns?: number }): TurnBudgetState {
60
- return { maxTurns: budget.maxTurns, graceTurns: budget.graceTurns ?? DEFAULT_TURN_BUDGET_GRACE_TURNS, outcome: "within-budget", turnCount: 0 };
61
- }
62
-
63
- export function turnBudgetState(budget: ResolvedTurnBudget, turnCount: number, exceeded: boolean): TurnBudgetState {
64
- return {
65
- ...budget,
66
- turnCount,
67
- outcome: exceeded ? "exceeded" : "wrap-up-requested",
68
- wrapUpRequestedAtTurn: budget.maxTurns,
69
- ...(exceeded ? { exceededAtTurn: turnCount } : {}),
70
- };
71
- }
72
-
73
- export function turnBudgetDeferredState(
74
- budget: ResolvedTurnBudget,
75
- turnCount: number,
76
- terminationDeferredAtTurn = turnCount,
77
- ): TurnBudgetState {
78
- return {
79
- ...budget,
80
- turnCount,
81
- outcome: "termination-deferred",
82
- wrapUpRequestedAtTurn: budget.maxTurns,
83
- terminationDeferredAtTurn,
84
- };
85
- }
86
-
87
- export function turnBudgetDecision(
88
- budget: ResolvedTurnBudget,
89
- turnCount: number,
90
- terminalAssistantStop: boolean,
91
- toolWorkActiveOrStarting: boolean,
92
- enforceHardLimit = false,
93
- ): "continue" | "defer" | "abort" {
94
- const hardLimit = budget.maxTurns + budget.graceTurns;
95
- if (terminalAssistantStop || turnCount < hardLimit) return "continue";
96
- if (toolWorkActiveOrStarting && !enforceHardLimit) return "defer";
97
- return "abort";
98
- }