# Architectural Differences Between Claude and Pi Agent Backends in Craft Agents

> Explore the architectural differences between Claude and Pi agent backends. Understand how in-process SDKs and separate subprocesses impact integration and deployment.

- Repository: [Craft Ai Agents/craft-agents-oss](https://github.com/craft-ai-agents/craft-agents-oss)
- Tags: architecture
- Published: 2026-07-03

---

**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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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.

```typescript
// 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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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.

```typescript
// 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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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:

```typescript
// 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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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:

```typescript
// 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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/pi-agent-server/dist/index.js) into `resources/pi-agent-server` via `copyPiAgentServer` in [`scripts/build/common.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/scripts/build/common.ts):

```typescript
// 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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/pi-agent-server/src/index.ts) with `--target=node --format=cjs`, plus a copy step in [`scripts/build/common.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/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.