@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.
- package/README.md +362 -53
- package/package.json +10 -7
- package/src/adaptive-scheduler.ts +224 -0
- package/src/admission-benchmark.ts +95 -0
- package/src/admission-policy.ts +78 -0
- package/src/agent-projection.ts +53 -0
- package/src/agents.ts +58 -1
- package/src/auto-transport.ts +114 -0
- package/src/blocking-status.ts +63 -0
- package/src/capabilities.ts +145 -0
- package/src/capability-grant.ts +115 -0
- package/src/capability-router.ts +107 -0
- package/src/completion-delivery.ts +257 -0
- package/src/config-status.ts +221 -0
- package/src/config-ui.ts +215 -236
- package/src/consult-resources.ts +4 -27
- package/src/consult.ts +9 -1
- package/src/create-stateful-transport.ts +55 -0
- package/src/delegation-contract.ts +417 -0
- package/src/execution-plan.ts +322 -0
- package/src/execution-profiles.ts +95 -0
- package/src/execution-ui.ts +320 -0
- package/src/execution.ts +1098 -158
- package/src/in-process-transport.ts +269 -25
- package/src/inspect-render.ts +101 -1
- package/src/inspect.ts +321 -3
- package/src/integration-controller.ts +98 -0
- package/src/limits.ts +3 -0
- package/src/orchestration-metrics.ts +109 -0
- package/src/outcome.ts +61 -0
- package/src/panel-child-group.ts +35 -0
- package/src/panel-contract.ts +343 -0
- package/src/panel-evidence.ts +59 -0
- package/src/panel-execution.ts +770 -0
- package/src/panel-failure.ts +56 -0
- package/src/panel-planning.ts +175 -0
- package/src/panel-prompts.ts +132 -0
- package/src/panel-reconciliation.ts +57 -0
- package/src/panel-render.ts +103 -0
- package/src/parallel-limit-ui.ts +112 -0
- package/src/params.ts +179 -3
- package/src/persistence.ts +182 -32
- package/src/prompt-resources.ts +38 -0
- package/src/registry-types.ts +175 -0
- package/src/registry.ts +466 -143
- package/src/render.ts +72 -6
- package/src/result-contract.ts +416 -0
- package/src/retained-semantic-state.ts +100 -0
- package/src/rpc-timeout-finalization.ts +207 -0
- package/src/rpc-transport-metadata.ts +65 -0
- package/src/rpc-transport.ts +990 -0
- package/src/rpc-turn-capture.ts +142 -0
- package/src/runner-result.ts +55 -0
- package/src/runner-usage.ts +48 -0
- package/src/runner.ts +325 -73
- package/src/semantic-snapshot.ts +214 -0
- package/src/settings.ts +254 -35
- package/src/spawn-idempotency.ts +61 -0
- package/src/stateful-config.ts +13 -0
- package/src/stateful-guidance.ts +1 -0
- package/src/stateful-lifecycle.ts +45 -2
- package/src/stateful-limit-ui.ts +246 -0
- package/src/stateful-limits.ts +96 -0
- package/src/stateful-prompt.ts +11 -2
- package/src/stateful-render.ts +48 -3
- package/src/stateful.ts +467 -357
- package/src/subagents.ts +114 -46
- package/src/subprocess-transport.ts +64 -5
- package/src/supervision.ts +103 -0
- package/src/timeout-checkpoint.ts +305 -0
- package/src/timeout-finalization.ts +75 -0
- package/src/transport-types.ts +68 -0
- package/src/transport-ui.ts +169 -0
- package/src/transport.ts +16 -4
- package/src/turn-budget.ts +109 -0
- package/src/verification-policy.ts +67 -0
- package/src/work-item-ledger.ts +931 -0
- package/src/work-item-persistence.ts +223 -0
- package/src/workflow-planning.ts +162 -0
- package/src/workflow-tree-identity.ts +289 -0
- package/src/workflow-ui.ts +61 -0
- package/src/workflow-verification.ts +296 -0
- 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
|
|
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(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
56
|
-
|
|
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
|
+
}
|