rho-coding-agent 1.12.2

A lightweight agent harness inspired by Pi
Documentation
use std::sync::Arc;

use async_trait::async_trait;
use serde::Deserialize;

use {
    crate::diagnostics::RuntimeDiagnostics,
    rho_sdk::tool::{
        OperationKind, PreparedToolInvocation, Tool as SdkTool, ToolContext as SdkToolContext,
        ToolError as SdkToolError, ToolErrorKind, ToolFuture, ToolInvocation, ToolMetadata,
        ToolOutput, ToolPreparationContext, ToolPrepareFuture, ToolSecurity,
    },
    rho_tools::tool::{Tool, ToolContext, ToolError, ToolResult, ToolSpec},
};

pub(super) fn sdk_bundle(
    diagnostics: RuntimeDiagnostics,
    max_output_bytes: usize,
) -> super::sdk_registry::StaticToolBundle {
    super::sdk_registry::StaticToolBundle::new(vec![Arc::new(SdkRho::with_max_output_bytes(
        diagnostics,
        max_output_bytes,
    ))])
}

pub(super) struct SdkRho {
    diagnostics: RuntimeDiagnostics,
    max_output_bytes: usize,
}

impl SdkRho {
    #[cfg(test)]
    pub(super) fn new(diagnostics: RuntimeDiagnostics) -> Self {
        Self::with_max_output_bytes(diagnostics, rho_tools::DEFAULT_MAX_OUTPUT_BYTES)
    }

    fn with_max_output_bytes(diagnostics: RuntimeDiagnostics, max_output_bytes: usize) -> Self {
        Self {
            diagnostics,
            max_output_bytes: max_output_bytes.max(1),
        }
    }

    fn execute(&self, args: Args) -> Result<ToolOutput, SdkToolError> {
        self.diagnostics
            .response(&args.action)
            .map(|content| {
                ToolOutput::text(rho_tools::tool::truncate(content, self.max_output_bytes))
            })
            .map_err(|message| SdkToolError::new(ToolErrorKind::InvalidArguments, message))
    }
}

impl SdkTool for SdkRho {
    fn spec(&self) -> rho_sdk::model::ToolSpec {
        Rho::new(self.diagnostics.clone()).spec()
    }

    fn security(&self) -> ToolSecurity {
        ToolSecurity::built_in([])
    }

    fn call<'a>(&'a self, invocation: ToolInvocation, context: SdkToolContext) -> ToolFuture<'a> {
        rho_sdk::tool::call_prepared(self, invocation, context)
    }

    fn prepare<'a>(
        &'a self,
        invocation: ToolInvocation,
        _context: ToolPreparationContext,
    ) -> ToolPrepareFuture<'a> {
        let args = parse_args(invocation.into_arguments());
        Box::pin(async move {
            let args = args?;
            Ok(PreparedToolInvocation::resource_aware(
                [],
                [],
                ToolMetadata::new().operation(OperationKind::Read),
                move |_context| Box::pin(async move { self.execute(args) }),
            ))
        })
    }
}

fn parse_args(arguments: serde_json::Value) -> Result<Args, SdkToolError> {
    let args: Args = serde_json::from_value(arguments)
        .map_err(|error| SdkToolError::new(ToolErrorKind::InvalidArguments, error.to_string()))?;
    if matches!(
        args.action.as_str(),
        "info" | "context" | "prompt_sources" | "tools" | "config"
    ) {
        Ok(args)
    } else {
        Err(SdkToolError::new(
            ToolErrorKind::InvalidArguments,
            format!(
                "unknown rho diagnostics action '{}'; load the rho-diagnostics skill for usage",
                args.action
            ),
        ))
    }
}

pub struct Rho {
    diagnostics: RuntimeDiagnostics,
}

impl Rho {
    pub fn new(diagnostics: RuntimeDiagnostics) -> Self {
        Self { diagnostics }
    }
}

#[derive(Deserialize)]
struct Args {
    action: String,
}

#[async_trait]
impl Tool for Rho {
    fn spec(&self) -> ToolSpec {
        ToolSpec {
            name: "rho".into(),
            description: "Inspect the running Rho harness. Use info for basic runtime identity."
                .into(),
            input_schema: serde_json::json!({
                "type": "object",
                "properties": {
                    "action": {
                        "type": "string",
                        "description": "Read-only diagnostics action"
                    }
                },
                "required": ["action"],
                "additionalProperties": false
            }),
        }
    }

    async fn call(
        &self,
        args: serde_json::Value,
        _ctx: ToolContext,
        id: String,
    ) -> Result<ToolResult, ToolError> {
        let args: Args = serde_json::from_value(args)?;
        let content = self
            .diagnostics
            .response(&args.action)
            .map_err(ToolError::Message)?;
        Ok(ToolResult {
            id,
            ok: true,
            content,
        })
    }
}

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