@pify/subagent 0.3.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,6 +9,7 @@ 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
+ - **`@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.)
12
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.
@@ -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
 
@@ -315,6 +317,29 @@ export default function subagent(pi: ExtensionAPI) {
315
317
 
316
318
  // ── Lifecycle ────────────────────────────────────────────────────────
317
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
+
318
343
  pi.on("session_start", async (_event, ctx) => {
319
344
  defs = loadAgentDefs(ctx.cwd, getAgentDir());
320
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.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"
@@ -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
+ }