04 对外 API 与事件模型
1. 约定
- 基础路径:
/v1(业务)、/admin/v1(管理,仅后台会话可访问)、/mcp(MCP Gateway,仅 Run Token 可访问)
- 鉴权:
Authorization: Bearer <api-key>;每个上层平台一个 Key,不区分使用者
- 浏览器中使用浏览器令牌(
bt_…,ADR-0019):平台后端用 API Key 调 POST /v1/browser-tokens { ttlSeconds? } 换取(默认 1 小时,最长 12 小时),权限与签发它的 Key 相同,但不能再签发令牌;停用或删除 Key 即同时作废
- 跨域:只对
/v1 开放给后台"设置 › 安全 › CORS 来源"中的地址,不携带 Cookie;/admin/v1 与 /mcp 不开放
- SDK 包:
GET /sdk/index.json 列出当前版本,GET /sdk/<file>.tgz 下载(公开,不含密钥,见 docs/integration.md)
- 格式:JSON;时间为 ISO 8601;ID 为带前缀的 ULID(
ses_、run_、itx_、file_、prf_)
- 错误:
{ "error": { "code": "session_busy", "message": "...", "details": {} } }
2. 资源接口
会话
| 方法 |
路径 |
说明 |
| POST |
/v1/sessions |
创建:{ profileId?, model?, title?, systemPromptAppend? }。Profile 必须是该 API Key 可用的(省略时用其默认 Profile,否则 403 profile_not_allowed);模型可自选(限可选模型);不能指定引擎和权限模式,都由 Profile 决定(ADR-0015) |
| GET |
/v1/sessions?cursor=&limit= |
当前 API Key 的会话列表(?scope=all 可列出所有 Key 的会话,需要 Key 具备该权限) |
| GET |
/v1/sessions/:id |
详情:元数据 + 消息摘要 + 未决交互 |
| PATCH |
/v1/sessions/:id |
改标题、归档 |
| DELETE |
/v1/sessions/:id |
删除(含工作区) |
| GET |
/v1/sessions/:id/events?after=<seq> |
拉取历史事件(用于渲染完整时间线) |
消息与 Run
| 方法 |
路径 |
说明 |
| POST |
/v1/sessions/:id/messages |
{ text, attachments?: fileId[] } → 202 { runId };会话忙时返回 409 session_busy |
| GET |
/v1/sessions/:id/stream |
SSE:订阅会话全部事件(推荐,一个会话一条连接) |
| GET |
/v1/runs/:id |
Run 状态与用量 |
| GET |
/v1/usage?days=30 |
本平台(调用者的 API Key)最近 N 天的用量:每日明细、按模型汇总 |
| POST |
/v1/browser-tokens |
平台后端用 API Key 签发浏览器令牌:{ ttlSeconds? } → 201 { token, expiresAt } |
| POST |
/v1/runs/:id/interrupt |
中断 |
交互
| 方法 |
路径 |
说明 |
| GET |
/v1/interactions?status=pending |
未决交互 |
| POST |
/v1/interactions/:id |
回复(见 §4) |
文件
| 方法 |
路径 |
说明 |
| POST |
/v1/sessions/:id/files |
multipart 上传(数据文件 / 截图),返回 fileId;写入工作区 uploads/ |
| GET |
/v1/sessions/:id/files |
列出工作区文件树(uploads / outputs) |
| GET |
/v1/files/:fileId |
下载;?inline=1 用于预览 |
| GET |
/v1/files/:fileId/preview |
预览渲染:图片缩略图、CSV/xlsx 前 N 行、docx/pdf 转 PDF/HTML |
其他
| 方法 |
路径 |
说明 |
| GET |
/v1/profiles |
当前 API Key 可用的 Profile(isDefault 为该平台的默认 Profile) |
| GET |
/v1/providers |
引擎可用性与模型列表 |
3. 统一事件模型
所有事件共享信封:
interface AgentEventEnvelope<T extends string, P> {
seq: number // 会话内单调递增;SSE 的 id
type: T
sessionId: string
runId: string
ts: string
payload: P
}
| type |
payload |
说明 |
run.queued |
{ position } |
排队中 |
run.started |
{ provider, model } |
|
message.delta |
{ messageId, text } |
助手文本增量 |
message.completed |
{ messageId, text } |
助手一段完整文本 |
reasoning.delta |
{ blockId, text } |
思考过程(前端默认折叠) |
tool.started |
{ toolCallId, name, title, input } |
title 是适配层生成的人类可读摘要,如 Read sales.xlsx |
tool.progress |
{ toolCallId, output } |
命令输出增量(可选) |
tool.completed |
{ toolCallId, status: 'ok'\|'error'\|'denied', output, truncated } |
|
file.changed |
{ fileId, path, kind: 'created'\|'modified', size, mime } |
工作区产出文件 |
interaction.required |
见 §4 |
需人工介入,Run 进入 waiting_input |
interaction.resolved |
{ interactionId, resolution, by } |
已回复/超时/取消 |
usage |
{ inputTokens, outputTokens, cacheReadTokens, costUsd? } |
订阅登录下 costUsd 为估算值 |
run.completed |
{ durationMs } |
|
run.failed |
{ code, message } |
|
run.interrupted |
{ reason: 'user'\|'restart'\|'timeout' } |
|
用户消息也写入事件流(user.message,payload 含文本与附件),以便事件流单独就能完整重建时间线。
4. 交互协议
// interaction.required payload
type InteractionRequest =
| { interactionId: string; kind: 'approval'
tool: string; title: string; input: unknown
detail?: { command?: string; diff?: string; url?: string; connector?: string }
options: Array<'allow_once' | 'allow_session' | 'deny'>
expiresAt: string }
| { interactionId: string; kind: 'question'
questions: Array<{ id: string; question: string; header?: string
multiSelect: boolean; options: { label: string; description?: string }[] }>
expiresAt: string }
// POST /v1/interactions/:id
type InteractionResponse =
| { decision: 'allow_once' | 'allow_session' }
| { decision: 'deny'; reason?: string } // reason 回传给 Agent
| { answers: Record<string, string | string[]> }
- 同一交互只接受第一次回复,之后的回复返回
409 already_resolved。
- 多个前端同时打开同一会话时,都会收到
interaction.resolved,用于同步关闭卡片。
- 超时默认按
deny 处理(reason: "timeout")。
5. SSE 细节
GET /v1/sessions/:id/stream,响应头 Content-Type: text/event-stream,每条 id: <seq>、event: <type>、data: <json>
- 重连时携带
Last-Event-ID,服务端补发 seq > lastId 的事件
- 每 15s 发一次
: ping 注释帧保活
- 浏览器原生
EventSource 无法设置 Authorization 头,因此 @agent-server/client 使用 fetch + ReadableStream 实现 SSE(支持自定义头和断线续传)
6. 客户端 SDK
完整的接入步骤与示例见 docs/integration.md。
import { AgentClient, browserTokenProvider } from '@agent-server/client'
import { AgentChat, useChatSession } from '@agent-server/chat-ui'
// 浏览器:令牌由平台后端换取,过期前自动刷新,401 时刷新重试一次
const client = new AgentClient({
baseUrl: 'https://agent.example.com',
getToken: browserTokenProvider(() => fetch('/api/agent-token', { method: 'POST' }).then(r => r.json())),
})
const s = await client.sessions.create({ profileId: 'prf_data_analyst' })
const chat = useChatSession(client, s.id) // 拉历史 + 订阅 SSE,交给 <AgentChat :client :session="chat">