Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 116 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,116 @@
# a2a-go
Golang SDK for A2A Protocol
# A2A Go SDK

[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
[![Nightly Check](https://github.com/a2aproject/a2a-go/actions/workflows/nightly.yaml/badge.svg)](https://github.com/a2aproject/a2a-go/actions/workflows/nightly.yaml)
[![Go Doc](https://img.shields.io/badge/Go%20Package-Doc-blue.svg)](https://pkg.go.dev/github.com/a2aproject/a2a-go)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/a2aproject/a2a-go)

<!-- markdownlint-disable no-inline-html -->

<div align="center">
<img src="https://raw.githubusercontent.com/a2aproject/A2A/refs/heads/main/docs/assets/a2a-logo-black.svg" width="256" alt="A2A Logo"/>
<h3>
A Go library for running agentic applications as A2A Servers, following the <a href="https://a2a-protocol.org">Agent2Agent (A2A) Protocol</a>.
</h3>
</div>

<!-- markdownlint-enable no-inline-html -->

---

## ✨ Features

- **A2A Protocol Compliant:** Build agentic applications that adhere to the Agent2Agent (A2A) Protocol.
- **Extensible:** Easily add support for different communication protocols and database backends.

---

## 🚀 Getting Started

Requires Go `1.24.4` or newer:

```bash
go get github.com/a2aproject/[email protected]
```

Visit [**pkg.go**](https://pkg.go.dev/github.com/a2aproject/a2a-go) for a full documentation.

## Examples

For a simple example refer to the [helloworld] example(./examples/helloworld).

### Server

For a full documentation visit [**pkg.go.dev/a2asrv**](https://pkg.go.dev/github.com/a2aproject/a2a-go/a2asrv).

1. Create a transport-agnostic A2A request handler:

```go
var options []a2asrv.RequestHandlerOption = newCustomOptions()
var agentExecutor a2asrv.AgentExecutor = newCustomAgentExecutor()
requestHandler := a2asrv.NewHandler(agentExecutor, options...)
```

2. Wrap the handler into a transport implementation:

```go
grpcHandler := a2agrpc.NewHandler(requestHandler)

// or

jsonrpcHandler := a2asrv.NewJSONRPCHandler(requestHandler)
```

3. Register handler with a server, for example:

```go
import "google.golang.org/grpc"
...
server := grpc.NewServer()
grpcHandler.RegisterWith(server)
err := server.Serve(listener)
```

### Client

For a full documentation visit [**pkg.go.dev/a2aclient**](https://pkg.go.dev/github.com/a2aproject/a2a-go/a2aclient).

1. Resolve an `AgentCard` to get an information about how an agent is exposed.

```go
card, err := agentcard.DefaultResolver.Resolve(ctx)
```

2. Create a transport-agnostic client from the `AgentCard`:

```go
var options a2aclient.FactoryOption = newCustomClientOptions()
client, err := a2aclient.NewFromCard(ctx, card, options...)
```

3. The connection is now open and can be used to send requests to a server:

```go
msg := a2a.NewMessage(a2a.MessageRoleUser, a2a.TextPart{Text: "..."})
resp, err := client.SendMessage(ctx, &a2a.MessageSendParams{Message: msg})
```

---

## 🌐 More Examples

You can find a variety of more detailed examples in the [a2a-samples](https://github.com/a2aproject/a2a-samples) repository.

---

## 🤝 Contributing

Contributions are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on how to get involved.

Before starting work on a new feature or significant change, please open an issue to discuss your proposed approach with the maintainers. This helps ensure your contribution aligns with the project's goals and prevents duplicated effort or wasted work

---

## 📄 License

This project is licensed under the Apache 2.0 License. See the [LICENSE](LICENSE) file for more details.
17 changes: 6 additions & 11 deletions a2a/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@ type AgentCapabilities struct {
// Extensions is a list of protocol extensions supported by the agent.
Extensions []AgentExtension `json:"extensions,omitempty" yaml:"extensions,omitempty" mapstructure:"extensions,omitempty"`

// PushNotifications indicates if the agent supports sending push notifications
// for asynchronous task updates.
// PushNotifications indicates if the agent supports sending push notifications for asynchronous task updates.
PushNotifications bool `json:"pushNotifications,omitempty" yaml:"pushNotifications,omitempty" mapstructure:"pushNotifications,omitempty"`

// StateTransitionHistory indicates if the agent provides a history of state transitions for a task.
StateTransitionHistory bool `json:"stateTransitionHistory,omitempty" yaml:"stateTransitionHistory,omitempty" mapstructure:"stateTransitionHistory,omitempty"`

// Streaming indicates if the agent supports Server-Sent Events (SSE) for streaming responses.
// Streaming indicates if the agent supports streaming responses.
Streaming bool `json:"streaming,omitempty" yaml:"streaming,omitempty" mapstructure:"streaming,omitempty"`
}

Expand Down Expand Up @@ -54,8 +53,7 @@ type AgentCard struct {
// - MAY reuse URLs if multiple transports are available at the same endpoint
// - MUST accurately declare the transport available at each URL
//
// Clients can select any interface from this list based on their transport
// capabilities
// Clients can select any interface from this list based on their transport capabilities
// and preferences. This enables transport negotiation and fallback scenarios.
AdditionalInterfaces []AgentInterface `json:"additionalInterfaces,omitempty" yaml:"additionalInterfaces,omitempty" mapstructure:"additionalInterfaces,omitempty"`

Expand Down Expand Up @@ -87,10 +85,8 @@ type AgentCard struct {
// If not specified, defaults to 'JSONRPC'.
//
// IMPORTANT: The transport specified here MUST be available at the main 'url'.
// This creates a binding between the main URL and its supported transport
// protocol.
// Clients should prefer this transport and URL combination when both are
// supported.
// This creates a binding between the main URL and its supported transport protocol.
// Clients should prefer this transport and URL combination when both are supported.
PreferredTransport TransportProtocol `json:"preferredTransport,omitempty" yaml:"preferredTransport,omitempty" mapstructure:"preferredTransport,omitempty"`

// ProtocolVersion is the version of the A2A protocol this agent supports.
Expand Down Expand Up @@ -198,8 +194,7 @@ type AgentSkill struct {
// ID is a unique identifier for the agent's skill.
ID string `json:"id" yaml:"id" mapstructure:"id"`

// InputModes is the set of supported input MIME types for this skill, overriding the agent's
// defaults.
// InputModes is the set of supported input MIME types for this skill, overriding the agent's defaults.
InputModes []string `json:"inputModes,omitempty" yaml:"inputModes,omitempty" mapstructure:"inputModes,omitempty"`

// Name is a human-readable name for the skill.
Expand Down
6 changes: 3 additions & 3 deletions a2a/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,12 @@ func (m *Message) TaskInfo() TaskInfo {
// TaskID is a unique identifier for the task, generated by the server for a new task.
type TaskID string

// NewTaskID creates a new random task identifier.
// NewTaskID generates a new random task identifier.
func NewTaskID() TaskID {
return TaskID(uuid.NewString())
}

// NewContextID creates a new random context identifier.
// NewContextID generates a new random context identifier.
func NewContextID() string {
return uuid.NewString()
}
Expand Down Expand Up @@ -315,7 +315,7 @@ func (m *Task) TaskInfo() TaskInfo {
// ArtifactID is a unique identifier for the artifact within the scope of the task.
type ArtifactID string

// NewArtifactID creates a new random artifact identifier.
// NewArtifactID generates a new random artifact identifier.
func NewArtifactID() ArtifactID {
return ArtifactID(uuid.NewString())
}
Expand Down
6 changes: 3 additions & 3 deletions a2a/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Package a2a contains core types and constants from the A2A protocol
// shared by client and server implementations.
//
// Package a2a contains core types and constants from the A2A protocol shared by client and server implementations.
// These types implement the A2A specification and are transport-agnostic.
//
// Additionally, the package provides factory methods for types implementing the [Event] interface.
package a2a
36 changes: 36 additions & 0 deletions a2aclient/agentcard/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2025 The A2A Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/*
Package agentcard provides utilities for fetching public [a2a.AgentCard].
A [Resolver] can be created with a custom [http.Client] or package-level DefaultResolver can be used.

card, err := agentcard.DefaultResolver.Resolve(ctx, baseURL)

// or

resolver := agentcard.NewResolver(customClient)
card, err := resolver.Resolve(ctx, baseURL)

By default the request is sent for a well-known card location, but custom
this can be configured by providing [ResolveOption]s.

card, err := resolver.Resolve(
ctx,
baseURL,
a2aclient.WithPath(customPath),
a2aclient.WithHeader(key, value),
)
*/
package agentcard
33 changes: 24 additions & 9 deletions a2aclient/agentcard/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,39 @@ func (e *ErrStatusNotOK) Error() string {

const defaultAgentCardPath = "/.well-known/agent-card.json"

// Resolver is used to fetch an AgentCard from the provided URL.
var defaultClient = &http.Client{Timeout: 30 * time.Second}

// DefaultResolver is configured with an [http.Client] with a 30-second timeout.
var DefaultResolver = &Resolver{Client: defaultClient}

// Resolver is used to fetch an [a2a.AgentCard].
type Resolver struct {
BaseURL string
// Client can be used to configure appropriate timeout, retry policy, and connection pooling
Client *http.Client
}

// NewResolver is a [Resolver] constructor function.
func NewResolver(client *http.Client) *Resolver {
return &Resolver{Client: client}
}

// ResolveOption is used to customize Resolve() behavior.
// ResolveOption is used to customize Resolve behavior.
type ResolveOption func(r *resolveRequest)

type resolveRequest struct {
path string
headers map[string]string
}

// Resolve fetches an AgentCard from the provided URL.
// By default fetches from the /.well-known/agent-card.json path.
func (r *Resolver) Resolve(ctx context.Context, opts ...ResolveOption) (*a2a.AgentCard, error) {
// Resolve fetches an [a2a.AgentCard] from the provided base URL.
// By default the request is sent for the /.well-known/agent-card.json path.
func (r *Resolver) Resolve(ctx context.Context, baseURL string, opts ...ResolveOption) (*a2a.AgentCard, error) {
reqSpec := &resolveRequest{path: defaultAgentCardPath, headers: make(map[string]string)}
for _, o := range opts {
o(reqSpec)
}

reqUrl, err := url.JoinPath(r.BaseURL, reqSpec.path)
reqUrl, err := url.JoinPath(baseURL, reqSpec.path)
if err != nil {
return nil, fmt.Errorf("url construction failed: %w", err)
}
Expand All @@ -73,7 +84,11 @@ func (r *Resolver) Resolve(ctx context.Context, opts ...ResolveOption) (*a2a.Age
req.Header.Add(h, val)
}

client := &http.Client{Timeout: 30 * time.Second}
client := r.Client
if client == nil {
client = defaultClient
}

resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("card request failed: %w", err)
Expand Down Expand Up @@ -101,7 +116,7 @@ func (r *Resolver) Resolve(ctx context.Context, opts ...ResolveOption) (*a2a.Age
return &card, nil
}

// WithPath makes Resolve fetch from the provided path relative to BaseURL.
// WithPath makes Resolve fetch from the provided path relative to base URL.
func WithPath(path string) ResolveOption {
return func(r *resolveRequest) {
r.path = path
Expand Down
18 changes: 9 additions & 9 deletions a2aclient/agentcard/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ func mustServe(t *testing.T, path string, body []byte, callback func(r *http.Req
func TestResolver_DefaultPath(t *testing.T) {
want := &a2a.AgentCard{Name: "TestResolver_DefaultPath"}
url := mustServe(t, defaultAgentCardPath, mustMarshal(t, want), nil)
resolver := Resolver{BaseURL: url}
resolver := Resolver{}

got, err := resolver.Resolve(t.Context())
got, err := resolver.Resolve(t.Context(), url)
if err != nil {
t.Fatalf("Resolve() failed with: %v", err)
}
Expand All @@ -77,8 +77,8 @@ func TestResolver_CustomPath(t *testing.T) {
want := &a2a.AgentCard{Name: "TestResolver_DefaultPath"}
url := mustServe(t, path, mustMarshal(t, want), nil)

resolver := Resolver{BaseURL: url}
got, err := resolver.Resolve(ctx)
resolver := Resolver{}
got, err := resolver.Resolve(ctx, url)
var httpErr *ErrStatusNotOK
if err == nil || !errors.As(err, &httpErr) {
t.Fatalf("expected Resolve() to fail with ErrStatusNotOK, got %v, %v", got, err)
Expand All @@ -88,7 +88,7 @@ func TestResolver_CustomPath(t *testing.T) {
}

for _, p := range []string{path, strings.TrimPrefix(path, "/")} {
got, err = resolver.Resolve(ctx, WithPath(p))
got, err = resolver.Resolve(ctx, url, WithPath(p))
if err != nil {
t.Fatalf("Resolve(%s) failed with %v", p, err)
}
Expand All @@ -107,8 +107,8 @@ func TestResolver_CustomHeader(t *testing.T) {
capturedHeader = req.Header[h]
})

resolver := Resolver{BaseURL: url}
_, err := resolver.Resolve(t.Context(), WithRequestHeader(h, hval))
resolver := NewResolver(nil)
_, err := resolver.Resolve(t.Context(), url, WithRequestHeader(h, hval))
if err != nil {
t.Fatalf("Resolve() failed with: %v", err)
}
Expand All @@ -121,8 +121,8 @@ func TestResolver_CustomHeader(t *testing.T) {
func TestResolver_MalformedJSON(t *testing.T) {
url := mustServe(t, defaultAgentCardPath, []byte(`}{`), nil)

resolver := Resolver{BaseURL: url}
got, err := resolver.Resolve(t.Context())
resolver := NewResolver(nil)
got, err := resolver.Resolve(t.Context(), url)
if err == nil {
t.Fatalf("expected Resolve() to fail on malformed response, got: %v", got)
}
Expand Down
Loading