blundergoat/strands-php-client

PHP client for consuming Strands AI agents over HTTP - invoke, stream via SSE, manage sessions.

Installs: 39

Dependents: 0

Suggesters: 0

Security: 0

Stars: 2

Watchers: 0

Forks: 0

Open Issues: 1

pkg:composer/blundergoat/strands-php-client

v1.2.0 2026-02-18 19:11 UTC

This package is auto-updated.

Last update: 2026-02-20 01:30:03 UTC


README

CI Latest Version PHP Version License

PHP client library for consuming Strands AI agents via HTTP. Invoke agents, stream responses via SSE, manage sessions -without running an agentic loop in PHP.

Works with any PHP framework -Laravel, Symfony, Slim, or vanilla PHP.

"Your PHP app doesn't need to become an AI platform. It just needs to talk to one."

composer require blundergoat/strands-php-client

Why This Exists

Strands Agents is an open-source Python SDK from AWS for building autonomous AI agents. It handles the hard parts -reasoning loops, tool orchestration, model routing (Claude, Nova, GPT, Ollama) -and exposes agents over HTTP.

But most web applications aren't written in Python. If your product runs on Symfony or any PHP framework, you need a way to consume those agents without reimplementing the agentic loop in PHP. That's what this library does.

Your Python agents handle the AI. Your PHP app handles the product. This client is the bridge.

graph LR
    A["PHP Application<br/><small>Laravel · Symfony · Slim</small>"] -->|"invoke() / stream()"| B["strands-php-client"]
    B -->|"HTTP + SSE"| C["Strands Agent<br/><small>Python · FastAPI</small>"]
    C -->|"Reasoning Loop"| D["LLM Provider<br/><small>Bedrock · Ollama · OpenAI</small>"]
    C -->|"Tool Calls"| E["Tools & APIs<br/><small>DB · Search · Custom</small>"]

    style A fill:#7c3aed,color:#fff,stroke:none
    style B fill:#2563eb,color:#fff,stroke:none
    style C fill:#059669,color:#fff,stroke:none
    style D fill:#d97706,color:#fff,stroke:none
    style E fill:#d97706,color:#fff,stroke:none
Loading

The request flow in detail:

sequenceDiagram
    participant App as PHP App
    participant Client as StrandsClient
    participant Transport as HttpTransport
    participant Agent as Python Agent
    participant LLM as LLM Provider

    App->>Client: invoke(message, context, sessionId)
    Client->>Client: Build JSON payload
    Client->>Client: Apply AuthStrategy headers
    Client->>Transport: post(url, headers, body)
    Transport->>Agent: HTTP POST /invoke
    Agent->>LLM: Reasoning loop + tool calls
    LLM-->>Agent: Response
    Agent-->>Transport: JSON response
    Transport-->>Client: Raw response
    Client->>Client: Hydrate AgentResponse
    Client-->>App: AgentResponse

    Note over App,Agent: Streaming follows the same path but uses<br/>SSE chunks parsed by StreamParser
Loading

For a full walkthrough with real-world examples, see the Usage Guide.

Quick Start

Symfony (with auto-detection)

If symfony/http-client is installed, the transport is auto-detected -just create a client and go:

use StrandsPhpClient\StrandsClient;
use StrandsPhpClient\Config\StrandsConfig;

$client = new StrandsClient(
    config: new StrandsConfig(endpoint: 'http://localhost:8081'),
);

$response = $client->invoke('Should we migrate to microservices?');

echo $response->text;
echo $response->agent;               // Which agent handled it (e.g. "analyst")
echo $response->usage->inputTokens;

For Symfony projects, the bundle adds YAML config and autowiring -see Symfony Bundle Integration below.

PSR-18 / Guzzle

Pass a PsrHttpTransport with your PSR-18 client:

use StrandsPhpClient\StrandsClient;
use StrandsPhpClient\Config\StrandsConfig;
use StrandsPhpClient\Http\PsrHttpTransport;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;

$client = new StrandsClient(
    config: new StrandsConfig(endpoint: 'http://localhost:8081'),
    transport: new PsrHttpTransport(
        httpClient: new Client(['timeout' => 120]),
        requestFactory: new HttpFactory(),
        streamFactory: new HttpFactory(),
    ),
);

$response = $client->invoke('Should we migrate to microservices?');
echo $response->text;

Any PSR-18 Client

Any library implementing PSR-18 (psr/http-client) works -Guzzle, Buzz, php-http/curl-client, etc.:

use StrandsPhpClient\Http\PsrHttpTransport;

