A2A server 

nr-llm can answer other agents that speak the Agent2Agent protocol (A2A) 1.0 over JSON-RPC. It publishes selected LLM configurations as skills, and a registered remote client sends a message to one of them. The message becomes an agent run that a worker executes; the client receives the result as an A2A task (ADR-280, ADR-282, ADR-283).

This page covers stage 3 of spec 051. A client cannot continue a task with a second message yet (stage 4), and streaming, push notifications and file parts are not offered (stages 5a to 5c).

Switch it on 

  1. Set Answer A2A requests (server role) (a2a.server.enabled) in the extension configuration. Off, no site answers an A2A request, whatever its settings say.
  2. Name the base URL in the settings of each site that should answer, for example in config/sites/<site>/settings.yaml:

    nrllm:
      a2a:
        baseUrl: 'https://www.example.org/a2a'
    Copied!

    The JSON-RPC endpoint is this URL; the agent card is published at https://www.example.org/a2a/.well-known/agent-card.json. A site without the setting answers neither. Every URL in the card and in a response is built from this setting, never from the request's Host header; a request to another host, scheme or port is not an A2A request and passes on to the site.

  3. Run a worker. A task is accepted only while a worker can be shown to run: a messenger:consume consumer with a heartbeat, or a tick of nrllm:agent:work within the last 120 seconds. Schedule nrllm:agent:work every minute on an installation without a consumer. nrllm:agent:status --transport=sweep --minimum-workers=1 checks the sweep. Without a worker, SendMessage answers HTTP 503 with WORKER_UNAVAILABLE and stores nothing.

Publish a skill 

Create an A2A Publication Profile record (administrators only).

Skill id, name, description, tags, examples
What the agent card lists for the skill. The skill id is how a client names it.
Domain handler
What turns the client's message into the run and the run's answer into the result. nr_llm.text sends the message text to the LLM configuration and returns the answer as one text artifact: a text check or a summary, with the instructions of the configuration's system prompt.
LLM configuration
The configuration the runs use. If its answer level keeps no result (metadata or none, set on its provider), the profile cannot be public, and a call is refused before a task exists with RESULT_NOT_RETAINABLE (ADR-282). A configuration that is switched off, or restricted to backend groups the client's technical user is not in, refuses a call with PROFILE_UNAVAILABLE. The agent card lists a skill only while its handler is available and its configuration is switched on and keeps a result.
Tools
Comma-separated tool names the run may use. A profile that may not write offers only tools that declare that they only read: a list with any other tool is not stored, and an existing list stays as it was. Empty: no tools.
Active task quota
How many tasks of this skill, from all clients together, may be queued or running at once. 0: no limit of its own.
Public
Listed on the card and callable. A profile that is not public cannot be called by any client.

Stage 3 accepts and returns text/plain only.

Register a client 

Create an A2A Client record (administrators only):

Technical backend user
Every task of the client runs as this backend user, with exactly its permissions. It must be enabled and must not be an administrator; the record cannot be enabled otherwise, and a request is refused if the user changed since. Prepare a user for this purpose.
Workspace
The workspace the client's runs act in; 0 is live.
Skills it may call
The publication profiles the client may use. A client allowed one skill needs to name none; a client allowed several names the skill in each message, as SendMessageRequest.metadata member https://nr-llm.netresearch.de/a2a/ext/profile/v1 with the skill id as value. Otherwise the call is refused with PROFILE_REQUIRED.
Task limit
How many of the client's tasks may be queued or running at once.
Enabled, expires
A disabled or expired client is refused like an unknown one.

Then issue its token:

vendor/bin/typo3 nrllm:a2a:client:token <client uid>
Copied!

The command prints the token once. The record keeps only its public prefix and its SHA-256; nobody can read the token from the database. Running the command again rotates the token: the old one stops working at once, and the client keeps its tasks, which belong to the record, not to the token. The client sends the token as Authorization: Bearer <token>.

What a client gets 

SendMessage
Creates a task and a queued run. The request never executes the run. Without returnImmediately, the request waits until the task is finished, failed, cancelled, rejected or waiting for input, at most a2a.server.blockingWaitSeconds (default 20, at most 100). After that it answers HTTP 504 with JSON-RPC -32603, reason BLOCKING_DEADLINE_EXCEEDED and the task id; the task goes on and is read with GetTask. A repeated messageId of the same client returns the task the first one created.
GetTask, ListTasks, CancelTask
Only the client's own tasks. Another client's task and a task that does not exist are the same -32001. ListTasks sorts by status time, newest first, pages with an opaque token bound to the client, its skills and the filter, and leaves out artifacts unless includeArtifacts is true.
Task states
SUBMITTED while queued, WORKING while running (also while the run waits for an approval in the backend or for a remote agent), COMPLETED with the answer, FAILED with a fixed message that names no internal detail, CANCELED, and REJECTED when the input was refused before the run started. The result is kept at the run's answer level and purged with the run (ADR-282).

Limits and protection 

  • Proxy timeouts. A waiting request holds a PHP-FPM worker. Keep a2a.server.blockingWaitSeconds below the read timeout of every proxy in front of PHP-FPM; a proxy that cuts the request earlier leaves the client with a transport error and the task unchanged.
  • Waiting requests. At most a2a.server.maxWaitingRequests (default 4) blocking requests wait at the same time on the installation; one more is refused with HTTP 503, WAITING_CAPACITY_EXHAUSTED. A request with returnImmediately takes no waiting slot.
  • Request size. A body above a2a.server.maxRequestBytes (default 1 MiB) is refused with HTTP 413 before it is parsed; a message has at most 16 parts.
  • Failed logins. A failed authentication is logged with the source address, never with the token, and counted per address: after 10 failures in 15 minutes a failed request from that address gets HTTP 429 until the window passes. A request with a valid token is not limited. The rate limiter is nrllm-a2a-auth; override it like TYPO3's own limiters in $GLOBALS['TYPO3_CONF_VARS']['SYS']['rateLimiter']['nrllm-a2a-auth']. Behind a reverse proxy, name it in $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP']; otherwise every client counts as the proxy's address and shares one limit.
  • Refusals name their reason (error.data holds a google.rpc.ErrorInfo) and, where a retry may help, a Retry-After header.