A2A (Agent2Agent) je otvoreni protokol, danas pod okriljem Linux Foundation-a, preko kog agenti napravljeni na različitim framework-ovima pozivaju jedni druge. Verzija 1.0.0 objavljena je 12. marta 2026. Zvanični SDK-ovi postoje za Python, JavaScript, Java, Go, .NET i Rust. PHP nije na toj listi, a PHP paketi koje sam našao na Packagist-u u septembru 2026. podržavaju protokol 0.3.0.

Zato sam napisao a2a-php-sdk i a2a-symfony-bundle. SDK za protokol vredi nešto samo ako razgovara sa implementacijama koje su napisali drugi ljudi, pa ovaj tekst nije o unutrašnjosti SDK-a. Reč je o jednom ponovljivom prolazu protiv referentne implementacije:

SmerKlijentServer
PHP → Pythona2a-php-sdkzvanični hello-world primer na a2a-sdk
Python → Symfonyzvanični a2a-sdk klijentSymfony 8 aplikacija sa bundle-om

Sve u nastavku potiče iz a2a-php-interop-demo. make demo ga gradi, pokreće oba smera i završava sa kodom 0. Kompletan izlaz prolaza je u repozitorijumu: docs/run-2026-09-15.log.

Verzije

KomponentaVerzija
PHP8.4.23
vbcherepanov/a2a-php-sdk1.0.0
vbcherepanov/a2a-symfony-bundle1.0.2
symfony/framework-bundle8.0.15
Python3.13.15
a2a-sdk1.1.0
a2a-samplescommit 6603ba3

Python agent nije kopija. Dockerfile preuzima __main__.py, agent_executor.py i requirements.txt baš iz tog commit-a a2a-samples i pokreće ih bez izmena.

Jedno ograničenje je odredilo celu postavku

Zvanični primer sluša na 127.0.0.1:9999 i istu adresu upisuje u svoju Agent Card. U Docker-u je ta adresa vidljiva samo unutar kontejnera. Umesto da menjam referentnu implementaciju, svi servisi u compose.yaml pridružuju se mrežnom prostoru Python agenta:

symfony-agent:
  build: ./symfony-agent
  network_mode: "service:python-agent"

Sada za svaki kontejner 127.0.0.1:9999 znači Python agent, a 127.0.0.1:8000 Symfony agent.

Smer 1: PHP poziva zvaničnog Python agenta

Klijent otkriva agenta, iz kartice uzima JSON-RPC interfejs i šalje istu poruku dvaput — blokirajuće i sa strimovanjem:

$http = HttpClient::create();
$options = new CallOptions(timeoutSeconds: 30.0);

$card = (new Discovery($http, allowPrivateNetwork: true))->discover($baseUrl, $options);

$client = new Client(new HttpTransport($http, $endpoint, jsonRpc: true), $options);
echo $client->sendMessage($request())->serializeToJsonString();

foreach ($client->sendStreamingMessage($request()) as $event) {
    echo $event->serializeToJsonString(), "\n";
}

allowPrivateNetwork: true je svestan izbor. Podrazumevano otkrivanje u SDK-u odbija adrese privatnih mreža, jer je Agent Card tuđi podatak. Za lokalni demo tu zaštitu treba eksplicitno isključiti — a u produkcionom kodu ostaje uključena.

Strim poziv je dao četiri događaja, tim redom:

task          TASK_STATE_SUBMITTED
statusUpdate  TASK_STATE_WORKING    "Processing request..."
artifactUpdate                      "Hello, World! I have received your request (Hi from a2a-php-sdk)"
statusUpdate  TASK_STATE_COMPLETED  "Request is completed!"

Te stringove piše executor iz Python primera. PHP strana je svaki događaj pretvorila u generisane protobuf klase bez ijednog ručnog mapiranja.

Smer 2: zvanični Python klijent poziva Symfony

Na Symfony strani agent čine četiri fajla povrh symfony/skeleton i composer require vbcherepanov/a2a-symfony-bundle.

Executor — jedna klasa koja vraća artefakt i završni status:

final class EchoExecutor implements Executor
{
    public function execute(SendMessageRequest $request, Task $task, CallContext $context): iterable
    {
        // sakupiti tekstualne delove dolazne poruke u $text
        yield (new StreamResponse())->setArtifactUpdate(/* "Hello from a Symfony agent. You said: $text" */);
        yield (new StreamResponse())->setStatusUpdate(/* TASK_STATE_COMPLETED */);
    }
}

Konfiguracija bundle-a i import ruta:

