use crate::canonical::{CanonicalRequest, Content, Message, Role};
use crate::store::{content_key, ReplayStash};
use super::IngressError;
pub(crate) const THINKING_REPLAY: &str = "thinking_replay";
pub(crate) fn reinject(
req: &mut CanonicalRequest,
stash: &ReplayStash,
reject: bool,
) -> Result<Vec<String>, IngressError> {
let reasoning = req.reasoning.is_some();
let mut fired = false;
for msg in req.messages.iter_mut() {
if msg.role != Role::Assistant {
continue;
}
let ids = tool_ids(msg);
match recall(stash, &ids, msg) {
Some(blocks) => inject(msg, blocks),
None if !ids.is_empty() && reasoning => {
if reject {
return Err(IngressError {
message: format!(
"replay stash miss for tool call `{}` on a reasoning continuation, \
and `lossy_overrides.thinking_replay = \"reject\"` refuses the \
degraded turn — drop the override to adapt, or start a fresh turn",
ids[0]
),
});
}
fired = true;
}
None => {}
}
}
Ok(if fired {
vec![THINKING_REPLAY.to_owned()]
} else {
Vec::new()
})
}
fn tool_ids(msg: &Message) -> Vec<String> {
msg.content
.iter()
.filter_map(|c| match c {
Content::ToolUse { id, .. } => Some(id.clone()),
_ => None,
})
.collect()
}
fn recall(stash: &ReplayStash, ids: &[String], msg: &Message) -> Option<Vec<Content>> {
let payload = if ids.is_empty() {
stash.recall(&content_key(&turn_text(msg)))
} else {
ids.iter().find_map(|id| stash.recall(id))
};
serde_json::from_slice(&payload?).ok()
}
fn turn_text(msg: &Message) -> String {
msg.content
.iter()
.filter_map(|c| match c {
Content::Text(t) => Some(t.as_str()),
_ => None,
})
.collect()
}
fn inject(msg: &mut Message, blocks: Vec<Content>) {
let mut lead = Vec::new();
for block in blocks {
match block {
Content::Thinking { .. } | Content::RedactedThinking { .. } => lead.push(block),
Content::ToolUse {
id,
signature: Some(sig),
..
} => {
for c in &mut msg.content {
if let Content::ToolUse {
id: cid, signature, ..
} = c
{
if *cid == id {
*signature = Some(sig.clone());
}
}
}
}
_ => {}
}
}
msg.content.splice(0..0, lead);
}