helgesverre / pagent
A Pest-inspired LLM Agent Framework for PHP with multi-provider support, automatic tool calling, safety guards, and multi-agent orchestration
Package info
pkg:composer/helgesverre/pagent
Requires
- php: ^8.3
- ext-curl: *
- guzzlehttp/guzzle: ^7.10
- nyholm/psr7: ^1.8
- open-telemetry/api: ^1.7
- open-telemetry/exporter-otlp: ^1.3
- open-telemetry/sdk: ^1.9
- open-telemetry/sem-conv: ^1.37
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/log: ^3.0
- swaggest/json-schema: ^0.12.43
- symfony/process: ^7.3
- teamtnt/tntsearch: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- laravel/pint: ^1.24
- nunomaduro/phpinsights: ^2.13
- pestphp/pest: ^4.0
- phpstan/phpstan: ^1.10
- spatie/ray: ^1.42
- vlucas/phpdotenv: ^5.6
README
A fluent LLM agent framework for PHP, inspired by Pest.
Pagent provides a compact API for building stateful AI agents with tool calling, streaming, multiple model providers, safety guards, evaluation, and multi-agent workflows.
Features
- Fluent, named agent configuration
- Anthropic, OpenAI, OpenCode Zen/Go, Ollama, and deterministic mock providers
- Automatic tool schemas generated from typed PHP closures
- Reusable class-based tools for files, search, shell commands, PDFs, and HTTP
- Streaming responses and Server-Sent Events support
- File and SQLite conversation persistence
- Guards, middleware, lifecycle events, and fallbacks
- Pipelines, handoffs, delegation, and multi-agent workflows
- Dataset-based evaluation with built-in and custom metrics
- Token and cost tracking
- OpenTelemetry tracing for agents, providers, tools, guards, and workflows
- Model Context Protocol (MCP) client support over stdio and HTTP/SSE
Requirements
- PHP 8.4.1 or later
- Composer 2
- The PHP cURL extension
- A provider API key, unless you use Ollama or the mock provider
Installation
composer require helgesverre/pagent
The core package intentionally has a small runtime footprint. Telemetry exporters,
the Bash tool, full-text search, and JSON-schema evaluation are optional; Composer
lists their packages under suggest. Install only the integrations your application
uses (for example, composer require open-telemetry/sdk for telemetry or
composer require symfony/process for Bash).
Set the environment variable for the provider you plan to use:
export OPENAI_API_KEY="your-api-key" export ANTHROPIC_API_KEY="your-api-key" export OPENCODE_API_KEY="your-api-key"
For local development in this repository, copy the supplied environment file and add your credentials:
cp .env.example .env composer install
Quick start
Composer loads Pagent's helper functions automatically. Define a named agent and send it a prompt:
<?php require __DIR__.'/vendor/autoload.php'; $assistant = agent('assistant') ->provider('openai') ->system('You are a concise and helpful PHP assistant.') ->temperature(0.3); $response = $assistant->prompt('Explain readonly properties in PHP.'); echo $response->content;
agent() always returns and immediately registers an Agent; it never depends on
builder destruction. Use getAgent() when a missing name must remain missing, or
defineAgent() when you want an explicit configuration boundary. build() remains
a harmless compatibility no-op on Agent.
Named agents are registered for reuse, so application code can retrieve the same configured agent later:
$response = agent('assistant')->prompt('Show a short example.');
See the vanilla PHP guide for a complete application layout, or choose one of the framework integration guides.
Providers
| Provider | Configuration | Typical use |
|---|---|---|
| Anthropic | ANTHROPIC_API_KEY |
Claude models |
| OpenAI | OPENAI_API_KEY |
OpenAI chat models |
| OpenCode | OPENCODE_API_KEY |
Zen/Go models over their protocol |
| Ollama | Local server, by default on port 11434 | Local and private model execution |
| Mock | In-memory response map | Unit tests and deterministic demos |
Use a provider name for standard configuration:
$agent = agent('writer') ->provider('anthropic') ->model('your-model-id') ->maxTokens(1_000);
Pass configuration options with the provider name or supply a provider instance when you need more control:
use Pagent\Providers\Ollama; $local = agent('local') ->provider(new Ollama([ 'base_url' => 'http://127.0.0.1:11434', 'timeout' => 180, ])) ->model('qwen3:8b');
Custom adapters should implement IdentifiedProvider and return a
Pagent\ProviderCapabilities value instead of relying on their class name.
Implement StreamingProvider when the adapter can produce incremental
StreamResponses. This makes provider identity, tools, system-message support,
and streaming explicit for both built-ins and third-party adapters.
Provider-specific request options can be passed to prompt():
$response = openai()->prompt('Return a JSON object with a status field.', [ 'model' => 'your-model-id', 'response_format' => ['type' => 'json_object'], ]);
Retry transient API and connection failures with the capability-preserving
decorator factory. Completed requests and stream establishment failures are
retried; once a StreamResponse has been returned, consumption failures are
never replayed, so partially emitted output cannot be duplicated:
use Pagent\Providers\OpenAI; use Pagent\Providers\RetryingProvider; $provider = RetryingProvider::wrap(new OpenAI, maxAttempts: 3);
Framework-defined failures share one catch boundary while retaining their
standard PHP parents (RuntimeException, InvalidArgumentException, and so on):
use Pagent\Exceptions\PagentException; try { $response = agent('writer')->prompt('Draft an outline.'); } catch (PagentException $exception) { // Provider, configuration, lifecycle, tool, and workflow failures. }
Exceptions thrown by application callbacks or third-party dependencies are not rewritten and may propagate unchanged.
OpenCode supports chat-completions, Responses, and Messages model protocols. The provider defaults to chat-completions; choose a protocol globally, per model, or per prompt when the selected OpenCode model requires it. The default model ID depends on the selected gateway:
// Zen uses https://opencode.ai/zen/v1 and x-preview-f-free. $zen = opencode(); $zenResponse = $zen->prompt('Hello!'); // Go uses https://opencode.ai/zen/go/v1 and ox-alpha-free. $go = opencode(['gateway' => 'go']); $goResponse = $go->prompt('Hello!'); // A model using the Responses protocol. $responses = opencode([ 'protocol' => 'responses', ]); // Or select protocols by model while keeping a chat-completions default. $mixed = opencode([ 'model_protocols' => ['your-responses-model' => 'responses'], ]); // String aliases are available for agent configuration. $coder = agent('coder') ->provider('opencode-go') ->model('ox-alpha-free');
For local inference setup and model selection, see the Ollama integration guide.
Tool calling
Pagent derives a JSON schema from a closure's parameter names, type declarations, and default values. The agent can then select and execute the tool during a model conversation.
$support = agent('order-support') ->provider('openai') ->system('Use the available tools to answer questions about orders.') ->tool( 'find_order', 'Find an order by its identifier', function (string $orderId, bool $includeItems = false): array { return [ 'id' => $orderId, 'status' => 'shipped', 'items' => $includeItems ? ['Keyboard', 'Mouse'] : [], ]; }, ); $response = $support->prompt('Where is order ORD-1042?');
For reusable tools, implement a class or use the included tools:
use Pagent\Tools\FileRead; use Pagent\Tools\Glob; use Pagent\Tools\Grep; $codebase = agent('codebase-assistant') ->provider('anthropic') ->tools([ new Glob(baseDir: __DIR__), new Grep(baseDir: __DIR__), new FileRead(baseDir: __DIR__), ]); $response = $codebase->prompt('Find the classes that implement the Provider contract.');
Pagent includes DataExtract, FileRead, FileWrite, Glob, Grep,
PdfReader, and WebFetch; Bash and SearchTool additionally require their
suggested Composer packages. Scope tools such as file and shell tools to the
narrowest directory and permissions your application requires.
Custom and MCP tools share one provider-neutral Pagent\Contracts\Tool contract
(getName(), getDescription(), getInputSchema(), and execute()). Pagent
serializes that JSON Schema at the provider boundary, so tool implementations do
not contain Anthropic- or OpenAI-specific wire formats.
Runnable examples: closure tools and MCP-provided tools.
Streaming
Use streamTo() for a callback-based interface:
$assistant->streamTo('Write a short introduction to PHP generators.', function ($chunk): void { if ($chunk->isText()) { echo $chunk->content; flush(); } });
Use stream() when you need to inspect start, text, tool, and end chunks or collect
the final response yourself:
$stream = $assistant->stream('Summarize dependency injection in three points.'); foreach ($stream->getStream() as $chunk) { if ($chunk->isText()) { echo $chunk->content; } }
Registered tools work in streaming mode too. Pagent assembles argument deltas,
executes each completed call, and continues streaming the follow-up response.
Use ['tool_mode' => 'manual'] to inspect normalized calls with
$stream->getToolCalls() without executing them, then pass externally produced
results to $assistant->continueToolCalls($stream, $resultsByCallId). Use
$assistant->discardToolCalls($stream) to abandon the pending turn, or
tool_mode => 'none' to omit tool schemas for that request.
Ordinary streams are incremental. Pagent intentionally quarantines a stream before
calling your callback when it has an output policy that needs the complete response
(such as PII/content guards), a legacy two-argument guard, or response-transforming
middleware. This prevents unsafe prefixes from being delivered; use phase-aware
incremental OutputGuards only when their policy is safe across chunk boundaries.
See the streaming guide for SSE endpoints, client code, error handling, and streaming tool calls. The repository also contains a basic streaming example and a complete SSE endpoint.
Conversation memory
Agents retain context in memory during a process. Add a storage adapter and session identifier to continue conversations across requests or application restarts:
$support = agent('support') ->provider('anthropic') ->memory('sqlite', ['path' => __DIR__.'/storage/conversations.db']) ->sessionId('customer-42') ->contextWindow(20_000); $support->prompt('My order number is ORD-1042.'); $response = $support->prompt('What order are we discussing?');
File and SQLite adapters are included. The memory and persistence guide covers session isolation, custom adapters, context windows, and production usage. See also the runnable file, SQLite, and multi-session examples.
Changing sessionId() clears the in-memory conversation and loads only that
session on the next turn. Failed turns are rolled back, so retries do not replay a
partial user message.
Guards and middleware
Guards validate agent interactions and can return a controlled fallback when a
rule is violated. PromptInjectionGuard is an input guard and runs before any
provider or tool call; PII and content guards are output guards:
$assistant = agent('public-assistant') ->provider('openai') ->guard('pii') ->guard('contentFilter') ->guard('promptInjection') ->fallback(fn (Throwable $error): string => 'This request cannot be processed.');
Middleware wraps requests and responses for cross-cutting behavior:
use Pagent\Middleware\RateLimitMiddleware; $assistant ->middleware('logging') ->middleware(new RateLimitMiddleware(maxRequests: 60));
Read the guards, middleware, and events guides for custom implementations and lifecycle hooks. Runnable demonstrations are available for guards and middleware.
Multi-agent workflows
Pipelines pass one agent's response to the next agent:
agent('researcher') ->provider('anthropic') ->system('Research the topic and return concise notes.'); agent('editor') ->provider('openai') ->system('Turn the supplied notes into a polished summary.'); $summary = pipeline('article') ->agent('researcher') ->agent('editor') ->run('How PHP fibers support cooperative concurrency');
Pagent also supports named workflow steps, transforms, handoffs, and supervised delegation. See the orchestration and workflows guide and the multi-agent, simple chain, and named pipeline examples.
Testing and evaluation
The mock provider makes application tests deterministic and requires no network access:
$provider = mock([ 'What is the order status?' => 'The order has shipped.', ]); $agent = agent('test-support') ->provider($provider) ->build(); $response = $agent->prompt('What is the order status?'); assert($response->content === 'The order has shipped.');
The evaluation framework runs datasets against an agent and scores responses with built-in or custom metrics:
use Pagent\Evaluation\Dataset; use Pagent\Evaluation\Metrics\KeywordMetric; $result = evaluate('test-support') ->dataset(Dataset::fromArray([ ['input' => 'What is the order status?', 'expected' => 'shipped'], ])) ->metric('status', new KeywordMetric(['shipped'])) ->run(); echo $result->getAverageScore('status');
Each dataset row uses a fresh conversation by default and the registered agent
definition is never mutated. For datasets that intentionally model a multi-turn
conversation, opt in with ->stateful().
See the evaluation example, the progressive evaluation example, and the evaluation tutorial for datasets, metrics, and HTML, Markdown, and JSON reports.
Usage tracking and observability
Enable per-agent token and cost tracking:
$assistant = agent('metered-assistant') ->provider('openai') ->trackUsage(); $assistant->prompt('Explain PHP attributes.'); $usage = $assistant->getUsage();
For tracing during development, send OpenTelemetry spans to the console:
telemetry_console(verbose: true); agent('traced-assistant') ->provider('anthropic') ->telemetry() ->prompt('Explain the repository pattern.');
Jaeger, Zipkin, and generic OTLP exporters are supported. The observability guide documents configuration, captured attributes, sampling, and production backends. Additional runnable examples cover console traces, Jaeger, workflow traces, and custom OTLP configuration.
Model Context Protocol
Pagent can discover tools from MCP servers, adapt them to Pagent tools, and attach them to an agent. Both local stdio servers and remote HTTP/SSE servers are supported.
See the MCP integration guide for connection lifecycle, tool discovery, transport configuration, error handling, and security guidance. The MCP client example demonstrates both transports.
Examples
The examples directory contains runnable programs organized by
feature:
| Area | Examples |
|---|---|
| Fundamentals | Basic chat, context, providers |
| Tools and safety | Tool calling, guards, middleware |
| Workflows | Chains, multi-agent, pipeline steps |
| Streaming | Basic streaming, SSE endpoint, SSE client |
| Persistence | File memory, SQLite memory, multiple sessions |
| Local models | Ollama basics, streaming, tools |
| Evaluation | Evaluation, progressive evaluation |
| Observability | Console, Jaeger, tools |
| External tool servers | MCP client |
Run an example from the repository root after installing dependencies:
php examples/01-basic-chat.php
Examples using OpenAI or Anthropic require the corresponding API key. Mock examples run without credentials. See the examples index for prerequisites and notes.
Documentation
Feature guides
- Documentation index
- Streaming
- Memory and persistence
- Guards
- Middleware
- Events
- Orchestration and workflows
- Observability
- MCP integration
- Ollama integration
Framework integration
For a longer, structured introduction, read the complete Pagent guide or choose a learning path in the guide index.
Development
Install dependencies and run the standard checks:
composer install
composer format:check
composer analyse
composer test
If just is installed, the repository also
provides shortcuts:
just setup
just format
just analyse
just test
just coverage
composer test excludes live-provider and external-service tests. Run live provider
coverage explicitly with credentials in .env:
composer test
composer test:live
composer test:external
composer test:observability
See CONTRIBUTING.md for the development workflow and pull request guidelines. Security issues should be reported according to SECURITY.md.
Changelog
See CHANGELOG.md for release history and notable changes.
License
Pagent is open-source software licensed under the MIT license.
Credits
Created by Helge Sverre. The fluent API is inspired by Pest.