@narumitw/pi-subagents 0.43.0 → 0.43.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 +53 -13
- package/package.json +1 -1
- package/src/agents.ts +16 -0
- package/src/config-ui.ts +151 -20
- package/src/consult-render.ts +194 -0
- package/src/consult.ts +164 -37
- package/src/cwd-policy.ts +183 -0
- package/src/execution.ts +127 -64
- package/src/in-process-transport.ts +3 -3
- package/src/inspect-render.ts +234 -0
- package/src/inspect.ts +51 -3
- package/src/persistence.ts +29 -0
- package/src/registry.ts +12 -0
- package/src/render-common.ts +252 -0
- package/src/render.ts +134 -99
- package/src/runner.ts +2 -0
- package/src/settings.ts +111 -11
- package/src/stateful-guidance.ts +35 -0
- package/src/stateful-lifecycle.ts +31 -0
- package/src/stateful-render.ts +249 -0
- package/src/stateful-safety.ts +91 -0
- package/src/stateful.ts +235 -218
- package/src/subagents.ts +60 -18
- package/src/subprocess-transport.ts +19 -2
package/src/render.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as os from "node:os";
|
|
2
1
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
3
2
|
import type { Message } from "@earendil-works/pi-ai";
|
|
4
3
|
import {
|
|
@@ -10,6 +9,7 @@ import {
|
|
|
10
9
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
11
10
|
import type { AgentScope, SubagentThinkingLevel } from "./agents.js";
|
|
12
11
|
import { hasUsableAggregator, type SubagentParams } from "./params.js";
|
|
12
|
+
import { expansionHint, formatToolActivity, safeBlock, safeLine } from "./render-common.js";
|
|
13
13
|
import {
|
|
14
14
|
getResultFinalOutput,
|
|
15
15
|
isResultError,
|
|
@@ -17,15 +17,15 @@ import {
|
|
|
17
17
|
type SubagentDetails,
|
|
18
18
|
} from "./runner.js";
|
|
19
19
|
|
|
20
|
-
const COLLAPSED_ITEM_COUNT =
|
|
20
|
+
const COLLAPSED_ITEM_COUNT = 5;
|
|
21
21
|
|
|
22
22
|
function previewTask(task: unknown, maxLength = 40): string {
|
|
23
|
-
|
|
24
|
-
return
|
|
23
|
+
const safe = safeLine(task, "...", 2 * 1024);
|
|
24
|
+
return safe.length > maxLength ? `${safe.slice(0, maxLength)}...` : safe;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function previewAgent(agent: unknown): string {
|
|
28
|
-
return
|
|
28
|
+
return safeLine(agent, "...", 256);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function formatTokens(count: number): string {
|
|
@@ -59,12 +59,12 @@ export function formatUsageStats(
|
|
|
59
59
|
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
60
60
|
if (usage.contextTokens && usage.contextTokens > 0)
|
|
61
61
|
parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
62
|
+
const safeProvider = actualProvider ? safeLine(actualProvider, "", 256) : undefined;
|
|
63
|
+
const safeModel = actualModel ? safeLine(actualModel, "", 256) : undefined;
|
|
62
64
|
const actual =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (actual ?? model) parts.push(actual ?? model ?? "");
|
|
67
|
-
if (thinkingLevel) parts.push(`requested-thinking:${thinkingLevel}`);
|
|
65
|
+
safeProvider && safeModel ? `${safeProvider}/${safeModel}` : (safeModel ?? safeProvider);
|
|
66
|
+
if (actual ?? model) parts.push(actual ?? safeLine(model, "", 256));
|
|
67
|
+
if (thinkingLevel) parts.push(`requested-thinking:${safeLine(thinkingLevel, "", 128)}`);
|
|
68
68
|
return parts.join(" ");
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -83,71 +83,7 @@ function formatToolCall(
|
|
|
83
83
|
args: Record<string, unknown>,
|
|
84
84
|
themeFg: (color: ThemeColor, text: string) => string,
|
|
85
85
|
): string {
|
|
86
|
-
|
|
87
|
-
const home = os.homedir();
|
|
88
|
-
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
switch (toolName) {
|
|
92
|
-
case "bash": {
|
|
93
|
-
const command = (args.command as string) || "...";
|
|
94
|
-
const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command;
|
|
95
|
-
return themeFg("muted", "$ ") + themeFg("toolOutput", preview);
|
|
96
|
-
}
|
|
97
|
-
case "read": {
|
|
98
|
-
const rawPath = (args.file_path || args.path || "...") as string;
|
|
99
|
-
const filePath = shortenPath(rawPath);
|
|
100
|
-
const offset = args.offset as number | undefined;
|
|
101
|
-
const limit = args.limit as number | undefined;
|
|
102
|
-
let text = themeFg("accent", filePath);
|
|
103
|
-
if (offset !== undefined || limit !== undefined) {
|
|
104
|
-
const startLine = offset ?? 1;
|
|
105
|
-
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
|
106
|
-
text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
|
107
|
-
}
|
|
108
|
-
return themeFg("muted", "read ") + text;
|
|
109
|
-
}
|
|
110
|
-
case "write": {
|
|
111
|
-
const rawPath = (args.file_path || args.path || "...") as string;
|
|
112
|
-
const filePath = shortenPath(rawPath);
|
|
113
|
-
const content = (args.content || "") as string;
|
|
114
|
-
const lines = content.split("\n").length;
|
|
115
|
-
let text = themeFg("muted", "write ") + themeFg("accent", filePath);
|
|
116
|
-
if (lines > 1) text += themeFg("dim", ` (${lines} lines)`);
|
|
117
|
-
return text;
|
|
118
|
-
}
|
|
119
|
-
case "edit": {
|
|
120
|
-
const rawPath = (args.file_path || args.path || "...") as string;
|
|
121
|
-
return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath));
|
|
122
|
-
}
|
|
123
|
-
case "ls": {
|
|
124
|
-
const rawPath = (args.path || ".") as string;
|
|
125
|
-
return themeFg("muted", "ls ") + themeFg("accent", shortenPath(rawPath));
|
|
126
|
-
}
|
|
127
|
-
case "find": {
|
|
128
|
-
const pattern = (args.pattern || "*") as string;
|
|
129
|
-
const rawPath = (args.path || ".") as string;
|
|
130
|
-
return (
|
|
131
|
-
themeFg("muted", "find ") +
|
|
132
|
-
themeFg("accent", pattern) +
|
|
133
|
-
themeFg("dim", ` in ${shortenPath(rawPath)}`)
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
case "grep": {
|
|
137
|
-
const pattern = (args.pattern || "") as string;
|
|
138
|
-
const rawPath = (args.path || ".") as string;
|
|
139
|
-
return (
|
|
140
|
-
themeFg("muted", "grep ") +
|
|
141
|
-
themeFg("accent", `/${pattern}/`) +
|
|
142
|
-
themeFg("dim", ` in ${shortenPath(rawPath)}`)
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
default: {
|
|
146
|
-
const argsStr = JSON.stringify(args);
|
|
147
|
-
const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr;
|
|
148
|
-
return themeFg("accent", toolName) + themeFg("dim", ` ${preview}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
86
|
+
return formatToolActivity(toolName, args, themeFg);
|
|
151
87
|
}
|
|
152
88
|
|
|
153
89
|
type DisplayItem =
|
|
@@ -160,10 +96,14 @@ function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
|
160
96
|
if (msg.role === "assistant") {
|
|
161
97
|
for (const part of msg.content) {
|
|
162
98
|
if (part.type === "text") {
|
|
163
|
-
const text = part.text.trim();
|
|
99
|
+
const text = safeBlock(part.text, "", 8 * 1024).trim();
|
|
164
100
|
if (text) items.push({ type: "text", text });
|
|
165
101
|
} else if (part.type === "toolCall") {
|
|
166
|
-
items.push({
|
|
102
|
+
items.push({
|
|
103
|
+
type: "toolCall",
|
|
104
|
+
name: safeLine(part.name, "tool", 256),
|
|
105
|
+
args: part.arguments,
|
|
106
|
+
});
|
|
167
107
|
}
|
|
168
108
|
}
|
|
169
109
|
}
|
|
@@ -181,6 +121,47 @@ function getCollapsedDisplayItems(result: SingleResult): { items: DisplayItem[];
|
|
|
181
121
|
return { items, total: items.length };
|
|
182
122
|
}
|
|
183
123
|
|
|
124
|
+
function sanitizeSingleResultForRender(result: SingleResult): SingleResult {
|
|
125
|
+
return {
|
|
126
|
+
...result,
|
|
127
|
+
agent: safeLine(result.agent, "subagent", 256),
|
|
128
|
+
task: safeBlock(result.task, "", 50 * 1024),
|
|
129
|
+
stderr: safeBlock(result.stderr, "", 8 * 1024),
|
|
130
|
+
model: result.model ? safeLine(result.model, "", 256) : undefined,
|
|
131
|
+
actualProvider: result.actualProvider ? safeLine(result.actualProvider, "", 256) : undefined,
|
|
132
|
+
actualModel: result.actualModel ? safeLine(result.actualModel, "", 256) : undefined,
|
|
133
|
+
stopReason: result.stopReason ? safeLine(result.stopReason, "", 256) : undefined,
|
|
134
|
+
errorMessage: result.errorMessage ? safeBlock(result.errorMessage, "", 8 * 1024) : undefined,
|
|
135
|
+
finalOutput: result.finalOutput ? safeBlock(result.finalOutput, "", 50 * 1024) : undefined,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function renderResultStatus(result: SingleResult, isPartial: boolean): string {
|
|
140
|
+
if (isResultError(result)) {
|
|
141
|
+
return result.stopReason === "aborted" || result.aborted ? "Cancelled" : "Failed";
|
|
142
|
+
}
|
|
143
|
+
return isPartial || result.exitCode === -1 ? "Running" : "Completed";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function coloredStatus(theme: Theme, status: string): string {
|
|
147
|
+
const color: ThemeColor =
|
|
148
|
+
status === "Failed" ? "error" : status === "Completed" ? "success" : "warning";
|
|
149
|
+
return theme.fg(color, status);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function coloredResultStatus(theme: Theme, result: SingleResult, isPartial: boolean): string {
|
|
153
|
+
return coloredStatus(theme, renderResultStatus(result, isPartial));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatResultPolicy(result: SingleResult): string {
|
|
157
|
+
if (!result.policy) return "";
|
|
158
|
+
return [
|
|
159
|
+
`inherited: ${result.policy.inherited.map((tool) => safeLine(tool, "", 256)).join(", ") || "none"}`,
|
|
160
|
+
`overridden: ${result.policy.overridden.map((tool) => safeLine(tool, "", 256)).join(", ") || "none"}`,
|
|
161
|
+
`unsupported: ${result.policy.unsupported.map((tool) => safeLine(tool, "", 256)).join(", ") || "none"}`,
|
|
162
|
+
].join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
184
165
|
export function renderSubagentCall(args: SubagentParams, theme: Theme) {
|
|
185
166
|
const scope: AgentScope = args.agentScope ?? "user";
|
|
186
167
|
if (args.chain && args.chain.length > 0) {
|
|
@@ -239,11 +220,22 @@ export function renderSubagentResult(
|
|
|
239
220
|
{ expanded, isPartial }: ToolRenderResultOptions,
|
|
240
221
|
theme: Theme,
|
|
241
222
|
) {
|
|
242
|
-
const
|
|
243
|
-
if (!
|
|
223
|
+
const rawDetails = result.details as SubagentDetails | undefined;
|
|
224
|
+
if (!rawDetails || rawDetails.results.length === 0) {
|
|
244
225
|
const text = result.content[0];
|
|
245
|
-
return new Text(
|
|
226
|
+
return new Text(
|
|
227
|
+
text?.type === "text" ? safeBlock(text.text, "(no output)", 8 * 1024) : "(no output)",
|
|
228
|
+
0,
|
|
229
|
+
0,
|
|
230
|
+
);
|
|
246
231
|
}
|
|
232
|
+
const details: SubagentDetails = {
|
|
233
|
+
...rawDetails,
|
|
234
|
+
results: rawDetails.results.map(sanitizeSingleResultForRender),
|
|
235
|
+
aggregator: rawDetails.aggregator
|
|
236
|
+
? sanitizeSingleResultForRender(rawDetails.aggregator)
|
|
237
|
+
: undefined,
|
|
238
|
+
};
|
|
247
239
|
|
|
248
240
|
const mdTheme = getMarkdownTheme();
|
|
249
241
|
|
|
@@ -254,7 +246,8 @@ export function renderSubagentResult(
|
|
|
254
246
|
if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
|
|
255
247
|
for (const item of toShow) {
|
|
256
248
|
if (item.type === "text") {
|
|
257
|
-
const
|
|
249
|
+
const safeText = safeBlock(item.text, "", 8 * 1024);
|
|
250
|
+
const preview = expanded ? safeText : safeText.split("\n").slice(0, 3).join("\n");
|
|
258
251
|
text += `${theme.fg("toolOutput", preview)}\n`;
|
|
259
252
|
} else {
|
|
260
253
|
text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
|
|
@@ -276,7 +269,7 @@ export function renderSubagentResult(
|
|
|
276
269
|
|
|
277
270
|
if (expanded) {
|
|
278
271
|
const container = new Container();
|
|
279
|
-
let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
272
|
+
let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${safeLine(r.agentSource, "unknown", 128)})`)} · ${coloredResultStatus(theme, r, isPartial)}`;
|
|
280
273
|
if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
281
274
|
container.addChild(new Text(header, 0, 0));
|
|
282
275
|
if (isError && r.errorMessage)
|
|
@@ -284,6 +277,12 @@ export function renderSubagentResult(
|
|
|
284
277
|
container.addChild(new Spacer(1));
|
|
285
278
|
container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
|
|
286
279
|
container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
|
|
280
|
+
const policy = formatResultPolicy(r);
|
|
281
|
+
if (policy) {
|
|
282
|
+
container.addChild(new Spacer(1));
|
|
283
|
+
container.addChild(new Text(theme.fg("muted", "─── Policy ───"), 0, 0));
|
|
284
|
+
container.addChild(new Text(theme.fg("dim", policy), 0, 0));
|
|
285
|
+
}
|
|
287
286
|
container.addChild(new Spacer(1));
|
|
288
287
|
container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
|
|
289
288
|
if (displayItems.length === 0 && !finalOutput) {
|
|
@@ -314,15 +313,16 @@ export function renderSubagentResult(
|
|
|
314
313
|
}
|
|
315
314
|
|
|
316
315
|
const collapsed = getCollapsedDisplayItems(r);
|
|
317
|
-
let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
316
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${safeLine(r.agentSource, "unknown", 128)})`)} · ${coloredResultStatus(theme, r, isPartial)}`;
|
|
318
317
|
if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
319
318
|
if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
320
319
|
if (collapsed.items.length > 0) {
|
|
321
320
|
text += `\n${renderDisplayItems(collapsed.items, COLLAPSED_ITEM_COUNT, collapsed.total)}`;
|
|
322
|
-
if (collapsed.total > COLLAPSED_ITEM_COUNT)
|
|
323
|
-
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
321
|
+
if (collapsed.total > COLLAPSED_ITEM_COUNT) text += `\n${expansionHint()}`;
|
|
324
322
|
} else if (finalOutput.trim()) {
|
|
325
|
-
|
|
323
|
+
const outputLines = finalOutput.trim().split("\n");
|
|
324
|
+
text += `\n${theme.fg("toolOutput", outputLines.slice(0, 3).join("\n"))}`;
|
|
325
|
+
if (outputLines.length > 3) text += `\n${expansionHint()}`;
|
|
326
326
|
} else if (!isError || !r.errorMessage) {
|
|
327
327
|
text += `\n${theme.fg("muted", isPartial && !isError ? "(running...)" : "(no output)")}`;
|
|
328
328
|
}
|
|
@@ -356,6 +356,11 @@ export function renderSubagentResult(
|
|
|
356
356
|
: successCount === details.results.length
|
|
357
357
|
? theme.fg("success", "✓")
|
|
358
358
|
: theme.fg("error", "✗");
|
|
359
|
+
const overallStatus = currentIsRunning
|
|
360
|
+
? "Running"
|
|
361
|
+
: successCount === details.results.length
|
|
362
|
+
? "Completed"
|
|
363
|
+
: "Failed";
|
|
359
364
|
|
|
360
365
|
if (expanded) {
|
|
361
366
|
const container = new Container();
|
|
@@ -364,7 +369,8 @@ export function renderSubagentResult(
|
|
|
364
369
|
icon +
|
|
365
370
|
" " +
|
|
366
371
|
theme.fg("toolTitle", theme.bold("chain ")) +
|
|
367
|
-
theme.fg("accent", `${successCount}/${details.results.length} steps`)
|
|
372
|
+
theme.fg("accent", `${successCount}/${details.results.length} steps`) +
|
|
373
|
+
` · ${coloredStatus(theme, overallStatus)}`,
|
|
368
374
|
0,
|
|
369
375
|
0,
|
|
370
376
|
),
|
|
@@ -383,12 +389,17 @@ export function renderSubagentResult(
|
|
|
383
389
|
container.addChild(new Spacer(1));
|
|
384
390
|
container.addChild(
|
|
385
391
|
new Text(
|
|
386
|
-
`${theme.fg("muted", `─── Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon}`,
|
|
392
|
+
`${theme.fg("muted", `─── Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon} ${coloredResultStatus(theme, r, currentIsRunning && r === currentResult)}`,
|
|
387
393
|
0,
|
|
388
394
|
0,
|
|
389
395
|
),
|
|
390
396
|
);
|
|
391
397
|
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
|
398
|
+
const policy = formatResultPolicy(r);
|
|
399
|
+
if (policy)
|
|
400
|
+
container.addChild(
|
|
401
|
+
new Text(theme.fg("muted", "Policy:\n") + theme.fg("dim", policy), 0, 0),
|
|
402
|
+
);
|
|
392
403
|
if (rFailed && r.errorMessage)
|
|
393
404
|
container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
394
405
|
|
|
@@ -429,7 +440,8 @@ export function renderSubagentResult(
|
|
|
429
440
|
icon +
|
|
430
441
|
" " +
|
|
431
442
|
theme.fg("toolTitle", theme.bold("chain ")) +
|
|
432
|
-
theme.fg("accent", `${successCount}/${details.results.length} steps`)
|
|
443
|
+
theme.fg("accent", `${successCount}/${details.results.length} steps`) +
|
|
444
|
+
` · ${coloredStatus(theme, overallStatus)}`;
|
|
433
445
|
for (const r of details.results) {
|
|
434
446
|
const rFailed = isResultError(r);
|
|
435
447
|
const rIcon = rFailed
|
|
@@ -439,7 +451,7 @@ export function renderSubagentResult(
|
|
|
439
451
|
: theme.fg("success", "✓");
|
|
440
452
|
const collapsed = getCollapsedDisplayItems(r);
|
|
441
453
|
const finalOutput = getResultFinalOutput(r).trim();
|
|
442
|
-
text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
454
|
+
text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon} ${coloredResultStatus(theme, r, currentIsRunning && r === currentResult)}`;
|
|
443
455
|
if (rFailed && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
444
456
|
if (collapsed.items.length > 0)
|
|
445
457
|
text += `\n${renderDisplayItems(collapsed.items, 5, collapsed.total)}`;
|
|
@@ -451,7 +463,7 @@ export function renderSubagentResult(
|
|
|
451
463
|
}
|
|
452
464
|
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
453
465
|
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
454
|
-
text += `\n${
|
|
466
|
+
text += `\n${expansionHint()}`;
|
|
455
467
|
return new Text(text, 0, 0);
|
|
456
468
|
}
|
|
457
469
|
|
|
@@ -485,12 +497,17 @@ export function renderSubagentResult(
|
|
|
485
497
|
: aggregator
|
|
486
498
|
? `${successCount}/${details.results.length} tasks + fan-in`
|
|
487
499
|
: `${successCount}/${details.results.length} tasks`;
|
|
500
|
+
const overallStatus = isRunning
|
|
501
|
+
? "Running"
|
|
502
|
+
: failCount > 0 || aggregatorFailed
|
|
503
|
+
? "Failed"
|
|
504
|
+
: "Completed";
|
|
488
505
|
|
|
489
506
|
if (expanded && !isRunning) {
|
|
490
507
|
const container = new Container();
|
|
491
508
|
container.addChild(
|
|
492
509
|
new Text(
|
|
493
|
-
`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
|
|
510
|
+
`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)} · ${coloredStatus(theme, overallStatus)}`,
|
|
494
511
|
0,
|
|
495
512
|
0,
|
|
496
513
|
),
|
|
@@ -508,9 +525,18 @@ export function renderSubagentResult(
|
|
|
508
525
|
|
|
509
526
|
container.addChild(new Spacer(1));
|
|
510
527
|
container.addChild(
|
|
511
|
-
new Text(
|
|
528
|
+
new Text(
|
|
529
|
+
`${theme.fg("muted", "─── ") + theme.fg("accent", r.agent)} ${rIcon} ${coloredResultStatus(theme, r, resultIsRunning(r))}`,
|
|
530
|
+
0,
|
|
531
|
+
0,
|
|
532
|
+
),
|
|
512
533
|
);
|
|
513
534
|
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
|
535
|
+
const policy = formatResultPolicy(r);
|
|
536
|
+
if (policy)
|
|
537
|
+
container.addChild(
|
|
538
|
+
new Text(theme.fg("muted", "Policy:\n") + theme.fg("dim", policy), 0, 0),
|
|
539
|
+
);
|
|
514
540
|
if (rFailed && r.errorMessage)
|
|
515
541
|
container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
516
542
|
|
|
@@ -550,7 +576,7 @@ export function renderSubagentResult(
|
|
|
550
576
|
container.addChild(new Spacer(1));
|
|
551
577
|
container.addChild(
|
|
552
578
|
new Text(
|
|
553
|
-
`${theme.fg("muted", "─── fan-in → ") + theme.fg("accent", aggregator.agent)} ${rIcon}`,
|
|
579
|
+
`${theme.fg("muted", "─── fan-in → ") + theme.fg("accent", aggregator.agent)} ${rIcon} ${coloredResultStatus(theme, aggregator, aggregatorRunning)}`,
|
|
554
580
|
0,
|
|
555
581
|
0,
|
|
556
582
|
),
|
|
@@ -558,6 +584,11 @@ export function renderSubagentResult(
|
|
|
558
584
|
container.addChild(
|
|
559
585
|
new Text(theme.fg("muted", "Task: ") + theme.fg("dim", aggregator.task), 0, 0),
|
|
560
586
|
);
|
|
587
|
+
const aggregatorPolicy = formatResultPolicy(aggregator);
|
|
588
|
+
if (aggregatorPolicy)
|
|
589
|
+
container.addChild(
|
|
590
|
+
new Text(theme.fg("muted", "Policy:\n") + theme.fg("dim", aggregatorPolicy), 0, 0),
|
|
591
|
+
);
|
|
561
592
|
if (aggregatorFailed && aggregator.errorMessage)
|
|
562
593
|
container.addChild(
|
|
563
594
|
new Text(theme.fg("error", `Error: ${aggregator.errorMessage}`), 0, 0),
|
|
@@ -592,7 +623,7 @@ export function renderSubagentResult(
|
|
|
592
623
|
}
|
|
593
624
|
|
|
594
625
|
// Collapsed view (or still running)
|
|
595
|
-
let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
|
|
626
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)} · ${coloredStatus(theme, overallStatus)}`;
|
|
596
627
|
for (const r of details.results) {
|
|
597
628
|
const rFailed = isResultError(r);
|
|
598
629
|
const rRunning = resultIsRunning(r);
|
|
@@ -603,7 +634,7 @@ export function renderSubagentResult(
|
|
|
603
634
|
: theme.fg("success", "✓");
|
|
604
635
|
const collapsed = getCollapsedDisplayItems(r);
|
|
605
636
|
const finalOutput = getResultFinalOutput(r).trim();
|
|
606
|
-
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
637
|
+
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon} ${coloredResultStatus(theme, r, rRunning)}`;
|
|
607
638
|
if (rFailed && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
608
639
|
if (collapsed.items.length > 0)
|
|
609
640
|
text += `\n${renderDisplayItems(collapsed.items, 5, collapsed.total)}`;
|
|
@@ -620,7 +651,7 @@ export function renderSubagentResult(
|
|
|
620
651
|
: theme.fg("success", "✓");
|
|
621
652
|
const collapsed = getCollapsedDisplayItems(aggregator);
|
|
622
653
|
const finalOutput = getResultFinalOutput(aggregator).trim();
|
|
623
|
-
text += `\n\n${theme.fg("muted", "─── fan-in → ")}${theme.fg("accent", aggregator.agent)} ${rIcon}`;
|
|
654
|
+
text += `\n\n${theme.fg("muted", "─── fan-in → ")}${theme.fg("accent", aggregator.agent)} ${rIcon} ${coloredResultStatus(theme, aggregator, aggregatorRunning)}`;
|
|
624
655
|
if (aggregatorFailed && aggregator.errorMessage)
|
|
625
656
|
text += `\n${theme.fg("error", `Error: ${aggregator.errorMessage}`)}`;
|
|
626
657
|
if (collapsed.items.length > 0)
|
|
@@ -636,10 +667,14 @@ export function renderSubagentResult(
|
|
|
636
667
|
const usageStr = formatUsageStats(aggregateUsage(usageResults));
|
|
637
668
|
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
638
669
|
}
|
|
639
|
-
if (!expanded) text += `\n${
|
|
670
|
+
if (!expanded) text += `\n${expansionHint()}`;
|
|
640
671
|
return new Text(text, 0, 0);
|
|
641
672
|
}
|
|
642
673
|
|
|
643
674
|
const text = result.content[0];
|
|
644
|
-
return new Text(
|
|
675
|
+
return new Text(
|
|
676
|
+
text?.type === "text" ? safeBlock(text.text, "(no output)", 8 * 1024) : "(no output)",
|
|
677
|
+
0,
|
|
678
|
+
0,
|
|
679
|
+
);
|
|
645
680
|
}
|
package/src/runner.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
|
6
6
|
import type { Message } from "@earendil-works/pi-ai";
|
|
7
7
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { AgentConfig, AgentScope, AgentSource, SubagentThinkingLevel } from "./agents.js";
|
|
9
|
+
import type { TargetPolicyAudit } from "./cwd-policy.js";
|
|
9
10
|
import {
|
|
10
11
|
appendBounded,
|
|
11
12
|
DEFAULT_MAX_CONTEXT_BYTES,
|
|
@@ -81,6 +82,7 @@ export interface SingleResult {
|
|
|
81
82
|
malformedEvents?: number;
|
|
82
83
|
launchFailed?: boolean;
|
|
83
84
|
processStarted?: boolean;
|
|
85
|
+
target?: TargetPolicyAudit;
|
|
84
86
|
policy?: {
|
|
85
87
|
inherited: string[];
|
|
86
88
|
overridden: string[];
|
package/src/settings.ts
CHANGED
|
@@ -6,8 +6,12 @@ import lockfile from "proper-lockfile";
|
|
|
6
6
|
import {
|
|
7
7
|
type AgentConfig,
|
|
8
8
|
CONSULT_RESOURCE_POLICIES,
|
|
9
|
+
CONSULTATION_CWD_POLICIES,
|
|
9
10
|
type CompletionDelivery,
|
|
11
|
+
type ConsultationCwdPolicy,
|
|
10
12
|
type ConsultResourcePolicy,
|
|
13
|
+
DELEGATION_CWD_POLICIES,
|
|
14
|
+
type DelegationCwdPolicy,
|
|
11
15
|
isThinkingLevel,
|
|
12
16
|
type SubagentAgentConfig,
|
|
13
17
|
type SubagentSettings,
|
|
@@ -158,6 +162,29 @@ export function normalizeSubagentSettings(value: unknown): SubagentSettings | un
|
|
|
158
162
|
}
|
|
159
163
|
settings.consult = consult;
|
|
160
164
|
}
|
|
165
|
+
if (hasOwn(value, "cwdPolicy")) {
|
|
166
|
+
if (!isPlainObject(value.cwdPolicy)) return undefined;
|
|
167
|
+
const cwdPolicy: NonNullable<SubagentSettings["cwdPolicy"]> = {};
|
|
168
|
+
if (hasOwn(value.cwdPolicy, "consultation")) {
|
|
169
|
+
if (
|
|
170
|
+
typeof value.cwdPolicy.consultation !== "string" ||
|
|
171
|
+
!CONSULTATION_CWD_POLICIES.includes(value.cwdPolicy.consultation as ConsultationCwdPolicy)
|
|
172
|
+
) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
cwdPolicy.consultation = value.cwdPolicy.consultation as ConsultationCwdPolicy;
|
|
176
|
+
}
|
|
177
|
+
if (hasOwn(value.cwdPolicy, "delegation")) {
|
|
178
|
+
if (
|
|
179
|
+
typeof value.cwdPolicy.delegation !== "string" ||
|
|
180
|
+
!DELEGATION_CWD_POLICIES.includes(value.cwdPolicy.delegation as DelegationCwdPolicy)
|
|
181
|
+
) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
cwdPolicy.delegation = value.cwdPolicy.delegation as DelegationCwdPolicy;
|
|
185
|
+
}
|
|
186
|
+
settings.cwdPolicy = cwdPolicy;
|
|
187
|
+
}
|
|
161
188
|
return settings;
|
|
162
189
|
}
|
|
163
190
|
|
|
@@ -165,6 +192,8 @@ const SETTINGS_FILE = "pi-subagents.json";
|
|
|
165
192
|
const LEGACY_SETTINGS_FILE = "pi-subagents-config.json";
|
|
166
193
|
const DEFAULT_COMPLETION_DELIVERY: CompletionDelivery = "next-turn";
|
|
167
194
|
export const DEFAULT_CONSULT_RESOURCE_POLICY: ConsultResourcePolicy = "project-context";
|
|
195
|
+
export const DEFAULT_CONSULTATION_CWD_POLICY: ConsultationCwdPolicy = "anywhere";
|
|
196
|
+
export const DEFAULT_DELEGATION_CWD_POLICY: DelegationCwdPolicy = "trusted-targets";
|
|
168
197
|
const SETTINGS_LOCK_FS_ADAPTER = {
|
|
169
198
|
mkdir: fs.mkdir,
|
|
170
199
|
mkdirSync: fs.mkdirSync,
|
|
@@ -261,6 +290,18 @@ export interface ConsultResourceSettingsSnapshot {
|
|
|
261
290
|
error?: string;
|
|
262
291
|
}
|
|
263
292
|
|
|
293
|
+
export interface CwdPolicyFieldSnapshot<T> {
|
|
294
|
+
value: T;
|
|
295
|
+
source: "default" | "user settings";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface CwdPolicySettingsSnapshot {
|
|
299
|
+
path: string;
|
|
300
|
+
consultation: CwdPolicyFieldSnapshot<ConsultationCwdPolicy>;
|
|
301
|
+
delegation: CwdPolicyFieldSnapshot<DelegationCwdPolicy>;
|
|
302
|
+
error?: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
264
305
|
export interface SubagentSettingsSnapshot {
|
|
265
306
|
path: string;
|
|
266
307
|
settings?: SubagentSettings;
|
|
@@ -302,16 +343,28 @@ function inspectSubagentSettingsPath(configPath: string): {
|
|
|
302
343
|
settings?: SubagentSettings;
|
|
303
344
|
error?: string;
|
|
304
345
|
} {
|
|
346
|
+
const fileName = path.basename(configPath);
|
|
347
|
+
let contents: string;
|
|
305
348
|
try {
|
|
306
|
-
|
|
307
|
-
const settings = normalizeSubagentSettings(raw);
|
|
308
|
-
if (!isPlainObject(raw) || !settings) {
|
|
309
|
-
throw new Error(`${path.basename(configPath)} is not a valid settings object`);
|
|
310
|
-
}
|
|
311
|
-
return { path: configPath, raw, settings };
|
|
349
|
+
contents = fs.readFileSync(configPath, "utf8");
|
|
312
350
|
} catch (error) {
|
|
313
|
-
|
|
351
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
352
|
+
return {
|
|
353
|
+
path: configPath,
|
|
354
|
+
error: `${fileName} could not be read${code ? ` (${safeErrorCode(code)})` : ""}`,
|
|
355
|
+
};
|
|
314
356
|
}
|
|
357
|
+
let raw: unknown;
|
|
358
|
+
try {
|
|
359
|
+
raw = JSON.parse(contents);
|
|
360
|
+
} catch {
|
|
361
|
+
return { path: configPath, error: `${fileName} contains malformed JSON` };
|
|
362
|
+
}
|
|
363
|
+
const settings = normalizeSubagentSettings(raw);
|
|
364
|
+
if (!isPlainObject(raw) || !settings) {
|
|
365
|
+
return { path: configPath, error: `${fileName} is not a valid settings object` };
|
|
366
|
+
}
|
|
367
|
+
return { path: configPath, raw, settings };
|
|
315
368
|
}
|
|
316
369
|
|
|
317
370
|
export function inspectSubagentSettings(): SubagentSettingsSnapshot {
|
|
@@ -343,6 +396,30 @@ export function inspectConsultResourceSettings(): ConsultResourceSettingsSnapsho
|
|
|
343
396
|
};
|
|
344
397
|
}
|
|
345
398
|
|
|
399
|
+
export function inspectCwdPolicySettings(): CwdPolicySettingsSnapshot {
|
|
400
|
+
const inspected = inspectSubagentSettingsDocument();
|
|
401
|
+
if (!inspected.raw || !inspected.settings) {
|
|
402
|
+
return {
|
|
403
|
+
path: inspected.path,
|
|
404
|
+
consultation: { value: DEFAULT_CONSULTATION_CWD_POLICY, source: "default" },
|
|
405
|
+
delegation: { value: DEFAULT_DELEGATION_CWD_POLICY, source: "default" },
|
|
406
|
+
...(inspected.error ? { error: inspected.error } : {}),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const rawPolicy = isPlainObject(inspected.raw.cwdPolicy) ? inspected.raw.cwdPolicy : undefined;
|
|
410
|
+
return {
|
|
411
|
+
path: inspected.path,
|
|
412
|
+
consultation: {
|
|
413
|
+
value: inspected.settings.cwdPolicy?.consultation ?? DEFAULT_CONSULTATION_CWD_POLICY,
|
|
414
|
+
source: rawPolicy && hasOwn(rawPolicy, "consultation") ? "user settings" : "default",
|
|
415
|
+
},
|
|
416
|
+
delegation: {
|
|
417
|
+
value: inspected.settings.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY,
|
|
418
|
+
source: rawPolicy && hasOwn(rawPolicy, "delegation") ? "user settings" : "default",
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
346
423
|
export function inspectDelegationWorkflowSettings(): DelegationWorkflowSettingsSnapshot {
|
|
347
424
|
const inspected = inspectSubagentSettingsDocument();
|
|
348
425
|
if (!inspected.raw || !inspected.settings) {
|
|
@@ -458,6 +535,29 @@ export function updateConsultResourceSetting(value: ConsultResourcePolicy): void
|
|
|
458
535
|
});
|
|
459
536
|
}
|
|
460
537
|
|
|
538
|
+
export function updateCwdPolicySetting(field: "consultation", value: ConsultationCwdPolicy): void;
|
|
539
|
+
export function updateCwdPolicySetting(field: "delegation", value: DelegationCwdPolicy): void;
|
|
540
|
+
export function updateCwdPolicySetting(
|
|
541
|
+
field: "consultation" | "delegation",
|
|
542
|
+
value: ConsultationCwdPolicy | DelegationCwdPolicy,
|
|
543
|
+
): void {
|
|
544
|
+
withSettingsMutationLock(() => {
|
|
545
|
+
const update = readSettingsObjectForUpdate();
|
|
546
|
+
const raw = update.document;
|
|
547
|
+
const cwdPolicy = raw.cwdPolicy;
|
|
548
|
+
if (cwdPolicy !== undefined && !isPlainObject(cwdPolicy)) {
|
|
549
|
+
throw new Error(`Cannot update invalid ${SETTINGS_FILE} cwdPolicy settings`);
|
|
550
|
+
}
|
|
551
|
+
writeSettingsObjectUnlocked(
|
|
552
|
+
{
|
|
553
|
+
...raw,
|
|
554
|
+
cwdPolicy: { ...(cwdPolicy ?? {}), [field]: value },
|
|
555
|
+
},
|
|
556
|
+
update.replaceCanonical,
|
|
557
|
+
);
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
461
561
|
export function updateAgentToolsSetting(name: string, tools: string[] | undefined): void {
|
|
462
562
|
withSettingsMutationLock(() => {
|
|
463
563
|
const update = readSettingsObjectForUpdate();
|
|
@@ -504,8 +604,8 @@ function readSettingsObjectForUpdate(): SettingsObjectForUpdate {
|
|
|
504
604
|
let parsed: unknown;
|
|
505
605
|
try {
|
|
506
606
|
parsed = JSON.parse(fs.readFileSync(activePath, "utf8"));
|
|
507
|
-
} catch
|
|
508
|
-
throw new Error(`Cannot update malformed ${activeFile}
|
|
607
|
+
} catch {
|
|
608
|
+
throw new Error(`Cannot update malformed ${activeFile}`);
|
|
509
609
|
}
|
|
510
610
|
if (!isPlainObject(parsed) || !normalizeSubagentSettings(parsed)) {
|
|
511
611
|
throw new Error(`Cannot update invalid ${activeFile}`);
|
|
@@ -585,8 +685,8 @@ function readSettingsSnapshot(configPath: string): {
|
|
|
585
685
|
}
|
|
586
686
|
}
|
|
587
687
|
|
|
588
|
-
function
|
|
589
|
-
return
|
|
688
|
+
function safeErrorCode(value: string): string {
|
|
689
|
+
return value.replace(/[^A-Z0-9_-]/giu, "?").slice(0, 64);
|
|
590
690
|
}
|
|
591
691
|
|
|
592
692
|
export function uniqueToolNames(tools: string[]): string[] {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CompletionDelivery } from "./agents.js";
|
|
2
|
+
|
|
3
|
+
export function createSpawnPromptGuidelines(
|
|
4
|
+
completionDelivery: CompletionDelivery,
|
|
5
|
+
blockingEnabled = true,
|
|
6
|
+
): string[] {
|
|
7
|
+
const deliveryGuidance =
|
|
8
|
+
completionDelivery === "auto-resume"
|
|
9
|
+
? blockingEnabled
|
|
10
|
+
? "With subagent_spawn completion delivery set to auto-resume, prefer one subagent_spawn for broad asynchronous research or review that covers related branches even when the final answer depends on its result; do not choose blocking parallel fan-out merely to keep delegation in the same turn."
|
|
11
|
+
: "With subagent_spawn completion delivery set to auto-resume, prefer one subagent_spawn for broad asynchronous research or review that covers related branches even when the final answer depends on its result."
|
|
12
|
+
: blockingEnabled
|
|
13
|
+
? "With subagent_spawn completion delivery set to next-turn (the default), prefer one subagent_spawn for broad asynchronous research or review only when the current response does not depend on its result; use the blocking subagent when the final answer depends on the detached result."
|
|
14
|
+
: "With subagent_spawn completion delivery set to next-turn (the default), use subagent_spawn only when the current response does not depend on its result; complete final-answer-dependent work directly because an idle root is not awakened.";
|
|
15
|
+
const noLocalWorkGuidance =
|
|
16
|
+
completionDelivery === "auto-resume"
|
|
17
|
+
? "After subagent_spawn returns, do useful non-overlapping local work immediately. If none remains, briefly tell the user what subagent_spawn launched and end the response; auto-resume will request a synthesis turn after completion."
|
|
18
|
+
: "After subagent_spawn returns, do useful non-overlapping local work immediately. If none remains, briefly tell the user what subagent_spawn launched and end the response only when the current response does not depend on its result; next-turn delivery will not wake an idle root.";
|
|
19
|
+
return [
|
|
20
|
+
"Do not use subagent_spawn for simple or critical-path work that the main agent can perform directly.",
|
|
21
|
+
"Set subagent_spawn thinkingLevel to the lowest sufficient thinking level for the delegated task: use off or minimal for extraction, formatting, or mechanical work; low for straightforward bounded work; medium for ordinary multi-step research or implementation; high for complex debugging, design, review, or cross-file analysis; xhigh for highly ambiguous, cross-system, or high-risk analysis; and max only for the hardest tasks when quality clearly outweighs latency and cost. Omit subagent_spawn thinkingLevel only to preserve the agent or child default.",
|
|
22
|
+
deliveryGuidance,
|
|
23
|
+
"Use a single subagent_spawn only for a concrete bounded subtask that can run independently and has an isolation or specialization benefit such as independent review, bounded context/output, a distinct model/tool profile, or workspace isolation.",
|
|
24
|
+
...(blockingEnabled
|
|
25
|
+
? [
|
|
26
|
+
"Use the blocking subagent instead of subagent_spawn when synchronous output is required before the main agent can continue and waiting is intentional; queued steering cannot be processed until that blocking call returns.",
|
|
27
|
+
"When subagent_spawn fits the completion-delivery policy, do not choose a blocking parallel subagent merely to keep delegation in the same turn.",
|
|
28
|
+
]
|
|
29
|
+
: []),
|
|
30
|
+
"Add another subagent_spawn only for truly independent work with safe workspace concurrency.",
|
|
31
|
+
noLocalWorkGuidance,
|
|
32
|
+
'Consume and synthesize available subagent_spawn completion messages; use subagent_manage with action "interrupt" or "close" for agents that are no longer needed.',
|
|
33
|
+
'Completion from subagent_spawn is delivered automatically. Do not poll with subagent_manage action "list" or subagent_mailbox action "read", repeatedly check progress, or duplicate the delegated work.',
|
|
34
|
+
];
|
|
35
|
+
}
|