use std::sync::Arc;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use crate::error::Result;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BackgroundResponseRecord {
pub response_id: String,
pub request_fingerprint: String,
}
#[async_trait]
pub trait BackgroundResponseJournal: Send + Sync {
async fn load(&self) -> Result<Option<BackgroundResponseRecord>>;
async fn store(&self, record: Option<&BackgroundResponseRecord>) -> Result<()>;
}
#[derive(Clone, Default)]
pub struct BackgroundCallContext {
journal: Option<Arc<dyn BackgroundResponseJournal>>,
cancel: Option<tokio::sync::watch::Receiver<bool>>,
}
impl BackgroundCallContext {
pub fn with_journal(mut self, journal: Arc<dyn BackgroundResponseJournal>) -> Self {
self.journal = Some(journal);
self
}
pub fn with_cancel(mut self, cancel: tokio::sync::watch::Receiver<bool>) -> Self {
self.cancel = Some(cancel);
self
}
pub fn journal(&self) -> Option<&Arc<dyn BackgroundResponseJournal>> {
self.journal.as_ref()
}
pub fn cancel_signal(&self) -> Option<tokio::sync::watch::Receiver<bool>> {
self.cancel.clone()
}
}
impl std::fmt::Debug for BackgroundCallContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BackgroundCallContext")
.field("journal", &self.journal.is_some())
.field("cancel", &self.cancel.is_some())
.finish()
}
}
pub fn request_fingerprint(body: &Value) -> String {
let mut body = body.clone();
if let Some(object) = body.as_object_mut() {
object.remove("metadata");
}
let bytes = serde_json::to_vec(&body).unwrap_or_default();
hex::encode(Sha256::digest(bytes))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn fingerprint_ignores_per_attempt_metadata_only() {
let first = json!({"model":"m","input":[{"role":"user","content":"hi"}],
"metadata":{"exec_id":"exec_1"}});
let retry = json!({"model":"m","input":[{"role":"user","content":"hi"}],
"metadata":{"exec_id":"exec_2"}});
let next_step = json!({"model":"m","input":[{"role":"user","content":"hi"},
{"type":"function_call_output","call_id":"c","output":"ok"}],
"metadata":{"exec_id":"exec_2"}});
assert_eq!(request_fingerprint(&first), request_fingerprint(&retry));
assert_ne!(request_fingerprint(&first), request_fingerprint(&next_step));
}
#[test]
fn empty_context_debug_hides_internals() {
let context = BackgroundCallContext::default();
assert!(context.journal().is_none());
assert!(context.cancel_signal().is_none());
assert_eq!(
format!("{context:?}"),
"BackgroundCallContext { journal: false, cancel: false }"
);
}
}