@arhen/pi-core-subagent 1.0.8 → 1.1.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 +1 -1
- package/src/index.ts +22 -15
- package/src/agents.ts +0 -70
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arhen/pi-core-subagent",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pi extension: fast in-process subagents with single/parallel/chain, background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -18,7 +18,6 @@ import { StringEnum, type Api, type AssistantMessage, type Model } from "@earend
|
|
|
18
18
|
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
19
19
|
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
20
20
|
import { Type } from "typebox";
|
|
21
|
-
import { lookupAgent } from "./agents.ts";
|
|
22
21
|
import { CHILD_TALK_TOOLS, createChildTools, createWatchdog, type ChildHandlers } from "./child.ts";
|
|
23
22
|
import { createMailbox, type Mailbox } from "./mailbox.ts";
|
|
24
23
|
|
|
@@ -251,6 +250,10 @@ function compactLines(run: RunSnapshot): string[] {
|
|
|
251
250
|
* └─ ✓ reviewer · 6 tools · 44s
|
|
252
251
|
* Static icons (no animation); latest activity + tool count + runtime per agent.
|
|
253
252
|
*/
|
|
253
|
+
const WIDGET_MAX_RUNS = 3;
|
|
254
|
+
const WIDGET_MAX_TASKS_PER_RUN = 4;
|
|
255
|
+
const WIDGET_MAX_LINES = 10;
|
|
256
|
+
|
|
254
257
|
class SubagentsWidget implements Component {
|
|
255
258
|
constructor(
|
|
256
259
|
private readonly getRuns: () => RunSnapshot[],
|
|
@@ -265,17 +268,18 @@ class SubagentsWidget implements Component {
|
|
|
265
268
|
const runs = this.getRuns();
|
|
266
269
|
if (runs.length === 0) return [];
|
|
267
270
|
const lines: string[] = [];
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
+
let renderedRuns = 0;
|
|
272
|
+
for (const run of runs.slice(0, WIDGET_MAX_RUNS)) {
|
|
273
|
+
if (run.tasks.length === 0) continue;
|
|
274
|
+
if (lines.length > 0) lines.push("");
|
|
271
275
|
const done = run.tasks.filter((t) => TERMINAL.includes(t.status)).length;
|
|
272
276
|
const active = !TERMINAL.includes(run.status);
|
|
273
277
|
const head = active ? "accent" : "dim";
|
|
274
278
|
lines.push(truncateToWidth(`${this.theme.fg(head, active ? "●" : "○")} ${this.theme.fg(head, `Subagents (${done}/${run.tasks.length})`)}`, width, "…"));
|
|
275
279
|
const allDone = TERMINAL.includes(run.status);
|
|
276
|
-
const visible = run.tasks.slice(0,
|
|
280
|
+
const visible = run.tasks.slice(0, WIDGET_MAX_TASKS_PER_RUN);
|
|
277
281
|
visible.forEach((task, i) => {
|
|
278
|
-
const last = i === visible.length - 1 && run.tasks.length <=
|
|
282
|
+
const last = i === visible.length - 1 && run.tasks.length <= WIDGET_MAX_TASKS_PER_RUN;
|
|
279
283
|
const conn = this.theme.fg("dim", last ? "└─" : "├─");
|
|
280
284
|
const activity =
|
|
281
285
|
!TERMINAL.includes(task.status) && task.lastActivity
|
|
@@ -287,8 +291,12 @@ class SubagentsWidget implements Component {
|
|
|
287
291
|
: `${statusIcon(task.status)} ${task.agent} · ${activity}${taskStatsWithUsage(task)} · ${taskTimer(task)}`;
|
|
288
292
|
lines.push(truncateToWidth(`${conn} ${line}`, width, "…"));
|
|
289
293
|
});
|
|
290
|
-
if (run.tasks.length >
|
|
291
|
-
|
|
294
|
+
if (run.tasks.length > WIDGET_MAX_TASKS_PER_RUN) lines.push(`${this.theme.fg("dim", "└─")} ${this.theme.fg("dim", `+${run.tasks.length - WIDGET_MAX_TASKS_PER_RUN} more tasks`)}`);
|
|
295
|
+
renderedRuns += 1;
|
|
296
|
+
if (lines.length >= WIDGET_MAX_LINES) break; // keep the editor visible
|
|
297
|
+
}
|
|
298
|
+
const hiddenRuns = runs.length - renderedRuns;
|
|
299
|
+
if (hiddenRuns > 0) lines.push(`${this.theme.fg("dim", "└─")} ${this.theme.fg("dim", `+${hiddenRuns} more run${hiddenRuns > 1 ? "s" : ""}`)}`);
|
|
292
300
|
return lines;
|
|
293
301
|
}
|
|
294
302
|
}
|
|
@@ -622,17 +630,16 @@ class SubagentManager {
|
|
|
622
630
|
|
|
623
631
|
// Inline params win; otherwise fall back to an existing agent file
|
|
624
632
|
// (~/.agents, .pi/agents, user dir). Never creates files.
|
|
625
|
-
const
|
|
626
|
-
const
|
|
627
|
-
const
|
|
628
|
-
const baseTools = input.tools ?? (input.write ? WRITE_TOOLS : fileAgent?.tools ?? READONLY_TOOLS);
|
|
633
|
+
const prompt = input.prompt?.trim();
|
|
634
|
+
const thinking = input.thinking;
|
|
635
|
+
const baseTools = input.tools ?? (input.write ? WRITE_TOOLS : READONLY_TOOLS);
|
|
629
636
|
const tools = [...baseTools, ...(run.allowIntercom ? CHILD_TALK_TOOLS : [])];
|
|
630
637
|
|
|
631
638
|
// Model + thinking resolve against the pi model registry; a bad request
|
|
632
639
|
// fails the TASK with a helpful message, not the whole run.
|
|
633
640
|
let model: Model<Api> | undefined;
|
|
634
641
|
try {
|
|
635
|
-
model = resolveChildModel(ctx, input.model
|
|
642
|
+
model = resolveChildModel(ctx, input.model);
|
|
636
643
|
validateThinking(model, thinking);
|
|
637
644
|
} catch (err) {
|
|
638
645
|
this.updateTask(run, task, {
|
|
@@ -646,7 +653,7 @@ class SubagentManager {
|
|
|
646
653
|
this.updateTask(run, task, {
|
|
647
654
|
status: "starting",
|
|
648
655
|
startedAt: Date.now(),
|
|
649
|
-
model: input.model
|
|
656
|
+
model: input.model,
|
|
650
657
|
thinking,
|
|
651
658
|
tools,
|
|
652
659
|
}, ctx, onUpdate);
|
|
@@ -1000,7 +1007,7 @@ let eventSeq = 0;
|
|
|
1000
1007
|
|
|
1001
1008
|
const TaskItem = Type.Object({
|
|
1002
1009
|
id: Type.Optional(Type.String({ description: "Optional stable task id" })),
|
|
1003
|
-
agent: Type.String({ minLength: 1, description: "Agent name
|
|
1010
|
+
agent: Type.String({ minLength: 1, description: "Agent name you invent. Always define the agent inline: prompt (system prompt) + toolset (write: true for write access). Never create agent files." }),
|
|
1004
1011
|
task: Type.String({ minLength: 1, description: "Task for this agent" }),
|
|
1005
1012
|
prompt: Type.Optional(Type.String({ description: "System prompt defining this agent's behavior. Optional — a minimal default is used." })),
|
|
1006
1013
|
write: Type.Optional(Type.Boolean({ description: "true = write toolset (read, bash, edit, write); default false = read-only (read, grep, find, ls)" })),
|
package/src/agents.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Optional agent-file lookup — fallback for inline definitions.
|
|
3
|
-
*
|
|
4
|
-
* The leader defines agents inline (prompt + toolset). If a task has no inline
|
|
5
|
-
* prompt, we LOOK UP a file by agent name — never create one:
|
|
6
|
-
* <cwd>/.agents/<name>.md (nearest project dir first)
|
|
7
|
-
* <cwd>/.pi/agents/<name>.md
|
|
8
|
-
* ~/.pi/agent/agents/<name>.md (user)
|
|
9
|
-
* First match wins; explicit inline params always override file contents.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import * as fs from "node:fs";
|
|
13
|
-
import * as path from "node:path";
|
|
14
|
-
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
|
|
16
|
-
export interface FileAgent {
|
|
17
|
-
prompt: string;
|
|
18
|
-
tools?: string[];
|
|
19
|
-
model?: string;
|
|
20
|
-
thinking?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function projectAgentDirs(cwd: string): string[] {
|
|
24
|
-
const out: string[] = [];
|
|
25
|
-
let dir = cwd;
|
|
26
|
-
for (;;) {
|
|
27
|
-
for (const sub of [".agents", path.join(".pi", "agents")]) {
|
|
28
|
-
const candidate = path.join(dir, sub);
|
|
29
|
-
try {
|
|
30
|
-
if (fs.statSync(candidate).isDirectory()) out.push(candidate);
|
|
31
|
-
} catch {
|
|
32
|
-
/* missing dir is fine */
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
const parent = path.dirname(dir);
|
|
36
|
-
if (parent === dir) break;
|
|
37
|
-
dir = parent;
|
|
38
|
-
}
|
|
39
|
-
return out;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function lookupAgent(name: string, cwd: string): FileAgent | undefined {
|
|
43
|
-
const dirs = [...projectAgentDirs(cwd), path.join(getAgentDir(), "agents")];
|
|
44
|
-
for (const dir of dirs) {
|
|
45
|
-
const filePath = path.join(dir, `${name}.md`);
|
|
46
|
-
let content: string;
|
|
47
|
-
try {
|
|
48
|
-
content = fs.readFileSync(filePath, "utf-8");
|
|
49
|
-
} catch {
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
try {
|
|
53
|
-
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
|
|
54
|
-
if (!frontmatter.name || frontmatter.name !== name) continue;
|
|
55
|
-
const tools = frontmatter.tools
|
|
56
|
-
?.split(",")
|
|
57
|
-
.map((t) => t.trim())
|
|
58
|
-
.filter(Boolean);
|
|
59
|
-
return {
|
|
60
|
-
prompt: body,
|
|
61
|
-
tools: tools && tools.length > 0 ? tools : undefined,
|
|
62
|
-
model: frontmatter.model,
|
|
63
|
-
thinking: frontmatter.thinking,
|
|
64
|
-
};
|
|
65
|
-
} catch {
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|