后端约定
这一页描述内置适配器实际读取的格式和 SDK 接入约定,可供后端团队对接。它不是外部协议官方规范或版本兼容清单;示例扩展需由部署明确支持。
结论
能力是一项一项独立的。接入方提供函数后启用对应能力,界面入口还受配置与当前状态约束。只有「发起一轮」是必需的。
停止、续接、插话、重新生成都靠轮次编号 指明是哪一轮。同一轮从头到尾用同一个编号。
续接靠事件编号 指明从哪之后接着发。协议的事件没有编号时,放在 SSE 的 id: 字段里。
历史里每条消息要带消息编号、所属轮次编号、时间;每一轮要带结果。
这份约定不规定地址、请求方式和鉴权。它们由前端的接入方在自己的函数里决定,和后端团队商定即可。
前端这一侧的分工
后端返回的格式和这里写的不一样时,由接入方在自己的函数里转换,见后端不符合约定时 。
几个名词
每项能力后端要做到什么
通用的规则
轮次编号
发起命令时使用本地 runId;直接流读到后端身份后,ADK、A2A、Hermes 保存映射,后续能力按各自规则使用后端编号。独立订阅在这一步之前分发,本地命令的事件路由编号必须与命令一致,不能指望受理响应自动建立映射。
同一轮用同一个编号
同一轮用同一个轮次编号 正确 整轮一个编号 请求模型 轮次 r-1 调用工具 轮次 r-1 再请求模型 轮次 r-1 给出回答 轮次 r-1 错误 每次调用一个编号 请求模型 轮次 a 调用工具 轮次 a 再请求模型 轮次 b 给出回答 轮次 b 历史读回来,一轮会变成两轮;停止、续接找不到完整的这一轮。 同一后端轮次使用稳定编号;本地编号可不同。直接流保存映射,独立订阅需先统一路由身份。
后端如果按「每次模型调用一个编号」来存,历史读回来一轮会变成好几轮。
事件编号
规则同样只有一条:协议的事件自带编号的,用协议的;没有的,放在 SSE 的 id: 字段里。
SSE 的 id: 字段是传输层的东西,不改变协议的数据内容:
id: e17
data: {"choices":[{"index":0,"delta":{"content":"你好"}}]}
续接的过程
续接的过程 界面与 Core 适配器 能力函数 接入方写 后端 run 返回的流:事件 e1、e2、e3 记下最后收到的事件编号:e3 连接断开。这一轮还没有结论,留在进行中 续接这一轮 调用 resume 轮次编号、事件编号 e3 发请求 带上轮次编号和 e3 只发 e3 之后的:e4、e5、结论 接在同一条回答后面 续接靠两个编号:轮次编号指明是哪一轮,事件编号指明从哪之后接着发。
后端要做到两点:
把一轮里发过的事件按顺序存下来,存到这一轮结束之后一段时间。
收到续接请求时,从指定的事件编号之后接着发。直接流应排除重复增量;并非所有适配器都会按后端事件编号去重。
协议里没有的信息放在哪
以下列出各适配器读取的位置。runs、active_run、metadata.lastEventId 等属于本文接入约定,不能因字段名相似就当成外部官方标准:
时间的写法:
会话目录由应用管理
Adapter 不规定目录接口或响应格式。应用将业务响应映射为 SessionInfo 数组,并传给受控 SessionList;创建、改名、删除、fork 与路由由应用实现。Adapter 的 history 只查询一个 sessionId 的消息历史。
OpenAI 兼容接口
流
本适配器读取 Chat Completions chunk。直接 HTTP 流识别 [DONE];finish_reason 标记模型步骤已有结论,读取器仍等待流结束,工具调用可能继续下一步模型请求。异步序列使用 finish_reason 后结束迭代,不传字符串 [DONE]。
出错可以用非 2xx 状态码,也可以在流里发一个带 error 对象的块。
历史
OpenAI 的消息数组,加上约定的字段。
{
"messages" : [
{ "id" : "m0" , "created_at" : 1760000000 , "role" : "system" , "content" : "你是助手" } ,
{ "id" : "m1" , "run_id" : "s-1:run:ab12:1" , "created_at" : 1760000001 , "role" : "user" , "content" : "北京天气" } ,
{
"id" : "m2" ,
"run_id" : "s-1:run:ab12:1" ,
"created_at" : 1760000002 ,
"role" : "assistant" ,
"content" : "我查一下" ,
"reasoning_content" : "需要查天气" ,
"tool_calls" : [
{ "id" : "call-1" , "type" : "function" , "function" : { "name" : "weather" , "arguments" : "{\"city\":\"北京\"}" } }
]
} ,
{ "id" : "m3" , "run_id" : "s-1:run:ab12:1" , "role" : "tool" , "tool_call_id" : "call-1" , "content" : "{\"temp\":20}" } ,
{ "id" : "m4" , "run_id" : "s-1:run:ab12:1" , "created_at" : 1760000004 , "role" : "assistant" , "content" : "北京 20 度" } ,
{ "id" : "m5" , "run_id" : "s-1:run:ab12:2" , "created_at" : 1760000010 , "role" : "user" , "content" : "上海呢" } ,
{ "id" : "m6" , "run_id" : "s-1:run:ab12:2" , "created_at" : 1760000011 , "role" : "assistant" , "content" : "上海" }
] ,
"runs" : [
{
"id" : "s-1:run:ab12:1" ,
"status" : "completed" ,
"created_at" : 1760000001 ,
"finished_at" : 1760000005 ,
"model" : "model-a" ,
"usage" : { "total_tokens" : 30 }
} ,
{ "id" : "s-1:run:ab12:2" , "status" : "running" , "last_event_id" : "e42" }
] ,
"cursor" : "before-m0" ,
"has_more" : true
}
tool 消息的内容是 JSON 字符串时前端会解析。内容恰好是 { "error": "原因" }(只有这一个键)时认作工具调用失败。
ADK
流
run_sse 的事件流,每个事件带 id 和 invocationId。正常结束需要 data: [DONE] 或约定的明确结束事件;普通连接关闭不代表执行完成,
出错发带 errorCode / errorMessage 的事件。
续接
请求内容是 { "sessionId", "streaming": true, "lastEventId" },不带 newMessage。
后端只发 lastEventId 之后的事件。
ADK 会话详情未提供执行状态时,适配器不猜测最后一轮仍在运行。需要自动续接时,后端或 history 函数组合的响应应明确包含:
{ "session" : { "status" : "running" } , "active_run" : { "run_id" : "invocation-123" , "event_id" : "event-45" } }
事件编号必须对应历史消息已经包含的前缀。active_run 为 null 表示已知无活动执行;缺省表示未知,不自动续接。ADK resume 的非 2xx 响应文本匹配 no active or completed stream 或 without last_event_id 时返回 false;这不补造完成结论,其余错误正常上报。
历史
会话详情,历史就是里面的 events,从早到晚。
{
"id" : "s-1" ,
"state" : { "title" : "北京天气" } ,
"lastUpdateTime" : 1760000005.2 ,
"events" : [
{
"id" : "e1" ,
"invocationId" : "inv-1" ,
"author" : "user" ,
"timestamp" : 1760000001 ,
"content" : { "role" : "user" , "parts" : [{ "text" : "北京天气" }] }
} ,
{
"id" : "e2" ,
"invocationId" : "inv-1" ,
"author" : "model" ,
"content" : {
"role" : "model" ,
"parts" : [{ "functionCall" : { "id" : "call-1" , "name" : "weather" , "args" : { "city" : "北京" } } }]
}
} ,
{
"id" : "e3" ,
"invocationId" : "inv-1" ,
"author" : "model" ,
"content" : {
"role" : "user" ,
"parts" : [{ "functionResponse" : { "id" : "call-1" , "name" : "weather" , "response" : { "temp" : 20 } } }]
}
} ,
{
"id" : "e4" ,
"invocationId" : "inv-1" ,
"author" : "model" ,
"content" : { "role" : "model" , "parts" : [{ "text" : "北京 20 度" }] }
}
]
}
Hermes
流
会话接口的事件流,每帧是 event: <名字> 加 data: <JSON>:
run.started → assistant.delta / tool.* → run.completed(或 run.failed / run.cancelled / run.interrupted)→ done
历史
会话的消息列表,加上约定的字段。
{
"data" : [
{ "id" : 1 , "run_id" : "run_a" , "role" : "user" , "content" : "北京天气" , "timestamp" : 1760000001 } ,
{
"id" : 2 ,
"run_id" : "run_a" ,
"role" : "assistant" ,
"content" : "我查一下" ,
"reasoning" : "需要查天气" ,
"tool_calls" : [{ "id" : "c1" , "function" : { "name" : "weather" , "arguments" : "{\"city\":\"北京\"}" } }]
} ,
{ "id" : 3 , "run_id" : "run_a" , "role" : "tool" , "tool_call_id" : "c1" , "content" : "20 度" } ,
{ "id" : 4 , "run_id" : "run_a" , "role" : "assistant" , "content" : "北京 20 度" }
] ,
"runs" : [{ "id" : "run_a" , "status" : "completed" , "created_at" : 1760000001 , "finished_at" : 1760000005 }]
}
A2A
流
JSON-RPC over SSE,每帧的 data 是一个 JSON-RPC 响应,result 里是 A2A 的事件(task、status-update、artifact-update)。
执行结论按任务状态映射;final: true 本身不代表 completed。直接 HTTP 流另接受 [DONE] 作为 SDK 兼容完成标记。出错发带 error 的 JSON-RPC 响应。
请求内容是填好的 JSON-RPC 信封,接入方原样发出:
停止和插话的响应是普通的 JSON-RPC 响应:状态码成功、内容里又没有 error 才算成功。
历史
这个会话(contextId)下的任务列表,从早到晚。一个任务就是一轮。
{
"tasks" : [
{
"kind" : "task" ,
"id" : "task-1" ,
"contextId" : "s-1" ,
"status" : { "state" : "completed" , "timestamp" : "2026-01-02T03:04:05Z" } ,
"history" : [
{ "kind" : "message" , "messageId" : "u1" , "role" : "user" , "parts" : [{ "kind" : "text" , "text" : "北京天气" }] } ,
{
"kind" : "message" ,
"messageId" : "m1" ,
"role" : "agent" ,
"parts" : [
{
"kind" : "data" ,
"data" : { "id" : "call-1" , "name" : "weather" , "args" : { "city" : "北京" } } ,
"metadata" : { "adk_type" : "function_call" }
}
]
} ,
{
"kind" : "message" ,
"messageId" : "m2" ,
"role" : "agent" ,
"parts" : [
{
"kind" : "data" ,
"data" : { "id" : "call-1" , "name" : "weather" , "response" : { "temp" : 20 } } ,
"metadata" : { "adk_type" : "function_response" }
}
]
}
] ,
"artifacts" : [{ "artifactId" : "a1" , "parts" : [{ "kind" : "text" , "text" : "北京 20 度" }] }]
} ,
{
"kind" : "task" ,
"id" : "task-2" ,
"contextId" : "s-1" ,
"status" : { "state" : "working" } ,
"metadata" : { "lastEventId" : "e42" } ,
"history" : [
{ "kind" : "message" , "messageId" : "u2" , "role" : "user" , "parts" : [{ "kind" : "text" , "text" : "上海呢" }] }
]
}
] ,
"nextPageToken" : "older"
}
服务端是 ADK 执行器(正文在 status-update 里流式下发)时,接入方会声明 profile: 'adk',历史的格式不变:
过程消息里是正文和工具调用,最后一条产出是最后一条过程消息的副本。
已知限制
第 1 条里,各协议的历史要带什么:
哪些行为要对着真实后端确认
以下是示例依赖的部署假设,并未对真实服务逐一验证,不能作为官方协议保证。接入时需要确认:
会话状态与持续接收
历史可附带 session.status(idle / running / error / unknown),状态缺失或无法映射时为 unknown,私有值保留在 metadata.backendStatus。active_run 定位仍在执行的轮次及游标。两者不能互相替代;状态在独立会话接口时,由 history 函数组合响应。
独立接收使用 subscribe:运行命令返回受理,事件通道提供输出。开始和结束事件都要能定位 runId。HTTP、分离接口、WS 与重放顺序见发送和接收 。