rho-coding-agent 1.20.0

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

use rho_sdk::model::{ContextUsage, ModelUsage};

use crate::tui::{app_state::SessionUiPhase, event_adapter::ViewModelEvent, tests::test_app};

fn model_call_metrics() -> rho_sdk::ModelCallMetrics {
    rho_sdk::ModelCallMetrics {
        output_tokens: Some(100),
        time_to_first_token: Some(Duration::from_millis(100)),
        generation_time: Some(Duration::from_secs(2)),
        total_latency: Duration::from_millis(2_100),
    }
}

#[test]
fn context_usage_event_is_tracked_separately_from_cumulative_usage() {
    let mut app = test_app();
    app.usage.cumulative_usage = Some(ModelUsage {
        input_tokens: Some(1_000),
        output_tokens: Some(500),
        ..ModelUsage::default()
    });

    assert!(app
        .record_agent_event(ViewModelEvent::ContextUsage(ContextUsage::estimated(
            250,
            Some(10_000),
        )))
        .is_none());

    assert_eq!(
        app.usage.current_context,
        Some(ContextUsage::estimated(250, Some(10_000)))
    );
    assert_eq!(
        app.usage
            .cumulative_usage
            .as_ref()
            .and_then(|usage| usage.input_tokens),
        Some(1_000)
    );
}

#[test]
fn completed_model_call_updates_the_active_model_average() {
    let mut app = test_app();
    let profile = app.info.runtime.model_call_profile();
    let metrics = model_call_metrics();

    app.record_agent_event(ViewModelEvent::ModelCallCompleted {
        profile: profile.clone(),
        metrics,
    });

    let summary = app.usage.model_performance.summary(&profile);
    assert_eq!(summary.latest_call, Some(metrics));
    assert_eq!(summary.average_output_tokens_per_second, Some(50.0));
    assert_eq!(summary.eligible_calls, 1);
}

#[test]
fn provider_stream_reset_preserves_completed_model_performance() {
    let mut app = test_app();
    let profile = app.info.runtime.model_call_profile();
    app.record_agent_event(ViewModelEvent::ModelCallCompleted {
        profile: profile.clone(),
        metrics: model_call_metrics(),
    });

    app.record_agent_event(ViewModelEvent::ProviderStreamReset);

    let summary = app.usage.model_performance.summary(&profile);
    assert_eq!(summary.average_output_tokens_per_second, Some(50.0));
    assert_eq!(summary.eligible_calls, 1);
}

#[test]
fn step_started_clears_stream_state_without_clearing_model_performance() {
    let mut app = test_app();
    let profile = app.info.runtime.model_call_profile();
    app.streams.assistant_stream.push_delta("current");
    app.streams.reasoning_stream.push_delta("reasoning");
    app.record_agent_event(ViewModelEvent::ModelCallCompleted {
        profile: profile.clone(),
        metrics: model_call_metrics(),
    });

    assert!(app
        .record_agent_event(ViewModelEvent::StepStarted(2))
        .is_none());

    assert!(app.streams.assistant_stream.is_empty());
    assert!(app.streams.reasoning_stream.is_empty());
    let summary = app.usage.model_performance.summary(&profile);
    assert_eq!(summary.average_output_tokens_per_second, Some(50.0));
    assert_eq!(app.turn.session_ui(), SessionUiPhase::ProviderTurn);
    assert_eq!(app.status, "running step 2");
}