af-agent-runtime 0.9.0

Recoverable Turn/Step loop, tool pipeline, retry and context compaction.
Documentation
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
    }
}