@narumitw/pi-subagents 0.1.21 → 0.1.22
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/README.md +5 -0
- package/package.json +1 -1
- package/src/subagents.ts +251 -168
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ Use it to split research, planning, implementation, and review work across focus
|
|
|
14
14
|
- Loads custom user agents from `~/.pi/agent/agents/*.md`.
|
|
15
15
|
- Optionally loads project agents from `.pi/agents/*.md` with confirmation.
|
|
16
16
|
- Supports per-task `cwd`, hard subprocess `timeoutMs`, abort propagation, and streaming progress.
|
|
17
|
+
- Publishes transient runtime status through Pi's generic extension status API while subagents are running.
|
|
17
18
|
- Returns complete worker output in tool details and a concise result for the main agent.
|
|
18
19
|
|
|
19
20
|
## 📦 Install
|
|
@@ -153,6 +154,10 @@ Each subprocess has a hard timeout to avoid runaway workers.
|
|
|
153
154
|
|
|
154
155
|
On timeout, the extension sends `SIGTERM`, escalates to `SIGKILL` after a short grace period, and returns any partial messages or stderr collected so far.
|
|
155
156
|
|
|
157
|
+
## 📡 Runtime status
|
|
158
|
+
|
|
159
|
+
While the `subagent` tool is running, `pi-subagents` publishes compact activity status with `ctx.ui.setStatus("subagents", "...")`. Any statusline extension that reads Pi's generic extension status API can display it; no package-to-package dependency is required.
|
|
160
|
+
|
|
156
161
|
## 🔒 Safety notes
|
|
157
162
|
|
|
158
163
|
Subagents are separate Pi processes and may use the tools allowed by their agent definition. Treat project-local agent prompts like executable project configuration: only enable them in trusted repositories.
|
package/package.json
CHANGED
package/src/subagents.ts
CHANGED
|
@@ -33,6 +33,8 @@ const MAX_CONCURRENCY = 4;
|
|
|
33
33
|
const COLLAPSED_ITEM_COUNT = 10;
|
|
34
34
|
const DEFAULT_TIMEOUT_MS = parsePositiveInteger(process.env.PI_SUBAGENT_TIMEOUT_MS) ?? 10 * 60 * 1000;
|
|
35
35
|
const KILL_GRACE_MS = 5000;
|
|
36
|
+
const STATUS_KEY = "subagents";
|
|
37
|
+
const activeStatuses = new Map<string, string>();
|
|
36
38
|
|
|
37
39
|
function parsePositiveInteger(value: string | undefined): number | undefined {
|
|
38
40
|
if (!value) return undefined;
|
|
@@ -40,6 +42,59 @@ function parsePositiveInteger(value: string | undefined): number | undefined {
|
|
|
40
42
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
interface StatusContext {
|
|
46
|
+
ui: { setStatus: (key: string, value: string | undefined) => void };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function startSubagentStatus(ctx: StatusContext, toolCallId: string, status: string) {
|
|
50
|
+
let cleared = false;
|
|
51
|
+
|
|
52
|
+
const update = (nextStatus: string) => {
|
|
53
|
+
if (cleared) return;
|
|
54
|
+
activeStatuses.set(toolCallId, nextStatus);
|
|
55
|
+
publishSubagentStatus(ctx);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
update(status);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
update,
|
|
62
|
+
clear() {
|
|
63
|
+
if (cleared) return;
|
|
64
|
+
cleared = true;
|
|
65
|
+
activeStatuses.delete(toolCallId);
|
|
66
|
+
publishSubagentStatus(ctx);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function publishSubagentStatus(ctx: StatusContext) {
|
|
72
|
+
const statuses = [...activeStatuses.values()];
|
|
73
|
+
if (statuses.length === 0) {
|
|
74
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const suffix = statuses.length > 1 ? ` +${statuses.length - 1}` : "";
|
|
79
|
+
ctx.ui.setStatus(STATUS_KEY, `${statuses[0]}${suffix}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function singleStatus(agent: string): string {
|
|
83
|
+
return `🧑🤝🧑 ${agent}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function chainStatus(step: number, total: number, agent?: string): string {
|
|
87
|
+
return `🧑🤝🧑 chain ${step}/${total}${agent ? ` ${agent}` : ""}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parallelStatus(done: number, total: number, running: number): string {
|
|
91
|
+
return `🧑🤝🧑 parallel ${done}/${total} done${running > 0 ? ` ${running} running` : ""}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function fanInStatus(agent: string): string {
|
|
95
|
+
return `🧑🤝🧑 fan-in ${agent}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
43
98
|
function formatTokens(count: number): string {
|
|
44
99
|
if (count < 1000) return count.toString();
|
|
45
100
|
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
@@ -544,7 +599,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
544
599
|
].join(" "),
|
|
545
600
|
parameters: SubagentParams,
|
|
546
601
|
|
|
547
|
-
async execute(
|
|
602
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
548
603
|
const agentScope: AgentScope = params.agentScope ?? "user";
|
|
549
604
|
const discovery = discoverAgents(ctx.cwd, agentScope);
|
|
550
605
|
const agents = discovery.agents;
|
|
@@ -612,56 +667,62 @@ export default function (pi: ExtensionAPI) {
|
|
|
612
667
|
if (params.chain && params.chain.length > 0) {
|
|
613
668
|
const results: SingleResult[] = [];
|
|
614
669
|
let previousOutput = "";
|
|
670
|
+
const status = startSubagentStatus(ctx, toolCallId, chainStatus(0, params.chain.length));
|
|
615
671
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
672
|
+
try {
|
|
673
|
+
for (let i = 0; i < params.chain.length; i++) {
|
|
674
|
+
const step = params.chain[i];
|
|
675
|
+
status.update(chainStatus(i + 1, params.chain.length, step.agent));
|
|
676
|
+
const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
|
|
677
|
+
|
|
678
|
+
// Create update callback that includes all previous results
|
|
679
|
+
const chainUpdate: OnUpdateCallback | undefined = onUpdate
|
|
680
|
+
? (partial) => {
|
|
681
|
+
// Combine completed results with current streaming result
|
|
682
|
+
const currentResult = partial.details?.results[0];
|
|
683
|
+
if (currentResult) {
|
|
684
|
+
const allResults = [...results, currentResult];
|
|
685
|
+
onUpdate({
|
|
686
|
+
content: partial.content,
|
|
687
|
+
details: makeDetails("chain")(allResults),
|
|
688
|
+
});
|
|
689
|
+
}
|
|
631
690
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
691
|
+
: undefined;
|
|
692
|
+
|
|
693
|
+
const result = await runSingleAgent(
|
|
694
|
+
ctx.cwd,
|
|
695
|
+
agents,
|
|
696
|
+
step.agent,
|
|
697
|
+
taskWithContext,
|
|
698
|
+
step.cwd,
|
|
699
|
+
i + 1,
|
|
700
|
+
signal,
|
|
701
|
+
step.timeoutMs ?? defaultTimeoutMs,
|
|
702
|
+
chainUpdate,
|
|
703
|
+
makeDetails("chain"),
|
|
704
|
+
);
|
|
705
|
+
results.push(result);
|
|
706
|
+
|
|
707
|
+
const isError =
|
|
708
|
+
result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
709
|
+
if (isError) {
|
|
710
|
+
const errorMsg = result.errorMessage || result.stderr || getResultFinalOutput(result) || "(no output)";
|
|
711
|
+
return {
|
|
712
|
+
content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
|
|
713
|
+
details: makeDetails("chain")(results),
|
|
714
|
+
isError: true,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
previousOutput = getResultFinalOutput(result);
|
|
658
718
|
}
|
|
659
|
-
|
|
719
|
+
return {
|
|
720
|
+
content: [{ type: "text", text: getResultFinalOutput(results[results.length - 1]) || "(no output)" }],
|
|
721
|
+
details: makeDetails("chain")(results),
|
|
722
|
+
};
|
|
723
|
+
} finally {
|
|
724
|
+
status.clear();
|
|
660
725
|
}
|
|
661
|
-
return {
|
|
662
|
-
content: [{ type: "text", text: getResultFinalOutput(results[results.length - 1]) || "(no output)" }],
|
|
663
|
-
details: makeDetails("chain")(results),
|
|
664
|
-
};
|
|
665
726
|
}
|
|
666
727
|
|
|
667
728
|
if (params.tasks && params.tasks.length > 0) {
|
|
@@ -676,141 +737,163 @@ export default function (pi: ExtensionAPI) {
|
|
|
676
737
|
details: makeDetails("parallel")([]),
|
|
677
738
|
};
|
|
678
739
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
740
|
+
const status = startSubagentStatus(ctx, toolCallId, parallelStatus(0, params.tasks.length, params.tasks.length));
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
// Track all results for streaming updates
|
|
744
|
+
const allResults: SingleResult[] = new Array(params.tasks.length);
|
|
745
|
+
|
|
746
|
+
// Initialize placeholder results
|
|
747
|
+
for (let i = 0; i < params.tasks.length; i++) {
|
|
748
|
+
allResults[i] = {
|
|
749
|
+
agent: params.tasks[i].agent,
|
|
750
|
+
agentSource: "unknown",
|
|
751
|
+
task: params.tasks[i].task,
|
|
752
|
+
exitCode: -1, // -1 = still running
|
|
753
|
+
messages: [],
|
|
754
|
+
stderr: "",
|
|
755
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
756
|
+
finalOutput: "",
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
let doneCount = 0;
|
|
761
|
+
let runningCount = params.tasks.length;
|
|
762
|
+
|
|
763
|
+
const emitParallelUpdate = () => {
|
|
764
|
+
status.update(parallelStatus(doneCount, allResults.length, runningCount));
|
|
765
|
+
if (onUpdate) {
|
|
766
|
+
onUpdate({
|
|
767
|
+
content: [
|
|
768
|
+
{
|
|
769
|
+
type: "text",
|
|
770
|
+
text: `Parallel: ${doneCount}/${allResults.length} done, ${runningCount} running...`,
|
|
771
|
+
},
|
|
772
|
+
],
|
|
773
|
+
details: makeDetails("parallel")([...allResults]),
|
|
774
|
+
});
|
|
775
|
+
}
|
|
693
776
|
};
|
|
694
|
-
}
|
|
695
777
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
778
|
+
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
|
|
779
|
+
const result = await runSingleAgent(
|
|
780
|
+
ctx.cwd,
|
|
781
|
+
agents,
|
|
782
|
+
t.agent,
|
|
783
|
+
t.task,
|
|
784
|
+
t.cwd,
|
|
785
|
+
undefined,
|
|
786
|
+
signal,
|
|
787
|
+
t.timeoutMs ?? defaultTimeoutMs,
|
|
788
|
+
// Per-task update callback
|
|
789
|
+
(partial) => {
|
|
790
|
+
if (partial.details?.results[0]) {
|
|
791
|
+
allResults[index] = { ...partial.details.results[0], exitCode: -1 };
|
|
792
|
+
emitParallelUpdate();
|
|
793
|
+
}
|
|
794
|
+
},
|
|
795
|
+
makeDetails("parallel"),
|
|
796
|
+
);
|
|
797
|
+
allResults[index] = result;
|
|
798
|
+
doneCount += 1;
|
|
799
|
+
runningCount -= 1;
|
|
800
|
+
emitParallelUpdate();
|
|
801
|
+
return result;
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
let aggregatorResult: SingleResult | undefined;
|
|
805
|
+
if (params.aggregator) {
|
|
806
|
+
const aggregator = params.aggregator;
|
|
807
|
+
status.update(fanInStatus(aggregator.agent));
|
|
808
|
+
const fanInContext = buildFanInContext(results);
|
|
809
|
+
const aggregatorTask = aggregator.task.includes("{previous}")
|
|
810
|
+
? aggregator.task.replace(/\{previous\}/g, fanInContext)
|
|
811
|
+
: `${aggregator.task}\n\nParallel task outputs:\n\n${fanInContext}`;
|
|
812
|
+
aggregatorResult = await runSingleAgent(
|
|
813
|
+
ctx.cwd,
|
|
814
|
+
agents,
|
|
815
|
+
aggregator.agent,
|
|
816
|
+
aggregatorTask,
|
|
817
|
+
aggregator.cwd,
|
|
818
|
+
undefined,
|
|
819
|
+
signal,
|
|
820
|
+
aggregator.timeoutMs ?? defaultTimeoutMs,
|
|
821
|
+
(partial) => {
|
|
822
|
+
status.update(fanInStatus(aggregator.agent));
|
|
823
|
+
if (onUpdate && partial.details?.results[0]) {
|
|
824
|
+
onUpdate({
|
|
825
|
+
content: partial.content,
|
|
826
|
+
details: makeDetails("parallel")(results, partial.details.results[0]),
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
makeDetails("parallel"),
|
|
831
|
+
);
|
|
706
832
|
}
|
|
707
|
-
};
|
|
708
833
|
|
|
709
|
-
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
834
|
+
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
835
|
+
const summaries = results.map((r) => {
|
|
836
|
+
const output = getResultFinalOutput(r);
|
|
837
|
+
const error = r.errorMessage || r.stderr.trim();
|
|
838
|
+
const summaryText = output || error;
|
|
839
|
+
const preview = summaryText.slice(0, 160) + (summaryText.length > 160 ? "..." : "");
|
|
840
|
+
return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${preview || "(no output)"}`;
|
|
841
|
+
});
|
|
842
|
+
const aggregatorOutput = aggregatorResult ? getResultFinalOutput(aggregatorResult) : "";
|
|
843
|
+
const aggregatorError = aggregatorResult?.errorMessage || aggregatorResult?.stderr.trim() || "";
|
|
844
|
+
return {
|
|
845
|
+
content: [
|
|
846
|
+
{
|
|
847
|
+
type: "text",
|
|
848
|
+
text: aggregatorResult
|
|
849
|
+
? aggregatorOutput || aggregatorError || `(aggregator ${aggregatorResult.agent} produced no output)`
|
|
850
|
+
: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
|
|
851
|
+
},
|
|
852
|
+
],
|
|
853
|
+
details: makeDetails("parallel")(results, aggregatorResult),
|
|
854
|
+
isError: aggregatorResult
|
|
855
|
+
? aggregatorResult.exitCode !== 0 ||
|
|
856
|
+
aggregatorResult.stopReason === "error" ||
|
|
857
|
+
aggregatorResult.stopReason === "aborted"
|
|
858
|
+
: undefined,
|
|
859
|
+
};
|
|
860
|
+
} finally {
|
|
861
|
+
status.clear();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
732
864
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
: `${params.aggregator.task}\n\nParallel task outputs:\n\n${fanInContext}`;
|
|
739
|
-
aggregatorResult = await runSingleAgent(
|
|
865
|
+
if (params.agent && params.task) {
|
|
866
|
+
const status = startSubagentStatus(ctx, toolCallId, singleStatus(params.agent));
|
|
867
|
+
|
|
868
|
+
try {
|
|
869
|
+
const result = await runSingleAgent(
|
|
740
870
|
ctx.cwd,
|
|
741
871
|
agents,
|
|
742
|
-
params.
|
|
743
|
-
|
|
744
|
-
params.
|
|
872
|
+
params.agent,
|
|
873
|
+
params.task,
|
|
874
|
+
params.cwd,
|
|
745
875
|
undefined,
|
|
746
876
|
signal,
|
|
747
|
-
params.
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
onUpdate({
|
|
751
|
-
content: partial.content,
|
|
752
|
-
details: makeDetails("parallel")(results, partial.details.results[0]),
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
},
|
|
756
|
-
makeDetails("parallel"),
|
|
877
|
+
params.timeoutMs ?? defaultTimeoutMs,
|
|
878
|
+
onUpdate,
|
|
879
|
+
makeDetails("single"),
|
|
757
880
|
);
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
});
|
|
768
|
-
const aggregatorOutput = aggregatorResult ? getResultFinalOutput(aggregatorResult) : "";
|
|
769
|
-
const aggregatorError = aggregatorResult?.errorMessage || aggregatorResult?.stderr.trim() || "";
|
|
770
|
-
return {
|
|
771
|
-
content: [
|
|
772
|
-
{
|
|
773
|
-
type: "text",
|
|
774
|
-
text: aggregatorResult
|
|
775
|
-
? aggregatorOutput || aggregatorError || `(aggregator ${aggregatorResult.agent} produced no output)`
|
|
776
|
-
: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
|
|
777
|
-
},
|
|
778
|
-
],
|
|
779
|
-
details: makeDetails("parallel")(results, aggregatorResult),
|
|
780
|
-
isError: aggregatorResult
|
|
781
|
-
? aggregatorResult.exitCode !== 0 ||
|
|
782
|
-
aggregatorResult.stopReason === "error" ||
|
|
783
|
-
aggregatorResult.stopReason === "aborted"
|
|
784
|
-
: undefined,
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
if (params.agent && params.task) {
|
|
789
|
-
const result = await runSingleAgent(
|
|
790
|
-
ctx.cwd,
|
|
791
|
-
agents,
|
|
792
|
-
params.agent,
|
|
793
|
-
params.task,
|
|
794
|
-
params.cwd,
|
|
795
|
-
undefined,
|
|
796
|
-
signal,
|
|
797
|
-
params.timeoutMs ?? defaultTimeoutMs,
|
|
798
|
-
onUpdate,
|
|
799
|
-
makeDetails("single"),
|
|
800
|
-
);
|
|
801
|
-
const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
802
|
-
if (isError) {
|
|
803
|
-
const errorMsg = result.errorMessage || result.stderr || getResultFinalOutput(result) || "(no output)";
|
|
881
|
+
const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
882
|
+
if (isError) {
|
|
883
|
+
const errorMsg = result.errorMessage || result.stderr || getResultFinalOutput(result) || "(no output)";
|
|
884
|
+
return {
|
|
885
|
+
content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
|
|
886
|
+
details: makeDetails("single")([result]),
|
|
887
|
+
isError: true,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
804
890
|
return {
|
|
805
|
-
content: [{ type: "text", text:
|
|
891
|
+
content: [{ type: "text", text: getResultFinalOutput(result) || "(no output)" }],
|
|
806
892
|
details: makeDetails("single")([result]),
|
|
807
|
-
isError: true,
|
|
808
893
|
};
|
|
894
|
+
} finally {
|
|
895
|
+
status.clear();
|
|
809
896
|
}
|
|
810
|
-
return {
|
|
811
|
-
content: [{ type: "text", text: getResultFinalOutput(result) || "(no output)" }],
|
|
812
|
-
details: makeDetails("single")([result]),
|
|
813
|
-
};
|
|
814
897
|
}
|
|
815
898
|
|
|
816
899
|
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|