use std::ops::Deref;
use std::sync::Arc;
use crate::types::{AgentMessage, AssistantMessage, LlmMessage, ToolResultMessage};
pub type ConvertToLlmFn = dyn Fn(&AgentMessage) -> Option<LlmMessage> + Send + Sync;
pub struct ContextMessages {
messages: Vec<AgentMessage>,
mirror: Vec<Option<Arc<LlmMessage>>>,
}
impl ContextMessages {
#[must_use]
pub fn new(messages: Vec<AgentMessage>) -> Self {
Self {
messages,
mirror: Vec::new(),
}
}
pub fn push(&mut self, message: AgentMessage) {
self.messages.push(message);
}
pub fn extend<I: IntoIterator<Item = AgentMessage>>(&mut self, iter: I) {
self.messages.extend(iter);
}
pub fn append(&mut self, other: &mut Vec<AgentMessage>) {
self.messages.append(other);
}
pub fn set(&mut self, index: usize, message: AgentMessage) {
self.messages[index] = message;
if index < self.mirror.len() {
self.mirror.truncate(index);
}
}
pub fn make_mut(&mut self) -> &mut Vec<AgentMessage> {
self.mirror.clear();
&mut self.messages
}
pub fn take_vec(&mut self) -> Vec<AgentMessage> {
self.mirror.clear();
std::mem::take(&mut self.messages)
}
#[must_use]
pub fn into_vec(self) -> Vec<AgentMessage> {
self.messages
}
#[must_use]
pub fn as_slice(&self) -> &[AgentMessage] {
&self.messages
}
pub fn snapshot_llm(&mut self) -> Arc<Vec<Arc<LlmMessage>>> {
let start = self.mirror.len();
for message in &self.messages[start..] {
self.mirror.push(match message {
AgentMessage::Llm(llm) => Some(Arc::new(llm.clone())),
AgentMessage::Custom(_) => None,
});
}
Arc::new(self.mirror.iter().flatten().cloned().collect())
}
}
impl Deref for ContextMessages {
type Target = [AgentMessage];
fn deref(&self) -> &Self::Target {
&self.messages
}
}
pub struct LoopState {
pub context_messages: ContextMessages,
pub pending_messages: Vec<AgentMessage>,
pub initial_new_messages_len: usize,
pub overflow_signal: bool,
pub overflow_recovery_attempted: bool,
pub turn_index: usize,
pub accumulated_usage: crate::types::Usage,
pub accumulated_cost: crate::types::Cost,
pub last_assistant_message: Option<AssistantMessage>,
pub last_tool_results: Vec<ToolResultMessage>,
pub transfer_chain: crate::transfer::TransferChain,
}
pub enum TurnOutcome {
ContinueInner,
BreakInner,
Return,
}
pub struct ToolCallInfo {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
pub is_incomplete: bool,
}
#[allow(clippy::large_enum_variant)]
pub enum StreamResult {
Message(AssistantMessage),
ContextOverflow,
Aborted,
ChannelClosed,
}
pub enum ToolExecOutcome {
Completed {
results: Vec<ToolResultMessage>,
tool_metrics: Vec<crate::metrics::ToolExecMetrics>,
transfer_signal: Option<crate::transfer::TransferSignal>,
injected_messages: Vec<AgentMessage>,
},
Stopped {
results: Vec<ToolResultMessage>,
tool_metrics: Vec<crate::metrics::ToolExecMetrics>,
reason: String,
injected_messages: Vec<AgentMessage>,
},
SteeringInterrupt {
completed: Vec<ToolResultMessage>,
cancelled: Vec<ToolResultMessage>,
steering_messages: Vec<AgentMessage>,
tool_metrics: Vec<crate::metrics::ToolExecMetrics>,
injected_messages: Vec<AgentMessage>,
},
Aborted {
results: Vec<ToolResultMessage>,
tool_metrics: Vec<crate::metrics::ToolExecMetrics>,
injected_messages: Vec<AgentMessage>,
},
ChannelClosed,
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::ContextMessages;
use crate::types::{AgentMessage, ContentBlock, LlmMessage, UserMessage};
fn user_msg(text: &str) -> AgentMessage {
AgentMessage::Llm(LlmMessage::User(UserMessage {
content: vec![ContentBlock::Text {
text: text.to_string(),
}],
timestamp: 0,
cache_hint: None,
}))
}
fn text_of(message: &LlmMessage) -> &str {
match message {
LlmMessage::User(u) => match &u.content[0] {
ContentBlock::Text { text } => text,
other => panic!("expected text content, got {other:?}"),
},
other => panic!("expected user message, got {other:?}"),
}
}
#[test]
fn snapshot_reuses_arcs_for_untouched_prefix() {
let mut context = ContextMessages::new(vec![user_msg("a"), user_msg("b")]);
let first = context.snapshot_llm();
context.push(user_msg("c"));
let second = context.snapshot_llm();
assert_eq!(first.len(), 2);
assert_eq!(second.len(), 3);
assert!(
Arc::ptr_eq(&first[0], &second[0]) && Arc::ptr_eq(&first[1], &second[1]),
"untouched prefix must be shared, not deep-copied"
);
assert_eq!(text_of(&second[2]), "c");
}
#[test]
fn set_invalidates_mirror_from_replaced_index() {
let mut context = ContextMessages::new(vec![user_msg("a"), user_msg("b"), user_msg("c")]);
let first = context.snapshot_llm();
context.set(1, user_msg("b2"));
let second = context.snapshot_llm();
assert!(
Arc::ptr_eq(&first[0], &second[0]),
"prefix before the replaced index stays shared"
);
assert!(
!Arc::ptr_eq(&first[1], &second[1]),
"the replaced message must be re-cloned"
);
assert_eq!(text_of(&second[1]), "b2");
assert_eq!(text_of(&second[2]), "c");
}
#[test]
fn make_mut_invalidates_whole_mirror_and_reflects_mutation() {
let mut context = ContextMessages::new(vec![user_msg("a"), user_msg("b")]);
let first = context.snapshot_llm();
if let AgentMessage::Llm(LlmMessage::User(u)) = &mut context.make_mut()[0] {
u.content = vec![ContentBlock::Text {
text: "a2".to_string(),
}];
}
let second = context.snapshot_llm();
assert!(
!Arc::ptr_eq(&first[0], &second[0]),
"bulk mutation must invalidate the mirror"
);
assert_eq!(text_of(&second[0]), "a2");
assert_eq!(text_of(&second[1]), "b");
}
#[test]
fn take_vec_empties_history_and_mirror() {
let mut context = ContextMessages::new(vec![user_msg("a")]);
let _ = context.snapshot_llm();
let taken = context.take_vec();
assert_eq!(taken.len(), 1);
assert!(context.as_slice().is_empty());
assert!(context.snapshot_llm().is_empty());
}
}