claude-code-sdk-rust 0.1.0

Async Rust SDK for the Claude Code CLI: streaming agent turns, tool use, and sessions.
Documentation
use claude_agent_sdk::{AssistantContent, ContentBlock, Message, UserContent};
use serde_json::json;

use claude_agent_sdk::internal::parser::parse_message_value;

#[test]
fn deserializes_user_message_from_cli_wire_shape() {
    let raw = json!({
        "type": "user",
        "message": {
            "role": "user",
            "content": "hello"
        },
        "parent_tool_use_id": null,
        "session_id": "default",
        "uuid": "user-1"
    });

    let parsed: Message = serde_json::from_value(raw).expect("valid user message");

    match parsed {
        Message::UserMsg {
            content:
                UserContent {
                    role,
                    content: serde_json::Value::String(text),
                },
            uuid,
            parent_tool_use_id,
            ..
        } => {
            assert_eq!(role, Some("user".to_string()));
            assert_eq!(text, "hello");
            assert_eq!(uuid, Some("user-1".to_string()));
            assert_eq!(parent_tool_use_id, None);
        }
        other => panic!("expected user message, got {other:?}"),
    }
}

#[test]
fn deserializes_assistant_message_with_nested_metadata_and_server_tools() {
    let raw = json!({
        "type": "assistant",
        "message": {
            "id": "msg_1",
            "model": "claude-sonnet-4-5",
            "stop_reason": "end_turn",
            "usage": {"input_tokens": 1, "output_tokens": 2},
            "content": [
                {"type": "text", "text": "hi"},
                {"type": "server_tool_use", "id": "srv_1", "name": "web_search", "input": {"query": "rust"}},
                {"type": "advisor_tool_result", "tool_use_id": "srv_1", "content": {"type": "web_search_result"}}
            ]
        },
        "session_id": "sess_1",
        "uuid": "assistant-1"
    });

    let parsed: Message = serde_json::from_value(raw).expect("valid assistant message");

    match parsed {
        Message::AssistantMsg {
            content:
                AssistantContent {
                    id,
                    model,
                    stop_reason,
                    usage,
                    content,
                },
            session_id,
            uuid,
            ..
        } => {
            assert_eq!(id, Some("msg_1".to_string()));
            assert_eq!(model, "claude-sonnet-4-5");
            assert_eq!(stop_reason, Some("end_turn".to_string()));
            assert_eq!(
                usage.unwrap().get("output_tokens").and_then(|v| v.as_i64()),
                Some(2)
            );
            assert_eq!(session_id, Some("sess_1".to_string()));
            assert_eq!(uuid, Some("assistant-1".to_string()));
            assert!(matches!(content[0], ContentBlock::Text { .. }));
            assert!(matches!(content[1], ContentBlock::ServerToolUse { .. }));
            assert!(matches!(content[2], ContentBlock::ServerToolResult { .. }));
        }
        other => panic!("expected assistant message, got {other:?}"),
    }
}

#[test]
fn deserializes_result_message_current_fields() {
    let raw = json!({
        "type": "result",
        "subtype": "success",
        "duration_ms": 10,
        "duration_api_ms": 8,
        "is_error": false,
        "num_turns": 1,
        "session_id": "sess_1",
        "stop_reason": "end_turn",
        "modelUsage": {"claude-sonnet-4-5": {"input_tokens": 1}},
        "deferred_tool_use": {"id": "toolu_1", "name": "Bash", "input": {"command": "pwd"}},
        "errors": ["recoverable"],
        "api_error_status": 429,
        "uuid": "result-1"
    });

    let parsed: Message = serde_json::from_value(raw).expect("valid result message");

    match parsed {
        Message::ResultMsg {
            model_usage,
            deferred_tool_use,
            errors,
            api_error_status,
            uuid,
            ..
        } => {
            assert!(model_usage.unwrap().contains_key("claude-sonnet-4-5"));
            assert_eq!(deferred_tool_use.unwrap().name, "Bash");
            assert_eq!(errors, Some(vec!["recoverable".to_string()]));
            assert_eq!(api_error_status, Some(429));
            assert_eq!(uuid, Some("result-1".to_string()));
        }
        other => panic!("expected result message, got {other:?}"),
    }
}

