rushai-core 0.1.0

Engine for rushai: sessions, storage, and the agent loop
Documentation
use rushai_core::store::{Store, StoredMessage};
use rushai_protocol::{FinishReason, MessageId, Part, Role, SessionId, TokenUsage};
use serde_json::json;

fn message(session: &SessionId, parts: Vec<Part>) -> StoredMessage {
    StoredMessage {
        id: MessageId::new(),
        session: session.clone(),
        role: Role::Assistant,
        provider: Some("anthropic".into()),
        model: Some("claude-opus-5".into()),
        parts,
        is_summary: false,
        created_at: 1,
    }
}

#[tokio::test]
async fn session_round_trip() {
    let store = Store::open_in_memory().unwrap();
    let parent = store.create_session("parent".into(), None).await.unwrap();
    let child = store
        .create_session("child".into(), Some(parent.id.clone()))
        .await
        .unwrap();

    assert_eq!(store.session(&child.id).await.unwrap().unwrap(), child);
    assert_eq!(child.parent.as_ref(), Some(&parent.id));
    assert!(
        store
            .session(&SessionId::from("missing"))
            .await
            .unwrap()
            .is_none()
    );

    let all = store.sessions().await.unwrap();
    assert_eq!(all.len(), 2);
}

#[tokio::test]
async fn message_parts_round_trip() {
    let store = Store::open_in_memory().unwrap();
    let session = store.create_session("s".into(), None).await.unwrap();
    let msg = message(
        &session.id,
        vec![
            Part::Text {
                text: "hello".into(),
            },
            Part::Reasoning {
                text: "thinking".into(),
                signature: Some("sig".into()),
            },
            Part::ToolCall {
                id: "c1".into(),
                name: "bash".into(),
                input: json!({ "command": "ls" }),
            },
            Part::ToolResult {
                id: "c1".into(),
                content: "Cargo.toml".into(),
                is_error: false,
            },
            Part::Finish {
                reason: FinishReason::EndTurn,
                usage: TokenUsage {
                    input: 10,
                    output: 4,
                    cache_read: 2,
                    cache_write: 1,
                },
            },
        ],
    );
    store.save_message(&msg).await.unwrap();

    let stored = store.messages(&session.id).await.unwrap();
    assert_eq!(stored, vec![msg]);
}

#[tokio::test]
async fn save_message_upserts() {
    let store = Store::open_in_memory().unwrap();
    let session = store.create_session("s".into(), None).await.unwrap();
    let mut msg = message(
        &session.id,
        vec![Part::Text {
            text: "draft".into(),
        }],
    );
    store.save_message(&msg).await.unwrap();

    msg.parts = vec![Part::Text {
        text: "final".into(),
    }];
    store.save_message(&msg).await.unwrap();

    let stored = store.messages(&session.id).await.unwrap();
    assert_eq!(stored.len(), 1);
    assert_eq!(stored[0].parts, msg.parts);
}

#[tokio::test]
async fn messages_keep_creation_order() {
    let store = Store::open_in_memory().unwrap();
    let session = store.create_session("s".into(), None).await.unwrap();
    for (at, text) in [(3, "third"), (1, "first"), (2, "second")] {
        let mut msg = message(&session.id, vec![Part::Text { text: text.into() }]);
        msg.created_at = at;
        store.save_message(&msg).await.unwrap();
    }
    let texts: Vec<_> = store
        .messages(&session.id)
        .await
        .unwrap()
        .into_iter()
        .map(|m| match &m.parts[0] {
            Part::Text { text } => text.clone(),
            other => panic!("unexpected part {other:?}"),
        })
        .collect();
    assert_eq!(texts, ["first", "second", "third"]);
}

#[tokio::test]
async fn deleting_a_session_removes_its_messages() {
    let store = Store::open_in_memory().unwrap();
    let session = store.create_session("s".into(), None).await.unwrap();
    let msg = message(&session.id, vec![Part::Text { text: "x".into() }]);
    store.save_message(&msg).await.unwrap();

    store.delete_session(&session.id).await.unwrap();
    assert!(store.session(&session.id).await.unwrap().is_none());
    assert!(store.messages(&session.id).await.unwrap().is_empty());
}

#[tokio::test]
async fn data_survives_reopen() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("rushai.db");

    let store = Store::open(&path).unwrap();
    let session = store
        .create_session("persisted".into(), None)
        .await
        .unwrap();
    let msg = message(
        &session.id,
        vec![Part::Text {
            text: "kept".into(),
        }],
    );
    store.save_message(&msg).await.unwrap();
    drop(store);

    let store = Store::open(&path).unwrap();
    assert_eq!(
        store.session(&session.id).await.unwrap().unwrap().title,
        "persisted"
    );
    assert_eq!(store.messages(&session.id).await.unwrap(), vec![msg]);
}