lash-protocol-standard 0.1.0-alpha.63

Standard protocol (native provider tool calls) for the lash agent runtime.
Documentation
use std::sync::Arc;

use lash_core::{
    LashRuntime, Message, MessageRole, ModelSpec, Part, PartKind, PersistedSessionConfig,
    PersistedTurnState, PruneState, RuntimePersistence, SessionGraph, SessionHead, TokenUsage,
};
use lash_sqlite_store::Store;

fn test_model_spec() -> ModelSpec {
    ModelSpec::from_token_limits("gpt-5.4-mini", None, 200_000, None)
        .expect("valid test model spec")
}

fn text_message(id: &str, role: MessageRole, content: &str) -> Message {
    Message {
        id: id.to_string(),
        role,
        parts: vec![Part {
            id: format!("{id}.p0"),
            kind: PartKind::Text,
            content: content.to_string(),
            attachment: None,
            tool_call_id: None,
            tool_name: None,
            tool_replay: None,
            prune_state: PruneState::Intact,
            reasoning_meta: None,
            response_meta: None,
        }]
        .into(),
        origin: None,
    }
}

#[tokio::test]
async fn embedded_runtime_builder_loads_state_from_store() {
    let store = Arc::new(Store::memory().await.expect("store"));
    let checkpoint_ref = store
        .put_checkpoint(&lash_core::store::HydratedSessionCheckpoint {
            turn_state: PersistedTurnState {
                turn_index: 3,
                token_usage: TokenUsage {
                    input_tokens: 20,
                    output_tokens: 5,
                    cached_input_tokens: 2,
                    reasoning_tokens: 1,
                },
                last_prompt_usage: None,
                protocol_turn_options: Default::default(),
            },
            tool_state_ref: None,
            tool_state: None,
            plugin_snapshot_ref: None,
            plugin_snapshot_revision: None,
            plugin_snapshot: None,
            execution_state_ref: None,
            execution_state: None,
        })
        .await
        .checkpoint_ref;
    store
        .save_session_head(SessionHead {
            session_id: "stored-session".to_string(),
            head_revision: 0,
            agent_frames: Vec::new(),
            current_agent_frame_id: String::new(),
            graph: SessionGraph::from_active_read_state(&[text_message(
                "u0",
                MessageRole::User,
                "stored question",
            )]),
            config: PersistedSessionConfig {
                provider_id: "openai-compatible".into(),
                model: test_model_spec(),
            },
            checkpoint_ref: Some(checkpoint_ref),
            token_ledger: Vec::new(),
        })
        .await;

    let runtime = LashRuntime::builder()
        .with_store(store.clone() as Arc<dyn RuntimePersistence>)
        .with_plugin_factories(vec![Arc::new(
            lash_protocol_standard::StandardProtocolPluginFactory,
        )])
        .build()
        .await
        .expect("runtime");

    let state = runtime.export_state();
    let read_view = state.read_view();
    assert_eq!(read_view.messages().len(), 1);
    assert_eq!(read_view.messages()[0].parts[0].content, "stored question");
    assert_eq!(state.turn_index, 3);
    assert_eq!(state.token_usage.input_tokens, 20);
    assert_eq!(state.policy.model.id, "gpt-5.4-mini");
    assert_eq!(state.session_id, "stored-session");
}

#[tokio::test]
async fn embedded_runtime_builder_rejects_store_bound_to_different_session_id() {
    let store = Arc::new(Store::memory().await.expect("store"));
    store
        .save_session_head(SessionHead {
            session_id: "alpha".to_string(),
            head_revision: 0,
            agent_frames: Vec::new(),
            current_agent_frame_id: String::new(),
            graph: SessionGraph::default(),
            config: PersistedSessionConfig {
                provider_id: "openai-compatible".into(),
                model: test_model_spec(),
            },
            checkpoint_ref: None,
            token_ledger: Vec::new(),
        })
        .await;

    let err = match LashRuntime::builder()
        .with_store(store as Arc<dyn RuntimePersistence>)
        .with_session_id("beta")
        .with_plugin_factories(vec![Arc::new(
            lash_protocol_standard::StandardProtocolPluginFactory,
        )])
        .build()
        .await
    {
        Ok(_) => panic!("mismatched store session should fail"),
        Err(err) => err,
    };

    assert!(err.to_string().contains("bound to session `alpha`"));
}