A framework-agnostic PHP implementation of the A2A (Agent-to-Agent) Protocol that enables AI agents to communicate and collaborate across different platforms, frameworks, and organizations.
The A2A (Agent-to-Agent) Protocol is an open standard that enables seamless communication and collaboration between AI agents. It provides a common language for agents built using diverse frameworks and by different vendors, fostering interoperability and breaking down silos.
Official Specification: https://a2a-protocol.org/latest/specification/
- ✅ Complete A2A Protocol Implementation - JSON-RPC 2.0 over HTTP
- ✅ Multi-Agent Architecture-Host multiple specialized agents in one application
- ✅
message/send- Send messages and receive agent responses - ✅
tasks/get- Retrieve a specific task by ID - ✅
tasks/list- List tasks with filtering and pagination - ✅
tasks/cancel- Cancel a running task - ✅
agent/getAuthenticatedExtendedCard- Get agent capabilities
- ✅ **Laravel—**Full integration with Artisan commands and routes
- ✅ Standalone - Framework-agnostic HTTP interfaces
- 🔄 **Other Frameworks—**Easy to add adapters (Symfony, Slim, etc.)
You can create your own server class extending NeuronCore\A2A\A2AServer. This class provides the main entry point to
expose your AI agent to the world. You need to implement two components to create a server:
- Task Repository - Store and retrieve tasks
- Message Handler - Handle messages and return task results
use NeuronCore\A2A\Server\A2AServer;
use NeuronCore\A2A\Contract\TaskRepositoryInterface;
use NeuronCore\A2A\Contract\MessageHandlerInterface;
use NeuronCore\A2A\Model\AgentCard\AgentCard;
class MyAIAgent extends A2AServer
{
protected function taskRepository(): TaskRepositoryInterface
{
return new MyTaskRepository();
}
protected function messageHandler(): MessageHandlerInterface
{
return new MyMessageHandler();
}
protected function agentCard(): AgentCard
{
return new AgentCard(
protocolVersion: '0.3.0',
name: 'My AI Agent',
description: 'A specialized AI agent for data analysis',
url: 'https://example.com/a2a',
preferredTransport: 'JSONRPC',
version: '1.0.0',
provider: new AgentProvider(
organization: 'My Company',
url: 'https://mycompany.com'
),
skills: [
new AgentSkill(
id: 'analysis',
name: 'Data Analysis',
description: 'Analyze datasets',
tags: ['data', 'analytics'],
examples: ['Analyze sales data'],
inputModes: ['text/plain'],
outputModes: ['text/plain']
)
]
);
}
}use NeuronCore\A2A\Contract\TaskRepositoryInterface;
use NeuronCore\A2A\Model\Task;
class MyTaskRepository implements TaskRepositoryInterface
{
public function save(Task $task): void
{
// Save to database, Redis, etc.
}
public function find(string $taskId): ?Task
{
// Retrieve from storage
}
public function findAll(array $filters = [], ?int $limit = null, ?int $offset = null): array
{
// Query with filters
}
public function count(array $filters = []): int
{
// Count tasks
}
public function generateTaskId(): string
{
return uniqid('task_', true);
}
public function generateContextId(): string
{
return uniqid('context_', true);
}
}The message handler is responsible for handling incoming messages and returning task results. It's the place where you execute your AI Agent and return the results.
use NeuronAI\Agent;
use NeuronAI\Chat\Messages\UserMessage;
use NeuronCore\A2A\Contract\MessageHandlerInterface;
use NeuronCore\A2A\Model\Task;
use NeuronCore\A2A\Model\Message;
use NeuronCore\A2A\Model\Part\TextPart;
use NeuronCore\A2A\Model\Artifact;
use NeuronCore\A2A\Model\TaskStatus;
use NeuronCore\A2A\Enum\TaskState;
class MyMessageHandler implements MessageHandlerInterface
{
public function handle(Task $task, array $messages): Task
{
$history = array_merge($task->history ?? [], $messages);
// Extract user message
$userText = $this->extractText($messages[0]);
$aiResponse = Agent::make()
->setProvider(...)
->chat(new UserMessage($userText))
->getContent();
// Create an agent response
$agentMessage = new Message(
role: 'agent',
parts: [new TextPart($aiResponse)]
);
$history[] = $agentMessage;
// Return a completed task
return new Task(
id: $task->id,
contextId: $task->contextId,
status: new TaskStatus(
state: TaskState::COMPLETED,
message: new TextPart('Task completed')
),
history: $history,
);
}
protected function extractText(Message $message): string
{
$text = '';
foreach ($message->parts as $part) {
if ($part instanceof TextPart) {
$text .= $part->text;
}
}
return $text;
}
}use NeuronCore\A2A\JsonRpc\JsonRpcRequest;
// Create a server instance
$server = new MyAIAgent();
// Handle JSON-RPC request
$request = JsonRpcRequest::fromArray([
'jsonrpc' => '2.0',
'id' => 1,
'method' => 'message/send',
'params' => [
'messages' => [
[
'role' => 'user',
'parts' => [
['kind' => 'text', 'text' => 'Hello!']
]
]
]
]
]);
$response = $server->handleRequest($request);
echo json_encode($response->toArray());See examples/a2a.php for a complete working example.
Laravel gets first-class support with Artisan commands, service providers, and routing helpers.
In config/app.php or bootstrap/providers.php based on your project structure:
'providers' => [
// ...
NeuronCore\A2A\Laravel\A2AServiceProvider::class,
],php artisan make:a2a DataAnalystThis generates:
app/A2A/DataAnalystServer.php- Main serverapp/A2A/DataAnalystTaskRepository.php- Task storageapp/A2A/DataAnalystMessageHandler.php- AI logic
You must implement the Task Repository and Message Handler.
In routes/api.php:
use NeuronCore\A2A\Laravel\A2A;
use App\A2A\DataAnalystServer;
A2A::route('/a2a/data-analyst', DataAnalystServer::class)
->middleware(['auth:api']);Done! Your agent is live at:
POST /a2a/data-analyst- JSON-RPC endpointGET /a2a/data-analyst/.well-known/agent-card.json- Agent card
Create and register as many agents as needed:
php artisan make:a2a DataAnalyst
php artisan make:a2a Translator
php artisan make:a2a CodeGenerator// routes/api.php
A2A::route('/a2a/data-analyst', DataAnalystServer::class);
A2A::route('/a2a/translator', TranslatorServer::class);
A2A::route('/a2a/code-generator', CodeGeneratorServer::class);Each agent is completely independent with its own:
- Task repository
- Message handler (AI logic)
- Middleware configuration
Messages follow the A2A protocol:
new Message(
role: 'user', // or 'agent'
parts: [
new TextPart('Hello'),
new FilePart($file, 'image/png'),
new DataPart(['key' => 'value'], 'application/json')
]
)The agent card is a JSON manifest that describes:
- Agent identity (name, description, version)
- Provider information
- Available skills with examples
- Input/output formats
- Authentication requirements
- Protocol capabilities
Tasks can be grouped by contextId for conversation continuity:
- Multiple tasks can share the same context
- Use
tasks/listwithcontextIdfilter to retrieve related tasks - Useful for multi-turn conversations
Abstract Methods:
abstract protected function taskRepository(): TaskRepositoryInterface;
abstract protected function messageHandler(): MessageHandlerInterface;
abstract protected function agentCard(): AgentCard;Public Methods:
public function handleRequest(JsonRpcRequest $request): JsonRpcResponse|JsonRpcError;
public function getAgentCard(): array;interface TaskRepositoryInterface
{
public function save(Task $task): void;
public function find(string $taskId): ?Task;
public function findAll(array $filters = [], ?int $limit = null, ?int $offset = null): array;
public function count(array $filters = []): int;
public function generateTaskId(): string;
public function generateContextId(): string;
}interface MessageHandlerInterface
{
public function handle(Task $task, array $messages): Task;
}Use Eloquent, Redis, File, or any storage backend:
class EloquentTaskRepository implements TaskRepositoryInterface
{
public function save(Task $task): void
{
TaskModel::updateOrCreate(
['id' => $task->id],
['data' => serialize($task)]
);
}
public function find(string $taskId): ?Task
{
$model = TaskModel::find($taskId);
return $model ? unserialize($model->data) : null;
}
}Laravel example with custom middleware:
A2A::route('/a2a/premium-agent', DataAnalystAgent::class)
->middleware(['auth:api']);Handle file uploads in messages:
use NeuronCore\A2A\Model\Part\FilePart;
use NeuronCore\A2A\Model\File\FileWithBytes;
use NeuronCore\A2A\Model\File\FileWithUri;
// Embedded file
$filePart = new FilePart(
file: new FileWithBytes(
bytes: base64_encode($fileContents),
fileName: 'document.pdf',
mimeType: 'application/pdf'
),
mimeType: 'application/pdf'
);
// File by URL
$filePart = new FilePart(
file: new FileWithUri(
uri: 'https://example.com/file.pdf',
fileName: 'document.pdf',
mimeType: 'application/pdf'
),
mimeType: 'application/pdf'
);Send and receive structured JSON data:
use NeuronCore\A2A\Model\Part\DataPart;
$dataPart = new DataPart(
data: [
'results' => [
['name' => 'Alice', 'score' => 95],
['name' => 'Bob', 'score' => 87]
],
'total' => 2
],
mimeType: 'application/json'
);When contributing to this project:
- Use modern PHP 8.1+ features
- Maintain interface-driven design
- Write minimal, clean code
- Focus on simplicity and clarity
MIT License—See LICENSE file for details