Files
galaxy/app/src/ai/runtime/event_translator_tests.rs
T

376 lines
13 KiB
Rust

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());
}