@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 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
- - **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 a system-prompt body. Project overrides global overrides builtin; a def without `tools:` defaults to read-only.
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)
@@ -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
- systemPrompt: promptOptions.customPrompt,
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
- ...(promptOptions.appendSystemPrompt ? [promptOptions.appendSystemPrompt] : []),
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.2.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.84.4",
65
- "@earendil-works/pi-tui": "^0.84.4",
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"
@@ -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"];
@@ -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