a-agent 0.2.1

Fast, terminal-native coding agent with progressive context
Documentation
use a_agent::model::{ContentBlock, StreamEvent};
use a_agent::provider::{anthropic, chat_completion, responses};
use serde_json::json;

#[test]
fn responses_normalizes_text_reasoning_tools_and_usage() {
    let values = vec![
        json!({"type":"response.output_text.delta","delta":"done"}),
        json!({"type":"response.reasoning_summary_text.delta","delta":"checking"}),
        json!({"type":"response.output_item.added","item":{"type":"function_call","id":"item_1","call_id":"call_1","name":"read","arguments":""}}),
        json!({"type":"response.function_call_arguments.delta","item_id":"item_1","delta":"{\"path\":"}),
        json!({"type":"response.function_call_arguments.delta","item_id":"item_1","delta":"\"a.rs\"}"}),
        json!({"type":"response.output_item.done","item":{"type":"function_call","id":"item_1","call_id":"call_1","name":"read","arguments":"{\"path\":\"a.rs\"}"}}),
        json!({"type":"response.completed","response":{"usage":{"input_tokens":10,"output_tokens":4,"total_tokens":14,"input_tokens_details":{"cached_tokens":3,"cache_write_tokens":2}}}}),
    ];
    let (turn, events) = responses::normalize_events(values).unwrap();
    assert_eq!(turn.tool_calls[0].arguments, "{\"path\":\"a.rs\"}");
    assert!(
        turn.blocks
            .iter()
            .any(|block| matches!(block, ContentBlock::Text(text) if text == "done"))
    );
    let usage = turn.usage.unwrap();
    assert_eq!(usage.input_tokens, Some(5));
    assert_eq!(usage.output_tokens, Some(4));
    assert_eq!(usage.cached_tokens, Some(3));
    assert_eq!(usage.cache_write_tokens, Some(2));
    assert_eq!(usage.total_tokens, Some(14));
    assert_eq!(usage.context_tokens(), Some(14));
    assert!(
        events
            .iter()
            .any(|event| matches!(event, StreamEvent::ReasoningDelta { .. }))
    );
}

#[test]
fn anthropic_normalizes_fragmented_tool_json() {
    let values = vec![
        json!({"type":"message_start","message":{"usage":{"input_tokens":8,"cache_read_input_tokens":2,"cache_creation_input_tokens":3}}}),
        json!({"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}),
        json!({"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"plan"}}),
        json!({"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"tool_1","name":"bash","input":{}}}),
        json!({"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"command\":"}}),
        json!({"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\"pwd\"}"}}),
        json!({"type":"content_block_stop","index":1}),
        json!({"type":"message_delta","usage":{"output_tokens":5}}),
        json!({"type":"message_stop"}),
    ];
    let (turn, _) = anthropic::normalize_events(values).unwrap();
    assert_eq!(turn.tool_calls[0].arguments, "{\"command\":\"pwd\"}");
    let usage = turn.usage.unwrap();
    assert_eq!(usage.input_tokens, Some(8));
    assert_eq!(usage.output_tokens, Some(5));
    assert_eq!(usage.cached_tokens, Some(2));
    assert_eq!(usage.cache_write_tokens, Some(3));
    assert_eq!(usage.total_tokens, Some(18));
    assert_eq!(usage.context_tokens(), Some(18));
}

#[test]
fn chat_completion_normalizes_indexed_tool_deltas_and_reasoning() {
    let values = vec![
        json!({"choices":[{"delta":{"reasoning_content":"think","tool_calls":[{"index":0,"id":"call_1","function":{"name":"read","arguments":"{\"pa"}}]}}]}),
        json!({"choices":[{"delta":{"content":"ok","tool_calls":[{"index":0,"function":{"arguments":"th\":\"x\"}"}}]},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":8,"completion_tokens":6,"total_tokens":14,"prompt_tokens_details":{"cached_tokens":3,"cache_write_tokens":2}}}),
    ];
    let (turn, events) = chat_completion::normalize_events(values).unwrap();
    assert_eq!(turn.tool_calls[0].arguments, "{\"path\":\"x\"}");
    assert!(
        turn.blocks
            .iter()
            .any(|block| matches!(block, ContentBlock::Text(text) if text == "ok"))
    );
    assert!(
        events
            .iter()
            .any(|event| matches!(event, StreamEvent::ReasoningDelta { .. }))
    );
    let usage = turn.usage.unwrap();
    assert_eq!(usage.input_tokens, Some(3));
    assert_eq!(usage.output_tokens, Some(6));
    assert_eq!(usage.cached_tokens, Some(3));
    assert_eq!(usage.cache_write_tokens, Some(2));
    assert_eq!(usage.total_tokens, Some(14));
    assert_eq!(usage.context_tokens(), Some(14));
}

#[test]
fn usage_context_fallback_includes_cache_tokens() {
    let usage = a_agent::model::Usage {
        input_tokens: Some(11),
        output_tokens: Some(7),
        cached_tokens: Some(5),
        cache_write_tokens: Some(3),
        total_tokens: None,
    };
    assert_eq!(usage.context_tokens(), Some(26));
}

#[test]
fn provider_errors_are_actionable() {
    let error = responses::normalize_events(vec![
        json!({"type":"error","message":"bad request","code":"invalid"}),
    ])
    .unwrap_err();
    let message = error.to_string();
    assert!(message.contains("bad request"));
    assert!(message.contains("invalid"));
}