Skip to content

Integrate OpenClaw with Pi SDK for AI Coding Agents

OpenClaw uses the pi SDK to embed an AI coding agent directly into its messaging Gateway architecture. This setup gives you deep control over how the agent behaves without needing external processes.

Instead of spawning pi as a subprocess or using RPC mode, you directly import and instantiate pi’s AgentSession via createAgentSession(). This embedded approach provides:

  1. Full control over session lifecycle and event handling.
  2. Custom tool injection for messaging, sandbox, and channel-specific actions.
  3. System prompt customization per channel or context.
  4. Session persistence with branching and compaction support.
  5. Multi-account auth profile rotation with failover.
  6. Provider-agnostic model switching.

To get started with the integration, you need to include specific packages from the pi ecosystem in your JSON configuration. These packages handle everything from LLM abstractions to terminal UI components.

{
"@mariozechner/pi-agent-core": "0.64.0",
"@mariozechner/pi-ai": "0.64.0",
"@mariozechner/pi-coding-agent": "0.64.0",
"@mariozechner/pi-tui": "0.64.0"
}
  1. pi-ai: Core LLM abstractions like Model, streamSimple, message types, and provider APIs.
  2. pi-agent-core: Handles the agent loop, tool execution, and AgentMessage types.
  3. pi-coding-agent: High-level SDK providing createAgentSession, SessionManager, AuthStorage, ModelRegistry, and built-in tools.
  4. pi-tui: Terminal UI components used in the local CLI mode.

The OpenClaw agent logic is organized within the src/agents/ directory to keep the codebase clean and modular. You will find everything from runner logic to specific tool implementations here.

src/agents/
├── pi-embedded-runner.ts # Re-exports from pi-embedded-runner/
├── pi-embedded-runner/
│ ├── run.ts # Main entry: runEmbeddedPiAgent()
│ ├── run/
│ │ ├── attempt.ts # Single attempt logic with session setup
│ │ ├── params.ts # RunEmbeddedPiAgentParams type
│ │ ├── payloads.ts # Build response payloads from run results
│ │ ├── images.ts # Vision model image injection
│ │ └── types.ts # EmbeddedRunAttemptResult
│ ├── abort.ts # Abort error detection
│ ├── cache-ttl.ts # Cache TTL tracking for context pruning
│ ├── compact.ts # Manual/auto compaction logic
│ ├── extensions.ts # Load pi extensions for embedded runs
│ ├── extra-params.ts # Provider-specific stream params
│ ├── google.ts # Google/Gemini turn ordering fixes
│ ├── history.ts # History limiting (DM vs group)
│ ├── lanes.ts # Session/global command lanes
│ ├── logger.ts # Subsystem logger
│ ├── model.ts # Model resolution via ModelRegistry
│ ├── runs.ts # Active run tracking, abort, queue
│ ├── sandbox-info.ts # Sandbox info for system prompt
│ ├── session-manager-cache.ts # SessionManager instance caching
│ ├── session-manager-init.ts # Session file initialization
│ ├── system-prompt.ts # System prompt builder
│ ├── tool-split.ts # Split tools into builtIn vs custom
│ ├── types.ts # EmbeddedPiAgentMeta, EmbeddedPiRunResult
│ └── utils.ts # ThinkLevel mapping, error description
├── pi-embedded-subscribe.ts # Session event subscription/dispatch
├── pi-embedded-subscribe.types.ts # SubscribeEmbeddedPiSessionParams
├── pi-embedded-subscribe.handlers.ts # Event handler factory
├── pi-embedded-subscribe.handlers.lifecycle.ts
├── pi-embedded-subscribe.handlers.types.ts
├── pi-embedded-block-chunker.ts # Streaming block reply chunking
├── pi-embedded-messaging.ts # Messaging tool sent tracking
├── pi-embedded-helpers.ts # Error classification, turn validation
├── pi-embedded-helpers/ # Helper modules
├── pi-embedded-utils.ts # Formatting utilities
├── pi-tools.ts # createOpenClawCodingTools()
├── pi-tools.abort.ts # AbortSignal wrapping for tools
├── pi-tools.policy.ts # Tool allowlist/denylist policy
├── pi-tools.read.ts # Read tool customizations
├── pi-tools.schema.ts # Tool schema normalization
├── pi-tools.types.ts # AnyAgentTool type alias
├── pi-tool-definition-adapter.ts # AgentTool -> ToolDefinition adapter
├── pi-settings.ts # Settings overrides
├── pi-hooks/ # Custom pi hooks
│ ├── compaction-safeguard.ts # Safeguard extension
│ ├── compaction-safeguard-runtime.ts
│ ├── context-pruning.ts # Cache-TTL context pruning extension
│ └── context-pruning/
├── model-auth.ts # Auth profile resolution
├── auth-profiles.ts # Profile store, cooldown, failover
├── model-selection.ts # Default model resolution
├── models-config.ts # models.json generation
├── model-catalog.ts # Model catalog cache
├── context-window-guard.ts # Context window validation
├── failover-error.ts # FailoverError class
├── defaults.ts # DEFAULT_PROVIDER, DEFAULT_MODEL
├── system-prompt.ts # buildAgentSystemPrompt()
├── system-prompt-params.ts # System prompt parameter resolution
├── system-prompt-report.ts # Debug report generation
├── tool-summaries.ts # Tool description summaries
├── tool-policy.ts # Tool policy resolution
├── transcript-policy.ts # Transcript validation policy
├── skills.ts # Skill snapshot/prompt building
├── skills/ # Skill subsystem
├── sandbox.ts # Sandbox context resolution
├── sandbox/ # Sandbox subsystem
├── channel-tools.ts # Channel-specific tool injection
├── openclaw-tools.ts # OpenClaw-specific tools
├── bash-tools.ts # exec/process tools
├── apply-patch.ts # apply_patch tool (OpenAI)
├── tools/ # Individual tool implementations
│ ├── browser-tool.ts
│ ├── canvas-tool.ts
│ ├── cron-tool.ts
│ ├── gateway-tool.ts
│ ├── image-tool.ts
│ ├── message-tool.ts
│ ├── nodes-tool.ts
│ ├── session*.ts
│ ├── web-*.ts
│ └── ...
└── ...

