af-agent 0.2.0

Stable Agent model, tool, inbox, and trusted-plugin contracts.
Documentation
//! The chat-model abstraction the agent loop runs against.
//!
//! Decoupling the loop from a concrete client means: (a) the loop is unit-
//! testable with a scripted mock — no network; (b) any backend (the real
//! [`LlmClient`], a local model, a replay harness) plugs in by implementing one
//! method.

use std::sync::Arc;

use af_llm::{CompletionRequest, CompletionResponse, LlmClient, LlmError};
use async_trait::async_trait;
use tokio::sync::mpsc::UnboundedSender;

/// Anything that can turn a chat-completion request into a response.
#[async_trait]
pub trait ChatModel: Send + Sync {
    /// Stream cumulative `(content, has_tool_calls)` updates and return the
    /// canonical terminal response. This is the only model invocation path.
    async fn complete_streaming(
        &self,
        request: &CompletionRequest,
        delta_tx: UnboundedSender<(String, bool)>,
    ) -> Result<CompletionResponse, LlmError>;
}

/// The production model is the real LLM client.
#[async_trait]
impl ChatModel for LlmClient {
    async fn complete_streaming(
        &self,
        request: &CompletionRequest,
        delta_tx: UnboundedSender<(String, bool)>,
    ) -> Result<CompletionResponse, LlmError> {
        self.complete_stream_single_attempt(request, |content, has_tools| {
            let _ = delta_tx.send((content.to_string(), has_tools));
        })
        .await
    }
}

/// A type-erased model, so callers (e.g. an HTTP service holding one `Agent` in
/// shared state) can pick the backend at runtime — real client vs stub vs
/// replay — without the loop being generic over every concrete type.
#[async_trait]
impl ChatModel for Arc<dyn ChatModel> {
    async fn complete_streaming(
        &self,
        request: &CompletionRequest,
        delta_tx: UnboundedSender<(String, bool)>,
    ) -> Result<CompletionResponse, LlmError> {
        (**self).complete_streaming(request, delta_tx).await
    }
}