Skip to content

Commit a4d8fbb

Browse files
authored
chore: Add README and more details package doc (a2aproject#72)
1. `README.md` using `a2a-python` as a template. 2. A `helloworld` gRPC example. 3. Updated package docs. Will be merged after all inflight-PRs are merged and the first release is created.
1 parent 5491030 commit a4d8fbb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+806
-249
lines changed

README.md

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,116 @@
1-
# a2a-go
2-
Golang SDK for A2A Protocol
1+
# A2A Go SDK
2+
3+
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
4+
[![Nightly Check](https://github.com/a2aproject/a2a-go/actions/workflows/nightly.yaml/badge.svg)](https://github.com/a2aproject/a2a-go/actions/workflows/nightly.yaml)
5+
[![Go Doc](https://img.shields.io/badge/Go%20Package-Doc-blue.svg)](https://pkg.go.dev/github.com/a2aproject/a2a-go)
6+
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/a2aproject/a2a-go)
7+
8+
<!-- markdownlint-disable no-inline-html -->
9+
10+
<div align="center">
11+
<img src="https://raw.githubusercontent.com/a2aproject/A2A/refs/heads/main/docs/assets/a2a-logo-black.svg" width="256" alt="A2A Logo"/>
12+
<h3>
13+
A Go library for running agentic applications as A2A Servers, following the <a href="https://a2a-protocol.org">Agent2Agent (A2A) Protocol</a>.
14+
</h3>
15+
</div>
16+
17+
<!-- markdownlint-enable no-inline-html -->
18+
19+
---
20+
21+
## ✨ Features
22+
23+
- **A2A Protocol Compliant:** Build agentic applications that adhere to the Agent2Agent (A2A) Protocol.
24+
- **Extensible:** Easily add support for different communication protocols and database backends.
25+
26+
---
27+
28+
## 🚀 Getting Started
29+
30+
Requires Go `1.24.4` or newer:
31+
32+
```bash
33+
go get github.com/a2aproject/[email protected]
34+
```
35+
36+
Visit [**pkg.go**](https://pkg.go.dev/github.com/a2aproject/a2a-go) for a full documentation.
37+
38+
## Examples
39+
40+
For a simple example refer to the [helloworld] example(./examples/helloworld).
41+
42+
### Server
43+
44+
For a full documentation visit [**pkg.go.dev/a2asrv**](https://pkg.go.dev/github.com/a2aproject/a2a-go/a2asrv).
45+
46+
1. Create a transport-agnostic A2A request handler:
47+
48+
```go
49+
var options []a2asrv.RequestHandlerOption = newCustomOptions()
50+
var agentExecutor a2asrv.AgentExecutor = newCustomAgentExecutor()
51+
requestHandler := a2asrv.NewHandler(agentExecutor, options...)
52+
```
53+
54+
2. Wrap the handler into a transport implementation:
55+
56+
```go
57+
grpcHandler := a2agrpc.NewHandler(requestHandler)
58+
59+
// or
60+
61+
jsonrpcHandler := a2asrv.NewJSONRPCHandler(requestHandler)
62+
```
63+
64+
3. Register handler with a server, for example:
65+
66+
```go
67+
import "google.golang.org/grpc"
68+
...
69+
server := grpc.NewServer()
70+
grpcHandler.RegisterWith(server)
71+
err := server.Serve(listener)
72+
```
73+
74+
### Client
75+
76+
For a full documentation visit [**pkg.go.dev/a2aclient**](https://pkg.go.dev/github.com/a2aproject/a2a-go/a2aclient).
77+
78+
1. Resolve an `AgentCard` to get an information about how an agent is exposed.
79+
80+
```go
81+
card, err := agentcard.DefaultResolver.Resolve(ctx)
82+
```
83+
84+
2. Create a transport-agnostic client from the `AgentCard`:
85+
86+
```go
87+
var options a2aclient.FactoryOption = newCustomClientOptions()
88+
client, err := a2aclient.NewFromCard(ctx, card, options...)
89+
```
90+
91+
3. The connection is now open and can be used to send requests to a server:
92+
93+
```go
94+
msg := a2a.NewMessage(a2a.MessageRoleUser, a2a.TextPart{Text: "..."})
95+
resp, err := client.SendMessage(ctx, &a2a.MessageSendParams{Message: msg})
96+
```
97+
98+
---
99+
100+
## 🌐 More Examples
101+
102+
You can find a variety of more detailed examples in the [a2a-samples](https://github.com/a2aproject/a2a-samples) repository.
103+
104+
---
105+
106+
## 🤝 Contributing
107+
108+
Contributions are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on how to get involved.
109+
110+
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
111+
112+
---
113+
114+
## 📄 License
115+
116+
This project is licensed under the Apache 2.0 License. See the [LICENSE](LICENSE) file for more details.

a2a/agent.go

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,13 @@ type AgentCapabilities struct {
1919
// Extensions is a list of protocol extensions supported by the agent.
2020
Extensions []AgentExtension `json:"extensions,omitempty" yaml:"extensions,omitempty" mapstructure:"extensions,omitempty"`
2121

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

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

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

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

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

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

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

205200
// Name is a human-readable name for the skill.

a2a/core.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,12 @@ func (m *Message) TaskInfo() TaskInfo {
194194
// TaskID is a unique identifier for the task, generated by the server for a new task.
195195
type TaskID string
196196

197-
// NewTaskID creates a new random task identifier.
197+
// NewTaskID generates a new random task identifier.
198198
func NewTaskID() TaskID {
199199
return TaskID(uuid.NewString())
200200
}
201201

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

318-
// NewArtifactID creates a new random artifact identifier.
318+
// NewArtifactID generates a new random artifact identifier.
319319
func NewArtifactID() ArtifactID {
320320
return ArtifactID(uuid.NewString())
321321
}

a2a/doc.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
// Package a2a contains core types and constants from the A2A protocol
16-
// shared by client and server implementations.
17-
//
15+
// Package a2a contains core types and constants from the A2A protocol shared by client and server implementations.
1816
// These types implement the A2A specification and are transport-agnostic.
17+
//
18+
// Additionally, the package provides factory methods for types implementing the [Event] interface.
1919
package a2a

a2aclient/agentcard/doc.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright 2025 The A2A Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/*
16+
Package agentcard provides utilities for fetching public [a2a.AgentCard].
17+
A [Resolver] can be created with a custom [http.Client] or package-level DefaultResolver can be used.
18+
19+
card, err := agentcard.DefaultResolver.Resolve(ctx, baseURL)
20+
21+
// or
22+
23+
resolver := agentcard.NewResolver(customClient)
24+
card, err := resolver.Resolve(ctx, baseURL)
25+
26+
By default the request is sent for a well-known card location, but custom
27+
this can be configured by providing [ResolveOption]s.
28+
29+
card, err := resolver.Resolve(
30+
ctx,
31+
baseURL,
32+
a2aclient.WithPath(customPath),
33+
a2aclient.WithHeader(key, value),
34+
)
35+
*/
36+
package agentcard

a2aclient/agentcard/resolver.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,28 +39,39 @@ func (e *ErrStatusNotOK) Error() string {
3939

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

42-
// Resolver is used to fetch an AgentCard from the provided URL.
42+
var defaultClient = &http.Client{Timeout: 30 * time.Second}
43+
44+
// DefaultResolver is configured with an [http.Client] with a 30-second timeout.
45+
var DefaultResolver = &Resolver{Client: defaultClient}
46+
47+
// Resolver is used to fetch an [a2a.AgentCard].
4348
type Resolver struct {
44-
BaseURL string
49+
// Client can be used to configure appropriate timeout, retry policy, and connection pooling
50+
Client *http.Client
51+
}
52+
53+
// NewResolver is a [Resolver] constructor function.
54+
func NewResolver(client *http.Client) *Resolver {
55+
return &Resolver{Client: client}
4556
}
4657

47-
// ResolveOption is used to customize Resolve() behavior.
58+
// ResolveOption is used to customize Resolve behavior.
4859
type ResolveOption func(r *resolveRequest)
4960

5061
type resolveRequest struct {
5162
path string
5263
headers map[string]string
5364
}
5465

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

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

76-
client := &http.Client{Timeout: 30 * time.Second}
87+
client := r.Client
88+
if client == nil {
89+
client = defaultClient
90+
}
91+
7792
resp, err := client.Do(req)
7893
if err != nil {
7994
return nil, fmt.Errorf("card request failed: %w", err)
@@ -101,7 +116,7 @@ func (r *Resolver) Resolve(ctx context.Context, opts ...ResolveOption) (*a2a.Age
101116
return &card, nil
102117
}
103118

104-
// WithPath makes Resolve fetch from the provided path relative to BaseURL.
119+
// WithPath makes Resolve fetch from the provided path relative to base URL.
105120
func WithPath(path string) ResolveOption {
106121
return func(r *resolveRequest) {
107122
r.path = path

a2aclient/agentcard/resolver_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ func mustServe(t *testing.T, path string, body []byte, callback func(r *http.Req
5959
func TestResolver_DefaultPath(t *testing.T) {
6060
want := &a2a.AgentCard{Name: "TestResolver_DefaultPath"}
6161
url := mustServe(t, defaultAgentCardPath, mustMarshal(t, want), nil)
62-
resolver := Resolver{BaseURL: url}
62+
resolver := Resolver{}
6363

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

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

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

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

124-
resolver := Resolver{BaseURL: url}
125-
got, err := resolver.Resolve(t.Context())
124+
resolver := NewResolver(nil)
125+
got, err := resolver.Resolve(t.Context(), url)
126126
if err == nil {
127127
t.Fatalf("expected Resolve() to fail on malformed response, got: %v", got)
128128
}

0 commit comments

Comments
 (0)