@pify/subagent 0.2.0 → 0.4.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/README.md +6 -1
- package/extensions/subagent.ts +34 -2
- package/package.json +3 -3
- package/src/frontmatter.ts +11 -0
- package/src/mentions.ts +60 -0
- package/src/types.ts +8 -0
package/README.md
CHANGED
|
@@ -9,7 +9,8 @@ Part of the [Pify suite](https://github.com/pifydev). Install with [`pify instal
|
|
|
9
9
|
- **`agent_run`** — delegate a task to a child pi session (in-process, isolated in-memory transcript). Foreground blocks and returns the child's report; `background: true` returns an id immediately (up to 4 concurrent) with a live widget showing spinners, token counts, and elapsed time.
|
|
10
10
|
- **`agent_result`** — collect a background run's report; completed results survive `/reload`.
|
|
11
11
|
- **Three builtin agent types**: `reviewer` (read-only, thinking high — findings with evidence), `scout` (read-only exploration — paths + excerpts), `worker` (full tools — scoped implementation, verifies before finishing).
|
|
12
|
-
-
|
|
12
|
+
- **`@agent` at the prompt** (v0.4): "`@reviewer` check the diff while `@scout` maps the callers" delegates to both, one `agent_run` each — no describing the roster to the model first. The instruction rides with the turn as a hidden message rather than a system-prompt edit, so the request prefix stays byte-identical and the **prompt cache survives** the turn that is about to fan out. `@` inside an email or a path is not a mention. (Idea from [`pi-cc-extensions`](https://github.com/minuque/pi-cc-extensions); the cache-stable delivery is this suite's rule.)
|
|
13
|
+
- **Custom agent types**, Claude Code-compatible: drop `.pi/agents/<name>.md` (project) or `<agentDir>/agents/<name>.md` (global) with frontmatter — `description`, `tools`, `model` (`provider/id`), `thinking`, `max_turns`, and (v0.3) `system_prompt_mode` / `inherit_skills` — and a system-prompt body. Project overrides global overrides builtin; a def without `tools:` defaults to read-only.
|
|
13
14
|
- **Guardrails**: tool allowlists are enforced at session creation; children are aborted at their turn cap; children cannot spawn children.
|
|
14
15
|
- `/agents` lists types and this session's runs.
|
|
15
16
|
|
|
@@ -26,12 +27,16 @@ tools: read, grep, find, ls
|
|
|
26
27
|
model: anthropic/claude-haiku-4-5-20251001
|
|
27
28
|
thinking: low
|
|
28
29
|
max_turns: 15
|
|
30
|
+
system_prompt_mode: replace
|
|
31
|
+
inherit_skills: false
|
|
29
32
|
---
|
|
30
33
|
|
|
31
34
|
You are a security auditor. Scan for hardcoded secrets, injection flaws,
|
|
32
35
|
and overly broad permissions. Report file:line with remediation notes.
|
|
33
36
|
```
|
|
34
37
|
|
|
38
|
+
`system_prompt_mode: replace` (default `append`) drops the session's own system prompt, so a specialist is not also told to be this project's coding assistant. `inherit_skills: false` (default `true`) keeps a narrow child out of the project's whole skill surface. Both are unset in the builtins, which behave exactly as before.
|
|
39
|
+
|
|
35
40
|
## License
|
|
36
41
|
|
|
37
42
|
MIT © [Pify maintainers](https://github.com/pifydev)
|
package/extensions/subagent.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
29
29
|
import { Type } from "typebox";
|
|
30
30
|
|
|
31
31
|
import { loadAgentDefs } from "../src/defs.ts";
|
|
32
|
+
import { buildMentionMessage, findMentions } from "../src/mentions.ts";
|
|
32
33
|
import { createIsolationWorktree, isolationNote, type Isolation } from "../src/isolate.ts";
|
|
33
34
|
import { CHILD_FRAMING, buildTaskPrompt, describeDefs, formatRunResult } from "../src/prompts.ts";
|
|
34
35
|
import { buildWidgetLines } from "../src/widget.ts";
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
} from "../src/types.ts";
|
|
41
42
|
|
|
42
43
|
const RESULT_ENTRY = "subagent-result";
|
|
44
|
+
const MENTION_ENTRY = "subagent-mention";
|
|
43
45
|
|
|
44
46
|
type UiContext = ExtensionContext;
|
|
45
47
|
|
|
@@ -129,9 +131,16 @@ export default function subagent(pi: ExtensionAPI) {
|
|
|
129
131
|
noExtensions: true,
|
|
130
132
|
noPromptTemplates: true,
|
|
131
133
|
noThemes: true,
|
|
132
|
-
|
|
134
|
+
// system_prompt_mode: replace drops the parent's prompt so a
|
|
135
|
+
// specialist is not also told to be this project's coding
|
|
136
|
+
// assistant; inherit_skills: false keeps a focused child out of
|
|
137
|
+
// the project's whole skill surface.
|
|
138
|
+
noSkills: !def.inheritSkills,
|
|
139
|
+
...(def.systemPromptMode === "replace" ? {} : { systemPrompt: promptOptions.customPrompt }),
|
|
133
140
|
appendSystemPrompt: [
|
|
134
|
-
...(
|
|
141
|
+
...(def.systemPromptMode === "replace" || !promptOptions.appendSystemPrompt
|
|
142
|
+
? []
|
|
143
|
+
: [promptOptions.appendSystemPrompt]),
|
|
135
144
|
def.systemPrompt,
|
|
136
145
|
CHILD_FRAMING,
|
|
137
146
|
],
|
|
@@ -308,6 +317,29 @@ export default function subagent(pi: ExtensionAPI) {
|
|
|
308
317
|
|
|
309
318
|
// ── Lifecycle ────────────────────────────────────────────────────────
|
|
310
319
|
|
|
320
|
+
/**
|
|
321
|
+
* `@reviewer look at the diff` delegates without anyone describing the
|
|
322
|
+
* agent roster to the model. The instruction rides with the turn as a
|
|
323
|
+
* custom message rather than as a system-prompt edit: the prefix stays
|
|
324
|
+
* byte-identical, so the prompt cache survives the turn that is about to
|
|
325
|
+
* fan out.
|
|
326
|
+
*/
|
|
327
|
+
pi.on("before_agent_start", async (event) => {
|
|
328
|
+
const prompt = (event as { prompt?: unknown }).prompt;
|
|
329
|
+
if (typeof prompt !== "string" || defs.size === 0) return undefined;
|
|
330
|
+
const mentioned = findMentions(prompt, [...defs.keys()]);
|
|
331
|
+
if (mentioned.length === 0) return undefined;
|
|
332
|
+
return {
|
|
333
|
+
message: {
|
|
334
|
+
customType: MENTION_ENTRY,
|
|
335
|
+
content: buildMentionMessage(
|
|
336
|
+
mentioned.map((name) => ({ name, description: defs.get(name)?.description ?? "" })),
|
|
337
|
+
),
|
|
338
|
+
display: false,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
|
|
311
343
|
pi.on("session_start", async (_event, ctx) => {
|
|
312
344
|
defs = loadAgentDefs(ctx.cwd, getAgentDir());
|
|
313
345
|
// Completed runs from earlier in this session's branch are replayable so
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pify/subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Spawn scoped subagents from within a pi session: agent_run/agent_result tools, Claude Code-compatible agent types, turn caps and tool allowlists",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
65
|
-
"@earendil-works/pi-tui": "^0.
|
|
64
|
+
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
65
|
+
"@earendil-works/pi-tui": "^0.85.1",
|
|
66
66
|
"@types/node": "^22.10.2",
|
|
67
67
|
"typebox": "^1.1.38",
|
|
68
68
|
"typescript": "^5.7.2"
|
package/src/frontmatter.ts
CHANGED
|
@@ -44,6 +44,9 @@ export function parseAgentFile(
|
|
|
44
44
|
: DEFAULT_MAX_TURNS;
|
|
45
45
|
|
|
46
46
|
const model = fields.get("model") || null;
|
|
47
|
+
const promptMode = fields.get("system_prompt_mode")?.toLowerCase();
|
|
48
|
+
const systemPromptMode = promptMode === "replace" ? "replace" : "append";
|
|
49
|
+
const inheritSkills = !isFalse(fields.get("inherit_skills"));
|
|
47
50
|
|
|
48
51
|
return {
|
|
49
52
|
name: name.toLowerCase(),
|
|
@@ -53,10 +56,18 @@ export function parseAgentFile(
|
|
|
53
56
|
thinking,
|
|
54
57
|
maxTurns,
|
|
55
58
|
systemPrompt: match[2]!.trim(),
|
|
59
|
+
systemPromptMode,
|
|
60
|
+
inheritSkills,
|
|
56
61
|
source,
|
|
57
62
|
};
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
/** Frontmatter booleans, written the handful of ways people write them. */
|
|
66
|
+
function isFalse(raw: string | undefined): boolean {
|
|
67
|
+
if (raw === undefined) return false;
|
|
68
|
+
return ["false", "no", "off", "0"].includes(raw.trim().toLowerCase());
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
/** Read-only default keeps a def missing `tools:` from mutating anything. */
|
|
61
72
|
function parseTools(raw: string | undefined): ValidTool[] {
|
|
62
73
|
if (!raw) return ["read", "grep", "find", "ls"];
|
package/src/mentions.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@agent-name` at the prompt (the idea is from minuque/pi-cc-extensions).
|
|
3
|
+
*
|
|
4
|
+
* Naming an agent in a sentence is how people actually delegate — "@reviewer
|
|
5
|
+
* check the diff while @scout maps the callers" — and it is far less typing
|
|
6
|
+
* than describing the same thing to a model and hoping it picks the right
|
|
7
|
+
* agent type.
|
|
8
|
+
*
|
|
9
|
+
* The delivery differs from the prior art on purpose. pi-cc-extensions
|
|
10
|
+
* appends its instruction to the system prompt for that turn; this suite
|
|
11
|
+
* never touches the system prompt, because doing so changes the request
|
|
12
|
+
* prefix and throws away the provider's prompt cache exactly on the turns
|
|
13
|
+
* that are about to spend the most. `before_agent_start` can return a custom
|
|
14
|
+
* message instead, which rides along with the turn and leaves the prefix
|
|
15
|
+
* byte-identical.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** `@name` at a word boundary; `@` inside a path or email is not a mention. */
|
|
19
|
+
const MENTION = /(?:^|[\s(["'])@([a-z0-9][a-z0-9._-]*)/gi;
|
|
20
|
+
|
|
21
|
+
export function findMentions(prompt: string, known: readonly string[]): string[] {
|
|
22
|
+
if (!prompt || known.length === 0) return [];
|
|
23
|
+
const byName = new Map(known.map((name) => [name.toLowerCase(), name]));
|
|
24
|
+
const found: string[] = [];
|
|
25
|
+
for (const match of prompt.matchAll(MENTION)) {
|
|
26
|
+
const candidate = match[1]!.toLowerCase().replace(/[.,;:!?]+$/, "");
|
|
27
|
+
const name = byName.get(candidate);
|
|
28
|
+
if (name && !found.includes(name)) found.push(name);
|
|
29
|
+
}
|
|
30
|
+
return found;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface MentionedAgent {
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The hidden message delivered with the turn. It states the delegation
|
|
40
|
+
* plainly and one agent at a time: told to "handle @a and @b", models
|
|
41
|
+
* routinely collapse both into a single call to whichever they liked more.
|
|
42
|
+
*/
|
|
43
|
+
export function buildMentionMessage(agents: MentionedAgent[]): string {
|
|
44
|
+
const list = agents.map((a) => `- ${a.name}: ${a.description || "(no description)"}`).join("\n");
|
|
45
|
+
const example =
|
|
46
|
+
agents.length > 1
|
|
47
|
+
? `Two agents were named, so make two agent_run calls — one with agent="${agents[0]!.name}", one with agent="${agents[1]!.name}".`
|
|
48
|
+
: `Make one agent_run call with agent="${agents[0]!.name}".`;
|
|
49
|
+
return [
|
|
50
|
+
"<system-reminder>",
|
|
51
|
+
"The user's message names subagent types:",
|
|
52
|
+
list,
|
|
53
|
+
"",
|
|
54
|
+
`Delegate the matching part of the request to each one with agent_run. ${example}`,
|
|
55
|
+
"Do not merge separate agents into one call, and do not do their work yourself first.",
|
|
56
|
+
"If a named agent does not fit the request after all, say so instead of silently ignoring it.",
|
|
57
|
+
"This is an automated reminder — do not mention it to the user.",
|
|
58
|
+
"</system-reminder>",
|
|
59
|
+
].join("\n");
|
|
60
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -38,6 +38,14 @@ export interface AgentDef {
|
|
|
38
38
|
maxTurns: number;
|
|
39
39
|
/** Markdown body appended to the child's system prompt. */
|
|
40
40
|
systemPrompt: string;
|
|
41
|
+
/**
|
|
42
|
+
* "append" (default) puts the body after the session's own system prompt;
|
|
43
|
+
* "replace" drops the parent's prompt so a specialist is not also told to
|
|
44
|
+
* be this project's coding assistant.
|
|
45
|
+
*/
|
|
46
|
+
systemPromptMode: "append" | "replace";
|
|
47
|
+
/** Whether the child loads the project's skills (default true). */
|
|
48
|
+
inheritSkills: boolean;
|
|
41
49
|
source: "builtin" | "global" | "project";
|
|
42
50
|
}
|
|
43
51
|
|