#[test]
fn parser_skips_unknown_message_types_for_forward_compatibility() {
    let raw = json!({
        "type": "future_message",
        "payload": {"kept": true}
    });

    let parsed = parse_message_value(raw).expect("unknown messages are not parse errors");

    assert!(parsed.is_none());
}

#[test]
fn parser_returns_typed_task_system_messages() {
    let raw = json!({
        "type": "system",
        "subtype": "task_progress",
        "task_id": "task-abc",
        "description": "Halfway",
        "usage": {"total_tokens": 123, "tool_uses": 2, "duration_ms": 456},
        "uuid": "uuid-1",
        "session_id": "session-1",
        "tool_use_id": "toolu_1",
        "last_tool_name": "Read"
    });

    let parsed = parse_message_value(raw)
        .expect("valid task message")
        .unwrap();

    match parsed {
        Message::TaskProgressMsg(message) => {
            assert_eq!(message.task_id, "task-abc");
            assert_eq!(message.usage.total_tokens, 123);
            assert_eq!(message.tool_use_id.as_deref(), Some("toolu_1"));
            assert_eq!(message.last_tool_name.as_deref(), Some("Read"));
        }
        other => panic!("expected task progress message, got {other:?}"),
    }
}

#[test]
fn parser_returns_typed_hook_system_messages() {
    let raw = json!({
        "type": "system",
        "subtype": "hook_response",
        "hook_event": "PostToolUse",
        "session_id": "session-1",
        "uuid": "uuid-1",
        "output": "",
        "exit_code": 0,
        "outcome": "success"
    });

    let parsed = parse_message_value(raw)
        .expect("valid hook message")
        .unwrap();

    match parsed {
        Message::HookEventMsg(message) => {
            assert_eq!(message.subtype, "hook_response");
            assert_eq!(message.hook_event_name.as_deref(), Some("PostToolUse"));
            assert_eq!(message.session_id.as_deref(), Some("session-1"));
            assert_eq!(message.data["outcome"], "success");
        }
        other => panic!("expected hook event message, got {other:?}"),
    }
}

#[test]
fn parser_returns_typed_mirror_error_messages() {
    let raw = json!({
        "type": "system",
        "subtype": "mirror_error",
        "key": {"project_key": "proj", "session_id": "sess"},
        "error": "disk full"
    });

    let parsed = parse_message_value(raw)
        .expect("valid mirror error")
        .unwrap();

    match parsed {
        Message::MirrorErrorMsg(message) => {
            assert_eq!(message.error, "disk full");
            assert_eq!(
                message.key.as_ref().and_then(|key| key.get("session_id")),
                Some(&json!("sess"))
            );
            assert_eq!(message.data["subtype"], "mirror_error");
        }
        other => panic!("expected mirror error message, got {other:?}"),
    }
}

#[test]
fn parser_accepts_python_rate_limit_wire_values() {
    let raw = json!({
        "type": "rate_limit_event",
        "rate_limit_info": {
            "status": "allowed_warning",
            "resetsAt": 1700000000,
            "rateLimitType": "five_hour",
            "utilization": 0.91
        },
        "uuid": "abc-123",
        "session_id": "session_xyz"
    });

    let parsed = parse_message_value(raw)
        .expect("valid rate limit event")
        .unwrap();

    match parsed {
        Message::RateLimitEventMsg {
            rate_limit_info,
            uuid,
            session_id,
        } => {
            assert_eq!(uuid, "abc-123");
            assert_eq!(session_id, "session_xyz");
            assert_eq!(rate_limit_info.resets_at, Some(1700000000));
            assert_eq!(rate_limit_info.utilization, Some(0.91));
        }
        other => panic!("expected rate limit event, got {other:?}"),
    }
}