Skip to content

Code Agents

The AgentCtrl facade provides a unified interface for invoking CLI-based code agents that can execute code, modify files, and perform complex multi-step tasks. Each agent runs as an external process, and the facade handles process management, output parsing, and response structuring.

Setup

Before using AgentCtrl, install the CLI agent you want to run and make sure its executable is available in PATH. The Laravel package does not install or authenticate these tools for you.

Agent Required binary Setup note
Claude Code claude Install Claude Code separately and sign in with its normal workflow
Codex codex Install the Codex CLI and ensure codex resolves on the server running Laravel
OpenCode opencode Install OpenCode and ensure opencode resolves on the server running Laravel

After the binary is available, configure Laravel defaults in config/instructor.php under agents for timeout, working directory, sandbox driver, and per-agent model overrides.

Supported Agents

Agent Description Use Case
Claude Code Anthropic's Claude agent with code execution General coding tasks, refactoring, file modifications
Codex OpenAI's Codex agent Code generation and completion
OpenCode Multi-model code agent Research and coding with model flexibility

Quick Start

use Cognesy\AgentCtrl\Config\AgentCtrlConfig;
use Cognesy\Instructor\Laravel\Facades\AgentCtrl;

// Execute a task with Claude Code
$response = AgentCtrl::claudeCode()
    ->withConfig(new AgentCtrlConfig(
        timeout: 300,
        workingDirectory: base_path(),
    ))
    ->execute('Generate a Laravel migration for a users table with name, email, and password fields');

// Check if successful
if ($response->isSuccess()) {
    echo $response->text();
}

Agent Selection

Claude Code

Best for general coding tasks with Anthropic's Claude models. Supports sandbox isolation, session resumption, and streaming output.

use Cognesy\Instructor\Laravel\Facades\AgentCtrl;

$response = AgentCtrl::claudeCode()
    ->withModel('claude-opus-4-5')
    ->inDirectory(base_path())
    ->withTimeout(300)
    ->execute('Refactor the User model to use DTOs');

echo $response->text();
echo "Session ID: " . (string) ($response->sessionId() ?? '');

Codex

Best for OpenAI-powered code generation.

$response = AgentCtrl::codex()
    ->withModel('codex')
    ->execute('Write unit tests for the UserService class');

OpenCode

Best for multi-model flexibility. Specify the model using the provider/model format.

$response = AgentCtrl::openCode()
    ->withModel('anthropic/claude-sonnet-4-5')
    ->execute('Analyze the codebase architecture');

Dynamic Selection

Select agent type at runtime based on configuration or business logic.

use Cognesy\AgentCtrl\Enum\AgentType;

$agentType = AgentType::from(config('app.default_agent'));

$response = AgentCtrl::make($agentType)
    ->execute('Generate API documentation');

Configuration

Builder Methods

All agents support the same set of builder methods for configuration. Use withConfig() when you want one typed object for the shared options, then layer agent-specific methods on top as needed. Builder methods override any defaults set in the Laravel config file.

use Cognesy\AgentCtrl\Config\AgentCtrlConfig;
use Cognesy\Sandbox\Enums\SandboxDriver;

AgentCtrl::claudeCode()
    ->withConfig(new AgentCtrlConfig(
        model: 'claude-opus-4-5',
        timeout: 300,
        workingDirectory: '/path/to/project',
        sandboxDriver: SandboxDriver::Host,
    ))
    ->execute('Your prompt');

AgentCtrlConfig::fromArray() also accepts the Laravel config-style keys used in config/instructor.php, so directory and sandbox are mapped automatically.

Laravel Configuration

Configure defaults in config/instructor.php. The facade automatically reads these values and applies them when you create a builder. Builder methods then override any defaults for that specific call.

'agents' => [
    // Default timeout for all agents
    'timeout' => env('INSTRUCTOR_AGENT_TIMEOUT', 300),

    // Default working directory
    'directory' => env('INSTRUCTOR_AGENT_DIRECTORY'),

    // Default sandbox driver: host, docker, podman, firejail, bubblewrap
    'sandbox' => env('INSTRUCTOR_AGENT_SANDBOX', 'host'),

    // Claude Code specific
    'claude_code' => [
        'model' => env('CLAUDE_CODE_MODEL', 'claude-sonnet-4-20250514'),
        'timeout' => env('CLAUDE_CODE_TIMEOUT'),
        'directory' => env('CLAUDE_CODE_DIRECTORY'),
        'sandbox' => env('CLAUDE_CODE_SANDBOX'),
    ],

    // Codex specific
    'codex' => [
        'model' => env('CODEX_MODEL', 'codex'),
        'timeout' => env('CODEX_TIMEOUT'),
        'directory' => env('CODEX_DIRECTORY'),
        'sandbox' => env('CODEX_SANDBOX'),
    ],

    // OpenCode specific
    'opencode' => [
        'model' => env('OPENCODE_MODEL', 'anthropic/claude-sonnet-4-20250514'),
        'timeout' => env('OPENCODE_TIMEOUT'),
        'directory' => env('OPENCODE_DIRECTORY'),
        'sandbox' => env('OPENCODE_SANDBOX'),
    ],
],

