pi-subagents 0.17.4 → 0.18.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 +28 -0
- package/README.md +19 -19
- package/agents/context-builder.md +1 -1
- package/agents/oracle-executor.md +1 -1
- package/agents/oracle.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/worker.md +1 -1
- package/async-execution.ts +29 -2
- package/async-job-tracker.ts +74 -7
- package/async-status.ts +74 -17
- package/chain-execution.ts +162 -26
- package/execution.ts +122 -4
- package/index.ts +124 -128
- package/install.mjs +2 -3
- package/intercom-bridge.ts +9 -0
- package/notify.ts +25 -6
- package/package.json +3 -6
- package/pi-args.ts +4 -0
- package/pi-spawn.ts +9 -6
- package/render.ts +20 -12
- package/result-watcher.ts +3 -5
- package/run-status.ts +134 -0
- package/schemas.ts +22 -7
- package/skills/pi-subagents/SKILL.md +50 -10
- package/subagent-control.ts +148 -0
- package/subagent-executor.ts +348 -6
- package/subagent-prompt-runtime.ts +6 -0
- package/subagent-runner.ts +218 -25
- package/subagents-status.ts +8 -1
- package/types.ts +74 -2
- package/utils.ts +1 -0
package/subagent-executor.ts
CHANGED
|
@@ -23,9 +23,11 @@ import {
|
|
|
23
23
|
import { discoverAvailableSkills, normalizeSkillInput } from "./skills.ts";
|
|
24
24
|
import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async-execution.ts";
|
|
25
25
|
import { createForkContextResolver } from "./fork-context.ts";
|
|
26
|
-
import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget } from "./intercom-bridge.ts";
|
|
26
|
+
import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "./intercom-bridge.ts";
|
|
27
|
+
import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "./subagent-control.ts";
|
|
27
28
|
import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
|
|
28
|
-
import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd } from "./utils.ts";
|
|
29
|
+
import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd } from "./utils.ts";
|
|
30
|
+
import { inspectSubagentStatus } from "./run-status.ts";
|
|
29
31
|
import { applyForceTopLevelAsyncOverride } from "./top-level-async.ts";
|
|
30
32
|
import {
|
|
31
33
|
cleanupWorktrees,
|
|
@@ -40,12 +42,17 @@ import {
|
|
|
40
42
|
type AgentProgress,
|
|
41
43
|
type ArtifactConfig,
|
|
42
44
|
type ArtifactPaths,
|
|
45
|
+
type ControlConfig,
|
|
46
|
+
type ControlEvent,
|
|
43
47
|
type Details,
|
|
44
48
|
type ExtensionConfig,
|
|
45
49
|
type MaxOutputConfig,
|
|
50
|
+
type ResolvedControlConfig,
|
|
46
51
|
type SingleResult,
|
|
47
52
|
type SubagentState,
|
|
48
53
|
DEFAULT_ARTIFACT_CONFIG,
|
|
54
|
+
SUBAGENT_CONTROL_EVENT,
|
|
55
|
+
SUBAGENT_CONTROL_INTERCOM_EVENT,
|
|
49
56
|
checkSubagentDepth,
|
|
50
57
|
resolveTopLevelParallelConcurrency,
|
|
51
58
|
resolveTopLevelParallelMaxTasks,
|
|
@@ -54,6 +61,8 @@ import {
|
|
|
54
61
|
wrapForkTask,
|
|
55
62
|
} from "./types.ts";
|
|
56
63
|
|
|
64
|
+
const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
|
|
65
|
+
|
|
57
66
|
interface TaskParam {
|
|
58
67
|
agent: string;
|
|
59
68
|
task: string;
|
|
@@ -65,6 +74,9 @@ interface TaskParam {
|
|
|
65
74
|
|
|
66
75
|
export interface SubagentParamsLike {
|
|
67
76
|
action?: string;
|
|
77
|
+
id?: string;
|
|
78
|
+
runId?: string;
|
|
79
|
+
dir?: string;
|
|
68
80
|
agent?: string;
|
|
69
81
|
task?: string;
|
|
70
82
|
chain?: ChainStep[];
|
|
@@ -75,6 +87,7 @@ export interface SubagentParamsLike {
|
|
|
75
87
|
async?: boolean;
|
|
76
88
|
clarify?: boolean;
|
|
77
89
|
share?: boolean;
|
|
90
|
+
control?: ControlConfig;
|
|
78
91
|
sessionDir?: string;
|
|
79
92
|
cwd?: string;
|
|
80
93
|
maxOutput?: MaxOutputConfig;
|
|
@@ -114,12 +127,131 @@ interface ExecutionContextData {
|
|
|
114
127
|
artifactsDir: string;
|
|
115
128
|
backgroundRequestedWhileClarifying: boolean;
|
|
116
129
|
effectiveAsync: boolean;
|
|
130
|
+
controlConfig: ResolvedControlConfig;
|
|
131
|
+
intercomBridge: IntercomBridgeState;
|
|
117
132
|
}
|
|
118
133
|
|
|
119
134
|
function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string {
|
|
120
135
|
return requestedCwd ? path.resolve(runtimeCwd, requestedCwd) : runtimeCwd;
|
|
121
136
|
}
|
|
122
137
|
|
|
138
|
+
function getForegroundControl(state: SubagentState, runId: string | undefined) {
|
|
139
|
+
if (runId) return state.foregroundControls.get(runId);
|
|
140
|
+
if (state.lastForegroundControlId) {
|
|
141
|
+
const latest = state.foregroundControls.get(state.lastForegroundControlId);
|
|
142
|
+
if (latest) return latest;
|
|
143
|
+
}
|
|
144
|
+
let newest: (SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never) | undefined;
|
|
145
|
+
for (const control of state.foregroundControls.values()) {
|
|
146
|
+
if (!newest || control.updatedAt > newest.updatedAt) newest = control;
|
|
147
|
+
}
|
|
148
|
+
return newest;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function formatForegroundActivity(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): string | undefined {
|
|
152
|
+
if (control.currentTool && control.currentToolStartedAt) {
|
|
153
|
+
return `tool ${control.currentTool} for ${Math.floor(Math.max(0, Date.now() - control.currentToolStartedAt) / 1000)}s`;
|
|
154
|
+
}
|
|
155
|
+
if (!control.lastActivityAt) return control.currentActivityState === "needs_attention" ? "needs attention" : undefined;
|
|
156
|
+
const seconds = Math.floor(Math.max(0, Date.now() - control.lastActivityAt) / 1000);
|
|
157
|
+
return control.currentActivityState === "needs_attention" ? `no activity for ${seconds}s` : `active ${seconds}s ago`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function foregroundStatusResult(control: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never): AgentToolResult<Details> {
|
|
161
|
+
const lines = [
|
|
162
|
+
`Run: ${control.runId}`,
|
|
163
|
+
"State: running",
|
|
164
|
+
`Mode: ${control.mode}`,
|
|
165
|
+
control.currentAgent ? `Current: ${control.currentAgent}${control.currentIndex !== undefined ? ` step ${control.currentIndex + 1}` : ""}` : undefined,
|
|
166
|
+
formatForegroundActivity(control) ? `Activity: ${formatForegroundActivity(control)}` : undefined,
|
|
167
|
+
].filter((line): line is string => Boolean(line));
|
|
168
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "management", results: [] } };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getAsyncInterruptTarget(state: SubagentState, runId: string | undefined): { asyncId: string; asyncDir: string } | undefined {
|
|
172
|
+
if (runId) {
|
|
173
|
+
const direct = state.asyncJobs.get(runId);
|
|
174
|
+
if (direct) return { asyncId: direct.asyncId, asyncDir: direct.asyncDir };
|
|
175
|
+
}
|
|
176
|
+
let newest: { asyncId: string; asyncDir: string; updatedAt: number } | undefined;
|
|
177
|
+
for (const job of state.asyncJobs.values()) {
|
|
178
|
+
if (job.status !== "running") continue;
|
|
179
|
+
if (!newest || (job.updatedAt ?? 0) > newest.updatedAt) {
|
|
180
|
+
newest = { asyncId: job.asyncId, asyncDir: job.asyncDir, updatedAt: job.updatedAt ?? 0 };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return newest ? { asyncId: newest.asyncId, asyncDir: newest.asyncDir } : undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function emitControlNotification(input: {
|
|
187
|
+
pi: ExtensionAPI;
|
|
188
|
+
controlConfig: ResolvedControlConfig;
|
|
189
|
+
intercomBridge: IntercomBridgeState;
|
|
190
|
+
event: ControlEvent;
|
|
191
|
+
}): void {
|
|
192
|
+
if (!shouldNotifyControlEvent(input.controlConfig, input.event)) return;
|
|
193
|
+
const childIntercomTarget = input.intercomBridge.active
|
|
194
|
+
? resolveSubagentIntercomTarget(input.event.runId, input.event.agent, input.event.index)
|
|
195
|
+
: undefined;
|
|
196
|
+
const payload = {
|
|
197
|
+
event: input.event,
|
|
198
|
+
source: "foreground" as const,
|
|
199
|
+
childIntercomTarget,
|
|
200
|
+
noticeText: formatControlNoticeMessage(input.event, childIntercomTarget),
|
|
201
|
+
};
|
|
202
|
+
if (input.controlConfig.notifyChannels.includes("event")) {
|
|
203
|
+
input.pi.events.emit(SUBAGENT_CONTROL_EVENT, payload);
|
|
204
|
+
}
|
|
205
|
+
if (input.controlConfig.notifyChannels.includes("intercom") && input.intercomBridge.active && input.intercomBridge.orchestratorTarget) {
|
|
206
|
+
input.pi.events.emit(SUBAGENT_CONTROL_INTERCOM_EVENT, {
|
|
207
|
+
...payload,
|
|
208
|
+
to: input.intercomBridge.orchestratorTarget,
|
|
209
|
+
message: formatControlIntercomMessage(input.event, childIntercomTarget),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function interruptAsyncRun(state: SubagentState, runId: string | undefined): AgentToolResult<Details> | null {
|
|
215
|
+
const target = getAsyncInterruptTarget(state, runId);
|
|
216
|
+
if (!target) return null;
|
|
217
|
+
const status = readStatus(target.asyncDir);
|
|
218
|
+
if (!status || status.state !== "running" || typeof status.pid !== "number") {
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: "text", text: `No running async run with an interrupt-capable pid was found for '${runId ?? "current"}'.` }],
|
|
221
|
+
isError: true,
|
|
222
|
+
details: { mode: "management", results: [] },
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
process.kill(status.pid, ASYNC_INTERRUPT_SIGNAL);
|
|
227
|
+
const tracked = state.asyncJobs.get(target.asyncId);
|
|
228
|
+
if (tracked) {
|
|
229
|
+
tracked.activityState = undefined;
|
|
230
|
+
tracked.updatedAt = Date.now();
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
content: [{ type: "text", text: `Interrupt requested for async run ${target.asyncId}.` }],
|
|
234
|
+
details: { mode: "management", results: [] },
|
|
235
|
+
};
|
|
236
|
+
} catch (error) {
|
|
237
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: "text", text: `Failed to interrupt async run ${target.asyncId}: ${message}` }],
|
|
240
|
+
isError: true,
|
|
241
|
+
details: { mode: "management", results: [] },
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function createForegroundControlNotifier(data: Pick<ExecutionContextData, "controlConfig" | "intercomBridge">, deps: Pick<ExecutorDeps, "pi">): (event: ControlEvent) => void {
|
|
247
|
+
return (event) => emitControlNotification({
|
|
248
|
+
pi: deps.pi,
|
|
249
|
+
controlConfig: data.controlConfig,
|
|
250
|
+
intercomBridge: data.intercomBridge,
|
|
251
|
+
event,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
123
255
|
function validateExecutionInput(
|
|
124
256
|
params: SubagentParamsLike,
|
|
125
257
|
agents: AgentConfig[],
|
|
@@ -346,6 +478,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
346
478
|
artifactConfig,
|
|
347
479
|
artifactsDir,
|
|
348
480
|
effectiveAsync,
|
|
481
|
+
controlConfig,
|
|
482
|
+
intercomBridge,
|
|
349
483
|
} = data;
|
|
350
484
|
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
351
485
|
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
@@ -395,6 +529,8 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
395
529
|
}));
|
|
396
530
|
const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
|
|
397
531
|
const currentProvider = ctx.model?.provider;
|
|
532
|
+
const controlIntercomTarget = intercomBridge.active ? intercomBridge.orchestratorTarget : undefined;
|
|
533
|
+
const childIntercomTarget = intercomBridge.active ? (agent: string, index: number) => resolveSubagentIntercomTarget(id, agent, index) : undefined;
|
|
398
534
|
|
|
399
535
|
if (hasTasks && params.tasks) {
|
|
400
536
|
const agentConfigs = params.tasks.map((task) => agents.find((agent) => agent.name === task.agent));
|
|
@@ -429,6 +565,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
429
565
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
430
566
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
431
567
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
568
|
+
controlConfig,
|
|
569
|
+
controlIntercomTarget,
|
|
570
|
+
childIntercomTarget,
|
|
432
571
|
});
|
|
433
572
|
}
|
|
434
573
|
|
|
@@ -452,6 +591,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
452
591
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
453
592
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
454
593
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
594
|
+
controlConfig,
|
|
595
|
+
controlIntercomTarget,
|
|
596
|
+
childIntercomTarget,
|
|
455
597
|
});
|
|
456
598
|
}
|
|
457
599
|
|
|
@@ -489,6 +631,9 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
489
631
|
maxSubagentDepth,
|
|
490
632
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
491
633
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
634
|
+
controlConfig,
|
|
635
|
+
controlIntercomTarget,
|
|
636
|
+
childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined,
|
|
492
637
|
});
|
|
493
638
|
}
|
|
494
639
|
|
|
@@ -510,7 +655,11 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
510
655
|
artifactConfig,
|
|
511
656
|
onUpdate,
|
|
512
657
|
sessionRoot,
|
|
658
|
+
controlConfig,
|
|
513
659
|
} = data;
|
|
660
|
+
const onControlEvent = createForegroundControlNotifier(data, deps);
|
|
661
|
+
const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
|
|
662
|
+
const foregroundControl = deps.state.foregroundControls.get(runId);
|
|
514
663
|
const normalized = normalizeSkillInput(params.skill);
|
|
515
664
|
const chainSkills = normalized === false ? [] : (normalized ?? []);
|
|
516
665
|
const chain = wrapChainTasksForFork(params.chain as ChainStep[], params.context);
|
|
@@ -531,6 +680,10 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
531
680
|
includeProgress: params.includeProgress,
|
|
532
681
|
clarify: params.clarify,
|
|
533
682
|
onUpdate,
|
|
683
|
+
onControlEvent,
|
|
684
|
+
controlConfig,
|
|
685
|
+
childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
|
|
686
|
+
foregroundControl,
|
|
534
687
|
chainSkills,
|
|
535
688
|
chainDir: params.chainDir,
|
|
536
689
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
@@ -574,6 +727,9 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
|
|
|
574
727
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
575
728
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
576
729
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
730
|
+
controlConfig,
|
|
731
|
+
controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
|
|
732
|
+
childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
|
|
577
733
|
});
|
|
578
734
|
}
|
|
579
735
|
|
|
@@ -599,6 +755,10 @@ interface ForegroundParallelRunInput {
|
|
|
599
755
|
modelOverrides: (string | undefined)[];
|
|
600
756
|
skillOverrides: (string[] | false | undefined)[];
|
|
601
757
|
behaviors: Array<ReturnType<typeof resolveStepBehavior>>;
|
|
758
|
+
controlConfig: ResolvedControlConfig;
|
|
759
|
+
onControlEvent?: (event: ControlEvent) => void;
|
|
760
|
+
childIntercomTarget?: (agent: string, index: number) => string | undefined;
|
|
761
|
+
foregroundControl?: SubagentState["foregroundControls"] extends Map<string, infer T> ? T : never;
|
|
602
762
|
concurrencyLimit: number;
|
|
603
763
|
liveResults: (SingleResult | undefined)[];
|
|
604
764
|
liveProgress: (AgentProgress | undefined)[];
|
|
@@ -687,9 +847,24 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
687
847
|
const overrideSkills = input.skillOverrides[index];
|
|
688
848
|
const effectiveSkills = overrideSkills === undefined ? input.behaviors[index]?.skills : overrideSkills;
|
|
689
849
|
const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index);
|
|
850
|
+
const interruptController = new AbortController();
|
|
851
|
+
if (input.foregroundControl) {
|
|
852
|
+
input.foregroundControl.currentAgent = task.agent;
|
|
853
|
+
input.foregroundControl.currentIndex = index;
|
|
854
|
+
input.foregroundControl.currentActivityState = undefined;
|
|
855
|
+
input.foregroundControl.updatedAt = Date.now();
|
|
856
|
+
input.foregroundControl.interrupt = () => {
|
|
857
|
+
if (interruptController.signal.aborted) return false;
|
|
858
|
+
interruptController.abort();
|
|
859
|
+
input.foregroundControl!.currentActivityState = undefined;
|
|
860
|
+
input.foregroundControl!.updatedAt = Date.now();
|
|
861
|
+
return true;
|
|
862
|
+
};
|
|
863
|
+
}
|
|
690
864
|
return runSync(input.ctx.cwd, input.agents, task.agent, input.taskTexts[index]!, {
|
|
691
865
|
cwd: taskCwd,
|
|
692
866
|
signal: input.signal,
|
|
867
|
+
interruptSignal: interruptController.signal,
|
|
693
868
|
runId: input.runId,
|
|
694
869
|
index,
|
|
695
870
|
sessionDir: input.sessionDirForIndex(index),
|
|
@@ -699,14 +874,27 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
699
874
|
artifactConfig: input.artifactConfig,
|
|
700
875
|
maxOutput: input.maxOutput,
|
|
701
876
|
maxSubagentDepth: input.maxSubagentDepths[index],
|
|
877
|
+
controlConfig: input.controlConfig,
|
|
878
|
+
onControlEvent: input.onControlEvent,
|
|
879
|
+
intercomSessionName: input.childIntercomTarget?.(task.agent, index),
|
|
702
880
|
modelOverride: input.modelOverrides[index],
|
|
703
881
|
availableModels: input.availableModels,
|
|
704
882
|
preferredModelProvider: input.ctx.model?.provider,
|
|
705
883
|
skills: effectiveSkills === false ? [] : effectiveSkills,
|
|
706
|
-
|
|
707
|
-
|
|
884
|
+
onUpdate: input.onUpdate
|
|
885
|
+
? (progressUpdate) => {
|
|
708
886
|
const stepResults = progressUpdate.details?.results || [];
|
|
709
887
|
const stepProgress = progressUpdate.details?.progress || [];
|
|
888
|
+
if (input.foregroundControl && stepProgress.length > 0) {
|
|
889
|
+
const current = stepProgress[0];
|
|
890
|
+
input.foregroundControl.currentAgent = task.agent;
|
|
891
|
+
input.foregroundControl.currentIndex = index;
|
|
892
|
+
input.foregroundControl.currentActivityState = current?.activityState;
|
|
893
|
+
input.foregroundControl.lastActivityAt = current?.lastActivityAt;
|
|
894
|
+
input.foregroundControl.currentTool = current?.currentTool;
|
|
895
|
+
input.foregroundControl.currentToolStartedAt = current?.currentToolStartedAt;
|
|
896
|
+
input.foregroundControl.updatedAt = Date.now();
|
|
897
|
+
}
|
|
710
898
|
if (stepResults.length > 0) input.liveResults[index] = stepResults[0];
|
|
711
899
|
if (stepProgress.length > 0) input.liveProgress[index] = stepProgress[0];
|
|
712
900
|
const mergedResults = input.liveResults.filter((result): result is SingleResult => result !== undefined);
|
|
@@ -717,11 +905,17 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
717
905
|
mode: "parallel",
|
|
718
906
|
results: mergedResults,
|
|
719
907
|
progress: mergedProgress,
|
|
908
|
+
controlEvents: progressUpdate.details?.controlEvents,
|
|
720
909
|
totalSteps: input.tasks.length,
|
|
721
910
|
},
|
|
722
911
|
});
|
|
723
912
|
}
|
|
724
913
|
: undefined,
|
|
914
|
+
}).finally(() => {
|
|
915
|
+
if (input.foregroundControl?.currentIndex === index) {
|
|
916
|
+
input.foregroundControl.interrupt = undefined;
|
|
917
|
+
input.foregroundControl.updatedAt = Date.now();
|
|
918
|
+
}
|
|
725
919
|
});
|
|
726
920
|
});
|
|
727
921
|
}
|
|
@@ -742,7 +936,10 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
742
936
|
backgroundRequestedWhileClarifying,
|
|
743
937
|
onUpdate,
|
|
744
938
|
sessionRoot,
|
|
939
|
+
controlConfig,
|
|
745
940
|
} = data;
|
|
941
|
+
const onControlEvent = createForegroundControlNotifier(data, deps);
|
|
942
|
+
const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined;
|
|
746
943
|
const allProgress: AgentProgress[] = [];
|
|
747
944
|
const allArtifactPaths: ArtifactPaths[] = [];
|
|
748
945
|
const tasks = params.tasks!;
|
|
@@ -866,6 +1063,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
866
1063
|
maxSubagentDepth: currentMaxSubagentDepth,
|
|
867
1064
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
868
1065
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
1066
|
+
controlConfig,
|
|
1067
|
+
controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
|
|
1068
|
+
childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
|
|
869
1069
|
});
|
|
870
1070
|
}
|
|
871
1071
|
}
|
|
@@ -873,6 +1073,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
873
1073
|
const behaviors = agentConfigs.map((config) => resolveStepBehavior(config, {}));
|
|
874
1074
|
const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined);
|
|
875
1075
|
const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined);
|
|
1076
|
+
const foregroundControl = deps.state.foregroundControls.get(runId);
|
|
876
1077
|
const { setup: worktreeSetup, errorResult } = createParallelWorktreeSetup(
|
|
877
1078
|
params.worktree,
|
|
878
1079
|
effectiveCwd,
|
|
@@ -908,6 +1109,10 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
908
1109
|
modelOverrides,
|
|
909
1110
|
skillOverrides,
|
|
910
1111
|
behaviors,
|
|
1112
|
+
controlConfig,
|
|
1113
|
+
onControlEvent,
|
|
1114
|
+
childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined,
|
|
1115
|
+
foregroundControl,
|
|
911
1116
|
concurrencyLimit: parallelConcurrency,
|
|
912
1117
|
maxSubagentDepths,
|
|
913
1118
|
liveResults,
|
|
@@ -925,6 +1130,19 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
925
1130
|
if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
|
|
926
1131
|
}
|
|
927
1132
|
|
|
1133
|
+
const interrupted = results.find((result) => result.interrupted);
|
|
1134
|
+
if (interrupted) {
|
|
1135
|
+
return {
|
|
1136
|
+
content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
|
|
1137
|
+
details: compactForegroundDetails({
|
|
1138
|
+
mode: "parallel",
|
|
1139
|
+
results,
|
|
1140
|
+
progress: params.includeProgress ? allProgress : undefined,
|
|
1141
|
+
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1142
|
+
}),
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
928
1146
|
const worktreeSuffix = buildParallelWorktreeSuffix(worktreeSetup, artifactsDir, tasks);
|
|
929
1147
|
const ok = results.filter((result) => result.exitCode === 0).length;
|
|
930
1148
|
const downgradeNote = backgroundRequestedWhileClarifying ? " (background requested, but clarify kept this run foreground)" : "";
|
|
@@ -972,7 +1190,10 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
972
1190
|
artifactsDir,
|
|
973
1191
|
onUpdate,
|
|
974
1192
|
sessionRoot,
|
|
1193
|
+
controlConfig,
|
|
975
1194
|
} = data;
|
|
1195
|
+
const onControlEvent = createForegroundControlNotifier(data, deps);
|
|
1196
|
+
const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget(runId, params.agent!, undefined) : undefined;
|
|
976
1197
|
const allProgress: AgentProgress[] = [];
|
|
977
1198
|
const allArtifactPaths: ArtifactPaths[] = [];
|
|
978
1199
|
const agentConfig = agents.find((a) => a.name === params.agent);
|
|
@@ -1068,6 +1289,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1068
1289
|
maxSubagentDepth,
|
|
1069
1290
|
worktreeSetupHook: deps.config.worktreeSetupHook,
|
|
1070
1291
|
worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
|
|
1292
|
+
controlConfig,
|
|
1293
|
+
controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined,
|
|
1294
|
+
childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined,
|
|
1071
1295
|
});
|
|
1072
1296
|
}
|
|
1073
1297
|
}
|
|
@@ -1085,10 +1309,42 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1085
1309
|
} else {
|
|
1086
1310
|
effectiveSkills = skillOverride;
|
|
1087
1311
|
}
|
|
1312
|
+
const interruptController = new AbortController();
|
|
1313
|
+
const foregroundControl = deps.state.foregroundControls.get(runId);
|
|
1314
|
+
if (foregroundControl) {
|
|
1315
|
+
foregroundControl.currentAgent = params.agent;
|
|
1316
|
+
foregroundControl.currentIndex = 0;
|
|
1317
|
+
foregroundControl.currentActivityState = undefined;
|
|
1318
|
+
foregroundControl.updatedAt = Date.now();
|
|
1319
|
+
foregroundControl.interrupt = () => {
|
|
1320
|
+
if (interruptController.signal.aborted) return false;
|
|
1321
|
+
interruptController.abort();
|
|
1322
|
+
foregroundControl.currentActivityState = undefined;
|
|
1323
|
+
foregroundControl.updatedAt = Date.now();
|
|
1324
|
+
return true;
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const forwardSingleUpdate = onUpdate
|
|
1329
|
+
? (update: AgentToolResult<Details>) => {
|
|
1330
|
+
if (foregroundControl) {
|
|
1331
|
+
const firstProgress = update.details?.progress?.[0];
|
|
1332
|
+
foregroundControl.currentAgent = params.agent;
|
|
1333
|
+
foregroundControl.currentIndex = firstProgress?.index ?? 0;
|
|
1334
|
+
foregroundControl.currentActivityState = firstProgress?.activityState;
|
|
1335
|
+
foregroundControl.lastActivityAt = firstProgress?.lastActivityAt;
|
|
1336
|
+
foregroundControl.currentTool = firstProgress?.currentTool;
|
|
1337
|
+
foregroundControl.currentToolStartedAt = firstProgress?.currentToolStartedAt;
|
|
1338
|
+
foregroundControl.updatedAt = Date.now();
|
|
1339
|
+
}
|
|
1340
|
+
onUpdate(update);
|
|
1341
|
+
}
|
|
1342
|
+
: undefined;
|
|
1088
1343
|
|
|
1089
1344
|
const r = await runSync(ctx.cwd, agents, params.agent!, task, {
|
|
1090
1345
|
cwd: effectiveCwd,
|
|
1091
1346
|
signal,
|
|
1347
|
+
interruptSignal: interruptController.signal,
|
|
1092
1348
|
allowIntercomDetach: agentConfig.systemPrompt?.includes("Intercom orchestration channel:") === true,
|
|
1093
1349
|
intercomEvents: deps.pi.events,
|
|
1094
1350
|
runId,
|
|
@@ -1100,12 +1356,23 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1100
1356
|
maxOutput: params.maxOutput,
|
|
1101
1357
|
outputPath,
|
|
1102
1358
|
maxSubagentDepth,
|
|
1103
|
-
onUpdate,
|
|
1359
|
+
onUpdate: forwardSingleUpdate,
|
|
1360
|
+
controlConfig,
|
|
1361
|
+
onControlEvent,
|
|
1362
|
+
intercomSessionName: childIntercomTarget,
|
|
1104
1363
|
modelOverride,
|
|
1105
1364
|
availableModels,
|
|
1106
1365
|
preferredModelProvider: currentProvider,
|
|
1107
1366
|
skills: effectiveSkills,
|
|
1108
1367
|
});
|
|
1368
|
+
if (foregroundControl?.currentIndex === 0) {
|
|
1369
|
+
foregroundControl.interrupt = undefined;
|
|
1370
|
+
foregroundControl.currentActivityState = r.progress?.activityState;
|
|
1371
|
+
foregroundControl.lastActivityAt = r.progress?.lastActivityAt;
|
|
1372
|
+
foregroundControl.currentTool = r.progress?.currentTool;
|
|
1373
|
+
foregroundControl.currentToolStartedAt = r.progress?.currentToolStartedAt;
|
|
1374
|
+
foregroundControl.updatedAt = Date.now();
|
|
1375
|
+
}
|
|
1109
1376
|
recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
|
|
1110
1377
|
|
|
1111
1378
|
if (r.progress) allProgress.push(r.progress);
|
|
@@ -1134,6 +1401,19 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1134
1401
|
};
|
|
1135
1402
|
}
|
|
1136
1403
|
|
|
1404
|
+
if (r.interrupted) {
|
|
1405
|
+
return {
|
|
1406
|
+
content: [{ type: "text", text: `Run paused after interrupt (${params.agent}). Waiting for explicit next action.` }],
|
|
1407
|
+
details: compactForegroundDetails({
|
|
1408
|
+
mode: "single",
|
|
1409
|
+
results: [r],
|
|
1410
|
+
progress: params.includeProgress ? allProgress : undefined,
|
|
1411
|
+
artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined,
|
|
1412
|
+
truncation: r.truncation,
|
|
1413
|
+
}),
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1137
1417
|
if (r.exitCode !== 0)
|
|
1138
1418
|
return {
|
|
1139
1419
|
content: [{ type: "text", text: r.error || "Failed" }],
|
|
@@ -1175,10 +1455,44 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1175
1455
|
ctx: ExtensionContext,
|
|
1176
1456
|
): Promise<AgentToolResult<Details>> => {
|
|
1177
1457
|
deps.state.baseCwd = ctx.cwd;
|
|
1458
|
+
deps.state.foregroundControls ??= new Map();
|
|
1459
|
+
deps.state.lastForegroundControlId ??= null;
|
|
1178
1460
|
const requestCwd = resolveRequestedCwd(ctx.cwd, params.cwd);
|
|
1179
1461
|
const paramsWithResolvedCwd = params.cwd === undefined ? params : { ...params, cwd: requestCwd };
|
|
1180
1462
|
if (params.action) {
|
|
1181
|
-
|
|
1463
|
+
if (params.action === "status") {
|
|
1464
|
+
const foreground = getForegroundControl(deps.state, paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId);
|
|
1465
|
+
if (foreground) return foregroundStatusResult(foreground);
|
|
1466
|
+
return inspectSubagentStatus(paramsWithResolvedCwd);
|
|
1467
|
+
}
|
|
1468
|
+
if (params.action === "interrupt") {
|
|
1469
|
+
const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id;
|
|
1470
|
+
const foreground = getForegroundControl(deps.state, targetRunId);
|
|
1471
|
+
if (foreground?.interrupt) {
|
|
1472
|
+
const interrupted = foreground.interrupt();
|
|
1473
|
+
if (interrupted) {
|
|
1474
|
+
foreground.updatedAt = Date.now();
|
|
1475
|
+
foreground.currentActivityState = undefined;
|
|
1476
|
+
return {
|
|
1477
|
+
content: [{ type: "text", text: `Interrupt requested for foreground run ${foreground.runId}.` }],
|
|
1478
|
+
details: { mode: "management", results: [] },
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
return {
|
|
1482
|
+
content: [{ type: "text", text: `Foreground run ${foreground.runId} has no active child step to interrupt.` }],
|
|
1483
|
+
isError: true,
|
|
1484
|
+
details: { mode: "management", results: [] },
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
const asyncInterruptResult = interruptAsyncRun(deps.state, targetRunId);
|
|
1488
|
+
if (asyncInterruptResult) return asyncInterruptResult;
|
|
1489
|
+
return {
|
|
1490
|
+
content: [{ type: "text", text: "No interrupt-capable run found in this session." }],
|
|
1491
|
+
isError: true,
|
|
1492
|
+
details: { mode: "management", results: [] },
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
const validActions = ["list", "get", "create", "update", "delete", "status", "interrupt"];
|
|
1182
1496
|
if (!validActions.includes(params.action)) {
|
|
1183
1497
|
return {
|
|
1184
1498
|
content: [{ type: "text", text: `Unknown action: ${params.action}. Valid: ${validActions.join(", ")}` }],
|
|
@@ -1260,6 +1574,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1260
1574
|
const backgroundRequestedWhileClarifying = hasTasks && requestedAsync && effectiveParams.clarify === true;
|
|
1261
1575
|
const effectiveAsync = requestedAsync
|
|
1262
1576
|
&& (hasChain ? effectiveParams.clarify === false : effectiveParams.clarify !== true);
|
|
1577
|
+
const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control);
|
|
1263
1578
|
|
|
1264
1579
|
const artifactConfig: ArtifactConfig = {
|
|
1265
1580
|
...DEFAULT_ARTIFACT_CONFIG,
|
|
@@ -1308,8 +1623,28 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1308
1623
|
artifactsDir,
|
|
1309
1624
|
backgroundRequestedWhileClarifying,
|
|
1310
1625
|
effectiveAsync,
|
|
1626
|
+
controlConfig,
|
|
1627
|
+
intercomBridge,
|
|
1311
1628
|
};
|
|
1312
1629
|
|
|
1630
|
+
const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single";
|
|
1631
|
+
const foregroundControl = effectiveAsync
|
|
1632
|
+
? undefined
|
|
1633
|
+
: {
|
|
1634
|
+
runId,
|
|
1635
|
+
mode: foregroundMode,
|
|
1636
|
+
startedAt: Date.now(),
|
|
1637
|
+
updatedAt: Date.now(),
|
|
1638
|
+
currentAgent: undefined,
|
|
1639
|
+
currentIndex: undefined,
|
|
1640
|
+
currentActivityState: undefined,
|
|
1641
|
+
interrupt: undefined,
|
|
1642
|
+
};
|
|
1643
|
+
if (foregroundControl) {
|
|
1644
|
+
deps.state.foregroundControls.set(runId, foregroundControl);
|
|
1645
|
+
deps.state.lastForegroundControlId = runId;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1313
1648
|
try {
|
|
1314
1649
|
const asyncResult = runAsyncPath(execData, deps);
|
|
1315
1650
|
if (asyncResult) return withForkContext(asyncResult, effectiveParams.context);
|
|
@@ -1327,6 +1662,13 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1327
1662
|
}
|
|
1328
1663
|
} catch (error) {
|
|
1329
1664
|
return toExecutionErrorResult(normalizedParams, error);
|
|
1665
|
+
} finally {
|
|
1666
|
+
if (foregroundControl) {
|
|
1667
|
+
deps.state.foregroundControls.delete(runId);
|
|
1668
|
+
if (deps.state.lastForegroundControlId === runId) {
|
|
1669
|
+
deps.state.lastForegroundControlId = null;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1330
1672
|
}
|
|
1331
1673
|
|
|
1332
1674
|
return withForkContext({
|
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
2
2
|
|
|
3
3
|
export const SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_PROJECT_CONTEXT";
|
|
4
4
|
export const SUBAGENT_INHERIT_SKILLS_ENV = "PI_SUBAGENT_INHERIT_SKILLS";
|
|
5
|
+
export const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "PI_SUBAGENT_INTERCOM_SESSION_NAME";
|
|
5
6
|
|
|
6
7
|
const PROJECT_CONTEXT_HEADER = "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n";
|
|
7
8
|
const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks.";
|
|
@@ -54,6 +55,11 @@ export function rewriteSubagentPrompt(
|
|
|
54
55
|
|
|
55
56
|
export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
56
57
|
pi.on("before_agent_start", async (event) => {
|
|
58
|
+
const intercomSessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim();
|
|
59
|
+
if (intercomSessionName && typeof pi.setSessionName === "function") {
|
|
60
|
+
pi.setSessionName(intercomSessionName);
|
|
61
|
+
}
|
|
62
|
+
|
|
57
63
|
const inheritProjectContext = readBooleanEnv(SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV);
|
|
58
64
|
const inheritSkills = readBooleanEnv(SUBAGENT_INHERIT_SKILLS_ENV);
|
|
59
65
|
if (inheritProjectContext === undefined && inheritSkills === undefined) return;
|