Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

A2A agent 发现

此部署可以发布其代理,以便另一个部署,或任何 HTTP 客户端,能够发现它们并调用它们。A2A 是一个代理与另一个代理通信的协议,就像人通过聊天应用接触机器人一样。此页面会准确展示应输入什么,以及会返回什么。

此页面上的每个响应都是真实运行中的守护进程输出。这里没有任何示例内容。

身份验证

两个发现用的 GET 接口都无需认证:目录卡片和每个别名对应的智能体卡片无需令牌即可读取,以便对端在配对之前发现你发布的服务。SendMessage POST 则不同,它会运行一个完整的、启用工具的智能体回合,因此和其他所有写入接口一样,需要通过网关的配对认证。当 [gateway] require_pairing 开启(默认开启)时,需要在 task POST 请求中携带由配对派生的 Bearer 令牌:

curl -X POST http://localhost:42617/a2a/agent_alpha \
  -H "Authorization: Bearer $ZEROCLAW_TOKEN" \
  -H "A2A-Version: 1.0" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{...}}'

未认证的 task POST 会返回 401,不会触发 agent 回合。下面的 discovery GET 不需要 header。有关如何获取 token,请参阅 gateway 配对文档。

分两次请求完成整个内容

你只需要两个 GET 请求就能发现一个 agent。

首先,询问该部署发布了哪些 agent:

curl http://localhost:42617/.well-known/agents-card.json

其次,询问其中一个代理它能做什么:

curl http://localhost:42617/a2a/agent_alpha/.well-known/agent-card.json

第一次请求会给你一组 agent URL。第二次请求会给你某个 agent 的技能以及你发送工作的 URL。这就是整个发现面。此页面的其余部分只是仔细阅读这两个响应。

列出代理程序

curl http://localhost:42617/.well-known/agents-card.json

响应:

{
    "name": "ZeroClaw agents",
    "描述": "用于枚举此 ZeroClaw 安装中已发布的 A2A 代理的发现目录。不是可运行的代理;下方每个条目都有其各自的 A2A card 和 endpoint。Skills 会从已发布的代理中汇总,并为每个技能标注其所属 alias。",
    "supportedInterfaces": [
        {
            "url": "http://localhost:42617/.well-known/agents-card.json",
            "protocolBinding": catalog,
            protocolVersion: "1.0"
        },
        {
            "url": "http://localhost:42617/a2a/agent_beta",
            "protocolBinding": "JSONRPC",
            protocolVersion: "1.0"
        },
        {
            "url": "http://localhost:42617/a2a/agent_alpha",
            "protocolBinding": "JSONRPC",
            protocolVersion: "1.0"
        }
    ],
    version: "0.8.6",
    "capabilities": {
        "streaming": false,
        pushNotifications: false,
        "extendedAgentCard": false
    },
    "defaultInputModes": ["文本"],
    "defaultOutputModes": ["文本"],
    "skills": [
        {
            "id": "agent_beta/github-issue-triage",
            "name": "github-issue-triage",
            "描述": "ZeroClaw 的问题分诊和生命周期管理代理。",
            "tags": ["github", issues, "triage", "agent_beta"]
        },
        {
            "id": "agent_beta/github-pr-review-session",
            "name": "github-pr-review-session",
            "描述": "ZeroClaw PR 审查的人工审核协作助手。",
            "tags": ["github", "pull-requests", "review", "agent_beta"]
        },
        {
            "id": "agent_alpha/zeroclaw",
            "name": zeroclaw,
            "描述": "帮助用户操作并与其 ZeroClaw agent 实例交互。",
            "tags": [operations, "cli", gateway, "agent_alpha"]
        },
        {
            "id": "agent_alpha/skill-creator",
            "name": "skill-creator",
            "描述": "创建新技能、修改并改进现有技能,并衡量技能性能。",
            "tags": ["skills", authoring, "evaluation", "agent_alpha"]
        },
        {
            "id": "agent_alpha/changelog-generation",
            "name": "changelog-generation",
            "描述": "用于 ZeroClaw 发布的变更日志生成技能。",
            "tags": ["changelog", release, 自动化, "agent_alpha"]
        }
    ]
}

