Files
pi-config/extensions/pi-crew/docs/research-extension-examples.md

9.8 KiB

Research: Extension Examples & Patterns

Ngày: 2026-04-29 | Read-only research | Source: source/pi-mono/packages/coding-agent/examples/extensions/

1. Example Catalog (86 files, 60+ extensions)

1.1 Sorted by relevance to pi-crew

Priority Example Relevance
subagent/ Most similar to pi-crew: child Pi spawning, parallel, chain
custom-compaction.ts Hook compaction — useful for preserving run state
event-bus.ts Cross-extension communication pattern
plan-mode/ State persistence, dynamic tools, widget management
structured-output.ts terminate: true — save LLM turns
handoff.ts Context transfer to new session
dynamic-tools.ts Register tools at runtime
permission-gate.ts Gate dangerous operations
trigger-compact.ts Proactive compaction monitoring
send-user-message.ts sendUserMessage pattern
dirty-repo-guard.ts Guard against uncommitted changes
model-status.ts Model status in footer
confirm-destructive.ts Confirm destructive operations

2. Deep Analysis of Key Examples

2.1 subagent/ — The Reference Implementation

Files:

  • index.ts (~530 dòng): Main tool with execute + render
  • agents.ts (~130 dòng): Agent discovery (user/project scope)

Architecture:

subagent tool
  ├── Single: runSingleAgent() → spawn pi --mode json -p
  ├── Parallel: mapWithConcurrencyLimit(tasks, 4, runSingleAgent)
  └── Chain: sequential loop with {previous} placeholder

Key patterns:

  • Agent discovery: discoverAgents(cwd, scope) — scans .md files with YAML frontmatter
  • Child process: getPiInvocation() detects current runtime (node/bun/pi binary)
  • Streaming: onUpdate callback for partial results during execution
  • Render: renderCall() + renderResult() with collapsed/expanded views
  • Abort: AbortSignal propagated to child process

What pi-crew does better:

  • Durable state (manifest, tasks, events) instead of in-memory only
  • Team/workflow abstraction instead of flat agent list
  • Task graph with DAG dependencies instead of linear chain
  • Async background runner with PID tracking
  • Policy engine for limits/retry/escalation
  • Mailbox for inter-task communication
  • Worktree isolation per task

What pi-crew could adopt from this:

  • terminate: true on final results (not used in example either, but available)
  • renderCall/Result custom rendering patterns
  • mapWithConcurrencyLimit pattern (pi-crew already has similar)

2.2 custom-compaction.ts — Custom Compaction

Pattern:

pi.on("session_before_compact", async (event, ctx) => {
  // 1. Get preparation data
  const { messagesToSummarize, turnPrefixMessages, tokensBefore, firstKeptEntryId } = event.preparation;

  // 2. Use different model for summarization (cheaper)
  const model = ctx.modelRegistry.find("google", "gemini-2.5-flash");

  // 3. Custom prompt
  const summary = await complete(model, { messages: [...] }, { apiKey, signal });

  // 4. Return custom compaction result
  return {
    compaction: { summary, firstKeptEntryId, tokensBefore }
  };
});

Relevance to pi-crew:

  • Can use cheap model to summarize completed tasks
  • Can protect foreground runs from being compacted mid-execution
  • Can store structured artifact index in compaction details

2.3 event-bus.ts — Cross-Extension Communication

Pattern:

// Extension A: emit events
pi.events.emit("my:notification", { message: "hello", from: "ext-a" });

// Extension B: listen
pi.events.on("my:notification", (data) => {
  currentCtx?.ui.notify(`Event from ${data.from}: ${data.message}`);
});

Relevance to pi-crew:

  • Already used for internal events (subagent.stuck-blocked)
  • Could publish structured events for other extensions to consume:
    • pi-crew:run:completed
    • pi-crew:subagent:completed
    • pi-crew:run:failed

2.4 plan-mode/ — State Persistence + Dynamic Tools

Key patterns:

State persistence:

// Save
pi.appendEntry("plan-mode", { enabled, todos, executing });

// Restore on session_start
const entries = ctx.sessionManager.getEntries();
const state = entries
  .filter(e => e.type === "custom" && e.customType === "plan-mode")
  .pop()?.data;

Dynamic tools:

// Switch between tool sets
if (planModeEnabled) {
  pi.setActiveTools(["read", "bash", "grep", "find", "ls"]);
} else {
  pi.setActiveTools(["read", "bash", "edit", "write"]);
}

Tool call gate:

pi.on("tool_call", async (event) => {
  if (planModeEnabled && event.toolName === "bash") {
    if (!isSafeCommand(event.input.command)) {
      return { block: true, reason: "..." };
    }
  }
});

