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
Requires
- php: >=8.2
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/log: ^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.
This package is auto-updated.
Last update: 2026-02-20 01:30:03 UTC
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
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-clientviaSymfonyHttpTransport. PSR-18 clients only supportinvoke()-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, 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 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