Announcing slim-a2a-go 0.1.0: Native A2A over SLIM for Go
We’re excited to announce the initial release of slim-a2a-go (v0.1.0): a Go library that lets any A2A agent communicate over SLIM instead of HTTP or gRPC — in just a few lines of code.
Why slim-a2a-go?
The A2A (Agent-to-Agent) protocol defines a standard interface for AI agents to discover and communicate with each other. Its reference Go SDK, a2a-go, ships with two transports out of the box: HTTP/JSON-RPC and gRPC. Both are great for many scenarios, but they carry the usual baggage of TCP-based networking: you need to know the remote agent’s hostname and port, manage TLS certificates, deal with firewalls and NAT traversal, and so on.
SLIM (Secure Low-Latency Interactive Messaging)
takes a different approach. Instead of addressing agents by IP and port, SLIM
uses a hierarchical naming scheme — think agntcy/demo/weather_agent — and
routes messages through lightweight SLIM nodes that act as brokers. Your agents
subscribe to a name, and peers reach them by that name, regardless of where they
physically run. SLIM also provides end-to-end MLS encryption and session-layer
reliability out of the box.
slim-a2a-go is the bridge between these two worlds. It adds SLIM as a third
transport option for a2a-go, letting you swap HTTP for SLIM with minimal
changes to your agent code.
Architecture
The library has two packages that mirror the client/server split in a2a-go:
a2aclient— wraps aslim_bindings.Channeland implements the fulla2aclient.Transportinterface, so every A2A operation (send message, get task, streaming, push notifications…) works over SLIM.a2aserver— wraps ana2asrv.RequestHandlerand registers it with aslim_bindings.Server, converting incoming SLIM RPC calls back into A2A domain types.
The generated protobuf/SLIM-RPC stubs in the a2apb package provide the
wire-format layer, produced by the
protoc-gen-slimrpc-go plugin from the
official A2A gRPC specification.
sequenceDiagram
participant C as Client<br/>a2aclient.Transport
participant SN as SLIM Node
participant S as Server<br/>a2aserver.Handler
C->>SN: subscribe(agntcy/demo/client)
S->>SN: subscribe(agntcy/demo/echo_agent)
C->>SN: SendMessage (protobuf over SLIM RPC)
SN->>S: routed by name
S->>SN: TaskStatusUpdateEvent + ArtifactUpdateEvent
SN->>C: streamed response
Under the hood, messages are serialised to protobuf (the same wire format as the
gRPC transport) and then carried over SLIM’s own binary transport layer. The
application code only ever sees a2a-go’s domain types — *a2a.Message,
*a2a.Task, etc.
Getting Started
Add slim-a2a-go to your module:
go get github.com/agntcy/slim-a2a-go@v0.1.0
Then download the pre-compiled SLIM native library:
go run github.com/agntcy/slim-bindings-go/cmd/slim-bindings-setup
This installs a platform-specific shared library that the Go bindings link
against at runtime. It is a one-time step per developer machine and is already
baked into the CI workflow in the slim-a2a-go repository.
Prerequisites
| Requirement | Version |
|---|---|
| Go | 1.23+ |
| SLIM node | any release from agntcy/slim |
slimctl (optional) |
same release |
A SLIM node can be started locally in seconds:
# Option 1 – via slimctl
slimctl slim start --endpoint 127.0.0.1:46357
# Option 2 – via Docker
docker run -p 46357:46357 ghcr.io/agntcy/slim:latest \
/slim --config /config.yaml
Repository layout
slim-a2a-go/
├── a2aclient/ # Client transport (slim_bindings.Channel → a2aclient.Transport)
├── a2aserver/ # Server handler (a2asrv.RequestHandler → slim_bindings.Server)
├── a2apb/ # Generated SLIM RPC stubs (protoc-gen-slimrpc-go)
└── examples/
└── echo_agent/
├── cmd/server/ # Echo agent server
└── cmd/client/ # Echo agent client
Example 1: A Simple Native A2A Agent over SLIM
Let’s walk through the echo-agent example that ships with the library. It demonstrates the full server + client lifecycle in about 80 lines each.
The server
An A2A server over SLIM has four steps:
- Initialise the runtime
- Create an application identity
- Build the A2A handler stack
- Call
Serve()
The full source is at examples/echo_agent/cmd/server/main.go.
// examples/echo_agent/cmd/server/main.go
package main
import (
"context"
"fmt"
"log/slog"
"github.com/a2aproject/a2a-go/a2a"
"github.com/a2aproject/a2a-go/a2asrv"
"github.com/a2aproject/a2a-go/a2asrv/eventqueue"
"github.com/agntcy/slim-a2a-go/a2aserver"
slim_bindings "github.com/agntcy/slim-bindings-go"
)
func run(endpoint string) error {
// 1. Initialise the global SLIM runtime (Rust library via CGo).
slim_bindings.InitializeWithDefaults()
svc := slim_bindings.GetGlobalService()
// 2. Create this agent's SLIM identity using a hierarchical name (org, namespace, service).
name := slim_bindings.NewName("agntcy", "demo", "echo_agent")
app, err := svc.CreateAppWithSecret(name, "my_shared_secret_for_testing_purposes_only")
if err != nil {
return fmt.Errorf("create app: %w", err)
}
// 3. Connect to the SLIM routing node and subscribe to our name.
connID, err := svc.Connect(slim_bindings.NewInsecureClientConfig(endpoint))
if err != nil {
return fmt.Errorf("connect: %w", err)
}
if err := app.Subscribe(name, &connID); err != nil {
return fmt.Errorf("subscribe: %w", err)
}
// 4. Build the A2A handler stack and register it with the SLIM server.
requestHandler := a2asrv.NewHandler(&echoExecutor{})
slimHandler := a2aserver.NewHandler(requestHandler)
server := slim_bindings.ServerNewWithConnection(app, name, &connID)
slimHandler.RegisterWith(server)
slog.Info("echo agent ready", "slim_name", "agntcy/demo/echo_agent")
return server.Serve()
}
The agent logic lives in an AgentExecutor, identical to any a2a-go executor
regardless of transport:
type echoExecutor struct{}
func (e *echoExecutor) Execute(
ctx context.Context,
reqCtx *a2asrv.RequestContext,
queue eventqueue.Queue,
) error {
if reqCtx.StoredTask == nil {
if err := queue.Write(ctx, a2a.NewStatusUpdateEvent(reqCtx, a2a.TaskStateSubmitted, nil)); err != nil {
return err
}
}
if err := queue.Write(ctx, a2a.NewStatusUpdateEvent(reqCtx, a2a.TaskStateWorking, nil)); err != nil {
return err
}
text := extractText(reqCtx.Message)
if err := queue.Write(ctx, a2a.NewArtifactEvent(reqCtx, a2a.TextPart{Text: text})); err != nil {
return err
}
completed := a2a.NewStatusUpdateEvent(reqCtx, a2a.TaskStateCompleted, nil)
completed.Final = true
return queue.Write(ctx, completed)
}
func (e *echoExecutor) Cancel(
ctx context.Context,
reqCtx *a2asrv.RequestContext,
queue eventqueue.Queue,
) error {
evt := a2a.NewStatusUpdateEvent(reqCtx, a2a.TaskStateCanceled, nil)
evt.Final = true
return queue.Write(ctx, evt)
}
Notice that nothing in the executor knows about SLIM — it just writes events to the queue, exactly as it would for an HTTP or gRPC server.
The client
On the client side, swap the usual HTTP/gRPC transport for slima2aclient.NewTransport:
The full source is at examples/echo_agent/cmd/client/main.go.
// examples/echo_agent/cmd/client/main.go
package main
import (
"context"
"fmt"
"github.com/a2aproject/a2a-go/a2a"
slima2aclient "github.com/agntcy/slim-a2a-go/a2aclient"
slim_bindings "github.com/agntcy/slim-bindings-go"
)
func run(endpoint, text string) error {
slim_bindings.InitializeWithDefaults()
svc := slim_bindings.GetGlobalService()
// The client needs its own SLIM identity so it can receive replies.
// The org and namespace don't have to match the server's — any unique
// name on the SLIM node works.
localName := slim_bindings.NewName("agntcy", "demo", "client")
app, err := svc.CreateAppWithSecret(localName, "my_shared_secret_for_testing_purposes_only")
if err != nil {
return fmt.Errorf("create app: %w", err)
}
connID, err := svc.Connect(slim_bindings.NewInsecureClientConfig(endpoint))
if err != nil {
return fmt.Errorf("connect: %w", err)
}
if err := app.Subscribe(localName, &connID); err != nil {
return fmt.Errorf("subscribe: %w", err)
}
// Open a channel to the echo agent by its SLIM name.
remoteName := slim_bindings.NewName("agntcy", "demo", "echo_agent")
channel := slim_bindings.ChannelNewWithConnection(app, remoteName, &connID)
defer channel.Destroy()
// Wrap the channel in the SLIM A2A transport.
transport := slima2aclient.NewTransport(channel)
defer transport.Destroy() //nolint:errcheck
result, err := transport.SendMessage(context.Background(), &a2a.MessageSendParams{
Message: &a2a.Message{
ID: a2a.NewMessageID(),
Role: a2a.MessageRoleUser,
Parts: []a2a.Part{a2a.TextPart{Text: text}},
},
})
if err != nil {
return fmt.Errorf("send message: %w", err)
}
fmt.Printf("> %s\n", text)
printResult(result)
return nil
}
Running the example
# Terminal 1 – server
go run ./examples/echo_agent/cmd/server
# Terminal 2 – client
go run ./examples/echo_agent/cmd/client --text "hello from SLIM"
# Output:
# > hello from SLIM
# hello from SLIM
Example 2: Integrating slim-a2a-go with Google ADK-Go
ADK-Go is Google’s open-source framework
for building AI agents backed by Gemini. Its examples/a2a example exposes an
ADK agent as an A2A server over HTTP/JSON-RPC and connects to it via a
remoteagent.NewA2A client. Let’s replace the HTTP transport with SLIM.
The original HTTP-based example
The original startWeatherAgentServer function looks like this:
// original: google/adk-go/examples/a2a/main.go (HTTP version)
func startWeatherAgentServer() string {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
baseURL := &url.URL{Scheme: "http", Host: listener.Addr().String()}
go func() {
adkAgent := newWeatherAgent(context.Background())
agentCard := &a2a.AgentCard{
Name: adkAgent.Name(),
Skills: adka2a.BuildAgentSkills(adkAgent),
PreferredTransport: a2a.TransportProtocolJSONRPC,
URL: baseURL.JoinPath("/invoke").String(),
}
mux := http.NewServeMux()
mux.Handle(a2asrv.WellKnownAgentCardPath, a2asrv.NewStaticAgentCardHandler(agentCard))
executor := adka2a.NewExecutor(adka2a.ExecutorConfig{...})
requestHandler := a2asrv.NewHandler(executor)
mux.Handle("/invoke", a2asrv.NewJSONRPCHandler(requestHandler))
http.Serve(listener, mux)
}()
return baseURL.String()
}
func main() {
a2aServerAddress := startWeatherAgentServer()
remoteAgent, _ := remoteagent.NewA2A(remoteagent.A2AConfig{
Name: "A2A Weather agent",
AgentCardSource: a2aServerAddress, // fetches the card over HTTP
})
// ...
}
The key components we’ll replace:
http.NewServeMux()+a2asrv.NewJSONRPCHandler()+http.Serve()→ SLIM server- The HTTP agent-card URL lookup → pass the
*a2a.AgentCarddirectly - The default
a2aclient.Factory(JSON-RPC/gRPC) → a SLIM-transport factory
The SLIM-based server
import (
"github.com/agntcy/slim-a2a-go/a2aserver"
slima2aclient "github.com/agntcy/slim-a2a-go/a2aclient"
slim_bindings "github.com/agntcy/slim-bindings-go"
)
const (
slimEndpoint = "http://127.0.0.1:46357"
sharedSecret = "my_shared_secret_for_testing_purposes_only"
)
// startWeatherAgentServer registers the ADK weather agent as an A2A service
// over SLIM and returns its agent card.
func startWeatherAgentServer(svc *slim_bindings.Service) *a2a.AgentCard {
// NewName takes (org, namespace, service).
agentSLIMName := slim_bindings.NewName("agntcy", "demo", "weather_agent")
app, err := svc.CreateAppWithSecret(agentSLIMName, sharedSecret)
if err != nil {
log.Fatalf("create server app: %v", err)
}
connID, err := svc.Connect(slim_bindings.NewInsecureClientConfig(slimEndpoint))
if err != nil {
log.Fatalf("connect: %v", err)
}
if err := app.Subscribe(agentSLIMName, &connID); err != nil {
log.Fatalf("subscribe: %v", err)
}
adkAgent := newWeatherAgent(context.Background())
agentCard := &a2a.AgentCard{
Name: adkAgent.Name(),
Skills: adka2a.BuildAgentSkills(adkAgent),
PreferredTransport: slima2aclient.SLIMProtocol,
// URL must be the SLIM name in "org/namespace/service" form and must
// match the fields used in agentSLIMName — the client factory uses it
// to resolve the remote endpoint.
URL: "agntcy/demo/weather_agent",
Capabilities: a2a.AgentCapabilities{Streaming: true},
}
executor := adka2a.NewExecutor(adka2a.ExecutorConfig{
RunnerConfig: runner.Config{
AppName: adkAgent.Name(),
Agent: adkAgent,
SessionService: session.InMemoryService(),
},
})
requestHandler := a2asrv.NewHandler(executor)
slimHandler := a2aserver.NewHandler(requestHandler)
server := slim_bindings.ServerNewWithConnection(app, agentSLIMName, &connID)
slimHandler.RegisterWith(server)
log.Printf("A2A/SLIM server starting at agntcy/demo/weather_agent")
go func() {
if err := server.Serve(); err != nil {
log.Printf("SLIM server stopped: %v", err)
}
}()
return agentCard
}
The SLIM client factory
remoteagent.NewA2A accepts a ClientFactory field that tells ADK how to build
an a2aclient.Client for the remote agent. We create one that knows how to open
a SLIM channel given the hierarchical SLIM name stored in the agent card’s URL:
func newSLIMClientFactory(
app *slim_bindings.App,
conn *slim_bindings.ConnectionID,
) *a2aclient.Factory {
slimTransportFactory := a2aclient.TransportFactoryFn(
func(ctx context.Context, url string, _ *a2a.AgentCard) (a2aclient.Transport, error) {
// url is the SLIM name in "org/project/component" form.
remoteName, err := slim_bindings.NewNameFromString(url)
if err != nil {
return nil, fmt.Errorf("invalid SLIM name %q: %w", url, err)
}
channel := slim_bindings.ChannelNewWithConnection(app, remoteName, conn)
return slima2aclient.NewTransport(channel), nil
},
)
return a2aclient.NewFactory(
a2aclient.WithDefaultsDisabled(),
a2aclient.WithTransport(slima2aclient.SLIMProtocol, slimTransportFactory),
)
}
WithDefaultsDisabled() prevents the factory from registering the built-in
JSON-RPC and gRPC transports — we only want SLIM here.
Putting it all together
func main() {
ctx := context.Background()
// Initialise the SLIM runtime once for the whole process.
slim_bindings.InitializeWithDefaults()
svc := slim_bindings.GetGlobalService()
// Start the weather agent over SLIM.
agentCard := startWeatherAgentServer(svc)
// Create a separate SLIM identity for the ADK client.
clientName := slim_bindings.NewName("agntcy", "demo", "adk_client")
clientApp, err := svc.CreateAppWithSecret(clientName, sharedSecret)
if err != nil {
log.Fatalf("create client app: %v", err)
}
clientConn, err := svc.Connect(slim_bindings.NewInsecureClientConfig(slimEndpoint))
if err != nil {
log.Fatalf("connect client: %v", err)
}
if err := clientApp.Subscribe(clientName, &clientConn); err != nil {
log.Fatalf("subscribe client: %v", err)
}
// Wire up the remote ADK agent backed by the SLIM client factory.
remoteAgent, err := remoteagent.NewA2A(remoteagent.A2AConfig{
Name: "A2A Weather agent",
AgentCard: agentCard, // no HTTP lookup needed
ClientFactory: newSLIMClientFactory(clientApp, &clientConn),
})
if err != nil {
log.Fatalf("create remote agent: %v", err)
}
config := &launcher.Config{
AgentLoader: agent.NewSingleLoader(remoteAgent),
}
l := full.NewLauncher()
if err = l.Execute(ctx, config, os.Args[1:]); err != nil {
log.Fatalf("run failed: %v\n\n%s", err, l.CommandLineSyntax())
}
}
The ADK launcher, the remoteagent, and all the ADK session/runner machinery
are completely unchanged — only the transport wiring is different. The key
differences from the original HTTP example are summarised in the diagram below:
flowchart LR
subgraph original["Original (HTTP)"]
direction LR
A1[ADK Launcher] -->|remoteagent.NewA2A| B1[HTTP JSON-RPC Client]
B1 -->|HTTP POST /invoke| C1[http.ServeMux]
C1 --> D1[a2asrv.JSONRPCHandler]
D1 --> E1[adka2a.Executor]
end
subgraph slim["SLIM Transport"]
direction LR
A2[ADK Launcher] -->|remoteagent.NewA2A| B2[SLIM Transport Client]
B2 -->|SLIM RPC protobuf| C2[SLIM Node]
C2 -->|routed by name| D2[slim_bindings.Server]
D2 --> E2[a2aserver.Handler]
E2 --> F2[adka2a.Executor]
end
What’s Next
This 0.1.0 release establishes the core transport layer. On the roadmap:
- A2A v1.0 support — the A2A specification has just releases v1.0.0. We will update the library to v1.0 release once a2a-go supports it.
- More ADK-Go examples — multi-agent scenarios where ADK agents discover and call each other purely by SLIM name
References
- slim-a2a-go on GitHub
- SLIM project
- SLIM documentation
- A2A protocol specification
- a2a-go SDK
- Google ADK-Go
- slim-bindings-go
Have questions? Join our Slack community or check out our GitHub.