starweaver-runtime 0.0.1

Agent-loop graph and runtime executor primitives for Starweaver
Documentation
#![allow(missing_docs, clippy::unwrap_used)]

use std::sync::{Arc, Mutex};

use starweaver_context::AgentContext;
use starweaver_core::SessionId;
use starweaver_model::{
    CodexSettings, FunctionModel, FunctionModelInfo, GatewaySettings, ModelProfile, ModelResponse,
    ModelSettings, OpenAiChatSettings, ProfileOverrideModel, ProtocolFamily, ProviderSettings,
};
use starweaver_runtime::Agent;

#[tokio::test]
async fn openai_chat_session_affinity_injects_prompt_cache_key() {
    let captured = Arc::new(Mutex::new(Vec::<Option<ModelSettings>>::new()));
    let model_captured = Arc::clone(&captured);
    let model = FunctionModel::new(move |_messages, settings, _info: FunctionModelInfo| {
        model_captured.lock().unwrap().push(settings);
        Ok(ModelResponse::text("ok"))
    })
    .with_model_name("gpt-4.1-mini");
    let agent = Agent::new(Arc::new(model));
    let mut context = AgentContext::default();
    context.set_session_id(SessionId::from_string("session_affinity_runtime"));

    let result = agent.run_with_context("hello", &mut context).await.unwrap();

    assert_eq!(result.output, "ok");
    let settings = captured.lock().unwrap()[0].clone().unwrap();
    assert_eq!(
        settings
            .provider_settings
            .openai_chat
            .as_ref()
            .and_then(|settings| settings.prompt_cache_key.as_deref()),
        Some("sw_session_affinity_runtime")
    );
}

#[tokio::test]
async fn explicit_model_settings_override_openai_chat_session_affinity() {
    let captured = Arc::new(Mutex::new(Vec::<Option<ModelSettings>>::new()));
    let model_captured = Arc::clone(&captured);
    let model = FunctionModel::new(move |_messages, settings, _info: FunctionModelInfo| {
        model_captured.lock().unwrap().push(settings);
        Ok(ModelResponse::text("ok"))
    })
    .with_model_name("gpt-4.1-mini");
    let agent = Agent::new(Arc::new(model)).with_model_settings(ModelSettings {
        provider_settings: ProviderSettings {
            openai_chat: Some(OpenAiChatSettings {
                prompt_cache_key: Some("manual-cache-key".to_string()),
                ..OpenAiChatSettings::default()
            }),
            ..ProviderSettings::default()
        },
        ..ModelSettings::default()
    });
    let mut context = AgentContext::default();
    context.set_session_id(SessionId::from_string("session_affinity_runtime"));

    let result = agent.run_with_context("hello", &mut context).await.unwrap();

    assert_eq!(result.output, "ok");
    let settings = captured.lock().unwrap()[0].clone().unwrap();
    assert_eq!(
        settings
            .provider_settings
            .openai_chat
            .as_ref()
            .and_then(|settings| settings.prompt_cache_key.as_deref()),
        Some("manual-cache-key")
    );
}

#[tokio::test]
async fn codex_responses_session_affinity_injects_typed_routing_ids() {
    let captured = Arc::new(Mutex::new(Vec::<(Option<ModelSettings>, String)>::new()));
    let model_captured = Arc::clone(&captured);
    let inner = Arc::new(FunctionModel::new(
        move |_messages, settings, info: FunctionModelInfo| {
            model_captured
                .lock()
                .unwrap()
                .push((settings, info.context.run_id.as_str().to_string()));
            Ok(ModelResponse::text("ok"))
        },
    ));
    let model = ProfileOverrideModel::new(
        inner,
        ModelProfile::for_protocol(ProtocolFamily::OpenAiResponses),
    )
    .with_provider_name(Some("codex".to_string()))
    .with_model_name("gpt-5.5");
    let agent = Agent::new(Arc::new(model));
    let mut context = AgentContext::default();
    context.set_session_id(SessionId::from_string("session_affinity_codex"));

    let result = agent.run_with_context("hello", &mut context).await.unwrap();

    assert_eq!(result.output, "ok");
    let (settings, run_id) = captured.lock().unwrap()[0].clone();
    let codex = settings.unwrap().provider_settings.codex.unwrap();
    assert_eq!(
        codex,
        CodexSettings {
            session_id: Some("session_affinity_codex".to_string()),
            thread_id: Some(run_id),
        }
    );
}

#[tokio::test]
async fn gateway_session_affinity_is_opt_in_for_gemini_and_bedrock_families() {
    let captured = Arc::new(Mutex::new(Vec::<Option<ModelSettings>>::new()));
    let model_captured = Arc::clone(&captured);
    let inner = Arc::new(FunctionModel::new(
        move |_messages, settings, _info: FunctionModelInfo| {
            model_captured.lock().unwrap().push(settings);
            Ok(ModelResponse::text("ok"))
        },
    ));
    let model = ProfileOverrideModel::new(
        inner,
        ModelProfile::for_protocol(ProtocolFamily::GeminiGenerateContent),
    )
    .with_provider_name(Some("gateway".to_string()))
    .with_model_name("gemini-2.5-pro");
    let agent = Agent::new(Arc::new(model));
    let mut context = AgentContext::default();
    context.set_session_id(SessionId::from_string("session_affinity_gateway"));
    context.metadata.insert(
        "starweaver.gateway_session_affinity".to_string(),
        serde_json::json!(true),
    );

    let result = agent.run_with_context("hello", &mut context).await.unwrap();

    assert_eq!(result.output, "ok");
    let gateway = captured.lock().unwrap()[0]
        .clone()
        .unwrap()
        .provider_settings
        .gateway
        .unwrap();
    assert_eq!(
        gateway,
        GatewaySettings {
            x_session_id: Some("session_affinity_gateway".to_string()),
            ..GatewaySettings::default()
        }
    );
}