claude-code-sdk-rust 0.2.0

Async Rust SDK for the Claude Code CLI: streaming agent turns, tool use, and sessions.
Documentation
use claude_code_sdk_rust::session_store::{
    project_key_for_directory, InMemorySessionStore, SessionKey, SessionStore,
};
use claude_code_sdk_rust::sessions::{
    delete_session_via_store, fork_session_via_store, get_session_info_from_store,
    get_session_messages_from_store, get_subagent_messages_from_store, list_sessions_from_store,
    list_subagents_from_store, rename_session_via_store, tag_session_via_store,
};

fn entry(
    message_type: &str,
    session_id: &str,
    content: &str,
) -> serde_json::Map<String, serde_json::Value> {
    serde_json::json!({
        "type": message_type,
        "uuid": uuid::Uuid::new_v4().to_string(),
        "timestamp": "2026-05-08T10:00:00Z",
        "session_id": session_id,
        "cwd": "/repo",
        "message": {
            "role": message_type,
            "content": content
        }
    })
    .as_object()
    .unwrap()
    .clone()
}

#[tokio::test]
async fn lists_sessions_from_store_using_summary_sidecars() {
    let cwd = std::env::temp_dir().join(format!("claude-rust-store-list-{}", uuid::Uuid::new_v4()));
    tokio::fs::create_dir_all(&cwd).await.unwrap();
    let project_key = project_key_for_directory(Some(&cwd));
    let store = InMemorySessionStore::new();
    let older = uuid::Uuid::new_v4().to_string();
    let newer = uuid::Uuid::new_v4().to_string();

    store
        .append(
            SessionKey {
                project_key: project_key.clone(),
                session_id: older.clone(),
                subpath: None,
            },
            vec![entry("user", &older, "older prompt")],
        )
        .await
        .unwrap();
    store
        .append(
            SessionKey {
                project_key,
                session_id: newer.clone(),
                subpath: None,
            },
            vec![entry("user", &newer, "newer prompt")],
        )
        .await
        .unwrap();
    let handle = claude_code_sdk_rust::session_store::SessionStoreHandle::new(store);

    let sessions = list_sessions_from_store(&handle, Some(cwd.to_str().unwrap()), None, 0)
        .await
        .unwrap();

    assert_eq!(sessions.len(), 2);
    assert_eq!(sessions[0].session_id, newer);
    assert_eq!(sessions[0].summary, "newer prompt");
    assert_eq!(sessions[1].session_id, older);

    let _ = tokio::fs::remove_dir_all(cwd).await;
}

