dot-ai 0.6.1

A minimal AI agent that lives in your terminal
Documentation
mod transport;
pub mod types;

use transport::AcpTransport;
pub use types::*;

use anyhow::{Context, Result};

pub enum AcpMessage {
    Notification(SessionNotification),
    IncomingRequest {
        id: u64,
        method: String,
        params: serde_json::Value,
    },
    PromptComplete(PromptResponse),
    Response {
        id: u64,
        result: std::result::Result<serde_json::Value, JsonRpcError>,
    },
}

pub struct AcpClient {
    transport: AcpTransport,
    session_id: Option<SessionId>,
    agent_info: Option<Implementation>,
    agent_capabilities: Option<AgentCapabilities>,
    modes: Option<SessionModeState>,
    config_options: Option<Vec<SessionConfigOption>>,
}

impl AcpClient {
    pub fn start(command: &str, args: &[String], env: &[(String, String)]) -> Result<Self> {
        Ok(Self {
            transport: AcpTransport::spawn(command, args, env)?,
            session_id: None,
            agent_info: None,
            agent_capabilities: None,
            modes: None,
            config_options: None,
        })
    }

    pub async fn initialize(&mut self) -> Result<InitializeResponse> {
        let params = serde_json::to_value(InitializeRequest {
            protocol_version: 1,
            client_capabilities: ClientCapabilities {
                fs: FsCapabilities {
                    read_text_file: true,
                    write_text_file: true,
                },
                terminal: true,
            },
            client_info: Some(Implementation {
                name: "dot".into(),
                title: Some("dot".into()),
                version: Some(env!("CARGO_PKG_VERSION").into()),
            }),
        })
        .context("serializing initialize request")?;

        let raw = self.transport.send_request("initialize", params).await?;
        let resp: InitializeResponse =
            serde_json::from_value(raw).context("parsing initialize response")?;
        self.agent_info = resp.agent_info.clone();
        self.agent_capabilities = Some(resp.agent_capabilities.clone());
        if let Some(ref info) = resp.agent_info {
            tracing::info!(agent = %info.name, version = ?info.version, "ACP initialized");
        }
        Ok(resp)
    }

    pub async fn authenticate(&mut self, method_id: &str) -> Result<AuthenticateResponse> {
        let params = serde_json::to_value(AuthenticateRequest {
            method_id: method_id.into(),
        })
        .context("serializing authenticate request")?;
        let raw = self.transport.send_request("authenticate", params).await?;
        serde_json::from_value(raw).context("parsing authenticate response")
    }

    pub async fn new_session(
        &mut self,
        cwd: &str,
        mcp_servers: Vec<McpServer>,
    ) -> Result<NewSessionResponse> {
        let params = serde_json::to_value(NewSessionRequest {
            cwd: cwd.into(),
            mcp_servers,
        })
        .context("serializing session/new request")?;
        let raw = self.transport.send_request("session/new", params).await?;
        let resp: NewSessionResponse =
            serde_json::from_value(raw).context("parsing session/new response")?;
        self.session_id = Some(resp.session_id.clone());
        self.modes = resp.modes.clone();
        self.config_options = resp.config_options.clone();
        tracing::info!(session_id = %resp.session_id, "ACP session created");
        Ok(resp)
    }

    pub async fn load_session(
        &mut self,
        session_id: &str,
        cwd: &str,
        mcp_servers: Vec<McpServer>,
    ) -> Result<LoadSessionResponse> {
        let params = serde_json::to_value(LoadSessionRequest {
            session_id: session_id.into(),
            cwd: cwd.into(),
            mcp_servers,
        })
        .context("serializing session/load request")?;
        let raw = self.transport.send_request("session/load", params).await?;
        let resp: LoadSessionResponse =
            serde_json::from_value(raw).context("parsing session/load response")?;
        self.session_id = Some(session_id.into());
        self.modes = resp.modes.clone();
        self.config_options = resp.config_options.clone();
        Ok(resp)
    }

    pub async fn send_prompt(&mut self, text: &str) -> Result<()> {
        let sid = self
            .session_id
            .as_deref()
            .context("no active session")?
            .to_string();
        let params = serde_json::to_value(PromptRequest {
            session_id: sid,
            prompt: vec![ContentBlock::Text { text: text.into() }],
        })
        .context("serializing session/prompt request")?;
        let id = self.transport.next_id();
        self.transport
            .write_request(id, "session/prompt", params)
            .await
    }

