---
status: resolved
claimed-by: "019fbffd-327f-77cb-be80-827c31407567"
claimed-at: "2026-08-01T21:01:15-04:00"
resolved-at: "2026-08-01T21:13:30-04:00"
parent: "[[083-pi-subagent-result-delivery-map|Pi subagent result delivery]]"
blocked-by: []
tags:
  - ticket/research
---

# Pi subagent result delivery runtime API research

## Question

What can Pi extensions and custom tools currently observe, persist, expose, and return for asynchronous subagent lifecycle and result delivery, and what constraints does that place on non-blocking retrieval, notifications, or event-style coordination?

## Answer

Pi already exposes enough extension, custom tool, SDK, RPC, session, and UI primitives to implement non-blocking subagent result retrieval inside the current subagent extension.
The strongest path is extension-owned supervision with custom tools for spawn, status, list, wait, result, and cancel, plus extension UI notifications or widgets for awareness, plus session custom entries for durable milestone state.
Pi does not expose a built-in cross-session background job registry or a parent-session event stream for external child sessions, so the subagent extension must own child process supervision, correlation ids, retention, and any durable recovery semantics.
Ordinary message injection into the parent conversation is possible through `pi.sendMessage()` and `pi.sendUserMessage()`, but the documented delivery modes constrain it to steer, follow-up, or next-turn delivery and make passive retrieval a safer default for avoiding HITL workflow disruption.

## Findings

### Extension and tool observation surface

Pi extensions can subscribe to lifecycle, session, agent, message, turn, tool, input, model, provider, and user bash events through `pi.on(...)`.
The extension docs list `agent_settled` as the event that fires when Pi will not continue automatically through retry, compaction retry, or queued continuation, which makes it the right parent-side event for clearing or reconciling status after a parent run fully settles.
Custom tools receive `ExtensionContext`, so they can read session state, current mode, UI availability, cwd, model, thinking level, abort signal, idle state, queued-message state, and context usage, but they do not receive command-only session replacement helpers such as `waitForIdle()`, `newSession()`, `fork()`, or `switchSession()`.
Command handlers receive `ExtensionCommandContext`, which adds `waitForIdle()` and session replacement helpers, but the docs state those helpers are command-only because they can deadlock if called from event handlers.
Custom tool execution returns an `AgentToolResult` whose `content` is sent to the LLM, whose `details` are persisted on the tool result and available for rendering or state reconstruction, and whose optional `usage` contributes to session token and cost totals.
Tool progress can be streamed through the `onUpdate` callback, and Pi emits `tool_execution_start`, `tool_execution_update`, `tool_result`, and `tool_execution_end` around tool execution.
These primitives mean subagent tools can return immediately from spawn with accepted ids, can return a still-running shape from result retrieval, and can expose current status without blocking the parent turn.

### Extension persistence and exposure surface

Pi documents `pi.appendEntry(customType, data?)` as durable extension state that does not participate in LLM context and can be reconstructed on `session_start` by reading `ctx.sessionManager.getEntries()`.
The session format documents `CustomEntry` as an extension state entry with `customType` and `data` that does not participate in LLM context, while interactive mode can render it with an entry renderer.
Pi also documents `pi.sendMessage()` as custom message injection that participates in LLM context, and recommends `pi.appendEntry()` with `pi.registerEntryRenderer()` for durable TUI-only content that should not be sent to the LLM.
The current subagent extension already uses `pi.appendEntry("subagent_milestone", { event, status })` on lifecycle milestones, which is the right built-in persistence primitive for terminal awareness and future recovery.
However, the current supervisor keeps live child records in process memory only, and `session_shutdown` cancels all non-terminal children before discarding the supervisor.
The current `list()` returns active children and recent terminal children subject to a TTL, and `result(id)` requires the id to still be present in memory.
Therefore, durable post-restart result delivery would require intentionally reconstructing status and terminal result records from session custom entries or from a separate extension-owned store.
Pi provides the primitives for this, but the current extension implementation does not yet persist the final result body through `appendEntry()`.

### UI and notification surface

In TUI mode, extensions can call `ctx.ui.notify()`, `setStatus()`, `setWidget()`, `setTitle()`, custom components, custom footer, custom header, custom working indicators, and custom renderers.
The TUI docs show `setWidget()` as a persistent above-or-below-editor surface and `notify()` as a non-blocking user notification.
The RPC docs state that extension UI fire-and-forget methods such as `notify`, `setStatus`, `setWidget`, `setTitle`, and `set_editor_text` are emitted as `extension_ui_request` events and do not expect a response.
The RPC docs also state that some TUI-specific methods degrade in RPC mode, with `custom()` returning `undefined`, several working and footer methods becoming no-ops, and `setWidget()` supporting only string arrays.
Because `ctx.hasUI` is true in TUI and RPC modes, while `ctx.mode === "tui"` is required for terminal-only UI, a subagent result delivery design should use `notify`, `setStatus`, and string-array widgets for mode-portable awareness, and reserve custom TUI components for a TUI-only enhancement.
The current subagent extension already sets a widget with a compact or expanded status summary when status changes.

### Async child lifecycle evidence

