pi-subagents 0.28.0 → 0.30.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 (47) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +26 -62
  3. package/package.json +1 -1
  4. package/skills/pi-subagents/SKILL.md +29 -35
  5. package/src/agents/agent-management.ts +29 -22
  6. package/src/agents/agent-selection.ts +2 -0
  7. package/src/agents/agent-serializer.ts +5 -10
  8. package/src/agents/agents.ts +339 -47
  9. package/src/agents/chain-serializer.ts +4 -9
  10. package/src/agents/proactive-skills.ts +191 -0
  11. package/src/extension/doctor.ts +4 -3
  12. package/src/extension/fanout-child.ts +1 -3
  13. package/src/extension/index.ts +6 -9
  14. package/src/extension/schemas.ts +63 -26
  15. package/src/intercom/intercom-bridge.ts +11 -1
  16. package/src/intercom/result-intercom.ts +0 -5
  17. package/src/runs/background/async-execution.ts +186 -74
  18. package/src/runs/background/async-resume.ts +53 -5
  19. package/src/runs/background/async-status.ts +4 -1
  20. package/src/runs/background/chain-append.ts +282 -0
  21. package/src/runs/background/chain-root-attachment.ts +161 -0
  22. package/src/runs/background/run-status.ts +2 -7
  23. package/src/runs/background/subagent-runner.ts +160 -219
  24. package/src/runs/foreground/chain-execution.ts +62 -58
  25. package/src/runs/foreground/execution.ts +39 -343
  26. package/src/runs/foreground/subagent-executor.ts +316 -111
  27. package/src/runs/shared/acceptance.ts +605 -22
  28. package/src/runs/shared/chain-outputs.ts +23 -8
  29. package/src/runs/shared/completion-guard.ts +3 -26
  30. package/src/runs/shared/dynamic-fanout.ts +1 -1
  31. package/src/runs/shared/model-fallback.ts +38 -0
  32. package/src/runs/shared/parallel-utils.ts +13 -10
  33. package/src/runs/shared/pi-args.ts +3 -2
  34. package/src/runs/shared/subagent-control.ts +8 -11
  35. package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
  36. package/src/runs/shared/workflow-graph.ts +2 -6
  37. package/src/shared/atomic-json.ts +68 -11
  38. package/src/shared/settings.ts +1 -0
  39. package/src/shared/types.ts +20 -49
  40. package/src/shared/utils.ts +2 -8
  41. package/src/slash/slash-bridge.ts +3 -1
  42. package/src/slash/slash-commands.ts +1 -1
  43. package/src/tui/render.ts +14 -29
  44. package/src/runs/shared/acceptance-contract.ts +0 -318
  45. package/src/runs/shared/acceptance-evaluation.ts +0 -221
  46. package/src/runs/shared/acceptance-finalization.ts +0 -173
  47. package/src/runs/shared/acceptance-reports.ts +0 -127
@@ -17,7 +17,7 @@ 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
19
  import { resolveChildCwd } from "../../shared/utils.ts";
20
- import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "../shared/model-fallback.ts";
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";
23
23
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
@@ -40,6 +40,7 @@ import {
40
40
  resolveChildMaxSubagentDepth,
41
41
  } from "../../shared/types.ts";
