# CMDC Gateway API 参考

> 所有端点的完整请求/响应规范。Base URL 示例: `http://localhost:4000`

---

## 目录

- [认证](#认证)
- [通用错误格式](#通用错误格式)
- [健康检查](#健康检查)
- [Session 管理](#session-管理)
  - [创建 Session](#创建-session)
  - [查询 Session](#查询-session)
  - [删除 Session](#删除-session)
- [Prompt API](#prompt-api)
- [事件流](#事件流)
  - [SSE 事件流](#sse-事件流)
  - [SSE 事件类型详解](#sse-事件类型详解)
  - [WebSocket 双向通信](#websocket-双向通信)
- [控制 API](#控制-api)
  - [审批通过](#审批通过)
  - [审批拒绝](#审批拒绝)
  - [回答 Agent 提问](#回答-agent-提问)
- [统计与历史](#统计与历史)
  - [用量统计](#用量统计)
  - [对话历史](#对话历史)
- [回调工具注册](#回调工具注册)

---

## 认证

所有 `/v1/*` 端点需要 API Key 认证。支持两种方式（二选一）：

| 方式 | 请求头 | 示例 |
|------|--------|------|
| X-API-Key 头 | `X-API-Key` | `X-API-Key: sk-abc123` |
| Bearer Token | `Authorization` | `Authorization: Bearer sk-abc123` |

API Key 在服务端配置中绑定 `tenant_id`，用于多租户隔离：

```elixir
config :cmdc_gateway, CMDCGateway.Plugs.Auth,
  api_keys: %{
    "sk-abc123" => "tenant-a",
    "sk-xyz789" => "tenant-b"
  }
```

**认证失败响应：**

```
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "error": "unauthorized",
  "message": "Missing or invalid API key"
}
```

> `/healthz` 端点无需认证。

---

## 通用错误格式

所有错误响应遵循统一 JSON 结构：

```json
{
  "error": "<error_code>",
  "message": "<人类可读描述>"
}
```

| HTTP 状态码 | error 代码 | 触发条件 |
|-------------|-----------|---------|
| 400 | `bad_request` | 缺少必要参数（如 prompt 的 `text`、approve 的 `approvalId`） |
| 401 | `unauthorized` | API Key 无效、缺失、或未配置 |
| 404 | `not_found` | Session ID 不存在 |
| 404 | `session_dead` | Session 存在但 Agent 进程已终止 |
| 422 | `create_failed` | Agent 创建失败（模型不支持、参数非法等） |
| 422 | `registration_failed` | 回调工具注册失败 |
| 429 | `rate_limited` | 超出请求频率限制 |

**429 特殊响应头：**

```
HTTP/1.1 429 Too Many Requests
Retry-After: 15
Content-Type: application/json

{
  "error": "rate_limited",
  "message": "Rate limit exceeded. Retry after 15 seconds.",
  "retryAfter": 15
}
```

**404 路由不匹配：**

```json
{
  "error": "not_found",
  "message": "No route matches GET /v1/unknown"
}
```

---

## 健康检查

检查 Gateway 服务状态，无需认证。

### `GET /healthz`

**请求：**

```
GET /healthz HTTP/1.1
Host: localhost:4000
```

无请求体、无认证。

**响应 200：**

```json
{
  "status": "ok",
  "version": "0.1.0",
  "sessions": {
    "active": 5
  },
  "meter": {
    "tracked_keys": 3
  },
  "timestamp": "2026-04-08T12:00:00.000000Z"
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `status` | string | 固定 `"ok"` |
| `version` | string | Gateway 版本号 |
| `sessions.active` | integer | 当前存活的 Session 数量 |
| `meter.tracked_keys` | integer | 有用量记录的 API Key 数量 |
| `timestamp` | string | ISO 8601 UTC 时间戳 |

---

## Session 管理

### 创建 Session

创建一个 CMDC Agent Session，Agent 进程随即启动并进入 `idle` 状态。

### `POST /v1/sessions`

**请求头：**

```
Content-Type: application/json
X-API-Key: sk-abc123
```

**请求体：**

```json
{
  "model": "deepseek:deepseek-chat",
  "sessionId": "my-session-001",
  "systemPrompt": "你是一个专业的编程助手",
  "workingDir": "/home/user/project",
  "tools": ["CMDC.Tool.Shell", "CMDC.Tool.ReadFile", "CMDC.Tool.WriteFile"],
  "plugins": ["CMDC.Plugin.Builtin.ApprovalGuard"],
  "blueprint": "CMDC.Blueprint.Base",
  "maxTurns": 50,
  "maxTokens": 4096,
  "skillsDirs": ["/home/user/skills"],
  "providerOpts": {
    "temperature": 0.7,
    "top_p": 0.95
  }
}
```

**请求体字段：**

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `model` | string | **是** | — | LLM 模型标识。格式 `provider:model`，如 `"deepseek:deepseek-chat"`、`"anthropic:claude-sonnet-4-20250514"` |
| `sessionId` | string | 否 | 自动生成 16 位 hex | 自定义 Session ID，用于后续所有操作的标识符 |
| `systemPrompt` | string | 否 | Blueprint 默认值 | Agent 系统提示词，决定 Agent 的行为和人格 |
| `workingDir` | string | 否 | `"."` | 工具（Shell、ReadFile 等）的工作根目录 |
| `tools` | string[] | 否 | `[]` | 启用的 CMDC Tool 模块名列表，使用 Elixir 模块全名 |
| `plugins` | string[] | 否 | `[]` | 启用的 CMDC Plugin 模块名列表 |
| `blueprint` | string | 否 | `nil` | Blueprint 模块名，定义 Agent 的完整配置模板 |
| `maxTurns` | integer | 否 | `100` | 最大 Agent 轮次数（一次 prompt → response 为一轮） |
| `maxTokens` | integer | 否 | 模型默认 | LLM 单次回复最大输出 token 数 |
| `skillsDirs` | string[] | 否 | `[]` | Skill 文件扫描目录列表 |
| `providerOpts` | object | 否 | `{}` | 透传给 LLM Provider 的额外参数（如 `temperature`、`top_p`） |

**响应 201 Created：**

```json
{
  "sessionId": "my-session-001",
  "status": "created"
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `sessionId` | string | Session 标识符（自定义或自动生成） |
| `status` | string | 固定 `"created"` |

**响应 422 Unprocessable Entity：**

```json
{
  "error": "create_failed",
  "message": ":unknown_provider"
}
```

**cURL 示例：**

```bash
curl -X POST http://localhost:4000/v1/sessions \
  -H "X-API-Key: sk-abc123" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek:deepseek-chat",
    "systemPrompt": "你是助手",
    "tools": ["CMDC.Tool.Shell"],
    "maxTurns": 20
  }'
```

---

### 查询 Session

获取 Session 的当前运行状态。

### `GET /v1/sessions/:id`

**路径参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `id` | string | Session ID |

**请求：**

```
GET /v1/sessions/my-session-001 HTTP/1.1
X-API-Key: sk-abc123
```

**响应 200：**

```json
{
  "sessionId": "my-session-001",
  "state": "idle",
  "turns": 3,
  "toolCalls": 5,
  "totalTokens": 12800,
  "uptimeMs": 45200
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `sessionId` | string | Session ID |
| `state` | string | Agent 状态机当前状态：`"idle"` / `"working"` / `"waiting_approval"` / `"waiting_input"` |
| `turns` | integer | 已完成的对话轮次数 |
| `toolCalls` | integer | 累计工具调用次数 |
| `totalTokens` | integer | 累计消耗 token 数 |
| `uptimeMs` | integer | Session 存活时间（毫秒） |

**响应 404：**

```json
{
  "error": "not_found",
  "message": "Session my-session-001 not found"
}
```

---

### 删除 Session

停止 Agent 进程，清理 Session 数据和已注册的回调工具。

### `DELETE /v1/sessions/:id`

**路径参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `id` | string | Session ID |

**请求：**

```
DELETE /v1/sessions/my-session-001 HTTP/1.1
X-API-Key: sk-abc123
```

**响应 200：**

```json
{
  "sessionId": "my-session-001",
  "status": "deleted"
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `sessionId` | string | 被删除的 Session ID |
| `status` | string | 固定 `"deleted"` |

**副作用：**

1. Agent 进程被 `CMDC.stop/1` 终止
2. `SessionStore` 中的条目被删除
3. 该 Session 注册的所有 `CallbackTool` 被清理
4. 活跃的 SSE/WebSocket 连接收到最终事件后断开

---

## Prompt API

向 Agent 发送用户消息（异步）。消息被投递到 Agent 状态机后立即返回 202，后续处理结果通过 SSE 或 WebSocket 推送。

### `POST /v1/sessions/:id/prompt`

**路径参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `id` | string | Session ID |

**请求头：**

```
Content-Type: application/json
X-API-Key: sk-abc123
```

**请求体：**

```json
{
  "text": "帮我用 Python 写一个快速排序"
}
```

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `text` | string | **是** | 用户消息文本。也接受 `"prompt"` 作为别名字段 |

**响应 202 Accepted：**

```json
{
  "requestId": "a1b2c3d4e5f67890",
  "sessionId": "my-session-001",
  "queued": false
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `requestId` | string | 本次请求的唯一 ID（16 位 hex） |
| `sessionId` | string | Session ID |
| `queued` | boolean | 是否排队等待（Agent 正忙时为 `true`） |

**响应 400：**

```json
{
  "error": "bad_request",
  "message": "Missing 'text' field"
}
```

**典型调用流程：**

```
1. POST /v1/sessions/:id/prompt  →  202 (消息已投递)
2. GET  /v1/sessions/:id/events  →  SSE 流接收处理事件
   - event: agent_start
   - event: message_delta (多次，逐 token)
   - event: tool_execution_start (如果 Agent 调用了工具)
   - event: tool_execution_end
   - event: agent_end (本轮完成)
```

**cURL 示例：**

```bash
curl -X POST http://localhost:4000/v1/sessions/my-session-001/prompt \
  -H "X-API-Key: sk-abc123" \
  -H "Content-Type: application/json" \
  -d '{"text": "帮我用 Python 写一个快速排序"}'
```

---

## 事件流

### SSE 事件流

建立 Server-Sent Events 长连接，实时接收 Agent 事件推送。

### `GET /v1/sessions/:id/events`

**路径参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `id` | string | Session ID |

**请求：**

```
GET /v1/sessions/my-session-001/events HTTP/1.1
X-API-Key: sk-abc123
Accept: text/event-stream
```

**响应头：**

```
HTTP/1.1 200 OK
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache
X-Accel-Buffering: no
Transfer-Encoding: chunked
```

**SSE 数据格式：**

每个事件由 `event:` 行（事件类型）和 `data:` 行（JSON 载荷）组成，以双换行分隔：

```
event: agent_start
data: {}

event: message_delta
data: {"delta":"你"}

event: message_delta
data: {"delta":"好"}

event: message_delta
data: {"delta":"！"}

event: agent_end
data: {"messageCount":2,"lastMessage":{"content":"你好！","role":"assistant"},"tokenUsage":{"promptTokens":50,"completionTokens":5,"totalTokens":55}}

```

**心跳：**

每 30 秒发送一次 SSE 注释行，保持连接：

```
: heartbeat

```

**连接终止条件：**

- 客户端主动断开
- 收到 `agent_end` 或 `agent_abort` 事件后 SSE 流自动关闭
- 60 秒无事件超时自动断开

**cURL 示例：**

```bash
curl -N http://localhost:4000/v1/sessions/my-session-001/events \
  -H "X-API-Key: sk-abc123"
```

---

### SSE 事件类型详解

共 15 种对外事件类型，按 Agent 生命周期阶段分组：

#### 1. 会话生命周期

**`agent_start`** — Agent 开始处理当前 prompt

```
event: agent_start
data: {}
```

| data 字段 | 类型 | 说明 |
|-----------|------|------|
| （空对象） | — | 无额外数据 |

---

**`agent_end`** — Agent 完成当前 prompt 处理

```
event: agent_end
data: {"messageCount":4,"lastMessage":{"content":"代码已完成","role":"assistant"},"tokenUsage":{"promptTokens":500,"completionTokens":200,"totalTokens":700}}
```

| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `messageCount` | integer | 当前对话总消息数 |
| `lastMessage` | object \| null | 最后一条 assistant 消息，含 `content`(string) 和 `role`(string) |
| `tokenUsage` | object | Token 用量统计 |
| `tokenUsage.promptTokens` | integer | 输入 token 数 |
| `tokenUsage.completionTokens` | integer | 输出 token 数 |
| `tokenUsage.totalTokens` | integer | 总 token 数 |

---

**`agent_abort`** — Agent 被中止

```
event: agent_abort
data: {"reason":"max_turns_exceeded"}
```

| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `reason` | string | 中止原因，如 `"max_turns_exceeded"`、`"user_cancelled"`、`"aborted"` |

---

**`prompt_received`** — Agent 确认收到用户 prompt

```
event: prompt_received
data: {"text":"帮我写个排序算法"}
```

| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `text` | string | 用户发送的 prompt 文本 |

---

#### 2. 流式响应

**`message_start`** — LLM 开始生成回复

```
event: message_start
data: {}
```

| data 字段 | 类型 | 说明 |
|-----------|------|------|
| （空对象） | — | 无额外数据 |

---

**`message_delta`** — 流式文本片段（逐 token）

```
event: message_delta
data: {"delta":"def quick_sort"}
```

| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `delta` | string | 文本增量片段，客户端应追加到当前消息 |

> 一次 LLM 回复会产生大量 `message_delta` 事件，客户端应逐个追加拼接完整回复。

---

**`thinking_start`** — Agent 思考链（Chain-of-Thought）开始

```
event: thinking_start
data: {}
```

| data 字段 | 类型 | 说明 |
|-----------|------|------|
| （空对象） | — | 仅在模型支持 thinking 功能时触发 |

---

**`thinking_delta`** — 思考链文本片段

```
event: thinking_delta
data: {"delta":"让我分析一下这个问题..."}
```

| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `delta` | string | 思考过程的文本增量 |

---

#### 3. 工具执行

**`tool_calls`** — Agent 本轮决定调用的工具数量

```
event: tool_calls
data: {"count":2}
```

| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `count` | integer | 本轮工具调用总数 |

---

**`tool_execution_start`** — 工具开始执行

```
event: tool_execution_start
data: {"toolName":"Shell","callId":"tc_a1b2c3","args":{"command":"python3 sort.py","working_dir":"."}}
```

| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `toolName` | string | 工具名称 |
| `callId` | string | 本次调用的唯一 ID |
| `args` | object | 工具调用参数（key-value 均为 string，超过 1024 字节的值会被截断并追加 `...[truncated]`） |

---

**`tool_execution_end`** — 工具执行完成

```
event: tool_execution_end
data: {"toolName":"Shell","callId":"tc_a1b2c3","status":"ok","result":"[1, 2, 3, 4, 5]"}
```

| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `toolName` | string | 工具名称 |
| `callId` | string | 调用 ID（与 `tool_execution_start` 的 `callId` 对应） |
| `status` | string | 执行状态：`"ok"` 成功 / `"error"` 失败 |
| `result` | string | 执行结果文本（超过 4096 字节会被截断并追加 `...[truncated]`） |

---

#### 4. 人机交互（HITL）

**`approval_required`** — Agent 需要人类审批才能继续

```
event: approval_required
data: {"approvalId":"apr_x7y8z9","toolName":"Shell","args":{"command":"rm -rf /tmp/old"},"hint":"危险操作：删除文件","requestedAt":"2026-04-08T12:00:00Z"}
```

| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `approvalId` | string | 审批请求 ID，用于后续 `approve` / `reject` 操作 |
| `toolName` | string | 待审批的工具名称 |
| `args` | object | 工具调用参数 |
| `hint` | string | 审批提示信息（可能为空字符串） |
| `requestedAt` | string \| null | 审批请求时间（ISO 8601） |

> 收到此事件后，Agent 会暂停等待，直到客户端发送 `approve` 或 `reject`。

---

**`approval_resolved`** — 审批已决定

```
event: approval_resolved
data: {"approvalId":"apr_x7y8z9","status":"approved"}
```

| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `approvalId` | string | 审批请求 ID |
| `status` | string | 审批结果：`"approved"` / `"rejected"` |

---

**`ask_user`** — Agent 向用户提问，等待回答

```
event: ask_user
data: {"ref":"ask_001","question":"你希望使用哪种排序算法？","options":["快速排序","归并排序","堆排序"]}
```

| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `ref` | string | 提问引用 ID，用于后续 `respond` 操作 |
| `question` | string | Agent 的问题文本 |
| `options` | array \| null | 可选的预设选项列表，`null` 表示自由文本回答 |

> 收到此事件后，Agent 会暂停等待，直到客户端发送 `respond`。

---

#### 5. 错误

**`error`** — 运行时错误

```
event: error
data: {"reason":"Provider stream failed: connection timeout"}
```

| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `reason` | string | 错误原因描述 |

---

### WebSocket 双向通信

全双工连接，出站推送事件 + 入站接收控制消息。适合需要实时双向交互的场景（如审批、对话式 Agent）。

### `WS /v1/sessions/:id/ws`

**连接参数：**

| 参数 | 位置 | 类型 | 必填 | 说明 |
|------|------|------|------|------|
| `id` | 路径 | string | 是 | Session ID |
| `api_key` | Query String | string | 是 | API Key（WebSocket 不支持自定义请求头，故用 query 传递） |

**连接 URL 示例：**

```
ws://localhost:4000/v1/sessions/my-session-001/ws?api_key=sk-abc123
```

**连接失败响应：**

| 场景 | HTTP 状态码 | 响应体 |
|------|-----------|--------|
| 未提供 API Key | 401 | `{"error":"unauthorized"}` |
| Session 不存在 | 404 | `{"error":"session_not_found"}` |

**连接配置：**

| 参数 | 值 | 说明 |
|------|---|------|
| idle_timeout | 300,000 ms (5 分钟) | 无消息超时断开 |
| heartbeat | 30 秒 | WebSocket ping 帧保活 |

---

#### 入站消息（客户端 → Gateway）

所有入站消息为 JSON 格式，必须包含 `action` 字段。

**1. 发送 Prompt**

```json
{
  "action": "prompt",
  "text": "帮我写一个 Hello World"
}
```

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `action` | string | 是 | 固定 `"prompt"` |
| `text` | string | 是 | 用户消息文本 |

成功响应：

```json
{"ok": true, "action": "prompt"}
```

---

**2. 审批通过**

```json
{
  "action": "approve",
  "approvalId": "apr_x7y8z9"
}
```

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `action` | string | 是 | 固定 `"approve"` |
| `approvalId` | string | 是 | 来自 `approval_required` 事件的审批 ID |

成功响应：

```json
{"ok": true, "action": "approve"}
```

---

**3. 审批拒绝**

```json
{
  "action": "reject",
  "approvalId": "apr_x7y8z9"
}
```

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `action` | string | 是 | 固定 `"reject"` |
| `approvalId` | string | 是 | 来自 `approval_required` 事件的审批 ID |

成功响应：

```json
{"ok": true, "action": "reject"}
```

---

**4. 回答 Agent 提问**

```json
{
  "action": "respond",
  "ref": "ask_001",
  "response": "快速排序"
}
```

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `action` | string | 是 | 固定 `"respond"` |
| `ref` | string | 是 | 来自 `ask_user` 事件的引用 ID |
| `response` | string | 是 | 用户的回答文本 |

成功响应：

```json
{"ok": true, "action": "respond"}
```

---

**入站错误响应：**

| 场景 | 响应 |
|------|------|
| JSON 解析失败 | `{"error": "invalid_json", "message": "Failed to parse JSON"}` |
| 缺少 `action` 字段 | `{"error": "missing_action", "message": "Message must contain 'action' field"}` |
| 未知 action | `{"error": "unknown_action", "action": "xxx"}` |
| Session 进程已终止 | `{"error": "session_not_found"}` |

---

#### 出站消息（Gateway → 客户端）

与 SSE 事件内容一致，但封装为 JSON 对象：

```json
{
  "event": "<event_type>",
  "data": { ... }
}
```

**示例：**

```json
{"event": "agent_start", "data": {}}
{"event": "message_delta", "data": {"delta": "Hello"}}
{"event": "message_delta", "data": {"delta": " World"}}
{"event": "tool_execution_start", "data": {"toolName": "Shell", "callId": "tc_001", "args": {"command": "echo hi"}}}
{"event": "tool_execution_end", "data": {"toolName": "Shell", "callId": "tc_001", "status": "ok", "result": "hi\n"}}
{"event": "approval_required", "data": {"approvalId": "apr_001", "toolName": "Shell", "args": {"command": "rm -rf /tmp"}, "hint": "危险操作", "requestedAt": null}}
{"event": "agent_end", "data": {"messageCount": 3, "lastMessage": {"content": "完成", "role": "assistant"}, "tokenUsage": {"promptTokens": 100, "completionTokens": 50, "totalTokens": 150}}}
```

**JavaScript 客户端示例：**

```javascript
const ws = new WebSocket('ws://localhost:4000/v1/sessions/my-session-001/ws?api_key=sk-abc123');

ws.onopen = () => {
  ws.send(JSON.stringify({ action: 'prompt', text: '你好' }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  switch (msg.event) {
    case 'message_delta':
      process.stdout.write(msg.data.delta);
      break;
    case 'approval_required':
      // 自动审批示例
      ws.send(JSON.stringify({ action: 'approve', approvalId: msg.data.approvalId }));
      break;
    case 'ask_user':
      ws.send(JSON.stringify({ action: 'respond', ref: msg.data.ref, response: '是的' }));
      break;
    case 'agent_end':
      console.log('\nAgent 完成');
      ws.close();
      break;
  }
};
```

---

## 控制 API

REST 版本的控制接口，适合不使用 WebSocket 的场景（如服务端集成、CLI 工具）。功能与 WebSocket 入站消息等价。

### 审批通过

### `POST /v1/sessions/:id/approve`

**路径参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `id` | string | Session ID |

**请求头：**

```
Content-Type: application/json
X-API-Key: sk-abc123
```

**请求体：**

```json
{
  "approvalId": "apr_x7y8z9"
}
```

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `approvalId` | string | **是** | 来自 `approval_required` 事件的审批 ID |

**响应 200：**

```json
{
  "ok": true,
  "action": "approve",
  "approvalId": "apr_x7y8z9"
}
```

**响应 400：**

```json
{
  "error": "bad_request",
  "message": "Missing 'approvalId'"
}
```

---

### 审批拒绝

### `POST /v1/sessions/:id/reject`

**路径参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `id` | string | Session ID |

**请求头：**

```
Content-Type: application/json
X-API-Key: sk-abc123
```

**请求体：**

```json
{
  "approvalId": "apr_x7y8z9"
}
```

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `approvalId` | string | **是** | 来自 `approval_required` 事件的审批 ID |

**响应 200：**

```json
{
  "ok": true,
  "action": "reject",
  "approvalId": "apr_x7y8z9"
}
```

**响应 400：**

```json
{
  "error": "bad_request",
  "message": "Missing 'approvalId'"
}
```

---

### 回答 Agent 提问

### `POST /v1/sessions/:id/respond`

**路径参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `id` | string | Session ID |

**请求头：**

```
Content-Type: application/json
X-API-Key: sk-abc123
```

**请求体：**

```json
{
  "ref": "ask_001",
  "response": "快速排序"
}
```

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `ref` | string | **是** | 来自 `ask_user` 事件的引用 ID |
| `response` | string | **是** | 用户的回答文本 |

**响应 200：**

```json
{
  "ok": true,
  "action": "respond",
  "ref": "ask_001"
}
```

**响应 400：**

```json
{
  "error": "bad_request",
  "message": "Missing 'ref' or 'response'"
}
```

---

## 统计与历史

### 用量统计

获取 Session 运行状态 + API Key 维度的用量计量数据。

### `GET /v1/sessions/:id/stats`

**路径参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `id` | string | Session ID |

**请求：**

```
GET /v1/sessions/my-session-001/stats HTTP/1.1
X-API-Key: sk-abc123
```

**响应 200：**

```json
{
  "sessionId": "my-session-001",
  "state": "idle",
  "turns": 5,
  "toolCalls": 12,
  "totalTokens": 25600,
  "uptimeMs": 120000,
  "meter": {
    "promptCount": 5,
    "totalPromptTokens": 15000,
    "totalCompletionTokens": 10600,
    "totalTokens": 25600
  }
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `sessionId` | string | Session ID |
| `state` | string | Agent 状态机当前状态 |
| `turns` | integer | 已完成的对话轮次数 |
| `toolCalls` | integer | 累计工具调用次数 |
| `totalTokens` | integer | 累计消耗 token 数（Agent 侧统计） |
| `uptimeMs` | integer | Session 存活时间（毫秒） |
| `meter.promptCount` | integer | 该 API Key 发送的 prompt 次数（跨 Session 累计） |
| `meter.totalPromptTokens` | integer | 该 API Key 的累计输入 token 数 |
| `meter.totalCompletionTokens` | integer | 该 API Key 的累计输出 token 数 |
| `meter.totalTokens` | integer | 该 API Key 的累计总 token 数 |

> `meter` 数据是 API Key 维度的全局统计，不仅限于当前 Session。

---

### 对话历史

获取 Session 的完整消息列表。

### `GET /v1/sessions/:id/messages`

**路径参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `id` | string | Session ID |

**请求：**

```
GET /v1/sessions/my-session-001/messages HTTP/1.1
X-API-Key: sk-abc123
```

**响应 200：**

```json
{
  "sessionId": "my-session-001",
  "messages": [
    {
      "id": "msg_001",
      "role": "system",
      "content": "你是一个专业的编程助手",
      "toolCalls": null,
      "callId": null,
      "name": null,
      "isError": false
    },
    {
      "id": "msg_002",
      "role": "user",
      "content": "帮我用 Python 写一个快速排序",
      "toolCalls": null,
      "callId": null,
      "name": null,
      "isError": false
    },
    {
      "id": "msg_003",
      "role": "assistant",
      "content": "好的，这是一个 Python 快速排序实现：\n\n```python\ndef quick_sort(arr):\n    ...\n```",
      "toolCalls": [
        {
          "id": "tc_001",
          "name": "WriteFile",
          "args": {"path": "sort.py", "content": "def quick_sort(arr): ..."}
        }
      ],
      "callId": null,
      "name": null,
      "isError": false
    },
    {
      "id": "msg_004",
      "role": "tool",
      "content": "File written successfully",
      "toolCalls": null,
      "callId": "tc_001",
      "name": "WriteFile",
      "isError": false
    }
  ]
}
```

**Message 对象字段：**

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 消息唯一 ID |
| `role` | string | 角色：`"system"` / `"user"` / `"assistant"` / `"tool"` |
| `content` | string \| null | 消息文本内容 |
| `toolCalls` | array \| null | 仅 `assistant` 消息可能包含，工具调用列表 |
| `toolCalls[].id` | string | 工具调用 ID |
| `toolCalls[].name` | string | 工具名称 |
| `toolCalls[].args` | object | 工具调用参数 |
| `callId` | string \| null | 仅 `tool` 消息包含，关联的工具调用 ID |
| `name` | string \| null | 仅 `tool` 消息包含，工具名称 |
| `isError` | boolean | 是否为错误消息 |

---

## 回调工具注册

为 Session 动态注册外部 HTTP 回调工具。注册后 Agent 可像使用内置工具一样调用它，Gateway 会代理 HTTP 请求到你的服务。

### `POST /v1/sessions/:id/tools`

**路径参数：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `id` | string | Session ID |

**请求头：**

```
Content-Type: application/json
X-API-Key: sk-abc123
```

**请求体：**

```json
{
  "name": "query_database",
  "description": "Run a SQL query on the production database",
  "callbackUrl": "https://my-service.example.com/tools/query",
  "parameters": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "SQL query to execute"
      },
      "database": {
        "type": "string",
        "description": "Target database name",
        "enum": ["production", "staging"]
      }
    },
    "required": ["query"]
  },
  "timeoutMs": 30000
}
```

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `name` | string | **是** | — | 工具名称，Agent 调用时使用此名称 |
| `description` | string | 否 | `"External tool: <name>"` | 工具描述，帮助 Agent 理解何时使用此工具 |
| `callbackUrl` | string | **是** | — | 你的 HTTP 服务端点 URL，Gateway 向此 URL 发 POST |
| `parameters` | object | 否 | `{"type":"object","properties":{}}` | JSON Schema 格式的参数定义，Agent 根据此 schema 构造调用参数 |
| `timeoutMs` | integer | 否 | `30000` | HTTP 请求超时时间（毫秒） |

**响应 201 Created：**

```json
{
  "ok": true,
  "sessionId": "my-session-001",
  "toolName": "query_database"
}
```

**响应 422：**

```json
{
  "error": "registration_failed",
  "message": "Missing required fields: name, callbackUrl"
}
```

---

#### 回调执行流程

当 Agent 决定调用已注册的回调工具时：

**1. Gateway → 你的服务（POST 请求）**

Gateway 向 `callbackUrl` 发送 POST 请求：

```
POST https://my-service.example.com/tools/query HTTP/1.1
Content-Type: application/json

{
  "callId": "tc_a1b2c3d4",
  "toolName": "query_database",
  "args": {
    "query": "SELECT count(*) FROM users WHERE active = true",
    "database": "production"
  },
  "sessionId": "my-session-001"
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `callId` | string | 工具调用唯一 ID |
| `toolName` | string | 工具名称 |
| `args` | object | Agent 传入的调用参数 |
| `sessionId` | string | Session ID |

**2. 你的服务 → Gateway（HTTP 响应）**

成功返回（HTTP 2xx）：

```json
{
  "result": "Active users: 42,851"
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `result` | string | 工具执行结果文本，将作为工具返回值传给 Agent |

错误返回（HTTP 2xx，业务错误）：

```json
{
  "error": "Permission denied: read-only user cannot execute write queries"
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `error` | string | 错误描述，Agent 会收到此错误信息并据此调整策略 |

> HTTP 非 2xx 状态码或网络错误会被 Gateway 转换为工具执行失败，Agent 会收到包含错误详情的 `tool_execution_end` 事件（`status: "error"`）。

---

#### 完整回调工具示例（Python Flask）

```python
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/tools/query', methods=['POST'])
def handle_query():
    data = request.json
    call_id = data['callId']
    args = data['args']
    query = args.get('query', '')
    
    try:
        result = execute_sql(query)  # 你的业务逻辑
        return jsonify({"result": str(result)})
    except Exception as e:
        return jsonify({"error": str(e)})

if __name__ == '__main__':
    app.run(port=9999)
```

对应注册请求：

```bash
curl -X POST http://localhost:4000/v1/sessions/my-session-001/tools \
  -H "X-API-Key: sk-abc123" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "query_database",
    "description": "Execute SQL queries on the database",
    "callbackUrl": "http://localhost:9999/tools/query",
    "parameters": {
      "type": "object",
      "properties": {
        "query": {"type": "string", "description": "SQL query"}
      },
      "required": ["query"]
    }
  }'
```
