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 applySystemPromptOverride to stamp three private fields and survive resets
  • Tools: Claude uses direct SDK wrapping with tool helper; Pi proxies through subprocess with getSessionToolProxyDefs and dual registration
  • Errors: Claude maps directly via mapClaudeSdkAssistantError; Pi serializes, deduplicates with MAX_IDENTICAL_SUBPROCESS_ERRORS, and buffers stderr
  • MCP: Claude uses createSdkMcpServer directly; Pi proxies through McpClientPool via callbackPort
  • Build: Claude uses standard esbuild; Pi requires scripts/electron-build-main.ts with Bun compilation and copyPiAgentServer resource 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →