Skip to content

tRPC-Agent-Go A2A Integration Guide

Overview

tRPC-Agent-Go provides a complete A2A (Agent-to-Agent) solution with two core components:

  • A2A Server: Exposes local Agents as A2A services for other Agents to call
  • A2AAgent: A client proxy for calling remote A2A services, allowing you to use remote Agents as if they were local

Core Capabilities

  • Zero Protocol Awareness: Developers only need to focus on Agent business logic without understanding A2A protocol details
  • Automatic Adaptation: The framework automatically converts Agent information to A2A AgentCard
  • Message Conversion: Automatically handles conversion between A2A protocol messages and Agent message formats

A2A Server: Exposing Agents as Services

Concept Introduction

A2A Server is a server-side component provided by tRPC-Agent-Go for quickly converting any local Agent into a network service that complies with the A2A protocol.

Core Features

  • One-Click Conversion: Expose Agents as A2A services through simple configuration
  • Automatic Protocol Adaptation: Automatically handles conversion between A2A protocol and Agent interfaces
  • AgentCard Generation: Automatically generates AgentCards required for service discovery
  • Streaming Support: Supports both streaming and non-streaming response modes

Automatic Conversion from Agent to A2A

tRPC-Agent-Go implements seamless conversion from Agent to A2A service through the server/a2a package:

func New(opts ...Option) (*a2a.A2AServer, error) {}

Automatic AgentCard Generation

The framework automatically extracts Agent metadata (name, description, tools, etc.) to generate an AgentCard that complies with the A2A protocol, including: - Basic Agent information (name, description, URL) - Capability declarations (streaming support) - Skill lists (automatically generated based on Agent tools)

Message Protocol Conversion

The framework includes a built-in messageProcessor that implements bidirectional conversion between A2A protocol messages and Agent message formats, so users don't need to worry about message format conversion details.

A2A Server Quick Start

Exposing Agent Services with A2A Server

With just a few lines of code, you can convert any Agent into an A2A service:

Basic Example: Creating A2A Server

package main

import (
    "trpc.group/trpc-go/trpc-agent-go/agent/llmagent"
    "trpc.group/trpc-go/trpc-agent-go/model/openai"
    a2aserver "trpc.group/trpc-go/trpc-agent-go/server/a2a"
)

func main() {
    // 1. Create a regular Agent
    model := openai.New("gpt-4o-mini")
    agent := llmagent.New("MyAgent",
        llmagent.WithModel(model),
        llmagent.WithDescription("An intelligent assistant"),
    )

    // 2. Convert to A2A service with one click
    server, _ := a2aserver.New(
        a2aserver.WithHost("localhost:8080"),
        a2aserver.WithAgent(agent, true), // Enable streaming
    )

    // 3. Start the service to accept A2A requests
    server.Start(":8080")
}

Streaming output event type (Message vs Artifact)

When streaming is enabled, A2A allows the server to emit incremental output in different ways:

  • TaskArtifactUpdateEvent (default): ADK-style streaming. Chunks are sent as task artifact updates (artifact-update).
  • Message: Lightweight streaming. Chunks are sent as message, so clients can render Message.parts directly without treating output as a persisted artifact.

To stream agent output as message instead of artifact-update, configure the server with:

1
2
3
4
5
6
7
server, _ := a2aserver.New(
    a2aserver.WithHost("localhost:8080"),
    a2aserver.WithAgent(agent, true),
    a2aserver.WithStreamingEventType(
        a2aserver.StreamingEventTypeMessage,
    ),
)

Task state updates (submitted, completed) are still emitted as TaskStatusUpdateEvent.

Direct A2A Protocol Client Call

import (
    "trpc.group/trpc-go/trpc-a2a-go/client"
    "trpc.group/trpc-go/trpc-a2a-go/protocol"
)

func main() {
    // Connect to A2A service
    client, _ := client.NewA2AClient("http://localhost:8080/")

    // Send message to Agent
    message := protocol.NewMessage(
        protocol.MessageRoleUser,
        []protocol.Part{protocol.NewTextPart("Hello, please help me analyze this code")},
    )

    // Agent will automatically process and return results
    response, _ := client.SendMessage(context.Background(),
        protocol.SendMessageParams{Message: message})
}

Hosting multiple A2A agents on one HTTP port (base paths)

Sometimes you want one service (one port) to expose multiple A2A Agents. The idiomatic A2A approach is to give each Agent its own base URL, and let the client select the Agent by choosing the URL (not by passing an agent_name parameter).

