pe-core 0.1.0

Core types for Potential Expectations — messages, channels, state, traits
Documentation
//! LLM provider abstraction — the interface every execution path depends on.
//!
//! `LlmProvider` is object-safe (`Box<dyn LlmProvider>` works) via explicit
//! `Pin<Box<dyn Future>>` return types. This is intentional — the trait must
//! be storable in runtime structs without generics.
//!
//! Based on Group 15.1 of the pre-plan.

use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;

use futures::Stream;
use serde::{Deserialize, Serialize};

use crate::error::PeError;
use crate::message::{AiMessage, Message};

/// Future returned by [`LlmProvider::stream`].
pub type StreamFuture<'a> = Pin<
    Box<
        dyn Future<Output = Result<Pin<Box<dyn Stream<Item = StreamChunk> + Send>>, PeError>>
            + Send
            + 'a,
    >,
>;

/// Core LLM abstraction. Every execution path in the library goes through this.
///
/// Implement this trait to plug in any model provider (OpenAI, Anthropic,
/// Ollama, local models, etc.).
///
/// The trait is object-safe: `Box<dyn LlmProvider>` compiles.
/// Uses `Pin<Box<dyn Future>>` instead of `async fn` for object safety.
///
/// # Example
///
/// ```ignore
/// let provider: Box<dyn LlmProvider> = Box::new(MyProvider::new());
/// let response = provider.complete(messages, tools).await?;
/// ```
pub trait LlmProvider: Send + Sync + 'static {
    /// Non-streaming completion. Returns the full response.
    ///
    /// `tools` is empty when no tool calling is needed.
    fn complete(
        &self,
        messages: &[Message],
        tools: &[ToolSchema],
    ) -> Pin<Box<dyn Future<Output = Result<LlmResponse, PeError>> + Send + '_>>;

    /// Streaming completion. Yields tokens (and tool call deltas) as they arrive.
    ///
    /// The final item in the stream is `StreamChunk::Done` carrying the full response.
    /// `tools` enables streaming tool calls (supported by Anthropic and OpenAI).
    fn stream(&self, messages: &[Message], tools: &[ToolSchema]) -> StreamFuture<'_>;

    /// Embed text into a vector. Used for semantic routing and memory search.
    fn embed(
        &self,
        text: &str,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<f32>, PeError>> + Send + '_>>;

    /// Human-readable provider name for logging ("openai", "anthropic", "mock").
    fn provider_name(&self) -> &'static str;
}

/// Response from an LLM completion call.
#[derive(Debug, Clone)]
pub struct LlmResponse {
    /// The AI message produced — contains content, tool_calls, usage.
    pub message: AiMessage,
    /// Raw provider metadata (model used, stop reason, etc.).
    pub provider_metadata: HashMap<String, serde_json::Value>,
}

/// A chunk from a streaming LLM response.
///
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum StreamChunk {
    /// A single token from the LLM output.
    Token(String),
    /// Streaming complete — carries the assembled full response.
    Done(LlmResponse),
    /// Error mid-stream.
    Error(PeError),
}

/// JSON schema description for a tool, sent to the LLM in completion calls.
///
/// The LLM uses this to understand what tools are available and how to call them.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolSchema {
    /// Tool name — must match the tool's registered name.
    pub name: String,
    /// Human-readable description of what the tool does.
    pub description: String,
    /// Parameters schema in JSON Schema format.
    pub parameters: serde_json::Value,
    /// Whether the LLM must strictly follow the schema (provider-dependent).
    #[serde(default)]
    pub strict: bool,
}