Pi SDK `AgentSession` exposes `prompt()`, `steer()`, `followUp()`, `subscribe()`, `abort()`, `waitForIdle()`, and event streaming.
The SDK docs state that `prompt()` resolves after the full accepted run finishes, including retries, while `steer()` and `followUp()` enqueue messages during streaming.
The RPC docs state that the RPC `prompt` command response is emitted after acceptance, queuing, or immediate handling, and that events continue streaming asynchronously after acceptance.
The RPC docs expose `agent_settled`, `message_update`, `tool_execution_*`, `queue_update`, and `get_last_assistant_text` over JSONL, which is enough for the current subprocess runner to observe child progress and final text.
The current subagent runner starts `pi --mode rpc --no-extensions --extension <subagents index> --name "subagent <id>"`, sends a `prompt`, observes JSONL events, treats `agent_settled` as completion, calls `get_last_assistant_text`, and then terminates the child process.
This aligns with the documented RPC protocol, where `agent_settled` means no automatic retry, compaction retry, or queued continuation remains, and `get_last_assistant_text` returns the text content of the last assistant message.

### Coordination and event-style options

Pi exposes a shared extension event bus as `pi.events` for communication between extensions.
The SDK docs also say a shared `eventBus` can be passed to `DefaultResourceLoader` to emit or listen from outside the session factory.
This can support in-process extension-to-extension notifications, but it is not documented as a persisted queue, a cross-process bus, or a child-session supervision registry.
For parent-facing async coordination, the reliable primitives are therefore extension-owned state plus tool polling or waiting, visible UI notifications, and optional in-process events for cooperating extensions in the same runtime.
A controlled push into the parent conversation is possible with `pi.sendMessage()` or `pi.sendUserMessage()`.
`pi.sendMessage()` can use `deliverAs: "steer"`, `"followUp"`, or `"nextTurn"`, with `triggerTurn: true` only applying to steer and follow-up while idle.
`pi.sendUserMessage()` always triggers a turn and requires `deliverAs` while streaming.
These semantics are useful for an explicit opt-in completion injection, but they are too disruptive as a default delivery path when a parent session is in a HITL grill or another active workflow.

## Current constraints for design

- Non-blocking spawn and retrieval are feasible today because custom tools can return immediately and later expose status or result from extension-owned supervision state.
- Notifications are feasible today through `ctx.ui.notify()` and status widgets, and these work in TUI and RPC in degraded but documented forms.
- Durable terminal awareness is feasible through `pi.appendEntry()` custom entries, but durable final result retrieval is not present unless the extension persists terminal result bodies or an external store.
- Event-style coordination is feasible only within the current process through `pi.events`, or through RPC event streams owned by a client, not through a built-in persistent background job bus.
- Parent conversation injection is feasible but should be explicit and controlled because `sendMessage()` and `sendUserMessage()` participate in LLM context or trigger turns.
- Command-only session replacement and `waitForIdle()` are not available to custom tools, so subagent tools should not depend on command-only APIs for result delivery.
- TUI custom components are not portable to RPC mode, so the minimum design should not require them.

## Limitations and evidence gaps

No primary Pi documentation found a built-in async job registry, persistent notification center, or background task inbox for extensions.
No primary Pi documentation found an extension API that observes lifecycle events from a different Pi process or a child RPC session unless the extension or SDK client starts and subscribes to that child itself.
The current subagent extension source persists lifecycle milestones but not terminal result bodies, so post-restart retrieval of completed results is an implementation gap rather than a Pi platform impossibility.
The source inspected was the installed Pi package at version `0.82.1` and the deployed dotfiles subagent extension, so upstream behavior can change after that package version.

## Citations

- Pi extension capabilities, event lifecycle, custom tools, `ExtensionContext`, command-only `ExtensionCommandContext`, `pi.sendMessage()`, `pi.sendUserMessage()`, `pi.appendEntry()`, UI methods, and `pi.events`: `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/docs/extensions.md`.
- Pi SDK `AgentSession`, runtime replacement, event subscription, prompt queueing, custom tools, extensions, session management, and shared event bus: `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/docs/sdk.md`.
- Pi RPC prompt acceptance, asynchronous event streaming, `agent_settled`, `get_last_assistant_text`, `get_entries`, and extension UI protocol degradation: `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/docs/rpc.md`.
- Pi session custom entries, custom messages, message entries, and `SessionManager` append and read APIs: `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/docs/session-format.md`.
- Pi TUI extension component and widget capabilities: `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/docs/tui.md`.
- Pi exported type definitions for `ExtensionUIContext`, `ExtensionContext`, `ExtensionCommandContext`, `ToolDefinition`, and `ExtensionAPI`: `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/dist/core/extensions/types.d.ts`.
- Pi runtime binding for `sendMessage`, `sendUserMessage`, and `appendEntry`, including the custom entry append and `entry_appended` emission: `/nix/store/p2xh13r9f890af4b8jdp60rnld1kbwsl-pi-coding-agent-0.82.1/lib/node_modules/pi-monorepo/dist/core/agent-session.js`.
- Current subagent extension tool surface and milestone persistence: `/home/alexion/wrk/dotfiles/modules/agents/pi/extensions/subagents/index.ts`.
- Current subagent supervisor lifecycle, in-memory child records, result access, wait behavior, TTL retention, milestone callbacks, and shutdown cancellation: `/home/alexion/wrk/dotfiles/modules/agents/pi/extensions/subagents/supervisor.ts`.
- Current subagent RPC child runner behavior using `agent_settled` and `get_last_assistant_text`: `/home/alexion/wrk/dotfiles/modules/agents/pi/extensions/subagents/runner.ts`.
- Current subagent data shapes for status, result, wait, child records, and runner callbacks: `/home/alexion/wrk/dotfiles/modules/agents/pi/extensions/subagents/types.ts`.
- Current subagent widget rendering: `/home/alexion/wrk/dotfiles/modules/agents/pi/extensions/subagents/ui.ts`.
