@narumitw/pi-subagents 0.28.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/README.md +7 -0
- package/package.json +7 -7
- package/src/agents.ts +14 -14
- package/src/context.ts +13 -16
- package/src/execution.ts +362 -340
- package/src/limits.ts +8 -2
- package/src/params.ts +49 -13
- package/src/render.ts +26 -26
- package/src/settings.ts +1 -2
- package/src/stateful-prompt.ts +2 -5
- package/src/subagents.ts +1 -0
- package/src/workspace.ts +7 -6
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Pi extension for delegating work to specialized isolated subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"typebox": "^1.3.
|
|
32
|
+
"typebox": "^1.3.8"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@earendil-works/pi-agent-core": "*",
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
"@earendil-works/pi-tui": "*"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@biomejs/biome": "2.5.
|
|
42
|
-
"@earendil-works/pi-agent-core": "0.
|
|
43
|
-
"@earendil-works/pi-ai": "0.
|
|
44
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
45
|
-
"@earendil-works/pi-tui": "0.
|
|
41
|
+
"@biomejs/biome": "2.5.5",
|
|
42
|
+
"@earendil-works/pi-agent-core": "0.82.1",
|
|
43
|
+
"@earendil-works/pi-ai": "0.82.1",
|
|
44
|
+
"@earendil-works/pi-coding-agent": "0.82.1",
|
|
45
|
+
"@earendil-works/pi-tui": "0.82.1",
|
|
46
46
|
"typescript": "7.0.2"
|
|
47
47
|
},
|
|
48
48
|
"repository": {
|
package/src/agents.ts
CHANGED
|
@@ -6,15 +6,7 @@ import * as fs from "node:fs";
|
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
|
-
export const THINKING_LEVELS = [
|
|
10
|
-
"off",
|
|
11
|
-
"minimal",
|
|
12
|
-
"low",
|
|
13
|
-
"medium",
|
|
14
|
-
"high",
|
|
15
|
-
"xhigh",
|
|
16
|
-
"max",
|
|
17
|
-
] as const;
|
|
9
|
+
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
18
10
|
|
|
19
11
|
export type SubagentThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
20
12
|
|
|
@@ -72,7 +64,8 @@ export interface SubagentSettings {
|
|
|
72
64
|
const BUILT_IN_AGENTS: AgentConfig[] = [
|
|
73
65
|
{
|
|
74
66
|
name: "scout",
|
|
75
|
-
description:
|
|
67
|
+
description:
|
|
68
|
+
"Read-only codebase reconnaissance; returns concise findings with paths and evidence.",
|
|
76
69
|
tools: ["read", "grep", "find", "ls", "bash"],
|
|
77
70
|
source: "built-in",
|
|
78
71
|
filePath: "built-in:scout",
|
|
@@ -185,7 +178,9 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
|
|
|
185
178
|
description: frontmatter.description,
|
|
186
179
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
187
180
|
model: frontmatter.model,
|
|
188
|
-
thinkingLevel: isThinkingLevel(frontmatter.thinkingLevel)
|
|
181
|
+
thinkingLevel: isThinkingLevel(frontmatter.thinkingLevel)
|
|
182
|
+
? frontmatter.thinkingLevel
|
|
183
|
+
: undefined,
|
|
189
184
|
systemPrompt: body,
|
|
190
185
|
source,
|
|
191
186
|
filePath,
|
|
@@ -228,7 +223,8 @@ export function discoverAgents(
|
|
|
228
223
|
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
229
224
|
|
|
230
225
|
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
231
|
-
const projectAgents =
|
|
226
|
+
const projectAgents =
|
|
227
|
+
scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
232
228
|
|
|
233
229
|
const agentMap = new Map<string, AgentConfig>();
|
|
234
230
|
|
|
@@ -258,7 +254,8 @@ export function discoverAgents(
|
|
|
258
254
|
nextAgent.model = override.model === null ? undefined : override.model;
|
|
259
255
|
}
|
|
260
256
|
if (hasOwn(override, "thinkingLevel")) {
|
|
261
|
-
nextAgent.thinkingLevel =
|
|
257
|
+
nextAgent.thinkingLevel =
|
|
258
|
+
override.thinkingLevel === null ? undefined : override.thinkingLevel;
|
|
262
259
|
}
|
|
263
260
|
if (hasOwn(override, "timeoutMs")) {
|
|
264
261
|
nextAgent.timeoutMs = override.timeoutMs === null ? undefined : override.timeoutMs;
|
|
@@ -269,7 +266,10 @@ export function discoverAgents(
|
|
|
269
266
|
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
270
267
|
}
|
|
271
268
|
|
|
272
|
-
export function formatAgentList(
|
|
269
|
+
export function formatAgentList(
|
|
270
|
+
agents: AgentConfig[],
|
|
271
|
+
maxItems: number,
|
|
272
|
+
): { text: string; remaining: number } {
|
|
273
273
|
if (agents.length === 0) return { text: "none", remaining: 0 };
|
|
274
274
|
const listed = agents.slice(0, maxItems);
|
|
275
275
|
const remaining = agents.length - listed.length;
|
package/src/context.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
DEFAULT_MAX_CONTEXT_BYTES,
|
|
4
|
-
truncateUtf8,
|
|
5
|
-
truncateUtf8Tail,
|
|
6
|
-
} from "./limits.js";
|
|
2
|
+
import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8, truncateUtf8Tail } from "./limits.js";
|
|
7
3
|
|
|
8
4
|
export type ContextMode = "none" | "all" | "summary" | number;
|
|
9
5
|
|
|
@@ -107,19 +103,20 @@ export function buildContextSnapshot(
|
|
|
107
103
|
const raw =
|
|
108
104
|
mode === "summary" && selected.length > 4
|
|
109
105
|
? [
|
|
110
|
-
`## Earlier context checkpoint\n${
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
106
|
+
`## Earlier context checkpoint\n${
|
|
107
|
+
truncateUtf8(
|
|
108
|
+
selected
|
|
109
|
+
.slice(0, -4)
|
|
110
|
+
.map((message) => `${message.role}: ${message.text}`)
|
|
111
|
+
.join("\n"),
|
|
112
|
+
Math.floor(maxBytes / 3),
|
|
113
|
+
).text
|
|
114
|
+
}`,
|
|
115
|
+
...selected.slice(-4).map((message) => `## ${message.role}\n${message.text}`),
|
|
120
116
|
].join("\n\n")
|
|
121
117
|
: selected.map((message) => `## ${message.role}\n${message.text}`).join("\n\n");
|
|
122
|
-
const bounded =
|
|
118
|
+
const bounded =
|
|
119
|
+
mode === "summary" ? truncateUtf8Tail(raw, maxBytes) : truncateUtf8(raw, maxBytes);
|
|
123
120
|
return {
|
|
124
121
|
text: bounded.text,
|
|
125
122
|
turns: selected.filter((message) => message.role === "user").length,
|
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,
|
|
@@ -105,376 +105,398 @@ export async function executeSubagent(
|
|
|
105
105
|
onUpdate: AgentToolUpdateCallback<SubagentDetails> | undefined,
|
|
106
106
|
ctx: ExtensionContext,
|
|
107
107
|
): Promise<AgentToolResult<SubagentDetails> & { isError?: boolean }> {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
108
|
+
assertSubagentDepthAllowed();
|
|
109
|
+
const agentScope: AgentScope = params.agentScope ?? "user";
|
|
110
|
+
const aggregator = hasUsableAggregator(params.aggregator) ? params.aggregator : undefined;
|
|
111
|
+
const config = readSubagentSettings();
|
|
112
|
+
const discovery = discoverAgents(ctx.cwd, agentScope, config);
|
|
113
|
+
const agents = discovery.agents;
|
|
114
|
+
const confirmProjectAgents = params.confirmProjectAgents ?? true;
|
|
115
|
+
const resolveTimeoutMs = (agentName: string, localTimeoutMs?: number) =>
|
|
116
|
+
localTimeoutMs ??
|
|
117
|
+
params.timeoutMs ??
|
|
118
|
+
agents.find((agent) => agent.name === agentName)?.timeoutMs ??
|
|
119
|
+
resolveDefaultSubagentTimeoutMs();
|
|
120
|
+
const resolveThinkingLevel = (agentName: string, localThinkingLevel?: SubagentThinkingLevel) =>
|
|
121
|
+
resolveSubagentThinkingLevel(agents, agentName, params.thinkingLevel, localThinkingLevel);
|
|
122
|
+
|
|
123
|
+
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
124
|
+
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
125
|
+
const hasSingle = Boolean(params.agent && params.task);
|
|
126
|
+
const modeCount = Number(hasChain) + Number(hasTasks) + Number(hasSingle);
|
|
127
|
+
|
|
128
|
+
const makeDetails =
|
|
129
|
+
(mode: "single" | "parallel" | "chain") =>
|
|
130
|
+
(results: SingleResult[], aggregator?: SingleResult): SubagentDetails => ({
|
|
131
|
+
mode,
|
|
132
|
+
agentScope,
|
|
133
|
+
projectAgentsDir: discovery.projectAgentsDir,
|
|
134
|
+
results,
|
|
135
|
+
aggregator,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (modeCount !== 1 || (aggregator && !hasTasks)) {
|
|
139
|
+
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
140
|
+
const reason =
|
|
141
|
+
modeCount !== 1
|
|
142
|
+
? "Provide exactly one mode."
|
|
143
|
+
: "Aggregator is only valid with parallel tasks.";
|
|
144
|
+
return {
|
|
145
|
+
content: [
|
|
146
|
+
{
|
|
147
|
+
type: "text",
|
|
148
|
+
text: `Invalid parameters. ${reason}\nAvailable agents: ${available}`,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
details: makeDetails("single")([]),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
153
154
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const projectAgentsRequested = Array.from(requestedAgentNames)
|
|
162
|
-
.map((name) => agents.find((a) => a.name === name))
|
|
163
|
-
.filter((a): a is AgentConfig => a?.source === "project");
|
|
164
|
-
|
|
165
|
-
if (projectAgentsRequested.length > 0) {
|
|
166
|
-
if (!ctx.isProjectTrusted()) {
|
|
167
|
-
throw new Error("Project-local subagent definitions require a trusted project");
|
|
168
|
-
}
|
|
169
|
-
if (confirmProjectAgents && ctx.hasUI) {
|
|
170
|
-
const names = projectAgentsRequested.map((a) => a.name).join(", ");
|
|
171
|
-
const dir = discovery.projectAgentsDir ?? "(unknown)";
|
|
172
|
-
const ok = await ctx.ui.confirm(
|
|
173
|
-
"Run project-local agents?",
|
|
174
|
-
`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
|
175
|
-
);
|
|
176
|
-
if (!ok) {
|
|
177
|
-
return {
|
|
178
|
-
content: [{ type: "text", text: "Canceled: project-local agents not approved." }],
|
|
179
|
-
details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
155
|
+
if (agentScope === "project" || agentScope === "both") {
|
|
156
|
+
const requestedAgentNames = new Set<string>();
|
|
157
|
+
if (params.chain) for (const step of params.chain) requestedAgentNames.add(step.agent);
|
|
158
|
+
if (params.tasks) for (const t of params.tasks) requestedAgentNames.add(t.agent);
|
|
159
|
+
if (aggregator) requestedAgentNames.add(aggregator.agent);
|
|
160
|
+
if (params.agent) requestedAgentNames.add(params.agent);
|
|
185
161
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const status = startSubagentStatus(ctx, toolCallId, chainStatus(0, params.chain.length));
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
for (let i = 0; i < params.chain.length; i++) {
|
|
193
|
-
const step = params.chain[i];
|
|
194
|
-
status.update(chainStatus(i + 1, params.chain.length, step.agent));
|
|
195
|
-
const taskWithContext = truncateUtf8(
|
|
196
|
-
step.task.replace(/\{previous\}/g, previousOutput),
|
|
197
|
-
DEFAULT_MAX_CONTEXT_BYTES,
|
|
198
|
-
).text;
|
|
199
|
-
|
|
200
|
-
// Create update callback that includes all previous results
|
|
201
|
-
const chainUpdate: OnUpdateCallback | undefined = onUpdate
|
|
202
|
-
? (partial) => {
|
|
203
|
-
// Combine completed results with current streaming result
|
|
204
|
-
const currentResult = partial.details?.results[0];
|
|
205
|
-
if (currentResult) {
|
|
206
|
-
const allResults = [...results, currentResult];
|
|
207
|
-
onUpdate({
|
|
208
|
-
content: partial.content,
|
|
209
|
-
details: makeDetails("chain")(allResults),
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
: undefined;
|
|
162
|
+
const projectAgentsRequested = Array.from(requestedAgentNames)
|
|
163
|
+
.map((name) => agents.find((a) => a.name === name))
|
|
164
|
+
.filter((a): a is AgentConfig => a?.source === "project");
|
|
214
165
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
);
|
|
228
|
-
results.push(result);
|
|
229
|
-
|
|
230
|
-
const isError = isResultError(result);
|
|
231
|
-
if (isError) {
|
|
232
|
-
const errorMsg = formatResultFailure(result);
|
|
233
|
-
return {
|
|
234
|
-
content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
|
|
235
|
-
details: { ...makeDetails("chain")(results), isError: true },
|
|
236
|
-
isError: true,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
previousOutput = getResultFinalOutput(result);
|
|
240
|
-
}
|
|
166
|
+
if (projectAgentsRequested.length > 0) {
|
|
167
|
+
if (!ctx.isProjectTrusted()) {
|
|
168
|
+
throw new Error("Project-local subagent definitions require a trusted project");
|
|
169
|
+
}
|
|
170
|
+
if (confirmProjectAgents && ctx.hasUI) {
|
|
171
|
+
const names = projectAgentsRequested.map((a) => a.name).join(", ");
|
|
172
|
+
const dir = discovery.projectAgentsDir ?? "(unknown)";
|
|
173
|
+
const ok = await ctx.ui.confirm(
|
|
174
|
+
"Run project-local agents?",
|
|
175
|
+
`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
|
176
|
+
);
|
|
177
|
+
if (!ok) {
|
|
241
178
|
return {
|
|
242
|
-
content: [{ type: "text", text:
|
|
243
|
-
details: makeDetails("chain")(
|
|
179
|
+
content: [{ type: "text", text: "Canceled: project-local agents not approved." }],
|
|
180
|
+
details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
|
|
244
181
|
};
|
|
245
|
-
} finally {
|
|
246
|
-
status.clear();
|
|
247
182
|
}
|
|
248
183
|
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
249
186
|
|
|
250
|
-
|
|
251
|
-
|
|
187
|
+
if (params.chain && params.chain.length > 0) {
|
|
188
|
+
const results: SingleResult[] = [];
|
|
189
|
+
let previousOutput = "";
|
|
190
|
+
const status = startSubagentStatus(ctx, toolCallId, chainStatus(0, params.chain.length));
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
for (let i = 0; i < params.chain.length; i++) {
|
|
194
|
+
const step = params.chain[i];
|
|
195
|
+
status.update(chainStatus(i + 1, params.chain.length, step.agent));
|
|
196
|
+
const taskWithContext = truncateUtf8(
|
|
197
|
+
step.task.replace(/\{previous\}/g, previousOutput),
|
|
198
|
+
DEFAULT_MAX_CONTEXT_BYTES,
|
|
199
|
+
).text;
|
|
200
|
+
|
|
201
|
+
// Create update callback that includes all previous results
|
|
202
|
+
const chainUpdate: OnUpdateCallback | undefined = onUpdate
|
|
203
|
+
? (partial) => {
|
|
204
|
+
// Combine completed results with current streaming result
|
|
205
|
+
const currentResult = partial.details?.results[0];
|
|
206
|
+
if (currentResult) {
|
|
207
|
+
const allResults = [...results, currentResult];
|
|
208
|
+
onUpdate({
|
|
209
|
+
content: partial.content,
|
|
210
|
+
details: makeDetails("chain")(allResults),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
: undefined;
|
|
215
|
+
|
|
216
|
+
const result = await runSingleAgent(
|
|
217
|
+
ctx.cwd,
|
|
218
|
+
agents,
|
|
219
|
+
step.agent,
|
|
220
|
+
taskWithContext,
|
|
221
|
+
step.cwd,
|
|
222
|
+
i + 1,
|
|
223
|
+
signal,
|
|
224
|
+
resolveThinkingLevel(step.agent, step.thinkingLevel),
|
|
225
|
+
resolveTimeoutMs(step.agent, step.timeoutMs),
|
|
226
|
+
chainUpdate,
|
|
227
|
+
makeDetails("chain"),
|
|
228
|
+
);
|
|
229
|
+
results.push(result);
|
|
230
|
+
|
|
231
|
+
const isError = isResultError(result);
|
|
232
|
+
if (isError) {
|
|
233
|
+
const errorMsg = formatResultFailure(result);
|
|
252
234
|
return {
|
|
253
235
|
content: [
|
|
254
|
-
{
|
|
255
|
-
type: "text",
|
|
256
|
-
text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
|
|
257
|
-
},
|
|
236
|
+
{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` },
|
|
258
237
|
],
|
|
259
|
-
details: makeDetails("
|
|
238
|
+
details: { ...makeDetails("chain")(results), isError: true },
|
|
239
|
+
isError: true,
|
|
260
240
|
};
|
|
241
|
+
}
|
|
242
|
+
previousOutput = getResultFinalOutput(result);
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{
|
|
247
|
+
type: "text",
|
|
248
|
+
text: getResultFinalOutput(results[results.length - 1]) || "(no output)",
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
details: makeDetails("chain")(results),
|
|
252
|
+
};
|
|
253
|
+
} finally {
|
|
254
|
+
status.clear();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
261
257
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
task: params.tasks[i].task,
|
|
274
|
-
exitCode: -1, // -1 = still running
|
|
275
|
-
messages: [],
|
|
276
|
-
stderr: "",
|
|
277
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
278
|
-
thinkingLevel: resolveThinkingLevel(params.tasks[i].agent, params.tasks[i].thinkingLevel),
|
|
279
|
-
finalOutput: "",
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
let doneCount = 0;
|
|
284
|
-
let runningCount = params.tasks.length;
|
|
285
|
-
|
|
286
|
-
const emitParallelUpdate = () => {
|
|
287
|
-
status.update(parallelStatus(doneCount, allResults.length, runningCount));
|
|
288
|
-
if (onUpdate) {
|
|
289
|
-
const aggregator = params.aggregator;
|
|
290
|
-
const pendingAggregator: SingleResult | undefined =
|
|
291
|
-
aggregator && !signal?.aborted && doneCount === allResults.length
|
|
292
|
-
? {
|
|
293
|
-
agent: aggregator.agent,
|
|
294
|
-
agentSource:
|
|
295
|
-
agents.find((agent) => agent.name === aggregator.agent)?.source ?? "unknown",
|
|
296
|
-
task: aggregator.task,
|
|
297
|
-
exitCode: -1,
|
|
298
|
-
messages: [],
|
|
299
|
-
stderr: "",
|
|
300
|
-
usage: {
|
|
301
|
-
input: 0,
|
|
302
|
-
output: 0,
|
|
303
|
-
cacheRead: 0,
|
|
304
|
-
cacheWrite: 0,
|
|
305
|
-
cost: 0,
|
|
306
|
-
contextTokens: 0,
|
|
307
|
-
turns: 0,
|
|
308
|
-
},
|
|
309
|
-
thinkingLevel: resolveThinkingLevel(
|
|
310
|
-
aggregator.agent,
|
|
311
|
-
aggregator.thinkingLevel,
|
|
312
|
-
),
|
|
313
|
-
timeoutMs: resolveTimeoutMs(aggregator.agent, aggregator.timeoutMs),
|
|
314
|
-
finalOutput: "",
|
|
315
|
-
}
|
|
316
|
-
: undefined;
|
|
317
|
-
onUpdate({
|
|
318
|
-
content: [
|
|
319
|
-
{
|
|
320
|
-
type: "text",
|
|
321
|
-
text: `Parallel: ${doneCount}/${allResults.length} done, ${runningCount} running...`,
|
|
322
|
-
},
|
|
323
|
-
],
|
|
324
|
-
details: makeDetails("parallel")([...allResults], pendingAggregator),
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
};
|
|
258
|
+
if (params.tasks && params.tasks.length > 0) {
|
|
259
|
+
if (params.tasks.length > MAX_PARALLEL_TASKS)
|
|
260
|
+
return {
|
|
261
|
+
content: [
|
|
262
|
+
{
|
|
263
|
+
type: "text",
|
|
264
|
+
text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
details: makeDetails("parallel")([]),
|
|
268
|
+
};
|
|
328
269
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
errorMessage: "Subagent was not started because the parent call was aborted",
|
|
362
|
-
};
|
|
363
|
-
allResults[index] = skipped;
|
|
364
|
-
doneCount += 1;
|
|
365
|
-
runningCount -= 1;
|
|
366
|
-
emitParallelUpdate();
|
|
367
|
-
return skipped;
|
|
368
|
-
});
|
|
270
|
+
const status = startSubagentStatus(
|
|
271
|
+
ctx,
|
|
272
|
+
toolCallId,
|
|
273
|
+
parallelStatus(0, params.tasks.length, params.tasks.length),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
// Track all results for streaming updates
|
|
278
|
+
const allResults: SingleResult[] = new Array(params.tasks.length);
|
|
279
|
+
|
|
280
|
+
// Initialize placeholder results
|
|
281
|
+
for (let i = 0; i < params.tasks.length; i++) {
|
|
282
|
+
allResults[i] = {
|
|
283
|
+
agent: params.tasks[i].agent,
|
|
284
|
+
agentSource: "unknown",
|
|
285
|
+
task: params.tasks[i].task,
|
|
286
|
+
exitCode: -1, // -1 = still running
|
|
287
|
+
messages: [],
|
|
288
|
+
stderr: "",
|
|
289
|
+
usage: {
|
|
290
|
+
input: 0,
|
|
291
|
+
output: 0,
|
|
292
|
+
cacheRead: 0,
|
|
293
|
+
cacheWrite: 0,
|
|
294
|
+
cost: 0,
|
|
295
|
+
contextTokens: 0,
|
|
296
|
+
turns: 0,
|
|
297
|
+
},
|
|
298
|
+
thinkingLevel: resolveThinkingLevel(params.tasks[i].agent, params.tasks[i].thinkingLevel),
|
|
299
|
+
finalOutput: "",
|
|
300
|
+
};
|
|
301
|
+
}
|
|
369
302
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
303
|
+
let doneCount = 0;
|
|
304
|
+
let runningCount = params.tasks.length;
|
|
305
|
+
|
|
306
|
+
const emitParallelUpdate = () => {
|
|
307
|
+
status.update(parallelStatus(doneCount, allResults.length, runningCount));
|
|
308
|
+
if (onUpdate) {
|
|
309
|
+
const pendingAggregator: SingleResult | undefined =
|
|
310
|
+
aggregator && !signal?.aborted && doneCount === allResults.length
|
|
311
|
+
? {
|
|
312
|
+
agent: aggregator.agent,
|
|
313
|
+
agentSource:
|
|
314
|
+
agents.find((agent) => agent.name === aggregator.agent)?.source ?? "unknown",
|
|
315
|
+
task: aggregator.task,
|
|
316
|
+
exitCode: -1,
|
|
317
|
+
messages: [],
|
|
318
|
+
stderr: "",
|
|
319
|
+
usage: {
|
|
320
|
+
input: 0,
|
|
321
|
+
output: 0,
|
|
322
|
+
cacheRead: 0,
|
|
323
|
+
cacheWrite: 0,
|
|
324
|
+
cost: 0,
|
|
325
|
+
contextTokens: 0,
|
|
326
|
+
turns: 0,
|
|
327
|
+
},
|
|
328
|
+
thinkingLevel: resolveThinkingLevel(aggregator.agent, aggregator.thinkingLevel),
|
|
329
|
+
timeoutMs: resolveTimeoutMs(aggregator.agent, aggregator.timeoutMs),
|
|
330
|
+
finalOutput: "",
|
|
398
331
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const successCount = results.filter((result) => !isResultError(result)).length;
|
|
405
|
-
const summaries = results.map((result) => {
|
|
406
|
-
const failed = isResultError(result);
|
|
407
|
-
const output = getResultFinalOutput(result);
|
|
408
|
-
const error = result.errorMessage || result.stderr.trim();
|
|
409
|
-
const summaryText = failed ? formatResultFailure(result) : output || error;
|
|
410
|
-
const preview = truncateUtf8(summaryText, 160).text;
|
|
411
|
-
return `[${result.agent}] ${failed ? "failed" : "completed"}: ${preview || "(no output)"}`;
|
|
412
|
-
});
|
|
413
|
-
const aggregatorFailed = aggregatorResult ? isResultError(aggregatorResult) : false;
|
|
414
|
-
const aggregatorOutput = aggregatorResult ? getResultFinalOutput(aggregatorResult) : "";
|
|
415
|
-
const aggregatorError = aggregatorResult?.errorMessage || aggregatorResult?.stderr.trim() || "";
|
|
416
|
-
return {
|
|
332
|
+
: undefined;
|
|
333
|
+
onUpdate({
|
|
417
334
|
content: [
|
|
418
335
|
{
|
|
419
336
|
type: "text",
|
|
420
|
-
text:
|
|
421
|
-
? aggregatorFailed
|
|
422
|
-
? formatResultFailure(aggregatorResult)
|
|
423
|
-
: aggregatorOutput ||
|
|
424
|
-
aggregatorError ||
|
|
425
|
-
`(aggregator ${aggregatorResult.agent} produced no output)`
|
|
426
|
-
: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
|
|
337
|
+
text: `Parallel: ${doneCount}/${allResults.length} done, ${runningCount} running...`,
|
|
427
338
|
},
|
|
428
339
|
],
|
|
429
|
-
details:
|
|
430
|
-
|
|
431
|
-
isError: aggregatorFailed,
|
|
432
|
-
},
|
|
433
|
-
isError: aggregatorResult ? aggregatorFailed : undefined,
|
|
434
|
-
};
|
|
435
|
-
} finally {
|
|
436
|
-
status.clear();
|
|
340
|
+
details: makeDetails("parallel")([...allResults], pendingAggregator),
|
|
341
|
+
});
|
|
437
342
|
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
if (params.agent && params.task) {
|
|
441
|
-
const status = startSubagentStatus(ctx, toolCallId, singleStatus(params.agent));
|
|
343
|
+
};
|
|
442
344
|
|
|
443
|
-
|
|
345
|
+
const results = await mapWithConcurrencyLimit(
|
|
346
|
+
params.tasks,
|
|
347
|
+
MAX_CONCURRENCY,
|
|
348
|
+
async (t, index) => {
|
|
444
349
|
const result = await runSingleAgent(
|
|
445
350
|
ctx.cwd,
|
|
446
351
|
agents,
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
352
|
+
t.agent,
|
|
353
|
+
t.task,
|
|
354
|
+
t.cwd,
|
|
450
355
|
undefined,
|
|
451
356
|
signal,
|
|
452
|
-
resolveThinkingLevel(
|
|
453
|
-
resolveTimeoutMs(
|
|
454
|
-
|
|
455
|
-
|
|
357
|
+
resolveThinkingLevel(t.agent, t.thinkingLevel),
|
|
358
|
+
resolveTimeoutMs(t.agent, t.timeoutMs),
|
|
359
|
+
// Per-task update callback
|
|
360
|
+
(partial) => {
|
|
361
|
+
if (partial.details?.results[0]) {
|
|
362
|
+
allResults[index] = { ...partial.details.results[0], exitCode: -1 };
|
|
363
|
+
emitParallelUpdate();
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
makeDetails("parallel"),
|
|
456
367
|
);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
368
|
+
allResults[index] = result;
|
|
369
|
+
doneCount += 1;
|
|
370
|
+
runningCount -= 1;
|
|
371
|
+
emitParallelUpdate();
|
|
372
|
+
return result;
|
|
373
|
+
},
|
|
374
|
+
signal,
|
|
375
|
+
(task, index) => {
|
|
376
|
+
const skipped: SingleResult = {
|
|
377
|
+
...allResults[index],
|
|
378
|
+
task: task.task,
|
|
379
|
+
exitCode: 130,
|
|
380
|
+
stopReason: "aborted",
|
|
381
|
+
aborted: true,
|
|
382
|
+
errorMessage: "Subagent was not started because the parent call was aborted",
|
|
469
383
|
};
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
384
|
+
allResults[index] = skipped;
|
|
385
|
+
doneCount += 1;
|
|
386
|
+
runningCount -= 1;
|
|
387
|
+
emitParallelUpdate();
|
|
388
|
+
return skipped;
|
|
389
|
+
},
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
let aggregatorResult: SingleResult | undefined;
|
|
393
|
+
if (aggregator && !signal?.aborted) {
|
|
394
|
+
status.update(fanInStatus(aggregator.agent));
|
|
395
|
+
const fanInContext = buildFanInContext(results);
|
|
396
|
+
const aggregatorTask = truncateUtf8(
|
|
397
|
+
aggregator.task.includes("{previous}")
|
|
398
|
+
? aggregator.task.replace(/\{previous\}/g, fanInContext)
|
|
399
|
+
: `${aggregator.task}\n\nParallel task outputs:\n\n${fanInContext}`,
|
|
400
|
+
DEFAULT_MAX_CONTEXT_BYTES,
|
|
401
|
+
).text;
|
|
402
|
+
aggregatorResult = await runSingleAgent(
|
|
403
|
+
ctx.cwd,
|
|
404
|
+
agents,
|
|
405
|
+
aggregator.agent,
|
|
406
|
+
aggregatorTask,
|
|
407
|
+
aggregator.cwd,
|
|
408
|
+
undefined,
|
|
409
|
+
signal,
|
|
410
|
+
resolveThinkingLevel(aggregator.agent, aggregator.thinkingLevel),
|
|
411
|
+
resolveTimeoutMs(aggregator.agent, aggregator.timeoutMs),
|
|
412
|
+
(partial) => {
|
|
413
|
+
status.update(fanInStatus(aggregator.agent));
|
|
414
|
+
if (onUpdate && partial.details?.results[0]) {
|
|
415
|
+
onUpdate({
|
|
416
|
+
content: partial.content,
|
|
417
|
+
details: makeDetails("parallel")(results, partial.details.results[0]),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
makeDetails("parallel"),
|
|
422
|
+
);
|
|
473
423
|
}
|
|
474
424
|
|
|
475
|
-
const
|
|
425
|
+
const successCount = results.filter((result) => !isResultError(result)).length;
|
|
426
|
+
const summaries = results.map((result) => {
|
|
427
|
+
const failed = isResultError(result);
|
|
428
|
+
const output = getResultFinalOutput(result);
|
|
429
|
+
const error = result.errorMessage || result.stderr.trim();
|
|
430
|
+
const summaryText = failed ? formatResultFailure(result) : output || error;
|
|
431
|
+
const preview = truncateUtf8(summaryText, 160).text;
|
|
432
|
+
return `[${result.agent}] ${failed ? "failed" : "completed"}: ${preview || "(no output)"}`;
|
|
433
|
+
});
|
|
434
|
+
const aggregatorFailed = aggregatorResult ? isResultError(aggregatorResult) : false;
|
|
435
|
+
const aggregatorOutput = aggregatorResult ? getResultFinalOutput(aggregatorResult) : "";
|
|
436
|
+
const aggregatorError =
|
|
437
|
+
aggregatorResult?.errorMessage || aggregatorResult?.stderr.trim() || "";
|
|
438
|
+
return {
|
|
439
|
+
content: [
|
|
440
|
+
{
|
|
441
|
+
type: "text",
|
|
442
|
+
text: aggregatorResult
|
|
443
|
+
? aggregatorFailed
|
|
444
|
+
? formatResultFailure(aggregatorResult)
|
|
445
|
+
: aggregatorOutput ||
|
|
446
|
+
aggregatorError ||
|
|
447
|
+
`(aggregator ${aggregatorResult.agent} produced no output)`
|
|
448
|
+
: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
details: {
|
|
452
|
+
...makeDetails("parallel")(results, aggregatorResult),
|
|
453
|
+
isError: aggregatorFailed,
|
|
454
|
+
},
|
|
455
|
+
isError: aggregatorResult ? aggregatorFailed : undefined,
|
|
456
|
+
};
|
|
457
|
+
} finally {
|
|
458
|
+
status.clear();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (params.agent && params.task) {
|
|
463
|
+
const status = startSubagentStatus(ctx, toolCallId, singleStatus(params.agent));
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
const result = await runSingleAgent(
|
|
467
|
+
ctx.cwd,
|
|
468
|
+
agents,
|
|
469
|
+
params.agent,
|
|
470
|
+
params.task,
|
|
471
|
+
params.cwd,
|
|
472
|
+
undefined,
|
|
473
|
+
signal,
|
|
474
|
+
resolveThinkingLevel(params.agent, params.thinkingLevel),
|
|
475
|
+
resolveTimeoutMs(params.agent, params.timeoutMs),
|
|
476
|
+
onUpdate,
|
|
477
|
+
makeDetails("single"),
|
|
478
|
+
);
|
|
479
|
+
const isError = isResultError(result);
|
|
480
|
+
if (isError) {
|
|
481
|
+
const errorMsg = formatResultFailure(result);
|
|
482
|
+
return {
|
|
483
|
+
content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
|
|
484
|
+
details: { ...makeDetails("single")([result]), isError: true },
|
|
485
|
+
isError: true,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
476
488
|
return {
|
|
477
|
-
content: [{ type: "text", text:
|
|
478
|
-
details: makeDetails("single")([]),
|
|
489
|
+
content: [{ type: "text", text: getResultFinalOutput(result) || "(no output)" }],
|
|
490
|
+
details: makeDetails("single")([result]),
|
|
479
491
|
};
|
|
492
|
+
} finally {
|
|
493
|
+
status.clear();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
498
|
+
return {
|
|
499
|
+
content: [{ type: "text", text: `Invalid parameters. Available agents: ${available}` }],
|
|
500
|
+
details: makeDetails("single")([]),
|
|
501
|
+
};
|
|
480
502
|
}
|
package/src/limits.ts
CHANGED
|
@@ -31,7 +31,10 @@ export function truncateUtf8(text: string, maxBytes: number): BoundedText {
|
|
|
31
31
|
originalBytes,
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
const prefix = Buffer.from(text, "utf8")
|
|
34
|
+
const prefix = Buffer.from(text, "utf8")
|
|
35
|
+
.subarray(0, limit - marker.length)
|
|
36
|
+
.toString("utf8")
|
|
37
|
+
.replace(/�+$/g, "");
|
|
35
38
|
return { text: `${prefix}${TRUNCATION_MARKER}`, truncated: true, originalBytes };
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -45,7 +48,10 @@ export function truncateUtf8Tail(text: string, maxBytes: number): BoundedText {
|
|
|
45
48
|
const marker = Buffer.from(TAIL_TRUNCATION_MARKER, "utf8");
|
|
46
49
|
if (marker.length >= limit) {
|
|
47
50
|
return {
|
|
48
|
-
text: bytes
|
|
51
|
+
text: bytes
|
|
52
|
+
.subarray(originalBytes - limit)
|
|
53
|
+
.toString("utf8")
|
|
54
|
+
.replace(/^�+/g, ""),
|
|
49
55
|
truncated: true,
|
|
50
56
|
originalBytes,
|
|
51
57
|
};
|
package/src/params.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
-
import {
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
3
|
import { THINKING_LEVELS } from "./agents.js";
|
|
4
4
|
|
|
5
5
|
const TimeoutMs = Type.Number({
|
|
@@ -29,13 +29,25 @@ 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({
|
|
35
|
+
description: "Name of the fan-in agent to invoke after parallel tasks complete",
|
|
36
|
+
}),
|
|
37
|
+
task: Type.String({
|
|
38
|
+
description: "Fan-in task. Use {previous} to include all parallel outputs.",
|
|
39
|
+
}),
|
|
40
|
+
cwd: Type.Optional(
|
|
41
|
+
Type.String({ description: "Working directory for the aggregator process" }),
|
|
42
|
+
),
|
|
43
|
+
timeoutMs: Type.Optional(TimeoutMs),
|
|
44
|
+
thinkingLevel: Type.Optional(ThinkingLevelSchema),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
description:
|
|
48
|
+
"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.",
|
|
49
|
+
},
|
|
50
|
+
);
|
|
39
51
|
|
|
40
52
|
const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
|
|
41
53
|
description:
|
|
@@ -44,18 +56,42 @@ const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
|
|
|
44
56
|
});
|
|
45
57
|
|
|
46
58
|
export const SubagentParams = Type.Object({
|
|
47
|
-
agent: Type.Optional(
|
|
59
|
+
agent: Type.Optional(
|
|
60
|
+
Type.String({ description: "Name of the agent to invoke (for single mode)" }),
|
|
61
|
+
),
|
|
48
62
|
task: Type.Optional(Type.String({ description: "Task to delegate (for single mode)" })),
|
|
49
|
-
tasks: Type.Optional(
|
|
50
|
-
|
|
63
|
+
tasks: Type.Optional(
|
|
64
|
+
Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" }),
|
|
65
|
+
),
|
|
66
|
+
chain: Type.Optional(
|
|
67
|
+
Type.Array(ChainItem, { description: "Array of {agent, task} for sequential execution" }),
|
|
68
|
+
),
|
|
51
69
|
aggregator: Type.Optional(AggregatorItem),
|
|
52
70
|
agentScope: Type.Optional(AgentScopeSchema),
|
|
53
71
|
confirmProjectAgents: Type.Optional(
|
|
54
|
-
Type.Boolean({
|
|
72
|
+
Type.Boolean({
|
|
73
|
+
description: "Prompt before running project-local agents. Default: true.",
|
|
74
|
+
default: true,
|
|
75
|
+
}),
|
|
76
|
+
),
|
|
77
|
+
cwd: Type.Optional(
|
|
78
|
+
Type.String({ description: "Working directory for the agent process (single mode)" }),
|
|
55
79
|
),
|
|
56
|
-
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
|
|
57
80
|
timeoutMs: Type.Optional(TimeoutMs),
|
|
58
81
|
thinkingLevel: Type.Optional(ThinkingLevelSchema),
|
|
59
82
|
});
|
|
60
83
|
|
|
61
84
|
export type SubagentParams = Static<typeof SubagentParams>;
|
|
85
|
+
|
|
86
|
+
export function hasUsableAggregator(
|
|
87
|
+
aggregator: unknown,
|
|
88
|
+
): aggregator is { agent: string; task: string } {
|
|
89
|
+
if (!aggregator || typeof aggregator !== "object") return false;
|
|
90
|
+
const candidate = aggregator as { agent?: unknown; task?: unknown };
|
|
91
|
+
return (
|
|
92
|
+
typeof candidate.agent === "string" &&
|
|
93
|
+
candidate.agent.trim().length > 0 &&
|
|
94
|
+
typeof candidate.task === "string" &&
|
|
95
|
+
candidate.task.trim().length > 0
|
|
96
|
+
);
|
|
97
|
+
}
|
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) +
|
|
@@ -438,8 +440,7 @@ export function renderSubagentResult(
|
|
|
438
440
|
const collapsed = getCollapsedDisplayItems(r);
|
|
439
441
|
const finalOutput = getResultFinalOutput(r).trim();
|
|
440
442
|
text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
441
|
-
if (rFailed && r.errorMessage)
|
|
442
|
-
text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
443
|
+
if (rFailed && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
443
444
|
if (collapsed.items.length > 0)
|
|
444
445
|
text += `\n${renderDisplayItems(collapsed.items, 5, collapsed.total)}`;
|
|
445
446
|
else if (currentIsRunning && r === currentResult)
|
|
@@ -603,8 +604,7 @@ export function renderSubagentResult(
|
|
|
603
604
|
const collapsed = getCollapsedDisplayItems(r);
|
|
604
605
|
const finalOutput = getResultFinalOutput(r).trim();
|
|
605
606
|
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
606
|
-
if (rFailed && r.errorMessage)
|
|
607
|
-
text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
607
|
+
if (rFailed && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
608
608
|
if (collapsed.items.length > 0)
|
|
609
609
|
text += `\n${renderDisplayItems(collapsed.items, 5, collapsed.total)}`;
|
|
610
610
|
else if (rRunning) text += `\n${theme.fg("muted", "(running...)")}`;
|
package/src/settings.ts
CHANGED
|
@@ -261,8 +261,7 @@ export function inspectCompletionDeliverySettings(): CompletionDeliverySettingsS
|
|
|
261
261
|
const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
262
262
|
const settings = normalizeSubagentSettings(raw);
|
|
263
263
|
if (!settings) throw new Error(`${SETTINGS_FILE} is not a valid settings object`);
|
|
264
|
-
const explicit =
|
|
265
|
-
isPlainObject(raw.stateful) && hasOwn(raw.stateful, "completionDelivery");
|
|
264
|
+
const explicit = isPlainObject(raw.stateful) && hasOwn(raw.stateful, "completionDelivery");
|
|
266
265
|
return {
|
|
267
266
|
path: configPath,
|
|
268
267
|
value: settings.stateful?.completionDelivery ?? DEFAULT_COMPLETION_DELIVERY,
|
package/src/stateful-prompt.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { AgentConfig } from "./agents.js";
|
|
2
|
+
import { redactPrivateText } from "./context.js";
|
|
2
3
|
import { resolveDefaultSubagentTimeoutMs } from "./execution.js";
|
|
3
4
|
import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
|
|
4
|
-
import { redactPrivateText } from "./context.js";
|
|
5
5
|
import type { ManagedAgent } from "./registry.js";
|
|
6
6
|
|
|
7
7
|
export function buildStatefulTurnPrompt(
|
|
8
|
-
record: Pick<
|
|
9
|
-
ManagedAgent,
|
|
10
|
-
"context" | "history" | "mailbox" | "currentMailboxMessageIds"
|
|
11
|
-
>,
|
|
8
|
+
record: Pick<ManagedAgent, "context" | "history" | "mailbox" | "currentMailboxMessageIds">,
|
|
12
9
|
task: string,
|
|
13
10
|
maxBytes = DEFAULT_MAX_CONTEXT_BYTES,
|
|
14
11
|
): { text: string; truncated: boolean } {
|
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
|
],
|
package/src/workspace.ts
CHANGED
|
@@ -27,10 +27,10 @@ export class WorkspaceManager {
|
|
|
27
27
|
if (relativeCwd.startsWith("..") || path.isAbsolute(relativeCwd)) {
|
|
28
28
|
throw new Error("Subagent cwd is outside the Git repository");
|
|
29
29
|
}
|
|
30
|
-
const status = (
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
30
|
+
const status = (await execFileAsync("git", ["-C", repositoryRoot, "status", "--porcelain"]))
|
|
31
|
+
.stdout;
|
|
32
|
+
if (status.trim())
|
|
33
|
+
throw new Error("Isolated subagent workspace requires a clean Git repository");
|
|
34
34
|
const rootPath = await fs.promises.mkdtemp(path.join(os.tmpdir(), WORKTREE_PREFIX));
|
|
35
35
|
let registered = false;
|
|
36
36
|
try {
|
|
@@ -86,8 +86,9 @@ export class WorkspaceManager {
|
|
|
86
86
|
workspace.rootPath,
|
|
87
87
|
]).catch(async () => {
|
|
88
88
|
await fs.promises.rm(workspace.rootPath, { recursive: true, force: true });
|
|
89
|
-
await execFileAsync("git", ["-C", workspace.repositoryRoot, "worktree", "prune"])
|
|
90
|
-
|
|
89
|
+
await execFileAsync("git", ["-C", workspace.repositoryRoot, "worktree", "prune"]).catch(
|
|
90
|
+
() => undefined,
|
|
91
|
+
);
|
|
91
92
|
});
|
|
92
93
|
await fs.promises.rm(`${workspace.rootPath}.owner`, { force: true });
|
|
93
94
|
}
|