A2A Protocol (Agent-to-Agent)
Kaman implements the A2A protocol (v0.3), enabling agent-to-agent communication over JSON-RPC 2.0. This allows external agents, orchestrators, and multi-agent systems to discover and interact with Kaman agents using a standardized protocol.
Base URL
/api/a2a
For self-hosted installations: http://kaman.ai/api/a2a
Authentication
All endpoints (except public agent card discovery) require a Bearer token:
Authorization: Bearer <your-kaman-token>
Endpoints Overview
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/a2a/agents | Required | List all available agents |
| GET | /api/a2a/{expertId}/.well-known/agent-card.json | Optional | Agent discovery card |
| POST | /api/a2a/{expertId} | Required | JSON-RPC 2.0 dispatcher |
| GET | /api/a2a/tasks/{taskId} | Required | Get task status |
Agent Discovery
List All Agents
curl http://kaman.ai/api/a2a/agents \
-H "Authorization: Bearer $KAMAN_TOKEN"
Response:
{
"agents": [
{
"id": "42_0",
"name": "Sales Assistant",
"description": "Handles sales inquiries and CRM operations",
"url": "http://kaman.ai/api/a2a/42_0",
"cardUrl": "http://kaman.ai/api/a2a/42_0/.well-known/agent-card.json"
}
],
"total": 1
}
Agent Card (Discovery)
The agent card is the A2A standard mechanism for discovering agent capabilities. It can be accessed without authentication (returns minimal info) or with a token (returns full capabilities).
curl http://kaman.ai/api/a2a/42_0/.well-known/agent-card.json
Response (Authenticated):
{
"name": "Kaman Agent 42_0",
"description": "Sales Assistant — Handles sales inquiries and CRM operations",
"version": "1.0",
"url": "http://kaman.ai/api/a2a/42_0",
"provider": {
"organization": "Yoctotta",
"url": "http://kaman.ai"
},
"capabilities": {
"streaming": true,
"pushNotifications": false,
"extendedAgentCard": true
},
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain", "application/json"],
"skills": [
{
"id": "getQuarterlySales",
"name": "getQuarterlySales",
"description": "Retrieves quarterly sales data",
"tags": ["sales", "reporting"],
"examples": ["What were Q3 sales?"]
},
{
"id": "sendEmail",
"name": "sendEmail",
"description": "Sends an email to specified recipients",
"tags": ["email", "communication"]
}
],
"securitySchemes": {
"bearer": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
},
"security": [{"bearer": []}]
}
Unauthenticated: Returns a minimal card with empty skills and generic description.
JSON-RPC 2.0 Methods
All methods are sent as POST requests to /api/a2a/{expertId} using JSON-RPC 2.0 format.
Message Format
Messages use A2A's multi-part format:
{
"role": "user",
"parts": [
{"kind": "text", "text": "Analyze the sales data"},
{"kind": "file", "file": {"url": "https://...", "mimeType": "text/csv"}},
{"kind": "data", "data": {"quarter": "Q3", "year": 2024}}
],
"messageId": "msg-123",
"contextId": "ctx-456",
"taskId": "task-789"
}
Part types:
| Kind | Fields | Description |
|---|---|---|
text | text | Plain text content |
file | file.url, file.bytes, file.mimeType, file.name | File attachment (URL or base64) |
data | data | Structured JSON data |
message/send — Non-Streaming
Send a message and wait for the complete response.
Request:
{
"jsonrpc": "2.0",
"id": "req-1",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [
{"kind": "text", "text": "What were last quarter's sales?"}
],
"messageId": "msg-001"
}
}
}
Response:
{
"jsonrpc": "2.0",
"id": "req-1",
"result": {
"id": "task-abc123",
"contextId": "ctx-def456",
"status": {
"state": "completed",
"timestamp": "2024-02-25T10:30:00Z"
},
"artifacts": [
{
"artifactId": "art-001",
"name": "response",
"parts": [
{"kind": "text", "text": "Last quarter's total sales were $2.4M, up 12% from Q2."}
]
}
],
"history": [
{
"role": "user",
"parts": [{"kind": "text", "text": "What were last quarter's sales?"}],
"messageId": "msg-001"
},
{
"role": "agent",
"parts": [{"kind": "text", "text": "Last quarter's total sales were $2.4M, up 12% from Q2."}],
"messageId": "msg-002"
}
],
"kind": "task"
}
}
message/stream — Streaming
Send a message and receive Server-Sent Events as the agent works.
Request: Same as message/send, but use method message/stream.
{
"jsonrpc": "2.0",
"id": "req-2",
"method": "message/stream",
"params": {
"message": {
"role": "user",
"parts": [{"kind": "text", "text": "Summarize the latest report"}]
}
}
}
SSE Response:
data: {"jsonrpc":"2.0","id":"req-2","result":{"kind":"status-update","taskId":"task-xyz","contextId":"ctx-abc","status":{"state":"working","timestamp":"2024-02-25T10:30:00Z"},"final":false}}
data: {"jsonrpc":"2.0","id":"req-2","result":{"kind":"artifact-update","taskId":"task-xyz","contextId":"ctx-abc","artifact":{"artifactId":"art-001","parts":[{"kind":"text","text":"The report shows "}],"lastChunk":false,"append":true}}}
data: {"jsonrpc":"2.0","id":"req-2","result":{"kind":"artifact-update","taskId":"task-xyz","contextId":"ctx-abc","artifact":{"artifactId":"art-001","parts":[{"kind":"text","text":"revenue growth of 15% year-over-year."}],"lastChunk":true,"append":true}}}
data: {"jsonrpc":"2.0","id":"req-2","result":{"kind":"status-update","taskId":"task-xyz","contextId":"ctx-abc","status":{"state":"completed","timestamp":"2024-02-25T10:30:05Z"},"final":true}}
Stream event types:
| Event Kind | Description |
|---|---|
status-update | Task state change (working, completed, failed, etc.) |
artifact-update | Partial content chunks (use append: true for incremental) |
tasks/get — Get Task Status
Retrieve a previously created task by ID.
{
"jsonrpc": "2.0",
"id": "req-3",
"method": "tasks/get",
"params": {"id": "task-abc123"}
}
You can also use the REST endpoint:
curl http://kaman.ai/api/a2a/tasks/task-abc123 \
-H "Authorization: Bearer $KAMAN_TOKEN"
tasks/cancel — Cancel a Task
Cancel a running task. Only works for tasks in non-terminal states.
{
"jsonrpc": "2.0",
"id": "req-4",
"method": "tasks/cancel",
"params": {"id": "task-abc123"}
}
Response: Updated task with state: "canceled".
Task Lifecycle
| State | Description |
|---|---|
submitted | Task received, queued for processing |
working | Agent is actively processing |
completed | Task finished successfully |
failed | Task encountered an error |
canceled | Task was canceled by client |
input_required | Agent needs human input (interrupt) |
auth_required | Agent needs auth confirmation |
rejected | Task was rejected |
Error Handling
Errors follow JSON-RPC 2.0 format:
{
"jsonrpc": "2.0",
"id": "req-1",
"error": {
"code": -32000,
"message": "Internal server error",
"data": {"detail": "Agent timeout after 300s"}
}
}
| Code | Meaning |
|---|---|
| -32700 | Parse error — invalid JSON |
| -32600 | Invalid request — missing required fields |
| -32601 | Method not found |
| -32602 | Invalid params |
| -32001 | Task not found |
| -32002 | Task in terminal state (cannot cancel) |
| -32000 | Server error |
Code Examples
Python
import requests
import json
import sseclient # pip install sseclient-py
BASE_URL = "http://kaman.ai/api/a2a"
TOKEN = "your-kaman-token"
HEADERS = {
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
}
# 1. Discover agents
agents = requests.get(f"{BASE_URL}/agents", headers=HEADERS).json()
agent_id = agents["agents"][0]["id"]
print(f"Using agent: {agents['agents'][0]['name']}")
# 2. Get agent card
card = requests.get(
f"{BASE_URL}/{agent_id}/.well-known/agent-card.json",
headers=HEADERS,
).json()
print(f"Skills: {[s['name'] for s in card['skills']]}")
# 3. Send message (non-streaming)
response = requests.post(
f"{BASE_URL}/{agent_id}",
headers=HEADERS,
json={
"jsonrpc": "2.0",
"id": "1",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"kind": "text", "text": "Hello, what can you do?"}],
}
},
},
)
result = response.json()["result"]
print(f"Status: {result['status']['state']}")
for artifact in result.get("artifacts", []):
for part in artifact["parts"]:
if part["kind"] == "text":
print(part["text"])
# 4. Stream message
response = requests.post(
f"{BASE_URL}/{agent_id}",
headers=HEADERS,
json={
"jsonrpc": "2.0",
"id": "2",
"method": "message/stream",
"params": {
"message": {
"role": "user",
"parts": [{"kind": "text", "text": "Analyze Q3 performance"}],
}
},
},
stream=True,
)
client = sseclient.SSEClient(response)
for event in client.events():
data = json.loads(event.data)
result = data.get("result", {})
if result.get("kind") == "artifact-update":
for part in result["artifact"]["parts"]:
if part["kind"] == "text":
print(part["text"], end="", flush=True)
elif result.get("kind") == "status-update":
if result["status"]["state"] == "completed":
print("\n[Done]")
TypeScript
const BASE_URL = "http://kaman.ai/api/a2a";
const TOKEN = "your-kaman-token";
// Discover agents
const agents = await fetch(`${BASE_URL}/agents`, {
headers: { Authorization: `Bearer ${TOKEN}` },
}).then((r) => r.json());
const agentId = agents.agents[0].id;
// Send message (non-streaming)
const response = await fetch(`${BASE_URL}/${agentId}`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: "1",
method: "message/send",
params: {
message: {
role: "user",
parts: [{ kind: "text", text: "Hello!" }],
},
},
}),
});
const { result } = await response.json();
console.log(result.artifacts[0].parts[0].text);
// Streaming
const stream = await fetch(`${BASE_URL}/${agentId}`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: "2",
method: "message/stream",
params: {
message: {
role: "user",
parts: [{ kind: "text", text: "Summarize report" }],
},
},
}),
});
const reader = stream.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
for (const line of text.split("\n")) {
if (!line.startsWith("data: ")) continue;
const data = JSON.parse(line.slice(6));
const event = data.result;
if (event?.kind === "artifact-update") {
for (const part of event.artifact.parts) {
if (part.kind === "text") process.stdout.write(part.text);
}
}
}
}
cURL
# List agents
curl http://kaman.ai/api/a2a/agents \
-H "Authorization: Bearer $KAMAN_TOKEN"
# Get agent card
curl http://kaman.ai/api/a2a/42_0/.well-known/agent-card.json
# Send message
curl -X POST http://kaman.ai/api/a2a/42_0 \
-H "Authorization: Bearer $KAMAN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"kind": "text", "text": "Hello!"}]
}
}
}'
# Stream message
curl -N -X POST http://kaman.ai/api/a2a/42_0 \
-H "Authorization: Bearer $KAMAN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "2",
"method": "message/stream",
"params": {
"message": {
"role": "user",
"parts": [{"kind": "text", "text": "Analyze Q3 data"}]
}
}
}'
Comparison: OpenAI API vs A2A
| Feature | OpenAI API | A2A Protocol |
|---|---|---|
| Protocol | REST + SSE | JSON-RPC 2.0 + SSE |
| SDK support | OpenAI SDK (Python, TS, etc.) | Any HTTP client |
| Agent discovery | GET /models | Agent Cards (.well-known) |
| Message format | {role, content} | {role, parts[]} (multi-modal) |
| Task tracking | Stateless (per-request) | Persistent tasks with status |
| Cancellation | Not supported | tasks/cancel |
| Multi-modal | Text only | Text, files, structured data |
| Best for | OpenAI SDK compatibility, LLM mode | Agent orchestration, multi-agent systems |
Timeouts & Limits
| Setting | Value |
|---|---|
| Request timeout | 5 minutes |
| Task TTL | 24 hours |
| Max concurrent tasks | 10,000 |
| CORS | Open (*) |
Next Steps
- OpenAI-Compatible API — Use standard OpenAI SDK
- Authentication — API authentication guide
- Tools API — Search and execute individual tools