claude-code-sdk-rust 0.2.0

Async Rust SDK for the Claude Code CLI: streaming agent turns, tool use, and sessions.
Documentation
use async_trait::async_trait;
use claude_code_sdk_rust::internal::session_store_validation::validate_session_store_options;
use claude_code_sdk_rust::{
    list_sessions_from_store, ClaudeAgentClient, ClaudeAgentOptions, InMemorySessionStore,
    SessionKey, SessionStore, SessionStoreEntry, SessionStoreHandle, SessionStoreListEntry,
};

#[derive(Clone)]
struct MinimalStore;

#[derive(Clone)]
struct FlakyListStore {
    good_session_id: String,
    bad_session_id: String,
}

#[async_trait]
impl SessionStore for MinimalStore {
    async fn append(
        &self,
        _key: SessionKey,
        _entries: Vec<SessionStoreEntry>,
    ) -> claude_code_sdk_rust::Result<()> {
        Ok(())
    }

    async fn load(
        &self,
        _key: SessionKey,
    ) -> claude_code_sdk_rust::Result<Option<Vec<SessionStoreEntry>>> {
        Ok(None)
    }
}

#[async_trait]
impl SessionStore for FlakyListStore {
    fn supports_list_sessions(&self) -> bool {
        true
    }

    async fn append(
        &self,
        _key: SessionKey,
        _entries: Vec<SessionStoreEntry>,
    ) -> claude_code_sdk_rust::Result<()> {
        Ok(())
    }

    async fn load(
        &self,
        key: SessionKey,
    ) -> claude_code_sdk_rust::Result<Option<Vec<SessionStoreEntry>>> {
        if key.session_id == self.bad_session_id {
            return Err(claude_code_sdk_rust::ClaudeSDKError::Session(
                "backend down".to_string(),
            ));
        }
        Ok(Some(vec![serde_json::json!({
            "type": "user",
            "uuid": uuid::Uuid::new_v4().to_string(),
            "session_id": self.good_session_id,
            "timestamp": "2026-05-09T10:00:00Z",
            "message": {"role": "user", "content": "good prompt"}
        })
        .as_object()
        .unwrap()
        .clone()]))
    }

    async fn list_sessions(
        &self,
        _project_key: &str,
    ) -> claude_code_sdk_rust::Result<Vec<SessionStoreListEntry>> {
        Ok(vec![
            SessionStoreListEntry {
                session_id: self.good_session_id.clone(),
                mtime: 20,
            },
            SessionStoreListEntry {
                session_id: self.bad_session_id.clone(),
                mtime: 10,
            },
        ])
    }
}

#[test]
fn session_store_options_accept_store_without_continue() {
    let options = ClaudeAgentOptions::builder()
        .session_store(MinimalStore)
        .build();

    validate_session_store_options(&options).expect("minimal store is valid without continue");
}

#[test]
fn session_store_continue_requires_list_sessions_when_resume_is_absent() {
    let options = ClaudeAgentOptions::builder()
        .session_store(MinimalStore)
        .continue_conversation(true)
        .build();

    let err = validate_session_store_options(&options).expect_err("continue should fail fast");
    assert!(err.to_string().contains("list_sessions"));
}

#[test]
fn session_store_continue_allows_explicit_resume_without_list_sessions() {
    let options = ClaudeAgentOptions::builder()
        .session_store(MinimalStore)
        .continue_conversation(true)
        .resume("00000000-0000-4000-8000-000000000000")
        .build();

    validate_session_store_options(&options).expect("resume wins over continue");
}

#[test]
fn session_store_continue_accepts_in_memory_store() {
    let options = ClaudeAgentOptions::builder()
        .session_store(InMemorySessionStore::new())
        .continue_conversation(true)
        .build();

    validate_session_store_options(&options).expect("in-memory store supports list_sessions");
}

#[test]
fn session_store_rejects_file_checkpointing_combo() {
    let options = ClaudeAgentOptions::builder()
        .session_store(InMemorySessionStore::new())
        .enable_file_checkpointing(true)
        .build();

    let err = validate_session_store_options(&options).expect_err("checkpointing should fail fast");
    assert!(err.to_string().contains("enable_file_checkpointing"));
}

#[test]
fn client_new_validates_session_store_options_before_spawn() {
    let options = ClaudeAgentOptions::builder()
        .session_store(MinimalStore)
        .continue_conversation(true)
        .build();

    let err = ClaudeAgentClient::new(options).expect_err("client construction should fail fast");
    assert!(err.to_string().contains("list_sessions"));
}

#[tokio::test]
async fn list_sessions_from_store_requires_list_sessions_support() {
    let handle = SessionStoreHandle::new(MinimalStore);

    let err = list_sessions_from_store(&handle, None, None, 0)
        .await
        .expect_err("list_sessions_from_store should require list_sessions");
    assert!(err.to_string().contains("list_sessions"));
}

#[tokio::test]
async fn list_sessions_from_store_degrades_rows_when_load_fails() {
    let good_session_id = uuid::Uuid::new_v4().to_string();
    let bad_session_id = uuid::Uuid::new_v4().to_string();
    let handle = SessionStoreHandle::new(FlakyListStore {
        good_session_id: good_session_id.clone(),
        bad_session_id: bad_session_id.clone(),
    });

    let sessions = list_sessions_from_store(&handle, None, None, 0)
        .await
        .expect("one bad row should not fail the whole list");

    let good = sessions
        .iter()
        .find(|session| session.session_id == good_session_id)
        .expect("good row");
    let bad = sessions
        .iter()
        .find(|session| session.session_id == bad_session_id)
        .expect("degraded bad row");
    assert_eq!(good.summary, "good prompt");
    assert_eq!(bad.summary, "");
    assert_eq!(bad.last_modified, 10);
}