$transport = new PsrHttpTransport(
    httpClient: $yourPsr18Client,        // ClientInterface
    requestFactory: $yourRequestFactory, // RequestFactoryInterface
    streamFactory: $yourStreamFactory,   // StreamFactoryInterface
);

$client = new StrandsClient(
    config: new StrandsConfig(endpoint: 'https://agent-api.example.com'),
    transport: $transport,
);

Features

Invoke (blocking)

use StrandsPhpClient\Context\AgentContext;

$response = $client->invoke(
    message: 'Analyse this proposal',
    context: AgentContext::create()
        ->withMetadata('persona', 'analyst')
        ->withSystemPrompt('Be concise.'),
    sessionId: 'session-001',
);

$response->text;        // Agent's response
$response->agent;       // Agent name (e.g. "analyst")
$response->sessionId;   // Session ID for follow-ups
$response->usage;       // Token usage (inputTokens, outputTokens)
$response->toolsUsed;   // Tools the agent called

Stream (SSE)

Real-time token-by-token streaming via Server-Sent Events. Requires symfony/http-client.

stream() returns a StreamResult with the accumulated text, session info, and usage stats:

use StrandsPhpClient\Streaming\StreamEvent;
use StrandsPhpClient\Streaming\StreamEventType;

$result = $client->stream(
    message: 'Explain quantum computing',
    onEvent: function (StreamEvent $event) {
        match ($event->type) {
            StreamEventType::Text     => print($event->text),
            StreamEventType::ToolUse  => print("[Using tool: {$event->toolName}]"),
            StreamEventType::Complete => print("\n[Done]"),
            StreamEventType::Error    => print("Error: {$event->errorMessage}"),
            default                   => null,
        };
    },
    sessionId: 'session-001',
);

echo $result->text;                   // Full accumulated text
echo $result->sessionId;              // Session ID
echo $result->usage->inputTokens;     // Token usage
echo $result->textEvents;             // Number of text chunks received
echo $result->totalEvents;            // Total events received

Note: SSE streaming requires symfony/http-client via SymfonyHttpTransport. PSR-18 clients only support invoke() -this is a limitation of the PSR-18 spec, not this library.

Sessions

The client sends a session_id -the server manages all state. Multi-turn conversations just work:

$r1 = $client->invoke('Draft a referral letter', sessionId: 'consult-001');
$r2 = $client->invoke('Make it more formal', sessionId: 'consult-001');
// Server remembers the full conversation

Context Builder

Immutable builder for passing application context to agents:

$context = AgentContext::create()
    ->withMetadata('clinic_id', 'CL-789')
    ->withMetadata('user_role', 'practitioner')
    ->withSystemPrompt('You are a clinical documentation assistant.')
    ->withPermission('read:patients')
    ->withDocument('referral.pdf', base64_encode($pdfBytes), 'application/pdf')
    ->withStructuredData('patient', ['id' => 'P-123', 'age' => 42]);

Logging

StrandsClient accepts an optional PSR-3 logger. When provided, it logs request/response details at debug level and retries at warning level:

use Psr\Log\LoggerInterface;

$client = new StrandsClient(
    config: new StrandsConfig(endpoint: 'http://localhost:8081'),
    logger: $yourLogger,  // Any PSR-3 LoggerInterface
);

In the Symfony bundle, the logger is injected automatically from Monolog.

Retries with Exponential Backoff

Configure automatic retries for transient errors (429, 502, 503, 504):

$config = new StrandsConfig(
    endpoint: 'https://api.example.com/agent',
    maxRetries: 3,          // Retry up to 3 times
    retryDelayMs: 500,      // 500ms → 1000ms → 2000ms (exponential backoff)
    connectTimeout: 5,      // Fail fast if server is down (separate from read timeout)
);

Retries apply to invoke() calls. Streaming requests are not retried.

Transport

Transport invoke() stream() Dependency
SymfonyHttpTransport Yes Yes symfony/http-client
PsrHttpTransport Yes No Any PSR-18 client

Auto-detection: If no transport is passed to the constructor, the client checks for symfony/http-client and uses SymfonyHttpTransport automatically. If Symfony isn't available, it throws with guidance to pass a PsrHttpTransport.

Timeout: SymfonyHttpTransport uses the timeout from StrandsConfig (default 120s). connectTimeout (default 10s) controls how long to wait for the initial TCP connection -separate from the read timeout so a down server fails fast without affecting slow LLM generation. For PsrHttpTransport, configure timeout on your PSR-18 client directly (e.g. new GuzzleHttp\Client(['timeout' => 120])).

Auth Strategies

