npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@blackbelt-technology/pi-dashboard-subagents

v0.2.2

Published

A lightweight subagent spawning and managing system for pi.

Readme

pi-dashboard-subagents

A lightweight foreground subagent extension for pi.

Spawns subagents in-memory (no new sessions appear in ~/.pi/agent/sessions/, no clutter in the dashboard's session list) and emits every event, tool call, and reasoning step as a structured timeline the pi-agent-dashboard can render in its subagent inspector and pop out into a new tab.

Scope

  • Foreground only. Subagents block the caller until completion.
  • No background spawning. No get_subagent_result. No steer_subagent.
  • No new sessions on disk. Sessions live entirely in memory.
  • Full observability. Every tool_execution_end, text_end, thinking_end, and error from the subagent's session is mirrored to the dashboard as a SubagentTimelineEntry.
  • Inherits parent context by default. Opt-out with isolated: true.

Context inheritance

By default, every subagent inherits a compressed copy of the parent's recent conversation. The compression strategy is verbatim-compaction (zero hallucination risk, no extra LLM calls):

Keep last N turn pairs (default N=6) verbatim.
For turns beyond the tool-output window (default last 2 turns):
  - tool_result blocks   → "[…tool output omitted, see earlier message]"
  - tool_use blocks      → "[tool_use: <name>]"
  - thinking blocks      → "[…thinking omitted…]"
  - large text (>2KB)    → first 1.5KB + "[…truncated…]"
Hard cap at 24K characters (~6K tokens) with mid-truncation.

Settings

Two persistent settings live at:

~/.pi/agent/extensions/pi-dashboard-subagents/config.json
{
  "inheritContext": true,
  "exposeInheritanceInTool": false,
  "inheritance": {
    "recentTurns": 6,
    "toolOutputWindow": 2,
    "maxChars": 24000
  }
}

| Setting | Meaning | | -------------------------- | ------------------------------------------------------------------------------------------------ | | inheritContext | When true, every subagent inherits a compressed copy of parent context. | | exposeInheritanceInTool | When true, the Agent tool's JSON schema exposes an isolated parameter the LLM can flip. When false (default), the schema is fixed and the global inheritContext setting always applies. | | inheritance.recentTurns | Verbatim turn pairs kept (default 6). | | inheritance.toolOutputWindow | Recent turns where tool outputs stay verbatim (default 2). | | inheritance.maxChars | Hard cap on the compressed context (default 24000 chars ≈ 6K tokens). |

Four usage modes:

inheritContext=true,  exposeInheritanceInTool=false  (default)
  → every subagent inherits. LLM cannot opt out. Lean tool schema.

inheritContext=false, exposeInheritanceInTool=false
  → every subagent is isolated. LLM cannot opt in. Lean tool schema.

inheritContext=true,  exposeInheritanceInTool=true
  → inherits by default. LLM can set `isolated: true` per call to opt out.

inheritContext=false, exposeInheritanceInTool=true
  → isolated by default. LLM can set `isolated: false` per call to opt in.

Missing file or fields fall back to baked-in defaults. Settings are cached after first read; edit the file and restart pi, or call invalidateSettingsCache() programmatically to pick up changes.

Compression is operator-controlled only

recentTurns, toolOutputWindow, and maxChars are never exposed to the LLM under any setting. They're operator concerns — fine-tune them globally by editing the config file, then restart pi.

Upstream prompt caching — future work

The ideal architecture for context inheritance is what Claude Code calls Fork Agents: the subagent's first provider call hits the parent's cache by sending a byte-identical prefix marked with cache_control: { type: "ephemeral" }. Result: 90% input cost reduction, 80% latency reduction on the inherited context.

This extension does NOT yet implement true cache-fork behavior because pi-coding-agent's public session.prompt(text) API takes a string — there's no path to inject pre-built message arrays with cache markers. For now we accept token-savings-only from compression and leave upstream caching as a follow-up.

Follow-up paths (when pi-coding-agent SDK supports them):

  • Inject parent messages as initial session state via the lower-level createAgentSessionFromServices / AgentSessionRuntime API.
  • Add cache_control markers at the inherited-prefix boundary.
  • Anthropic: 90/80% reduction. OpenAI: automatic (just needs identical prefix). Gemini: explicit cache via createCachedContent.

Why another subagent extension?

The two existing subagent extensions for pi each have a tradeoff:

| Extension | Spawn model | Session list | Rich timeline | Background | | ---------------------------------- | ------------------ | ------------- | ------------- | ---------- | | @tintinweb/pi-subagents | In-memory | Clean ✓ | Summary only | Yes | | pi-subagents (Nico Bailon) | Separate process | Cluttered ✗ | Full ✓ | Yes | | pi-dashboard-subagents (this)| In-memory ✓ | Clean ✓ | Full ✓ | No (by design) |

This extension drops background/async complexity entirely and focuses on one thing: foreground subagent runs with first-class observability for the pi-agent-dashboard inspector.

Install

npm install -g @blackbelt-technology/pi-dashboard-subagents

Then add to ~/.pi/agent/settings.json:

{
  "packages": [
    "npm:@blackbelt-technology/pi-dashboard-subagents"
  ]
}

Or for local development:

{
  "packages": [
    "/absolute/path/to/pi-dashboard-subagents"
  ]
}

Usage

In any pi session, invoke the tool:

Agent(
  subagent_type: "Explore",
  description: "Find auth flows",
  prompt: "Look through src/auth and summarize OAuth providers"
)

The subagent runs in-memory under the parent pi process. Live progress is streamed back to the caller via the standard AgentDetails payload plus the entries[] timeline field. The subagent never appears as a separate session.

Agent .md files

v0.2.0 added YAML frontmatter parsing for agent .md definition files plus a 3-tier resolver and a bundled default Explore agent.

Frontmatter schema

Every field is optional. Missing fields fall through to current pre-frontmatter behaviour, so an .md with no frontmatter still works.

---
description: Fast read-only codebase & docs exploration
model: anthropic/claude-haiku-4-5      # OR "@role" — see below
thinking: high                          # (alt: "model: id:high" suffix)
tools: [read, grep, find, ls, bash]    # allowlist (built-in + extension tools)
inherit_context: false                  # per-agent override of the global setting
prompt: |                              # OPTIONAL — body fallback below
  You are an Explore subagent. Be fast and read-only.
---

The markdown body becomes the agent prompt when no `prompt:` field is set.
This matches the convention used by Claude Code and pi-coding-agent's own
prompt-template / skill files.

| Field | Effect | | ----------------- | --------------------------------------------------------------------------------------------------- | | description | Overrides displayName on the dashboard card. | | model | "@role", "provider/model-id", "provider/model-id:thinking", or bare "model-id". See Model resolution. | | tools | Allowlist intersected with the parent's active tool set (minus Agent). Unknown names dropped silently. | | inherit_context | true → inherit parent context. false → isolated. Per-agent; overrides the global inheritContext.| | prompt | Prepended as <agent-prompt>...</agent-prompt> before the task. Body of the .md is used if the field is absent. |

All fields are read once at spawn time. Editing the .md while a subagent is running has no effect on that subagent; the next spawn picks up changes.

Three-tier resolution

When the LLM calls Agent({ subagent_type: "Explore", ... }), the extension looks up Explore.md in three tiers, most-specific first:

1. <cwd>/.pi/agents/Explore.md          → source: "project"   (per-project override)
2. ~/.pi/agent/agents/Explore.md        → source: "user"      (per-user override)
3. <EXTENSION_ROOT>/agents/Explore.md   → source: "bundled"   (ships with this package)

The first match wins. The tier is surfaced as AgentDetails.agentMdSource so the dashboard card can render "Explore (bundled)" / "Explore (user)" badges.

Bundled Explore agent

The package ships agents/Explore.md — a fast, read-only codebase / docs explorer informed by Claude Code's Explore agent and the production guidance in Ranjan Kumar's Subagents: How to Run Parallelism Inside a Single Agent Session (April 2026):

  • Model: "@fast" — role alias resolved at spawn time by the dashboard's roles plugin. Operators pick the underlying model behind @fast via Settings → Roles. This makes model choice operator-controlled at runtime rather than baked into the shipped file.
  • Tools: [read, grep, find, ls, bash] — no write/edit/Agent.
  • Inherit context: false — fresh window, parent's context not imported.
  • Output contract: structured ## Answer / ## Evidence / ## Notes with hard limits (≤2000 tokens, no raw file dumps).

The bundled Explore requires a model:resolve handler to be loaded (so @fast can be looked up in ~/.pi/agent/providers.json). The handler ships with pi-agent-dashboard and (optionally) pi-flows. Without one of them, @role references HARD-FAIL the spawn; literal model ids still resolve via the in-process registry fallback. See Model resolution below.

To customise (e.g. to run without the dashboard, or to pin a specific model):

mkdir -p ~/.pi/agent/agents
cp "$(node -e 'console.log(require.resolve("@blackbelt-technology/pi-dashboard-subagents/agents/Explore.md"))')" \
   ~/.pi/agent/agents/Explore.md
# Edit ~/.pi/agent/agents/Explore.md — e.g. change `model:` to a literal
# "provider/model-id" so it works without the roles-plugin bridge.

The user-global override automatically wins over the bundled file (tier 2 > 3).

Model resolution (model:)

The model: field accepts four input forms, in priority order:

| Form | Example | How it resolves | | --------------------------------- | ---------------------------------- | ------------------------------------------------------------------ | | @role (role alias) | @fast | Handler reads ~/.pi/agent/providers.json#roles — needs handler. | | provider/model-id | anthropic/claude-opus-4 | pi.modelRegistry.find(provider, id). | | provider/model-id:thinking | anthropic/claude-haiku-4-5:high | Same as above; :thinking parsed off and surfaced separately. | | Bare model-id ("like" query) | claude-haiku-4-5 | pi.modelRegistry.getAll().find(m => m.id === ref) — first wins. |

The extension resolves the field in two phases:

1. Primary — model:resolve event. The extension emits a probe on pi.events:

const probe = { ref: "@fast" };       // or "anthropic/opus", or "opus-4-5"
pi.events.emit("model:resolve", probe);
if (probe.model)   { /* success */ }
if (probe.error)   { /* handler reported a miss */ }
// else: silent emit (no handler) — fall through to the fallback below

A handler is provided by pi-agent-dashboard (always) and (optionally) pi-flows. The handler is responsible for all four input forms above.

2. Fallback — in-process registry. When the emit returns with both probe.model and probe.error unset (no handler reacted), the extension resolves literal forms locally via pi.modelRegistry:

  • provider/model-id[:thinking]registry.find(provider, id)
  • Bare model-id[:thinking]registry.getAll().find(m => m.id === ref)
  • @roleNOT supported by the fallback (no providers.json access); fails with a clear "install pi-agent-dashboard or pi-flows" message.

This means: subagents using literal or bare-id models always work, with or without the dashboard. Only @role requires a handler.

Failure surface

When resolution fails (handler error, fallback miss, no handler for @role), the tool call returns isError: true with a structured message that:

  • names the unresolved ref,
  • includes the agent .md path that specified it,
  • distinguishes "role unknown" vs "model unknown" vs "no resolver available",
  • suggests the right fix (install plugin, use literal form, add to providers.json, etc.),
  • on bare-id misses includes a hint of registered model ids (capped at 20).

Implementing a model:resolve handler

Any pi extension can register a handler. Use the cooperative early-return idiom so multiple handlers (e.g. pi-flows + pi-agent-dashboard) coexist without fighting:

pi.events.on("model:resolve", (probe) => {
  if (probe.model) return;                  // someone else already handled it

  // 1. @role indirection (if you own roles)
  // 2. provider/model split + registry.find()
  // 3. bare-id “like” query against registry.getAll()

  if (resolvedSuccessfully) {
    probe.resolved      = "provider/id";    // canonical literal
    probe.model         = m;                // Model object
    probe.thinkingLevel = thk;              // parsed from ":high" suffix, optional
    probe.auth          = a;                // optional, registry-defined shape
  } else {
    probe.error    ??= reason;              // first error sticks
    probe.available ??= { roles, models };  // optional diagnostics
  }
});

Probe shape (TypeScript):

interface ModelResolveProbe {
  ref: string;                                          // input
  resolved?: string;                                    // "provider/model-id"
  model?: Model<any>;
  thinkingLevel?: "minimal" | "low" | "medium" | "high" | "xhigh" | "off";
  auth?: { ok?: boolean; error?: string; [k: string]: unknown };
  error?: string;
  available?: {
    roles?: Record<string, string>;
    models?: string[];
  };
}

Standalone behaviour matrix

| | @role | provider/id | bare id | | ------------------------------- | ------------ | ------------- | ---------- | | With pi-agent-dashboard | event ✅ | event ✅ | event ✅ | | With pi-flows (optional handler)| event ✅ | event ✅ | event ✅ | | Neither — standalone pi | ❌ (install) | fallback ✅ | fallback ✅ |

When neither handler is loaded, only @role fails. Literal provider/model and bare model-id continue to work via the in-process registry fallback.

Per-call model override (model tool-call param)

The Agent tool's parameter schema accepts an optional model field that short-circuits any frontmatter model: value:

Agent({
  subagent_type: "research-spike",   // any label — no `.md` required
  description:   "audit auth flow",
  prompt:        "Review extensions/agent.ts for auth issues.",
  model:         "@fast",            // OR "anthropic/claude-haiku-4-5"
                                     // OR bare "claude-haiku-4-5"
})

The model arg accepts the same three forms as the frontmatter field (@role, provider/model[:thinking], bare model-id) and resolves via the same model:resolve event-bus + in-process fallback pipeline. No duplication, no second resolver — it's the identical machinery.

Precedence (highest wins):

  args.model      (tool-call argument)
   > agentConfig.model    (`.md` frontmatter)
    > pi default (settings.json)

When args.model is non-empty it WINS and the .md's model: is ignored. When args.model is omitted (or empty/whitespace) the .md value applies. When both are absent, the parent's default model is inherited.

Failure modes are identical to the frontmatter path. Error messages cite the source of the unresolvable ref — either the .md file path or the literal label (tool-call argument) — so operators can trace bad refs.

Wire-protocol contract

This section locks the producer-side contract consumed by the dashboard inspector.

Emission channels

Every run emits on four pi.events.emit(channel, data) channels. The dashboard bridge's emit intercept renames them to its protocol event types:

| Producer channel | Dashboard protocol event | When | | ----------------------- | ------------------------ | --------------------------------------------- | | subagents:created | subagent_created | Tool invocation begins (before any session work) | | subagents:started | subagent_started | Initial "running" emission, AND all progress ticks (re-uses channel; dashboard reducer merges) | | subagents:completed | subagent_completed | await session.prompt(...) resolves successfully | | subagents:failed | subagent_failed | Any throw, abort, or session error path |

Progress emissions are throttled to ≤4 per second per subagent (PROGRESS_THROTTLE_MS = 250). The final progress snapshot is always flushed before completed/failed.

Emissions are no-ops when pi.events is undefined — the run continues; the parent just doesn't get the rich UI.

Payload shape

All four channels send { id: string, ..., details: AgentDetails }. The id equals details.agentId.

Per-channel extras:

subagents:created   → { id, type, description, details }
subagents:started   → { id, type?, description?, details }    // type/description on first emission only
subagents:completed → { id, result, durationMs, tokens, toolUses, details }
subagents:failed    → { id, error, durationMs, toolUses?, details }

AgentDetails field reference

The details payload (defined in extensions/events.ts) carries everything the inspector renders.

| Field | Type | Purpose | | -------------- | ------------------------------- | ------------------------------------------------------------------------ | | agentId | string | Stable id; drives the popout URL /session/<sid>/subagent/<agentId> | | displayName | string | Human-readable name (defaults to subagent_type) | | description | string | The 5–10-word task description passed in by the LLM | | subagentType | string | The .md agent type identifier (e.g. "Explore") | | status | AgentStatus | One of queued \| running \| completed \| aborted \| stopped \| error | | activity? | string | Live current-activity line ("running bash", "thinking", …) | | entries? | SubagentTimelineEntry[] | Full timeline (cumulative; dashboard REPLACES on each emission) | | toolUses | number | Cumulative count of completed tool calls | | tokens | string | Display-formatted total ("12.3k") | | tokensUsage? | { input, output, total } | Raw integer counts (populated on completed/failed) | | turnCount? | number | Assistant turns so far | | maxTurns? | number | Reserved — not enforced in v0.1.x | | durationMs | number | Elapsed milliseconds since subagents:created | | modelName? | string | Resolved model id (e.g. "claude-sonnet-4-6") | | tags? | string[] | Notable config flags (e.g. ["thinking: high"]) | | agentMdPath? | string | Absolute path to the .md definition (project > user > bundled) | | agentMdSource? | "project" \| "user" \| "bundled" | Tier that supplied agentMdPath. v0.2.0+. Undefined when path is undefined or producer is older. | | error? | string | Set on failed emissions |

SubagentTimelineEntry kinds

| { kind: "tool";     toolName, input, output?, isError?, ts }
| { kind: "text";     text, ts }              // assistant text (text_end)
| { kind: "thinking"; text, ts }              // assistant thinking (thinking_end)
| { kind: "error";    text, ts }              // assistant error

Only _end-flavored session events become entries — each entry is final / idempotent. Live activity is conveyed via details.activity instead.

Persistence model

  • Subagent's conversation — in-memory only, dies with the parent turn (uses SessionManager.inMemory(cwd)).
  • Subagent's final result + timeline — embedded in the AgentToolResult<AgentDetails> returned to the parent. Pi persists it inside the parent's ToolResultMessage.details in the parent session's JSONL. Survives /resume.
  • Live progress — streamed only; not buffered server-side. A dashboard refresh while a subagent is still running loses the live card until the parent's tool result lands in JSONL (then state-replay re-hydrates from details). See the dashboard's add-subagent-inspector change for the consumer-side replay seam.

License

MIT