use crate::{
AgentRuntime, CancellationToken, EventWriter, RuntimeError, RuntimeOutcome, TurnRequest,
};
use af_agent::{PreStepDecision, StepContext};
use af_agent_session::{Event, RunStatus};
impl AgentRuntime {
pub(super) async fn run_compaction(
&self,
request: &TurnRequest,
writer: &dyn EventWriter,
cancellation: CancellationToken,
) -> Result<RuntimeOutcome, RuntimeError> {
let projection = &request.history.projection;
if !projection.open_tool_calls.is_empty()
|| request
.history
.events
.iter()
.any(|event| matches!(&event.event, Event::ToolCall { run_id, .. } if run_id == &request.run_id))
{
return Err(RuntimeError::Invariant(
"manual compaction cannot execute historical tools".into(),
));
}
let mut transcript = request.history.transcript.clone();
let mut usage = projection.usage_for(&request.run_id);
if projection.open_compaction.is_some() {
let recovered = self
.recover_open_surface(
request,
projection,
writer,
cancellation,
&mut transcript,
usage,
)
.await?;
return recovered.terminal.ok_or_else(|| {
RuntimeError::Invariant("unfinished compaction did not settle".into())
});
}
if projection.active_run_id.is_none() {
writer
.append(vec![
Event::InputClaimed {
input_id: request.input_id.clone(),
run_id: request.run_id.clone(),
},
Event::RunStarted {
input_id: request.input_id.clone(),
run_id: request.run_id.clone(),
},
])
.await?;
}
if projection.open_turn.is_none() {
writer
.append(vec![Event::TurnStarted {
run_id: request.run_id.clone(),
turn: 1,
}])
.await?;
}
let already_compacted = request.history.events.iter().any(|event| matches!(&event.event,
Event::CompactionFinished { run_id, status, error: None, .. } if run_id == &request.run_id && status == "completed"));
let mut step = projection.open_steps.iter().next().copied();
if !already_compacted && !transcript.is_empty() && !cancellation.is_cancelled() {
if step.is_none() {
step = Some(1);
writer
.append(vec![Event::StepStarted {
run_id: request.run_id.clone(),
step: 1,
}])
.await?;
}
let step_number = step.expect("step opened above");
let context: StepContext = self.step_context(request, step_number);
if let PreStepDecision::Reject { reason } = self.pre_step(&context).await {
writer
.append(vec![Event::Extension {
run_id: request.run_id.clone(),
plugin_id: "af-agent-runtime".into(),
event_type: "pre_step_rejected".into(),
payload: serde_json::json!({"reason": reason}),
}])
.await?;
return self
.finish_open(
writer,
&request.run_id,
step,
RunStatus::Cancelled,
None,
usage,
)
.await;
}
match self
.compact_if_needed(
writer,
request,
step_number,
&mut transcript,
&[],
cancellation.clone(),
)
.await
{
Ok(tokens) => {
usage.0 += tokens.0;
usage.1 += tokens.1;
}
Err(RuntimeError::Cancelled) => {
return self
.finish_open(
writer,
&request.run_id,
step,
RunStatus::Cancelled,
None,
usage,
)
.await
}
Err(error) => return Err(error),
}
}
let status = if cancellation.is_cancelled() {
RunStatus::Cancelled
} else {
RunStatus::Completed
};
self.finish_open(writer, &request.run_id, step, status, None, usage)
.await
}
}