# 04 对外 API 与事件模型 ## 1. 约定 - 基础路径:`/v1`(业务)、`/admin/v1`(管理,仅后台会话可访问)、`/mcp`(MCP Gateway,仅 Run Token 可访问) - 鉴权:`Authorization: Bearer `;每个上层平台一个 Key,不区分使用者 - 格式: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=` | 拉取历史事件(用于渲染完整时间线) | ### 消息与 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 状态与用量 | | 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. 统一事件模型 所有事件共享信封: ```ts interface AgentEventEnvelope { 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. 交互协议 ```ts // 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 } ``` - 同一交互只接受第一次回复,之后的回复返回 `409 already_resolved`。 - 多个前端同时打开同一会话时,都会收到 `interaction.resolved`,用于同步关闭卡片。 - 超时默认按 `deny` 处理(`reason: "timeout"`)。 ## 5. SSE 细节 - `GET /v1/sessions/:id/stream`,响应头 `Content-Type: text/event-stream`,每条 `id: `、`event: `、`data: ` - 重连时携带 `Last-Event-ID`,服务端补发 `seq > lastId` 的事件 - 每 15s 发一次 `: ping` 注释帧保活 - 浏览器原生 `EventSource` 无法设置 Authorization 头,因此 `@agent-server/client` 使用 `fetch` + `ReadableStream` 实现 SSE(支持自定义头和断线续传) ## 6. 客户端 SDK 示意 ```ts import { AgentClient } from '@agent-server/client' const client = new AgentClient({ baseUrl, apiKey }) const s = await client.sessions.create({ profileId: 'prf_data_analyst' }) const file = await s.upload(screenshotBlob) s.on('event', ev => store.apply(ev)) // 交给 chat-ui 的 store s.on('interaction', itx => ui.prompt(itx)) await s.send({ text: '分析这张图和附件里的销售数据', attachments: [file.id] }) await client.interactions.reply(itxId, { decision: 'allow_once' }) ```