Strategy Use Case Status
NullAuth Local dev via Docker Compose Available
ApiKeyAuth API Gateway / reverse proxy with API keys Available
SigV4Auth AWS service-to-service (IAM) Planned
use StrandsPhpClient\Auth\NullAuth;
use StrandsPhpClient\Auth\ApiKeyAuth;

// No auth (default -local development)
$config = new StrandsConfig(
    endpoint: 'http://localhost:8081',
);

// API key auth (production)
$config = new StrandsConfig(
    endpoint: 'https://api.example.com/agent',
    auth: new ApiKeyAuth('sk-your-api-key'),
);

// Custom header (e.g. X-API-Key instead of Authorization: Bearer)
$config = new StrandsConfig(
    endpoint: 'https://api.example.com/agent',
    auth: new ApiKeyAuth('sk-your-api-key', headerName: 'X-API-Key', valuePrefix: ''),
);

For details on writing custom auth strategies, see docs/auth.md.

Symfony Bundle Integration

The Symfony bundle adds YAML config, autowired named agents, and automatic PSR-3 logging:

# config/packages/strands.yaml
strands:
    agents:
        analyst:
            endpoint: '%env(AGENT_ENDPOINT)%'
            timeout: 300
        skeptic:
            endpoint: '%env(AGENT_ENDPOINT)%'
            timeout: 300
            auth:
                driver: api_key
                api_key: '%env(AGENT_API_KEY)%'
use StrandsPhpClient\StrandsClient;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class ChatController extends AbstractController
{
    public function __construct(
        #[Autowire(service: 'strands.client.analyst')]
        private readonly StrandsClient $analyst,

        #[Autowire(service: 'strands.client.skeptic')]
        private readonly StrandsClient $skeptic,
    ) {}
}

Bundle registration:

// config/bundles.php
return [
    StrandsPhpClient\Integration\Symfony\StrandsBundle::class => ['all' => true],
];

For the full configuration reference, see docs/symfony-config.md.

Laravel Integration

The Laravel service provider adds config-driven agent registration, dependency injection, and a facade:

// config/strands.php (publish with: php artisan vendor:publish --tag=strands-config)
return [
    'default' => env('STRANDS_DEFAULT_AGENT', 'default'),

    'agents' => [
        'default' => [
            'endpoint' => env('STRANDS_ENDPOINT', 'http://localhost:8081'),
            'auth' => [
                'driver' => env('STRANDS_AUTH_DRIVER', 'null'),
                'api_key' => env('STRANDS_API_KEY'),
            ],
            'timeout' => (int) env('STRANDS_TIMEOUT', 120),
        ],
        'analyst' => [
            'endpoint' => env('AGENT_ENDPOINT'),
            'auth' => ['driver' => 'api_key', 'api_key' => env('AGENT_API_KEY')],
            'timeout' => 300,
        ],
    ],
];

Inject by type-hint or resolve named agents:

use StrandsPhpClient\StrandsClient;

class ChatController extends Controller
{
    public function __construct(
        private readonly StrandsClient $client, // default agent
    ) {}
}

// Named agent via container
$analyst = app('strands.client.analyst');

Use the facade for quick calls:

use StrandsPhpClient\Integration\Laravel\Facades\Strands;

$response = Strands::invoke('Analyse this proposal');

Auto-discovery is configured via composer.json -no manual provider registration needed.

For the full configuration reference, see docs/laravel-config.md.

Requirements

  • PHP 8.2+
  • One of:
    • symfony/http-client ^6.4 or ^7.0 -for full support (invoke + streaming), auto-detected
    • Any PSR-18 HTTP client (e.g. guzzlehttp/guzzle) -for invoke only, via PsrHttpTransport

Installation

Laravel projects:

composer require blundergoat/strands-php-client
php artisan vendor:publish --tag=strands-config

Symfony projects:

composer require blundergoat/strands-php-client
# symfony/http-client is likely already installed -transport auto-detects

PSR-18 / Guzzle projects:

composer require blundergoat/strands-php-client guzzlehttp/guzzle

Want streaming too?

composer require blundergoat/strands-php-client symfony/http-client

Documentation

Document Description
Usage Guide Real-world patterns from the-summit-chat
Authentication Auth strategies, custom drivers, framework config
Laravel Config Full PHP config reference with every option
Symfony Config Full YAML config reference with every option
Changelog Version history and breaking changes
Contributing How to set up dev, run tests, submit PRs

Testing

composer install
vendor/bin/phpunit

All tests use mocked HTTP responses -no Docker, no API keys, no network calls.

Related Repos

Repo What
the-summit-chatroom Demo app -three AI agents debating your decisions (coming soon)

License

Apache 2.0