@ferris1225/pi-subagents 0.8.0 → 0.9.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/package.json +2 -2
- package/src/index.ts +52 -17
- package/src/monitor.ts +26 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Focused sub-agent delegation for pi: explore / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@earendil-works/pi-coding-agent": "^0.83.0",
|
|
44
44
|
"@earendil-works/pi-tui": "^0.83.0",
|
|
45
45
|
"@types/node": "^22.10.0",
|
|
46
|
-
"typebox": "^1.3.
|
|
46
|
+
"typebox": "^1.3.9",
|
|
47
47
|
"typescript": "^5.9.0",
|
|
48
48
|
"vitest": "^4.1.0"
|
|
49
49
|
},
|
package/src/index.ts
CHANGED
|
@@ -33,17 +33,24 @@ import {
|
|
|
33
33
|
type SubagentLiveEvent,
|
|
34
34
|
type UsageStats,
|
|
35
35
|
} from "./spawn.ts";
|
|
36
|
-
import { formatToolActivity, monitor, statusColor, statusIcon, statusLabel } from "./monitor.ts";
|
|
36
|
+
import { formatTaskSummary, formatToolActivity, monitor, statusColor, statusIcon, statusLabel } from "./monitor.ts";
|
|
37
|
+
|
|
38
|
+
const NON_BLANK_TASK_OPTIONS = { minLength: 1, pattern: "\\S" } as const;
|
|
37
39
|
|
|
38
40
|
const TaskItem = Type.Object({
|
|
39
41
|
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
40
|
-
task: Type.String({
|
|
42
|
+
task: Type.String({
|
|
43
|
+
...NON_BLANK_TASK_OPTIONS,
|
|
44
|
+
description: "Self-contained task to delegate (the agent has no memory of this conversation)",
|
|
45
|
+
}),
|
|
41
46
|
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
|
42
47
|
});
|
|
43
48
|
|
|
44
49
|
const SubagentParams = Type.Object({
|
|
45
50
|
agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (single mode)" })),
|
|
46
|
-
task: Type.Optional(
|
|
51
|
+
task: Type.Optional(
|
|
52
|
+
Type.String({ ...NON_BLANK_TASK_OPTIONS, description: "Self-contained task to delegate (single mode)" }),
|
|
53
|
+
),
|
|
47
54
|
tasks: Type.Optional(Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" })),
|
|
48
55
|
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
|
|
49
56
|
});
|
|
@@ -240,7 +247,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
240
247
|
}));
|
|
241
248
|
|
|
242
249
|
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
243
|
-
const hasSingle = Boolean(params.agent && params.task
|
|
250
|
+
const hasSingle = Boolean(params.agent) && params.task !== undefined;
|
|
244
251
|
|
|
245
252
|
const makeDetails =
|
|
246
253
|
(mode: "single" | "parallel", background = false) =>
|
|
@@ -260,12 +267,37 @@ export default function (pi: ExtensionAPI): void {
|
|
|
260
267
|
};
|
|
261
268
|
}
|
|
262
269
|
|
|
270
|
+
if (hasTasks) {
|
|
271
|
+
const blankTaskIndex = params.tasks?.findIndex(({ task }) => task.trim().length === 0) ?? -1;
|
|
272
|
+
if (blankTaskIndex !== -1) {
|
|
273
|
+
return {
|
|
274
|
+
content: [
|
|
275
|
+
{
|
|
276
|
+
type: "text",
|
|
277
|
+
text: `Invalid parameters. tasks[${blankTaskIndex}].task must contain at least one non-whitespace character. No background tasks were started. Enabled agents: ${catalog}.`,
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
details: makeDetails("parallel")([]),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
} else if (params.task?.trim().length === 0) {
|
|
284
|
+
return {
|
|
285
|
+
content: [
|
|
286
|
+
{
|
|
287
|
+
type: "text",
|
|
288
|
+
text: `Invalid parameters. task must contain at least one non-whitespace character. Enabled agents: ${catalog}.`,
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
details: makeDetails("single")([]),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
263
295
|
const startBackground = (agentName: string, task: string, cwd?: string): SingleResult => {
|
|
264
296
|
const agent = agents.find((candidate) => candidate.name === agentName);
|
|
265
297
|
if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
|
|
266
298
|
|
|
267
299
|
const pending = queuedResult(agent, task);
|
|
268
|
-
const runId = monitor.addRun(agent.name, agent.model);
|
|
300
|
+
const runId = monitor.addRun(agent.name, task, agent.model);
|
|
269
301
|
const onLive = makeLiveHandler(runId);
|
|
270
302
|
|
|
271
303
|
backgroundQueue.enqueue(
|
|
@@ -301,7 +333,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
301
333
|
pi.sendMessage(
|
|
302
334
|
{
|
|
303
335
|
customType: "subagent-result",
|
|
304
|
-
content: `### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}\n\n${getResultOutput(result)}`,
|
|
336
|
+
content: `### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}\n\nTask: ${formatTaskSummary(result.task)}\n\n${getResultOutput(result)}`,
|
|
305
337
|
display: true,
|
|
306
338
|
},
|
|
307
339
|
// The result is both durable context and a wake-up signal. If the
|
|
@@ -318,17 +350,17 @@ export default function (pi: ExtensionAPI): void {
|
|
|
318
350
|
// Sub-agents intentionally detach from the foreground turn. This makes the
|
|
319
351
|
// editor available immediately; completion messages later wake the main agent.
|
|
320
352
|
if (params.tasks && params.tasks.length > 0) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
353
|
+
if (params.tasks.length > config.maxParallelTasks) {
|
|
354
|
+
return {
|
|
355
|
+
content: [
|
|
356
|
+
{
|
|
357
|
+
type: "text",
|
|
358
|
+
text: `Too many parallel tasks (${params.tasks.length}). Max is ${config.maxParallelTasks} (configurable via /subagents-setup).`,
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
details: makeDetails("parallel", true)([]),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
332
364
|
|
|
333
365
|
const results = params.tasks.map((task) => startBackground(task.agent, task.task, task.cwd));
|
|
334
366
|
const started = results.filter((result) => result.exitCode === -1).length;
|
|
@@ -442,6 +474,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
442
474
|
const icon = statusIcon(r.status, theme);
|
|
443
475
|
const label = theme.fg(statusColor(r.status), statusLabel(r.status));
|
|
444
476
|
lines.push(truncateToWidth(` ${icon} ${monitor.summarize(r)} · ${label}`, width, ""));
|
|
477
|
+
if (r.status === "queued" || r.status === "running") {
|
|
478
|
+
lines.push(truncateToWidth(theme.fg("dim", ` task: ${formatTaskSummary(r.task)}`), width, ""));
|
|
479
|
+
}
|
|
445
480
|
// Activity sits one indent level below the agent name.
|
|
446
481
|
if (r.activity) lines.push(truncateToWidth(theme.fg("dim", ` ${r.activity}`), width, ""));
|
|
447
482
|
}
|
package/src/monitor.ts
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* conversation, so a stale "done" row must not linger in the widget.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { stripVTControlCharacters } from "node:util";
|
|
13
14
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
14
16
|
import type { UsageStats } from "./spawn.ts";
|
|
15
17
|
|
|
16
18
|
// ---------------------------------------------------------------------------
|
|
@@ -22,6 +24,7 @@ export type RunStatus = "queued" | "running" | "done" | "failed";
|
|
|
22
24
|
export interface RunView {
|
|
23
25
|
id: number;
|
|
24
26
|
agent: string;
|
|
27
|
+
task: string;
|
|
25
28
|
model?: string;
|
|
26
29
|
status: RunStatus;
|
|
27
30
|
usage: UsageStats;
|
|
@@ -37,6 +40,27 @@ export interface RunView {
|
|
|
37
40
|
// Formatting helpers
|
|
38
41
|
// ---------------------------------------------------------------------------
|
|
39
42
|
|
|
43
|
+
const TASK_SUMMARY_MAX = 80;
|
|
44
|
+
const TASK_SUMMARY_ELLIPSIS = "…";
|
|
45
|
+
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
46
|
+
|
|
47
|
+
/** One-line task preview, capped by terminal display columns (including the ellipsis). */
|
|
48
|
+
export function formatTaskSummary(task: string): string {
|
|
49
|
+
const oneLine = stripVTControlCharacters(task).replace(/\s+/g, " ").trim();
|
|
50
|
+
if (visibleWidth(oneLine) <= TASK_SUMMARY_MAX) return oneLine;
|
|
51
|
+
|
|
52
|
+
const prefixMax = TASK_SUMMARY_MAX - visibleWidth(TASK_SUMMARY_ELLIPSIS);
|
|
53
|
+
let prefix = "";
|
|
54
|
+
let prefixWidth = 0;
|
|
55
|
+
for (const { segment } of graphemeSegmenter.segment(oneLine)) {
|
|
56
|
+
const segmentWidth = visibleWidth(segment);
|
|
57
|
+
if (prefixWidth + segmentWidth > prefixMax) break;
|
|
58
|
+
prefix += segment;
|
|
59
|
+
prefixWidth += segmentWidth;
|
|
60
|
+
}
|
|
61
|
+
return `${prefix}${TASK_SUMMARY_ELLIPSIS}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
40
64
|
function formatTokens(count: number): string {
|
|
41
65
|
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
42
66
|
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
@@ -142,11 +166,12 @@ export class MonitorStore {
|
|
|
142
166
|
this.notify();
|
|
143
167
|
}
|
|
144
168
|
|
|
145
|
-
addRun(agent: string, model?: string): number {
|
|
169
|
+
addRun(agent: string, task: string, model?: string): number {
|
|
146
170
|
const id = this.nextId++;
|
|
147
171
|
this.runs.push({
|
|
148
172
|
id,
|
|
149
173
|
agent,
|
|
174
|
+
task,
|
|
150
175
|
model,
|
|
151
176
|
status: "queued",
|
|
152
177
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|