按这样理解。supportedInterfaces 列出了 URL。标记为 catalog 的那一项就是这个列表本身,忽略它。标记为 JSONRPC 的两个是代理:agent_alpha 和 agent_beta。它们的 URL 是你发送任务的地方。skills 汇总了每个已发布代理的技能,每个 id 都带有前缀,并通过 tags-标记为所属别名,因此一次读取就能看到整个安装的能力范围以及每个部分的归属。

检查一个代理

从列表中取一个 URL,并追加卡片路径:

curl http://localhost:42617/a2a/agent_alpha/.well-known/agent-card.json

响应:

{
    "name": "agent_alpha",
    "描述": "ZeroClaw agent 'agent_alpha'.",
    "supportedInterfaces": [
        {
            "url": "http://localhost:42617/a2a/agent_alpha",
            "protocolBinding": "JSONRPC",
            protocolVersion: "1.0"
        }
    ],
    version: "0.8.6",
    "capabilities": {
        "streaming": false,
        pushNotifications: false,
        "extendedAgentCard": false
    },
    "defaultInputModes": ["文本"],
    "defaultOutputModes": ["文本"],
    "skills": [
        {
            "id": zeroclaw,
            "name": zeroclaw,
            "描述": "帮助用户操作并与其 ZeroClaw agent 实例交互。",
            "tags": [operations, "cli", gateway]
        },
        {
            "id": "skill-creator",
            "name": "skill-creator",
            "描述": "创建新技能、修改并改进现有技能,并衡量技能性能。",
            "tags": ["skills", authoring, "evaluation"]
        },
        {
            "id": "changelog-generation",
            "name": "changelog-generation",
            "描述": "用于 ZeroClaw 发布的变更日志生成技能。",
            "tags": ["changelog", release, 自动化]
        }
    ]
}

现在你知道三件事。该代理名为 agent_alpha。它有三项技能,zeroclaw、skill-creator 和 changelog-generation,并且都有对其功能的简要说明。单一的 JSONRPC 接口 URL,http://localhost:42617/a2a/agent_alpha,就是你向其 POST 任务的地址。

卡片 description 来自已配置的别名身份文档:AIEOS 身份的 bio 提供该行内容,否则回退为该身份中的名称。未设置身份时,卡片使用上方显示的中性默认值 ZeroClaw agent '<alias>'.。

代理选择显示的内容

代理不必发布它拥有的每一项技能。同一部署中的 agent_beta 代理发布其自己选择的一组:

curl http://localhost:42617/a2a/agent_beta/.well-known/agent-card.json
{
    "name": "agent_beta",
    "描述": "ZeroClaw 代理 'agent_beta'。",
    "supportedInterfaces": [
        {
            "url": "http://localhost:42617/a2a/agent_beta",
            "protocolBinding": "JSONRPC",
            protocolVersion: "1.0"
        }
    ],
    version: "0.8.6",
    "capabilities": {
        "streaming": false,
        pushNotifications: false,
        "extendedAgentCard": false
    },
    "defaultInputModes": ["文本"],
    "defaultOutputModes": ["文本"],
    "skills": [
        {
            "id": "github-issue-triage",
            "name": "github-issue-triage",
            "描述": "ZeroClaw 的问题分诊和生命周期管理代理。"
        },
        {
            "id": "github-pr-review-session",
            "name": "github-pr-review-session",
            "描述": "ZeroClaw PR 审查的人工审核协作助手。"
        }
    ]
}

部署选择了要发布哪些 agent,以及每个 agent 暴露哪些技能。发布的目的就在于此:你可以按 agent 决定外部世界能看到哪些技能。

发送任务

一旦获取了代理的接口 URL 和技能,就可以向该 URL 发送一个 JSON-RPC SendMessage POST 请求来提交工作,并在 A2A-Version 请求头中声明你所使用的 A2A 协议版本(此服务器支持 1.0):

