blundergoat / strands-php-client
PHP client for consuming Strands AI agents over HTTP - invoke, stream via SSE, manage sessions.
Requires
- php: >=8.2
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/log: ^1.0 || ^2.0 || ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- illuminate/support: ^10.0 || ^11.0 || ^12.0
- infection/infection: ^0.32.4
- phpmd/phpmd: ^2.15
- phpstan/phpstan: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^11.0
- symfony/config: ^6.4 || ^7.0
- symfony/dependency-injection: ^6.4 || ^7.0
- symfony/framework-bundle: ^6.4 || ^7.0
- symfony/http-client: ^6.4 || ^7.0
- symfony/yaml: ^6.4 || ^7.0
- vlucas/phpdotenv: ^5.6
Suggests
- guzzlehttp/guzzle: PSR-18 HTTP client -use with PsrHttpTransport for invoke() support without Symfony.
- illuminate/support: Required for Laravel service provider integration (^10.0|^11.0|^12.0)
- symfony/framework-bundle: Required for Symfony bundle integration (^6.4|^7.0)
- symfony/http-client: Required for SSE streaming support via SymfonyHttpTransport. Without it, only invoke() is available via PsrHttpTransport.
README
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
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, Buzz, etc.)
Any PSR-18 client works via PsrHttpTransport (invoke only, no streaming):
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;
Features
Rich Input (AgentInput)
Send multi-modal content to agents - images, documents, S3 locations - alongside your text message:
use StrandsPhpClient\Context\AgentInput; // Text with an image $input = AgentInput::text("What's in this image?") ->withImage(base64_encode($imageBytes), 'image/png'); $response = $client->invoke(message: $input); // Text with a document from S3 $input = AgentInput::text('Summarise this report') ->withDocumentFromS3('s3://my-bucket/report.pdf', 'pdf', 'report'); // Resume after an interrupt (human-in-the-loop) $input = AgentInput::interruptResponse($interruptId, ['approved' => true]);
When no content blocks are attached, AgentInput::text() serializes as a plain string - fully backward compatible. See docs/rich-input.md for the full API reference.
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->usage->totalTokens(); // Total tokens (input + output) $response->toolsUsed; // Tools the agent called $response->metadata; // Unrecognised response fields (forward-compat) // Interrupt handling (human-in-the-loop) if ($response->isInterrupted()) { foreach ($response->interrupts as $interrupt) { echo $interrupt->toolName; // Tool that needs approval echo $interrupt->reason; // Why the agent paused } } // Guardrail traces (content safety) if ($response->guardrailTrace !== null) { echo $response->guardrailTrace->action; // 'INTERVENED' or 'NONE' }
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->usage->totalTokens(); // Total tokens (input + output) echo $result->textEvents; // Number of text chunks received echo $result->totalEvents; // Total events received echo $result->timeToFirstTextTokenMs; // Client-measured TTFT in ms echo $result->isInterrupted() ? 'yes' : 'no'; // Whether the agent was interrupted
Note: SSE streaming requires
symfony/http-clientviaSymfonyHttpTransport. PSR-18 clients only supportinvoke()-this is a limitation of the PSR-18 spec, not this library.
Custom Endpoints (postJson / streamSse)
For agent endpoints with custom request/response schemas, use postJson() and streamSse(). These bypass the standard invoke/stream contract and work with arbitrary payloads:
// Synchronous - returns raw decoded JSON array $result = $client->postJson('/file-summarise', [ 'file_base64' => base64_encode($fileBytes), 'file_name' => 'report.pdf', ], timeout: 120); // Streaming - delivers raw decoded JSON arrays to the callback $client->streamSse('/file-summarise-stream', [ 'file_base64' => base64_encode($fileBytes), ], function (array $event) { match ($event['type'] ?? '') { 'text' => print($event['content']), 'complete' => print("\n[Done]"), default => null, }; }, timeout: 120);
Both methods accept optional timeout: overrides and stream callbacks can return false to cancel.
Error Handling
AgentErrorException carries the full response body for debugging structured errors:
use StrandsPhpClient\Exceptions\AgentErrorException; try { $client->postJson('/validate', $payload); } catch (AgentErrorException $e) { $e->statusCode; // 422 $e->getMessage(); // "Validation failed" $e->responseBody; // ['detail' => '...', 'errors' => [...]] }
Sessions & Context
Pass sessionId for multi-turn conversations -the server manages all state:
$r1 = $client->invoke('Draft a referral letter', sessionId: 'consult-001'); $r2 = $client->invoke('Make it more formal', sessionId: 'consult-001');
Use the immutable AgentContext builder to pass application context:
$context = AgentContext::create() ->withMetadata('clinic_id', 'CL-789') ->withSystemPrompt('You are a clinical documentation assistant.') ->withPermission('read:patients') ->withDocument('referral.pdf', base64_encode($pdfBytes), 'application/pdf');
Retries, Timeouts & Logging
$config = new StrandsConfig( endpoint: 'https://api.example.com/agent', maxRetries: 3, // Retry up to 3 times (invoke/postJson only) retryDelayMs: 500, // 500ms → 1s → 2s (exponential backoff with jitter) connectTimeout: 5, // Fail fast if server is down ); $client = new StrandsClient(config: $config, logger: $yourPsr3Logger);
Retries apply to invoke() and postJson(). Streaming requests are not retried. The Symfony bundle injects Monolog automatically.
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) | Available |
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: ''), ); // AWS SigV4 auth (agents behind API Gateway with IAM) use StrandsPhpClient\Auth\SigV4Auth; $config = new StrandsConfig( endpoint: 'https://abc123.execute-api.us-east-1.amazonaws.com/prod', auth: new SigV4Auth( accessKeyId: 'AKIA...', secretAccessKey: 'wJalr...', region: 'us-east-1', ), ); // SigV4 from environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) $config = new StrandsConfig( endpoint: 'https://abc123.execute-api.us-east-1.amazonaws.com/prod', auth: SigV4Auth::fromEnvironment(region: 'us-east-1'), );
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, viaPsrHttpTransport
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 and examples |
| Authentication | Auth strategies, custom drivers, framework config |
| Rich Input | Multi-modal input: images, documents, S3 locations |
| Interrupts & Guardrails | Human-in-the-loop and content safety |
| 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
| Repo | What |
|---|---|
| the-summit-chatroom | Demo app - three AI agents debating your decisions |
| terraform-aws-strands | Terraform module for deploying Strands agents on AWS |
Maintained by Matthew Hansen.