A2A

这一页说明怎么接一个 A2A(JSON-RPC over SSE)的 Agent:每个函数收到什么、要返回什么,以及这个协议要注意的地方。

结论

  • 对话、停止、续接、插话的请求内容是适配器填好的 JSON-RPC 信封,接入方原样发出。
  • 本 SDK 的 sessions 和按会话返回 tasks 的 history 是接入方约定,须由后端按后端约定另外提供。
  • 本 SDK 从 SSE id 或异步序列外层 eventId 取续接游标。tasks/resubscribe 请求体没有游标字段,接入方必须另行约定重放位置。
  • 仅在后端使用本文所述 ADK 事件排列时选择 profile: adk;不能只凭服务端名称选择。
能力本 SDK 的请求约定给函数之前要确认
run有:message/stream—
abort有:tasks/cancelAgent 实现了这个方法
resume有:tasks/resubscribeAgent 实现了这个方法,并且流里带事件编号
steer有:message/sendAgent 接受给进行中的任务追加消息,以后端的实际行为为准
regenerate / edit接入方扩展见下文「重新生成与编辑后重发」
history接入方扩展后端提供了按会话取任务列表的接口

最小接入示例

只给 run,直连本机的 Agent:

adapter.ts
import { createA2aAdapter } from '@volcengine/chatkit/adapters/a2a';

// 直连本机 Agent 的 JSON-RPC 端点,只用于本地开发
const RPC = 'http://localhost:9999/';

export const adapter = createA2aAdapter({
  // 请求内容是适配器填好的 JSON-RPC 信封,原样发出
  run: ({ body }) =>
    fetch(RPC, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
      body: JSON.stringify(body),
    }),
});
有什么没有什么
多轮对话会话列表
流式显示、工具调用历史:刷新后界面清空
停止(取消请求)重新生成、编辑后重发

适配器把 sessionId 编到 message.contextId,不会主动建会话。后端是否接受客户端给定 contextId、是否按它保存上下文,需要确认;适配器不会把服务端另发的 contextId 自动替换为当前 sessionId。

走自己的代理

生产环境的请求发给自己的服务,由它带上凭据转发给 Agent:

adapter.ts
import { createA2aAdapter } from '@volcengine/chatkit/adapters/a2a';

// 请求发给自己的服务,由它带上凭据转发给 Agent
const rpc = (body: unknown) =>
  fetch('/api/a2a', {
    method: 'POST',
    credentials: 'include', // 带上登录态,服务端据此知道用户是谁
    headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
    body: JSON.stringify(body),
  });

export const adapter = createA2aAdapter({
  run: ({ body }) => rpc(body),
  abort: ({ body }) => rpc(body),
});
要点说明
信封原样转发代理不用理解 JSON-RPC 的内容,加上凭据转发就行
按 method 做权限检查代理可以读信封里的 method,只放行允许的方法
事件流原样透传不要缓冲
连接断开要传到上游用户点停止时前端会断开请求,代理要跟着断开和上游的连接

全部能力

adapter.ts
import { createA2aAdapter } from '@volcengine/chatkit/adapters/a2a';

const RPC = '/api/a2a';
const API = '/api/chat';
const rpc = (body: unknown, headers: Record<string, string> = {}) =>
  fetch(RPC, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream', ...headers },
    body: JSON.stringify(body),
  });
