@narumitw/pi-subagents 0.49.3 → 0.52.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 (83) hide show
  1. package/README.md +362 -53
  2. package/package.json +10 -7
  3. package/src/adaptive-scheduler.ts +224 -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 +1098 -158
  24. package/src/in-process-transport.ts +269 -25
  25. package/src/inspect-render.ts +101 -1
  26. package/src/inspect.ts +321 -3
  27. package/src/integration-controller.ts +98 -0
  28. package/src/limits.ts +3 -0
  29. package/src/orchestration-metrics.ts +109 -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 +770 -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 +179 -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 +67 -0
  77. package/src/work-item-ledger.ts +931 -0
  78. package/src/work-item-persistence.ts +223 -0
  79. package/src/workflow-planning.ts +162 -0
  80. package/src/workflow-tree-identity.ts +289 -0
  81. package/src/workflow-ui.ts +61 -0
  82. package/src/workflow-verification.ts +296 -0
  83. package/src/workspace.ts +69 -12
package/src/subagents.ts CHANGED
@@ -4,10 +4,7 @@
4
4
  * Spawns a separate `pi` process for each subagent invocation,
5
5
  * giving it an isolated context window.
6
6
  *
7
- * Supports three modes:
8
- * - Single: { agent: "name", task: "..." }
9
- * - Parallel: { tasks: [{ agent: "name", task: "..." }, ...] }
10
- * - Chain: { chain: [{ agent: "name", task: "... {previous} ..." }, ...] }
7
+ * Supports blocking single, parallel, chain, workflow, and panel modes.
11
8
  *
12
9
  * Uses JSON mode to capture structured output from subagents.
13
10
  */
@@ -25,10 +22,11 @@ import {
25
22
  formatAgentCatalog,
26
23
  type SubagentSettings,
27
24
  } from "./agents.js";
28
- import { registerSubagentConfigCommand } from "./config-ui.js";
25
+ import { registerSubagentConfigCommand, registerSubagentConfigLifecycle } from "./config-ui.js";
29
26
  import { registerSubagentConsult } from "./consult.js";
30
27
  import { executeSubagent } from "./execution.js";
31
28
  import { registerSubagentInspect } from "./inspect.js";
29
+ import { MAX_BLOCKING_PARALLEL_CONCURRENCY } from "./limits.js";
32
30
  import { SubagentParams } from "./params.js";
33
31
  import { renderSubagentCall, renderSubagentResult } from "./render.js";
34
32
  import type { SubagentDetails } from "./runner.js";
@@ -39,10 +37,12 @@ import {
39
37
  DEFAULT_DELEGATION_CWD_POLICY,
40
38
  inspectSubagentSettings,
41
39
  readSubagentSettings,
40
+ resolveBlockingMaxParallelTasks,
42
41
  } from "./settings.js";
43
42
  import { registerStatefulSubagents } from "./stateful.js";
44
43
 
45
44
  export default function (pi: ExtensionAPI) {
45
+ const configOwner = registerSubagentConfigLifecycle(pi);
46
46
  const settings = readSubagentSettings();
47
47
  let currentSettings: SubagentSettings | undefined = settings;
48
48
  let currentCatalog = "";
@@ -80,6 +80,7 @@ export default function (pi: ExtensionAPI) {
80
80
  });
81
81
  refreshStatefulCatalog = statefulRuntime.setAgentCatalog;
82
82
  const getBlockingEnabled = () => blockingEnabled;
83
+ const getMaxParallelTasks = () => resolveBlockingMaxParallelTasks(currentSettings);
83
84
  const getConsultResourcePolicy = () =>
84
85
  currentSettings?.consult?.resources ?? DEFAULT_CONSULT_RESOURCE_POLICY;
85
86
  const getConsultationCwdPolicy = () =>
