Runtime architecture
This overview describes ownership and integration points. Package READMEs own supported behavior; this page does not add guarantees. Use the glossary for local terms, the object model for how they relate, and verification to select evidence.
Source and loading
Section titled “Source and loading”| Source | Responsibility | Outside this boundary |
|---|---|---|
pi/ | Persona, settings, agent definitions, prompts, themes, and Bun launcher | Dotfiles owns machine wiring; Pi owns the host runtime |
extensions/ | Private, independently versioned extension packages | Installed third-party packages and external CLI engines have their own owners |
skills/ | Canonical instruction tree and supporting scripts | The LP repository’s mirror is generated |
evals/ | Manual instruction scenarios, shared projection, and runtime evaluators | A local result does not qualify other deployments or untested models |
apps/ | Pi frontends, Pi core, and clink | Pi never loads them: the frontends drive Pi core, which runs Pi as a separate process, and clink runs chat agents on pi-durable in its own process |
The live agent directory points through the ~/.pi link to the primary checkout. Settings select the local extensions/ aggregate. Its manifest lists packages in load order; each package manifest selects its entrypoints. Pi loads TypeScript directly through jiti. There is no compile or publication step for these local packages. Pi and the workspace run on Bun (decision).
Pi gives entrypoints separate module caches. Cross-package imports therefore carry stateless helpers, types, and constants, not shared mutable module state. The shared work widget publishes a service under a Symbol.for key; producers find it on each call. The async-task service is another, published by async-tasks and read by user-turns, herdr-report, the subagent child, and the worktree remove gate; readers that load before async-tasks find it on each use, never at session start. The user-turns service publishes the open user turns, which display reads. See cross-package imports, the widget protocol, and the decision.
Session, run, and user turn
Section titled “Session, run, and user turn”A prompt enters Pi’s input and pre-agent hooks, then starts a run: model and tool work until its answer. agent_end can precede retries or queued runs, so the session stays busy until agent_settled, when Pi will not continue on its own. A user turn can outlast that: it waits while a wake-capable async task it launched is live, and the wake’s run belongs to it. A run, a busy span, and a user turn are different boundaries; the object model defines each. The user-turns package owns them (decision). It loads before display, and both handle the same Pi events, so display reads each event’s effect on the user turn (load order). Tracking, origin accounting and summary recording are backend concerns: they run in every mode, children included, without tui-display loaded and regardless of /display off. Answer continuation runs in native TUI and RPC, including frontends and subagent children, also independent of display. A child’s automatic settle report reads the versioned user-turns service; explicit done/ping remains a terminal override. Print and JSON continuation stay off.
User-turns also owns a default-off request-completion policy, separate from answer revision. Only enabled ordinary root native TUI/RPC sessions use a bounded finish decision. Startup/reload snapshots enablement; off and managed/unknown sessions receive no added model surface. Print/one-shot JSON are native no-ops with no launcher or lifetime hook. Existing owner protocols remain authoritative.
An idle triggerTurn message starts an extension-triggered run, such as a bridge resume, a stall retry, or a wake. The subagent roster remains durable transcript context. User-turns’ answer continuation instead projects private context, scoped to the latest delivered user input, at model-request time, without persistence or another model request.
The display owner controls presentation, not execution or permission, and patches private Pi presentation seams to do so (decision). The footer owns the session-cost calculation it displays. TUI-only services stay absent from protocol streams; RPC can support dialogs without supporting a terminal widget.
Asynchronous work
Section titled “Asynchronous work”Async tasks are launched by a call that returns at once, and each delivers a result to its session when it ends. Owned Relay loads before async-tasks and carries both peer mail and these native result/ping messages through one filesystem mailbox and placement-acknowledging receiver; task publication, process lifetime and authority stay with their current owners. Subagents launch named headless Pi children through RPC; the package replaced a third-party fork (decision). A subagent’s result is a message keyed by launch ID. Shell tasks, in the same async-tasks package, launch shell process groups. A result can wake the session, but its delivery and run-trigger flags are distinct. The package lists both kinds in one async-task service: each task with its launching tool call, delivery flags, wake capability, status, and outcome, so its readers need not match tool names and result types.
A child decides when it is finished, not its parent. After each settle it reports waiting while its user-turn domain remains open — including completing work awaiting result placement and the answering run — or done when the domain is empty, and the parent then closes its input. The child policy preserves the standalone fallback only without a domain publisher, holds unknown state, and keeps explicit done/ping terminal. Only the child’s own shutdown stops its tasks, which run in their own process groups, so the parent gives a closing child time to finish that shutdown before it signals.
User-turns and the separately managed Herdr state integration consume this evidence for different purposes; herdr-report applies the Herdr integration’s rules to frontend sessions. A user turn waits for its wake-capable work. Herdr reports pane state. A persistent job is up rather than progressing and holds neither open. A session’s busy state is a third reading; do not replace these policies with one universal flag.
Result delivery and descendant-process cleanup are different events. Subagent cleanup tests observe a real descendant and the bounded, explicitly unconfirmed outcome; replacement integration checks that the next session starts only after the group is gone. See shared-boundary evidence for the checks and their limits.
Model-provider policy
Section titled “Model-provider policy”Providers initializes physical providers independently and owns their transport, catalogs and retirement. Profiles alone owns native/legacy visibility and model selection, retaining original native objects through Pi’s registry. After applying a usable view/model it commits profile state, publishes the unchanged notification, and requests displaced-source retirement through the provider-owned my-pi.providers.retirement.v1 service. Its key, types and stateless reader live in shared/contracts/; no profile instruction flows back to providers. Stock Pi’s late session hook and global early --list-models boundary remain unchanged.
Work root
Section titled “Work root”The worktree owner separates creation and removal from entry, and entry replaces nothing. worktree enter sets the session’s work root: Pi’s built-in file and shell tools run there, and packages that resolve paths or spawn processes read it through workRootOf(ctx). The session keeps its file, its launch cwd, and its project resources, so running subagents, shell tasks, and goals are untouched.
Pi still replaces sessions for /resume, /new, /fork, and reload. Pi invalidates the previous session context on replacement; later callbacks must not treat it as current.
Supervision and external boundaries
Section titled “Supervision and external boundaries”The supervisor manages a durable project of units and attempts. A coordinator lease permits actions; an inbox records communication. Herdr supplies liveness facts, while reports and mechanical checks supply completion evidence. Human-input authorization is separate from an automatic wake. State requires a local filesystem, and the guard is cooperative policy, not a sandbox.
Pi’s built-in MCP support loads global servers and admits project .pi/mcp.json only after project trust. MCP calls still pass through Pi’s tool-permission pipeline. ast-grep reads and rewrites the working tree independently. Codebase memory exposes upstream graph tools through native MCP for the current work root, with explicit indexing when requested. Queries use existing snapshots without a preparation lease; snapshots may lag source or include uncommitted changes. Stall recovery depends on Pi settling and fetch cancellation.
Frontends
Section titled “Frontends”A frontend owns all presentation, and its backend runs Pi as a separate process through pi/bin/pi --mode rpc; Pi’s own TUI does not start (decision). Pi loads the same settings, extensions, skills, and session files it uses interactively, so a session can move between a frontend and native Pi. The process boundary is Pi’s RPC protocol: JSONL commands and extension UI responses on stdin, and command responses, session events, and extension UI requests on stdout. The TUI app is the first frontend. Pi core is its backend: it runs Pi, speaks the protocol, and keeps the session as a view model that the TUI and its design reference both draw.
RPC mode changes what extensions do. ctx.mode is rpc and ctx.hasUI is true: dialogs, notices, status entries, and string widgets reach the frontend as extension UI requests, while terminal components, custom editors, and theme access do not. Features gated on the TUI, such as display’s widget, the footer, asides, and terminal cwd reporting, are off. User-turns’ answer continuation uses the same origin-answer projection in native TUI and all RPC sessions, independent of child identity; print and JSON stay off. Prompts from the frontend carry the rpc input source, which the supervisor does not accept as human input.
A subagent child is also an RPC session, and it inherits its parent’s environment but not its command line. A behavior meant only for frontend-driven sessions therefore rides on the command line, or on an environment marker that Pi removes from its environment when it loads, before it starts any child. Herdr’s own integration reports only TUI sessions; herdr-report reports a frontend’s session to Herdr through such a marker, PI_FRONTEND, and stays inert in subagent children. The frontend bridge is Pi core’s half inside Pi: Pi core loads it with --extension into every Pi it starts, before any package. It carries the work-status rows that tui-display would draw, a marker at each session start, and the session’s work root, to Pi core as widget lines on Pi’s stdout, in order with the events. Pi core never draws those widgets; it keeps the rows by the store’s rules and shows the work root as the session’s cwd and branch.
Clink is the other app in apps/, beside the frontends and Pi core: it runs chat agents on pi-durable in one process, with no Pi process and no frontend. Each deployment is a clink instance, another app in apps/, such as apps/jobson. See clink and decision.
Candidate versus live source
Section titled “Candidate versus live source”A candidate worktree is not the live runtime. Use runtime trial isolation for host or interactive checks, and the candidate instruction trial for instruction assessment. Verify the root and any child paths before interpreting results. Code, instructions, installed dependencies, and credentials are separate inputs.
Delivery is also separate from activation. After an authorized merge, update the primary checkout and reload the session under the activation procedure. Never point the live agent directory at a trial to make a test pass.