Relevance to pi-crew:

  • pi.appendEntry pattern for cross-session run awareness
  • pi.setActiveTools could be used to restrict tools during team runs
  • tool_call gate for destructive team actions

2.5 structured-output.ts — terminate: true

Pattern:

async execute(_toolCallId, params) {
  return {
    content: [{ type: "text", text: "Done" }],
    details: { headline, summary, actionItems },
    terminate: true,  // ← No follow-up LLM turn needed
  };
}

Relevance to pi-crew:

  • Agent tool results could use terminate: true when background run queued
  • get_subagent_result could terminate when result is final
  • team tool status/list/recommend actions could terminate

2.6 handoff.ts — Context Transfer to New Session

Pattern:

// 1. Extract conversation context
const messages = ctx.sessionManager.getBranch()
  .filter(e => e.type === "message")
  .map(e => e.message);

// 2. Generate focused prompt
const prompt = await complete(model, { systemPrompt, messages }, { apiKey });

// 3. Create new session with pre-filled editor
await ctx.newSession({
  parentSession: currentSessionFile,
  withSession: async (replacementCtx) => {
    replacementCtx.ui.setEditorText(prompt);
  },
});

Relevance to pi-crew:

  • When a task in a team run needs isolated context, could handoff to new session
  • Parent session tracking via parentSession

2.7 permission-gate.ts — Dangerous Operation Gate

Pattern:

pi.on("tool_call", async (event, ctx) => {
  if (event.toolName !== "bash") return;
  if (isDangerousPattern(event.input.command)) {
    const choice = await ctx.ui.select("Allow?", ["Yes", "No"]);
    if (choice !== "Yes") {
      return { block: true, reason: "Blocked by user" };
    }
  }
});

Relevance to pi-crew:

  • Gate destructive team actions (delete, forget, prune)
  • Only allow with explicit confirm: true parameter

2.8 trigger-compact.ts — Proactive Compaction

Pattern:

pi.on("turn_end", (_event, ctx) => {
  const usage = ctx.getContextUsage();
  if (usage?.tokens && usage.tokens > THRESHOLD) {
    ctx.compact({ customInstructions: "..." });
  }
});

Relevance to pi-crew:

  • Monitor context during long team runs
  • Auto-compact before hitting overflow errors
  • Use compact's callback to track state

3. Pattern Summary

3.1 Patterns pi-crew already implements well

Pattern pi-crew implementation
Child Pi spawning SubagentManager + spawn.ts with full process management
Parallel execution mapConcurrent in team runner
State persistence Durable file-based (manifest, tasks, events, artifacts)
Widget rendering CrewWidget, LiveRunSidebar, Powerbar
Lifecycle hooks session_start, session_before_switch, session_shutdown
Config merge loadConfig with user/project priority
Abort propagation AbortController trees in foreground runs

3.2 Patterns pi-crew could adopt

Pattern Current status Recommendation
terminate: true Not used Add to Agent/get_subagent_result
session_before_compact hook Not hooked Cancel compact during foreground runs
Custom compaction model Not used Use Haiku/Gemini Flash for task summaries
pi.events publish ⚠️ Internal only Add public structured events
pi.appendEntry Not used Cross-session run references
tool_call permission gate Not gated Gate destructive team actions
Config-driven tool registration Always all Register tools per config
Working indicator Widget only Use ctx.ui.setWorkingIndicator
Session name auto-set Manual only Auto-name from team run context
ctx.compact() proactive No monitoring Monitor + auto-compact at threshold

4. Example: Complete Tool with terminate + render

This shows a hypothetical optimized pi-crew Agent tool:

// OPTIMIZED Agent tool pattern
const AgentTool = defineTool({
  name: "Agent",
  label: "Agent",
  description: "Launch a real pi-crew subagent...",
  parameters: Type.Object({
    prompt: Type.String(),
    description: Type.String(),
    subagent_type: Type.String(),
    run_in_background: Type.Optional(Type.Boolean()),
  }),
  async execute(_id, params, signal, _onUpdate, ctx) {
    // ... spawn subagent ...
    if (params.run_in_background) {
      return {
        content: [{ type: "text", text: `Agent queued. ID: ${record.id}` }],
        details: { agentId: record.id, status: "queued" },
        terminate: true,  // ← No need for LLM follow-up
      };
    }
    await record.promise;
    const output = readResult(record);
    return {
      content: [{ type: "text", text: output }],
      details: { agentId: record.id, status: record.status },
      terminate: true,  // ← Final result, save LLM turn
    };
  },
  renderResult(result, { expanded }, theme) {
    // Custom rendering with colored status icons
    // Collapsed/expanded views
    // Usage stats display
  },
});