agecli-skill-protocol 0.2.0

Wire protocol contract for agecli skill ↔ host UDS communication (binary framing + payload types)
Documentation
//! Write skill messages to a stream.

use super::message::*;
use serde::Serialize;
use tokio::io::{AsyncWrite, AsyncWriteExt};

/// Writes binary-framed skill messages to a stream.
pub struct SkillMessageWriter<W> {
    writer: W,
}

impl<W: AsyncWrite + Unpin> SkillMessageWriter<W> {
    pub fn new(writer: W) -> Self {
        Self { writer }
    }

    /// Write a raw skill message to the stream.
    async fn write_message(&mut self, msg: &SkillMessage) -> std::io::Result<()> {
        let type_bytes = (msg.msg_type as u32).to_be_bytes();
        let len_bytes = (msg.payload.len() as u32).to_be_bytes();

        self.writer.write_all(&type_bytes).await?;
        self.writer.write_all(&len_bytes).await?;
        if !msg.payload.is_empty() {
            self.writer.write_all(&msg.payload).await?;
        }
        self.writer.flush().await?;
        Ok(())
    }

    /// Write a typed payload as a message.
    async fn write_typed<T: Serialize>(
        &mut self,
        msg_type: SkillMessageType,
        payload: &T,
    ) -> std::io::Result<()> {
        let json = serde_json::to_vec(payload)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
        self.write_message(&SkillMessage::new(msg_type, json)).await
    }

    // ── Server → Skill messages ──────────────────────────────────────────

    pub async fn write_execute(&mut self, payload: &ExecutePayload) -> std::io::Result<()> {
        self.write_typed(SkillMessageType::Execute, payload).await
    }

    pub async fn write_cancel(&mut self, execution_id: &str) -> std::io::Result<()> {
        let payload = execution_id.as_bytes().to_vec();
        self.write_message(&SkillMessage::new(SkillMessageType::Cancel, payload))
            .await
    }

    pub async fn write_stdin_data(&mut self, payload: &StdinDataPayload) -> std::io::Result<()> {
        self.write_typed(SkillMessageType::StdinData, payload).await
    }

    pub async fn write_resize(&mut self, payload: &ResizePayload) -> std::io::Result<()> {
        self.write_typed(SkillMessageType::Resize, payload).await
    }

    pub async fn write_signal(&mut self, payload: &SignalPayload) -> std::io::Result<()> {
        self.write_typed(SkillMessageType::Signal, payload).await
    }

    pub async fn write_start_session(
        &mut self,
        payload: &StartSessionPayload,
    ) -> std::io::Result<()> {
        self.write_typed(SkillMessageType::StartSession, payload)
            .await
    }

    pub async fn write_shutdown(&mut self) -> std::io::Result<()> {
        self.write_message(&SkillMessage::new(SkillMessageType::Shutdown, vec![]))
            .await
    }

    // ── Skill → Server messages ──────────────────────────────────────────

    pub async fn write_ack(&mut self, payload: &AckPayload) -> std::io::Result<()> {
        self.write_typed(SkillMessageType::Ack, payload).await
    }

    pub async fn write_stdout_chunk(&mut self, payload: &DataChunkPayload) -> std::io::Result<()> {
        self.write_typed(SkillMessageType::StdoutChunk, payload)
            .await
    }

    pub async fn write_stderr_chunk(&mut self, payload: &DataChunkPayload) -> std::io::Result<()> {
        self.write_typed(SkillMessageType::StderrChunk, payload)
            .await
    }

    pub async fn write_progress(&mut self, payload: &ProgressPayload) -> std::io::Result<()> {
        self.write_typed(SkillMessageType::Progress, payload).await
    }

    pub async fn write_completed(&mut self, payload: &CompletedPayload) -> std::io::Result<()> {
        self.write_typed(SkillMessageType::Completed, payload).await
    }

    pub async fn write_error(&mut self, payload: &ErrorPayload) -> std::io::Result<()> {
        self.write_typed(SkillMessageType::Error, payload).await
    }

    pub async fn write_session_started(
        &mut self,
        payload: &SessionStartedPayload,
    ) -> std::io::Result<()> {
        self.write_typed(SkillMessageType::SessionStarted, payload)
            .await
    }
}

#[cfg(test)]
#[path = "writer_tests.rs"]
mod tests;