# How Craft Agents Architecture Supports Both Claude and Pi Agent Backends

> Discover how Craft Agents' backend-agnostic LLM abstraction layer seamlessly supports both Claude and Pi agents. Learn about unified connections and normalized tool calls.

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

---

**Craft Agents implements a backend-agnostic LLM abstraction layer that routes requests through a unified connection model, allowing the same UI and server code to instantiate either Claude or Pi agents via a factory pattern while normalizing tool calls through a shared MCP pool.**

The **craft-ai-agents/craft-agents-oss** repository achieves seamless multi-backend support through a provider-agnostic architecture. By decoupling the LLM implementation from the application logic, Craft Agents architecture supports Claude and Pi agent backends interchangeably without requiring changes to the frontend components or tool integration layers.

## Unified LLM Connection Layer

At the core of the abstraction sits the **LLM-connections** layer defined in [`packages/shared/src/config/llm-connections.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/shared/src/config/llm-connections.ts). This module establishes a common `LlmConnection` shape that stores the provider type, authentication method, and custom endpoints.

### Provider Type Detection

The system distinguishes backends using the **`LlmProviderType`** enum. When a connection is initialized, the code examines the `provider` field to determine which SDK to instantiate:

- **`anthropic`** – Routes to the Claude Code backend using the Claude Agent SDK (pure ESM)
- **`pi_compat`** – Routes to the Pi Agent backend using the `@earendil-works/pi-coding-agent` package

The `isPiProvider` check in [`llm-connections.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/llm-connections.ts) enables conditional logic throughout the credential manager and UI layers to surface the correct API tokens and connection strings.

### Connection Schema

Both backends share an identical connection schema, allowing the UI to treat them interchangeably. The factory at [`packages/shared/src/agent/backend/factory.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/shared/src/agent/backend/factory.ts) reads the provider field and returns a concrete implementation—either `ClaudeAgent` or `PiAgent`—while keeping the rest of the codebase independent of SDK specifics.

## Backend Factory Pattern

The **`resolveBackend`** function (a thin wrapper around the factory) hides SDK differences by returning a normalized `query` function compatible with both `ClaudeAgent.query` and `PiAgent.queryLlm`.

### Subprocess Isolation for Pi

Because the Pi SDK is ESM-only and contains native dependencies, Craft Agents spawns it as a separate Node process called the **Pi agent server**. The build scripts in [`scripts/electron-build-main.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/scripts/electron-build-main.ts) (lines 211-229) and [`scripts/electron-dev.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/scripts/electron-dev.ts) (lines 30-33) compile this server with `--target=bun --format=esm` flags and bundle the resulting [`index.js`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/index.js) into the app resources.

This subprocess starts on demand and exposes a minimal RPC layer that matches the Claude SDK's `queryLlm` signature, ensuring the main process communicates with both backends using the same interface.

## Normalizing Tool Calls Across Backends

Tool call interoperability is achieved through two shared layers that normalize the differing response formats between Claude and Pi.

### MCP Pool Registration

The **MCP (Model-Code-Proxy) Pool** at [`packages/shared/src/mcp/mcp-pool.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/shared/src/mcp/mcp-pool.ts) registers **proxy tool definitions** that are sent to both backends. This ensures that tool schemas and capabilities are identical regardless of which LLM provider is active.

### Network Interceptor

The **[`unified-network-interceptor.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/unified-network-interceptor.ts)** (lines 850-860) normalizes tool-call deltas streaming from Claude or Pi. By intercepting and transforming the network responses, the system presents a single stream of "tool calls" to the UI, masking backend-specific formatting differences.

## Credential and Prompt Management

### API Key Storage

The credential manager at [`packages/shared/src/credentials/manager.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/shared/src/credentials/manager.ts) (lines 232-244) stores both `anthropic_api_key` and `pi_api_key`. The `isPiProvider` check determines which token to surface to the active connection, allowing users to maintain separate credentials for each backend.

### Dynamic System Prompts