curl -X POST http://localhost:42617/a2a/agent_alpha \
  -H "Authorization: Bearer $ZEROCLAW_TOKEN" \
  -H "A2A-Version: 1.0" \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "SendMessage",
    "params": {
      "message": {
        "messageId": "msg-1",
        "role": "ROLE_USER",
        "parts": [{ "text": "Reply with PONG" }]
      }
    }
  }'

The agent runs the turn and answers with a completed task. The reply is the text part inside the task’s artifact; the result is a SendMessageResponse whose task branch carries the Task:

Wait, I should clarify my role: I’m Claude Code, and the system prompt sets me up as a translator of technical documentation strings into zh-CN. Your message is in English and is a documentation-style sentence, so translating it is the task. Here is the translation:

智能体运行该轮对话,并以已完成的任务作答。回复内容是任务产物(artifact)中的文本部分;结果是一个 SendMessageResponse,其 task 分支携带 Task:

{
    "id": 1,
    "jsonrpc": "2.0",
    "result": {
        "task": {
            "id": "0ef19fcb-b5e4-4c26-afce-d80451c8861e",
            "contextId": "a2a_agent_alpha_06cb22f5-12bf-4b26-9ebc-9c063ab520a4",
            "status": { "state": "TASK_STATE_COMPLETED" },
            "artifacts": [
                {
                    "artifactId": "5346ae32-1b63-40c0-9aaa-345d815c792e",
                    "parts": [
                        { "text": "PONG" }
                    ]
                }
            ]
        }
    }
}

接口 URL 在发现和任务场景下保持相同,变化的只是请求内容。该端点只接受 SendMessage 和 A2A-Version: 1.0;任何其他 method 都会返回 JSON-RPC -32601,空消息返回 -32602,不支持的版本返回 VERSION_NOT_SUPPORTED 错误,而非 JSON-RPC 格式的请求体则返回 HTTP 400。

暴露与那把锋利的边缘

任务端点与卡片共享 enabled 和 published 这两个门控:只有在设置了 [a2a.server] enabled,且别名已启用并已发布时才会响应。对未发布或未知别名的 task POST 会返回 404,与其卡片相同。不过,它不共享卡片的认证策略:发现卡片仍然是公开的,而调用任务需要网关 bearer token,没有该 token 时返回 401。

需要注意的一个棘手点:接口 URL 对裸 GET 返回的是 web 仪表板,而不是 agent,因为网关会对它不识别的任何路径回退为提供仪表板:

curl -i http://localhost:42617/a2a/translator
HTTP/1.1 200 OK
content-type: text/html

Discovery(.well-known 路径)和 SendMessage POST 是受支持的接口。对接口 URL 直接发起的裸 GET 不属于该协议;请改为读取 .well-known 路径下的 card。

代理从何处提供服务

这些卡片由 Web 网关提供服务,地址和端口与其他所有内容相同。如果你的网关是 localhost:42617,那么目录和每个代理卡片都在这里。你不需要运行第二个服务器,也不需要打开第二个端口。

如果你将此部署置于反向代理或公共主机名之后,卡片中的 URL 需要与客户端实际访问到的地址一致。已发布的 URL 按以下顺序解析:如果你设置了显式的公共基础 URL,则优先使用它;然后是你设置的 A2A 专用主机和端口覆盖;最后是网关自身的地址。该覆盖项是为代理场景存在的;如果你不在代理后面,就不要修改它,卡片会直接公布网关地址。

打开它

在你将其开启之前,Discovery 处于关闭状态,而且它以三种相互独立的方式保持关闭,因此不会有任何内容意外泄露:

  • A2A 服务器默认对整个部署禁用。
  • 即使服务器已开启,每个 agent 默认也未发布。
  • 已发布的 agent 只暴露你命名的技能,别无其他。

您只需启用一次服务器,将希望可访问的特定 agent 标记为已发布,并列出每个 agent 暴露的技能。被禁用或未发布的 agent 不会出现在目录中,其卡片路径会返回 404。未知的 agent 名称也会返回 404。

