use super::session_compact::{self, COMPACT_MIN_TURNS};
use super::session_compact_call;
use super::session_state::SessionState;
use super::session_universe::SessionUniverse;
use crate::agent::provider;
use crate::config::runtime::RuntimeConfig;
use crate::interactive::tui::types::{App, CompactOutcome};
use std::sync::Arc;
pub(crate) struct CompactResult {
pub(crate) message: String,
pub(crate) failed: bool,
pub(crate) usage: Option<saya_agent::TokenUsage>,
pub(crate) summary: Option<String>,
pub(crate) compacted_turns: usize,
pub(crate) automatic: bool,
}
pub(crate) async fn run(
runtime: &RuntimeConfig,
state: &mut SessionState,
_session: &SessionUniverse,
) -> CompactResult {
run_with_trigger(runtime, state, _session, false).await
}
pub(crate) async fn run_with_trigger(
runtime: &RuntimeConfig,
state: &mut SessionState,
_session: &SessionUniverse,
automatic: bool,
) -> CompactResult {
let no_work = |message: String| CompactResult {
message,
failed: false,
usage: None,
summary: None,
compacted_turns: 0,
automatic,
};
let failed = |message: String, usage: Option<saya_agent::TokenUsage>| CompactResult {
message,
failed: true,
usage,
summary: None,
compacted_turns: 0,
automatic,
};
let history = state.provider_history();
let Some(plan) = session_compact::plan(&state.turns, &history) else {
return no_work("Nothing to compact: the conversation is short enough already.".into());
};
if state.turns.len() <= COMPACT_MIN_TURNS {
return no_work("Nothing to compact: the conversation is short enough already.".into());
}
let overrides = state.prompt_overrides();
let ai = crate::agent::runtime::effective_ai(&runtime.resolved.ai, &overrides);
let resolver = runtime.secret_resolver();
let provider = match provider::build(&ai, &resolver) {
Ok(provider) => provider,
Err(error) => {
return failed(
session_compact::failure_message(&format!("the summariser errored: {error}")),
None,
);
}
};
match session_compact_call::summarise(&*provider, &ai.model, &plan).await {
Ok(outcome) => match session_compact::apply(state, &plan, &outcome.summary) {
Ok(()) => CompactResult {
message: session_compact::success_message(plan.compacted_turns, &outcome.summary),
failed: false,
usage: outcome.usage,
summary: Some(outcome.summary),
compacted_turns: plan.compacted_turns,
automatic,
},
Err(reason) => failed(session_compact::failure_message(&reason), outcome.usage),
},
Err(reason) => failed(session_compact::failure_message(&reason), None),
}
}
pub(crate) fn start(app: &mut App, state: &SessionState) {
start_with_trigger(app, state, false);
}
pub(crate) fn start_automatic(app: &mut App, state: &SessionState) {
start_with_trigger(app, state, true);
}
fn start_with_trigger(app: &mut App, state: &SessionState, automatic: bool) {
use crate::interactive::tui::transcript::BlockKind;
if state.turns.is_empty() {
app.transcript.push(
BlockKind::Error,
String::from("Nothing to compact: there is no conversation yet."),
);
return;
}
if app.compact_task.is_some() {
app.transcript.push(
BlockKind::System,
String::from("A compaction is already running — wait for it to finish."),
);
return;
}
let runtime = Arc::clone(&app.runtime);
let mut owned = state.clone();
let universe = Arc::clone(&app.session);
let (tx, rx) = std::sync::mpsc::channel();
app.compact_task = Some(rx);
app.request.started = Some(std::time::Instant::now());
app.request.activity = Some("compacting".into());
std::thread::spawn(move || {
let result = super::session_resume::block_on(run_with_trigger(
&runtime, &mut owned, &universe, automatic,
));
let _ = tx.send(CompactOutcome {
message: result.message,
failed: result.failed,
usage: result.usage,
automatic: result.automatic,
summary: result.summary,
compacted_turns: result.compacted_turns,
});
});
}
pub(crate) fn poll(app: &mut App, state: &mut SessionState) {
use super::auto_compact::{auto_failure_message, auto_success_message};
use crate::interactive::tui::transcript::BlockKind;
let ready = app
.compact_task
.as_ref()
.is_some_and(|rx| match rx.try_recv() {
Ok(outcome) => {
state.usage.record_learning(outcome.usage);
app.request.started = None;
app.request.activity = None;
if outcome.automatic {
if outcome.failed {
state.auto_compact_failed = true;
app.transcript
.push(BlockKind::Error, auto_failure_message(&outcome.message));
} else if let Some(summary) = outcome.summary.as_deref() {
let plan_check =
session_compact::plan(&state.turns, &state.provider_history());
let applicable = plan_check.is_some_and(|plan| {
plan.compacted_turns == outcome.compacted_turns
&& session_compact::validate(summary, &plan.pinned).is_ok()
});
if applicable
&& let Some(plan) =
session_compact::plan(&state.turns, &state.provider_history())
&& session_compact::apply(state, &plan, summary).is_ok()
{
state.auto_compact_failed = false;
app.transcript.push(
BlockKind::System,
auto_success_message(outcome.compacted_turns, summary),
);
} else {
state.auto_compact_failed = true;
app.transcript.push(
BlockKind::Error,
auto_failure_message("the conversation changed while compacting"),
);
}
} else {
app.transcript.push(BlockKind::System, outcome.message);
}
} else if outcome.failed {
app.transcript.push(BlockKind::Error, outcome.message);
} else {
if let Some(summary) = outcome.summary.as_deref() {
let plan_check =
session_compact::plan(&state.turns, &state.provider_history());
let applicable = plan_check.is_some_and(|plan| {
plan.compacted_turns == outcome.compacted_turns
&& session_compact::validate(summary, &plan.pinned).is_ok()
});
if applicable
&& let Some(plan) =
session_compact::plan(&state.turns, &state.provider_history())
&& session_compact::apply(state, &plan, summary).is_ok()
{
state.auto_compact_failed = false;
}
}
app.transcript.push(BlockKind::System, outcome.message);
}
true
}
Err(std::sync::mpsc::TryRecvError::Empty) => false,
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
app.request.started = None;
app.request.activity = None;
app.transcript.push(
BlockKind::Error,
session_compact::failure_message("the compaction worker stopped"),
);
true
}
});
if ready {
app.compact_task = None;
}
}