a2a-agent-sdk
A framework-agnostic SDK for building A2A-compliant
agent runners that work with kagent's BYO (bring-your-own) agent
pattern. Plug in any agent implementation — LangGraph, a plain function, a
different framework entirely — and get a fully working A2A service for free:
agent-card discovery, message/send, message/stream, health, and API-key auth.
The point of this SDK: your agent implementation can change completely, but the input your callers send and the output they get back never do.
Install
pip install -e /path/to/a2a-agent-sdk
(Not yet published to a package index — install from a local checkout or a git URL.)
Quickstart
Implement one method, invoke, that takes the conversation so far and returns text:
from a2a_agent_sdk import A2ARunner, RunnerConfig, Message
class MyAgent:
def invoke(self, messages: list[Message]) -> str:
last_user_text = messages[-1].content
return f"You said: {last_user_text}"
runner = A2ARunner(
agent=MyAgent(),
config=RunnerConfig(name="My Agent", description="Echoes the user."),
)
app = runner.app # a plain FastAPI instance
Run it:
uvicorn app:app --host 0.0.0.0 --port 8080
Try it:
curl -X POST http://localhost:8080/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"parts":[{"kind":"text","text":"hello"}]}}}'
See examples/function_agent/ for the complete runnable version of this example.
Adding real streaming
If your agent can produce text incrementally, implement astream too and
message/stream will emit it token-by-token instead of as one chunk:
from typing import AsyncIterator
class MyStreamingAgent:
def invoke(self, messages: list[Message]) -> str:
return "hello there friend "
async def astream(self, messages: list[Message]) -> AsyncIterator[str]:
for word in "hello there friend".split():
yield word + " "
If you don't implement astream, message/stream still works — the SDK calls
invoke() once and emits the whole result as a single chunk. Callers never need
to know which case they're in.
Any agent that implements astream (in addition to invoke) satisfies the
StreamingAgent protocol, importable from a2a_agent_sdk. It's not required
anywhere in the SDK's own code, but it's useful for type-annotating a variable
that should support real streaming, or for an isinstance(agent, StreamingAgent)
check in your own code.
Plugging in LangGraph
Nothing about the SDK is LangGraph-specific — it just needs an object with
invoke/astream. See examples/langgraph_agent/ for a complete example that
bridges a compiled StateGraph to the Agent protocol:
class LangGraphAgent:
def __init__(self, graph):
self._graph = graph
def invoke(self, messages: list[Message]) -> str:
result = self._graph.invoke({"messages": [(m.role, m.content) for m in messages]})
return result["messages"][-1].content
async def astream(self, messages: list[Message]):
graph_input = {"messages": [(m.role, m.content) for m in messages]}
async for chunk, _meta in self._graph.astream(graph_input, stream_mode="messages"):
if chunk.content:
yield chunk.content
Swap LangGraphAgent for a class wrapping any other framework and the rest of
your deployment — endpoints, request/response shapes, auth, agent card — doesn't
change at all.
RunnerConfig reference
| Field | Env var (from_env()) |
Purpose |
|---|---|---|
id |
AGENT_ID |
Internal identifier; used as an Agent Card name fallback |
name |
AGENT_NAME |
Agent Card name |
description |
AGENT_DESCRIPTION |
Agent Card description |
url |
AGENT_URL |
Agent Card url — the endpoint callers should hit |
api_key |
RUNNER_API_KEY |
If set, invoke requires a matching key (see Auth below) |
skills |
— (construct SkillDescriptor list directly) |
Skills advertised on the Agent Card |
RunnerConfig deliberately does not include model/temperature/prompt/skill
content — those belong inside your own Agent implementation, since the SDK
doesn't assume you're even calling an LLM.
config = RunnerConfig.from_env() # read AGENT_*/RUNNER_API_KEY from the environment
config = RunnerConfig(name="My Agent") # or construct directly
Advertising skills
Pass a list of SkillDescriptors to advertise what your agent can do:
from a2a_agent_sdk import RunnerConfig, SkillDescriptor
config = RunnerConfig(
name="My Agent",
skills=[SkillDescriptor(name="Refund Lookup", description="Looks up refund status", tags=["billing"])],
)
These show up in the Agent Card's skills array so A2A clients can discover
what your agent is capable of.
Endpoints
| Route | Behavior |
|---|---|
GET /health |
{status, ready, agent_id, agent_name} |
GET /.well-known/agent-card.json, GET /.well-known/agent.json |
A2A Agent Card |
POST /, POST /a2a/message |
message/stream → SSE; anything else → JSON-RPC task result |
Both POST routes accept the same payload shapes: JSON-RPC message/send/
message/stream, a direct {"message": {...}} body, or a bare
{"parts": [...]}/{"text": "..."} shortcut for local testing.
Auth
If RunnerConfig.api_key is set, every POST / / POST /a2a/message request
must present a matching key, either as:
- the
X-API-Keyheader, or params.metadata.runner_api_keyin the JSON-RPC payload (for proxies that don't forward custom headers, e.g. kagent's A2A proxy)
If api_key is left unset, the check is skipped entirely — useful for local
development. There's no third state: either a key is configured and enforced,
or it isn't configured and nothing is enforced.
Errors
| Status | When |
|---|---|
400 |
No user text found in the request |
401 |
api_key is configured and the request's key is missing/invalid |
502 |
agent.invoke/agent.astream raised an exception (streaming: emitted as a failed status event on the open connection instead) |
Deployment
The SDK produces a plain FastAPI app (runner.app) — deploy it with any
ASGI server. For parity with kagent's BYO agent expectation, serve on port
8080:
uvicorn app:app --host 0.0.0.0 --port 8080
Examples
examples/function_agent/— the smallest possible agent: one method, no framework.examples/langgraph_agent/— the same SDK, with a LangGraph graph underneath.
Install example dependencies and run their tests with:
pip install -e ".[dev,examples]"
pytest examples/function_agent/test_app.py -v
pytest examples/langgraph_agent/test_app.py -v
Run each example's tests as a separate pytest invocation, not combined — both use bare
(non-packaged) app/agent/test_app modules so they can be run exactly like an external
consumer would (uvicorn app:app), which means pytest can't collect both test_app.py files
in the same session.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file a2a_agent_sdk-0.1.0.tar.gz.
File metadata
- Download URL: a2a_agent_sdk-0.1.0.tar.gz
- Upload date:
- Size: 15.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d4a2cb3310b855c3f71fed2c19ee977e4f4844790b8479145e77c040c9f1dbd
|
|
| MD5 |
2476d6c1bf3816e62884b7434f4bb91b
|
|
| BLAKE2b-256 |
80de5f26ab6d0595af328961da49a93a44eb5e49544c062dc51213fcf0e501f4
|
File details
Details for the file a2a_agent_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: a2a_agent_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
34e8c120d1bf8a64096a7057a7f55653437dd1f5c5cd6e5724a2ea362860bbd3
|
|
| MD5 |
a3659a88aa23afcf85cc6f719f9d91a0
|
|
| BLAKE2b-256 |
cef4ff8053325788f773f6bda3f5eb3d130442e79400eacaecf7ac694f596970
|