- 支持 tool.call 和 tool.result 消息类型处理 - 引入 Tool 调度与执行逻辑,支持超时与结果截断 - 增加 ToolRegistry 和 ToolExecutor 管理工具定义与执行 - 更新上下文构建与消息映射逻辑,适配工具闭环处理 - 扩展配置与环境变量,支持 Tool 调用相关选项 - 增强单元测试覆盖工具调用与执行情景 - 更新文档和 OpenAPI,新增工具相关说明与模型定义
12 KiB
12 KiB
ChatSession & Message API(MVP-1 + Agent Run MVP-0)
基地址:http://localhost:8000/api(FrankenPHP 容器 8000 端口)
认证方式:JWT,Authorization: Bearer {token}
自然语言:中文
变更记录
- 2025-02-14:新增 ChatSession 创建、消息追加、增量查询接口;支持状态门禁与 dedupe 幂等。
- 2025-02-14:MVP-1.1 增加会话列表、会话更新(重命名/状态变更),列表附带最后一条消息摘要。
- 2025-02-15:Agent Run MVP-0 —— RunDispatcher + AgentRunJob + DummyProvider;自动在 user.prompt 后触发一次 Run,落地 run.status / agent.message。
- 2025-12-18:Agent Run 可靠性增强 —— 并发幂等、终态去重、取消语义加强、Provider 超时/重试/错误归一,SSE gap 回补与心跳。
- 2025-12-19:AgentProvider Streaming 接入 —— ProviderEvent 统一事件流,新增 message.delta 输出与 OpenAI-compatible 适配器。
- 2025-12-21:Tool 子 Run 模式 —— Provider 支持 tool.delta→tool.call,父 Run 调度子 Run 执行工具并写入 tool.result。
本次变更摘要(2025-12-21)
- RunDispatcher 并发幂等:同 trigger_message_id 只产生一个 RUNNING,且仅新建时 dispatch。
- RunLoop/OutputSink 幂等:agent.message、run.status、tool.call、tool.result 均采用 dedupe_key。
- Cancel 强化:多检查点取消,确保不落 agent.message 且落 CANCELED 终态;父 Run 取消会终止等待的子 Run。
- Provider 可靠性:超时/重试/429/5xx,错误落库包含 retryable/http_status/provider/latency_ms。
- Streaming:AgentProvider 产出 message.delta / tool.delta / done;finish_reason=tool_calls 会触发子 Run 执行工具。
- 工具闭环:tool.call(role=AGENT)落库→子 Run 调度→tool.result(role=TOOL)回灌→进入下一轮 LLM。
领域模型
ChatSession:session_id(UUID)、session_name、status(OPEN/LOCKED/CLOSED)、last_seqMessage:message_id(UUID)、session_id、role(USER/AGENT/TOOL/SYSTEM)、type(字符串)、content、payload(json)、seq(会话内递增)、reply_to(UUID)、dedupe_key- 幂等:
UNIQUE (session_id, dedupe_key);同一 dedupe_key 返回已有消息。 - 状态门禁:
CLOSED禁止追加,例外role=SYSTEM && type in [run.status, error];LOCKED禁止role=USER && type=user.prompt。 - 会话缓存:
chat_sessions.last_message_id记录最后一条消息;appendMessage事务内同步更新last_seq、last_message_id、updated_at。 - 工具消息:
tool.call(role=AGENT,携带 tool_call_id/name/arguments)、tool.result(role=TOOL,携带 parent_run_id/run_id/status/result)。
接口
创建会话
-
POST /sessions -
请求体字段
字段 必填 类型 说明 session_name 否 string(≤255) 会话名称 -
响应 201(JSON)
| 字段 | 类型 | 说明 | | --- | --- | --- | | session_id | uuid | 主键 | | session_name | string|null | 会话名 | | status | enum |
OPEN|LOCKED|CLOSED| | last_seq | int | 当前最大 seq | | last_message_id | uuid|null | 最后一条消息 | | created_at, updated_at | datetime | 时间戳 | -
错误:401 未授权
追加消息
POST /sessions/{session_id}/messages- 请求体字段
字段 必填 类型 说明 role 是 enum `USER type 是 string(≤64) 如 user.prompt/agent.message/message.delta等content 否 string 文本内容 payload 否 object jsonb 结构 reply_to 否 uuid 引用消息 dedupe_key 否 string(≤128) 幂等键 - 响应 201(JSON)
字段:message_id, session_id, seq, role, type, content, payload, reply_to, dedupe_key, created_at - 幂等:同 session + dedupe_key 返回已存在的消息(同
message_id/seq)。 - 错误:401 未授权;403 违反状态门禁(CLOSED 禁止,LOCKED 禁止 user.prompt);404 session 不存在;422 校验失败。
按序增量查询
GET /sessions/{session_id}/messages?after_seq=0&limit=50- 查询参数
参数 默认 类型 说明 after_seq 0 int 仅返回 seq 大于该值 limit 50 int(≤200) 返回数量上限 - 响应 200:
data数组,元素字段同“追加消息”响应。 - 错误:401/404/422
会话列表
GET /sessions?page=1&per_page=15&status=OPEN&q=keyword- 查询参数
参数 默认 类型 说明 page 1 int 分页页码 per_page 15 int(≤100) 分页大小 status - enum 过滤 `OPEN q - string ILIKE 模糊匹配 session_name - 响应 200:分页结构(
data/links/meta),data每项字段:字段 类型 说明 session_id uuid 会话主键 session_name string null status enum `OPEN last_seq int 当前最大 seq last_message_id uuid null last_message_at datetime null last_message_preview string content 截断 120,空内容返回空字符串 last_message_role string null last_message_type string null created_at, updated_at datetime 时间戳 - 排序:
updated_atDESC - 错误:401/422
会话更新
PATCH /sessions/{session_id}请求体(至少一项,否则 422)字段 必填 类型 说明 session_name 否 string 1..255 自动 trim status 否 enum `OPEN - 规则:
CLOSED不可改回OPEN(返回 403)。- 任意更新都会刷新
updated_at。
- 响应 200:字段同“会话列表”项。
- 错误:401 未授权;403 状态门禁;404 session 不存在;422 校验失败。
获取会话详情
GET /sessions/{session_id}- 响应 200:字段同“会话列表”项。
- 错误:401 未授权;404 session 不存在。
归档会话(Archive)
POST /sessions/{session_id}/archive- 行为:将
status置为CLOSED,更新updated_at,幂等(重复归档返回当前状态)。 - 响应 200:字段同“会话列表”项(status=CLOSED)。
- 错误:401 未授权;404 session 不存在。
获取单条消息(带会话校验)
GET /sessions/{session_id}/messages/{message_id}- 行为:校验
message.session_id与路径参数一致,否则 404。 - 响应 200:字段同“追加消息”响应。
- 错误:401 未授权;404 不存在或不属于该会话。
SSE 实时增量
GET /sessions/{session_id}/sse?after_seq=123- 头部:
Accept: text/event-stream,可带Last-Event-ID(优先于 query)用于断线续传。 - 查询参数
参数 默认 类型 说明 after_seq 0 int backlog 起始 seq(若有 Last-Event-ID 则覆盖) limit 200 int(≤500) backlog 最多条数 - SSE 输出格式:
id: {seq} event: message data: {...message json...}id为消息seq,便于续传;data为消息 JSON(同追加消息响应字段)。
- Backlog:建立连接后先补发
seq > after_seq的消息(order asc,最多limit条),再进入实时订阅。 - 实时:Redis channel
session:{session_id}:messages发布消息 ID,SSE 侧读取后按 seq 去重、推送。 - Gap 回补:若订阅推送的 seq 与 last_sent_seq 存在缺口,会主动回补 backlog。
- 心跳:周期输出
: ping保活。 - 错误:401 未授权;404 session 不存在。
Agent Run MVP-0(RunDispatcher + AgentRunJob)
流程概述
- 用户追加
role=USER && type=user.prompt后,Controller 自动调用RunDispatcher->dispatchForPrompt。 - 并发保护:同会话只允许一个 RUNNING;同一个
trigger_message_id幂等复用已有run_id。 - 立即写入
run.status(SYSTEM/run.status,payload{run_id,status:'RUNNING',trigger_message_id},dedupe_key=run:trigger:{message_id})。 - 仅在新建 RUNNING 时推送
AgentRunJob(session_id, run_id)到队列(测试环境 QUEUE=sync 会同步执行)。 - RunLoop(默认 HttpAgentProvider,未配置 endpoint 时回退 DummyAgentProvider):
- 终态检测:若已 DONE/FAILED/CANCELED 则直接返回。
- Cancel 检查:存在
run.cancel.request(payload.run_id) 则写入run.status=CANCELED,不产出 agent.message。 - ContextBuilder:提取最近 20 条 USER/AGENT 消息(type in user.prompt/agent.message),seq 升序提供给 Provider。
- Provider 以 Streaming 事件流产出文本增量(message.delta)。
- OutputSink 持续写入
message.delta,最终写入agent.message(payload 含 run_id, provider,dedupe_key=run:{run_id}:agent:message)与run.status=DONE(dedupe_key=run:{run_id}:status:DONE)。
- 异常:ProviderException 写入
error+run.status=FAILED(dedupe),error payload 包含 retryable/http_status/provider/latency_ms。
Run 相关消息类型(落库即真相源)
| type | role | payload 关键字段 | 说明 |
|---|---|---|---|
| run.status | SYSTEM | run_id, status(RUNNING/DONE/CANCELED/FAILED), trigger_message_id?, error? | Run 生命周期事件,CLOSED 状态下允许写入 |
| agent.message | AGENT | run_id, provider | Provider 的一次性回复 |
| message.delta | AGENT | run_id, delta_index | Provider 的增量输出(Streaming) |
| run.cancel.request | USER/SYSTEM | run_id | CancelChecker 依据该事件判断是否中止 |
| error | SYSTEM | run_id, message, retryable?, http_status?, provider?, latency_ms?, raw_message? | 任务异常时落库 |
触发 Run(调试入口)
POST /sessions/{session_id}/runs- 请求体字段
字段 必填 类型 说明 trigger_message_id 是 uuid 通常为 user.prompt消息 ID - 行为:同
trigger_message_id幂等;若已有 RUNNING 则复用其 run_id。 - 响应 201:
{ run_id } - 错误:401 未授权;404 session 不存在或 trigger_message 不属于该 session。
cURL 示例
# 创建会话
SESSION_ID=$(curl -s -X POST http://localhost:8000/api/sessions \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"session_name":"Demo"}' | jq -r '.data.session_id')
# 追加消息(支持 dedupe_key 幂等)
curl -s -X POST http://localhost:8000/api/sessions/$SESSION_ID/messages \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"role":"USER","type":"user.prompt","content":"hello","dedupe_key":"k1"}'
# 增量查询
curl -s "http://localhost:8000/api/sessions/$SESSION_ID/messages?after_seq=0&limit=50" \
-H "Authorization: Bearer $TOKEN"
# 归档
curl -X POST http://localhost:8000/api/sessions/$SESSION_ID/archive \
-H "Authorization: Bearer $TOKEN"
# 获取单条消息
curl -s http://localhost:8000/api/sessions/$SESSION_ID/messages/{message_id} \
-H "Authorization: Bearer $TOKEN"
# SSE(断线续传:可带 Last-Event-ID)
curl -N http://localhost:8000/api/sessions/$SESSION_ID/sse?after_seq=10 \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: text/event-stream"
# 手动触发 Run(调试用,实际 user.prompt 会自动触发)
curl -s -X POST http://localhost:8000/api/sessions/$SESSION_ID/runs \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"trigger_message_id":"'$MESSAGE_ID'"}'