pi-subagents 0.29.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 +17 -0
- package/README.md +9 -2
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +25 -0
- package/src/agents/agent-management.ts +19 -2
- package/src/agents/agent-serializer.ts +5 -0
- package/src/agents/agents.ts +36 -1
- package/src/agents/proactive-skills.ts +191 -0
- package/src/extension/fanout-child.ts +1 -1
- package/src/extension/index.ts +3 -1
- package/src/extension/schemas.ts +32 -5
- package/src/runs/background/async-execution.ts +166 -63
- 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 +1 -0
- package/src/runs/background/subagent-runner.ts +79 -8
- package/src/runs/foreground/execution.ts +1 -0
- package/src/runs/foreground/subagent-executor.ts +288 -12
- package/src/runs/shared/chain-outputs.ts +23 -8
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +3 -2
- package/src/runs/shared/subagent-control.ts +8 -11
- package/src/shared/types.ts +10 -1
- package/src/slash/slash-bridge.ts +3 -1
- package/src/slash/slash-commands.ts +1 -1
|
@@ -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();
|
|
@@ -101,6 +102,7 @@ interface AsyncExecutionContext {
|
|
|
101
102
|
interface AsyncChainParams {
|
|
102
103
|
chain: ChainStep[];
|
|
103
104
|
task?: string;
|
|
105
|
+
attachRoot?: ImportedAsyncRoot & { agent: string; outputName?: string; label?: string };
|
|
104
106
|
resultMode?: Exclude<SubagentRunMode, "single">;
|
|
105
107
|
agents: AgentConfig[];
|
|
106
108
|
ctx: AsyncExecutionContext;
|
|
@@ -157,6 +159,33 @@ interface AsyncExecutionResult {
|
|
|
157
159
|
isError?: boolean;
|
|
158
160
|
}
|
|
159
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
|
+
|
|
160
189
|
export function formatAsyncStartedMessage(headline: string): string {
|
|
161
190
|
return [
|
|
162
191
|
headline,
|
|
@@ -174,6 +203,14 @@ export function isAsyncAvailable(): boolean {
|
|
|
174
203
|
return jitiCliPath !== undefined;
|
|
175
204
|
}
|
|
176
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
|
+
|
|
177
214
|
/**
|
|
178
215
|
* Spawn the async runner process
|
|
179
216
|
*/
|
|
@@ -195,8 +232,9 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number;
|
|
|
195
232
|
const cfgPath = getAsyncConfigPath(suffix);
|
|
196
233
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg));
|
|
197
234
|
const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
|
|
235
|
+
const nodeCommand = resolveAsyncRunnerNodeCommand();
|
|
198
236
|
|
|
199
|
-
const proc = spawn(
|
|
237
|
+
const proc = spawn(nodeCommand, [jitiCliPath, runner, cfgPath], {
|
|
200
238
|
cwd,
|
|
201
239
|
detached: true,
|
|
202
240
|
stdio: "ignore",
|
|
@@ -225,36 +263,28 @@ const UNAVAILABLE_SUBAGENT_SKILL_ERROR = "Skills not found: pi-subagents";
|
|
|
225
263
|
class UnavailableSubagentSkillError extends Error {}
|
|
226
264
|
class AsyncStartValidationError extends Error {}
|
|
227
265
|
|
|
228
|
-
|
|
229
|
-
* Execute a chain asynchronously
|
|
230
|
-
*/
|
|
231
|
-
export function executeAsyncChain(
|
|
232
|
-
id: string,
|
|
233
|
-
params: AsyncChainParams,
|
|
234
|
-
): AsyncExecutionResult {
|
|
266
|
+
export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildParams): AsyncRunnerStepBuildResult {
|
|
235
267
|
const {
|
|
236
268
|
chain,
|
|
237
269
|
agents,
|
|
238
270
|
ctx,
|
|
239
271
|
cwd,
|
|
240
|
-
maxOutput,
|
|
241
|
-
artifactsDir,
|
|
242
|
-
artifactConfig,
|
|
243
|
-
shareEnabled,
|
|
244
|
-
sessionRoot,
|
|
245
272
|
sessionFilesByFlatIndex,
|
|
246
273
|
maxSubagentDepth,
|
|
247
|
-
|
|
248
|
-
worktreeSetupHookTimeoutMs,
|
|
249
|
-
controlConfig,
|
|
250
|
-
controlIntercomTarget,
|
|
251
|
-
childIntercomTarget,
|
|
252
|
-
nestedRoute,
|
|
274
|
+
asyncDir,
|
|
253
275
|
} = params;
|
|
254
276
|
const resultMode = params.resultMode ?? "chain";
|
|
255
277
|
const chainSkills = params.chainSkills ?? [];
|
|
256
278
|
const availableModels = params.availableModels;
|
|
257
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;
|
|
258
288
|
const firstStep = chain[0];
|
|
259
289
|
const originalTask = params.task ?? (firstStep
|
|
260
290
|
? (isParallelStep(firstStep)
|
|
@@ -264,46 +294,28 @@ export function executeAsyncChain(
|
|
|
264
294
|
: (firstStep as SequentialStep).task)
|
|
265
295
|
: undefined);
|
|
266
296
|
try {
|
|
267
|
-
|
|
297
|
+
if (params.validateOutputBindings !== false) {
|
|
298
|
+
validateChainOutputBindings(chain, { maxItems: params.dynamicFanoutMaxItems });
|
|
299
|
+
}
|
|
268
300
|
} catch (error) {
|
|
269
|
-
if (error instanceof ChainOutputValidationError) return
|
|
301
|
+
if (error instanceof ChainOutputValidationError) return { error: error.message };
|
|
270
302
|
throw error;
|
|
271
303
|
}
|
|
272
|
-
const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps:
|
|
304
|
+
const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: graphChain });
|
|
273
305
|
|
|
274
306
|
for (const s of chain) {
|
|
275
307
|
const stepAgents = isParallelStep(s)
|
|
276
308
|
? s.parallel.map((t) => t.agent)
|
|
277
309
|
: isDynamicParallelStep(s)
|
|
278
310
|
? [s.parallel.agent]
|
|
279
|
-
|
|
311
|
+
: [(s as SequentialStep).agent];
|
|
280
312
|
for (const agentName of stepAgents) {
|
|
281
313
|
if (!agents.find((x) => x.name === agentName)) {
|
|
282
|
-
return {
|
|
283
|
-
content: [{ type: "text", text: `Unknown agent: ${agentName}` }],
|
|
284
|
-
isError: true,
|
|
285
|
-
details: { mode: resultMode, results: [] },
|
|
286
|
-
};
|
|
314
|
+
return { error: `Unknown agent: ${agentName}` };
|
|
287
315
|
}
|
|
288
316
|
}
|
|
289
317
|
}
|
|
290
318
|
|
|
291
|
-
const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
|
|
292
|
-
const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
|
|
293
|
-
const asyncDir = inheritedNestedRoute
|
|
294
|
-
? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
295
|
-
: path.join(ASYNC_DIR, id);
|
|
296
|
-
try {
|
|
297
|
-
fs.mkdirSync(asyncDir, { recursive: true });
|
|
298
|
-
} catch (error) {
|
|
299
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
300
|
-
return {
|
|
301
|
-
content: [{ type: "text", text: `Failed to create async run directory '${asyncDir}': ${message}` }],
|
|
302
|
-
isError: true,
|
|
303
|
-
details: { mode: resultMode, results: [] },
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
319
|
let progressInstructionCreated = false;
|
|
308
320
|
const buildStepOverrides = (s: SequentialStep): StepOverrides => {
|
|
309
321
|
const stepSkillInput = normalizeSkillInput(s.skill);
|
|
@@ -361,6 +373,7 @@ export function executeAsyncChain(
|
|
|
361
373
|
),
|
|
362
374
|
tools: a.tools,
|
|
363
375
|
extensions: a.extensions,
|
|
376
|
+
subagentOnlyExtensions: a.subagentOnlyExtensions,
|
|
364
377
|
mcpDirectTools: a.mcpDirectTools,
|
|
365
378
|
completionGuard: a.completionGuard,
|
|
366
379
|
systemPrompt,
|
|
@@ -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)!;
|
|
@@ -450,12 +462,102 @@ export function executeAsyncChain(
|
|
|
450
462
|
}
|
|
451
463
|
return buildSeqStep(s as SequentialStep, nextSessionFile());
|
|
452
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 } : {}) };
|
|
453
482
|
} catch (error) {
|
|
454
|
-
if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return
|
|
483
|
+
if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return { error: error.message };
|
|
455
484
|
throw error;
|
|
456
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;
|
|
457
555
|
let childTargetIndex = 0;
|
|
458
556
|
const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
|
|
557
|
+
if (!("parallel" in step) && step.importAsyncRoot) {
|
|
558
|
+
childTargetIndex++;
|
|
559
|
+
return [undefined];
|
|
560
|
+
}
|
|
459
561
|
if ("parallel" in step) {
|
|
460
562
|
if (!Array.isArray(step.parallel)) {
|
|
461
563
|
childTargetIndex++;
|
|
@@ -513,17 +615,17 @@ export function executeAsyncChain(
|
|
|
513
615
|
}
|
|
514
616
|
|
|
515
617
|
if (spawnResult.pid) {
|
|
516
|
-
const
|
|
517
|
-
const firstAgents = isParallelStep(
|
|
518
|
-
?
|
|
519
|
-
: isDynamicParallelStep(
|
|
520
|
-
? [
|
|
521
|
-
: [(
|
|
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];
|
|
522
624
|
const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
|
|
523
625
|
const flatAgents: string[] = [];
|
|
524
626
|
let flatStepStart = 0;
|
|
525
|
-
for (let stepIndex = 0; stepIndex <
|
|
526
|
-
const step =
|
|
627
|
+
for (let stepIndex = 0; stepIndex < eventChain.length; stepIndex++) {
|
|
628
|
+
const step = eventChain[stepIndex]!;
|
|
527
629
|
if (isParallelStep(step)) {
|
|
528
630
|
parallelGroups.push({ start: flatStepStart, count: step.parallel.length, stepIndex });
|
|
529
631
|
flatAgents.push(...step.parallel.map((task) => task.agent));
|
|
@@ -561,7 +663,7 @@ export function executeAsyncChain(
|
|
|
561
663
|
state: "running",
|
|
562
664
|
agent: firstAgents[0],
|
|
563
665
|
agents: flatAgents,
|
|
564
|
-
chainStepCount:
|
|
666
|
+
chainStepCount: eventChain.length,
|
|
565
667
|
parallelGroups,
|
|
566
668
|
startedAt: now,
|
|
567
669
|
lastUpdate: now,
|
|
@@ -578,15 +680,15 @@ export function executeAsyncChain(
|
|
|
578
680
|
mode: resultMode,
|
|
579
681
|
agent: firstAgents[0],
|
|
580
682
|
agents: flatAgents,
|
|
581
|
-
task: isParallelStep(
|
|
582
|
-
?
|
|
583
|
-
: isDynamicParallelStep(
|
|
584
|
-
?
|
|
585
|
-
: (
|
|
586
|
-
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) =>
|
|
587
689
|
isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : isDynamicParallelStep(s) ? `expand:${s.parallel.agent}` : (s as SequentialStep).agent,
|
|
588
690
|
),
|
|
589
|
-
chainStepCount:
|
|
691
|
+
chainStepCount: eventChain.length,
|
|
590
692
|
parallelGroups,
|
|
591
693
|
workflowGraph,
|
|
592
694
|
cwd: runnerCwd,
|
|
@@ -691,6 +793,7 @@ export function executeAsyncSingle(
|
|
|
691
793
|
),
|
|
692
794
|
tools: agentConfig.tools,
|
|
693
795
|
extensions: agentConfig.extensions,
|
|
796
|
+
subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
|
|
694
797
|
mcpDirectTools: agentConfig.mcpDirectTools,
|
|
695
798
|
completionGuard: agentConfig.completionGuard,
|
|
696
799
|
systemPrompt,
|
|
@@ -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 {
|