pi-subagents 0.29.0 → 0.31.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 +43 -0
- package/README.md +125 -19
- package/agents/context-builder.md +3 -3
- package/agents/planner.md +1 -1
- package/agents/researcher.md +1 -1
- package/agents/scout.md +1 -1
- package/package.json +7 -7
- package/skills/pi-subagents/SKILL.md +30 -0
- package/src/agents/agent-management.ts +189 -8
- package/src/agents/agent-serializer.ts +35 -12
- package/src/agents/agents.ts +243 -24
- package/src/agents/frontmatter.ts +66 -2
- package/src/agents/proactive-skills.ts +191 -0
- package/src/agents/skills.ts +117 -20
- package/src/extension/doctor.ts +20 -0
- package/src/extension/fanout-child.ts +2 -1
- package/src/extension/index.ts +50 -5
- package/src/extension/schemas.ts +40 -79
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/runs/background/async-execution.ts +180 -67
- package/src/runs/background/async-job-tracker.ts +56 -11
- 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/result-watcher.ts +11 -2
- package/src/runs/background/run-status.ts +1 -0
- package/src/runs/background/stale-run-reconciler.ts +9 -4
- package/src/runs/background/subagent-runner.ts +158 -11
- package/src/runs/foreground/chain-execution.ts +26 -2
- package/src/runs/foreground/execution.ts +114 -8
- package/src/runs/foreground/subagent-executor.ts +611 -87
- package/src/runs/shared/acceptance.ts +285 -34
- package/src/runs/shared/chain-outputs.ts +23 -8
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/dynamic-fanout.ts +5 -3
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
- package/src/runs/shared/parallel-utils.ts +13 -1
- package/src/runs/shared/pi-args.ts +12 -3
- package/src/runs/shared/single-output.ts +15 -1
- package/src/runs/shared/subagent-control.ts +8 -11
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +17 -2
- package/src/shared/utils.ts +19 -1
- package/src/slash/prompt-template-bridge.ts +26 -3
- package/src/slash/slash-bridge.ts +3 -1
- package/src/slash/slash-commands.ts +34 -4
- package/src/tui/render.ts +265 -13
|
@@ -31,7 +31,10 @@ 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";
|
|
37
|
+
import { validateAcceptanceInput } from "../shared/acceptance.ts";
|
|
35
38
|
import { createForkContextResolver } from "../../shared/fork-context.ts";
|
|
36
39
|
import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
|
|
37
40
|
import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
|
|
@@ -47,7 +50,8 @@ import {
|
|
|
47
50
|
resolveSubagentResultStatus,
|
|
48
51
|
stripDetailsOutputsForIntercomReceipt,
|
|
49
52
|
} from "../../intercom/result-intercom.ts";
|
|
50
|
-
import { buildRevivedAsyncTask, resolveAsyncResumeTarget } from "../background/async-resume.ts";
|
|
53
|
+
import { buildRevivedAsyncTask, interruptLiveAsyncResumeTarget, resolveAsyncResumeTarget } from "../background/async-resume.ts";
|
|
54
|
+
import { resolveAsyncRootResultPath } from "../background/chain-root-attachment.ts";
|
|
51
55
|
import { createNestedRoute, readNestedControlResults, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts";
|
|
52
56
|
import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts";
|
|
53
57
|
import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
|
|
@@ -79,7 +83,9 @@ import {
|
|
|
79
83
|
type SingleResult,
|
|
80
84
|
type SubagentRunMode,
|
|
81
85
|
type SubagentState,
|
|
86
|
+
ASYNC_DIR,
|
|
82
87
|
DEFAULT_ARTIFACT_CONFIG,
|
|
88
|
+
RESULTS_DIR,
|
|
83
89
|
SUBAGENT_ACTIONS,
|
|
84
90
|
SUBAGENT_CONTROL_EVENT,
|
|
85
91
|
SUBAGENT_CONTROL_INTERCOM_EVENT,
|
|
@@ -123,6 +129,8 @@ export interface SubagentParamsLike {
|
|
|
123
129
|
worktree?: boolean;
|
|
124
130
|
context?: "fresh" | "fork";
|
|
125
131
|
async?: boolean;
|
|
132
|
+
timeoutMs?: number;
|
|
133
|
+
maxRuntimeMs?: number;
|
|
126
134
|
clarify?: boolean;
|
|
127
135
|
share?: boolean;
|
|
128
136
|
control?: ControlConfig;
|
|
@@ -150,6 +158,7 @@ interface ExecutorDeps {
|
|
|
150
158
|
expandTilde: (p: string) => string;
|
|
151
159
|
discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] };
|
|
152
160
|
allowMutatingManagementActions?: boolean;
|
|
161
|
+
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
interface ExecutionContextData {
|
|
@@ -164,6 +173,7 @@ interface ExecutionContextData {
|
|
|
164
173
|
sessionRoot: string;
|
|
165
174
|
sessionDirForIndex: (idx?: number) => string;
|
|
166
175
|
sessionFileForIndex: (idx?: number) => string | undefined;
|
|
176
|
+
sessionFileForTask: (agentName: string, idx?: number) => string | undefined;
|
|
167
177
|
artifactConfig: ArtifactConfig;
|
|
168
178
|
artifactsDir: string;
|
|
169
179
|
backgroundRequestedWhileClarifying: boolean;
|
|
@@ -171,6 +181,9 @@ interface ExecutionContextData {
|
|
|
171
181
|
controlConfig: ResolvedControlConfig;
|
|
172
182
|
intercomBridge: IntercomBridgeState;
|
|
173
183
|
nestedRoute?: NestedRouteInfo;
|
|
184
|
+
timeoutMs?: number;
|
|
185
|
+
deadlineAt?: number;
|
|
186
|
+
contextPolicy: AgentDefaultContextPolicy;
|
|
174
187
|
}
|
|
175
188
|
|
|
176
189
|
function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
|
|
@@ -317,7 +330,7 @@ function isExactResumeError(error: unknown, source: "async" | "foreground", requ
|
|
|
317
330
|
return new RegExp(`\\b${source} run '${escapeRegExp(requested)}'`, "i").test(error.message);
|
|
318
331
|
}
|
|
319
332
|
|
|
320
|
-
function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState): ResumeSourceTarget {
|
|
333
|
+
function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState, options: { asyncRequireSessionFile?: boolean } = {}): ResumeSourceTarget {
|
|
321
334
|
const requested = (params.id ?? params.runId)?.trim() ?? "";
|
|
322
335
|
let foregroundTarget: ForegroundResumeSourceTarget | undefined;
|
|
323
336
|
let foregroundError: unknown;
|
|
@@ -331,7 +344,7 @@ function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState):
|
|
|
331
344
|
foregroundError = error;
|
|
332
345
|
}
|
|
333
346
|
try {
|
|
334
|
-
asyncTarget = { source: "async", ...resolveAsyncResumeTarget(params) };
|
|
347
|
+
asyncTarget = { source: "async", ...resolveAsyncResumeTarget(params, {}, { requireSessionFile: options.asyncRequireSessionFile }) };
|
|
335
348
|
} catch (error) {
|
|
336
349
|
asyncError = error;
|
|
337
350
|
}
|
|
@@ -402,7 +415,7 @@ function emitControlNotification(input: {
|
|
|
402
415
|
}
|
|
403
416
|
}
|
|
404
417
|
|
|
405
|
-
function interruptAsyncRun(state: SubagentState, runId: string | undefined): AgentToolResult<Details> | null {
|
|
418
|
+
function interruptAsyncRun(state: SubagentState, runId: string | undefined, kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean): AgentToolResult<Details> | null {
|
|
406
419
|
const target = getAsyncInterruptTarget(state, runId);
|
|
407
420
|
if (!target) return null;
|
|
408
421
|
const status = readStatus(target.asyncDir);
|
|
@@ -414,7 +427,7 @@ function interruptAsyncRun(state: SubagentState, runId: string | undefined): Age
|
|
|
414
427
|
};
|
|
415
428
|
}
|
|
416
429
|
try {
|
|
417
|
-
process.kill(status.pid, ASYNC_INTERRUPT_SIGNAL);
|
|
430
|
+
(kill ?? process.kill)(status.pid, ASYNC_INTERRUPT_SIGNAL);
|
|
418
431
|
const tracked = state.asyncJobs.get(target.asyncId);
|
|
419
432
|
if (tracked) {
|
|
420
433
|
tracked.activityState = undefined;
|
|
@@ -434,6 +447,196 @@ function interruptAsyncRun(state: SubagentState, runId: string | undefined): Age
|
|
|
434
447
|
}
|
|
435
448
|
}
|
|
436
449
|
|
|
450
|
+
function duplicateNames(names: string[]): string[] {
|
|
451
|
+
const seen = new Set<string>();
|
|
452
|
+
const duplicates = new Set<string>();
|
|
453
|
+
for (const name of names) {
|
|
454
|
+
if (seen.has(name)) duplicates.add(name);
|
|
455
|
+
else seen.add(name);
|
|
456
|
+
}
|
|
457
|
+
return [...duplicates];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function appendStepToAsyncChain(input: {
|
|
461
|
+
params: SubagentParamsLike;
|
|
462
|
+
requestCwd: string;
|
|
463
|
+
ctx: ExtensionContext;
|
|
464
|
+
deps: ExecutorDeps;
|
|
465
|
+
}): AgentToolResult<Details> {
|
|
466
|
+
const targetRunId = input.params.id ?? input.params.runId;
|
|
467
|
+
if (!targetRunId) {
|
|
468
|
+
return {
|
|
469
|
+
content: [{ type: "text", text: "action='append-step' requires id." }],
|
|
470
|
+
isError: true,
|
|
471
|
+
details: { mode: "management", results: [] },
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
if (!input.params.chain || input.params.chain.length !== 1) {
|
|
475
|
+
return {
|
|
476
|
+
content: [{ type: "text", text: "action='append-step' requires chain with exactly one step." }],
|
|
477
|
+
isError: true,
|
|
478
|
+
details: { mode: "management", results: [] },
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
const acceptanceErrors = validateExecutionAcceptance(input.params);
|
|
482
|
+
if (acceptanceErrors.length > 0) {
|
|
483
|
+
return {
|
|
484
|
+
content: [{ type: "text", text: `Cannot append step: ${acceptanceErrors.join(" ")}` }],
|
|
485
|
+
isError: true,
|
|
486
|
+
details: { mode: "management", results: [] },
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let resolved: ResolvedSubagentRunId | undefined;
|
|
491
|
+
try {
|
|
492
|
+
resolved = resolveSubagentRunId(targetRunId, { state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) });
|
|
493
|
+
} catch (error) {
|
|
494
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
495
|
+
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
|
|
496
|
+
}
|
|
497
|
+
if (!resolved) {
|
|
498
|
+
return {
|
|
499
|
+
content: [{ type: "text", text: `No async chain run found for '${targetRunId}'.` }],
|
|
500
|
+
isError: true,
|
|
501
|
+
details: { mode: "management", results: [] },
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
if (resolved.kind !== "async" || !resolved.location.asyncDir) {
|
|
505
|
+
return {
|
|
506
|
+
content: [{ type: "text", text: `Run '${resolved.id}' is not an append-capable async chain run.` }],
|
|
507
|
+
isError: true,
|
|
508
|
+
details: { mode: "management", results: [] },
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const status = readStatus(resolved.location.asyncDir);
|
|
513
|
+
if (!status) {
|
|
514
|
+
return {
|
|
515
|
+
content: [{ type: "text", text: `No async run status found for '${resolved.id}'.` }],
|
|
516
|
+
isError: true,
|
|
517
|
+
details: { mode: "management", results: [] },
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (status.mode !== "chain") {
|
|
521
|
+
return {
|
|
522
|
+
content: [{ type: "text", text: `Run '${resolved.id}' is ${status.mode}; only active chain runs accept appended steps.` }],
|
|
523
|
+
isError: true,
|
|
524
|
+
details: { mode: "management", results: [] },
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
if (status.state !== "running") {
|
|
528
|
+
return {
|
|
529
|
+
content: [{ type: "text", text: `Run '${resolved.id}' is ${status.state}; only running chain runs accept appended steps.` }],
|
|
530
|
+
isError: true,
|
|
531
|
+
details: { mode: "management", results: [] },
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
const stillInProgress = (status.steps ?? []).some((step) => step.status === "running" || step.status === "pending") || (status.pendingAppends ?? 0) > 0;
|
|
535
|
+
if (!stillInProgress) {
|
|
536
|
+
return {
|
|
537
|
+
content: [{ type: "text", text: `Run '${resolved.id}' has no running or pending chain steps left; append-step must target an in-progress chain.` }],
|
|
538
|
+
isError: true,
|
|
539
|
+
details: { mode: "management", results: [] },
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const pendingAppendRequests = readPendingChainAppendRequests(resolved.location.asyncDir);
|
|
544
|
+
const reservedOutputNames = new Set<string>([
|
|
545
|
+
...Object.keys(status.outputs ?? {}),
|
|
546
|
+
...(status.steps ?? []).map((step) => step.outputName).filter((name): name is string => Boolean(name)),
|
|
547
|
+
...pendingAppendRequests.flatMap((request) => runnerStepOutputNames(request.steps)),
|
|
548
|
+
]);
|
|
549
|
+
try {
|
|
550
|
+
validateChainOutputBindingsWithContext(input.params.chain, { maxItems: input.deps.config.chain?.dynamicFanout?.maxItems }, {
|
|
551
|
+
priorOutputNames: reservedOutputNames,
|
|
552
|
+
startStepIndex: status.chainStepCount ?? status.steps?.length ?? 0,
|
|
553
|
+
});
|
|
554
|
+
} catch (error) {
|
|
555
|
+
if (!(error instanceof ChainOutputValidationError)) throw error;
|
|
556
|
+
return {
|
|
557
|
+
content: [{ type: "text", text: `Cannot append step to run '${resolved.id}': ${error.message}` }],
|
|
558
|
+
isError: true,
|
|
559
|
+
details: { mode: "management", results: [] },
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope);
|
|
564
|
+
const agents = input.deps.discoverAgents(input.requestCwd, scope).agents;
|
|
565
|
+
const contextPolicy = resolveExplicitContextPolicy(input.params);
|
|
566
|
+
const chainSkillInput = normalizeSkillInput(input.params.skill);
|
|
567
|
+
const chainSkills = chainSkillInput === false ? [] : (chainSkillInput ?? []);
|
|
568
|
+
const asyncCtx = {
|
|
569
|
+
pi: input.deps.pi,
|
|
570
|
+
cwd: input.ctx.cwd,
|
|
571
|
+
currentSessionId: resolveCurrentSessionId(input.ctx.sessionManager),
|
|
572
|
+
parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
|
|
573
|
+
currentModelProvider: input.ctx.model?.provider,
|
|
574
|
+
currentModel: input.ctx.model,
|
|
575
|
+
};
|
|
576
|
+
const built = buildAsyncRunnerSteps(resolved.id, {
|
|
577
|
+
chain: wrapChainTasksForFork(input.params.chain, contextPolicy),
|
|
578
|
+
task: input.params.task,
|
|
579
|
+
resultMode: "chain",
|
|
580
|
+
agents,
|
|
581
|
+
ctx: asyncCtx,
|
|
582
|
+
availableModels: input.ctx.modelRegistry.getAvailable().map(toModelInfo),
|
|
583
|
+
cwd: status.cwd ?? input.requestCwd,
|
|
584
|
+
chainSkills,
|
|
585
|
+
dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
|
|
586
|
+
maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
|
|
587
|
+
asyncDir: resolved.location.asyncDir,
|
|
588
|
+
validateOutputBindings: false,
|
|
589
|
+
});
|
|
590
|
+
if ("error" in built) {
|
|
591
|
+
return {
|
|
592
|
+
content: [{ type: "text", text: built.error }],
|
|
593
|
+
isError: true,
|
|
594
|
+
details: { mode: "management", results: [] },
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const appendedOutputNames = runnerStepOutputNames(built.steps);
|
|
598
|
+
const duplicateAppendedOutputs = duplicateNames(appendedOutputNames);
|
|
599
|
+
if (duplicateAppendedOutputs.length > 0) {
|
|
600
|
+
return {
|
|
601
|
+
content: [{ type: "text", text: `Cannot append step to run '${resolved.id}': duplicate output name in appended step: ${duplicateAppendedOutputs.join(", ")}.` }],
|
|
602
|
+
isError: true,
|
|
603
|
+
details: { mode: "management", results: [] },
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const pendingOutputNames = new Set(pendingAppendRequests.flatMap((request) => runnerStepOutputNames(request.steps)));
|
|
607
|
+
const pendingDuplicateOutputs = appendedOutputNames.filter((name) => pendingOutputNames.has(name));
|
|
608
|
+
if (pendingDuplicateOutputs.length > 0) {
|
|
609
|
+
return {
|
|
610
|
+
content: [{ type: "text", text: `Cannot append step to run '${resolved.id}': output name already belongs to a pending append: ${pendingDuplicateOutputs.join(", ")}.` }],
|
|
611
|
+
isError: true,
|
|
612
|
+
details: { mode: "management", results: [] },
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
const result = enqueueChainAppendRequest({
|
|
618
|
+
asyncDir: resolved.location.asyncDir,
|
|
619
|
+
runId: resolved.id,
|
|
620
|
+
steps: built.steps,
|
|
621
|
+
});
|
|
622
|
+
const stepText = built.steps.length === 1 ? "step" : "steps";
|
|
623
|
+
return {
|
|
624
|
+
content: [{
|
|
625
|
+
type: "text",
|
|
626
|
+
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}.`,
|
|
627
|
+
}],
|
|
628
|
+
details: { mode: "management", results: [], asyncId: resolved.id, asyncDir: resolved.location.asyncDir },
|
|
629
|
+
};
|
|
630
|
+
} catch (error) {
|
|
631
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
632
|
+
return {
|
|
633
|
+
content: [{ type: "text", text: `Failed to append step to chain run ${resolved.id}: ${message}` }],
|
|
634
|
+
isError: true,
|
|
635
|
+
details: { mode: "management", results: [] },
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
437
640
|
function nestedRunSessionFile(run: NestedRunSummary): string | undefined {
|
|
438
641
|
return run.sessionFile ?? (run.steps?.length === 1 ? run.steps[0]?.sessionFile : undefined);
|
|
439
642
|
}
|
|
@@ -554,7 +757,8 @@ async function resumeAsyncRun(input: {
|
|
|
554
757
|
deps: ExecutorDeps;
|
|
555
758
|
}): Promise<AgentToolResult<Details>> {
|
|
556
759
|
const followUp = (input.params.message ?? input.params.task ?? "").trim();
|
|
557
|
-
|
|
760
|
+
const attachChain = (input.params.chain?.length ?? 0) > 0 ? input.params.chain as ChainStep[] : undefined;
|
|
761
|
+
if (!followUp && !attachChain) {
|
|
558
762
|
return {
|
|
559
763
|
content: [{ type: "text", text: "action='resume' requires message." }],
|
|
560
764
|
isError: true,
|
|
@@ -568,6 +772,13 @@ async function resumeAsyncRun(input: {
|
|
|
568
772
|
const requestedId = input.params.id ?? input.params.runId;
|
|
569
773
|
const resolved = requestedId ? resolveSubagentRunId(requestedId, { state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) }) : undefined;
|
|
570
774
|
if (resolved?.kind === "nested") {
|
|
775
|
+
if (attachChain) {
|
|
776
|
+
return {
|
|
777
|
+
content: [{ type: "text", text: "Attaching a running subagent as a chain root is currently available for top-level async runs only." }],
|
|
778
|
+
isError: true,
|
|
779
|
+
details: { mode: "management", results: [] },
|
|
780
|
+
};
|
|
781
|
+
}
|
|
571
782
|
if (resolved.match.run.state === "running" || resolved.match.run.state === "queued") {
|
|
572
783
|
return resumeLiveNestedRun({ target: resolved, message: followUp });
|
|
573
784
|
}
|
|
@@ -577,14 +788,26 @@ async function resumeAsyncRun(input: {
|
|
|
577
788
|
];
|
|
578
789
|
target = resolveNestedResumeTarget(resolved, trustedSessionRoots);
|
|
579
790
|
} else {
|
|
580
|
-
target = resolveResumeTarget(input.params, input.deps.state);
|
|
791
|
+
target = resolveResumeTarget(input.params, input.deps.state, { asyncRequireSessionFile: !attachChain });
|
|
581
792
|
}
|
|
582
793
|
} catch (error) {
|
|
583
794
|
const message = error instanceof Error ? error.message : String(error);
|
|
584
795
|
return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } };
|
|
585
796
|
}
|
|
586
797
|
|
|
587
|
-
if (target.kind === "live") {
|
|
798
|
+
if (target.kind === "live" && !attachChain) {
|
|
799
|
+
const interrupt = interruptLiveAsyncResumeTarget({
|
|
800
|
+
target,
|
|
801
|
+
state: input.deps.state,
|
|
802
|
+
kill: input.deps.kill,
|
|
803
|
+
});
|
|
804
|
+
if (!interrupt.ok) {
|
|
805
|
+
return {
|
|
806
|
+
content: [{ type: "text", text: interrupt.message }],
|
|
807
|
+
isError: true,
|
|
808
|
+
details: { mode: "management", results: [] },
|
|
809
|
+
};
|
|
810
|
+
}
|
|
588
811
|
const delivered = await deliverSubagentIntercomMessageEvent(
|
|
589
812
|
input.deps.pi.events,
|
|
590
813
|
target.intercomTarget,
|
|
@@ -594,7 +817,7 @@ async function resumeAsyncRun(input: {
|
|
|
594
817
|
);
|
|
595
818
|
if (delivered) {
|
|
596
819
|
return {
|
|
597
|
-
content: [{ type: "text", text: [`
|
|
820
|
+
content: [{ type: "text", text: [`Interrupted live async child, then delivered follow-up.`, `Run: ${target.runId}`, `Intercom target: ${target.intercomTarget}`].join("\n") }],
|
|
598
821
|
details: { mode: "management", results: [] },
|
|
599
822
|
};
|
|
600
823
|
}
|
|
@@ -637,6 +860,75 @@ async function resumeAsyncRun(input: {
|
|
|
637
860
|
};
|
|
638
861
|
}
|
|
639
862
|
|
|
863
|
+
if (attachChain) {
|
|
864
|
+
if (target.source !== "async") {
|
|
865
|
+
return {
|
|
866
|
+
content: [{ type: "text", text: "Attaching a running subagent as a chain root is currently available for async runs only." }],
|
|
867
|
+
isError: true,
|
|
868
|
+
details: { mode: "management", results: [] },
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
if (!isAsyncAvailable()) {
|
|
872
|
+
return {
|
|
873
|
+
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." }],
|
|
874
|
+
isError: true,
|
|
875
|
+
details: { mode: "chain", results: [] },
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
const runId = randomUUID().slice(0, 8);
|
|
879
|
+
const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
|
|
880
|
+
const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
|
|
881
|
+
const contextPolicy = resolveExplicitContextPolicy(input.params);
|
|
882
|
+
const chain = wrapChainTasksForFork(attachChain, contextPolicy);
|
|
883
|
+
const normalized = normalizeSkillInput(input.params.skill);
|
|
884
|
+
const result = executeAsyncChain(runId, {
|
|
885
|
+
chain,
|
|
886
|
+
task: (input.params.task ?? followUp) || undefined,
|
|
887
|
+
attachRoot: {
|
|
888
|
+
runId: target.runId,
|
|
889
|
+
asyncDir: target.asyncDir ?? path.join(ASYNC_DIR, target.runId),
|
|
890
|
+
resultPath: resolveAsyncRootResultPath(RESULTS_DIR, target.runId),
|
|
891
|
+
index: target.index,
|
|
892
|
+
agent: target.agent,
|
|
893
|
+
label: `Attached ${target.runId}`,
|
|
894
|
+
},
|
|
895
|
+
agents,
|
|
896
|
+
ctx: {
|
|
897
|
+
pi: input.deps.pi,
|
|
898
|
+
cwd: input.requestCwd,
|
|
899
|
+
currentSessionId: input.deps.state.currentSessionId,
|
|
900
|
+
parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
|
|
901
|
+
currentModelProvider: input.ctx.model?.provider,
|
|
902
|
+
currentModel: input.ctx.model,
|
|
903
|
+
},
|
|
904
|
+
availableModels,
|
|
905
|
+
cwd: effectiveCwd,
|
|
906
|
+
maxOutput: input.params.maxOutput,
|
|
907
|
+
artifactsDir: input.deps.tempArtifactsDir,
|
|
908
|
+
artifactConfig,
|
|
909
|
+
shareEnabled: input.params.share === true,
|
|
910
|
+
sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile),
|
|
911
|
+
chainSkills: normalized === false ? [] : (normalized ?? []),
|
|
912
|
+
dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems,
|
|
913
|
+
maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth),
|
|
914
|
+
worktreeSetupHook: input.deps.config.worktreeSetupHook,
|
|
915
|
+
worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs,
|
|
916
|
+
controlConfig: resolveControlConfig(input.deps.config.control, input.params.control),
|
|
917
|
+
controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined,
|
|
918
|
+
childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
|
|
919
|
+
});
|
|
920
|
+
if (result.isError) return result;
|
|
921
|
+
const attachedId = result.details.asyncId ?? runId;
|
|
922
|
+
const lines = [
|
|
923
|
+
`Attached async subagent ${target.runId} as the first step of a new chain.`,
|
|
924
|
+
`Chain run: ${attachedId}`,
|
|
925
|
+
`Root: ${target.agent} (step ${target.index + 1})`,
|
|
926
|
+
result.details.asyncDir ? `Async dir: ${result.details.asyncDir}` : undefined,
|
|
927
|
+
`Status if needed: subagent({ action: "status", id: "${attachedId}" })`,
|
|
928
|
+
].filter((line): line is string => Boolean(line));
|
|
929
|
+
return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n")) }], details: result.details };
|
|
930
|
+
}
|
|
931
|
+
|
|
640
932
|
const runId = randomUUID().slice(0, 8);
|
|
641
933
|
const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false };
|
|
642
934
|
const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo);
|
|
@@ -648,6 +940,7 @@ async function resumeAsyncRun(input: {
|
|
|
648
940
|
pi: input.deps.pi,
|
|
649
941
|
cwd: input.requestCwd,
|
|
650
942
|
currentSessionId: input.deps.state.currentSessionId,
|
|
943
|
+
parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
|
|
651
944
|
currentModelProvider: input.ctx.model?.provider,
|
|
652
945
|
currentModel: input.ctx.model,
|
|
653
946
|
},
|
|
@@ -795,6 +1088,15 @@ function validateExecutionInput(
|
|
|
795
1088
|
};
|
|
796
1089
|
}
|
|
797
1090
|
|
|
1091
|
+
const acceptanceErrors = validateExecutionAcceptance(params);
|
|
1092
|
+
if (acceptanceErrors.length > 0) {
|
|
1093
|
+
return {
|
|
1094
|
+
content: [{ type: "text", text: acceptanceErrors.join(" ") }],
|
|
1095
|
+
isError: true,
|
|
1096
|
+
details: { mode: getRequestedModeLabel(params), results: [] },
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
798
1100
|
if (hasSingle && params.agent && !agents.find((agent) => agent.name === params.agent)) {
|
|
799
1101
|
return {
|
|
800
1102
|
content: [{ type: "text", text: `Unknown agent: ${params.agent}` }],
|
|
@@ -872,6 +1174,42 @@ function validateExecutionInput(
|
|
|
872
1174
|
return null;
|
|
873
1175
|
}
|
|
874
1176
|
|
|
1177
|
+
function validateExecutionChainBindings(params: SubagentParamsLike, dynamicFanoutMaxItems?: number): AgentToolResult<Details> | null {
|
|
1178
|
+
if ((params.chain?.length ?? 0) === 0) return null;
|
|
1179
|
+
try {
|
|
1180
|
+
validateChainOutputBindingsWithContext(params.chain as ChainStep[], { maxItems: dynamicFanoutMaxItems });
|
|
1181
|
+
} catch (error) {
|
|
1182
|
+
if (error instanceof ChainOutputValidationError) {
|
|
1183
|
+
return {
|
|
1184
|
+
content: [{ type: "text", text: error.message }],
|
|
1185
|
+
isError: true,
|
|
1186
|
+
details: { mode: "chain" as const, results: [] },
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
throw error;
|
|
1190
|
+
}
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function validateExecutionAcceptance(params: SubagentParamsLike): string[] {
|
|
1195
|
+
const errors: string[] = [];
|
|
1196
|
+
errors.push(...validateAcceptanceInput(params.acceptance, "acceptance"));
|
|
1197
|
+
for (const [index, task] of (params.tasks ?? []).entries()) {
|
|
1198
|
+
errors.push(...validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`));
|
|
1199
|
+
}
|
|
1200
|
+
for (const [stepIndex, step] of (params.chain ?? []).entries()) {
|
|
1201
|
+
errors.push(...validateAcceptanceInput((step as { acceptance?: unknown }).acceptance, `chain[${stepIndex}].acceptance`));
|
|
1202
|
+
if (isParallelStep(step)) {
|
|
1203
|
+
for (const [taskIndex, task] of step.parallel.entries()) {
|
|
1204
|
+
errors.push(...validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`));
|
|
1205
|
+
}
|
|
1206
|
+
} else if (isDynamicParallelStep(step)) {
|
|
1207
|
+
errors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`));
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return errors;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
875
1213
|
function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
|
|
876
1214
|
if ((params.chain?.length ?? 0) > 0) return "chain";
|
|
877
1215
|
if ((params.tasks?.length ?? 0) > 0) return "parallel";
|
|
@@ -879,16 +1217,46 @@ function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
|
|
|
879
1217
|
return "single";
|
|
880
1218
|
}
|
|
881
1219
|
|
|
882
|
-
|
|
883
|
-
|
|
1220
|
+
interface AgentDefaultContextPolicy {
|
|
1221
|
+
params: SubagentParamsLike;
|
|
1222
|
+
contextForAgent(agentName: string): "fresh" | "fork";
|
|
1223
|
+
usesFork: boolean;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function resolveAgentDefaultContextPolicy(params: SubagentParamsLike, agents: AgentConfig[]): AgentDefaultContextPolicy {
|
|
1227
|
+
if (params.context !== undefined) {
|
|
1228
|
+
return resolveExplicitContextPolicy(params);
|
|
1229
|
+
}
|
|
884
1230
|
const byName = new Map(agents.map((agent) => [agent.name, agent]));
|
|
1231
|
+
const contextForAgent = (agentName: string): "fresh" | "fork" =>
|
|
1232
|
+
byName.get(agentName)?.defaultContext === "fork" ? "fork" : "fresh";
|
|
1233
|
+
const usesFork = collectRequestedAgentNames(params).some((name) => contextForAgent(name) === "fork");
|
|
1234
|
+
return {
|
|
1235
|
+
params: usesFork ? { ...params, context: "fork" } : params,
|
|
1236
|
+
contextForAgent,
|
|
1237
|
+
usesFork,
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function resolveExplicitContextPolicy(params: SubagentParamsLike): AgentDefaultContextPolicy {
|
|
1242
|
+
const context = params.context === "fork" ? "fork" : "fresh";
|
|
1243
|
+
return {
|
|
1244
|
+
params,
|
|
1245
|
+
contextForAgent: () => context,
|
|
1246
|
+
usesFork: context === "fork",
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function collectRequestedAgentNames(params: SubagentParamsLike): string[] {
|
|
885
1251
|
const names: string[] = [];
|
|
886
1252
|
if (params.agent) names.push(params.agent);
|
|
887
1253
|
for (const task of params.tasks ?? []) names.push(task.agent);
|
|
888
1254
|
for (const step of params.chain ?? []) names.push(...getStepAgents(step));
|
|
889
|
-
return names
|
|
890
|
-
|
|
891
|
-
|
|
1255
|
+
return names;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function shouldForkAgent(contextPolicy: AgentDefaultContextPolicy, agentName: string): boolean {
|
|
1259
|
+
return contextPolicy.contextForAgent(agentName) === "fork";
|
|
892
1260
|
}
|
|
893
1261
|
|
|
894
1262
|
function buildRequestedModeError(params: SubagentParamsLike, message: string): AgentToolResult<Details> {
|
|
@@ -902,6 +1270,22 @@ function buildRequestedModeError(params: SubagentParamsLike, message: string): A
|
|
|
902
1270
|
);
|
|
903
1271
|
}
|
|
904
1272
|
|
|
1273
|
+
function resolveForegroundTimeout(params: SubagentParamsLike): { timeoutMs?: number; error?: string } {
|
|
1274
|
+
const rawTimeout = params.timeoutMs;
|
|
1275
|
+
const rawMaxRuntime = params.maxRuntimeMs;
|
|
1276
|
+
if (rawTimeout === undefined && rawMaxRuntime === undefined) return {};
|
|
1277
|
+
for (const [name, value] of [["timeoutMs", rawTimeout], ["maxRuntimeMs", rawMaxRuntime]] as const) {
|
|
1278
|
+
if (value === undefined) continue;
|
|
1279
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
1280
|
+
return { error: `${name} must be a positive integer.` };
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
if (rawTimeout !== undefined && rawMaxRuntime !== undefined && rawTimeout !== rawMaxRuntime) {
|
|
1284
|
+
return { error: "timeoutMs and maxRuntimeMs are aliases; provide only one value or use the same value for both." };
|
|
1285
|
+
}
|
|
1286
|
+
return { timeoutMs: rawTimeout ?? rawMaxRuntime };
|
|
1287
|
+
}
|
|
1288
|
+
|
|
905
1289
|
function expandTopLevelTaskCounts(tasks: TaskParam[]): { tasks?: TaskParam[]; error?: string } {
|
|
906
1290
|
const expanded: TaskParam[] = [];
|
|
907
1291
|
for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) {
|
|
@@ -989,14 +1373,14 @@ function toExecutionErrorResult(params: SubagentParamsLike, error: unknown): Age
|
|
|
989
1373
|
|
|
990
1374
|
function collectChainSessionFiles(
|
|
991
1375
|
chain: ChainStep[],
|
|
992
|
-
|
|
1376
|
+
sessionFileForTask: (agentName: string, idx?: number) => string | undefined,
|
|
993
1377
|
): (string | undefined)[] {
|
|
994
1378
|
const sessionFiles: (string | undefined)[] = [];
|
|
995
1379
|
let flatIndex = 0;
|
|
996
1380
|
for (const step of chain) {
|
|
997
1381
|
if (isParallelStep(step)) {
|
|
998
|
-
for (
|
|
999
|
-
sessionFiles.push(
|
|
1382
|
+
for (const task of step.parallel) {
|
|
1383
|
+
sessionFiles.push(sessionFileForTask(task.agent, flatIndex));
|
|
1000
1384
|
flatIndex++;
|
|
1001
1385
|
}
|
|
1002
1386
|
continue;
|
|
@@ -1005,21 +1389,22 @@ function collectChainSessionFiles(
|
|
|
1005
1389
|
sessionFiles.push(undefined);
|
|
1006
1390
|
continue;
|
|
1007
1391
|
}
|
|
1008
|
-
sessionFiles.push(
|
|
1392
|
+
sessionFiles.push(sessionFileForTask((step as SequentialStep).agent, flatIndex));
|
|
1009
1393
|
flatIndex++;
|
|
1010
1394
|
}
|
|
1011
1395
|
return sessionFiles;
|
|
1012
1396
|
}
|
|
1013
1397
|
|
|
1014
|
-
function wrapChainTasksForFork(chain: ChainStep[],
|
|
1015
|
-
if (context !== "fork") return chain;
|
|
1398
|
+
function wrapChainTasksForFork(chain: ChainStep[], contextPolicy: AgentDefaultContextPolicy): ChainStep[] {
|
|
1016
1399
|
return chain.map((step, stepIndex) => {
|
|
1017
1400
|
if (isParallelStep(step)) {
|
|
1018
1401
|
return {
|
|
1019
1402
|
...step,
|
|
1020
1403
|
parallel: step.parallel.map((task) => ({
|
|
1021
1404
|
...task,
|
|
1022
|
-
task:
|
|
1405
|
+
task: shouldForkAgent(contextPolicy, task.agent)
|
|
1406
|
+
? wrapForkTask(task.task ?? "{previous}")
|
|
1407
|
+
: task.task,
|
|
1023
1408
|
})),
|
|
1024
1409
|
};
|
|
1025
1410
|
}
|
|
@@ -1028,18 +1413,59 @@ function wrapChainTasksForFork(chain: ChainStep[], context: SubagentParamsLike["
|
|
|
1028
1413
|
...step,
|
|
1029
1414
|
parallel: {
|
|
1030
1415
|
...step.parallel,
|
|
1031
|
-
task:
|
|
1416
|
+
task: shouldForkAgent(contextPolicy, step.parallel.agent)
|
|
1417
|
+
? wrapForkTask(step.parallel.task ?? "{previous}")
|
|
1418
|
+
: step.parallel.task,
|
|
1032
1419
|
},
|
|
1033
1420
|
};
|
|
1034
1421
|
}
|
|
1035
1422
|
const sequential = step as SequentialStep;
|
|
1036
1423
|
return {
|
|
1037
1424
|
...sequential,
|
|
1038
|
-
task:
|
|
1425
|
+
task: shouldForkAgent(contextPolicy, sequential.agent)
|
|
1426
|
+
? wrapForkTask(sequential.task ?? (stepIndex === 0 ? "{task}" : "{previous}"))
|
|
1427
|
+
: sequential.task,
|
|
1039
1428
|
};
|
|
1040
1429
|
});
|
|
1041
1430
|
}
|
|
1042
1431
|
|
|
1432
|
+
function preflightForkSessionsForStaticTasks(
|
|
1433
|
+
params: SubagentParamsLike,
|
|
1434
|
+
contextPolicy: AgentDefaultContextPolicy,
|
|
1435
|
+
sessionFileForTask: (agentName: string, idx?: number) => string | undefined,
|
|
1436
|
+
): void {
|
|
1437
|
+
if (!contextPolicy.usesFork) return;
|
|
1438
|
+
if (params.agent) {
|
|
1439
|
+
if (shouldForkAgent(contextPolicy, params.agent)) sessionFileForTask(params.agent, 0);
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
if (params.tasks) {
|
|
1443
|
+
params.tasks.forEach((task, index) => {
|
|
1444
|
+
if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, index);
|
|
1445
|
+
});
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
if (!params.chain?.length) return;
|
|
1449
|
+
let flatIndex = 0;
|
|
1450
|
+
for (const step of params.chain) {
|
|
1451
|
+
if (isParallelStep(step)) {
|
|
1452
|
+
for (const task of step.parallel) {
|
|
1453
|
+
if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, flatIndex);
|
|
1454
|
+
flatIndex++;
|
|
1455
|
+
}
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
if (isDynamicParallelStep(step)) {
|
|
1459
|
+
if (shouldForkAgent(contextPolicy, step.parallel.agent)) sessionFileForTask(step.parallel.agent, flatIndex);
|
|
1460
|
+
flatIndex++;
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
const sequential = step as SequentialStep;
|
|
1464
|
+
if (shouldForkAgent(contextPolicy, sequential.agent)) sessionFileForTask(sequential.agent, flatIndex);
|
|
1465
|
+
flatIndex++;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1043
1469
|
function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentToolResult<Details> | null {
|
|
1044
1470
|
const {
|
|
1045
1471
|
params,
|
|
@@ -1049,12 +1475,14 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1049
1475
|
shareEnabled,
|
|
1050
1476
|
sessionRoot,
|
|
1051
1477
|
sessionFileForIndex,
|
|
1478
|
+
sessionFileForTask,
|
|
1052
1479
|
artifactConfig,
|
|
1053
1480
|
artifactsDir,
|
|
1054
1481
|
effectiveAsync,
|
|
1055
1482
|
controlConfig,
|
|
1056
1483
|
intercomBridge,
|
|
1057
1484
|
nestedRoute,
|
|
1485
|
+
contextPolicy,
|
|
1058
1486
|
} = data;
|
|
1059
1487
|
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
1060
1488
|
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
@@ -1095,6 +1523,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1095
1523
|
pi: deps.pi,
|
|
1096
1524
|
cwd: ctx.cwd,
|
|
1097
1525
|
currentSessionId: deps.state.currentSessionId!,
|
|
1526
|
+
parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
|
|
1098
1527
|
currentModelProvider: ctx.model?.provider,
|
|
1099
1528
|
currentModel: ctx.model,
|
|
1100
1529
|
};
|
|
@@ -1112,7 +1541,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1112
1541
|
const skillOverrides = params.tasks.map((task) => normalizeSkillInput(task.skill));
|
|
1113
1542
|
const parallelTasks = params.tasks.map((task, index) => ({
|
|
1114
1543
|
agent: task.agent,
|
|
1115
|
-
task:
|
|
1544
|
+
task: shouldForkAgent(contextPolicy, task.agent) ? wrapForkTask(task.task) : task.task,
|
|
1116
1545
|
cwd: task.cwd,
|
|
1117
1546
|
...(modelOverrides[index] ? { model: modelOverrides[index] } : {}),
|
|
1118
1547
|
...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}),
|
|
@@ -1139,7 +1568,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1139
1568
|
shareEnabled,
|
|
1140
1569
|
sessionRoot,
|
|
1141
1570
|
chainSkills: [],
|
|
1142
|
-
sessionFilesByFlatIndex: params.tasks.map((
|
|
1571
|
+
sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index)),
|
|
1143
1572
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
1144
1573
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1145
1574
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
@@ -1153,7 +1582,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1153
1582
|
if (hasChain && params.chain) {
|
|
1154
1583
|
const normalized = normalizeSkillInput(params.skill);
|
|
1155
1584
|
const chainSkills = normalized === false ? [] : (normalized ?? []);
|
|
1156
|
-
const chain = wrapChainTasksForFork(params.chain as ChainStep[],
|
|
1585
|
+
const chain = wrapChainTasksForFork(params.chain as ChainStep[], contextPolicy);
|
|
1157
1586
|
return executeAsyncChain(id, {
|
|
1158
1587
|
chain,
|
|
1159
1588
|
task: params.task,
|
|
@@ -1167,7 +1596,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1167
1596
|
shareEnabled,
|
|
1168
1597
|
sessionRoot,
|
|
1169
1598
|
chainSkills,
|
|
1170
|
-
sessionFilesByFlatIndex: collectChainSessionFiles(chain,
|
|
1599
|
+
sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask),
|
|
1171
1600
|
dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
|
|
1172
1601
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
1173
1602
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
@@ -1197,7 +1626,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1197
1626
|
const modelOverride = resolveSubagentModelOverride((params.model as string | undefined) ?? a.model, ctx.model, availableModels, currentProvider);
|
|
1198
1627
|
return executeAsyncSingle(id, {
|
|
1199
1628
|
agent: params.agent!,
|
|
1200
|
-
task: params.
|
|
1629
|
+
task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
|
|
1201
1630
|
agentConfig: a,
|
|
1202
1631
|
ctx: asyncCtx,
|
|
1203
1632
|
availableModels,
|
|
@@ -1207,7 +1636,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
1207
1636
|
artifactConfig,
|
|
1208
1637
|
shareEnabled,
|
|
1209
1638
|
sessionRoot,
|
|
1210
|
-
sessionFile:
|
|
1639
|
+
sessionFile: sessionFileForTask(params.agent!, 0),
|
|
1211
1640
|
skills,
|
|
1212
1641
|
output: effectiveOutput,
|
|
1213
1642
|
outputMode: effectiveOutputMode,
|
|
@@ -1237,18 +1666,20 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1237
1666
|
shareEnabled,
|
|
1238
1667
|
sessionDirForIndex,
|
|
1239
1668
|
sessionFileForIndex,
|
|
1669
|
+
sessionFileForTask,
|
|
1240
1670
|
artifactsDir,
|
|
1241
1671
|
artifactConfig,
|
|
1242
1672
|
onUpdate,
|
|
1243
1673
|
sessionRoot,
|
|
1244
1674
|
controlConfig,
|
|
1675
|
+
contextPolicy,
|
|
1245
1676
|
} = data;
|
|
1246
1677
|
const onControlEvent = createForegroundControlNotifier(data, deps);
|
|
1247
1678
|
const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
|
|
1248
1679
|
const foregroundControl = deps.state.foregroundControls.get(runId);
|
|
1249
1680
|
const normalized = normalizeSkillInput(params.skill);
|
|
1250
1681
|
const chainSkills = normalized === false ? [] : (normalized ?? []);
|
|
1251
|
-
const chain = wrapChainTasksForFork(params.chain as ChainStep[],
|
|
1682
|
+
const chain = wrapChainTasksForFork(params.chain as ChainStep[], contextPolicy);
|
|
1252
1683
|
const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
|
|
1253
1684
|
const chainResult = await executeChain({
|
|
1254
1685
|
chain,
|
|
@@ -1262,6 +1693,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1262
1693
|
shareEnabled,
|
|
1263
1694
|
sessionDirForIndex,
|
|
1264
1695
|
sessionFileForIndex,
|
|
1696
|
+
sessionFileForTask,
|
|
1265
1697
|
artifactsDir,
|
|
1266
1698
|
artifactConfig,
|
|
1267
1699
|
includeProgress: params.includeProgress,
|
|
@@ -1279,9 +1711,14 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1279
1711
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
1280
1712
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1281
1713
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
1714
|
+
timeoutMs: data.timeoutMs,
|
|
1715
|
+
deadlineAt: data.deadlineAt,
|
|
1282
1716
|
});
|
|
1283
1717
|
|
|
1284
1718
|
if (chainResult.requestedAsync) {
|
|
1719
|
+
if (data.timeoutMs !== undefined) {
|
|
1720
|
+
return buildRequestedModeError(params, "timeoutMs/maxRuntimeMs are only supported for foreground runs; background launch from clarify cannot preserve the timeout.");
|
|
1721
|
+
}
|
|
1285
1722
|
if (!isAsyncAvailable()) {
|
|
1286
1723
|
return {
|
|
1287
1724
|
content: [{ type: "text", text: "Background mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-subagents package dependencies are installed." }],
|
|
@@ -1294,10 +1731,11 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1294
1731
|
pi: deps.pi,
|
|
1295
1732
|
cwd: ctx.cwd,
|
|
1296
1733
|
currentSessionId: deps.state.currentSessionId!,
|
|
1734
|
+
parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
|
|
1297
1735
|
currentModelProvider: ctx.model?.provider,
|
|
1298
1736
|
currentModel: ctx.model,
|
|
1299
1737
|
};
|
|
1300
|
-
const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain,
|
|
1738
|
+
const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, contextPolicy);
|
|
1301
1739
|
return executeAsyncChain(id, {
|
|
1302
1740
|
chain: asyncChain,
|
|
1303
1741
|
task: params.task,
|
|
@@ -1311,7 +1749,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
1311
1749
|
shareEnabled,
|
|
1312
1750
|
sessionRoot,
|
|
1313
1751
|
chainSkills: chainResult.requestedAsync.chainSkills,
|
|
1314
|
-
sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain,
|
|
1752
|
+
sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask),
|
|
1315
1753
|
dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems,
|
|
1316
1754
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
1317
1755
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
@@ -1357,11 +1795,13 @@ interface ForegroundParallelRunInput {
|
|
|
1357
1795
|
runId: string;
|
|
1358
1796
|
sessionDirForIndex: (idx?: number) => string | undefined;
|
|
1359
1797
|
sessionFileForIndex: (idx?: number) => string | undefined;
|
|
1798
|
+
sessionFileForTask: (agentName: string, idx?: number) => string | undefined;
|
|
1360
1799
|
shareEnabled: boolean;
|
|
1361
1800
|
artifactConfig: ArtifactConfig;
|
|
1362
1801
|
artifactsDir: string;
|
|
1363
1802
|
maxOutput?: MaxOutputConfig;
|
|
1364
1803
|
paramsCwd: string;
|
|
1804
|
+
progressDir: string;
|
|
1365
1805
|
maxSubagentDepths: number[];
|
|
1366
1806
|
availableModels: ModelInfo[];
|
|
1367
1807
|
modelOverrides: (string | undefined)[];
|
|
@@ -1377,6 +1817,8 @@ interface ForegroundParallelRunInput {
|
|
|
1377
1817
|
liveProgress: (AgentProgress | undefined)[];
|
|
1378
1818
|
onUpdate?: (r: AgentToolResult<Details>) => void;
|
|
1379
1819
|
worktreeSetup?: WorktreeSetup;
|
|
1820
|
+
timeoutMs?: number;
|
|
1821
|
+
deadlineAt?: number;
|
|
1380
1822
|
}
|
|
1381
1823
|
|
|
1382
1824
|
function buildParallelModeError(message: string): AgentToolResult<Details> {
|
|
@@ -1487,7 +1929,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
1487
1929
|
? buildChainInstructions({ ...behavior, output: false, progress: false }, taskCwd, false)
|
|
1488
1930
|
: { prefix: "", suffix: "" };
|
|
1489
1931
|
const progressInstructions = behavior
|
|
1490
|
-
? buildChainInstructions({ ...behavior, output: false, reads: false }, input.
|
|
1932
|
+
? buildChainInstructions({ ...behavior, output: false, reads: false }, input.progressDir, index === input.firstProgressIndex)
|
|
1491
1933
|
: { prefix: "", suffix: "" };
|
|
1492
1934
|
const outputPath = resolveSingleOutputPath(behavior?.output, input.ctx.cwd, taskCwd);
|
|
1493
1935
|
const taskText = injectSingleOutputInstruction(
|
|
@@ -1510,6 +1952,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
1510
1952
|
}
|
|
1511
1953
|
const agentConfig = input.agents.find((agent) => agent.name === task.agent);
|
|
1512
1954
|
return runSync(input.ctx.cwd, input.agents, task.agent, taskText, {
|
|
1955
|
+
parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined,
|
|
1513
1956
|
cwd: taskCwd,
|
|
1514
1957
|
signal: input.signal,
|
|
1515
1958
|
interruptSignal: interruptController.signal,
|
|
@@ -1518,7 +1961,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
1518
1961
|
runId: input.runId,
|
|
1519
1962
|
index,
|
|
1520
1963
|
sessionDir: input.sessionDirForIndex(index),
|
|
1521
|
-
sessionFile: input.
|
|
1964
|
+
sessionFile: input.sessionFileForTask(task.agent, index),
|
|
1522
1965
|
share: input.shareEnabled,
|
|
1523
1966
|
artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
|
|
1524
1967
|
artifactConfig: input.artifactConfig,
|
|
@@ -1537,39 +1980,41 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
1537
1980
|
skills: effectiveSkills === false ? [] : effectiveSkills,
|
|
1538
1981
|
acceptance: task.acceptance,
|
|
1539
1982
|
acceptanceContext: { mode: "parallel" },
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
if (stepProgress.length > 0) input.liveProgress[index] = stepProgress[0];
|
|
1560
|
-
const mergedResults = input.liveResults.filter((result): result is SingleResult => result !== undefined);
|
|
1561
|
-
const mergedProgress = input.liveProgress.filter((progress): progress is AgentProgress => progress !== undefined);
|
|
1562
|
-
input.onUpdate?.({
|
|
1563
|
-
content: progressUpdate.content,
|
|
1564
|
-
details: {
|
|
1565
|
-
mode: "parallel",
|
|
1566
|
-
results: mergedResults,
|
|
1567
|
-
progress: mergedProgress,
|
|
1568
|
-
controlEvents: progressUpdate.details?.controlEvents,
|
|
1569
|
-
totalSteps: input.tasks.length,
|
|
1570
|
-
},
|
|
1571
|
-
});
|
|
1983
|
+
timeoutMs: input.timeoutMs,
|
|
1984
|
+
deadlineAt: input.deadlineAt,
|
|
1985
|
+
onUpdate: input.onUpdate
|
|
1986
|
+
? (progressUpdate) => {
|
|
1987
|
+
const stepResults = progressUpdate.details?.results || [];
|
|
1988
|
+
const stepProgress = progressUpdate.details?.progress || [];
|
|
1989
|
+
if (input.foregroundControl && stepProgress.length > 0) {
|
|
1990
|
+
const current = stepProgress[0];
|
|
1991
|
+
input.foregroundControl.currentAgent = task.agent;
|
|
1992
|
+
input.foregroundControl.currentIndex = index;
|
|
1993
|
+
input.foregroundControl.currentActivityState = current?.activityState;
|
|
1994
|
+
input.foregroundControl.lastActivityAt = current?.lastActivityAt;
|
|
1995
|
+
input.foregroundControl.currentTool = current?.currentTool;
|
|
1996
|
+
input.foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
|
|
1997
|
+
input.foregroundControl.currentPath = current?.currentPath;
|
|
1998
|
+
input.foregroundControl.turnCount = current?.turnCount;
|
|
1999
|
+
input.foregroundControl.tokens = current?.tokens;
|
|
2000
|
+
input.foregroundControl.toolCount = current?.toolCount;
|
|
2001
|
+
input.foregroundControl.updatedAt = Date.now();
|
|
1572
2002
|
}
|
|
2003
|
+
if (stepResults.length > 0) input.liveResults[index] = stepResults[0];
|
|
2004
|
+
if (stepProgress.length > 0) input.liveProgress[index] = stepProgress[0];
|
|
2005
|
+
const mergedResults = input.liveResults.filter((result): result is SingleResult => result !== undefined);
|
|
2006
|
+
const mergedProgress = input.liveProgress.filter((progress): progress is AgentProgress => progress !== undefined);
|
|
2007
|
+
input.onUpdate?.({
|
|
2008
|
+
content: progressUpdate.content,
|
|
2009
|
+
details: {
|
|
2010
|
+
mode: "parallel",
|
|
2011
|
+
results: mergedResults,
|
|
2012
|
+
progress: mergedProgress,
|
|
2013
|
+
controlEvents: progressUpdate.details?.controlEvents,
|
|
2014
|
+
totalSteps: input.tasks.length,
|
|
2015
|
+
},
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
1573
2018
|
: undefined,
|
|
1574
2019
|
}).finally(() => {
|
|
1575
2020
|
if (input.foregroundControl?.currentIndex === index) {
|
|
@@ -1590,6 +2035,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1590
2035
|
runId,
|
|
1591
2036
|
sessionDirForIndex,
|
|
1592
2037
|
sessionFileForIndex,
|
|
2038
|
+
sessionFileForTask,
|
|
1593
2039
|
shareEnabled,
|
|
1594
2040
|
artifactConfig,
|
|
1595
2041
|
artifactsDir,
|
|
@@ -1597,6 +2043,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1597
2043
|
onUpdate,
|
|
1598
2044
|
sessionRoot,
|
|
1599
2045
|
controlConfig,
|
|
2046
|
+
contextPolicy,
|
|
1600
2047
|
} = data;
|
|
1601
2048
|
const onControlEvent = createForegroundControlNotifier(data, deps);
|
|
1602
2049
|
const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
|
|
@@ -1699,6 +2146,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1699
2146
|
}
|
|
1700
2147
|
|
|
1701
2148
|
if (result.runInBackground) {
|
|
2149
|
+
if (data.timeoutMs !== undefined) {
|
|
2150
|
+
return buildRequestedModeError(params, "timeoutMs/maxRuntimeMs are only supported for foreground runs; background launch from clarify cannot preserve the timeout.");
|
|
2151
|
+
}
|
|
1702
2152
|
if (!isAsyncAvailable()) {
|
|
1703
2153
|
return {
|
|
1704
2154
|
content: [{ type: "text", text: "Background mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-subagents package dependencies are installed." }],
|
|
@@ -1711,11 +2161,12 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1711
2161
|
pi: deps.pi,
|
|
1712
2162
|
cwd: ctx.cwd,
|
|
1713
2163
|
currentSessionId: deps.state.currentSessionId!,
|
|
2164
|
+
parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
|
|
1714
2165
|
currentModelProvider: ctx.model?.provider,
|
|
1715
2166
|
currentModel: ctx.model,
|
|
1716
2167
|
};
|
|
1717
2168
|
const parallelTasks = tasks.map((t, i) => {
|
|
1718
|
-
const taskText =
|
|
2169
|
+
const taskText = shouldForkAgent(contextPolicy, t.agent) ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
|
|
1719
2170
|
const progress = taskDisallowsFileUpdates(taskText) ? false : behaviorOverrides[i]?.progress;
|
|
1720
2171
|
return {
|
|
1721
2172
|
agent: t.agent,
|
|
@@ -1743,7 +2194,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1743
2194
|
shareEnabled,
|
|
1744
2195
|
sessionRoot,
|
|
1745
2196
|
chainSkills: [],
|
|
1746
|
-
sessionFilesByFlatIndex: tasks.map((
|
|
2197
|
+
sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index)),
|
|
1747
2198
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
1748
2199
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1749
2200
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
@@ -1786,14 +2237,14 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1786
2237
|
}
|
|
1787
2238
|
|
|
1788
2239
|
const parallelProgressPrecreated = firstProgressIndex !== -1;
|
|
1789
|
-
|
|
2240
|
+
const parallelProgressDir = path.join(artifactsDir, "progress", runId);
|
|
2241
|
+
if (parallelProgressPrecreated) writeInitialProgressFile(parallelProgressDir);
|
|
1790
2242
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
taskTexts[i] = wrapForkTask(taskTexts[i]!);
|
|
1794
|
-
}
|
|
2243
|
+
for (let i = 0; i < taskTexts.length; i++) {
|
|
2244
|
+
if (shouldForkAgent(contextPolicy, tasks[i]!.agent)) taskTexts[i] = wrapForkTask(taskTexts[i]!);
|
|
1795
2245
|
}
|
|
1796
2246
|
|
|
2247
|
+
const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined);
|
|
1797
2248
|
const results = await runForegroundParallelTasks({
|
|
1798
2249
|
tasks,
|
|
1799
2250
|
taskTexts,
|
|
@@ -1804,11 +2255,13 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1804
2255
|
runId,
|
|
1805
2256
|
sessionDirForIndex,
|
|
1806
2257
|
sessionFileForIndex,
|
|
2258
|
+
sessionFileForTask,
|
|
1807
2259
|
shareEnabled,
|
|
1808
2260
|
artifactConfig,
|
|
1809
2261
|
artifactsDir,
|
|
1810
2262
|
maxOutput: params.maxOutput,
|
|
1811
2263
|
paramsCwd: effectiveCwd,
|
|
2264
|
+
progressDir: parallelProgressDir,
|
|
1812
2265
|
availableModels,
|
|
1813
2266
|
modelOverrides,
|
|
1814
2267
|
behaviors,
|
|
@@ -1824,6 +2277,8 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1824
2277
|
liveProgress,
|
|
1825
2278
|
onUpdate,
|
|
1826
2279
|
worktreeSetup,
|
|
2280
|
+
timeoutMs: data.timeoutMs,
|
|
2281
|
+
deadlineAt,
|
|
1827
2282
|
});
|
|
1828
2283
|
for (let i = 0; i < results.length; i++) {
|
|
1829
2284
|
const run = results[i]!;
|
|
@@ -1884,6 +2339,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1884
2339
|
output: result.truncation?.text || getSingleResultOutput(result),
|
|
1885
2340
|
exitCode: result.exitCode,
|
|
1886
2341
|
error: result.error,
|
|
2342
|
+
timedOut: result.timedOut,
|
|
1887
2343
|
})),
|
|
1888
2344
|
(i, agent) => `=== Task ${i + 1}: ${agent} ===`,
|
|
1889
2345
|
);
|
|
@@ -1911,13 +2367,14 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1911
2367
|
signal,
|
|
1912
2368
|
runId,
|
|
1913
2369
|
sessionDirForIndex,
|
|
1914
|
-
|
|
2370
|
+
sessionFileForTask,
|
|
1915
2371
|
shareEnabled,
|
|
1916
2372
|
artifactConfig,
|
|
1917
2373
|
artifactsDir,
|
|
1918
2374
|
onUpdate,
|
|
1919
2375
|
sessionRoot,
|
|
1920
2376
|
controlConfig,
|
|
2377
|
+
contextPolicy,
|
|
1921
2378
|
} = data;
|
|
1922
2379
|
const onControlEvent = createForegroundControlNotifier(data, deps);
|
|
1923
2380
|
const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget(runId, params.agent!, 0) : undefined;
|
|
@@ -1981,6 +2438,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1981
2438
|
if (override?.skills !== undefined) skillOverride = override.skills;
|
|
1982
2439
|
|
|
1983
2440
|
if (result.runInBackground) {
|
|
2441
|
+
if (data.timeoutMs !== undefined) {
|
|
2442
|
+
return buildRequestedModeError(params, "timeoutMs/maxRuntimeMs are only supported for foreground runs; background launch from clarify cannot preserve the timeout.");
|
|
2443
|
+
}
|
|
1984
2444
|
if (!isAsyncAvailable()) {
|
|
1985
2445
|
return {
|
|
1986
2446
|
content: [{ type: "text", text: "Background mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-subagents package dependencies are installed." }],
|
|
@@ -1993,12 +2453,13 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1993
2453
|
pi: deps.pi,
|
|
1994
2454
|
cwd: ctx.cwd,
|
|
1995
2455
|
currentSessionId: deps.state.currentSessionId!,
|
|
2456
|
+
parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
|
|
1996
2457
|
currentModelProvider: ctx.model?.provider,
|
|
1997
2458
|
currentModel: ctx.model,
|
|
1998
2459
|
};
|
|
1999
2460
|
return executeAsyncSingle(id, {
|
|
2000
2461
|
agent: params.agent!,
|
|
2001
|
-
task: params.
|
|
2462
|
+
task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task,
|
|
2002
2463
|
agentConfig,
|
|
2003
2464
|
ctx: asyncCtx,
|
|
2004
2465
|
availableModels,
|
|
@@ -2008,7 +2469,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
2008
2469
|
artifactConfig,
|
|
2009
2470
|
shareEnabled,
|
|
2010
2471
|
sessionRoot,
|
|
2011
|
-
sessionFile:
|
|
2472
|
+
sessionFile: sessionFileForTask(params.agent!, 0),
|
|
2012
2473
|
skills: skillOverride === false ? [] : skillOverride,
|
|
2013
2474
|
output: effectiveOutput,
|
|
2014
2475
|
outputMode: effectiveOutputMode,
|
|
@@ -2023,7 +2484,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
2023
2484
|
}
|
|
2024
2485
|
}
|
|
2025
2486
|
|
|
2026
|
-
if (params.
|
|
2487
|
+
if (shouldForkAgent(contextPolicy, params.agent!)) {
|
|
2027
2488
|
task = wrapForkTask(task);
|
|
2028
2489
|
}
|
|
2029
2490
|
const cleanTask = task;
|
|
@@ -2076,7 +2537,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
2076
2537
|
}
|
|
2077
2538
|
: undefined;
|
|
2078
2539
|
|
|
2540
|
+
const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined);
|
|
2079
2541
|
const r = await runSync(ctx.cwd, agents, params.agent!, task, {
|
|
2542
|
+
parentSessionId: ctx.sessionManager.getSessionId() ?? undefined,
|
|
2080
2543
|
cwd: effectiveCwd,
|
|
2081
2544
|
signal,
|
|
2082
2545
|
interruptSignal: interruptController.signal,
|
|
@@ -2084,7 +2547,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
2084
2547
|
intercomEvents: deps.pi.events,
|
|
2085
2548
|
runId,
|
|
2086
2549
|
sessionDir: sessionDirForIndex(0),
|
|
2087
|
-
sessionFile:
|
|
2550
|
+
sessionFile: sessionFileForTask(params.agent!, 0),
|
|
2088
2551
|
share: shareEnabled,
|
|
2089
2552
|
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
2090
2553
|
artifactConfig,
|
|
@@ -2105,6 +2568,8 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
2105
2568
|
skills: effectiveSkills,
|
|
2106
2569
|
acceptance: params.acceptance,
|
|
2107
2570
|
acceptanceContext: { mode: "single" },
|
|
2571
|
+
timeoutMs: data.timeoutMs,
|
|
2572
|
+
deadlineAt,
|
|
2108
2573
|
});
|
|
2109
2574
|
if (foregroundControl?.currentIndex === 0) {
|
|
2110
2575
|
foregroundControl.interrupt = undefined;
|
|
@@ -2189,6 +2654,23 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
2189
2654
|
};
|
|
2190
2655
|
}
|
|
2191
2656
|
|
|
2657
|
+
function inferExecutionMode(params: SubagentParamsLike): SubagentRunMode {
|
|
2658
|
+
if ((params.chain?.length ?? 0) > 0) return "chain";
|
|
2659
|
+
if ((params.tasks?.length ?? 0) > 0) return "parallel";
|
|
2660
|
+
return "single";
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResult<Details> {
|
|
2664
|
+
return {
|
|
2665
|
+
content: [{
|
|
2666
|
+
type: "text",
|
|
2667
|
+
text: "Rejected: a subagent call is already in progress. Issue exactly ONE subagent call per turn.",
|
|
2668
|
+
}],
|
|
2669
|
+
isError: true,
|
|
2670
|
+
details: { mode: inferExecutionMode(params), results: [] },
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2192
2674
|
export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
2193
2675
|
execute: (
|
|
2194
2676
|
id: string,
|
|
@@ -2225,7 +2707,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2225
2707
|
let orchestratorTarget: string | undefined;
|
|
2226
2708
|
try {
|
|
2227
2709
|
orchestratorTarget = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
|
|
2228
|
-
} catch {
|
|
2710
|
+
} catch (error) {
|
|
2711
|
+
if (!sessionError) sessionError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
2712
|
+
}
|
|
2229
2713
|
return {
|
|
2230
2714
|
content: [{
|
|
2231
2715
|
type: "text",
|
|
@@ -2268,6 +2752,9 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2268
2752
|
if (params.action === "resume") {
|
|
2269
2753
|
return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
|
|
2270
2754
|
}
|
|
2755
|
+
if (params.action === "append-step") {
|
|
2756
|
+
return appendStepToAsyncChain({ params: paramsWithResolvedCwd, requestCwd, ctx, deps });
|
|
2757
|
+
}
|
|
2271
2758
|
if (params.action === "interrupt") {
|
|
2272
2759
|
const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
|
|
2273
2760
|
let resolved: ResolvedSubagentRunId | undefined;
|
|
@@ -2297,7 +2784,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2297
2784
|
details: { mode: "management", results: [] },
|
|
2298
2785
|
};
|
|
2299
2786
|
}
|
|
2300
|
-
const asyncInterruptResult = interruptAsyncRun(deps.state, resolved?.kind === "async" ? resolved.id : targetRunId);
|
|
2787
|
+
const asyncInterruptResult = interruptAsyncRun(deps.state, resolved?.kind === "async" ? resolved.id : targetRunId, deps.kill);
|
|
2301
2788
|
if (asyncInterruptResult) return asyncInterruptResult;
|
|
2302
2789
|
return {
|
|
2303
2790
|
content: [{ type: "text", text: "No interrupt-capable run found in this session." }],
|
|
@@ -2319,7 +2806,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2319
2806
|
details: { mode: "management" as const, results: [] },
|
|
2320
2807
|
};
|
|
2321
2808
|
}
|
|
2322
|
-
return handleManagementAction(params.action, paramsWithResolvedCwd, { ...ctx, cwd: requestCwd });
|
|
2809
|
+
return handleManagementAction(params.action, paramsWithResolvedCwd, { ...ctx, cwd: requestCwd, config: deps.config });
|
|
2323
2810
|
}
|
|
2324
2811
|
|
|
2325
2812
|
const { blocked, depth, maxDepth } = checkSubagentDepth(deps.config.maxSubagentDepth);
|
|
@@ -2348,13 +2835,16 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2348
2835
|
depth,
|
|
2349
2836
|
deps.config.forceTopLevelAsync === true,
|
|
2350
2837
|
);
|
|
2838
|
+
const foregroundTimeout = resolveForegroundTimeout(effectiveParams);
|
|
2839
|
+
if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error);
|
|
2351
2840
|
|
|
2352
2841
|
const scope: AgentScope = resolveExecutionAgentScope(effectiveParams.agentScope);
|
|
2353
2842
|
const effectiveCwd = effectiveParams.cwd ?? ctx.cwd;
|
|
2354
2843
|
const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
2355
2844
|
deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
2356
2845
|
const discoveredAgents = deps.discoverAgents(effectiveCwd, scope).agents;
|
|
2357
|
-
|
|
2846
|
+
const contextPolicy = resolveAgentDefaultContextPolicy(effectiveParams, discoveredAgents);
|
|
2847
|
+
effectiveParams = contextPolicy.params;
|
|
2358
2848
|
const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
|
|
2359
2849
|
const intercomBridge = resolveIntercomBridge({
|
|
2360
2850
|
config: deps.config.intercomBridge,
|
|
@@ -2388,15 +2878,18 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2388
2878
|
);
|
|
2389
2879
|
if (validationError) return validationError;
|
|
2390
2880
|
|
|
2391
|
-
let
|
|
2881
|
+
let forkSessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
|
|
2392
2882
|
try {
|
|
2393
|
-
|
|
2883
|
+
forkSessionFileForIndex = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined).sessionFileForIndex;
|
|
2394
2884
|
} catch (error) {
|
|
2395
2885
|
return toExecutionErrorResult(effectiveParams, error);
|
|
2396
2886
|
}
|
|
2397
2887
|
const requestedAsync = effectiveParams.async ?? deps.asyncByDefault;
|
|
2398
2888
|
const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true;
|
|
2399
2889
|
const effectiveAsync = requestedAsync && effectiveParams.clarify !== true;
|
|
2890
|
+
if (foregroundTimeout.timeoutMs !== undefined && effectiveAsync) {
|
|
2891
|
+
return buildRequestedModeError(effectiveParams, "timeoutMs/maxRuntimeMs are only supported for foreground runs; set async: false or omit the timeout for background runs.");
|
|
2892
|
+
}
|
|
2400
2893
|
const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
|
|
2401
2894
|
|
|
2402
2895
|
const artifactConfig: ArtifactConfig = {
|
|
@@ -2425,8 +2918,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2425
2918
|
}
|
|
2426
2919
|
const sessionDirForIndex = (idx?: number) =>
|
|
2427
2920
|
path.join(sessionRoot, `run-${idx ?? 0}`);
|
|
2921
|
+
const forkSessionFileForTask = (agentName: string, idx?: number) =>
|
|
2922
|
+
shouldForkAgent(contextPolicy, agentName) ? forkSessionFileForIndex(idx) : undefined;
|
|
2923
|
+
const childSessionFileForTask = (agentName: string, idx?: number) =>
|
|
2924
|
+
forkSessionFileForTask(agentName, idx) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
|
|
2428
2925
|
const childSessionFileForIndex = (idx?: number) =>
|
|
2429
|
-
|
|
2926
|
+
path.join(sessionDirForIndex(idx), "session.jsonl");
|
|
2927
|
+
try {
|
|
2928
|
+
preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask);
|
|
2929
|
+
} catch (error) {
|
|
2930
|
+
return toExecutionErrorResult(effectiveParams, error);
|
|
2931
|
+
}
|
|
2932
|
+
const chainBindingsError = validateExecutionChainBindings(effectiveParams, deps.config.chain?.dynamicFanout?.maxItems);
|
|
2933
|
+
if (chainBindingsError) return chainBindingsError;
|
|
2430
2934
|
|
|
2431
2935
|
const onUpdateWithContext = onUpdate
|
|
2432
2936
|
? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r, effectiveParams.context))
|
|
@@ -2444,6 +2948,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2444
2948
|
sessionRoot,
|
|
2445
2949
|
sessionDirForIndex,
|
|
2446
2950
|
sessionFileForIndex: childSessionFileForIndex,
|
|
2951
|
+
sessionFileForTask: childSessionFileForTask,
|
|
2447
2952
|
artifactConfig,
|
|
2448
2953
|
artifactsDir,
|
|
2449
2954
|
backgroundRequestedWhileClarifying,
|
|
@@ -2451,6 +2956,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2451
2956
|
controlConfig,
|
|
2452
2957
|
intercomBridge,
|
|
2453
2958
|
nestedRoute,
|
|
2959
|
+
timeoutMs: foregroundTimeout.timeoutMs,
|
|
2960
|
+
contextPolicy,
|
|
2454
2961
|
};
|
|
2455
2962
|
|
|
2456
2963
|
const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
|
|
@@ -2575,5 +3082,22 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
2575
3082
|
}, effectiveParams.context);
|
|
2576
3083
|
};
|
|
2577
3084
|
|
|
2578
|
-
|
|
3085
|
+
const executeWithSingleDispatchGuard = async (
|
|
3086
|
+
id: string,
|
|
3087
|
+
params: SubagentParamsLike,
|
|
3088
|
+
signal: AbortSignal,
|
|
3089
|
+
onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
|
|
3090
|
+
ctx: ExtensionContext,
|
|
3091
|
+
): Promise<AgentToolResult<Details>> => {
|
|
3092
|
+
if (params.action) return execute(id, params, signal, onUpdate, ctx);
|
|
3093
|
+
if (deps.state.subagentInProgress === true) return duplicateSubagentCallResult(params);
|
|
3094
|
+
deps.state.subagentInProgress = true;
|
|
3095
|
+
try {
|
|
3096
|
+
return await execute(id, params, signal, onUpdate, ctx);
|
|
3097
|
+
} finally {
|
|
3098
|
+
deps.state.subagentInProgress = false;
|
|
3099
|
+
}
|
|
3100
|
+
};
|
|
3101
|
+
|
|
3102
|
+
return { execute: executeWithSingleDispatchGuard };
|
|
2579
3103
|
}
|