Why: Q3 WP4 requires A2A 1.0 interop from zero: publish this team's capabilities as an Agent Card and consume remote agents as squad.Executors with the A2A task state machine mapped onto ExecResult. Evidence: adapters/a2a conformance with a local fake A2A server (both directions): card build from role/blueprint registries, well-known JSON serving, FetchCard fail-closed validation, message/send -> tasks/get polling through submitted/working -> completed with TextPart/FilePart artifacts mapped to patches/targets, failed/canceled/rejected/input-required mapped to Success=false fail-closed (no cross-process hold), direct Message result, JSON-RPC protocol and ctx-cancel error propagation. Module green with -race; squad root suite unaffected (SELF). CI authoritative (GATE) for the root module; the adapter module follows the adapters/intent and adapters/native separate-go.mod precedent. Impact: new module agent-squad/adapters/a2a (card.go, executor.go); no changes to the squad root module. Refs: Q3 plan WP4 (2026-10-07-agent-stack-q2q3-plans.md); task card .task/feature-agent-stack-q3.md
180 lines
6.0 KiB
Go
180 lines
6.0 KiB
Go
// Package a2a 把小队接入 A2A(Agent2Agent)1.0 互通面(Q3 WP4):
|
||
// - 发布侧:BuildCard 从角色注册表与蓝图注册表渲染 Agent Card(描述本
|
||
// 团队能力),CardHandler 以 JSON 服务(挂载在
|
||
// /.well-known/agent-card.json);
|
||
// - 消费侧:FetchCard 发现远端 Agent Card(fail-closed 校验必备字段),
|
||
// Executor 实现 squad.Executor——message/send 发起任务,tasks/get 轮询
|
||
// A2A 任务状态机(submitted/working/input-required → completed/failed/
|
||
// canceled),终态映射 ExecResult:completed 的 artifacts(TextPart/
|
||
// FilePart)落成 patches/目标文件;failed/canceled/input-required 一律
|
||
// Success=false(跨进程挂起不在本适配面内,fail-closed 交引擎重试/降级
|
||
// 语义处理)。
|
||
//
|
||
// 传输为 JSON-RPC 2.0 over HTTP(A2A 1.0 形态);不引入协议之外的依赖。
|
||
package a2a
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
|
||
"agent-squad/squad"
|
||
)
|
||
|
||
// ProtocolVersion 对齐的 A2A 协议版本。
|
||
const ProtocolVersion = "1.0"
|
||
|
||
// WellKnownPath 是 Agent Card 的发现路径(A2A 1.0)。
|
||
const WellKnownPath = "/.well-known/agent-card.json"
|
||
|
||
// AgentCard 是 A2A 1.0 Agent Card(发布与消费共用形态)。
|
||
type AgentCard struct {
|
||
ProtocolVersion string `json:"protocolVersion"`
|
||
Name string `json:"name"`
|
||
Description string `json:"description,omitempty"`
|
||
// URL 是 JSON-RPC 服务端点(消费侧据此发 message/send)。
|
||
URL string `json:"url"`
|
||
Version string `json:"version"`
|
||
Capabilities CardCapabilities `json:"capabilities"`
|
||
DefaultInputModes []string `json:"defaultInputModes,omitempty"`
|
||
DefaultOutputModes []string `json:"defaultOutputModes,omitempty"`
|
||
Skills []AgentSkill `json:"skills,omitempty"`
|
||
}
|
||
|
||
// CardCapabilities 能力声明(本适配面:非流式、无推送、无状态历史)。
|
||
type CardCapabilities struct {
|
||
Streaming bool `json:"streaming"`
|
||
PushNotifications bool `json:"pushNotifications"`
|
||
StateTransitionHistory bool `json:"stateTransitionHistory"`
|
||
}
|
||
|
||
// AgentSkill 是一项可被远端调用的能力。
|
||
type AgentSkill struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"`
|
||
Description string `json:"description,omitempty"`
|
||
Tags []string `json:"tags,omitempty"`
|
||
Examples []string `json:"examples,omitempty"`
|
||
}
|
||
|
||
// CardOptions 覆盖 BuildCard 的身份字段(空则用缺省)。
|
||
type CardOptions struct {
|
||
Name string
|
||
Description string
|
||
// URL JSON-RPC 端点(必填:远端没有端点就无法调用本团队)。
|
||
URL string
|
||
// Version 卡片版本(空 = "1.0.0")。
|
||
Version string
|
||
// Skills 显式能力清单;空 = 从注册表派生。
|
||
Skills []AgentSkill
|
||
}
|
||
|
||
// BuildCard 从角色注册表与蓝图注册表渲染 Agent Card(描述本团队能力)。
|
||
// 角色 → 技能(关键词作 tags);蓝图 → 技能(match 词作 examples)。
|
||
func BuildCard(reg *squad.RoleRegistry, bps *squad.BlueprintRegistry, opts CardOptions) (AgentCard, error) {
|
||
if strings.TrimSpace(opts.URL) == "" {
|
||
return AgentCard{}, fmt.Errorf("a2a: Agent Card 需要 JSON-RPC 端点 URL")
|
||
}
|
||
card := AgentCard{
|
||
ProtocolVersion: ProtocolVersion,
|
||
Name: orDefault(opts.Name, "agent-squad"),
|
||
Description: opts.Description,
|
||
URL: opts.URL,
|
||
Version: orDefault(opts.Version, "1.0.0"),
|
||
DefaultInputModes: []string{"text/plain"},
|
||
DefaultOutputModes: []string{"text/plain"},
|
||
Skills: append([]AgentSkill(nil), opts.Skills...),
|
||
}
|
||
if len(card.Skills) == 0 {
|
||
if reg != nil {
|
||
for _, role := range reg.List() {
|
||
desc := firstLine(role.Instructions)
|
||
if desc == "" {
|
||
desc = role.Name
|
||
}
|
||
card.Skills = append(card.Skills, AgentSkill{
|
||
ID: role.Role, Name: orDefault(role.Name, role.Role),
|
||
Description: desc, Tags: append([]string(nil), role.Keywords...),
|
||
})
|
||
}
|
||
}
|
||
if bps != nil {
|
||
for _, bp := range bps.List() {
|
||
card.Skills = append(card.Skills, AgentSkill{
|
||
ID: bp.Key, Name: orDefault(bp.Title, bp.Key),
|
||
Description: bp.Description, Examples: append([]string(nil), bp.Match...),
|
||
})
|
||
}
|
||
}
|
||
}
|
||
return card, nil
|
||
}
|
||
|
||
// CardHandler 以 JSON 服务 Agent Card(调用方挂载到 WellKnownPath)。
|
||
func CardHandler(card AgentCard) http.Handler {
|
||
body, _ := json.Marshal(card)
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
_, _ = w.Write(body)
|
||
})
|
||
}
|
||
|
||
// FetchCard 发现并校验远端 Agent Card(fail-closed:必备字段缺失即拒)。
|
||
func FetchCard(client *http.Client, wellKnownURL string) (AgentCard, error) {
|
||
if client == nil {
|
||
client = http.DefaultClient
|
||
}
|
||
resp, err := client.Get(wellKnownURL)
|
||
if err != nil {
|
||
return AgentCard{}, fmt.Errorf("a2a: 获取 Agent Card: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != http.StatusOK {
|
||
return AgentCard{}, fmt.Errorf("a2a: Agent Card 端点返回 %d", resp.StatusCode)
|
||
}
|
||
var card AgentCard
|
||
if err := json.NewDecoder(resp.Body).Decode(&card); err != nil {
|
||
return AgentCard{}, fmt.Errorf("a2a: Agent Card 解析失败: %w", err)
|
||
}
|
||
if err := validateCard(card); err != nil {
|
||
return AgentCard{}, err
|
||
}
|
||
return card, nil
|
||
}
|
||
|
||
// validateCard 消费侧最小校验(A2A 1.0 必备字段)。
|
||
func validateCard(card AgentCard) error {
|
||
if card.ProtocolVersion == "" {
|
||
return fmt.Errorf("a2a: Agent Card 缺 protocolVersion")
|
||
}
|
||
if strings.TrimSpace(card.Name) == "" {
|
||
return fmt.Errorf("a2a: Agent Card 缺 name")
|
||
}
|
||
if strings.TrimSpace(card.URL) == "" {
|
||
return fmt.Errorf("a2a: Agent Card 缺 url")
|
||
}
|
||
if strings.TrimSpace(card.Version) == "" {
|
||
return fmt.Errorf("a2a: Agent Card 缺 version")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func orDefault(v, def string) string {
|
||
if strings.TrimSpace(v) != "" {
|
||
return v
|
||
}
|
||
return def
|
||
}
|
||
|
||
func firstLine(s string) string {
|
||
s = strings.TrimSpace(s)
|
||
if i := strings.IndexByte(s, '\n'); i >= 0 {
|
||
s = s[:i]
|
||
}
|
||
if len(s) > 120 {
|
||
s = s[:120] + "…"
|
||
}
|
||
return s
|
||
}
|