collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
//! ACP (Agent Client Protocol) message types.
//!
//! JSON-RPC 2.0 based protocol for IDE ↔ Agent communication.
//! Reuses `mcp::protocol::{JsonRpcRequest, JsonRpcResponse, JsonRpcError}` for the envelope.

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

// ---------------------------------------------------------------------------
// ACP method names
// ---------------------------------------------------------------------------

pub const METHOD_SESSION_INITIALIZE: &str = "session/initialize";
pub const METHOD_SESSION_NEW: &str = "session/new";
pub const METHOD_SESSION_PROMPT: &str = "session/prompt";
pub const METHOD_SESSION_UPDATE: &str = "session/update";
pub const METHOD_SESSION_CANCEL: &str = "session/cancel";
pub const METHOD_SESSION_CLOSE: &str = "session/close";

// ---------------------------------------------------------------------------
// session/initialize
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitializeParams {
    /// Client name (e.g. "IntelliJ IDEA", "Zed")
    #[serde(default)]
    pub client_name: String,
    /// Client version
    #[serde(default)]
    pub client_version: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AcpCapabilities {
    pub text_prompts: bool,
    pub tool_calls: bool,
    pub file_operations: bool,
    pub session_resume: bool,
    pub modes: Vec<String>,
}

impl Default for AcpCapabilities {
    fn default() -> Self {
        Self {
            text_prompts: true,
            tool_calls: true,
            file_operations: true,
            session_resume: true,
            modes: vec![
                "code".to_string(),
                "ask".to_string(),
                "architect".to_string(),
            ],
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitializeResult {
    pub name: String,
    pub version: String,
    pub capabilities: AcpCapabilities,
}

// ---------------------------------------------------------------------------
// session/new
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionNewParams {
    /// Working directory for this session.
    pub cwd: String,
    /// Optional MCP server configurations to connect.
    #[serde(default)]
    pub mcp_servers: Vec<McpServerConfig>,
    /// Agent mode: "code" | "ask" | "architect"
    #[serde(default = "default_mode")]
    pub mode: String,
}

fn default_mode() -> String {
    "code".to_string()
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerConfig {
    pub name: String,
    #[serde(default)]
    pub command: Option<String>,
    #[serde(default)]
    pub args: Vec<String>,
    #[serde(default)]
    pub env: std::collections::HashMap<String, String>,
    #[serde(default)]
    pub url: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionNewResult {
    pub session_id: String,
}

// ---------------------------------------------------------------------------
// session/prompt
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionPromptParams {
    pub session_id: String,
    pub text: String,
    /// Override mode for this prompt only.
    #[serde(default)]
    pub mode: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionPromptResult {
    pub accepted: bool,
}

// ---------------------------------------------------------------------------
// session/update (notification, server → client)
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum UpdatePayload {
    Token {
        text: String,
    },
    Response {
        text: String,
    },
    ToolCall {
        name: String,
        args: String,
    },
    ToolResult {
        name: String,
        result: String,
        success: bool,
    },
    FileModified {
        path: String,
    },
    Status {
        iteration: u32,
        elapsed_secs: u64,
    },
    Phase {
        label: String,
    },
    Plan {
        text: String,
    },
    Error {
        message: String,
    },
    Done,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionUpdateNotification {
    pub session_id: String,
    pub payload: UpdatePayload,
}

// ---------------------------------------------------------------------------
// session/cancel
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionCancelParams {
    pub session_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionCancelResult {
    pub cancelled: bool,
}

// ---------------------------------------------------------------------------
// session/close
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionCloseParams {
    pub session_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionCloseResult {
    pub closed: bool,
}

// ---------------------------------------------------------------------------
// JSON-RPC notification (no id)
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcNotification {
    pub jsonrpc: String,
    pub method: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub params: Option<Value>,
}

impl JsonRpcNotification {
    pub fn new(method: &str, params: Value) -> Self {
        Self {
            jsonrpc: "2.0".to_string(),
            method: method.to_string(),
            params: Some(params),
        }
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn test_initialize_params_roundtrip() {
        let params = InitializeParams {
            client_name: "IntelliJ IDEA".to_string(),
            client_version: "2026.1".to_string(),
        };
        let json = serde_json::to_value(&params).unwrap();
        let decoded: InitializeParams = serde_json::from_value(json).unwrap();
        assert_eq!(decoded.client_name, "IntelliJ IDEA");
    }

    #[test]
    fn test_capabilities_default() {
        let caps = AcpCapabilities::default();
        assert!(caps.text_prompts);
        assert!(caps.tool_calls);
        assert!(caps.session_resume);
        assert_eq!(caps.modes.len(), 3);
    }

    #[test]
    fn test_session_new_params_roundtrip() {
        let json = json!({
            "cwd": "/home/user/project",
            "mode": "ask"
        });
        let params: SessionNewParams = serde_json::from_value(json).unwrap();
        assert_eq!(params.cwd, "/home/user/project");
        assert_eq!(params.mode, "ask");
        assert!(params.mcp_servers.is_empty());
    }

    #[test]
    fn test_update_payload_tagged() {
        let payload = UpdatePayload::Token {
            text: "hello".to_string(),
        };
        let json = serde_json::to_value(&payload).unwrap();
        assert_eq!(json["type"], "token");
        assert_eq!(json["text"], "hello");
    }

    #[test]
    fn test_update_payload_done() {
        let payload = UpdatePayload::Done;
        let json = serde_json::to_value(&payload).unwrap();
        assert_eq!(json["type"], "done");
    }

    #[test]
    fn test_notification_no_id() {
        let notif = JsonRpcNotification::new(
            "session/update",
            json!({"session_id": "abc", "payload": {"type": "done"}}),
        );
        let serialized = serde_json::to_string(&notif).unwrap();
        assert!(!serialized.contains("\"id\""));
        assert!(serialized.contains("session/update"));
    }
}