use anyhow::Result;
use tokio_util::sync::CancellationToken;
use mj_checkpoint::archive::CanonicalSessionSnapshot;
use mj_core::config::{Config, HarnessProfile};
pub(crate) fn profile_handoff_bytes(profile: Option<&HarnessProfile>) -> usize {
profile
.and_then(|profile| profile.context_window_bytes)
.unwrap_or(crate::compaction::DEFAULT_CONTEXT_BYTES)
}
pub(crate) async fn build_handoff_context(
session_id: &str,
config: &Config,
snapshot: &CanonicalSessionSnapshot,
context_bytes: usize,
cancel: &CancellationToken,
) -> Result<String> {
let candidates = match crate::utility_llm::UtilityLlmRuntime::shared()
.resolve(config, cancel)
.await
{
Ok(candidates) => candidates,
Err(error) if cancel.is_cancelled() => return Err(error),
Err(error) => {
tracing::warn!(
session_id,
error = format!("{error:#}"),
"no utility model is available for the handoff; handing over the most recent transcript verbatim"
);
return Ok(crate::compaction::render_recent_snapshot(
snapshot,
context_bytes,
));
}
};
let backend = crate::utility_llm::UtilityCompactionBackend::new(candidates, cancel.clone());
let page_bytes = backend.page_bytes();
summarize_or_verbatim(
session_id,
snapshot,
context_bytes,
&backend,
page_bytes,
cancel,
)
.await
}
async fn summarize_or_verbatim(
session_id: &str,
snapshot: &CanonicalSessionSnapshot,
context_bytes: usize,
backend: &impl crate::compaction::CompactionBackend,
page_bytes: usize,
cancel: &CancellationToken,
) -> Result<String> {
let budget = crate::compaction::CompactionBudget {
page_bytes,
handoff_bytes: context_bytes,
};
match crate::compaction::compact_snapshot(snapshot, budget, backend).await {
Ok(handoff) => Ok(handoff),
Err(error) if cancel.is_cancelled() => Err(error),
Err(error) => {
tracing::warn!(
session_id,
error = format!("{error:#}"),
"utility summarizer failed; handing over the most recent transcript verbatim"
);
Ok(crate::compaction::render_recent_snapshot(
snapshot,
context_bytes,
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compaction::{CompactionBackend, HANDOFF_PREAMBLE};
use mj_checkpoint::archive::{
CanonicalExecutionState, CanonicalSessionState, CanonicalTranscriptBody,
CanonicalTranscriptItem,
};
use std::collections::BTreeMap;
use std::future::Future;
use std::pin::Pin;
struct FakeBackend;
impl CompactionBackend for FakeBackend {
fn compact<'a>(
&'a self,
_prompt: String,
) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>> {
Box::pin(async { Ok("<state_snapshot>kept</state_snapshot>".into()) })
}
}
struct FailingBackend;
impl CompactionBackend for FailingBackend {
fn compact<'a>(
&'a self,
_prompt: String,
) -> Pin<Box<dyn Future<Output = Result<String>> + Send + 'a>> {
Box::pin(async { Err(anyhow::anyhow!("summarizer exploded")) })
}
}
fn one_exchange_snapshot() -> CanonicalSessionSnapshot {
let bodies = vec![
CanonicalTranscriptBody::User {
content: vec![serde_json::json!({"type": "text", "text": "fix the bug"})],
},
CanonicalTranscriptBody::Agent {
chunks: vec![serde_json::json!({"content": {"type": "text", "text": "done"}})],
streaming: false,
},
];
let transcript = bodies
.into_iter()
.enumerate()
.map(|(index, body)| CanonicalTranscriptItem {
stable_id: format!("item-{index}"),
position: index as u64 + 1,
latest_content_event_ordinal: None,
created_at_ms: 0,
last_changed_at_ms: 0,
body,
})
.collect();
CanonicalSessionSnapshot {
event_frontier: 0,
event_frontier_digest: "0".repeat(64),
session: CanonicalSessionState {
execution: CanonicalExecutionState::Idle,
last_activity_at_ms: None,
session_title: None,
configuration: BTreeMap::new(),
},
transcript,
queued_prompts: Vec::new(),
}
}
#[tokio::test]
async fn a_resolved_backend_produces_a_summarized_handoff() {
let handoff = summarize_or_verbatim(
"session-under-test",
&one_exchange_snapshot(),
64 * 1024,
&FakeBackend,
64 * 1024,
&CancellationToken::new(),
)
.await
.unwrap();
assert!(handoff.starts_with(HANDOFF_PREAMBLE), "{handoff}");
assert!(
handoff.contains("<state_snapshot>kept</state_snapshot>"),
"{handoff}"
);
assert!(
!handoff.contains("No summarizer was available"),
"a summarized handoff must not carry the verbatim preamble: {handoff}"
);
}
#[tokio::test]
async fn a_failing_summarizer_falls_back_to_verbatim() {
let handoff = summarize_or_verbatim(
"session-under-test",
&one_exchange_snapshot(),
64 * 1024,
&FailingBackend,
64 * 1024,
&CancellationToken::new(),
)
.await
.unwrap();
assert!(handoff.starts_with(HANDOFF_PREAMBLE), "{handoff}");
assert!(handoff.contains("fix the bug"), "{handoff}");
}
#[tokio::test]
async fn no_model_falls_back_to_a_verbatim_handoff() {
let handoff = build_handoff_context(
"session-under-test",
&Config::default(),
&one_exchange_snapshot(),
64 * 1024,
&CancellationToken::new(),
)
.await
.unwrap();
assert!(handoff.starts_with(HANDOFF_PREAMBLE), "{handoff}");
assert!(
handoff.contains("No summarizer was available"),
"the fallback handoff names its lack of a summarizer: {handoff}"
);
assert!(handoff.contains("fix the bug"), "{handoff}");
}
}