fluers-runtime 0.6.0

The Fluers agent harness: agent definition, sessions, skills, sandbox, events
Documentation
//! Embedding proof: an in-process `ModelProvider` with zero network.
//!
//! This is the shape fae uses to embed `fluers-runtime` behind mistral.rs /
//! llama.cpp: the host owns the inference loop and returns assistant messages
//! directly — no HTTP, no `reqwest`, no provider adapter types. It proves the
//! [`fluers_core::ModelProvider`] trait accepts what a local backend needs.
//!
//! Run with `cargo run -p fluers-runtime --example embedding_local_provider`.

use std::sync::Mutex;

use fluers_core::{AgentMessage, ContentBlock, ModelProvider, ModelRequest, ModelResponse, Role};
use fluers_runtime::{define_agent, local, JsonFileAdapter};

/// A scripted in-process provider: returns a queue of canned assistant
/// messages, one per `invoke`. This stands in for a mistral.rs/llama.cpp
/// adapter that owns native inference state behind the same trait — the trait
/// surface is identical, only the body of `invoke` changes.
struct LocalScriptedProvider {
    /// The pre-loaded replies (a tiny state machine). `Mutex` because
    /// `ModelProvider: Send + Sync` and the host may share one provider.
    replies: Mutex<Vec<String>>,
}

impl LocalScriptedProvider {
    /// Build a provider that answers with the given canned replies, in order.
    fn new(replies: Vec<String>) -> Self {
        Self {
            replies: Mutex::new(replies),
        }
    }
}

#[async_trait::async_trait]
impl ModelProvider for LocalScriptedProvider {
    async fn invoke(&self, _request: ModelRequest) -> fluers_core::Result<ModelResponse> {
        // Pop the next canned reply. In a real embedding (fae + mistral.rs)
        // this body would run native inference over `request.messages` and
        // stream/return the generated tokens — still zero network.
        let body = self
            .replies
            .lock()
            .map_err(|e| fluers_core::CoreError::ModelProvider(format!("provider lock: {e}")))?
            .pop()
            .unwrap_or_else(|| "(no scripted reply left)".to_string());
        Ok(ModelResponse {
            messages: vec![AgentMessage {
                role: Role::Assistant,
                content: vec![ContentBlock::Text { text: body }],
            }],
        })
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. The provider: a local, in-process implementation of the trait —
    //    exactly the surface fae's mistral.rs/llama.cpp adapter implements.
    let provider: std::sync::Arc<dyn ModelProvider> =
        std::sync::Arc::new(LocalScriptedProvider::new(vec![
            "Hello from the embedded runtime.".into(),
        ]));

    // 2. Prove the trait surface works end-to-end with zero network: build a
    //    minimal request, invoke the provider, read the assistant reply.
    let request = ModelRequest {
        model: fluers_core::Model::new("local/embedded"),
        messages: Vec::new(),
        tools: Vec::new(),
        thinking: fluers_core::ThinkingLevel::default(),
        params: std::collections::BTreeMap::new(),
    };
    let response = provider.invoke(request).await?;
    let reply = response
        .messages
        .into_iter()
        .next()
        .ok_or("provider returned no message")?;
    println!("embedded provider reply: {reply:?}");

    // 3. Construct an agent fully in-process — agent + provider + sandbox +
    //    session store — proving fluers-runtime embeds without any server or
    //    HTTP layer. The provider is host-owned; the sandbox is the local
    //    filesystem backend (fd-anchored path containment); the session store
    //    is a JSON-file adapter under a temp dir.
    let tmp = std::env::temp_dir().join("fluers-embedding-example");
    let sandbox: std::sync::Arc<dyn fluers_runtime::Sandbox> = std::sync::Arc::new(local());
    let agent = define_agent(|b| {
        b.model("local/embedded")
            .instructions("You are an embedded demo.")
            .sandbox(sandbox.clone());
        Ok(())
    })
    .await?;

    // The agent owns its profile (model + instructions + the wired local
    // sandbox). The host passes the provider at turn time; nothing here
    // touched the network. The store below is the persistence adapter a turn
    // loop would hand to a session runner.
    let _store = JsonFileAdapter::new(tmp);
    println!(
        "agent constructed in-process; model = {:?}, {} tools wired",
        agent.profile.model.id,
        agent.profile.tools.len()
    );
    println!("embedding proof OK — zero network, zero HTTP types.");

    Ok(())
}