@arhen/pi-core-subagent 1.3.25 → 1.3.27
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 +23 -2
- package/package.json +1 -1
- package/src/agentfile.ts +132 -0
- package/src/index.ts +2 -2
- package/src/manager.ts +8 -5
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ flowchart LR
|
|
|
50
50
|
- **A bad graph fails before it spawns.** Unknown ids, self-edges and cycles are rejected at call time — never halfway through a run with three children already burning tokens.
|
|
51
51
|
- **Proof is an exit code, never a self-report.** Tasks are asked for a runnable `Verify:` command; the leader checks `git diff --stat`. Agents auditing their own work score ~0. ([why](#why-9-is-a-verification-command-not-a-self-report))
|
|
52
52
|
- **No ceremony without edges.** Six independent reviewers stay six independent reviewers — no waves, no gates, no graph vocabulary imposed on flat work.
|
|
53
|
-
- **
|
|
53
|
+
- **Agent files respected.** A spawn goal (name + task) that matches a user agent file's `description` (`.agents/agents`, `.claude/agents`, `.pi/agents` — project then home) loads that file — body = system prompt, frontmatter `model`/`tools` apply, file `model` validated against the pi model registry. File wins over inline; no match → on-demand definition.
|
|
54
54
|
- **Two toolsets only.** Read-only (`read, grep, find, ls` — default) or write (`read, grep, find, ls, bash, edit, write` — `write: true`). No per-agent tool config surface.
|
|
55
55
|
- **In-process** — children are `AgentSession`s in the same runtime. No process spawn, no context bleed.
|
|
56
56
|
- **Zero parent-context injection.** No catalog, no context hook. 6 slim tools total.
|
|
@@ -83,7 +83,7 @@ The dotted arrows are the whole point: a child may burn 200k tokens reading file
|
|
|
83
83
|
|
|
84
84
|
## Usage — the leader invents the agents
|
|
85
85
|
|
|
86
|
-
Define agents inline per call
|
|
86
|
+
Define agents inline per call, or reference a named agent file (see [Agent files](#agent-files)). Model resolution: explicit `model` → agent-file `model` (validated against the pi model registry) → the parent's current model → settings default.
|
|
87
87
|
|
|
88
88
|
```json
|
|
89
89
|
{
|
|
@@ -116,6 +116,27 @@ Chain — `{previous}` is replaced with the prior agent's output:
|
|
|
116
116
|
}
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
## Agent files
|
|
120
|
+
|
|
121
|
+
A user agent file in an agents directory is matched by its `description` frontmatter against the spawn goal (`agent` name + `task`) — not by name. When matched, the file is **authoritative**: body = system prompt, frontmatter `model`/`tools` apply, inline `prompt`/`model`/`tools` are ignored. No match → the inline on-demand definition stands. The model stays in control: it names the agent and states the goal; user files that describe that goal take over.
|
|
122
|
+
|
|
123
|
+
```md
|
|
124
|
+
---
|
|
125
|
+
name: api-reviewer
|
|
126
|
+
description: reviews APIs for auth, rate limiting, and error handling
|
|
127
|
+
model: claude-opus-4-6
|
|
128
|
+
tools: read, grep, find, ls
|
|
129
|
+
---
|
|
130
|
+
You are a strict API reviewer. Check auth, rate limiting, and error handling. Cite file:line.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Lookup order** (first directory with a match wins):
|
|
134
|
+
|
|
135
|
+
1. `.agents/agents/` then `.claude/agents/` then `.pi/agents/` in each directory from the task `cwd` up to the filesystem root (nearest ancestor wins).
|
|
136
|
+
2. Home: `~/.agents/agents/` (single source) → `~/.claude/agents/` → `~/.pi/agents/`.
|
|
137
|
+
|
|
138
|
+
Within a directory the file with the highest description-overlap score wins (≥2 shared meaningful tokens). A file `model` is validated against the pi model registry (unknown model fails the task with a catalog message). Files without a `description` frontmatter never match.
|
|
139
|
+
|
|
119
140
|
## Graph mode — `needs`
|
|
120
141
|
|
|
121
142
|
`parallel` runs everything at once; `chain` runs everything one at a time. Most real work is neither. Give a task an `id` and list the ids it `needs`:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arhen/pi-core-subagent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pi extension: fast in-process subagents with a dependency-graph scheduler (needs edges gate tasks and carry upstream output into dependent prompts), plus background runs, intercom and agent-to-agent mailbox. Leader defines agents inline.",
|
|
6
6
|
"license": "MIT",
|
package/src/agentfile.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/** Agent-file resolution — matched by description (goal), not by name.
|
|
2
|
+
* The model names a subagent with a goal (name + task); user agent files in
|
|
3
|
+
* `.agents/agents`, `.claude/agents`, `.pi/agents` are scored by token overlap
|
|
4
|
+
* between their `description` frontmatter and that goal. Best match wins;
|
|
5
|
+
* ties break by directory priority. A matched file is authoritative (file wins
|
|
6
|
+
* over inline prompt/model/tools). No match → inline on-demand definition. */
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
export interface AgentFileInfo {
|
|
12
|
+
body: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
tools?: string[];
|
|
15
|
+
description?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const AGENT_DIRS = [".agents/agents", ".claude/agents", ".pi/agents"] as const;
|
|
19
|
+
const STOP = new Set([
|
|
20
|
+
"the",
|
|
21
|
+
"a",
|
|
22
|
+
"an",
|
|
23
|
+
"of",
|
|
24
|
+
"for",
|
|
25
|
+
"and",
|
|
26
|
+
"or",
|
|
27
|
+
"to",
|
|
28
|
+
"in",
|
|
29
|
+
"on",
|
|
30
|
+
"with",
|
|
31
|
+
"by",
|
|
32
|
+
"at",
|
|
33
|
+
"during",
|
|
34
|
+
"your",
|
|
35
|
+
"you",
|
|
36
|
+
"their",
|
|
37
|
+
"its",
|
|
38
|
+
"is",
|
|
39
|
+
"are",
|
|
40
|
+
"be",
|
|
41
|
+
"as",
|
|
42
|
+
"how",
|
|
43
|
+
"what",
|
|
44
|
+
"when",
|
|
45
|
+
"who",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/** Lowercase, split, drop stopwords, strip plural -s/-es. */
|
|
49
|
+
function tokens(text: string): string[] {
|
|
50
|
+
return (text.toLowerCase().match(/[a-z0-9]+/g) ?? [])
|
|
51
|
+
.filter((t) => !STOP.has(t) && t.length > 1)
|
|
52
|
+
.map((t) => {
|
|
53
|
+
if (t.endsWith("ing") && t.length > 5) t = t.slice(0, -3);
|
|
54
|
+
if (t.endsWith("es") && t.length > 4) t = t.slice(0, -2);
|
|
55
|
+
else if (t.endsWith("s") && t.length > 3) t = t.slice(0, -1);
|
|
56
|
+
return t;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function score(query: string[], desc: string[]): number {
|
|
61
|
+
let shared = 0;
|
|
62
|
+
for (const t of query) if (desc.includes(t)) shared += 1;
|
|
63
|
+
return shared >= 2 ? shared : 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readAgentFile(dir: string): AgentFileInfo[] {
|
|
67
|
+
if (!existsSync(dir)) return [];
|
|
68
|
+
const out: AgentFileInfo[] = [];
|
|
69
|
+
for (const entry of readdirSync(dir)) {
|
|
70
|
+
if (!entry.endsWith(".md")) continue;
|
|
71
|
+
const { frontmatter, body } = parseFrontmatter(readFileSync(join(dir, entry), "utf8"));
|
|
72
|
+
const tools =
|
|
73
|
+
typeof frontmatter.tools === "string"
|
|
74
|
+
? frontmatter.tools
|
|
75
|
+
.split(",")
|
|
76
|
+
.map((t) => t.trim())
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
: Array.isArray(frontmatter.tools)
|
|
79
|
+
? frontmatter.tools.map(String)
|
|
80
|
+
: undefined;
|
|
81
|
+
out.push({
|
|
82
|
+
body,
|
|
83
|
+
model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
|
|
84
|
+
tools: tools?.length ? tools : undefined,
|
|
85
|
+
description: typeof frontmatter.description === "string" ? frontmatter.description : undefined,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Best agent-file match for a spawn goal. Order: `.agents/agents` (single
|
|
93
|
+
* source) → `.claude/agents` → `.pi/agents` per cwd ancestor (nearest first),
|
|
94
|
+
* then home (`~/.agents` → `~/.claude` → `~/.pi`). Within a dir, highest
|
|
95
|
+
* description-overlap score wins; the first dir with a match is returned.
|
|
96
|
+
* `agentDir` is the pi agent dir (`~/.pi/agent`); home is derived from it.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveAgentFile(name: string, task: string, cwd: string, agentDir: string): AgentFileInfo | undefined {
|
|
99
|
+
const query = tokens(`${name} ${task}`);
|
|
100
|
+
let dir = cwd;
|
|
101
|
+
while (true) {
|
|
102
|
+
for (const sub of AGENT_DIRS) {
|
|
103
|
+
let best: AgentFileInfo | undefined;
|
|
104
|
+
let bestScore = 0;
|
|
105
|
+
for (const file of readAgentFile(join(dir, sub))) {
|
|
106
|
+
const s = score(query, tokens(file.description ?? ""));
|
|
107
|
+
if (s > bestScore) {
|
|
108
|
+
best = file;
|
|
109
|
+
bestScore = s;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (best) return best; // first dir with any match wins (priority over score)
|
|
113
|
+
}
|
|
114
|
+
const parent = dirname(dir);
|
|
115
|
+
if (parent === dir) break;
|
|
116
|
+
dir = parent;
|
|
117
|
+
}
|
|
118
|
+
const home = dirname(dirname(agentDir)); // ~/.pi/agent → ~
|
|
119
|
+
for (const sub of AGENT_DIRS) {
|
|
120
|
+
let best: AgentFileInfo | undefined;
|
|
121
|
+
let bestScore = 0;
|
|
122
|
+
for (const file of readAgentFile(join(home, sub))) {
|
|
123
|
+
const s = score(query, tokens(file.description ?? ""));
|
|
124
|
+
if (s > bestScore) {
|
|
125
|
+
best = file;
|
|
126
|
+
bestScore = s;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (best) return best;
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -127,7 +127,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
127
127
|
// ponytail: this string is billed on every request. No example block — an example
|
|
128
128
|
// biases the model toward one shape; guidelines + JSON schema describe all of them.
|
|
129
129
|
description:
|
|
130
|
-
"Run isolated subagents (own context, own session). You invent each agent: name, optional system prompt, toolset (read-only default, write:true to edit). Use `agent`+`task` for one, `tasks` for many. `needs` declares dependency edges: a task waits for its needs and receives their outputs prepended to its prompt. Every run is background: the call returns a runId immediately and completion notifies you. Set autoAwait:true when you need the result before your next step — the call parks until the run finishes and returns runId + final result in one response. allowIntercom:true lets children talk to you and each other.",
|
|
130
|
+
"Run isolated subagents (own context, own session). You invent each agent: name, optional system prompt, toolset (read-only default, write:true to edit). Use `agent`+`task` for one, `tasks` for many. `needs` declares dependency edges: a task waits for its needs and receives their outputs prepended to its prompt. If a user agent file in `.agents/agents`, `.claude/agents`, or `.pi/agents` (project dirs, then home) has a `description` matching the spawn goal (name + task), that file is authoritative: body = system prompt, frontmatter `model`/`tools` apply, inline prompt/model/tools ignored. No match → the inline definition stands. Every run is background: the call returns a runId immediately and completion notifies you. Set autoAwait:true when you need the result before your next step — the call parks until the run finishes and returns runId + final result in one response. allowIntercom:true lets children talk to you and each other.",
|
|
131
131
|
promptSnippet: "Define and delegate work to specialized subagents.",
|
|
132
132
|
promptGuidelines: [
|
|
133
133
|
"Use subagent when independent review, testing, research, or parallel analysis improves quality.",
|
|
@@ -135,7 +135,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
135
135
|
"Order comes from `needs`, not from separate calls: give tasks an `id`, list the ids each depends on. Tasks with no unmet needs run in parallel; dependents receive their upstream outputs automatically — do not restate them.",
|
|
136
136
|
"Prefer flat `tasks` (plain parallel) unless a real dependency exists — only add `needs` edges when ordering genuinely matters.",
|
|
137
137
|
"End each task with a runnable check, e.g. 'Verify: npx tsc --noEmit && bun test'. A subagent's claim of success is not evidence.",
|
|
138
|
-
"Define each agent yourself: invented name, focused system prompt, and read-only (default) or write:true. Prefer read-only.",
|
|
138
|
+
"Define each agent yourself: invented name, focused system prompt, and read-only (default) or write:true. Prefer read-only. A user agent file (`.agents/agents`, `.claude/agents`, `.pi/agents` — project first, then home) whose `description` matches the spawn goal (name + task) takes over: its body is the system prompt, frontmatter `model`/`tools` apply and are validated against the model registry. Matching is by description, not name — name the agent whatever fits the goal.",
|
|
139
139
|
"When you need a run's result before your next step, spawn with autoAwait:true — the call returns runId + final result in one response. Otherwise spawn background and settle results (await_subagent / subagent_result) before continuing dependent work.",
|
|
140
140
|
"For long multi-task runs, don't autoAwait the whole run: spawn background, then loop await_subagent with short timeoutMs slices (e.g. 20s), processing whichever tasks completed in each slice while the rest keep running. You get incremental results instead of one big wait.",
|
|
141
141
|
"allowIntercom:true only when a child may need to ask you something.",
|
package/src/manager.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type ToolDefinition,
|
|
17
17
|
} from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import type { TUI } from "@earendil-works/pi-tui";
|
|
19
|
+
import { resolveAgentFile } from "./agentfile.ts";
|
|
19
20
|
import { CHILD_TALK_TOOLS, type ChildHandlers, createChildTools, createWatchdog, type Watchdog } from "./child.ts";
|
|
20
21
|
import {
|
|
21
22
|
activitySnippet,
|
|
@@ -631,18 +632,20 @@ export class SubagentManager {
|
|
|
631
632
|
): Promise<void> {
|
|
632
633
|
if (TERMINAL.includes(task.status)) return; // canceled while queued
|
|
633
634
|
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
|
|
635
|
+
// Matched user agent file (`.agents/agents` etc., by description): the file
|
|
636
|
+
// is authoritative — body = system prompt, frontmatter model/tools win over
|
|
637
|
+
// inline. No match → inline on-demand definition as usual.
|
|
638
|
+
const file = resolveAgentFile(input.agent, input.task, task.cwd, getAgentDir());
|
|
639
|
+
const prompt = file?.body ?? input.prompt?.trim();
|
|
637
640
|
const thinking = input.thinking;
|
|
638
|
-
const baseTools = input.tools ?? (input.write ? WRITE_TOOLS : READONLY_TOOLS);
|
|
641
|
+
const baseTools = file?.tools ?? input.tools ?? (input.write ? WRITE_TOOLS : READONLY_TOOLS);
|
|
639
642
|
const tools = [...baseTools, ...(run.allowIntercom ? CHILD_TALK_TOOLS : [])];
|
|
640
643
|
|
|
641
644
|
// Model + thinking resolve against the pi model registry; a bad request
|
|
642
645
|
// fails the TASK with a helpful message, not the whole run.
|
|
643
646
|
let model: Model<Api> | undefined;
|
|
644
647
|
try {
|
|
645
|
-
model = resolveChildModel(ctx, input.model);
|
|
648
|
+
model = resolveChildModel(ctx, file?.model ?? input.model);
|
|
646
649
|
validateThinking(model, thinking);
|
|
647
650
|
} catch (err) {
|
|
648
651
|
this.updateTask(
|