rho-coding-agent 1.15.0

A lightweight agent harness inspired by Pi
Documentation
use std::time::Duration;

use pretty_assertions::assert_eq;
use rho_sdk::{
    tool::ToolProgress, ProviderStreamResetReason, Revision, RunEvent, RunId, ToolCallId,
    ToolCompletion,
};
use serde_json::json;

use super::{parse_duration, JsonlAdapter};

#[test]
fn parses_human_durations_and_rejects_invalid_values() {
    assert_eq!(parse_duration("20m").unwrap(), Duration::from_secs(1_200));
    assert!(parse_duration("soon")
        .unwrap_err()
        .contains("invalid duration"));
}

#[test]
fn serializes_started_event_with_version_and_monotonic_sequence() {
    let mut adapter = JsonlAdapter::new();
    adapter.set_run_context("session-1", std::path::Path::new("/workspace"));
    let run_id = RunId::from_string("run-1").unwrap();

    let started = adapter
        .event(&RunEvent::Started {
            run_id,
            revision: Revision::from_u64(0),
        })
        .unwrap();
    let delta = adapter
        .event(&RunEvent::AssistantTextDelta { text: "hi".into() })
        .unwrap();

    assert_eq!(
        serde_json::to_value(started).unwrap(),
        json!({
            "schema_version": 1,
            "seq": 1,
            "type": "run.started",
            "run_id": "run-1",
            "session_id": "session-1",
            "workspace": "/workspace"
        })
    );
    assert_eq!(
        serde_json::to_value(delta).unwrap(),
        json!({
            "schema_version": 1,
            "seq": 2,
            "type": "assistant.text_delta",
            "attempt": 1,
            "text": "hi"
        })
    );
}

#[test]
fn emits_safe_tool_lifecycle_fields_only() {
    let mut adapter = JsonlAdapter::new();
    let call_id = ToolCallId::from_string("call-1").unwrap();

    assert!(adapter
        .event(&RunEvent::ToolUpdated {
            call_id: call_id.clone(),
            progress: ToolProgress::message("private progress text"),
        })
        .is_none());
    let updated = adapter
        .event(&RunEvent::ToolUpdated {
            call_id: call_id.clone(),
            progress: ToolProgress::message("private progress text").units(1, 2),
        })
        .unwrap();
    let finished = adapter
        .event(&RunEvent::ToolFinished {
            call_id,
            result: ToolCompletion::Unavailable,
        })
        .unwrap();

    assert_eq!(
        serde_json::to_value(updated).unwrap(),
        json!({
            "schema_version": 1,
            "seq": 1,
            "type": "tool.updated",
            "call_id": "call-1",
            "completed_units": 1,
            "total_units": 2
        })
    );
    assert_eq!(
        serde_json::to_value(finished).unwrap(),
        json!({
            "schema_version": 1,
            "seq": 2,
            "type": "tool.finished",
            "call_id": "call-1",
            "status": "unavailable"
        })
    );
}

#[test]
fn reset_closes_the_old_attempt_and_advances_new_deltas() {
    let mut adapter = JsonlAdapter::new();
    let first = adapter
        .event(&RunEvent::AssistantTextDelta {
            text: "discarded".into(),
        })
        .unwrap();
    let reset = adapter
        .event(&RunEvent::ProviderStreamReset {
            reason: ProviderStreamResetReason::InvalidResponse,
            detail: "private provider detail".into(),
        })
        .unwrap();
    let second = adapter
        .event(&RunEvent::AssistantTextDelta {
            text: "kept".into(),
        })
        .unwrap();

    assert_eq!(serde_json::to_value(first).unwrap()["attempt"], 1);
    assert_eq!(
        serde_json::to_value(reset).unwrap(),
        json!({
            "schema_version": 1,
            "seq": 2,
            "type": "assistant.text_reset",
            "attempt": 1
        })
    );
    assert_eq!(serde_json::to_value(second).unwrap()["attempt"], 2);
    assert_eq!(adapter.partial_text().as_deref(), Some("kept"));
}