Add subagent result delivery artifacts
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
---
|
||||
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`.
|
||||
Reference in New Issue
Block a user