Laravel package for running pi.dev coding agents via RPC

Maintainers

Package info

github.com/filippotoso/pi

pkg:composer/filippo-toso/pi

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-04-06 20:35 UTC

This package is auto-updated.

Last update: 2026-04-06 20:37:34 UTC


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
  • pi CLI 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