klieo-core 0.5.0

Core traits + runtime for the klieo agent framework.
Documentation
//! [`RunOptions`] and its defaults / `Debug` impl.

use crate::guardrail::Guardrail;
use std::sync::Arc;

/// Default cap on the total bytes a streaming run will forward to the
/// caller before aborting. Sized at 4 MiB — comfortably above any sane
/// chat response while small enough to bound a runaway provider.
const DEFAULT_MAX_RESPONSE_BYTES: usize = 4 * 1024 * 1024;

/// Tunables for [`super::run_steps`] and [`super::run_steps_streaming`].
#[derive(Clone)]
pub struct RunOptions {
    /// Maximum number of (LLM call + tool dispatch) iterations.
    pub max_steps: u32,
    /// Maximum tokens of short-term memory to load into the prompt.
    pub max_history_tokens: usize,
    /// Cap on bytes accumulated across a single streaming cycle's
    /// content + serialised tool-call payloads. Exceeding this aborts
    /// the run with a terminal `Err`. Applies only to
    /// [`super::run_steps_streaming`]; [`super::run_steps`] is
    /// unaffected (the non-streaming path inherits the provider's own
    /// limits).
    pub max_response_bytes: usize,
    /// Pre/post-LLM validators consulted around every LLM call.
    ///
    /// Empty by default — existing callers see no behavioural change.
    /// Each guardrail is invoked in registration order; the first
    /// non-[`GuardrailOutcome::Pass`](crate::guardrail::GuardrailOutcome::Pass)
    /// outcome short-circuits the run with
    /// [`Error::Refused`](crate::error::Error::Refused) or
    /// [`Error::Handoff`](crate::error::Error::Handoff). In
    /// [`super::run_steps_streaming`], post-LLM guardrails see the
    /// buffered assistant message at the end of each cycle, and a
    /// non-`Pass` outcome propagates as a terminal
    /// `LlmError::Server("refused: …")` or `"handoff to <agent>: …"`
    /// chunk.
    pub guardrails: Vec<Arc<dyn Guardrail>>,
}

impl std::fmt::Debug for RunOptions {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("RunOptions")
            .field("max_steps", &self.max_steps)
            .field("max_history_tokens", &self.max_history_tokens)
            .field("max_response_bytes", &self.max_response_bytes)
            .field("guardrails", &self.guardrails.len())
            .finish()
    }
}

impl Default for RunOptions {
    fn default() -> Self {
        Self {
            max_steps: 16,
            max_history_tokens: 8_000,
            max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES,
            guardrails: Vec::new(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Pin the streaming byte cap so an accidental retune of
    /// [`DEFAULT_MAX_RESPONSE_BYTES`] is caught at test time rather
    /// than silently changing the runtime contract for every caller
    /// using `RunOptions::default()`.
    #[test]
    fn default_max_response_bytes_is_four_mebibytes() {
        assert_eq!(RunOptions::default().max_response_bytes, 4 * 1024 * 1024);
    }
}