af-agent-runtime 0.2.0

Recoverable Turn/Step loop, tool pipeline, retry and context compaction.
Documentation
use std::time::Duration;
use std::time::Instant;

use af_agent_session::{ContentBlock, Event, SessionEvent};
use af_llm::ChatMessage;
use async_trait::async_trait;

use crate::{CancellationToken, RuntimeError};

#[derive(Debug, Clone)]
pub struct RuntimeLimits {
    pub max_steps: u32,
    pub max_tool_calls: u32,
    pub max_tokens: u64,
    pub max_parallel_tools: usize,
    pub provider_attempts: u32,
    pub provider_deadline: Duration,
}

impl Default for RuntimeLimits {
    fn default() -> Self {
        Self {
            max_steps: 8,
            max_tool_calls: 24,
            max_tokens: 64_000,
            max_parallel_tools: 4,
            provider_attempts: 3,
            provider_deadline: Duration::from_secs(90),
        }
    }
}

#[derive(Debug, Clone)]
pub struct TurnRequest {
    pub context: af_context::RequestContext,
    pub session_id: String,
    pub run_id: String,
    pub input_id: String,
    pub content: Vec<ContentBlock>,
    pub history: Vec<SessionEvent>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct RuntimeOutcome {
    pub status: String,
    pub final_text: Option<String>,
    pub prompt_tokens: u64,
    pub completion_tokens: u64,
    pub waiting_interaction_id: Option<String>,
}

#[async_trait]
pub trait EventWriter: Send + Sync {
    async fn append(&self, events: Vec<Event>) -> Result<Vec<SessionEvent>, RuntimeError>;
    async fn load_after(&self, seq: u64) -> Result<Vec<SessionEvent>, RuntimeError>;
}

pub trait TokenMeter: Send + Sync {
    fn count(&self, model: &str, messages: &[ChatMessage]) -> u64;
}

pub struct ApproximateTokenMeter;

impl TokenMeter for ApproximateTokenMeter {
    fn count(&self, _model: &str, messages: &[ChatMessage]) -> u64 {
        messages
            .iter()
            .map(|message| {
                message
                    .content
                    .as_deref()
                    .map_or(0, |content| content.chars().count().div_ceil(4) as u64)
            })
            .sum()
    }
}

#[async_trait]
pub trait Compactor: Send + Sync {
    fn name(&self) -> &str {
        "compactor"
    }

    async fn summarize(
        &self,
        model: &str,
        messages: &[ChatMessage],
        operation_id: &str,
        cancellation: CancellationToken,
        deadline: Instant,
    ) -> Result<CompactionResult, RuntimeError>;
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompactionResult {
    pub summary: String,
    pub prompt_tokens: u64,
    pub completion_tokens: u64,
}