Channel-specific message action runtimes now live in the plugin-owned extension directories instead of under src/agents/tools. For example, you will find the action runtime files for Discord, Slack, Telegram, and WhatsApp in their respective plugin folders.

Integrating the agent involves a few key steps, starting from running the embedded agent to handling events and prompts. You can follow this flow to set up the OpenClaw AI capabilities in your own environment.

  1. Running an Embedded Agent The main entry point is runEmbeddedPiAgent() in pi-embedded-runner/run.ts:
import { runEmbeddedPiAgent } from "./agents/pi-embedded-runner.js";
const result = await runEmbeddedPiAgent({
sessionId: "user-123",
sessionKey: "main:whatsapp:+1234567890",
sessionFile: "/path/to/session.jsonl",
workspaceDir: "/path/to/workspace",
config: openclawConfig,
prompt: "Hello, how are you?",
provider: "anthropic",
model: "claude-sonnet-4-6",
timeoutMs: 120_000,
runId: "run-abc",
onBlockReply: async (payload) => {
await sendToChannel(payload.text, payload.mediaUrls);
},
});
  1. Session Creation Inside runEmbeddedAttempt(), the pi SDK is used to set up the environment:
import {
createAgentSession,
DefaultResourceLoader,
SessionManager,
SettingsManager,
} from "@mariozechner/pi-coding-agent";
const resourceLoader = new DefaultResourceLoader({
cwd: resolvedWorkspace,
agentDir,
settingsManager,
additionalExtensionPaths,
});
await resourceLoader.reload();
const { session } = await createAgentSession({
cwd: resolvedWorkspace,
agentDir,
authStorage: params.authStorage,
modelRegistry: params.modelRegistry,
model: params.model,
thinkingLevel: mapThinkingLevel(params.thinkLevel),
tools: builtInTools,
customTools: allCustomTools,
sessionManager,
settingsManager,
resourceLoader,
});
applySystemPromptOverrideToSession(session, systemPromptOverride);
  1. Event Subscription subscribeEmbeddedPiSession() subscribes to pi’s AgentSession events to handle streaming and tool results:
const subscription = subscribeEmbeddedPiSession({
session: activeSession,
runId: params.runId,
verboseLevel: params.verboseLevel,
reasoningMode: params.reasoningLevel,
toolResultFormat: params.toolResultFormat,
onToolResult: params.onToolResult,
onReasoningStream: params.onReasoningStream,
onBlockReply: params.onBlockReply,
onPartialReply: params.onPartialReply,
onAgentEvent: params.onAgentEvent,
});

Events handled include:

  1. message_start / message_end / message_update (streaming text/thinking).

  2. tool_execution_start / tool_execution_update / tool_execution_end.

  3. turn_start / turn_end.

  4. agent_start / agent_end.

  5. compaction_start / compaction_end.

  6. Prompting After setup, you prompt the session to start the agent loop:

