model string in the payload. They are not abstracted into one — the codebase treats them as separate runtimes that happen to feed the same downstream event stream.
Why two engines?
| Driver | Claude SDK strength | Pi strength |
|---|---|---|
| Cost per turn | High (Sonnet/Opus pricing) | Low (Gemini Flash ~36× cheaper than Sonnet for heartbeat) |
| Reasoning depth | Deep, nuanced writing, heavy tool orchestration | Adequate for routine summary/digest/scan |
| Provider variety | Anthropic only | OpenAI, Google, OpenRouter, Anthropic-via-Pi, etc. |
| Process model | Spawned child (sdk-runner.mjs) | In-process (piAgentQuery) |
.claude/rules/runtime/cron-engine-routing.md — the authoritative rule for engine selection.
How a turn gets routed
The routing logic is small and lives insrc-tauri/sidecar/engine/model-router.mjs:23-54. The branch on the resolved route happens at handler.mjs:2172. ✓ VERIFIED
Parity matrix
The two engines have functional parity for tools, skills, sandbox, and subagents. They differ at the runtime layer. This table comes fromcron-engine-routing.md and has been verified against the audit:
| Aspect | Claude SDK | Pi |
|---|---|---|
| Process model | Spawned subprocess (sdk-runner.mjs) | In-process (piAgentQuery) |
| Cancellation | gracefulKill() SIGTERM → SIGKILL | AbortController.abort() |
| OS-level sandbox (Bash) | sandbox-exec via SDK | @carderne/sandbox-runtime (same primitive) |
| Filesystem policy | additionalDirectories allowlist | assertSandboxPath + path-policy |
| MCP tools | Native SDK MCP servers | Bridged via mcp-bridge.mjs or proxy mode |
| Skills | prompts/skills/ + vault skills/ | Same — passed via skillsPaths dep |
| System prompt | buildOrionSystemPrompt({engine:'claude'}) | buildOrionSystemPrompt({engine:'pi'}) |
| Subagent primitive | Native Agent/Task tool | spawn_subagent (single/chain/parallel) |
Hard-blocks (.env, *.pem, .aws/) | Via sandbox-exec | Via path-policy.denyWrite (non-removable) |
| Billing prefix | proxy-* via Worker | cron-pi-* via reportPiUsage |
Cron-mode (no canUseTool) | Settings tool denials silently | noUserAvailable: true → auto-deny prompts |
When to choose which
- Pick Pi
- Pick Claude
- Pick neither — Recipe mode
- Routine summary, digest, scan, monitor
- Cost-sensitive jobs running many times/day
- Local-only file ops in the vault
- Ambient services: heartbeat, enrichment, consolidation
- Default model:
google/gemini-2.5-flash(~36× cheaper than Sonnet)
Key files
src-tauri/sidecar/engine/model-router.mjs
src-tauri/sidecar/engine/model-router.mjs
routeModel(modelString) at L23-54. Parses the model string into {engine, provider, model}. Also exports isOpenAiResponsesModel() at L96-123 used by the MCP proxy gate.src-tauri/sidecar/query/handler.mjs
src-tauri/sidecar/query/handler.mjs
Main query handler. Engine routing branch at L2172. Pi
AgentSession cache lookup at L2191-2234. createCanUseTool factory called at L2091-2097 for both engine paths.src-tauri/sidecar/sdk-runner.mjs
src-tauri/sidecar/sdk-runner.mjs
Thin launcher. Stdin/stdout JSON-line bridge between Rust and Node.
src-tauri/sidecar/engine/pi-session.mjs
src-tauri/sidecar/engine/pi-session.mjs
Pi runtime’s session module. First-turn vs reused-session split at L294-591.
noPromptTemplates: true isolation prevents Pi from discovering user-global prompt templates.Next
Sessions
Both engines have session lifecycles, but they persist differently and reload differently. Reused Pi sessions freeze the system prompt — that has implications.
Event Layer
How both engines’ native events become one normalized stream the frontend can consume.