rho-coding-agent 0.27.1

A lightweight agent harness inspired by Pi
Documentation
use super::*;
use crate::tool::*;
use serde::Deserialize;
use serde_json::json;
use std::time::Duration;

const STOP_GRACE: Duration = Duration::from_secs(2);

macro_rules! result {
    ($id:expr,$value:expr) => {
        Ok(ToolResult {
            id: $id,
            ok: true,
            content: serde_json::to_string(&$value).unwrap(),
        })
    };
}

#[derive(Clone)]
pub struct Process(ProcessManager);

impl Process {
    pub fn new(manager: ProcessManager) -> Self {
        Self(manager)
    }
}

#[derive(Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
enum ProcessArgs {
    Start {
        command: String,
        timeout_seconds: Option<u64>,
    },
    Poll {
        process_id: String,
        cursor: Option<u64>,
        #[serde(default)]
        wait_seconds: u64,
    },
    Stop {
        process_id: String,
    },
}

#[async_trait::async_trait]
impl Tool for Process {
    fn spec(&self) -> ToolSpec {
        ToolSpec {
            name: "process".into(),
            description: "Manage a background process: start requires command; poll and stop require process_id.".into(),
            input_schema: json!({
                "type": "object",
                "properties": {
                    "action": {"type": "string", "enum": ["start", "poll", "stop"]},
                    "command": {"type": "string"},
                    "timeout_seconds": {"type": "integer", "minimum": 1},
                    "process_id": {"type": "string"},
                    "cursor": {"type": "integer", "minimum": 0},
                    "wait_seconds": {"type": "integer", "minimum": 0, "maximum": 30}
                },
                "required": ["action"]
            }),
        }
    }

    fn display_command(&self, args: &serde_json::Value) -> Option<String> {
        (args["action"] == "start")
            .then(|| args["command"].as_str().map(str::to_owned))
            .flatten()
    }

    async fn call(
        &self,
        args: serde_json::Value,
        context: ToolContext,
        id: String,
    ) -> Result<ToolResult, ToolError> {
        match serde_json::from_value(args)? {
            ProcessArgs::Start {
                command,
                timeout_seconds,
            } => {
                let snapshot = self
                    .0
                    .start(
                        command,
                        &context.cwd,
                        timeout_seconds.map(Duration::from_secs),
                    )
                    .await
                    .map_err(ToolError::Message)?;
                result!(id, snapshot)
            }
            ProcessArgs::Poll {
                process_id,
                cursor,
                wait_seconds,
            } => {
                if wait_seconds > 30 {
                    return Err(ToolError::Message(
                        "wait_seconds must be between 0 and 30".into(),
                    ));
                }
                let snapshot = self
                    .0
                    .poll_bounded(
                        &process_id,
                        cursor,
                        Duration::from_secs(wait_seconds),
                        context.max_output_bytes,
                    )
                    .await
                    .map_err(ToolError::Message)?;
                result!(id, snapshot)
            }
            ProcessArgs::Stop { process_id } => {
                self.0
                    .stop(&process_id, STOP_GRACE)
                    .await
                    .map_err(ToolError::Message)?;
                result!(id, json!({"process_id":process_id,"stop_requested":true}))
            }
        }
    }
}