await session.prompt(effectivePrompt, { images: imageResult.images });

The SDK handles the full agent loop, including sending to the LLM, executing tool calls, and streaming responses. Image injection is prompt-local; OpenClaw loads image refs from the current prompt and passes them via images for that turn only. It does not re-scan older history turns to re-inject image payloads.

Understanding how OpenClaw handles tools helps you build more capable agents. It uses a structured pipeline to manage everything from basic file operations to complex messaging actions across different platforms.

  1. Base Tools: These include the standard coding tools like read, bash, edit, and write.
  2. Custom Replacements: OpenClaw swaps out the standard bash tool for exec or process and adapts file tools for sandbox environments.
  3. OpenClaw Tools: You get access to a wide range of features like messaging, browser interaction, canvas, sessions, cron jobs, and the Gateway.
  4. Channel Tools: These are specific actions designed for platforms like Discord, Telegram, Slack, or WhatsApp.
  5. Policy Filtering: The system filters tools based on your profile, provider, agent settings, and sandbox policies.
  6. Schema Normalization: Tool schemas are cleaned up to handle specific quirks in models like Gemini or OpenAI.
  7. AbortSignal Wrapping: All tools are wrapped to make sure they respect abort signals when a request is cancelled.

The AgentTool in pi-agent-core has a different execution signature than the ToolDefinition used in the coding agent. You can bridge this gap using the adapter found in pi-tool-definition-adapter.ts.

export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] {
return tools.map((tool) => ({
name: tool.name,
label: tool.label ?? name,
description: tool.description ?? "",
parameters: tool.parameters,
execute: async (toolCallId, params, onUpdate, _ctx, signal) => {
// pi-coding-agent signature differs from pi-agent-core
return await tool.execute(toolCallId, params, signal, onUpdate);
},
}));
}

When you use splitSdkTools(), the system passes all your tools through the customTools property. This ensures that OpenClaw keeps its policy filtering and sandbox integration consistent no matter which provider you use.

export function splitSdkTools(options: { tools: AnyAgentTool[]; sandboxEnabled: boolean }) {
return {
builtInTools: [], // Empty. We override everything
customTools: toToolDefinitions(options.tools),
};
}

The system prompt acts as the core instruction set for your agent, and OpenClaw builds it dynamically using buildAgentSystemPrompt(). It combines various sections to give the agent full context of its capabilities and environment.

The final prompt includes details on tooling, safety guardrails, OpenClaw CLI references, and workspace settings. It also covers messaging preferences, voice settings, and runtime metadata. If you have memory or reactions enabled, those are added too. For smaller subagents, the system trims these sections down to keep the prompt minimal.

Once a session is created, you apply the prompt using applySystemPromptOverrideToSession():

const systemPromptOverride = createSystemPromptOverride(appendPrompt);
applySystemPromptOverrideToSession(session, systemPromptOverride);

Efficient session management is key to keeping your agent’s conversations organized and responsive. OpenClaw uses JSONL files to store conversation trees and implements caching to keep things fast.

Your sessions are saved as JSONL files where messages are linked by ID and parent ID. While the standard SessionManager handles saving and loading, OpenClaw adds a layer of safety with guardSessionManager() to protect tool results.

const sessionManager = SessionManager.open(params.sessionFile);

To avoid parsing the same files repeatedly, the session-manager-cache.ts utility keeps active sessions in memory. This speeds up access when your agent is handling multiple requests.

await prewarmSessionFile(params.sessionFile);
sessionManager = SessionManager.open(params.sessionFile);
trackSessionManagerAccess(params.sessionFile);

The system also uses limitHistoryTurns() to trim the conversation based on the channel type, such as a direct message or a group chat. If the context window gets too full, auto-compaction triggers. You might see errors like context length exceeded or request_too_large when this is needed. You can also manually trigger this process using compactEmbeddedPiSessionDirect().

const compactResult = await compactEmbeddedPiSessionDirect({
sessionId, sessionFile, provider, model, ...
});

Managing API keys and model selection can be complex, but OpenClaw simplifies this with an auth profile store. This allows you to manage multiple keys and automatically handle failures.

The system maintains a store of auth profiles and determines the best order to use them based on your configuration. This helps you rotate through keys if one hits a limit or fails.

const authStore = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
const profileOrder = resolveAuthProfileOrder({ cfg, store: authStore, provider, preferredProfile });

If a profile fails, OpenClaw marks the failure and moves to the next available profile while tracking a cooldown period.

await markAuthProfileFailure({ store, profileId, reason, cfg, agentDir });
const rotated = await advanceAuthProfile();