Agent-specific settings (e.g., claude_code.timeout) take precedence over the global defaults (e.g., agents.timeout).

Environment Variables

# Default agent settings
INSTRUCTOR_AGENT_TIMEOUT=300
INSTRUCTOR_AGENT_DIRECTORY=/path/to/project
INSTRUCTOR_AGENT_SANDBOX=host

# Claude Code
CLAUDE_CODE_MODEL=claude-opus-4-5

# Codex
CODEX_MODEL=codex

# OpenCode
OPENCODE_MODEL=anthropic/claude-sonnet-4-20250514

Streaming

Process output in real-time with streaming callbacks. The three callback types fire at different points during execution.

$response = AgentCtrl::claudeCode()
    ->onText(function (string $text) {
        // Called as text is generated -- use for live output
        echo $text;
    })
    ->onToolUse(function (string $tool, array $input, ?string $output) {
        // Called when agent uses a tool (file read, shell command, etc.)
        echo "Tool: $tool\n";
        echo "Input: " . json_encode($input) . "\n";
    })
    ->onComplete(function (AgentResponse $response) {
        // Called once when execution finishes
        echo "\nDone! Exit code: " . $response->exitCode;
    })
    ->executeStreaming('Generate a REST API for products');

Response Object

The AgentResponse object contains the agent's output along with metadata about the execution.

$response = AgentCtrl::claudeCode()->execute('...');

// Main content
$response->text();           // string -- Generated text output
$response->isSuccess();      // bool -- True if exitCode is 0

// Metadata
$response->exitCode;         // int -- Process exit code
$response->sessionId();      // AgentSessionId|null -- Session ID for resuming
$response->agentType;        // AgentType -- Which agent was used

// Usage (when available)
$response->usage;            // ?TokenUsage -- Token statistics
$response->usage->input;     // int -- Input tokens
$response->usage->output;    // int -- Output tokens
$response->usage->total();   // int -- Total tokens

// Cost (when available)
$response->cost;             // ?float -- Cost in USD

// Tool calls
$response->toolCalls;        // array<ToolCall> -- Tools used during execution
foreach ($response->toolCalls as $call) {
    $call->tool;             // string -- Tool name
    $call->input;            // array -- Tool input parameters
    $call->output;           // ?string -- Tool output
    $call->isError;          // bool -- Whether the tool call failed
}

Session Management

Resume previous sessions for continued work. This is useful for multi-turn interactions where the agent needs context from a prior execution.

// First execution
$response = AgentCtrl::claudeCode()
    ->execute('Start refactoring the User model');

$sessionId = (string) ($response->sessionId() ?? '');

// Later: Resume the session with full context from the previous run
$response = AgentCtrl::claudeCode()
    ->resumeSession($sessionId)
    ->execute('Continue with the Address model');

Error Handling

Always check isSuccess() and handle failures gracefully. Agent executions can fail due to timeouts, sandbox errors, or issues in the generated code.

use Cognesy\Instructor\Laravel\Facades\AgentCtrl;

try {
    $response = AgentCtrl::claudeCode()
        ->withTimeout(60)
        ->execute($prompt);

    if (!$response->isSuccess()) {
        // Non-zero exit code
        Log::error('Agent failed', [
            'exit_code' => $response->exitCode,
            'output' => $response->text(),
        ]);
        return null;
    }

    // Check for tool errors
    foreach ($response->toolCalls as $call) {
        if ($call->isError) {
            Log::warning('Tool error', [
                'tool' => $call->tool,
                'error' => $call->output,
            ]);
        }
    }

    return $response->text();

} catch (\Throwable $e) {
    // Timeout, sandbox error, etc.
    Log::error('Agent exception', ['error' => $e->getMessage()]);
    return null;
}

Testing

Use AgentCtrl::fake() for testing without actual agent execution. See the Testing guide for full documentation of AgentCtrlFake.

