adk-server 2.3.0

HTTP server and A2A protocol for Rust Agent Development Kit (ADK-Rust) agents
Documentation
use super::*;
use adk_core::{Content, Part};

fn response(parts: Vec<Part>, partial: bool) -> adk_core::Event {
    let mut event = adk_core::Event::with_id("response", "invocation");
    event.llm_response.partial = partial;
    event.llm_response.content = Some(Content { role: "model".into(), parts });
    if !partial {
        event.llm_response.provider_metadata = Some(json!({"content_complete":true}));
    }
    event
}

#[test]
fn snapshots_close_original_messages_and_preserve_whitespace() {
    for suffix in ["", " "] {
        let mut stream = AgUiStream::default();
        let thinking =
            response(vec![Part::Thinking { thinking: "Thought".into(), signature: None }], true);
        let text = response(vec![Part::Text { text: "Hello".into() }], true);
        let mut output = stream.translate(&thinking, "thread", "run");
        output.extend(stream.translate(&text, "thread", "run"));
        let terminal = response(
            vec![
                Part::Thinking { thinking: "Thought".into(), signature: Some("signed".into()) },
                Part::Text { text: format!("Hello{suffix}") },
            ],
            false,
        );
        output.extend(stream.translate(&terminal, "thread", "run"));
        let deltas: String = output
            .iter()
            .filter(|item| {
                matches!(item["type"].as_str(), Some("TEXT_MESSAGE_CHUNK" | "TEXT_MESSAGE_CONTENT"))
            })
            .filter_map(|item| item["delta"].as_str())
            .collect();
        assert_eq!(deltas, format!("Hello{suffix}"));
        for (kind, id) in [
            ("TEXT_MESSAGE_END", "response-text-0"),
            ("REASONING_MESSAGE_END", "response-reasoning-0"),
        ] {
            let endings: Vec<_> = output.iter().filter(|item| item["type"] == kind).collect();
            assert_eq!(endings.len(), 1);
            assert_eq!(endings[0]["messageId"], id);
        }
        assert!(!output.iter().any(|item| matches!(
            item["type"].as_str(),
            Some("TEXT_MESSAGE_START" | "REASONING_MESSAGE_START" | "REASONING_MESSAGE_CONTENT")
        )));
        assert!(stream.messages.is_empty());
    }
}

#[test]
fn unstreamed_response_keeps_start_content_and_end() {
    let mut stream = AgUiStream::default();
    let event = response(vec![Part::Text { text: "Hello".into() }], false);
    let output = stream.translate(&event, "thread", "run");
    let kinds: Vec<_> = output.iter().map(|item| item["type"].as_str().unwrap()).collect();
    assert_eq!(kinds, ["TEXT_MESSAGE_START", "TEXT_MESSAGE_CONTENT", "TEXT_MESSAGE_END"]);
}

#[test]
fn closes_messages_omitted_from_the_terminal_snapshot() {
    for keep_text in [false, true] {
        let mut stream = AgUiStream::default();
        let partial = response(
            vec![
                Part::Thinking { thinking: "Draft thought".into(), signature: None },
                Part::Text { text: "Hello".into() },
            ],
            true,
        );
        stream.translate(&partial, "thread", "run");
        let mut terminal = response(vec![Part::Text { text: "Hello".into() }], false);
        if !keep_text {
            terminal.llm_response.content = None;
        }
        let output = stream.translate(&terminal, "thread", "run");
        for (kind, id) in [
            ("TEXT_MESSAGE_END", "response-text-1"),
            ("REASONING_MESSAGE_END", "response-reasoning-0"),
        ] {
            assert_eq!(
                output
                    .iter()
                    .filter(|item| item["type"] == kind && item["messageId"] == id)
                    .count(),
                1
            );
        }
        assert!(stream.messages.is_empty());
    }
}