@bacnh85/pi-subagent 0.7.0 → 0.8.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/agent-format.md +12 -0
- package/extensions/agents.ts +26 -5
- package/extensions/index.ts +75 -26
- package/extensions/render.ts +3 -2
- package/extensions/runner.ts +10 -4
- package/extensions/service.ts +8 -2
- package/extensions/thread-viewer.ts +1 -0
- package/package.json +1 -1
package/agent-format.md
CHANGED
|
@@ -21,9 +21,21 @@ description: ... # Required. When to use this agent.
|
|
|
21
21
|
tools: read, grep, ... # Optional. Comma-separated tool names. Defaults to all.
|
|
22
22
|
model: provider/model # Optional. Defaults to parent's model.
|
|
23
23
|
thinking: low # Optional: off|minimal|low|medium|high|xhigh|max.
|
|
24
|
+
sandbox: read-only # Optional: read-only | workspace-write. Auto-derives tool restrictions.
|
|
25
|
+
color: cyan # Optional: red|blue|green|yellow|purple|orange|pink|cyan.
|
|
24
26
|
---
|
|
25
27
|
```
|
|
26
28
|
|
|
29
|
+
### `sandbox`
|
|
30
|
+
|
|
31
|
+
- `read-only`: Restricts tools to `read`, `grep`, `find`, `ls`. Overrides any `tools` field.
|
|
32
|
+
- `workspace-write` (default): Uses the agent's `tools` list or defaults to all tools.
|
|
33
|
+
|
|
34
|
+
### `color`
|
|
35
|
+
|
|
36
|
+
Display color for the agent name in the TUI thread picker, viewer, and result summary.
|
|
37
|
+
Accepted values: `red`, `blue`, `green`, `yellow`, `purple`, `orange`, `pink`, `cyan`.
|
|
38
|
+
|
|
27
39
|
Only `name` and `description` are required.
|
|
28
40
|
|
|
29
41
|
## Body
|
package/extensions/agents.ts
CHANGED
|
@@ -48,6 +48,7 @@ interface AgentCache {
|
|
|
48
48
|
scope: AgentScope;
|
|
49
49
|
agents: AgentConfig[];
|
|
50
50
|
projectAgentsDir: string | null;
|
|
51
|
+
diagnostics: AgentDiscoveryDiagnostic[];
|
|
51
52
|
/** File-level signature per directory (name:mtime:size for each .md file) */
|
|
52
53
|
dirSignatures: Map<string, string>;
|
|
53
54
|
}
|
|
@@ -104,7 +105,22 @@ function loadAgentsFromDir(
|
|
|
104
105
|
continue;
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
let frontmatter: Record<string, unknown>;
|
|
111
|
+
let body: string;
|
|
112
|
+
try {
|
|
113
|
+
const parsed = parseFrontmatter<Record<string, unknown>>(content);
|
|
114
|
+
frontmatter = parsed.frontmatter;
|
|
115
|
+
body = parsed.body;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
diagnostics.push({
|
|
118
|
+
filePath,
|
|
119
|
+
issue: `Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
|
|
120
|
+
severity: "error",
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
108
124
|
|
|
109
125
|
if (typeof frontmatter.name !== "string" || typeof frontmatter.description !== "string") {
|
|
110
126
|
if (typeof frontmatter.name !== "string" && typeof frontmatter.description !== "string") {
|
|
@@ -169,7 +185,7 @@ function loadAgentsFromDir(
|
|
|
169
185
|
if (!validSandboxes.includes(frontmatter.sandbox)) {
|
|
170
186
|
diagnostics.push({
|
|
171
187
|
filePath,
|
|
172
|
-
issue: `Invalid sandbox mode "${frontmatter.sandbox}". Valid values: ${validSandboxes.join(", ")}.
|
|
188
|
+
issue: `Invalid sandbox mode "${frontmatter.sandbox}". Valid values: ${validSandboxes.join(", ")}. Ignoring.`,
|
|
173
189
|
severity: "warn",
|
|
174
190
|
});
|
|
175
191
|
}
|
|
@@ -225,8 +241,12 @@ function dirSignature(dir: string): string {
|
|
|
225
241
|
.filter((e) => e.name.endsWith(".md") && (e.isFile() || e.isSymbolicLink()))
|
|
226
242
|
.map((e) => {
|
|
227
243
|
const file = path.join(dir, e.name);
|
|
228
|
-
|
|
229
|
-
|
|
244
|
+
try {
|
|
245
|
+
const st = fs.statSync(file);
|
|
246
|
+
return `${e.name}:${st.mtimeMs}:${st.size}`;
|
|
247
|
+
} catch {
|
|
248
|
+
return `${e.name}:broken`;
|
|
249
|
+
}
|
|
230
250
|
})
|
|
231
251
|
.sort();
|
|
232
252
|
return `exists:${entries.join("|")}`;
|
|
@@ -277,7 +297,7 @@ export function discoverAgents(
|
|
|
277
297
|
}
|
|
278
298
|
}
|
|
279
299
|
if (!stale) {
|
|
280
|
-
return { agents: _cache.agents, projectAgentsDir: _cache.projectAgentsDir, diagnostics:
|
|
300
|
+
return { agents: _cache.agents, projectAgentsDir: _cache.projectAgentsDir, diagnostics: _cache.diagnostics };
|
|
281
301
|
}
|
|
282
302
|
// Cache is stale — rebuild below
|
|
283
303
|
_cache = null;
|
|
@@ -316,6 +336,7 @@ export function discoverAgents(
|
|
|
316
336
|
scope,
|
|
317
337
|
agents,
|
|
318
338
|
projectAgentsDir,
|
|
339
|
+
diagnostics,
|
|
319
340
|
dirSignatures,
|
|
320
341
|
};
|
|
321
342
|
|
package/extensions/index.ts
CHANGED
|
@@ -25,11 +25,12 @@ import {
|
|
|
25
25
|
getAgentDir,
|
|
26
26
|
getMarkdownTheme,
|
|
27
27
|
ModelRegistry,
|
|
28
|
+
type ThemeColor,
|
|
28
29
|
} from "@earendil-works/pi-coding-agent";
|
|
29
30
|
import { Container, Markdown, SelectList, Spacer, Text } from "@earendil-works/pi-tui";
|
|
30
31
|
import { Type } from "typebox";
|
|
31
32
|
|
|
32
|
-
import { type AgentConfig, type AgentScope, discoverAgents, formatAgentList, invalidateAgentCache } from "./agents.ts";
|
|
33
|
+
import { type AgentColor, type AgentConfig, type AgentScope, discoverAgents, formatAgentList, invalidateAgentCache } from "./agents.ts";
|
|
33
34
|
import {
|
|
34
35
|
type SubAgentResult,
|
|
35
36
|
getFinalOutput,
|
|
@@ -44,6 +45,7 @@ import {
|
|
|
44
45
|
validateAgentTools,
|
|
45
46
|
truncateParallelOutput,
|
|
46
47
|
validateExecutionRequest,
|
|
48
|
+
READ_ONLY_TOOLS,
|
|
47
49
|
MAX_CONCURRENCY,
|
|
48
50
|
MAX_PARALLEL_TASKS,
|
|
49
51
|
MAX_CHAIN_LENGTH,
|
|
@@ -159,7 +161,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
159
161
|
// Inject available agent catalog into system prompt for semantic auto-delegation
|
|
160
162
|
pi.on("before_agent_start", async (event) => {
|
|
161
163
|
const ctx = currentCtx;
|
|
162
|
-
const discovery = discoverAgents(
|
|
164
|
+
const discovery = discoverAgents(ctx?.cwd ?? process.cwd(), "both", bundledAgentsDir);
|
|
163
165
|
const catalog = discovery.agents
|
|
164
166
|
.map((a) => {
|
|
165
167
|
const modelInfo = a.model ? ` (model: ${a.model})` : " (inherits parent)";
|
|
@@ -192,7 +194,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
192
194
|
request.respond({ id: request.id, ok: false, error: `Unknown agent: ${request.agent}` });
|
|
193
195
|
return;
|
|
194
196
|
}
|
|
195
|
-
const thread = threadStore.createThread({ agentName: agent.name, task: request.task, mode: "single", color: agent.color });
|
|
197
|
+
const thread = threadStore.createThread({ agentName: agent.name, task: request.task, mode: "single", color: agent.color ? AGENT_TO_THEME_COLOR[agent.color as AgentColor] : undefined });
|
|
196
198
|
void runNamedAgent({
|
|
197
199
|
agent: request.readOnly ? { ...agent, tools: ["read", "grep", "find", "ls"] } : agent,
|
|
198
200
|
task: request.task,
|
|
@@ -207,11 +209,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
207
209
|
status: isFailedResult(result) ? (result.stopReason === "aborted" ? "aborted" : "failed") : "completed",
|
|
208
210
|
result,
|
|
209
211
|
});
|
|
210
|
-
|
|
211
|
-
|
|
212
|
+
try {
|
|
213
|
+
if (isFailedResult(result)) request.respond({ id: request.id, ok: false, error: getResultOutput(result) });
|
|
214
|
+
else request.respond({ id: request.id, ok: true, result });
|
|
215
|
+
} catch { /* respond channel closed */ }
|
|
212
216
|
}, (error) => {
|
|
213
217
|
threadStore.updateThread(thread.id, { status: "failed" });
|
|
214
|
-
|
|
218
|
+
try {
|
|
219
|
+
request.respond({ id: request.id, ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
220
|
+
} catch { /* respond channel closed */ }
|
|
215
221
|
});
|
|
216
222
|
});
|
|
217
223
|
|
|
@@ -228,9 +234,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
228
234
|
const list = formatAgentList(fresh.agents, 20);
|
|
229
235
|
const extra = list.remaining > 0 ? `\n ... +${list.remaining} more` : "";
|
|
230
236
|
const dirs = fresh.projectAgentsDir ? `project: ${fresh.projectAgentsDir}` : "no project agents dir";
|
|
237
|
+
const diagText = fresh.diagnostics.length > 0
|
|
238
|
+
? "\n\nWarnings:\n" + fresh.diagnostics.map(d => ` - [${d.severity}] ${d.filePath}: ${d.issue}`).join("\n")
|
|
239
|
+
: "";
|
|
231
240
|
pi.sendMessage({
|
|
232
241
|
customType: "pi-subagent",
|
|
233
|
-
content: `Agent definitions reloaded.\n\nAvailable agents (${fresh.agents.length}):\n ${list.text}${extra}\n\nDirectories searched:\n user: ${path.join(getAgentDir(), "agents")}\n ${dirs}\n bundled: ${bundledAgentsDir}`,
|
|
242
|
+
content: `Agent definitions reloaded.\n\nAvailable agents (${fresh.agents.length}):\n ${list.text}${extra}${diagText}\n\nDirectories searched:\n user: ${path.join(getAgentDir(), "agents")}\n ${dirs}\n bundled: ${bundledAgentsDir}`,
|
|
234
243
|
display: true,
|
|
235
244
|
});
|
|
236
245
|
ctx.ui.notify("Agent definitions reloaded", "info");
|
|
@@ -242,9 +251,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
242
251
|
const list = formatAgentList(discovery.agents, 20);
|
|
243
252
|
const extra = list.remaining > 0 ? `\n ... +${list.remaining} more` : "";
|
|
244
253
|
const dirs = discovery.projectAgentsDir ? `\n project: ${discovery.projectAgentsDir}` : "";
|
|
254
|
+
const diagText = discovery.diagnostics.length > 0
|
|
255
|
+
? "\n\nWarnings:\n" + discovery.diagnostics.map(d => ` - [${d.severity}] ${d.filePath}: ${d.issue}`).join("\n")
|
|
256
|
+
: "";
|
|
245
257
|
pi.sendMessage({
|
|
246
258
|
customType: "pi-subagent",
|
|
247
|
-
content: `Available agents (${discovery.agents.length}):\n ${list.text}${extra}\n\nScopes searched:\n user: ${path.join(getAgentDir(), "agents")}${dirs}\n bundled: ${bundledAgentsDir}\n\nUse /subagent <name> for agent details, /subagent reload to refresh.`,
|
|
259
|
+
content: `Available agents (${discovery.agents.length}):\n ${list.text}${extra}${diagText}\n\nScopes searched:\n user: ${path.join(getAgentDir(), "agents")}${dirs}\n bundled: ${bundledAgentsDir}\n\nUse /subagent <name> for agent details, /subagent reload to refresh.`,
|
|
248
260
|
display: true,
|
|
249
261
|
});
|
|
250
262
|
return;
|
|
@@ -281,14 +293,45 @@ export default function (pi: ExtensionAPI) {
|
|
|
281
293
|
const list = formatAgentList(discovery.agents, 20);
|
|
282
294
|
const extra = list.remaining > 0 ? `\n ... +${list.remaining} more` : "";
|
|
283
295
|
const dirs = discovery.projectAgentsDir ? `\n project: ${discovery.projectAgentsDir}` : "";
|
|
296
|
+
const diagText = discovery.diagnostics.length > 0
|
|
297
|
+
? "\n\nWarnings:\n" + discovery.diagnostics.map(d => ` - [${d.severity}] ${d.filePath}: ${d.issue}`).join("\n")
|
|
298
|
+
: "";
|
|
284
299
|
pi.sendMessage({
|
|
285
300
|
customType: "pi-subagent",
|
|
286
|
-
content: `Available agents (${discovery.agents.length}):\n ${list.text}${extra}\n\nScopes searched:\n user: ${path.join(getAgentDir(), "agents")}${dirs}\n bundled: ${bundledAgentsDir}\n\nUse /subagent <name> for agent details, /subagent reload to refresh.`,
|
|
301
|
+
content: `Available agents (${discovery.agents.length}):\n ${list.text}${extra}${diagText}\n\nScopes searched:\n user: ${path.join(getAgentDir(), "agents")}${dirs}\n bundled: ${bundledAgentsDir}\n\nUse /subagent <name> for agent details, /subagent reload to refresh.`,
|
|
287
302
|
display: true,
|
|
288
303
|
});
|
|
289
304
|
},
|
|
290
305
|
});
|
|
291
306
|
|
|
307
|
+
/** Map AgentColor (from agent frontmatter) to ThemeColor (for pi TUI). */
|
|
308
|
+
const AGENT_TO_THEME_COLOR: Record<AgentColor, ThemeColor> = {
|
|
309
|
+
red: "error",
|
|
310
|
+
blue: "accent",
|
|
311
|
+
green: "success",
|
|
312
|
+
yellow: "warning",
|
|
313
|
+
purple: "syntaxType",
|
|
314
|
+
orange: "syntaxString",
|
|
315
|
+
pink: "customMessageLabel",
|
|
316
|
+
cyan: "syntaxVariable",
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/** Resolve agent-defined color to a valid ThemeColor for thread creation. */
|
|
320
|
+
const agentToThemeColor = (agentName: string): ThemeColor | undefined => {
|
|
321
|
+
const ctx = currentCtx;
|
|
322
|
+
if (!ctx) return undefined;
|
|
323
|
+
const agent = discoverAgents(ctx.cwd, "both", bundledAgentsDir).agents.find(a => a.name === agentName);
|
|
324
|
+
return agent?.color ? AGENT_TO_THEME_COLOR[agent.color] : undefined;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/** Look up agent color by name for TUI rendering. */
|
|
328
|
+
const resolveAgentColor = (name: string): ThemeColor => {
|
|
329
|
+
const ctx = currentCtx;
|
|
330
|
+
if (!ctx) return "accent";
|
|
331
|
+
const found = discoverAgents(ctx.cwd, "both", bundledAgentsDir).agents.find(a => a.name === name);
|
|
332
|
+
return found?.color ? AGENT_TO_THEME_COLOR[found.color] : "accent";
|
|
333
|
+
};
|
|
334
|
+
|
|
292
335
|
pi.registerTool({
|
|
293
336
|
name: "subagent",
|
|
294
337
|
label: "Subagent",
|
|
@@ -438,10 +481,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
438
481
|
}
|
|
439
482
|
|
|
440
483
|
// Helper: validate and normalise tools for an agent.
|
|
441
|
-
function resolveChildTools(agentTools: string[] | undefined, readOnly?: boolean): string[] {
|
|
484
|
+
function resolveChildTools(agentTools: string[] | undefined, sandbox?: string, readOnly?: boolean): string[] {
|
|
442
485
|
const defaultTools = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
443
|
-
|
|
444
|
-
|
|
486
|
+
let rawTools = agentTools ?? defaultTools;
|
|
487
|
+
// sandbox overrides tools: silently strip mutation tools, not an error
|
|
488
|
+
if (sandbox === "read-only") {
|
|
489
|
+
rawTools = rawTools.filter(t => READ_ONLY_TOOLS.includes(t));
|
|
490
|
+
if (rawTools.length === 0) rawTools = [...READ_ONLY_TOOLS];
|
|
491
|
+
}
|
|
492
|
+
const effectiveReadOnly = readOnly || sandbox === "read-only";
|
|
493
|
+
const result = validateAgentTools({ tools: rawTools, readOnly: effectiveReadOnly });
|
|
445
494
|
if (result.errors.length > 0) {
|
|
446
495
|
throw new Error(`Tool validation errors: ${result.errors.join("; ")}`);
|
|
447
496
|
}
|
|
@@ -476,6 +525,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
476
525
|
agent: agentName,
|
|
477
526
|
task,
|
|
478
527
|
exitCode: 1,
|
|
528
|
+
status: "error",
|
|
529
|
+
stopReason: "error",
|
|
479
530
|
messages: [],
|
|
480
531
|
stderr: `Unknown agent: "${agentName}". Available: ${available}.`,
|
|
481
532
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
@@ -491,6 +542,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
491
542
|
agent: agentName,
|
|
492
543
|
task,
|
|
493
544
|
exitCode: 1,
|
|
545
|
+
status: "error",
|
|
546
|
+
stopReason: "error",
|
|
494
547
|
messages: [],
|
|
495
548
|
stderr: `Model not found for agent "${agentName}". Tried: ${tried}. Parent model: ${parentInfo}. Check agent definition and pi model configuration.`,
|
|
496
549
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
@@ -505,7 +558,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
505
558
|
try {
|
|
506
559
|
// Inject parent's API key so --api-key and other runtime overrides work
|
|
507
560
|
await injectApiKey(resolved.model);
|
|
508
|
-
tools = resolveChildTools(agent.tools, isReadOnly);
|
|
561
|
+
tools = resolveChildTools(agent.tools, agent.sandbox, isReadOnly);
|
|
509
562
|
effectiveTimeoutMs = resolveChildTimeout(timeoutMs, params.timeout);
|
|
510
563
|
safeCwd = resolveChildCwd(cwd);
|
|
511
564
|
} catch (err: unknown) {
|
|
@@ -514,6 +567,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
514
567
|
agent: agentName,
|
|
515
568
|
task,
|
|
516
569
|
exitCode: 1,
|
|
570
|
+
status: "error",
|
|
571
|
+
stopReason: "error",
|
|
517
572
|
messages: [],
|
|
518
573
|
stderr: `Validation error: ${errorMsg}`,
|
|
519
574
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
@@ -547,14 +602,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
547
602
|
|
|
548
603
|
for (let i = 0; i < params.chain.length; i++) {
|
|
549
604
|
const step = params.chain[i];
|
|
550
|
-
const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
|
|
605
|
+
const taskWithContext = step.task.replace(/\{previous\}/g, () => previousOutput);
|
|
551
606
|
|
|
552
607
|
const thread = threadStore.createThread({
|
|
553
608
|
agentName: step.agent,
|
|
554
609
|
task: taskWithContext,
|
|
555
610
|
mode: "chain-step",
|
|
556
611
|
toolCallId: _toolCallId,
|
|
557
|
-
color:
|
|
612
|
+
color: agentToThemeColor(step.agent),
|
|
558
613
|
});
|
|
559
614
|
const result = await runOne(
|
|
560
615
|
step.agent, taskWithContext, step.cwd,
|
|
@@ -646,7 +701,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
646
701
|
task: t.task,
|
|
647
702
|
mode: "parallel-task",
|
|
648
703
|
toolCallId: _toolCallId,
|
|
649
|
-
color:
|
|
704
|
+
color: agentToThemeColor(t.agent),
|
|
650
705
|
}),
|
|
651
706
|
);
|
|
652
707
|
|
|
@@ -689,6 +744,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
689
744
|
agent: t.agent,
|
|
690
745
|
task: t.task,
|
|
691
746
|
exitCode: 1,
|
|
747
|
+
status: "error",
|
|
692
748
|
messages: [],
|
|
693
749
|
stderr: "",
|
|
694
750
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
@@ -761,7 +817,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
761
817
|
task: params.task,
|
|
762
818
|
mode: "single",
|
|
763
819
|
toolCallId: _toolCallId,
|
|
764
|
-
color:
|
|
820
|
+
color: agentToThemeColor(params.agent),
|
|
765
821
|
});
|
|
766
822
|
const result = await runOne(
|
|
767
823
|
params.agent, params.task, params.cwd,
|
|
@@ -814,14 +870,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
814
870
|
// TUI rendering
|
|
815
871
|
// ------------------------------------------------------------------
|
|
816
872
|
|
|
817
|
-
/** Look up agent color by name for TUI rendering. */
|
|
818
|
-
const resolveAgentColor = (name: string): string => {
|
|
819
|
-
const ctx = currentCtx;
|
|
820
|
-
if (!ctx) return "accent";
|
|
821
|
-
const found = discoverAgents(ctx.cwd, "both", bundledAgentsDir).agents.find(a => a.name === name);
|
|
822
|
-
return found?.color ?? "accent";
|
|
823
|
-
};
|
|
824
|
-
|
|
825
873
|
renderCall(args, theme, _context) {
|
|
826
874
|
const scope: AgentScope = args.agentScope ?? "user";
|
|
827
875
|
const fg = theme.fg.bind(theme);
|
|
@@ -890,7 +938,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
890
938
|
|
|
891
939
|
// --- Single ---
|
|
892
940
|
if (details.mode === "single" && details.results.length === 1) {
|
|
893
|
-
|
|
941
|
+
const r = details.results[0];
|
|
942
|
+
return renderSingleResult(r, expanded, theme, resolveAgentColor(r.agent));
|
|
894
943
|
}
|
|
895
944
|
|
|
896
945
|
// --- Chain ---
|
package/extensions/render.ts
CHANGED
|
@@ -188,6 +188,7 @@ export function renderSingleResult(
|
|
|
188
188
|
result: SubAgentResult,
|
|
189
189
|
expanded: boolean,
|
|
190
190
|
theme: { fg: (c: any, t: string) => string; bold: (t: string) => string },
|
|
191
|
+
agentColor?: string,
|
|
191
192
|
): Container | Text {
|
|
192
193
|
const isError = isFailedResult(result);
|
|
193
194
|
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
@@ -197,7 +198,7 @@ export function renderSingleResult(
|
|
|
197
198
|
if (expanded) {
|
|
198
199
|
const mdTheme = getMarkdownTheme();
|
|
199
200
|
const container = new Container();
|
|
200
|
-
let header = `${icon} ${theme.fg("toolTitle", theme.bold(result.agent))}`;
|
|
201
|
+
let header = `${icon} ${theme.fg(agentColor ?? "toolTitle", theme.bold(result.agent))}`;
|
|
201
202
|
if (isError && result.stopReason) {
|
|
202
203
|
const reasonColor = result.stopReason === "timeout" ? "warning" : "error";
|
|
203
204
|
header += ` ${theme.fg(reasonColor, `[${result.stopReason}]`)}`;
|
|
@@ -239,7 +240,7 @@ export function renderSingleResult(
|
|
|
239
240
|
}
|
|
240
241
|
|
|
241
242
|
// Collapsed
|
|
242
|
-
let text = `${icon} ${theme.fg("toolTitle", theme.bold(result.agent))}`;
|
|
243
|
+
let text = `${icon} ${theme.fg(agentColor ?? "toolTitle", theme.bold(result.agent))}`;
|
|
243
244
|
if (isError && result.stopReason) {
|
|
244
245
|
const reasonColor = result.stopReason === "timeout" ? "warning" : "error";
|
|
245
246
|
text += ` ${theme.fg(reasonColor, `[${result.stopReason}]`)}`;
|
package/extensions/runner.ts
CHANGED
|
@@ -148,6 +148,8 @@ export async function runSubAgent(options: {
|
|
|
148
148
|
result.stopReason = isTimeout ? "timeout" : "aborted";
|
|
149
149
|
result.errorMessage = combinedSignal.reason instanceof Error ? combinedSignal.reason.message : "Sub-agent aborted before start";
|
|
150
150
|
result.status = classifyStopReason(result.stopReason, !isTimeout, isTimeout);
|
|
151
|
+
cleanupCombined?.();
|
|
152
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
151
153
|
return result;
|
|
152
154
|
}
|
|
153
155
|
|
|
@@ -167,6 +169,7 @@ export async function runSubAgent(options: {
|
|
|
167
169
|
let cleanupEventAbort: (() => void) | undefined;
|
|
168
170
|
let abortedBySignal = false;
|
|
169
171
|
let timedOut = false;
|
|
172
|
+
let eventUnsubscribe: (() => void) | undefined;
|
|
170
173
|
|
|
171
174
|
try {
|
|
172
175
|
// Wire combined abort signal to session
|
|
@@ -191,7 +194,8 @@ export async function runSubAgent(options: {
|
|
|
191
194
|
fn();
|
|
192
195
|
};
|
|
193
196
|
|
|
194
|
-
|
|
197
|
+
let unsubscribe: (() => void) | undefined;
|
|
198
|
+
unsubscribe = session.subscribe((event) => {
|
|
195
199
|
try {
|
|
196
200
|
switch (event.type) {
|
|
197
201
|
case "message_end": {
|
|
@@ -223,7 +227,7 @@ export async function runSubAgent(options: {
|
|
|
223
227
|
result.messages = event.messages as unknown as Message[];
|
|
224
228
|
}
|
|
225
229
|
finish(() => {
|
|
226
|
-
unsubscribe();
|
|
230
|
+
unsubscribe?.();
|
|
227
231
|
resolve();
|
|
228
232
|
});
|
|
229
233
|
break;
|
|
@@ -231,18 +235,19 @@ export async function runSubAgent(options: {
|
|
|
231
235
|
}
|
|
232
236
|
} catch (err) {
|
|
233
237
|
finish(() => {
|
|
234
|
-
unsubscribe();
|
|
238
|
+
unsubscribe?.();
|
|
235
239
|
reject(err);
|
|
236
240
|
});
|
|
237
241
|
}
|
|
238
242
|
});
|
|
243
|
+
eventUnsubscribe = unsubscribe;
|
|
239
244
|
|
|
240
245
|
// Resolve on abort so the eventPromise doesn't hang
|
|
241
246
|
const onAbortResolve = () => {
|
|
242
247
|
finish(() => {
|
|
243
248
|
result.exitCode = 1;
|
|
244
249
|
if (!result.errorMessage) result.errorMessage = "Sub-agent aborted";
|
|
245
|
-
unsubscribe();
|
|
250
|
+
unsubscribe?.();
|
|
246
251
|
resolve();
|
|
247
252
|
});
|
|
248
253
|
};
|
|
@@ -279,6 +284,7 @@ export async function runSubAgent(options: {
|
|
|
279
284
|
cleanupAbort?.();
|
|
280
285
|
cleanupEventAbort?.();
|
|
281
286
|
cleanupCombined();
|
|
287
|
+
eventUnsubscribe?.();
|
|
282
288
|
if (timeoutId) clearTimeout(timeoutId);
|
|
283
289
|
try {
|
|
284
290
|
session.dispose();
|
package/extensions/service.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
normalizeTimeout,
|
|
8
8
|
resolveSafeCwd,
|
|
9
9
|
MAX_INSTRUCTIONS_LENGTH,
|
|
10
|
+
READ_ONLY_TOOLS,
|
|
10
11
|
} from "./security.ts";
|
|
11
12
|
|
|
12
13
|
export const SUBAGENT_REQUEST_EVENT = "pi-subagent:run";
|
|
@@ -53,8 +54,13 @@ export async function runNamedAgent(options: {
|
|
|
53
54
|
const effectiveTimeoutMs = normalizeTimeout({ requested: options.timeout }).timeoutMs;
|
|
54
55
|
|
|
55
56
|
// Security: validate tools against allowlist.
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
let rawTools = options.agent.tools ?? ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
58
|
+
// Enforce read-only sandbox: strip mutating and execution tools
|
|
59
|
+
if (options.agent.sandbox === "read-only") {
|
|
60
|
+
rawTools = rawTools.filter(t => READ_ONLY_TOOLS.includes(t));
|
|
61
|
+
if (rawTools.length === 0) rawTools = [...READ_ONLY_TOOLS];
|
|
62
|
+
}
|
|
63
|
+
const toolValidation = validateAgentTools({ tools: rawTools, readOnly: options.agent.sandbox === "read-only" });
|
|
58
64
|
if (toolValidation.errors.length > 0) {
|
|
59
65
|
throw new Error(`Tool validation errors for agent "${options.agent.name}": ${toolValidation.errors.join("; ")}`);
|
|
60
66
|
}
|
|
@@ -226,6 +226,7 @@ export class ThreadViewer {
|
|
|
226
226
|
? Math.max(0, total - (maxVisible - 1))
|
|
227
227
|
: 0;
|
|
228
228
|
const offset = Math.max(0, Math.min(this.scrollOffset, maxOffset));
|
|
229
|
+
this.scrollOffset = offset;
|
|
229
230
|
|
|
230
231
|
// Reserve space for scroll indicators
|
|
231
232
|
const aboveShown = offset > 0;
|
package/package.json
CHANGED