use galaxy_agent_core::{ AgentEvent, RuntimeActivity, RuntimeActivityStatus, RuntimeCapabilities, StopReason, Usage, }; use warp_multi_agent_api::{client_action, message, response_event}; use super::{ProviderRunResponseProjector, RuntimeResponseConfig, RuntimeResponseTranslator}; use crate::ai::agent::runtime_activity; use crate::ai::runtime::provider_run_coordinator::ProviderRunProjection; fn provider_translator() -> RuntimeResponseTranslator { RuntimeResponseTranslator::new(RuntimeResponseConfig { task_id: "task".to_owned(), conversation_id: "conversation".to_owned(), needs_create_task: false, user_query: None, model_id: "model".to_owned(), max_context_tokens: Some(1_000), capabilities: RuntimeCapabilities::provider(), empty_output_message: None, }) } fn session_translator() -> RuntimeResponseTranslator { RuntimeResponseTranslator::new(RuntimeResponseConfig { task_id: "task".to_owned(), conversation_id: String::new(), needs_create_task: false, user_query: None, model_id: "session-runtime".to_owned(), max_context_tokens: None, capabilities: RuntimeCapabilities::session_runtime(), empty_output_message: Some("> runtime completed without text".to_owned()), }) } #[test] fn restored_provider_projection_skips_stream_initialization() { let config = RuntimeResponseConfig { task_id: "task".to_owned(), conversation_id: "conversation".to_owned(), needs_create_task: true, user_query: Some("do not duplicate".to_owned()), model_id: "model".to_owned(), max_context_tokens: Some(1_000), capabilities: RuntimeCapabilities::provider(), empty_output_message: None, }; let mut projector = ProviderRunResponseProjector::restored(config); let work_id = galaxy_agent_core::ExternalWorkId { run_id: galaxy_agent_core::ProviderRunId::new("run"), epoch: galaxy_agent_core::RunEpoch::new(2), }; assert!(projector .project(ProviderRunProjection::ModelTurnStarted { work_id: work_id.clone(), profile: galaxy_agent_core::ProviderRequestProfile::new("base"), runtime_id: "runtime".to_owned(), model_id: "model".to_owned(), runtime_request_id: "request".to_owned(), retry_attempt: 0, elapsed_ms: 1, }) .unwrap() .is_empty()); let events = projector .project(ProviderRunProjection::ModelEvent { work_id, event: AgentEvent::TextDelta { text: "continued".to_owned(), }, }) .unwrap(); assert_eq!(events.len(), 1); let Some(response_event::Type::ClientActions(actions)) = &events[0].r#type else { panic!("restored output should append through a client action"); }; assert!(matches!( actions.actions[0].action, Some(client_action::Action::AddMessagesToTask(_)) )); } #[test] fn provider_followup_turn_starts_a_distinct_text_message() { let mut projector = ProviderRunResponseProjector::new(RuntimeResponseConfig { task_id: "task".to_owned(), conversation_id: "conversation".to_owned(), needs_create_task: false, user_query: None, model_id: "model".to_owned(), max_context_tokens: Some(1_000), capabilities: RuntimeCapabilities::provider(), empty_output_message: None, }); let first_work_id = galaxy_agent_core::ExternalWorkId { run_id: galaxy_agent_core::ProviderRunId::new("run"), epoch: galaxy_agent_core::RunEpoch::new(1), }; projector .project(ProviderRunProjection::ModelTurnStarted { work_id: first_work_id.clone(), profile: galaxy_agent_core::ProviderRequestProfile::new("base"), runtime_id: "runtime".to_owned(), model_id: "model".to_owned(), runtime_request_id: "request-1".to_owned(), retry_attempt: 0, elapsed_ms: 1, }) .unwrap(); let first_text = projector .project(ProviderRunProjection::ModelEvent { work_id: first_work_id, event: AgentEvent::TextDelta { text: "before tool".to_owned(), }, }) .unwrap(); let Some(response_event::Type::ClientActions(first_actions)) = &first_text[0].r#type else { panic!("expected first text action"); }; let Some(client_action::Action::AddMessagesToTask(first_add)) = &first_actions.actions[0].action else { panic!("first turn should add a text message"); }; let first_message_id = first_add.messages[0].id.clone(); let second_work_id = galaxy_agent_core::ExternalWorkId { run_id: galaxy_agent_core::ProviderRunId::new("run"), epoch: galaxy_agent_core::RunEpoch::new(2), }; projector .project(ProviderRunProjection::ModelTurnStarted { work_id: second_work_id.clone(), profile: galaxy_agent_core::ProviderRequestProfile::new("base"), runtime_id: "runtime".to_owned(), model_id: "model".to_owned(), runtime_request_id: "request-2".to_owned(), retry_attempt: 0, elapsed_ms: 1, }) .unwrap(); let second_text = projector .project(ProviderRunProjection::ModelEvent { work_id: second_work_id, event: AgentEvent::TextDelta { text: "after tool".to_owned(), }, }) .unwrap(); let Some(response_event::Type::ClientActions(second_actions)) = &second_text[0].r#type else { panic!("expected follow-up text action"); }; let Some(client_action::Action::AddMessagesToTask(second_add)) = &second_actions.actions[0].action else { panic!("follow-up turn should add a text message"); }; assert_ne!(first_message_id, second_add.messages[0].id); } #[test] fn provider_and_session_runtimes_share_text_translation() { for mut translator in [provider_translator(), session_translator()] { let first = translator .translate(AgentEvent::TextDelta { text: "one".to_owned(), }) .expect("first delta"); let second = translator .translate(AgentEvent::TextDelta { text: " two".to_owned(), }) .expect("second delta"); let Some(response_event::Type::ClientActions(first_actions)) = &first[1].r#type else { panic!("expected first client action"); }; assert!(matches!( first_actions.actions[0].action, Some(client_action::Action::AddMessagesToTask(_)) )); let Some(response_event::Type::ClientActions(second_actions)) = &second[0].r#type else { panic!("expected append client action"); }; assert!(matches!( second_actions.actions[0].action, Some(client_action::Action::AppendToMessageContent(_)) )); } } #[test] fn retargeting_starts_new_text_and_reasoning_messages_on_the_new_task() { let mut translator = provider_translator(); translator .translate(AgentEvent::TextDelta { text: "root text".to_owned(), }) .expect("root text"); translator .translate(AgentEvent::ReasoningDelta { text: "root reasoning".to_owned(), }) .expect("root reasoning"); translator.set_task_id("cli-task"); for event in [ AgentEvent::TextDelta { text: "cli text".to_owned(), }, AgentEvent::ReasoningDelta { text: "cli reasoning".to_owned(), }, ] { let translated = translator.translate(event).expect("retargeted output"); let Some(response_event::Type::ClientActions(actions)) = &translated[0].r#type else { panic!("expected retargeted client action"); }; let Some(client_action::Action::AddMessagesToTask(add)) = &actions.actions[0].action else { panic!("retargeted output must start a new message"); }; assert_eq!(add.task_id, "cli-task"); assert_eq!(add.messages[0].task_id, "cli-task"); } } #[test] fn reasoning_uses_the_native_reasoning_message_contract() { let mut translator = provider_translator(); let first = translator .translate(AgentEvent::ReasoningDelta { text: "think".to_owned(), }) .expect("reasoning"); let second = translator .translate(AgentEvent::ReasoningDelta { text: " more".to_owned(), }) .expect("reasoning append"); let Some(response_event::Type::ClientActions(actions)) = &first[1].r#type else { panic!("expected reasoning action"); }; let Some(client_action::Action::AddMessagesToTask(add)) = &actions.actions[0].action else { panic!("expected reasoning message"); }; assert!(matches!( add.messages[0].message, Some(message::Message::AgentReasoning(_)) )); let Some(response_event::Type::ClientActions(actions)) = &second[0].r#type else { panic!("expected reasoning append"); }; assert!(matches!( actions.actions[0].action, Some(client_action::Action::AppendToMessageContent(_)) )); } #[test] fn session_activity_updates_the_same_structured_message() { let mut translator = session_translator(); let started = translator .translate(AgentEvent::RuntimeActivityUpdated { activity: RuntimeActivity { id: "tool-1".to_owned(), title: "Run tests".to_owned(), status: Some(RuntimeActivityStatus::InProgress), output: None, }, }) .expect("started"); let Some(response_event::Type::ClientActions(actions)) = &started[1].r#type else { panic!("expected activity action"); }; let Some(client_action::Action::AddMessagesToTask(add)) = &actions.actions[0].action else { panic!("expected activity message"); }; let message_id = add.messages[0].id.clone(); let completed = translator .translate(AgentEvent::RuntimeActivityUpdated { activity: RuntimeActivity { id: "tool-1".to_owned(), title: String::new(), status: Some(RuntimeActivityStatus::Completed), output: Some("ok".to_owned()), }, }) .expect("completed"); let Some(response_event::Type::ClientActions(actions)) = &completed[0].r#type else { panic!("expected activity update"); }; let Some(client_action::Action::UpdateTaskMessage(update)) = &actions.actions[0].action else { panic!("expected in-place update"); }; let message = update.message.as_ref().expect("updated message"); assert_eq!(message.id, message_id); assert_eq!( runtime_activity::decode(&message.server_message_data), Some(RuntimeActivity { id: "tool-1".to_owned(), title: "Run tests".to_owned(), status: Some(RuntimeActivityStatus::Completed), output: Some("ok".to_owned()), }) ); } #[test] fn usage_shape_follows_history_ownership_capability() { let mut provider = provider_translator(); provider .translate(AgentEvent::UsageUpdated { usage: Usage { input_tokens: 250, output_tokens: 10, ..Usage::default() }, }) .expect("provider usage"); let provider_finished = provider .translate(AgentEvent::TurnStopped { reason: StopReason::Completed, }) .expect("provider finished"); let Some(response_event::Type::Finished(finished)) = &provider_finished[1].r#type else { panic!("expected provider finish"); }; assert_eq!(finished.token_usage[0].total_input, 250); let mut session = session_translator(); session .translate(AgentEvent::ContextUsageUpdated { used_tokens: 25, context_size: 100, }) .expect("context usage"); let session_finished = session .translate(AgentEvent::TurnStopped { reason: StopReason::Completed, }) .expect("session finished"); let Some(response_event::Type::Finished(finished)) = &session_finished[2].r#type else { panic!("expected session finish"); }; assert_eq!(finished.token_usage[0].total_input, 0); assert_eq!( finished .conversation_usage_metadata .as_ref() .expect("context metadata") .context_window_usage, 0.25 ); } #[test] fn capabilities_reject_events_owned_by_the_other_runtime_shape() { assert!(provider_translator() .translate(AgentEvent::ContextUsageUpdated { used_tokens: 1, context_size: 2, }) .is_err()); assert!(session_translator() .translate(AgentEvent::UsageUpdated { usage: Usage::default(), }) .is_err()); }