DASP/1.0 — Durable AI Session Protocol
1. 文档状态
本文定义 DASP/1.0(Durable AI Session Protocol,持久 AI 会话协议)的稳定协议与实现约束。
DASP 是与平台、模型厂商和存储实现无关的公开协议名称。产品可以使用自己的域名、认证和 内部实现,但不得改变 DASP/1.0 已定义的路径语义、字段、事件、状态与错误契约。
文档按以下职责拆分,避免把介绍、接口事实和实现建议混在一起:
dasp-api-reference-v1.md:接口事实、逐场景入参/出参、注意事项、 错误和客户端处理规则;开发与测试应优先查阅;- 本文:协议语义、架构、持久化、执行、容灾、性能和演进约束;
stateful-session-client-guide-v1.md:客户端和 SDK 独立接入模块。
若示例与规范性事实发生歧义,以 API 事实参考中的接口契约和本文的规范性约束为准;示例中的域名、 模型、SID 和 Response ID 均为占位值。
该协议面向通用 AI 客户端,不包含产品或厂商专属字段。公开协议中不得出现 产品前缀、公开金额、内部 Provider、内部路由、租户实现、执行租约或数据库版本。
V1 的目标是:
- 每个模型会话只使用一个 GUID 格式的
sid;History 可一次读取多个sid并由服务端合并; - 客户端每次只提交新增输入和本次模型配置;
- 服务端管理历史、有效上下文、压缩、流式输出、恢复、并发和内部计费;
- 流式与非流式响应表达相同的输出 Item 和 Usage;
- 协议一旦发布,不改变既有路径、字段、事件含义和错误语义;
- 破坏性变更只能通过新的主版本路径发布。
V1 不是 Agent 业务协议。Run、Work、Employee、Skill、工具权限、文件权限、审批、
业务 Timeline 等继续由客户端系统负责。客户端系统可以为不同 Agent Thread 分配不同
sid,服务端不感知它们之间的业务关系。
2. 为什么需要补充该协议
现有 POST /v1/responses 是无状态兼容入口。调用方负责提交本次模型调用所需的完整
输入,网关负责模型路由、协议转换、用量统计和响应格式转换。
该入口不能直接改为默认有状态,原因如下:
sid缺失若表示“自动创建会话”,所有已有无状态调用都会被持久化,改变既有语义;- 现有压缩能力要求调用方提交待压缩内容,不能代表网关已经持有会话历史;
- 上下文超限保护只能拒绝请求,不能自动读取会话、压缩并继续;
- HTTP 连接生命周期不能作为模型任务生命周期,客户端掉线后需要独立恢复;
- 有状态调用需要会话所有权、单写者、幂等结算、恢复快照和历史投影,这些不属于 普通协议转换 Middleware 的职责;
- 同一平台必须同时保留无状态和有状态两种明确入口,避免隐式行为。
因此 V1 保留:
POST /v1/responses
作为无状态入口,并新增:
POST /v1/session/responses
PUT /v1/session/responses
GET /v1/session/responses
POST /v1/session/responses/interrupt
POST /v1/session/responses/cancel
GET /v1/session/history
PATCH /v1/session
DELETE /v1/session
作为有状态入口。
3. 核心术语
3.1 Session
一个由 sid 唯一标识的模型会话。sid 是服务端生成的随机 GUID,不编码用户、租户、
模型、时间或业务含义。
3.2 Exchange
一次模型输入和对应模型输出:
{
"q": [],
"a": []
}
q 和 a 始终是 Item 数组,不能根据内容数量退化成字符串或单个对象。
3.3 Context
模型下一次调用真正使用的有界上下文:
{
"system_prompt": "",
"summary": "",
"qa": [
{
"q": [],
"a": []
}
]
}
Context 只包含压缩摘要、近期 Exchange 和尚未闭合的协议项,不代表完整历史。
上例是组装给模型时的逻辑视图;为允许运行中无冲突切换,system_prompt 的当前值使用独立
Prompt State 持久化,每个 Response 在接受时冻结并只覆盖到本次逻辑 Context。冻结快照不得写回
SessionContexts.ContextJson。
3.4 History
会话所有已形成终态且仍位于当前可见投影中的 Exchange 原始标准化输入输出,包括正常完成和 安全停止前已经公开的部分输出。Context 压缩不得修改 History。
3.5 Active Response
一个 Session 当前正在执行或最近完成的模型响应。一个 sid 同时最多存在一个正在运行的
Active Response。
4. 身份与请求头
4.1 SID Header
固定请求头名称:
sid: 20d82b37-a78e-4f91-8ed1-66f44e0c847f
Header 名称大小写不敏感。除 History 外,值必须是一个标准 GUID 字符串。
History 为了合并主会话、子会话或其他并行会话,复用同一个 Header 名称,传入 1~100 个以逗号
分隔的 GUID;不新增 sids、parentSid 或会话关系字段:
sid: 20d82b37-a78e-4f91-8ed1-66f44e0c847f,cb3433dd-bb67-4ccc-8852-d0c1babff3e0
多 SID 只是一次只读合并查询,不改变各 SID 的 Context、并发锁、生命周期或所有权。
4.2 创建规则
POST /v1/session/responses 未携带 sid,或携带空白 sid 时,服务端生成新的 GUID。
文档和 SDK 应推荐“不发送该 Header”,不推荐发送空 Header,因为代理可能删除空值。
新建或续写成功后,响应始终包含:
sid: 20d82b37-a78e-4f91-8ed1-66f44e0c847f
Access-Control-Expose-Headers: sid
4.3 所有权
sid 不是鉴权凭证。服务端必须把 Session 绑定到当前认证主体及租户范围。
- SID 存在且属于当前主体:允许访问;
- SID 不存在:返回
404 session_not_found; - SID 属于其他主体:同样返回
404 session_not_found,不得泄露其存在性; - 禁止仅凭 SID 进行匿名越权访问。
5. 统一 Item 模型
V1 使用 Item 数组表达输入和输出。第一版固定支持以下 Item 类型:
text
reasoning
compaction
input_file
file_analysis
function_call
function_call_output
客户端必须忽略不认识的输出 Item 类型,服务端不得改变已发布类型的含义。
5.1 Text
{
"type": "text",
"data": "你好"
}
5.2 Reasoning
{
"id": "r1",
"type": "reasoning",
"status": "completed",
"output": "正在分析数据库索引结构。"
}
只能返回 Provider 明确公开的 reasoning 内容或摘要。不得生成、猜测或暴露模型隐藏思维链。
5.3 Compaction
{
"id": "c1",
"type": "compaction",
"status": "completed",
"output": "此前会话讨论了会话存储和故障恢复。",
"usage": {
"input_tokens": 12000,
"output_tokens": 320,
"total_tokens": 12320
}
}
5.4 Function Call
{
"id": "f1",
"type": "function_call",
"call_id": "call_456",
"name": "database_inspect",
"arguments": {
"table": "Orders"
}
}
5.5 Function Call Output
{
"type": "function_call_output",
"call_id": "call_456",
"output": {
"result": "检查完成,缺少 CreatedAt 和 Status 联合索引"
}
}
function_call_output.call_id 必须与先前尚未闭合的 function_call.call_id 完全一致。
服务端在协议调用尚未闭合时不得跨越该位置进行 Context 压缩。
5.6 File Input 与 File Analysis
会话文件输入只接受无需认证即可读取的公开 HTTP/HTTPS URL,不接受本地路径、内网地址、file_id、
Base64 或上传字节:
{
"type": "input_file",
"file_url": "https://files.example.com/order-report.pdf"
}
服务端在接受请求和实际下载/转发前都必须校验 URL,禁止 HTTP、userinfo、非标准端口、环回、 私网、链路本地、保留地址及重定向到上述地址,防止 SSRF 与 DNS Rebinding。
- 目标模型具备多模态/文件能力:保持原生 Responses
input_file结构交给 Provider; - 目标模型不具备该能力:服务端自动调用
mcp/multimodal-analysis的file_analysis,将结果装入 本次和后续模型 Context,客户端不参与 MCP 编排; - MCP 分析过程以
file_analysisItem 的running/completed/failed状态公开; - History 的
question保留原始input_file,answer保留可还原的file_analysis投影; - Context 的
q同时保留原始文件引用和自动分析结果,a不重复注入该投影; - MCP 与最终生成的 Token 都计入本次累计 Usage,内部调用分别使用独立 Invocation Identity, 不得重复计费。
{
"id": "fa_1",
"type": "file_analysis",
"status": "completed",
"file_url": "https://files.example.com/order-report.pdf",
"data": "报告包含 120 个订单,异常状态 3 个。",
"source": "mcp/multimodal-analysis",
"usage": {
"input_tokens": 800,
"output_tokens": 90,
"total_tokens": 890
}
}
6. 提交接口
6.1 路径
POST /v1/session/responses
该接口只做一件事:在指定 Session 上接受一个新增输入并生成一个新响应。
6.2 文本请求
POST /v1/session/responses
sid: 20d82b37-a78e-4f91-8ed1-66f44e0c847f
Content-Type: application/json
{
"model": "gpt-5.6-sol",
"input": "继续完成任务",
"stream": true
}
字符串 input 规范化为:
[
{
"type": "text",
"data": "继续完成任务"
}
]
6.3 Item 数组请求
{
"model": "gpt-5.6-sol",
"input": [
{
"type": "function_call_output",
"call_id": "call_456",
"output": {
"result": "检查完成"
}
}
],
"stream": true
}
6.4 模型配置
模型配置以本次请求为准,不永久绑定 Session。V1 允许复用现有 Responses 配置字段,包括但 不限于:
model
stream
max_output_tokens
temperature
top_p
tools
tool_choice
parallel_tool_calls
text
reasoning
think
同一 SID 可以切换模型。标准化 Item 和纯文本 Summary 是模型无关事实;服务端使用新模型目录 重新计算窗口、输出预留和压缩阈值,不把无界完整 History 载入热路径。只有内部 Context Schema 本身不兼容时,才按 Keyset 分批从完整历史重建,客户端不参与迁移。
6.5 并发规则
一个 SID 同时只允许一个运行中的响应。
- Session 空闲:接受 POST;
- Session 运行中:返回
409 session_busy; - Session 运行中需要补充信息:只允许调用 Interrupt POST,由服务端原子停止并串行提交补充响应;
- 多个 GET 观察者可以同时恢复同一响应;
- 网络断开不改变 Session 运行状态;
- 只有显式调用取消接口才能停止执行;断开流、刷新页面和删除请求都不隐式取消模型调用。
服务端不得把运行期间的第二个 POST 自动解释为重连,也不得静默排队成不可见任务。
7. 停止、中途补充、编辑上一条消息与删除
三种操作必须保持独立:
停止:终止当前 Active Response,保留 Session
中途补充:停止当前 Response,并把补充信息作为串行后继 Response 提交
编辑:替换最近一条用户输入,剪切其后全部消息,再从该位置重新生成
删除:删除整个 Session,不代表停止当前响应
7.1 安全停止当前输出
POST /v1/session/responses/cancel
sid: {GUID}
该请求没有请求体,只能作用于该 SID 当前的 Active Response。服务端必须先执行认证、Session 所有权检查和当前执行 Epoch 检查,不能仅凭 SID 向任意 Executor 发送取消信号。
接受停止请求后返回:
202 Accepted
{
"status": "canceling"
}
取消是幂等操作。重复请求不得重复调用 Provider Cancel、重复写 History 或重复结算。停止请求与
正常完成发生竞争时,以数据库最先提交的终态为准:如果响应已经完成,返回 200 和真实终态;
不能把已完成结果改写为已取消。
Session 尚无 Active Response 时返回 404 response_not_found。Active Response 已经形成终态时,
重复 Cancel 返回 200 和该响应的真实终态。
Executor 收到停止请求后:
- 原子地把 Active Response 标记为
canceling; - 停止继续向客户端发布新 Delta;
- 在 Provider 支持时发送上游取消;
- 等待或确认上游结束,保存已经产生的可见输出和真实 Usage;
- 原子提交
canceled终态并释放 Session 单写者; - 向现有流和恢复流发送最终
usage,然后发送:
data: {"type":"done","id":"resp_123","status":"canceled"}
202 canceling 只表示取消已被接受,不表示 Provider 已停止或 Session 已经可写。客户端必须等待
恢复流中的 done,或重新 GET 确认终态后,才能提交下一条消息。
Provider 在取消生效前已消耗的 Token 必须计入 Usage;取消接口本身不产生模型费用。无法确认
Provider 是否停止时进入 provider_outcome_unknown,不得提前释放 Session 后静默发起第二次调用。
7.2 会话中途打断并提交补充信息
用户在当前回答尚未完成时补充条件,客户端使用一个原子动作:
POST /v1/session/responses/interrupt
sid: {GUID}
Content-Type: application/json
请求体与普通提交相同:
{
"model": "gpt-5.6-sol",
"input": "补充:支付表也要检查 Status 与 CreatedAt 联合索引。",
"stream": true
}
服务端在一个串行化事务中完成以下动作:
- 锁定当前 Session 和 Active Response;
- 把旧 Response 标记为
canceling,冻结其已公开输出和 Usage; - 创建唯一后继 Response,并把
ActiveResponseId指向后继; - 后继 Question 按 Item 顺序保存“被打断的原始 Question + 本次补充 Input”;
- 旧 Response 形成终态并释放执行 Lease 后,才允许后继调用 Provider。
因此 Interrupt 不是两个客户端请求拼接出来的 Cancel + POST,不存在两者之间被另一个写入抢占的
空窗,也不会有两个 Provider 同时更新同一个 Context。旧 Response 的部分 Answer 不进入模型
Context;后继使用原始 Question 和补充 Input 重新生成。History 最终保留两条真实事实:旧 Exchange
为 canceled,后继 Exchange 为 completed、canceled 或明确错误。
流式交接先返回极简状态事件,再进入普通响应事件:
data: {"type":"interrupt","id":"resp_old","status":"canceling"}
data: {"type":"interrupt","id":"resp_old","status":"canceled","usage":{"input_tokens":120,"output_tokens":36,"total_tokens":156,"cached_tokens":0,"reasoning_tokens":0}}
data: {"type":"start","id":"resp_new","model":"gpt-5.6-sol"}
...
data: {"type":"done","id":"resp_new","status":"completed"}
第二个 interrupt 事件携带旧 Response 的最终累计 Usage;后继仍通过自己的 usage 事件独立报告
消耗,客户端不得把两个 Usage 覆盖成一笔。旧执行器异常退出且无法确认 Provider 结果时,第二个
事件可以是 status: outcome_unknown,后继仍只在旧 Lease 失效和旧状态持久化后启动。
Interrupt 的非流请求等待后继 Response 的终态后返回,响应结构与普通非流 POST 相同。流连接断开
后,GET /v1/session/responses 使用同一 SID 恢复后继的完整交接流,不需要旧 Response ID。交接
期间再次 Interrupt 返回 409 session_busy;普通 POST/PUT 也返回 409 session_busy。Cancel 可以
安全取消尚在等待的后继 Response。
若处理 Interrupt 时旧 Response 恰好已经形成终态,服务端把该动作退化为普通续写,确保补充消息
不因完成竞争而丢失;此时不产生 interrupt 事件。协议不允许客户端通过关闭旧连接来模拟该动作。
7.3 编辑上一条用户消息
PUT /v1/session/responses
sid: {GUID}
Content-Type: application/json
请求体与提交接口相同,input 表示替换后的用户输入,模型配置表示重新生成所用的本次配置:
{
"model": "gpt-5.6-sol",
"input": "检查订单表及支付表索引",
"stream": true
}
V1 的“上一条”固定指当前可见投影中最近一条用户发起的输入,不允许客户端通过任意 Message ID
改写更早历史。PUT 使用新 input 替换该用户输入,并剪切该输入之后产生的全部消息,包括:
若最近一次用户输入因 Provider Error 或 outcome_unknown 未形成 History Exchange,PUT 仍以该次
持久化 Response 的 Question 作为编辑目标;它不会错误地回退并剪切更早的已完成用户消息。
- Assistant 文本和公开 Reasoning;
- Function Call 及其 Function Call Output;
- Compaction 的客户端投影;
- 已停止响应留下的部分输出;
- 基于旧输入继续生成的其他下游内容。
“剪切”表示这些内容立即从当前会话投影、GET History 和后续模型 Context 中移除。服务端可以为了 审计和故障恢复在内部保留旧 Revision,但不得再把旧 Revision 返回为当前历史,也不得把其中内容 带入新模型调用。
例如当前投影为:
Q1 -> A1 -> Q2 -> Function Call -> Function Output -> A2(partial)
编辑最近一条用户消息 Q2 为 Q2' 后,当前投影原子地变为:
Q1 -> A1 -> Q2'
随后以 Q1 -> A1 -> Q2' 作为有效 Context 重新生成新的回答。旧 Function Call、Function Output
和 A2(partial) 不再可见,也不能与新回答混合。
编辑必须在 Session 空闲时执行;响应运行中返回 409 session_busy。客户端应先调用停止接口并等待
canceled,再调用 PUT。服务端在一个事务中:
- 锁定 Session Revision;
- 将被替换输入及其后全部消息标记为
superseded,并从当前投影剪切; - 从替换位置之前的有效 History 重建 Context;
- 冻结新输入并启动新的 Active Response。
PUT 的流式和非流式响应与 POST 完全相同。GET History 默认只返回当前可见投影,不返回已被替换 的旧分支;服务端内部仍保留带 Revision 的旧事实,用于审计、争议处理和故障恢复。旧分支已经产生 的 Usage 不退款,也不能在新响应中重复结算。
没有可编辑用户输入时返回 409 no_editable_message。并发 PUT/POST 仍只有一个操作能通过
RowVersion/CAS 成功。
7.4 删除会话
DELETE /v1/session
sid: {GUID}
删除没有请求体,不用于取消模型执行。Session 仍在运行或取消尚未完成时返回
409 session_busy;客户端必须先停止并等待终态,再删除。
删除成功返回 204 No Content。服务端先在数据库事务中写入 Tombstone,使新的 POST、PUT、GET
和恢复立即不可见,再清除 Redis Context、Lease 与 Stream,最后按照数据保留策略异步清理
Context、History、响应快照和关联对象。计费与安全审计记录按合规周期独立保留,不能因删除会话
破坏账务一致性。
删除操作必须验证 Session 所有权。对其他主体的 SID 返回 404 session_not_found。同一所有者在
Tombstone 保留期内重复删除返回 204,从而保持幂等;Tombstone 清理后按不存在处理。
7.5 更新 System Prompt
PATCH /v1/session
sid: {GUID}
Content-Type: application/json
{ "system_prompt": "你是订单审计助手,回答必须简洁。" }
返回 200 {"status":"updated"}。system_prompt 是会话级持久配置,写入独立的 SQL Prompt
Revision;之后被接受的 Provider 调用把冻结快照放在压缩摘要和历史之前。发送 null、空字符串或
纯空白表示清空。最大 65,536 字符。
Session 空闲、运行、取消中或结果未知时都允许更新。每个 Response 在接受事务中冻结 Prompt 快照:
- PATCH 之前已接受的 Response 继续使用旧快照,不修改正在传输的 Provider 请求;
- PATCH 成功之后接受的普通续写、PUT 编辑和 Interrupt 后继使用新快照;
- 如果用户要求当前问题立即改用新 Prompt,客户端先等待 PATCH 成功,再发起 Interrupt;
- 同 SID 并发 PATCH 在 Prompt RowVersion 上由服务端内部重试,最后成功提交的 Revision 生效;
- Prompt 写与 Executor 的 Context/终态写相互隔离,不得使正在运行的 Response 失败、结果未知或丢失 Context;
- 更新 Prompt 不修改 History,不触发模型调用或 Usage,但 Prompt Revision 变化会使旧 History Cursor 失效。
8. 非思考与深度思考
8.1 使用模型默认行为
同时省略 think 和 reasoning:
{
"model": "gpt-5.6-sol",
"input": "返回简洁答案",
"stream": true
}
服务端不注入推理强度,使用模型目录声明的默认行为。
8.2 明确关闭思考
{
"model": "gpt-5.6-sol",
"input": "返回简洁答案",
"think": false,
"stream": true
}
关闭时不得再向 Provider 发送推理强度。输出中通常不出现 reasoning Item;如果 Provider
仍返回公开 reasoning 内容,服务端可以按 Item 原样返回,但不能伪造。
8.3 开启深度思考
{
"model": "gpt-5.6-sol",
"input": "分析这个并发问题",
"reasoning": {
"effort": "high"
},
"stream": true
}
平台公开的稳定强度值为:
low
medium
high
xhigh
非法值返回 400 invalid_reasoning_effort。思考过程以 reasoning Item 和相应 delta
返回;Provider 不公开思考内容时,只返回状态及 Usage,不得构造文本。
9. 轻量流式协议
9.1 传输
stream: true 返回:
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-store
每帧只使用 data:,以空行结束:
data: {JSON}
V1 固定七种顶层事件:
start
item
delta
usage
done
error
interrupt
不使用冗长事件名,例如 response.output_item.added;不使用任何产品前缀。
9.2 Start
data: {"type":"start","id":"resp_123","model":"gpt-5.6-sol"}
sid 已经通过响应头返回,不在每帧重复。
9.3 Item Upsert
data: {"type":"item","item":{"id":"c1","type":"compaction","status":"running"}}
item 使用 Upsert 语义:相同 Item ID 首次出现为新增,后续出现为完整状态更新。
固定 Item 状态:
running
completed
failed
9.4 Delta
data: {"type":"delta","id":"m1","data":"订单表缺少联合索引。"}
id 指向已经通过 item 发布的 Item。客户端按到达顺序把 data 追加到该 Item 的输出。
9.5 Usage
data: {"type":"usage","input_tokens":15320,"output_tokens":1240,"total_tokens":16560,"cached_tokens":2600,"reasoning_tokens":430}
流中的 Usage 是截至该事件的本次请求累计值;后一个 Usage 覆盖前一个累计值。
9.6 Done
data: {"type":"done","id":"resp_123","status":"completed"}
done 只能在最终响应、Context、History 和内部 Usage 结算事实已经持久化后发送。
done.status 固定支持 completed 和 canceled。canceled 表示安全停止已经形成持久化终态,
不是单纯断开客户端连接。
9.7 Error
data: {"type":"error","code":"context_overflow","message":"The context exceeds the model limit."}
发送 error 后立即关闭流,不再发送 done。
9.8 Interrupt
data: {"type":"interrupt","id":"resp_old","status":"canceling"}
interrupt 只出现在中途补充的后继 Response 重放中,id 指被打断的旧 Response。首个事件表示
取消已接受;旧 Response 持久化终态后再次发送同类型事件,并在该终态事件中携带其最终 usage。
客户端保存该状态用于展示和账务诊断,但不把旧 Usage 当成后继 Response 的累计 Usage。
9.9 Heartbeat
在压缩、深度思考或 Provider 长时间无增量时,服务端可以发送 SSE Comment:
: ping
Heartbeat 不属于协议事件,不改变任何状态,客户端应忽略。
9.10 固定顺序
典型成功顺序:
start
item(compaction, running) 可选
item(compaction, completed) 可选
item(reasoning, running) 可选
delta(reasoning) 可选、可重复
item(reasoning, completed) 可选
item(text, running)
delta(text) 可重复
item(text, completed)
usage
done
工具调用可以代替最终 Text Item:
item(function_call, completed)
usage
done
客户端执行工具后,使用下一次 POST 提交匹配的 function_call_output。
中途补充的后继 Response 在上述普通顺序之前固定增加:
interrupt(canceling, predecessor)
interrupt(canceled | outcome_unknown, predecessor, usage)
start(successor)
...
10. 流式示例
10.1 无思考、无压缩
请求:
{
"model": "gpt-5.6-sol",
"input": "你好",
"think": false,
"stream": true
}
响应:
data: {"type":"start","id":"resp_1","model":"gpt-5.6-sol"}
data: {"type":"item","item":{"id":"m1","type":"text","status":"running"}}
data: {"type":"delta","id":"m1","data":"你好!"}
data: {"type":"item","item":{"id":"m1","type":"text","status":"completed","data":"你好!"}}
data: {"type":"usage","input_tokens":12,"output_tokens":3,"total_tokens":15,"cached_tokens":0,"reasoning_tokens":0}
data: {"type":"done","id":"resp_1","status":"completed"}
10.2 深度思考并触发压缩
data: {"type":"start","id":"resp_2","model":"gpt-5.6-sol"}
data: {"type":"item","item":{"id":"c1","type":"compaction","status":"running"}}
data: {"type":"item","item":{"id":"c1","type":"compaction","status":"completed","output":"此前会话讨论了有状态协议。","usage":{"input_tokens":12000,"output_tokens":320,"total_tokens":12320}}}
data: {"type":"item","item":{"id":"r1","type":"reasoning","status":"running"}}
data: {"type":"delta","id":"r1","data":"正在检查并发与故障恢复边界。"}
data: {"type":"item","item":{"id":"r1","type":"reasoning","status":"completed","output":"正在检查并发与故障恢复边界。"}}
data: {"type":"item","item":{"id":"m1","type":"text","status":"running"}}
data: {"type":"delta","id":"m1","data":"建议使用 Redis Lease 与数据库 RowVersion 双重保护。"}
data: {"type":"item","item":{"id":"m1","type":"text","status":"completed","data":"建议使用 Redis Lease 与数据库 RowVersion 双重保护。"}}
data: {"type":"usage","input_tokens":15320,"output_tokens":1240,"total_tokens":16560,"cached_tokens":2600,"reasoning_tokens":430}
data: {"type":"done","id":"resp_2","status":"completed"}
11. 非流式协议
stream: false 或省略 stream 时,HTTP 请求等待同一个后台响应进入终态,并返回一次 JSON。
HTTP 等待连接断开不取消后台任务,客户端仍可以通过恢复接口读取结果。
11.1 成功响应
{
"id": "resp_2",
"status": "completed",
"model": "gpt-5.6-sol",
"output": [
{
"id": "c1",
"type": "compaction",
"status": "completed",
"output": "此前会话讨论了有状态协议。",
"usage": {
"input_tokens": 12000,
"output_tokens": 320,
"total_tokens": 12320
}
},
{
"id": "r1",
"type": "reasoning",
"status": "completed",
"output": "正在检查并发与故障恢复边界。"
},
{
"id": "m1",
"type": "text",
"status": "completed",
"data": "建议使用 Redis Lease 与数据库 RowVersion 双重保护。"
}
],
"usage": {
"input_tokens": 15320,
"output_tokens": 1240,
"total_tokens": 16560,
"cached_tokens": 2600,
"reasoning_tokens": 430
}
}
11.2 失败响应
如果错误发生在 HTTP 响应开始前,返回对应 HTTP 状态和:
{
"error": {
"code": "context_overflow",
"message": "The context exceeds the model limit."
}
}
如果错误发生在流开始后,使用 error 事件。
12. 自动压缩
12.1 触发
服务端从实时模型目录读取:
context_window
max_output_tokens
context_compression_threshold
context_compression_target_threshold
在本次输入、预留输出和安全边界达到压缩阈值时,自动执行压缩。客户端不提交完整历史, 也不主动调用压缩接口。
12.2 输入
压缩输入是:
兼容的旧 Summary
+ 已闭合的较早 Exchange
以下内容不得被压缩或跨越:
- 本次新增输入;
- 最近三条用户消息;该数量由服务端统一配置,但不得在一次压缩中静默降为更小值;
- 尚未匹配
function_call_output的function_call; - 当前正在生成的输出;
- 模型和协议要求原样保留的近期内容。
服务端优先使用原会话模型,以便复用该模型已有的 Prompt Cache。超出单次安全输入预算时,较早 Exchange 按 Token 预算分块并滚动合并 Summary,每段历史只进入一个成功的压缩调用。原模型或上游失败时,服务端 按配置的压缩模型列表自动切换;客户端不需要重装或改写已有会话。
12.3 输出与状态
压缩通过 compaction Item 明确返回运行状态、可读摘要和该阶段 Usage。未发生压缩时不输出
compaction Item。
12.4 兼容性
内部压缩快照至少绑定:
模型/上下文策略指纹
Tokenizer Revision
Summary Contract Revision
压缩覆盖到的 History Sequence
模型或策略变化后,兼容则复用,不兼容则从完整 History 重新压缩。不得把不兼容摘要静默带入 新模型调用。
12.5 失败
- 压缩失败但原始 Context 仍能安全放入模型窗口:继续生成,并返回可诊断状态;
- 压缩失败且原始 Context 无法放入窗口:返回
context_compaction_failed; - 禁止静默丢弃历史来绕过窗口限制。
13. 恢复接口
13.1 路径
GET /v1/session/responses
sid: {GUID}
Accept: text/event-stream
该接口不接收请求体,也不创建新响应。
13.2 只有 SID 的恢复语义
V1 不要求客户端保存响应 ID 或事件游标。仅有 SID 时,服务端无法知道客户端已经接收了多少 字符,因此恢复采用“从当前响应开头重放”的固定语义:
- 响应正在运行:从
start开始重放已有 Item 和 Delta,然后继续实时输出; - 响应已经完成:重放完整响应直到
done,随后关闭连接; - 响应已经失败:重放直到
error,随后关闭连接; - Session 尚无响应:返回
404 response_not_found。
SDK 在重连前应清除当前未完成的回答投影,再根据重放流重新构建。该语义可能重复传输数据, 但不重复执行模型、不重复写 History、不重复结算 Usage。
13.3 客户端掉线
客户端连接关闭只结束当前观察者,不取消后台模型任务。后台任务继续推进、保存输出和结算用量。
13.4 多观察者
同一个 SID 可以同时存在多个恢复 GET。每个观察者都从当前响应开头获得相同的确定性投影。 慢观察者不得反向阻塞 Provider 或 Session Executor。
14. 历史查询
14.1 路径
GET /v1/session/history?limit=50&order={asc|desc}&cursor={opaque}
sid: {GUID}[,{GUID}...]
Accept: application/json
一个 SID 返回单会话历史;2~100 个 SID 返回这些会话的自动合并历史。客户端只声明需要读取的
SID 集合,不提交父子关系或 Sequence 映射。order 可选为 asc 或 desc,默认 asc。服务端必须验证集合中每个 SID 都属于当前
认证主体;任意一个不存在或无权访问时,整次请求返回 404 session_not_found,不得部分返回。
14.2 返回
返回按 question_at 全局稳定合并的真实 Exchange。默认按时间和内部持久行键升序;order=desc 时
两者均倒序,从最新边缘开始并沿 Cursor 向更早时间翻页。客户端不得依赖各 SID 内部 Sequence 做跨会话排序:
[
{
"sid": "20d82b37-a78e-4f91-8ed1-66f44e0c847f",
"status": "completed",
"question_at": "2026-08-27T02:28:53.1040000Z",
"question": [
{
"type": "text",
"data": "检查订单表索引"
}
],
"answer_at": "2026-08-27T02:28:59.5330000Z",
"answer": [
{
"id": "f1",
"type": "function_call",
"call_id": "call_456",
"name": "database_inspect",
"arguments": {
"table": "Orders"
}
}
]
},
{
"sid": "cb3433dd-bb67-4ccc-8852-d0c1babff3e0",
"status": "completed",
"question_at": "2026-08-27T02:29:01.1000000Z",
"question": [
{
"type": "function_call_output",
"call_id": "call_456",
"output": {
"result": "检查完成"
}
}
],
"answer_at": "2026-08-27T02:29:02.8000000Z",
"answer": [
{
"id": "m1",
"type": "text",
"data": "订单表缺少 CreatedAt 和 Status 联合索引。"
}
]
}
]
History 返回标准化后的真实输入输出,不返回 Context Summary 来代替原始内容。每个 Exchange 固定
返回其来源 sid、用户消息时间 question_at、回答终态时间 answer_at 和 status。时间使用 UTC
ISO 8601/RFC 3339 可解析格式;已取消 Exchange 的 Answer 是停止前已经
对客户端公开的部分输出。被 PUT 替换的 superseded 分支不进入默认可见投影。
默认 include_system_prompt=false 时,响应体始终保持上述友好数组,不为分页增加外层 Envelope。
分页元数据固定放在响应头:
has-more: true
next-cursor: djE6Mjoy
Access-Control-Expose-Headers: sid, next-cursor, has-more
limit默认 50,范围 1–100;order只允许asc或desc,默认asc;非法值返回400 invalid_order;- 首次不传
cursor;has-more: true时使用next-cursor获取下一页; - Cursor 是不透明值,客户端不得解析或自行构造;
- 页内及跨页均保持请求的
order;Cursor 使用时间与内部行键组成的排他边界,不重复上一页末项; - Cursor 绑定完整 SID 集合、排序方向、时间线模式及每个 Session Revision;续页更换排序方向或时间线模式时返回
400 invalid_cursor;更换 SID 集合、PUT 编辑任一会话或更新任一 System Prompt 会使历史快照失效并返回409 cursor_stale,客户端丢弃已拼接分页并从无 Cursor 的第一页重新读取; has-more: false时不返回next-cursor。
聊天时间线应使用 order=desc 取得最近一页,把单页内容按时间正序展示;用户上拉时使用原样 Cursor
请求更早一页,并将其插入现有内容前面。这样首屏和网络分页都不需要扫描最旧页面。
显式 include_system_prompt=true 时切换到可选时间线表示:
{
"history": [],
"system_prompts": [
{
"sid": "20d82b37-a78e-4f91-8ed1-66f44e0c847f",
"revision": 3,
"system_prompt_at": "2026-08-27T04:32:10.0000000Z",
"system_prompt": "你是订单审计助手。"
}
]
}
limit 约束两个数组的总行数。查询必须先把可见 SessionContents 与 SessionPromptHistory 按
事件时间 + 类型 + 内部行键 合并、排序和 Keyset 分页,再分别投影到两个数组;不能分别各取 limit
行。该模式使用独立 Cursor 版本,Cursor 绑定 SID 集合、Prompt Revision 与 Content Revision,不能和
默认 History Cursor 混用。system_prompt: null 表示清空,所有记录包含所属 sid 和服务端 UTC
时间。内部 Context 和 Prompt State 只保留最新值,不装入 Prompt 历史数组。
正在运行但尚未形成终态的 Exchange 不进入 History;通过恢复接口读取其当前输出。模型调用形成
completed 或 canceled 终态后,该 Exchange 才对 History 可见。只有 completed Exchange 才能
进入后续模型 Context;取消产生的部分 Answer 不得作为完整回答注入下一次模型调用。
14.3 大历史
虽然客户端只提交 SID 集合,服务端仍必须按 QuestionAt + 内部行键 使用 Keyset 分页,只读取
limit + 1 行来判断是否还有下一页,禁止一次加载全部 Session History。查询索引至少覆盖
SessionId + SupersededAt + QuestionAt + Id。分页前后都要核对 SID 集合的 Revision 摘要,避免编辑
与分页并发时返回混合版本。
带 Prompt 的时间线查询同样只能读取合并后的 limit + 1 行;Prompt History 索引至少覆盖
SessionId + ChangedAt + Id。不能在每一页重复返回全部 Prompt 历史。
15. 持久化模型
V1 使用 Context、Prompt State、Prompt History、Content、Response 和 Invocation 六类持久事实。
15.1 SessionContexts
一会话一行,保存模型真正使用的热 Context:
Sid uniqueidentifier PK
ApiKeyId uniqueidentifier
OwnerUserId uniqueidentifier nullable
OwnerTenantId uniqueidentifier nullable
Status nvarchar(32)
LastSequence bigint
Revision bigint
ActiveResponseId uniqueidentifier nullable
Version rowversion
Context varbinary(max)
UpdatedAt datetime2
ExpiresAt datetime2 nullable
DeletedAt datetime2 nullable
Context 文档:
{
"summary": "此前会话讨论了有状态模型协议。",
"qa": [
{
"q": [
{
"type": "text",
"data": "继续完成任务"
}
],
"a": [
{
"type": "text",
"data": "任务已经完成。"
}
]
}
]
}
Context 必须有字节、Item、Exchange 和 Token 硬上限。大型文件、图片和巨型工具输出只能保存 受权限保护的引用及有界摘要。
15.2 SessionPromptStates
一会话至多一行,保存当前 Prompt 与独立并发版本:
Sid uniqueidentifier PK/FK
SystemPrompt nvarchar(max) nullable
Revision bigint
Version rowversion
UpdatedAt datetime2
Prompt State 是当前 Prompt 的 SQL 真相,且只保留最新值。SessionContexts.ContextJson 不保存 Prompt
快照或 Prompt 历史。现有会话若仍有旧版本兼容字段,服务端在首次继续或 PATCH 时读取一次,将它迁入
Prompt State 后从 Context 移除;组装 Provider 请求时始终使用 Response 接受时冻结的 Prompt 快照。
Prompt 单独使用 RowVersion,使运行中 PATCH 不与 Executor 更新 SessionContexts 的状态、摘要或 Q/A 发生行级乐观并发冲突。删除 Session 时 Prompt State 级联删除。
15.3 SessionPromptHistory
每次 Prompt 的有效值发生变化时追加一行不可变事实:
Id bigint identity PK
Sid uniqueidentifier
Revision bigint
SystemPrompt nvarchar(max) nullable
ChangedAt datetime2
UNIQUE (Sid, Revision)
SystemPrompt = null 表示清空。重复 PATCH 相同目标值是幂等 no-op,不新增 Revision 或历史行。Prompt
State 与 Prompt History 必须在同一串行化事务中更新;内部 Prompt State 只保留最新有效值,Context
不复制 Prompt 值或完整 Prompt 历史。索引至少覆盖 Sid + ChangedAt + Id,用于单/多 SID 时间线
Keyset 分页。
15.4 SessionContents
保存全部终态 Exchange 及被编辑替换的 Revision,不因压缩而修改:
Id bigint identity PK
Sid uniqueidentifier
Sequence bigint
Revision bigint
Status nvarchar(32)
Question varbinary(max)
Answer varbinary(max)
QuestionAt datetime2
CreatedAt datetime2
CompletedAt datetime2
SupersededAt datetime2 nullable
唯一索引:
UNIQUE (Sid, Sequence, Revision)
QuestionAt 是用户 Input 被接受的服务端 UTC 时间,CompletedAt 是 Answer 进入终态的 UTC 时间。
建议使用 Id bigint identity 作为聚集键,避免随机 GUID 在大历史表中产生持续页分裂,并作为相同
QuestionAt 下的稳定分页平局键;公开协议不暴露该 Id。
15.5 SessionResponses
保存当前和最近响应的执行状态与有界重放快照,不承担完整 History 职责:
Id uniqueidentifier PK
PublicId nvarchar(64) unique
Sid uniqueidentifier
PredecessorResponseId uniqueidentifier nullable, unique when non-null
Sequence bigint
Revision bigint
InvocationIdentity uniqueidentifier unique
Status nvarchar(32)
Model nvarchar(200)
Request varbinary(max)
Question varbinary(max)
Output varbinary(max)
ReplaySnapshot varbinary(max)
Usage token counters
ErrorStatusCode int nullable
LeaseEpoch bigint
Version rowversion
StartedAt datetime2 nullable
CancelRequestedAt datetime2 nullable
CompletedAt datetime2 nullable
ReplaySnapshot 必须有硬上限和终态保留期;长期真实内容仍由 SessionContents 保存。
Request 内部保存接受时冻结的 Prompt 快照。该内部字段不得出现在公开 DASP 请求、响应或下游
Provider 请求中;它只用于故障恢复、重新领取与并发一致性。
15.6 SessionInvocations
每次真实 Provider 调用一行,包括 Generation 和 Compaction:
Id uniqueidentifier PK
Sid uniqueidentifier
ResponseId uniqueidentifier
InvocationIdentity uniqueidentifier unique
Kind generation | compaction
Status accepted | running | completed | canceled | failed | outcome_unknown
InputTokens bigint
OutputTokens bigint
CachedTokens bigint
ReasoningTokens bigint
CompletedAt datetime2 nullable
该表是 Provider 调用、Usage 和内部结算的幂等事实。Session 删除后可以按合规周期保留最小账务事实, 但不得保留无必要的会话正文。
15.7 原子性
请求接受和终态写入必须分别使用数据库事务:
接受:验证 Session + 占用 Running 状态 + 冻结新输入与当前 Prompt 快照
Prompt 切换:校验所有权 + 更新最新 Prompt State + 追加不可变 Prompt History
中途补充:取消旧 Response + 创建唯一后继 + 切换 ActiveResponseId
终态:追加 SessionContents + 更新 SessionContexts + 写 Usage 事实 + 清除 Running 状态
关键写入必须全部成功或全部回滚。Redis 只能在 SQL 提交成功后更新。
16. Redis 热状态
Redis 只服务活跃 Session,不保存全部历史,也不是最终真相来源。
建议内部 Key:
session:{ownerScope:sid}:context
session:{ownerScope:sid}:lock
session:{ownerScope:sid}:stream
这些 Key 不属于公开协议。
职责:
context:热摘要、近期 Exchange、Session Revision 和 Active Response 快照;当前 Prompt 不以 Redis 为真相,每个 Response 使用 SQL 接受事务冻结的快照;lock:带随机 Token、TTL 和续租的执行 Lease;stream:当前响应的短期重放和实时通知;每条内部事件带单调 Index,观察者只增量读取。
Redis Stream 必须设置有界长度和终态 TTL。裁剪、Redis 重启或事件 Index 出现空洞时,观察者按 已经投影的事件数从 SQL ReplaySnapshot 补齐,不重新调用 Provider。Redis Pub/Sub 可以用于唤醒 观察者,但不能作为唯一重放来源。
缓存 Miss 从 SQL 恢复;Redis 丢失不得导致历史、最终响应或内部 Usage 结算丢失。
若现有通用缓存接口只支持 Get/Set,应为 Session Runtime 增加专用的 Context、Lease 和 Stream 抽象,不把阻塞读取、Lua 续租和 Stream 语义塞进普通缓存接口。
17. 执行与并发
17.1 HTTP 与任务解耦
正确执行链路:
POST
-> SQL 持久接受
-> 后台 Executor 领取
-> 上下文加载/压缩
-> Provider 调用
-> Redis 发布增量
-> SQL 保存恢复快照
-> SQL 原子终态
HTTP/SSE 只是观察者。禁止把 HttpContext.RequestAborted 直接等同于模型任务取消。
17.2 单写者
Redis Lease 用于减少节点竞争,SQL rowversion 用于最终并发裁决。仅依赖 Redis Lock 不足以
保证一致性。
执行器必须持有当前 Session Revision 和 Lease Epoch。旧执行器失去 Lease 后,即使重新运行也 不能覆盖新执行器结果。
Interrupt 的后继 Response 通过唯一 PredecessorResponseId 建立单向交接链。Executor 必须先确认
前驱已经形成终态且同 SID Lease 已释放,才能把后继从 accepted 改为 running;进程在终态提交
与入队之间退出时,恢复扫描从 SQL 重新发现后继,不能依赖单机内存队列。
17.3 多 Session 扩展
单个 SID 内串行;不同 SID 完全并行。水平扩展按 SID 分散到多个 Executor,避免全局 Session 热点。
18. 故障转移
18.1 客户端掉线
- 任务继续;
- 输出继续进入热 Stream 和周期性恢复快照;
- GET 恢复不触发新 Provider 调用;
- 不新增 History,不重复结算。
18.2 Executor 节点宕机
- Lease 到期后其他节点接管;
- 新节点读取 SQL 中的 Running Session、Active Response 快照和已结算调用;
- 已结算调用直接重放;
- Provider 支持 Idempotency 时使用同一内部 Invocation Identity 重试;
- Provider 无法确认结果时进入
outcome_unknown,不得假定调用未发生并静默重复收费调用。
上游网络流无法跨进程继续。故障转移保证业务状态可判定和不会重复结算,不承诺恢复原 TCP 连接。
当前生产默认使用 60 秒可续租 Lease、60 秒 Running 陈旧窗口、5 秒恢复扫描和 60 秒容器优雅停机
窗口。正常重启应在停机窗口内把在途调用收口;进程被强制终止时,在最后一次心跳后的目标恢复
上界为约 65 秒。Provider 不支持跨进程恢复时,客户端会收到确定性的
provider_outcome_unknown,服务端不得为了“继续输出”自动重复一次可能已经计费的调用。
18.3 Redis 故障
- SQL 仍为真相来源;
- 使用 SQL CAS 保持单写;
- 新调用可以降级从 SQL 读取 Context;
- 实时恢复能力允许暂时降级,但最终结果和内部 Usage 不得丢失;
- Redis 恢复后按活跃 Session 重新预热。
18.4 数据库故障
数据库不可用时不得仅写 Redis 后调用收费模型。新 POST 返回:
503 session_storage_unavailable
已有执行器只能在不破坏终态和结算一致性的范围内短暂等待数据库恢复;超过上限后进入可诊断 失败状态。
18.5 Provider 故障
Provider 重试必须使用稳定的内部调用身份。只有能够证明未结算或 Provider 支持幂等的调用才可 自动重试。失败不得导致 Context 静默缺项。
19. 性能与运行效率
19.1 热路径
模型热路径只读取:
Redis 热 Context
或 SessionContexts 一行
禁止为每次模型调用扫描 SessionContents 全历史。
19.2 流式写入
禁止每个 Token 重写 SQL 大字段。文本增量先进入有界进程缓冲和 Redis Stream,在以下任一条件 满足时批量写恢复快照:
累计 200~500ms
累计 16~64KiB
出现关键协议项
进入终态
function_call、压缩完成、失败和最终响应属于关键事实,应立即持久化。
19.3 历史规模
SessionContexts 始终有界;SessionContents 可以按 QuestionAt 时间分区、归档或迁移到冷存储。完整历史量级
不应影响模型调用延迟。
大对象保存引用,不直接嵌入历史行。删除大 Session 时使用异步分批清理,避免大范围级联锁。
19.4 缓存规模
Redis 只缓存正在运行、近期访问或处于恢复窗口的 Session。禁止预热全部历史 Session。Redis 容量应取决于同时活跃会话数,而不是历史会话总数。
当 Session Stream 对现有 Rate Limit、验证码或普通缓存造成明显内存竞争时,应使用独立 Redis 实例或集群。
20. Usage 与内部计费
20.1 公开协议
公开协议只返回 Token Usage,不返回:
billing
amount
currency
receipt_id
内部价格
最终 Usage 汇总本次响应真实发生的全部模型调用,包括必要的压缩和正式生成。压缩 Item 同时 返回本阶段 Usage,便于客户端解释额外 Token 消耗。
流式与非流式使用同一个轻量 Usage 结构:
{
"input_tokens": 15320,
"output_tokens": 1240,
"total_tokens": 16560,
"cached_tokens": 2600,
"reasoning_tokens": 430
}
20.2 内部结算
服务端仍必须按实时模型目录和 Provider Usage 执行标准计费:
- 压缩调用和正式生成分别记录,再聚合到本次响应;
- Cached Token 按模型目录定义结算;
- Reasoning Token 按 Provider 规则归入对应计费项,不重复计算;
- 每个 Provider 调用具有稳定内部 Invocation Identity;
- 同一 Invocation 最多结算一次;
- GET 恢复、GET History、事件重放不产生模型费用;
- Provider 调用前失败不计模型费用;
- 客户端重复观察不得导致重复余额扣减;
- 内部 Usage、调用日志和余额事务必须能够通过稳定请求身份审计对应。
计费是服务端实现责任,不进入公开 Session 协议。
21. HTTP 状态与稳定错误码
V1 至少固定以下错误:
| HTTP | Code | 含义 |
|---|---|---|
| 400 | invalid_request |
请求 JSON 或字段不合法 |
| 400 | invalid_sid |
SID 非 GUID |
| 400 | invalid_reasoning_effort |
推理强度非法 |
| 400 | invalid_file_url |
文件 URL 非公开 HTTP(S) 地址或解析到受限网络 |
| 400 | invalid_cursor |
History Cursor 非法或版本不支持 |
| 400 | invalid_limit |
History Limit 超出 1–100 |
| 400 | invalid_order |
History Order 不是 asc 或 desc |
| 400 | invalid_system_prompt |
System Prompt 超过固定上限 |
| 400 | function_call_mismatch |
Tool Output 无匹配 Call |
| 404 | session_not_found |
Session 不存在或无权访问 |
| 404 | response_not_found |
Session 尚无可恢复响应 |
| 404 | model_not_available |
本次请求模型不存在或不可用 |
| 409 | session_busy |
同一 SID 已有运行任务 |
| 409 | no_editable_message |
当前投影没有可编辑的用户输入 |
| 409 | cursor_stale |
分页期间会话 Revision 已变化,必须重新读取 |
| 429 | rate_limit_exceeded |
调用频率或并发超限 |
| 500 | context_compaction_failed |
必须压缩但压缩失败 |
| 500 | provider_outcome_unknown |
上游调用结果无法确认 |
| 500 | session_internal_error |
未分类的服务端会话错误;响应不泄露内部实现细节 |
| 502 | provider_error |
Provider 明确失败 |
| 502 | file_analysis_failed |
普通模型所需的自动文件分析失败 |
| 503 | session_storage_unavailable |
持久存储不可用 |
| 503 | session_unavailable |
灰度或保护开关暂停接受新响应 |
| 504 | provider_timeout |
Provider 调用超时 |
错误码含义一经发布不得改变。新增错误码必须保持旧客户端可以按 HTTP 状态和通用 error
处理。
22. 客户端最小实现
普通客户端只需:
- 首次 POST 时不传 SID;
- 从响应头保存 SID;
- 后续 POST 带 SID 和新增
input; - 流式响应处理七种顶层事件;
- 连接断开时用同一 SID 调用恢复 GET,并替换当前未完成回答;
- 需要完整历史时调用 History GET。
需要编辑和删除能力的客户端再实现:
- 停止时调用 Cancel POST,并等待
done(status=canceled); - 中途补充时调用 Interrupt POST,由一个流观察旧响应终态和新响应;
- 编辑上一条消息时,在 Session 空闲后调用 PUT;
- 删除前确保 Session 已终态,再调用 Session DELETE。
伪代码:
switch (event.type) {
case "start":
begin(event)
break
case "item":
upsert(event.item)
break
case "delta":
append(event.id, event.data)
break
case "usage":
updateUsage(event)
break
case "done":
complete(event)
break
case "error":
fail(event)
break
case "interrupt":
trackInterrupt(event)
break
}
SDK 应自动处理 SID 本地持久化、重连、当前回答替换、Heartbeat 和未知 Item 忽略。客户端不需要 保存 History、Context、Summary、Token Boundary 或 Provider 信息。
23. 协议演进规则
V1 发布后遵守:
- 不改变既有路径及其 HTTP 操作的既有语义;
- 不改变 SID Header 名称和 GUID 格式;
- 不删除或重命名已有字段;
- 不改变七种顶层事件含义;
- 不改变既有 Item 类型含义;
- 不改变恢复“从当前响应开头重放”的语义;
- 不把取消、编辑或删除合并为同一种操作;
- 可以增加可选字段和新的 Item 类型;
- 客户端必须忽略未知可选字段和未知 Item 类型;
- 破坏性变更发布
/v2/session/*; - Context 和持久化文档使用独立 Schema Revision,以支持服务端内部迁移而不改变公开协议。
24. 实现目标
实现完成后必须同时达到以下目标,不能只做到接口可调用:
24.1 客户端目标
- 首次调用无需 SID,服务端创建并通过响应头返回;
- 续写只提交 SID、新 Input 和本次 Model Config;
- 客户端不保存完整历史,不计算上下文窗口,不发起压缩;
- 流与非流使用相同 Item、终态和 Usage 语义;
- 页面刷新或网络中断后,只凭 SID 恢复当前响应;
- 运行中可以切换 Prompt,无需停止当前流;当前响应与后续响应的生效边界确定;
- 停止、中途补充、编辑最近消息和删除是四个可预测且互不混淆的操作;
- SDK 对未知可选字段和 Item 前向兼容。
24.2 服务端目标
- SQL 保存真相,Redis 只加速活跃会话;
- 单 SID 串行、多 SID 水平扩展;
- Context 始终有界,History 规模不进入模型热路径;
- HTTP 观察者与后台执行生命周期解耦;
- 节点、Redis 和网络故障不造成双重执行、上下文倒退或重复结算;
- 压缩、Reasoning、工具调用、取消和编辑均能还原为确定性投影;
- Prompt 使用独立 Revision,并在 Response 接受时冻结;Prompt 切换与 Context 终态写互不覆盖;
- 公开协议只暴露标准 Token Usage,价格与结算保持内部化;
- 删除满足用户数据清理需求,同时保留法规要求的最小账务和安全事实。
24.3 运行目标
- 无压缩热请求只读取一次 Redis;Cache Miss 最多读取一行 SessionContexts;
- 增量不逐 Token 写数据库;
- 恢复首帧可以从 Redis 或 SQL 持久快照产生,不依赖原 Executor 存活;
- Session 数量增长主要增加冷 History 存储,不线性增加 Redis 常驻内存;
- 所有终态、Usage 和余额变化可通过内部 Invocation Identity 审计。
25. 实现原则
以下原则优先于局部实现便利:
- 协议与实现分离:公开字段不暴露数据库键、租约、Provider、价格或产品前缀;
- SQL 是真相源:Redis、进程内状态和 SSE 连接都可以丢失并重建;
- 先持久化再收费调用:请求未形成可恢复的接受事实前,不调用收费模型;
- 单写多读:一个 SID 只有一个 Executor 写入,可以有多个观察者读取;
- 终态唯一:完成、取消和失败竞争时,只允许一个数据库终态获胜;
- 事实不可覆盖:编辑使用 Revision 和
superseded,不原地篡改已经结算的调用; - 投影可以重建:Context、恢复流和用户 History 都必须能从持久事实重建;
- 上下文有界、历史完整:压缩只改变 Context,不破坏当前 Revision 的真实 History;
- 工具链闭合:Function Call 与 Output 作为不可拆分边界处理;
- 取消不是断线,删除不是取消:三者的状态机和权限检查完全独立;
- 至少一次领取、至多一次提交和结算:允许任务重新领取,不允许重复终态与扣费;
- 背压优先于失控:慢客户端、慢 Redis 或数据库抖动时使用有界缓冲并明确失败;
- 默认安全:SID 必须叠加认证主体和租户范围,错误不得泄露其他会话存在性;
- 兼容优先:不改变现有无状态
/v1/responses,有状态能力通过独立路径提供; - 可观测而不泄密:日志记录身份、阶段和计数,不记录密钥、隐藏思维链或无界正文。
26. 当前项目的实现边界
结合现有分层,建议按职责落位,不把完整 Session Runtime 堆进单个 Middleware:
AnyAPI.Gateway
Session Controller / SSE Observer / Authentication Boundary
AnyAPI.Application.Proxy
Session Command Handler / Context Builder / Compaction Orchestrator
Response Projector / Cancel and Edit Coordinator
AnyAPI.Domain.Proxy
Session Aggregate / Active Response State Machine / Revision Rules
Context and History Contracts / Invocation Identity
AnyAPI.Infrastructure.Data
SessionContexts / SessionContents / ActiveResponses / Usage Fact
EF Configuration / Migration / CAS Repository
AnyAPI.Infrastructure.Caching
Session Context Cache / Lease / Replay Stream / Distributed Notification
AnyAPI.Gateway existing pipeline
Authentication / Rate Limit / Model Route / Reasoning Contract
Context Guard / Retry / Circuit Breaker / Provider Normalization
AnyAPI.Application.Statistics and Billing
Idempotent Usage Fact Consumer / Internal Settlement / Audit
实现约束:
- Controller 只解析协议、返回 SID 和挂接观察者,不直接拼装 Context;
- Middleware 可以复用现有认证、路由、Reasoning、重试和响应标准化能力,但不得持有 Session 真相;
- Session Executor 是独立后台服务,不能依赖原始
HttpContext; - 普通
RedisCacheService保持简单 KV 语义;Lease、Stream、阻塞观察和 Lua CAS 使用专用接口; - Billing 只消费已经持久化的 Usage Fact,不解析公开 SSE 来扣费;
- Provider Adapter 负责把统一 Item 转换为具体上游协议,并保留稳定 Invocation Identity。
建议的核心内部接口:
ISessionCommandService
ISessionQueryService
ISessionRepository
ISessionContextStore
ISessionLeaseManager
ISessionReplayStore
ISessionExecutor
IContextBuilder
IContextCompactor
IResponseProjector
IProviderInvocationClient
IUsageFactWriter
这些名称是实现建议,不属于公开协议;可以调整名称,不能合并掉对应责任边界。
26.1 当前代码落位
当前项目已经按上述责任完成第一版落位:
AnyAPI.Gateway/Controllers/SessionController.cs
固定 HTTP 路径、SID Header、SSE、Heartbeat、流与非流输出
AnyAPI.Gateway/StatefulSessions
Command / Query / bounded Queue / Executor / Compactor / Cleanup / Telemetry
AnyAPI.Domain.Proxy/Entities/StatefulSession.cs
SessionContext / SessionContent / SessionResponse / SessionInvocation
AnyAPI.Infrastructure.Data
EF Configuration / Brotli varbinary 转换 / Stateful Session 与 History 时间戳 Migration
AnyAPI.Infrastructure.Caching
Context Cache / Compare-token Lease / bounded Replay Stream
真实 Provider 调用以稳定 Invocation Identity 重新进入现有 /v1/responses 管线,因此继续复用
认证、模型路由、Provider 适配、Usage 日志和标准内部结算。公开 /v1/session/* 只是控制与聚合
请求,不二次计费;内部 Session Header 在鉴权后使用,并在转发和日志前删除或脱敏。
主要运行参数位于 Session 配置节:
Enabled / AcceptNew
WorkerBaseUrl / InternalWorkerToken
MaxConcurrentExecutions
ContextCacheHours / ReplayHours / TombstoneHours
LeaseSeconds / RecoveryScanSeconds / RunningStaleSeconds
ProviderTimeoutSeconds / CancellationPollMilliseconds
SnapshotMilliseconds / MaxOutputBytes
AcceptNew=false 停止 POST/PUT/Interrupt 接受;已有 Session 的恢复、History、Cancel 和 Delete 继续可用。
通用 HTTP Retry 和公共 Circuit Breaker 不重放 Session 控制请求,Provider 内部调用仍使用原有模型
保护链路。
27. 实施节奏
按以下阶段递进,每一阶段都必须能够独立验证和回滚。不得先上线自动压缩或故障接管,再补终态 幂等和 Usage 唯一性。
27.1 阶段零:契约冻结与容量基线
- 冻结本文件中的路径、Header、Item、事件、状态和错误码;
- 为所有请求、流帧和 History 建立 Golden Contract;
- 记录现有
/v1/responses延迟、错误率、Token Usage 和吞吐基线; - 确认模型目录可提供窗口、Tokenizer、Reasoning 和压缩能力信息;
- 输出数据库、Redis 和单响应最大尺寸预算。
完成门槛:契约测试可在没有真实 Provider 时完全运行,现有无状态测试无回归。
27.2 阶段一:持久化与基本非流式闭环
- 创建 SessionContexts、SessionContents、ActiveResponses 和 Invocation/Usage 唯一约束;
- 实现 SID 创建、所有权、POST 非流式、GET History;
- 实现单节点 Executor 和 SQL RowVersion/CAS;
- 暂不启用 Redis、自动压缩、恢复追流、PUT、Interrupt、Cancel 和 Delete;
- 使用 Feature Flag,仅内部测试主体可访问。
完成门槛:进程重启后可查询已完成 History;并发 POST 只有一个成功;Usage 不重复。
27.3 阶段二:流式、持久快照与恢复
- 实现七种流事件、Heartbeat、有界增量缓冲和批量快照;
- HTTP 观察者与 Executor 解耦;
- 实现 Redis Replay Stream 和 SQL 快照回退;
- 实现仅凭 SID 从头重放并继续追流;
- 验证刷新、断网、多观察者和慢消费者。
完成门槛:杀掉客户端连接不停止任务;重连不触发第二次 Provider 调用或结算。
27.4 阶段三:工具、思考与自动压缩
- 完成统一 Item 到各 Provider 的双向映射;
- 支持默认、关闭及四档 Reasoning;
- 落实 Function Call 闭合边界;
- 实现 Context Token 预算、自动压缩、兼容指纹和压缩 Usage;
- 验证模型切换时 Context 复用或重建。
完成门槛:压缩前后当前 Revision History 一致;工具链不被拆断;流与非流最终投影一致。
27.5 阶段四:停止、中途补充、编辑和删除
- 实现安全 Cancel 状态机和 Provider Cancel Adapter;
- 实现 PUT 的原子剪切、Revision、Context 重建与重新生成;
- 实现 Tombstone、缓存失效和异步数据清理;
- 验证完成/取消竞争、取消/编辑顺序和运行中删除拒绝;
- 对外发布前完成数据保留与合规评审。
完成门槛:三种操作互不替代;任何竞争只形成一个当前投影和一份结算事实。
27.6 阶段五:多节点故障转移与容量验证
- 实现 Redis Lease、续租、Epoch/Fencing 和 SQL 最终裁决;
- 实现 Running Session 扫描、失租接管和
outcome_unknown; - 注入 Executor、Redis、SQL 和 Provider 故障;
- 运行峰值并发、超大 History、长 Reasoning、工具风暴和重连风暴压测;
- 确认 Redis 与普通限流/验证码缓存的资源隔离。
完成门槛:旧 Epoch 无法提交;节点故障不重复结算;容量达到发布目标并留有安全余量。
27.7 阶段六:灰度与稳定发布
- 按内部主体、低风险租户、百分比逐级启用;
- 灰度期间同时观察协议错误、恢复成功率、重复 Invocation、上下文重建和余额对账;
- 每一级至少覆盖一个完整业务高峰与故障演练窗口;
- 指标异常时只关闭新 Session 接受,不破坏已有 Session 查询、恢复和删除;
- 达到全部门槛后标记 V1 Stable,之后只做向后兼容扩展。
28. 工程细节、运行指标与发布控制
28.1 状态机
Active Response 至少包含:
accepted -> running -> completed
-> canceling -> canceled
-> failed
-> outcome_unknown
状态变更必须使用条件更新;任何终态不能回到 Running。outcome_unknown 是需人工或对账任务处理的
受控状态,不得自动当作 Failed 重试。
28.2 唯一键和幂等
至少建立:
UNIQUE (Sid, Sequence)
UNIQUE (Sid, ActiveRunningMarker) where running
UNIQUE (InvocationIdentity)
UNIQUE (InvocationIdentity, UsageFactType)
内部 Invocation Identity 在接受事务中生成,Provider 重试、Usage、结算和日志全部复用。公开协议
无 SID 的首次创建允许客户端提供 Idempotency-Key。服务端按认证主体、Key 和规范化请求摘要原子绑定
首个 SID/Response;相同摘要重放同一结果,不同摘要返回 idempotency_conflict。已有 SID 的续写、
打断、编辑和 Function Output 不接受该键,仍由单写者与恢复协议处理结果未知。
28.3 数据与迁移
- 迁移先创建可空字段和索引,再启用写路径,最后收紧约束;
QuestionAt迁移先以可空列加入,优先用同 Session/Sequence/Revision 的 ResponseCreatedAt回填,旧数据缺少 Response 时退化到 ContentCreatedAt,回填完成后再收紧为非空;- 大表索引使用在线或低锁策略;
- Context、Question、Answer 和快照都带内部 Schema Revision;
- 二进制 JSON 使用明确压缩格式和最大解压尺寸,防止压缩炸弹;
- Tombstone 与审计记录设置独立保留周期;
- 数据删除任务必须可断点、可重试、限速并记录删除范围。
28.4 安全与隐私
- SID 使用密码学安全 GUID,不可枚举且不得记录在公开错误中;
- 每次操作都重新校验认证主体和租户,不能相信 Redis 中孤立的 Owner 信息;
- Session 正文静态加密,服务间传输使用 TLS;
- 公共 Session 控制请求日志只记录 SID 哈希、Response ID、阶段、允许列出的模型配置、输入项数量、
字节数和 Token 数;
input正文、工具输出与未知扩展字段一律脱敏; - 工具输出执行大小、内容类型和恶意指令检查;
- History 和恢复响应使用
Cache-Control: no-store; - 删除、编辑和取消写安全审计,但审计不复制完整正文;
- Reasoning 永远不返回隐藏思维链。
28.5 限额与背压
至少配置以下硬限制:
每主体并发 Session 数
单 SID 单次 Input 字节数和 Item 数
单响应输出字节数
单工具输出字节数
Context 解压后字节数和 Token 数
Redis Stream 长度与 TTL
History 单次传输最大持续时间
观察者数量和单观察者缓冲
取消等待与 Provider 超时
达到限制必须返回稳定错误或安全截断策略,不能无限分配内存。客户端慢于有界缓冲时断开该观察者, 后台任务继续,客户端随后可以从头恢复。
28.6 SLO 与核心指标
发布前为每个环境设定具体阈值,至少监控:
POST 接受延迟 p50/p95/p99
首个可见 Item/Delta 延迟
恢复首帧延迟与恢复成功率
Session Busy、Cancel、Compaction 和 Provider 错误率
Context Cache Hit Ratio 与重建耗时
Redis Stream、进程缓冲和 SQL 快照大小
Running Session 年龄与 Lease 接管次数
Invocation 重复拒绝数与 outcome_unknown 数
Usage Fact、Provider Usage、余额事务三方差异
History 查询延迟与传输字节数
多 SID History 数量、合并查询延迟与 cursor_stale 率
删除积压、Tombstone 数和清理失败数
所有 HTTP 请求接受 GUID 格式 X-Request-Id,响应原样返回该值;缺失或非法时由服务端生成。
从请求开始计时,耗时严格大于 500ms 的请求必须以 Warning 级别写入结构化 SlowRequest 日志,
至少包含同一 Request ID、方法、路径、状态码、耗时和阈值。日志不得包含 API Key、System Prompt、
Question、Answer、文件分析正文或隐藏思考过程。端到端报告以 Request ID 对慢操作逐条核对。
告警必须按“用户可见故障、数据一致性风险、成本风险、容量风险”分级。重复 Invocation、重复结算、 旧 Epoch 提交和余额对账差异属于高优先级告警。
28.7 灰度、开关与回滚
功能开关至少区分:
新 Session 接受
流式恢复
自动压缩
Cancel
Edit
Delete 清理 Worker
多节点接管
Provider 适配器
关闭新 Session 接受后,已有 Session 的 GET、History、Cancel 和 Delete 必须继续可用。回滚应用版本 前先确认数据库 Schema 向后兼容;不得通过回滚删除新表或丢弃 Running Session。协议已经发出的 字段和事件不能由 Feature Flag 改变含义。
28.8 完成定义
一项能力只有同时满足以下条件才算完成:
- 协议契约、领域状态机和持久化事务均已实现;
- Unit、Integration、Contract、并发、故障注入和性能测试通过;
- 指标、日志、Tracing、Dashboard 和告警可用;
- 数据迁移、灰度、回滚和运维 Runbook 已演练;
- Usage 与内部结算完成对账;
- 安全、隐私和数据删除检查通过;
- 文档示例与实际响应逐字节契约测试一致。
29. 客户端接入规范
本章是面向客户端、SDK 和业务应用开发者的摘要模块。可独立交付、评审和版本化的完整客户端文档
位于 stateful-session-client-guide-v1.md,该文件是客户端
实现的接入入口。客户端不需要理解数据库、Redis、Lease、结算和 Executor 实现。
29.1 客户端职责边界
客户端负责:
- 保存当前业务会话对应的 SID;
- 每次只提交新增 Input 和本次 Model Config;
- 把流事件投影为界面内容;
- 网络中断后使用相同 SID 恢复;
- 执行本地或业务工具,并提交 Function Call Output;
- 根据用户操作调用停止、中途补充、编辑和删除接口。
客户端不负责:
- 拼接完整模型历史;
- 保存或上传 Context Summary;
- 计算 Context Window 和压缩阈值;
- 重新提交断线前的 POST;
- 根据金额自行推算结算结果;
- 保存 Provider、Lease、Epoch、Revision 或内部 Invocation Identity。
29.2 最小接入流程
首次对话:
POST /v1/session/responses
无 sid Header
Body: { model, input, stream }
客户端从响应头读取并保存:
sid: 20d82b37-a78e-4f91-8ed1-66f44e0c847f
后续续写:
POST /v1/session/responses
sid: 20d82b37-a78e-4f91-8ed1-66f44e0c847f
Body: { model, input, stream }
不得把 GET History 返回值再次拼入 POST Input,否则会导致上下文重复和额外 Token 消耗。
29.3 SID 的本地保存
- Web 应用应把 SID 绑定到自身业务 Thread ID,而不是设置为全站唯一全局变量;
- 页面刷新后允许从受控持久存储恢复 SID;
- SID 不能代替登录态或 API Key,也不能出现在 URL Query、Analytics、Referer 或公开日志中;
- 用户切换账号或租户时必须清空旧主体的 SID 映射;
- 删除成功后立即删除本地 SID;
- 收到
404 session_not_found时不得无限重试旧 SID,应由业务决定创建新会话还是提示用户。
浏览器必须能够读取响应头 sid;服务端通过 Access-Control-Expose-Headers: sid 暴露该 Header。
29.4 客户端本地状态机
建议每个 SID 维护以下 UI 状态:
idle
submitting
streaming
reconnecting
canceling
interrupting
editing
deleting
completed
failed
状态约束:
streaming期间普通 POST/PUT 返回 Busy;用户明确中途补充时只调用 Interrupt;canceling和interrupting期间不允许再次 POST、PUT 或 Interrupt;canceling只有收到done(canceled)或恢复查询确认终态后才能进入 idle;- 网络错误从 streaming 进入 reconnecting,不进入 failed,也不重新 POST;
- 协议
error才表示当前响应失败; - 删除只能从无 Active Response 的状态发起。
29.5 流事件处理
浏览器原生 EventSource 不能可靠设置自定义 sid 和认证 Header,因此浏览器 SDK 应使用支持
ReadableStream 的 fetch 读取 SSE,或通过受信任的业务后端代理。不得为了使用 EventSource 把 SID
或 API Key 放入 URL Query。跨域部署必须允许 sid 请求头及 POST、PUT、PATCH、GET、DELETE 方法,
并暴露响应头 sid、next-cursor 和 has-more。
客户端只实现七种顶层事件:
function applyEvent(state, event) {
switch (event.type) {
case "start":
return beginResponse(state, event)
case "item":
return upsertItem(state, event.item)
case "delta":
return appendDelta(state, event.id, event.data)
case "usage":
return replaceUsage(state, event)
case "done":
return finishResponse(state, event.status)
case "error":
return failResponse(state, event.code, event.message)
case "interrupt":
return trackInterrupt(state, event.id, event.status, event.usage)
default:
return state
}
}
处理规则:
- 相同 Item ID 使用 Upsert,不重复插入;
- Delta 只能追加到对应 Item ID;未知 ID 应暂存一个很短的有界窗口,仍未收到 Item 则报告协议错误;
- 后到的累计 Usage 覆盖先前 Usage;
: ping是 Heartbeat,不交给 JSON 解析器;- 未知 Item 类型保留原始对象或忽略展示,但不能使整个流失败;
- 收到
done或error后关闭当前读取器,不再接受该流的后续帧。
29.6 Item 的推荐展示
| Item Type | 客户端展示 |
|---|---|
text |
Assistant 正文,Delta 实时追加 |
reasoning |
可折叠的公开分析或摘要,不标记为隐藏思维链 |
compaction |
“正在整理上下文”状态、可读摘要及阶段 Usage |
function_call |
工具名称、参数、执行状态和确认 UI |
function_call_output |
工具执行结果,可根据业务权限折叠 |
History 中的 question 和 answer 都是 Item 数组。客户端必须按 Item 顺序还原,不能假定 Question
永远是文本,也不能把工具调用对象转成普通 Answer 字符串后丢失 call_id。
29.7 断线和刷新恢复
流连接异常关闭且尚未收到 done 或 error 时:
- 保留 SID;
- 将当前未完成回答标记为临时投影;
- 调用
GET /v1/session/responses,携带相同 SID; - 收到
start后清除或替换临时投影; - 从重放流开头重新应用所有 Item 和 Delta;
- 若响应仍在运行,继续读取实时增量;
- 直到收到
done或error。
恢复接口从头重放,所以客户端不得把重放 Delta 直接追加到断线前的残留文本。建议 SDK 使用新的 Response Projection 构建完成后替换旧投影,避免界面闪烁和重复字符。
恢复重试使用有上限的指数退避和随机抖动,例如:
0.5s, 1s, 2s, 4s, 8s,随后封顶
404 response_not_found、404 session_not_found 和明确协议错误不自动重试。网络错误、502、
503 和 504 可以在产品策略允许时重试 GET;不得因此重试原 POST。
29.8 非流式客户端
非流式请求等待终态 JSON。HTTP 连接超时或客户端主动断开并不证明任务失败,也不能安全地重新 POST。客户端应转为调用恢复 GET,读取当前响应的真实终态。
非流式和流式的最终 output Item 与 Usage 应使用同一渲染和统计代码,避免形成两套业务语义。
29.9 工具调用循环
收到:
{
"type": "function_call",
"call_id": "call_456",
"name": "database_inspect",
"arguments": {
"table": "Orders"
}
}
客户端在自身权限和审批规则下执行工具,然后发起下一次 POST:
{
"model": "gpt-5.6-sol",
"input": [
{
"type": "function_call_output",
"call_id": "call_456",
"output": {
"result": "检查完成"
}
}
],
"stream": true
}
客户端必须原样保留 call_id。工具失败也应提交结构化 Output 表达失败,不得伪造一个新的 Call ID。
是否自动执行工具属于客户端业务,不由 Session 协议授予权限。
29.10 安全停止
用户点击“停止”时:
- 禁用重复停止和新的提交按钮;
- 调用
POST /v1/session/responses/cancel; 202 canceling后继续读取原流,原流断开则进入恢复;- 收到最终 Usage 和
done(status=canceled); - 把已公开的部分 Answer 标记为已停止;
- 恢复输入、编辑和删除能力。
客户端不能通过关闭 Fetch、AbortController、SSE 或页面来替代取消接口。取消请求超时后也不能假定 任务已经停止,必须通过恢复 GET 确认。
29.11 中途打断并补充
用户在输出中途补充条件时,不先在客户端串行调用 Cancel 和普通 POST,而是直接调用
POST /v1/session/responses/interrupt,请求体仍为 Input 和 Model Config。客户端进入
interrupting,依次处理旧 Response 的 interrupt(canceling)、带最终 Usage 的终态
interrupt、新 Response 的 start/item/delta/usage/done。旧部分 Answer 标记为已停止,新的
Question 展示原问题和补充信息;两笔 Usage 分别保存。掉线后只用相同 SID 恢复整个交接流。
29.12 编辑最近一条用户消息
用户确认编辑后:
- 如果仍在生成,先安全停止并等待
canceled; - 调用
PUT /v1/session/responses,提交替换后的完整 Input 和本次 Model Config; - 立即从 UI 当前投影移除旧用户消息之后的 Assistant、Reasoning、工具和部分输出;
- 使用 PUT 返回的新流,从该位置渲染新回答;
- 如果 PUT 失败,在本地恢复编辑前投影,避免界面与服务端 History 不一致;
- 成功后以 GET History 返回的当前 Revision 为最终事实。
客户端不得把编辑实现为普通 POST,也不得在本地保留旧回答却把新回答追加在后面。服务端内部可能 保留旧 Revision,但客户端默认历史不展示它。
29.13 删除会话
删除前客户端应进行明确的用户确认:
- Active Response 存在时,先提示并执行安全停止;
- 等待响应进入终态;
- 调用
DELETE /v1/session; - 收到
204后清除本地 SID、消息投影、Usage 和工具状态; - 返回会话列表或创建新会话界面。
409 session_busy 表示仍需等待取消终态。删除失败时不得提前清除本地 SID,以便用户重试或导出。
29.14 History 查询与渲染
客户端调用:
GET /v1/session/history
sid: {GUID}[,{GUID}...]
客户端按 limit + cursor 逐页读取,只有 has-more: true 时才请求 next-cursor;遇到
cursor_stale 时清空已拼接页并用相同 SID 集合从第一页重读。默认响应体是 Exchange 数组;需要
Prompt 时间线时增加 include_system_prompt=true,并按 {history, system_prompts} 读取。
渲染规则:
- 每条 Exchange 使用返回的
sid标识来源,不能用数组位置猜测主/子会话; - 按服务端数组顺序展示,不自行用设备时间或各 SID Sequence 重新排序;
question_at和answer_at是服务端 UTC 时间,可用于消息时间显示和并行会话 Timeline;question和answer保持 Item 数组;status: canceled的部分 Answer 显示“已停止”;- 不展示
superseded旧分支,因为默认 History 不返回; - History 仅用于展示和客户端恢复 UI,不能重新提交给模型;
- Prompt 时间线中的
system_prompt_at与消息时间共同排序,null表示清空;该数组不得注入 Context; - 大 History 应边读取边解析或虚拟滚动,避免一次渲染全部 DOM 节点。
29.15 错误处理建议
| HTTP/Code | 客户端行为 |
|---|---|
400 invalid_request |
标记客户端请求错误,不自动重试 |
400 function_call_mismatch |
检查并恢复原 call_id |
404 session_not_found |
停止恢复,提示会话不存在或已删除 |
404 response_not_found |
Session 存在但没有可恢复响应 |
409 session_busy |
不重复 POST;恢复当前响应或等待取消 |
409 no_editable_message |
禁用编辑并刷新 History |
429 rate_limit_exceeded |
按服务端 Retry-After 或退避重试新操作 |
500 provider_outcome_unknown |
保留 SID,提示结果待确认,不重新提交 |
502/503/504 |
GET 可以退避重试;POST 不自动重试,先用 SID 恢复确认 |
错误消息用于展示,业务分支必须使用稳定 code,不能解析自然语言 Message。
29.16 客户端 SDK 建议结构
SessionClient
createOrAppend(input, modelConfig)
interrupt(input, modelConfig)
recover()
historyPage(cursor, limit)
cancel()
editLast(input, modelConfig)
updateSystemPrompt(systemPrompt)
delete()
SessionStore
sidByBusinessThread
activeResponseProjection
connectionState
usage
SessionStreamDecoder
SSE framing
event validation
item upsert
delta projection
SessionReconnectPolicy
bounded exponential backoff
terminal error classification
SDK 可以封装这些方法,但不得把内部 Session Context 暴露给普通业务调用者。
29.17 客户端验收清单
- 首次请求获取并保存 SID;
- 刷新后继续使用相同 SID 续写;
- 流与非流得到相同最终投影;
- Heartbeat 和未知 Item 不导致崩溃;
- 断线恢复没有重复字符、重复消息和重复 POST;
- Cancel 等待真实
canceled终态; - Interrupt 使用单次请求完成停止与补充,正确展示旧响应终态和两笔独立 Usage;
- 运行中 PATCH Prompt 不停止当前响应;需要立即按新 Prompt 重答时,PATCH 成功后再 Interrupt;
- Edit 剪切最近用户消息之后的全部内容;
- Delete 成功后彻底清除本地 SID;
- 工具 Call ID 跨请求保持一致;
- History 可还原 Text、Reasoning、Compaction 和工具项;
- Usage 使用最终累计值,不把中间累计值相加;
- 账号或租户切换不会复用其他主体 SID;
- 所有错误按 Code 处理并有清晰用户反馈。
30. 发布前验证
正式发布前至少验证:
契约
- 流式与非流式输出包含相同的最终 Item 和 Usage;
- 无思考、默认思考、四档深度思考;
- 无压缩、自动压缩、压缩失败;
- 文本、Function Call、Function Call Output;
- 运行中 Interrupt 的 canceling/终态交接、旧/新独立 Usage 和断线恢复;
- 历史返回与 Context 压缩相互独立;
- 单 SID 与多 SID History 都返回来源 SID、Question/Answer 时间戳并按时序稳定分页;
- 任一合并会话被编辑时,旧的合并 Cursor 返回 cursor_stale;
- 旧客户端忽略新增 Item。
并发
- 同 SID 并发 POST 只有一个成功;
- Cancel 与完成竞争时只能形成一个终态;
- PUT 原子退出旧投影并创建新 Revision;
- DELETE 在 Active Response 运行时不能绕过停止流程;
- 运行中 PATCH Prompt 成功且当前 Response 保持旧快照,之后接受的 Response 使用新版本;
- 同 SID 并发 PATCH 最后成功 Revision 生效,不产生
session_busy、Context 回退或 Response 失败; - 多 SID 水平并发互不阻塞;
- 多个恢复观察者不影响 Provider;
- 旧 Lease Executor 不能覆盖新 Epoch。
故障
- 客户端流中断后任务继续并可恢复;
- Executor 节点在 Provider 调用前、调用中、终态前宕机;
- Redis 重启和短时不可用;
- SQL 短时不可用;
- Provider 超时、明确失败和结果未知;
- 重放、恢复和故障接管不重复结算。
性能
- 大 History 不进入模型热路径;
- Redis 只保存活跃 Session;
- SSE Delta 不逐 Token 写 SQL;
- History 使用有界 Keyset 读取和流式 JSON;
- 多 SID History 只读取 limit + 1 行,不把多个会话完整加载到内存后排序;
- Context、Redis Stream 和单次输出均有硬上限;
- 压缩后 Context Token 数满足目标阈值。
完成以上验证后,才可将 DASP/1.0 标记为稳定协议。