项目文件夹

文件
Asim Aslam 307b94aab7
goreleaser / goreleaser (push) Has been cancelled
Implement A2A protocol gateway and update documentation (#2974)
* feat(a2a): Agent2Agent protocol gateway

Add gateway/a2a — exposes registered agents over the open A2A protocol so
agents on other frameworks can discover and call them. Agent Cards are
generated from registry metadata (the same way the MCP gateway derives
tools from service endpoints); incoming A2A tasks translate to the
agent's existing Agent.Chat RPC, so there's no per-agent code.

v1 is the synchronous JSON-RPC binding: message/send returns a completed
Task, tasks/get retrieves it, and Agent Cards are served for discovery;
streaming and push notifications are advertised as unsupported. Run with
'micro a2a serve' (cmd/micro/a2a). Tests cover card generation,
message/send, tasks/get, listing, and unknown-method errors.

* docs: A2A guide, README contents + A2A section, universe A2A check

- Add a Contents table of contents at the top of the README and an A2A
  subsection under Building Agents.
- Add the Agent2Agent (A2A) guide and register it in the docs nav.
- Exercise the A2A gateway in the universe harness: the concierge agent
  is reached over A2A (message/send -> Agent.Chat -> completed task).

* feat(a2a): outbound client — call external A2A agents

Add a2a.Client (Send/Card) so a Go Micro agent or flow can call an agent
on any framework by URL — the outbound counterpart to the gateway. Wired
in two places: flow.A2A(url) as a workflow step (the cross-framework
Dispatch), and agent delegate to an http(s) URL routes over A2A. The
universe harness now drives the gateway through the client, exercising
both directions. Tests cover client send/card and the round trip.

* docs: A2A both-directions — guide, README, changelog, blog #26

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-17 20:33:20 +01:00

158 行
3.9 KiB
Go

package a2a
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
)
// Client calls a remote agent that speaks the A2A protocol. It is the
// outbound counterpart to the gateway: where the gateway exposes a Go
// Micro agent over A2A, the Client lets a Go Micro agent or flow call an
// agent on any framework, by URL.
type Client struct {
url string
http *http.Client
}
// NewClient returns a Client for the agent at url (its JSON-RPC endpoint,
// i.e. the `url` field of the agent's card).
func NewClient(url string) *Client {
return &Client{url: url, http: &http.Client{Timeout: 60 * time.Second}}
}
// WithHTTPClient sets the underlying HTTP client (for timeouts, auth
// transports, etc.).
func (c *Client) WithHTTPClient(h *http.Client) *Client {
if h != nil {
c.http = h
}
return c
}
// Card fetches the remote agent's Agent Card.
func (c *Client) Card(ctx context.Context) (*AgentCard, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.url+"/.well-known/agent.json", nil)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("agent card: status %d", resp.StatusCode)
}
var card AgentCard
if err := json.NewDecoder(resp.Body).Decode(&card); err != nil {
return nil, err
}
return &card, nil
}
// Send sends a text message to the remote agent and returns its reply.
// If the agent returns a task that isn't yet terminal, Send polls
// tasks/get until it completes or ctx is done.
func (c *Client) Send(ctx context.Context, text string) (string, error) {
res, err := c.call(ctx, "message/send", sendParams{Message: Message{
Role: "user",
Kind: "message",
MessageID: uuid.New().String(),
Parts: []Part{{Kind: "text", Text: text}},
}})
if err != nil {
return "", err
}
// The result is a Message or a Task; the "kind" field disambiguates.
var probe struct {
Kind string `json:"kind"`
}
_ = json.Unmarshal(res, &probe)
if probe.Kind == "message" {
var m Message
if err := json.Unmarshal(res, &m); err != nil {
return "", err
}
return textOf(m.Parts), nil
}
var task Task
if err := json.Unmarshal(res, &task); err != nil {
return "", err
}
for !terminal(task.Status.State) {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(300 * time.Millisecond):
}
got, err := c.call(ctx, "tasks/get", getParams{ID: task.ID})
if err != nil {
return "", err
}
if err := json.Unmarshal(got, &task); err != nil {
return "", err
}
}
if task.Status.State != stateCompleted {
return "", fmt.Errorf("remote task %s ended in state %q", task.ID, task.Status.State)
}
return artifactsText(task.Artifacts), nil
}
// call performs one JSON-RPC request and returns the raw result.
func (c *Client) call(ctx context.Context, method string, params any) (json.RawMessage, error) {
body, _ := json.Marshal(map[string]any{
"jsonrpc": "2.0",
"id": uuid.New().String(),
"method": method,
"params": params,
})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var out struct {
Result json.RawMessage `json:"result"`
Error *rpcError `json:"error"`
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, err
}
if out.Error != nil {
return nil, fmt.Errorf("a2a %s: %s (%d)", method, out.Error.Message, out.Error.Code)
}
return out.Result, nil
}
func terminal(state string) bool {
switch state {
case "completed", "failed", "canceled", "rejected":
return true
}
return false
}
func artifactsText(arts []Artifact) string {
var parts []Part
for _, a := range arts {
parts = append(parts, a.Parts...)
}
return textOf(parts)
}