Skip to main content
The Agent-to-Agent (A2A) protocol, originally specified by Google, enables cross-framework agent interoperability. It allows agents built with different frameworks to discover each other’s capabilities and exchange tasks.

A2A Server

Publish Agent Cards and accept task submissions

A2A Client

Discover external agents and send tasks to them
OpenFang implements A2A in both directions, enabling seamless integration with other agent frameworks.

Agent Card

An Agent Card is a JSON document that describes an agent’s identity, capabilities, and supported interaction modes. It is served at the well-known path /.well-known/agent.json per the A2A specification.

Agent Card Structure

pub struct AgentCard {
    pub name: String,
    pub description: String,
    pub url: String,                         // endpoint URL
    pub version: String,                     // protocol version
    pub capabilities: AgentCapabilities,
    pub skills: Vec<AgentSkill>,             // A2A skill descriptors
    pub default_input_modes: Vec<String>,    // e.g., ["text"]
    pub default_output_modes: Vec<String>,   // e.g., ["text"]
}

Agent Capabilities

pub struct AgentCapabilities {
    pub streaming: bool,                 // true — OpenFang supports streaming
    pub push_notifications: bool,        // false — not currently implemented
    pub state_transition_history: bool,  // true — task status history available
}

Agent Skills

A2A skills are capability descriptors, not the same as OpenFang skills. These describe what the agent can do for cross-framework discovery.
pub struct AgentSkill {
    pub id: String,           // matches the OpenFang tool name
    pub name: String,         // human-readable
    pub description: String,
    pub tags: Vec<String>,
    pub examples: Vec<String>,
}

Example Agent Card

{
  "name": "code-reviewer",
  "description": "Reviews code for bugs, security issues, and style",
  "url": "http://127.0.0.1:50051/a2a",
  "version": "0.1.0",
  "capabilities": {
    "streaming": true,
    "pushNotifications": false,
    "stateTransitionHistory": true
  },
  "skills": [
    {
      "id": "file_read",
      "name": "file read",
      "description": "Can use the file_read tool",
      "tags": ["tool"],
      "examples": []
    }
  ],
  "defaultInputModes": ["text"],
  "defaultOutputModes": ["text"]
}
Agent Cards are built from OpenFang agent manifests via build_agent_card(). Each tool in the agent’s capability list becomes an A2A skill descriptor.

A2A Server

OpenFang serves A2A requests through the REST API:

Agent Card

Publication at /.well-known/agent.json

Agent Listing

All agents at /a2a/agents

Task Tracking

Via A2aTaskStore

A2aTaskStore

The A2aTaskStore is an in-memory, bounded store for tracking A2A task lifecycle:
pub struct A2aTaskStore {
    tasks: Mutex<HashMap<String, A2aTask>>,
    max_tasks: usize,  // default: 1000
}

Key Properties

  • Bounded: When reaching max_tasks, evicts oldest completed/failed/cancelled task (FIFO)
  • Thread-safe: Uses Mutex<HashMap> for concurrent access
  • Kernel field: Stored as kernel.a2a_task_store

Methods

Add a new task, evicting old ones if at capacity
Retrieve a task by ID
Change a task’s status
Mark as completed with response
Mark as failed with error
Mark as cancelled

Task Submission Flow

When POST /a2a/tasks/send is called:
1

Extract message

Extract the message text from A2A request format (parts with type “text”)
2

Find target agent

Locate the target agent (currently uses first registered agent)
3

Create task

Create an A2aTask with status Working and insert into task store
4

Send to agent

Send message to agent via kernel.send_message()
5

Handle response

On success: complete task with agent’s response
On failure: fail task with error message
6

Return state

Return the final task state

A2A Client

The A2aClient struct discovers and interacts with external A2A agents:
pub struct A2aClient {
    client: reqwest::Client,  // 30-second timeout
}

Methods

discover(url)

Fetches {url}/.well-known/agent.json and parses the Agent Card

send_task(...)

Sends a JSON-RPC task submission

get_task(...)

Polls for task status

Auto-Discovery at Boot

When the kernel starts and A2A is enabled with external agents configured:
1

Create client

Creates an A2aClient
2

Iterate agents

Iterates each configured ExternalAgent
3

Fetch cards

Fetches each agent’s card from {url}/.well-known/agent.json
4

Log discoveries

Logs successful discoveries (name, URL, skill count)
5

Store results

Stores discovered (name, AgentCard) pairs in kernel.a2a_external_agents
Failed discoveries are logged as warnings but do not prevent boot.

Sending Tasks to External Agents

let client = A2aClient::new();
let task = client.send_task(
    "https://other-agent.example.com/a2a",
    "Analyze this dataset for anomalies",
    Some("session-123"),
).await?;
println!("Task {}: {:?}", task.id, task.status);
The client sends a JSON-RPC request:
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tasks/send",
  "params": {
    "message": {
      "role": "user",
      "parts": [{ "type": "text", "text": "Analyze this dataset..." }]
    },
    "sessionId": "session-123"
  }
}

