pi-subagents 0.31.0 → 0.32.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/CHANGELOG.md +48 -0
- package/README.md +170 -8
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +6 -1
- package/src/agents/agent-management.ts +6 -1
- package/src/agents/agents.ts +55 -11
- package/src/extension/companion-suggestions.ts +359 -0
- package/src/extension/config.ts +27 -4
- package/src/extension/doctor.ts +2 -0
- package/src/extension/fanout-child.ts +1 -0
- package/src/extension/index.ts +69 -4
- package/src/extension/schemas.ts +2 -2
- package/src/intercom/intercom-bridge.ts +25 -1
- package/src/profiles/profiles.ts +637 -0
- package/src/runs/background/async-execution.ts +138 -33
- package/src/runs/background/async-job-tracker.ts +77 -1
- package/src/runs/background/async-resume.ts +11 -13
- package/src/runs/background/async-status.ts +41 -9
- package/src/runs/background/chain-root-attachment.ts +34 -4
- package/src/runs/background/control-channel.ts +227 -0
- package/src/runs/background/run-status.ts +1 -0
- package/src/runs/background/stale-run-reconciler.ts +28 -1
- package/src/runs/background/subagent-runner.ts +459 -113
- package/src/runs/foreground/chain-execution.ts +29 -7
- package/src/runs/foreground/execution.ts +24 -6
- package/src/runs/foreground/subagent-executor.ts +240 -44
- package/src/runs/shared/acceptance.ts +45 -22
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +4 -0
- package/src/runs/shared/nested-events.ts +58 -0
- package/src/runs/shared/parallel-utils.ts +49 -1
- package/src/runs/shared/pi-args.ts +5 -3
- package/src/runs/shared/pi-spawn.ts +52 -20
- package/src/runs/shared/single-output.ts +2 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
- package/src/runs/shared/worktree.ts +28 -5
- package/src/shared/artifacts.ts +15 -1
- package/src/shared/fork-context.ts +133 -22
- package/src/shared/types.ts +82 -3
- package/src/shared/utils.ts +99 -14
- package/src/slash/slash-commands.ts +726 -40
- package/src/tui/render.ts +16 -4
|
@@ -16,7 +16,7 @@ import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveS
|
|
|
16
16
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
17
17
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
18
18
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
19
|
-
import { resolveChildCwd } from "../../shared/utils.ts";
|
|
19
|
+
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, resolveChildCwd } from "../../shared/utils.ts";
|
|
20
20
|
import { buildModelCandidates, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
|
|
21
21
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
22
22
|
import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
ASYNC_DIR,
|
|
36
36
|
RESULTS_DIR,
|
|
37
37
|
SUBAGENT_ASYNC_STARTED_EVENT,
|
|
38
|
+
SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
38
39
|
TEMP_ROOT_DIR,
|
|
39
40
|
getAsyncConfigPath,
|
|
40
41
|
resolveChildMaxSubagentDepth,
|
|
@@ -117,16 +118,21 @@ interface AsyncChainParams {
|
|
|
117
118
|
sessionRoot?: string;
|
|
118
119
|
chainSkills?: string[];
|
|
119
120
|
sessionFilesByFlatIndex?: (string | undefined)[];
|
|
121
|
+
thinkingOverridesByFlatIndex?: (AgentConfig["thinking"] | undefined)[];
|
|
120
122
|
progressDir?: string;
|
|
121
123
|
dynamicFanoutMaxItems?: number;
|
|
122
124
|
maxSubagentDepth: number;
|
|
123
125
|
worktreeSetupHook?: string;
|
|
124
126
|
worktreeSetupHookTimeoutMs?: number;
|
|
127
|
+
worktreeBaseDir?: string;
|
|
125
128
|
controlConfig?: ResolvedControlConfig;
|
|
126
129
|
controlIntercomTarget?: string;
|
|
127
130
|
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
128
131
|
nestedRoute?: NestedRouteInfo;
|
|
129
132
|
acceptance?: AcceptanceInput;
|
|
133
|
+
timeoutMs?: number;
|
|
134
|
+
/** Global cap on simultaneously-running subagent tasks within the async run. */
|
|
135
|
+
globalConcurrencyLimit?: number;
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
interface AsyncSingleParams {
|
|
@@ -144,16 +150,20 @@ interface AsyncSingleParams {
|
|
|
144
150
|
skills?: string[];
|
|
145
151
|
output?: string | boolean;
|
|
146
152
|
outputMode?: "inline" | "file-only";
|
|
153
|
+
outputBaseDir?: string;
|
|
147
154
|
modelOverride?: string;
|
|
155
|
+
thinkingOverride?: AgentConfig["thinking"];
|
|
148
156
|
availableModels?: AvailableModelInfo[];
|
|
149
157
|
maxSubagentDepth: number;
|
|
150
158
|
worktreeSetupHook?: string;
|
|
151
159
|
worktreeSetupHookTimeoutMs?: number;
|
|
160
|
+
worktreeBaseDir?: string;
|
|
152
161
|
controlConfig?: ResolvedControlConfig;
|
|
153
162
|
controlIntercomTarget?: string;
|
|
154
163
|
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
155
164
|
nestedRoute?: NestedRouteInfo;
|
|
156
165
|
acceptance?: AcceptanceInput;
|
|
166
|
+
timeoutMs?: number;
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
interface AsyncExecutionResult {
|
|
@@ -173,10 +183,13 @@ export interface AsyncRunnerStepBuildParams {
|
|
|
173
183
|
cwd?: string;
|
|
174
184
|
chainSkills?: string[];
|
|
175
185
|
sessionFilesByFlatIndex?: (string | undefined)[];
|
|
186
|
+
thinkingOverridesByFlatIndex?: (AgentConfig["thinking"] | undefined)[];
|
|
176
187
|
progressDir?: string;
|
|
177
188
|
dynamicFanoutMaxItems?: number;
|
|
178
189
|
maxSubagentDepth: number;
|
|
190
|
+
worktreeBaseDir?: string;
|
|
179
191
|
asyncDir: string;
|
|
192
|
+
outputBaseDir?: string;
|
|
180
193
|
validateOutputBindings?: boolean;
|
|
181
194
|
}
|
|
182
195
|
|
|
@@ -207,14 +220,47 @@ export function isAsyncAvailable(): boolean {
|
|
|
207
220
|
return jitiCliPath !== undefined;
|
|
208
221
|
}
|
|
209
222
|
|
|
223
|
+
function isNodeExecutableName(execPath: string): boolean {
|
|
224
|
+
const basename = path.basename(execPath).toLowerCase();
|
|
225
|
+
return basename === "node" || basename === "node.exe" || basename === "nodejs" || basename === "nodejs.exe";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function canUseCurrentNodeExecutable(execPath: string): boolean {
|
|
229
|
+
try {
|
|
230
|
+
fs.accessSync(execPath, process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
|
|
231
|
+
return true;
|
|
232
|
+
} catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
210
237
|
function resolveAsyncRunnerNodeCommand(): string {
|
|
211
|
-
|
|
212
|
-
if (basename === "node" || basename === "node.exe" || basename === "nodejs" || basename === "nodejs.exe") {
|
|
238
|
+
if (isNodeExecutableName(process.execPath) && canUseCurrentNodeExecutable(process.execPath)) {
|
|
213
239
|
return process.execPath;
|
|
214
240
|
}
|
|
215
241
|
return process.platform === "win32" ? "node.exe" : "node";
|
|
216
242
|
}
|
|
217
243
|
|
|
244
|
+
export function resolveAsyncRunnerLogPaths(cfg: object): { stdoutPath: string; stderrPath: string } | undefined {
|
|
245
|
+
const asyncDir = typeof (cfg as { asyncDir?: unknown }).asyncDir === "string"
|
|
246
|
+
? (cfg as { asyncDir: string }).asyncDir
|
|
247
|
+
: undefined;
|
|
248
|
+
if (!asyncDir) return undefined;
|
|
249
|
+
return {
|
|
250
|
+
stdoutPath: path.join(asyncDir, "runner.stdout.log"),
|
|
251
|
+
stderrPath: path.join(asyncDir, "runner.stderr.log"),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function closeFd(fd: number | undefined): void {
|
|
256
|
+
if (fd === undefined) return;
|
|
257
|
+
try {
|
|
258
|
+
fs.closeSync(fd);
|
|
259
|
+
} catch {
|
|
260
|
+
// Best-effort cleanup; child process already owns its duplicated stdio fd.
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
218
264
|
/**
|
|
219
265
|
* Spawn the async runner process
|
|
220
266
|
*/
|
|
@@ -238,20 +284,40 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number;
|
|
|
238
284
|
const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
|
|
239
285
|
const nodeCommand = resolveAsyncRunnerNodeCommand();
|
|
240
286
|
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
287
|
+
const logPaths = resolveAsyncRunnerLogPaths(cfg);
|
|
288
|
+
let stdoutFd: number | undefined;
|
|
289
|
+
let stderrFd: number | undefined;
|
|
290
|
+
try {
|
|
291
|
+
if (logPaths) {
|
|
292
|
+
fs.mkdirSync(path.dirname(logPaths.stdoutPath), { recursive: true });
|
|
293
|
+
stdoutFd = fs.openSync(logPaths.stdoutPath, "a");
|
|
294
|
+
stderrFd = fs.openSync(logPaths.stderrPath, "a");
|
|
295
|
+
}
|
|
296
|
+
const proc = spawn(nodeCommand, [jitiCliPath, runner, cfgPath], {
|
|
297
|
+
cwd,
|
|
298
|
+
detached: true,
|
|
299
|
+
stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
|
|
300
|
+
windowsHide: true,
|
|
301
|
+
env: {
|
|
302
|
+
...process.env,
|
|
303
|
+
...(piPackageRoot ? { [PI_CODING_AGENT_PACKAGE_ROOT_ENV]: piPackageRoot } : {}),
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
closeFd(stdoutFd);
|
|
307
|
+
closeFd(stderrFd);
|
|
308
|
+
proc.on("error", (error) => {
|
|
309
|
+
console.error(`[pi-subagents] async spawn failed: ${error.message}`);
|
|
310
|
+
});
|
|
311
|
+
if (typeof proc.pid !== "number") {
|
|
312
|
+
return { error: `async runner did not produce a pid for cwd: ${cwd}` };
|
|
313
|
+
}
|
|
314
|
+
proc.unref();
|
|
315
|
+
return { pid: proc.pid };
|
|
316
|
+
} catch (error) {
|
|
317
|
+
closeFd(stdoutFd);
|
|
318
|
+
closeFd(stderrFd);
|
|
319
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
252
320
|
}
|
|
253
|
-
proc.unref();
|
|
254
|
-
return { pid: proc.pid };
|
|
255
321
|
}
|
|
256
322
|
|
|
257
323
|
function formatAsyncStartError(mode: SubagentRunMode, message: string): AsyncExecutionResult {
|
|
@@ -274,9 +340,12 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
274
340
|
ctx,
|
|
275
341
|
cwd,
|
|
276
342
|
sessionFilesByFlatIndex,
|
|
343
|
+
thinkingOverridesByFlatIndex,
|
|
277
344
|
maxSubagentDepth,
|
|
345
|
+
worktreeBaseDir,
|
|
278
346
|
asyncDir,
|
|
279
347
|
} = params;
|
|
348
|
+
const outputBaseDir = params.outputBaseDir;
|
|
280
349
|
const resultMode = params.resultMode ?? "chain";
|
|
281
350
|
const chainSkills = params.chainSkills ?? [];
|
|
282
351
|
const availableModels = params.availableModels;
|
|
@@ -333,7 +402,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
333
402
|
...(s.model ? { model: s.model } : {}),
|
|
334
403
|
};
|
|
335
404
|
};
|
|
336
|
-
const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior) => {
|
|
405
|
+
const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, flatIndex?: number) => {
|
|
337
406
|
const a = agents.find((x) => x.name === s.agent)!;
|
|
338
407
|
const stepCwd = resolveChildCwd(runnerCwd, s.cwd);
|
|
339
408
|
const instructionCwd = behaviorCwd ?? stepCwd;
|
|
@@ -352,7 +421,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
352
421
|
const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
|
|
353
422
|
if (behavior.progress) progressInstructionCreated = true;
|
|
354
423
|
const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, progressDir, isFirstProgressAgent);
|
|
355
|
-
const outputPath = resolveSingleOutputPath(behavior.output, ctx.cwd, instructionCwd);
|
|
424
|
+
const outputPath = resolveSingleOutputPath(behavior.output, ctx.cwd, instructionCwd, outputBaseDir);
|
|
356
425
|
systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath);
|
|
357
426
|
const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
|
|
358
427
|
if (validationError) throw new AsyncStartValidationError(validationError);
|
|
@@ -363,7 +432,9 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
363
432
|
|
|
364
433
|
const requestedModel = behavior.model ?? a.model;
|
|
365
434
|
const primaryModel = resolveSubagentModelOverride(requestedModel, ctx.currentModel, availableModels, ctx.currentModelProvider);
|
|
366
|
-
const
|
|
435
|
+
const thinkingOverride = flatIndex === undefined ? undefined : thinkingOverridesByFlatIndex?.[flatIndex];
|
|
436
|
+
const effectiveThinking = thinkingOverride ?? a.thinking;
|
|
437
|
+
const model = applyThinkingSuffix(primaryModel, effectiveThinking, thinkingOverride !== undefined);
|
|
367
438
|
return {
|
|
368
439
|
parentSessionId: ctx.parentSessionId ?? ctx.currentSessionId,
|
|
369
440
|
agent: s.agent,
|
|
@@ -374,9 +445,9 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
374
445
|
structured: Boolean(s.outputSchema),
|
|
375
446
|
cwd: stepCwd,
|
|
376
447
|
model,
|
|
377
|
-
thinking: resolveEffectiveThinking(model,
|
|
448
|
+
thinking: resolveEffectiveThinking(model, effectiveThinking),
|
|
378
449
|
modelCandidates: buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
|
|
379
|
-
applyThinkingSuffix(candidate,
|
|
450
|
+
applyThinkingSuffix(candidate, effectiveThinking, thinkingOverride !== undefined),
|
|
380
451
|
),
|
|
381
452
|
tools: a.tools,
|
|
382
453
|
extensions: a.extensions,
|
|
@@ -406,10 +477,16 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
406
477
|
};
|
|
407
478
|
|
|
408
479
|
let flatStepIndex = 0;
|
|
409
|
-
const
|
|
480
|
+
const nextFlatStep = (): { index: number; sessionFile?: string; thinkingOverride?: AgentConfig["thinking"] } => {
|
|
481
|
+
const index = flatStepIndex;
|
|
410
482
|
const sessionFile = sessionFilesByFlatIndex?.[flatStepIndex];
|
|
483
|
+
const thinkingOverride = thinkingOverridesByFlatIndex?.[flatStepIndex];
|
|
411
484
|
flatStepIndex++;
|
|
412
|
-
return
|
|
485
|
+
return {
|
|
486
|
+
index,
|
|
487
|
+
...(sessionFile ? { sessionFile } : {}),
|
|
488
|
+
...(thinkingOverride ? { thinkingOverride } : {}),
|
|
489
|
+
};
|
|
413
490
|
};
|
|
414
491
|
|
|
415
492
|
try {
|
|
@@ -429,12 +506,13 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
429
506
|
let behaviorCwd: string | undefined;
|
|
430
507
|
if (s.worktree) {
|
|
431
508
|
try {
|
|
432
|
-
behaviorCwd = resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s${stepIndex}`, taskIndex);
|
|
509
|
+
behaviorCwd = resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s${stepIndex}`, taskIndex, worktreeBaseDir);
|
|
433
510
|
} catch {
|
|
434
511
|
behaviorCwd = undefined;
|
|
435
512
|
}
|
|
436
513
|
}
|
|
437
|
-
|
|
514
|
+
const staticStep = nextFlatStep();
|
|
515
|
+
return buildSeqStep(t, staticStep.sessionFile, behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex], staticStep.index);
|
|
438
516
|
}),
|
|
439
517
|
concurrency: s.concurrency,
|
|
440
518
|
failFast: s.failFast,
|
|
@@ -449,6 +527,8 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
449
527
|
writeInitialProgressFile(progressDir);
|
|
450
528
|
progressInstructionCreated = true;
|
|
451
529
|
}
|
|
530
|
+
const maxItems = s.expand.maxItems ?? params.dynamicFanoutMaxItems ?? 0;
|
|
531
|
+
const dynamicFlatSteps = Array.from({ length: maxItems }, () => nextFlatStep());
|
|
452
532
|
return {
|
|
453
533
|
expand: s.expand,
|
|
454
534
|
parallel: buildSeqStep(s.parallel as SequentialStep, undefined, undefined, progressPrecreated, behavior),
|
|
@@ -457,6 +537,8 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
457
537
|
failFast: s.failFast,
|
|
458
538
|
phase: s.phase,
|
|
459
539
|
label: s.label,
|
|
540
|
+
sessionFiles: dynamicFlatSteps.map((step) => step.sessionFile),
|
|
541
|
+
thinkingOverrides: dynamicFlatSteps.map((step) => step.thinkingOverride),
|
|
460
542
|
effectiveAcceptance: resolveEffectiveAcceptance({
|
|
461
543
|
explicit: s.acceptance,
|
|
462
544
|
agentName: s.parallel.agent,
|
|
@@ -467,7 +549,8 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
467
549
|
}),
|
|
468
550
|
};
|
|
469
551
|
}
|
|
470
|
-
|
|
552
|
+
const staticStep = nextFlatStep();
|
|
553
|
+
return buildSeqStep(s as SequentialStep, staticStep.sessionFile, undefined, false, undefined, staticStep.index);
|
|
471
554
|
});
|
|
472
555
|
const steps = params.attachRoot
|
|
473
556
|
? [{
|
|
@@ -510,9 +593,11 @@ export function executeAsyncChain(
|
|
|
510
593
|
shareEnabled,
|
|
511
594
|
sessionRoot,
|
|
512
595
|
sessionFilesByFlatIndex,
|
|
596
|
+
thinkingOverridesByFlatIndex,
|
|
513
597
|
maxSubagentDepth,
|
|
514
598
|
worktreeSetupHook,
|
|
515
599
|
worktreeSetupHookTimeoutMs,
|
|
600
|
+
worktreeBaseDir,
|
|
516
601
|
controlConfig,
|
|
517
602
|
controlIntercomTarget,
|
|
518
603
|
childIntercomTarget,
|
|
@@ -546,9 +631,12 @@ export function executeAsyncChain(
|
|
|
546
631
|
cwd,
|
|
547
632
|
chainSkills: params.chainSkills,
|
|
548
633
|
sessionFilesByFlatIndex,
|
|
549
|
-
|
|
634
|
+
thinkingOverridesByFlatIndex,
|
|
635
|
+
progressDir: params.progressDir ?? (artifactsDir ? path.join(artifactsDir, "progress", id) : resultMode === "parallel" ? path.join(asyncDir, "progress") : undefined),
|
|
636
|
+
outputBaseDir: artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined,
|
|
550
637
|
dynamicFanoutMaxItems: params.dynamicFanoutMaxItems,
|
|
551
638
|
maxSubagentDepth,
|
|
639
|
+
worktreeBaseDir,
|
|
552
640
|
asyncDir,
|
|
553
641
|
});
|
|
554
642
|
if ("error" in built) {
|
|
@@ -560,6 +648,7 @@ export function executeAsyncChain(
|
|
|
560
648
|
return formatAsyncStartError(resultMode, built.error);
|
|
561
649
|
}
|
|
562
650
|
const { steps, runnerCwd, workflowGraph, eventChain } = built;
|
|
651
|
+
const deadlineAt = params.timeoutMs !== undefined ? Date.now() + params.timeoutMs : undefined;
|
|
563
652
|
let childTargetIndex = 0;
|
|
564
653
|
const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
|
|
565
654
|
if (!("parallel" in step) && step.importAsyncRoot) {
|
|
@@ -596,11 +685,15 @@ export function executeAsyncChain(
|
|
|
596
685
|
piArgv1: process.argv[1],
|
|
597
686
|
worktreeSetupHook,
|
|
598
687
|
worktreeSetupHookTimeoutMs,
|
|
688
|
+
worktreeBaseDir,
|
|
599
689
|
controlConfig,
|
|
600
690
|
controlIntercomTarget,
|
|
601
691
|
childIntercomTargets,
|
|
602
692
|
resultMode,
|
|
603
693
|
dynamicFanoutMaxItems: params.dynamicFanoutMaxItems,
|
|
694
|
+
timeoutMs: params.timeoutMs,
|
|
695
|
+
deadlineAt,
|
|
696
|
+
globalConcurrencyLimit: params.globalConcurrencyLimit,
|
|
604
697
|
workflowGraph,
|
|
605
698
|
nestedRoute: nestedRoute ?? inheritedNestedRoute,
|
|
606
699
|
nestedSelf: inheritedNestedRoute && nestedAddress ? {
|
|
@@ -673,6 +766,7 @@ export function executeAsyncChain(
|
|
|
673
766
|
agents: flatAgents,
|
|
674
767
|
chainStepCount: eventChain.length,
|
|
675
768
|
parallelGroups,
|
|
769
|
+
...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
|
|
676
770
|
startedAt: now,
|
|
677
771
|
lastUpdate: now,
|
|
678
772
|
},
|
|
@@ -682,6 +776,7 @@ export function executeAsyncChain(
|
|
|
682
776
|
}
|
|
683
777
|
}
|
|
684
778
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
779
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
685
780
|
id,
|
|
686
781
|
pid: spawnResult.pid,
|
|
687
782
|
sessionId: ctx.currentSessionId,
|
|
@@ -701,6 +796,7 @@ export function executeAsyncChain(
|
|
|
701
796
|
workflowGraph,
|
|
702
797
|
cwd: runnerCwd,
|
|
703
798
|
asyncDir,
|
|
799
|
+
...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
|
|
704
800
|
nestedRoute,
|
|
705
801
|
});
|
|
706
802
|
}
|
|
@@ -713,7 +809,7 @@ export function executeAsyncChain(
|
|
|
713
809
|
|
|
714
810
|
return {
|
|
715
811
|
content: [{ type: "text", text: formatAsyncStartedMessage(`Async ${resultMode}: ${chainDesc} [${id}]`) }],
|
|
716
|
-
details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph },
|
|
812
|
+
details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph, ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}) },
|
|
717
813
|
};
|
|
718
814
|
}
|
|
719
815
|
|
|
@@ -738,6 +834,7 @@ export function executeAsyncSingle(
|
|
|
738
834
|
maxSubagentDepth,
|
|
739
835
|
worktreeSetupHook,
|
|
740
836
|
worktreeSetupHookTimeoutMs,
|
|
837
|
+
worktreeBaseDir,
|
|
741
838
|
controlConfig,
|
|
742
839
|
controlIntercomTarget,
|
|
743
840
|
childIntercomTarget,
|
|
@@ -772,7 +869,7 @@ export function executeAsyncSingle(
|
|
|
772
869
|
}
|
|
773
870
|
|
|
774
871
|
const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
|
|
775
|
-
const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, runnerCwd);
|
|
872
|
+
const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, runnerCwd, params.outputBaseDir ?? (artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined));
|
|
776
873
|
systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath);
|
|
777
874
|
const outputMode = params.outputMode ?? "inline";
|
|
778
875
|
const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
|
|
@@ -784,7 +881,9 @@ export function executeAsyncSingle(
|
|
|
784
881
|
availableModels,
|
|
785
882
|
ctx.currentModelProvider,
|
|
786
883
|
);
|
|
787
|
-
const
|
|
884
|
+
const effectiveThinking = params.thinkingOverride ?? agentConfig.thinking;
|
|
885
|
+
const model = applyThinkingSuffix(primaryModel, effectiveThinking, params.thinkingOverride !== undefined);
|
|
886
|
+
const deadlineAt = params.timeoutMs !== undefined ? Date.now() + params.timeoutMs : undefined;
|
|
788
887
|
let spawnResult: { pid?: number; error?: string } = {};
|
|
789
888
|
try {
|
|
790
889
|
spawnResult = spawnRunner(
|
|
@@ -797,9 +896,9 @@ export function executeAsyncSingle(
|
|
|
797
896
|
task: taskWithOutputInstruction,
|
|
798
897
|
cwd: runnerCwd,
|
|
799
898
|
model,
|
|
800
|
-
thinking: resolveEffectiveThinking(model,
|
|
899
|
+
thinking: resolveEffectiveThinking(model, effectiveThinking),
|
|
801
900
|
modelCandidates: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
|
|
802
|
-
applyThinkingSuffix(candidate,
|
|
901
|
+
applyThinkingSuffix(candidate, effectiveThinking, params.thinkingOverride !== undefined),
|
|
803
902
|
),
|
|
804
903
|
tools: agentConfig.tools,
|
|
805
904
|
extensions: agentConfig.extensions,
|
|
@@ -838,7 +937,10 @@ export function executeAsyncSingle(
|
|
|
838
937
|
piArgv1: process.argv[1],
|
|
839
938
|
worktreeSetupHook,
|
|
840
939
|
worktreeSetupHookTimeoutMs,
|
|
940
|
+
worktreeBaseDir,
|
|
841
941
|
controlConfig,
|
|
942
|
+
timeoutMs: params.timeoutMs,
|
|
943
|
+
deadlineAt,
|
|
842
944
|
controlIntercomTarget,
|
|
843
945
|
childIntercomTargets: childIntercomTarget ? [childIntercomTarget(agent, 0)] : undefined,
|
|
844
946
|
resultMode: "single",
|
|
@@ -888,6 +990,7 @@ export function executeAsyncSingle(
|
|
|
888
990
|
agent,
|
|
889
991
|
agents: [agent],
|
|
890
992
|
chainStepCount: 1,
|
|
993
|
+
...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
|
|
891
994
|
startedAt: now,
|
|
892
995
|
lastUpdate: now,
|
|
893
996
|
},
|
|
@@ -897,6 +1000,7 @@ export function executeAsyncSingle(
|
|
|
897
1000
|
}
|
|
898
1001
|
}
|
|
899
1002
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
1003
|
+
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
900
1004
|
id,
|
|
901
1005
|
pid: spawnResult.pid,
|
|
902
1006
|
sessionId: ctx.currentSessionId,
|
|
@@ -905,12 +1009,13 @@ export function executeAsyncSingle(
|
|
|
905
1009
|
task: task?.slice(0, 50),
|
|
906
1010
|
cwd: runnerCwd,
|
|
907
1011
|
asyncDir,
|
|
1012
|
+
...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}),
|
|
908
1013
|
nestedRoute,
|
|
909
1014
|
});
|
|
910
1015
|
}
|
|
911
1016
|
|
|
912
1017
|
return {
|
|
913
1018
|
content: [{ type: "text", text: formatAsyncStartedMessage(`Async: ${agent} [${id}]`) }],
|
|
914
|
-
details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir },
|
|
1019
|
+
details: { mode: "single", runId: id, results: [], asyncId: id, asyncDir, ...(params.timeoutMs !== undefined ? { timeoutMs: params.timeoutMs, deadlineAt } : {}) },
|
|
915
1020
|
};
|
|
916
1021
|
}
|
|
@@ -17,6 +17,7 @@ import { readStatus } from "../../shared/utils.ts";
|
|
|
17
17
|
import { normalizeParallelGroups } from "./parallel-groups.ts";
|
|
18
18
|
import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
|
|
19
19
|
import { hasLiveNestedDescendants, updateAsyncJobNestedProjection } from "../shared/nested-events.ts";
|
|
20
|
+
import { listAsyncRuns, type AsyncRunSummary } from "./async-status.ts";
|
|
20
21
|
|
|
21
22
|
interface AsyncJobTrackerOptions {
|
|
22
23
|
completionRetentionMs?: number;
|
|
@@ -35,6 +36,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
35
36
|
handleStarted: (data: unknown) => void;
|
|
36
37
|
handleComplete: (data: unknown) => void;
|
|
37
38
|
resetJobs: (ctx?: ExtensionContext) => void;
|
|
39
|
+
restoreActiveJobs: (ctx?: ExtensionContext) => void;
|
|
38
40
|
} {
|
|
39
41
|
const completionRetentionMs = options.completionRetentionMs ?? 10000;
|
|
40
42
|
const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
@@ -43,6 +45,58 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
43
45
|
renderWidget(ctx, jobs);
|
|
44
46
|
ctx.ui.requestRender?.();
|
|
45
47
|
};
|
|
48
|
+
const restoredControlEventCursor = (asyncDir: string) => {
|
|
49
|
+
try {
|
|
50
|
+
return fs.statSync(path.join(asyncDir, "events.jsonl")).size;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return 0;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const summaryToJob = (run: AsyncRunSummary): AsyncJobState => {
|
|
57
|
+
const groups = normalizeParallelGroups(run.parallelGroups, run.steps.length, run.chainStepCount ?? run.steps.length);
|
|
58
|
+
const activeGroup = run.currentStep !== undefined
|
|
59
|
+
? groups.find((group) => run.currentStep! >= group.start && run.currentStep! < group.start + group.count)
|
|
60
|
+
: undefined;
|
|
61
|
+
const visibleSteps = activeGroup
|
|
62
|
+
? run.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count).map((step, index) => ({ ...step, index: activeGroup.start + index }))
|
|
63
|
+
: run.steps.map((step, index) => ({ ...step, index }));
|
|
64
|
+
return {
|
|
65
|
+
asyncId: run.id,
|
|
66
|
+
asyncDir: run.asyncDir,
|
|
67
|
+
status: run.state,
|
|
68
|
+
sessionId: run.sessionId,
|
|
69
|
+
activityState: run.activityState,
|
|
70
|
+
lastActivityAt: run.lastActivityAt,
|
|
71
|
+
currentTool: run.currentTool,
|
|
72
|
+
currentToolStartedAt: run.currentToolStartedAt,
|
|
73
|
+
currentPath: run.currentPath,
|
|
74
|
+
turnCount: run.turnCount,
|
|
75
|
+
toolCount: run.toolCount,
|
|
76
|
+
mode: run.mode,
|
|
77
|
+
agents: visibleSteps.map((step) => step.agent),
|
|
78
|
+
currentStep: run.currentStep,
|
|
79
|
+
chainStepCount: run.chainStepCount,
|
|
80
|
+
parallelGroups: groups,
|
|
81
|
+
steps: visibleSteps,
|
|
82
|
+
stepsTotal: visibleSteps.length,
|
|
83
|
+
runningSteps: visibleSteps.filter((step) => step.status === "running").length,
|
|
84
|
+
completedSteps: visibleSteps.filter((step) => step.status === "complete" || step.status === "completed").length,
|
|
85
|
+
hasParallelGroups: groups.length > 0,
|
|
86
|
+
activeParallelGroup: Boolean(activeGroup),
|
|
87
|
+
startedAt: run.startedAt,
|
|
88
|
+
updatedAt: run.lastUpdate ?? run.startedAt,
|
|
89
|
+
timeoutMs: run.timeoutMs,
|
|
90
|
+
deadlineAt: run.deadlineAt,
|
|
91
|
+
timedOut: run.timedOut,
|
|
92
|
+
sessionDir: run.sessionDir,
|
|
93
|
+
outputFile: run.outputFile,
|
|
94
|
+
totalTokens: run.totalTokens,
|
|
95
|
+
sessionFile: run.sessionFile,
|
|
96
|
+
controlEventCursor: restoredControlEventCursor(run.asyncDir),
|
|
97
|
+
nestedChildren: run.nestedChildren,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
46
100
|
const cancelCleanup = (asyncId: string) => {
|
|
47
101
|
const existingTimer = state.cleanupTimers.get(asyncId);
|
|
48
102
|
if (!existingTimer) return;
|
|
@@ -248,6 +302,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
248
302
|
job.sessionDir = status.sessionDir ?? job.sessionDir;
|
|
249
303
|
job.outputFile = status.outputFile ?? job.outputFile;
|
|
250
304
|
job.totalTokens = status.totalTokens ?? job.totalTokens;
|
|
305
|
+
job.timeoutMs = status.timeoutMs ?? job.timeoutMs;
|
|
306
|
+
job.deadlineAt = status.deadlineAt ?? job.deadlineAt;
|
|
307
|
+
job.timedOut = status.timedOut ?? job.timedOut;
|
|
251
308
|
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
252
309
|
if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && !nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
253
310
|
scheduleCleanup(job.asyncId);
|
|
@@ -305,6 +362,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
305
362
|
activeParallelGroup: Boolean(firstGroupCount && firstGroupCount > 0),
|
|
306
363
|
startedAt: now,
|
|
307
364
|
updatedAt: now,
|
|
365
|
+
timeoutMs: info.timeoutMs,
|
|
366
|
+
deadlineAt: info.deadlineAt,
|
|
308
367
|
controlEventCursor: 0,
|
|
309
368
|
});
|
|
310
369
|
ensurePoller();
|
|
@@ -351,5 +410,22 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
351
410
|
}
|
|
352
411
|
};
|
|
353
412
|
|
|
354
|
-
|
|
413
|
+
const restoreActiveJobs = (ctx?: ExtensionContext) => {
|
|
414
|
+
if (ctx?.hasUI) state.lastUiContext = ctx;
|
|
415
|
+
let runs: AsyncRunSummary[];
|
|
416
|
+
try {
|
|
417
|
+
runs = listAsyncRuns(asyncDirRoot, { states: ["queued", "running"], resultsDir, kill: options.kill, now: options.now });
|
|
418
|
+
} catch (error) {
|
|
419
|
+
console.error(`Failed to restore active async jobs from '${asyncDirRoot}':`, error);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
for (const run of runs) {
|
|
423
|
+
state.asyncJobs.set(run.id, summaryToJob(run));
|
|
424
|
+
}
|
|
425
|
+
if (runs.length === 0) return;
|
|
426
|
+
ensurePoller();
|
|
427
|
+
if (state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
return { ensurePoller, handleStarted, handleComplete, resetJobs, restoreActiveJobs };
|
|
355
431
|
}
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type SubagentState } from "../../shared/types.ts";
|
|
4
4
|
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
5
|
+
import { deliverInterruptRequest } from "./control-channel.ts";
|
|
5
6
|
import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
|
|
6
7
|
|
|
7
8
|
export const ASYNC_RESUME_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
|
|
@@ -38,33 +39,30 @@ export type AsyncResumeTarget = {
|
|
|
38
39
|
|
|
39
40
|
type KillFn = (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
40
41
|
|
|
41
|
-
function readAsyncStatus(asyncDir: string): AsyncStatus | null {
|
|
42
|
-
const statusPath = path.join(asyncDir, "status.json");
|
|
43
|
-
try {
|
|
44
|
-
return JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
|
|
45
|
-
} catch (error) {
|
|
46
|
-
const code = error && typeof error === "object" && "code" in error ? (error as NodeJS.ErrnoException).code : undefined;
|
|
47
|
-
if (code === "ENOENT") return null;
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
42
|
export function interruptLiveAsyncResumeTarget(input: {
|
|
53
43
|
target: AsyncResumeTarget & { kind: "live" };
|
|
54
44
|
state?: Pick<SubagentState, "asyncJobs">;
|
|
55
45
|
kill?: KillFn;
|
|
56
46
|
now?: () => number;
|
|
47
|
+
resultsDir?: string;
|
|
57
48
|
}): { ok: true; asyncId: string } | { ok: false; message: string } {
|
|
58
49
|
const asyncId = input.target.runId;
|
|
59
50
|
if (!input.target.asyncDir) {
|
|
60
51
|
return { ok: false, message: `Async run ${asyncId} is live but does not have an async directory to interrupt.` };
|
|
61
52
|
}
|
|
62
|
-
const status =
|
|
53
|
+
const status = reconcileAsyncRun(input.target.asyncDir, { resultsDir: input.resultsDir, kill: input.kill, now: input.now }).status;
|
|
63
54
|
if (!status || status.state !== "running" || typeof status.pid !== "number") {
|
|
64
55
|
return { ok: false, message: `Async run ${asyncId} is live but no interrupt-capable runner pid was found.` };
|
|
65
56
|
}
|
|
66
57
|
try {
|
|
67
|
-
(
|
|
58
|
+
deliverInterruptRequest({
|
|
59
|
+
asyncDir: input.target.asyncDir,
|
|
60
|
+
pid: status.pid,
|
|
61
|
+
kill: input.kill,
|
|
62
|
+
signal: ASYNC_RESUME_INTERRUPT_SIGNAL,
|
|
63
|
+
now: input.now,
|
|
64
|
+
source: "async-resume",
|
|
65
|
+
});
|
|
68
66
|
const tracked = input.state?.asyncJobs.get(asyncId);
|
|
69
67
|
if (tracked) {
|
|
70
68
|
tracked.activityState = undefined;
|