@@ -89,6 +90,7 @@ export default function (pi: ExtensionAPI) {
89
90
  registerSubagentInspect(pi, {
90
91
  ...statefulRuntime,
91
92
  getBlockingEnabled,
93
+ getMaxParallelTasks,
92
94
  getConsultResourcePolicy,
93
95
  getConsultationCwdPolicy,
94
96
  getDelegationCwdPolicy,
@@ -98,35 +100,61 @@ export default function (pi: ExtensionAPI) {
98
100
  getSettings: () => currentSettings,
99
101
  });
100
102
  }
101
- registerSubagentConfigCommand(pi, {
102
- ...statefulRuntime,
103
- getBlockingEnabled,
104
- getConsultResourcePolicy,
105
- getConsultationCwdPolicy,
106
- getDelegationCwdPolicy,
107
- setConsultResourcePolicy(value: ConsultResourcePolicy) {
108
- currentSettings = {
109
- ...(currentSettings ?? {}),
110
- consult: { ...(currentSettings?.consult ?? {}), resources: value },
111
- };
112
- refreshConsultCatalog(currentCatalog);
103
+ registerSubagentConfigCommand(
104
+ pi,
105
+ {
106
+ ...statefulRuntime,
107
+ getBlockingEnabled,
108
+ getMaxParallelTasks,
109
+ getConsultResourcePolicy,
110
+ getConsultationCwdPolicy,
111
+ getDelegationCwdPolicy,
112
+ setMaxParallelTasks(value: number) {
113
+ const previousSettings = currentSettings;
114
+ currentSettings = {
115
+ ...(currentSettings ?? {}),
116
+ blocking: { ...(currentSettings?.blocking ?? {}), maxParallelTasks: value },
117
+ };
118
+ try {
119
+ refreshBlockingCatalog(currentCatalog);
120
+ } catch (applyError) {
121
+ currentSettings = previousSettings;
122
+ try {
123
+ refreshBlockingCatalog(currentCatalog);
124
+ } catch (rollbackError) {
125
+ throw new AggregateError(
126
+ [applyError, rollbackError],
127
+ "Failed to apply and roll back the parallel-worker limit",
128
+ );
129
+ }
130
+ throw applyError;
131
+ }
132
+ },
133
+ setConsultResourcePolicy(value: ConsultResourcePolicy) {
134
+ currentSettings = {
135
+ ...(currentSettings ?? {}),
136
+ consult: { ...(currentSettings?.consult ?? {}), resources: value },
137
+ };
138
+ refreshConsultCatalog(currentCatalog);
139
+ },
140
+ setConsultationCwdPolicy(value: ConsultationCwdPolicy) {
141
+ currentSettings = {
142
+ ...(currentSettings ?? {}),
143
+ cwdPolicy: { ...(currentSettings?.cwdPolicy ?? {}), consultation: value },
144
+ };
145
+ refreshConsultCatalog(currentCatalog);
146
+ },
147
+ setDelegationCwdPolicy(value: DelegationCwdPolicy) {
148
+ currentSettings = {
149
+ ...(currentSettings ?? {}),
150
+ cwdPolicy: { ...(currentSettings?.cwdPolicy ?? {}), delegation: value },
151
+ };
152
+ refreshBlockingCatalog(currentCatalog);
153
+ statefulRuntime.refreshSettingsGuidance();
154
+ },
113
155
  },
114
- setConsultationCwdPolicy(value: ConsultationCwdPolicy) {
115
- currentSettings = {
116
- ...(currentSettings ?? {}),
117
- cwdPolicy: { ...(currentSettings?.cwdPolicy ?? {}), consultation: value },
118
- };
119
- refreshConsultCatalog(currentCatalog);
120
- },
121
- setDelegationCwdPolicy(value: DelegationCwdPolicy) {
122
- currentSettings = {
123
- ...(currentSettings ?? {}),
124
- cwdPolicy: { ...(currentSettings?.cwdPolicy ?? {}), delegation: value },
125
- };
126
- refreshBlockingCatalog(currentCatalog);
127
- statefulRuntime.refreshSettingsGuidance();
128
- },
129
- });
156
+ configOwner,
157
+ );
130
158
  }
131
159
 
132
160
  function registerBlockingSubagent(
@@ -134,36 +162,70 @@ function registerBlockingSubagent(
134
162
  getSettings: () => SubagentSettings | undefined,
135
163
  ): (catalog: string) => void {
136
164
  let catalog = "";
165
+ const activeControllers = new Set<AbortController>();
166
+ const activeWork = new Set<Promise<unknown>>();
167
+ const cancelAndWaitForWork = async (reason: string) => {
168
+ for (const controller of activeControllers) {
169
+ controller.abort(new DOMException(reason, "AbortError"));
170
+ }
171
+ await Promise.allSettled([...activeWork]);
172
+ };
173
+ pi.on("session_start", () => cancelAndWaitForWork("Blocking subagent session replaced"));
174
+ pi.on("session_shutdown", () => cancelAndWaitForWork("Blocking subagent session shut down"));
137
175
  const baseDescription = () =>
138
176
  [
139
177
  "Run specialized subagents as a blocking operation with isolated contexts.",
140
178
  "The call blocks the main agent until every worker and optional aggregator finishes, so queued steering waits.",
141
- "Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
142
- "Parallel mode may include an aggregator fan-in step that receives all task outputs. Use subagent_consult instead for one synchronous child that must be executor-constrained to read-only tools.",
179
+ "Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder), workflow (named dependency tasks with optional capability routing), or panel (independent reviewers plus evidence-preserving synthesis).",
180
+ "Parallel mode may include an aggregator fan-in step; workflow mode validates dependencies, authority, artifacts, scope conflicts, retries, and hedging before scheduling. Use subagent_consult instead for one synchronous child that must be executor-constrained to read-only tools.",
143
181
  'Default agent scope is "user" (from ~/.pi/agent/agents).',
144
182
  `To enable project-local agents in ${CONFIG_DIR_NAME}/agents, pass agentScope: "both" (or "project") as a top-level argument for that call.`,
183
+ `Maximum parallel worker tasks per call: ${resolveBlockingMaxParallelTasks(getSettings())}. Parallel execution starts at most ${MAX_BLOCKING_PARALLEL_CONCURRENCY} workers at once.`,
145
184
  `Working-directory target policy: ${getSettings()?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY}. This controls launch targets and protected project resources, not filesystem access or sandboxing.`,
146
185
  ].join(" ");
186
+ const promptGuidelines = () => [
187
+ "Use subagent only when delegation fits; the main agent should decide how many subagents to spawn from task shape instead of waiting for the user to specify a count.",
188
+ "Use no subagent for simple answers, quick targeted edits, latency-sensitive one-step work, tasks requiring frequent user back-and-forth, or critical-path work the main agent can perform directly.",
189
+ "Use the blocking subagent tool only when delegated outputs are required before the main agent's next action and waiting is intentional; the main agent cannot process queued steering until the call returns.",
190
+ "Use a blocking subagent single, parallel, chain, workflow, panel, or fan-in call only when synchronous context or output isolation is worth making the main agent unavailable while it runs.",
191
+ `If a blocking parallel subagent call is genuinely required, keep tasks independent, stay within the configured max ${resolveBlockingMaxParallelTasks(getSettings())}, and avoid write-heavy implementation touching the same files or shared state.`,
192
+ "For parallel subagent calls, omit the aggregator key entirely unless a fan-in step is required; do not send null, empty strings, or an empty object for unused optional fields.",
193
+ "Use workflow mode for explicit dependencies or capability routing; declare read/write or ownership scopes, require structured-v2 artifacts when downstream tasks consume them, and use retry or hedging only with the required side-effect contract.",
194
+ "Use panel mode only for consequential review or research that benefits from at least two independent reviewers and one bounded synthesis; agreement is not proof, dissent and blocking objections remain visible, and simple or latency-sensitive work should not use a panel.",
195
+ 'Do not use subagent with project-local agents unless the user explicitly wants project agents or sets agentScope to "project" or "both"; keep confirmation enabled for untrusted repositories.',
196
+ "When using subagent, write self-contained tasks with file paths, context, expected output, and whether the subagent may edit files.",
197
+ "Set subagent timeoutMs to the shortest realistic work deadline for the task difficulty, just as thinkingLevel should match reasoning difficulty; split oversized tasks instead of extending the deadline merely to compensate for broad scope. Use totalTimeoutMs to cap an entire blocking workflow, idleTimeoutMs for stalled work, and maxTurns or maxToolCalls to stop repeated work without progress. Every budget stop preserves a bounded checkpoint and may make one separately bounded summary attempt.",
198
+ ];
147
199
  const definition: ToolDefinition<typeof SubagentParams, SubagentDetails> = {
148
200
  name: "subagent",
149
201
  label: "Blocking Subagent",
150
202
  description: appendAgentCatalog(baseDescription(), catalog),
151
203
  promptSnippet:
152
204
  "Run blocking isolated subagents only when their outputs are required before the main agent can continue.",
153
- promptGuidelines: [
154
- "Use subagent only when delegation fits; the main agent should decide how many subagents to spawn from task shape instead of waiting for the user to specify a count.",
155
- "Use no subagent for simple answers, quick targeted edits, latency-sensitive one-step work, tasks requiring frequent user back-and-forth, or critical-path work the main agent can perform directly.",
156
- "Use the blocking subagent tool only when delegated outputs are required before the main agent's next action and waiting is intentional; the main agent cannot process queued steering until the call returns.",
157
- "Use a blocking subagent single, parallel, chain, or fan-in call only when synchronous context or output isolation is worth making the main agent unavailable while it runs.",
158
- "If a blocking parallel subagent call is genuinely required, keep tasks independent, stay within the hard max 8, and avoid write-heavy implementation touching the same files or shared state.",
159
- "For parallel subagent calls, omit the aggregator key entirely unless a fan-in step is required; do not send null, empty strings, or an empty object for unused optional fields.",
160
- 'Do not use subagent with project-local agents unless the user explicitly wants project agents or sets agentScope to "project" or "both"; keep confirmation enabled for untrusted repositories.',
161
- "When using subagent, write self-contained tasks with file paths, context, expected output, and whether the subagent may edit files.",
162
- ],
205
+ promptGuidelines: promptGuidelines(),
163
206
  parameters: SubagentParams,
164
207
 
165
208
  async execute(toolCallId, params, signal, onUpdate, ctx) {
166
- return executeSubagent(toolCallId, params, signal, onUpdate, ctx, getSettings());
209
+ const lifecycleController = new AbortController();
210
+ activeControllers.add(lifecycleController);
211
+ const effectiveSignal = signal
212
+ ? AbortSignal.any([signal, lifecycleController.signal])
213
+ : lifecycleController.signal;
214
+ const work = executeSubagent(
215
+ toolCallId,
216
+ params,
217
+ effectiveSignal,
218
+ onUpdate,
219
+ ctx,
220
+ getSettings(),
221
+ );
222
+ activeWork.add(work);
223
+ try {
224
+ return await work;
225
+ } finally {
226
+ activeControllers.delete(lifecycleController);
227
+ activeWork.delete(work);
228
+ }
167
229
  },
168
230
 
169
231
  renderCall(args, theme) {
@@ -183,6 +245,7 @@ function registerBlockingSubagent(
183
245
  return (nextCatalog: string) => {
184
246
  catalog = nextCatalog;
185
247
  definition.description = appendAgentCatalog(baseDescription(), catalog);
248
+ definition.promptGuidelines = promptGuidelines();
186
249
  pi.registerTool<typeof SubagentParams, SubagentDetails>(definition);
187
250
  };
188
251
  }
@@ -198,22 +261,27 @@ export {
198
261
  DEFAULT_CONSULT_RESOURCE_POLICY,
199
262
  DEFAULT_CONSULTATION_CWD_POLICY,
200
263
  DEFAULT_DELEGATION_CWD_POLICY,
264
+ inspectBlockingParallelLimitSettings,
201
265
  inspectCompletionDeliverySettings,
202
266
  inspectConsultResourceSettings,
203
267
  inspectCwdPolicySettings,
204
268
  inspectDelegationWorkflowSettings,
269
+ inspectStatefulLimitSettings,
205
270
  inspectSubagentSettings,
206
271
  normalizeAgentSettings,
207
272
  normalizeSubagentSettings,
208
273
  readSubagentSettings,
274
+ resolveBlockingMaxParallelTasks,
209
275
  resolveSubagentThinkingLevel,
210
276
  sameToolSet,
211
277
  saveSubagentConfig,
212
278
  subagentSettingsFilePath,
213
279
  uniqueToolNames,
214
280
  updateAgentToolsSetting,
281
+ updateBlockingMaxParallelTasksSetting,
215
282
  updateCompletionDeliverySetting,
216
283
  updateConsultResourceSetting,
217
284
  updateCwdPolicySetting,
218
285
  updateDelegationWorkflowSetting,
286
+ updateStatefulLimitSetting,
219
287
  } from "./settings.js";
@@ -4,11 +4,13 @@ import {
4
4
  type SubagentSettings,
5
5
  type SubagentThinkingLevel,
6
6
  } from "./agents.js";
7
+ import { resolvePiPromptResources } from "./prompt-resources.js";
7
8
  import type { ManagedAgent, TurnOutcome } from "./registry.js";
8
9
  import { getResultFinalOutput, runSingleAgent, type SubagentDetails } from "./runner.js";
9
10
  import { readSubagentSettings, resolveSubagentThinkingLevel } from "./settings.js";
10
11
  import { buildStatefulTurnPrompt, resolveStatefulTurnTimeout } from "./stateful-prompt.js";
11
12
  import type { SubagentTransport } from "./transport.js";
13
+ import type { TransportProgressCallback, TransportTelemetry } from "./transport-types.js";
12
14
 
13
15
  export function resolveStatefulSubprocessThinkingLevel(
14
16
  agents: readonly Pick<AgentConfig, "name" | "thinkingLevel">[],
@@ -26,11 +28,30 @@ export class SubprocessTransport implements SubagentTransport {
26
28
 
27
29
  constructor(private readonly options: SubprocessTransportOptions = {}) {}
28
30
 
29
- async runTurn(record: ManagedAgent, task: string, signal: AbortSignal): Promise<TurnOutcome> {
31
+ async runTurn(
32
+ record: ManagedAgent,
33
+ task: string,
34
+ signal: AbortSignal,
35
+ onProgress?: TransportProgressCallback,
36
+ ): Promise<TurnOutcome> {
37
+ const startedAt = Date.now();
38
+ const starting: TransportTelemetry = {
39
+ transport: "subprocess",
40
+ phase: "starting",
41
+ updatedAt: startedAt,
42
+ timing: { startedAt, transportStartedAt: startedAt },
43
+ };
44
+ onProgress?.(starting);
30
45
  const settings = this.options.getSettings ? this.options.getSettings() : readSubagentSettings();
31
46
  const discovery = discoverAgents(record.cwd, record.agentScope ?? "user", settings);
32
47
  const agent = discovery.agents.find((candidate) => candidate.name === record.agent);
33
48
  const boundedTask = buildStatefulTurnPrompt(record, task);
49
+ const projectTrust =
50
+ record.target?.trust.projectTrusted ??
51
+ (record.agentScope === "project" || record.agentScope === "both");
52
+ const promptResources = agent?.systemPrompt.trim()
53
+ ? await resolvePiPromptResources(record.cwd, projectTrust)
54
+ : undefined;
34
55
  const makeDetails = (results: SubagentDetails["results"]): SubagentDetails => ({
35
56
  mode: "single",
36
57
  agentScope: record.agentScope ?? "user",
@@ -46,16 +67,52 @@ export class SubprocessTransport implements SubagentTransport {
46
67
  undefined,
47
68
  signal,
48
69
  resolveStatefulSubprocessThinkingLevel(discovery.agents, record),
49
- resolveStatefulTurnTimeout(agent),
70
+ record.currentTimeoutMs ?? record.timeoutMs ?? resolveStatefulTurnTimeout(agent),
50
71
  undefined,
51
72
  makeDetails,
52
73
  undefined,
53
74
  {
54
- projectTrust:
55
- record.target?.trust.projectTrusted ??
56
- (record.agentScope === "project" || record.agentScope === "both"),
75
+ projectTrust,
76
+ ...(record.executionPlan ? { tools: record.executionPlan.effectiveTools } : {}),
77
+ appendSystemPromptPaths: promptResources?.appendSystemPromptPaths,
78
+ timeoutResultFormat: record.resultFormat,
79
+ turnLimits: {
80
+ idleTimeoutMs: record.currentIdleTimeoutMs ?? record.idleTimeoutMs,
81
+ maxTurns: record.currentMaxTurns ?? record.maxTurns,
82
+ maxToolCalls: record.currentMaxToolCalls ?? record.maxToolCalls,
83
+ },
84
+ resultFormat: record.resultFormat,
85
+ contract: record.contract,
86
+ executionPlan: record.executionPlan,
87
+ displayTask: task,
57
88
  },
58
89
  );
90
+ const settledAt = Date.now();
91
+ const telemetry: TransportTelemetry = {
92
+ ...starting,
93
+ phase: single.aborted ? "interrupted" : single.exitCode === 0 ? "settled" : "failed",
94
+ failurePhase: single.exitCode === 0 ? undefined : "running",
95
+ updatedAt: settledAt,
96
+ timing: {
97
+ ...starting.timing,
98
+ promptAcceptedAt: single.processStarted ? startedAt : undefined,
99
+ firstActivityAt: single.messages.length > 0 ? settledAt : undefined,
100
+ settledAt,
101
+ },
102
+ provider: single.actualProvider,
103
+ model: single.actualModel ?? single.model,
104
+ thinkingLevel: single.thinkingLevel,
105
+ usage: {
106
+ input: single.usage.input,
107
+ output: single.usage.output,
108
+ cacheRead: single.usage.cacheRead,
109
+ cacheWrite: single.usage.cacheWrite,
110
+ totalTokens: single.usage.totalTokens ?? 0,
111
+ cost: single.usage.cost,
112
+ turns: single.usage.turns,
113
+ },
114
+ };
115
+ onProgress?.(telemetry);
59
116
  return {
60
117
  output: getResultFinalOutput(single),
61
118
  exitCode: single.exitCode,
@@ -63,6 +120,8 @@ export class SubprocessTransport implements SubagentTransport {
63
120
  truncated: single.truncated || boundedTask.truncated,
64
121
  error: single.errorMessage || single.stderr || undefined,
65
122
  policy: single.policy,
123
+ termination: single.termination,
124
+ telemetry,
66
125
  };
67
126
  }
68
127
  }
@@ -0,0 +1,103 @@
1
+ import { isResultError, type SingleResult } from "./runner.js";
2
+
3
+ const HEDGE_LOSER_GRACE_MS = 5_000;
4
+
5
+ export async function runHedgedAttempt(
6
+ run: (signal: AbortSignal | undefined) => Promise<SingleResult>,
7
+ parentSignal: AbortSignal | undefined,
8
+ hedgeAfterMs: number | undefined,
9
+ loserGraceMs = HEDGE_LOSER_GRACE_MS,
10
+ ): Promise<{ result: SingleResult; hedged: boolean }> {
11
+ if (hedgeAfterMs === undefined) return { result: await run(parentSignal), hedged: false };
12
+ const primaryLink = linkedAbortController(parentSignal);
13
+ const hedgeLink = linkedAbortController(parentSignal);
14
+ const primaryController = primaryLink.controller;
15
+ const hedgeController = hedgeLink.controller;
16
+ let hedgeStarted = false;
17
+ let hedgePromise: Promise<SingleResult> | undefined;
18
+ let timer: ReturnType<typeof setTimeout> | undefined;
19
+ const primaryPromise = run(primaryController.signal);
20
+ const primaryTagged = primaryPromise.then((result) => ({ source: "primary" as const, result }));
21
+ const delayedHedge = new Promise<{ source: "hedge"; result: SingleResult }>((resolve, reject) => {
22
+ timer = setTimeout(() => {
23
+ hedgeStarted = true;
24
+ hedgePromise = run(hedgeController.signal);
25
+ hedgePromise.then((result) => resolve({ source: "hedge", result }), reject);
26
+ }, hedgeAfterMs);
27
+ });
28
+ try {
29
+ const first = await Promise.race([primaryTagged, delayedHedge]);
30
+ if (!isResultError(first.result) || !hedgeStarted || !hedgePromise) {
31
+ return { result: first.result, hedged: hedgeStarted };
32
+ }
33
+ const other = first.source === "primary" ? await hedgePromise : await primaryPromise;
34
+ return {
35
+ result: isResultError(other) ? first.result : other,
36
+ hedged: true,
37
+ };
38
+ } finally {
39
+ if (timer) clearTimeout(timer);
40
+ primaryController.abort();
41
+ hedgeController.abort();
42
+ primaryLink.dispose();
43
+ hedgeLink.dispose();
44
+ await settleWithin(
45
+ [primaryPromise, hedgePromise].filter(
46
+ (value): value is Promise<SingleResult> => value !== undefined,
47
+ ),
48
+ loserGraceMs,
49
+ );
50
+ }
51
+ }
52
+
53
+ export function isRetryableResult(result: SingleResult): boolean {
54
+ if (!isResultError(result)) return false;
55
+ if (result.outcome) return result.outcome.retryable;
56
+ return result.aborted === true || result.exitCode !== 0;
57
+ }
58
+
59
+ export async function supervisionDelay(
60
+ milliseconds: number,
61
+ signal: AbortSignal | undefined,
62
+ ): Promise<void> {
63
+ if (milliseconds <= 0 || signal?.aborted) return;
64
+ await new Promise<void>((resolve) => {
65
+ let timer: ReturnType<typeof setTimeout>;
66
+ const finish = () => {
67
+ clearTimeout(timer);
68
+ signal?.removeEventListener("abort", finish);
69
+ resolve();
70
+ };
71
+ timer = setTimeout(finish, milliseconds);
72
+ signal?.addEventListener("abort", finish, { once: true });
73
+ });
74
+ }
75
+
76
+ async function settleWithin(promises: Promise<unknown>[], timeoutMs: number): Promise<void> {
77
+ let timer: ReturnType<typeof setTimeout> | undefined;
78
+ try {
79
+ await Promise.race([
80
+ Promise.allSettled(promises),
81
+ new Promise<void>((resolve) => {
82
+ timer = setTimeout(resolve, timeoutMs);
83
+ timer.unref();
84
+ }),
85
+ ]);
86
+ } finally {
87
+ if (timer) clearTimeout(timer);
88
+ }
89
+ }
90
+
91
+ function linkedAbortController(parentSignal: AbortSignal | undefined): {
92
+ controller: AbortController;
93
+ dispose(): void;
94
+ } {
95
+ const controller = new AbortController();
96
+ const abort = () => controller.abort();
97
+ if (parentSignal?.aborted) controller.abort();
98
+ else parentSignal?.addEventListener("abort", abort, { once: true });
99
+ return {
100
+ controller,
101
+ dispose: () => parentSignal?.removeEventListener("abort", abort),
102
+ };
103
+ }