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.
- package/CHANGELOG.md +31 -0
- package/README.md +26 -62
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +29 -35
- package/src/agents/agent-management.ts +29 -22
- package/src/agents/agent-selection.ts +2 -0
- package/src/agents/agent-serializer.ts +5 -10
- package/src/agents/agents.ts +339 -47
- package/src/agents/chain-serializer.ts +4 -9
- package/src/agents/proactive-skills.ts +191 -0
- package/src/extension/doctor.ts +4 -3
- package/src/extension/fanout-child.ts +1 -3
- package/src/extension/index.ts +6 -9
- package/src/extension/schemas.ts +63 -26
- package/src/intercom/intercom-bridge.ts +11 -1
- package/src/intercom/result-intercom.ts +0 -5
- package/src/runs/background/async-execution.ts +186 -74
- package/src/runs/background/async-resume.ts +53 -5
- package/src/runs/background/async-status.ts +4 -1
- package/src/runs/background/chain-append.ts +282 -0
- package/src/runs/background/chain-root-attachment.ts +161 -0
- package/src/runs/background/run-status.ts +2 -7
- package/src/runs/background/subagent-runner.ts +160 -219
- package/src/runs/foreground/chain-execution.ts +62 -58
- package/src/runs/foreground/execution.ts +39 -343
- package/src/runs/foreground/subagent-executor.ts +316 -111
- package/src/runs/shared/acceptance.ts +605 -22
- package/src/runs/shared/chain-outputs.ts +23 -8
- package/src/runs/shared/completion-guard.ts +3 -26
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +38 -0
- package/src/runs/shared/parallel-utils.ts +13 -10
- package/src/runs/shared/pi-args.ts +3 -2
- package/src/runs/shared/subagent-control.ts +8 -11
- package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
- package/src/runs/shared/workflow-graph.ts +2 -6
- package/src/shared/atomic-json.ts +68 -11
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +20 -49
- package/src/shared/utils.ts +2 -8
- package/src/slash/slash-bridge.ts +3 -1
- package/src/slash/slash-commands.ts +1 -1
- package/src/tui/render.ts +14 -29
- package/src/runs/shared/acceptance-contract.ts +0 -318
- package/src/runs/shared/acceptance-evaluation.ts +0 -221
- package/src/runs/shared/acceptance-finalization.ts +0 -173
- 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(
|
|
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
|
-
|
|
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
|
-
|
|
297
|
+
if (params.validateOutputBindings !== false) {
|
|
298
|
+
validateChainOutputBindings(chain, { maxItems: params.dynamicFanoutMaxItems });
|
|
299
|
+
}
|
|
267
300
|
} catch (error) {
|
|
268
|
-
if (error instanceof ChainOutputValidationError) return
|
|
301
|
+
if (error instanceof ChainOutputValidationError) return { error: error.message };
|
|
269
302
|
throw error;
|
|
270
303
|
}
|
|
271
|
-
const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps:
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
509
|
-
const firstAgents = isParallelStep(
|
|
510
|
-
?
|
|
511
|
-
: isDynamicParallelStep(
|
|
512
|
-
? [
|
|
513
|
-
: [(
|
|
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 <
|
|
518
|
-
const step =
|
|
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:
|
|
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(
|
|
574
|
-
?
|
|
575
|
-
: isDynamicParallelStep(
|
|
576
|
-
?
|
|
577
|
-
: (
|
|
578
|
-
chain:
|
|
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:
|
|
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
|
|
663
|
-
|
|
664
|
-
|
|
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(
|
|
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
|
-
|
|
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 {
|