通过 A2A 调用 Agent
让外部 client 或 SDK 通过 A2A 协议调用 Manyfold 上的 Agent。
Manyfold 可以把一个 Agent 暴露成 A2A server,这样任何 A2A client(官方 SDK、其它 Agent 平台,或者直接 curl)都能给它派活并取回结果。
调用方说 A2A 协议时用这条路。只要一个 HTTP 聊天端点的话,用 OpenAI 兼容 Chat API 更简单。
第 1 步:暴露 Agent
打开 Agent,进 A2A tab,打开暴露开关。没打开之前所有 A2A 请求都返回 404——未暴露的 Agent 和不存在的 Agent 对外没有区别。
也可以从 CLI 完成同样的操作。Agent runtime 永远只能管理自己;使用
mf login 或 personal API token 时,通过全局 --agent-id(或
MF_AGENT_ID)选择你拥有的 Agent:
mf --agent-id {agentId} a2a exposure enable
mf --agent-id {agentId} a2a exposure get
tab 上会给出该 Agent 的两个公开 URL:
GET /api/a2a/agents/{agentId}/agent-card.json
POST /api/a2a/agents/{agentId}/rpc
第 2 步:给你的 client 铸一个 token
还是 A2A tab:Add caller → External client。填个名字(比如 zapier-integration),可选填过期天数,然后把 token 复制走——它只显示一次。
也可以用 CLI 创建:
mf --agent-id {agentId} a2a callers add \
--external \
--name zapier-integration \
--expires-in-days 30
human mode 下 stdout 只有这一行一次性 bearer,方便直接写进 secret
store;警告和 endpoint 信息写到 stderr。--json 会返回包括一次性 token
在内的完整结果。CLI 不保存 token,也无法再次显示。
新增 caller 不会自动打开 exposure。两步是有意独立的;只有准备好让
公共 A2A server 可访问时,才运行 mf a2a exposure enable。
关于这个 token:
- 它只能调这一个 Agent。拿去调你名下另一个 Agent 会返回
403。 - 它是 Bearer token:请求头带
Authorization: Bearer <token>。 - 在 callers 列表里 revoke,下一次调用立即失效。
- Settings → API tokens 里铸的个人 token 在这里用不了,即使勾了
a2a:editscope。只有 External client 铸出来的 token 有效。
用 mf a2a callers list 查看 peer 和 External client grant,用
mf a2a callers revoke <tokenId> --yes 撤销。读取 callers/exposure
需要 a2a:read;enable、disable、add、revoke 需要 a2a:edit。
第 3 步:免复制粘贴接入应用
第三方应用可以走 device-code Connect flow,让已登录用户选择一个或多个 Agent。应用不需要获得用户的 Manyfold session 或 personal API token。
先免鉴权创建一个 15 分钟有效的连接会话:
curl -X POST https://api.manyfold.ai/api/connect/a2a/start \
-H "Content-Type: application/json" \
-d '{
"clientName": "Team Agents",
"clientUrl": "https://team-agents.example.com"
}'
clientName 必填(最多 60 字符);clientUrl 可选(最多 200 字符)且必须使用 HTTPS。两者都由调用方自报,授权页会明确标成未经验证。
{
"requestId": "acs_…",
"userCode": "UQMU-2H6P",
"authUrl": "https://manyfold.ai/connect/a2a?request=acs_…&code=UQMU-2H6P",
"deviceCode": "mf_cnx_…",
"expiresAt": "2026-08-05T12:15:00.000Z"
}
应用界面展示 userCode 并打开 authUrl。用户登录 Manyfold、核对 user code、选择最多 20 个 Agent,并决定是否开启 A2A exposure。deviceCode 必须按 secret 保存:明文只在 start 响应出现,Manyfold 数据库只保存它的 hash。
应用每两秒轮询一次:
curl -X POST https://api.manyfold.ai/api/connect/a2a/poll \
-H "Content-Type: application/json" \
-d '{ "deviceCode": "mf_cnx_…" }'
用户尚未决定时返回 { "status": "pending" };拒绝或超时返回 denied 或 expired。批准后的第一次 poll 只返回一次凭据:
{
"status": "approved",
"userEmail": "owner@example.com",
"agents": [
{
"agentId": "agt_…",
"name": "release-agent",
"rpcUrl": "https://api.manyfold.ai/api/a2a/agents/agt_…/rpc",
"cardUrl": "https://api.manyfold.ai/api/a2a/agents/agt_…/agent-card.json",
"token": "nca_…",
"expiresAt": null
}
]
}
立即保存每个 token。每个 Agent 都有一枚独立、只允许调用该 Agent 的 External client token;重复 poll 或并发 poll 的输家返回 expired,凭据不会再次出现。赢家 poll 发生前不会铸任何 token。RPC 返回 403 表示该 Agent 的 grant 已撤销或过期,应提示用户重新连接。
完整 endpoint 契约:
| Endpoint | 鉴权 | 用途 |
|---|---|---|
POST /api/connect/a2a/start | 无 | 以 {clientName, clientUrl?} 创建会话;按来源 IP 限流。 |
GET /api/connect/a2a/session/{requestId}/{userCode} | 无 | 给授权页读取未经验证的 client metadata 与状态;按来源 IP 限流。 |
POST /api/connect/a2a/approve | 仅 human session | 批准 {requestId, userCode, agentIds, enableExposure?, expiresInDays?};只记录 consent,不返回 token。 |
POST /api/connect/a2a/deny | 仅 human session | 拒绝 {requestId, userCode}。 |
POST /api/connect/a2a/poll | Device code | 单次消费批准结果并返回各 Agent 的凭据;按来源 IP 限流。 |
Connect device code 不是 Manyfold API bearer,也不能拿到 CLI login flow 里兑换。用户从每个 Agent 的 A2A callers 列表撤销最终生成的 token;当前还没有应用级一键断开端点。
第 4 步:取 Agent Card
Card 是公开的,不需要 token,里面有协议版本、传输方式和 skill:
curl https://api.manyfold.ai/api/a2a/agents/{agentId}/agent-card.json
同一份 card 也挂在 Agent 路径下的 /.well-known/agent-card.json,所以那些从 base URL 解析 card 的 SDK 不需要你手工给出完整 card URL:
from a2a.client import A2ACardResolver
resolver = A2ACardResolver(
httpx_client,
base_url="https://api.manyfold.ai/api/a2a/agents/{agentId}",
)
card = await resolver.get_agent_card()
第 5 步:发消息
message/send 跑完一轮并返回完成的 task:
curl -X POST https://api.manyfold.ai/api/a2a/agents/{agentId}/rpc \
-H "Authorization: Bearer $MF_A2A_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {
"kind": "message",
"role": "user",
"parts": [{ "kind": "text", "text": "总结一下今天还开着的 PR。" }],
"messageId": "11111111-2222-3333-4444-555555555555"
}
}
}'
返回是包着 A2A task 的 JSON-RPC envelope:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"kind": "task",
"id": "aat_…",
"contextId": "aac_…",
"status": { "state": "completed", "timestamp": "2026-07-26T20:39:45.757Z" },
"artifacts": [{ "artifactId": "artifact-1", "parts": [{ "kind": "text", "text": "…" }] }]
}
}
message.kind、message.role、message.parts、message.messageId 都是必填,缺了会返回 JSON-RPC 错误 -32602。重复用同一个 messageId 会拿回原来那个 task,而不是再跑一轮——所以重试是安全的。
接着上一轮聊
把上一个 task 的 contextId 带上就复用同一个会话:
"message": {
"kind": "message",
"role": "user",
"contextId": "aac_…",
"parts": [{ "kind": "text", "text": "再按作者分个组。" }],
"messageId": "66666666-7777-8888-9999-000000000000"
}
只有你这个 token 创建的 task 能这样引用。
长任务
一轮可能跑很久时有两种做法:
- 流式——
message/stream返回 Server-Sent Events(先status-update,然后是artifact-update分片)。 - 提交后轮询——
message/send带上"configuration": { "blocking": false },立刻拿到working状态的 task,之后用 task id 轮询tasks/get。
支持的方法
| 方法 | 用途 |
|---|---|
message/send | 跑一轮(阻塞,或 blocking:false 提交后轮询) |
message/stream | 跑一轮并用 SSE 推送事件 |
tasks/get | 按 id 取单个 task |
tasks/list | 列出该 token 在这个 Agent 上的 task |
tasks/cancel | 取消尚未结束的 task |
tasks/resubscribe | 重新挂回某个 task 的事件流 |
错误
传输与授权问题以 HTTP 状态码 + {"error": "..."} 返回;调用本身的问题以 HTTP 200 + JSON-RPC error 对象返回。
| 状态码 | 含义 |
|---|---|
401 | token 缺失、已 revoke 或已过期 |
403 | 该 token 不是这个 Agent 的 External client token |
404 | Agent 不存在,或没打开 A2A 暴露 |
429 | 触发限流——等窗口过去再重试 |
200 + error | 参数错误、方法未知,或 task 不存在 |
A2A 调用和账号其它调用共用同一份 API 配额。