Files
classic-models-agent-flows/adapter/README.md
T
Stéphane Tailland ed99334201 Deploy the adapter fix and document the four outcomes
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 14:39:25 +02:00

64 lines
3.0 KiB
Markdown

# a2a-n8n-adapter
An A2A facade in front of an n8n workflow. One image, one instance per agent,
configured entirely by environment.
The repo's notes proposed serving this from the gateway instead — a static Agent
Card and a route to the webhook, no new component. That does not reach. A2A is
not request/response: a caller gets a `Task` with an id, a context and a state
that can be `input-required` or `failed`, and none of that can be synthesised
from a webhook's single reply by a proxy. Templating the whole lifecycle in
gateway configuration is an adapter, written in the wrong language.
So: the alternative those same notes anticipated, written once for both agents.
## It owns the protocol and nothing else
It holds no Keycloak client and no credentials. It forwards the end user's token
to the workflow in `X-Subject-Token` — a header rather than the body, so a
workflow that logs its input does not log the token with it — and the workflow
exchanges it, because the component that can mint tokens should be the one that
uses them.
The workflow answers `{"answer": "...", "escalate": false}`. Anything else is
returned as raw text rather than guessed at: a workflow that has drifted from
the contract should look wrong, not be quietly reinterpreted.
`escalate: true` fails the task rather than completing it. An autonomous agent
that meets a rule needing approval must hand it back, never decide, and a
completed task is the wrong shape for "a human has to look at this".
## Configuration
| | |
|---|---|
| `ADAPTER_AGENT_NAME` | `approvisionnement` |
| `ADAPTER_AGENT_DESCRIPTION` | what the card says it is |
| `ADAPTER_SKILL_ID` | `check_availability` |
| `ADAPTER_SKILL_NAME`, `_DESCRIPTION`, `_EXAMPLES` | the card's one skill; examples are newline-separated |
| `ADAPTER_WEBHOOK_URL` | the workflow's production webhook |
| `ADAPTER_PUBLIC_URL` | what callers dial — the Service DNS name, not the bind address |
| `ADAPTER_CA_BUNDLE` | the lab CA, so TLS is verified rather than disabled |
One skill per instance, deliberately. A second one is a card change, which is
the moment to ask whether the agent has quietly become a data proxy.
## Four outcomes, and only one of them completes
HTTP 200 from n8n does not mean the workflow answered. It returns 200 with an
**empty body** when a node throws before the Respond node runs — a refused token
exchange is the common way in. The adapter used to read that as a completed task
carrying "(the workflow returned nothing)", which told the caller the task had
succeeded: the one thing certainly false.
| `Outcome` | A2A task state |
|---|---|
| `ANSWERED` — the contract, `escalate: false` | `completed` |
| `ESCALATED` — the contract, `escalate: true` | `failed` |
| `SILENT` — 200 with an empty body | `failed` |
| `OFF_CONTRACT` — a body that is not `{"answer", "escalate"}` | `failed` |
Off contract is returned **raw** so a workflow that has drifted is visible — but
never as agreement. Escalation is `failed` for the older reason: an autonomous
agent meeting a rule that needs approval must fail cleanly, never decide.