roder-api 0.1.0

Agentic software development tools and SDKs for Roder.
Documentation
use roder_api::events::{
    EventEnvelope, EventSource, RoderEvent, ThreadId, TranscriptItemAppended, TurnCompleted,
    TurnInterrupted, TurnStarted,
};
use roder_api::inference::{RuntimeProfile, TokenUsage};
use roder_api::thread::project_turns_from_events;
use roder_api::transcript::{AssistantMessage, TranscriptItem, UserMessage};
use time::OffsetDateTime;

fn envelope(seq: u64, timestamp: OffsetDateTime, event: RoderEvent) -> EventEnvelope {
    let kind = event.kind().to_string();
    EventEnvelope {
        event_id: format!("event-{seq}"),
        seq,
        timestamp,
        source: EventSource::Core,
        kind,
        thread_id: event.thread_id().cloned(),
        turn_id: event.turn_id().cloned(),
        event,
    }
}

#[test]
fn extension_authors_can_project_turns_from_raw_events() {
    let thread_id: ThreadId = "thread-a".to_string();
    let first_turn = "turn-a".to_string();
    let second_turn = "turn-b".to_string();
    let started_at = OffsetDateTime::UNIX_EPOCH;
    let appended_at = started_at + time::Duration::seconds(1);
    let completed_at = started_at + time::Duration::seconds(2);
    let interrupted_at = started_at + time::Duration::seconds(3);
    let usage = TokenUsage::new(10, 5, 15);

    let events = vec![
        envelope(
            1,
            started_at,
            RoderEvent::TurnStarted(TurnStarted {
                thread_id: thread_id.clone(),
                turn_id: first_turn.clone(),
                runtime_profile: RuntimeProfile::Interactive,
                timestamp: started_at,
            }),
        ),
        envelope(
            2,
            appended_at,
            RoderEvent::TranscriptItemAppended(TranscriptItemAppended {
                thread_id: thread_id.clone(),
                turn_id: first_turn.clone(),
                item_type: "user_message".to_string(),
                item_index: Some(0),
                item: Some(TranscriptItem::UserMessage(UserMessage::text("hello"))),
                timestamp: appended_at,
            }),
        ),
        envelope(
            3,
            completed_at,
            RoderEvent::TranscriptItemAppended(TranscriptItemAppended {
                thread_id: thread_id.clone(),
                turn_id: first_turn.clone(),
                item_type: "assistant_message".to_string(),
                item_index: Some(1),
                item: Some(TranscriptItem::AssistantMessage(AssistantMessage {
                    text: "hi".to_string(),
                    phase: None,
                })),
                timestamp: completed_at,
            }),
        ),
        envelope(
            4,
            completed_at,
            RoderEvent::TurnCompleted(TurnCompleted {
                thread_id: thread_id.clone(),
                turn_id: first_turn.clone(),
                usage: Some(usage.clone()),
                finish_reason: Some("stop".to_string()),
                timestamp: completed_at,
            }),
        ),
        envelope(
            5,
            interrupted_at,
            RoderEvent::TranscriptItemAppended(TranscriptItemAppended {
                thread_id: thread_id.clone(),
                turn_id: second_turn.clone(),
                item_type: "assistant_message".to_string(),
                item_index: None,
                item: None,
                timestamp: interrupted_at,
            }),
        ),
        envelope(
            6,
            interrupted_at,
            RoderEvent::TurnInterrupted(TurnInterrupted {
                thread_id: thread_id.clone(),
                turn_id: second_turn.clone(),
                timestamp: interrupted_at,
            }),
        ),
    ];

    let turns = project_turns_from_events(&thread_id, &events);

    assert_eq!(turns.len(), 2);
    assert_eq!(turns[0].thread_id, thread_id);
    assert_eq!(turns[0].turn_id, first_turn);
    assert_eq!(turns[0].created_at, started_at);
    assert_eq!(turns[0].completed_at, Some(completed_at));
    assert_eq!(turns[0].usage, Some(usage));
    assert_eq!(
        turns[0].items,
        vec![
            TranscriptItem::UserMessage(UserMessage::text("hello")),
            TranscriptItem::AssistantMessage(AssistantMessage {
                text: "hi".to_string(),
                phase: None,
            }),
        ]
    );
    assert_eq!(turns[1].turn_id, second_turn);
    assert_eq!(turns[1].created_at, interrupted_at);
    assert_eq!(turns[1].completed_at, Some(interrupted_at));
    assert!(turns[1].items.is_empty());
}