A2A Core
A2A Core and 1.0 Compatibility
ProtoLink is an A2A-first runtime. AgentCard, Task, Message, Part,
Artifact, task states, and discovery are first-class concepts throughout its
Python API, structured flows, agent delegation, storage, and observability.
ProtoLink then adds the execution substrate A2A leaves open: pluggable LLMs,
native and MCP tools, transports, registry services, state, policy,
authentication, logging, and telemetry.
From native A2A 0.3 primitives to A2A 1.0
ProtoLink was originally built natively on the A2A 0.3
specification: AgentCard,
Task, Message, Part, Artifact, task states, and discovery became its
public Python objects and its internal runtime language. Here, 0.3 is the A2A
protocol version, not the ProtoLink package version, and native means that
these primitives underpin the runtime itself rather than existing only in an
integration adapter.
That foundation gives local and remote agents one small, typed language for
identity, work, results, and lifecycle. Because it is independent of any model
provider or transport, the same simple Agent and Task API can support local
inference, native and MCP tools, deterministic flows, persistence, and remote
delegation. ProtoLink extended the A2A 0.3 model with execution semantics such
as inference instructions, tool actions, flow state, runtime context, and
events; those are ProtoLink runtime features, not additional A2A operations.
As A2A evolved to 1.0, its canonical cards, operations, and wire shapes evolved
too. ProtoLink keeps its established native runtime model and places A2A 1.0
interoperability at an explicit, versioned boundary. With a2a=True, a standard
peer discovers a canonical Agent Card and exchanges canonical messages, tasks,
parts, artifacts, and states through the implemented JSON-RPC operations.
ProtoLink translates that surface to the same executor used by its native API.
Compatibility therefore comes from explicit two-way translation and TCK
verification, not from claiming that the A2A 0.3-based runtime objects are
identical to the A2A 1.0 wire schema.
This design lets an A2A 1.0 peer communicate with a ProtoLink agent without forcing application code, tools, LLMs, or flows to adopt a second API.
This page addresses the narrower question of canonical A2A 1.0 wire compatibility for the implemented JSON-RPC surface. That is a property of the versioned HTTP adapter and must be tested independently from the A2A-based runtime architecture. The official A2A Technology Compatibility Kit (TCK) is the source of evidence.
Enable the A2A 1.0 boundary
ProtoLink's native A2A 0.3-based runtime primitives are always present. Translation for the implemented A2A 1.0 JSON-RPC surface is an explicit HTTP capability:
from protolink import Agent, AgentCard
agent = Agent(
card=AgentCard(
name="planner",
description="Builds execution plans",
url="http://127.0.0.1:8000",
),
transport="http",
a2a=True,
)
The default a2a=False is the pre-0.6.6 behavior: HTTP serves ProtoLink's
native task, card, status, health, chat, and control endpoints, and outbound
calls use the native contract. a2a=True requires the exact HTTP transport and
adds both:
- Inbound translation:
GET /.well-known/agent-card.jsonandPOST /expose the implemented A2A 1.0 Agent Card and JSON-RPC operations. - Outbound translation: the agent's existing
AgentClientcan discover and call a standard A2A 1.0 JSON-RPC peer.
The native endpoints stay mounted, so enabling A2A does not change
handle_task(Task), registry/state APIs, status/chat utilities, or normal
ProtoLink-to-ProtoLink calls.
Choose the outbound protocol
from protolink import Task
task = Task.create_infer(prompt="Prepare a release plan")
# Default: prefer ProtoLink when the peer has a native card, otherwise discover A2A.
result = await agent.call_agent(peer_url, task, protocol="auto")
# Explicit choices skip the native-vs-A2A selection step.
result = await agent.call_agent(peer_url, task, protocol="protolink")
result = await agent.call_agent(peer_url, task, protocol="a2a")
"auto" probes the native ProtoLink card first. Only a definitive 404 or
405 moves discovery to the standard A2A Agent Card, and the result is cached
for five minutes. This process-local protocol cache is bounded to 1,024 peers;
expired and oldest entries are removed. Authentication, connection, timeout,
and server errors are not treated as evidence that the peer uses another
protocol, avoiding an unsafe second task submission. Native remains preferred
when a peer exposes both surfaces because it preserves ProtoLink-specific task
and flow information.
An explicit protocol="protolink" call goes directly to the native task route.
An explicit protocol="a2a" call still fetches and validates the standard Agent
Card so ProtoLink can select its compatible JSON-RPC 1.0 interface; it only
skips the native-versus-A2A choice.
A standalone client uses the same opt-in:
from protolink.client import AgentClient
client = AgentClient(
transport="http",
url="http://127.0.0.1:0",
a2a=True,
)
card = await client.get_a2a_agent_card(peer_url)
result = await client.send_task(peer_url, task, protocol="a2a")
Trust advertised interface URLs
The secure default is a2a_allow_cross_origin=False. Before sending a task or
credentials, ProtoLink requires the JSON-RPC interface advertised by the
standard Agent Card to use the same origin: scheme, hostname, and effective
port. A cross-origin card is rejected before its interface receives a request.
Some deployments intentionally publish discovery and execution on different origins. Enable that behavior only when the advertised endpoint is explicitly trusted:
client = AgentClient(
transport="http",
url="https://discovery.example",
a2a=True,
a2a_allow_cross_origin=True,
)
The compact Agent facade always keeps this same-origin policy. The override
belongs only to a dedicated AgentClient: it trusts any cross-origin interface
advertised by the discovered card and is not an origin allowlist, so use it only
when that split-origin deployment is under your control.
This page describes an engineering harness, not a compatibility claim.
ProtoLink should only claim a passing A2A 1.0 binding after the official TCK
finishes successfully and its report is published. A passing internal transport
test, a working Agent, or a valid-looking Agent Card is not a substitute.
The commands below pin the TCK to commit
5996b79f9cefa6fc390980e383e358a66fb9e49e.
Pinning makes local and CI results reproducible even while the upstream TCK
continues to evolve.
What the TCK expects
Before running operation tests, the TCK fetches:
http://127.0.0.1:9999/.well-known/agent-card.json
The A2A 1.0 card must declare at least one entry in supportedInterfaces.
This harness starts with only JSONRPC, keeping the first compatibility target
narrow and auditable. The TCK then sends JSON-RPC 2.0 requests with A2A method
names such as SendMessage, GetTask, ListTasks, and CancelTask. It also
checks canonical A2A data shapes, error mappings, version handling, and any
capabilities declared by the card. See the A2A 1.0
specification for the normative
contract.
The adapter keeps at most 1,024 tasks for one hour in the serving
Agent process. Expired and oldest inactive tasks are removed first; active
operations are protected from eviction. If every retained slot is active, a new
task is rejected instead of silently dropping live work. The index is still
in-memory and disappears on restart. A deployment that fans one logical agent
out across multiple worker processes must add a shared task router or store
before presenting those workers as one A2A interface.
Do not point the TCK at the legacy /.well-known/agent.json endpoint. A 404 at
/.well-known/agent-card.json means the A2A 1.0 binding is not ready yet; it
must not be worked around by weakening the preflight check.
Implemented JSON-RPC scope
The 1.0 adapter currently implements:
SendMessage, including blocking andreturnImmediatelyexecution modes.GetTask,ListTasks, andCancelTaskover a process-local task index.- Task visibility scoped to the request tenant and, when the HTTP agent uses a
ProtoLink
Authenticator, the authenticated principal. - History limits, task filtering, descending status-time ordering, pagination,
and the
includeArtifactslist policy. - Canonical card, task, message, part, artifact, enum, timestamp, and
google.rpc.ErrorInfoJSON shapes. - A2A version and HTTP content-type validation.
The card declares streaming, push notifications, and the extended card as disabled. Calls to those operations return their standard A2A errors instead of silently advertising or emulating unsupported capabilities.
Translation and portability
ProtoLink keeps protocol translation at the boundary rather than changing its runtime execution rules:
- An inbound A2A
ROLE_USERtext part remains a ProtoLinkPart(type="text"), so custom handlers see canonical text. The adapter marks the task withtask.metadata["a2a_inbound"]; when an LLM is configured, the default Agent engine recognizes that metadata and treats the text as an inference request. - An outbound ProtoLink
inferprompt becomes a standard A2A text part. - Text, JSON data, raw/file content, URI content, messages, artifacts, task status, and supported security declarations have canonical translations.
- A2A servers assign their own task IDs. ProtoLink preserves the caller's local task ID and stores the remote ID, context, state, and status timestamp in namespaced task metadata for continuation and cancellation.
send_message()returns a responseMessage. If an A2A peer completes with artifacts but no response message, usesend_task()to receive and inspect the complete task rather than losing the artifacts behind the convenience API.
Some native ProtoLink semantics are intentionally not portable. tool_call
parts, flow_state, run budgets and context, registry/state control operations,
and arbitrary framework metadata have no universal A2A execution meaning.
Unknown part types can cross as structured data tagged with
protolinkPartType, but an arbitrary A2A peer is not required to understand
that extension. Use protocol="protolink" when two ProtoLink agents need the
full native contract; "auto" already makes that the preferred path.
Outbound task IDs and cancellation
The local-to-remote task-ID mappings used for A2A continuation and cancellation are process-local, bounded to 1,024 entries, and expire after one hour. Oldest entries are removed when the bound is exceeded. Restarting the process or losing a mapping means the client can no longer safely cancel that remote task by its local ProtoLink ID.
cancel_task(..., reason=..., metadata=...) translates both values through A2A
CancelTask metadata. The inbound adapter reconstructs the corresponding
ProtoLink cancellation request, including its reason and metadata.
A blocking outbound A2A SendMessage does not expose the server-assigned task
ID until its response is returned. Consequently, that operation cannot be
canceled through this client while the initial call is still blocked. In
protocol="auto", ProtoLink first confirms that the peer is A2A and raises
A2AClientError when no mapping exists; it never guesses by sending the local
ID to ProtoLink's native cancellation route.
Roadmap beyond 0.6.6
The following A2A 1.0 capabilities remain future work and are advertised as disabled today:
SendStreamingMessageandSubscribeToTaskstreaming operations.- Task push-notification configuration and delivery.
- Extended authenticated Agent Cards.
Until those capabilities land with focused tests and TCK evidence, use ProtoLink's native SSE, WebSocket, gRPC, or runtime streaming APIs where live events are required.
Run locally
The fixture uses a normal ProtoLink Agent with transport="http", a2a=True. Its
handle_task() implementation is deterministic and provider-free, so the TCK
does not need an API key or an LLM account.
From the ProtoLink repository root, install the HTTP dependencies and start the fixture:
uv pip install -e ".[test,http]"
uv run python examples/a2a_tck_agent.py
Leave that process running. In a second terminal, verify the A2A 1.0 discovery endpoint before cloning or running the TCK:
curl --fail --silent \
http://127.0.0.1:9999/.well-known/agent-card.json \
| python -m json.tool
Clone and pin the official TCK outside the ProtoLink checkout:
git clone https://github.com/a2aproject/a2a-tck.git /tmp/a2a-tck
git -C /tmp/a2a-tck checkout --detach \
5996b79f9cefa6fc390980e383e358a66fb9e49e
cd /tmp/a2a-tck
uv sync --frozen
Run the mandatory JSON-RPC requirements first:
uv run ./run_tck.py \
--sut-host http://127.0.0.1:9999 \
--transport jsonrpc \
--level must \
-v
The TCK writes these artifacts under /tmp/a2a-tck/reports/:
compatibility.jsonfor machine-readable requirement results.compatibility.htmlfor the human-readable compatibility report.tck_report.htmlfor the underlying pytest report.junitreport.xmlfor CI systems.
Re-run one failure with full logs by passing pytest arguments after --:
uv run ./run_tck.py \
--sut-host http://127.0.0.1:9999 \
--transport jsonrpc \
--level must \
--verbose-log \
-- -k "CORE-SEND-001" --tb=long
Recorded baseline
On 14 July 2026, the command above at the pinned commit completed with:
1 failed, 67 passed, 167 skipped, 30 deselected
The sole failure is the generic CORE-SEND-003 requirement test. ProtoLink
returns the required JSON-RPC ContentTypeNotSupportedError (-32005) for the
unsupported media type, and the TCK's dedicated error-code test passes. At this
pin, however, the upstream CORE-SEND-003 requirement
entry
omits its expected_error. The generic requirement
runner
therefore treats the required error response as a failed operation.
This is an upstream defect in the pinned TCK, but the checked-in workflow is deliberately left red: ProtoLink does not patch the conformance kit or convert the failure into a pass. Re-run against a newer, explicitly pinned TCK commit after upstream corrects the requirement entry, and record that new pin and report before making a passing claim.
Interpreting a result
A successful, unmodified --transport jsonrpc --level must run at an
explicitly recorded TCK commit supports a precise statement such as:
ProtoLink passed the A2A 1.0 JSON-RPC MUST suite at TCK commit
<commit>.
It does not demonstrate gRPC or HTTP+JSON compatibility, and it does not mean that every SHOULD or MAY requirement was exercised. Run the unfiltered suite and publish its exact report before making a broader claim.
Manual CI probe
The A2A TCK GitHub Actions workflow is intentionally available only through
workflow_dispatch. It starts the same provider-free fixture, enforces the
1.0 Agent Card preflight, runs the pinned JSON-RPC MUST suite, and uploads the
logs and reports even after a failure. It is not a required pull-request check
and currently surfaces the known CORE-SEND-003 failure described above. It
should not be represented as a compatibility badge until an unmodified,
explicitly pinned TCK report passes.