In tRPC-Agent-Go, a2a.WithHost(...) supports URLs with a path segment. When the host URL contains a path (for example http://localhost:8888/agents/math), the A2A server will automatically use that path as its base path for routing.

Key idea:

  • Create one A2A server per Agent (each with a different base path)
  • Mount all A2A servers onto one shared http.Server via server.Handler()

Example:

mathServer, err := a2a.New(
    a2a.WithHost("http://localhost:8888/agents/math"),
    a2a.WithAgent(mathAgent, false),
)
if err != nil {
    panic(err)
}

weatherServer, err := a2a.New(
    a2a.WithHost("http://localhost:8888/agents/weather"),
    a2a.WithAgent(weatherAgent, false),
)
if err != nil {
    panic(err)
}

mux := http.NewServeMux()
mux.Handle("/agents/math/", mathServer.Handler())
mux.Handle("/agents/weather/", weatherServer.Handler())

if err := http.ListenAndServe(":8888", mux); err != nil {
    panic(err)
}

After the server starts, each Agent has its own AgentCard endpoint:

  • http://localhost:8888/agents/math/.well-known/agent-card.json
  • http://localhost:8888/agents/weather/.well-known/agent-card.json

Full runnable example: examples/a2amultipath.

A2AAgent: Calling Remote A2A Services

Corresponding to A2A Server, tRPC-Agent-Go also provides A2AAgent for calling remote A2A services, enabling communication between Agents.

Concept Introduction

A2AAgent is a special Agent implementation that doesn't directly handle user requests but forwards them to remote A2A services. From the user's perspective, A2AAgent looks like a regular Agent, but it's actually a local proxy for a remote Agent.

Simple Understanding: - A2A Server: I have an Agent and want others to call it → Expose as A2A service - A2AAgent: I want to call someone else's Agent → Call through A2AAgent proxy

Core Features

  • Transparent Proxy: Use remote Agents as if they were local Agents
  • Automatic Discovery: Automatically discover remote Agent capabilities through AgentCard
  • Protocol Conversion: Automatically handle conversion between local message formats and A2A protocol
  • Streaming Support: Support both streaming and non-streaming communication modes
  • State Transfer: Support transferring local state to remote Agents
  • Error Handling: Comprehensive error handling and retry mechanisms

Use Cases

  1. Distributed Agent Systems: Call Agents from other services in microservice architectures
  2. Agent Orchestration: Combine multiple specialized Agents into complex workflows
  3. Cross-Team Collaboration: Call Agent services provided by other teams

A2AAgent Quick Start

Basic Usage

package main

import (
    "context"
    "fmt"

    "trpc.group/trpc-go/trpc-agent-go/agent/a2aagent"
    "trpc.group/trpc-go/trpc-agent-go/runner"
    "trpc.group/trpc-go/trpc-agent-go/model"
    "trpc.group/trpc-go/trpc-agent-go/session/inmemory"
)

func main() {
    // 1. Create A2AAgent pointing to remote A2A service
    a2aAgent, err := a2aagent.New(
        a2aagent.WithAgentCardURL("http://localhost:8888"),
    )
    if err != nil {
        panic(err)
    }

    // 2. Use it like a regular Agent
    sessionService := inmemory.NewSessionService()
    runner := runner.NewRunner("test", a2aAgent, 
        runner.WithSessionService(sessionService))

    // 3. Send message
    events, err := runner.Run(
        context.Background(),
        "user1",
        "session1", 
        model.NewUserMessage("Please tell me a joke"),
    )
    if err != nil {
        panic(err)
    }

    // 4. Handle response
    for event := range events {
        if event.Response != nil && len(event.Response.Choices) > 0 {
            fmt.Print(event.Response.Choices[0].Message.Content)
        }
    }
}

In multi-agent systems, A2AAgent is often used as a SubAgent of a local coordinator Agent (for example an LLMAgent). You can combine A2AAgent with LLMAgent.SetSubAgents to dynamically load and refresh remote SubAgents from a registry without recreating the coordinator.

Advanced Configuration

// Create A2AAgent with advanced configuration
a2aAgent, err := a2aagent.New(
    // Specify remote service address
    a2aagent.WithAgentCardURL("http://remote-agent:8888"),

    // Set streaming buffer size
    a2aagent.WithStreamingChannelBufSize(2048),

    // Custom protocol conversion
    a2aagent.WithCustomEventConverter(customEventConverter),

    a2aagent.WithCustomA2AConverter(customA2AConverter),

    // Explicitly control streaming mode (overrides AgentCard capability declaration)
    a2aagent.WithEnableStreaming(true),
)

Complete Example: A2A Server + A2AAgent Combined Usage

Here's a complete example showing how to run both A2A Server (exposing local Agent) and A2AAgent (calling remote service) in the same program:

package main

import (
    "context"
    "fmt"
    "time"

    "trpc.group/trpc-go/trpc-agent-go/agent/a2aagent"
    "trpc.group/trpc-go/trpc-agent-go/agent/llmagent"
    "trpc.group/trpc-go/trpc-agent-go/model/openai"
    "trpc.group/trpc-go/trpc-agent-go/runner"
    "trpc.group/trpc-go/trpc-agent-go/server/a2a"
    "trpc.group/trpc-go/trpc-agent-go/session/inmemory"
)

func main() {
    // 1. Create and start remote Agent service
    remoteAgent := createRemoteAgent()
    startA2AServer(remoteAgent, "localhost:8888")

    time.Sleep(1 * time.Second) // Wait for service to start

    // 2. Create A2AAgent connecting to remote service
    a2aAgent, err := a2aagent.New(
        a2aagent.WithAgentCardURL("http://localhost:8888"),
        a2aagent.WithTransferStateKey("user_context"),
    )
    if err != nil {
        panic(err)
    }

    // 3. Create local Agent
    localAgent := createLocalAgent()

    // 4. Compare local and remote Agent responses
    compareAgents(localAgent, a2aAgent)
}

func createRemoteAgent() agent.Agent {
    model := openai.New("gpt-4o-mini")
    return llmagent.New("JokeAgent",
        llmagent.WithModel(model),
        llmagent.WithDescription("I am a joke-telling agent"),
        llmagent.WithInstruction("Always respond with a funny joke"),
    )
}

func createLocalAgent() agent.Agent {
    model := openai.New("gpt-4o-mini") 
    return llmagent.New("LocalAgent",
        llmagent.WithModel(model),
        llmagent.WithDescription("I am a local assistant"),
    )
}

func startA2AServer(agent agent.Agent, host string) {
    server, err := a2a.New(
        a2a.WithHost(host),
        a2a.WithAgent(agent, true), // Enable streaming
    )
    if err != nil {
        panic(err)
    }

    go func() {
        server.Start(host)
    }()
}

func compareAgents(localAgent, remoteAgent agent.Agent) {
    sessionService := inmemory.NewSessionService()

    localRunner := runner.NewRunner("local", localAgent,
        runner.WithSessionService(sessionService))
    remoteRunner := runner.NewRunner("remote", remoteAgent,
        runner.WithSessionService(sessionService))

    userMessage := "Please tell me a joke"

    // Call local Agent
    fmt.Println("=== Local Agent Response ===")
    processAgent(localRunner, userMessage)

    // Call remote Agent (via A2AAgent)
    fmt.Println("\n=== Remote Agent Response (via A2AAgent) ===")
    processAgent(remoteRunner, userMessage)
}

func processAgent(runner runner.Runner, message string) {
    events, err := runner.Run(
        context.Background(),
        "user1",
        "session1",
        model.NewUserMessage(message),
        agent.WithRuntimeState(map[string]any{
            "user_context": "test_context",
        }),
    )
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }

    for event := range events {
        if event.Response != nil && len(event.Response.Choices) > 0 {
            content := event.Response.Choices[0].Message.Content
            if content == "" {
                content = event.Response.Choices[0].Delta.Content
            }
            if content != "" {
                fmt.Print(content)
            }
        }
    }
    fmt.Println()
}

