@narumitw/pi-subagents 0.27.0 → 0.30.1
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 +11 -3
- package/package.json +2 -2
- package/src/execution.ts +6 -7
- package/src/index.ts +1 -0
- package/src/params.ts +26 -7
- package/src/render.ts +24 -22
- package/src/subagents.ts +1 -0
package/README.md
CHANGED
|
@@ -178,6 +178,10 @@ Run multiple agents in parallel with a shared thinking level and one per-task ov
|
|
|
178
178
|
}
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
+
Omit `aggregator` entirely when parallel worker outputs should return directly. Do not send `null`,
|
|
182
|
+
empty strings, or an empty object for an unused optional field; for compatibility, an aggregator with
|
|
183
|
+
an empty or whitespace-only `agent` or `task` is treated as absent.
|
|
184
|
+
|
|
181
185
|
Run parallel workers, then aggregate their results:
|
|
182
186
|
|
|
183
187
|
```json
|
|
@@ -339,6 +343,9 @@ Existing `subagent` requests remain unchanged:
|
|
|
339
343
|
| Parallel | Input order, with at most four active children. | Collects all task results; partial worker failure is reported in summaries but does not discard successful results. |
|
|
340
344
|
| Parallel + aggregator | Source input order, then aggregator. | The aggregator runs with both successful outputs and failure descriptions; aggregator failure marks the tool result as an error. |
|
|
341
345
|
|
|
346
|
+
An aggregator whose `agent` or `task` is empty or whitespace-only is treated as absent, so successful
|
|
347
|
+
parallel outputs remain available instead of being replaced by a malformed fan-in failure.
|
|
348
|
+
|
|
342
349
|
Timeout precedence remains: task/step/aggregator → call → agent setting → `PI_SUBAGENT_TIMEOUT_MS` → 600000 ms. Blocking thinking precedence remains: task/step/aggregator → call → agent setting → child default. Stateful spawn thinking precedence is: `subagent_spawn.thinkingLevel` → agent setting → transport fallback. Project-agent resolution and confirmation behavior is unchanged.
|
|
343
350
|
|
|
344
351
|
## 🤖 Built-in agents
|
|
@@ -491,7 +498,8 @@ Stateful records are stored as versioned mode-0600 JSON under `~/.pi/agent/pi-su
|
|
|
491
498
|
```txt
|
|
492
499
|
extensions/pi-subagents/
|
|
493
500
|
├── src/
|
|
494
|
-
│ ├──
|
|
501
|
+
│ ├── index.ts # Pi package entrypoint
|
|
502
|
+
│ ├── subagents.ts # Extension registration and blocking tool schema
|
|
495
503
|
│ ├── stateful.ts # Detached lifecycle registration and dispatch
|
|
496
504
|
│ ├── stateful-tool-params.ts # Consolidated action schemas and validation
|
|
497
505
|
│ └── *.ts # Package-local discovery, execution, rendering, and config modules
|
|
@@ -501,12 +509,12 @@ extensions/pi-subagents/
|
|
|
501
509
|
└── package.json
|
|
502
510
|
```
|
|
503
511
|
|
|
504
|
-
|
|
512
|
+
`index.ts` is the Pi entrypoint and forwards to `subagents.ts`; the other source modules are internal. The package exposes its Pi extension through `package.json`:
|
|
505
513
|
|
|
506
514
|
```json
|
|
507
515
|
{
|
|
508
516
|
"pi": {
|
|
509
|
-
"extensions": ["./src/
|
|
517
|
+
"extensions": ["./src/index.ts"]
|
|
510
518
|
}
|
|
511
519
|
}
|
|
512
520
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.1",
|
|
4
4
|
"description": "Pi extension for delegating work to specialized isolated subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"pi": {
|
|
22
22
|
"extensions": [
|
|
23
|
-
"./src/
|
|
23
|
+
"./src/index.ts"
|
|
24
24
|
]
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
package/src/execution.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { AgentToolResult, AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import {
|
|
4
|
-
discoverAgents,
|
|
5
4
|
type AgentConfig,
|
|
6
5
|
type AgentScope,
|
|
6
|
+
discoverAgents,
|
|
7
7
|
type SubagentThinkingLevel,
|
|
8
8
|
} from "./agents.js";
|
|
9
9
|
import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
|
|
10
|
-
import type
|
|
10
|
+
import { hasUsableAggregator, type SubagentParams } from "./params.js";
|
|
11
11
|
import {
|
|
12
12
|
buildFanInContext,
|
|
13
13
|
formatResultFailure,
|
|
@@ -107,6 +107,7 @@ export async function executeSubagent(
|
|
|
107
107
|
): Promise<AgentToolResult<SubagentDetails> & { isError?: boolean }> {
|
|
108
108
|
assertSubagentDepthAllowed();
|
|
109
109
|
const agentScope: AgentScope = params.agentScope ?? "user";
|
|
110
|
+
const aggregator = hasUsableAggregator(params.aggregator) ? params.aggregator : undefined;
|
|
110
111
|
const config = readSubagentSettings();
|
|
111
112
|
const discovery = discoverAgents(ctx.cwd, agentScope, config);
|
|
112
113
|
const agents = discovery.agents;
|
|
@@ -134,7 +135,7 @@ export async function executeSubagent(
|
|
|
134
135
|
aggregator,
|
|
135
136
|
});
|
|
136
137
|
|
|
137
|
-
if (modeCount !== 1 || (
|
|
138
|
+
if (modeCount !== 1 || (aggregator && !hasTasks)) {
|
|
138
139
|
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
139
140
|
const reason =
|
|
140
141
|
modeCount !== 1
|
|
@@ -155,7 +156,7 @@ export async function executeSubagent(
|
|
|
155
156
|
const requestedAgentNames = new Set<string>();
|
|
156
157
|
if (params.chain) for (const step of params.chain) requestedAgentNames.add(step.agent);
|
|
157
158
|
if (params.tasks) for (const t of params.tasks) requestedAgentNames.add(t.agent);
|
|
158
|
-
if (
|
|
159
|
+
if (aggregator) requestedAgentNames.add(aggregator.agent);
|
|
159
160
|
if (params.agent) requestedAgentNames.add(params.agent);
|
|
160
161
|
|
|
161
162
|
const projectAgentsRequested = Array.from(requestedAgentNames)
|
|
@@ -286,7 +287,6 @@ export async function executeSubagent(
|
|
|
286
287
|
const emitParallelUpdate = () => {
|
|
287
288
|
status.update(parallelStatus(doneCount, allResults.length, runningCount));
|
|
288
289
|
if (onUpdate) {
|
|
289
|
-
const aggregator = params.aggregator;
|
|
290
290
|
const pendingAggregator: SingleResult | undefined =
|
|
291
291
|
aggregator && !signal?.aborted && doneCount === allResults.length
|
|
292
292
|
? {
|
|
@@ -368,8 +368,7 @@ export async function executeSubagent(
|
|
|
368
368
|
});
|
|
369
369
|
|
|
370
370
|
let aggregatorResult: SingleResult | undefined;
|
|
371
|
-
if (
|
|
372
|
-
const aggregator = params.aggregator;
|
|
371
|
+
if (aggregator && !signal?.aborted) {
|
|
373
372
|
status.update(fanInStatus(aggregator.agent));
|
|
374
373
|
const fanInContext = buildFanInContext(results);
|
|
375
374
|
const aggregatorTask = truncateUtf8(
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./subagents.js";
|
package/src/params.ts
CHANGED
|
@@ -29,13 +29,19 @@ const ChainItem = Type.Object({
|
|
|
29
29
|
thinkingLevel: Type.Optional(ThinkingLevelSchema),
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
const AggregatorItem = Type.Object(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
const AggregatorItem = Type.Object(
|
|
33
|
+
{
|
|
34
|
+
agent: Type.String({ description: "Name of the fan-in agent to invoke after parallel tasks complete" }),
|
|
35
|
+
task: Type.String({ description: "Fan-in task. Use {previous} to include all parallel outputs." }),
|
|
36
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the aggregator process" })),
|
|
37
|
+
timeoutMs: Type.Optional(TimeoutMs),
|
|
38
|
+
thinkingLevel: Type.Optional(ThinkingLevelSchema),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
description:
|
|
42
|
+
"Optional fan-in step for parallel mode. Omit this key entirely when no aggregation is needed; empty or whitespace-only agent/task values are treated as absent.",
|
|
43
|
+
},
|
|
44
|
+
);
|
|
39
45
|
|
|
40
46
|
const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
|
|
41
47
|
description:
|
|
@@ -59,3 +65,16 @@ export const SubagentParams = Type.Object({
|
|
|
59
65
|
});
|
|
60
66
|
|
|
61
67
|
export type SubagentParams = Static<typeof SubagentParams>;
|
|
68
|
+
|
|
69
|
+
export function hasUsableAggregator(
|
|
70
|
+
aggregator: unknown,
|
|
71
|
+
): aggregator is { agent: string; task: string } {
|
|
72
|
+
if (!aggregator || typeof aggregator !== "object") return false;
|
|
73
|
+
const candidate = aggregator as { agent?: unknown; task?: unknown };
|
|
74
|
+
return (
|
|
75
|
+
typeof candidate.agent === "string" &&
|
|
76
|
+
candidate.agent.trim().length > 0 &&
|
|
77
|
+
typeof candidate.task === "string" &&
|
|
78
|
+
candidate.task.trim().length > 0
|
|
79
|
+
);
|
|
80
|
+
}
|
package/src/render.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
11
11
|
import type { AgentScope, SubagentThinkingLevel } from "./agents.js";
|
|
12
|
-
import type
|
|
12
|
+
import { hasUsableAggregator, type SubagentParams } from "./params.js";
|
|
13
13
|
import {
|
|
14
14
|
getResultFinalOutput,
|
|
15
15
|
isResultError,
|
|
@@ -19,6 +19,15 @@ import {
|
|
|
19
19
|
|
|
20
20
|
const COLLAPSED_ITEM_COUNT = 10;
|
|
21
21
|
|
|
22
|
+
function previewTask(task: unknown, maxLength = 40): string {
|
|
23
|
+
if (typeof task !== "string" || task.length === 0) return "...";
|
|
24
|
+
return task.length > maxLength ? `${task.slice(0, maxLength)}...` : task;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function previewAgent(agent: unknown): string {
|
|
28
|
+
return typeof agent === "string" && agent.length > 0 ? agent : "...";
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
export function formatTokens(count: number): string {
|
|
23
32
|
if (count < 1000) return count.toString();
|
|
24
33
|
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
@@ -180,16 +189,16 @@ export function renderSubagentCall(args: SubagentParams, theme: Theme) {
|
|
|
180
189
|
theme.fg("accent", `chain (${args.chain.length} steps)`) +
|
|
181
190
|
theme.fg("muted", ` [${scope}]`);
|
|
182
191
|
for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
|
|
183
|
-
const step = args.chain[i];
|
|
192
|
+
const step = args.chain[i] as { agent?: unknown; task?: unknown } | undefined;
|
|
184
193
|
// Clean up {previous} placeholder for display
|
|
185
|
-
const cleanTask =
|
|
186
|
-
|
|
194
|
+
const cleanTask =
|
|
195
|
+
typeof step?.task === "string" ? step.task.replace(/\{previous\}/g, "").trim() : undefined;
|
|
187
196
|
text +=
|
|
188
197
|
"\n " +
|
|
189
198
|
theme.fg("muted", `${i + 1}.`) +
|
|
190
199
|
" " +
|
|
191
|
-
theme.fg("accent", step
|
|
192
|
-
theme.fg("dim", ` ${
|
|
200
|
+
theme.fg("accent", previewAgent(step?.agent)) +
|
|
201
|
+
theme.fg("dim", ` ${previewTask(cleanTask)}`);
|
|
193
202
|
}
|
|
194
203
|
if (args.chain.length > 3)
|
|
195
204
|
text += `\n ${theme.fg("muted", `... +${args.chain.length - 3} more`)}`;
|
|
@@ -200,30 +209,23 @@ export function renderSubagentCall(args: SubagentParams, theme: Theme) {
|
|
|
200
209
|
theme.fg("toolTitle", theme.bold("subagent ")) +
|
|
201
210
|
theme.fg("accent", `parallel (${args.tasks.length} tasks)`) +
|
|
202
211
|
theme.fg("muted", ` [${scope}]`);
|
|
203
|
-
for (const
|
|
204
|
-
const
|
|
205
|
-
text += `\n ${theme.fg("accent",
|
|
212
|
+
for (const task of args.tasks.slice(0, 3)) {
|
|
213
|
+
const item = task as { agent?: unknown; task?: unknown } | undefined;
|
|
214
|
+
text += `\n ${theme.fg("accent", previewAgent(item?.agent))}${theme.fg("dim", ` ${previewTask(item?.task)}`)}`;
|
|
206
215
|
}
|
|
207
216
|
if (args.tasks.length > 3)
|
|
208
217
|
text += `\n ${theme.fg("muted", `... +${args.tasks.length - 3} more`)}`;
|
|
209
|
-
if (args.aggregator) {
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
? `${args.aggregator.task.slice(0, 40)}...`
|
|
213
|
-
: args.aggregator.task;
|
|
214
|
-
text += `\n ${theme.fg("muted", "fan-in → ")}${theme.fg("accent", args.aggregator.agent)}${theme.fg(
|
|
218
|
+
if (hasUsableAggregator(args.aggregator)) {
|
|
219
|
+
const aggregator = args.aggregator;
|
|
220
|
+
text += `\n ${theme.fg("muted", "fan-in → ")}${theme.fg("accent", previewAgent(aggregator.agent))}${theme.fg(
|
|
215
221
|
"dim",
|
|
216
|
-
` ${
|
|
222
|
+
` ${previewTask(aggregator.task)}`,
|
|
217
223
|
)}`;
|
|
218
224
|
}
|
|
219
225
|
return new Text(text, 0, 0);
|
|
220
226
|
}
|
|
221
|
-
const agentName = args.agent
|
|
222
|
-
const preview = args.task
|
|
223
|
-
? args.task.length > 60
|
|
224
|
-
? `${args.task.slice(0, 60)}...`
|
|
225
|
-
: args.task
|
|
226
|
-
: "...";
|
|
227
|
+
const agentName = previewAgent(args.agent);
|
|
228
|
+
const preview = previewTask(args.task, 60);
|
|
227
229
|
let text =
|
|
228
230
|
theme.fg("toolTitle", theme.bold("subagent ")) +
|
|
229
231
|
theme.fg("accent", agentName) +
|
package/src/subagents.ts
CHANGED
|
@@ -41,6 +41,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
41
41
|
"Use the blocking subagent tool only when delegated outputs are required before the main agent's next action and waiting is intentional; the main agent cannot process queued steering until the call returns.",
|
|
42
42
|
"Use a blocking subagent single, parallel, chain, or fan-in call only when synchronous context or output isolation is worth making the main agent unavailable while it runs.",
|
|
43
43
|
"If a blocking parallel subagent call is genuinely required, keep tasks independent, stay within the hard max 8, and avoid write-heavy implementation touching the same files or shared state.",
|
|
44
|
+
"For parallel subagent calls, omit the aggregator key entirely unless a fan-in step is required; do not send null, empty strings, or an empty object for unused optional fields.",
|
|
44
45
|
'Do not use subagent with project-local agents unless the user explicitly wants project agents or sets agentScope to "project" or "both"; keep confirmation enabled for untrusted repositories.',
|
|
45
46
|
"When using subagent, write self-contained tasks with file paths, context, expected output, and whether the subagent may edit files.",
|
|
46
47
|
],
|