Model resolution is handled by checking the provider and model ID against the registry and your local config. This ensures the correct API key is set for the runtime.

import { resolveModel } from "./pi-embedded-runner/model.js";
const { model, error, authStorage, modelRegistry } = resolveModel(
provider,
modelId,
agentDir,
config,
);
// Uses pi's ModelRegistry and AuthStorage
authStorage.setRuntimeApiKey(model.provider, apiKeyInfo.apiKey);

If a model fails to respond correctly, the FailoverError can trigger a fallback to a different model if you have that configured. This keeps your agent running even during provider outages.

if (fallbackConfigured && isFailoverErrorMessage(errorText)) {
throw new FailoverError(errorText, {
reason: promptFailoverReason ?? "unknown",
provider,
model: modelId,
profileId,
status: resolveFailoverStatus(promptFailoverReason),
});
}

You can use OpenClaw Pi Extensions to inject specialized logic into your agents, allowing for more control over how history and context are handled. These hooks provide a clean way to extend the core functionality of your OpenClaw setup without modifying the base engine.

The Compaction Safeguard located at src/agents/pi-hooks/compaction-safeguard.ts adds essential guardrails to the history compaction process. It uses adaptive token budgeting and provides summaries for tool failures and file operations to ensure your agent doesn’t lose track of important context.

if (resolveCompactionMode(params.cfg) === "safeguard") {
setCompactionSafeguardRuntime(params.sessionManager, { maxHistoryShare });
paths.push(resolvePiExtensionPath("compaction-safeguard"));
}

To manage your context window more effectively, you can use the Context Pruning extension found in src/agents/pi-hooks/context-pruning.ts. This logic implements pruning based on cache-TTL, which helps keep your sessions lean by removing older data that hasn’t been touched recently.

if (cfg?.agents?.defaults?.contextPruning?.mode === "cache-ttl") {
setContextPruningRuntime(params.sessionManager, {
settings,
contextWindowTokens,
isToolPrunable,
lastCacheTouchAt,
});
paths.push(resolvePiExtensionPath("context-pruning"));
}

Managing real-time output in OpenClaw requires precise handling of data chunks to ensure a smooth user experience. You can use these built-in tools to manage how streaming text is processed, cleaned, and delivered to your front-end or Gateway.

The EmbeddedBlockChunker is the primary tool for managing how streaming text is broken down into discrete reply blocks. This allows you to handle large outputs in smaller, more manageable pieces rather than waiting for the entire response to finish.

const blockChunker = blockChunking ? new EmbeddedBlockChunker(blockChunking) : null;

When working with models that output internal reasoning, you often need to strip out <think> or <thinking> blocks before showing the result to the user. OpenClaw processes the streaming output to remove these tags and extract only the content wrapped in <final> tags if necessary.

const stripBlockTags = (text: string, state: { thinking: boolean; final: boolean }) => {
// Strip <think>...</think> content
// If enforceFinalTag, only return <final>...</final> content
};

You can embed specific instructions directly in the model’s output using Reply Directives like [[media:url]], [[voice]], or [[reply:id]]. The system automatically parses these strings and extracts them into a clean JSON format so your application can trigger the right actions.

const { text: cleanedText, mediaUrls, audioAsVoice, replyToId } = consumeReplyDirectives(chunk);

Managing OpenClaw error handling effectively is a core part of building a reliable system. You need to know if a failure happened because of a simple timeout or a more complex authentication issue to keep your application running smoothly.

The pi-embedded-helpers.ts file is your main tool for sorting through different error types. It allows you to categorize issues so your application can react appropriately to specific technical failures.

  1. You can check for context overflows, authentication failures, or rate limits using these specific functions.
  2. Use the classification helper to determine if a failover is necessary based on the error text provided by the API.
isContextOverflowError(errorText) // Context too large
isCompactionFailureError(errorText) // Compaction failed
isAuthAssistantError(lastAssistant) // Auth failure
isRateLimitAssistantError(...) // Rate limited
isFailoverAssistantError(...) // Should failover
classifyFailoverReason(errorText) // "auth" | "rate_limit" | "quota" | "timeout" | ...

Sometimes a specific thinking level just isn’t supported by the provider you’re using. OpenClaw handles this by automatically dropping back to a compatible level so your process doesn’t just crash.

  1. The pickFallbackThinkingLevel function checks the error message against your attempted level to find a suitable alternative.
  2. If a fallback is found, the logic updates the thinkLevel and continues the loop without manual intervention.