const send = (method: string, path: string, body?: unknown) =>
  fetch(`${API}${path}`, {
    method,
    headers: { 'Content-Type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
const query = (cursor?: string) => (cursor ? `?pageToken=${encodeURIComponent(cursor)}` : '');

export const adapter = createA2aAdapter({
  // 以下是协议里有的方法:请求内容是适配器填好的 JSON-RPC 信封,原样发出
  run: ({ body }) => rpc(body),
  abort: ({ body }) => rpc(body),
  steer: ({ body }) => rpc(body),
  // 最后收到的事件编号放进请求头,后端只发它之后的事件
  resume: ({ body, eventId }) => rpc(body, { 'Last-Event-ID': eventId }),
  // 以下要后端按约定另外提供,地址和请求头只是一种排法
  regenerate: ({ targetRunId, body }) => rpc(body, { 'X-Replace-From': targetRunId }),
  edit: ({ targetRunId, body }) => rpc(body, { 'X-Replace-From': targetRunId }),
  history: ({ sessionId, cursor }) => send('GET', `/contexts/${sessionId}/tasks${query(cursor)}`),
});

注释里标了「另外提供」的函数,地址和请求头只是一种排法,以和后端团队商定的为准。

每个函数收到什么、要返回什么

表中“事件流”指未配置 subscribe 时的 Response 或异步事件序列。配置 subscribe 后,run / resume / regenerate / edit 只返回受理 Response,输出全部走订阅;其余能力仍返回 Response。

函数请求上的字段请求内容 body(JSON-RPC 信封)要返回的响应
runsessionId、runId、bodymethod: 'message/stream',params: { message }事件流,每帧一个 JSON-RPC 响应
regenerate / edit同 run,多一个 targetRunId同 run同 run
resumesessionId、runId、eventId、bodymethod: 'tasks/resubscribe',params: { id: 任务编号 }事件流,格式同 run
steersessionId、runId、bodymethod: 'message/send',params: { message },message.taskId 是进行中的任务JSON-RPC 响应,带 error 就是失败
abortsessionId、runId、bodymethod: 'tasks/cancel',params: { id: 任务编号 }JSON-RPC 响应,带 error 就是失败
historysessionId、cursor?没有任务列表 { tasks, nextPageToken? }

steer 和 abort 的响应和别的协议不同:状态码成功、内容里又没有 error 才算成功。

响应的格式和例子见后端约定 · A2A。

编号

编号取自说明
会话编号sessionId就是 A2A 的 contextId
轮次编号任务编号 taskIdabort、resume、steer 收到的 runId 就是它。还没拿到任务编号时不调 abort、steer,停止返回未受理,状态仍待确认
事件编号SSE 的 id:直接 HTTP 流取 id,异步序列取外层 eventId;未提供游标时不能自动续流

业务状态与内容扩展

聊天里的业务字段通过 transformOutput 转成内容变更。独立业务状态由应用维护,再交给插槽组件;适配器不提供业务状态通知或累积状态 getter。

输入怎么放进请求

输入里的放在哪
正文 text消息的文字片段
附件消息的文件片段。data: 地址给内容,其余给地址。附件类型不限
引用消息的 metadata
metadata不进请求,只挂在界面上的用户消息上

后端认别的字段时用 transformInput 换掉:

adapter.ts
import { createA2aAdapter } from '@volcengine/chatkit/adapters/a2a';

export const adapter = createA2aAdapter({
  run: ({ body }) =>
    fetch('/api/a2a', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
      body: JSON.stringify(body),
    }),
  // 默认协议编码完成后,返回完整请求体。
  transformInput: (input, { defaultInput }) => {
    if (defaultInput.method === 'tasks/resubscribe') return defaultInput;
    return {
      ...defaultInput,
      params: {
        ...defaultInput.params,
        message: {
          ...defaultInput.params.message,
          metadata: { ...defaultInput.params.message.metadata, tenant: input?.metadata?.tenant },
        },
      },
    };
  },
});

注意点

选择事件翻译配置

这是本 SDK 的两种翻译配置,请求相同,事件解读不同;不等于自动识别官方协议版本:

profile适用的服务端正文取自结束时的产出
'a2a'(默认)产出增量模式artifact-update就是正文
'adk'ADK 执行器进行中的 status-update 里的消息最后一条消息的副本,跳过

选错了的表现:

现象原因
回答要等结束才整段出现,工具之间的说明文字丢失服务端是 ADK 执行器,没有传 profile: 'adk'
正文出现两遍服务端是标准 A2A,却传了 profile: 'adk'

重新生成与编辑后重发

本适配器没有编码标准化的作废请求。是否提供这两个函数,取决于后端如何管理上下文:

服务端怎么做
不按 contextId 记上下文重做就是把输入再发一次,可以直接写 regenerate: run, edit: run
记着上下文,后端提供了作废旧内容的办法在函数里把 targetRunId 带给后端
记着上下文,没有作废旧内容的办法不要给。给了,得到的是一个带着旧上下文的「重新生成」

任务的结束状态

任务状态界面上
completed这一轮完成
failed、rejected失败卡片
canceled、input-required、auth-required这一轮中断,不算失败

流中途断开

连接状态与执行结果分开:断线或缺少终态只表示结果未知。给了 history 时刷新消息与轮次结论;有已收到的游标与 resume 时续流。恢复不会重新调用 run。停止后仍接收输出,远端受理取消不等于已经停止。详见断线恢复与停止确认。

历史里的顺序

任务快照不记过程消息和产出之间的先后。历史里过程消息在前、产出在后。 一轮里正文和工具调用交替出现时,刷新后的顺序可能和当时看到的不一样。

错误

情况界面上排查信息
run 返回非 2xx发送失败cause 里有 capability: 'run' 和状态码
流里有带 error 的 JSON-RPC 响应失败卡片,错误码是 error.codecause 里是整个 error 对象
连接中断或请求抛错执行结果未知可查询或续流,不自动重发
history、sessions.* 返回非 2xx调用失败;已有内容保留,初次加载可能为空cause 里有是哪个函数和状态码

会话业务

会话目录、创建、删除、改名和 fork 由应用调用业务 API,取得真实 sessionId 后传给 useChatSessions。Adapter 不提供 sessions 配置。历史仍由 history 能力函数转换。详见会话契约。

事件形状与状态边界

HTTP run/resume 的 SSE data 通常是 JSON-RPC 信封;异步序列的 data 可直接给 result 内的 A2A 事件,也支持保留已有信封。直接流和独立订阅、本地发送和被动接收都使用同一个解码入口,不会二次包装。事件带 contextId 时必须与当前 sessionId 一致;错会话事件不写入消息或恢复游标。见订阅接入边界。

完成由任务 completed 状态确定;final: true 本身只关闭订阅读取器,不把 working 转成完成。SDK 也接受直接 HTTP 流的 [DONE] 作为完成标记,这属于兼容行为,不能据此要求所有 A2A 服务发送。input-required / auth-required 在 ChatKit 映射为 interrupted;这不表示后端任务永久终止,也不自动提供审批或补充输入工作流。

本适配器的 history 将未识别或缺失的任务状态也作为非终态候选,可能选择它为 activeRun。后端应返回准确状态,未知任务不应附带可恢复游标;SDK 不保证它实际仍在执行。

工具展示中的 adk_type、adk_thought 等 metadata 是本适配器识别的扩展,不是通用 A2A 工具调用保证。上述 JSON-RPC 方法、字段和 profile 描述本适配器的行为,不声明兼容所有或最新 A2A 规范版本。