pi-subagents 0.31.1 → 0.32.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 +40 -4
- package/README.md +97 -7
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +6 -1
- package/src/agents/agent-management.ts +6 -1
- package/src/agents/agents.ts +55 -11
- package/src/extension/companion-suggestions.ts +359 -0
- package/src/extension/config.ts +27 -4
- package/src/extension/doctor.ts +2 -0
- package/src/extension/fanout-child.ts +1 -0
- package/src/extension/index.ts +58 -4
- package/src/extension/schemas.ts +2 -2
- package/src/runs/background/async-execution.ts +138 -33
- package/src/runs/background/async-job-tracker.ts +77 -1
- package/src/runs/background/async-status.ts +41 -9
- package/src/runs/background/chain-root-attachment.ts +34 -4
- package/src/runs/background/control-channel.ts +50 -0
- package/src/runs/background/run-status.ts +1 -0
- package/src/runs/background/stale-run-reconciler.ts +28 -1
- package/src/runs/background/subagent-runner.ts +454 -115
- package/src/runs/foreground/chain-execution.ts +29 -7
- package/src/runs/foreground/execution.ts +24 -6
- package/src/runs/foreground/subagent-executor.ts +209 -35
- package/src/runs/shared/acceptance.ts +45 -22
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +4 -0
- package/src/runs/shared/nested-events.ts +58 -0
- package/src/runs/shared/parallel-utils.ts +49 -1
- package/src/runs/shared/pi-args.ts +5 -3
- package/src/runs/shared/pi-spawn.ts +52 -20
- package/src/runs/shared/single-output.ts +2 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +2 -2
- package/src/runs/shared/worktree.ts +28 -5
- package/src/shared/artifacts.ts +15 -1
- package/src/shared/fork-context.ts +133 -22
- package/src/shared/types.ts +81 -3
- package/src/shared/utils.ts +99 -14
- package/src/slash/slash-commands.ts +117 -0
- package/src/tui/render.ts +16 -4
|
@@ -34,7 +34,8 @@ import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skill
|
|
|
34
34
|
import { INTERCOM_BRIDGE_MARKER } from "../../intercom/intercom-bridge.ts";
|
|
35
35
|
import { runSync } from "./execution.ts";
|
|
36
36
|
import { buildChainSummary } from "../../shared/formatters.ts";
|
|
37
|
-
import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd } from "../../shared/utils.ts";
|
|
37
|
+
import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
|
|
38
|
+
import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
|
|
38
39
|
import { recordRun } from "../shared/run-history.ts";
|
|
39
40
|
import {
|
|
40
41
|
cleanupWorktrees,
|
|
@@ -103,6 +104,7 @@ interface ParallelChainRunInput {
|
|
|
103
104
|
sessionDirForIndex: (idx?: number) => string | undefined;
|
|
104
105
|
sessionFileForIndex?: (idx?: number) => string | undefined;
|
|
105
106
|
sessionFileForTask?: (agentName: string, idx?: number) => string | undefined;
|
|
107
|
+
thinkingOverrideForTask?: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined;
|
|
106
108
|
shareEnabled: boolean;
|
|
107
109
|
artifactConfig: ArtifactConfig;
|
|
108
110
|
artifactsDir: string;
|
|
@@ -139,6 +141,7 @@ interface ParallelChainRunInput {
|
|
|
139
141
|
nestedRoute?: NestedRouteInfo;
|
|
140
142
|
timeoutMs?: number;
|
|
141
143
|
deadlineAt?: number;
|
|
144
|
+
globalSemaphore?: Semaphore;
|
|
142
145
|
}
|
|
143
146
|
|
|
144
147
|
function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details {
|
|
@@ -151,7 +154,9 @@ function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details
|
|
|
151
154
|
totalSteps: input.totalSteps,
|
|
152
155
|
currentStepIndex: input.currentStepIndex,
|
|
153
156
|
outputs: input.outputs,
|
|
154
|
-
|
|
157
|
+
totalChildUsage: sumResultsUsage(input.results),
|
|
158
|
+
totalCost: sumResultsCost(input.results),
|
|
159
|
+
workflowGraph: buildWorkflowGraphSnapshot({
|
|
155
160
|
runId: input.runId,
|
|
156
161
|
mode: "chain",
|
|
157
162
|
steps: input.chainSteps,
|
|
@@ -280,6 +285,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
280
285
|
sessionDir: input.sessionDirForIndex(input.globalTaskIndex + taskIndex),
|
|
281
286
|
sessionFile: input.sessionFileForTask?.(task.agent, input.globalTaskIndex + taskIndex)
|
|
282
287
|
?? input.sessionFileForIndex?.(input.globalTaskIndex + taskIndex),
|
|
288
|
+
thinkingOverride: input.thinkingOverrideForTask?.(task.agent, input.globalTaskIndex + taskIndex),
|
|
283
289
|
share: input.shareEnabled,
|
|
284
290
|
artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
|
|
285
291
|
artifactConfig: input.artifactConfig,
|
|
@@ -355,6 +361,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
|
|
|
355
361
|
recordRun(task.agent, cleanTask, result.exitCode, result.progressSummary?.durationMs ?? 0);
|
|
356
362
|
return result;
|
|
357
363
|
},
|
|
364
|
+
input.globalSemaphore,
|
|
358
365
|
);
|
|
359
366
|
|
|
360
367
|
return parallelResults;
|
|
@@ -373,6 +380,7 @@ interface ChainExecutionParams {
|
|
|
373
380
|
sessionDirForIndex: (idx?: number) => string | undefined;
|
|
374
381
|
sessionFileForIndex?: (idx?: number) => string | undefined;
|
|
375
382
|
sessionFileForTask?: (agentName: string, idx?: number) => string | undefined;
|
|
383
|
+
thinkingOverrideForTask?: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined;
|
|
376
384
|
artifactsDir: string;
|
|
377
385
|
artifactConfig: ArtifactConfig;
|
|
378
386
|
includeProgress?: boolean;
|
|
@@ -403,8 +411,11 @@ interface ChainExecutionParams {
|
|
|
403
411
|
nestedRoute?: NestedRouteInfo;
|
|
404
412
|
worktreeSetupHook?: string;
|
|
405
413
|
worktreeSetupHookTimeoutMs?: number;
|
|
414
|
+
worktreeBaseDir?: string;
|
|
406
415
|
timeoutMs?: number;
|
|
407
416
|
deadlineAt?: number;
|
|
417
|
+
/** Global cap on simultaneously-running tasks within this chain. Defaults to DEFAULT_GLOBAL_CONCURRENCY_LIMIT. */
|
|
418
|
+
globalConcurrencyLimit?: number;
|
|
408
419
|
}
|
|
409
420
|
|
|
410
421
|
interface ChainExecutionResult {
|
|
@@ -433,6 +444,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
433
444
|
sessionDirForIndex,
|
|
434
445
|
sessionFileForIndex,
|
|
435
446
|
sessionFileForTask,
|
|
447
|
+
thinkingOverrideForTask,
|
|
436
448
|
artifactsDir,
|
|
437
449
|
artifactConfig,
|
|
438
450
|
includeProgress,
|
|
@@ -596,6 +608,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
596
608
|
}
|
|
597
609
|
|
|
598
610
|
const deadlineAt = params.deadlineAt ?? (params.timeoutMs !== undefined ? Date.now() + params.timeoutMs : undefined);
|
|
611
|
+
const globalSemaphore = new Semaphore(params.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
|
|
599
612
|
let prev = "";
|
|
600
613
|
let globalTaskIndex = 0;
|
|
601
614
|
let progressCreated = false;
|
|
@@ -622,6 +635,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
622
635
|
setupHook: params.worktreeSetupHook
|
|
623
636
|
? { hookPath: params.worktreeSetupHook, timeoutMs: params.worktreeSetupHookTimeoutMs }
|
|
624
637
|
: undefined,
|
|
638
|
+
baseDir: params.worktreeBaseDir,
|
|
625
639
|
});
|
|
626
640
|
} catch (error) {
|
|
627
641
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -662,6 +676,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
662
676
|
sessionDirForIndex,
|
|
663
677
|
sessionFileForIndex,
|
|
664
678
|
sessionFileForTask,
|
|
679
|
+
thinkingOverrideForTask,
|
|
665
680
|
shareEnabled,
|
|
666
681
|
artifactConfig,
|
|
667
682
|
artifactsDir,
|
|
@@ -685,6 +700,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
685
700
|
maxSubagentDepth: params.maxSubagentDepth,
|
|
686
701
|
timeoutMs: params.timeoutMs,
|
|
687
702
|
deadlineAt,
|
|
703
|
+
globalSemaphore,
|
|
688
704
|
});
|
|
689
705
|
globalTaskIndex += step.parallel.length;
|
|
690
706
|
|
|
@@ -770,6 +786,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
770
786
|
if (worktreeSetup) cleanupWorktrees(worktreeSetup);
|
|
771
787
|
}
|
|
772
788
|
} else if (isDynamicParallelStep(step)) {
|
|
789
|
+
const dynamicStartIndex = globalTaskIndex;
|
|
790
|
+
const reservedDynamicItems = step.expand.maxItems ?? params.dynamicFanoutMaxItems ?? 0;
|
|
773
791
|
let materialized: ReturnType<typeof materializeDynamicParallelStep>;
|
|
774
792
|
try {
|
|
775
793
|
materialized = materializeDynamicParallelStep(step, outputs, stepIndex, { maxItems: params.dynamicFanoutMaxItems });
|
|
@@ -828,6 +846,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
828
846
|
}
|
|
829
847
|
}
|
|
830
848
|
prev = "Dynamic fanout produced 0 results.";
|
|
849
|
+
globalTaskIndex = dynamicStartIndex + reservedDynamicItems;
|
|
831
850
|
continue;
|
|
832
851
|
}
|
|
833
852
|
|
|
@@ -872,6 +891,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
872
891
|
sessionDirForIndex,
|
|
873
892
|
sessionFileForIndex,
|
|
874
893
|
sessionFileForTask,
|
|
894
|
+
thinkingOverrideForTask,
|
|
875
895
|
shareEnabled,
|
|
876
896
|
artifactConfig,
|
|
877
897
|
artifactsDir,
|
|
@@ -894,8 +914,9 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
894
914
|
maxSubagentDepth: params.maxSubagentDepth,
|
|
895
915
|
timeoutMs: params.timeoutMs,
|
|
896
916
|
deadlineAt,
|
|
917
|
+
globalSemaphore,
|
|
897
918
|
});
|
|
898
|
-
globalTaskIndex
|
|
919
|
+
globalTaskIndex = dynamicStartIndex + reservedDynamicItems;
|
|
899
920
|
|
|
900
921
|
for (const result of parallelResults) {
|
|
901
922
|
results.push(result);
|
|
@@ -910,7 +931,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
910
931
|
content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${interrupted.agent}). Waiting for explicit next action.` }],
|
|
911
932
|
details: buildChainExecutionDetails(makeDetailsInput({
|
|
912
933
|
currentStepIndex: stepIndex,
|
|
913
|
-
currentFlatIndex:
|
|
934
|
+
currentFlatIndex: dynamicStartIndex + interruptedIndexInStep,
|
|
914
935
|
})),
|
|
915
936
|
};
|
|
916
937
|
}
|
|
@@ -921,7 +942,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
921
942
|
content: [{ type: "text", text: `Chain detached for intercom coordination at step ${stepIndex + 1} (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
|
|
922
943
|
details: buildChainExecutionDetails(makeDetailsInput({
|
|
923
944
|
currentStepIndex: stepIndex,
|
|
924
|
-
currentFlatIndex:
|
|
945
|
+
currentFlatIndex: dynamicStartIndex + detachedIndexInStep,
|
|
925
946
|
})),
|
|
926
947
|
};
|
|
927
948
|
}
|
|
@@ -943,7 +964,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
943
964
|
isError: true,
|
|
944
965
|
details: buildChainExecutionDetails(makeDetailsInput({
|
|
945
966
|
currentStepIndex: stepIndex,
|
|
946
|
-
currentFlatIndex:
|
|
967
|
+
currentFlatIndex: dynamicStartIndex + failures[0]!.originalIndex,
|
|
947
968
|
})),
|
|
948
969
|
};
|
|
949
970
|
}
|
|
@@ -952,7 +973,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
952
973
|
} catch (error) {
|
|
953
974
|
const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
|
|
954
975
|
dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
|
|
955
|
-
return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex:
|
|
976
|
+
return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: dynamicStartIndex }));
|
|
956
977
|
}
|
|
957
978
|
outputs[step.collect.as] = {
|
|
958
979
|
text: JSON.stringify(collected),
|
|
@@ -1085,6 +1106,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
1085
1106
|
sessionDir: sessionDirForIndex(globalTaskIndex),
|
|
1086
1107
|
sessionFile: sessionFileForTask?.(seqStep.agent, globalTaskIndex)
|
|
1087
1108
|
?? sessionFileForIndex?.(globalTaskIndex),
|
|
1109
|
+
thinkingOverride: thinkingOverrideForTask?.(seqStep.agent, globalTaskIndex),
|
|
1088
1110
|
share: shareEnabled,
|
|
1089
1111
|
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
1090
1112
|
artifactConfig,
|
|
@@ -180,15 +180,16 @@ async function runSingleAttempt(
|
|
|
180
180
|
originalTask?: string;
|
|
181
181
|
},
|
|
182
182
|
): Promise<SingleResult> {
|
|
183
|
-
const
|
|
184
|
-
|
|
183
|
+
const effectiveThinking = options.thinkingOverride ?? agent.thinking;
|
|
184
|
+
const modelArg = applyThinkingSuffix(model, effectiveThinking, options.thinkingOverride !== undefined);
|
|
185
|
+
const { args, env: sharedEnv, tempDir } = buildPiArgs({
|
|
185
186
|
baseArgs: ["--mode", "json", "-p"],
|
|
186
187
|
task,
|
|
187
188
|
sessionEnabled: shared.sessionEnabled,
|
|
188
189
|
sessionDir: options.sessionDir,
|
|
189
190
|
sessionFile: options.sessionFile,
|
|
190
|
-
model,
|
|
191
|
-
thinking:
|
|
191
|
+
model: modelArg,
|
|
192
|
+
thinking: effectiveThinking,
|
|
192
193
|
systemPromptMode: agent.systemPromptMode,
|
|
193
194
|
inheritProjectContext: agent.inheritProjectContext,
|
|
194
195
|
inheritSkills: agent.inheritSkills,
|
|
@@ -518,8 +519,11 @@ async function runSingleAttempt(
|
|
|
518
519
|
const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
|
|
519
520
|
? evt.args as Record<string, unknown>
|
|
520
521
|
: {};
|
|
522
|
+
let shouldDetachForBlockingIntercom = false;
|
|
521
523
|
if (options.allowIntercomDetach && (evt.toolName === "intercom" || evt.toolName === "contact_supervisor")) {
|
|
522
524
|
intercomStarted = true;
|
|
525
|
+
shouldDetachForBlockingIntercom = (evt.toolName === "intercom" && toolArgs.action === "ask")
|
|
526
|
+
|| (evt.toolName === "contact_supervisor" && (toolArgs.reason === "need_decision" || toolArgs.reason === "interview_request"));
|
|
523
527
|
}
|
|
524
528
|
progress.toolCount++;
|
|
525
529
|
progress.currentTool = evt.toolName;
|
|
@@ -530,6 +534,9 @@ async function runSingleAttempt(
|
|
|
530
534
|
observedMutationAttempt = observedMutationAttempt || mutates;
|
|
531
535
|
pendingToolResult = { tool: evt.toolName ?? "tool", path: progress.currentPath, mutates, startedAt: now };
|
|
532
536
|
fireUpdate();
|
|
537
|
+
if (shouldDetachForBlockingIntercom && !detached && !processClosed) {
|
|
538
|
+
detachForIntercom();
|
|
539
|
+
}
|
|
533
540
|
}
|
|
534
541
|
|
|
535
542
|
if (evt.type === "tool_execution_end") {
|
|
@@ -772,6 +779,16 @@ async function runSingleAttempt(
|
|
|
772
779
|
: `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
|
|
773
780
|
}
|
|
774
781
|
}
|
|
782
|
+
if (result.exitCode === 0 && !result.error) {
|
|
783
|
+
const finalText = getFinalOutput(result.messages);
|
|
784
|
+
const missingStructuredOutput = options.structuredOutput
|
|
785
|
+
? !existsSync(options.structuredOutput.outputPath)
|
|
786
|
+
: false;
|
|
787
|
+
if (!finalText?.trim() && (!options.structuredOutput || missingStructuredOutput)) {
|
|
788
|
+
result.exitCode = 1;
|
|
789
|
+
result.error = "Subagent produced no output (possible model cold-start or empty response).";
|
|
790
|
+
}
|
|
791
|
+
}
|
|
775
792
|
if (options.structuredOutput && result.exitCode === 0 && !result.error) {
|
|
776
793
|
const structured = readStructuredOutput({
|
|
777
794
|
schema: options.structuredOutput.schema,
|
|
@@ -966,7 +983,6 @@ export async function runSync(
|
|
|
966
983
|
const modelsToTry = candidates.length > 0 ? candidates : [undefined];
|
|
967
984
|
for (let i = 0; i < modelsToTry.length; i++) {
|
|
968
985
|
const candidate = modelsToTry[i];
|
|
969
|
-
if (candidate) attemptedModels.push(candidate);
|
|
970
986
|
const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
|
|
971
987
|
const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, options, {
|
|
972
988
|
sessionEnabled,
|
|
@@ -980,12 +996,14 @@ export async function runSync(
|
|
|
980
996
|
originalTask: task,
|
|
981
997
|
});
|
|
982
998
|
lastResult = result;
|
|
999
|
+
if (result.model) attemptedModels.push(result.model);
|
|
1000
|
+
else if (candidate) attemptedModels.push(candidate);
|
|
983
1001
|
sumUsage(aggregateUsage, result.usage);
|
|
984
1002
|
totalToolCount += result.progressSummary?.toolCount ?? 0;
|
|
985
1003
|
totalDurationMs += result.progressSummary?.durationMs ?? 0;
|
|
986
1004
|
const attemptSucceeded = result.exitCode === 0 && !result.error;
|
|
987
1005
|
const attempt: ModelAttempt = {
|
|
988
|
-
model:
|
|
1006
|
+
model: result.model ?? candidate ?? agent.model ?? "default",
|
|
989
1007
|
success: attemptSucceeded,
|
|
990
1008
|
exitCode: result.exitCode,
|
|
991
1009
|
error: result.error,
|