#![allow(clippy::result_large_err)]
pub mod constructor;
pub mod context;
pub mod joining_strategy;
pub mod message;
pub mod session;
pub mod state;
pub mod store;
pub mod turn_input;
use crate::ToPrompt;
use crate::agent::chat::Chat;
use crate::agent::dialogue::joining_strategy::JoiningStrategy;
use crate::agent::persona::Persona;
use crate::agent::{Agent, AgentError, Payload, PayloadMessage};
use async_trait::async_trait;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use tokio::task::JoinSet;
use tracing::{debug, error, trace};
pub use context::{DialogueContext, TalkStyle, TalkStyleTemplate};
pub use message::{
DialogueMessage, MessageId, MessageMetadata, MessageOrigin, Speaker, format_messages_to_prompt,
};
pub use session::DialogueSession;
pub use store::MessageStore;
pub use turn_input::{ContextMessage, ParticipantInfo, TurnInput};
use state::{BroadcastState, SessionState};
fn format_dialogue_history_as_text(history: &[DialogueTurn]) -> String {
let mut output = String::from("# Previous Conversation History\n\n");
output.push_str("The following is the conversation history from previous sessions. ");
output.push_str("Please use this context to maintain continuity in the discussion.\n\n");
for (idx, turn) in history.iter().enumerate() {
let speaker_label = match &turn.speaker {
Speaker::System => "[System]".to_string(),
Speaker::User { name, .. } => format!("[{}]", name),
Speaker::Agent { name, role, icon } => match icon {
Some(icon) => format!("[{} {} ({})]", icon, name, role),
None => format!("[{} ({})]", name, role),
},
};
output.push_str(&format!("{}. {}\n", idx + 1, speaker_label));
output.push_str(&turn.content);
output.push_str("\n\n");
}
output.push_str("---\n");
output.push_str("End of previous conversation. Continue from here.\n");
output
}
fn extract_mentions_with_strategy<'a>(
text: &str,
participant_names: &'a [&'a str],
strategy: MentionMatchStrategy,
) -> Vec<&'a str> {
use std::collections::HashSet;
let mut mentioned = HashSet::new();
match strategy {
MentionMatchStrategy::ExactWord => {
let mention_regex =
Regex::new(r#"@([^\s@,.!?;:()\[\]{}<>"'`/\\|]+)"#).expect("Invalid regex pattern");
for cap in mention_regex.captures_iter(text) {
if let Some(mention) = cap.get(1) {
let mention_str = mention.as_str();
if let Some(&matched_name) =
participant_names.iter().find(|&&name| name == mention_str)
{
mentioned.insert(matched_name);
}
}
}
}
MentionMatchStrategy::Name => {
for &name in participant_names {
let pattern = format!("@{}(?:\\s|[,.!?;:]|$)", regex::escape(name));
if let Ok(name_regex) = Regex::new(&pattern)
&& name_regex.is_match(text)
{
mentioned.insert(name);
}
}
let mentioned_copy: Vec<&str> = mentioned.iter().copied().collect();
mentioned.retain(|&name| {
!mentioned_copy
.iter()
.any(|&other| other != name && other.starts_with(name))
});
}
MentionMatchStrategy::Partial => {
let mention_regex =
Regex::new(r#"@([^\s@,.!?;:()\[\]{}<>"'`/\\|]+)"#).expect("Invalid regex pattern");
for cap in mention_regex.captures_iter(text) {
if let Some(mention) = cap.get(1) {
let mention_str = mention.as_str();
let mut matches: Vec<&str> = participant_names
.iter()
.filter(|&&name| name.starts_with(mention_str))
.copied()
.collect();
matches.sort_by_key(|b| std::cmp::Reverse(b.len()));
if let Some(&longest_match) = matches.first() {
mentioned.insert(longest_match);
}
}
}
}
}
mentioned.into_iter().collect()
}
#[cfg(test)]
#[deprecated(since = "0.53.0", note = "Use extract_mentions_with_strategy instead")]
fn extract_mentions<'a>(text: &str, participant_names: &'a [&'a str]) -> Vec<&'a str> {
extract_mentions_with_strategy(text, participant_names, MentionMatchStrategy::ExactWord)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DialogueBlueprint {
pub agenda: String,
pub context: String,
pub participants: Option<Vec<Persona>>,
pub execution_strategy: Option<ExecutionModel>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DialogueTurn {
pub speaker: Speaker,
pub content: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MentionMatchStrategy {
ExactWord,
Name,
Partial,
}
impl Default for MentionMatchStrategy {
fn default() -> Self {
Self::ExactWord
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionModel {
Sequential,
OrderedSequential(SequentialOrder),
Broadcast,
OrderedBroadcast(BroadcastOrder),
Mentioned {
#[serde(default)]
strategy: MentionMatchStrategy,
},
Moderator,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ReactionStrategy {
Always,
UserOnly,
AgentOnly,
ExceptSystem,
Conversational,
ExceptContextInfo,
}
impl Default for ReactionStrategy {
fn default() -> Self {
Self::Always
}
}
#[derive(Debug, Clone)]
pub(super) struct PendingParticipant {
pub joining_strategy: JoiningStrategy,
}
pub(super) struct Participant {
pub(super) persona: Persona,
pub(super) agent: Arc<crate::agent::AnyAgent<String>>,
pub(super) joining_strategy: Option<JoiningStrategy>,
pub(super) has_sent_once: bool,
}
impl Clone for Participant {
fn clone(&self) -> Self {
Self {
persona: self.persona.clone(),
agent: Arc::clone(&self.agent),
joining_strategy: self.joining_strategy,
has_sent_once: self.has_sent_once,
}
}
}
impl Participant {
pub(super) fn name(&self) -> &str {
&self.persona.name
}
pub(super) fn to_speaker(&self) -> Speaker {
match &self.persona.visual_identity {
Some(identity) => Speaker::agent_with_icon(
self.persona.name.clone(),
self.persona.role.clone(),
identity.icon.clone(),
),
None => Speaker::agent(self.persona.name.clone(), self.persona.role.clone()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BroadcastOrder {
Completion,
ParticipantOrder,
Explicit(Vec<String>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SequentialOrder {
AsAdded,
Explicit(Vec<String>),
}
#[derive(Clone)]
pub struct Dialogue {
pub(super) participants: Vec<Participant>,
pub(super) message_store: MessageStore,
pub(super) execution_model: ExecutionModel,
pub(super) context: Option<DialogueContext>,
pub(super) reaction_strategy: ReactionStrategy,
pub(super) moderator: Option<Arc<crate::agent::AnyAgent<ExecutionModel>>>,
pub(super) pending_participants: HashMap<String, PendingParticipant>,
}
struct BroadcastContext {
participants_info: Vec<ParticipantInfo>,
joining_history_contexts: Vec<Option<Vec<PayloadMessage>>>,
unsent_incoming: Vec<PayloadMessage>,
unsent_from_agent: Vec<PayloadMessage>,
message_ids_to_mark: Vec<MessageId>,
}
impl Dialogue {
fn create_participant<T>(
persona: Persona,
llm_agent: T,
joining_strategy: Option<JoiningStrategy>,
) -> Participant
where
T: Agent<Output = String> + 'static,
{
let chat_agent = Chat::new(llm_agent)
.with_persona(persona.clone())
.with_history(true)
.with_joining_strategy(joining_strategy)
.build();
Participant {
persona,
agent: Arc::new(*chat_agent),
joining_strategy,
has_sent_once: false,
}
}
fn create_participants<T>(
personas: Vec<Persona>,
llm_agent: T,
joining_strategy: Option<JoiningStrategy>,
) -> Vec<Participant>
where
T: Agent<Output = String> + Clone + 'static,
{
personas
.into_iter()
.map(|persona| Self::create_participant(persona, llm_agent.clone(), joining_strategy))
.collect()
}
fn get_participants_info(&self) -> Vec<ParticipantInfo> {
self.participants
.iter()
.map(|p| {
let mut capabilities = p.persona.capabilities.clone();
if let Some(ref context) = self.context
&& let Some(ref policy) = context.policy
&& let Some(allowed) = policy.get(&p.persona.name)
{
capabilities = capabilities.map(|caps| {
caps.into_iter()
.filter(|cap| allowed.contains(cap))
.collect()
});
}
ParticipantInfo::new(
p.persona.name.clone(),
p.persona.role.clone(),
p.persona.background.clone(),
)
.with_capabilities(capabilities.unwrap_or_default())
})
.collect()
}
fn resolve_sequential_indices(
&self,
order: &SequentialOrder,
) -> Result<Vec<usize>, AgentError> {
match order {
SequentialOrder::AsAdded => Ok((0..self.participants.len()).collect()),
SequentialOrder::Explicit(order) => {
let mut indices = Vec::with_capacity(self.participants.len());
let mut seen = HashSet::new();
for name in order {
let idx = self
.participants
.iter()
.position(|p| p.name() == name)
.ok_or_else(|| {
AgentError::ExecutionFailed(format!(
"Sequential order references unknown participant '{}'",
name
))
})?;
if seen.insert(idx) {
indices.push(idx);
}
}
for (idx, _) in self.participants.iter().enumerate() {
if seen.insert(idx) {
indices.push(idx);
}
}
Ok(indices)
}
}
}
pub fn add_participant<T>(&mut self, persona: Persona, llm_agent: T) -> &mut Self
where
T: Agent<Output = String> + 'static,
{
self.participants
.push(Self::create_participant(persona, llm_agent, None));
self
}
pub fn join_in_progress<T>(
&mut self,
persona: Persona,
llm_agent: T,
joining_strategy: JoiningStrategy,
) -> &mut Self
where
T: Agent<Output = String> + 'static,
{
let participant_name = persona.name.clone();
self.participants.push(Self::create_participant(
persona, llm_agent, None, ));
self.pending_participants
.insert(participant_name, PendingParticipant { joining_strategy });
self
}
pub fn add_agent<T>(&mut self, persona: Persona, agent: T) -> &mut Self
where
T: Agent<Output = String> + 'static,
{
let chat_agent = Chat::new(agent).with_history(true).build();
self.participants.push(Participant {
persona,
agent: Arc::new(*chat_agent),
joining_strategy: None,
has_sent_once: false,
});
self
}
pub fn participant_names(&self) -> Vec<&str> {
self.participants.iter().map(|p| p.name()).collect()
}
pub fn remove_participant(&mut self, name: &str) -> Result<(), AgentError> {
let position = self
.participants
.iter()
.position(|p| p.name() == name)
.ok_or_else(|| {
AgentError::ExecutionFailed(format!(
"Cannot remove participant '{}': participant not found",
name
))
})?;
self.participants.remove(position);
Ok(())
}
fn apply_metadata_attachments(mut payload: Payload, messages: &[PayloadMessage]) -> Payload {
for msg in messages {
for attachment in msg.metadata.attachments() {
payload = payload.with_attachment(attachment.clone());
}
}
payload
}
fn store_payload_messages(
&mut self,
payload: &Payload,
turn: usize,
) -> (String, Vec<MessageId>) {
let (messages, prompt_text) = self.extract_messages_from_payload(payload, turn);
let mut stored_ids = Vec::new();
for msg in messages {
let id = msg.id;
self.message_store.push(msg);
stored_ids.push(id);
}
(prompt_text, stored_ids)
}
fn should_react(&self, payload: &Payload) -> bool {
use crate::agent::dialogue::Speaker;
use crate::agent::dialogue::message::MessageType;
let messages = payload.to_messages();
if messages.is_empty() {
return !matches!(self.reaction_strategy, ReactionStrategy::AgentOnly);
}
let is_context_info = |msg: &crate::agent::PayloadMessage| {
msg.metadata
.message_type
.as_ref()
.map(|t| matches!(t, MessageType::ContextInfo))
.unwrap_or(false)
};
let all_context_info = messages.iter().all(is_context_info);
if all_context_info {
return false;
}
match &self.reaction_strategy {
ReactionStrategy::Always => {
true
}
ReactionStrategy::UserOnly => {
messages
.iter()
.any(|msg| matches!(msg.speaker, Speaker::User { .. }) && !is_context_info(msg))
}
ReactionStrategy::AgentOnly => {
messages.iter().any(|msg| {
matches!(msg.speaker, Speaker::Agent { .. }) && !is_context_info(msg)
})
}
ReactionStrategy::ExceptSystem => {
messages
.iter()
.any(|msg| !matches!(msg.speaker, Speaker::System) && !is_context_info(msg))
}
ReactionStrategy::Conversational => {
messages.iter().any(|msg| {
(matches!(msg.speaker, Speaker::User { .. })
|| matches!(msg.speaker, Speaker::Agent { .. }))
&& !is_context_info(msg)
})
}
ReactionStrategy::ExceptContextInfo => {
true
}
}
}
fn next_turn(&self) -> usize {
self.message_store.latest_turn() + 1
}
fn join_pending_participant(
&mut self,
speaker: Speaker,
current_turn: usize,
) -> Option<Vec<PayloadMessage>> {
let name = speaker.name();
if let Some(pending_info) = self.pending_participants.get(name) {
let filtered_history: Vec<PayloadMessage> = {
let all_messages = self.message_store.all_messages();
let message_refs: Vec<&DialogueMessage> = all_messages.to_vec();
let history_refs = pending_info
.joining_strategy
.historical_messages(&message_refs, current_turn);
history_refs
.iter()
.map(|&msg| PayloadMessage::from(msg))
.collect()
};
self.pending_participants.remove(name);
self.message_store.mark_as_sent_all_for(speaker.clone());
trace!(
target = "llm_toolkit::dialogue",
participant = name,
turn = current_turn,
history_count = filtered_history.len(),
"Activated pending participant with filtered history"
);
Some(filtered_history)
} else {
None
}
}
fn prepare_broadcast_context(&mut self, current_turn: usize) -> BroadcastContext {
let participants_info = self.get_participants_info();
let mut joining_history_contexts = vec![];
for idx in 0..self.participants.len() {
let participant = &self.participants[idx];
let speaker = participant.to_speaker();
let joining_history_context = self.join_pending_participant(speaker, current_turn);
joining_history_contexts.push(joining_history_context);
}
let unsent_from_agent: Vec<PayloadMessage> = self
.message_store
.unsent_messages_with_origin(MessageOrigin::AgentGenerated)
.into_iter()
.map(PayloadMessage::from)
.collect();
let mut message_ids_to_mark: Vec<_> = self
.message_store
.unsent_messages_with_origin(MessageOrigin::AgentGenerated)
.iter()
.map(|msg| msg.id)
.collect();
let unsent_incoming: Vec<PayloadMessage> = self
.message_store
.unsent_messages_with_origin(MessageOrigin::IncomingPayload)
.into_iter()
.map(PayloadMessage::from)
.collect();
message_ids_to_mark.extend(
self.message_store
.unsent_messages_with_origin(MessageOrigin::IncomingPayload)
.iter()
.map(|msg| msg.id),
);
BroadcastContext {
participants_info,
joining_history_contexts,
unsent_incoming,
unsent_from_agent,
message_ids_to_mark,
}
}
#[crate::tracing::instrument(
name = "dialogue.run",
skip(self, initial_prompt),
fields(
execution_model = ?self.execution_model,
participants_count = self.participants.len(),
)
)]
pub async fn run(
&mut self,
initial_prompt: impl Into<Payload>,
) -> Result<Vec<DialogueTurn>, AgentError> {
let payload = initial_prompt.into();
let current_turn = self.next_turn();
let (stored_prompt, _) = self.store_payload_messages(&payload, current_turn);
if !self.should_react(&payload) {
crate::tracing::trace!(
target = "llm_toolkit::dialogue",
turn = current_turn,
stored_prompt = stored_prompt.len(),
"Starting run passed no react"
);
return Ok(vec![]);
}
crate::tracing::trace!(
target = "llm_toolkit::dialogue",
turn = current_turn,
stored_prompt = stored_prompt.len(),
execution_model = ?self.execution_model,
participant_count = self.participants.len(),
"Starting run"
);
match self.execution_model.clone() {
ExecutionModel::Sequential => {
self.run_sequential(current_turn, &SequentialOrder::AsAdded)
.await
}
ExecutionModel::OrderedSequential(order) => {
self.run_sequential(current_turn, &order).await
}
ExecutionModel::Broadcast => {
self.run_broadcast(current_turn, BroadcastOrder::Completion)
.await
}
ExecutionModel::OrderedBroadcast(order) => {
self.run_broadcast(current_turn, order).await
}
ExecutionModel::Mentioned { strategy } => {
self.run_mentioned(current_turn, strategy).await
}
ExecutionModel::Moderator => {
self.run_with_moderator(current_turn, payload).await
}
}
}
async fn run_with_moderator(
&mut self,
current_turn: usize,
payload: Payload,
) -> Result<Vec<DialogueTurn>, AgentError> {
debug!(
target = "llm_toolkit::dialogue",
turn = current_turn,
execution_model = "moderator",
participant_count = self.participants.len(),
"Consulting moderator for execution strategy"
);
let moderator = self.moderator.as_ref().ok_or_else(|| {
AgentError::ExecutionFailed(
"ExecutionModel::Moderator requires a moderator agent. Use with_moderator() to set one.".to_string(),
)
})?;
let moderator_context = self.build_moderator_context(&payload, current_turn);
let decided_model = moderator.execute(moderator_context).await?;
debug!(
target = "llm_toolkit::dialogue",
turn = current_turn,
decided_model = ?decided_model,
"Moderator decided execution strategy"
);
match decided_model {
ExecutionModel::Sequential => {
self.run_sequential(current_turn, &SequentialOrder::AsAdded)
.await
}
ExecutionModel::OrderedSequential(order) => {
self.run_sequential(current_turn, &order).await
}
ExecutionModel::Broadcast => {
self.run_broadcast(current_turn, BroadcastOrder::Completion)
.await
}
ExecutionModel::OrderedBroadcast(order) => {
self.run_broadcast(current_turn, order).await
}
ExecutionModel::Mentioned { strategy } => {
self.run_mentioned(current_turn, strategy).await
}
ExecutionModel::Moderator => {
Err(AgentError::ExecutionFailed(
"Moderator cannot return Moderator execution model (infinite recursion)"
.to_string(),
))
}
}
}
fn build_moderator_context(&self, payload: &Payload, current_turn: usize) -> Payload {
let mut context_messages = Vec::new();
if current_turn > 1 {
let history = self.history();
let history_text = format!(
"Conversation history ({} previous turns):\n{}",
history.len(),
history
.iter()
.map(|turn| format!("[{}]: {}", turn.speaker.name(), turn.content))
.collect::<Vec<_>>()
.join("\n")
);
context_messages.push(PayloadMessage::new(Speaker::System, history_text));
}
let participants_info = self
.participants
.iter()
.map(|p| {
format!(
"- {} ({}): {}",
p.persona.name, p.persona.role, p.persona.background
)
})
.collect::<Vec<_>>()
.join("\n");
context_messages.push(PayloadMessage::new(
Speaker::System,
format!("Available participants:\n{}", participants_info),
));
for msg in payload.to_messages() {
context_messages.push(msg);
}
Payload::from_messages(context_messages)
}
async fn run_broadcast(
&mut self,
current_turn: usize,
_order: BroadcastOrder,
) -> Result<Vec<DialogueTurn>, AgentError> {
debug!(
target = "llm_toolkit::dialogue",
turn = current_turn,
execution_model = "broadcast",
participant_count = self.participants.len(),
has_context = self.context.is_some(),
"Starting dialogue.run() in broadcast mode"
);
let mut pending = self.spawn_broadcast_tasks(current_turn);
let mut dialogue_turns = Vec::new();
while let Some(Ok((idx, _name, result))) = pending.join_next().await {
match result {
Ok(content) => {
let speaker = self.participants[idx].to_speaker();
let metadata =
MessageMetadata::new().with_origin(MessageOrigin::AgentGenerated);
let response_message =
DialogueMessage::new(current_turn, speaker.clone(), content.clone())
.with_metadata(&metadata);
self.message_store.push(response_message);
dialogue_turns.push(DialogueTurn { speaker, content });
}
Err(err) => return Err(err),
}
}
Ok(dialogue_turns)
}
async fn run_sequential(
&mut self,
current_turn: usize,
order: &SequentialOrder,
) -> Result<Vec<DialogueTurn>, AgentError> {
debug!(
target = "llm_toolkit::dialogue",
turn = current_turn,
execution_model = "sequential",
participant_count = self.participants.len(),
has_context = self.context.is_some(),
"Starting dialogue.run() in sequential mode"
);
let participants_info = self.get_participants_info();
let unsent_messages_incoming: Vec<PayloadMessage> = self
.message_store
.unsent_messages_with_origin(MessageOrigin::IncomingPayload)
.into_iter()
.map(PayloadMessage::from)
.collect();
let incoming_message_ids: Vec<_> = self
.message_store
.unsent_messages_with_origin(MessageOrigin::IncomingPayload)
.iter()
.map(|msg| msg.id)
.collect();
if !unsent_messages_incoming.is_empty() {
trace!(
target = "llm_toolkit::dialogue",
turn = current_turn,
incoming_message_count = unsent_messages_incoming.len(),
"Sequential mode: First agent will receive {} incoming messages",
unsent_messages_incoming.len()
);
}
let sequence_indices = self.resolve_sequential_indices(order)?;
let mut final_turn = None;
for (sequence_idx, participant_idx) in sequence_indices.iter().enumerate() {
let participant_idx = *participant_idx;
let joining_history_context = {
let speaker = {
let participant = &self.participants[participant_idx];
participant.to_speaker()
};
self.join_pending_participant(speaker, current_turn)
};
let participant = &self.participants[participant_idx];
let agent = &participant.agent;
let agent_name = participant.name().to_string();
let (current_messages, messages_with_metadata, message_ids_to_mark) = if sequence_idx
== 0
{
let mut messages = Vec::new();
let mut metadata_messages = Vec::new();
if current_turn > 1 {
let prev_turn_messages: Vec<PayloadMessage> = self
.message_store
.messages_for_turn(current_turn - 1)
.into_iter()
.filter(|msg| matches!(msg.speaker, Speaker::Agent { .. }))
.map(PayloadMessage::from)
.collect();
trace!(
target = "llm_toolkit::dialogue",
turn = current_turn,
agent_idx = participant_idx,
sequence_idx,
agent_name = %agent_name,
prev_turn_message_count = prev_turn_messages.len(),
"Sequential mode: First agent receiving {} previous turn agent messages",
prev_turn_messages.len()
);
messages.extend(prev_turn_messages.clone());
metadata_messages.extend(prev_turn_messages);
}
if let Some(context) = joining_history_context {
messages.extend(context)
}
messages.extend(unsent_messages_incoming.clone());
metadata_messages.extend(unsent_messages_incoming.clone());
(messages, metadata_messages, vec![])
} else {
let prev_agent_messages: Vec<PayloadMessage> = self
.message_store
.messages_for_turn(current_turn)
.into_iter()
.filter(|msg| matches!(msg.speaker, Speaker::Agent { .. }))
.map(PayloadMessage::from)
.collect();
let mut messages: Vec<PayloadMessage> = joining_history_context.unwrap_or_default();
messages.extend(prev_agent_messages.clone());
messages.extend(unsent_messages_incoming.clone());
let mut metadata_messages = prev_agent_messages.clone();
metadata_messages.extend(unsent_messages_incoming.clone());
trace!(
target = "llm_toolkit::dialogue",
turn = current_turn,
agent_idx = participant_idx,
sequence_idx,
agent_name = %agent_name,
prev_message_count = prev_agent_messages.len(),
incoming_message_count = unsent_messages_incoming.len(),
"Sequential mode: Agent {} receiving {} previous agent messages + {} incoming messages",
agent_name,
prev_agent_messages.len(),
unsent_messages_incoming.len()
);
(messages, metadata_messages, vec![])
};
let turn_input = TurnInput::with_messages_and_context(
current_messages,
vec![], participants_info.clone(),
agent_name.clone(),
);
let messages = turn_input.to_messages();
let mut input_payload = Payload::from_messages(messages);
if let Some(ref context) = self.context {
input_payload = input_payload.with_context(context.to_prompt());
}
input_payload =
Self::apply_metadata_attachments(input_payload, &messages_with_metadata);
input_payload = input_payload.with_participants(participants_info.clone());
let response = agent.execute(input_payload).await?;
let speaker = participant.to_speaker();
let metadata = MessageMetadata::new().with_origin(MessageOrigin::AgentGenerated);
let response_message =
DialogueMessage::new(current_turn, speaker.clone(), response.clone())
.with_metadata(&metadata);
self.message_store.push(response_message);
if !message_ids_to_mark.is_empty() {
self.message_store.mark_all_as_sent(&message_ids_to_mark);
trace!(
target = "llm_toolkit::dialogue",
turn = current_turn,
agent_idx = participant_idx,
sequence_idx,
agent_name = %agent_name,
marked_sent_count = message_ids_to_mark.len(),
"Marked {} messages as sent after agent {} execution",
message_ids_to_mark.len(),
agent_name
);
}
final_turn = Some(DialogueTurn {
speaker,
content: response,
});
}
if !incoming_message_ids.is_empty() {
self.message_store.mark_all_as_sent(&incoming_message_ids);
trace!(
target = "llm_toolkit::dialogue",
turn = current_turn,
marked_sent_count = incoming_message_ids.len(),
"Sequential mode: Marked {} incoming messages as sent after all agents",
incoming_message_ids.len()
);
}
Ok(final_turn.into_iter().collect())
}
async fn run_mentioned(
&mut self,
current_turn: usize,
strategy: MentionMatchStrategy,
) -> Result<Vec<DialogueTurn>, AgentError> {
debug!(
target = "llm_toolkit::dialogue",
turn = current_turn,
execution_model = "mentioned",
participant_count = self.participants.len(),
has_context = self.context.is_some(),
"Starting dialogue.run() in mentioned mode"
);
let mut pending = self.spawn_mentioned_tasks(current_turn, strategy);
let mut dialogue_turns = Vec::new();
while let Some(Ok((idx, _name, result))) = pending.join_next().await {
match result {
Ok(content) => {
let speaker = self.participants[idx].to_speaker();
let metadata =
MessageMetadata::new().with_origin(MessageOrigin::AgentGenerated);
let response_message =
DialogueMessage::new(current_turn, speaker.clone(), content.clone())
.with_metadata(&metadata);
self.message_store.push(response_message);
dialogue_turns.push(DialogueTurn { speaker, content });
}
Err(err) => return Err(err),
}
}
Ok(dialogue_turns)
}
pub fn partial_session(&mut self, initial_prompt: impl Into<Payload>) -> DialogueSession<'_> {
self.partial_session_internal(initial_prompt, None)
}
pub fn partial_session_with_order(
&mut self,
initial_prompt: impl Into<Payload>,
broadcast_order: BroadcastOrder,
) -> DialogueSession<'_> {
self.partial_session_internal(initial_prompt, Some(broadcast_order))
}
fn partial_session_internal(
&mut self,
initial_prompt: impl Into<Payload>,
broadcast_order_override: Option<BroadcastOrder>,
) -> DialogueSession<'_> {
let original_model = self.execution_model.clone();
if let Some(broadcast_order) = broadcast_order_override {
self.execution_model = ExecutionModel::OrderedBroadcast(broadcast_order);
}
let payload: Payload = initial_prompt.into();
let current_turn = self.next_turn();
let (stored_prompt, _) = self.store_payload_messages(&payload, current_turn);
if !self.should_react(&payload) {
crate::tracing::trace!(
target = "llm_toolkit::dialogue",
turn = current_turn,
participant_count = self.participants.len(),
"Starting partial_session passed no react"
);
let model = self.execution_model.clone();
self.execution_model = original_model;
return DialogueSession {
dialogue: self,
state: SessionState::Completed,
model,
};
}
trace!(
target = "llm_toolkit::dialogue",
turn = current_turn,
prompt_length = stored_prompt.len(),
total_store_size = self.message_store.len(),
"Stored incoming payload in MessageStore"
);
let model = self.execution_model.clone();
let state = match &model {
ExecutionModel::Sequential => {
let participants_info = self.get_participants_info();
let prev_agent_outputs: Vec<PayloadMessage> = if current_turn > 1 {
self.message_store
.messages_for_turn(current_turn - 1)
.into_iter()
.filter(|msg| matches!(msg.speaker, Speaker::Agent { .. }))
.map(PayloadMessage::from)
.collect()
} else {
Vec::new()
};
match self.resolve_sequential_indices(&SequentialOrder::AsAdded) {
Ok(sequence) => SessionState::Sequential {
next_index: 0,
current_turn,
sequence,
payload,
prev_agent_outputs,
current_turn_outputs: Vec::new(),
participants_info,
},
Err(err) => {
error!(
target = "llm_toolkit::dialogue",
turn = current_turn,
execution_model = "sequential",
participant_count = self.participants.len(),
error = %err,
"Failed to resolve sequential order for partial session"
);
SessionState::Failed(Some(err))
}
}
}
ExecutionModel::OrderedSequential(order) => {
let participants_info = self.get_participants_info();
let prev_agent_outputs: Vec<PayloadMessage> = if current_turn > 1 {
self.message_store
.messages_for_turn(current_turn - 1)
.into_iter()
.filter(|msg| matches!(msg.speaker, Speaker::Agent { .. }))
.map(PayloadMessage::from)
.collect()
} else {
Vec::new()
};
match self.resolve_sequential_indices(order) {
Ok(sequence) => SessionState::Sequential {
next_index: 0,
current_turn,
sequence,
payload,
prev_agent_outputs,
current_turn_outputs: Vec::new(),
participants_info,
},
Err(err) => {
error!(
target = "llm_toolkit::dialogue",
turn = current_turn,
execution_model = "sequential",
participant_count = self.participants.len(),
error = %err,
"Failed to resolve sequential order for partial session"
);
SessionState::Failed(Some(err))
}
}
}
ExecutionModel::Broadcast => {
let pending = self.spawn_broadcast_tasks(current_turn);
SessionState::Broadcast(BroadcastState::new(
pending,
BroadcastOrder::Completion,
self.participants.len(),
current_turn,
))
}
ExecutionModel::OrderedBroadcast(order) => {
let pending = self.spawn_broadcast_tasks(current_turn);
SessionState::Broadcast(BroadcastState::new(
pending,
order.clone(),
self.participants.len(),
current_turn,
))
}
ExecutionModel::Mentioned { strategy } => {
let pending = self.spawn_mentioned_tasks(current_turn, *strategy);
SessionState::Broadcast(BroadcastState::new(
pending,
BroadcastOrder::Completion,
self.participants.len(),
current_turn,
))
}
ExecutionModel::Moderator => {
error!(
target = "llm_toolkit::dialogue",
"Moderator mode is not supported in partial_session, use run() instead"
);
SessionState::Failed(Some(AgentError::ExecutionFailed(
"Moderator mode requires run() method, not partial_session()".to_string(),
)))
}
};
DialogueSession {
dialogue: self,
state,
model,
}
}
pub(super) fn spawn_broadcast_tasks(
&mut self,
current_turn: usize,
) -> JoinSet<(usize, String, Result<String, AgentError>)> {
let ctx = self.prepare_broadcast_context(current_turn);
let mut pending = JoinSet::new();
for idx in 0..self.participants.len() {
let participant: &Participant = &self.participants[idx];
let joining_history_context = ctx.joining_history_contexts.get(idx);
let agent: Arc<crate::AnyAgent<String>> = Arc::clone(&participant.agent);
let participant_name = participant.name().to_string();
let mut current_messages: Vec<PayloadMessage> = vec![];
if let Some(Some(context)) = joining_history_context {
current_messages.extend_from_slice(context)
}
let unsent_payload_messages: Vec<PayloadMessage> = ctx
.unsent_from_agent
.iter()
.filter(|msg| msg.speaker.name() != participant_name)
.cloned()
.collect();
current_messages.extend(unsent_payload_messages);
current_messages.extend(ctx.unsent_incoming.clone());
let messages_with_metadata = current_messages.clone();
let turn_input = TurnInput::with_messages_and_context(
current_messages,
vec![],
ctx.participants_info.clone(),
participant_name.clone(),
);
let messages = turn_input.to_messages();
let mut payload = Payload::from_messages(messages);
if let Some(ref context) = self.context {
payload = payload.with_context(context.to_prompt());
}
payload = Self::apply_metadata_attachments(payload, &messages_with_metadata);
let input_payload = payload.with_participants(ctx.participants_info.clone());
pending.spawn(async move {
let result = agent.execute(input_payload).await;
(idx, participant_name, result)
});
}
self.message_store
.mark_all_as_sent(&ctx.message_ids_to_mark);
if !ctx.message_ids_to_mark.is_empty() {
trace!(
target = "llm_toolkit::dialogue",
marked_sent_count = ctx.message_ids_to_mark.len(),
"Marked messages as sent_to_agents in MessageStore"
);
}
pending
}
pub(super) fn spawn_mentioned_tasks(
&mut self,
current_turn: usize,
strategy: MentionMatchStrategy,
) -> JoinSet<(usize, String, Result<String, AgentError>)> {
let unsent_messages_incoming: Vec<PayloadMessage> = self
.message_store
.unsent_messages_with_origin(MessageOrigin::IncomingPayload)
.into_iter()
.map(PayloadMessage::from)
.collect();
let incoming_message_ids: Vec<_> = self
.message_store
.unsent_messages_with_origin(MessageOrigin::IncomingPayload)
.iter()
.map(|msg| msg.id)
.collect();
let unsent_messages_from_agent: Vec<PayloadMessage> = self
.message_store
.unsent_messages_with_origin(MessageOrigin::AgentGenerated)
.into_iter()
.map(PayloadMessage::from)
.collect();
let agent_message_ids: Vec<_> = self
.message_store
.unsent_messages_with_origin(MessageOrigin::AgentGenerated)
.iter()
.map(|msg| msg.id)
.collect();
trace!(
target = "llm_toolkit::dialogue",
turn = current_turn,
incoming_count = unsent_messages_incoming.len(),
agent_count = unsent_messages_from_agent.len(),
"Retrieved unsent messages from MessageStore for mention extraction"
);
let mentions_text = {
let incoming_text = unsent_messages_incoming
.iter()
.map(|msg| msg.content.as_str())
.collect::<Vec<_>>()
.join("\n");
let agent_text = unsent_messages_from_agent
.iter()
.map(|msg| msg.content.as_str())
.collect::<Vec<_>>()
.join("\n");
format!("{}\n{}", incoming_text, agent_text)
};
let participant_names: Vec<String> = self
.participants
.iter()
.map(|p| p.name().to_string())
.collect();
let participant_name_refs: Vec<&str> =
participant_names.iter().map(|s| s.as_str()).collect();
let mentioned_names =
extract_mentions_with_strategy(&mentions_text, &participant_name_refs, strategy);
trace!(
target = "llm_toolkit::dialogue",
turn = current_turn,
mentions_text_preview = &mentions_text[..mentions_text.len().min(100)],
incoming_message_count = unsent_messages_incoming.len(),
agent_message_count = unsent_messages_from_agent.len(),
all_participants = ?participant_name_refs,
mentioned = ?mentioned_names,
"Extracting mentions for Mentioned execution mode"
);
let target_participants: Vec<&str> = if mentioned_names.is_empty() {
debug!(
target = "llm_toolkit::dialogue",
turn = current_turn,
"No mentions found, falling back to broadcast mode"
);
participant_name_refs
} else {
trace!(
target = "llm_toolkit::dialogue",
turn = current_turn,
mentioned_count = mentioned_names.len(),
mentioned_participants = ?mentioned_names,
"Mentions detected - executing selective participants"
);
mentioned_names
};
let participants_info = self.get_participants_info();
let executing_participants: Vec<_> = self
.participants
.iter()
.filter(|p| target_participants.contains(&p.name()))
.map(|p| p.name())
.collect();
let skipped_participants: Vec<_> = self
.participants
.iter()
.filter(|p| !target_participants.contains(&p.name()))
.map(|p| p.name())
.collect();
trace!(
target = "llm_toolkit::dialogue",
turn = current_turn,
executing_count = executing_participants.len(),
executing_participants = ?executing_participants,
skipped_count = skipped_participants.len(),
skipped_participants = ?skipped_participants,
"Mention-based execution plan determined"
);
let mentioned_indices: Vec<usize> = self
.participants
.iter()
.enumerate()
.filter(|(_, p)| target_participants.contains(&p.name()))
.map(|(idx, _)| idx)
.collect();
let mut joining_history_contexts = vec![];
for &idx in &mentioned_indices {
let participant = &self.participants[idx];
let speaker = participant.to_speaker();
let joining_history_context = self.join_pending_participant(speaker, current_turn);
joining_history_contexts.push(joining_history_context);
}
let mut pending = JoinSet::new();
for (i, &idx) in mentioned_indices.iter().enumerate() {
let participant = &self.participants[idx];
let participant_name = participant.name().to_string();
let agent = Arc::clone(&participant.agent);
let joining_history_context = &joining_history_contexts[i];
let mut current_messages: Vec<PayloadMessage> = vec![];
if let Some(context) = joining_history_context {
current_messages.extend(context.clone());
}
current_messages.extend(unsent_messages_incoming.clone());
let messages_with_metadata = current_messages.clone();
let turn_input = TurnInput::with_messages_and_context(
current_messages.clone(),
vec![],
participants_info.clone(),
participant_name.clone(),
);
let messages = turn_input.to_messages();
let mut payload = Payload::from_messages(messages);
if let Some(ref context) = self.context {
payload = payload.with_context(context.to_prompt());
}
payload = Self::apply_metadata_attachments(payload, &messages_with_metadata);
let input_payload = payload.with_participants(participants_info.clone());
trace!(
target = "llm_toolkit::dialogue",
turn = current_turn,
participant = %participant_name,
"Spawning task for mentioned participant"
);
pending.spawn(async move {
let result = agent.execute(input_payload).await;
(idx, participant_name, result)
});
}
let mut all_message_ids = agent_message_ids;
all_message_ids.extend(incoming_message_ids);
self.message_store.mark_all_as_sent(&all_message_ids);
if !all_message_ids.is_empty() {
trace!(
target = "llm_toolkit::dialogue",
marked_sent_count = all_message_ids.len(),
"Marked messages as sent_to_agents in MessageStore"
);
}
pending
}
#[cfg(test)]
pub(crate) fn format_history(&self) -> String {
self.history()
.iter()
.map(|turn| format!("[{}]: {}", turn.speaker.name(), turn.content))
.collect::<Vec<_>>()
.join("\n")
}
fn extract_messages_from_payload(
&self,
payload: &Payload,
turn: usize,
) -> (Vec<DialogueMessage>, String) {
use crate::agent::PayloadContent;
let mut messages = Vec::new();
let mut text_parts = Vec::new();
for content in payload.contents() {
match content {
PayloadContent::Message {
speaker,
content,
metadata,
} => {
let metadata = metadata
.clone()
.ensure_origin(MessageOrigin::IncomingPayload);
messages.push(
DialogueMessage::new(turn, speaker.clone(), content.clone())
.with_metadata(&metadata),
);
text_parts.push(content.as_str());
}
PayloadContent::Text(text) => {
let metadata =
MessageMetadata::new().with_origin(MessageOrigin::IncomingPayload);
messages.push(
DialogueMessage::new(
turn,
Speaker::System, text.clone(),
)
.with_metadata(&metadata),
);
text_parts.push(text.as_str());
}
PayloadContent::Attachment(_)
| PayloadContent::Participants(_)
| PayloadContent::Document(_)
| PayloadContent::Context(_) => {
}
}
}
if messages.is_empty() {
let prompt_text = payload.to_text();
messages.push(DialogueMessage::new(
turn,
Speaker::System,
prompt_text.clone(),
));
return (messages, prompt_text);
}
let attachments: Vec<_> = payload.attachments().into_iter().cloned().collect();
if !attachments.is_empty() {
if let Some(first_msg) = messages.first_mut() {
let metadata = first_msg.metadata.clone().with_attachments(attachments);
first_msg.metadata = metadata;
} else {
let metadata = MessageMetadata::new()
.with_origin(MessageOrigin::IncomingPayload)
.with_attachments(attachments);
let attachment_message = DialogueMessage::new(turn, Speaker::System, String::new())
.with_metadata(&metadata);
messages.push(attachment_message);
}
}
let prompt_text = text_parts.join("\n");
(messages, prompt_text)
}
pub fn history(&self) -> Vec<DialogueTurn> {
self.message_store
.all_messages()
.into_iter()
.map(|msg| DialogueTurn {
speaker: msg.speaker.clone(),
content: msg.content.clone(),
})
.collect()
}
pub fn message_store(&self) -> &MessageStore {
&self.message_store
}
pub fn participants(&self) -> Vec<&Persona> {
self.participants.iter().map(|p| &p.persona).collect()
}
pub fn participant_count(&self) -> usize {
self.participants.len()
}
pub fn save_history(&self, path: impl AsRef<std::path::Path>) -> Result<(), AgentError> {
let history_to_save = self.history(); let json = serde_json::to_string_pretty(&history_to_save).map_err(|e| {
AgentError::ExecutionFailed(format!("Failed to serialize history: {}", e))
})?;
std::fs::write(path, json).map_err(|e| {
AgentError::ExecutionFailed(format!("Failed to write history file: {}", e))
})?;
Ok(())
}
pub fn load_history(
path: impl AsRef<std::path::Path>,
) -> Result<Vec<DialogueTurn>, AgentError> {
let json = std::fs::read_to_string(path).map_err(|e| {
AgentError::ExecutionFailed(format!("Failed to read history file: {}", e))
})?;
serde_json::from_str(&json).map_err(|e| {
AgentError::ExecutionFailed(format!("Failed to deserialize history: {}", e))
})
}
}
#[async_trait]
impl Agent for Dialogue {
type Output = Vec<DialogueTurn>;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
&"Multi-agent dialogue facilitating collaborative conversations with diverse perspectives"
}
fn name(&self) -> String {
if self.participants.is_empty() {
"EmptyDialogue".to_string()
} else {
let model_str = match &self.execution_model {
ExecutionModel::Sequential => "Sequential",
ExecutionModel::OrderedSequential(_) => "Sequential",
ExecutionModel::Broadcast => "Broadcast",
ExecutionModel::OrderedBroadcast(_) => "Broadcast",
ExecutionModel::Mentioned { .. } => "Mentioned",
ExecutionModel::Moderator => "Moderator",
};
if self.participants.len() == 1 {
format!(
"{}Dialogue({})",
model_str, self.participants[0].persona.name
)
} else {
format!(
"{}Dialogue({} participants)",
model_str,
self.participants.len()
)
}
}
}
async fn execute(&self, payload: Payload) -> Result<Self::Output, AgentError> {
let mut dialogue_clone = self.clone();
dialogue_clone.run(payload).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use tokio::time::{Duration, sleep};
#[derive(Clone)]
struct MockAgent {
name: String,
responses: Vec<String>,
call_count: std::sync::Arc<std::sync::Mutex<usize>>,
payloads: std::sync::Arc<std::sync::Mutex<Vec<Payload>>>,
}
impl MockAgent {
fn new(name: impl Into<String>, responses: Vec<String>) -> Self {
Self {
name: name.into(),
responses,
call_count: std::sync::Arc::new(std::sync::Mutex::new(0)),
payloads: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
}
}
fn get_payloads(&self) -> Vec<Payload> {
self.payloads.lock().unwrap().clone()
}
fn get_call_count(&self) -> usize {
*self.call_count.lock().unwrap()
}
}
#[async_trait]
impl Agent for MockAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Mock agent for testing";
&EXPERTISE
}
fn name(&self) -> String {
self.name.clone()
}
async fn execute(&self, payload: Payload) -> Result<Self::Output, AgentError> {
self.payloads.lock().unwrap().push(payload);
let mut count = self.call_count.lock().unwrap();
let response_idx = *count % self.responses.len();
*count += 1;
Ok(self.responses[response_idx].clone())
}
}
#[tokio::test]
async fn test_broadcast_strategy() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::broadcast();
let persona1 = Persona {
name: "Agent1".to_string(),
role: "Tester".to_string(),
background: "Test agent 1".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Agent2".to_string(),
role: "Tester".to_string(),
background: "Test agent 2".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(
persona1,
MockAgent::new("Agent1", vec!["Response 1".to_string()]),
)
.add_participant(
persona2,
MockAgent::new("Agent2", vec!["Response 2".to_string()]),
);
let turns = dialogue.run("Initial prompt".to_string()).await.unwrap();
assert_eq!(turns.len(), 2);
assert_eq!(turns[0].speaker.name(), "Agent1");
assert_eq!(turns[0].content, "Response 1");
assert_eq!(turns[1].speaker.name(), "Agent2");
assert_eq!(turns[1].content, "Response 2");
assert_eq!(dialogue.history().len(), 3);
assert_eq!(dialogue.history()[0].speaker.name(), "System");
assert_eq!(dialogue.history()[1].speaker.name(), "Agent1");
assert_eq!(dialogue.history()[2].speaker.name(), "Agent2");
}
#[tokio::test]
async fn test_dialogue_with_no_participants() {
let mut dialogue = Dialogue::broadcast();
let result = dialogue.run("Test".to_string()).await;
assert!(result.is_ok());
let turns = result.unwrap();
assert_eq!(turns.len(), 0);
assert_eq!(dialogue.history().len(), 1);
}
#[tokio::test]
async fn test_dialogue_format_history() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::broadcast();
let persona = Persona {
name: "Agent1".to_string(),
role: "Tester".to_string(),
background: "Test agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue.add_participant(persona, MockAgent::new("Agent1", vec!["Hello".to_string()]));
dialogue.run("Start".to_string()).await.unwrap();
let formatted = dialogue.format_history();
assert!(formatted.contains("[System]: Start"));
assert!(formatted.contains("[Agent1]: Hello"));
}
#[tokio::test]
async fn test_sequential_strategy() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::sequential();
let persona1 = Persona {
name: "Summarizer".to_string(),
role: "Summarizer".to_string(),
background: "Summarizes inputs".to_string(),
communication_style: "Concise".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Translator".to_string(),
role: "Translator".to_string(),
background: "Translates content".to_string(),
communication_style: "Formal".to_string(),
visual_identity: None,
capabilities: None,
};
let persona3 = Persona {
name: "Finalizer".to_string(),
role: "Finalizer".to_string(),
background: "Finalizes output".to_string(),
communication_style: "Professional".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(
persona1,
MockAgent::new("Summarizer", vec!["Summary: input received".to_string()]),
)
.add_participant(
persona2,
MockAgent::new(
"Translator",
vec!["Translated: previous output".to_string()],
),
)
.add_participant(
persona3,
MockAgent::new("Finalizer", vec!["Final output: all done".to_string()]),
);
let turns = dialogue.run("Initial prompt".to_string()).await.unwrap();
assert_eq!(turns.len(), 1);
assert_eq!(turns[0].speaker.name(), "Finalizer");
assert_eq!(turns[0].content, "Final output: all done");
assert_eq!(dialogue.history().len(), 4);
assert_eq!(dialogue.history()[0].speaker.name(), "System");
assert_eq!(dialogue.history()[0].content, "Initial prompt");
assert_eq!(dialogue.history()[1].speaker.name(), "Summarizer");
assert_eq!(dialogue.history()[1].content, "Summary: input received");
assert_eq!(dialogue.history()[2].speaker.name(), "Translator");
assert_eq!(dialogue.history()[2].content, "Translated: previous output");
assert_eq!(dialogue.history()[3].speaker.name(), "Finalizer");
assert_eq!(dialogue.history()[3].content, "Final output: all done");
}
#[tokio::test]
async fn test_partial_session_sequential_yields_intermediate_turns() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::sequential();
let persona1 = Persona {
name: "Step1".to_string(),
role: "Stage".to_string(),
background: "first".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Step2".to_string(),
role: "Stage".to_string(),
background: "second".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(
persona1.clone(),
MockAgent::new("Step1", vec!["S1 output".to_string()]),
)
.add_participant(
persona2.clone(),
MockAgent::new("Step2", vec!["S2 output".to_string()]),
);
let mut session = dialogue.partial_session("Initial".to_string());
assert_eq!(
session.execution_model(),
ExecutionModel::OrderedSequential(SequentialOrder::AsAdded)
);
let first = session.next_turn().await.unwrap().unwrap();
assert_eq!(first.speaker.name(), "Step1");
assert_eq!(first.content, "S1 output");
let second = session.next_turn().await.unwrap().unwrap();
assert_eq!(second.speaker.name(), "Step2");
assert_eq!(second.content, "S2 output");
assert!(session.next_turn().await.is_none());
assert_eq!(dialogue.history().len(), 3);
assert_eq!(dialogue.history()[0].speaker.name(), "System");
assert_eq!(dialogue.history()[1].speaker.name(), "Step1");
assert_eq!(dialogue.history()[2].speaker.name(), "Step2");
}
#[derive(Clone)]
struct DelayAgent {
name: String,
delay_ms: u64,
}
impl DelayAgent {
fn new(name: impl Into<String>, delay_ms: u64) -> Self {
Self {
name: name.into(),
delay_ms,
}
}
}
#[async_trait]
impl Agent for DelayAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Delayed agent";
&EXPERTISE
}
async fn execute(&self, intent: Payload) -> Result<Self::Output, AgentError> {
sleep(Duration::from_millis(self.delay_ms)).await;
Ok(format!("{} handled {}", self.name, intent.to_text()))
}
}
#[tokio::test]
async fn test_partial_session_broadcast_streams_responses() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::broadcast();
let fast = Persona {
name: "Fast".to_string(),
role: "Fast responder".to_string(),
background: "Quick replies".to_string(),
communication_style: "Snappy".to_string(),
visual_identity: None,
capabilities: None,
};
let slow = Persona {
name: "Slow".to_string(),
role: "Slow responder".to_string(),
background: "Takes time".to_string(),
communication_style: "Measured".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(fast, DelayAgent::new("Fast", 10))
.add_participant(slow, DelayAgent::new("Slow", 50));
let mut session = dialogue.partial_session("Hello".to_string());
assert_eq!(
session.execution_model(),
ExecutionModel::OrderedBroadcast(BroadcastOrder::Completion)
);
let first = session.next_turn().await.unwrap().unwrap();
assert_eq!(first.speaker.name(), "Fast");
assert!(first.content.contains("Fast handled"));
let second = session.next_turn().await.unwrap().unwrap();
assert_eq!(second.speaker.name(), "Slow");
assert!(second.content.contains("Slow handled"));
assert!(session.next_turn().await.is_none());
}
#[tokio::test]
async fn test_partial_session_broadcast_ordered_mode_respects_participant_order() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::broadcast();
let slow = Persona {
name: "Slow".to_string(),
role: "Deliberate responder".to_string(),
background: "Prefers careful analysis".to_string(),
communication_style: "Measured".to_string(),
visual_identity: None,
capabilities: None,
};
let fast = Persona {
name: "Fast".to_string(),
role: "Quick responder".to_string(),
background: "Snappy insights".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(slow, DelayAgent::new("Slow", 50))
.add_participant(fast, DelayAgent::new("Fast", 10));
let mut session = dialogue
.partial_session_with_order("Hello".to_string(), BroadcastOrder::ParticipantOrder);
let first = session.next_turn().await.unwrap().unwrap();
assert_eq!(first.speaker.name(), "Slow");
assert!(first.content.contains("Slow handled"));
let second = session.next_turn().await.unwrap().unwrap();
assert_eq!(second.speaker.name(), "Fast");
assert!(second.content.contains("Fast handled"));
assert!(session.next_turn().await.is_none());
}
#[tokio::test]
async fn test_from_persona_team_broadcast() {
use crate::agent::persona::{Persona, PersonaTeam};
let mut team = PersonaTeam::new("Test Team".to_string(), "Testing scenario".to_string());
team.add_persona(Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Senior engineer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
});
team.add_persona(Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UX specialist".to_string(),
communication_style: "User-focused".to_string(),
visual_identity: None,
capabilities: None,
});
team.execution_strategy =
Some(ExecutionModel::OrderedBroadcast(BroadcastOrder::Completion));
let llm = MockAgent::new("Mock", vec!["Response".to_string()]);
let mut dialogue = Dialogue::from_persona_team(team, llm).unwrap();
assert_eq!(dialogue.participant_count(), 2);
let turns = dialogue.run("Test prompt".to_string()).await.unwrap();
assert_eq!(turns.len(), 2); }
#[tokio::test]
async fn test_from_persona_team_sequential() {
use crate::agent::persona::{Persona, PersonaTeam};
let mut team = PersonaTeam::new(
"Sequential Team".to_string(),
"Sequential testing".to_string(),
);
team.add_persona(Persona {
name: "First".to_string(),
role: "Analyzer".to_string(),
background: "Data analyst".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
});
team.add_persona(Persona {
name: "Second".to_string(),
role: "Synthesizer".to_string(),
background: "Content creator".to_string(),
communication_style: "Creative".to_string(),
visual_identity: None,
capabilities: None,
});
team.execution_strategy = Some(ExecutionModel::OrderedSequential(SequentialOrder::AsAdded));
let llm = MockAgent::new("Mock", vec!["Step output".to_string()]);
let mut dialogue = Dialogue::from_persona_team(team, llm).unwrap();
let turns = dialogue.run("Process this".to_string()).await.unwrap();
assert_eq!(turns.len(), 1); }
#[tokio::test]
async fn test_add_participant_dynamically() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::broadcast();
let initial_persona = Persona {
name: "Initial".to_string(),
role: "Initial Agent".to_string(),
background: "Initial background".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue.add_participant(
initial_persona,
MockAgent::new("Initial", vec!["Response 1".to_string()]),
);
assert_eq!(dialogue.participant_count(), 1);
let expert = Persona {
name: "Expert".to_string(),
role: "Domain Expert".to_string(),
background: "20 years experience".to_string(),
communication_style: "Authoritative".to_string(),
visual_identity: None,
capabilities: None,
};
let llm = MockAgent::new("ExpertLLM", vec!["Expert response".to_string()]);
dialogue.add_participant(expert, llm);
assert_eq!(dialogue.participant_count(), 2);
let turns = dialogue.run("Consult experts".to_string()).await.unwrap();
assert_eq!(turns.len(), 2);
}
#[tokio::test]
async fn test_persona_team_round_trip() {
use crate::agent::persona::{Persona, PersonaTeam};
use tempfile::NamedTempFile;
let mut team = PersonaTeam::new(
"Round Trip Team".to_string(),
"Testing save/load".to_string(),
);
team.add_persona(Persona {
name: "Charlie".to_string(),
role: "Tester".to_string(),
background: "QA specialist".to_string(),
communication_style: "Thorough".to_string(),
visual_identity: None,
capabilities: None,
});
let temp_file = NamedTempFile::new().unwrap();
team.save(temp_file.path()).unwrap();
let loaded_team = PersonaTeam::load(temp_file.path()).unwrap();
let llm = MockAgent::new("Mock", vec!["Response".to_string()]);
let dialogue = Dialogue::from_persona_team(loaded_team, llm).unwrap();
assert_eq!(dialogue.participant_count(), 1);
}
#[tokio::test]
async fn test_remove_participant() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::broadcast();
let persona1 = Persona {
name: "Agent1".to_string(),
role: "Tester".to_string(),
background: "Test".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Agent2".to_string(),
role: "Tester".to_string(),
background: "Test".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona3 = Persona {
name: "Agent3".to_string(),
role: "Tester".to_string(),
background: "Test".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue.add_participant(persona1, MockAgent::new("Agent1", vec!["R1".to_string()]));
dialogue.add_participant(persona2, MockAgent::new("Agent2", vec!["R2".to_string()]));
dialogue.add_participant(persona3, MockAgent::new("Agent3", vec!["R3".to_string()]));
assert_eq!(dialogue.participant_count(), 3);
dialogue.remove_participant("Agent2").unwrap();
assert_eq!(dialogue.participant_count(), 2);
let turns = dialogue.run("Test".to_string()).await.unwrap();
assert_eq!(turns.len(), 2);
assert_eq!(turns[0].speaker.name(), "Agent1");
assert_eq!(turns[1].speaker.name(), "Agent3");
}
#[tokio::test]
async fn test_remove_participant_not_found() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::broadcast();
let persona = Persona {
name: "Agent1".to_string(),
role: "Tester".to_string(),
background: "Test".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue.add_participant(persona, MockAgent::new("Agent1", vec!["R1".to_string()]));
let result = dialogue.remove_participant("NonExistent");
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("participant not found")
);
}
#[tokio::test]
async fn test_from_blueprint_with_predefined_participants() {
use crate::agent::persona::{Persona, PersonaTeam};
let persona1 = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Senior engineer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UX specialist".to_string(),
communication_style: "User-focused".to_string(),
visual_identity: None,
capabilities: None,
};
let blueprint = DialogueBlueprint {
agenda: "Feature Planning".to_string(),
context: "Planning new feature".to_string(),
participants: Some(vec![persona1, persona2]),
execution_strategy: Some(ExecutionModel::OrderedBroadcast(BroadcastOrder::Completion)),
};
#[derive(Clone)]
struct MockGeneratorAgent;
#[async_trait]
impl Agent for MockGeneratorAgent {
type Output = PersonaTeam;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Generator";
&EXPERTISE
}
fn name(&self) -> String {
"Generator".to_string()
}
async fn execute(&self, _payload: Payload) -> Result<Self::Output, AgentError> {
panic!("Generator agent should not be called when participants are provided");
}
}
let dialogue_agent = MockAgent::new("DialogueAgent", vec!["Response".to_string()]);
let mut dialogue = Dialogue::from_blueprint(blueprint, MockGeneratorAgent, dialogue_agent)
.await
.unwrap();
assert_eq!(dialogue.participant_count(), 2);
let turns = dialogue.run("Test".to_string()).await.unwrap();
assert_eq!(turns.len(), 2);
assert_eq!(turns[0].speaker.name(), "Alice");
assert_eq!(turns[1].speaker.name(), "Bob");
}
#[tokio::test]
async fn test_guest_participant_workflow() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::broadcast();
let core1 = Persona {
name: "CoreMember1".to_string(),
role: "Core Member".to_string(),
background: "Core team member".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let core2 = Persona {
name: "CoreMember2".to_string(),
role: "Core Member".to_string(),
background: "Core team member".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue.add_participant(
core1,
MockAgent::new("CoreMember1", vec!["Core response 1".to_string()]),
);
dialogue.add_participant(
core2,
MockAgent::new("CoreMember2", vec!["Core response 2".to_string()]),
);
assert_eq!(dialogue.participant_count(), 2);
let guest = Persona {
name: "Guest Expert".to_string(),
role: "Domain Specialist".to_string(),
background: "Guest invited for this session".to_string(),
communication_style: "Expert".to_string(),
visual_identity: None,
capabilities: None,
};
let guest_llm = MockAgent::new("Guest", vec!["Guest insight".to_string()]);
dialogue.add_participant(guest, guest_llm);
assert_eq!(dialogue.participant_count(), 3);
let with_guest = dialogue
.run("Topic requiring expert input".to_string())
.await
.unwrap();
assert_eq!(with_guest.len(), 3);
dialogue.remove_participant("Guest Expert").unwrap();
assert_eq!(dialogue.participant_count(), 2);
let core_only = dialogue
.run("Continue discussion".to_string())
.await
.unwrap();
assert_eq!(core_only.len(), 2); }
#[tokio::test]
async fn test_participants_getter() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::broadcast();
assert_eq!(dialogue.participants().len(), 0);
let persona1 = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Senior engineer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue.add_participant(
persona1.clone(),
MockAgent::new("Alice", vec!["Response 1".to_string()]),
);
let persona2 = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UX specialist".to_string(),
communication_style: "User-focused".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue.add_participant(
persona2.clone(),
MockAgent::new("Bob", vec!["Response 2".to_string()]),
);
let persona3 = Persona {
name: "Charlie".to_string(),
role: "Manager".to_string(),
background: "Product manager".to_string(),
communication_style: "Strategic".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue.add_participant(
persona3.clone(),
MockAgent::new("Charlie", vec!["Response 3".to_string()]),
);
let participants = dialogue.participants();
assert_eq!(participants.len(), 3);
assert_eq!(participants[0].name, "Alice");
assert_eq!(participants[1].name, "Bob");
assert_eq!(participants[2].name, "Charlie");
assert_eq!(participants[0].role, "Developer");
assert_eq!(participants[1].role, "Designer");
assert_eq!(participants[2].role, "Manager");
assert_eq!(participants[0].background, "Senior engineer");
assert_eq!(participants[1].background, "UX specialist");
assert_eq!(participants[2].background, "Product manager");
assert_eq!(participants[0].communication_style, "Technical");
assert_eq!(participants[1].communication_style, "User-focused");
assert_eq!(participants[2].communication_style, "Strategic");
dialogue.remove_participant("Bob").unwrap();
let participants_after_removal = dialogue.participants();
assert_eq!(participants_after_removal.len(), 2);
assert_eq!(participants_after_removal[0].name, "Alice");
assert_eq!(participants_after_removal[1].name, "Charlie");
}
#[tokio::test]
async fn test_partial_session_with_multimodal_payload() {
use crate::agent::persona::Persona;
use crate::attachment::Attachment;
let mut dialogue = Dialogue::broadcast();
let persona = Persona {
name: "Analyst".to_string(),
role: "Image Analyst".to_string(),
background: "Expert in image analysis".to_string(),
communication_style: "Technical and precise".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue.add_participant(
persona,
MockAgent::new("Analyst", vec!["Image analysis complete".to_string()]),
);
let payload = Payload::text("Analyze this image")
.with_attachment(Attachment::local("/test/image.png"));
let mut session = dialogue.partial_session(payload);
let turn = session.next_turn().await.unwrap().unwrap();
assert_eq!(turn.speaker.name(), "Analyst");
assert_eq!(turn.content, "Image analysis complete");
assert_eq!(dialogue.history().len(), 2);
assert_eq!(dialogue.history()[0].speaker.name(), "System");
assert_eq!(dialogue.history()[0].content, "Analyze this image");
let messages = dialogue.message_store().all_messages();
assert_eq!(messages.len(), 2);
let first_message = messages[0];
assert!(
first_message.metadata.has_attachments,
"First message should have attachments flag set"
);
assert_eq!(
first_message.metadata.attachments.len(),
1,
"First message should contain 1 attachment"
);
match &first_message.metadata.attachments[0] {
Attachment::Local(path) => {
assert_eq!(path.to_str().unwrap(), "/test/image.png");
}
_ => panic!("Expected Local attachment"),
}
}
#[tokio::test]
async fn test_run_with_string_literal_backward_compatibility() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::broadcast();
let persona = Persona {
name: "Agent".to_string(),
role: "Tester".to_string(),
background: "Test agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue.add_participant(
persona,
MockAgent::new("Agent", vec!["Response".to_string()]),
);
let turns = dialogue.run("Hello").await.unwrap();
assert_eq!(turns.len(), 1);
assert_eq!(turns[0].content, "Response");
}
#[tokio::test]
async fn test_run_with_multimodal_payload() {
use crate::agent::persona::Persona;
use crate::attachment::Attachment;
let mut dialogue = Dialogue::sequential();
let persona1 = Persona {
name: "Analyzer".to_string(),
role: "Data Analyzer".to_string(),
background: "Analyzes data".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Summarizer".to_string(),
role: "Summarizer".to_string(),
background: "Summarizes results".to_string(),
communication_style: "Concise".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(
persona1,
MockAgent::new("Analyzer", vec!["Data analyzed".to_string()]),
)
.add_participant(
persona2,
MockAgent::new("Summarizer", vec!["Summary complete".to_string()]),
);
let payload = Payload::text("Process this data")
.with_attachment(Attachment::local("/test/data.csv"))
.with_attachment(Attachment::local("/test/metadata.json"));
let turns = dialogue.run(payload).await.unwrap();
assert_eq!(turns.len(), 1);
assert_eq!(turns[0].speaker.name(), "Summarizer");
assert_eq!(turns[0].content, "Summary complete");
assert_eq!(dialogue.history().len(), 3);
let messages = dialogue.message_store().all_messages();
assert_eq!(messages.len(), 3);
let first_message = messages[0];
assert!(
first_message.metadata.has_attachments,
"First message should have attachments flag set"
);
assert_eq!(
first_message.metadata.attachments.len(),
2,
"First message should contain 2 attachments"
);
let attachment_paths: Vec<String> = first_message
.metadata
.attachments
.iter()
.filter_map(|a| match a {
Attachment::Local(path) => path.to_str().map(|s| s.to_string()),
_ => None,
})
.collect();
assert_eq!(attachment_paths.len(), 2);
assert!(
attachment_paths.contains(&"/test/data.csv".to_string()),
"Should contain data.csv attachment"
);
assert!(
attachment_paths.contains(&"/test/metadata.json".to_string()),
"Should contain metadata.json attachment"
);
}
#[tokio::test]
async fn test_partial_session_with_ordered_broadcast_and_payload() {
use crate::agent::persona::Persona;
use crate::attachment::Attachment;
let mut dialogue = Dialogue::broadcast();
let persona1 = Persona {
name: "First".to_string(),
role: "First Responder".to_string(),
background: "Quick analysis".to_string(),
communication_style: "Fast".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Second".to_string(),
role: "Second Responder".to_string(),
background: "Detailed analysis".to_string(),
communication_style: "Thorough".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(persona1, DelayAgent::new("First", 50))
.add_participant(persona2, DelayAgent::new("Second", 10));
let payload = Payload::text("Analyze").with_attachment(Attachment::local("/test/file.txt"));
let mut session =
dialogue.partial_session_with_order(payload, BroadcastOrder::ParticipantOrder);
let first = session.next_turn().await.unwrap().unwrap();
assert_eq!(first.speaker.name(), "First");
let second = session.next_turn().await.unwrap().unwrap();
assert_eq!(second.speaker.name(), "Second");
assert!(session.next_turn().await.is_none());
}
#[tokio::test]
async fn test_with_history_injection() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::broadcast();
let persona = Persona {
name: "Agent1".to_string(),
role: "Tester".to_string(),
background: "Test agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue.add_participant(
persona.clone(),
MockAgent::new("Agent1", vec!["Response 1".to_string()]),
);
let turns = dialogue.run("Initial prompt".to_string()).await.unwrap();
assert_eq!(turns.len(), 1);
let history = dialogue.history().to_vec();
assert_eq!(history.len(), 2);
let mut dialogue2 = Dialogue::broadcast().with_history(history);
dialogue2.add_participant(
persona,
MockAgent::new("Agent1", vec!["Response 2".to_string()]),
);
assert_eq!(dialogue2.history().len(), 2);
assert_eq!(dialogue2.history()[0].speaker.name(), "System");
assert_eq!(dialogue2.history()[1].speaker.name(), "Agent1");
assert_eq!(dialogue2.history()[1].content, "Response 1");
let new_turns = dialogue2.run("Continue".to_string()).await.unwrap();
assert_eq!(new_turns.len(), 1);
assert_eq!(dialogue2.history().len(), 4);
}
#[tokio::test]
async fn test_save_and_load_history() {
use crate::agent::persona::Persona;
use tempfile::NamedTempFile;
let mut dialogue = Dialogue::broadcast();
let persona = Persona {
name: "Agent1".to_string(),
role: "Tester".to_string(),
background: "Test agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue.add_participant(
persona,
MockAgent::new("Agent1", vec!["Test response".to_string()]),
);
let turns = dialogue.run("Test prompt".to_string()).await.unwrap();
assert_eq!(turns.len(), 1);
let temp_file = NamedTempFile::new().unwrap();
dialogue.save_history(temp_file.path()).unwrap();
let loaded_history = Dialogue::load_history(temp_file.path()).unwrap();
assert_eq!(loaded_history.len(), dialogue.history().len());
assert_eq!(loaded_history[0].speaker.name(), "System");
assert_eq!(loaded_history[0].content, "Test prompt");
assert_eq!(loaded_history[1].speaker.name(), "Agent1");
assert_eq!(loaded_history[1].content, "Test response");
}
#[tokio::test]
async fn test_session_resumption_workflow() {
use crate::agent::persona::Persona;
use tempfile::NamedTempFile;
let mut session1 = Dialogue::broadcast();
let persona = Persona {
name: "Agent1".to_string(),
role: "Tester".to_string(),
background: "Test agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
session1.add_participant(
persona.clone(),
MockAgent::new("Agent1", vec!["First response".to_string()]),
);
let turns1 = session1.run("First prompt".to_string()).await.unwrap();
assert_eq!(turns1.len(), 1);
assert_eq!(turns1[0].content, "First response");
let temp_file = NamedTempFile::new().unwrap();
session1.save_history(temp_file.path()).unwrap();
let loaded_history = Dialogue::load_history(temp_file.path()).unwrap();
let mut session2 = Dialogue::broadcast().with_history(loaded_history);
session2.add_participant(
persona,
MockAgent::new("Agent1", vec!["Second response".to_string()]),
);
assert_eq!(session2.history().len(), 2);
let turns2 = session2.run("Second prompt".to_string()).await.unwrap();
assert_eq!(turns2.len(), 1);
assert_eq!(turns2[0].content, "Second response");
assert_eq!(session2.history().len(), 4);
assert_eq!(session2.history()[0].speaker.name(), "System");
assert_eq!(session2.history()[0].content, "First prompt");
assert_eq!(session2.history()[1].speaker.name(), "Agent1");
assert_eq!(session2.history()[1].content, "First response");
assert_eq!(session2.history()[2].speaker.name(), "System");
assert_eq!(session2.history()[2].content, "Second prompt");
assert_eq!(session2.history()[3].speaker.name(), "Agent1");
assert_eq!(session2.history()[3].content, "Second response");
}
#[tokio::test]
async fn test_dialogue_turn_serialization() {
let turn = DialogueTurn {
speaker: Speaker::agent("TestAgent", "Tester"),
content: "Test content".to_string(),
};
let json = serde_json::to_string(&turn).unwrap();
let deserialized: DialogueTurn = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.speaker.name(), "TestAgent");
assert_eq!(deserialized.speaker.role(), Some("Tester"));
assert_eq!(deserialized.content, "Test content");
}
#[tokio::test]
async fn test_multi_turn_broadcast_history_visibility() {
use crate::agent::persona::Persona;
#[derive(Clone)]
struct EchoAgent {
name: String,
}
#[async_trait]
impl Agent for EchoAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Echo agent";
&EXPERTISE
}
fn name(&self) -> String {
self.name.clone()
}
async fn execute(&self, payload: Payload) -> Result<Self::Output, AgentError> {
Ok(format!(
"[EchoAgent]{} received: {}",
self.name,
payload.to_text()
))
}
}
let mut dialogue = Dialogue::broadcast();
let persona_a = Persona {
name: "AgentA".to_string(),
role: "Tester".to_string(),
background: "Test agent A".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona_b = Persona {
name: "AgentB".to_string(),
role: "Tester".to_string(),
background: "Test agent B".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(
persona_a,
EchoAgent {
name: "AgentA".to_string(),
},
)
.add_participant(
persona_b,
EchoAgent {
name: "AgentB".to_string(),
},
);
let turns1 = dialogue.run("First message").await.unwrap();
assert_eq!(turns1.len(), 2);
assert!(turns1[0].content.contains("First message"));
assert!(turns1[1].content.contains("First message"));
assert_eq!(dialogue.history().len(), 3);
assert_eq!(dialogue.history()[0].speaker.name(), "System");
assert_eq!(dialogue.history()[0].content, "First message");
let turns2 = dialogue.run("Second message").await.unwrap();
assert_eq!(turns2.len(), 2);
for turn in &turns2 {
if turn.speaker.name() == "AgentA" {
assert!(
turn.content.contains("AgentB"),
"AgentA should see AgentB's context. Got: {}",
turn.content
);
} else if turn.speaker.name() == "AgentB" {
assert!(
turn.content.contains("AgentA"),
"AgentB should see AgentA's context. Got: {}",
turn.content
);
}
assert!(
turn.content.contains("Second message"),
"Agent {} should see the current task. Got: {}",
turn.speaker.name(),
turn.content
);
}
assert_eq!(dialogue.history().len(), 6);
assert_eq!(dialogue.history()[0].speaker.name(), "System");
assert_eq!(dialogue.history()[0].content, "First message");
assert_eq!(dialogue.history()[1].speaker.name(), "AgentA");
assert_eq!(dialogue.history()[2].speaker.name(), "AgentB");
assert_eq!(dialogue.history()[3].speaker.name(), "System");
assert_eq!(dialogue.history()[3].content, "Second message");
assert_eq!(dialogue.history()[4].speaker.name(), "AgentA");
assert_eq!(dialogue.history()[5].speaker.name(), "AgentB");
}
#[tokio::test]
async fn test_dialogue_history_format_with_chat_agents() {
use crate::agent::chat::Chat;
use crate::agent::persona::Persona;
#[derive(Clone)]
struct VerboseEchoAgent {
name: String,
}
#[async_trait]
impl Agent for VerboseEchoAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Verbose echo agent";
&EXPERTISE
}
fn name(&self) -> String {
self.name.clone()
}
async fn execute(&self, payload: Payload) -> Result<Self::Output, AgentError> {
let input = payload.to_text();
let preview = if input.len() > 500 {
if let Some(start) = input.find("# Recent History") {
let end = start + 500.min(input.len() - start);
format!("...{}...", &input[start..end])
} else if let Some(start) = input.find("# Participants") {
let end = start + 500.min(input.len() - start);
format!("...{}...", &input[start..end])
} else {
format!("{}...", &input[..500])
}
} else {
input.clone()
};
Ok(format!(
"[{}] Received {} chars: {}",
self.name,
input.len(),
preview
))
}
}
let mut dialogue = Dialogue::broadcast();
let persona_a = Persona {
name: "AgentA".to_string(),
role: "Tester".to_string(),
background: "Test agent A".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona_b = Persona {
name: "AgentB".to_string(),
role: "Tester".to_string(),
background: "Test agent B".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let chat_a = Chat::new(VerboseEchoAgent {
name: "AgentA".to_string(),
})
.with_persona(persona_a.clone())
.with_history(true) .build();
let chat_b = Chat::new(VerboseEchoAgent {
name: "AgentB".to_string(),
})
.with_persona(persona_b.clone())
.with_history(true)
.build();
dialogue.participants.push(Participant {
persona: persona_a,
agent: Arc::new(*chat_a),
joining_strategy: None,
has_sent_once: false,
});
dialogue.participants.push(Participant {
persona: persona_b,
agent: Arc::new(*chat_b),
joining_strategy: None,
has_sent_once: false,
});
let turns1 = dialogue.run("First message").await.unwrap();
println!("\n=== Turn 1 ===");
for turn in &turns1 {
println!("[{}]: {}", turn.speaker.name(), turn.content);
}
let turns2 = dialogue.run("Second message").await.unwrap();
println!("\n=== Turn 2 ===");
for turn in &turns2 {
println!("[{}]: {}", turn.speaker.name(), turn.content);
if turn.content.contains("Previous Conversation") {
println!(
"✓ Agent {} maintains its own conversation history (expected)",
turn.speaker.name()
);
}
if turn.speaker.name() == "AgentA" {
assert!(
turn.content.contains("AgentB") || turn.content.contains("# Request"),
"AgentA should receive context or request"
);
} else if turn.speaker.name() == "AgentB" {
assert!(
turn.content.contains("AgentA") || turn.content.contains("# Request"),
"AgentB should receive context or request"
);
}
}
}
#[tokio::test]
async fn test_multi_message_payload() {
use crate::agent::dialogue::message::Speaker;
let mut dialogue = Dialogue::broadcast();
dialogue.add_participant(
Persona {
name: "Agent1".to_string(),
role: "Tester".to_string(),
background: "Test agent 1".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
},
MockAgent::new("Agent1", vec!["Response from Agent1".to_string()]),
);
dialogue.add_participant(
Persona {
name: "Agent2".to_string(),
role: "Tester".to_string(),
background: "Test agent 2".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
},
MockAgent::new("Agent2", vec!["Response from Agent2".to_string()]),
);
let payload = Payload::from_messages(vec![
PayloadMessage::system("System: Initializing conversation"),
PayloadMessage::user("Alice", "Product Manager", "User: What should we build?"),
]);
let _turns = dialogue.run(payload).await.unwrap();
let history = dialogue.history();
assert!(
history.len() >= 4,
"Expected at least 4 messages, got {}",
history.len()
);
assert_eq!(history[0].speaker, Speaker::System);
assert_eq!(history[0].content, "System: Initializing conversation");
assert_eq!(
history[1].speaker,
Speaker::user("Alice", "Product Manager")
);
assert_eq!(history[1].content, "User: What should we build?");
assert!(matches!(history[2].speaker, Speaker::Agent { .. }));
assert!(matches!(history[3].speaker, Speaker::Agent { .. }));
}
#[tokio::test]
async fn test_multi_message_payload_sequential() {
use crate::agent::dialogue::message::Speaker;
let mut dialogue = Dialogue::sequential();
dialogue.add_participant(
Persona {
name: "Agent1".to_string(),
role: "Analyzer".to_string(),
background: "First agent".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
},
MockAgent::new("Agent1", vec!["Analysis result".to_string()]),
);
dialogue.add_participant(
Persona {
name: "Agent2".to_string(),
role: "Reviewer".to_string(),
background: "Second agent".to_string(),
communication_style: "Critical".to_string(),
visual_identity: None,
capabilities: None,
},
MockAgent::new("Agent2", vec!["Review complete".to_string()]),
);
let payload = Payload::from_messages(vec![
PayloadMessage::system("Context: Project initialization"),
PayloadMessage::user("Bob", "Engineer", "Request: Analyze architecture"),
]);
let turns = dialogue.run(payload).await.unwrap();
assert_eq!(turns.len(), 1);
assert_eq!(turns[0].speaker.name(), "Agent2");
let history = dialogue.history();
assert!(
history.len() >= 4,
"Expected at least 4 messages in history, got {}",
history.len()
);
assert_eq!(history[0].speaker, Speaker::System);
assert_eq!(history[1].speaker, Speaker::user("Bob", "Engineer"));
}
#[tokio::test]
async fn test_partial_session_multi_turn_history_continuity() {
use crate::agent::persona::Persona;
#[derive(Clone)]
struct HistoryEchoAgent {
name: String,
}
#[async_trait]
impl Agent for HistoryEchoAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "History echo agent";
&EXPERTISE
}
fn name(&self) -> String {
self.name.clone()
}
async fn execute(&self, payload: Payload) -> Result<Self::Output, AgentError> {
Ok(format!("{} received: {}", self.name, payload.to_text()))
}
}
let mut dialogue = Dialogue::broadcast();
let persona = Persona {
name: "Agent1".to_string(),
role: "Tester".to_string(),
background: "Test agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue.add_participant(
persona,
HistoryEchoAgent {
name: "Agent1".to_string(),
},
);
let mut session1 = dialogue.partial_session("First message");
let mut turn1_results = Vec::new();
while let Some(Ok(turn)) = session1.next_turn().await {
turn1_results.push(turn);
}
assert_eq!(turn1_results.len(), 1);
assert!(turn1_results[0].content.contains("First message"));
drop(session1);
assert_eq!(dialogue.history().len(), 2); assert_eq!(dialogue.history()[0].speaker.name(), "System");
assert_eq!(dialogue.history()[0].content, "First message");
assert_eq!(dialogue.history()[1].speaker.name(), "Agent1");
let mut session2 = dialogue.partial_session("Second message");
let mut turn2_results = Vec::new();
while let Some(Ok(turn)) = session2.next_turn().await {
turn2_results.push(turn);
}
assert_eq!(turn2_results.len(), 1);
assert!(turn2_results[0].content.contains("Second message"));
drop(session2);
assert_eq!(dialogue.history().len(), 4); assert_eq!(dialogue.history()[0].speaker.name(), "System");
assert_eq!(dialogue.history()[0].content, "First message");
assert_eq!(dialogue.history()[1].speaker.name(), "Agent1");
assert_eq!(dialogue.history()[2].speaker.name(), "System");
assert_eq!(dialogue.history()[2].content, "Second message");
assert_eq!(dialogue.history()[3].speaker.name(), "Agent1");
assert_eq!(dialogue.message_store.latest_turn(), 2);
}
#[tokio::test]
async fn test_partial_session_broadcast_multi_agent_with_messages() {
use crate::agent::persona::Persona;
#[derive(Clone)]
struct EchoAgent {
name: String,
}
#[async_trait]
impl Agent for EchoAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Echo agent";
&EXPERTISE
}
fn name(&self) -> String {
self.name.clone()
}
async fn execute(&self, _payload: Payload) -> Result<Self::Output, AgentError> {
Ok(format!("[{}]", self.name))
}
}
let mut dialogue = Dialogue::broadcast();
let persona_a = Persona {
name: "AgentA".to_string(),
role: "TesterA".to_string(),
background: "Test agent A".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona_b = Persona {
name: "AgentB".to_string(),
role: "TesterB".to_string(),
background: "Test agent B".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(
persona_a,
EchoAgent {
name: "AgentA".to_string(),
},
)
.add_participant(
persona_b,
EchoAgent {
name: "AgentB".to_string(),
},
);
let mut session1 = dialogue.partial_session("First text message");
let mut turn1_results = Vec::new();
while let Some(Ok(turn)) = session1.next_turn().await {
turn1_results.push(turn);
}
assert_eq!(turn1_results.len(), 2); assert_eq!(turn1_results[0].content, "[AgentA]");
assert_eq!(turn1_results[1].content, "[AgentB]");
drop(session1);
assert_eq!(dialogue.history().len(), 3);
assert_eq!(dialogue.history()[0].speaker.name(), "System");
assert_eq!(dialogue.history()[0].content, "First text message");
assert_eq!(dialogue.history()[1].speaker.name(), "AgentA");
assert_eq!(dialogue.history()[1].content, "[AgentA]");
assert_eq!(dialogue.history()[2].speaker.name(), "AgentB");
assert_eq!(dialogue.history()[2].content, "[AgentB]");
let payload2 = Payload::from_messages(vec![
PayloadMessage::user("User1", "Human", "User message 1"),
PayloadMessage::system("System alert"),
PayloadMessage::user("User2", "Human", "User message 2"),
]);
let mut session2 = dialogue.partial_session(payload2);
let mut turn2_results = Vec::new();
while let Some(Ok(turn)) = session2.next_turn().await {
turn2_results.push(turn);
}
assert_eq!(turn2_results.len(), 2); drop(session2);
assert_eq!(dialogue.history().len(), 8);
assert_eq!(dialogue.history()[0].speaker.name(), "System");
assert_eq!(dialogue.history()[0].content, "First text message");
assert_eq!(dialogue.history()[1].speaker.name(), "AgentA");
assert_eq!(dialogue.history()[2].speaker.name(), "AgentB");
assert_eq!(dialogue.history()[3].speaker.name(), "User1");
assert_eq!(dialogue.history()[3].content, "User message 1");
assert_eq!(dialogue.history()[4].speaker.name(), "System");
assert_eq!(dialogue.history()[4].content, "System alert");
assert_eq!(dialogue.history()[5].speaker.name(), "User2");
assert_eq!(dialogue.history()[5].content, "User message 2");
assert_eq!(dialogue.history()[6].speaker.name(), "AgentA");
assert_eq!(dialogue.history()[6].content, "[AgentA]");
assert_eq!(dialogue.history()[7].speaker.name(), "AgentB");
assert_eq!(dialogue.history()[7].content, "[AgentB]");
assert_eq!(dialogue.message_store.latest_turn(), 2);
}
#[tokio::test]
async fn test_partial_session_vs_run_multi_turn_equivalence() {
use crate::agent::persona::Persona;
#[derive(Clone)]
struct SimpleAgent {
response: String,
}
#[async_trait]
impl Agent for SimpleAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Simple agent";
&EXPERTISE
}
fn name(&self) -> String {
"SimpleAgent".to_string()
}
async fn execute(&self, _payload: Payload) -> Result<Self::Output, AgentError> {
Ok(self.response.clone())
}
}
let persona = Persona {
name: "Agent".to_string(),
role: "Tester".to_string(),
background: "Test".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let mut dialogue_run = Dialogue::broadcast();
dialogue_run.add_participant(
persona.clone(),
SimpleAgent {
response: "Response".to_string(),
},
);
dialogue_run.run("Turn 1").await.unwrap();
dialogue_run.run("Turn 2").await.unwrap();
let history_run = dialogue_run.history();
let mut dialogue_partial = Dialogue::broadcast();
dialogue_partial.add_participant(
persona,
SimpleAgent {
response: "Response".to_string(),
},
);
let mut session1 = dialogue_partial.partial_session("Turn 1");
while (session1.next_turn().await).is_some() {}
drop(session1);
let mut session2 = dialogue_partial.partial_session("Turn 2");
while (session2.next_turn().await).is_some() {}
drop(session2);
let history_partial = dialogue_partial.history();
assert_eq!(history_run.len(), history_partial.len());
assert_eq!(
dialogue_run.message_store.latest_turn(),
dialogue_partial.message_store.latest_turn()
);
for (i, (run_msg, partial_msg)) in
history_run.iter().zip(history_partial.iter()).enumerate()
{
assert_eq!(
run_msg.speaker.name(),
partial_msg.speaker.name(),
"Speaker mismatch at index {}",
i
);
assert_eq!(
run_msg.content, partial_msg.content,
"Content mismatch at index {}",
i
);
}
}
#[tokio::test]
async fn test_multi_turn_sequential_2_members() {
use crate::agent::persona::Persona;
use tokio::sync::Mutex;
#[derive(Clone)]
struct TrackingAgent {
name: String,
responses: Arc<Mutex<Vec<String>>>,
}
#[async_trait]
impl Agent for TrackingAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Tracking agent";
&EXPERTISE
}
fn name(&self) -> String {
self.name.clone()
}
async fn execute(&self, payload: Payload) -> Result<Self::Output, AgentError> {
let input = payload.to_text();
let response = format!("[{}] received input", self.name);
self.responses.lock().await.push(input.clone());
Ok(response)
}
}
let mut dialogue = Dialogue::sequential();
let persona_a = Persona {
name: "AgentA".to_string(),
role: "First".to_string(),
background: "First agent in chain".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona_b = Persona {
name: "AgentB".to_string(),
role: "Second".to_string(),
background: "Second agent in chain".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_a_responses = Arc::new(Mutex::new(Vec::new()));
let agent_b_responses = Arc::new(Mutex::new(Vec::new()));
dialogue
.add_participant(
persona_a,
TrackingAgent {
name: "AgentA".to_string(),
responses: Arc::clone(&agent_a_responses),
},
)
.add_participant(
persona_b,
TrackingAgent {
name: "AgentB".to_string(),
responses: Arc::clone(&agent_b_responses),
},
);
println!("\n=== Turn 1 ===");
let turns1 = dialogue.run("First message").await.unwrap();
assert_eq!(turns1.len(), 1);
assert_eq!(turns1[0].speaker.name(), "AgentB");
assert_eq!(turns1[0].content, "[AgentB] received input");
let a_inputs_t1 = agent_a_responses.lock().await;
let b_inputs_t1 = agent_b_responses.lock().await;
println!("Turn 1 - AgentA received: {}", a_inputs_t1[0]);
println!("Turn 1 - AgentB received: {}", b_inputs_t1[0]);
assert!(
a_inputs_t1[0].contains("First message"),
"AgentA should see 'First message' in Turn 1. Got: {}",
a_inputs_t1[0]
);
assert!(
b_inputs_t1[0].contains("[AgentA] received input"),
"AgentB should see AgentA's output in Turn 1. Got: {}",
b_inputs_t1[0]
);
drop(a_inputs_t1);
drop(b_inputs_t1);
assert_eq!(dialogue.history().len(), 3);
assert_eq!(dialogue.history()[0].speaker.name(), "System");
assert_eq!(dialogue.history()[0].content, "First message");
assert_eq!(dialogue.history()[1].speaker.name(), "AgentA");
assert_eq!(dialogue.history()[1].content, "[AgentA] received input");
assert_eq!(dialogue.history()[2].speaker.name(), "AgentB");
assert_eq!(dialogue.history()[2].content, "[AgentB] received input");
println!("\n=== Turn 2 ===");
let turns2 = dialogue.run("Second message").await.unwrap();
assert_eq!(turns2.len(), 1);
assert_eq!(turns2[0].speaker.name(), "AgentB");
assert_eq!(turns2[0].content, "[AgentB] received input");
let a_inputs_t2 = agent_a_responses.lock().await;
let b_inputs_t2 = agent_b_responses.lock().await;
println!("Turn 2 - AgentA received: {}", a_inputs_t2[1]);
println!("Turn 2 - AgentB received: {}", b_inputs_t2[1]);
assert!(
a_inputs_t2[1].contains("[AgentA] received input"),
"AgentA should see its own Turn 1 output as context in Turn 2. Got: {}",
a_inputs_t2[1]
);
assert!(
a_inputs_t2[1].contains("[AgentB] received input"),
"AgentA should see AgentB's Turn 1 output as context in Turn 2. Got: {}",
a_inputs_t2[1]
);
assert!(
a_inputs_t2[1].contains("Second message"),
"AgentA should see new message in Turn 2. Got: {}",
a_inputs_t2[1]
);
assert!(
b_inputs_t2[1].contains("[AgentA] received input"),
"AgentB should see AgentA's Turn 2 output. Got: {}",
b_inputs_t2[1]
);
assert!(
b_inputs_t2[1].contains("Second message"),
"AgentB should see new message. Got: {}",
b_inputs_t2[1]
);
assert_eq!(dialogue.history().len(), 6);
assert_eq!(dialogue.history()[3].speaker.name(), "System"); assert_eq!(dialogue.history()[3].content, "Second message");
assert_eq!(dialogue.history()[4].speaker.name(), "AgentA"); assert_eq!(dialogue.history()[5].speaker.name(), "AgentB"); }
#[tokio::test]
async fn test_multi_turn_sequential_session_2_members() {
use crate::agent::persona::Persona;
use tokio::sync::Mutex;
#[derive(Clone)]
struct TrackingAgent {
name: String,
responses: Arc<Mutex<Vec<String>>>,
}
#[async_trait]
impl Agent for TrackingAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Tracking agent";
&EXPERTISE
}
fn name(&self) -> String {
self.name.clone()
}
async fn execute(&self, payload: Payload) -> Result<Self::Output, AgentError> {
let input = payload.to_text();
let response = format!("[{}] received input", self.name);
self.responses.lock().await.push(input);
Ok(response)
}
}
let mut dialogue = Dialogue::sequential();
let persona_a = Persona {
name: "AgentA".to_string(),
role: "First".to_string(),
background: "First agent in chain".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona_b = Persona {
name: "AgentB".to_string(),
role: "Second".to_string(),
background: "Second agent in chain".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_a_responses = Arc::new(Mutex::new(Vec::new()));
let agent_b_responses = Arc::new(Mutex::new(Vec::new()));
dialogue
.add_participant(
persona_a,
TrackingAgent {
name: "AgentA".to_string(),
responses: Arc::clone(&agent_a_responses),
},
)
.add_participant(
persona_b,
TrackingAgent {
name: "AgentB".to_string(),
responses: Arc::clone(&agent_b_responses),
},
);
let mut session1 = dialogue.partial_session("First message");
let mut turns1 = Vec::new();
while let Some(result) = session1.next_turn().await {
turns1.push(result.unwrap());
}
drop(session1);
assert_eq!(turns1.len(), 2);
assert_eq!(turns1[0].speaker.name(), "AgentA");
assert_eq!(turns1[0].content, "[AgentA] received input");
assert_eq!(turns1[1].speaker.name(), "AgentB");
assert_eq!(turns1[1].content, "[AgentB] received input");
let a_inputs_t1 = agent_a_responses.lock().await;
let b_inputs_t1 = agent_b_responses.lock().await;
assert!(
a_inputs_t1[0].contains("First message"),
"AgentA should see 'First message' in Turn 1. Got: {}",
a_inputs_t1[0]
);
assert!(
b_inputs_t1[0].contains("[AgentA] received input"),
"AgentB should see AgentA's output in Turn 1. Got: {}",
b_inputs_t1[0]
);
drop(a_inputs_t1);
drop(b_inputs_t1);
assert_eq!(dialogue.history().len(), 3);
assert_eq!(dialogue.history()[0].speaker.name(), "System");
assert_eq!(dialogue.history()[0].content, "First message");
assert_eq!(dialogue.history()[1].speaker.name(), "AgentA");
assert_eq!(dialogue.history()[1].content, "[AgentA] received input");
assert_eq!(dialogue.history()[2].speaker.name(), "AgentB");
assert_eq!(dialogue.history()[2].content, "[AgentB] received input");
let mut session2 = dialogue.partial_session("Second message");
let mut turns2 = Vec::new();
while let Some(result) = session2.next_turn().await {
turns2.push(result.unwrap());
}
drop(session2);
assert_eq!(turns2.len(), 2);
assert_eq!(turns2[0].speaker.name(), "AgentA");
assert_eq!(turns2[0].content, "[AgentA] received input");
assert_eq!(turns2[1].speaker.name(), "AgentB");
assert_eq!(turns2[1].content, "[AgentB] received input");
let a_inputs_t2 = agent_a_responses.lock().await;
let b_inputs_t2 = agent_b_responses.lock().await;
assert!(
a_inputs_t2[1].contains("[AgentA] received input"),
"AgentA should see its own Turn 1 output as context in Turn 2. Got: {}",
a_inputs_t2[1]
);
assert!(
a_inputs_t2[1].contains("[AgentB] received input"),
"AgentA should see AgentB's Turn 1 output as context in Turn 2. Got: {}",
a_inputs_t2[1]
);
assert!(
a_inputs_t2[1].contains("Second message"),
"AgentA should see new message in Turn 2. Got: {}",
a_inputs_t2[1]
);
assert!(
b_inputs_t2[1].contains("[AgentA] received input"),
"AgentB should see AgentA's Turn 2 output. Got: {}",
b_inputs_t2[1]
);
assert!(
b_inputs_t2[1].contains("Second message"),
"AgentB should see new message. Got: {}",
b_inputs_t2[1]
);
drop(a_inputs_t2);
drop(b_inputs_t2);
assert_eq!(dialogue.history().len(), 6);
assert_eq!(dialogue.history()[3].speaker.name(), "System");
assert_eq!(dialogue.history()[3].content, "Second message");
assert_eq!(dialogue.history()[4].speaker.name(), "AgentA");
assert_eq!(dialogue.history()[5].speaker.name(), "AgentB");
}
#[tokio::test]
async fn test_multi_turn_sequential_3_members() {
use crate::agent::persona::Persona;
use tokio::sync::Mutex;
#[derive(Clone)]
struct TrackingAgent {
name: String,
responses: Arc<Mutex<Vec<String>>>,
}
#[async_trait]
impl Agent for TrackingAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Tracking agent";
&EXPERTISE
}
fn name(&self) -> String {
self.name.clone()
}
async fn execute(&self, payload: Payload) -> Result<Self::Output, AgentError> {
let input = payload.to_text();
let response = format!("[{}] processed", self.name);
self.responses.lock().await.push(input.clone());
Ok(response)
}
}
let mut dialogue = Dialogue::sequential();
let persona_a = Persona {
name: "AgentA".to_string(),
role: "First".to_string(),
background: "First agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona_b = Persona {
name: "AgentB".to_string(),
role: "Second".to_string(),
background: "Second agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona_c = Persona {
name: "AgentC".to_string(),
role: "Third".to_string(),
background: "Third agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_a_responses = Arc::new(Mutex::new(Vec::new()));
let agent_b_responses = Arc::new(Mutex::new(Vec::new()));
let agent_c_responses = Arc::new(Mutex::new(Vec::new()));
dialogue
.add_participant(
persona_a,
TrackingAgent {
name: "AgentA".to_string(),
responses: Arc::clone(&agent_a_responses),
},
)
.add_participant(
persona_b,
TrackingAgent {
name: "AgentB".to_string(),
responses: Arc::clone(&agent_b_responses),
},
)
.add_participant(
persona_c,
TrackingAgent {
name: "AgentC".to_string(),
responses: Arc::clone(&agent_c_responses),
},
);
println!("\n=== Turn 1 (3 members) ===");
let turns1 = dialogue.run("First message").await.unwrap();
assert_eq!(turns1.len(), 1);
assert_eq!(turns1[0].speaker.name(), "AgentC");
let a_inputs_t1 = agent_a_responses.lock().await;
let b_inputs_t1 = agent_b_responses.lock().await;
let c_inputs_t1 = agent_c_responses.lock().await;
assert!(a_inputs_t1[0].contains("First message"));
assert!(b_inputs_t1[0].contains("[AgentA] processed"));
assert!(c_inputs_t1[0].contains("[AgentB] processed"));
drop(a_inputs_t1);
drop(b_inputs_t1);
drop(c_inputs_t1);
assert_eq!(dialogue.history().len(), 4);
println!("\n=== Turn 2 (3 members) ===");
let turns2 = dialogue.run("Second message").await.unwrap();
assert_eq!(turns2.len(), 1);
assert_eq!(turns2[0].speaker.name(), "AgentC");
let a_inputs_t2 = agent_a_responses.lock().await;
let b_inputs_t2 = agent_b_responses.lock().await;
let c_inputs_t2 = agent_c_responses.lock().await;
println!("Turn 2 - AgentA received: {}", a_inputs_t2[1]);
println!("Turn 2 - AgentB received: {}", b_inputs_t2[1]);
println!("Turn 2 - AgentC received: {}", c_inputs_t2[1]);
assert!(
a_inputs_t2[1].contains("[AgentA] processed"),
"AgentA should see its own Turn 1 output. Got: {}",
a_inputs_t2[1]
);
assert!(
a_inputs_t2[1].contains("[AgentB] processed"),
"AgentA should see AgentB's Turn 1 output. Got: {}",
a_inputs_t2[1]
);
assert!(
a_inputs_t2[1].contains("[AgentC] processed"),
"AgentA should see AgentC's Turn 1 output. Got: {}",
a_inputs_t2[1]
);
assert!(
a_inputs_t2[1].contains("Second message"),
"AgentA should see new message. Got: {}",
a_inputs_t2[1]
);
assert!(
b_inputs_t2[1].contains("[AgentA] processed"),
"AgentB should see AgentA's Turn 2 output. Got: {}",
b_inputs_t2[1]
);
assert!(
b_inputs_t2[1].contains("Second message"),
"AgentB should see new message. Got: {}",
b_inputs_t2[1]
);
assert!(
c_inputs_t2[1].contains("[AgentA] processed"),
"AgentC should see AgentA's Turn 2 output. Got: {}",
c_inputs_t2[1]
);
assert!(
c_inputs_t2[1].contains("[AgentB] processed"),
"AgentC should see AgentB's Turn 2 output. Got: {}",
c_inputs_t2[1]
);
assert!(
c_inputs_t2[1].contains("Second message"),
"AgentC should see new message. Got: {}",
c_inputs_t2[1]
);
assert_eq!(dialogue.history().len(), 8);
assert_eq!(dialogue.history()[4].speaker.name(), "System"); assert_eq!(dialogue.history()[5].speaker.name(), "AgentA"); assert_eq!(dialogue.history()[6].speaker.name(), "AgentB"); assert_eq!(dialogue.history()[7].speaker.name(), "AgentC"); }
#[tokio::test]
async fn test_multi_turn_sequential_session_3_members() {
use crate::agent::persona::Persona;
use tokio::sync::Mutex;
#[derive(Clone)]
struct TrackingAgent {
name: String,
responses: Arc<Mutex<Vec<String>>>,
}
#[async_trait]
impl Agent for TrackingAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Tracking agent";
&EXPERTISE
}
fn name(&self) -> String {
self.name.clone()
}
async fn execute(&self, payload: Payload) -> Result<Self::Output, AgentError> {
let input = payload.to_text();
let response = format!("[{}] processed", self.name);
self.responses.lock().await.push(input);
Ok(response)
}
}
let mut dialogue = Dialogue::sequential();
let persona_a = Persona {
name: "AgentA".to_string(),
role: "First".to_string(),
background: "First agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona_b = Persona {
name: "AgentB".to_string(),
role: "Second".to_string(),
background: "Second agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona_c = Persona {
name: "AgentC".to_string(),
role: "Third".to_string(),
background: "Third agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_a_responses = Arc::new(Mutex::new(Vec::new()));
let agent_b_responses = Arc::new(Mutex::new(Vec::new()));
let agent_c_responses = Arc::new(Mutex::new(Vec::new()));
dialogue
.add_participant(
persona_a,
TrackingAgent {
name: "AgentA".to_string(),
responses: Arc::clone(&agent_a_responses),
},
)
.add_participant(
persona_b,
TrackingAgent {
name: "AgentB".to_string(),
responses: Arc::clone(&agent_b_responses),
},
)
.add_participant(
persona_c,
TrackingAgent {
name: "AgentC".to_string(),
responses: Arc::clone(&agent_c_responses),
},
);
let mut session1 = dialogue.partial_session("First message");
let mut turns1 = Vec::new();
while let Some(result) = session1.next_turn().await {
turns1.push(result.unwrap());
}
drop(session1);
assert_eq!(turns1.len(), 3);
assert_eq!(turns1[0].speaker.name(), "AgentA");
assert_eq!(turns1[0].content, "[AgentA] processed");
assert_eq!(turns1[1].speaker.name(), "AgentB");
assert_eq!(turns1[1].content, "[AgentB] processed");
assert_eq!(turns1[2].speaker.name(), "AgentC");
assert_eq!(turns1[2].content, "[AgentC] processed");
let a_inputs_t1 = agent_a_responses.lock().await;
let b_inputs_t1 = agent_b_responses.lock().await;
let c_inputs_t1 = agent_c_responses.lock().await;
assert!(
a_inputs_t1[0].contains("First message"),
"AgentA should see 'First message' in Turn 1. Got: {}",
a_inputs_t1[0]
);
assert!(
b_inputs_t1[0].contains("[AgentA] processed"),
"AgentB should see AgentA's Turn 1 output. Got: {}",
b_inputs_t1[0]
);
assert!(
c_inputs_t1[0].contains("[AgentB] processed"),
"AgentC should see AgentB's Turn 1 output. Got: {}",
c_inputs_t1[0]
);
drop(a_inputs_t1);
drop(b_inputs_t1);
drop(c_inputs_t1);
assert_eq!(dialogue.history().len(), 4);
assert_eq!(dialogue.history()[0].speaker.name(), "System");
assert_eq!(dialogue.history()[0].content, "First message");
assert_eq!(dialogue.history()[1].speaker.name(), "AgentA");
assert_eq!(dialogue.history()[2].speaker.name(), "AgentB");
assert_eq!(dialogue.history()[3].speaker.name(), "AgentC");
let mut session2 = dialogue.partial_session("Second message");
let mut turns2 = Vec::new();
while let Some(result) = session2.next_turn().await {
turns2.push(result.unwrap());
}
drop(session2);
assert_eq!(turns2.len(), 3);
assert_eq!(turns2[0].speaker.name(), "AgentA");
assert_eq!(turns2[0].content, "[AgentA] processed");
assert_eq!(turns2[1].speaker.name(), "AgentB");
assert_eq!(turns2[1].content, "[AgentB] processed");
assert_eq!(turns2[2].speaker.name(), "AgentC");
assert_eq!(turns2[2].content, "[AgentC] processed");
let a_inputs_t2 = agent_a_responses.lock().await;
let b_inputs_t2 = agent_b_responses.lock().await;
let c_inputs_t2 = agent_c_responses.lock().await;
assert!(
a_inputs_t2[1].contains("[AgentA] processed"),
"AgentA should see its own Turn 1 output. Got: {}",
a_inputs_t2[1]
);
assert!(
a_inputs_t2[1].contains("[AgentB] processed"),
"AgentA should see AgentB's Turn 1 output. Got: {}",
a_inputs_t2[1]
);
assert!(
a_inputs_t2[1].contains("[AgentC] processed"),
"AgentA should see AgentC's Turn 1 output. Got: {}",
a_inputs_t2[1]
);
assert!(
a_inputs_t2[1].contains("Second message"),
"AgentA should see new message. Got: {}",
a_inputs_t2[1]
);
assert!(
b_inputs_t2[1].contains("[AgentA] processed"),
"AgentB should see AgentA's Turn 2 output. Got: {}",
b_inputs_t2[1]
);
assert!(
b_inputs_t2[1].contains("Second message"),
"AgentB should see new message. Got: {}",
b_inputs_t2[1]
);
assert!(
c_inputs_t2[1].contains("[AgentA] processed"),
"AgentC should see AgentA's Turn 2 output. Got: {}",
c_inputs_t2[1]
);
assert!(
c_inputs_t2[1].contains("[AgentB] processed"),
"AgentC should see AgentB's Turn 2 output. Got: {}",
c_inputs_t2[1]
);
assert!(
c_inputs_t2[1].contains("Second message"),
"AgentC should see new message. Got: {}",
c_inputs_t2[1]
);
drop(a_inputs_t2);
drop(b_inputs_t2);
drop(c_inputs_t2);
assert_eq!(dialogue.history().len(), 8);
assert_eq!(dialogue.history()[4].speaker.name(), "System");
assert_eq!(dialogue.history()[4].content, "Second message");
assert_eq!(dialogue.history()[5].speaker.name(), "AgentA");
assert_eq!(dialogue.history()[6].speaker.name(), "AgentB");
assert_eq!(dialogue.history()[7].speaker.name(), "AgentC");
}
#[tokio::test]
async fn test_partial_session_sequential_applies_context() {
use crate::agent::persona::Persona;
use tokio::sync::Mutex;
#[derive(Clone)]
struct TrackingAgent {
name: String,
payloads: Arc<Mutex<Vec<String>>>,
}
#[async_trait]
impl Agent for TrackingAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Context tracking agent";
&EXPERTISE
}
fn name(&self) -> String {
self.name.clone()
}
async fn execute(&self, payload: Payload) -> Result<Self::Output, AgentError> {
let mut full_text = String::new();
for msg in payload.to_messages() {
full_text.push_str(&msg.content);
full_text.push('\n');
}
let text = payload.to_text();
if !text.is_empty() {
full_text.push_str(&text);
}
self.payloads.lock().await.push(full_text);
Ok(format!("[{}] ok", self.name))
}
}
let persona_a = Persona {
name: "AgentA".to_string(),
role: "First".to_string(),
background: "First".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona_b = Persona {
name: "AgentB".to_string(),
role: "Second".to_string(),
background: "Second".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let payloads = Arc::new(Mutex::new(Vec::new()));
let mut dialogue = Dialogue::sequential();
dialogue
.with_talk_style(TalkStyle::Brainstorm)
.add_participant(
persona_a,
TrackingAgent {
name: "AgentA".to_string(),
payloads: Arc::clone(&payloads),
},
)
.add_participant(
persona_b,
TrackingAgent {
name: "AgentB".to_string(),
payloads: Arc::clone(&payloads),
},
);
let mut session = dialogue.partial_session("Kickoff");
while let Some(result) = session.next_turn().await {
result.unwrap();
}
drop(session);
let payloads = payloads.lock().await;
assert!(
payloads
.iter()
.all(|text| text.contains("Brainstorming Session")),
"Each participant should receive brainstorming context. Payloads: {:?}",
*payloads
);
}
#[tokio::test]
async fn test_sequential_explicit_order_respected() {
use crate::agent::persona::Persona;
use tokio::sync::Mutex;
#[derive(Clone)]
struct RecordingAgent {
name: String,
log: Arc<Mutex<Vec<String>>>,
}
#[async_trait]
impl Agent for RecordingAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Order recording agent";
&EXPERTISE
}
async fn execute(&self, payload: Payload) -> Result<Self::Output, AgentError> {
let _ = payload;
self.log.lock().await.push(self.name.clone());
Ok(format!("[{}] done", self.name))
}
}
let personas = [
Persona {
name: "AgentA".to_string(),
role: "First".to_string(),
background: "First".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
},
Persona {
name: "AgentB".to_string(),
role: "Second".to_string(),
background: "Second".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
},
Persona {
name: "AgentC".to_string(),
role: "Third".to_string(),
background: "Third".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
},
];
let log = Arc::new(Mutex::new(Vec::new()));
let mut dialogue = Dialogue::sequential_with_order(SequentialOrder::Explicit(vec![
"AgentB".to_string(),
"AgentA".to_string(),
]));
for persona in personas {
let persona_name = persona.name.clone();
dialogue.add_participant(
persona,
RecordingAgent {
name: persona_name,
log: Arc::clone(&log),
},
);
}
dialogue.run("Start".to_string()).await.unwrap();
let order = log.lock().await.clone();
assert_eq!(
order,
vec![
"AgentB".to_string(),
"AgentA".to_string(),
"AgentC".to_string()
],
"Explicit order should run AgentB, then AgentA, then remaining AgentC"
);
}
#[tokio::test]
async fn test_partial_session_sequential_order_error_propagates() {
use crate::agent::persona::Persona;
#[derive(Clone)]
struct PassthroughAgent;
#[async_trait]
impl Agent for PassthroughAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Passthrough";
&EXPERTISE
}
async fn execute(&self, payload: Payload) -> Result<Self::Output, AgentError> {
Ok(payload.to_text())
}
}
let persona = Persona {
name: "AgentA".to_string(),
role: "Only".to_string(),
background: "Only".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let mut dialogue =
Dialogue::sequential_with_order(SequentialOrder::Explicit(vec!["Ghost".to_string()]));
dialogue.add_participant(persona, PassthroughAgent);
let mut session = dialogue.partial_session("Hello world");
match session.next_turn().await {
Some(Err(AgentError::ExecutionFailed(message))) => {
assert!(
message.contains("Ghost"),
"Error message should mention missing participant. Got {message}"
);
}
other => panic!(
"Expected ExecutionFailed error from invalid sequential order, got {:?}",
other
),
}
assert!(
session.next_turn().await.is_none(),
"Session should complete after propagating the error"
);
}
#[tokio::test]
async fn test_dialogue_context_brainstorm() {
use crate::agent::persona::Persona;
use tokio::sync::Mutex;
#[derive(Clone)]
struct TrackingAgent {
name: String,
received_payloads: Arc<Mutex<Vec<String>>>,
}
#[async_trait]
impl Agent for TrackingAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Tracking agent";
&EXPERTISE
}
fn name(&self) -> String {
self.name.clone()
}
async fn execute(&self, payload: Payload) -> Result<Self::Output, AgentError> {
let payload_text = payload.to_text();
self.received_payloads
.lock()
.await
.push(payload_text.clone());
Ok(format!("[{}] responded", self.name))
}
}
let mut dialogue = Dialogue::broadcast();
let persona = Persona {
name: "Agent1".to_string(),
role: "Participant".to_string(),
background: "Test agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let received_payloads = Arc::new(Mutex::new(Vec::new()));
dialogue
.with_talk_style(TalkStyle::Brainstorm)
.add_participant(
persona,
TrackingAgent {
name: "Agent1".to_string(),
received_payloads: Arc::clone(&received_payloads),
},
);
dialogue.run("Let's generate some ideas").await.unwrap();
let payloads = received_payloads.lock().await;
assert_eq!(payloads.len(), 1, "Should have received one payload");
let payload_text = &payloads[0];
assert!(
payload_text.contains("Brainstorming Session"),
"Payload should contain context title. Got: {}",
payload_text
);
assert!(
payload_text.contains("Encourage wild ideas"),
"Payload should contain context guidelines. Got: {}",
payload_text
);
}
#[test]
#[allow(deprecated)]
fn test_extract_mentions() {
let text = "@Alice what do you think?";
let participants = vec!["Alice", "Bob", "Charlie"];
let mentions = extract_mentions(text, &participants);
assert_eq!(mentions.len(), 1);
assert!(mentions.contains(&"Alice"));
let text = "@Alice @Bob please discuss this";
let mentions = extract_mentions(text, &participants);
assert_eq!(mentions.len(), 2);
assert!(mentions.contains(&"Alice"));
assert!(mentions.contains(&"Bob"));
let text = "@Alice what do you think? @Alice?";
let mentions = extract_mentions(text, &participants);
assert_eq!(mentions.len(), 1);
assert!(mentions.contains(&"Alice"));
let text = "What does everyone think?";
let mentions = extract_mentions(text, &participants);
assert_eq!(mentions.len(), 0);
let text = "@David @Alice what do you think?";
let mentions = extract_mentions(text, &participants);
assert_eq!(mentions.len(), 1);
assert!(mentions.contains(&"Alice"));
assert!(!mentions.contains(&"David"));
let participants = vec!["Alice", "Ali"];
let text = "@Ali what do you think?";
let mentions = extract_mentions(text, &participants);
assert_eq!(mentions.len(), 1);
assert!(mentions.contains(&"Ali"));
assert!(!mentions.contains(&"Alice"));
}
#[test]
fn test_extract_mentions_with_strategy_exact_word() {
let participants = vec!["Alice", "Bob", "Ayaka Nakamura"];
let text = "@Alice @Bob what do you think?";
let mentions =
extract_mentions_with_strategy(text, &participants, MentionMatchStrategy::ExactWord);
assert_eq!(mentions.len(), 2);
assert!(mentions.contains(&"Alice"));
assert!(mentions.contains(&"Bob"));
let text = "@Ayaka @Nakamura please review";
let mentions =
extract_mentions_with_strategy(text, &participants, MentionMatchStrategy::ExactWord);
assert_eq!(
mentions.len(),
0,
"ExactWord should not match partial words of space-containing names"
);
let participants_jp = vec!["太郎", "花子", "Alice"];
let text = "@太郎 @花子 please discuss";
let mentions =
extract_mentions_with_strategy(text, &participants_jp, MentionMatchStrategy::ExactWord);
assert_eq!(mentions.len(), 2);
assert!(mentions.contains(&"太郎"));
assert!(mentions.contains(&"花子"));
}
#[test]
fn test_extract_mentions_with_strategy_name() {
let participants = vec!["Alice", "Bob", "Ayaka Nakamura", "John Smith"];
let text = "@Ayaka Nakamura please review this";
let mentions =
extract_mentions_with_strategy(text, &participants, MentionMatchStrategy::Name);
assert_eq!(mentions.len(), 1);
assert!(mentions.contains(&"Ayaka Nakamura"));
let text = "@Ayaka Nakamura and @John Smith, please discuss";
let mentions =
extract_mentions_with_strategy(text, &participants, MentionMatchStrategy::Name);
assert_eq!(mentions.len(), 2);
assert!(mentions.contains(&"Ayaka Nakamura"));
assert!(mentions.contains(&"John Smith"));
let text = "@Alice @Bob what do you think?";
let mentions =
extract_mentions_with_strategy(text, &participants, MentionMatchStrategy::Name);
assert_eq!(mentions.len(), 2);
assert!(mentions.contains(&"Alice"));
assert!(mentions.contains(&"Bob"));
let text = "@Ayaka please review";
let mentions =
extract_mentions_with_strategy(text, &participants, MentionMatchStrategy::Name);
assert_eq!(
mentions.len(),
0,
"Name strategy requires exact full name match"
);
let participants_with_overlap = vec!["Ayaka", "Ayaka Nakamura", "Bob"];
let text = "@Ayaka Nakamura please review";
let mentions = extract_mentions_with_strategy(
text,
&participants_with_overlap,
MentionMatchStrategy::Name,
);
assert_eq!(mentions.len(), 1, "Should only match full name");
assert!(
mentions.contains(&"Ayaka Nakamura"),
"Should match 'Ayaka Nakamura', not 'Ayaka'"
);
assert!(
!mentions.contains(&"Ayaka"),
"'Ayaka' should not match in '@Ayaka Nakamura'"
);
let text = "@Ayaka what do you think?";
let mentions = extract_mentions_with_strategy(
text,
&participants_with_overlap,
MentionMatchStrategy::Name,
);
assert_eq!(mentions.len(), 1);
assert!(mentions.contains(&"Ayaka"));
}
#[test]
fn test_extract_mentions_with_strategy_partial() {
let participants = vec!["Alice", "Ayaka Nakamura", "Ayaka Tanaka", "Bob"];
let text = "@Ayaka please review";
let mentions =
extract_mentions_with_strategy(text, &participants, MentionMatchStrategy::Partial);
assert_eq!(mentions.len(), 1);
assert!(mentions.iter().any(|&name| name.starts_with("Ayaka")));
let text = "@Alice what do you think?";
let mentions =
extract_mentions_with_strategy(text, &participants, MentionMatchStrategy::Partial);
assert_eq!(mentions.len(), 1);
assert!(mentions.contains(&"Alice"));
let text = "@Ayaka and @Bob, please discuss";
let mentions =
extract_mentions_with_strategy(text, &participants, MentionMatchStrategy::Partial);
assert_eq!(mentions.len(), 2);
assert!(mentions.iter().any(|&name| name.starts_with("Ayaka")));
assert!(mentions.contains(&"Bob"));
let text = "@Charlie what do you think?";
let mentions =
extract_mentions_with_strategy(text, &participants, MentionMatchStrategy::Partial);
assert_eq!(mentions.len(), 0);
}
#[test]
fn test_extract_mentions_japanese_names() {
let participants = vec!["あやか なかむら", "太郎 山田", "Alice"];
let text = "@あやか なかむら さん、お願いします";
let mentions =
extract_mentions_with_strategy(text, &participants, MentionMatchStrategy::Name);
assert_eq!(mentions.len(), 1);
assert!(mentions.contains(&"あやか なかむら"));
let text = "@Alice and @太郎 山田, please review";
let mentions =
extract_mentions_with_strategy(text, &participants, MentionMatchStrategy::Name);
assert_eq!(mentions.len(), 2);
assert!(mentions.contains(&"Alice"));
assert!(mentions.contains(&"太郎 山田"));
}
#[tokio::test]
async fn test_participants_method() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::broadcast();
let persona1 = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Backend engineer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UI/UX specialist".to_string(),
communication_style: "Visual".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(persona1, MockAgent::new("Alice", vec!["Hi".to_string()]))
.add_participant(persona2, MockAgent::new("Bob", vec!["Hello".to_string()]));
let participants = dialogue.participant_names();
assert_eq!(participants.len(), 2);
assert!(participants.contains(&"Alice"));
assert!(participants.contains(&"Bob"));
}
#[tokio::test]
async fn test_mentioned_mode_with_mentions() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::mentioned();
let persona1 = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Backend engineer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UI/UX specialist".to_string(),
communication_style: "Visual".to_string(),
visual_identity: None,
capabilities: None,
};
let persona3 = Persona {
name: "Charlie".to_string(),
role: "Tester".to_string(),
background: "QA engineer".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(
persona1,
MockAgent::new("Alice", vec!["Alice's response".to_string()]),
)
.add_participant(
persona2,
MockAgent::new("Bob", vec!["Bob's response".to_string()]),
)
.add_participant(
persona3,
MockAgent::new("Charlie", vec!["Charlie's response".to_string()]),
);
let turns = dialogue
.run("@Alice @Bob what do you think about this feature?")
.await
.unwrap();
assert_eq!(turns.len(), 2);
let responders: Vec<&str> = turns.iter().map(|t| t.speaker.name()).collect();
assert!(responders.contains(&"Alice"));
assert!(responders.contains(&"Bob"));
assert!(!responders.contains(&"Charlie"));
}
#[tokio::test]
async fn test_mentioned_mode_fallback_to_broadcast() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::mentioned();
let persona1 = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Backend engineer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UI/UX specialist".to_string(),
communication_style: "Visual".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(
persona1,
MockAgent::new("Alice", vec!["Alice's response".to_string()]),
)
.add_participant(
persona2,
MockAgent::new("Bob", vec!["Bob's response".to_string()]),
);
let turns = dialogue
.run("What does everyone think about this?")
.await
.unwrap();
assert_eq!(turns.len(), 2);
let responders: Vec<&str> = turns.iter().map(|t| t.speaker.name()).collect();
assert!(responders.contains(&"Alice"));
assert!(responders.contains(&"Bob"));
}
#[tokio::test]
async fn test_mentioned_mode_single_mention() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::mentioned();
let persona1 = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Backend engineer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UI/UX specialist".to_string(),
communication_style: "Visual".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(
persona1,
MockAgent::new("Alice", vec!["Alice's response".to_string()]),
)
.add_participant(
persona2,
MockAgent::new("Bob", vec!["Bob's response".to_string()]),
);
let turns = dialogue.run("@Alice can you help?").await.unwrap();
assert_eq!(turns.len(), 1);
assert_eq!(turns[0].speaker.name(), "Alice");
assert_eq!(turns[0].content, "Alice's response");
}
#[tokio::test]
async fn test_mentioned_mode_multi_turn_context_propagation() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::mentioned();
let persona1 = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Backend engineer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UI/UX specialist".to_string(),
communication_style: "Visual".to_string(),
visual_identity: None,
capabilities: None,
};
let persona3 = Persona {
name: "Charlie".to_string(),
role: "Tester".to_string(),
background: "QA engineer".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(
persona1,
MockAgent::new(
"Alice",
vec![
"Alice: Turn 1 response".to_string(),
"Alice: Turn 2 response".to_string(),
],
),
)
.add_participant(
persona2,
MockAgent::new(
"Bob",
vec![
"Bob: Turn 1 response".to_string(),
"Bob: Turn 2 response".to_string(),
],
),
)
.add_participant(
persona3,
MockAgent::new("Charlie", vec!["Charlie: Turn 2 response".to_string()]),
);
let turn1 = dialogue
.run("@Alice @Bob what's your initial thoughts?")
.await
.unwrap();
assert_eq!(turn1.len(), 2);
let turn1_responders: Vec<&str> = turn1.iter().map(|t| t.speaker.name()).collect();
assert!(turn1_responders.contains(&"Alice"));
assert!(turn1_responders.contains(&"Bob"));
let turn2 = dialogue
.run("@Charlie what do you think about their responses?")
.await
.unwrap();
assert_eq!(turn2.len(), 1);
assert_eq!(turn2[0].speaker.name(), "Charlie");
let history = dialogue.history();
assert_eq!(history.len(), 5);
assert_eq!(history[0].speaker.name(), "System");
let turn1_names: Vec<&str> = vec![history[1].speaker.name(), history[2].speaker.name()];
assert!(turn1_names.contains(&"Alice"));
assert!(turn1_names.contains(&"Bob"));
assert_eq!(history[3].speaker.name(), "System"); assert_eq!(history[4].speaker.name(), "Charlie"); }
#[tokio::test]
async fn test_mentioned_mode_partial_session() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::mentioned();
let persona1 = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Backend engineer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UI/UX specialist".to_string(),
communication_style: "Visual".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(
persona1,
MockAgent::new("Alice", vec!["Alice's response".to_string()]),
)
.add_participant(
persona2,
MockAgent::new("Bob", vec!["Bob's response".to_string()]),
);
let mut session = dialogue.partial_session("@Alice what do you think?");
let mut turns = Vec::new();
while let Some(result) = session.next_turn().await {
turns.push(result.unwrap());
}
assert_eq!(turns.len(), 1);
assert_eq!(turns[0].speaker.name(), "Alice");
assert_eq!(turns[0].content, "Alice's response");
}
#[tokio::test]
async fn test_mentioned_mode_three_members_progressive() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::mentioned();
let persona1 = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Backend engineer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UI/UX specialist".to_string(),
communication_style: "Visual".to_string(),
visual_identity: None,
capabilities: None,
};
let persona3 = Persona {
name: "Charlie".to_string(),
role: "Tester".to_string(),
background: "QA engineer".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(
persona1,
MockAgent::new("Alice", vec!["Alice response".to_string()]),
)
.add_participant(
persona2,
MockAgent::new("Bob", vec!["Bob response".to_string()]),
)
.add_participant(
persona3,
MockAgent::new("Charlie", vec!["Charlie response".to_string()]),
);
let turns = dialogue
.run("@Alice @Bob @Charlie everyone needs to respond")
.await
.unwrap();
assert_eq!(turns.len(), 3);
let responders: Vec<&str> = turns.iter().map(|t| t.speaker.name()).collect();
assert!(responders.contains(&"Alice"));
assert!(responders.contains(&"Bob"));
assert!(responders.contains(&"Charlie"));
}
#[tokio::test]
async fn test_mentioned_mode_receives_other_participants_context() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::mentioned();
let persona1 = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Backend engineer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UI/UX specialist".to_string(),
communication_style: "Visual".to_string(),
visual_identity: None,
capabilities: None,
};
let persona3 = Persona {
name: "Charlie".to_string(),
role: "Tester".to_string(),
background: "QA engineer".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(
persona1,
MockAgent::new(
"Alice",
vec![
"Alice: Turn 1".to_string(),
"Alice: Turn 2 after seeing Bob".to_string(),
],
),
)
.add_participant(
persona2,
MockAgent::new("Bob", vec!["Bob: Turn 1".to_string()]),
)
.add_participant(
persona3,
MockAgent::new(
"Charlie",
vec!["Charlie: Turn 2 after seeing Alice and Bob".to_string()],
),
);
let turn1 = dialogue
.run("@Alice @Bob initial discussion")
.await
.unwrap();
assert_eq!(turn1.len(), 2);
let turn2 = dialogue.run("@Charlie your thoughts?").await.unwrap();
assert_eq!(turn2.len(), 1);
assert_eq!(turn2[0].speaker.name(), "Charlie");
let history = dialogue.history();
assert_eq!(history.len(), 5);
assert_eq!(history[0].speaker.name(), "System");
let turn1_speakers: Vec<&str> = vec![history[1].speaker.name(), history[2].speaker.name()];
assert!(turn1_speakers.contains(&"Alice"));
assert!(turn1_speakers.contains(&"Bob"));
assert_eq!(history[3].speaker.name(), "System");
assert_eq!(history[4].speaker.name(), "Charlie");
}
#[tokio::test]
async fn test_payload_with_both_messages_and_text() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::broadcast();
let persona = Persona {
name: "TestAgent".to_string(),
role: "Tester".to_string(),
background: "Test agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue.add_participant(
persona,
MockAgent::new("TestAgent", vec!["Response".to_string()]),
);
let payload = Payload::from_messages(vec![
PayloadMessage::user("Alice", "Product Manager", "What should we do?"),
PayloadMessage::system("Context: This is a test scenario"),
])
.merge(Payload::text("Additional text content"));
dialogue.run(payload).await.unwrap();
let history = dialogue.history();
assert_eq!(history.len(), 4);
assert!(matches!(history[0].speaker, Speaker::User { .. }));
assert_eq!(history[0].speaker.name(), "Alice");
assert_eq!(history[0].content, "What should we do?");
assert!(matches!(history[1].speaker, Speaker::System));
assert_eq!(history[1].content, "Context: This is a test scenario");
assert!(matches!(history[2].speaker, Speaker::System));
assert_eq!(history[2].content, "Additional text content");
assert!(matches!(history[3].speaker, Speaker::Agent { .. }));
assert_eq!(history[3].speaker.name(), "TestAgent");
}
#[tokio::test]
async fn test_mentioned_mode_includes_previous_agent_outputs_in_mention_extraction() {
use crate::agent::persona::Persona;
let mut dialogue = Dialogue::mentioned();
let alice_persona = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Backend engineer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
let bob_persona = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UI/UX specialist".to_string(),
communication_style: "Visual".to_string(),
visual_identity: None,
capabilities: None,
};
let charlie_persona = Persona {
name: "Charlie".to_string(),
role: "Tester".to_string(),
background: "QA engineer".to_string(),
communication_style: "Detail-oriented".to_string(),
visual_identity: None,
capabilities: None,
};
dialogue
.add_participant(
alice_persona,
MockAgent::new(
"Alice",
vec!["I think we should use @Bob's design".to_string()],
),
)
.add_participant(
bob_persona,
MockAgent::new("Bob", vec!["Thanks Alice!".to_string()]),
)
.add_participant(
charlie_persona,
MockAgent::new("Charlie", vec!["I agree".to_string()]),
);
let turn1 = dialogue.run("@Alice what do you think?").await.unwrap();
assert_eq!(turn1.len(), 1);
assert_eq!(turn1[0].speaker.name(), "Alice");
assert_eq!(turn1[0].content, "I think we should use @Bob's design");
let turn2 = dialogue.run("Continue the discussion").await.unwrap();
assert_eq!(turn2.len(), 1);
assert_eq!(turn2[0].speaker.name(), "Bob");
assert_eq!(turn2[0].content, "Thanks Alice!");
let history = dialogue.history();
assert_eq!(history.len(), 4);
assert_eq!(history[0].speaker.name(), "System");
assert_eq!(history[1].speaker.name(), "Alice");
assert_eq!(history[2].speaker.name(), "System");
assert_eq!(history[3].speaker.name(), "Bob");
}
#[derive(Clone)]
struct RecordingAgent {
name: String,
response: String,
received_payloads: std::sync::Arc<std::sync::Mutex<Vec<Payload>>>,
}
impl RecordingAgent {
fn new(name: impl Into<String>, response: impl Into<String>) -> Self {
Self {
name: name.into(),
response: response.into(),
received_payloads: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
}
}
fn get_received_payloads(&self) -> Vec<Payload> {
self.received_payloads.lock().unwrap().clone()
}
}
#[async_trait]
impl Agent for RecordingAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Recording agent for testing";
&EXPERTISE
}
fn name(&self) -> String {
self.name.clone()
}
async fn execute(&self, payload: Payload) -> Result<Self::Output, AgentError> {
self.received_payloads.lock().unwrap().push(payload);
Ok(self.response.clone())
}
}
#[tokio::test]
async fn test_reaction_strategy_broadcast_context_info() {
use crate::agent::dialogue::message::{MessageMetadata, MessageType};
use crate::agent::persona::Persona;
let persona = Persona {
name: "Agent1".to_string(),
role: "Assistant".to_string(),
background: "Test agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let agent = RecordingAgent::new("Agent1", "I can help with that");
let mut dialogue = Dialogue::broadcast();
dialogue.add_participant(persona, agent.clone());
let context_payload = Payload::new().add_message_with_metadata(
Speaker::System,
"Analysis completed: 3 issues found",
MessageMetadata::new().with_type(MessageType::ContextInfo),
);
let turns = dialogue.run(context_payload).await.unwrap();
assert_eq!(turns.len(), 0, "ContextInfo should not trigger reaction");
let history = dialogue.history();
assert_eq!(history.len(), 1);
assert_eq!(history[0].content, "Analysis completed: 3 issues found");
let user_payload = Payload::from_messages(vec![PayloadMessage::new(
Speaker::user("Alice", "User"),
"Tell me more about the issues",
)]);
let turns = dialogue.run(user_payload).await.unwrap();
assert_eq!(turns.len(), 1, "User message should trigger reaction");
assert_eq!(turns[0].speaker.name(), "Agent1");
assert_eq!(turns[0].content, "I can help with that");
let received = agent.get_received_payloads();
assert_eq!(received.len(), 1, "Agent should have been called once");
let received_messages = received[0].to_messages();
assert_eq!(
received_messages.len(),
2,
"Agent should receive both ContextInfo and User message"
);
assert_eq!(received_messages[0].speaker.name(), "System");
assert_eq!(
received_messages[0].content,
"Analysis completed: 3 issues found"
);
assert!(
received_messages[0]
.metadata
.is_type(&MessageType::ContextInfo),
"Metadata should be preserved"
);
assert_eq!(received_messages[1].speaker.name(), "Alice");
assert_eq!(
received_messages[1].content,
"Tell me more about the issues"
);
}
#[tokio::test]
async fn test_reaction_strategy_sequential_context_info() {
use crate::agent::dialogue::message::{MessageMetadata, MessageType};
use crate::agent::persona::Persona;
let persona1 = Persona {
name: "Agent1".to_string(),
role: "Analyzer".to_string(),
background: "First agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Agent2".to_string(),
role: "Reviewer".to_string(),
background: "Second agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let agent1 = RecordingAgent::new("Agent1", "Analysis done");
let agent2 = RecordingAgent::new("Agent2", "Review complete");
let mut dialogue = Dialogue::sequential();
dialogue
.add_participant(persona1, agent1.clone())
.add_participant(persona2, agent2.clone());
let context_payload = Payload::new().add_message_with_metadata(
Speaker::System,
"Background: Project uses Rust",
MessageMetadata::new().with_type(MessageType::ContextInfo),
);
let turns = dialogue.run(context_payload).await.unwrap();
assert_eq!(turns.len(), 0, "ContextInfo should not trigger reactions");
let user_payload = Payload::from_messages(vec![PayloadMessage::new(
Speaker::user("Bob", "User"),
"Analyze the code",
)]);
let turns = dialogue.run(user_payload).await.unwrap();
assert_eq!(turns.len(), 1); assert_eq!(turns[0].speaker.name(), "Agent2");
let agent1_received = agent1.get_received_payloads();
assert_eq!(
agent1_received.len(),
1,
"Agent1 should execute once for User message"
);
let agent1_payload = agent1_received[0].to_messages();
assert!(
agent1_payload
.iter()
.any(|m| m.content == "Analyze the code"),
"Should contain user message"
);
assert!(
agent1_payload
.iter()
.any(|m| m.content == "Background: Project uses Rust"),
"ContextInfo should be in history"
);
let agent2_received = agent2.get_received_payloads();
assert_eq!(agent2_received.len(), 1, "Agent2 should execute once");
}
#[tokio::test]
async fn test_reaction_strategy_mentioned_context_info() {
use crate::agent::dialogue::message::{MessageMetadata, MessageType};
use crate::agent::persona::Persona;
let persona1 = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "First agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Bob".to_string(),
role: "Reviewer".to_string(),
background: "Second agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let agent1 = RecordingAgent::new("Alice", "I'll handle it");
let agent2 = RecordingAgent::new("Bob", "Sounds good");
let mut dialogue = Dialogue::mentioned();
dialogue
.add_participant(persona1, agent1.clone())
.add_participant(persona2, agent2.clone());
let context_payload = Payload::new().add_message_with_metadata(
Speaker::System,
"Note: Use async/await",
MessageMetadata::new().with_type(MessageType::ContextInfo),
);
let turns = dialogue.run(context_payload).await.unwrap();
assert_eq!(turns.len(), 0, "ContextInfo should not trigger reactions");
let user_payload = Payload::from_messages(vec![PayloadMessage::new(
Speaker::user("User", "User"),
"@Alice can you implement this?",
)]);
let turns = dialogue.run(user_payload).await.unwrap();
assert_eq!(turns.len(), 1);
assert_eq!(turns[0].speaker.name(), "Alice");
let alice_received = agent1.get_received_payloads();
assert_eq!(
alice_received.len(),
1,
"Alice should execute once for @mention"
);
let alice_payload = alice_received[0].to_messages();
assert!(alice_payload.iter().any(|m| m.content.contains("@Alice")));
assert!(
alice_payload
.iter()
.any(|m| m.content == "Note: Use async/await")
);
let bob_received = agent2.get_received_payloads();
assert_eq!(
bob_received.len(),
0,
"Bob should not execute (no mentions)"
);
}
#[tokio::test]
async fn test_reaction_strategy_partial_session() {
use crate::agent::dialogue::message::{MessageMetadata, MessageType};
use crate::agent::persona::Persona;
let persona = Persona {
name: "Agent1".to_string(),
role: "Assistant".to_string(),
background: "Test agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let agent = RecordingAgent::new("Agent1", "Understood");
let mut dialogue = Dialogue::broadcast();
dialogue.add_participant(persona, agent.clone());
let context_payload = Payload::new().add_message_with_metadata(
Speaker::System,
"System ready",
MessageMetadata::new().with_type(MessageType::ContextInfo),
);
let mut session = dialogue.partial_session(context_payload);
let mut turn_count = 0;
while let Some(result) = session.next_turn().await {
result.unwrap();
turn_count += 1;
}
assert_eq!(turn_count, 0, "ContextInfo should not produce turns");
let user_payload = Payload::from_messages(vec![PayloadMessage::new(
Speaker::user("User", "User"),
"Hello",
)]);
let mut session = dialogue.partial_session(user_payload);
let mut turn_count = 0;
while let Some(result) = session.next_turn().await {
let turn = result.unwrap();
assert_eq!(turn.speaker.name(), "Agent1");
assert_eq!(turn.content, "Understood");
turn_count += 1;
}
assert_eq!(turn_count, 1);
let received = agent.get_received_payloads();
assert_eq!(received.len(), 1);
let messages = received[0].to_messages();
assert_eq!(messages.len(), 2);
assert_eq!(messages[0].content, "System ready");
assert!(messages[0].metadata.is_type(&MessageType::ContextInfo));
assert_eq!(messages[1].content, "Hello");
}
#[tokio::test]
async fn test_multiple_context_info_accumulation() {
use crate::agent::dialogue::message::{MessageMetadata, MessageType};
use crate::agent::persona::Persona;
let persona = Persona {
name: "Agent1".to_string(),
role: "Assistant".to_string(),
background: "Test agent".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let agent = RecordingAgent::new("Agent1", "Got it");
let mut dialogue = Dialogue::broadcast();
dialogue.add_participant(persona, agent.clone());
for i in 1..=3 {
let context_payload = Payload::new().add_message_with_metadata(
Speaker::System,
format!("Context {}", i),
MessageMetadata::new().with_type(MessageType::ContextInfo),
);
let turns = dialogue.run(context_payload).await.unwrap();
assert_eq!(turns.len(), 0);
}
let history = dialogue.history();
assert_eq!(history.len(), 3);
let user_payload = Payload::from_messages(vec![PayloadMessage::new(
Speaker::user("User", "User"),
"Process all context",
)]);
let turns = dialogue.run(user_payload).await.unwrap();
assert_eq!(turns.len(), 1);
let received = agent.get_received_payloads();
assert_eq!(received.len(), 1);
let messages = received[0].to_messages();
assert_eq!(
messages.len(),
4,
"Should receive 3 ContextInfo + 1 User message"
);
for (i, message) in messages.iter().enumerate().take(3) {
assert_eq!(message.content, format!("Context {}", i + 1));
assert!(message.metadata.is_type(&MessageType::ContextInfo));
}
assert_eq!(messages[3].content, "Process all context");
}
#[tokio::test]
async fn test_add_agent_with_context_config() {
use crate::agent::persona::{ContextConfig, PersonaAgent};
let mock_agent = MockAgent::new("Alice", vec!["Response from Alice".to_string()]);
let persona = Persona {
name: "Alice".to_string(),
role: "Engineer".to_string(),
background: "Senior developer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
let config = ContextConfig {
long_conversation_threshold: 1000,
recent_messages_count: 10,
participants_after_context: true,
include_trailing_prompt: true,
};
let persona_agent =
PersonaAgent::new(mock_agent, persona.clone()).with_context_config(config);
let mut dialogue = Dialogue::sequential();
dialogue.add_agent(persona, persona_agent);
assert_eq!(dialogue.participant_count(), 1);
assert_eq!(dialogue.participant_names(), vec!["Alice"]);
let payload = Payload::text("Test message");
let turns = dialogue.run(payload).await.unwrap();
assert_eq!(turns.len(), 1);
assert_eq!(turns[0].speaker.name(), "Alice");
assert_eq!(turns[0].content, "Response from Alice");
}
#[tokio::test]
async fn test_dialogue_as_agent_basic() {
let persona1 = Persona {
name: "Alice".to_string(),
role: "Engineer".to_string(),
background: "Senior developer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UX specialist".to_string(),
communication_style: "Creative".to_string(),
visual_identity: None,
capabilities: None,
};
let agent1 = MockAgent::new("Alice", vec!["Technical perspective".to_string()]);
let agent2 = MockAgent::new("Bob", vec!["Design perspective".to_string()]);
let mut dialogue = Dialogue::broadcast();
dialogue.add_participant(persona1, agent1);
dialogue.add_participant(persona2, agent2);
let agent_name = dialogue.name();
assert!(agent_name.contains("Broadcast"));
assert!(agent_name.contains("2 participants"));
let expertise = dialogue.expertise();
assert!(expertise.contains("Multi-agent dialogue"));
let payload = Payload::text("Discuss the new feature");
let output = dialogue.execute(payload).await.unwrap();
assert_eq!(output.len(), 2);
assert!(output.iter().any(|turn| turn.speaker.name() == "Alice"));
assert!(output.iter().any(|turn| turn.speaker.name() == "Bob"));
}
#[tokio::test]
async fn test_dialogue_as_agent_sequential() {
let persona1 = Persona {
name: "Analyzer".to_string(),
role: "Data Analyst".to_string(),
background: "Statistics expert".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Writer".to_string(),
role: "Technical Writer".to_string(),
background: "Documentation specialist".to_string(),
communication_style: "Clear and concise".to_string(),
visual_identity: None,
capabilities: None,
};
let agent1 = MockAgent::new("Analyzer", vec!["Data shows trend X".to_string()]);
let agent2 = MockAgent::new("Writer", vec!["Documented the findings".to_string()]);
let mut dialogue = Dialogue::sequential();
dialogue.add_participant(persona1, agent1);
dialogue.add_participant(persona2, agent2);
let agent_name = dialogue.name();
assert!(agent_name.contains("Sequential"));
let payload = Payload::text("Analyze and document the data");
let output = dialogue.execute(payload).await.unwrap();
assert_eq!(output.len(), 1);
assert_eq!(output[0].speaker.name(), "Writer");
assert_eq!(output[0].content, "Documented the findings");
}
#[tokio::test]
async fn test_dialogue_as_agent_clone_independence() {
let persona = Persona {
name: "Agent".to_string(),
role: "Assistant".to_string(),
background: "Helper".to_string(),
communication_style: "Friendly".to_string(),
visual_identity: None,
capabilities: None,
};
let agent = MockAgent::new("Agent", vec!["Response 1".to_string()]);
let mut dialogue = Dialogue::broadcast();
dialogue.add_participant(persona, agent);
let payload1 = Payload::text("First request");
let output1 = dialogue.execute(payload1).await.unwrap();
assert_eq!(output1.len(), 1);
let payload2 = Payload::text("Second request");
let output2 = dialogue.execute(payload2).await.unwrap();
assert_eq!(output2.len(), 1);
assert_eq!(dialogue.history().len(), 0);
}
#[tokio::test]
async fn test_moderator_execution_model() {
#[derive(Clone)]
struct MockModerator;
#[async_trait]
impl Agent for MockModerator {
type Output = ExecutionModel;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Mock moderator for testing";
&EXPERTISE
}
fn name(&self) -> String {
"MockModerator".to_string()
}
async fn execute(&self, _payload: Payload) -> Result<Self::Output, AgentError> {
Ok(ExecutionModel::OrderedSequential(
SequentialOrder::Explicit(vec!["Bob".to_string(), "Alice".to_string()]),
))
}
}
let persona1 = Persona {
name: "Alice".to_string(),
role: "Engineer".to_string(),
background: "Developer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
let persona2 = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UX specialist".to_string(),
communication_style: "Creative".to_string(),
visual_identity: None,
capabilities: None,
};
let agent1 = MockAgent::new("Alice", vec!["Alice's response".to_string()]);
let agent2 = MockAgent::new("Bob", vec!["Bob's response".to_string()]);
let mut dialogue = Dialogue::moderator();
dialogue.with_moderator(MockModerator);
dialogue.add_participant(persona1, agent1);
dialogue.add_participant(persona2, agent2);
let payload = Payload::text("Discuss the feature");
let output = dialogue.run(payload).await.unwrap();
assert_eq!(output.len(), 1);
assert_eq!(output[0].speaker.name(), "Alice");
assert_eq!(output[0].content, "Alice's response");
}
#[tokio::test]
async fn test_moderator_without_agent_fails() {
let persona = Persona {
name: "Alice".to_string(),
role: "Engineer".to_string(),
background: "Developer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
let agent = MockAgent::new("Alice", vec!["Response".to_string()]);
let mut dialogue = Dialogue::moderator();
dialogue.add_participant(persona, agent);
let payload = Payload::text("Test");
let result = dialogue.run(payload).await;
assert!(result.is_err());
let err = result.unwrap_err();
match err {
AgentError::ExecutionFailed(msg) => {
assert!(msg.contains("moderator agent"));
}
_ => panic!("Expected ExecutionFailed error"),
}
}
#[tokio::test]
async fn test_moderator_prevents_infinite_recursion() {
#[derive(Clone)]
struct BadModerator;
#[async_trait]
impl Agent for BadModerator {
type Output = ExecutionModel;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Bad moderator";
&EXPERTISE
}
fn name(&self) -> String {
"BadModerator".to_string()
}
async fn execute(&self, _payload: Payload) -> Result<Self::Output, AgentError> {
Ok(ExecutionModel::Moderator) }
}
let persona = Persona {
name: "Alice".to_string(),
role: "Engineer".to_string(),
background: "Developer".to_string(),
communication_style: "Technical".to_string(),
visual_identity: None,
capabilities: None,
};
let agent = MockAgent::new("Alice", vec!["Response".to_string()]);
let mut dialogue = Dialogue::moderator();
dialogue.with_moderator(BadModerator);
dialogue.add_participant(persona, agent);
let payload = Payload::text("Test");
let result = dialogue.run(payload).await;
assert!(result.is_err());
let err = result.unwrap_err();
match err {
AgentError::ExecutionFailed(msg) => {
assert!(msg.contains("infinite recursion"));
}
_ => panic!("Expected ExecutionFailed error about infinite recursion"),
}
}
#[tokio::test]
async fn test_join_in_progress_with_fresh_strategy() {
use crate::agent::dialogue::joining_strategy::JoiningStrategy;
use crate::agent::persona::Persona;
let alice = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Backend engineer".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let bob = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UI/UX specialist".to_string(),
communication_style: "Creative".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_alice = MockAgent::new(
"Alice",
vec!["Alice turn 1".to_string(), "Alice turn 2".to_string()],
);
let agent_bob = MockAgent::new(
"Bob",
vec!["Bob turn 1".to_string(), "Bob turn 2".to_string()],
);
let mut dialogue = Dialogue::broadcast();
dialogue.add_participant(alice, agent_alice.clone());
dialogue.add_participant(bob, agent_bob.clone());
let _turn1 = dialogue.run("What's the plan?").await.unwrap();
let _turn2 = dialogue.run("Let's proceed").await.unwrap();
let consultant = Persona {
name: "Carol".to_string(),
role: "Security Consultant".to_string(),
background: "Security expert".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_carol = MockAgent::new("Carol", vec!["Carol's fresh perspective".to_string()]);
let carol_clone = agent_carol.clone();
dialogue.join_in_progress(consultant, agent_carol, JoiningStrategy::Fresh);
let turn3 = dialogue.run("Carol, what do you think?").await.unwrap();
assert!(turn3.iter().any(|t| t.speaker.name() == "Carol"));
assert_eq!(
carol_clone.get_call_count(),
1,
"Carol should be called once"
);
let carol_payloads = carol_clone.get_payloads();
assert_eq!(carol_payloads.len(), 1, "Carol should receive 1 payload");
let carol_first_payload = &carol_payloads[0];
let messages = carol_first_payload.to_messages();
let historical_messages: Vec<_> = messages
.iter()
.filter(|msg| msg.speaker.name() == "Alice" || msg.speaker.name() == "Bob")
.collect();
assert_eq!(
historical_messages.len(),
0,
"Fresh strategy: Carol should not see historical messages from Alice or Bob"
);
let system_messages: Vec<_> = messages
.iter()
.filter(|msg| matches!(msg.speaker, Speaker::System))
.collect();
assert!(
system_messages.is_empty(),
"Fresh strategy: Carol should NOT see any system messages on initial turn"
);
let turn4 = dialogue.run("Let's continue").await.unwrap();
assert!(turn4.iter().any(|t| t.speaker.name() == "Carol"));
assert_eq!(
carol_clone.get_call_count(),
2,
"Carol should be called twice"
);
let carol_second_payload = &carol_clone.get_payloads()[1];
let turn4_messages = carol_second_payload.to_messages();
let turn3_agent_messages: Vec<_> = turn4_messages
.iter()
.filter(|msg| {
(msg.speaker.name() == "Alice" || msg.speaker.name() == "Bob")
&& !matches!(msg.speaker, Speaker::System)
})
.collect();
assert!(
!turn3_agent_messages.is_empty(),
"In Turn 4, Carol should see Turn 3 messages from Alice and Bob"
);
}
#[tokio::test]
async fn test_join_in_progress_with_full_strategy() {
use crate::agent::dialogue::joining_strategy::JoiningStrategy;
use crate::agent::persona::Persona;
let alice = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Backend engineer".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_alice = MockAgent::new(
"Alice",
vec![
"Alice turn 1".to_string(),
"Alice turn 2".to_string(),
"Alice turn 3".to_string(),
],
);
let alice_clone = agent_alice.clone();
let mut dialogue = Dialogue::broadcast();
dialogue.add_participant(alice, agent_alice);
let _turn1 = dialogue.run("First topic").await.unwrap();
let _turn2 = dialogue.run("Second topic").await.unwrap();
assert_eq!(
alice_clone.get_call_count(),
2,
"Alice should be called twice"
);
let bob = Persona {
name: "Bob".to_string(),
role: "New Member".to_string(),
background: "Just joined".to_string(),
communication_style: "Curious".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_bob = MockAgent::new(
"Bob",
vec!["Bob caught up".to_string(), "Bob turn 4".to_string()],
);
let bob_clone = agent_bob.clone();
dialogue.join_in_progress(bob, agent_bob, JoiningStrategy::Full);
let turn3 = dialogue.run("Bob, your thoughts?").await.unwrap();
assert!(turn3.iter().any(|t| t.speaker.name() == "Bob"));
assert_eq!(bob_clone.get_call_count(), 1, "Bob should be called once");
let bob_payloads = bob_clone.get_payloads();
assert_eq!(bob_payloads.len(), 1, "Bob should receive 1 payload");
let bob_first_payload = &bob_payloads[0];
let messages = bob_first_payload.to_messages();
let alice_historical_messages: Vec<_> = messages
.iter()
.filter(|msg| msg.speaker.name() == "Alice")
.collect();
assert_eq!(
alice_historical_messages.len(),
2,
"Full strategy: Bob should see ALL 2 historical messages from Alice (Turn 1 and Turn 2)"
);
let alice_contents: Vec<&str> = alice_historical_messages
.iter()
.map(|msg| msg.content.as_str())
.collect();
assert!(
alice_contents.contains(&"Alice turn 1"),
"Bob should see Alice's Turn 1 response"
);
assert!(
alice_contents.contains(&"Alice turn 2"),
"Bob should see Alice's Turn 2 response"
);
let turn4 = dialogue.run("Let's continue").await.unwrap();
assert!(turn4.iter().any(|t| t.speaker.name() == "Bob"));
assert_eq!(bob_clone.get_call_count(), 2, "Bob should be called twice");
let bob_second_payload = &bob_clone.get_payloads()[1];
let turn4_messages = bob_second_payload.to_messages();
let alice_turn4_messages: Vec<_> = turn4_messages
.iter()
.filter(|msg| msg.speaker.name() == "Alice")
.collect();
assert_eq!(
alice_turn4_messages.len(),
1,
"In Turn 4, Bob should only see Alice's Turn 3 response (not Turn 1 and 2 again)"
);
assert_eq!(
alice_turn4_messages[0].content, "Alice turn 3",
"Bob should see Alice's Turn 3 response in Turn 4"
);
}
#[tokio::test]
async fn test_join_in_progress_with_recent_strategy() {
use crate::agent::dialogue::joining_strategy::JoiningStrategy;
use crate::agent::persona::Persona;
let alice = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Backend engineer".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_alice = MockAgent::new(
"Alice",
vec![
"Alice turn 1".to_string(),
"Alice turn 2".to_string(),
"Alice turn 3".to_string(),
"Alice turn 4".to_string(),
"Alice turn 5".to_string(),
"Alice turn 6".to_string(),
"Alice turn 7".to_string(),
],
);
let alice_clone = agent_alice.clone();
let mut dialogue = Dialogue::broadcast();
dialogue.add_participant(alice, agent_alice);
for i in 1..=5 {
let _ = dialogue.run(format!("Message {}", i)).await.unwrap();
}
assert_eq!(
alice_clone.get_call_count(),
5,
"Alice should be called 5 times"
);
let bob = Persona {
name: "Bob".to_string(),
role: "Reviewer".to_string(),
background: "Code reviewer".to_string(),
communication_style: "Focused".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_bob = MockAgent::new(
"Bob",
vec!["Bob reviews recent".to_string(), "Bob turn 7".to_string()],
);
let bob_clone = agent_bob.clone();
dialogue.join_in_progress(bob, agent_bob, JoiningStrategy::recent_with_turns(2));
let turn6 = dialogue.run("Bob, review please").await.unwrap();
assert!(turn6.iter().any(|t| t.speaker.name() == "Bob"));
assert_eq!(bob_clone.get_call_count(), 1, "Bob should be called once");
let bob_payloads = bob_clone.get_payloads();
assert_eq!(bob_payloads.len(), 1, "Bob should receive 1 payload");
let bob_first_payload = &bob_payloads[0];
let messages = bob_first_payload.to_messages();
let alice_historical_messages: Vec<_> = messages
.iter()
.filter(|msg| msg.speaker.name() == "Alice")
.collect();
assert_eq!(
alice_historical_messages.len(),
2,
"Recent(2) strategy: Bob should see 2 recent historical messages from Alice"
);
let alice_contents: Vec<&str> = alice_historical_messages
.iter()
.map(|msg| msg.content.as_str())
.collect();
assert!(
alice_contents.contains(&"Alice turn 4"),
"Bob should see Alice's Turn 4 response"
);
assert!(
alice_contents.contains(&"Alice turn 5"),
"Bob should see Alice's Turn 5 response"
);
assert!(
!alice_contents.contains(&"Alice turn 1"),
"Bob should NOT see Alice's Turn 1 response (too old)"
);
assert!(
!alice_contents.contains(&"Alice turn 2"),
"Bob should NOT see Alice's Turn 2 response (too old)"
);
assert!(
!alice_contents.contains(&"Alice turn 3"),
"Bob should NOT see Alice's Turn 3 response (too old)"
);
let turn7 = dialogue.run("Let's continue").await.unwrap();
assert!(turn7.iter().any(|t| t.speaker.name() == "Bob"));
assert_eq!(bob_clone.get_call_count(), 2, "Bob should be called twice");
let bob_second_payload = &bob_clone.get_payloads()[1];
let turn7_messages = bob_second_payload.to_messages();
let alice_turn7_messages: Vec<_> = turn7_messages
.iter()
.filter(|msg| msg.speaker.name() == "Alice")
.collect();
assert_eq!(
alice_turn7_messages.len(),
1,
"In Turn 7, Bob should only see Alice's Turn 6 response"
);
assert_eq!(
alice_turn7_messages[0].content, "Alice turn 6",
"Bob should see Alice's Turn 6 response in Turn 7"
);
}
#[tokio::test]
async fn test_join_in_progress_mentioned_mode_with_fresh_strategy() {
use crate::agent::dialogue::joining_strategy::JoiningStrategy;
use crate::agent::persona::Persona;
let alice = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Backend engineer".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let bob = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UI/UX specialist".to_string(),
communication_style: "Creative".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_alice = MockAgent::new(
"Alice",
vec![
"Alice turn 1".to_string(),
"Alice turn 2".to_string(),
"Alice turn 3".to_string(),
],
);
let agent_bob = MockAgent::new(
"Bob",
vec!["Bob turn 1".to_string(), "Bob turn 2".to_string()],
);
let alice_clone = agent_alice.clone();
let mut dialogue = Dialogue::mentioned();
dialogue.add_participant(alice, agent_alice);
dialogue.add_participant(bob, agent_bob);
let _turn1 = dialogue.run("@Alice what's the plan?").await.unwrap();
let _turn2 = dialogue.run("@Bob your thoughts?").await.unwrap();
assert_eq!(
alice_clone.get_call_count(),
1,
"Alice should be called once (only Turn 1)"
);
let carol = Persona {
name: "Carol".to_string(),
role: "Security Consultant".to_string(),
background: "Security expert".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_carol = MockAgent::new(
"Carol",
vec![
"Carol's fresh analysis".to_string(),
"Carol turn 4".to_string(),
],
);
let carol_clone = agent_carol.clone();
dialogue.join_in_progress(carol, agent_carol, JoiningStrategy::Fresh);
let turn3 = dialogue.run("@Carol security review please").await.unwrap();
assert!(turn3.iter().any(|t| t.speaker.name() == "Carol"));
assert_eq!(
carol_clone.get_call_count(),
1,
"Carol should be called once"
);
let carol_payloads = carol_clone.get_payloads();
assert_eq!(carol_payloads.len(), 1, "Carol should receive 1 payload");
let carol_first_payload = &carol_payloads[0];
let messages = carol_first_payload.to_messages();
let historical_agent_messages: Vec<_> = messages
.iter()
.filter(|msg| msg.speaker.name() == "Alice" || msg.speaker.name() == "Bob")
.collect();
assert_eq!(
historical_agent_messages.len(),
0,
"Fresh strategy in mentioned mode: Carol should not see historical messages"
);
let _turn4 = dialogue
.run("@Carol and @Alice continue discussion")
.await
.unwrap();
assert_eq!(
carol_clone.get_call_count(),
2,
"Carol should be called twice"
);
assert_eq!(
alice_clone.get_call_count(),
2,
"Alice should be called twice"
);
let carol_second_payload = &carol_clone.get_payloads()[1];
let turn4_messages = carol_second_payload.to_messages();
let alice_messages: Vec<_> = turn4_messages
.iter()
.filter(|msg| msg.speaker.name() == "Alice")
.collect();
assert_eq!(
alice_messages.len(),
0,
"In Turn 4, Carol should NOT see Alice's Turn 4 response (parallel execution)"
);
let has_alice_turn1 = turn4_messages
.iter()
.any(|msg| msg.speaker.name() == "Alice" && msg.content.contains("turn 1"));
assert!(
!has_alice_turn1,
"Carol should NOT see Alice's Turn 1 response in Turn 4 (marked as sent)"
);
}
#[tokio::test]
async fn test_join_in_progress_sequential_mode_with_fresh_strategy() {
use crate::agent::dialogue::joining_strategy::JoiningStrategy;
use crate::agent::persona::Persona;
let alice = Persona {
name: "Alice".to_string(),
role: "Analyzer".to_string(),
background: "Data analyst".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
let bob = Persona {
name: "Bob".to_string(),
role: "Reviewer".to_string(),
background: "Code reviewer".to_string(),
communication_style: "Critical".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_alice = MockAgent::new(
"Alice",
vec![
"Alice analyzed: turn 1".to_string(),
"Alice analyzed: turn 2".to_string(),
"Alice analyzed: turn 3".to_string(),
"Alice analyzed: turn 4".to_string(),
],
);
let agent_bob = MockAgent::new(
"Bob",
vec![
"Bob reviewed: turn 1".to_string(),
"Bob reviewed: turn 2".to_string(),
"Bob reviewed: turn 3".to_string(),
"Bob reviewed: turn 4".to_string(),
],
);
let alice_clone = agent_alice.clone();
let bob_clone = agent_bob.clone();
let mut dialogue = Dialogue::sequential();
dialogue.add_participant(alice, agent_alice);
dialogue.add_participant(bob, agent_bob);
let _turn1 = dialogue.run("Analyze this data").await.unwrap();
let _turn2 = dialogue.run("Continue analysis").await.unwrap();
assert_eq!(
alice_clone.get_call_count(),
2,
"Alice should be called twice"
);
assert_eq!(bob_clone.get_call_count(), 2, "Bob should be called twice");
let carol = Persona {
name: "Carol".to_string(),
role: "Summarizer".to_string(),
background: "Summary specialist".to_string(),
communication_style: "Concise".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_carol = MockAgent::new(
"Carol",
vec![
"Carol summarized".to_string(),
"Carol summary 2".to_string(),
],
);
let carol_clone = agent_carol.clone();
dialogue.join_in_progress(carol, agent_carol, JoiningStrategy::Fresh);
let turn3 = dialogue.run("Final analysis").await.unwrap();
assert_eq!(
turn3.len(),
1,
"Sequential mode returns only last agent's output"
);
assert_eq!(
turn3[0].speaker.name(),
"Carol",
"Last agent should be Carol"
);
assert_eq!(
alice_clone.get_call_count(),
3,
"Alice should be called 3 times"
);
assert_eq!(
bob_clone.get_call_count(),
3,
"Bob should be called 3 times"
);
assert_eq!(
carol_clone.get_call_count(),
1,
"Carol should be called once"
);
let carol_payloads = carol_clone.get_payloads();
assert_eq!(carol_payloads.len(), 1, "Carol should receive 1 payload");
let carol_first_payload = &carol_payloads[0];
let messages = carol_first_payload.to_messages();
let turn1_turn2_messages: Vec<_> = messages
.iter()
.filter(|msg| {
let content = msg.content.as_str();
(msg.speaker.name() == "Alice" || msg.speaker.name() == "Bob")
&& (content.contains("turn 1") || content.contains("turn 2"))
})
.collect();
assert_eq!(
turn1_turn2_messages.len(),
0,
"Fresh strategy in sequential mode: Carol should not see Turn 1 or Turn 2 historical messages"
);
let bob_turn3_messages: Vec<_> = messages
.iter()
.filter(|msg| msg.speaker.name() == "Bob" && msg.content.contains("turn 3"))
.collect();
assert_eq!(
bob_turn3_messages.len(),
1,
"Carol should see Bob's Turn 3 output (her immediate input in sequential chain)"
);
let _turn4 = dialogue.run("Continue").await.unwrap();
assert_eq!(
carol_clone.get_call_count(),
2,
"Carol should be called twice"
);
let carol_second_payload = &carol_clone.get_payloads()[1];
let turn4_messages = carol_second_payload.to_messages();
let bob_historical: Vec<_> = turn4_messages
.iter()
.filter(|msg| {
msg.speaker.name() == "Bob"
&& (msg.content.contains("turn 1") || msg.content.contains("turn 2"))
})
.collect();
assert_eq!(
bob_historical.len(),
0,
"In Turn 4, Carol should NOT see Bob's Turn 1 or Turn 2 (marked as sent)"
);
let bob_turn4: Vec<_> = turn4_messages
.iter()
.filter(|msg| msg.speaker.name() == "Bob" && msg.content.contains("turn 4"))
.collect();
assert_eq!(
bob_turn4.len(),
1,
"In Turn 4, Carol should see Bob's Turn 4 output (new chain input)"
);
}
#[tokio::test]
async fn test_join_in_progress_partial_session_sequential_with_fresh_strategy() {
use crate::agent::dialogue::joining_strategy::JoiningStrategy;
use crate::agent::persona::Persona;
let alice = Persona {
name: "Alice".to_string(),
role: "Analyzer".to_string(),
background: "Data analyst".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
let bob = Persona {
name: "Bob".to_string(),
role: "Reviewer".to_string(),
background: "Code reviewer".to_string(),
communication_style: "Critical".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_alice = MockAgent::new(
"Alice",
vec![
"Alice analyzed: turn 1".to_string(),
"Alice analyzed: turn 2".to_string(),
"Alice analyzed: turn 3".to_string(),
"Alice analyzed: turn 4".to_string(),
],
);
let agent_bob = MockAgent::new(
"Bob",
vec![
"Bob reviewed: turn 1".to_string(),
"Bob reviewed: turn 2".to_string(),
"Bob reviewed: turn 3".to_string(),
"Bob reviewed: turn 4".to_string(),
],
);
let alice_clone = agent_alice.clone();
let mut dialogue = Dialogue::sequential();
dialogue.add_participant(alice, agent_alice);
dialogue.add_participant(bob, agent_bob);
let mut session1 = dialogue.partial_session("Analyze this");
while let Some(turn) = session1.next_turn().await {
turn.unwrap();
}
let mut session2 = dialogue.partial_session("Continue analysis");
while let Some(turn) = session2.next_turn().await {
turn.unwrap();
}
assert_eq!(
alice_clone.get_call_count(),
2,
"Alice called twice via partial_session"
);
let carol = Persona {
name: "Carol".to_string(),
role: "Summarizer".to_string(),
background: "Summary specialist".to_string(),
communication_style: "Concise".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_carol = MockAgent::new(
"Carol",
vec![
"Carol summarized".to_string(),
"Carol summary 2".to_string(),
],
);
let carol_clone = agent_carol.clone();
dialogue.join_in_progress(carol, agent_carol, JoiningStrategy::Fresh);
let mut session3 = dialogue.partial_session("Final summary");
let mut turn_count = 0;
while let Some(turn) = session3.next_turn().await {
turn.unwrap();
turn_count += 1;
}
assert_eq!(
turn_count, 3,
"Should have 3 turns in sequential partial_session"
);
assert_eq!(carol_clone.get_call_count(), 1, "Carol called once");
let carol_payloads = carol_clone.get_payloads();
assert_eq!(carol_payloads.len(), 1, "Carol should receive 1 payload");
let carol_first_payload = &carol_payloads[0];
let messages = carol_first_payload.to_messages();
let historical_messages: Vec<_> = messages
.iter()
.filter(|msg| {
let content = msg.content.as_str();
(msg.speaker.name() == "Alice" || msg.speaker.name() == "Bob")
&& (content.contains("turn 1") || content.contains("turn 2"))
})
.collect();
assert_eq!(
historical_messages.len(),
0,
"Fresh strategy in partial_session sequential: Carol should not see Turn 1 or Turn 2"
);
let bob_turn3: Vec<_> = messages
.iter()
.filter(|msg| msg.speaker.name() == "Bob" && msg.content.contains("turn 3"))
.collect();
assert_eq!(
bob_turn3.len(),
1,
"Carol should see Bob's Turn 3 output via partial_session"
);
let mut session4 = dialogue.partial_session("Continue");
while let Some(turn) = session4.next_turn().await {
turn.unwrap();
}
assert_eq!(carol_clone.get_call_count(), 2, "Carol called twice");
let carol_second_payload = &carol_clone.get_payloads()[1];
let turn4_messages = carol_second_payload.to_messages();
let historical_turn4: Vec<_> = turn4_messages
.iter()
.filter(|msg| {
msg.speaker.name() == "Bob"
&& (msg.content.contains("turn 1") || msg.content.contains("turn 2"))
})
.collect();
assert_eq!(
historical_turn4.len(),
0,
"In Turn 4 via partial_session, Carol should NOT see Turn 1 or Turn 2 (marked as sent)"
);
}
#[tokio::test]
async fn test_join_in_progress_mentioned_mode_with_full_strategy() {
use crate::agent::dialogue::joining_strategy::JoiningStrategy;
use crate::agent::persona::Persona;
let alice = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Backend engineer".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let bob = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UI/UX specialist".to_string(),
communication_style: "Creative".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_alice = MockAgent::new(
"Alice",
vec![
"Alice turn 1".to_string(),
"Alice turn 2".to_string(),
"Alice turn 3".to_string(),
"Alice turn 4".to_string(),
],
);
let agent_bob = MockAgent::new(
"Bob",
vec![
"Bob turn 1".to_string(),
"Bob turn 2".to_string(),
"Bob turn 3".to_string(),
],
);
let alice_clone = agent_alice.clone();
let bob_clone = agent_bob.clone();
let mut dialogue = Dialogue::mentioned();
dialogue.add_participant(alice, agent_alice);
dialogue.add_participant(bob, agent_bob);
let _turn1 = dialogue.run("@Alice what's the plan?").await.unwrap();
let _turn2 = dialogue.run("@Bob your thoughts?").await.unwrap();
assert_eq!(
alice_clone.get_call_count(),
1,
"Alice should be called once (Turn 1)"
);
assert_eq!(
bob_clone.get_call_count(),
1,
"Bob should be called once (Turn 2)"
);
let carol = Persona {
name: "Carol".to_string(),
role: "Security Consultant".to_string(),
background: "Security expert".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_carol = MockAgent::new(
"Carol",
vec![
"Carol's full analysis".to_string(),
"Carol turn 4".to_string(),
],
);
let carol_clone = agent_carol.clone();
dialogue.join_in_progress(carol, agent_carol, JoiningStrategy::Full);
let turn3 = dialogue.run("@Carol security review please").await.unwrap();
assert!(turn3.iter().any(|t| t.speaker.name() == "Carol"));
assert_eq!(
carol_clone.get_call_count(),
1,
"Carol should be called once"
);
let carol_payloads = carol_clone.get_payloads();
assert_eq!(carol_payloads.len(), 1, "Carol should receive 1 payload");
let carol_first_payload = &carol_payloads[0];
let messages = carol_first_payload.to_messages();
let alice_historical: Vec<_> = messages
.iter()
.filter(|msg| msg.speaker.name() == "Alice")
.collect();
let bob_historical: Vec<_> = messages
.iter()
.filter(|msg| msg.speaker.name() == "Bob")
.collect();
assert_eq!(
alice_historical.len(),
1,
"Full strategy in mentioned mode: Carol should see Alice's Turn 1"
);
assert_eq!(
bob_historical.len(),
1,
"Full strategy in mentioned mode: Carol should see Bob's Turn 2"
);
assert_eq!(alice_historical[0].content, "Alice turn 1");
assert_eq!(bob_historical[0].content, "Bob turn 1");
let _turn4 = dialogue
.run("@Carol and @Alice continue discussion")
.await
.unwrap();
assert_eq!(
carol_clone.get_call_count(),
2,
"Carol should be called twice"
);
assert_eq!(
alice_clone.get_call_count(),
2,
"Alice should be called twice"
);
let carol_second_payload = &carol_clone.get_payloads()[1];
let turn4_messages = carol_second_payload.to_messages();
let alice_turn4_messages: Vec<_> = turn4_messages
.iter()
.filter(|msg| msg.speaker.name() == "Alice")
.collect();
assert_eq!(
alice_turn4_messages.len(),
0,
"In Turn 4, Carol should NOT see Alice's concurrent Turn 4 response"
);
let has_alice_turn1 = turn4_messages
.iter()
.any(|msg| msg.speaker.name() == "Alice" && msg.content.contains("turn 1"));
let has_bob_turn1 = turn4_messages
.iter()
.any(|msg| msg.speaker.name() == "Bob" && msg.content.contains("turn 1"));
assert!(
!has_alice_turn1,
"Carol should NOT see Alice's Turn 1 in Turn 4 (marked as sent)"
);
assert!(
!has_bob_turn1,
"Carol should NOT see Bob's Turn 1 in Turn 4 (marked as sent)"
);
}
#[tokio::test]
async fn test_join_in_progress_mentioned_mode_with_recent_strategy() {
use crate::agent::dialogue::joining_strategy::JoiningStrategy;
use crate::agent::persona::Persona;
let alice = Persona {
name: "Alice".to_string(),
role: "Developer".to_string(),
background: "Backend engineer".to_string(),
communication_style: "Direct".to_string(),
visual_identity: None,
capabilities: None,
};
let bob = Persona {
name: "Bob".to_string(),
role: "Designer".to_string(),
background: "UI/UX specialist".to_string(),
communication_style: "Creative".to_string(),
visual_identity: None,
capabilities: None,
};
let dave = Persona {
name: "Dave".to_string(),
role: "QA Engineer".to_string(),
background: "Testing specialist".to_string(),
communication_style: "Meticulous".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_alice = MockAgent::new(
"Alice",
vec![
"Alice turn 1".to_string(),
"Alice turn 2".to_string(),
"Alice turn 3".to_string(),
"Alice turn 4".to_string(),
"Alice turn 5".to_string(),
"Alice turn 6".to_string(),
],
);
let agent_bob = MockAgent::new(
"Bob",
vec![
"Bob turn 1".to_string(),
"Bob turn 3".to_string(),
"Bob turn 5".to_string(),
],
);
let agent_dave = MockAgent::new(
"Dave",
vec!["Dave turn 2".to_string(), "Dave turn 4".to_string()],
);
let mut dialogue = Dialogue::mentioned();
dialogue.add_participant(alice, agent_alice);
dialogue.add_participant(bob, agent_bob);
dialogue.add_participant(dave, agent_dave);
let _turn1 = dialogue.run("@Alice and @Bob start").await.unwrap();
let _turn2 = dialogue.run("@Dave check this").await.unwrap();
let _turn3 = dialogue.run("@Alice and @Bob continue").await.unwrap();
let _turn4 = dialogue.run("@Dave verify").await.unwrap();
let _turn5 = dialogue.run("@Alice and @Bob finalize").await.unwrap();
let carol = Persona {
name: "Carol".to_string(),
role: "Security Consultant".to_string(),
background: "Security expert".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_carol = MockAgent::new(
"Carol",
vec![
"Carol's recent analysis".to_string(),
"Carol turn 7".to_string(),
],
);
let carol_clone = agent_carol.clone();
dialogue.join_in_progress(carol, agent_carol, JoiningStrategy::recent_with_turns(2));
let turn6 = dialogue.run("@Carol security review").await.unwrap();
assert!(turn6.iter().any(|t| t.speaker.name() == "Carol"));
assert_eq!(
carol_clone.get_call_count(),
1,
"Carol should be called once"
);
let carol_payloads = carol_clone.get_payloads();
assert_eq!(carol_payloads.len(), 1, "Carol should receive 1 payload");
let carol_first_payload = &carol_payloads[0];
let messages = carol_first_payload.to_messages();
let agent_messages: Vec<_> = messages
.iter()
.filter(|msg| {
msg.speaker.name() == "Alice"
|| msg.speaker.name() == "Bob"
|| msg.speaker.name() == "Dave"
})
.collect();
let has_dave_turn4 = agent_messages
.iter()
.any(|msg| msg.speaker.name() == "Dave" && msg.content.contains("turn 4"));
let has_alice_turn3 = agent_messages
.iter()
.any(|msg| msg.speaker.name() == "Alice" && msg.content.contains("turn 3"));
let has_bob_turn5 = agent_messages
.iter()
.any(|msg| msg.speaker.name() == "Bob" && msg.content.contains("turn 5"));
assert!(
has_dave_turn4,
"Recent(2) strategy: Carol should see Dave's Turn 4"
);
assert!(
has_alice_turn3,
"Recent(2) strategy: Carol should see Alice's response in Turn 5 (her 3rd call)"
);
assert!(
has_bob_turn5,
"Recent(2) strategy: Carol should see Bob's response in Turn 5 (his 3rd call)"
);
let has_alice_turn1 = agent_messages
.iter()
.any(|msg| msg.speaker.name() == "Alice" && msg.content.contains("turn 1"));
let has_bob_turn1 = agent_messages
.iter()
.any(|msg| msg.speaker.name() == "Bob" && msg.content.contains("turn 1"));
let has_dave_turn2 = agent_messages
.iter()
.any(|msg| msg.speaker.name() == "Dave" && msg.content.contains("turn 2"));
let has_bob_turn3 = agent_messages
.iter()
.any(|msg| msg.speaker.name() == "Bob" && msg.content.contains("turn 3"));
assert!(
!has_alice_turn1,
"Recent(2) strategy: Carol should NOT see Alice's Turn 1 (too old)"
);
assert!(
!has_bob_turn1,
"Recent(2) strategy: Carol should NOT see Bob's Turn 1 (too old)"
);
assert!(
!has_dave_turn2,
"Recent(2) strategy: Carol should NOT see Dave's Turn 2 (too old)"
);
assert!(
!has_bob_turn3,
"Recent(2) strategy: Carol should NOT see Bob's Turn 3 (too old)"
);
let _turn7 = dialogue.run("@Carol and @Alice continue").await.unwrap();
assert_eq!(
carol_clone.get_call_count(),
2,
"Carol should be called twice"
);
let carol_second_payload = &carol_clone.get_payloads()[1];
let turn7_messages = carol_second_payload.to_messages();
let alice_turn7: Vec<_> = turn7_messages
.iter()
.filter(|msg| msg.speaker.name() == "Alice")
.collect();
assert_eq!(
alice_turn7.len(),
0,
"In Turn 7, Carol should NOT see Alice's concurrent response"
);
let has_historical = turn7_messages.iter().any(|msg| {
msg.content.contains("turn 1")
|| msg.content.contains("turn 2")
|| msg.content.contains("turn 3")
|| msg.content.contains("turn 4")
|| msg.content.contains("turn 5")
});
assert!(
!has_historical,
"In Turn 7, Carol should NOT see any historical messages (marked as sent)"
);
}
#[tokio::test]
async fn test_join_in_progress_sequential_mode_with_full_strategy() {
use crate::agent::dialogue::joining_strategy::JoiningStrategy;
use crate::agent::persona::Persona;
let alice = Persona {
name: "Alice".to_string(),
role: "Analyzer".to_string(),
background: "Data analyst".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
let bob = Persona {
name: "Bob".to_string(),
role: "Reviewer".to_string(),
background: "Code reviewer".to_string(),
communication_style: "Critical".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_alice = MockAgent::new(
"Alice",
vec![
"Alice analyzed: turn 1".to_string(),
"Alice analyzed: turn 2".to_string(),
"Alice analyzed: turn 3".to_string(),
"Alice analyzed: turn 4".to_string(),
],
);
let agent_bob = MockAgent::new(
"Bob",
vec![
"Bob reviewed: turn 1".to_string(),
"Bob reviewed: turn 2".to_string(),
"Bob reviewed: turn 3".to_string(),
"Bob reviewed: turn 4".to_string(),
],
);
let alice_clone = agent_alice.clone();
let bob_clone = agent_bob.clone();
let mut dialogue = Dialogue::sequential();
dialogue.add_participant(alice, agent_alice);
dialogue.add_participant(bob, agent_bob);
let _turn1 = dialogue.run("Analyze this data").await.unwrap();
let _turn2 = dialogue.run("Continue analysis").await.unwrap();
assert_eq!(
alice_clone.get_call_count(),
2,
"Alice should be called twice"
);
assert_eq!(bob_clone.get_call_count(), 2, "Bob should be called twice");
let carol = Persona {
name: "Carol".to_string(),
role: "Summarizer".to_string(),
background: "Summary specialist".to_string(),
communication_style: "Concise".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_carol = MockAgent::new(
"Carol",
vec![
"Carol full summary".to_string(),
"Carol summary 2".to_string(),
],
);
let carol_clone = agent_carol.clone();
dialogue.join_in_progress(carol, agent_carol, JoiningStrategy::Full);
let turn3 = dialogue.run("Final analysis").await.unwrap();
assert_eq!(
turn3.len(),
1,
"Sequential mode returns only last agent's output"
);
assert_eq!(
turn3[0].speaker.name(),
"Carol",
"Last agent should be Carol"
);
assert_eq!(
alice_clone.get_call_count(),
3,
"Alice should be called 3 times"
);
assert_eq!(
bob_clone.get_call_count(),
3,
"Bob should be called 3 times"
);
assert_eq!(
carol_clone.get_call_count(),
1,
"Carol should be called once"
);
let carol_payloads = carol_clone.get_payloads();
assert_eq!(carol_payloads.len(), 1, "Carol should receive 1 payload");
let carol_first_payload = &carol_payloads[0];
let messages = carol_first_payload.to_messages();
let turn1_turn2_messages: Vec<_> = messages
.iter()
.filter(|msg| {
let content = msg.content.as_str();
(msg.speaker.name() == "Alice" || msg.speaker.name() == "Bob")
&& (content.contains("turn 1") || content.contains("turn 2"))
})
.collect();
assert_eq!(
turn1_turn2_messages.len(),
4,
"Full strategy in sequential mode: Carol should see ALL 4 historical messages (Alice Turn 1, Bob Turn 1, Alice Turn 2, Bob Turn 2)"
);
let bob_turn3_messages: Vec<_> = messages
.iter()
.filter(|msg| msg.speaker.name() == "Bob" && msg.content.contains("turn 3"))
.collect();
assert_eq!(
bob_turn3_messages.len(),
1,
"Carol should see Bob's Turn 3 output (her immediate input in sequential chain)"
);
let _turn4 = dialogue.run("Continue").await.unwrap();
assert_eq!(
carol_clone.get_call_count(),
2,
"Carol should be called twice"
);
let carol_second_payload = &carol_clone.get_payloads()[1];
let turn4_messages = carol_second_payload.to_messages();
let bob_historical: Vec<_> = turn4_messages
.iter()
.filter(|msg| {
msg.speaker.name() == "Bob"
&& (msg.content.contains("turn 1")
|| msg.content.contains("turn 2")
|| msg.content.contains("turn 3"))
})
.collect();
assert_eq!(
bob_historical.len(),
0,
"In Turn 4, Carol should NOT see Bob's Turn 1, 2, or 3 (marked as sent)"
);
let bob_turn4: Vec<_> = turn4_messages
.iter()
.filter(|msg| msg.speaker.name() == "Bob" && msg.content.contains("turn 4"))
.collect();
assert_eq!(
bob_turn4.len(),
1,
"In Turn 4, Carol should see Bob's Turn 4 output (new chain input)"
);
}
#[tokio::test]
async fn test_join_in_progress_sequential_mode_with_recent_strategy() {
use crate::agent::dialogue::joining_strategy::JoiningStrategy;
use crate::agent::persona::Persona;
let alice = Persona {
name: "Alice".to_string(),
role: "Analyzer".to_string(),
background: "Data analyst".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
let bob = Persona {
name: "Bob".to_string(),
role: "Reviewer".to_string(),
background: "Code reviewer".to_string(),
communication_style: "Critical".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_alice = MockAgent::new(
"Alice",
vec![
"Alice analyzed: turn 1".to_string(),
"Alice analyzed: turn 2".to_string(),
"Alice analyzed: turn 3".to_string(),
"Alice analyzed: turn 4".to_string(),
"Alice analyzed: turn 5".to_string(),
"Alice analyzed: turn 6".to_string(),
"Alice analyzed: turn 7".to_string(),
],
);
let agent_bob = MockAgent::new(
"Bob",
vec![
"Bob reviewed: turn 1".to_string(),
"Bob reviewed: turn 2".to_string(),
"Bob reviewed: turn 3".to_string(),
"Bob reviewed: turn 4".to_string(),
"Bob reviewed: turn 5".to_string(),
"Bob reviewed: turn 6".to_string(),
"Bob reviewed: turn 7".to_string(),
],
);
let alice_clone = agent_alice.clone();
let bob_clone = agent_bob.clone();
let mut dialogue = Dialogue::sequential();
dialogue.add_participant(alice, agent_alice);
dialogue.add_participant(bob, agent_bob);
for i in 1..=5 {
let _ = dialogue.run(format!("Message {}", i)).await.unwrap();
}
assert_eq!(
alice_clone.get_call_count(),
5,
"Alice should be called 5 times"
);
assert_eq!(
bob_clone.get_call_count(),
5,
"Bob should be called 5 times"
);
let carol = Persona {
name: "Carol".to_string(),
role: "Summarizer".to_string(),
background: "Summary specialist".to_string(),
communication_style: "Concise".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_carol = MockAgent::new(
"Carol",
vec![
"Carol recent summary".to_string(),
"Carol summary 2".to_string(),
],
);
let carol_clone = agent_carol.clone();
dialogue.join_in_progress(carol, agent_carol, JoiningStrategy::recent_with_turns(2));
let turn6 = dialogue.run("Recent analysis").await.unwrap();
assert_eq!(
turn6.len(),
1,
"Sequential mode returns only last agent's output"
);
assert_eq!(
turn6[0].speaker.name(),
"Carol",
"Last agent should be Carol"
);
assert_eq!(
alice_clone.get_call_count(),
6,
"Alice should be called 6 times"
);
assert_eq!(
bob_clone.get_call_count(),
6,
"Bob should be called 6 times"
);
assert_eq!(
carol_clone.get_call_count(),
1,
"Carol should be called once"
);
let carol_payloads = carol_clone.get_payloads();
assert_eq!(carol_payloads.len(), 1, "Carol should receive 1 payload");
let carol_first_payload = &carol_payloads[0];
let messages = carol_first_payload.to_messages();
let turn4_turn5_messages: Vec<_> = messages
.iter()
.filter(|msg| {
let content = msg.content.as_str();
(msg.speaker.name() == "Alice" || msg.speaker.name() == "Bob")
&& (content.contains("turn 4") || content.contains("turn 5"))
})
.collect();
assert_eq!(
turn4_turn5_messages.len(),
4,
"Recent(2) strategy in sequential mode: Carol should see 4 recent messages (Alice Turn 4, Bob Turn 4, Alice Turn 5, Bob Turn 5)"
);
let turn1_turn2_turn3: Vec<_> = messages
.iter()
.filter(|msg| {
let content = msg.content.as_str();
content.contains("turn 1")
|| content.contains("turn 2")
|| content.contains("turn 3")
})
.collect();
assert_eq!(
turn1_turn2_turn3.len(),
0,
"Recent(2) strategy: Carol should NOT see Turn 1, 2, or 3 (too old)"
);
let bob_turn6_messages: Vec<_> = messages
.iter()
.filter(|msg| msg.speaker.name() == "Bob" && msg.content.contains("turn 6"))
.collect();
assert_eq!(
bob_turn6_messages.len(),
1,
"Carol should see Bob's Turn 6 output (her immediate input in sequential chain)"
);
let _turn7 = dialogue.run("Continue").await.unwrap();
assert_eq!(
carol_clone.get_call_count(),
2,
"Carol should be called twice"
);
let carol_second_payload = &carol_clone.get_payloads()[1];
let turn7_messages = carol_second_payload.to_messages();
let bob_historical: Vec<_> = turn7_messages
.iter()
.filter(|msg| {
msg.speaker.name() == "Bob"
&& (msg.content.contains("turn 1")
|| msg.content.contains("turn 2")
|| msg.content.contains("turn 3")
|| msg.content.contains("turn 4")
|| msg.content.contains("turn 5")
|| msg.content.contains("turn 6"))
})
.collect();
assert_eq!(
bob_historical.len(),
0,
"In Turn 7, Carol should NOT see Bob's historical turns (marked as sent)"
);
let bob_turn7: Vec<_> = turn7_messages
.iter()
.filter(|msg| msg.speaker.name() == "Bob" && msg.content.contains("turn 7"))
.collect();
assert_eq!(
bob_turn7.len(),
1,
"In Turn 7, Carol should see Bob's Turn 7 output (new chain input)"
);
}
#[tokio::test]
async fn test_join_in_progress_partial_session_sequential_with_full_strategy() {
use crate::agent::dialogue::joining_strategy::JoiningStrategy;
use crate::agent::persona::Persona;
let alice = Persona {
name: "Alice".to_string(),
role: "Analyzer".to_string(),
background: "Data analyst".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
let bob = Persona {
name: "Bob".to_string(),
role: "Reviewer".to_string(),
background: "Code reviewer".to_string(),
communication_style: "Critical".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_alice = MockAgent::new(
"Alice",
vec![
"Alice analyzed: turn 1".to_string(),
"Alice analyzed: turn 2".to_string(),
"Alice analyzed: turn 3".to_string(),
"Alice analyzed: turn 4".to_string(),
],
);
let agent_bob = MockAgent::new(
"Bob",
vec![
"Bob reviewed: turn 1".to_string(),
"Bob reviewed: turn 2".to_string(),
"Bob reviewed: turn 3".to_string(),
"Bob reviewed: turn 4".to_string(),
],
);
let alice_clone = agent_alice.clone();
let mut dialogue = Dialogue::sequential();
dialogue.add_participant(alice, agent_alice);
dialogue.add_participant(bob, agent_bob);
let mut session1 = dialogue.partial_session("Analyze this");
while let Some(turn) = session1.next_turn().await {
turn.unwrap();
}
let mut session2 = dialogue.partial_session("Continue analysis");
while let Some(turn) = session2.next_turn().await {
turn.unwrap();
}
assert_eq!(
alice_clone.get_call_count(),
2,
"Alice called twice via partial_session"
);
let carol = Persona {
name: "Carol".to_string(),
role: "Summarizer".to_string(),
background: "Summary specialist".to_string(),
communication_style: "Concise".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_carol = MockAgent::new(
"Carol",
vec![
"Carol full summary".to_string(),
"Carol summary 2".to_string(),
],
);
let carol_clone = agent_carol.clone();
dialogue.join_in_progress(carol, agent_carol, JoiningStrategy::Full);
let mut session3 = dialogue.partial_session("Final summary");
let mut turn_count = 0;
while let Some(turn) = session3.next_turn().await {
turn.unwrap();
turn_count += 1;
}
assert_eq!(
turn_count, 3,
"Should have 3 turns in sequential partial_session"
);
assert_eq!(carol_clone.get_call_count(), 1, "Carol called once");
let carol_payloads = carol_clone.get_payloads();
assert_eq!(carol_payloads.len(), 1, "Carol should receive 1 payload");
let carol_first_payload = &carol_payloads[0];
let messages = carol_first_payload.to_messages();
let historical_messages: Vec<_> = messages
.iter()
.filter(|msg| {
let content = msg.content.as_str();
(msg.speaker.name() == "Alice" || msg.speaker.name() == "Bob")
&& (content.contains("turn 1") || content.contains("turn 2"))
})
.collect();
assert_eq!(
historical_messages.len(),
4,
"Full strategy in partial_session sequential: Carol should see ALL 4 historical messages (Alice Turn 1, Bob Turn 1, Alice Turn 2, Bob Turn 2)"
);
let bob_turn3: Vec<_> = messages
.iter()
.filter(|msg| msg.speaker.name() == "Bob" && msg.content.contains("turn 3"))
.collect();
assert_eq!(
bob_turn3.len(),
1,
"Carol should see Bob's Turn 3 output via partial_session"
);
let mut session4 = dialogue.partial_session("Continue");
while let Some(turn) = session4.next_turn().await {
turn.unwrap();
}
assert_eq!(carol_clone.get_call_count(), 2, "Carol called twice");
let carol_second_payload = &carol_clone.get_payloads()[1];
let turn4_messages = carol_second_payload.to_messages();
let historical_turn4: Vec<_> = turn4_messages
.iter()
.filter(|msg| {
msg.speaker.name() == "Bob"
&& (msg.content.contains("turn 1")
|| msg.content.contains("turn 2")
|| msg.content.contains("turn 3"))
})
.collect();
assert_eq!(
historical_turn4.len(),
0,
"In Turn 4 via partial_session, Carol should NOT see Turn 1, 2, or 3 (marked as sent)"
);
let bob_turn4: Vec<_> = turn4_messages
.iter()
.filter(|msg| msg.speaker.name() == "Bob" && msg.content.contains("turn 4"))
.collect();
assert_eq!(
bob_turn4.len(),
1,
"In Turn 4 via partial_session, Carol should see Bob's Turn 4 output"
);
}
#[tokio::test]
async fn test_join_in_progress_partial_session_sequential_with_recent_strategy() {
use crate::agent::dialogue::joining_strategy::JoiningStrategy;
use crate::agent::persona::Persona;
let alice = Persona {
name: "Alice".to_string(),
role: "Analyzer".to_string(),
background: "Data analyst".to_string(),
communication_style: "Analytical".to_string(),
visual_identity: None,
capabilities: None,
};
let bob = Persona {
name: "Bob".to_string(),
role: "Reviewer".to_string(),
background: "Code reviewer".to_string(),
communication_style: "Critical".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_alice = MockAgent::new(
"Alice",
vec![
"Alice analyzed: turn 1".to_string(),
"Alice analyzed: turn 2".to_string(),
"Alice analyzed: turn 3".to_string(),
"Alice analyzed: turn 4".to_string(),
"Alice analyzed: turn 5".to_string(),
"Alice analyzed: turn 6".to_string(),
"Alice analyzed: turn 7".to_string(),
],
);
let agent_bob = MockAgent::new(
"Bob",
vec![
"Bob reviewed: turn 1".to_string(),
"Bob reviewed: turn 2".to_string(),
"Bob reviewed: turn 3".to_string(),
"Bob reviewed: turn 4".to_string(),
"Bob reviewed: turn 5".to_string(),
"Bob reviewed: turn 6".to_string(),
"Bob reviewed: turn 7".to_string(),
],
);
let alice_clone = agent_alice.clone();
let mut dialogue = Dialogue::sequential();
dialogue.add_participant(alice, agent_alice);
dialogue.add_participant(bob, agent_bob);
for i in 1..=5 {
let mut session = dialogue.partial_session(format!("Message {}", i));
while let Some(turn) = session.next_turn().await {
turn.unwrap();
}
}
assert_eq!(
alice_clone.get_call_count(),
5,
"Alice called 5 times via partial_session"
);
let carol = Persona {
name: "Carol".to_string(),
role: "Summarizer".to_string(),
background: "Summary specialist".to_string(),
communication_style: "Concise".to_string(),
visual_identity: None,
capabilities: None,
};
let agent_carol = MockAgent::new(
"Carol",
vec![
"Carol recent summary".to_string(),
"Carol summary 2".to_string(),
],
);
let carol_clone = agent_carol.clone();
dialogue.join_in_progress(carol, agent_carol, JoiningStrategy::recent_with_turns(2));
let mut session6 = dialogue.partial_session("Recent summary");
let mut turn_count = 0;
while let Some(turn) = session6.next_turn().await {
turn.unwrap();
turn_count += 1;
}
assert_eq!(
turn_count, 3,
"Should have 3 turns in sequential partial_session"
);
assert_eq!(carol_clone.get_call_count(), 1, "Carol called once");
let carol_payloads = carol_clone.get_payloads();
assert_eq!(carol_payloads.len(), 1, "Carol should receive 1 payload");
let carol_first_payload = &carol_payloads[0];
let messages = carol_first_payload.to_messages();
let turn4_turn5_messages: Vec<_> = messages
.iter()
.filter(|msg| {
let content = msg.content.as_str();
(msg.speaker.name() == "Alice" || msg.speaker.name() == "Bob")
&& (content.contains("turn 4") || content.contains("turn 5"))
})
.collect();
assert_eq!(
turn4_turn5_messages.len(),
4,
"Recent(2) strategy in partial_session sequential: Carol should see 4 recent messages (Alice Turn 4, Bob Turn 4, Alice Turn 5, Bob Turn 5)"
);
let turn1_turn2_turn3: Vec<_> = messages
.iter()
.filter(|msg| {
let content = msg.content.as_str();
content.contains("turn 1")
|| content.contains("turn 2")
|| content.contains("turn 3")
})
.collect();
assert_eq!(
turn1_turn2_turn3.len(),
0,
"Recent(2) strategy: Carol should NOT see Turn 1, 2, or 3 (too old)"
);
let bob_turn6: Vec<_> = messages
.iter()
.filter(|msg| msg.speaker.name() == "Bob" && msg.content.contains("turn 6"))
.collect();
assert_eq!(
bob_turn6.len(),
1,
"Carol should see Bob's Turn 6 output via partial_session"
);
let mut session7 = dialogue.partial_session("Continue");
while let Some(turn) = session7.next_turn().await {
turn.unwrap();
}
assert_eq!(carol_clone.get_call_count(), 2, "Carol called twice");
let carol_second_payload = &carol_clone.get_payloads()[1];
let turn7_messages = carol_second_payload.to_messages();
let historical_turn7: Vec<_> = turn7_messages
.iter()
.filter(|msg| {
msg.speaker.name() == "Bob"
&& (msg.content.contains("turn 1")
|| msg.content.contains("turn 2")
|| msg.content.contains("turn 3")
|| msg.content.contains("turn 4")
|| msg.content.contains("turn 5")
|| msg.content.contains("turn 6"))
})
.collect();
assert_eq!(
historical_turn7.len(),
0,
"In Turn 7 via partial_session, Carol should NOT see historical turns (marked as sent)"
);
let bob_turn7: Vec<_> = turn7_messages
.iter()
.filter(|msg| msg.speaker.name() == "Bob" && msg.content.contains("turn 7"))
.collect();
assert_eq!(
bob_turn7.len(),
1,
"In Turn 7 via partial_session, Carol should see Bob's Turn 7 output"
);
}
}