Go ADK
The Go ADK (github.com/inference-gateway/adk) is the original and reference Agent Development Kit for building A2A (Agent-to-Agent) servers and clients. It is the implementation whose handler semantics and wire protocol the TypeScript ADK and Rust ADK mirror, so agents written against any of the three speak the same canonical A2A schema.
Early-stage status. The ADK is under active development and its public API may change between minor versions. Pin an exact version in production and be prepared to update when upgrading.
This page is a feature reference for the module: the server and agent builders, the client and its full JSON-RPC method surface, custom tools and task handlers, lifecycle callbacks, streaming via CloudEvents, storage backends, authentication, artifacts, telemetry, build-time metadata, and the environment-variable configuration surface. The canonical upstream source is the repository README.md and the examples/ directory.
Installation
go get github.com/inference-gateway/adkThe module requires Go 1.26 or later. The public packages are:
| Import path | Purpose |
|---|---|
github.com/inference-gateway/adk/server | Server builder, agent builder, task handlers, tools, callbacks. |
github.com/inference-gateway/adk/server/config | The Config struct and its environment-variable loader. |
github.com/inference-gateway/adk/client | The A2AClient for talking to any A2A server. |
github.com/inference-gateway/adk/types | Generated A2A schema types (Task, Message, AgentCard, etc.). |
Quick start
Minimal server
The smallest runnable server attaches a custom TaskHandler and an agent card. No LLM credentials are required - the handler below simply echoes the user's message back. Build() validates its inputs: an agent card is required, and at least one task handler must be configured.
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
uuid "github.com/google/uuid"
zap "go.uber.org/zap"
server "github.com/inference-gateway/adk/server"
config "github.com/inference-gateway/adk/server/config"
types "github.com/inference-gateway/adk/types"
)
type EchoHandler struct{ agent server.OpenAICompatibleAgent }
func (h *EchoHandler) HandleTask(ctx context.Context, task *types.Task, message *types.Message) (*types.Task, error) {
text := "Hello! Send me a message and I'll echo it back."
if message != nil {
for _, part := range message.Parts {
if part.Text != nil {
text = "Echo: " + *part.Text
}
}
}
reply := types.Message{
MessageID: uuid.New().String(),
ContextID: &task.ContextID,
TaskID: &task.ID,
Role: types.RoleAgent,
Parts: []types.Part{types.CreateTextPart(text)},
}
task.History = append(task.History, reply)
task.Status.State = types.TaskStateCompleted
task.Status.Message = &reply
return task, nil
}
func (h *EchoHandler) SetAgent(a server.OpenAICompatibleAgent) { h.agent = a }
func (h *EchoHandler) GetAgent() server.OpenAICompatibleAgent { return h.agent }
func main() {
logger, _ := zap.NewDevelopment()
defer func() { _ = logger.Sync() }()
port := "8080"
agentURL := fmt.Sprintf("http://localhost:%s", port)
cfg := config.Config{
AgentName: "minimal-agent",
AgentDescription: "A minimal A2A server that echoes messages",
AgentVersion: "0.1.0",
Debug: true,
QueueConfig: config.QueueConfig{CleanupInterval: 5 * time.Minute},
ServerConfig: config.ServerConfig{Port: port},
}
a2aServer, err := server.NewA2AServerBuilder(cfg, logger).
WithBackgroundTaskHandler(&EchoHandler{}).
WithAgentCard(types.AgentCard{
Name: cfg.AgentName,
Description: cfg.AgentDescription,
Version: cfg.AgentVersion,
SupportedInterfaces: []types.AgentInterface{{URL: agentURL, ProtocolBinding: "JSONRPC", ProtocolVersion: "1.0"}},
Capabilities: types.AgentCapabilities{
Streaming: &[]bool{false}[0],
PushNotifications: &[]bool{false}[0],
},
DefaultInputModes: []string{"text/plain"},
DefaultOutputModes: []string{"text/plain"},
Skills: []types.AgentSkill{},
}).
Build()
if err != nil {
logger.Fatal("failed to create server", zap.Error(err))
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
if err := a2aServer.Start(ctx); err != nil {
logger.Fatal("server failed to start", zap.Error(err))
}
}()
logger.Info("server running", zap.String("port", port))
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
_ = a2aServer.Stop(shutdownCtx)
}See the minimal example for the full runnable version with a companion client.
AI-powered server
Wire an LLM by building an OpenAICompatibleAgent and attaching it to the server. config.Load reads the environment (AGENT_CLIENT_PROVIDER, AGENT_CLIENT_MODEL, AGENT_CLIENT_API_KEY, ...) on top of any defaults you set programmatically. With an agent attached, WithDefaultTaskHandlers() gives you working SendMessage and SendStreamingMessage handling - including automatic input-required pausing - without writing handler code.
ctx := context.Background()
// Start from defaults, then overlay environment values (AGENT_CLIENT_*, etc.).
cfg, err := config.Load(ctx, &config.Config{
AgentName: server.BuildAgentName,
AgentDescription: server.BuildAgentDescription,
AgentVersion: server.BuildAgentVersion,
CapabilitiesConfig: config.CapabilitiesConfig{Streaming: true},
ServerConfig: config.ServerConfig{Port: "8080"},
})
if err != nil {
logger.Fatal("failed to load config", zap.Error(err))
}
// A custom tool the model can call.
weatherTool := server.NewBasicTool(
"get_weather",
"Get current weather information for a city",
map[string]any{
"type": "object",
"properties": map[string]any{
"location": map[string]any{"type": "string", "description": "The city name"},
},
"required": []string{"location"},
},
func(ctx context.Context, args map[string]any) (string, error) {
location, _ := args["location"].(string)
return fmt.Sprintf(`{"location": %q, "temperature": "22C", "condition": "sunny"}`, location), nil
},
)
toolBox := server.NewDefaultToolBox(&cfg.AgentConfig.ToolBoxConfig)
toolBox.AddTool(weatherTool)
llmClient, err := server.NewOpenAICompatibleLLMClient(&cfg.AgentConfig, logger)
if err != nil {
logger.Fatal("failed to create LLM client", zap.Error(err))
}
agent, err := server.NewAgentBuilder(logger).
WithConfig(&cfg.AgentConfig).
WithLLMClient(llmClient).
WithSystemPrompt("You are a helpful weather assistant. Be concise.").
WithMaxChatCompletion(10).
WithToolBox(toolBox).
Build()
if err != nil {
logger.Fatal("failed to build agent", zap.Error(err))
}
agentURL := fmt.Sprintf("http://localhost:%s", cfg.ServerConfig.Port)
a2aServer, err := server.NewA2AServerBuilder(*cfg, logger).
WithAgent(agent).
WithDefaultTaskHandlers().
WithAgentCard(types.AgentCard{
Name: cfg.AgentName,
Description: cfg.AgentDescription,
Version: cfg.AgentVersion,
SupportedInterfaces: []types.AgentInterface{{URL: agentURL, ProtocolBinding: "JSONRPC", ProtocolVersion: "1.0"}},
Capabilities: types.AgentCapabilities{
Streaming: &cfg.CapabilitiesConfig.Streaming,
PushNotifications: &cfg.CapabilitiesConfig.PushNotifications,
},
DefaultInputModes: []string{"text/plain"},
DefaultOutputModes: []string{"text/plain"},
Skills: []types.AgentSkill{},
}).
Build()See ai-powered and ai-powered-streaming for the complete programs.
The server and its builder
A2AServer terminates the A2A JSON-RPC protocol. You never construct it directly - A2AServerBuilder assembles one through a fluent interface, and Start(ctx) binds the listener while Stop(ctx) shuts it down gracefully. Create the builder with NewA2AServerBuilder(cfg, logger).
| Method | Purpose |
|---|---|
WithAgent(agent) | Attach a pre-built OpenAICompatibleAgent for the task handlers to delegate to. |
WithAgentCard(card) | Set the card served at /.well-known/agent-card.json from an in-memory value. Required. |
WithAgentCardFromFile(path, overrides) | Load the card from a JSON file, replacing top-level attributes from the overrides map. |
WithBackgroundTaskHandler(h) | Register a custom SendMessage (polling/queue) handler. |
WithStreamingTaskHandler(h) | Register a custom SendStreamingMessage (SSE) handler. |
WithDefaultBackgroundTaskHandler() | Opt into the bundled background handler. |
WithDefaultStreamingTaskHandler() | Opt into the bundled streaming handler. |
WithDefaultTaskHandlers() | Opt into both bundled handlers at once. |
WithTaskResultProcessor(p) | Plug in custom business logic for deciding when a task is complete from tool-call results. |
WithArtifactService(svc) | Supply an artifact service so the agent can emit URI-based downloadable files. |
WithLogger(logger) | Override the logger used by the builder and resulting server. |
Build() | Validate and construct the A2AServer. |
Builder validation.
Build()returns an error unless an agent card is configured and at least one task handler is present. It also cross-checks the card'sCapabilities.Streamingflag: a streaming-enabled card requires a streaming handler, and a streaming-disabled card requires a background handler.WithDefaultTaskHandlers()satisfies both. When the card advertisesPushNotifications, the builder wires an HTTP push-notification sender automatically.
Agent card caching
/.well-known/agent-card.json is served with HTTP caching headers per A2A spec section 8.6: Cache-Control: public, max-age=300, an ETag derived from the card body, and a Last-Modified of server start. A conditional request carrying If-None-Match or If-Modified-Since is answered with 304 Not Modified, so polling clients revalidate cheaply. The five-minute freshness window is fixed and not configurable.
The default handlers delegate to the agent registered via WithAgent when one is present; without an agent they fall back to a built-in reply, which is what lets the minimal server run with no LLM wired up. Storage, authentication, telemetry, and artifacts are driven by Config (see the configuration reference) and applied during Build().
AgentBuilder
AgentBuilder constructs the OpenAICompatibleAgent that lives inside the server. Create it with NewAgentBuilder(logger), then either seed it with an entire AgentConfig via WithConfig(&cfg) or layer individual setters on top.
agent, err := server.NewAgentBuilder(logger).
WithConfig(&cfg.AgentConfig).
WithLLMClient(llmClient).
WithSystemPrompt("You are a helpful assistant").
WithMaxChatCompletion(10).
WithToolBox(toolBox).
WithCallbacks(&server.CallbackConfig{ /* see Lifecycle callbacks */ }).
Build()| Method | Purpose |
|---|---|
WithConfig(*AgentConfig) | Seed the builder from an AgentConfig (provider, model, key, generation parameters). |
WithLLMClient(LLMClient) | Attach a pre-configured LLM client (build one with NewOpenAICompatibleLLMClient). |
WithToolBox(ToolBox) | Provide a custom toolbox of callable tools. |
WithDefaultToolBox() | Use the default toolbox (built-in input_required, plus create_artifact when enabled). |
WithSystemPrompt(string) | Override the system prompt prepended to every conversation. |
WithMaxChatCompletion(int) | Cap the chat-completion/tool-call loop iterations per turn (default 50). |
WithMaxConversationHistory(int) | Cap the conversation-history messages retained per context (default 20). |
WithCallbacks(*CallbackConfig) | Register lifecycle callbacks (see Lifecycle callbacks). |
Build() | Construct the agent. |
Convenience constructors wrap common combinations: SimpleAgent(logger), AgentWithConfig(logger, cfg), AgentWithLLM(logger, client), and FullyConfiguredAgent(logger, cfg, client, toolBox).
Tools and the ToolBox
A ToolBox is a named collection of Tools the agent exposes to the model as OpenAI-style function calls. The quickest way to define a tool is NewBasicTool(name, description, parametersSchema, executor); register it with toolBox.AddTool(tool).
toolBox := server.NewToolBox() // or NewDefaultToolBox(&cfg.AgentConfig.ToolBoxConfig)
toolBox.AddTool(server.NewBasicTool(
"get_current_time",
"Get the current date and time",
map[string]any{"type": "object", "properties": map[string]any{}},
func(ctx context.Context, args map[string]any) (string, error) {
return server.JSONTool(map[string]any{"now": time.Now().Format(time.RFC3339)})
},
))NewDefaultToolBox(cfg) pre-registers two built-ins:
input_required- always present. The agent intercepts calls to it and pauses the task in theinput-requiredstate, surfacing the tool'smessageargument to the caller. This is the mechanism behind interactive, multi-turn conversations.create_artifact- registered only whenToolBoxConfig.EnableCreateArtifactistrue(envAGENT_CLIENT_TOOLS_CREATE_ARTIFACT=true). It lets the model autonomously persist content as a downloadable artifact. See Artifacts.
To implement a richer tool, satisfy the Tool interface directly (GetName, GetDescription, GetParameters, Execute); BasicTool is just the function-callback implementation of it. To expose tools from an external MCP server instead of defining them inline, see the MCP client.
MCP client
The ADK can connect an agent to one or more Model Context Protocol (MCP) servers over streamable HTTP, discover the tools they expose, and let the LLM invoke them - useful when a common MCP server (or a fleet of them) should be reused across A2A agents. It is built on metoro-io/mcp-golang, disabled by default (MCP_ENABLED=false), and only makes sense when an LLM-backed agent is configured. The Rust ADK ships the same MCP client with an identical selector-tool design.
The selector pattern
A single MCP server can expose dozens or hundreds of tools, and loading every schema into the model context would overwhelm the window. So the ADK does not register MCP tools individually. Instead the MCPClientManager registers just two selector tools into the ToolBox, regardless of how many tools the servers offer:
mcp_list_tools- lists discovered tools (server, name, description, input schema); accepts an optionalsearchfilter.mcp_call_tool- invokes a tool byname(optionally disambiguated byserver) with anargumentsobject.
A typical turn: the model calls mcp_list_tools to find a relevant tool, reads its input schema, then calls mcp_call_tool to run it. Only the metadata the model actually asks for reaches the context window. The catalog is discovered in the background and refreshed on an interval, so mcp_list_tools returns fast from an in-memory snapshot.
Wiring it up
MCPClientManager is a standalone piece you construct, start, and register into the agent's ToolBox. Registration only makes sense with an LLM-backed agent - the selector tools are useless without a model to drive them:
toolBox := server.NewDefaultToolBox(&cfg.AgentConfig.ToolBoxConfig)
if cfg.MCPConfig.Enable {
mcpManager, err := server.NewMCPClientManager(cfg.MCPConfig, logger)
if err != nil {
logger.Fatal("failed to create MCP client manager", zap.Error(err))
}
mcpManager.Start(ctx) // background connect + refresh
defer mcpManager.Close()
mcpManager.RegisterTools(toolBox) // adds mcp_list_tools + mcp_call_tool
}
agent, _ := server.NewAgentBuilder(logger).
WithConfig(&cfg.AgentConfig).
WithLLMClient(llmClient).
WithToolBox(toolBox).
Build()Start returns immediately; it never blocks server startup when an MCP server is not yet up. See the mcp example for a full runnable server.
Resilience
Built for cloud-native rollouts where an MCP server may start after, or restart independently of, the A2A server:
- Connection retries - each server connects in a background goroutine with exponential backoff (
MCP_RETRY_INTERVALdoubling up toMCP_RETRY_MAX_INTERVAL), retrying forever by default (MCP_MAX_RETRIES=0). - Polling backoff - once connected, the catalog refreshes every
MCP_REFRESH_INTERVAL. A failed refresh (server restarted or briefly unreachable) backs off - doubling up toMCP_RETRY_MAX_INTERVAL- and resumes the normal interval as soon as the server responds. A failing server never drops the catalog of the healthy ones.
Tuning knobs for both live in the MCP_ configuration block.
Limitations
- Transport is streamable HTTP only; stdio/subprocess MCP servers are not wired.
- Tool listing fetches a single page - cursor-based pagination is not yet handled.
- MCP resources and prompts are not exposed; tools only.
Task handlers
The ADK defines two handler interfaces, one per protocol entrypoint:
type TaskHandler interface {
HandleTask(ctx context.Context, task *types.Task, message *types.Message) (*types.Task, error)
SetAgent(agent OpenAICompatibleAgent)
GetAgent() OpenAICompatibleAgent
}
type StreamableTaskHandler interface {
HandleStreamingTask(ctx context.Context, task *types.Task, message *types.Message) (<-chan cloudevents.Event, error)
SetAgent(agent OpenAICompatibleAgent)
GetAgent() OpenAICompatibleAgent
}TaskHandlerbacksSendMessageand background queue processing. It runs the task to a terminal state and returns the updatedTask.StreamableTaskHandlerbacksSendStreamingMessage. It returns a channel of CloudEvents that the protocol handler forwards to the client as Server-Sent Events; close the channel when streaming is complete.
You can register your own (WithBackgroundTaskHandler / WithStreamingTaskHandler) or use the bundled defaults (WithDefaultBackgroundTaskHandler / WithDefaultStreamingTaskHandler / WithDefaultTaskHandlers). The default handlers consume the agent's event stream, drive task status transitions, attach usage metadata on terminal states, and handle input-required pausing automatically. Streaming handlers require an agent to be configured.
Blocking SendMessage
SendMessage is blocking per A2A spec section 3.2.2: the server enqueues the task and then holds the response until the task reaches a terminal state (completed, failed, canceled, rejected) or an interrupted one (input-required, auth-required). The reply therefore carries a finished task, not a submitted placeholder. types.TaskState exposes both predicates - IsTerminal() and IsInterrupted() - for handler code that needs the same classification.
A client that does not want to wait sets configuration.returnImmediately: true, which answers with the freshly created task right away and leaves the client to poll GetTask (or subscribe). configuration.historyLength caps the number of history messages on the returned task and is honoured by SendMessage, SendStreamingMessage, GetTask, and ListTasks:
returnImmediately := true
historyLength := 5
resp, err := a2a.SendTask(ctx, types.MessageSendParams{
Message: types.Message{
MessageID: uuid.New().String(),
Role: types.RoleUser,
Parts: []types.Part{types.CreateTextPart("Summarise this quarter's incidents")},
},
Configuration: &types.SendMessageConfiguration{
ReturnImmediately: &returnImmediately,
HistoryLength: &historyLength,
},
})Long-running agents. A task that takes minutes will hold the HTTP request open for minutes, and clients hit their own read timeout first. If your handler is slow, either tell callers to set
returnImmediately: trueand poll, or useSendStreamingMessageso progress arrives as it happens.
Answering without a task
A background handler may also implement the optional server.MessageResponder interface. When RespondToMessage returns a non-nil Message, SendMessage answers with that message directly and creates no task; returning nil falls through to the regular task flow. This suits trivial request/response exchanges where task bookkeeping buys nothing.
type GreetHandler struct{ agent server.OpenAICompatibleAgent }
func (h *GreetHandler) RespondToMessage(ctx context.Context, message *types.Message) (*types.Message, error) {
if !isGreeting(message) {
return nil, nil // not ours - run the normal task flow
}
return &types.Message{
MessageID: uuid.New().String(),
ContextID: message.ContextID,
Role: types.RoleAgent,
Parts: []types.Part{types.CreateTextPart("Hello!")},
}, nil
}HandleTask, SetAgent, and GetAgent are still required - MessageResponder extends the handler, it does not replace it.
Usage metadata
With AGENT_CLIENT_ENABLE_USAGE_METADATA on (the default), the default handlers count the tokens every LLM call of a task spends and how the agent loop ran, and attach both to the task once it reaches a terminal or interrupted state. The ADK publishes them as the usage extension:
- The server declares the extension in the agent card it serves, with
required: false. Your card needs no change. - A request activates it with the
A2A-Extensionsheader, and the response echoes the URI. Tasks returned to a request that did not activate it leave the keys out, and push notifications never carry them. The stored task always keeps them. - The keys are
types.UsageMetadataKey(prompt_tokens,completion_tokens,total_tokens) andtypes.ExecutionStatsMetadataKey(iterations,messages,tool_calls,failed_tools).Task.WithoutExtension(uri)drops an extension's keys from a task.
Streaming completions ask for stream_options.include_usage, and the agent counts the usage from the final chunk, so streamed tasks report their tokens too.
A client activates the extension through its headers and reads the keys from the task metadata:
cfg := client.DefaultConfig("http://localhost:8080")
cfg.Headers["A2A-Extensions"] = types.UsageExtensionURI
a2a := client.NewClientWithConfig(cfg)
usage, ok := (*task.Metadata)[types.UsageMetadataKey].(map[string]any)
if ok {
fmt.Println("tokens:", usage["total_tokens"])
}Streaming and CloudEvents
Agents emit progress as CloudEvents. A StreamableTaskHandler returns <-chan cloudevents.Event; each event carries a typed payload you read with event.DataAs(&target). The event types are stable string constants in the types package, all under the adk.agent.* namespace:
| Constant | Type string | Emitted when |
|---|---|---|
types.EventDelta | adk.agent.delta | An incremental token/text delta is produced. |
types.EventIterationCompleted | adk.agent.iteration.completed | One chat-completion iteration finishes; carries a Message. |
types.EventToolStarted | adk.agent.tool.started | A tool call begins. |
types.EventToolCompleted | adk.agent.tool.completed | A tool call returns successfully. |
types.EventToolFailed | adk.agent.tool.failed | A tool call errors. |
types.EventToolResult | adk.agent.tool.result | A tool result is available. |
types.EventInputRequired | adk.agent.input.required | The agent needs more input; the task pauses. |
types.EventTaskInterrupted | adk.agent.task.interrupted | The task is interrupted. |
types.EventTaskStatusChanged | adk.agent.task.status.changed | The task transitions to a new TaskStatus state. |
types.EventStreamFailed | adk.agent.stream.failed | The agent stream fails. |
types.EventTaskArtifactUpdated | adk.agent.task.artifact.updated | An artifact is produced or extended; carries a TaskArtifactUpdateEvent. |
A handler typically drives the agent with agent.RunWithStream(ctx, messages) and reacts to the events:
streamChan, err := h.agent.RunWithStream(ctx, []types.Message{*message})
if err != nil {
return nil, err
}
var full string
for event := range streamChan {
switch event.Type() {
case types.EventDelta:
var delta types.Message
if event.DataAs(&delta) == nil {
for _, part := range delta.Parts {
if part.Text != nil {
full += *part.Text
}
}
}
case types.EventIterationCompleted:
h.logger.Debug("iteration completed")
}
}Streaming artifacts
A streaming handler emits types.EventTaskArtifactUpdated with a types.TaskArtifactUpdateEvent payload to hand the client an artifact as it is produced. The server fills in taskId and contextId, records the update on the task via Task.ApplyArtifactUpdate, and forwards it to the client as an artifactUpdate response. Set append: true to add parts to an artifact already sent under the same artifactId, and lastChunk: true on the final piece:
func artifactEvent(artifact types.Artifact, appendParts, lastChunk bool) cloudevents.Event {
event := cloudevents.NewEvent()
event.SetType(types.EventTaskArtifactUpdated)
_ = event.SetData(cloudevents.ApplicationJSON, types.TaskArtifactUpdateEvent{
Artifact: artifact,
Append: &appendParts,
LastChunk: &lastChunk,
})
return event
}
// Stream one artifact in two chunks.
chunk := types.Artifact{
ArtifactID: uuid.New().String(),
Parts: []types.Part{types.CreateTextPart("chunk-1 ")},
}
events <- artifactEvent(chunk, false, false)
chunk.Parts = []types.Part{types.CreateTextPart("chunk-2")}
events <- artifactEvent(chunk, true, true)Because the update is recorded on the task, a later GetTask returns the assembled artifact even for a client that missed the stream. The tck-sut example is a reference handler covering chunked artifacts, MessageResponder, and interrupted-task subscription; CI runs the A2A TCK against it.
Lifecycle callbacks
Callbacks hook into agent execution at six points, grouped into three Before/After pairs. Register them on the agent with WithCallbacks(&server.CallbackConfig{...}); each field is a slice, so multiple callbacks run in order.
callbacks := &server.CallbackConfig{
BeforeModel: []server.BeforeModelCallback{
func(ctx context.Context, cc *server.CallbackContext, req *server.LLMRequest) *server.LLMResponse {
// Return a non-nil LLMResponse here to short-circuit the LLM call (caching, guardrails).
return nil // nil => proceed to the model
},
},
AfterTool: []server.AfterToolCallback{
func(ctx context.Context, tool server.Tool, args map[string]any, tc *server.ToolContext, result map[string]any) map[string]any {
// Return a non-nil map to replace the tool result; nil keeps it unchanged.
return nil
},
},
}
agent, _ := server.NewAgentBuilder(logger).WithConfig(&cfg.AgentConfig).WithCallbacks(callbacks).Build()The Before and After callbacks differ in how a non-nil return is treated:
| Callback | Signature returns | A non-nil return... |
|---|---|---|
BeforeAgentCallback | *types.Message | Short-circuits agent execution and uses the message as the final response. |
AfterAgentCallback | *types.Message | Replaces the agent's output. |
BeforeModelCallback | *LLMResponse | Short-circuits the LLM call and uses the response as if it came from the model. |
AfterModelCallback | *LLMResponse | Replaces the raw LLM response. |
BeforeToolCallback | map[string]any | Short-circuits tool execution and uses the map as the tool result. |
AfterToolCallback | map[string]any | Replaces the tool result. |
Flow-control details that hold across all six:
- Before callbacks run in sequence; the first non-nil return wins and skips both the default behavior and any remaining Before callbacks.
- After callbacks chain - each receives the previous callback's (possibly modified) output, and the final non-nil value is used.
- A panic in any callback is recovered and logged; execution then continues with the next callback or the default behavior.
AfterModelalways runs, even whenBeforeModelsupplied a synthetic response, so post-processing (logging, redaction) sees every response.
CallbackContext (agent/model callbacks) and ToolContext (tool callbacks) expose AgentName, InvocationID, TaskID, ContextID, a mutable State map[string]any, and a Logger. See the callbacks example for guardrail, caching, and logging patterns.
The A2A client
client.A2AClient talks to any A2A server - one built with this ADK, or any other A2A-compliant agent. Construct one with client.NewClient(baseURL) for defaults, or client.NewClientWithConfig(&client.Config{...}) to set timeout, headers, retries, a custom *http.Client, or a logger.
import "github.com/inference-gateway/adk/client"
a2a := client.NewClient("http://localhost:8080")
resp, err := a2a.SendTask(ctx, types.MessageSendParams{
Message: types.Message{
MessageID: uuid.New().String(),
Role: types.RoleUser,
Parts: []types.Part{types.CreateTextPart("What's the weather in Berlin?")},
},
})The interface covers the entire A2A JSON-RPC surface:
| Method | A2A method | Returns |
|---|---|---|
GetAgentCard(ctx) | GET /.well-known/agent-card.json | *types.AgentCard |
GetAuthenticatedExtendedCard(ctx, params) | GetExtendedAgentCard | *types.JSONRPCSuccessResponse |
GetHealth(ctx) | GET /health | *client.HealthResponse |
SendTask(ctx, params) | SendMessage | *types.JSONRPCSuccessResponse |
SendTaskStreaming(ctx, params) | SendStreamingMessage | <-chan types.JSONRPCSuccessResponse |
GetTask(ctx, params) | GetTask | *types.JSONRPCSuccessResponse |
ListTasks(ctx, params) | ListTasks | *types.JSONRPCSuccessResponse |
CancelTask(ctx, params) | CancelTask | *types.JSONRPCSuccessResponse |
ResubscribeTask(ctx, params) | SubscribeToTask | <-chan types.JSONRPCSuccessResponse |
SetTaskPushNotificationConfig(ctx, params) | CreateTaskPushNotificationConfig | *types.JSONRPCSuccessResponse |
GetTaskPushNotificationConfig(ctx, params) | GetTaskPushNotificationConfig | *types.JSONRPCSuccessResponse |
ListTaskPushNotificationConfig(ctx, params) | ListTaskPushNotificationConfigs | *types.JSONRPCSuccessResponse |
DeleteTaskPushNotificationConfig(ctx, params) | DeleteTaskPushNotificationConfig | *types.JSONRPCSuccessResponse |
The wire names in the middle column are the A2A v1.0.1 methods, generated from the canonical schema as the types.A2AMethod* constants (types.A2AMethodSendMessage, types.A2AMethodGetTask, ...). Use those constants - not string literals - when you hand-build a JSON-RPC envelope instead of going through the client. The v0.x slash names (message/send, tasks/get, ...) are no longer registered and answer -32601 Method not found.
Configuration helpers (SetTimeout, SetHTTPClient, GetBaseURL, SetLogger, GetLogger) and GetArtifactHelper() round out the interface.
Error handling
Client errors are typed, so a failure is matched with errors.Is / errors.As instead of by its message text. Two shapes come out of the types package:
import (
"errors"
"fmt"
"net/http"
"github.com/inference-gateway/adk/types"
)
resp, err := a2a.GetTask(ctx, types.GetTaskRequest{ID: taskID})
if err != nil {
switch {
case errors.Is(err, types.ErrTaskNotFound):
return fmt.Errorf("task %s expired or was never created", taskID)
case errors.Is(err, types.ErrUnsupportedOperation):
return errors.New("agent rejected the operation in this task state")
}
var rpcErr *types.JSONRPCError
if errors.As(err, &rpcErr) {
return fmt.Errorf("A2A error %d: %s", rpcErr.Code, rpcErr.Message)
}
var statusErr *types.HTTPStatusError
if errors.As(err, &statusErr) && statusErr.StatusCode == http.StatusUnauthorized {
return refreshTokenAndRetry()
}
return err
}*types.JSONRPCError implements error, and its Unwrap maps the wire code to a sentinel, which is what makes errors.Is work:
| Sentinel | Code |
|---|---|
types.ErrParseError | -32700 |
types.ErrInvalidRequest | -32600 |
types.ErrMethodNotFound | -32601 |
types.ErrInvalidParams | -32602 |
types.ErrInternalError | -32603 |
types.ErrServerError | -32000 |
types.ErrTaskNotFound | -32001 |
types.ErrTaskNotCancelable | -32002 |
types.ErrPushNotificationNotSupported | -32003 |
types.ErrUnsupportedOperation | -32004 |
types.ErrExtendedAgentCardNotConfigured | -32007 |
types.ErrVersionNotSupported | -32009 |
An unrecognized code unwraps to nil, so no sentinel matches, but errors.As still yields the *types.JSONRPCError with its raw Code.
*types.HTTPStatusError covers unexpected HTTP statuses from the JSON-RPC endpoint, the agent card endpoint, the health endpoint, and artifact downloads - the cases that never produce a JSON-RPC envelope. It carries:
StatusCode- read this for auth failures (401missing or expired credentials,403a token that authenticates but is not authorized for the agent). See Client-side flow for the discovery-then-retry sequence this feeds.Operation- which request failed (agent card,health check,artifact download), empty for the JSON-RPC endpoint.Body- the raw response body, when the endpoint returned one.
Message text is unchanged from earlier versions, so existing string matching keeps working - but the typed form is the supported way to branch on a failure.
Sending images to an agent
A user message can carry image file parts alongside its text. The agent forwards them to the configured LLM as OpenAI-compatible image_url content parts, so a vision-capable model can read a screenshot, a diagram, or a captcha:
image, err := os.ReadFile("captcha.png")
if err != nil {
log.Fatal(err)
}
encoded := base64.StdEncoding.EncodeToString(image)
resp, err := a2a.SendTask(ctx, types.MessageSendParams{
Message: types.Message{
MessageID: uuid.New().String(),
Role: types.RoleUser,
Parts: []types.Part{
types.CreateTextPart("What characters are in this captcha?"),
types.CreateFilePart("captcha.png", "image/png", &encoded, nil),
},
},
})- Helper vs. literal.
types.CreateFileParttakes plainstringname and media type, but the generatedtypes.FilePartit builds has optionalNameandMediaType(*string). Reading a part back means dereferencing them; building a literal means taking addresses.types.MessageSendParams.Messageis a required value-typedtypes.Message, as above. - Bytes vs URI.
fileWithBytes(the&encodedargument above) is inlined as adata:<mediaType>;base64,<bytes>URL.fileWithUri- the fourth argument - is passed through unchanged, so the provider has to fetch it. Artifact-server URLs that only resolve inside your cluster are typically unreachable from a hosted provider; send bytes in that case. - Ordering. Text and image parts reach the model in the order they appear in the A2A message.
- What is forwarded. Only
image/*parts on user-role messages. Other media types (PDF, audio) are skipped, and file parts on agent-role messages are never sent, because OpenAI-compatible assistant messages cannot carry images. - Model. The operator picks the model with
AGENT_CLIENT_MODEL. A model without vision support rejects the request, and the task ends asfailedcarrying the provider error. - Agent card. An agent that accepts images should advertise it, e.g.
DefaultInputModes: []string{"text/plain", "image/png"}, so clients can discover the capability.
No extra configuration is needed: this works with WithDefaultTaskHandlers() and with custom handlers that call RunWithStream.
Streaming a task
SendTaskStreaming returns a channel of success responses, one per server-sent event:
events, err := a2a.SendTaskStreaming(ctx, params)
if err != nil {
log.Fatal(err)
}
for evt := range events {
payload, _ := json.Marshal(evt.Result)
log.Printf("event: %s", payload)
}Cancelling, listing, and resubscribing
// CancelTask - works for any non-terminal task.
_, _ = a2a.CancelTask(ctx, types.TaskIdParams{ID: taskID})
// ListTasks - Limit (server caps at 100, default 50) + Offset paginate; filter by ContextID or State.
resp, _ := a2a.ListTasks(ctx, types.TaskListParams{Limit: 20, Offset: 0})
// SubscribeToTask - re-attach to a streaming task after the SSE connection dropped.
// Name is a *string on the generated request types.
events, _ := a2a.ResubscribeTask(ctx, types.TaskResubscriptionParams{Name: &taskID})
for evt := range events {
// server re-emits current state, then forwards further events
}ListTasks and GetTask both accept HistoryLength to trim the history of the tasks they return, the same knob SendMessage takes.
SubscribeToTask only works on a task that can still change: a task already in a terminal state is rejected with -32004 (UnsupportedOperationError), since there is nothing left to stream. A task in an interrupted state (input-required, auth-required) is subscribable - the subscription follows it until it terminates.
Push-notification configuration
Register webhooks the server POSTs to as a task changes state. The four methods form a CRUD cycle keyed by the task ID; both the methods themselves and server-side push require CAPABILITIES_PUSH_NOTIFICATIONS=true, which puts capabilities.pushNotifications: true on the agent card.
configID := uuid.New().String()
token := "shared-secret"
_, _ = a2a.SetTaskPushNotificationConfig(ctx, types.TaskPushNotificationConfig{
Name: taskID,
PushNotificationConfig: types.PushNotificationConfig{
ID: &configID,
URL: "https://example.com/webhook",
Token: &token,
},
})
_, _ = a2a.GetTaskPushNotificationConfig(ctx, types.GetTaskPushNotificationConfigParams{Name: &taskID})
_, _ = a2a.ListTaskPushNotificationConfig(ctx, types.ListTaskPushNotificationConfigParams{Parent: &taskID})
_, _ = a2a.DeleteTaskPushNotificationConfig(ctx, types.DeleteTaskPushNotificationConfigParams{Name: &taskID})A config can also be registered inline with the first message: a taskPushNotificationConfig on SendMessage params is stored just as CreateTaskPushNotificationConfig would, saving a round trip. Deletion is idempotent - deleting a config that is already gone succeeds instead of erroring.
Capability gating
A card whose capabilities.pushNotifications is false or absent is unsupported: all four methods are rejected with -32003 (PushNotificationNotSupportedError) before any config is stored, per A2A spec section 5.4. Like the other A2A errors, the envelope carries a google.rpc.ErrorInfo entry in error.data:
{
"jsonrpc": "2.0",
"id": "1",
"error": {
"code": -32003,
"message": "push notification is not supported",
"data": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "PUSH_NOTIFICATION_NOT_SUPPORTED",
"domain": "a2a-protocol.org"
}
]
}
}Behavior change. Earlier versions stored and returned push-notification configs even with the capability disabled - only the outbound sender was gated. Agents that relied on that must now advertise the capability (
CAPABILITIES_PUSH_NOTIFICATIONS=true, orPushNotifications: &[]bool{true}[0]on a hand-built card).
TaskPushNotificationConfig.Name (the set payload) is a plain string, but the get / list / delete params are aliases for the generated request types, where Name and Parent are *string - pass an address, as above. The same holds for TaskResubscriptionParams.Name and CancelTaskRequest.Name; types.TaskIdParams.ID, used by CancelTask, stays a string.
Authenticated extended card
GetAuthenticatedExtendedCard is the JSON-RPC counterpart to the public agent card. The response is the same AgentCard, but the call passes through the server's auth middleware - useful when an extended card should only be visible to authenticated callers. See Card-driven authentication flow for the full discovery-to-extended-card sequence and its error contract.
The protocol-methods example ties all of these together end to end.
Storage backends
Task state lives in a pluggable queue/store selected by QUEUE_PROVIDER:
memory(default) - fast in-memory storage for development and single-instance deployments.redis- persistent storage with horizontal scaling. SetQUEUE_URLto aredis://orrediss://connection string. Tasks survive restarts and multiple server instances can share one queue.
export QUEUE_PROVIDER=redis
export QUEUE_URL=redis://localhost:6379
# auth / db / TLS variants:
export QUEUE_URL=redis://username:password@redis.example.com:6379/1
export QUEUE_URL=rediss://username:password@redis.example.com:6380/0See the queue-storage example for in-memory and Redis variants side by side.
Task retention
Finished tasks (completed, failed, cancelled) are kept after they terminate, bounded by per-state caps:
| Variable | Default | Purpose |
|---|---|---|
TASK_RETENTION_MAX_COMPLETED_TASKS | 100 | Completed tasks retained; 0 disables the cap. |
TASK_RETENTION_MAX_FAILED_TASKS | 50 | Failed/cancelled tasks retained; 0 disables. |
TASK_RETENTION_CLEANUP_INTERVAL | 5m | How often retention trims tasks over the caps. |
Every TASK_RETENTION_CLEANUP_INTERVAL tick the oldest finished tasks beyond each cap are deleted; everything within the caps stays queryable via GetTask and ListTasks. Raise the caps to keep more history, and remember that the in-memory provider still loses everything on restart - use QUEUE_PROVIDER=redis for retention that survives restarts.
QUEUE_CLEANUP_INTERVAL is deprecated and unused. It previously purged every finished task on each tick (default 120s), ignoring the retention caps; setting it now has no effect and it can be removed from your configuration.
Authentication
Enable OIDC/OAuth2 bearer-token authentication with the AUTH_* variables. When enabled, the server validates tokens against the configured issuer before dispatching JSON-RPC calls.
| Variable | Default | Purpose |
|---|---|---|
AUTH_ENABLED | false | Enable OIDC authentication. |
AUTH_ISSUER_URL | http://keycloak:8080/realms/inference-gateway-realm | OIDC issuer for discovery/JWKS. |
AUTH_CLIENT_ID | inference-gateway-client | OIDC client ID; used as the expected token audience when AUTH_AUDIENCE is empty. |
AUTH_AUDIENCE | (empty) | Comma-separated list of accepted aud values, for example an API identifier. Empty means AUTH_CLIENT_ID. |
Card-driven authentication flow
Beyond validating bearer tokens, the ADK implements the A2A spec, section 7 card-driven auth model: the agent card advertises how to authenticate, and clients transmit credentials obtained out-of-band on every request. A2A does not run OAuth flows in-protocol.
- Discovery - the client fetches the public card from
/.well-known/agent-card.json(always unauthenticated). The card declaressecuritySchemes(named schemes the agent accepts:apiKey,http,oauth2,openIdConnect,mutualTLS) andsecurityRequirements(a requirement list with OR-of-ANDs semantics - satisfying any one entry is sufficient). - Credential acquisition is out-of-band - the client obtains a token/key however the chosen scheme dictates.
- Transmission - the client sends the credential (e.g.
Authorization: Bearer <token>) on every request. - Server enforcement - with
AUTH_ENABLED=truethe/a2aendpoint is protected; unauthenticated requests get401with aWWW-Authenticatechallenge. - Extended card - if the card sets
capabilities.extendedAgentCard: true, an authenticated client MAY callGetExtendedAgentCardfor a richer card and SHOULD replace its cached public card with the response.
Declaring security schemes on the card
With AUTH_ENABLED=true the served card must declare securitySchemes so clients can discover how to authenticate. The server.OIDCSecuritySchemes helper builds the OIDC declaration from the auth config:
schemes, security := server.OIDCSecuritySchemes(cfg.AuthConfig)
card.SecuritySchemes = schemes
card.SecurityRequirements = securityOr declare it directly in the card JSON:
{
"securitySchemes": {
"openId": {
"openIdConnectSecurityScheme": {
"openIdConnectUrl": "https://issuer.example.com/realms/app/.well-known/openid-configuration"
}
}
},
"securityRequirements": [{ "schemes": { "openId": { "list": [] } } }]
}If AUTH_ENABLED=true but the card declares no securitySchemes (or the inverse), the server logs a startup warning - discovery would otherwise be broken.
The authenticated extended card
The extended card is served only to authenticated callers via GetExtendedAgentCard. Configure it with the builder; WithExtendedAgentCard also forces capabilities.extendedAgentCard: true on the public card:
srv, _ := server.NewA2AServerBuilder(cfg, logger).
WithAgentCard(publicCard).
WithExtendedAgentCard(extendedCard). // extra skills, capability detail, ...
WithDefaultTaskHandlers().
Build()The error contract (spec 3.3.4) for GetExtendedAgentCard:
| Card state | Result |
|---|---|
Does not declare capabilities.extendedAgentCard | -32004 (UnsupportedOperationError) |
capabilities.extendedAgentCard: true, no extended configured | -32007 (ExtendedAgentCardNotConfigured) |
capabilities.extendedAgentCard: true, extended configured | the extended card is returned |
Card shape. From v0.30.0 the Go ADK serves the A2A v1.0.1
AgentCard: the extended-card flag lives oncapabilities.extendedAgentCard, security requirements onsecurityRequirements, and the endpoint list onsupportedInterfaces(there is no top-levelurl/preferredTransport/protocolVersion, and nocapabilities.stateTransitionHistory). See Deprecated card fields for the mapping from the older names.
Client-side flow
Discover the schemes, attach the out-of-band credential, then fetch the richer card:
c := client.NewClient("https://agent.example.com")
// 1. Discover how to authenticate.
card, _ := c.GetAgentCard(ctx)
_ = card.SecuritySchemes // inspect which schemes the agent accepts
// 2. Attach the out-of-band credential to every request.
authed := client.NewClientWithConfig(&client.Config{
BaseURL: "https://agent.example.com",
Headers: map[string]string{"Authorization": "Bearer " + token},
})
// 3. Fetch the richer authenticated card.
resp, _ := authed.GetAuthenticatedExtendedCard(ctx, types.GetAuthenticatedExtendedCardParams{})Authorization via callbacks
Authentication answers "who is calling"; authorization ("may they do this") is implementation-specific per spec 7.5. Use the agent lifecycle callbacks as the policy seam rather than framework code - a non-nil BeforeAgent return short-circuits the request (deny):
agent, _ := server.NewAgentBuilder(logger).
WithLLMClient(llm).
WithCallbacks(&server.CallbackConfig{
BeforeAgent: []server.BeforeAgentCallback{
func(ctx context.Context, cbCtx *server.CallbackContext) *types.Message {
// inspect identity from ctx; return a non-nil message to deny.
return nil
},
},
}).
Build()BeforeTool / BeforeModel callbacks work the same way for finer-grained gating. The authentication example wires the server and client flow end to end.
Out of scope for app code. mTLS is transport-layer (terminate at the proxy; a card may still declare the
mutualTLSscheme). OAuth2 credential acquisition is out-of-band (spec 7.3) - the client only transmits credentials.
Artifacts
Artifacts are files, structured data, or rich content an agent produces during a task. Enable the subsystem with ARTIFACTS_ENABLED=true and pick a storage backend with ARTIFACTS_STORAGE_PROVIDER:
filesystem(default) - store on local disk (ARTIFACTS_STORAGE_BASE_PATH), suitable for single-instance deployments.minio- S3-compatible object storage for distributed deployments. ConfigureARTIFACTS_STORAGE_ENDPOINT,ARTIFACTS_STORAGE_ACCESS_KEY,ARTIFACTS_STORAGE_SECRET_KEY, andARTIFACTS_STORAGE_BUCKET_NAME.
Storage Layout
Stored artifacts are grouped by the A2A context (session) id. Both backends use the same layout:
{contextId}/{artifactId}/{filename}Download URLs mirror the storage layout. When proxied through the artifacts HTTP server, the URL is /artifacts/{contextId}/{artifactId}/{filename}. When using direct URLs (ARTIFACTS_STORAGE_BASE_URL), the path follows the same shape.
Clients are unaffected - they always fetch the opaque uri returned on the file part.
By default downloads are proxied through the artifacts HTTP server (ARTIFACTS_SERVER_PORT, default 8081) for auth and logging; set ARTIFACTS_STORAGE_BASE_URL to serve direct download URLs from the storage backend instead.
Artifact retention
| Variable | Default | Purpose |
|---|---|---|
ARTIFACTS_RETENTION_MAX_ARTIFACTS | 5 | Artifacts kept per context, oldest deleted first; 0 = unlimited. |
ARTIFACTS_RETENTION_MAX_AGE | 168h | Maximum artifact age; 0 = no age limit. |
ARTIFACTS_RETENTION_CLEANUP_INTERVAL | 24h | How often cleanup runs; 0 = manual cleanup only. |
ARTIFACTS_RETENTION_MAX_ARTIFACTS is a per-context cap: each cleanup run keeps the newest N artifacts under {contextId}/ and deletes the rest. Both caps are enforced by that background run on the ARTIFACTS_RETENTION_CLEANUP_INTERVAL schedule, not at artifact creation - so a context can exceed the cap between runs, and growth is bounded by the interval as much as by the cap. The Rust ADK uses the same variable name and default but caps the store as a whole rather than per context.
Durations are Go durations: d is not a valid unit, so write 168h, not 7d.
There are two ways to produce artifacts:
Programmatically, from a custom handler, with the artifact helper:
gohelper := server.NewArtifactHelper() art := helper.CreateTextArtifact("Report", "Analysis output", "...content...") helper.AddArtifactToTask(task, art)The helper also creates file artifacts (
CreateFileArtifactFromBytes(ctx, contextID, ...),CreateFileArtifactFromURI(ctx, contextID, ...)- these now takecontextIDas their first argument), structured-data artifacts (CreateDataArtifact), and multi-part artifacts (CreateMultiPartArtifact).Autonomously, by enabling the built-in
create_artifacttool (AGENT_CLIENT_TOOLS_CREATE_ARTIFACT=trueplusWithDefaultToolBox()), letting the model save content and return a download URL on its own. The default handlers automatically passtask.ContextID.
On the client side, a2a.GetArtifactHelper() extracts artifacts from responses and downloads them to disk, handling both byte-embedded and URI-referenced files:
helper := a2a.GetArtifactHelper()
// Blocking SendMessage: unwrap the task from the wrapped result.
resp, err := a2a.SendTask(ctx, params)
if err != nil {
log.Fatal(err)
}
task, err := helper.ExtractTaskFromResponse(resp)
if err != nil {
log.Fatal(err)
}
_, _ = helper.DownloadAllArtifacts(ctx, task, &client.DownloadConfig{
OutputDir: "downloads",
OrganizeByArtifactID: true,
})
// Streaming: artifactUpdate responses arrive as chunks, task snapshots as tasks.
events, err := a2a.SendTaskStreaming(ctx, params)
if err != nil {
log.Fatal(err)
}
for evt := range events {
if update, ok := helper.ExtractArtifactUpdateFromStreamEvent(evt.Result); ok {
log.Printf("artifact chunk: %s", update.Artifact.ArtifactID)
continue
}
task, err := helper.ExtractTaskFromResponse(&evt)
if err != nil {
continue
}
log.Printf("task %s carries %d artifacts", task.ID, helper.GetArtifactCount(task))
}ExtractTaskFromResponse unwraps the task field from a SendMessage or streaming result and still accepts the bare task GetTask returns. It returns an error when the result carries no task - a direct message reply, a status update, or an artifact update - so skip those events rather than treating the zero-value task as empty. ExtractArtifactUpdateFromStreamEvent matches an artifactUpdate stream response (the v0.3 kind: artifact-update discriminator is gone) and also accepts a bare types.TaskArtifactUpdateEvent or either in decoded map[string]any form, for a custom SSE consumer.
Artifacts attached to a task without an artifactUpdate event never reach the stream as chunks - they appear on the task snapshot, or via GetTask once the task completes.
The artifacts-filesystem, artifacts-minio, artifacts-autonomous-tool, and artifacts-with-default-handlers examples cover each path; the upstream artifacts guide has the full API.
Telemetry
The ADK's telemetry layer is built on OpenTelemetry. TELEMETRY_ENABLED=true is the master switch; once it is on, the standard OTEL_* variables select which exporters run per signal. Metrics are exported either through a Prometheus pull endpoint (/metrics, default port 9090) or pushed via OTLP to a collector; traces are exported over OTLP by default. The OpenTelemetry service.name resource attribute is derived from the agent's build-time name (the agent card name), and service.version from the build-time version - so no extra variables are needed. For agents generated by the ADL CLI, these values are sourced from the ADL manifest metadata.name and metadata.version via Go linker flags. They are build-time metadata and are NOT overridable via environment variables (the ADK does not env-map them).
export TELEMETRY_ENABLED=true
export OTEL_METRICS_EXPORTER=prometheus # prometheus | otlp | none
export OTEL_TRACES_EXPORTER=otlp # otlp | none
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318Exporters are selected with the standard OpenTelemetry variables, which are read without a prefix:
| Variable | Default | Purpose |
|---|---|---|
TELEMETRY_ENABLED | false | Master switch for the telemetry subsystem. |
OTEL_METRICS_EXPORTER | prometheus | Metrics exporter: prometheus, otlp, or none. |
OTEL_TRACES_EXPORTER | otlp | Traces exporter: otlp or none. |
OTEL_EXPORTER_OTLP_ENDPOINT | http://localhost:4318 | OTLP endpoint base URL shared by traces and metrics. |
OTEL_EXPORTER_OTLP_PROTOCOL | http/protobuf | OTLP transport: http/protobuf or grpc. |
OTEL_EXPORTER_PROMETHEUS_HOST | (all) | Bind host for the Prometheus pull endpoint (empty = all). |
OTEL_EXPORTER_PROMETHEUS_PORT | 9090 | Port for the Prometheus pull endpoint. |
When OTEL_METRICS_EXPORTER=otlp, metrics are pushed to OTEL_EXPORTER_OTLP_ENDPOINT with a periodic reader and the Prometheus pull server is not started - the pull server runs only when metrics resolve to the prometheus exporter.
The session-id and tool-call-id keys used for both baggage propagation and the span attributes written from them are configurable, and default to the OTel semantic conventions:
| Variable | Default | Purpose |
|---|---|---|
TELEMETRY_ATTR_SESSION_ID_KEY | session.id | Baggage member and span attribute key for the session id. |
TELEMETRY_ATTR_TOOL_CALL_ID_KEY | gen_ai.tool.call.id | Baggage member and span attribute key for the tool-call id. |
The original TELEMETRY_* names remain supported as deprecated aliases (TELEMETRY_METRICS_HOST/PORT, TELEMETRY_TRACE_ENDPOINT, TELEMETRY_TRACE_HEADERS); the OTEL_* variable wins whenever both are set. Prefer the standard names in new deployments. See the upstream telemetry guide for the full reference.
Build-time agent metadata
The agent's identity fields (AgentName, AgentDescription, AgentVersion) are not environment-configurable - they are baked into the binary at build time through Go linker flags, making them immutable in production images. The package exposes three variables in server you can set with -ldflags:
| Variable | LD-flag target |
|---|---|
server.BuildAgentName | github.com/inference-gateway/adk/server.BuildAgentName |
server.BuildAgentDescription | github.com/inference-gateway/adk/server.BuildAgentDescription |
server.BuildAgentVersion | github.com/inference-gateway/adk/server.BuildAgentVersion |
go build -ldflags "\
-X 'github.com/inference-gateway/adk/server.BuildAgentName=Weather Assistant' \
-X 'github.com/inference-gateway/adk/server.BuildAgentDescription=AI-powered weather agent' \
-X 'github.com/inference-gateway/adk/server.BuildAgentVersion=0.1.1'" \
-o bin/agent .In a Dockerfile, drive these from ARGs so images can be tagged per release:
FROM golang:1.26.7-alpine AS builder
ARG AGENT_NAME="My A2A Agent"
ARG AGENT_DESCRIPTION="A custom A2A agent built with the ADK"
ARG AGENT_VERSION="0.1.0"
WORKDIR /app
COPY . .
RUN go mod download && go build -ldflags "\
-X 'github.com/inference-gateway/adk/server.BuildAgentName=${AGENT_NAME}' \
-X 'github.com/inference-gateway/adk/server.BuildAgentDescription=${AGENT_DESCRIPTION}' \
-X 'github.com/inference-gateway/adk/server.BuildAgentVersion=${AGENT_VERSION}'" -o bin/agent .Reference them in code as server.BuildAgentName, etc., when constructing the Config and AgentCard.
Configuration reference
config.Config is loaded with config.Load(ctx, &base), which overlays environment variables onto an optional base config and applies struct-tag defaults. Variables are grouped by prefix; every value is optional and falls back to the default shown.
Core server - the listener and top-level toggles.
| Variable | Default | Purpose |
|---|---|---|
SERVER_PORT | 8080 | HTTP listener port. |
DEBUG | false | Verbose logging. |
AGENT_URL | (unset) | Public URL advertised for the agent. |
AGENT_CARD_FILE_PATH | (unset) | Path to a static agent-card JSON file. |
TIMEZONE | UTC | Timezone for timestamps. |
STREAMING_STATUS_UPDATE_INTERVAL | 1s | How often to emit streaming status updates. |
SERVER_TLS_ENABLED | false | Terminate TLS/HTTPS on the listener. |
SERVER_TLS_CERT_PATH | (unset) | Path to the TLS certificate. |
SERVER_TLS_KEY_PATH | (unset) | Path to the TLS private key. |
Agent / LLM client (AGENT_CLIENT_ prefix)
| Variable | Default | Purpose |
|---|---|---|
AGENT_CLIENT_PROVIDER | (unset) | LLM provider (openai, anthropic, ...). |
AGENT_CLIENT_MODEL | (unset) | Model name. |
AGENT_CLIENT_BASE_URL | (unset) | Custom LLM endpoint URL. |
AGENT_CLIENT_API_KEY | (unset) | Provider API key. |
AGENT_CLIENT_TIMEOUT | 30s | Per-request timeout. |
AGENT_CLIENT_MAX_RETRIES | 3 | Retry budget for failed LLM calls, streaming and non-streaming. |
AGENT_CLIENT_MAX_CHAT_COMPLETION_ITERATIONS | 50 | Max tool-call/completion loop iterations. |
AGENT_CLIENT_MAX_TOKENS | 4096 | Max tokens per completion. |
AGENT_CLIENT_TEMPERATURE | 0.7 | Sampling temperature. |
AGENT_CLIENT_SYSTEM_PROMPT | (default prompt) | System prompt prepended to conversations. |
AGENT_CLIENT_MAX_CONVERSATION_HISTORY | 20 | Messages retained per context. |
AGENT_CLIENT_ENABLE_USAGE_METADATA | true | Serve the usage extension on terminal tasks. |
AGENT_CLIENT_TOOLS_CREATE_ARTIFACT | false | Register the autonomous create_artifact tool. |
AGENT_CLIENT_MAX_RETRIES covers both the non-streaming CreateChatCompletion call and the streaming path AgentBuilder agents use. On the streaming path only the attempt to open the upstream SSE connection is retried - up to AGENT_CLIENT_MAX_RETRIES times with a one second linear backoff. Once the first delta has been forwarded, a stream that breaks fails the task: deltas are never replayed.
Capabilities (CAPABILITIES_ prefix)
| Variable | Default | Purpose |
|---|---|---|
CAPABILITIES_STREAMING | true | Advertise SendStreamingMessage support. |
CAPABILITIES_PUSH_NOTIFICATIONS | true | Advertise push-notification support. Required for the push-notification config methods - with it false they answer -32003. |
CAPABILITIES_STATE_TRANSITION_HISTORY is gone: the A2A v1.0.1 AgentCard dropped the flag, so the ADK no longer reads it and generated configurations no longer set it.
MCP client (MCP_ prefix) - connect the agent to MCP servers; disabled by default. When the ADK config is nested under A2A_ (as in the examples), the prefix becomes A2A_MCP_.
| Variable | Default | Purpose |
|---|---|---|
MCP_ENABLED | false | Enable the MCP client. |
MCP_SERVERS | (unset) | Comma-separated MCP server base URLs, e.g. http://mcp:8080. |
MCP_ENDPOINT | /mcp | Path appended to each server URL for the streamable MCP endpoint. |
MCP_REFRESH_INTERVAL | 5m | How often to refresh the tool catalog from each server. |
MCP_DIAL_TIMEOUT | 30s | Timeout for initializing / listing tools. |
MCP_CALL_TIMEOUT | 30s | Timeout for a single tool invocation. |
MCP_MAX_RETRIES | 0 | Max initial connection attempts per server (0 = retry forever). |
MCP_RETRY_INTERVAL | 2s | Initial backoff between connection/refresh retries (doubles). |
MCP_RETRY_MAX_INTERVAL | 30s | Maximum backoff between retries. |
Queue / storage, retention, auth, telemetry, artifacts are documented in Storage backends, Task retention, Authentication, Telemetry, and Artifacts. QUEUE_CLEANUP_INTERVAL is deprecated and unused - see Task retention.
Examples
The examples/ directory ships eighteen runnable scenarios, each a self-contained Go module with its own README and (where relevant) a docker-compose.yaml.
Without an LLM - no provider keys required:
| Example | What it shows |
|---|---|
| minimal | Bare A2A server + client with an echo handler - no agent. |
| static-agent-card | Load agent metadata from a JSON file via WithAgentCardFromFile. |
| streaming | A custom streaming handler emits CloudEvents over SSE. |
| default-handlers | Built-in task processing with WithDefaultTaskHandlers(). |
| protocol-methods | End-to-end walk-through of every A2A JSON-RPC method. |
| tck-sut | Reference handler exercised by the A2A TCK in CI. |
With an LLM - require an Inference Gateway / provider key:
| Example | What it shows |
|---|---|
| ai-powered | An LLM agent with custom function tools (weather, time). |
| ai-powered-streaming | The same agent streamed over SendStreamingMessage. |
| callbacks | Lifecycle hooks for guardrails, caching, and logging. |
| usage-metadata | Token usage + execution metrics attached to terminal tasks. |
| input-required | Interactive pausing in input-required, streaming and non-streaming. |
| mcp | Reach an MCP server's tools through the mcp_list_tools / mcp_call_tool selector tools. |
Storage, security, and artifacts:
| Example | What it shows |
|---|---|
| queue-storage | In-memory and Redis storage backends. |
| tls-server | HTTPS termination with TLS certificates. |
| artifacts-filesystem | Filesystem-backed downloadable artifacts. |
| artifacts-minio | MinIO/S3-backed artifacts with direct and proxy download modes. |
| artifacts-autonomous-tool | The model creates artifacts via the create_artifact tool. |
| artifacts-with-default-handlers | Artifacts combined with the default handlers. |
Related
- Agent Definition Language (ADL) - define an agent declaratively in YAML instead of wiring the builders by hand.
- ADL CLI - scaffold an A2A agent project (built on this ADK) from an ADL file, then fill in the generated tool stubs.
- A2A Integration - the Agent-to-Agent protocol these agents speak, and how the gateway consumes them.
- TypeScript ADK - the sibling ADK for Node/TypeScript agents.
- Rust ADK - the sibling ADK for Rust agents.
- A2A Debugger - inspect and exercise A2A agents from the command line.
- Go ADK on GitHub - source, issues, and the full example catalogue.
- Inference Gateway - the gateway the agents call for LLM completions.