#[tokio::test]
async fn gets_info_and_messages_from_store() {
    let cwd = std::env::temp_dir().join(format!(
        "claude-rust-store-messages-{}",
        uuid::Uuid::new_v4()
    ));
    tokio::fs::create_dir_all(&cwd).await.unwrap();
    let project_key = project_key_for_directory(Some(&cwd));
    let session_id = uuid::Uuid::new_v4().to_string();
    let store = InMemorySessionStore::new();
    store
        .append(
            SessionKey {
                project_key,
                session_id: session_id.clone(),
                subpath: None,
            },
            vec![
                entry("user", &session_id, "hello"),
                entry("assistant", &session_id, "hi"),
                serde_json::json!({"type": "tag", "tag": "important"})
                    .as_object()
                    .unwrap()
                    .clone(),
            ],
        )
        .await
        .unwrap();
    let handle = claude_code_sdk_rust::session_store::SessionStoreHandle::new(store);

    let info = get_session_info_from_store(&handle, &session_id, Some(cwd.to_str().unwrap()))
        .await
        .unwrap()
        .expect("info");
    assert_eq!(info.summary, "hello");
    assert_eq!(info.tag.as_deref(), Some("important"));

    let messages =
        get_session_messages_from_store(&handle, &session_id, Some(cwd.to_str().unwrap()), None, 0)
            .await
            .unwrap();
    assert_eq!(messages.len(), 2);
    assert_eq!(messages[0].r#type, "user");
    assert_eq!(messages[1].r#type, "assistant");

    let messages = get_session_messages_from_store(
        &handle,
        &session_id,
        Some(cwd.to_str().unwrap()),
        Some(0),
        0,
    )
    .await
    .unwrap();
    assert_eq!(messages.len(), 2);

    let _ = tokio::fs::remove_dir_all(cwd).await;
}

#[tokio::test]
async fn invalid_store_session_id_returns_empty_results() {
    let handle =
        claude_code_sdk_rust::session_store::SessionStoreHandle::new(InMemorySessionStore::new());

    assert!(
        get_session_info_from_store(&handle, "../../etc/passwd", None)
            .await
            .unwrap()
            .is_none()
    );
    assert!(
        get_session_messages_from_store(&handle, "../../etc/passwd", None, None, 0)
            .await
            .unwrap()
            .is_empty()
    );
}

#[tokio::test]
async fn mutates_store_session_metadata_and_delete() {
    let cwd =
        std::env::temp_dir().join(format!("claude-rust-store-mutate-{}", uuid::Uuid::new_v4()));
    tokio::fs::create_dir_all(&cwd).await.unwrap();
    let project_key = project_key_for_directory(Some(&cwd));
    let session_id = uuid::Uuid::new_v4().to_string();
    let store = InMemorySessionStore::new();
    store
        .append(
            SessionKey {
                project_key,
                session_id: session_id.clone(),
                subpath: None,
            },
            vec![entry("user", &session_id, "original")],
        )
        .await
        .unwrap();
    let handle = claude_code_sdk_rust::session_store::SessionStoreHandle::new(store);

    rename_session_via_store(
        &handle,
        &session_id,
        "  renamed  ",
        Some(cwd.to_str().unwrap()),
    )
    .await
    .unwrap();
    tag_session_via_store(
        &handle,
        &session_id,
        Some(" important "),
        Some(cwd.to_str().unwrap()),
    )
    .await
    .unwrap();

    let info = get_session_info_from_store(&handle, &session_id, Some(cwd.to_str().unwrap()))
        .await
        .unwrap()
        .unwrap();
    assert_eq!(info.summary, "renamed");
    assert_eq!(info.custom_title.as_deref(), Some("renamed"));
    assert_eq!(info.tag.as_deref(), Some("important"));

    tag_session_via_store(&handle, &session_id, None, Some(cwd.to_str().unwrap()))
        .await
        .unwrap();
    let info = get_session_info_from_store(&handle, &session_id, Some(cwd.to_str().unwrap()))
        .await
        .unwrap()
        .unwrap();
    assert_eq!(info.tag, None);

    delete_session_via_store(&handle, &session_id, Some(cwd.to_str().unwrap()))
        .await
        .unwrap();
    assert!(
        get_session_info_from_store(&handle, &session_id, Some(cwd.to_str().unwrap()))
            .await
            .unwrap()
            .is_none()
    );

    let _ = tokio::fs::remove_dir_all(cwd).await;
}

#[tokio::test]
async fn lists_and_reads_subagent_messages_from_store() {
    let cwd = std::env::temp_dir().join(format!(
        "claude-rust-store-subagents-{}",
        uuid::Uuid::new_v4()
    ));
    tokio::fs::create_dir_all(&cwd).await.unwrap();
    let project_key = project_key_for_directory(Some(&cwd));
    let session_id = uuid::Uuid::new_v4().to_string();
    let store = InMemorySessionStore::new();
    store
        .append(
            SessionKey {
                project_key: project_key.clone(),
                session_id: session_id.clone(),
                subpath: Some("subagents/workflows/run-1/agent-alpha".to_string()),
            },
            vec![
                entry("user", &session_id, "sub hello"),
                serde_json::json!({"type": "agent_metadata", "name": "alpha"})
                    .as_object()
                    .unwrap()
                    .clone(),
            ],
        )
        .await
        .unwrap();
    store
        .append(
            SessionKey {
                project_key,
                session_id: session_id.clone(),
                subpath: Some("metadata/ignored".to_string()),
            },
            vec![entry("user", &session_id, "ignored")],
        )
        .await
        .unwrap();
    let handle = claude_code_sdk_rust::session_store::SessionStoreHandle::new(store);

    let subagents = list_subagents_from_store(&handle, &session_id, Some(cwd.to_str().unwrap()))
        .await
        .unwrap();
    assert_eq!(subagents, vec!["alpha".to_string()]);

    let messages = get_subagent_messages_from_store(
        &handle,
        &session_id,
        "alpha",
        Some(cwd.to_str().unwrap()),
        None,
        0,
    )
    .await
    .unwrap();
    assert_eq!(messages.len(), 1);
    assert_eq!(messages[0].message["content"], "sub hello");

    let _ = tokio::fs::remove_dir_all(cwd).await;
}

#[tokio::test]
async fn forks_store_session_with_new_session_and_message_ids() {
    let cwd = std::env::temp_dir().join(format!("claude-rust-store-fork-{}", uuid::Uuid::new_v4()));
    tokio::fs::create_dir_all(&cwd).await.unwrap();
    let project_key = project_key_for_directory(Some(&cwd));
    let session_id = uuid::Uuid::new_v4().to_string();
    let store = InMemorySessionStore::new();
    let first = uuid::Uuid::new_v4().to_string();
    let second = uuid::Uuid::new_v4().to_string();
    let mut first_entry = entry("user", &session_id, "first");
    first_entry.insert("uuid".to_string(), serde_json::json!(first));
    let mut second_entry = entry("assistant", &session_id, "second");
    second_entry.insert("uuid".to_string(), serde_json::json!(second));
    store
        .append(
            SessionKey {
                project_key,
                session_id: session_id.clone(),
                subpath: None,
            },
            vec![first_entry, second_entry],
        )
        .await
        .unwrap();
    let handle = claude_code_sdk_rust::session_store::SessionStoreHandle::new(store);

    let fork = fork_session_via_store(
        &handle,
        &session_id,
        Some(cwd.to_str().unwrap()),
        None,
        Some("fork title"),
    )
    .await
    .unwrap();

    assert_ne!(fork.session_id, session_id);
    let fork_messages = get_session_messages_from_store(
        &handle,
        &fork.session_id,
        Some(cwd.to_str().unwrap()),
        None,
        0,
    )
    .await
    .unwrap();
    assert_eq!(fork_messages.len(), 2);
    assert_ne!(fork_messages[0].uuid, first);
    assert_eq!(fork_messages[0].session_id, fork.session_id);

    let info = get_session_info_from_store(&handle, &fork.session_id, Some(cwd.to_str().unwrap()))
        .await
        .unwrap()
        .unwrap();
    assert_eq!(info.custom_title.as_deref(), Some("fork title"));

    let _ = tokio::fs::remove_dir_all(cwd).await;
}