gemini-cli-sdk 0.1.0

Rust SDK wrapping Google's Gemini CLI as a subprocess via JSON-RPC 2.0
Documentation
//! Wire capabilities types — what the client advertises and what the agent reports.

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// Capabilities we advertise to the agent during `initialize`.
///
/// We deliberately advertise an empty set — no filesystem access, no terminal.
/// All optional extensions from the agent's perspective are captured in `extra`.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ClientCapabilities {
    /// Unknown/future capability keys are preserved for forward-compatibility.
    #[serde(flatten)]
    pub extra: Value,
}

/// Capabilities the agent reports back in the `initialize` response.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AgentCapabilities {
    /// Whether the agent supports streaming session updates.
    #[serde(default)]
    pub streaming: Option<bool>,
    /// List of tool names the agent exposes.
    #[serde(default)]
    pub tools: Option<Vec<String>>,
    /// Unknown/future capability keys are preserved for forward-compatibility.
    #[serde(flatten)]
    pub extra: Value,
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn test_empty_client_capabilities_serializes_to_empty_object() {
        let caps = ClientCapabilities::default();
        let serialized = serde_json::to_value(&caps).expect("serialize ClientCapabilities");
        // An empty ClientCapabilities with a null `extra` (from Default) should round-trip
        // to a JSON object — either `{}` or `{"key": null}` depending on flatten behavior.
        // What matters is that it is an object, not null/array.
        assert!(
            serialized.is_object(),
            "ClientCapabilities must serialize to a JSON object, got: {serialized}"
        );
    }

    #[test]
    fn test_client_capabilities_roundtrip_with_extra() {
        let raw = json!({ "experimentalFeatureX": true, "version": 2 });
        let caps: ClientCapabilities =
            serde_json::from_value(raw.clone()).expect("deserialize ClientCapabilities");
        let back = serde_json::to_value(&caps).expect("re-serialize ClientCapabilities");
        assert_eq!(back, raw, "extra fields must survive a roundtrip");
    }

    #[test]
    fn test_agent_capabilities_defaults() {
        let caps = AgentCapabilities::default();
        assert!(caps.streaming.is_none());
        assert!(caps.tools.is_none());
    }

    #[test]
    fn test_agent_capabilities_roundtrip() {
        let raw = json!({
            "streaming": true,
            "tools": ["read_file", "run_shell"],
            "futureField": "ignored-gracefully"
        });
        let caps: AgentCapabilities =
            serde_json::from_value(raw.clone()).expect("deserialize AgentCapabilities");
        assert_eq!(caps.streaming, Some(true));
        let expected: &[String] = &[
            "read_file".to_string(),
            "run_shell".to_string(),
        ];
        assert_eq!(caps.tools.as_deref(), Some(expected));
        let back = serde_json::to_value(&caps).expect("re-serialize AgentCapabilities");
        assert_eq!(back, raw, "all fields including extra must survive a roundtrip");
    }
}