feat: 实现 AgentScope ReAct Runner 两阶段执行并重构事件处理

This commit is contained in:
zl-q
2026-03-16 09:01:01 +08:00
parent 072c09d99d
commit dcceb48d84
51 changed files with 5015 additions and 5663 deletions
+360
View File
@@ -0,0 +1,360 @@
# Agent API Endpoints
本文档列出所有 Agent 相关的 API 端点。
Base URL: `/api/v1/agent`
---
## 端点清单
| 方法 | 路径 | 描述 |
|------|------|------|
| POST | `/runs` | 发起 Agent 运行 |
| GET | `/runs/{thread_id}/events` | SSE 事件流 |
| GET | `/history` | 获取对话历史快照 |
| POST | `/attachments` | 上传附件 |
| GET | `/attachments/signed-url` | 生成附件签名 URL |
| POST | `/transcribe` | 语音转文字 (ASR) |
---
## 1. POST /runs
发起一个 Agent 运行任务。
### Request
Request Body: `RunAgentInput`
详细数据结构见 [run-agent-input.md](./run-agent-input.md)
### Response
```typescript
{
taskId: string, // 任务 ID
threadId: string, // 会话 ID
runId: string, // 运行 ID
created: string // ISO-8601 时间戳
}
```
### Example
```bash
curl -X POST https://api.example.com/api/v1/agent/runs \
-H "Content-Type: application/json" \
-d '{
"threadId": "550e8400-e29b-41d4-a716-446655440000",
"runId": "run-001",
"state": {},
"messages": [
{
"id": "msg-001",
"role": "user",
"content": "帮我查一下北京今天的天气"
}
],
"tools": [],
"context": [],
"forwardedProps": {}
}'
```
### Response Example
```json
{
"taskId": "task-abc123",
"threadId": "550e8400-e29b-41d4-a716-446655440000",
"runId": "run-001",
"created": "2026-03-16T10:00:00Z"
}
```
---
## 2. GET /runs/{thread_id}/events
获取 SSE 事件流,用于实时接收 Agent 运行过程中的事件。
### Path Parameters
| 参数 | 类型 | 描述 |
|------|------|------|
| thread_id | string | 会话 ID |
### Query Parameters
| 参数 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| Last-Event-ID | string | - | 可选,用于断点续传的事件 ID |
| idle_limit | integer | 300 | 最大空闲轮询次数 (1-3600) |
### Headers
| 参数 | 描述 |
|------|------|
| Last-Event-ID | 可选的事件 ID,用于从指定位置恢复 |
### Response
SSE (Server-Sent Events) 流,Content-Type: `text/event-stream`
事件类型详情见 [sse-events.md](./sse-events.md)
### Example
```javascript
const eventSource = new EventSource(
'https://api.example.com/api/v1/agent/runs/550e8400-e29b-41d4-a716-446655440000/events'
);
eventSource.addEventListener('run.started', (e) => {
const data = JSON.parse(e.data);
console.log('Started:', data);
});
eventSource.addEventListener('text.delta', (e) => {
const data = JSON.parse(e.data);
console.log('Delta:', data.data.delta);
});
eventSource.addEventListener('run.finished', (e) => {
const data = JSON.parse(e.data);
console.log('Finished:', data);
});
```
---
## 3. GET /history
获取对话历史快照。
### Query Parameters
| 参数 | 类型 | 必填 | 描述 |
|------|------|------|------|
| threadId | string | 否 | 会话 ID,不指定则返回最新会话 |
| before | date | 否 | 日期格式 `YYYY-MM-DD`,返回该日期之前的快照 |
### Response
```typescript
{
scope: "history_day",
threadId: string | null,
day: string | null, // ISO date format "YYYY-MM-DD"
hasMore: boolean,
messages: HistoryMessage[]
}
```
详细数据结构见 [run-agent-input.md](./run-agent-input.md)
### Example
```bash
curl "https://api.example.com/api/v1/agent/history?threadId=550e8400-e29b-41d4-a716-446655440000&before=2026-03-15"
```
### Response Example
```json
{
"scope": "history_day",
"threadId": "550e8400-e29b-41d4-a716-446655440000",
"day": "2026-03-15",
"hasMore": false,
"messages": [
{
"id": "msg-001",
"seq": 1,
"role": "user",
"content": "帮我创建一个日程",
"url": null,
"timestamp": "2026-03-15T10:00:00Z"
},
{
"id": "msg-002",
"seq": 2,
"role": "assistant",
"content": "好的,我来帮您创建日程。",
"uiSchema": {
"version": "2.0",
"locale": "zh-CN",
"status": "success",
"theme": "default",
"root": {
"type": "stack",
"appearance": "card",
"children": [
{"type": "text", "content": "日程已创建", "role": "title"},
{"type": "badge", "label": "SUCCESS", "status": "success"}
]
}
},
"timestamp": "2026-03-15T10:00:05Z"
}
]
}
```
---
## 4. POST /attachments
上传附件到存储。
### Form Data
| 参数 | 类型 | 描述 |
|------|------|------|
| threadId | string | 会话 ID |
| file | file | 要上传的文件 |
### Response
```typescript
{
attachment: {
bucket: string,
path: string,
mimeType: string,
size: number,
url: string // 临时访问 URL
}
}
```
### Example
```bash
curl -X POST https://api.example.com/api/v1/agent/attachments \
-F "threadId=550e8400-e29b-41d4-a716-446655440000" \
-F "file=@/path/to/image.png"
```
### Limits
- 最大文件大小: 5MB
- 支持的文件类型: 见后端配置
---
## 5. GET /attachments/signed-url
生成附件的签名 URL,用于直接访问存储中的文件。
### Query Parameters
| 参数 | 类型 | 必填 | 描述 |
|------|------|------|------|
| bucket | string | 是 | 存储桶名称 |
| path | string | 是 | 文件路径 |
### Response
```typescript
{
bucket: string,
path: string,
url: string // 签名 URL
}
```
### Example
```bash
curl "https://api.example.com/api/v1/agent/attachments/signed-url?bucket=agent-inputs&path=user-123/image.png"
```
### Response Example
```json
{
"bucket": "agent-inputs",
"path": "user-123/image.png",
"url": "https://storage.example.com/agent-inputs/user-123/image.png?signature=abc123..."
}
```
---
## 6. POST /transcribe
语音转文字 (ASR)。
### Request
Form Data:
| 参数 | 类型 | 描述 |
|------|------|------|
| audio | file | 音频文件 (WAV 格式) |
### Headers
| 参数 | 描述 |
|------|------|
| content-length | 文件大小 |
### Response
```typescript
{
transcript: string, // 转录文本
language: string, // 检测到的语言
duration: number // 音频时长 (秒)
}
```
### Example
```bash
curl -X POST https://api.example.com/api/v1/agent/transcribe \
-F "audio=@/path/to/audio.wav"
```
### Response Example
```json
{
"transcript": "今天天气真不错",
"language": "zh-CN",
"duration": 3.5
}
```
### Limits
- 支持格式: `audio/wav`, `audio/x-wav`, `audio/wave`
- 最大文件大小: 10MB
- 速率限制: 20 次/分钟/用户
---
## 错误响应
所有端点可能返回以下错误:
| 状态码 | 描述 |
|--------|------|
| 400 | 请求参数错误 |
| 401 | 未认证 |
| 403 | 无权限 |
| 404 | 资源不存在 |
| 413 | 请求体过大 |
| 422 | 数据验证失败 |
| 429 | 速率限制 |
| 500 | 服务器内部错误 |
### Error Response Format
```json
{
"detail": "错误详情信息"
}
```
+449
View File
@@ -0,0 +1,449 @@
# Agent Run Input Protocol
> **NOTE**: This document defines the RunAgentInput data structure for `POST /api/v1/agent/runs`.
## Version
- **Current**: `1.0`
- **Status**: Active
---
## Overview
`POST /api/v1/agent/runs` accepts `RunAgentInput` as request body to initiate an agent execution.
---
## RunAgentInput Schema
```typescript
interface RunAgentInput {
threadId: string; // 必须是有效 UUID
runId: string; // 最大 128 字符
parentRunId?: string;
state?: any;
messages: Message[]; // 最多 200 条
tools?: Tool[];
context?: Context[];
forwardedProps?: any;
}
```
### Required Fields
| Field | Type | Constraints |
|-------|------|-------------|
| `threadId` | string | 必须为有效 UUID |
| `runId` | string | 最大 128 字符 |
| `messages` | array | 最多 200 条,**必须恰好包含 1 条 user message** |
| `state` | any | - |
| `tools` | array | 可选 |
| `context` | array | 可选 |
| `forwardedProps` | any | - |
---
## Message Types
### UserMessage
```typescript
interface UserMessage {
id: string;
role: "user";
content: string | ContentBlock[];
name?: string;
encryptedValue?: string;
}
type ContentBlock = TextContentBlock | BinaryContentBlock;
interface TextContentBlock {
type: "text";
text: string;
}
interface BinaryContentBlock {
type: "binary";
mimeType: string; // 必须为 image/* 类型
id?: string;
url?: string; // 必须是有效的 signed URL
data?: string; // 不允许使用 data 字段
filename?: string;
}
```
### AssistantMessage
```typescript
interface AssistantMessage {
id: string;
role: "assistant";
content?: string | null;
name?: string | null;
toolCalls?: ToolCall[];
encryptedValue?: string | null;
}
interface ToolCall {
id: string;
type: "function";
function: {
name: string;
arguments: string;
};
encryptedValue?: string | null;
}
```
### SystemMessage
```typescript
interface SystemMessage {
id: string;
role: "system";
content: string;
name?: string | null;
encryptedValue?: string | null;
}
```
### ToolMessage
```typescript
interface ToolMessage {
id: string;
role: "tool";
content: string;
toolCallId: string;
error?: string | null;
encryptedValue?: string | null;
}
```
### Other Message Types
- **DeveloperMessage**: `role: "developer"`
- **ReasoningMessage**: `role: "reasoning"`
- **ActivityMessage**: `role: "activity"` (for progress updates)
---
## Tool Schema
```typescript
interface Tool {
name: string;
description: string;
parameters: object; // JSON Schema 格式
}
```
### Example
```json
{
"name": "get_weather",
"description": "Get current weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["location"]
}
}
```
### Backend Processing
Backend 使用 `build_tools_prompt` 函数将 tools 转换为 prompt 格式:
```
<!-- TOOLS_START -->
- get_weather: Get current weather for a location
- args_schema: {"type":"object","properties":{"location":{"type":"string","description":"City name"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["location"]}
- searchDocuments: Search for documents
- args_schema: {"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}
Note: tool arguments must strictly match args_schema.
<!-- TOOLS_END -->
```
---
## Context Schema
```typescript
interface Context {
description: string;
value: string;
}
```
---
## Validation Rules
Backend 实现了以下验证规则:
| Rule | Error Message |
|------|---------------|
| payload ≤ 256KB | `RunAgentInput payload exceeds size limit` |
| threadId 必须是 UUID | `threadId must be a valid UUID` |
| runId 最大 128 字符 | `runId exceeds length limit` |
| messages ≤ 200 条 | `RunAgentInput.messages exceeds limit` |
| user text ≤ 10,000 字符 | `RunAgentInput user message text exceeds limit` |
| **恰好 1 条 user message** | `RunAgentInput.messages must contain exactly one user message` |
| user message 必须在第一条 | `RunAgentInput.messages[0].role must be user` |
| binary 必须是 image/* | `binary content requires image mimeType` |
| binary 必须有 url | `binary content requires url` |
| binary 不允许使用 data | `binary content data is not allowed` |
---
## Request Example
### 纯文本请求
```json
{
"threadId": "550e8400-e29b-41d4-a716-446655440000",
"runId": "run-001",
"state": {},
"messages": [
{
"id": "msg-001",
"role": "user",
"content": "帮我查一下北京今天的天气"
}
],
"tools": [],
"context": [],
"forwardedProps": {}
}
```
### 多模态请求 (带图片)
```json
{
"threadId": "550e8400-e29b-41d4-a716-446655440000",
"runId": "run-002",
"state": {},
"messages": [
{
"id": "msg-001",
"role": "user",
"content": [
{
"type": "text",
"text": "这张图片里的内容是什么?"
},
{
"type": "binary",
"mimeType": "image/png",
"url": "https://storage.example.com/agent-inputs/user-123/image.png?signature=xxx"
}
]
}
],
"tools": [],
"context": [],
"forwardedProps": {}
}
```
### 带 Tools 的请求
```json
{
"threadId": "550e8400-e29b-41d4-a716-446655440000",
"runId": "run-003",
"state": {},
"messages": [
{
"id": "msg-001",
"role": "user",
"content": "北京天气怎么样?"
}
],
"tools": [
{
"name": "get_weather",
"description": "获取指定城市的天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称"
}
},
"required": ["city"]
}
}
],
"context": [],
"forwardedProps": {}
}
```
---
## Response
成功响应返回 `TaskAcceptedResponse`
```typescript
interface TaskAcceptedResponse {
taskId: string;
threadId: string;
runId: string;
created: string; // ISO-8601 timestamp
}
```
---
## History API
`GET /api/v1/agent/history` 返回对话历史快照。
### Request
| Parameter | Type | Description |
|-----------|------|-------------|
| `threadId` | string (query) | 可选,指定会话 ID,不指定则返回最新会话 |
| `before` | string (query) | 可选,日期格式 `YYYY-MM-DD`,返回该日期之前的快照 |
### Response
返回 `HistorySnapshotResponse`
```typescript
interface HistorySnapshotResponse {
scope: "history_day";
threadId: string | null;
day: string | null; // ISO date format "YYYY-MM-DD"
hasMore: boolean;
messages: HistoryMessage[];
}
```
### HistoryMessage
根据消息 role 不同,返回字段有所差异:
```typescript
// role = "user"
interface HistoryMessageUser {
id: string;
seq: number;
role: "user";
content: string;
url: string | null; // 附件临时访问 URL
timestamp: string; // ISO-8601 timestamp
}
// role = "tool"
interface HistoryMessageTool {
id: string;
seq: number;
role: "tool";
content: string;
uiSchema: UiSchemaRenderer | null; // 由 tool_agent_output.ui_hints 编译
timestamp: string;
}
// role = "assistant"
interface HistoryMessageAssistant {
id: string;
seq: number;
role: "assistant";
content: string;
uiSchema: UiSchemaRenderer | null; // 由 worker_agent_output.ui_hints 编译
timestamp: string;
}
type HistoryMessage = HistoryMessageUser | HistoryMessageTool | HistoryMessageAssistant;
```
### UiSchemaRenderer
编译后的 UI 渲染结构,详见 [UiSchema Protocol](../ui/ui-schema.md)
```typescript
interface UiSchemaRenderer {
version: "2.0";
locale: string;
status: "info" | "success" | "warning" | "error" | "pending";
theme: "default" | "light" | "dark";
meta?: {
requestId?: string;
toolId?: string;
traceId?: string;
userId?: string;
};
root: UiLayoutNode;
}
```
### Example
```json
{
"scope": "history_day",
"threadId": "550e8400-e29b-41d4-a716-446655440000",
"day": "2026-03-15",
"hasMore": true,
"messages": [
{
"id": "msg-001",
"seq": 1,
"role": "user",
"content": "帮我创建一个日程",
"url": null,
"timestamp": "2026-03-15T10:00:00Z"
},
{
"id": "msg-002",
"seq": 2,
"role": "assistant",
"content": "好的,我来帮您创建日程。",
"uiSchema": {
"version": "2.0",
"locale": "zh-CN",
"status": "success",
"theme": "default",
"root": {
"type": "stack",
"appearance": "card",
"children": [
{"type": "text", "content": "日程已创建", "role": "title"},
{"type": "badge", "label": "SUCCESS", "status": "success"},
{"type": "text", "content": "您的会议日程创建成功", "role": "body"}
]
}
},
"timestamp": "2026-03-15T10:00:05Z"
}
]
}
```
---
## Compatibility Notes
- `UserMessage.content` 支持 string 或 array 格式,前端优先使用 array 格式以支持多模态
- binary content 的 url 必须是有效的 signed URL,由 `/api/v1/agent/attachments` 端点生成
- backend 验证通过后,会将 binary url 转换为内部存储路径
- tools 为空数组时,prompt 中不会包含工具说明
+370
View File
@@ -0,0 +1,370 @@
# Agent SSE Events
本文档记录 Agent Runtime 产生的所有 Server-Sent Events (SSE) 事件,用于前端实时展示。
## 事件流转架构
```
pipeline.emit()
AgentScopeEventPipeline.emit()
├─→ store.persist() → 持久化到数据库
└─→ bus.publish() → 发布到 Redis Stream
前端通过 GET /runs/{thread_id}/events 读取
```
## 事件统一格式
所有事件在 Redis 中传输时都包含以下字段:
```typescript
{
type: string, // 事件类型
threadId: string, // 会话 ID
runId: string, // 运行 ID
data: object // 事件数据
}
```
---
## 1. Orchestrator 生命周期事件
### run.started
Agent 开始运行时触发。
```typescript
{
type: "run.started",
threadId: "xxx",
runId: "yyy",
data: {}
}
```
### run.finished
Agent 成功完成时触发。
```typescript
{
type: "run.finished",
threadId: "xxx",
runId: "yyy",
data: {}
}
```
### run.error
Agent 运行出错时触发。
```typescript
{
type: "run.error",
threadId: "xxx",
runId: "yyy",
data: {
message: "runtime execution failed"
}
}
```
---
## 2. Step 阶段事件
### step.start
阶段开始时触发。
```typescript
{
type: "step.start",
threadId: "xxx",
runId: "yyy",
data: {
stepName: "router" | "worker"
}
}
```
### step.finish
阶段结束时触发。
```typescript
{
type: "step.finish",
threadId: "xxx",
runId: "yyy",
data: {
stepName: "router" | "worker"
}
}
```
---
## 3. Worker 运行时事件
### 3.1 文本消息事件
#### text.start
文本消息开始时触发。
```typescript
{
type: "text.start",
threadId: "xxx",
runId: "yyy",
data: {
messageId: "msg-xxx",
role: "assistant",
stage: "worker"
}
}
```
#### text.delta
文本内容增量更新时触发。
```typescript
{
type: "text.delta",
threadId: "xxx",
runId: "yyy",
data: {
messageId: "msg-xxx",
delta: "这是新增的文本内容",
stage: "worker"
}
}
```
#### text.end
文本消息结束时触发,包含完整输出和使用统计。
```typescript
{
type: "text.end",
threadId: "xxx",
runId: "yyy",
data: {
messageId: "msg-xxx",
role: "assistant",
stage: "worker",
workerAgentOutput: {
status: "success" | "partial_success" | "failed",
answer: "主回复文本",
key_points: ["要点1", "要点2"],
result_type: "execution_report" | "clarification" | "error_report" | "unknown",
suggested_actions: ["建议操作1"],
error: null | { code: string, message: string },
ui_hints: { ... } | null // 仅在 ui_mode 非空时存在
},
model: "gpt-4o",
inputTokens: 1000,
outputTokens: 500,
cost: 0.025,
latencyMs: 1500
}
}
```
**workerAgentOutput 详细结构:**
```typescript
// WorkerAgentOutputLite
{
status: "success" | "partial_success" | "failed",
answer: string,
key_points: string[],
result_type: "execution_report" | "clarification" | "error_report" | "unknown",
suggested_actions: string[],
error: { code: string, message: string } | null
}
// WorkerAgentOutputRich (当 ui.ui_mode 非空时)
{
// ... WorkerAgentOutputLite 字段
ui_hints: {
ui_mode: string,
ui_state: object,
actions: UiHintAction[]
}
}
```
---
### 3.2 工具调用事件
#### tool.start
工具调用开始时触发。
```typescript
{
type: "tool.start",
threadId: "xxx",
runId: "yyy",
data: {
messageId: "msg-xxx",
toolCallId: "call-abc",
toolName: "calendar_read",
stage: "worker"
}
}
```
#### tool.args
工具调用参数时触发。
```typescript
{
type: "tool.args",
threadId: "xxx",
runId: "yyy",
data: {
messageId: "msg-xxx",
toolCallId: "call-abc",
toolName: "calendar_read",
args: { start_date: "2024-01-01", end_date: "2024-01-07" },
stage: "worker"
}
}
```
#### tool.end
工具调用结束时触发。
```typescript
{
type: "tool.end",
threadId: "xxx",
runId: "yyy",
data: {
messageId: "msg-xxx",
toolCallId: "call-abc",
toolName: "calendar_read",
stage: "worker"
}
}
```
#### tool.result
工具执行结果时触发。
```typescript
{
type: "tool.result",
threadId: "xxx",
runId: "yyy",
data: {
messageId: "msg-xxx",
toolCallId: "call-abc",
toolName: "calendar_read",
stage: "worker",
toolAgentOutput: {
tool_name: "calendar_read",
tool_call_id: "call-abc",
tool_call_args: { start_date: "2024-01-01", end_date: "2024-01-07" },
status: "success" | "failed",
result_summary: "找到3个事件...",
ui_hints: null,
attachments: null
}
}
}
```
**toolAgentOutput 详细结构:**
```typescript
{
tool_name: string,
tool_call_id: string,
tool_call_args: object | null,
status: "success" | "failed",
result_summary: string,
ui_hints: object | null,
attachments: Array<{
bucket: string,
path: string,
mime_type: string,
url: string
}> | null
}
```
---
## 使用统计字段
`text.end` 事件中包含使用统计:
| 字段 | 类型 | 说明 |
|------|------|------|
| `model` | string | 使用的模型名称 |
| `inputTokens` | number | 输入 token 数量 |
| `outputTokens` | number | 输出 token 数量 |
| `cost` | number | 花费(美元) |
| `latencyMs` | number | 延迟(毫秒) |
---
## 前端接收示例
```javascript
const eventSource = new EventSource(`/runs/${threadId}/events`);
eventSource.addEventListener('run.started', (e) => {
console.log('Agent started:', JSON.parse(e.data));
});
eventSource.addEventListener('step.start', (e) => {
console.log('Step started:', JSON.parse(e.data));
});
eventSource.addEventListener('text.delta', (e) => {
const data = JSON.parse(e.data);
console.log('Text delta:', data.data.delta);
});
eventSource.addEventListener('tool.start', (e) => {
const data = JSON.parse(e.data);
console.log('Tool called:', data.data.toolName);
});
eventSource.addEventListener('tool.result', (e) => {
const data = JSON.parse(e.data);
console.log('Tool result:', data.data.toolAgentOutput);
});
eventSource.addEventListener('text.end', (e) => {
const data = JSON.parse(e.data);
console.log('Worker output:', data.data.workerAgentOutput);
console.log('Usage:', {
inputTokens: data.data.inputTokens,
outputTokens: data.data.outputTokens,
cost: data.data.cost
});
});
eventSource.addEventListener('run.finished', (e) => {
console.log('Agent finished:', JSON.parse(e.data));
});
eventSource.addEventListener('run.error', (e) => {
console.log('Agent error:', JSON.parse(e.data));
});
```