    pub async fn send_prompt_with_content(&mut self, content: Vec<ContentBlock>) -> Result<()> {
        let sid = self
            .session_id
            .as_deref()
            .context("no active session")?
            .to_string();
        let params = serde_json::to_value(PromptRequest {
            session_id: sid,
            prompt: content,
        })
        .context("serializing session/prompt request")?;
        let id = self.transport.next_id();
        self.transport
            .write_request(id, "session/prompt", params)
            .await
    }

    pub async fn read_next(&mut self) -> Result<AcpMessage> {
        if let Some(n) = self.transport.buffered_notifications.pop_front()
            && let Ok(sn) = serde_json::from_value::<SessionNotification>(n.params.clone())
        {
            return Ok(AcpMessage::Notification(sn));
        }
        if let Some(r) = self.transport.buffered_requests.pop_front() {
            return Ok(AcpMessage::IncomingRequest {
                id: r.id,
                method: r.method,
                params: r.params,
            });
        }
        loop {
            let msg = self.transport.read_message().await?;
            match msg {
                JsonRpcMessage::Notification(n) => {
                    if let Ok(sn) = serde_json::from_value::<SessionNotification>(n.params.clone())
                    {
                        return Ok(AcpMessage::Notification(sn));
                    }
                }
                JsonRpcMessage::Request(r) => {
                    return Ok(AcpMessage::IncomingRequest {
                        id: r.id,
                        method: r.method,
                        params: r.params,
                    });
                }
                JsonRpcMessage::Response(resp) => {
                    if let Some(err) = resp.error {
                        return Ok(AcpMessage::Response {
                            id: resp.id,
                            result: Err(err),
                        });
                    }
                    let result = resp.result.unwrap_or(serde_json::Value::Null);
                    if let Ok(pr) = serde_json::from_value::<PromptResponse>(result.clone()) {
                        return Ok(AcpMessage::PromptComplete(pr));
                    }
                    return Ok(AcpMessage::Response {
                        id: resp.id,
                        result: Ok(result),
                    });
                }
            }
        }
    }

    pub async fn cancel(&mut self) -> Result<()> {
        let sid = self
            .session_id
            .as_deref()
            .context("no active session")?
            .to_string();
        let params = serde_json::to_value(CancelNotification { session_id: sid })
            .context("serializing cancel")?;
        self.transport
            .send_notification("session/cancel", params)
            .await
    }

    pub async fn set_mode(&mut self, mode_id: &str) -> Result<SetSessionModeResponse> {
        let sid = self
            .session_id
            .as_deref()
            .context("no active session")?
            .to_string();
        let params = serde_json::to_value(SetSessionModeRequest {
            session_id: sid,
            mode_id: mode_id.into(),
        })
        .context("serializing set_mode request")?;
        let raw = self
            .transport
            .send_request("session/set_mode", params)
            .await?;
        serde_json::from_value(raw).context("parsing set_mode response")
    }

    pub fn drain_notifications(&mut self) -> Vec<SessionNotification> {
        self.transport
            .drain_notifications()
            .into_iter()
            .filter_map(|n| serde_json::from_value::<SessionNotification>(n.params).ok())
            .collect()
    }

    pub fn drain_incoming_requests(&mut self) -> Vec<JsonRpcRequest> {
        self.transport.drain_requests()
    }

    pub async fn respond(&mut self, id: u64, result: serde_json::Value) -> Result<()> {
        self.transport.send_response(id, result).await
    }

    pub async fn respond_error(&mut self, id: u64, code: i32, message: &str) -> Result<()> {
        self.transport.send_error_response(id, code, message).await
    }

    pub fn session_id(&self) -> Option<&str> {
        self.session_id.as_deref()
    }

    pub fn agent_info(&self) -> Option<&Implementation> {
        self.agent_info.as_ref()
    }

    pub fn current_mode(&self) -> Option<&str> {
        self.modes.as_ref().map(|m| m.current_mode_id.as_str())
    }

    pub fn available_modes(&self) -> &[SessionMode] {
        self.modes
            .as_ref()
            .map(|m| m.available_modes.as_slice())
            .unwrap_or(&[])
    }

    pub fn set_current_mode(&mut self, mode_id: &str) {
        if let Some(ref mut modes) = self.modes {
            modes.current_mode_id = mode_id.to_string();
        }
    }

    pub fn config_options(&self) -> &[SessionConfigOption] {
        self.config_options.as_deref().unwrap_or(&[])
    }

    pub fn set_config_options(&mut self, options: Vec<SessionConfigOption>) {
        self.config_options = Some(options);
    }

    pub fn kill(&mut self) -> Result<()> {
        self.transport.kill()
    }
}