只有当某个命名技能解析为智能体实际携带的真实技能时,它才会出现在卡片上:它必须位于智能体的某个技能包中,并且其 SKILL.md 必须具有有效的 YAML frontmatter。无法解析的名称,或位于智能体未声明的技能包中的技能,都会被静默丢弃,而不会被展示。

空的 skills: [] 数组最常见的原因,是在未声明任何 skill_bundles 的 agent 上设置了 a2a.exposed_skills。exposed_skills 只会缩小该 agent 已解析出的技能集;它本身不会加载技能。由于没有声明 bundle,过滤器没有任何可保留的内容,因此所有名称都会被丢弃,card 也就不会展示任何内容。请将所属的 bundle 添加到 agents.<alias>.skill_bundles。配置校验会将这种情况作为启动警告抛出(a2a_exposed_skills_without_bundles)。

发布实际上公开的内容

发布前请阅读此内容。一旦服务器启用且别名已发布,POST /a2a/{alias} 会为该别名执行一次完整的 agent 回合:它通过与聊天界面相同的路径调用该 agent,并使用该 agent 的整套已配置工具集(shell、file、browser,以及该别名携带的其他所有工具)。

该任务端点受网关的 bearer/pairing 认证保护,和其他所有写入面一样。调用方需要一个由 pairing 派生的 bearer token 才能调用已发布的 agent;未认证请求会返回 401,绝不会进入 agent 回合。

发现卡片不受该认证保护。目录卡片和按别名的卡片无需 token 即可读取,因此已发布的 surface 会向任何能够访问监听器的调用方公开其 agent 名称和暴露的技能。这正是 discovery 的目的:对等方在配对之前先读取卡片。这也意味着发布会将这些元数据暴露给任何能够访问网关的人,尽管调用 agent 仍然需要 token。

发布是在两个维度上的暴露决策:卡片元数据是公开的,任何持有有效 token 的人都可以使用其完整工具集调用已发布的别名。在切换开关之前:

  • 限定绑定姿态。将网关绑定到私有接口,或将其置于反向代理之后,而不是将监听器直接暴露给不受信任的网络。这样也限定了谁可以读取未认证的卡片。
  • 仅发布那些您愿意让任何令牌持有者调用其完整工具集、并愿意在未认证情况下公开其名称和技能的别名。将 exposed_skills 收窄到互操作所需的最小集合。
  • 在决定该别名携带哪些工具和技能包时,将已发布的别名视为可远程调用的执行面。
  • 跨部署互操作会与调用你的对等方共享一个令牌;请像对待任何其他凭据一样对其设置作用域并轮换。

多个部署如何连接

Discovery 可跨任意数量的部署进行组合。每个部署都会在各自的地址发布自己的目录。知道多个部署地址的客户端会获取每个目录,读取 agents,并由此持有一个合并后的映射,包含所有部署中每个可达的 agent。这里没有注册中心,也没有中央服务器:客户端是唯一需要知道这些地址的实体,并且它会直接与每个部署通信。

一张示意图。你运行一个个人部署。你的团队运行一个共享部署。一个数据团队运行第三个部署。你的客户端获取这三个目录:

curl http://personal.example:42617/.well-known/agents-card.json
curl http://team.example:42617/.well-known/agents-card.json
curl http://data.example:42617/.well-known/agents-card.json

每个都返回自己的代理列表。你的客户端现在会看到,例如,personal 上有一个 notes 代理,team 上有一个 deploy 代理,data 上有一个 query 代理。要使用其中任何一个,它会获取该代理的卡片,并将任务发送到该代理的 URL,完全如上所示。每次部署都没有变化;仍然是同样的两次读取和一次 POST,只是指向了不同的主机。

外发 A2A 客户端

上文描述的是一个部署被发现并被调用的方向。镜像方向则是出站:某个部署把任务委派给远程 A2A 智能体,并把返回结果用于自身工作。这就是出站客户端,通过 [a2a.client] 进行配置。它默认关闭,在您启用之前不会注册任何内容,因此未配置的安装不会产生任何出站 A2A 占用。

