rho-coding-agent 0.28.1

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

use pretty_assertions::assert_eq;
use serde_json::json;
use tokio::{
    io::{AsyncReadExt, AsyncWriteExt},
    net::TcpListener,
};

use super::*;
use crate::{
    credentials::{save_xai_tokens, MemoryCredentialStore, XaiTokens},
    model::Message,
    tool::ToolSpec,
};

#[test]
fn responses_body_preserves_tools_cache_key_and_supported_reasoning() {
    let messages = [
        Message::System("follow instructions".into()),
        Message::user_text("fix it"),
    ];
    let tools = [ToolSpec {
        name: "read_file".into(),
        description: "read a file".into(),
        input_schema: json!({"type": "object"}),
    }];

    let body = build_xai_responses_body(
        "grok-4.5",
        ModelRequest {
            messages: &messages,
            tools: &tools,
            cancellation: Default::default(),
            prompt_cache_key: Some("rho:session"),
        },
        xai_reasoning_effort("grok-4.5", ReasoningLevel::High),
    )
    .unwrap();

    assert_eq!(body["model"], "grok-4.5");
    assert_eq!(body["instructions"], "follow instructions");
    assert_eq!(body["input"][0]["role"], "user");
    assert_eq!(body["tools"][0]["name"], "read_file");
    assert_eq!(body["tool_choice"], "auto");
    assert_eq!(body["prompt_cache_key"], "rho:session");
    assert_eq!(body["reasoning"], json!({"effort": "high"}));
    assert_eq!(body["stream"], true);
    assert_eq!(body["store"], false);
}

#[test]
fn reasoning_effort_is_only_sent_for_supported_models() {
    assert_eq!(
        xai_reasoning_effort("grok-4.5", ReasoningLevel::Off),
        Some("low")
    );
    assert_eq!(
        xai_reasoning_effort("grok-4.3", ReasoningLevel::Off),
        Some("none")
    );
    assert_eq!(
        xai_reasoning_effort("grok-build-0.1", ReasoningLevel::High),
        None
    );
    assert_eq!(
        xai_reasoning_effort("grok-composer-2.5-fast", ReasoningLevel::High),
        None
    );
}

#[tokio::test]
async fn provider_posts_to_responses_and_collects_stream() {
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let api_base = format!("http://{}", listener.local_addr().unwrap());
    let server = tokio::spawn(async move {
        let (mut stream, _) = listener.accept().await.unwrap();
        let mut request = Vec::new();
        let mut chunk = [0_u8; 4096];
        loop {
            let count = stream.read(&mut chunk).await.unwrap();
            request.extend_from_slice(&chunk[..count]);
            let text = String::from_utf8_lossy(&request);
            let Some((headers, body)) = text.split_once("\r\n\r\n") else {
                continue;
            };
            let content_length = headers
                .lines()
                .find_map(|line| {
                    let (name, value) = line.split_once(':')?;
                    name.eq_ignore_ascii_case("content-length")
                        .then(|| value.trim().parse::<usize>().unwrap())
                })
                .unwrap();
            if body.len() >= content_length {
                break;
            }
        }
        let request = String::from_utf8(request).unwrap();
        assert!(request.starts_with("POST /responses HTTP/1.1\r\n"));
        assert!(request.contains("authorization: Bearer access-token\r\n"));
        let body: Value = serde_json::from_str(request.split_once("\r\n\r\n").unwrap().1).unwrap();
        assert_eq!(body["reasoning"], json!({"effort": "medium"}));

        let event = "data: {\"type\":\"response.output_text.delta\",\"delta\":\"hello\"}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"response-1\"}}\n\ndata: [DONE]\n\n";
        let response = format!(
            "HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{event}",
            event.len()
        );
        stream.write_all(response.as_bytes()).await.unwrap();
    });

    let store = Arc::new(MemoryCredentialStore::default());
    save_xai_tokens(
        store.as_ref(),
        &XaiTokens {
            access_token: "access-token".into(),
            refresh_token: None,
            expires_at_unix: None,
            id_token: None,
        },
    )
    .unwrap();
    let provider =
        XaiProvider::new_with_api_base("grok-4.5".into(), store, ReasoningLevel::Medium, api_base)
            .unwrap();

    let response = provider
        .send_turn(ModelRequest {
            messages: &[Message::user_text("hello")],
            tools: &[],
            cancellation: Default::default(),
            prompt_cache_key: None,
        })
        .await
        .unwrap();

    let ModelResponse::Assistant(blocks) = response;
    assert!(matches!(
        blocks.as_slice(),
        [crate::model::ContentBlock::Text(text)] if text == "hello"
    ));
    server.await.unwrap();
}