///|
/// ═══ 功能组:snapshot(会话快照存储,T8 stage B)═══
/// ConversationStore(宿主注入 KV)+ 快照还原 + convert_request_with_store。
///
/// 背景(IR 提案:.agent-workplace/docs/report/ir-proposal-previous-response-id.md):
/// Responses 的 previous_response_id 是有状态续接——客户端第二轮只带新
/// input + 上一轮 response id,历史在服务端。中间件把它转到无状态端点
/// (chat/messages/gemini)时,必须本地保存 response_id → 会话快照,
/// 编码前还原历史,否则第二轮必败(最小矩阵 #10 的拒绝语义)。
///
/// 与 wasm 无状态导出兼容:存储由宿主持有(闭包/外部 KV),SDK 只持有
/// get/put 函数对。
///|
/// ConversationStore — 宿主注入的会话快照存储
///
/// get:按 response_id 取快照(无则 None)
/// put:存快照(同 id 覆盖)
pub struct ConversationStore {
get : (String) -> @lux.ConversationSnapshot?
put : (@lux.ConversationSnapshot) -> Unit
}
///|
pub fn ConversationStore::new(
get : (String) -> @lux.ConversationSnapshot?,
put : (@lux.ConversationSnapshot) -> Unit,
) -> ConversationStore {
{ get, put, }
}
///|
/// 取请求的 previous_response_id(标准字段优先,extras 旧载荷回退)
pub fn previous_response_id_of(req : @lux.LucentRequest) -> String? {
match req.options.previous_response_id {
Some(id) => Some(id)
None =>
match req.options.extras {
Some(m) =>
match m.get("previous_response_id") {
Some(String(id)) => Some(id)
_ => None
}
None => None
}
}
}
///|
/// 快照还原:快照历史 + 新请求项合并为完整对话
///
/// 规则:
/// - conversation:快照请求的会话项全部在前,新请求项追加在后
/// - instructions / tools / tool_choice:新请求优先,缺失时继承快照
/// - options / capabilities / reasoning / metadata / extra:取新请求
/// - previous_response_id **保留**:responses → responses 时后端仍持
/// 有状态(正确续接);无状态目标端由编码器既有逻辑剔除并打诊断
pub fn restore_request_from_snapshot(
req : @lux.LucentRequest,
snapshot : @lux.ConversationSnapshot,
) -> @lux.LucentRequest {
let merged : Array[@lux.LucentConversationItem] = []
for item in snapshot.request.conversation {
merged.push(item)
}
for item in req.conversation {
merged.push(item)
}
let instructions = match req.instructions {
Some(i) => Some(i)
None => snapshot.request.instructions
}
let tools = match req.tools {
Some(t) => Some(t)
None => snapshot.request.tools
}
let tool_choice = match req.tool_choice {
Some(tc) => Some(tc)
None => snapshot.request.tool_choice
}
@lux.LucentRequest::new(
req.model,
instructions,
merged,
tools,
tool_choice,
req.options,
req.capabilities,
req.reasoning,
req.metadata,
req.extra,
)
}
///|
/// 从「已解码请求 + 已解码响应」构造并存储快照(中间件转发成功后调用)
pub fn store_snapshot_from_response(
store : ConversationStore,
req : @lux.LucentRequest,
response : @lux.LucentResponse,
) -> Unit {
(store.put)(
@lux.ConversationSnapshot::new(response.id, req, Some(response.get_text())),
)
}
///|
/// 需要时还原:
/// - 无 previous_response_id → 原样返回
/// - 有快照 → 还原(Responses 的 input 只是新项,快照历史 + 新项合并)
/// - 无快照但请求自带内容 → 原样返回(目标端编码器剔除 id 并打诊断)
/// - 无快照且无内容 → 显式拒绝(无可恢复历史,不允许伪造续接——矩阵 #10)
fn maybe_restore_request(
req : @lux.LucentRequest,
store : ConversationStore,
) -> Result[@lux.LucentRequest, String] {
match previous_response_id_of(req) {
None => Ok(req)
Some(prev_id) =>
match (store.get)(prev_id) {
Some(snap) => Ok(restore_request_from_snapshot(req, snap))
None =>
if req.conversation.length() > 0 {
Ok(req)
} else {
Err(
"no local snapshot for previous_response_id '" +
prev_id +
"': stateless continuation cannot be converted without history",
)
}
}
}
}
///|
/// convert_request_with_store — 请求转换 + 会话快照还原(T8)
///
/// 管线:source decode →(previous_response_id 且无历史 → 快照还原)
/// → target encode。诊断按 decode/encode 两段合并。
/// 无快照时显式 Err(与直接 convert_request 的拒绝语义一致)。
pub fn convert_request_with_store(
source : String,
json_str : String,
target : String,
store : ConversationStore,
) -> Result[@lux.ConversionResult[String], String] {
match resolve_pair(source, target) {
Ok((src, dst)) =>
match (src.request_decode)(json_str) {
Ok(dcr) =>
match maybe_restore_request(dcr.value(), store) {
Ok(restored) =>
match (dst.request_encode)(restored) {
Ok(ecr) => Ok(ecr.with_diagnostics(dcr.diagnostics()))
Err(e) => Err(e)
}
Err(e) => Err(e)
}
Err(e) => Err(e)
}
Err(e) => Err(e)
}
}