Task Lifecycle

An A2aTask tracks the full lifecycle of a cross-agent interaction:
pub struct A2aTask {
    pub id: String,
    pub session_id: Option<String>,
    pub status: A2aTaskStatus,
    pub messages: Vec<A2aMessage>,
    pub artifacts: Vec<A2aArtifact>,
}

Task States

StatusDescription
SubmittedTask received but not yet started
WorkingTask is being actively processed by the agent
InputRequiredAgent needs more information from the caller
CompletedTask finished successfully
CancelledTask was cancelled by the caller
FailedTask encountered an error

Message Format

Messages use an A2A-specific format with typed content parts:
pub struct A2aMessage {
    pub role: String,          // "user" or "agent"
    pub parts: Vec<A2aPart>,
}

pub enum A2aPart {
    Text { text: String },
    File { name: String, mime_type: String, data: String },  // base64
    Data { mime_type: String, data: serde_json::Value },
}

Artifacts

Tasks can produce artifacts (files, structured data) alongside messages:
pub struct A2aArtifact {
    pub name: String,
    pub parts: Vec<A2aPart>,
}

A2A Configuration

A2A is configured in config.toml under the [a2a] section:
[a2a]
enabled = true
listen_path = "/a2a"

[[a2a.external_agents]]
name = "research-agent"
url = "https://research.example.com"

[[a2a.external_agents]]
name = "data-analyst"
url = "https://data.example.com"

Configuration Fields

FieldTypeDefaultDescription
enabledboolfalseWhether A2A endpoints are active
listen_pathString"/a2a"Base path for A2A endpoints
external_agentsVec<ExternalAgent>[]External agents to discover at boot

External Agent Configuration

FieldTypeDescription
nameStringDisplay name for this external agent
urlStringBase URL where the agent’s card is published
If a2a is None (not present in config), all A2A features are disabled. The A2A endpoints are always registered in the router but discovery and task store functionality requires enabled = true.

A2A API Endpoints

MethodPathDescription
GET/.well-known/agent.jsonAgent Card for the primary agent
GET/a2a/agentsList all agent cards
POST/a2a/tasks/sendSubmit a task to an agent
GET/a2a/tasks/{id}Get task status and messages
POST/a2a/tasks/{id}/cancelCancel a running task

GET /.well-known/agent.json

Returns the Agent Card for the first registered agent. If no agents are spawned, returns a placeholder card.

GET /a2a/agents

Lists all registered agents as Agent Cards:
{
  "agents": [
    {
      "name": "code-reviewer",
      "description": "Reviews code for bugs and security issues",
      "url": "http://127.0.0.1:50051/a2a",
      "version": "0.1.0",
      "capabilities": {
        "streaming": true,
        "pushNotifications": false,
        "stateTransitionHistory": true
      },
      "skills": [...],
      "defaultInputModes": ["text"],
      "defaultOutputModes": ["text"]
    }
  ],
  "total": 1
}

POST /a2a/tasks/send

Submit a task. Request body follows JSON-RPC 2.0 format:
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tasks/send",
  "params": {
    "message": {
      "role": "user",
      "parts": [{ "type": "text", "text": "Review this code for security issues" }]
    },
    "sessionId": "optional-session-id"
  }
}
Response (completed task):
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "sessionId": "optional-session-id",
  "status": "completed",
  "messages": [
    {
      "role": "user",
      "parts": [{ "type": "text", "text": "Review this code for security issues" }]
    },
    {
      "role": "agent",
      "parts": [{ "type": "text", "text": "I found 2 potential issues..." }]
    }
  ],
  "artifacts": []
}

GET /a2a/tasks/

Poll for task status. Returns 404 if the task is not found or has been evicted.

POST /a2a/tasks//cancel

Cancel a running task. Sets its status to Cancelled. Returns 404 if the task is not found.

Security

Rate Limiting

A2A endpoints go through the same GCRA rate limiter as all other API endpoints.

API Authentication

When api_key is set in kernel config, all API endpoints (including A2A) require Authorization: Bearer <key> header. Exception: /.well-known/agent.json and health endpoint.

Task Store Bounds

A2aTaskStore is bounded (default 1000 tasks) with FIFO eviction of completed/failed/cancelled tasks, preventing memory exhaustion.

External Agent Discovery

A2aClient uses 30-second timeout and sends User-Agent: OpenFang/0.1 A2A header. Failed discoveries are logged but do not block kernel boot.

Kernel-Level Protection

A2A tool execution flows through the same security pipeline as all other tool calls: capability-based access control, tool result truncation (50K character hard cap), universal 60-second timeout, loop guard detection, and taint tracking.

Source Files

  • Protocol types and logic: crates/openfang-runtime/src/a2a.rs
  • API routes: crates/openfang-api/src/routes.rs
  • Config types: crates/openfang-types/src/config.rs