How Craft Agents Architecture Supports Both Claude and Pi Agent Backends
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. 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-agentpackage
The isPiProvider check in 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 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 (lines 211-229) and scripts/electron-dev.ts (lines 30-33) compile this server with --target=bun --format=esm flags and bundle the resulting 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 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 (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 (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 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
{
"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
{
"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 routes the request to the Pi-agent server.
Switching Backends in Code
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
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
LlmConnectionschema inllm-connections.tsprovides a unified interface for both Claude and Pi providers. - Factory pattern:
packages/shared/src/agent/backend/factory.tsinstantiatesClaudeAgentorPiAgentbased on theprovidertype, 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_keyandpi_api_key, selecting the appropriate token based on the active provider. - Consistent UI: Components like
ChatPage.tsxuseresolveEffectiveConnectionSlugto 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 and the factory at 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 (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.
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 →