A2A server.

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/neuron-core/a2a

1.0.0 2025-10-21 18:04 UTC

This package is auto-updated.

Last update: 2025-10-21 18:07:46 UTC


README

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.

Table of Contents

Introduction

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/

What This Library Provides

  • Complete A2A Protocol Implementation - JSON-RPC 2.0 over HTTP
  • ✅ **Multi-Agent Architecture—**Host multiple specialized agents in one application
  • Modern PHP 8.1+ - Enums, constructor promotion, readonly classes, union types

Features

Supported A2A Methods

  • 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

Agent Card

  • ✅ Served at /.well-known/agent-card.json
  • ✅ Describes agent capabilities, skills, and authentication
  • ✅ Enables agent discovery and interoperability

Framework Support

  • ✅ **Laravel—**Full integration with Artisan commands and routing
  • Standalone - Framework-agnostic HTTP interfaces
  • 🔄 **Other Frameworks—**Easy to add adapters (Symfony, Slim, etc.)

Architecture

Design Principles

One Server = One Agent

Each concrete A2AServer class represents a single specialized AI agent. This enables:

  • Clear separation of concerns
  • Independent configuration per agent
  • Easy scaling and deployment
  • Multiple agents in one application

Interface-Driven

Two core interfaces must be implemented for each agent:

  1. TaskRepositoryInterface - How tasks are persisted
  2. MessageHandlerInterface - How messages are processed (your AI logic)

The agent card is returned directly via the agentCard() method.

Quick Start

Standalone Usage

1. Create Your Agent Server

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']
                )
            ]
        );
    }
}

2. Implement Task Repository

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);
    }
}

3. Implement Message Handler (Your AI Logic)

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]);

        // Call your AI service (OpenAI, Claude, local model, etc.)
        $aiResponse = $this->callAI($userText);

        // Create an agent response
        $agentMessage = new Message(
            role: 'agent',
            parts: [new TextPart($aiResponse)]
        );

        $history[] = $agentMessage;

        // Create artifact
        $artifact = new Artifact(
            id: uniqid('artifact_'),
            parts: [new TextPart($aiResponse)]
        );

        // 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,
            artifacts: [$artifact]
        );
    }

    protected function callAI(string $input): string
    {
        // Your AI integration here
        return "AI response to: {$input}";
    }

    protected function extractText(Message $message): string
    {
        $text = '';
        foreach ($message->parts as $part) {
            if ($part instanceof TextPart) {
                $text .= $part->text;
            }
        }
        return $text;
    }
}

4. Use the Server

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 Integration

Laravel gets first-class support with Artisan commands, service providers, and routing helpers.

1. Register Service Provider

In config/app.php or bootstrap/providers.php based on your project structure:

'providers' => [
    // ...
    NeuronCore\A2A\Laravel\A2AServiceProvider::class,
],

2. Generate an Agent server

php artisan make:a2a DataAnalyst

This generates:

  • app/A2A/DataAnalystServer.php - Main server
  • app/A2A/DataAnalystTaskRepository.php - Task storage
  • app/A2A/DataAnalystMessageHandler.php - AI logic
  • app/A2A/DataAnalystAgentCard.php - Agent capabilities

3. Implement Your AI Logic

Open app/A2A/DataAnalystMessageHandler.php:

public function handle(Task $task, array $messages): Task
{
    // Your AI implementation
    $response = app(\OpenAI\Client::class)->chat()->create([
        'model' => 'gpt-4',
        'messages' => $this->convertMessages($messages),
    ]);

    // Return a completed task
    // ... (scaffolded code included)
}

4. Configure Agent Card

Open app/A2A/DataAnalystAgentCard.php and update:

  • Agent name and description
  • Skills and capabilities
  • Input/output formats
  • Tags and examples

5. Register Routes

In routes/api.php:

use NeuronCore\A2A\Laravel\A2A;
use App\A2A\DataAnalystServer;

A2A::route('/a2a/data-analyst', DataAnalystServer::class)
    ->middleware(['auth:api', 'throttle:60,1']);

Done! Your agent is live at:

  • POST /a2a/data-analyst - JSON-RPC endpoint
  • GET /a2a/data-analyst/.well-known/agent-card.json - Agent card

Multiple Agents

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)
  • Agent card (capabilities)
  • Middleware configuration

Core Concepts

Task Lifecycle

Tasks progress through these states:

QUEUED → RUNNING → COMPLETED
                ↓
              FAILED
                ↓
             CANCELED
                ↓
             REJECTED

Terminal states: COMPLETED, FAILED, CANCELED, REJECTED

Once a task reaches a terminal state, it cannot be modified.

Message Structure

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')
    ]
)

Agent Card

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

Task Context

Tasks can be grouped by contextId for conversation continuity:

  • Multiple tasks can share the same context
  • Use tasks/list with contextId filter to retrieve related tasks
  • Useful for multi-turn conversations

Complete Examples

Example 1: Echo Agent (Standalone)

File: examples/a2a.php

A complete working example:

  • Creating a concrete A2AServer
  • Implementing message handling
  • Defining agent card
  • Handling JSON-RPC requests
  • Getting the agent card

Run with:

php examples/a2a.php

API Reference

A2AServer Abstract Class

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;

TaskRepositoryInterface

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;
}

MessageHandlerInterface

interface MessageHandlerInterface
{
    public function handle(Task $task, array $messages): Task;
}

JSON-RPC Methods

message/send

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "message/send",
  "params": {
    "taskId": "optional-existing-task-id",
    "messages": [
      {
        "role": "user",
        "parts": [
          {"kind": "text", "text": "Hello"}
        ]
      }
    ]
  }
}

tasks/get

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tasks/get",
  "params": {
    "taskId": "task_123"
  }
}

tasks/list

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tasks/list",
  "params": {
    "contextId": "context_abc",
    "limit": 10,
    "offset": 0
  }
}

tasks/cancel

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tasks/cancel",
  "params": {
    "taskId": "task_123"
  }
}

Advanced Usage

Custom Task Repository

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;
    }
}

Authentication & Middleware

Laravel example with custom middleware:

A2A::route('/a2a/premium-agent', DataAnalystAgent::class)
    ->middleware(['auth:api']);

File Attachments

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'
);

Structured Data

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'
);

Contributing

When contributing to this project:

  • Use modern PHP 8.1+ features
  • Maintain interface-driven design
  • Write minimal, clean code
  • Focus on simplicity and clarity

License

MIT License—See LICENSE file for details