const fallbackThinking = pickFallbackThinkingLevel({
message: errorText,
attempted: attemptedThinking,
});
if (fallbackThinking) {
thinkLevel = fallbackThinking;
continue;
}

When you want to run code or handle files securely, OpenClaw provides a sandbox mode that keeps everything isolated. This ensures that tools and file paths are strictly constrained to a safe environment to prevent accidental system changes.

  1. You start by resolving the sandbox context using your configuration and a unique session key.
  2. The system then maps the workspace directory to a secure root to prevent access to sensitive system files.
  3. All file edits and reads happen through sandboxed tools, and any execution commands run inside a Docker container.
  4. Browser interactions are routed through a bridge URL to maintain isolation and security.
const sandbox = await resolveSandboxContext({
config: params.config,
sessionKey: sandboxSessionKey,
workspaceDir: resolvedWorkspace,
});
if (sandboxRoot) {
// Use sandboxed read/edit/write tools
// Exec runs in container
// Browser uses bridge URL
}

Handle Provider-Specific Logic in OpenClaw

Section titled “Handle Provider-Specific Logic in OpenClaw”

When you are building with OpenClaw, you will find that Provider-Specific Handling is built right into the core to manage how different models behave. This ensures that your integration stays stable regardless of whether you are using Anthropic, Google, or OpenAI.

  1. For Anthropic, the system performs refusal magic string scrubbing and turn validation for consecutive roles. It also enforces strict upstream Pi tool parameter validation to keep your data consistent.
  2. For Google/Gemini, you get automatic plugin-owned tool schema sanitization to ensure everything works with the API.
  3. For OpenAI, the logic includes the apply_patch tool for Codex models and manages thinking level downgrade handling when the model requires it.

OpenClaw also includes a local TUI mode that lets you interact with the system directly from your terminal. This CLI experience is powered by pi-tui components to give you a responsive and familiar interface.

  1. The TUI mode uses pi-tui components directly to build the interface within your terminal.
  2. This setup provides an interactive terminal experience that is very similar to the native pi mode.
src/tui/tui.ts
import { ... } from "@mariozechner/pi-tui";

If you are used to working with the standard Pi CLI, you will find that OpenClaw offers a more integrated way to build your agents. While the CLI is great for quick tasks, the OpenClaw embedded version lets you bake the logic directly into your own applications using the SDK.

AspectPi CLIOpenClaw Embedded
Invocationpi command / RPCSDK via createAgentSession()
ToolsDefault coding toolsCustom OpenClaw tool suite
System promptAGENTS.md + promptsflexible per-channel/context
Session storage~/.pi/agent/sessions/~/.openclaw/agents/<agentId>/sessions/ (or $OPENCLAW_STATE_DIR/agents/<agentId>/sessions/)
AuthSingle credentialMulti-profile with rotation
ExtensionsLoaded from diskProgrammatic + disk paths
Event handlingTUI renderingCallback-based (onBlockReply, etc.)

We are always thinking about how to make the developer experience better and more efficient. There are a few specific parts of the code that we might change to make the OpenClaw integration even more direct.

  1. Tool signature alignment: We are currently working on adapting between pi-agent-core and pi-coding-agent signatures.
  2. Session manager wrapping: The guardSessionManager adds safety but it also makes the logic more complex.
  3. Extension loading: We could use the Pi ResourceLoader more directly in future versions.
  4. Streaming handler complexity: The subscribeEmbeddedPiSession function has become quite large and might need a cleanup.
  5. Provider quirks: There are many provider-specific paths that Pi could handle instead of us doing it manually.

Testing is the best way to ensure your OpenClaw setup is solid and ready for production. Our test suites cover everything from JSON handling to specific API interactions.

  1. src/agents/pi-*.test.ts
  2. src/agents/pi-auth-json.test.ts
  3. src/agents/pi-embedded-*.test.ts
  4. src/agents/pi-embedded-helpers*.test.ts
  5. src/agents/pi-embedded-runner*.test.ts
  6. src/agents/pi-embedded-runner/**/*.test.ts
  7. src/agents/pi-embedded-subscribe*.test.ts
  8. src/agents/pi-tools*.test.ts
  9. src/agents/pi-tool-definition-adapter*.test.ts
  10. src/agents/pi-settings.test.ts
  11. src/agents/pi-hooks/**/*.test.ts

If you want to run the live tests, you can use this specific suite:

  1. src/agents/pi-embedded-runner-extraparams.live.test.ts (enable OPENCLAW_LIVE_TEST=1)

For more details on how we handle the development cycle, check out the Pi Development Workflow guide.

OpenClaw

OpenClaw Expert

Still stuck?

If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.