Prompt helpers in [`packages/shared/src/prompts/system.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/shared/src/prompts/system.ts) inject a **`backendName`** variable (default "Claude Code") into system prompts. When a Pi connection is active, this value overrides to "Pi Agent", which updates the "Powered by X" badge and modifies **stable/volatile** block handling behavior described in the prompt text.

## Runtime Configuration Examples

### Declaring a Claude Connection

```json
{
  "name": "Claude-Sonnet-4-6",
  "provider": "anthropic",
  "authType": "apiKey",
  "apiKey": "<your-anthropic-key>"
}

```

The UI picks this up via `@config/llm-connections` and the factory creates a `ClaudeAgent` instance.

### Declaring a Pi Connection

```json
{
  "name": "Pi-OpenAI-GPT-4-o",
  "provider": "pi_compat",
  "authType": "apiKey",
  "apiKey": "<your-openai-key>",
  "midStreamBehavior": "queue",
  "model": "gpt-4o"
}

```

Because `provider` is `pi_compat`, [`llm-connections.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/llm-connections.ts) routes the request to the Pi-agent server.

### Switching Backends in Code

```typescript
import { resolveBackend } from '@craft-agent/shared/config/llm-connections';

// `connection` is pulled from the UI state (e.g. a selected model)
const { backend, query } = resolveBackend(connection);

/* `backend` is either "Claude" or "Pi" – useful for UI badges */
console.log(`Using ${backend} back-end`);

/* `query` has the same signature for both SDKs */
const response = await query({
  messages: [{ role: 'user', content: 'Explain the difference between Claude and Pi.' }],
});

```

### Configuring a Custom Pi Endpoint

```typescript
const connection = {
  name: 'Pi-Custom-Claude-Compat',
  provider: 'pi_compat',
  authType': 'apiKey',
  apiKey: '<key>',
  customEndpoint: {
    baseUrl: 'https://my-pi-proxy.example.com',
    apiVersion: 'v1'
  }
};

```

The `customEndpoint` field is honored by the Pi-agent server while being ignored for Claude connections.

## Summary

- **Backend-agnostic architecture**: The `LlmConnection` schema in [`llm-connections.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/llm-connections.ts) provides a unified interface for both Claude and Pi providers.
- **Factory pattern**: [`packages/shared/src/agent/backend/factory.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/shared/src/agent/backend/factory.ts) instantiates `ClaudeAgent` or `PiAgent` based on the `provider` type, keeping implementation details isolated.
- **Subprocess isolation**: Pi runs as a separate ESM-only Node process compiled with Bun targets, while Claude integrates directly.
- **Normalized tool streams**: The MCP pool and unified network interceptor ensure tool calls appear identical to the UI regardless of backend.
- **Unified credentials**: The credential manager stores both `anthropic_api_key` and `pi_api_key`, selecting the appropriate token based on the active provider.
- **Consistent UI**: Components like [`ChatPage.tsx`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/ChatPage.tsx) use `resolveEffectiveConnectionSlug` to render the same interface for both backends.

## Frequently Asked Questions

### How does Craft Agents detect which backend to use?

The system checks the `provider` field in the connection object. If the value is `anthropic`, it instantiates the Claude Agent SDK; if `pi_compat`, it routes to the Pi agent server. This detection happens in [`packages/shared/src/config/llm-connections.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/shared/src/config/llm-connections.ts) and the factory at [`packages/shared/src/agent/backend/factory.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/shared/src/agent/backend/factory.ts).

### Why does Pi run in a separate subprocess?

The Pi SDK (`@earendil-works/pi-coding-agent`) is ESM-only and contains native dependencies that conflict with the main application's CommonJS architecture. By spawning it as a separate Node process built with `--target=bun --format=esm`, Craft Agents avoids module system conflicts while exposing a compatible RPC interface.

### Can I use custom endpoints with Pi?

Yes. The `customEndpoint` field in the connection configuration allows specifying a `baseUrl` and `apiVersion` that the Pi-agent server uses to route requests. This field is ignored when using Claude connections, allowing hybrid deployments where Claude uses standard Anthropic endpoints while Pi connects to custom proxies.

### Is the tool call format identical for both backends?

While the underlying SDKs return different delta formats, Craft Agents normalizes these through the [`unified-network-interceptor.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/unified-network-interceptor.ts) (lines 850-860) and the MCP pool. The UI receives a standardized stream of tool calls, making the developer experience identical regardless of whether Claude or Pi is generating the response.