# config/packages/a2a.yaml
a2a:
    executor: App\Agent\EchoExecutor
    card_file: '%kernel.project_dir%/config/agent-card.json'
    public_url: '%env(A2A_PUBLIC_URL)%'
    auth:
        tokens:
            python-client: '%env(A2A_DEMO_TOKEN)%'

# config/routes/a2a.yaml
a2a:
    resource: .
    type: a2a

Agent Card sadrži samo identitet, mogućnosti i veštine. Interfejse bundle popunjava sam — iz public_url i uključenih transporta.

Python klijent je zvanični a2a-sdk API, a Bearer token se prosleđuje kroz httpx:

async with httpx.AsyncClient(headers={'Authorization': f'Bearer {TOKEN}'}) as http:
    client = await create_client(
        agent=card,
        client_config=ClientConfig(streaming=True, httpx_client=http),
    )
    async for chunk in client.send_message(request):
        print(chunk)

Ovo je zvanični klijent ispisao o Symfony agentu i njegovom odgovoru:

Name        : Symfony Echo Agent
  [0] http://127.0.0.1:8000/a2a/rpc  (JSONRPC 1.0)
  [1] http://127.0.0.1:8000/a2a  (HTTP+JSON 1.0)
Streaming           : True

artifact_update  "Hello from a Symfony agent. You said: Hi from the official A2A Python SDK"
status_update    TASK_STATE_COMPLETED

Zatim je isti klijent pokrenut sa pogrešnim tokenom:

a2a.client.errors.A2AClientError: HTTP Error 401: Client error '401 Unauthorized'
for url 'http://127.0.0.1:8000/a2a/rpc'

Bundle nema anonimni režim. A2A endpoint izvršava posao u ime pozivaoca, pa se kontejner neće kompajlirati dok ne podesite mapu tokena, Symfony access-token handler ili sopstveni autentifikator.

Dva baga koja je mogla da nađe samo prava instalacija

Pripremajući demo, instalirao sam bundle onako kako bi to uradio neko nepoznat — composer require u svež symfony/skeleton — i pukao je dvaput.

1.0.0: Symfony Flex automatski registruje bundle, čak i bez recepta, pa zatim pokreće cache:clear. Konfiguracija bundle-a je card_file, executor i public_url označavala kao obavezne, pa je instalacija pucala sa The child config “card_file” under “a2a” must be configured pre nego što je iko stigao išta da podesi. CI bundle-a je i ranije instalirao paket u svež projekat, ali ta provera nije prolazila kroz Symfony Flex. Ispravljeno u 1.0.1: bez sekcije a2a bundle ne registruje ništa, a CI sada instalira kroz skeleton.

1.0.1: kada config/routes/a2a.yaml postoji, a konfiguracije još nema, rutiranje je pucalo sa Cannot load resource ”.” Make sure there is a loader supporting the “a2a” type. To je bitno jer Flex recept ubacuje routes fajl odmah pri instalaciji. Ispravljeno u 1.0.2: loader ruta se uvek registruje i vraća praznu kolekciju dok bundle nije podešen.

Nijedan od ta dva baga nije bio vidljiv unit testovima. Oba su vidljiva u prvom minutu instalacije paketa po njegovoj sopstvenoj dokumentaciji.

Šta ovaj prolaz dokazuje, a šta ne

Dokazuje da su, za navedene verzije, PHP SDK i Symfony bundle kompatibilni sa referentnom Python implementacijom preko JSON-RPC-a: otkrivanje Agent Card-a, blokirajući SendMessage i strim SendMessage, uključujući Bearer autentifikaciju na Symfony strani.

Ne pokriva REST i gRPC binding-e, push obaveštenja ni proširenu Agent Card protiv Python-a — SDK za to ima sopstvene testove, ali ne u ovom demou. Ugrađeni PHP server je samo za demo.

Usklađenost sa specifikacijom je posebno pitanje, i na njega odgovara zvanični skup testova, a ne jedan primer. SDK pokreće zvanični a2a-tck 1.0.0 na sva tri transporta: 244 passed, 3 failed, 18 skipped. Sva tri pada su jedan zahtev, CORE-SEND-003, koji ispravan odgovor sa greškom računa kao pad jer u njegovoj definiciji nije navedena očekivana greška. Defekt je prijavljen kao a2a-tck#202, a sa ispravkom iz PR #203 rezultat je 247 passed, 18 skipped. To nije sertifikacija i ne predstavljam je tako.

Isprobajte

git clone https://github.com/vbcherepanov/a2a-php-interop-demo.git
cd a2a-php-interop-demo
make demo
make down

Ako na vašoj mašini ne proradi, otvorite issue u demo repozitorijumu sa izlazom make demo — pao prolaz kompatibilnosti je upravo izveštaj koji ovim paketima treba.