Covonaut 的 A2A 协议支持
a2a 实现 Agent2Agent 协议,使 Covonaut 与其他兼容 Agent 互操作。
功能
- Agent Card 发现:
/.well-known/agent-card.json,旧agent.json别名。 - A2A 1.0 的 11 个标准 JSON-RPC 方法,包括推送配置和扩展卡片。
client.V1()的类型化请求、响应和流。- 声明安全要求,并支持认证、版本及必需扩展的协商。
- 旧
tasks/*和 Go API 兼容。 - 管理任务生命周期,支持同步响应和 SSE,传输文本、文件及结构化数据,并支持 Webhook 和远程 Agent 交接。
架构
types 定义 Card、Task、Message、Part 和 Artifact;server 提供基于 HTTP 的 JSON-RPC 服务;client 调用远程 Agent;handoff 将远程调用接入本地 Agent 交接机制。
快速开始
1. 将 Agent 暴露为服务
配置模型和密钥后,可以运行以下程序。Agent Card 描述服务的能力和技能,DefaultAgentHandler 负责将请求交给 Agent 执行。
package main
import (
"log"
"os"
"github.com/covoyage/covonaut/agentcore"
"github.com/covoyage/covonaut/a2a"
"github.com/covoyage/covonaut/provider/chatcompat"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
// Create your agent
agent, err := agentcore.NewWithError(agentcore.Config{
ModelConfig: agentcore.ModelConfig{
Name: "weather-agent",
Model: os.Getenv("AGENT_MODEL"),
Provider: chatcompat.New(chatcompat.Config{APIKey: os.Getenv("OPENAI_API_KEY")}),
},
SystemPrompt: "You are a weather assistant.",
})
if err != nil {
return err
}
defer agent.Close()
// Define your agent card
card := a2a.AgentCard{
Name: "weather-agent",
Description: "Provides weather information for any location",
URL: "http://localhost:8080",
Version: "1.0.0",
Capabilities: a2a.AgentCapabilities{
Streaming: true,
},
Skills: []a2a.AgentSkill{
{
ID: "get-weather",
Name: "Get Weather",
Description: "Get current weather for a location",
Tags: []string{"weather", "forecast"},
},
},
}
// Create A2A server
handler := a2a.NewDefaultAgentHandler(card, agent, agent.Config())
server := a2a.NewServer(handler)
log.Println("A2A server listening on :8080")
return server.ListenAndServe(":8080")
}生产环境还需处理认证、超时和资源释放。无模型本地联调可运行服务与客户端示例。
2. 调用远程 A2A 1.0 Agent
先获取远程服务的 Agent Card,再通过标准客户端发送请求。响应可能直接包含消息,也可能返回需要继续跟踪的任务状态。
package main
import (
"context"
"errors"
"fmt"
"log"
"time"
"github.com/covoyage/covonaut/a2a"
)
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
client := a2a.NewClient("http://remote-agent.example.com")
// Discover agent capabilities
card, err := client.GetAgentCard(ctx)
if err != nil {
return err
}
fmt.Printf("Connected to: %s\n", card.Name)
// Use the standards-based client facade.
response, err := client.V1().SendMessage(ctx, a2a.V1SendMessageRequest{
Message: a2a.V1Message{
MessageID: "message-123",
Role: "ROLE_USER",
Parts: []a2a.V1Part{a2a.NewV1TextPart("What's the weather in Tokyo?")},
},
})
if err != nil {
return err
}
if response.Message != nil {
fmt.Printf("Direct response: %+v\n", response.Message)
} else if response.Task != nil {
fmt.Printf("Task state: %s\n", response.Task.Status.State)
} else {
return errors.New("response contains neither a message nor a task")
}
return nil
}3. 订阅流式任务更新
片段应放在返回错误的函数中,并使用设有超时的 context。收到 EOF 表示流已结束,其他错误需要返回给调用方。使用结束后应关闭流,编译这段代码时还需导入 errors 和 io。
stream, err := client.V1().SendStreamingMessage(ctx, a2a.V1SendMessageRequest{
Message: a2a.V1Message{
MessageID: "message-456",
Role: "ROLE_USER",
Parts: []a2a.V1Part{a2a.NewV1TextPart("Write a long story")},
},
})
if err != nil {
return err
}
defer stream.Close()
for {
ev, err := stream.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return err
}
if ev.Task != nil {
fmt.Printf("Initial state: %s\n", ev.Task.Status.State)
}
if ev.StatusUpdate != nil {
fmt.Printf("State: %s\n", ev.StatusUpdate.Status.State)
}
}4. 接入交接系统
调用方应检查创建错误,并在使用结束后关闭 Agent。注册远程目标后,模型可调用 transfer_to_math-expert 等工具委托任务。
import (
"github.com/covoyage/covonaut/agentcore"
"github.com/covoyage/covonaut/a2a"
)
func setupAgent(model string, provider agentcore.Provider) (*agentcore.Agent, error) {
// Create remote handoff extension
remoteAgents := []a2a.RemoteHandoffConfig{
{
Name: "math-expert",
Description: "Expert in mathematics and calculations",
URL: "http://math-agent.example.com",
},
{
Name: "code-reviewer",
Description: "Reviews code and suggests improvements",
URL: "http://code-agent.example.com",
},
}
ext := a2a.NewRemoteHandoffExtension(remoteAgents)
return agentcore.NewWithError(agentcore.Config{
ModelConfig: agentcore.ModelConfig{
Name: "coordinator", Model: model, Provider: provider,
},
SystemPrompt: "You coordinate tasks between specialized agents.",
Extensions: []agentcore.Extension{ext},
})
}5. 带回调的适配器
回调可处理输入、输出和状态变化;记录日志时避免泄漏用户内容。
adapter := a2a.NewAgentAdapter(card, agent, cfg, &a2a.AdapterCallbacks{
BeforeRun: func(ctx context.Context, taskID, input string) (string, error) {
log.Printf("Processing task %s", taskID)
return input, nil
},
AfterRun: func(ctx context.Context, taskID, output string, err error) (*a2a.Task, error) {
if err != nil {
log.Printf("Task %s failed: %v", taskID, err)
} else {
log.Printf("Task %s completed", taskID)
}
return nil, nil // use default result
},
OnStatusChange: func(taskID string, state a2a.TaskState) {
log.Printf("Task %s status changed to %s", taskID, state)
},
})
server := a2a.NewServer(adapter)API 参考
服务端点
| 方法 | 端点 | 说明 |
|---|---|---|
| GET | /.well-known/agent-card.json | 1.0 发现 |
| GET | /.well-known/agent.json | 旧发现 |
| POST | / | JSON-RPC |
A2A 1.0 JSON-RPC 方法
| 方法 | 说明 |
|---|---|
SendMessage | 发送/继续消息 |
SendStreamingMessage | 标准 JSON-RPC SSE |
GetTask | 任务状态,支持 historyLength |
ListTasks | 游标分页和字段控制 |
CancelTask | 取消任务 |
SubscribeToTask | 订阅未终止任务 |
CreateTaskPushNotificationConfig | 创建 webhook |
GetTaskPushNotificationConfig | 获取配置 |
ListTaskPushNotificationConfigs | 列出配置 |
DeleteTaskPushNotificationConfig | 删除配置 |
GetExtendedAgentCard | 认证后的扩展 Card |
请求可以通过 A2A-Version: 1.0 使用 1.0 协议。不填写版本或指定 0.3 时保留旧行为,其他版本会返回 VersionNotSupportedError。WithA2AExtensions 配置必需扩展;WithExtendedAgentCard 和 WithAuth 用于配置需要认证才能访问的扩展 Agent Card。
旧 JSON-RPC 方法
| 方法 | 参数 | 说明 |
|---|---|---|
tasks/send | SendTaskRequest | 同步任务 |
tasks/sendSubscribe | SendTaskRequest | SSE 任务 |
tasks/get | GetTaskRequest | 获取状态 |
tasks/cancel | CancelTaskRequest | 取消 |
tasks/pushNotification/set | SetPushNotificationRequest | 设置 webhook |
tasks/pushNotification/get | {id} | 获取 webhook |
Part 类型
文本、结构化数据、Base64 文件和 URI 文件使用不同构造器:
// Text
textPart := a2a.NewTextPart("Hello world")
// Structured data
dataPart := a2a.NewDataPart(map[string]any{"key": "value"})
// File (base64)
inlineFilePart := a2a.NewFilePartBytes("report.pdf", "application/pdf", base64Data)
// File (URI)
remoteFilePart := a2a.NewFilePartURI("image.png", "image/png", "http://example.com/image.png")任务状态
任务提交后处于 submitted,开始执行后进入 working,成功结束时进入 completed。需要更多输入时进入 input-required,补充输入后可以继续;任务也可能以 canceled 或 failed 结束。
测试
go test ./a2a/ -v