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
|
@@ -31,7 +31,9 @@ import {
|
|
|
31
31
|
type StepOverrides,
|
|
32
32
|
} from "../../shared/settings.ts";
|
|
33
33
|
import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts";
|
|
34
|
-
import { executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable } from "../background/async-execution.ts";
|
|
34
|
+
import { buildAsyncRunnerSteps, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable } from "../background/async-execution.ts";
|
|
35
|
+
import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts";
|
|
36
|
+
import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts";
|
|
35
37
|
import { createForkContextResolver } from "../../shared/fork-context.ts";
|
|
36
38
|
import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
|
|
37
39
|
import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
|
|
@@ -47,7 +49,8 @@ import {
|
|
|
47
49
|
resolveSubagentResultStatus,
|
|
48
50
|
stripDetailsOutputsForIntercomReceipt,
|
|
49
51
|
} from "../../intercom/result-intercom.ts";
|
|
50
|
-
import { buildRevivedAsyncTask, resolveAsyncResumeTarget } from "../background/async-resume.ts";
|
|
52
|
+
import { buildRevivedAsyncTask, interruptLiveAsyncResumeTarget, resolveAsyncResumeTarget } from "../background/async-resume.ts";
|
|
53
|
+
import { resolveAsyncRootResultPath } from "../background/chain-root-attachment.ts";
|
|
51
54
|
import { createNestedRoute, readNestedControlResults, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
52
55
|
import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
|
|
53
56
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
@@ -79,7 +82,9 @@ import {
|
|
|
79
82
|
type SingleResult,
|
|
80
83
|
type SubagentRunMode,
|
|
81
84
|
type SubagentState,
|
|
85
|
+
ASYNC_DIR,
|
|
82
86
|
DEFAULT_ARTIFACT_CONFIG,
|
|
87
|
+
RESULTS_DIR,
|
|
83
88
|
SUBAGENT_ACTIONS,
|
|
84
89
|
SUBAGENT_CONTROL_EVENT,
|
|
85
90
|
SUBAGENT_CONTROL_INTERCOM_EVENT,
|
|
@@ -150,6 +155,7 @@ interface ExecutorDeps {
|
|
|
150
155
|
expandTilde: (p: string) => string;
|
|
151
156
|
discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
|
|
152
157
|
allowMutatingManagementActions?: boolean;
|
|
158
|
+
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
interface ExecutionContextData {
|
|
@@ -317,7 +323,7 @@ function isExactResumeError(error: unknown, source: "async" | "foreground", requ
|
|
|
317
323
|
return new RegExp(`\\b${source} run '${escapeRegExp(requested)}'`, "i").test(error.message);
|
|
318
324
|
}
|
|
319
325
|
|
|
320
|
-
function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState): ResumeSourceTarget {
|
|
326
|
+
function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState, options: { asyncRequireSessionFile?: boolean } = {}): ResumeSourceTarget {
|
|
321
327
|
const requested = (params.id ?? params.runId)?.trim() ?? "";
|
|
322
328
|
let foregroundTarget: ForegroundResumeSourceTarget | undefined;
|
|
323
329
|
let foregroundError: unknown;
|
|
@@ -331,7 +337,7 @@ function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState):
|
|
|
331
337
|
foregroundError = error;
|
|
332
338
|
}
|
|
333
339
|
try {
|
|
334
|
-
asyncTarget = { source: "async", ...resolveAsyncResumeTarget(params) };
|
|
340
|
+
asyncTarget = { source: "async", ...resolveAsyncResumeTarget(params, {}, { requireSessionFile: options.asyncRequireSessionFile }) };
|
|
335
341
|
} catch (error) {
|
|
336
342
|
asyncError = error;
|
|
337
343
|
}
|
|
@@ -402,7 +408,7 @@ function emitControlNotification(input: {
|
|
|
402
408
|
}
|
|
403
409
|
}
|
|
404
410
|
|
|
405
|
-
function interruptAsyncRun(state: SubagentState, runId: string | undefined): AgentToolResult<Details> | null {
|
|
411
|
+
function interruptAsyncRun(state: SubagentState, runId: string | undefined, kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean): AgentToolResult<Details> | null {
|
|
406
412
|
const target = getAsyncInterruptTarget(state, runId);
|
|
407
413
|
if (!target) return null;
|
|
408
414
|
const status = readStatus(target.asyncDir);
|
|
@@ -414,7 +420,7 @@ function interruptAsyncRun(state: SubagentState, runId: string | undefined): Age
|
|
|
414
420
|
};
|
|
415
421
|
}
|
|
416
422
|
try {
|
|
417
|
-
process.kill(status.pid, ASYNC_INTERRUPT_SIGNAL);
|
|
423
|
+
(kill ?? process.kill)(status.pid, ASYNC_INTERRUPT_SIGNAL);
|
|
418
424
|
const tracked = state.asyncJobs.get(target.asyncId);
|
|
419
425
|
if (tracked) {
|
|
420
426
|
tracked.activityState = undefined;
|
|
@@ -434,6 +440,186 @@ function interruptAsyncRun(state: SubagentState, runId: string | undefined): Age
|
|
|
434
440
|
}
|
|
435
441
|
}
|
|
436
442
|
|
|
443
|
+
function duplicateNames(names: string[]): string[] {
|
|
444
|
+
const seen = new Set<string>();
|
|
445
|
+
const duplicates = new Set<string>();
|
|
446
|
+
for (const name of names) {
|
|
447
|
+
if (seen.has(name)) duplicates.add(name);
|
|
448
|
+
else seen.add(name);
|
|
449
|
+
}
|
|
450
|
+
return [...duplicates];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function appendStepToAsyncChain(input: {
|
|
454
|
+
params: SubagentParamsLike;
|
|
455
|
+
requestCwd: string;
|
|
456
|
+
ctx: ExtensionContext;
|
|
457
|
+
deps: ExecutorDeps;
|
|
458
|
+
}): AgentToolResult<Details> {
|
|
459
|
+
const targetRunId = input.params.id ?? input.params.runId;
|
|
460
|
+
if (!targetRunId) {
|
|
461
|
+
return {
|
|
462
|
+
content: [{ type: "text", text: "action='append-step' requires id." }],
|
|
463
|
+
isError: true,
|
|
464
|
+
details: { mode: "management", results: [] },
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (!input.params.chain || input.params.chain.length !== 1) {
|
|
468
|
+
return {
|
|
469
|
+
content: [{ type: "text", text: "action='append-step' requires chain with exactly one step." }],
|
|
470
|
+
isError: true,
|
|
471
|
+
details: { mode: "management", results: [] },
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
let resolved: ResolvedSubagentRunId | undefined;
|
|
476
|
+
try {
|
|
477
|
+
resolved = resolveSubagentRunId(targetRunId, { state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) });
|
|
478
|
+
} catch (error) {
|
|
479
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
480
|
+
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
|
|
481
|
+
}
|
|
482
|
+
if (!resolved) {
|
|
483
|
+
return {
|
|
484
|
+
content: [{ type: "text", text: `No async chain run found for '${targetRunId}'.` }],
|
|
485
|
+
isError: true,
|
|
486
|
+
details: { mode: "management", results: [] },
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (resolved.kind !== "async" || !resolved.location.asyncDir) {
|
|
490
|
+
return {
|
|
491
|
+
content: [{ type: "text", text: `Run '${resolved.id}' is not an append-capable async chain run.` }],
|
|
492
|
+
isError: true,
|
|
493
|
+
details: { mode: "management", results: [] },
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const status = readStatus(resolved.location.asyncDir);
|
|
498
|
+
if (!status) {
|
|
499
|
+
return {
|
|
500
|
+
content: [{ type: "text", text: `No async run status found for '${resolved.id}'.` }],
|
|
501
|
+
isError: true,
|
|
502
|
+
details: { mode: "management", results: [] },
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
if (status.mode !== "chain") {
|
|
506
|
+
return {
|
|
507
|
+
content: [{ type: "text", text: `Run '${resolved.id}' is ${status.mode}; only active chain runs accept appended steps.` }],
|
|
508
|
+
isError: true,
|
|
509
|
+
details: { mode: "management", results: [] },
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (status.state !== "running") {
|
|
513
|
+
return {
|
|
514
|
+
content: [{ type: "text", text: `Run '${resolved.id}' is ${status.state}; only running chain runs accept appended steps.` }],
|
|
515
|
+
isError: true,
|
|
516
|
+
details: { mode: "management", results: [] },
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const stillInProgress = (status.steps ?? []).some((step) => step.status === "running" || step.status === "pending") || (status.pendingAppends ?? 0) > 0;
|
|
520
|
+
if (!stillInProgress) {
|
|
521
|
+
return {
|
|
522
|
+
content: [{ type: "text", text: `Run '${resolved.id}' has no running or pending chain steps left; append-step must target an in-progress chain.` }],
|
|
523
|
+
isError: true,
|
|
524
|
+
details: { mode: "management", results: [] },
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const pendingAppendRequests = readPendingChainAppendRequests(resolved.location.asyncDir);
|
|
529
|
+
const reservedOutputNames = new Set<string>([
|
|
530
|
+
...Object.keys(status.outputs ?? {}),
|
|
531
|
+
...(status.steps ?? []).map((step) => step.outputName).filter((name): name is string => Boolean(name)),
|
|
532
|
+
...pendingAppendRequests.flatMap((request) => runnerStepOutputNames(request.steps)),
|
|
533
|
+
]);
|
|
534
|
+
try {
|
|
535
|
+
validateChainOutputBindingsWithContext(input.params.chain, { maxItems: input.deps.config.chain?.dynamicFanout?.maxItems }, {
|
|
536
|
+
priorOutputNames: reservedOutputNames,
|
|
537
|
+
startStepIndex: status.chainStepCount ?? status.steps?.length ?? 0,
|
|
538
|
+
});
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (!(error instanceof ChainOutputValidationError)) throw error;
|
|
541
|
+
return {
|
|
542
|
+
content: [{ type: "text", text: `Cannot append step to run '${resolved.id}': ${error.message}` }],
|
|
543
|
+
isError: true,
|
|
544
|
+
details: { mode: "management", results: [] },
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope);
|
|
549
|
+
const agents = input.deps.discoverAgents(input.requestCwd, scope).agents;
|
|
550
|
+
const chainSkillInput = normalizeSkillInput(input.params.skill);
|
|
551
|
+
const chainSkills = chainSkillInput === false ? [] : (chainSkillInput ?? []);
|
|
552
|
+
const asyncCtx = {
|
|
553
|
+
pi: input.deps.pi,
|
|
554
|
+
cwd: input.ctx.cwd,
|
|
555
|
+
currentSessionId: resolveCurrentSessionId(input.ctx.sessionManager),
|
|
556
|
+
currentModelProvider: input.ctx.model?.provider,
|
|
557
|
+
currentModel: input.ctx.model,
|
|
558
|
+
};
|
|
559
|
+
const built = buildAsyncRunnerSteps(resolved.id, {
|
|
560
|
+
chain: wrapChainTasksForFork(input.params.chain, input.params.context),
|
|
561
|
+
task: input.params.task,
|
|
562
|
+
resultMode: "chain",
|
|
563
|
+
agents,
|
|
564
|
+
ctx: asyncCtx,
|
|
565
|
+
availableModels: input.ctx.modelRegistry.getAvailable().map(toModelInfo),
|
|
566
|
+
cwd: status.cwd ?? input.requestCwd,
|
|
567
|
+
chainSkills,
|
|
568
|
+
dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
|
|
569
|
+
maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
|
|
570
|
+
asyncDir: resolved.location.asyncDir,
|
|
571
|
+
validateOutputBindings: false,
|
|
572
|
+
});
|
|
573
|
+
if ("error" in built) {
|
|
574
|
+
return {
|
|
575
|
+
content: [{ type: "text", text: built.error }],
|
|
576
|
+
isError: true,
|
|
577
|
+
details: { mode: "management", results: [] },
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
const appendedOutputNames = runnerStepOutputNames(built.steps);
|
|
581
|
+
const duplicateAppendedOutputs = duplicateNames(appendedOutputNames);
|
|
582
|
+
if (duplicateAppendedOutputs.length > 0) {
|
|
583
|
+
return {
|
|
584
|
+
content: [{ type: "text", text: `Cannot append step to run '${resolved.id}': duplicate output name in appended step: ${duplicateAppendedOutputs.join(", ")}.` }],
|
|
585
|
+
isError: true,
|
|
586
|
+
details: { mode: "management", results: [] },
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
const pendingOutputNames = new Set(pendingAppendRequests.flatMap((request) => runnerStepOutputNames(request.steps)));
|
|
590
|
+
const pendingDuplicateOutputs = appendedOutputNames.filter((name) => pendingOutputNames.has(name));
|
|
591
|
+
if (pendingDuplicateOutputs.length > 0) {
|
|
592
|
+
return {
|
|
593
|
+
content: [{ type: "text", text: `Cannot append step to run '${resolved.id}': output name already belongs to a pending append: ${pendingDuplicateOutputs.join(", ")}.` }],
|
|
594
|
+
isError: true,
|
|
595
|
+
details: { mode: "management", results: [] },
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
try {
|
|
600
|
+
const result = enqueueChainAppendRequest({
|
|
601
|
+
asyncDir: resolved.location.asyncDir,
|
|
602
|
+
runId: resolved.id,
|
|
603
|
+
steps: built.steps,
|
|
604
|
+
});
|
|
605
|
+
const stepText = built.steps.length === 1 ? "step" : "steps";
|
|
606
|
+
return {
|
|
607
|
+
content: [{
|
|
608
|
+
type: "text",
|
|
609
|
+
text: `Append queued for chain run ${resolved.id}: ${built.steps.length} ${stepText}. It becomes eligible after the chain's already-queued steps finish. Pending appends: ${result.pendingCount}.`,
|
|
610
|
+
}],
|
|
611
|
+
details: { mode: "management", results: [], asyncId: resolved.id, asyncDir: resolved.location.asyncDir },
|
|
612
|
+
};
|
|
613
|
+
} catch (error) {
|
|
614
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
615
|
+
return {
|
|
616
|
+
content: [{ type: "text", text: `Failed to append step to chain run ${resolved.id}: ${message}` }],
|
|
617
|
+
isError: true,
|
|
618
|
+
details: { mode: "management", results: [] },
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
437
623
|
function nestedRunSessionFile(run: NestedRunSummary): string | undefined {
|
|
438
624
|
return run.sessionFile ?? (run.steps?.length === 1 ? run.steps[0]?.sessionFile : undefined);
|
|
439
625
|
}
|
|
@@ -554,7 +740,8 @@ async function resumeAsyncRun(input: {
|
|
|
554
740
|
deps: ExecutorDeps;
|
|
555
741
|
}): Promise<AgentToolResult<Details>> {
|
|
556
742
|
const followUp = (input.params.message ?? input.params.task ?? "").trim();
|
|
557
|
-
|
|
743
|
+
const attachChain = (input.params.chain?.length ?? 0) > 0 ? input.params.chain as ChainStep[] : undefined;
|
|
744
|
+
if (!followUp && !attachChain) {
|
|
558
745
|
return {
|
|
559
746
|
content: [{ type: "text", text: "action='resume' requires message." }],
|
|
560
747
|
isError: true,
|
|
@@ -568,6 +755,13 @@ async function resumeAsyncRun(input: {
|
|
|
568
755
|
const requestedId = input.params.id ?? input.params.runId;
|
|
569
756
|
const resolved = requestedId ? resolveSubagentRunId(requestedId, { state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) }) : undefined;
|
|
570
757
|
if (resolved?.kind === "nested") {
|
|
758
|
+
if (attachChain) {
|
|
759
|
+
return {
|
|
760
|
+
content: [{ type: "text", text: "Attaching a running subagent as a chain root is currently available for top-level async runs only." }],
|
|
761
|
+
isError: true,
|
|
762
|
+
details: { mode: "management", results: [] },
|
|
763
|
+
};
|
|
764
|
+
}
|
|
571
765
|
if (resolved.match.run.state === "running" || resolved.match.run.state === "queued") {
|
|
572
766
|
return resumeLiveNestedRun({ target: resolved, message: followUp });
|
|
573
767
|
}
|
|
@@ -577,14 +771,26 @@ async function resumeAsyncRun(input: {
|
|
|
577
771
|
];
|
|
578
772
|
target = resolveNestedResumeTarget(resolved, trustedSessionRoots);
|
|
579
773
|
} else {
|
|
580
|
-
target = resolveResumeTarget(input.params, input.deps.state);
|
|
774
|
+
target = resolveResumeTarget(input.params, input.deps.state, { asyncRequireSessionFile: !attachChain });
|
|
581
775
|
}
|
|
582
776
|
} catch (error) {
|
|
583
777
|
const message = error instanceof Error ? error.message : String(error);
|
|
584
778
|
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
|
|
585
779
|
}
|
|
586
780
|
|
|
587
|
-
if (target.kind === "live") {
|
|
781
|
+
if (target.kind === "live" && !attachChain) {
|
|
782
|
+
const interrupt = interruptLiveAsyncResumeTarget({
|
|
783
|
+
target,
|
|
784
|
+
state: input.deps.state,
|
|
785
|
+
kill: input.deps.kill,
|
|
786
|
+
});
|
|
787
|
+
if (!interrupt.ok) {
|
|
788
|
+
return {
|
|
789
|
+
content: [{ type: "text", text: interrupt.message }],
|
|
790
|
+
isError: true,
|
|
791
|
+
details: { mode: "management", results: [] },
|
|
792
|
+
};
|
|
793
|
+
}
|
|
588
794
|
const delivered = await deliverSubagentIntercomMessageEvent(
|
|
589
795
|
input.deps.pi.events,
|
|
590
796
|
target.intercomTarget,
|
|
@@ -594,7 +800,7 @@ async function resumeAsyncRun(input: {
|
|
|
594
800
|
);
|
|
595
801
|
if (delivered) {
|
|
596
802
|
return {
|
|
597
|
-
content: [{ type: "text", text: [`
|
|
803
|
+
content: [{ type: "text", text: [`Interrupted live async child, then delivered follow-up.`, `Run: ${target.runId}`, `Intercom target: ${target.intercomTarget}`].join("\n") }],
|
|
598
804
|
details: { mode: "management", results: [] },
|
|
599
805
|
};
|
|
600
806
|
}
|
|
@@ -637,6 +843,73 @@ async function resumeAsyncRun(input: {
|
|
|
637
843
|
};
|
|
638
844
|
}
|
|
639
845
|
|
|
846
|
+
if (attachChain) {
|
|
847
|
+
if (target.source !== "async") {
|
|
848
|
+
return {
|
|
849
|
+
content: [{ type: "text", text: "Attaching a running subagent as a chain root is currently available for async runs only." }],
|
|
850
|
+
isError: true,
|
|
851
|
+
details: { mode: "management", results: [] },
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
if (!isAsyncAvailable()) {
|
|
855
|
+
return {
|
|
856
|
+
content: [{ type: "text", text: "Async mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-subagents package dependencies are installed." }],
|
|
857
|
+
isError: true,
|
|
858
|
+
details: { mode: "chain", results: [] },
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
const runId = randomUUID().slice(0, 8);
|
|
862
|
+
const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
|
|
863
|
+
const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
|
|
864
|
+
const chain = wrapChainTasksForFork(attachChain, input.params.context);
|
|
865
|
+
const normalized = normalizeSkillInput(input.params.skill);
|
|
866
|
+
const result = executeAsyncChain(runId, {
|
|
867
|
+
chain,
|
|
868
|
+
task: (input.params.task ?? followUp) || undefined,
|
|
869
|
+
attachRoot: {
|
|
870
|
+
runId: target.runId,
|
|
871
|
+
asyncDir: target.asyncDir ?? path.join(ASYNC_DIR, target.runId),
|
|
872
|
+
resultPath: resolveAsyncRootResultPath(RESULTS_DIR, target.runId),
|
|
873
|
+
index: target.index,
|
|
874
|
+
agent: target.agent,
|
|
875
|
+
label: `Attached ${target.runId}`,
|
|
876
|
+
},
|
|
877
|
+
agents,
|
|
878
|
+
ctx: {
|
|
879
|
+
pi: input.deps.pi,
|
|
880
|
+
cwd: input.requestCwd,
|
|
881
|
+
currentSessionId: input.deps.state.currentSessionId,
|
|
882
|
+
currentModelProvider: input.ctx.model?.provider,
|
|
883
|
+
currentModel: input.ctx.model,
|
|
884
|
+
},
|
|
885
|
+
availableModels,
|
|
886
|
+
cwd: effectiveCwd,
|
|
887
|
+
maxOutput: input.params.maxOutput,
|
|
888
|
+
artifactsDir: input.deps.tempArtifactsDir,
|
|
889
|
+
artifactConfig,
|
|
890
|
+
shareEnabled: input.params.share === true,
|
|
891
|
+
sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile),
|
|
892
|
+
chainSkills: normalized === false ? [] : (normalized ?? []),
|
|
893
|
+
dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
|
|
894
|
+
maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
|
|
895
|
+
worktreeSetupHook: input.deps.config.worktreeSetupHook,
|
|
896
|
+
worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
|
|
897
|
+
controlConfig: resolveControlConfig(input.deps.config.control, input.params.control),
|
|
898
|
+
controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
|
|
899
|
+
childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
|
|
900
|
+
});
|
|
901
|
+
if (result.isError) return result;
|
|
902
|
+
const attachedId = result.details.asyncId ?? runId;
|
|
903
|
+
const lines = [
|
|
904
|
+
`Attached async subagent ${target.runId} as the first step of a new chain.`,
|
|
905
|
+
`Chain run: ${attachedId}`,
|
|
906
|
+
`Root: ${target.agent} (step ${target.index + 1})`,
|
|
907
|
+
result.details.asyncDir ? `Async dir: ${result.details.asyncDir}` : undefined,
|
|
908
|
+
`Status if needed: subagent({ action: "status", id: "${attachedId}" })`,
|
|
909
|
+
].filter((line): line is string => Boolean(line));
|
|
910
|
+
return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n")) }], details: result.details };
|
|
911
|
+
}
|
|
912
|
+
|
|
640
913
|
const runId = randomUUID().slice(0, 8);
|
|
641
914
|
const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
|
|
642
915
|
const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
|
|
@@ -2268,6 +2541,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2268
2541
|
if (params.action === "resume") {
|
|
2269
2542
|
return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
|
|
2270
2543
|
}
|
|
2544
|
+
if (params.action === "append-step") {
|
|
2545
|
+
return appendStepToAsyncChain({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
|
|
2546
|
+
}
|
|
2271
2547
|
if (params.action === "interrupt") {
|
|
2272
2548
|
const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
|
|
2273
2549
|
let resolved: ResolvedSubagentRunId | undefined;
|
|
@@ -2297,7 +2573,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2297
2573
|
details: { mode: "management", results: [] },
|
|
2298
2574
|
};
|
|
2299
2575
|
}
|
|
2300
|
-
const asyncInterruptResult = interruptAsyncRun(deps.state, resolved?.kind === "async" ? resolved.id : targetRunId);
|
|
2576
|
+
const asyncInterruptResult = interruptAsyncRun(deps.state, resolved?.kind === "async" ? resolved.id : targetRunId, deps.kill);
|
|
2301
2577
|
if (asyncInterruptResult) return asyncInterruptResult;
|
|
2302
2578
|
return {
|
|
2303
2579
|
content: [{ type: "text", text: "No interrupt-capable run found in this session." }],
|
|
@@ -2319,7 +2595,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2319
2595
|
details: { mode: "management" as const, results: [] },
|
|
2320
2596
|
};
|
|
2321
2597
|
}
|
|
2322
|
-
return handleManagementAction(params.action, paramsWithResolvedCwd, { ...ctx, cwd: requestCwd });
|
|
2598
|
+
return handleManagementAction(params.action, paramsWithResolvedCwd, { ...ctx, cwd: requestCwd, config: deps.config });
|
|
2323
2599
|
}
|
|
2324
2600
|
|
|
2325
2601
|
const { blocked, depth, maxDepth } = checkSubagentDepth(deps.config.maxSubagentDepth);
|
|
@@ -8,6 +8,11 @@ const SAFE_OUTPUT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
8
8
|
|
|
9
9
|
export class ChainOutputValidationError extends Error {}
|
|
10
10
|
|
|
11
|
+
export interface ChainOutputValidationContext {
|
|
12
|
+
priorOutputNames?: Iterable<string>;
|
|
13
|
+
startStepIndex?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
function outputNamesForStep(step: ChainStep): string[] {
|
|
12
17
|
if (isParallelStep(step)) return step.parallel.map((task) => task.as).filter((name): name is string => Boolean(name));
|
|
13
18
|
if (isDynamicParallelStep(step)) return [step.collect.as];
|
|
@@ -22,27 +27,37 @@ function taskTemplatesForStep(step: ChainStep): string[] {
|
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export function validateChainOutputBindings(steps: ChainStep[], dynamicFanoutConfig: DynamicFanoutConfig = {}): void {
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
validateChainOutputBindingsWithContext(steps, dynamicFanoutConfig);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function validateChainOutputBindingsWithContext(
|
|
34
|
+
steps: ChainStep[],
|
|
35
|
+
dynamicFanoutConfig: DynamicFanoutConfig = {},
|
|
36
|
+
context: ChainOutputValidationContext = {},
|
|
37
|
+
): void {
|
|
38
|
+
const priorOutputNames = [...(context.priorOutputNames ?? [])];
|
|
39
|
+
const available = new Set<string>(priorOutputNames);
|
|
40
|
+
const seen = new Set<string>(priorOutputNames);
|
|
27
41
|
for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
|
|
42
|
+
const displayStepIndex = (context.startStepIndex ?? 0) + stepIndex + 1;
|
|
28
43
|
const step = steps[stepIndex]!;
|
|
29
44
|
if (hasDynamicFanoutFields(step)) {
|
|
30
45
|
if (!isDynamicParallelStep(step)) {
|
|
31
|
-
throw new ChainOutputValidationError(`Dynamic chain step ${
|
|
46
|
+
throw new ChainOutputValidationError(`Dynamic chain step ${displayStepIndex} requires expand, a single parallel template object, and collect; dynamic expand/collect cannot be mixed with static parallel arrays.`);
|
|
32
47
|
}
|
|
33
48
|
try {
|
|
34
|
-
validateDynamicStepShape(step,
|
|
49
|
+
validateDynamicStepShape(step, displayStepIndex - 1, dynamicFanoutConfig);
|
|
35
50
|
} catch (error) {
|
|
36
51
|
if (error instanceof DynamicFanoutError) throw new ChainOutputValidationError(error.message);
|
|
37
52
|
throw error;
|
|
38
53
|
}
|
|
39
54
|
if (!available.has(step.expand.from.output)) {
|
|
40
|
-
throw new ChainOutputValidationError(`Dynamic chain step ${
|
|
55
|
+
throw new ChainOutputValidationError(`Dynamic chain step ${displayStepIndex} references unknown output '${step.expand.from.output}'. Named outputs are only available after producing step/group completes.`);
|
|
41
56
|
}
|
|
42
57
|
}
|
|
43
58
|
for (const name of outputNamesForStep(step)) {
|
|
44
59
|
if (!SAFE_OUTPUT_NAME_PATTERN.test(name)) {
|
|
45
|
-
throw new ChainOutputValidationError(`Invalid chain output name '${name}' at step ${
|
|
60
|
+
throw new ChainOutputValidationError(`Invalid chain output name '${name}' at step ${displayStepIndex}. Use /^[A-Za-z_][A-Za-z0-9_]*$/.`);
|
|
46
61
|
}
|
|
47
62
|
if (seen.has(name)) {
|
|
48
63
|
throw new ChainOutputValidationError(`Duplicate chain output name '${name}'. Each as name must be unique.`);
|
|
@@ -54,10 +69,10 @@ export function validateChainOutputBindings(steps: ChainStep[], dynamicFanoutCon
|
|
|
54
69
|
const rawReference = match[0];
|
|
55
70
|
const name = match[1]!;
|
|
56
71
|
if (!SAFE_OUTPUT_NAME_PATTERN.test(name)) {
|
|
57
|
-
throw new ChainOutputValidationError(`Invalid chain output reference '${rawReference}' at step ${
|
|
72
|
+
throw new ChainOutputValidationError(`Invalid chain output reference '${rawReference}' at step ${displayStepIndex}. Use {outputs.name} with /^[A-Za-z_][A-Za-z0-9_]*$/ names.`);
|
|
58
73
|
}
|
|
59
74
|
if (!available.has(name)) {
|
|
60
|
-
throw new ChainOutputValidationError(`Unknown chain output reference '${rawReference}' at step ${
|
|
75
|
+
throw new ChainOutputValidationError(`Unknown chain output reference '${rawReference}' at step ${displayStepIndex}. Named outputs are only available after producing step/group completes.`);
|
|
61
76
|
}
|
|
62
77
|
}
|
|
63
78
|
}
|
|
@@ -48,7 +48,7 @@ const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outpu
|
|
|
48
48
|
const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
|
|
49
49
|
...DYNAMIC_PARALLEL_KEYS,
|
|
50
50
|
"outputName", "structured", "inheritProjectContext", "inheritSkills", "skills", "outputPath", "maxSubagentDepth",
|
|
51
|
-
"structuredOutput", "structuredOutputSchema", "tools", "extensions", "mcpDirectTools", "completionGuard", "systemPrompt",
|
|
51
|
+
"structuredOutput", "structuredOutputSchema", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "completionGuard", "systemPrompt",
|
|
52
52
|
"systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance",
|
|
53
53
|
]);
|
|
54
54
|
const DYNAMIC_COLLECT_KEYS = new Set(["as", "outputSchema"]);
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export interface RunnerSubagentStep {
|
|
2
2
|
agent: string;
|
|
3
3
|
task: string;
|
|
4
|
+
importAsyncRoot?: {
|
|
5
|
+
runId: string;
|
|
6
|
+
asyncDir: string;
|
|
7
|
+
resultPath: string;
|
|
8
|
+
index: number;
|
|
9
|
+
};
|
|
4
10
|
phase?: string;
|
|
5
11
|
label?: string;
|
|
6
12
|
outputName?: string;
|
|
@@ -11,6 +17,7 @@ export interface RunnerSubagentStep {
|
|
|
11
17
|
modelCandidates?: string[];
|
|
12
18
|
tools?: string[];
|
|
13
19
|
extensions?: string[];
|
|
20
|
+
subagentOnlyExtensions?: string[];
|
|
14
21
|
mcpDirectTools?: string[];
|
|
15
22
|
completionGuard?: boolean;
|
|
16
23
|
systemPrompt?: string | null;
|
|
@@ -39,6 +39,7 @@ interface BuildPiArgsInput {
|
|
|
39
39
|
inheritSkills: boolean;
|
|
40
40
|
tools?: string[];
|
|
41
41
|
extensions?: string[];
|
|
42
|
+
subagentOnlyExtensions?: string[];
|
|
42
43
|
systemPrompt?: string | null;
|
|
43
44
|
mcpDirectTools?: string[];
|
|
44
45
|
cwd?: string;
|
|
@@ -120,11 +121,11 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
120
121
|
: [PROMPT_RUNTIME_EXTENSION_PATH];
|
|
121
122
|
if (input.extensions !== undefined) {
|
|
122
123
|
args.push("--no-extensions");
|
|
123
|
-
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...input.extensions])]) {
|
|
124
|
+
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...input.extensions, ...(input.subagentOnlyExtensions ?? [])])]) {
|
|
124
125
|
args.push("--extension", extPath);
|
|
125
126
|
}
|
|
126
127
|
} else {
|
|
127
|
-
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths])]) {
|
|
128
|
+
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...(input.subagentOnlyExtensions ?? [])])]) {
|
|
128
129
|
args.push("--extension", extPath);
|
|
129
130
|
}
|
|
130
131
|
}
|
|
@@ -173,9 +173,8 @@ export function formatControlNoticeMessage(event: ControlEvent, childIntercomTar
|
|
|
173
173
|
].filter((line): line is string => Boolean(line)).join("\n");
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
: undefined;
|
|
176
|
+
const nudgeMessage = "What are you blocked on? Reply with the smallest next step or ask for a decision.";
|
|
177
|
+
const nudgeCommand = `subagent({ action: "resume", id: "${runTarget}", ${event.index !== undefined ? `index: ${event.index}, ` : ""}message: "${nudgeMessage}" })`;
|
|
179
178
|
if (event.type === "active_long_running") {
|
|
180
179
|
const facts = formatLongRunningFacts(event);
|
|
181
180
|
return [
|
|
@@ -183,10 +182,9 @@ export function formatControlNoticeMessage(event: ControlEvent, childIntercomTar
|
|
|
183
182
|
`Run: ${runTarget}${event.index !== undefined ? ` step ${event.index + 1}` : ""}`,
|
|
184
183
|
`Signal: ${event.message}`,
|
|
185
184
|
facts ? `Facts: ${facts}` : undefined,
|
|
186
|
-
"Hint: Inspect status, then nudge if the work seems stuck.",
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
: "Nudge: no child message route registered",
|
|
185
|
+
"Hint: Inspect status, then nudge if the work seems stuck. Live async nudges interrupt the child before sending the follow-up.",
|
|
186
|
+
`Nudge: ${nudgeCommand}`,
|
|
187
|
+
childIntercomTarget ? `Direct intercom target: ${childIntercomTarget}` : undefined,
|
|
190
188
|
`Status: subagent({ action: "status", id: "${runTarget}" })`,
|
|
191
189
|
`Interrupt: subagent({ action: "interrupt", id: "${runTarget}" })`,
|
|
192
190
|
].filter((line): line is string => Boolean(line)).join("\n");
|
|
@@ -197,10 +195,9 @@ export function formatControlNoticeMessage(event: ControlEvent, childIntercomTar
|
|
|
197
195
|
`Run: ${runTarget}${event.index !== undefined ? ` step ${event.index + 1}` : ""}`,
|
|
198
196
|
`Signal: ${event.message}`,
|
|
199
197
|
event.recentFailureSummary ? `Recent failures: ${event.recentFailureSummary}` : undefined,
|
|
200
|
-
"Hint: Inspect status first unless the run is clearly blocked.",
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
: "Nudge: no child message route registered",
|
|
198
|
+
"Hint: Inspect status first unless the run is clearly blocked. Live async nudges interrupt the child before sending the follow-up.",
|
|
199
|
+
`Nudge: ${nudgeCommand}`,
|
|
200
|
+
childIntercomTarget ? `Direct intercom target: ${childIntercomTarget}` : undefined,
|
|
204
201
|
`Status: subagent({ action: "status", id: "${runTarget}" })`,
|
|
205
202
|
`Interrupt: subagent({ action: "interrupt", id: "${runTarget}" })`,
|
|
206
203
|
].filter((line): line is string => Boolean(line)).join("\n");
|
package/src/shared/types.ts
CHANGED
|
@@ -579,6 +579,7 @@ export interface AsyncStatus {
|
|
|
579
579
|
cwd?: string;
|
|
580
580
|
currentStep?: number;
|
|
581
581
|
chainStepCount?: number;
|
|
582
|
+
pendingAppends?: number;
|
|
582
583
|
parallelGroups?: AsyncParallelGroupStatus[];
|
|
583
584
|
workflowGraph?: WorkflowGraphSnapshot;
|
|
584
585
|
steps?: Array<{
|
|
@@ -815,6 +816,13 @@ interface ExtensionChainConfig {
|
|
|
815
816
|
};
|
|
816
817
|
}
|
|
817
818
|
|
|
819
|
+
export interface ProactiveSkillSubagentsConfig {
|
|
820
|
+
enabled?: boolean;
|
|
821
|
+
minReferences?: number;
|
|
822
|
+
maxRecommendations?: number;
|
|
823
|
+
preferredAgent?: string;
|
|
824
|
+
}
|
|
825
|
+
|
|
818
826
|
export interface ExtensionConfig {
|
|
819
827
|
asyncByDefault?: boolean;
|
|
820
828
|
forceTopLevelAsync?: boolean;
|
|
@@ -826,6 +834,7 @@ export interface ExtensionConfig {
|
|
|
826
834
|
worktreeSetupHook?: string;
|
|
827
835
|
worktreeSetupHookTimeoutMs?: number;
|
|
828
836
|
intercomBridge?: IntercomBridgeConfig;
|
|
837
|
+
proactiveSkillSubagents?: ProactiveSkillSubagentsConfig | false;
|
|
829
838
|
}
|
|
830
839
|
|
|
831
840
|
// ============================================================================
|
|
@@ -916,7 +925,7 @@ export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
|
|
|
916
925
|
export const POLL_INTERVAL_MS = 250;
|
|
917
926
|
export const MAX_WIDGET_JOBS = 4;
|
|
918
927
|
export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
|
|
919
|
-
export const SUBAGENT_ACTIONS = ["list", "get", "create", "update", "delete", "status", "interrupt", "resume", "doctor"] as const;
|
|
928
|
+
export const SUBAGENT_ACTIONS = ["list", "get", "create", "update", "delete", "status", "interrupt", "resume", "append-step", "doctor"] as const;
|
|
920
929
|
|
|
921
930
|
export const DEFAULT_FORK_PREAMBLE =
|
|
922
931
|
"You are a delegated subagent running from a fork of the parent session. " +
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
interface SlashSubagentRequest {
|
|
14
14
|
requestId: string;
|
|
15
15
|
params: SubagentParamsLike;
|
|
16
|
+
/** Optional requester context for in-process extension bridge calls. */
|
|
17
|
+
ctx?: ExtensionContext;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export interface SlashSubagentResponse {
|
|
@@ -77,7 +79,7 @@ export function registerSlashSubagentBridge(options: SlashBridgeOptions): {
|
|
|
77
79
|
if (typeof request.requestId !== "string" || !request.params) return;
|
|
78
80
|
const { requestId, params } = request as SlashSubagentRequest;
|
|
79
81
|
|
|
80
|
-
const ctx = options.getContext();
|
|
82
|
+
const ctx = request.ctx ?? options.getContext();
|
|
81
83
|
if (!ctx) {
|
|
82
84
|
const response: SlashSubagentResponse = {
|
|
83
85
|
requestId,
|
|
@@ -245,7 +245,7 @@ async function requestSlashRun(
|
|
|
245
245
|
next();
|
|
246
246
|
};
|
|
247
247
|
|
|
248
|
-
pi.events.emit(SLASH_SUBAGENT_REQUEST_EVENT, { requestId, params });
|
|
248
|
+
pi.events.emit(SLASH_SUBAGENT_REQUEST_EVENT, { requestId, params, ctx });
|
|
249
249
|
|
|
250
250
|
// Bridge emits STARTED synchronously during REQUEST emit.
|
|
251
251
|
// If not started, no bridge received the request.
|