Give Pi extensions a first-class way to wait until a custom/background result has been delivered into a model-visible turn before clearing UI-only pending state.
This is for generic long-running awaited tools (for example pi-background-tool), not for adding model-controlled background parameters to tools.
Inspected installed Pi source under (paths relative to the active Node node_modules root, e.g. ~/.nvm/versions/node/<v>/lib/node_modules):
@earendil-works/pi-coding-agent/dist/core/agent-session.js@earendil-works/pi-coding-agent/dist/core/sdk.js@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js@earendil-works/pi-coding-agent/dist/core/messages.js
Observed path:
- Extension
pi.sendMessage(message, { deliverAs: "steer" | "followUp", triggerTurn: true })is bound byAgentSession._bindExtensionCore()tothis.sendCustomMessage(message, options). AgentSession.sendCustomMessage()converts extension input to anAgentMessagewithrole: "custom".- If Pi is streaming,
deliverAs: "steer"callsthis.agent.steer(appMessage)anddeliverAs: "followUp"callsthis.agent.followUp(appMessage). - If Pi is idle and
triggerTurnis true,sendCustomMessage()callsthis.agent.prompt(appMessage). pi-agent-core/dist/agent-loop.jsdrains steering messages before the next assistant response and follow-up messages after the agent would otherwise stop.- Immediately before each model call,
agent-loop.jsappliestransformContext, thenconvertToLlm(messages), then callsstreamFn(model, llmContext, options). - Pi's
convertToLlm()mapsrole: "custom"messages to LLMrole: "user"messages. Therefore a custom background result is model-visible when it reaches this provider-request boundary. dist/core/sdk.jswiresstreamFnoptions:onPayloademitsbefore_provider_requestand can replace the provider payload.onResponseemitsafter_provider_responseafter response headers are received and before the stream is consumed.
Implication: today's public extension events can prove two different things:
before_provider_requestproves the background result text is in the provider payload Pi is about to send.after_provider_responseafter a matchingbefore_provider_requestis stronger: it proves Pi received provider response headers for a request whose payload included the background result text.
There is no single first-class runtime API like waitForMessageDeliveredToModel(messageId) or event like message_delivered_to_model.
A package can approximate delivery by correlating:
before_provider_requestpayload contains the custom result content, then- the next
after_provider_responsefires.
That approximation is based on real Pi events, but it is still an extension-side heuristic because:
after_provider_responsedoes not include a provider request id, payload hash, or message ids.before_provider_requestexposes provider-shaped payloads, so content matching is brittle and may include escaped/transformed text.- There is no runtime-owned acknowledgement tied to a specific
AgentMessageor session entry.
Add a delivery-oriented extension event or promise API at the provider boundary, after convertToLlm() and before/after provider dispatch.
Preferred event shape:
interface ProviderRequestPreparedEvent {
type: "provider_request_prepared";
requestId: string;
sessionId?: string;
messages: Message[];
payload: unknown;
}
interface ProviderRequestDeliveredEvent {
type: "provider_request_delivered";
requestId: string;
sessionId?: string;
status: number;
headers: Record<string, string>;
}Minimum viable alternative:
- Add a stable
requestIdto both existingbefore_provider_requestandafter_provider_responseevents. - Optionally include the converted
messagesor a runtime-computedmessageIdslist if AgentMessages can retain ids at that boundary.
Use pi-mock as an end-to-end UX harness, not only provider payload matching.
Regression shape:
- Temporary extension registers a slow tool using a generic background runner.
- Tool starts work immediately.
- It completes after the auto-background threshold.
- Pending UI starts only after promotion.
- Final custom message is sent with
deliverAs+triggerTurn. - A provider request containing the custom background context occurs.
- Pending UI finishes only after the provider delivery signal, not merely after
sendMessage()returns. - The model responds to the background context.
Current local proof in the sibling pi-background-tool repo:
npm test
✔ unit lifecycle tests
✔ pi-mock integration: pending finishes after background result is present in the next LLM request
Until Pi exposes a first-class request/delivery id, pi-background-tool should keep a small waitForInjection seam. The default watcher should use the strongest available real events:
- register the target before calling
pi.sendMessage()to avoid missing immediate turns; - observe matching content in
before_provider_request; - resolve only after the following
after_provider_responseby default; - allow a test-only/legacy option to resolve at
before_provider_request.
This keeps pi-pending as UI only and avoids treating model output as protocol.