AgentCard Automatic Discovery

A2AAgent supports automatically obtaining remote Agent information through the standard AgentCard discovery mechanism:

// A2AAgent automatically retrieves AgentCard from the following path
// http://remote-agent:8888/.well-known/agent.json

type AgentCard struct {
    Name         string                 `json:"name"`
    Description  string                 `json:"description"`
    URL          string                 `json:"url"`
    Capabilities AgentCardCapabilities  `json:"capabilities"`
}

type AgentCardCapabilities struct {
    Streaming *bool `json:"streaming,omitempty"`
}

State Transfer

A2AAgent supports transferring local runtime state to remote Agents:

a2aAgent, _ := a2aagent.New(
    a2aagent.WithAgentCardURL("http://remote-agent:8888"),
    // Specify state keys to transfer
    a2aagent.WithTransferStateKey("user_id", "session_context", "preferences"),
)

// Runtime state is passed to remote Agent through A2A protocol metadata field
events, _ := runner.Run(ctx, userID, sessionID, message,
    agent.WithRuntimeState(map[string]any{
        "user_id":         "12345",
        "session_context": "shopping_cart",
        "preferences":     map[string]string{"language": "en"},
    }),
)

Custom HTTP Headers

You can pass custom HTTP headers to A2A agent for each request using WithA2ARequestOptions:

import "trpc.group/trpc-go/trpc-a2a-go/client"

events, err := runner.Run(
    context.Background(),
    userID,
    sessionID,
    model.NewUserMessage("your question"),
    // Pass custom HTTP headers for this request
    agent.WithA2ARequestOptions(
        client.WithRequestHeader("X-Custom-Header", "custom-value"),
        client.WithRequestHeader("X-Request-ID", fmt.Sprintf("req-%d", time.Now().UnixNano())),
        client.WithRequestHeader("Authorization", "Bearer your-token"),
    ),
)

