ADK

ADK(Agent Development Kit)是用来开发和部署 Agent 的框架,它的服务端通过 run_sse 接口以 SSE 事件流返回 Agent 的执行过程。

这一页说明怎么接一个提供 ADK run_sse 接口的后端:每个函数收到什么、要返回什么,以及这个协议要注意的地方。

结论

  • 本适配器编码 run_sse 请求,解析 ADK 事件、会话详情和会话数组。实际部署是否提供对应接口,需要逐项确认。
  • 停止、续接、插话、重新生成、编辑后重发、改名的示例采用接入方扩展,后端须按后端约定另外提供。
  • 请求内容里没有应用名和用户 ID,由接入方在函数里补。
  • 发起对话之前会话要已经存在。
能力示例接入方式给函数之前要确认
run直接映射示例接口—
history直接映射示例接口—
abort接入方扩展后端提供了按轮次编号停止的接口
resume接入方扩展后端支持只带 lastEventId 的 run_sse
steer接入方扩展后端提供了给进行中的一轮追加输入的接口
regenerate / edit接入方扩展后端能作废指定的一轮和它之后的内容

最简接入

只给 run,直连本机的服务:

adapter.ts
import { createAdkAdapter } from '@volcengine/chatkit/adapters/adk';

// 直连本机的 ADK 服务,只用于本地开发
const BASE = 'http://localhost:8000';
const APP = 'my-agent';
const USER = 'dev';
const post = (path: string, body: unknown) =>
  fetch(`${BASE}${path}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });

export const adapter = createAdkAdapter({
  run: async ({ sessionId, body }) => {
    // 请求内容里没有应用名和用户 ID,在这里补
    const send = () => post('/run_sse', { appName: APP, userId: USER, ...body });
    const response = await send();
    if (response.status !== 404) return response;
    // 会话还不存在:用这个会话编号建一个,再发一次
    await post(`/apps/${APP}/users/${USER}/sessions/${encodeURIComponent(sessionId)}`, {});
    return send();
  },
});
有什么没有什么
多轮对话会话列表
流式显示、工具调用历史:刷新后界面清空(后端的会话还在)
停止(取消请求)重新生成、编辑后重发

会话要先存在

适配器不替接入方建会话。两种做法:

做法怎么写
不给在 run 里自己保证:后端回 404 时用这个会话编号建一个,再发一次

上面的代码演示第二种,但仅按 404 判断且没有检查建会话响应。接入生产时应先确认 404 原因、检查创建成功并处理并发创建,不能把任意 404 都当作会话不存在。后端对不存在的会话返回什么、能不能用指定的编号建会话,以后端的实际行为为准, 确认办法见后端约定。

走自己的代理

生产环境的请求发给自己的服务:

adapter.ts
import { createAdkAdapter } from '@volcengine/chatkit/adapters/adk';

// 请求发给自己的服务。应用名、用户 ID、凭据都由它在服务端补,浏览器里没有
const send = (method: string, path: string, body?: unknown) =>
  fetch(`/api/chat${path}`, {
    method,
    credentials: 'include', // 带上登录态,服务端据此知道用户是谁
    headers: { 'Content-Type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
const at = (sessionId: string) => `/sessions/${encodeURIComponent(sessionId)}`;

export const adapter = createAdkAdapter({
  run: ({ body }) => send('POST', '/run', body),
  history: ({ sessionId }) => send('GET', at(sessionId)),
});

代理要做的事:

收到的请求转发到要补的
POST /runPOST /run_sse应用名、用户 ID
GET /sessionsGET /apps/{应用}/users/{用户}/sessions地址里的应用名、用户 ID
POST /sessionsPOST /apps/{应用}/users/{用户}/sessions同上
GET /sessions/{id}GET /apps/{应用}/users/{用户}/sessions/{id}同上
DELETE /sessions/{id}DELETE /apps/{应用}/users/{用户}/sessions/{id}同上
要点说明
用户 ID 从登录态取不要让前端传。前端传的用户 ID 可以被改成别人的
事件流原样透传不要缓冲。网关或代理把流攒起来再发,界面上就没有流式效果
状态码原样透传后端返回 404、500 时代理也返回同样的状态码,前端靠它判断成功与失败
连接断开要传到上游用户点停止时前端会断开请求,代理要跟着断开和上游的连接

全部能力

adapter.ts
import { createAdkAdapter } from '@volcengine/chatkit/adapters/adk';

const BASE = '/api/agent';
const APP = 'my-agent';
const USER = 'dev';
const SESSIONS = `/apps/${APP}/users/${USER}/sessions`;
const send = (method: string, path: string, body?: unknown) =>
  fetch(`${BASE}${path}`, {
    method,
    headers: { 'Content-Type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
const at = (sessionId: string) => `${SESSIONS}/${encodeURIComponent(sessionId)}`;

export const adapter = createAdkAdapter({
  // 以下三组是标准 ADK 服务就有的接口
  run: ({ body }) => send('POST', '/run_sse', { appName: APP, userId: USER, ...body }),
  history: ({ sessionId }) => send('GET', at(sessionId)),
  resume: ({ body }) => send('POST', '/run_sse', { appName: APP, userId: USER, ...body }),
  abort: ({ sessionId, runId }) => send('POST', `${at(sessionId)}/runs/${runId}/cancel`, {}),
  steer: ({ sessionId, runId, body }) =>
    send('POST', `${at(sessionId)}/runs/${runId}/steer`, { appName: APP, userId: USER, ...body }),
  regenerate: ({ targetRunId, body }) =>
    send('POST', '/run_sse', { appName: APP, userId: USER, rewindTo: targetRunId, ...body }),
  edit: ({ targetRunId, body }) =>
    send('POST', '/run_sse', { appName: APP, userId: USER, rewindTo: targetRunId, ...body }),
});

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

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

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

函数请求上的字段请求内容 body要返回的响应
runsessionId、runId、body{ sessionId, streaming: true, newMessage, stateDelta? }事件流
regenerate / edit同 run,多一个 targetRunId同 run事件流
resumesessionId、runId、eventId、body{ sessionId, streaming: true, lastEventId },不带 newMessage事件流
steersessionId、runId、body同 run只看状态码
abortsessionId、runId没有只看状态码
historysessionId没有会话详情,里面带 events

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

编号

编号取自说明
会话编号sessionId就是 ADK 的 sessionId
轮次编号事件的 invocationId流里出现过之后,abort、resume、steer 收到的 runId 就是它。还没出现过时是前端生成的那个
事件编号事件的 idresume 收到的 eventId 和 body.lastEventId 是同一个值

业务状态与内容扩展

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

输入怎么放进请求

输入里的放在哪
正文 textnewMessage.parts 里的文字
附件newMessage.parts 里的文件。附件类型不限
引用stateDelta.references。Agent 里从会话状态的 references 读
metadata不进请求,只挂在界面上的用户消息上

后端约定了别的字段时用 transformInput 改。它拿到默认的请求内容,返回要发出去的请求内容:

adapter.ts
import { createAdkAdapter } from '@volcengine/chatkit/adapters/adk';

export const adapter = createAdkAdapter({
  run: ({ body }) =>
    fetch('/api/chat/run', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    }),
  // 默认协议编码完成后,返回完整请求体。
  transformInput: (input, { defaultInput }) => ({
    ...defaultInput,
    stateDelta: { ...defaultInput.stateDelta, tenant: input?.metadata?.tenant },
  }),
});

注意点

当前历史解析的限制

history 必须返回包含 events 数组的会话对象;空历史使用 events: []。非法 JSON、缺少 events 或错误类型会按历史请求失败处理,保留界面中原有的内容。

当前 ADK history 只重建事件内容和可选的会话摘要、active_run,不输出 runSnapshots;即使 events 含错误,也不会重建可操作的历史终态轮次。因此:

影响说明
刷新后历史里的轮次不能重新生成、不能编辑这个页面里跑过的轮次不受影响
自动续接需要明确活动轮次需要顶层 active_run: { run_id, event_id },仅提供 resume 或尾部事件不足以触发

多副本部署的会话亲和

多副本部署靠响应头把同一个会话的请求送到同一个副本时,适配器不处理这个响应头。在自己的函数里记下并带回:

adapter.ts
import { createAdkAdapter } from '@volcengine/chatkit/adapters/adk';

// 多副本部署靠响应头里的会话亲和标识把同一个会话的请求送到同一个副本时:
// 读响应头、按会话记下、下次请求写回
const affinity = new Map<string, string>();

const withAffinity = async (
  sessionId: string,
  send: (headers: Record<string, string>) => Promise<Response>
): Promise<Response> => {
  const known = affinity.get(sessionId);
  const response = await send(known ? { 'X-Session-Id': known } : {});
  const fresh = response.headers.get('X-Session-Id');
  if (fresh) affinity.set(sessionId, fresh);
  return response;
};

export const adapter = createAdkAdapter({
  run: ({ sessionId, body }) =>
    withAffinity(sessionId, headers =>
      fetch('/api/chat/run', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...headers },
        body: JSON.stringify(body),
      })
    ),
});

流中途断开

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

错误

情况界面上排查信息
run 返回非 2xx发送失败,显示后端给的错误文字cause 里有 capability: 'run' 和状态码
流里有带 errorCode 的事件失败卡片错误码就是 errorCode
连接中断或请求抛错执行结果未知可查询或续流,不自动重发
history、sessions.* 返回非 2xx调用失败;已有内容不因此清空,初次加载可能仍为空cause 里有是哪个函数和状态码

会话业务

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

SDK 识别的结束与扩展

当前读取器把 data: [DONE]、turnComplete: true 或 type: run.completed 视为完成;普通 EOF 不会完成。这里描述的是 SDK 识别规则,不保证 ADK 官方服务会发送这些标记。部署若仅靠 EOF 结束,需由有可靠执行结论的服务补标记,不能把意外断线转换成完成。errorCode、旧式 error 是失败证据;SSE event: error 被当作连接中断。

直接流用事件 data.id 作为协议游标,SSE id 不代替缺失的 ADK 事件 id;transformOutput.context.eventId 则取 SSE id,可能为空或与协议游标不同。普通事件的 invocationId 还用于映射后端轮次;targetRunId 也会使用已知的后端编号。

输入附件的 MIME 不被适配器限制,但后端支持范围另行确认。会话列表和历史都不分页。列表标题读 state.title;history 的会话摘要由公共解析器读取顶层 session 或顶层字段,不会自动把 state.title 作为摘要标题。

示例地址和“标准服务”注释仅作为接入模板,不能视为所有 ADK 版本的接口与结束行为保证。