agent-sessions 0.2.1

Stream Claude Code and Codex JSONL sessions as typed Rust events with source offsets
Documentation
use agent_sessions::*;
use serde_json::json;
use std::io::Cursor;

fn events(agent: Agent, s: &str, include: EventKinds) -> Vec<Located<Event>> {
    read_from(
        agent,
        Cursor::new(s),
        &ReadOptions {
            include,
            ..Default::default()
        },
    )
    .unwrap()
    .collect::<Result<Vec<_>, _>>()
    .unwrap()
}

#[test]
fn codex_origin_preserves_raw_evidence_and_structured_source_wins() {
    let cases = [
        (
            json!({"source":{"subagent":{}},"originator":"Codex Desktop"}),
            Origin::Subagent,
        ),
        (
            json!({"source":"exec","originator":"Codex Desktop"}),
            Origin::Exec,
        ),
        (
            json!({"source":"vscode","originator":"codex-tui"}),
            Origin::Ide,
        ),
        (
            json!({"source":"cli","thread_source":"subagent"}),
            Origin::Subagent,
        ),
        (json!({"originator":"Codex Desktop"}), Origin::Ide),
        (json!({"source":"future"}), Origin::Unknown),
        (json!({"source":"cli"}), Origin::Interactive),
    ];
    for (payload, expected) in cases {
        let row = json!({"type":"session_meta","payload":payload}).to_string();
        let e = events(Agent::Codex, &row, EventKinds::ALL);
        let Event::Meta(m) = &e[0].value else {
            panic!("meta expected")
        };
        assert_eq!(m.origin, Some(expected));
        assert_eq!(m.source, payload.get("source").cloned());
    }
}

#[test]
fn meta_is_kept_and_timestamp_compatibility_is_explicit() {
    for flag in ["isMeta", "is_meta"] {
        let mut row = json!({"type":"user","created_at":"100","message":{"content":"  meta  "}});
        row[flag] = json!(true);
        let e = events(Agent::ClaudeCode, &row.to_string(), EventKinds::ALL);
        assert_eq!(e[0].at.unwrap().timestamp(), 100);
        assert!(matches!(&e[0].value,Event::Message(m) if m.is_meta && m.text=="  meta  "));
    }
}

#[test]
fn canonical_event_index_survives_filtering() {
    let row=json!({"type":"assistant","cwd":"/work","message":{"content":[
        {"type":"text","text":"hello"},{"type":"tool_use","id":"t","name":"Read","input":{"path":"a"}}
    ]}}).to_string();
    let all = events(Agent::ClaudeCode, &row, EventKinds::ALL);
    let calls = events(Agent::ClaudeCode, &row, EventKinds::TOOL_CALL);
    assert_eq!(calls.len(), 1);
    assert_eq!(calls[0], all[2]);
}

#[test]
fn codex_tool_arguments_are_not_repaired_or_reinterpreted() {
    let row = json!({"type":"response_item","payload":{"type":"function_call","name":"exec",
        "call_id":"t1","arguments":"{incomplete"}})
    .to_string();
    let e = events(Agent::Codex, &row, EventKinds::ALL);
    assert!(
        matches!(&e[0].value,Event::ToolCall(t) if t.arguments==ToolArgs::RawString("{incomplete".into()))
    );
}

#[test]
fn legacy_progress_and_tool_results_are_separate_events() {
    let row = json!({"type":"progress","data":{"message":{"message":{"id":"m","content":[
        {"type":"tool_use","id":"t","name":"Read","input":{}}
    ]}}}})
    .to_string();
    assert!(matches!(
        &events(Agent::ClaudeCode, &row, EventKinds::ALL)[0].value,
        Event::ToolCall(_)
    ));
    let row = json!({"type":"user","message":{"content":[{"type":"tool_result",
        "tool_use_id":"t","content":"result","is_error":true}]}})
    .to_string();
    let e = events(Agent::ClaudeCode, &row, EventKinds::ALL);
    assert!(matches!(&e[0].value,Event::Message(m) if m.text.is_empty()));
    assert!(
        matches!(&e[1].value,Event::ToolResult(t) if t.text=="result" && t.is_error==Some(true))
    );
}

#[test]
fn unknown_diagnostics_are_bounded_and_content_drift_is_visible() {
    let mut rows = (0..500)
        .map(|i| json!({"type":format!("unknown-{i}")}).to_string())
        .collect::<Vec<_>>();
    rows.push(json!({"type":"user","message":{"content":[{"type":"future_content"}]}}).to_string());
    let mut r = read_from(
        Agent::ClaudeCode,
        Cursor::new(rows.join("\n")),
        &ReadOptions::default(),
    )
    .unwrap();
    for event in r.by_ref() {
        event.unwrap();
    }
    let s = r.finish();
    assert_eq!(s.unknown_types.len(), 129);
    assert_eq!(s.unknown_types.values().sum::<u64>(), 501);
}