use Cognesy\Instructor\Laravel\Facades\AgentCtrl;

test('generates migration code', function () {
    // Setup fake with expected responses
    $fake = AgentCtrl::fake([
        'Generated migration file: 2024_01_01_create_users_table.php',
    ]);

    // Execute code under test
    $result = app(MigrationGenerator::class)->generate('users');

    // Assertions
    $fake->assertExecuted();
    $fake->assertExecutedTimes(1);
    $fake->assertUsedClaudeCode();
    $fake->assertExecutedWith('migration');

    expect($result)->toContain('users_table');
});

Real-World Examples

Code Generation Service

namespace App\Services;

use Cognesy\Instructor\Laravel\Facades\AgentCtrl;

class CodeGenerationService
{
    public function generateMigration(array $schema): string
    {
        $prompt = $this->buildMigrationPrompt($schema);

        $response = AgentCtrl::claudeCode()
            ->inDirectory(database_path('migrations'))
            ->execute($prompt);

        if (!$response->isSuccess()) {
            throw new \RuntimeException('Failed to generate migration');
        }

        return $response->text();
    }

    public function generateTest(string $className): string
    {
        $response = AgentCtrl::claudeCode()
            ->inDirectory(base_path('tests'))
            ->execute("Generate comprehensive tests for: $className");

        return $response->text();
    }

    private function buildMigrationPrompt(array $schema): string
    {
        return "Generate a Laravel migration for:\n" . json_encode($schema, JSON_PRETTY_PRINT);
    }
}

Queued Code Generation

For long-running agent tasks, dispatch them to a queue so the user does not have to wait.

namespace App\Jobs;

use Cognesy\Instructor\Laravel\Facades\AgentCtrl;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;

class GenerateCodeJob implements ShouldQueue
{
    use Queueable;

    public function __construct(
        public string $prompt,
        public string $outputPath,
    ) {}

    public function handle(): void
    {
        $response = AgentCtrl::claudeCode()
            ->withTimeout(600) // 10 minutes for complex tasks
            ->inDirectory(dirname($this->outputPath))
            ->execute($this->prompt);

        if ($response->isSuccess()) {
            file_put_contents($this->outputPath, $response->text());
        }
    }
}

// Usage
GenerateCodeJob::dispatch(
    'Generate a complete CRUD controller for Products',
    app_path('Http/Controllers/ProductController.php')
);

Interactive Code Review

use Cognesy\Instructor\Laravel\Facades\AgentCtrl;

class CodeReviewer
{
    public function review(string $filePath, callable $onProgress = null): array
    {
        $builder = AgentCtrl::claudeCode()
            ->inDirectory(dirname($filePath));

        if ($onProgress) {
            $builder->onText($onProgress);
        }

        $response = $builder->execute(
            "Review this file for bugs, security issues, and improvements: $filePath"
        );

        return [
            'review' => $response->text(),
            'success' => $response->isSuccess(),
            'session' => (string) ($response->sessionId() ?? ''),
        ];
    }
}

Sandbox Drivers

Control the isolation level of agent execution. The sandbox driver determines whether the agent runs directly on the host or inside a container.

Driver Description Use Case
host Direct execution (no isolation) Development, trusted environments
docker Docker container isolation Production, untrusted code
podman Podman container isolation Rootless containers
firejail Linux sandbox Lightweight isolation
bubblewrap Minimal sandbox CI/CD environments
use Cognesy\Sandbox\Enums\SandboxDriver;

// Development (direct execution)
AgentCtrl::claudeCode()
    ->withSandboxDriver(SandboxDriver::Host)
    ->execute('...');

// Production (Docker isolation)
AgentCtrl::claudeCode()
    ->withSandboxDriver(SandboxDriver::Docker)
    ->execute('...');

Best Practices

  1. Set Timeouts -- Always set appropriate timeouts for your use case. Complex code generation can take several minutes.
  2. Use Sandbox Isolation -- In production, use Docker or another container-based sandbox driver to prevent agents from making unintended changes.
  3. Handle Errors -- Check isSuccess() and handle failures gracefully. Agents can fail for many reasons, including API limits, invalid code, and sandbox restrictions.
  4. Log Sessions -- Store session IDs for debugging and continuation. They let you resume work and trace agent behavior.
  5. Test with Fakes -- Use AgentCtrl::fake() in tests to avoid API calls and process execution.
  6. Queue Long Tasks -- Use Laravel queues for time-consuming code generation to keep web responses fast.