---
title: "ADR-278: A2A is an agent interface in both roles"
manual: "LLM Foundation"
version: "main"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-278@main"
source: "Adr/Adr278A2aIsAnAgentInterfaceInBothRoles.rst"
rendered: "2026-11-12T15:41:35+00:00"
---

# ADR-278: A2A is an agent interface in both roles {#adr-278}

-   *Status:* Accepted
-   *Date:* 2026-11-10
-   *Authors:* Netresearch DTT GmbH

## Context {#context}

The Agent2Agent protocol (A2A) lets one agent give a task to another over
HTTP and follow it to a result. Two requests reached nr-llm: a run should be
able to hand a step to a remote agent, and an installation should be able to
offer some of its configurations to remote agents. MCP
([ADR-116](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-116@main), [ADR-259](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-259@main)) does not cover either:
an MCP server offers tools that the calling model drives step by step, while an
A2A agent accepts a task, runs it on its own and reports a state and artifacts.

A2A 1.0 was published as tag v1.0.1 of `a2aproject/A2A`. The specification
names `specification/a2a.proto` as its single normative definition (its
section 1.4); the prose disagrees with the proto in several places, the most
consequential being that `SendMessage` blocks by default
(`SendMessageConfiguration.return_immediately` defaults to false) while the
prose of section 3.1.1 says it returns immediately. A v1.1.0 release candidate
was tagged on 2026-10-29 and adds fields and an error code. Three bindings
exist: JSON-RPC, gRPC and HTTP+JSON/REST. Protocol 0.3 is still in use and is
not compatible with 1.0 (a different blocking flag, a `kind` discriminator,
other member names).

## Decision {#decision}

**nr-llm speaks A2A in both roles.** As a client, a run delegates to a remote
agent an administrator registered. As a server, nr-llm publishes selected
configurations as skills to remote clients an administrator registered. The
roles share the protocol code and nothing else; each can be enabled alone.

**Protocol 1.0, JSON-RPC binding, nothing else.** Requests carry
`A2A-Version: 1.0`. Negotiation compares major and minor only, as the
specification requires: the server accepts `1.0` and any `1.0.x`, and the
client accepts a card interface whose `protocol_version` is `1.0` or
`1.0.x`. The server answers an absent version (which the specification
defines as 0.3) and every other major or minor version with
`VersionNotSupportedError` (`-32009`). The REST and gRPC bindings and
protocol 0.3 are not built in either role; they are added when a partner needs
one, by a decision of their own. JSON-RPC is one of the three transports the
conformance kit (`a2aproject/a2a-tck`) tests, needs no protobuf runtime on
the wire, and runs in a PHP-FPM request.

**The normative source is the proto, pinned.** `a2a.proto` at v1.0.1 (sha256
`e195bf96ab630c69797851970203e1b2b6b19528f2e9803b7d904b91a5104016`) is
committed as a test fixture. Where prose and proto disagree, nr-llm implements
the proto, and each disagreement is a requirement with its own test
(`specs/051-a2a-agent-interface`, "Prose and proto"). Documentation links
point at `https://a2a-protocol.org/v1.0.1/specification/`; `/latest/`
follows the development branch.

**nr-llm writes its own JSON-RPC layer and value objects; no A2A SDK is a
runtime dependency.** No official PHP SDK exists. The stage-0 spike (report of
2026-11-10, branch `spike/a2a-stage0`) measured `praveendias1180/a2a-php`
v1.0.3, generated from the v1.0.0 proto (which differs from v1.0.1 in comments
only):

-   Its pure-PHP protobuf runtime (google/protobuf v5.36.2) calls `bccomp()`
    for every int32 and timestamp read from JSON. Without `ext-bcmath`, which
    TYPO3 14.3 neither requires nor suggests, its core suite fails with 40 errors
    and 18 failures, and `ListTasks` against `a2a-python` 1.3.0 died with an
    uncaught `Error`. The substitutes are `ext-bcmath`, `ext-protobuf`
    (3 failures remain) or a polyfill (phpseclib plus two packages, 374 files).
-   Its client parses responses with unknown fields rejected; a task carrying
    v1.1 fields (`generation`, `status.progressHint`) failed with
    "progressHint is unknown". That breaks section 5.7 and the forward
    compatibility below. `a2a-python` 1.3.0 read the same responses.
-   nr-llm needs its own transport anyway (address guard, explicit 3xx refusal,
    the vault credential; [ADR-279](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-279@main)) and its own public types for
    the client API. In the spike the transport took 95 code lines and a
    hand-written JSON-RPC client 39, which passed every interop cell the SDK
    passed plus the missing-version cell the SDK cannot produce.
-   It adds four packages, 655 files and 4,680 kB, one of them a Composer
    plugin; on 2026-11-10 it had 203 downloads, one dependent and one
    maintainer.

The SDK and `a2a-python` 1.3.0 remain test partners in an interop CI job.

**Parsing is forward-compatible.** Unrecognised fields are ignored, as the
specification asks (section 5.7). A stream or response member of an unknown
kind is skipped and logged, not treated as an error, so a v1.1 server does not
break the client. An unknown task state is never read as completed, and an
unknown `Part` kind is skipped and logged rather than read as an empty part.
Optional fields stay optional: `a2a-python` 1.3.0 sends a SUBMITTED status
without `timestamp`.

## Consequences {#consequences}

-   ●● One protocol version and one binding keep the surface testable: one
    proto fixture, one codec, one TCK transport.
-   ● Implementing the proto where the prose differs follows the source the
    specification itself makes normative; the interop job against
    `a2a-python` 1.3.0 checks that a reference SDK reads it the same way.
-   ◑ A partner that speaks only 0.3, REST or gRPC cannot connect until a
    further decision adds it.
-   ◑ A v1.1 server can send things the client skips; nothing of v1.1 is used.
-   ✕ Two new attack surfaces exist where there were none; ADR-280 and the
    specification's security requirements bound them.

Net Score: +3

## Alternatives considered {#alternatives-considered}

**Client role only.** Rejected: the server role was requested, and building the
protocol code once for both roles costs less than twice.

**Support 0.3 as well.** Rejected for now: two incompatible wire formats double
the codec and the tests for no current partner.

**Use** `praveendias1180/a2a-php` **as codec or client.** Rejected for the
measured reasons in the decision: a bcmath requirement TYPO3 does not carry,
strict parsing against section 5.7, and nothing left for it to do once the
transport and the public types are nr-llm's own.

**Follow the prose where it is explicit.** Rejected: the specification itself
makes the proto normative, and a non-blocking `SendMessage` would break every
1.0 client that relies on the default.