42
42
  import { nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
43
+ import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
43
44
 
44
45
  const require = createRequire(import.meta.url);
45
46
  const piPackageRoot = resolvePiPackageRoot();
@@ -95,11 +96,13 @@ interface AsyncExecutionContext {
95
96
  cwd: string;
96
97
  currentSessionId: string;
97
98
  currentModelProvider?: string;
99
+ currentModel?: ParentModel;
98
100
  }
99
101
 
100
102
  interface AsyncChainParams {
101
103
  chain: ChainStep[];
102
104
  task?: string;
105
+ attachRoot?: ImportedAsyncRoot & { agent: string; outputName?: string; label?: string };
103
106
  resultMode?: Exclude<SubagentRunMode, "single">;
104
107
  agents: AgentConfig[];
105
108
  ctx: AsyncExecutionContext;
@@ -156,6 +159,33 @@ interface AsyncExecutionResult {
156
159
  isError?: boolean;
157
160
  }
158
161
 
162
+ export interface AsyncRunnerStepBuildParams {
163
+ chain: ChainStep[];
164
+ task?: string;
165
+ attachRoot?: ImportedAsyncRoot & { agent: string; outputName?: string; label?: string };
166
+ resultMode?: SubagentRunMode;
167
+ agents: AgentConfig[];
168
+ ctx: AsyncExecutionContext;
169
+ availableModels?: AvailableModelInfo[];
170
+ cwd?: string;
171
+ chainSkills?: string[];
172
+ sessionFilesByFlatIndex?: (string | undefined)[];
173
+ dynamicFanoutMaxItems?: number;
174
+ maxSubagentDepth: number;
175
+ asyncDir: string;
176
+ validateOutputBindings?: boolean;
177
+ }
178
+
179
+ export type AsyncRunnerStepBuildResult =
180
+ | {
181
+ steps: RunnerStep[];
182
+ runnerCwd: string;
183
+ workflowGraph: ReturnType<typeof buildWorkflowGraphSnapshot>;
184
+ eventChain: ChainStep[];
185
+ originalTask?: string;
186
+ }
187
+ | { error: string };
188
+
159
189
  export function formatAsyncStartedMessage(headline: string): string {
160
190
  return [
161
191
  headline,
@@ -173,6 +203,14 @@ export function isAsyncAvailable(): boolean {
173
203
  return jitiCliPath !== undefined;
174
204
  }
175
205
 
206
+ function resolveAsyncRunnerNodeCommand(): string {
207
+ const basename = path.basename(process.execPath).toLowerCase();
208
+ if (basename === "node" || basename === "node.exe" || basename === "nodejs" || basename === "nodejs.exe") {
209
+ return process.execPath;
210
+ }
211
+ return process.platform === "win32" ? "node.exe" : "node";
212
+ }
213
+
176
214
  /**
177
215
  * Spawn the async runner process
178
216
  */
@@ -194,8 +232,9 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number;
194
232
  const cfgPath = getAsyncConfigPath(suffix);
195
233
  fs.writeFileSync(cfgPath, JSON.stringify(cfg));
196
234
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
235
+ const nodeCommand = resolveAsyncRunnerNodeCommand();
197
236
 
198
- const proc = spawn(process.execPath, [jitiCliPath, runner, cfgPath], {
237
+ const proc = spawn(nodeCommand, [jitiCliPath, runner, cfgPath], {
199
238
  cwd,
200
239
  detached: true,
201
240
  stdio: "ignore",
@@ -224,36 +263,28 @@ const UNAVAILABLE_SUBAGENT_SKILL_ERROR = "Skills not found: pi-subagents";
224
263
  class UnavailableSubagentSkillError extends Error {}
225
264
  class AsyncStartValidationError extends Error {}
226
265
 
227
- /**
228
- * Execute a chain asynchronously
229
- */
230
- export function executeAsyncChain(
231
- id: string,
232
- params: AsyncChainParams,
233
- ): AsyncExecutionResult {
266
+ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildParams): AsyncRunnerStepBuildResult {
234
267
  const {
235
268
  chain,
236
269
  agents,
237
270
  ctx,
238
271
  cwd,
239
- maxOutput,
240
- artifactsDir,
241
- artifactConfig,
242
- shareEnabled,
243
- sessionRoot,
244
272
  sessionFilesByFlatIndex,
245
273
  maxSubagentDepth,
246
- worktreeSetupHook,
247
- worktreeSetupHookTimeoutMs,
248
- controlConfig,
249
- controlIntercomTarget,
250
- childIntercomTarget,
251
- nestedRoute,
274
+ asyncDir,
252
275
  } = params;
253
276
  const resultMode = params.resultMode ?? "chain";
254
277
  const chainSkills = params.chainSkills ?? [];
255
278
  const availableModels = params.availableModels;
256
279
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
280
+ const graphChain: ChainStep[] = params.attachRoot
281
+ ? [{
282
+ agent: params.attachRoot.agent,
283
+ task: `Attach async root ${params.attachRoot.runId}`,
284
+ label: params.attachRoot.label ?? `Attached root ${params.attachRoot.runId}`,
285
+ ...(params.attachRoot.outputName ? { as: params.attachRoot.outputName } : {}),
286
+ }, ...chain]
287
+ : chain;
257
288
  const firstStep = chain[0];
258
289
  const originalTask = params.task ?? (firstStep
259
290
  ? (isParallelStep(firstStep)
@@ -263,46 +294,28 @@ export function executeAsyncChain(
263
294
  : (firstStep as SequentialStep).task)
264
295
  : undefined);
265
296
  try {
266
- validateChainOutputBindings(chain, { maxItems: params.dynamicFanoutMaxItems });
297
+ if (params.validateOutputBindings !== false) {
298
+ validateChainOutputBindings(chain, { maxItems: params.dynamicFanoutMaxItems });
299
+ }
267
300
  } catch (error) {
268
- if (error instanceof ChainOutputValidationError) return formatAsyncStartError(resultMode, error.message);
301
+ if (error instanceof ChainOutputValidationError) return { error: error.message };
269
302
  throw error;
270
303
  }
271
- const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: chain });
304
+ const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: graphChain });
272
305
 
273
306
  for (const s of chain) {
274
307
  const stepAgents = isParallelStep(s)
275
308
  ? s.parallel.map((t) => t.agent)
276
309
  : isDynamicParallelStep(s)
277
310
  ? [s.parallel.agent]
278
- : [(s as SequentialStep).agent];
311
+ : [(s as SequentialStep).agent];
279
312
  for (const agentName of stepAgents) {
280
313
  if (!agents.find((x) => x.name === agentName)) {
281
- return {
282
- content: [{ type: "text", text: `Unknown agent: ${agentName}` }],
283
- isError: true,
284
- details: { mode: resultMode, results: [] },
285
- };
314
+ return { error: `Unknown agent: ${agentName}` };
286
315
  }
287
316
  }
288
317
  }
289
318
 
290
- const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
291
- const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
292
- const asyncDir = inheritedNestedRoute
293
- ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
294
- : path.join(ASYNC_DIR, id);
295
- try {
296
- fs.mkdirSync(asyncDir, { recursive: true });
297
- } catch (error) {
298
- const message = error instanceof Error ? error.message : String(error);
299
- return {
300
- content: [{ type: "text", text: `Failed to create async run directory '${asyncDir}': ${message}` }],
301
- isError: true,
302
- details: { mode: resultMode, results: [] },
303
- };
304
- }
305
-
306
319
  let progressInstructionCreated = false;
307
320
  const buildStepOverrides = (s: SequentialStep): StepOverrides => {
308
321
  const stepSkillInput = normalizeSkillInput(s.skill);
@@ -342,7 +355,8 @@ export function executeAsyncChain(
342
355
  taskTemplate = taskTemplate.replace(/\{chain_dir\}/g, runnerCwd);
343
356
  const task = injectSingleOutputInstruction(`${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`, outputPath);
344
357
 
345
- const primaryModel = resolveModelCandidate(behavior.model ?? a.model, availableModels, ctx.currentModelProvider);
358
+ const requestedModel = behavior.model ?? a.model;
359
+ const primaryModel = resolveSubagentModelOverride(requestedModel, ctx.currentModel, availableModels, ctx.currentModelProvider);
346
360
  const model = applyThinkingSuffix(primaryModel, a.thinking);
347
361
  return {
348
362
  agent: s.agent,
@@ -354,11 +368,12 @@ export function executeAsyncChain(
354
368
  cwd: stepCwd,
355
369
  model,
356
370
  thinking: resolveEffectiveThinking(model, a.thinking),
357
- modelCandidates: buildModelCandidates(behavior.model ?? a.model, a.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
371
+ modelCandidates: buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
358
372
  applyThinkingSuffix(candidate, a.thinking),
359
373
  ),
360
374
  tools: a.tools,
361
375
  extensions: a.extensions,
376
+ subagentOnlyExtensions: a.subagentOnlyExtensions,
362
377
  mcpDirectTools: a.mcpDirectTools,
363
378
  completionGuard: a.completionGuard,
364
379
  systemPrompt,
@@ -370,8 +385,6 @@ export function executeAsyncChain(
370
385
  outputMode: behavior.outputMode,
371
386
  sessionFile,
372
387
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
373
- maxExecutionTimeMs: a.maxExecutionTimeMs,
374
- maxTokens: a.maxTokens,
375
388
  effectiveAcceptance: resolveEffectiveAcceptance({
376
389
  explicit: s.acceptance,
377
390
  agentName: s.agent,
@@ -392,9 +405,8 @@ export function executeAsyncChain(
392
405
  return sessionFile;
393
406
  };
394
407
 
395
- let steps: RunnerStep[];
396
408
  try {
397
- steps = chain.map((s, stepIndex) => {
409
+ const builtSteps = chain.map((s, stepIndex) => {
398
410
  if (isParallelStep(s)) {
399
411
  const parallelBehaviors = s.parallel.map((task) => {
400
412
  const agent = agents.find((candidate) => candidate.name === task.agent)!;
@@ -438,16 +450,114 @@ export function executeAsyncChain(
438
450
  failFast: s.failFast,
439
451
  phase: s.phase,
440
452
  label: s.label,
453
+ effectiveAcceptance: resolveEffectiveAcceptance({
454
+ explicit: s.acceptance,
455
+ agentName: s.parallel.agent,
456
+ task: s.parallel.task,
457
+ mode: resultMode,
458
+ async: true,
459
+ dynamicGroup: true,
460
+ }),
441
461
  };
442
462
  }
443
463
  return buildSeqStep(s as SequentialStep, nextSessionFile());
444
464
  });
465
+ const steps = params.attachRoot
466
+ ? [{
467
+ agent: params.attachRoot.agent,
468
+ task: "",
469
+ label: params.attachRoot.label ?? `Attached root ${params.attachRoot.runId}`,
470
+ outputName: params.attachRoot.outputName,
471
+ importAsyncRoot: {
472
+ runId: params.attachRoot.runId,
473
+ asyncDir: params.attachRoot.asyncDir,
474
+ resultPath: params.attachRoot.resultPath,
475
+ index: params.attachRoot.index,
476
+ },
477
+ inheritProjectContext: false,
478
+ inheritSkills: false,
479
+ }, ...builtSteps]
480
+ : builtSteps;
481
+ return { steps, runnerCwd, workflowGraph, eventChain: graphChain, ...(originalTask !== undefined ? { originalTask } : {}) };
445
482
  } catch (error) {
446
- if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return formatAsyncStartError(resultMode, error.message);
483
+ if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return { error: error.message };
447
484
  throw error;
448
485
  }
486
+ }
487
+
488
+ /**
489
+ * Execute a chain asynchronously
490
+ */
491
+ export function executeAsyncChain(
492
+ id: string,
493
+ params: AsyncChainParams,
494
+ ): AsyncExecutionResult {
495
+ const {
496
+ chain,
497
+ agents,
498
+ ctx,
499
+ cwd,
500
+ maxOutput,
501
+ artifactsDir,
502
+ artifactConfig,
503
+ shareEnabled,
504
+ sessionRoot,
505
+ sessionFilesByFlatIndex,
506
+ maxSubagentDepth,
507
+ worktreeSetupHook,
508
+ worktreeSetupHookTimeoutMs,
509
+ controlConfig,
510
+ controlIntercomTarget,
511
+ childIntercomTarget,
512
+ nestedRoute,
513
+ } = params;
514
+ const resultMode = params.resultMode ?? "chain";
515
+ const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
516
+ const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
517
+ const asyncDir = inheritedNestedRoute
518
+ ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
519
+ : path.join(ASYNC_DIR, id);
520
+ try {
521
+ fs.mkdirSync(asyncDir, { recursive: true });
522
+ } catch (error) {
523
+ const message = error instanceof Error ? error.message : String(error);
524
+ return {
525
+ content: [{ type: "text", text: `Failed to create async run directory '${asyncDir}': ${message}` }],
526
+ isError: true,
527
+ details: { mode: resultMode, results: [] },
528
+ };
529
+ }
530
+
531
+ const built = buildAsyncRunnerSteps(id, {
532
+ chain,
533
+ task: params.task,
534
+ attachRoot: params.attachRoot,
535
+ resultMode,
536
+ agents,
537
+ ctx,
538
+ availableModels: params.availableModels,
539
+ cwd,
540
+ chainSkills: params.chainSkills,
541
+ sessionFilesByFlatIndex,
542
+ dynamicFanoutMaxItems: params.dynamicFanoutMaxItems,
543
+ maxSubagentDepth,
544
+ asyncDir,
545
+ });
546
+ if ("error" in built) {
547
+ try {
548
+ fs.rmSync(asyncDir, { recursive: true, force: true });
549
+ } catch {
550
+ // Best-effort cleanup for validation failures before the runner is spawned.
551
+ }
552
+ return formatAsyncStartError(resultMode, built.error);
553
+ }
554
+ const { steps, runnerCwd, workflowGraph, eventChain } = built;
449
555
  let childTargetIndex = 0;
450
556
  const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
557
+ if (!("parallel" in step) && step.importAsyncRoot) {
558
+ childTargetIndex++;
559
+ return [undefined];
560
+ }
451
561
  if ("parallel" in step) {
452
562
  if (!Array.isArray(step.parallel)) {
453
563
  childTargetIndex++;
@@ -505,17 +615,17 @@ export function executeAsyncChain(
505
615
  }
506
616
 
507
617
  if (spawnResult.pid) {
508
- const firstStep = chain[0];
509
- const firstAgents = isParallelStep(firstStep)
510
- ? firstStep.parallel.map((t) => t.agent)
511
- : isDynamicParallelStep(firstStep)
512
- ? [firstStep.parallel.agent]
513
- : [(firstStep as SequentialStep).agent];
618
+ const eventFirstStep = eventChain[0];
619
+ const firstAgents = isParallelStep(eventFirstStep)
620
+ ? eventFirstStep.parallel.map((t) => t.agent)
621
+ : isDynamicParallelStep(eventFirstStep)
622
+ ? [eventFirstStep.parallel.agent]
623
+ : [(eventFirstStep as SequentialStep).agent];
514
624
  const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
515
625
  const flatAgents: string[] = [];
516
626
  let flatStepStart = 0;
517
- for (let stepIndex = 0; stepIndex < chain.length; stepIndex++) {
518
- const step = chain[stepIndex]!;
627
+ for (let stepIndex = 0; stepIndex < eventChain.length; stepIndex++) {
628
+ const step = eventChain[stepIndex]!;
519
629
  if (isParallelStep(step)) {
520
630
  parallelGroups.push({ start: flatStepStart, count: step.parallel.length, stepIndex });
521
631
  flatAgents.push(...step.parallel.map((task) => task.agent));
@@ -553,7 +663,7 @@ export function executeAsyncChain(
553
663
  state: "running",
554
664
  agent: firstAgents[0],
555
665
  agents: flatAgents,
556
- chainStepCount: chain.length,
666
+ chainStepCount: eventChain.length,
557
667
  parallelGroups,
558
668
  startedAt: now,
559
669
  lastUpdate: now,
@@ -570,15 +680,15 @@ export function executeAsyncChain(
570
680
  mode: resultMode,
571
681
  agent: firstAgents[0],
572
682
  agents: flatAgents,
573
- task: isParallelStep(firstStep)
574
- ? firstStep.parallel[0]?.task?.slice(0, 50)
575
- : isDynamicParallelStep(firstStep)
576
- ? firstStep.parallel.task?.slice(0, 50)
577
- : (firstStep as SequentialStep).task?.slice(0, 50),
578
- chain: chain.map((s) =>
683
+ task: isParallelStep(eventFirstStep)
684
+ ? eventFirstStep.parallel[0]?.task?.slice(0, 50)
685
+ : isDynamicParallelStep(eventFirstStep)
686
+ ? eventFirstStep.parallel.task?.slice(0, 50)
687
+ : (eventFirstStep as SequentialStep).task?.slice(0, 50),
688
+ chain: eventChain.map((s) =>
579
689
  isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : isDynamicParallelStep(s) ? `expand:${s.parallel.agent}` : (s as SequentialStep).agent,
580
690
  ),
581
- chainStepCount: chain.length,
691
+ chainStepCount: eventChain.length,
582
692
  parallelGroups,
583
693
  workflowGraph,
584
694
  cwd: runnerCwd,
@@ -659,10 +769,13 @@ export function executeAsyncSingle(
659
769
  const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
660
770
  if (validationError) return formatAsyncStartError("single", validationError);
661
771
  const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath);
662
- const model = applyThinkingSuffix(
663
- resolveModelCandidate(params.modelOverride ?? agentConfig.model, availableModels, ctx.currentModelProvider),
664
- agentConfig.thinking,
772
+ const primaryModel = resolveSubagentModelOverride(
773
+ params.modelOverride ?? agentConfig.model,
774
+ ctx.currentModel,
775
+ availableModels,
776
+ ctx.currentModelProvider,
665
777
  );
778
+ const model = applyThinkingSuffix(primaryModel, agentConfig.thinking);
666
779
  let spawnResult: { pid?: number; error?: string } = {};
667
780
  try {
668
781
  spawnResult = spawnRunner(
@@ -675,11 +788,12 @@ export function executeAsyncSingle(
675
788
  cwd: runnerCwd,
676
789
  model,
677
790
  thinking: resolveEffectiveThinking(model, agentConfig.thinking),
678
- modelCandidates: buildModelCandidates(params.modelOverride ?? agentConfig.model, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
791
+ modelCandidates: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
679
792
  applyThinkingSuffix(candidate, agentConfig.thinking),
680
793
  ),
681
794
  tools: agentConfig.tools,
682
795
  extensions: agentConfig.extensions,
796
+ subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
683
797
  mcpDirectTools: agentConfig.mcpDirectTools,
684
798
  completionGuard: agentConfig.completionGuard,
685
799
  systemPrompt,
@@ -691,8 +805,6 @@ export function executeAsyncSingle(
691
805
  outputMode,
692
806
  sessionFile,
693
807
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
694
- maxExecutionTimeMs: agentConfig.maxExecutionTimeMs,
695
- maxTokens: agentConfig.maxTokens,
696
808
  effectiveAcceptance: resolveEffectiveAcceptance({
697
809
  explicit: params.acceptance,
698
810
  agentName: agent,
@@ -1,9 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus } from "../../shared/types.ts";
3
+ import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type SubagentState } from "../../shared/types.ts";
4
4
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
5
5
  import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
6
6
 
7
+ export const ASYNC_RESUME_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
8
+
7
9
  export interface AsyncResumeParams {
8
10
  id?: string;
9
11
  runId?: string;
@@ -18,6 +20,10 @@ export interface AsyncResumeDeps {
18
20
  now?: () => number;
19
21
  }
20
22
 
23
+ export interface AsyncResumeOptions {
24
+ requireSessionFile?: boolean;
25
+ }
26
+
21
27
  export type AsyncResumeTarget = {
22
28
  kind: "live" | "revive";
23
29
  runId: string;
@@ -30,6 +36,47 @@ export type AsyncResumeTarget = {
30
36
  sessionFile?: string;
31
37
  };
32
38
 
39
+ type KillFn = (pid: number, signal?: NodeJS.Signals | 0) => boolean;
40
+
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
+ export function interruptLiveAsyncResumeTarget(input: {
53
+ target: AsyncResumeTarget & { kind: "live" };
54
+ state?: Pick<SubagentState, "asyncJobs">;
55
+ kill?: KillFn;
56
+ now?: () => number;
57
+ }): { ok: true; asyncId: string } | { ok: false; message: string } {
58
+ const asyncId = input.target.runId;
59
+ if (!input.target.asyncDir) {
60
+ return { ok: false, message: `Async run ${asyncId} is live but does not have an async directory to interrupt.` };
61
+ }
62
+ const status = readAsyncStatus(input.target.asyncDir);
63
+ if (!status || status.state !== "running" || typeof status.pid !== "number") {
64
+ return { ok: false, message: `Async run ${asyncId} is live but no interrupt-capable runner pid was found.` };
65
+ }
66
+ try {
67
+ (input.kill ?? process.kill)(status.pid, ASYNC_RESUME_INTERRUPT_SIGNAL);
68
+ const tracked = input.state?.asyncJobs.get(asyncId);
69
+ if (tracked) {
70
+ tracked.activityState = undefined;
71
+ tracked.updatedAt = input.now?.() ?? Date.now();
72
+ }
73
+ return { ok: true, asyncId };
74
+ } catch (error) {
75
+ const message = error instanceof Error ? error.message : String(error);
76
+ return { ok: false, message: `Failed to interrupt async run ${asyncId}: ${message}` };
77
+ }
78
+ }
79
+
33
80
  interface AsyncResultFile {
34
81
  id?: string;
35
82
  runId?: string;
@@ -236,9 +283,10 @@ function validateResumeSessionFile(runId: string, sessionFile: string): string {
236
283
  return resolved;
237
284
  }
238
285
 
239
- export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncResumeDeps = {}): AsyncResumeTarget {
286
+ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncResumeDeps = {}, options: AsyncResumeOptions = {}): AsyncResumeTarget {
240
287
  const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
241
288
  const resultsDir = deps.resultsDir ?? RESULTS_DIR;
289
+ const requireSessionFile = options.requireSessionFile ?? true;
242
290
  const location = resolveAsyncRunLocation(params, asyncDirRoot, resultsDir);
243
291
  if (!location.asyncDir && !location.resultPath) {
244
292
  throw new Error("Async run not found. Provide id or dir.");
@@ -313,8 +361,8 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
313
361
  const sessionFile = statusSteps[index]?.sessionFile
314
362
  ?? resultSteps[index]?.sessionFile
315
363
  ?? (stepCount === 1 ? status?.sessionFile ?? result?.sessionFile : undefined);
316
- if (!sessionFile) throw new Error(`Async run '${runId}' child ${index} does not have a persisted session file to resume from.`);
317
- const resolvedSessionFile = validateResumeSessionFile(runId, sessionFile);
364
+ if (!sessionFile && requireSessionFile) throw new Error(`Async run '${runId}' child ${index} does not have a persisted session file to resume from.`);
365
+ const resolvedSessionFile = sessionFile ? validateResumeSessionFile(runId, sessionFile) : undefined;
318
366
 
319
367
  return {
320
368
  kind: "revive",
@@ -325,7 +373,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
325
373
  index,
326
374
  intercomTarget: resolveSubagentIntercomTarget(runId, agent, index),
327
375
  cwd: status?.cwd ?? result?.cwd,
328
- sessionFile: resolvedSessionFile,
376
+ ...(resolvedSessionFile ? { sessionFile: resolvedSessionFile } : {}),
329
377
  };
330
378
  }
331
379
 
@@ -56,6 +56,7 @@ export interface AsyncRunSummary {
56
56
  endedAt?: number;
57
57
  currentStep?: number;
58
58
  chainStepCount?: number;
59
+ pendingAppends?: number;
59
60
  parallelGroups?: AsyncParallelGroupStatus[];
60
61
  steps: AsyncRunStepSummary[];
61
62
  sessionDir?: string;
@@ -188,6 +189,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
188
189
  endedAt: status.endedAt,
189
190
  currentStep: status.currentStep,
190
191
  ...(status.chainStepCount !== undefined ? { chainStepCount: status.chainStepCount } : {}),
192
+ ...(status.pendingAppends !== undefined ? { pendingAppends: status.pendingAppends } : {}),
191
193
  ...(parallelGroups.length ? { parallelGroups } : {}),
192
194
  steps: summarizedSteps,
193
195
  ...(nestedChildren.length ? { nestedChildren } : {}),
@@ -309,7 +311,8 @@ function formatRunHeader(run: AsyncRunSummary): string {
309
311
  const stepLabel = formatAsyncRunProgressLabel(run);
310
312
  const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
311
313
  const activity = formatActivityFacts(run);
312
- return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode} | ${stepLabel} | ${cwd}`;
314
+ const pending = run.pendingAppends ? ` | ${run.pendingAppends} pending append${run.pendingAppends === 1 ? "" : "s"}` : "";
315
+ return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode} | ${stepLabel}${pending} | ${cwd}`;
313
316
  }
314
317
 
315
318
  export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active async runs"): string {