Orion runs two functionally-parallel agent runtimes. Which one handles a given turn depends on the 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?

DriverClaude SDK strengthPi strength
Cost per turnHigh (Sonnet/Opus pricing)Low (Gemini Flash ~36× cheaper than Sonnet for heartbeat)
Reasoning depthDeep, nuanced writing, heavy tool orchestrationAdequate for routine summary/digest/scan
Provider varietyAnthropic onlyOpenAI, Google, OpenRouter, Anthropic-via-Pi, etc.
Process modelSpawned child (sdk-runner.mjs)In-process (piAgentQuery)
Source: .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 in src-tauri/sidecar/engine/model-router.mjs:23-54. The branch on the resolved route happens at handler.mjs:2172. ✓ VERIFIED
// "claude-sonnet-4-6" → engine: 'claude'
// "claude-haiku-4-5-20251001" → engine: 'claude'
// Anything starting with "claude-" routes to the SDK.

Parity matrix

The two engines have functional parity for tools, skills, sandbox, and subagents. They differ at the runtime layer. This table comes from cron-engine-routing.md and has been verified against the audit:
AspectClaude SDKPi
Process modelSpawned subprocess (sdk-runner.mjs)In-process (piAgentQuery)
CancellationgracefulKill() SIGTERM → SIGKILLAbortController.abort()
OS-level sandbox (Bash)sandbox-exec via SDK@carderne/sandbox-runtime (same primitive)
Filesystem policyadditionalDirectories allowlistassertSandboxPath + path-policy
MCP toolsNative SDK MCP serversBridged via mcp-bridge.mjs or proxy mode
Skillsprompts/skills/ + vault skills/Same — passed via skillsPaths dep
System promptbuildOrionSystemPrompt({engine:'claude'})buildOrionSystemPrompt({engine:'pi'})
Subagent primitiveNative Agent/Task toolspawn_subagent (single/chain/parallel)
Hard-blocks (.env, *.pem, .aws/)Via sandbox-execVia path-policy.denyWrite (non-removable)
Billing prefixproxy-* via Workercron-pi-* via reportPiUsage
Cron-mode (no canUseTool)Settings tool denials silentlynoUserAvailable: true → auto-deny prompts
There are no remaining capability asymmetries as of v5 (2026-04-16). The only invisible differences are the underlying sandbox primitive and the process model. ✓ VERIFIED

When to choose which

  • 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

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.
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.
Thin launcher. Stdin/stdout JSON-line bridge between Rust and Node.
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.