whycodes-session 0.6.31

Conversation state, history and compaction for WhyCodes
Documentation
use super::*;
use whycodes_core::types::{ContentBlock, ImageSource, MessageContent};

fn call(id: &str, name: &str) -> ToolCall {
    ToolCall {
        id: id.into(),
        name: name.into(),
        arguments: serde_json::json!({}),
    }
}

fn result(id: &str, is_error: bool) -> ToolResult {
    ToolResult {
        tool_call_id: id.into(),
        content: "out".into(),
        is_error,
    }
}

fn todo(status: TodoStatus) -> TodoItem {
    TodoItem {
        id: String::new(),
        content: "x".into(),
        status,
    }
}

#[test]
fn records_requests_per_model_and_tools_with_failures() {
    let mut run = RunStats::default();
    let usage = Usage {
        input_tokens: 1_500,
        output_tokens: 20,
        ..Default::default()
    };
    run.record_request("cf", "glm", &usage);
    run.record_request("cf", "glm", &Usage::default());
    run.record_request("cf", "kimi", &usage);
    run.record_tools(
        &[call("a", "bash"), call("b", "bash"), call("c", "read")],
        &[result("a", true), result("b", false), result("c", false)],
    );

    assert_eq!(run.requests(), 3);
    assert_eq!(
        run.models["cf/glm"],
        ModelTally {
            requests: 2,
            input_tokens: 1_500,
            output_tokens: 20
        }
    );
    assert_eq!(run.tool_calls(), 3);
    assert_eq!(run.tool_errors(), 1);
    assert_eq!(
        run.tools["bash"],
        ToolTally {
            calls: 2,
            errors: 1
        }
    );
}

#[test]
fn prompt_count_skips_notes_summaries_and_tool_carriers() {
    let mut s = Session::new("/p".into(), "sys".into());
    s.add_user_message("fix it");
    s.add_user_message("   ");
    s.add_user_message("[Compacted] 1. Primary Request: x");
    s.add_user_message("note");
    if let Some(m) = s.messages.last_mut() {
        m.name = Some(whycodes_core::types::AGENT_NOTE_NAME.into());
    }
    s.add_user_message("blocks");
    if let Some(m) = s.messages.last_mut() {
        m.content = MessageContent::Blocks(vec![ContentBlock::Image {
            source: ImageSource::Url {
                url: "a.png".into(),
            },
        }]);
    }
    s.add_user_message("carrier");
    if let Some(m) = s.messages.last_mut() {
        m.content = MessageContent::Blocks(vec![ContentBlock::ToolResult {
            tool_use_id: "t".into(),
            content: "r".into(),
            is_error: None,
        }]);
    }
    s.add_assistant_message(vec![ContentBlock::Text { text: "ok".into() }]);

    assert_eq!(s.prompt_count(), 2);
}

#[test]
fn report_lists_models_tools_subagents_and_todos() {
    let mut s = Session::new("/p".into(), "sys".into());
    s.add_user_message("go");
    s.usage.input_tokens = 2_500_000;
    s.usage.output_tokens = 999;
    s.run_stats.record_request("cf", "glm", &s.usage.clone());
    s.run_stats.record_tools(
        &[call("a", "task"), call("b", "bash"), call("c", "bash")],
        &[result("a", false), result("b", true), result("c", false)],
    );
    let todos = [
        todo(TodoStatus::Completed),
        todo(TodoStatus::InProgress),
        todo(TodoStatus::Pending),
        todo(TodoStatus::Cancelled),
    ];
    let now = s.created_at + chrono::Duration::minutes(75);

    let out = stats_report(&s, &todos, now);

    assert!(out.contains("duration:  1h 15m"), "{out}");
    assert!(out.contains("prompts:   1"), "{out}");
    assert!(out.contains("requests:  1 this run"), "{out}");
    assert!(
        out.contains("cf/glm: 1 request · 2.5M in / 999 out"),
        "{out}"
    );
    assert!(out.contains("tools:     3 calls · 1 failed"), "{out}");
    assert!(out.contains("bash 2 (1 failed) · task 1"), "{out}");
    assert!(out.contains("subagents: 1 launched"), "{out}");
    assert!(
        out.contains("todos:     1/4 done · 1 in progress · 1 pending · 1 cancelled"),
        "{out}"
    );
    assert!(out.contains("tokens:    2.5M in / 999 out"), "{out}");
}

#[test]
fn report_on_a_fresh_session_shows_zeroes() {
    let s = Session::new("/p".into(), "sys".into());
    let out = stats_report(&s, &[], s.created_at + chrono::Duration::seconds(30));

    assert!(out.contains("duration:  0m"), "{out}");
    assert!(out.contains("requests:  0 this run"), "{out}");
    assert!(out.contains("tools:     0 calls · 0 failed"), "{out}");
    assert!(out.contains("subagents: 0 launched"), "{out}");
    assert!(out.contains("todos:     0/0 done"), "{out}");
}

#[test]
fn compact_scales_thousands() {
    assert_eq!(compact(12_300), "12.3k");
    assert_eq!(compact(7), "7");
    assert_eq!(compact(3_000_000), "3.0M");
}