Judged by its feature list, PI-Desktop is easy to sum up as "desktop chat + plugins". Recently, though, I walked one Agent turn through the source — startup, model call, tool execution, session save — and what turned out to be more interesting is how it divides responsibilities: who owns the conversation loop, who persists sessions, and through which boundaries plugins and sub-agents attach.
What follows is not a list of product features but an account of how the repository actually implements things. Code links point to the project's currently public source; the implementation is still moving, so specific behaviour depends on the version.
First, where does a single request travel?
桌面界面
│ IPC / 状态事件
▼
Electron 主进程
├── host-core(Rust):会话数据、索引、工具与权限入口
└── agent sidecar(Node.js):Agent 运行时、模型请求、工具编排
│
└── 已配置的模型服务 / 本地模型 / 网关
agent-runtime/src/sidecar.ts defines the sidecar as a Node process that communicates with the Electron main process over NDJSON JSON-RPC on stdio; the sidecar's access to the host is ParentHostProxy forwarded, rather than letting the renderer hold the model loop or a database connection directly. The main process assembles the session startup parameters, and the runtime then wires up the model stream, tool calls and events.
The split has a very practical point: model adaptation, desktop permissions and the UI do not have to be crammed into one module. The sidecar owns the Agent's behaviour, for example, while session persistence and anything needing desktop authorisation still go through the main process / host path.
A session is not "one chat table stuffed into SQLite"
Look at crates/host-core/src/transcripts.rs, and each session's body lives in its own JSONL file:
sessions/<session_id>.jsonl: the current conversation transcript;sessions/<session_id>.revisions.jsonl: branches archived when you regenerate or edit;sessions/<session_id>.inflight.json: checkpoints for a streaming reply that has not finished yet.
SQLite holds session configuration and indexes.db/schema.rs contains the sessions table, which records the project, model, mode and permission configuration;messages table stores index fields such as sequence number, role and searchable text, with FTS5 for full-text search. Keeping bodies and indexes apart avoids stuffing large message payloads into the query table.
The design also shows up in the write order:sessions.rs is where the append_record appends to JSONL first and only then updates the SQLite index. The source comments explicitly treat the transcript file as the source of truth: if the process exits right after appending to the file but before the index commit, what you lose is a rebuildable index row, not the only copy of the body. Later read/repair logic handles that kind of inconsistency. Streaming replies get their own checkpoints, so a complete reply is not re-appended to the transcript on every burst of tokens.
So "a session can be resumed" does not mean the in-memory Agent object lives forever.sidecar.ts caches the live DesktopAgentRuntimekeyed by session ID; if the corresponding runtime is absent, it goes through session.get to re-read history, restore compaction records and attachments, and create a new Agent instance. What persists is session data; the runtime can be rebuilt from it.
When you switch models, what actually switches underneath?
provider-binding.ts binds the API style to a concrete adapter based on the provider and model configuration. In the source you can see paths for OpenAI Chat Completions, Responses, Anthropic Messages, Codex Responses, Google Generative AI, pi-messages and more; the API style of a custom endpoint takes part in the binding as well.runtime.ts then issues the request with the resolved model, streaming interface and current session context.
Which means "supporting many models" is not a matter of pointing the same HTTP request at a different URL: protocols, model capabilities, thinking levels and request headers all have to match. The project funnels some of those compatibility rules into the provider binding layer, so the Agent's upper loop rarely deals with each service's specifics directly.
Credentials have boundaries too. Take an OAuth provider: the sidecar's startup configuration carries no access token; when it sends a request it asks the Electron main process, through a callback, for the auth material belonging to the current provider. API keys, OAuth and custom gateways still need user configuration. With a remote model, the session context you send leaves the machine; "data is stored locally" does not mean "inference is fully offline".
Task sub-agents are not the same as opening another chat session
There are two parallel mechanisms in the source that are easy to conflate. Start with the Agent Runtime's built-in Task.
packages/agent-runtime/src/subagent.ts creates another pi Agentfor a delegated task: it has its own task prompt, an optional model binding and a restricted tool set, but it still runs inside the current sidecar process and executes tools over the same host channel. It is not a brand-new persistent desktop session.
The parent Agent does not stuff the child's entire reasoning and tool output back into its own model context; the runtime mainly hands the sub-task's final report back to the parent and provides only brief status while it runs. Sub-task messages and tool records are still written to the transcript, for the UI to display and for after-the-fact inspection.Task starts, it can return first, and afterwards you can use TaskWait to collect the result,TaskList to check status,TaskStop and to request a stop. The source also caps concurrency and report length, so context and task counts cannot expand without bound.
The other path is the official plugin's persistent Worker Session. The accepted ADR 0237 explicitly places the Session Orchestrator in the plugin layer: through the host's session/create it creates real sessions, then uses agent/prompt to start work; the plugin only stores the parent/child relationship and task state, while the Worker's transcript is still managed by host-core. Its difference from Task is that a Worker has its own durable session ID and full context; it is not a temporary delegation inside the same session runtime.
The ADR also spells out the constraints on this plugin path: a Worker can only be controlled by its parent session, there is a concurrency cap, and the waiting phase uses bounded status polling rather than pretending a complete lifecycle event bus already exists. Boundaries like that are more concrete — and much easier to check — than "Agents automatically form a team".
Tools and plugins are not a back door around the main process
Once the Agent produces a tool call,agent-runtime/src/runtime.ts first checks whether the tool is allowed in the current mode.plan and goal are the modes that establish an execution agreement first, and the source gives them a read-only tool allowlist; only the agent mode that actually performs tasks opens the corresponding execution paths. Tool permission requests are sent back from host-core to the Electron main process and then handed to the UI as events, so the user can see the requested tool, its risk and a summary of the arguments.
Plugins do not get an unrestricted desktop object either.apps/desktop/electron/main/plugin-runtime.ts has a host API allowlist and checks calls against plugin permissions; the code also puts native user confirmation for dangerous desktop operations, confirmation of undeclared file scopes, and protection of the app's own data directory on the host side.
This should not be read as "plugins are absolutely safe" or "every operation pops a dialog". What it says is that plugin capabilities are provided by the host and pass through permission boundaries, rather than plugin code being free to take over the session database and every desktop API. When you install a plugin, still look at what it declares and requests.
What I think is most worth noticing about this architecture
Connecting the code up, PI-Desktop's core is not a magic Agent algorithm but several links that can each be replaced and checked:
- host-core owns persistence and indexing: the JSONL transcript is the source of the body, while SQLite handles session metadata, indexes and search.
- the sidecar owns the Agent runtime: it restores context from a saved session and handles model requests, tool calls, compaction and sub-agents.
- provider binding owns protocol adaptation: once a session picks a provider/model, the runtime picks the matching wire API and capability parameters.
- plugins extend capability through host APIs: tools, MCP, the work panel and so on join the existing workspace, but still go through host permission paths.
- short and long tasks are modelled separately: in-session
Tasksuits bounded delegation; when you need a complete, independent history, use a real Worker Session.
So my aim with PI-Desktop is not to reskin a chat box, but to break Agent work into owned projects, resumable sessions, replaceable models, controllable tools and traceable delegation. That direction does not mean the current version is mature: the repository is still labelled Early Preview, and both the plugin ecosystem and the cross-platform experience are still moving.
If you want to verify this yourself, start from these implementation entry points:
- sidecar communication and session resume
- Agent runtime and tool gating
- provider/API binding
- sub-agent implementation
- JSONL transcript format
- SQLite session and message index
- plugin host runtime
Download the latest release · Project docs · source repository