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:
| Smer | Klijent | Server |
|---|---|---|
| PHP → Python | a2a-php-sdk | zvanični hello-world primer na a2a-sdk |
| Python → Symfony | zvanični a2a-sdk klijent | Symfony 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
| Komponenta | Verzija |
|---|---|
| PHP | 8.4.23 |
| vbcherepanov/a2a-php-sdk | 1.0.0 |
| vbcherepanov/a2a-symfony-bundle | 1.0.2 |
| symfony/framework-bundle | 8.0.15 |
| Python | 3.13.15 |
| a2a-sdk | 1.1.0 |
| a2a-samples | commit 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
- SDK: github.com/vbcherepanov/a2a-php-sdk · Packagist
- Symfony bundle: github.com/vbcherepanov/a2a-symfony-bundle · Packagist
- Protokol: a2a-protocol.org
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.