Laravel package for running pi.dev coding agents via RPC
v1.0.0
2026-04-06 20:35 UTC
Requires
- php: ^8.2
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^11.0
README
A Laravel package for running pi coding agents via RPC. Spawn headless pi agent subprocesses from your Laravel app, send prompts, stream responses, execute tools, and manage sessions — all through a clean PHP API.
Requirements
- PHP 8.2+
- Laravel 11 or 12
piCLI installed and accessible from$PATH(or configure a custom binary path)
Installation
composer require filippo-toso/pi
Publish the config file:
php artisan vendor:publish --tag=pi-config
Configuration
Set these in your .env:
PI_BINARY=pi # Path to pi binary (default: "pi" from $PATH) PI_PROVIDER=anthropic # Default LLM provider PI_MODEL= # Default model pattern (e.g. "anthropic/claude-sonnet-4-20250514") PI_SESSION=false # Persist sessions to disk PI_SESSION_DIR= # Custom session storage directory PI_WORKING_DIR= # Working directory for pi (default: Laravel base_path()) PI_TIMEOUT= # Response timeout in seconds (null = no timeout)
Or edit config/pi.php directly.
Quick Start
Simple Ask (Blocking)
use FilippoToso\Pi\Facades\Pi; $agent = Pi::create(); $answer = $agent->ask('What is the capital of France?'); echo $answer; // "The capital of France is Paris." $agent->close();
Streaming Responses
$agent = Pi::create(); $agent->prompt('Write a haiku about Laravel'); foreach ($agent->stream() as $event) { if ($text = $event->getTextDelta()) { echo $text; } } $agent->close();
Streaming with Callbacks
$agent = Pi::create(); $agent->prompt('Refactor the User model'); $fullText = $agent->streamWith( onText: fn(string $delta) => echo $delta, onThinking: fn(string $delta) => logger()->debug("Thinking: $delta"), onToolStart: fn(array $data) => logger()->info("Tool started: {$data['toolName']}"), onToolEnd: fn(array $data) => logger()->info("Tool finished: {$data['toolName']}"), onEvent: fn($event) => null, // access every raw event ); $agent->close();
Custom Agent Options
$agent = Pi::create([ 'provider' => 'openai', 'model' => 'o3', 'working_dir' => '/path/to/project', 'timeout' => 120, 'session' => true, 'session_dir' => storage_path('pi-sessions'), 'env' => [ 'OPENAI_API_KEY' => config('services.openai.key'), ], ]);
API Reference
Creating Agents
// Via facade $agent = Pi::create(); $agent = Pi::create(['provider' => 'anthropic', 'model' => 'claude-sonnet-4-20250514']); // Via dependency injection public function __construct(private PiAgentFactory $factory) {} $agent = $this->factory->create();
Prompting
// Send a prompt (non-blocking, use stream() to consume events) $agent->prompt('Hello!'); // Send with images $agent->prompt('What is in this image?', images: [ ['type' => 'image', 'data' => base64_encode($imageData), 'mimeType' => 'image/png'], ]); // Send while agent is already streaming $agent->prompt('New instruction', streamingBehavior: 'steer'); $agent->prompt('After you finish...', streamingBehavior: 'followUp'); // Simple blocking ask $text = $agent->ask('What is 2+2?'); // Steering (while agent is running) $agent->steer('Focus on error handling instead'); // Follow-up (after agent finishes) $agent->followUp('Now summarize what you did'); // Abort current operation $agent->abort();
Streaming Events
$agent->prompt('Do something'); foreach ($agent->stream() as $event) { match ($event->type) { 'message_update' => handleDelta($event->getTextDelta()), 'tool_execution_start' => handleToolStart($event->data), 'tool_execution_end' => handleToolEnd($event->data), 'agent_end' => handleComplete($event->getMessages()), default => null, }; }
Model Management
$agent->setModel('anthropic', 'claude-sonnet-4-20250514'); $agent->cycleModel(); $models = $agent->getAvailableModels();
Thinking Level
$agent->setThinkingLevel('high'); // off, minimal, low, medium, high, xhigh $agent->cycleThinkingLevel();
Bash Execution
// Execute a command (output included in next LLM prompt) $result = $agent->bash('ls -la'); echo $result['output']; echo $result['exitCode']; // Abort a running command $agent->abortBash();
Session Management
$state = $agent->getState(); $stats = $agent->getSessionStats(); $messages = $agent->getMessages(); $agent->newSession(); $agent->switchSession('/path/to/session.jsonl'); $agent->setSessionName('my-feature-work'); // Forking $forkable = $agent->getForkMessages(); $agent->fork($forkable[0]['entryId']); // Export $htmlPath = $agent->exportHtml('/tmp/session.html'); // Last assistant text $lastText = $agent->getLastAssistantText();
Compaction
$agent->compact(); $agent->compact(customInstructions: 'Focus on code changes'); $agent->setAutoCompaction(true);
Retry
$agent->setAutoRetry(true); $agent->abortRetry();
Queue Modes
// Steering: deliver all queued messages at once, or one per turn $agent->setSteeringMode('all'); // or 'one-at-a-time' $agent->setFollowUpMode('one-at-a-time'); // or 'all'
Commands
$commands = $agent->getCommands(); // Returns: [['name' => 'fix-tests', 'source' => 'prompt', ...], ...] // Invoke a command via prompt $agent->prompt('/fix-tests');
Extension UI Protocol
// When streaming, handle extension UI requests foreach ($agent->stream() as $event) { if ($event->isExtensionUiRequest()) { $method = $event->data['method']; $id = $event->data['id']; match ($method) { 'select' => $agent->respondToExtensionUi($id, ['value' => 'Allow']), 'confirm' => $agent->respondToExtensionUi($id, ['confirmed' => true]), 'input' => $agent->respondToExtensionUi($id, ['value' => 'user input']), 'notify' => null, // fire-and-forget, no response needed default => $agent->respondToExtensionUi($id, ['cancelled' => true]), }; } }
Low-Level Access
// Send any raw command $response = $agent->sendCommand(['type' => 'get_state']); // Fire-and-forget (no response waiting) $agent->sendRaw(['type' => 'abort']); // Read a single event $event = $agent->readEvent(timeout: 5.0); // Access the underlying connection $connection = $agent->getConnection(); $stderrOutput = $connection->readStderr();
Lifecycle
$agent->isRunning(); // Check if the pi process is alive $agent->close(); // Terminate the subprocess (also called on __destruct)
Event Types
Events received during streaming:
| Event | Description |
|---|---|
agent_start |
Agent begins processing |
agent_end |
Agent completes (includes all generated messages) |
turn_start |
New turn begins |
turn_end |
Turn completes |
message_start |
Message begins |
message_update |
Streaming delta (text/thinking/tool call) |
message_end |
Message completes |
tool_execution_start |
Tool begins |
tool_execution_update |
Tool progress |
tool_execution_end |
Tool completes |
queue_update |
Pending queue changed |
compaction_start |
Compaction begins |
compaction_end |
Compaction completes |
auto_retry_start |
Retry begins |
auto_retry_end |
Retry completes |
extension_error |
Extension error |
extension_ui_request |
Extension UI interaction needed |
RpcEvent Helpers
$event->type; // Event type string $event->isResponse(); // Is this a command response? $event->isExtensionUiRequest(); // Is this an extension UI request? $event->getTextDelta(); // Get text delta string (or null) $event->getDelta(); // Get full assistantMessageEvent array $event->getMessage(); // Get message from turn_end/message_start/message_end $event->getMessages(); // Get messages from agent_end $event->toArray(); // Get raw data array
Error Handling
use FilippoToso\Pi\Exceptions\PiException; use FilippoToso\Pi\Exceptions\PiCommandException; use FilippoToso\Pi\Exceptions\PiConnectionException; try { $agent = Pi::create(); $answer = $agent->ask('Hello'); } catch (PiConnectionException $e) { // Process failed to start, pipe broken, timeout } catch (PiCommandException $e) { // Command returned success: false echo $e->command; // The command type that failed } catch (PiException $e) { // Base exception for all pi errors }
License
MIT