use llm_toolkit::agent::{Agent, AgentError, AgentOutput, DynamicAgent, Payload};
use llm_toolkit::orchestrator::{
BlueprintWorkflow, ExecutionJournal, ParallelOrchestrator, StepRecord, StepStatus, StrategyMap,
StrategyStep,
};
use serde_json::{Value as JsonValue, json};
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
#[derive(Clone)]
struct MockSuccessAgent {
name: String,
output: JsonValue,
}
impl MockSuccessAgent {
fn new(name: impl Into<String>, output: JsonValue) -> Self {
Self {
name: name.into(),
output,
}
}
}
#[async_trait::async_trait]
impl Agent for MockSuccessAgent {
type Output = JsonValue;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Mock success agent for journal testing";
&EXPERTISE
}
async fn execute(&self, _input: Payload) -> Result<Self::Output, AgentError> {
Ok(self.output.clone())
}
}
#[async_trait::async_trait]
impl DynamicAgent for MockSuccessAgent {
fn name(&self) -> String {
self.name.clone()
}
fn description(&self) -> &str {
"Mock success agent for journal testing"
}
async fn execute_dynamic(&self, input: Payload) -> Result<AgentOutput, AgentError> {
let output = self.execute(input).await?;
Ok(AgentOutput::Success(output))
}
}
#[derive(Clone)]
struct MockFailingAgent {
name: String,
}
impl MockFailingAgent {
fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
}
#[async_trait::async_trait]
impl Agent for MockFailingAgent {
type Output = JsonValue;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Mock failing agent for journal testing";
&EXPERTISE
}
async fn execute(&self, _input: Payload) -> Result<Self::Output, AgentError> {
Err(AgentError::ExecutionFailed(
"Intentional failure".to_string(),
))
}
}
#[async_trait::async_trait]
impl DynamicAgent for MockFailingAgent {
fn name(&self) -> String {
self.name.clone()
}
fn description(&self) -> &str {
"Mock failing agent for journal testing"
}
async fn execute_dynamic(&self, input: Payload) -> Result<AgentOutput, AgentError> {
self.execute(input).await?;
unreachable!()
}
}
#[test]
fn test_execution_journal_creation() {
let strategy = StrategyMap::new("Test Strategy".to_string());
let journal = ExecutionJournal::new(strategy.clone());
assert_eq!(journal.strategy.goal, "Test Strategy");
assert_eq!(journal.steps.len(), 0);
}
#[test]
fn test_execution_journal_record_step() {
let mut strategy = StrategyMap::new("Test Strategy".to_string());
strategy.add_step(StrategyStep::new(
"step_1".to_string(),
"First step".to_string(),
"TestAgent".to_string(),
"Do something".to_string(),
"Step 1".to_string(),
));
let mut journal = ExecutionJournal::new(strategy.clone());
let step = &strategy.steps[0];
let record = StepRecord::from_step(
step,
StepStatus::Completed,
Some(json!({"result": "success"})),
None,
);
journal.record_step(record);
assert_eq!(journal.steps.len(), 1);
assert_eq!(journal.steps[0].step_id, "step_1");
assert_eq!(journal.steps[0].title, "First step");
assert!(matches!(journal.steps[0].status, StepStatus::Completed));
}
#[test]
fn test_step_record_with_timestamp() {
let step = StrategyStep::new(
"step_1".to_string(),
"Test step".to_string(),
"TestAgent".to_string(),
"Do something".to_string(),
"Step 1".to_string(),
);
let fixed_timestamp = 1234567890u64;
let record =
StepRecord::with_timestamp(&step, StepStatus::Running, None, None, fixed_timestamp);
assert_eq!(record.recorded_at_ms, fixed_timestamp);
assert!(matches!(record.status, StepStatus::Running));
}
#[tokio::test]
async fn test_parallel_orchestrator_journal_on_success() {
let mut strategy = StrategyMap::new("Test Journal".to_string());
strategy.add_step(StrategyStep::new(
"step_1".to_string(),
"First step".to_string(),
"Agent1".to_string(),
"Process task".to_string(),
"Step 1".to_string(),
));
strategy.add_step(StrategyStep::new(
"step_2".to_string(),
"Second step".to_string(),
"Agent2".to_string(),
"Process task".to_string(),
"Step 2".to_string(),
));
let blueprint = BlueprintWorkflow::new("Test workflow".to_string());
let mut orchestrator = ParallelOrchestrator::new(blueprint);
orchestrator.set_strategy_map(strategy);
let agent1 = Arc::new(MockSuccessAgent::new("Agent1", json!({"data": "step1"})));
let agent2 = Arc::new(MockSuccessAgent::new("Agent2", json!({"data": "step2"})));
orchestrator.add_agent("Agent1", agent1);
orchestrator.add_agent("Agent2", agent2);
let result = orchestrator
.execute("test task", CancellationToken::new(), None, None)
.await
.unwrap();
assert!(result.success);
assert_eq!(result.steps_executed, 2);
let journal = result.journal.expect("Journal should be captured");
assert_eq!(journal.strategy.goal, "Test Journal");
assert_eq!(journal.steps.len(), 2);
let step1_record = journal
.steps
.iter()
.find(|s| s.step_id == "step_1")
.expect("step_1 should be in journal");
let step2_record = journal
.steps
.iter()
.find(|s| s.step_id == "step_2")
.expect("step_2 should be in journal");
assert!(matches!(step1_record.status, StepStatus::Completed));
assert!(matches!(step2_record.status, StepStatus::Completed));
}
#[derive(Clone)]
struct NoRedesignAgent;
#[async_trait::async_trait]
impl Agent for NoRedesignAgent {
type Output = String;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Mock agent that never triggers redesign";
&EXPERTISE
}
async fn execute(&self, _input: Payload) -> Result<Self::Output, AgentError> {
Ok("FAIL".to_string())
}
}
#[derive(Clone)]
struct DummyStrategyGenerator;
#[async_trait::async_trait]
impl Agent for DummyStrategyGenerator {
type Output = StrategyMap;
type Expertise = &'static str;
fn expertise(&self) -> &&'static str {
const EXPERTISE: &str = "Dummy strategy generator (should not be called)";
&EXPERTISE
}
async fn execute(&self, _input: Payload) -> Result<Self::Output, AgentError> {
panic!("DummyStrategyGenerator should never be called");
}
}
#[tokio::test]
async fn test_parallel_orchestrator_journal_on_failure() {
let mut strategy = StrategyMap::new("Test Failure Journal".to_string());
strategy.add_step(StrategyStep::new(
"step_1".to_string(),
"Failing step".to_string(),
"FailAgent".to_string(),
"Process task".to_string(),
"Step 1".to_string(),
));
strategy.add_step(StrategyStep::new(
"step_2".to_string(),
"Dependent step".to_string(),
"Agent2".to_string(),
"Process {{ step_1_output }}".to_string(),
"Step 2".to_string(),
));
let blueprint = BlueprintWorkflow::new("Test workflow".to_string());
let internal_agent = Box::new(NoRedesignAgent);
let internal_json_agent = Box::new(DummyStrategyGenerator);
let mut orchestrator =
ParallelOrchestrator::with_internal_agents(blueprint, internal_agent, internal_json_agent);
orchestrator.set_strategy_map(strategy);
let fail_agent = Arc::new(MockFailingAgent::new("FailAgent"));
let agent2 = Arc::new(MockSuccessAgent::new("Agent2", json!({"data": "step2"})));
orchestrator.add_agent("FailAgent", fail_agent);
orchestrator.add_agent("Agent2", agent2);
let result = orchestrator
.execute("test task", CancellationToken::new(), None, None)
.await
.unwrap();
assert!(!result.success);
let journal = result
.journal
.expect("Journal should be captured on failure");
assert_eq!(journal.steps.len(), 2);
let step1_record = journal
.steps
.iter()
.find(|s| s.step_id == "step_1")
.expect("step_1 should be in journal");
let step2_record = journal
.steps
.iter()
.find(|s| s.step_id == "step_2")
.expect("step_2 should be in journal");
assert!(matches!(step1_record.status, StepStatus::Failed));
assert!(step1_record.error.is_some());
assert!(matches!(step2_record.status, StepStatus::Skipped));
}
#[tokio::test]
async fn test_parallel_orchestrator_execution_journal_accessor() {
let mut strategy = StrategyMap::new("Test Journal Accessor".to_string());
strategy.add_step(StrategyStep::new(
"step_1".to_string(),
"Step 1".to_string(),
"Agent1".to_string(),
"Process".to_string(),
"Step 1".to_string(),
));
let blueprint = BlueprintWorkflow::new("Test workflow".to_string());
let mut orchestrator = ParallelOrchestrator::new(blueprint);
orchestrator.set_strategy_map(strategy);
let agent = Arc::new(MockSuccessAgent::new("Agent1", json!({"result": "ok"})));
orchestrator.add_agent("Agent1", agent);
assert!(orchestrator.execution_journal().is_none());
let _ = orchestrator
.execute("test", CancellationToken::new(), None, None)
.await
.unwrap();
let journal = orchestrator
.execution_journal()
.expect("Journal should be available after execution");
assert_eq!(journal.strategy.goal, "Test Journal Accessor");
assert_eq!(journal.steps.len(), 1);
}