Architectural Differences Between Claude and Pi Agent Backends in Craft Agents
Claude runs as an in-process SDK while Pi spawns a separate subprocess communicating over JSONL, creating fundamentally different integration patterns for session management, error handling, and deployment.
The craft-ai-agents/craft-agents-oss repository implements two distinct first-party AI backends that power agent interactions with radically different architectural approaches. Understanding these architectural differences between Claude and Pi agent backends is essential for debugging integration issues, optimizing build pipelines, and extending the agent framework.
Execution Model and Process Architecture
Claude: Direct SDK Integration
In packages/shared/src/agent/claude-agent.ts, the Claude backend imports @anthropic-ai/claude-agent-sdk directly and executes entirely within the main Node process. All method calls occur in the same event loop without process boundaries, using standard esbuild or tsc for bundling.
// In-process SDK usage
import { ClaudeAgent } from '@anthropic-ai/claude-agent-sdk';
const session = await ClaudeAgent.create({ apiKey });
Pi: Subprocess-Based Architecture
The Pi backend cannot bundle its ESM-only SDK (@earendil-works/pi-coding-agent) directly. Instead, packages/shared/src/agent/pi-agent.ts spawns a ChildProcess running the pi-agent-server binary built with bun. The parent communicates via JSONL streams over readline interfaces.
// packages/shared/src/agent/pi-agent.ts
this.subprocess = spawn('node', [serverPath], {
stdio: ['pipe', 'pipe', 'pipe']
});
// Communication via JSONL
this.subprocess.stdin.write(JSON.stringify({ type: 'initialize', config }) + '\n');
The build pipeline differs significantly: Claude uses standard tooling, while Pi requires bun build packages/pi-agent-server/src/index.ts --target=node --format=cjs as defined in scripts/electron-build-main.ts.
Session Management and System Prompt Handling
System Prompt Persistence Strategies
Claude preserves system prompts across turns using the SDK's append: option, writing to state.systemPrompt directly. The SDK handles persistence internally.
Pi rewrites the system prompt on every turn, wiping state.systemPrompt. Craft implements applySystemPromptOverride to stamp three private fields and survive these resets:
// Workaround for Pi's system prompt wiping
applySystemPromptOverride(state, prompt) {
state.systemPrompt = prompt;
state._baseSystemPrompt = prompt;
state._rebuildSystemPrompt = true;
}
This fix is documented in apps/electron/resources/release-notes/0.9.2.md.
Session Identification
Claude stores sessionId returned directly from the SDK. Pi generates a piSessionId reported via JSONL session_id events and maintained in PiAgent.piSessionId, requiring the PiEventAdapter to recreate AgentEvent objects from the stream.
Tool Integration and Permission Layer
Tool Wrapping Mechanisms
Claude tools are wrapped by the SDK itself using the tool helper, with the UI registering BUILT_IN_TOOLS. Pi tools require dual registration: first with the subprocess (SESSION_TOOL_NAMES), then proxied via JSONL (SESSION_BACKEND_TOOL_NAMES). The PiAgent class implements getSessionToolProxyDefs to forward tool definitions.
Permission Enforcement
Claude enforces PermissionMode internally via mode-manager.ts. Pi duplicates checks in the parent (runPreToolUseChecks) before sending requests, with the subprocess rejecting unauthorized calls explicitly.
MCP Integration
Claude uses createSdkMcpServer directly in-process. Pi uses a shared McpClientPool in the main process, passing a callbackPort to the subprocess for proxying requests back to the pool:
// PiAgent manages callback port for MCP proxying
callbackPort: number;
// Subprocess forwards MCP requests to this port
Error Handling and Diagnostics
Error Propagation
Claude errors convert via mapClaudeSdkAssistantError into AgentError objects immediately. Pi errors serialize into JSONL, rehydrate through parseError, and deduplicate via MAX_IDENTICAL_SUBPROCESS_ERRORS to prevent UI flooding.
Diagnostics and Recovery
The Pi parent buffers subprocess stderr in stderrBuffer for connection-test diagnostics, while Claude receives errors directly through SDK callbacks. Pi also implements a 5-state overflow recovery state machine in PiEventAdapter to handle context-overflow and compaction races, documented in release-notes 0.9.1, whereas Claude uses simpler midStreamBehavior logic (queue versus steer).
Model Resolution and Provider Catalog
Model Configuration
Claude resolves models via isClaudeModel and getModelContextWindow against the Anthropic catalog. Pi pulls from a centralized registry (getModelById) supporting multiple providers (OpenRouter, Bedrock). The catalog auto-generates from packages/pi-agent-server/src/custom-endpoint-models.ts.
Thinking Configuration
Claude uses resolveClaudeThinkingOptions with THINKING_TO_EFFORT mappings. Pi maps thinking levels through the SDK's own thinkingLevelMap, fetched via getModelById.
Build and Deployment Pipeline
Electron Packaging
Claude bundles the SDK directly into Electron resources. Pi copies the compiled pi-agent-server/dist/index.js into resources/pi-agent-server via copyPiAgentServer in scripts/build/common.ts:
// scripts/build/common.ts
await copyPiAgentServer('resources/pi-agent-server');
Docker Requirements
The Claude Dockerfile installs only the SDK package. The Pi Dockerfile (Dockerfile.server) builds the server with bun build and copies the CJS output, requiring Bun toolchain and recent Node versions that can load the compiled output.
Summary
- Process Model: Claude runs in-process via direct SDK import; Pi spawns a JSONL-speaking subprocess built with
bun - System Prompts: Claude appends persistently; Pi requires
applySystemPromptOverrideto stamp three private fields and survive resets - Tools: Claude uses direct SDK wrapping with
toolhelper; Pi proxies through subprocess withgetSessionToolProxyDefsand dual registration - Errors: Claude maps directly via
mapClaudeSdkAssistantError; Pi serializes, deduplicates withMAX_IDENTICAL_SUBPROCESS_ERRORS, and buffers stderr - MCP: Claude uses
createSdkMcpServerdirectly; Pi proxies throughMcpClientPoolviacallbackPort - Build: Claude uses standard esbuild; Pi requires
scripts/electron-build-main.tswith Bun compilation andcopyPiAgentServerresource copying
Frequently Asked Questions
Why does Pi require a subprocess while Claude runs in-process?
The Pi SDK (@earendil-works/pi-coding-agent) is ESM-only and cannot be bundled with the main Electron process. The pi-agent-server subprocess built with bun bridges this limitation by communicating over JSONL streams, whereas the Claude SDK (@anthropic-ai/claude-agent-sdk) supports standard CommonJS bundling compatible with esbuild and tsc.
How does system prompt handling differ between the two backends?
Claude's SDK preserves system prompts across turns using an append: option that writes to state.systemPrompt directly without overwriting previous state. Pi's SDK wipes the system prompt on every session.prompt() call, requiring Craft's applySystemPromptOverride helper to stamp three private fields (_baseSystemPrompt, _rebuildSystemPrompt) to prevent state loss, as documented in release-notes 0.9.2.
What additional build tooling is required for Pi compared to Claude?
Claude builds with standard esbuild or tsc without special requirements. Pi requires the bun compiler to build packages/pi-agent-server/src/index.ts with --target=node --format=cjs, plus a copy step in scripts/build/common.ts to move the compiled binary into Electron resources. The Pi backend also depends on @earendil-works/pi-agent-core and the server binary at runtime, while Claude only needs the SDK package.
How do error handling strategies compare between Claude and Pi backends?
Claude converts errors directly via mapClaudeSdkAssistantError into AgentError objects within the same process. Pi serializes errors into JSONL for cross-process communication, deduplicates identical subprocess errors using MAX_IDENTICAL_SUBPROCESS_ERRORS to prevent UI flooding, and buffers stderr in stderrBuffer for diagnostic visibility during connection failures. Pi also implements a 5-state overflow recovery state machine in PiEventAdapter absent from the Claude implementation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →