What it is
The a2a package implements the Agent-to-Agent (A2A) protocol,
which defines a standard HTTP interface for agents to call each other across process and network boundaries.
A Phero agent can be exposed as an A2A server so other agents (or systems) can call it over HTTP.
Conversely, any remote A2A server can be consumed from a Phero agent as an ordinary llm.Tool.
The two sides
a2a.Server: wraps a Pheroagent.Agentand exposes it as an A2A-compliant HTTP handler set (AgentCard + JSON-RPC, and optionally HTTP+JSON/SSE).a2a.Client: resolves a remote AgentCard by URL and exposes the remote agent as anllm.Toolyou can attach to a local agent.
Example: exposing an agent as an A2A server
See the full code in examples/a2a/server.
package main
import (
"net/http"
"os"
"github.com/henomis/phero/a2a"
"github.com/henomis/phero/agent"
"github.com/henomis/phero/llm/openai"
)
func main() {
client := openai.New(os.Getenv("OPENAI_API_KEY"))
a, err := agent.New(
client,
"math-assistant",
"You are a helpful math assistant. Answer concisely.",
)
if err != nil {
panic(err)
}
srv, err := a2a.New(a, "http://localhost:8080",
a2a.WithVersion("1.0"),
a2a.WithStreaming(),
)
if err != nil {
panic(err)
}
mux := http.NewServeMux()
srv.Mount(mux) // registers /.well-known/agent-card.json and /
_ = http.ListenAndServe(":8080", mux)
}
Mount(mux) is the one-call convenience that registers all active handlers on the mux.
It is equivalent to calling AgentCardHandler() and JSONRPCHandler() separately,
and also registers the REST handler when WithRESTTransport() is used.
Example: calling a remote A2A agent as a tool
See the full code in examples/a2a/client.
package main
import (
"context"
"fmt"
"os"
"github.com/henomis/phero/a2a"
"github.com/henomis/phero/agent"
"github.com/henomis/phero/llm/openai"
)
func main() {
ctx := context.Background()
// Resolve the remote agent card and create a client
remoteClient, err := a2a.NewClient(ctx, "http://localhost:8080",
a2a.WithAcceptedOutputModes("text/plain"),
)
if err != nil {
panic(err)
}
// Inspect the discovered AgentCard
fmt.Println(remoteClient.Card().Name)
// Expose the remote agent as a local llm.Tool
mathTool, err := remoteClient.AsTool()
if err != nil {
panic(err)
}
// Use the tool in a local orchestrator agent
llmClient := openai.New(os.Getenv("OPENAI_API_KEY"))
orchestrator, err := agent.New(
llmClient,
"orchestrator",
"You are an orchestrator. Use the math-assistant tool for any math questions.",
)
if err != nil {
panic(err)
}
if err := orchestrator.AddTool(mathTool); err != nil {
panic(err)
}
result, err := orchestrator.Run(ctx, "What is the square root of 144?")
if err != nil {
panic(err)
}
fmt.Println(result.Content)
}
NewClient fetches the remote AgentCard from the well-known path and builds a transport client.
AsTool() wraps the remote agent as an llm.Tool whose name and description come from
the AgentCard itself — no manual wiring needed.
Card() returns the resolved *AgentCard so callers can inspect metadata before use.
The client handles both synchronous (inline Message) and asynchronous (Task-based) responses transparently.
For async tasks it subscribes to the event stream, or falls back to polling GetTask at a
configurable interval (default 500 ms) until the task reaches a terminal state.
Multi-agent A2A pipeline
For a production-style example, see
examples/a2a/multi-agent.
It shows three specialised agents (researcher, writer, editor) each running as a separate A2A server,
coordinated by a local orchestrator that calls each one as an llm.Tool.
# Terminal 1
OPENAI_API_KEY=<key> go run ./examples/a2a/multi-agent/researcher
# Terminal 2
OPENAI_API_KEY=<key> go run ./examples/a2a/multi-agent/writer
# Terminal 3
OPENAI_API_KEY=<key> go run ./examples/a2a/multi-agent/editor
# Terminal 4 — orchestrator
OPENAI_API_KEY=<key> go run ./examples/a2a/multi-agent/orchestrator -topic "quantum computing"
API reference
Server
a2a.New(agent, baseURL, opts...) (*Server, error)— create a server for the given agent and public base URLsrv.AgentCard() *AgentCard— derive the AgentCard from the wrapped agent and configurationsrv.Mount(mux)— register all active handlers on the mux (recommended)srv.AgentCardHandler() http.Handler— handler for/.well-known/agent-card.jsonsrv.JSONRPCHandler() http.Handler— handler for the A2A JSON-RPC endpointsrv.RESTHandler() http.Handler— handler for the HTTP+JSON/SSE endpoint (nil unlessWithRESTTransport()was used; mount with/rest/prefix stripped)
Server options
WithVersion(v string)— agent version in the AgentCard (default"1.0")WithSkills(skills ...AgentSkill)— append additional skill entries to the AgentCardWithInputModes(modes ...string)— supported input MIME types (default["text/plain"])WithOutputModes(modes ...string)— supported output MIME types (default["text/plain"])WithProvider(org, url string)— provider organisation and URL in the AgentCardWithIconURL(url string)— agent icon URL in the AgentCardWithDocURL(url string)— agent documentation URL in the AgentCardWithRESTTransport()— enable the HTTP+JSON/SSE transport in addition to JSON-RPCWithStreaming()— advertise streaming capability in the AgentCardWithPushNotifications(store, sender)— enable push notification endpointsWithTaskStore(store)— replace the default in-memory task store (for persistence or clustering)WithCallInterceptors(interceptors ...)— request/response middleware (auth, logging, rate limiting, …)WithExecutorContextInterceptors(interceptors ...)— enrich the ExecutorContext before each agent invocationWithConcurrencyLimit(config)— cap concurrent agent invocationsWithLogger(logger)— custom structured*slog.LoggerWithExtendedCard(producer)— authenticated extended AgentCard
Client
a2a.NewClient(ctx, baseURL, opts...) (*Client, error)— resolve the remote AgentCard and open a clientclient.Card() *AgentCard— return the resolved AgentCardclient.AsTool() (*llm.Tool, error)— expose the remote agent as an LLM-callable tool
Client options
WithResolver(r)— override the default AgentCard resolverWithAcceptedOutputModes(modes ...string)— MIME types the client can consumeWithPreferredTransports(protocols ...)— ordered transport protocol preferencesWithClientInterceptors(interceptors ...)— inject auth headers or tracing per callWithPollingInterval(d time.Duration)— tune theGetTaskpolling interval for async tasksWithPushConfig(cfg)— default push notification configuration for tasks
Errors
ErrAgentRequired— nil agent passed toNewErrBaseURLRequired— empty base URL passed toNewErrURLRequired— empty base URL passed toNewClientErrInvalidBaseURL— base URL is not a well-formed absolute URLErrNoTextContent— remote response contains no text partErrEmptyResponse— remote response is nil
Run the examples
# Start the A2A server in one terminal
go run ./examples/a2a/server
# Call it from another terminal
go run ./examples/a2a/client