A2A Agent Card 与任务模型
正在读取学习记录…
A2A是Agent之间交换任务与结果的协议。调用方不用知道远端Agent内部如何规划、调用什么模型,只要按共同的接口发送请求、跟踪任务并接收产物。
先分清三个对象。AgentCard是服务名片,说明服务能处理哪些内容、有哪些技能、支持什么接口。Message是一条交互消息,可以发起请求或补充资料。Task是一项有状态的工作,让调用方在连接结束后仍能用编号跟踪进度。
名片像店门口的招牌,Message像顾客提出的问题,Task像已经受理的业务单。看到招牌不表示业务已经办完,收到一条“处理中”的话也不等于拿到结果。
用A2A 1.0字段读一组对象
服务名片的supportedInterfaces列出接口。例子选JSONRPC绑定、protocolVersion为1.0、HTTPS地址https://refund.example/a2a。version为1.0.0表示这位Agent自身的版本,它与协议版本不是同一个字段。
JSONRPC是JSON-RPC远程调用的绑定名称;这里的1.0表示A2A协议版本。实际JSON-RPC报文使用"jsonrpc": "2.0",并按A2A要求选择请求的协议版本。名片的协议版本与承载报文的JSON-RPC版本需要分别读取,可对照规范中的AgentInterface和JSON-RPC请求结构。
用户Message包含messageId、role和parts。1.0的JSON角色值使用ROLE_USER或ROLE_AGENT;文本Part直接写text字段。任务状态写在status.state中,使用TASK_STATE_WORKING等枚举名称。
程序在本地创建这些字典,检查示例字段与状态变化;https://refund.example只是固定名片数据,没有发网络请求。对象结构按1.0阅读,传输与认证要在接入服务时另外实现。
保存完整程序并运行
在自己的学习目录打开PowerShell,创建一个新的子目录:
mkdir l6-a2a
cd l6-a2a把下面完整代码保存为 a2a_demo.py。固定数据和运行入口都已包含,文件名保留 .py 后缀。
"""按A2A 1.0字段编写的本地对象与业务状态检查。"""
from copy import deepcopy
from urllib.parse import urlparse
CARD = {
"name": "退款资料助手", "description": "读取退款规则并整理依据",
"version": "1.0.0",
"supportedInterfaces": [{
"url": "https://refund.example/a2a", "protocolBinding": "JSONRPC",
"protocolVersion": "1.0",
}],
"capabilities": {"streaming": False},
"defaultInputModes": ["text/plain"], "defaultOutputModes": ["text/plain"],
"skills": [{"id": "refund.read", "name": "退款规则读取",
"description": "按订单查询可用规则", "tags": ["refund"]}],
}
MESSAGE = {"messageId": "M-17", "role": "ROLE_USER",
"parts": [{"text": "查询O-17的退款规则"}]}
STATES = {"TASK_STATE_" + value for value in (
"UNSPECIFIED", "SUBMITTED", "WORKING", "COMPLETED", "FAILED",
"CANCELED", "INPUT_REQUIRED", "REJECTED", "AUTH_REQUIRED")}
TERMINAL = {"TASK_STATE_" + value for value in ("COMPLETED", "FAILED", "CANCELED", "REJECTED")}
NEXT = {
"TASK_STATE_SUBMITTED": {"TASK_STATE_WORKING", "TASK_STATE_REJECTED"},
"TASK_STATE_WORKING": {"TASK_STATE_INPUT_REQUIRED", "TASK_STATE_AUTH_REQUIRED"} | TERMINAL,
"TASK_STATE_INPUT_REQUIRED": {"TASK_STATE_WORKING", "TASK_STATE_CANCELED"},
"TASK_STATE_AUTH_REQUIRED": {"TASK_STATE_WORKING", "TASK_STATE_CANCELED"},
}
def text(value):
return isinstance(value, str) and bool(value.strip())
def strings(value):
return isinstance(value, list) and bool(value) and all(text(item) for item in value)
def validate_card(card):
# 检查当前例子所用的字段和JSONRPC 1.0接口配置。
for key in ("name", "description", "version"):
if not text(card.get(key)):
raise ValueError("AgentCard缺少" + key)
if not isinstance(card.get("capabilities"), dict):
raise ValueError("capabilities必须是对象")
for key in ("defaultInputModes", "defaultOutputModes"):
if not strings(card.get(key)):
raise ValueError(key + "必须是非空媒体类型列表")
interfaces = card.get("supportedInterfaces")
if not isinstance(interfaces, list) or not interfaces:
raise ValueError("缺少supportedInterfaces")
for item in interfaces:
address = urlparse(item.get("url", ""))
if address.scheme != "https" or not address.netloc:
raise ValueError("接口地址必须是HTTPS绝对地址")
if (item.get("protocolBinding"), item.get("protocolVersion")) != ("JSONRPC", "1.0"):
raise ValueError("当前客户端需要JSONRPC 1.0接口")
skills = card.get("skills")
if not isinstance(skills, list) or not skills:
raise ValueError("缺少skills")
for skill in skills:
if not all(text(skill.get(key)) for key in ("id", "name", "description")) or not strings(skill.get("tags")):
raise ValueError("技能字段不完整")
def validate_parts(parts):
if not isinstance(parts, list) or not parts:
raise ValueError("parts不能为空")
for part in parts:
# v1.0的内容联合字段,不再使用旧kind判别字段。
if not isinstance(part, dict) or len(set(part) & {"text", "raw", "url", "data"}) != 1:
raise ValueError("每个Part只能有一种内容")
if "text" in part and not isinstance(part["text"], str):
raise ValueError("text必须是字符串")
def validate_message(message, task=None):
if not text(message.get("messageId")) or message.get("role") not in {"ROLE_USER", "ROLE_AGENT"}:
raise ValueError("消息编号或角色不合法")
validate_parts(message.get("parts"))
if message["role"] == "ROLE_AGENT" and not text(message.get("contextId")):
raise ValueError("服务端消息缺少contextId")
if task is not None:
if message.get("taskId") not in (None, task["id"]):
raise ValueError("taskId不匹配")
if message.get("contextId") not in (None, task.get("contextId")):
raise ValueError("contextId不匹配")
def transition(task, state):
current = task["status"]["state"]
if state not in STATES:
raise ValueError("未知任务状态")
if current in TERMINAL:
raise ValueError("终态任务不能重新运行")
# 允许边由这份退款应用定义;协议定义状态含义。
if state not in NEXT.get(current, set()):
raise ValueError("应用不允许此状态变化")
result = deepcopy(task)
result["status"]["state"] = state
return result
def main():
validate_card(CARD)
validate_message(MESSAGE)
interface = CARD["supportedInterfaces"][0]
print("名片接口:" + interface["protocolBinding"] + " " + interface["protocolVersion"])
print("用户消息:" + MESSAGE["parts"][0]["text"])
task = {"id": "T-17", "contextId": "C-17", "status": {"state": "TASK_STATE_SUBMITTED"}}
print("任务:" + task["id"] + " / " + task["contextId"])
for state in ["TASK_STATE_WORKING", "TASK_STATE_INPUT_REQUIRED",
"TASK_STATE_WORKING", "TASK_STATE_COMPLETED"]:
task = transition(task, state)
print("状态:" + state)
try:
transition(task, "TASK_STATE_WORKING")
except ValueError as error:
print("再次运行:" + str(error))
if __name__ == "__main__":
main()保存后执行 dir,确认能看到 a2a_demo.py,再运行:
ls
python -X utf8 a2a_demo.py程序输出:
名片接口:JSONRPC 1.0
用户消息:查询O-17的退款规则
任务:T-17 / C-17
状态:TASK_STATE_WORKING
状态:TASK_STATE_INPUT_REQUIRED
状态:TASK_STATE_WORKING
状态:TASK_STATE_COMPLETED
再次运行:终态任务不能重新运行名片告诉调用方怎样交互
supportedInterfaces不是一个裸URL,而是接口对象列表。每项包含url、protocolBinding与protocolVersion;列表第一项表达优先接口。规范的核心绑定包括JSONRPC、GRPC与HTTP+JSON。validate_card把当前客户端配置限定为JSONRPC 1.0,因此其他合法绑定也会被这位客户端拒绝,这是客户端适配范围而不是协议不支持。
defaultInputModes和defaultOutputModes使用媒体类型,例子都是text/plain。skills表示具体业务能力,包含id、name、description和tags。capabilities中的streaming表示是否支持流式更新,它不等于某项退款业务技能。
这份检查器核对所用字段、非空列表和HTTPS接口配置。部署时还需要完整规范或对应版本SDK检查更多对象、可选字段与传输错误。能力声明帮助选择候选服务,服务认证与订单权限仍要由独立机制核对。
消息内容与三个编号怎样关联
messageId标识一条消息;Task.id标识一项工作;contextId用于归组相关交互。一个上下文可以关联多个任务,一项任务也可以经历多条消息。不要用messageId替换taskId去查询任务。
Part是内容块,1.0中text、raw、url和data四种内容字段每块只能选一种。两块内容可以放在两个Part里;一块同时写text和data会被例子拒绝。旧示例中的kind="text"不再是1.0的内容判别方式。
客户端补充消息可以带taskId。若同时携带contextId,两者应属于同一任务关系;服务端Message需要contextId。validate_message可以接收task参数来核对这种关系,避免把另一个会话的补充内容塞进当前退款单。
等输入、成功结束与失败结束
输出依次进入WORKING、INPUT_REQUIRED、WORKING、COMPLETED。INPUT_REQUIRED表示需要额外输入,例如缺订单号;这是可恢复的中断。AUTH_REQUIRED则表示等待所需认证。它们和成功或失败的终态不同。
COMPLETED、FAILED、CANCELED和REJECTED都是终态。已经结束的任务不能重新变成WORKING;后续相关新工作应有新任务。程序最后一次尝试重开完成任务,被拒绝。
NEXT给出这个退款应用采用的允许边,例如待处理任务先进入工作,再等待输入。协议定义各状态含义,应用还要根据业务决定何时更新、允许哪些路径;不要把例子的NEXT当成全协议唯一状态机。
具体结果通常以任务的artifacts返回。Message可说明进度、澄清问题或补充输入,Task负责跟踪,Artifact承载实际产物。任务成功状态也需要与真正保存的产物对应,不能只更新一个枚举就宣布资料已经交付。
改一个内容块,触发本地校验
在MESSAGE的现有Part中增加data字段,使它成为{"text": "查询O-17的退款规则", "data": {"order": "O-17"}},运行python -X utf8 a2a_demo.py。程序报“每个Part只能有一种内容”。
若需要同时传文本和结构数据,把parts改为两个对象:[{"text": "查询退款规则"}, {"data": {"order": "O-17"}}],再运行同一命令。联合字段检查通过;输出中用户消息显示第一块文本。
把接口protocolVersion的1.0改为0.3再运行,会报当前客户端需要JSONRPC 1.0接口。不要只改版本字符串而继续发送旧字段;协议版本改变时,应同时核对对象结构、传输绑定和状态枚举。
先将MESSAGE.parts恢复为原来的一个text对象,再将protocolVersion恢复为1.0,保存并重跑。应重新显示四次状态变化及终态拒绝,随后再运行配套检查。
参考阅读:A2A规范重点对照AgentCard、AgentInterface、Message、Part与TaskState字段;Life of a Task关注可恢复中断和不可重开的终态。
运行配套检查
把下面测试完整保存为 test_a2a_demo.py,与主程序放在同一目录。6项检查覆盖名片与消息、接口配置、Part联合字段、服务端上下文、任务关联和终态约束。
查看测试代码:test_a2a_demo.py
"""检查v1.0示例中的字段、内容联合、关联编号和终态。"""
import copy
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import a2a_demo as demo
class A2ATests(unittest.TestCase):
def test_valid_card_and_message(self):
demo.validate_card(demo.CARD)
demo.validate_message(demo.MESSAGE)
def test_missing_interface_and_wrong_version(self):
for key, value in (("supportedInterfaces", []), ("skills", [])):
broken = copy.deepcopy(demo.CARD)
broken[key] = value
with self.subTest(key=key), self.assertRaises(ValueError):
demo.validate_card(broken)
broken = copy.deepcopy(demo.CARD)
broken["supportedInterfaces"][0]["protocolVersion"] = "0.3"
with self.assertRaises(ValueError):
demo.validate_card(broken)
def test_part_requires_exactly_one_content(self):
for parts in ([], [{}], [{"text": "规则", "data": {}}], [{"text": 12}]):
with self.subTest(parts=parts), self.assertRaises(ValueError):
demo.validate_parts(parts)
def test_server_message_requires_context(self):
message = dict(demo.MESSAGE, role="ROLE_AGENT")
with self.assertRaises(ValueError):
demo.validate_message(message)
demo.validate_message(dict(message, contextId="C-17"))
def test_task_context_mismatch(self):
task = {"id": "T-17", "contextId": "C-17"}
with self.assertRaises(ValueError):
demo.validate_message(dict(demo.MESSAGE, taskId="T-other"), task)
def test_interruption_can_resume_but_terminal_cannot(self):
task = {"id": "T-17", "status": {"state": "TASK_STATE_WORKING"}}
task = demo.transition(task, "TASK_STATE_INPUT_REQUIRED")
task = demo.transition(task, "TASK_STATE_WORKING")
task = demo.transition(task, "TASK_STATE_COMPLETED")
with self.assertRaises(ValueError):
demo.transition(task, "TASK_STATE_WORKING")
if __name__ == "__main__":
unittest.main()python -X utf8 -m unittest -v test_a2a_demo.py测试名称后应显示 ok,末尾显示 Ran 6 tests 和 OK。若找不到模块,先用 dir 确认两个文件保存在同一目录。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · Agent Card和Task的用途怎样区分
Agent Card和Task的用途怎样区分?
对应知识点:L6-a2a
简答题 · 解释机制
A2A服务能力和任务状态怎样接起来?
对应知识点:L6-a2a
代码题 · 核对给定数据
写一个断言检查任务状态从工作中变成已完成后存在产物。
对应知识点:L6-a2a
场景题 · 分析具体变化
收到请求已受理后,还需要观察什么才能取得退款资料?
对应知识点:L6-a2a
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。