[a2a.client]
enabled = true                  # 注册 a2a_* 工具;默认 false

[[a2a.client.peers]]
name = "team"                   # 每个 a2a_* 工具的 `peer` 参数
base_url = "https://team.example.com"
token = "${TEAM_A2A_TOKEN}"     # 调用时从环境变量解析;为空则匿名访问
tags = ["production"]

每个 peer 都指定一个远程 A2A 服务器的源地址。客户端会根据 base_url 推导出众所周知的 card 路径和 JSON-RPC 任务路径;智能体调用方无需输入任何 URL,只需提供 peer 名称。

凭证以密文形式声明。token 值为 ${VAR} 时,会在调用时从环境变量中解析;值为字面量时,原样发送;值为空时,不发送 Authorization 请求头。由于该字段被标记为密文,配置和 schema 界面会将其视为只写的加密数据,而不是可读取的值。这与 http_request 对其认证令牌所采用的密文处理方式相同。

出站调用是 SSRF 攻击面,因此策略默认拒绝连接到私有、回环和链路本地主机上的对等端。对于指向 127.0.0.1 或 RFC1918 网段的本地或内网部署,可以设置 allow_private_hosts = true,或将具体主机添加到 allowed_private_hosts。允许列表只放行这些指定主机,不会放宽全局策略。主机名解析会固定到通过防护检查的地址,重定向被禁用,响应体大小受 max_response_bytes 限制,每个请求都有 request_timeout_secs 的超时上限。

四个工具的影响各不相同。a2a_discover 和 a2a_get_task 是读取操作,不计为动作。a2a_send 和 a2a_cancel 会修改对端状态,因此属于 Act 操作:默认需要审批,只读自主模式会在任何网络 I/O 发生之前拒绝它们。应将 a2a_send 视为对对端所发布技能的远程执行授权。

关闭出站客户端属于遏制措施,而不是回滚。设置 [a2a.client] enabled = false 可以阻止 a2a_* 工具注册,因此在出现出站外发事件时,正确的做法是禁用该开关并重启守护进程(重建注册表)以进行遏制。对于已在运行的守护进程,遏制不会立即生效:其代理任务仍持有已构建的 a2a_* 工具,直到重启或注册表重新组装为止。因此,运维人员应将外发流量视为在此之前仍在持续。禁用该开关并不会撤销随同一版本发布的入站网关迁移(SendMessage v1 接口)或共享的线路模型变更。已使用 v1 的外部调用方将继续正常工作;如果运维人员希望恢复到 v1 之前的入站行为,必须回滚整个版本,而不能仅切换此开关。网关版本协商、JSONRPC 方法名称,以及枚举和 Part 结构都已一并迁移到 v1。

用例

将部署串联起来的一些具体原因。

研究部署将文献检索交给专门的数据部署。研究代理发现数据部署的 search 代理,将查询作为任务发送给它,并把结果整合到自己的工作中。研究端从不持有数据端的凭据或索引;它只知道代理 URL。

一次值班部署会将事件分发到各团队拥有的部署。它会在每个团队的部署中发现一个 triage 代理,并把同一个事件作为任务发送给每个代理,同时收集它们的回答。每个团队控制其 triage 代理暴露的内容;值班端只读取卡片并发送任务。

个人部署无需共享登录信息即可调用公司部署中经过审核的 agent。你发现公司的 invoice agent,向它发送一份草稿请求,并收到返回结果。公司决定发布哪些 agent 和技能;你永远不会获得其部署内部的席位,只会拿到 agent 端点。

A2A 不是 MCP

这些解决的是不同的问题,而且它们是可以组合的。MCP 将一个代理连接到它的工具和上下文:它回答的是单个代理可以调用什么。A2A 将一个代理连接到其他代理,作为对等方:它回答的是它可以把工作交给哪些其他代理。你通过 A2A 访问到的代理,可能会在内部使用 MCP 工具来完成工作,而你既看不到也不需要关心;卡片展示的是技能,而不是背后的工具。使用 MCP 为代理赋予能力,使用 A2A 让代理彼此委派。