Common Use Cases:

  1. Authentication: Pass authentication tokens

    1
    2
    3
    agent.WithA2ARequestOptions(
        client.WithRequestHeader("Authorization", "Bearer "+token),
    )
    

  2. Distributed Tracing: Add request/trace IDs

    1
    2
    3
    4
    agent.WithA2ARequestOptions(
        client.WithRequestHeader("X-Request-ID", requestID),
        client.WithRequestHeader("X-Trace-ID", traceID),
    )
    

Configuring UserID Header:

Both client and server support configuring which HTTP header to use for UserID, default is X-User-ID:

// Client side: Configure which header to send UserID in
a2aAgent, _ := a2aagent.New(
    a2aagent.WithAgentCardURL("http://remote-agent:8888"),
    // Default is "X-User-ID", can be customized
    a2aagent.WithUserIDHeader("X-Custom-User-ID"),
)

// Server side: Configure which header to read UserID from
server, _ := a2a.New(
    a2a.WithHost("localhost:8888"),
    a2a.WithAgent(agent, true),
    // Default is "X-User-ID", can be customized
    a2a.WithUserIDHeader("X-Custom-User-ID"),
)

The UserID from invocation.Session.UserID will be automatically sent via the configured header to the A2A server.

ADK Compatibility Mode

If you need to interoperate with Google ADK (Agent Development Kit) Python clients, you can enable ADK compatibility mode. When enabled, the Server will write additional adk_-prefixed keys (such as adk_type, adk_thought) in metadata to be compatible with ADK's part converter parsing logic:

1
2
3
4
5
server, _ := a2a.New(
    a2a.WithHost("localhost:8888"),
    a2a.WithAgent(agent, true),
    a2a.WithADKCompatibility(true), // Disabled by default
)

Custom Converters

For special requirements, you can customize message and event converters:

// Custom A2A message converter (Invocation -> A2A Message)
// Implements the a2aagent.InvocationA2AConverter interface
type CustomA2AConverter struct{}

func (c *CustomA2AConverter) ConvertToA2AMessage(
    isStream bool, 
    agentName string, 
    invocation *agent.Invocation,
) (*protocol.Message, error) {
    // Custom message conversion logic
    msg := protocol.NewMessage(protocol.MessageRoleUser, []protocol.Part{
        protocol.NewTextPart(invocation.Message.Content),
    })
    return &msg, nil
}

// Custom event converter (A2A Response -> Event)
// Implements the a2aagent.A2AEventConverter interface
type CustomEventConverter struct{}

func (c *CustomEventConverter) ConvertToEvents(
    result protocol.MessageResult,
    agentName string,
    invocation *agent.Invocation,
) ([]*event.Event, error) {
    // Custom non-streaming event conversion logic
    return []*event.Event{event.New(invocation.InvocationID, agentName)}, nil
}

func (c *CustomEventConverter) ConvertStreamingToEvents(
    result protocol.StreamingMessageEvent,
    agentName string,
    invocation *agent.Invocation,
) ([]*event.Event, error) {
    // Custom streaming event conversion logic
    return []*event.Event{event.New(invocation.InvocationID, agentName)}, nil
}

// Use custom converters
a2aAgent, _ := a2aagent.New(
    a2aagent.WithAgentCardURL("http://remote-agent:8888"),
    a2aagent.WithCustomA2AConverter(&CustomA2AConverter{}),
    a2aagent.WithCustomEventConverter(&CustomEventConverter{}),
)

Protocol Interaction Specification

For detailed specifications on how tool calls, code execution, reasoning content, and other events are transmitted through the A2A protocol, as well as Metadata field definitions, ADK compatibility mode, and distributed tracing, please refer to the dedicated document:

A2A Protocol Interaction Specification

This document defines the extension specification of trpc-agent-go on top of the A2A protocol, serving as the standard reference for Client and Server implementations.

Summary: A2A Server vs A2AAgent

Component Role Use Case Core Functions
A2A Server Service Provider Expose local Agent for other systems to call • Protocol conversion
• AgentCard generation
• Message routing
• Streaming support
A2AAgent Service Consumer Call remote A2A services • Transparent proxy
• Automatic discovery
• State transfer
• Protocol adaptation

Typical Architecture Pattern

1
2
3
4
5
6
7
8
┌─────────────┐ A2A protocol  ┌───────────────┐
│   Client    │──────────────→│ A2A Server    │
│ (A2AAgent)  │               │ (local Agent) │
└─────────────┘               └───────────────┘
      ↑                              ↑
      │                              │
   Call remote                   Expose local
   Agent service                 Agent service

Through the combined use of A2A Server and A2AAgent, you can easily build distributed Agent systems.