use std::fmt;
use std::sync::Arc;
use async_trait::async_trait;
use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use cognis_core::error::{CognisError, Result};
use cognis_core::tools::base::BaseTool;
use cognis_core::CancellationToken;
type GeneratorFn = Box<dyn Fn(&str) -> Result<String> + Send + Sync>;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PlanStepStatus {
Pending,
InProgress,
Completed,
Failed,
Skipped,
}
impl fmt::Display for PlanStepStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PlanStepStatus::Pending => write!(f, "Pending"),
PlanStepStatus::InProgress => write!(f, "InProgress"),
PlanStepStatus::Completed => write!(f, "Completed"),
PlanStepStatus::Failed => write!(f, "Failed"),
PlanStepStatus::Skipped => write!(f, "Skipped"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanStep {
pub index: usize,
pub description: String,
pub status: PlanStepStatus,
pub result: Option<String>,
}
impl PlanStep {
pub fn new(index: usize, description: impl Into<String>) -> Self {
Self {
index,
description: description.into(),
status: PlanStepStatus::Pending,
result: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Plan {
pub goal: String,
pub steps: Vec<PlanStep>,
}
impl Plan {
pub fn new(goal: impl Into<String>, steps: Vec<PlanStep>) -> Self {
Self {
goal: goal.into(),
steps,
}
}
pub fn next_step(&mut self) -> Option<&mut PlanStep> {
self.steps
.iter_mut()
.find(|s| s.status == PlanStepStatus::Pending)
}
pub fn is_complete(&self) -> bool {
!self.steps.is_empty()
&& self.steps.iter().all(|s| {
matches!(
s.status,
PlanStepStatus::Completed | PlanStepStatus::Skipped | PlanStepStatus::Failed
)
})
}
pub fn progress(&self) -> (usize, usize) {
let done = self
.steps
.iter()
.filter(|s| {
s.status == PlanStepStatus::Completed || s.status == PlanStepStatus::Skipped
})
.count();
(done, self.steps.len())
}
}
pub trait Planner: Send + Sync {
fn create_plan(&self, goal: &str) -> Result<Plan>;
}
pub struct SimplePlanner;
impl SimplePlanner {
pub fn new() -> Self {
Self
}
}
impl Default for SimplePlanner {
fn default() -> Self {
Self::new()
}
}
impl Planner for SimplePlanner {
fn create_plan(&self, goal: &str) -> Result<Plan> {
let re = Regex::new(r"(?m)^\s*\d+[\.\)]\s*(.+)$")
.map_err(|e| CognisError::Other(format!("Regex error: {}", e)))?;
let steps: Vec<PlanStep> = re
.captures_iter(goal)
.enumerate()
.map(|(i, cap)| PlanStep::new(i, cap[1].trim()))
.collect();
if steps.is_empty() {
Ok(Plan::new(goal, vec![PlanStep::new(0, goal)]))
} else {
Ok(Plan::new(goal, steps))
}
}
}
pub struct TemplatePlanner {
template: String,
generator: Option<GeneratorFn>,
}
impl TemplatePlanner {
pub fn new(template: impl Into<String>) -> Self {
Self {
template: template.into(),
generator: None,
}
}
pub fn with_generator(
mut self,
gen: impl Fn(&str) -> Result<String> + Send + Sync + 'static,
) -> Self {
self.generator = Some(Box::new(gen));
self
}
pub fn expand_template(&self, goal: &str) -> String {
self.template.replace("{goal}", goal)
}
}
impl Planner for TemplatePlanner {
fn create_plan(&self, goal: &str) -> Result<Plan> {
let expanded = self.expand_template(goal);
let plan_text = if let Some(ref gen) = self.generator {
gen(&expanded)?
} else {
expanded.clone()
};
let re = Regex::new(r"(?m)^\s*\d+[\.\)]\s*(.+)$")
.map_err(|e| CognisError::Other(format!("Regex error: {}", e)))?;
let steps: Vec<PlanStep> = re
.captures_iter(&plan_text)
.enumerate()
.map(|(i, cap)| PlanStep::new(i, cap[1].trim()))
.collect();
if steps.is_empty() {
Ok(Plan::new(goal, vec![PlanStep::new(0, goal)]))
} else {
Ok(Plan::new(goal, steps))
}
}
}
#[async_trait]
pub trait StepExecutor: Send + Sync {
async fn execute_step(&self, step: &PlanStep, context: &Value) -> Result<String>;
}
pub struct ToolStepExecutor {
tools: Vec<Arc<dyn BaseTool>>,
}
impl ToolStepExecutor {
pub fn new(tools: Vec<Arc<dyn BaseTool>>) -> Self {
Self { tools }
}
fn find_tool(&self, description: &str) -> Option<Arc<dyn BaseTool>> {
let desc_lower = description.to_lowercase();
self.tools
.iter()
.find(|t| desc_lower.contains(&t.name().to_lowercase()))
.cloned()
}
}
#[async_trait]
impl StepExecutor for ToolStepExecutor {
async fn execute_step(&self, step: &PlanStep, context: &Value) -> Result<String> {
if let Some(tool) = self.find_tool(&step.description) {
let input = if context.is_null() {
Value::String(step.description.clone())
} else {
context.clone()
};
let result = tool.run_json(&input).await?;
match result {
Value::String(s) => Ok(s),
other => Ok(serde_json::to_string(&other).unwrap_or_default()),
}
} else {
Ok(format!(
"No matching tool found. Step: {}",
step.description
))
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanAndExecuteResult {
pub plan: Plan,
pub output: String,
pub total_steps: usize,
pub replans: usize,
pub step_results: Vec<(String, String)>,
}
pub struct PlanAndExecuteAgentBuilder {
planner: Option<Box<dyn Planner>>,
executor: Option<Box<dyn StepExecutor>>,
max_replans: usize,
}
impl PlanAndExecuteAgentBuilder {
pub fn new() -> Self {
Self {
planner: None,
executor: None,
max_replans: 3,
}
}
pub fn planner(mut self, planner: impl Planner + 'static) -> Self {
self.planner = Some(Box::new(planner));
self
}
pub fn executor(mut self, executor: impl StepExecutor + 'static) -> Self {
self.executor = Some(Box::new(executor));
self
}
pub fn max_replans(mut self, max: usize) -> Self {
self.max_replans = max;
self
}
pub fn build(self) -> Result<PlanAndExecuteAgent> {
let planner = self
.planner
.ok_or_else(|| CognisError::Other("PlanAndExecuteAgent requires a planner".into()))?;
let executor = self
.executor
.ok_or_else(|| CognisError::Other("PlanAndExecuteAgent requires an executor".into()))?;
Ok(PlanAndExecuteAgent {
planner,
executor,
max_replans: self.max_replans,
})
}
}
impl Default for PlanAndExecuteAgentBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct PlanAndExecuteAgent {
planner: Box<dyn Planner>,
executor: Box<dyn StepExecutor>,
max_replans: usize,
}
impl PlanAndExecuteAgent {
pub fn builder() -> PlanAndExecuteAgentBuilder {
PlanAndExecuteAgentBuilder::new()
}
pub async fn run(&self, goal: &str) -> Result<PlanAndExecuteResult> {
self.run_with_callback(goal, |_| {}).await
}
pub async fn run_with_callback(
&self,
goal: &str,
on_step: impl Fn(&PlanStep),
) -> Result<PlanAndExecuteResult> {
self.run_with_cancel_and_callback(goal, CancellationToken::new(), on_step)
.await
}
pub async fn run_with_cancel(
&self,
goal: &str,
cancel: CancellationToken,
) -> Result<PlanAndExecuteResult> {
self.run_with_cancel_and_callback(goal, cancel, |_| {})
.await
}
pub async fn run_with_cancel_and_callback(
&self,
goal: &str,
cancel: CancellationToken,
on_step: impl Fn(&PlanStep),
) -> Result<PlanAndExecuteResult> {
cancel.check("cancelled before planning")?;
let mut plan = self.planner.create_plan(goal)?;
let mut replans = 0;
let mut step_results: Vec<(String, String)> = Vec::new();
let mut total_steps = 0;
let context = Value::Null;
loop {
while let Some(step) = plan.next_step() {
cancel.check("cancelled between plan steps")?;
on_step(step);
step.status = PlanStepStatus::InProgress;
let desc = step.description.clone();
let idx = step.index;
let exec_result = tokio::select! {
biased;
_ = cancel.cancelled() => {
return Err(CognisError::Cancelled(
"cancelled during plan-and-execute step".into(),
));
}
r = self.executor.execute_step(step, &context) => r,
};
match exec_result {
Ok(result) => {
plan.steps[idx].status = PlanStepStatus::Completed;
plan.steps[idx].result = Some(result.clone());
step_results.push((desc, result));
total_steps += 1;
}
Err(e) => {
plan.steps[idx].status = PlanStepStatus::Failed;
plan.steps[idx].result = Some(format!("Error: {}", e));
step_results.push((desc, format!("Error: {}", e)));
total_steps += 1;
if replans < self.max_replans {
replans += 1;
for s in plan.steps.iter_mut() {
if s.status == PlanStepStatus::Pending {
s.status = PlanStepStatus::Skipped;
}
}
plan = self.planner.create_plan(goal)?;
break;
} else {
for s in plan.steps.iter_mut() {
if s.status == PlanStepStatus::Pending {
s.status = PlanStepStatus::Skipped;
}
}
}
}
}
}
if plan.is_complete() {
break;
}
}
let output = step_results
.last()
.map(|(_, r)| r.clone())
.unwrap_or_default();
Ok(PlanAndExecuteResult {
plan,
output,
total_steps,
replans,
step_results,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
struct MockPlanner {
steps: Vec<String>,
}
impl MockPlanner {
fn new(steps: Vec<&str>) -> Self {
Self {
steps: steps.into_iter().map(String::from).collect(),
}
}
}
impl Planner for MockPlanner {
fn create_plan(&self, goal: &str) -> Result<Plan> {
let plan_steps = self
.steps
.iter()
.enumerate()
.map(|(i, desc)| PlanStep::new(i, desc.as_str()))
.collect();
Ok(Plan::new(goal, plan_steps))
}
}
struct MockExecutor {
result: String,
}
impl MockExecutor {
fn new(result: &str) -> Self {
Self {
result: result.to_string(),
}
}
}
#[async_trait]
impl StepExecutor for MockExecutor {
async fn execute_step(&self, _step: &PlanStep, _context: &Value) -> Result<String> {
Ok(self.result.clone())
}
}
struct FailingExecutor {
fail_on_indices: Vec<usize>,
call_count: AtomicUsize,
}
impl FailingExecutor {
fn new(fail_on_indices: Vec<usize>) -> Self {
Self {
fail_on_indices,
call_count: AtomicUsize::new(0),
}
}
}
#[async_trait]
impl StepExecutor for FailingExecutor {
async fn execute_step(&self, step: &PlanStep, _context: &Value) -> Result<String> {
let call = self.call_count.fetch_add(1, Ordering::SeqCst);
if self.fail_on_indices.contains(&call) {
Err(CognisError::Other(format!("Step {} failed", step.index)))
} else {
Ok(format!("Result for step {}", step.index))
}
}
}
struct RecordingExecutor {
recorded: std::sync::Mutex<Vec<String>>,
}
impl RecordingExecutor {
fn new() -> Self {
Self {
recorded: std::sync::Mutex::new(Vec::new()),
}
}
fn get_recorded(&self) -> Vec<String> {
self.recorded.lock().unwrap().clone()
}
}
#[async_trait]
impl StepExecutor for RecordingExecutor {
async fn execute_step(&self, step: &PlanStep, _context: &Value) -> Result<String> {
self.recorded.lock().unwrap().push(step.description.clone());
Ok(format!("Done: {}", step.description))
}
}
#[test]
fn plan_step_status_display() {
assert_eq!(PlanStepStatus::Pending.to_string(), "Pending");
assert_eq!(PlanStepStatus::InProgress.to_string(), "InProgress");
assert_eq!(PlanStepStatus::Completed.to_string(), "Completed");
assert_eq!(PlanStepStatus::Failed.to_string(), "Failed");
assert_eq!(PlanStepStatus::Skipped.to_string(), "Skipped");
}
#[test]
fn plan_step_status_equality() {
assert_eq!(PlanStepStatus::Pending, PlanStepStatus::Pending);
assert_ne!(PlanStepStatus::Pending, PlanStepStatus::Completed);
}
#[test]
fn plan_step_new_defaults() {
let step = PlanStep::new(0, "Do something");
assert_eq!(step.index, 0);
assert_eq!(step.description, "Do something");
assert_eq!(step.status, PlanStepStatus::Pending);
assert!(step.result.is_none());
}
#[test]
fn plan_step_serialization() {
let step = PlanStep::new(1, "Test step");
let json = serde_json::to_string(&step).unwrap();
assert!(json.contains("\"index\":1"));
assert!(json.contains("Test step"));
let deserialized: PlanStep = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.index, 1);
assert_eq!(deserialized.description, "Test step");
}
#[test]
fn plan_new_with_steps() {
let plan = Plan::new(
"test goal",
vec![PlanStep::new(0, "step 1"), PlanStep::new(1, "step 2")],
);
assert_eq!(plan.goal, "test goal");
assert_eq!(plan.steps.len(), 2);
}
#[test]
fn plan_next_step_returns_first_pending() {
let mut plan = Plan::new(
"goal",
vec![PlanStep::new(0, "first"), PlanStep::new(1, "second")],
);
let step = plan.next_step().unwrap();
assert_eq!(step.index, 0);
}
#[test]
fn plan_next_step_skips_completed() {
let mut plan = Plan::new(
"goal",
vec![PlanStep::new(0, "first"), PlanStep::new(1, "second")],
);
plan.steps[0].status = PlanStepStatus::Completed;
let step = plan.next_step().unwrap();
assert_eq!(step.index, 1);
}
#[test]
fn plan_next_step_none_when_all_done() {
let mut plan = Plan::new("goal", vec![PlanStep::new(0, "first")]);
plan.steps[0].status = PlanStepStatus::Completed;
assert!(plan.next_step().is_none());
}
#[test]
fn plan_is_complete_all_completed() {
let mut plan = Plan::new("goal", vec![PlanStep::new(0, "a"), PlanStep::new(1, "b")]);
plan.steps[0].status = PlanStepStatus::Completed;
plan.steps[1].status = PlanStepStatus::Completed;
assert!(plan.is_complete());
}
#[test]
fn plan_is_complete_with_skipped() {
let mut plan = Plan::new("goal", vec![PlanStep::new(0, "a"), PlanStep::new(1, "b")]);
plan.steps[0].status = PlanStepStatus::Completed;
plan.steps[1].status = PlanStepStatus::Skipped;
assert!(plan.is_complete());
}
#[test]
fn plan_is_not_complete_with_pending() {
let mut plan = Plan::new("goal", vec![PlanStep::new(0, "a"), PlanStep::new(1, "b")]);
plan.steps[0].status = PlanStepStatus::Completed;
assert!(!plan.is_complete());
}
#[test]
fn plan_is_not_complete_when_empty() {
let plan = Plan::new("goal", vec![]);
assert!(!plan.is_complete());
}
#[test]
fn plan_progress_all_pending() {
let plan = Plan::new(
"goal",
vec![
PlanStep::new(0, "a"),
PlanStep::new(1, "b"),
PlanStep::new(2, "c"),
],
);
assert_eq!(plan.progress(), (0, 3));
}
#[test]
fn plan_progress_some_completed() {
let mut plan = Plan::new(
"goal",
vec![
PlanStep::new(0, "a"),
PlanStep::new(1, "b"),
PlanStep::new(2, "c"),
],
);
plan.steps[0].status = PlanStepStatus::Completed;
plan.steps[1].status = PlanStepStatus::Skipped;
assert_eq!(plan.progress(), (2, 3));
}
#[test]
fn simple_planner_parses_numbered_steps_dot() {
let planner = SimplePlanner::new();
let goal =
"Do the following:\n1. Research the topic\n2. Write an outline\n3. Draft the article";
let plan = planner.create_plan(goal).unwrap();
assert_eq!(plan.steps.len(), 3);
assert_eq!(plan.steps[0].description, "Research the topic");
assert_eq!(plan.steps[1].description, "Write an outline");
assert_eq!(plan.steps[2].description, "Draft the article");
}
#[test]
fn simple_planner_parses_numbered_steps_paren() {
let planner = SimplePlanner::new();
let goal = "1) First step\n2) Second step";
let plan = planner.create_plan(goal).unwrap();
assert_eq!(plan.steps.len(), 2);
assert_eq!(plan.steps[0].description, "First step");
assert_eq!(plan.steps[1].description, "Second step");
}
#[test]
fn simple_planner_single_step_fallback() {
let planner = SimplePlanner::new();
let goal = "Just do this one thing";
let plan = planner.create_plan(goal).unwrap();
assert_eq!(plan.steps.len(), 1);
assert_eq!(plan.steps[0].description, "Just do this one thing");
}
#[test]
fn simple_planner_default() {
let planner = SimplePlanner::default();
let plan = planner.create_plan("test").unwrap();
assert_eq!(plan.steps.len(), 1);
}
#[test]
fn template_planner_expands_template() {
let planner = TemplatePlanner::new("Plan for: {goal}");
let expanded = planner.expand_template("build a house");
assert_eq!(expanded, "Plan for: build a house");
}
#[test]
fn template_planner_with_numbered_template() {
let template = "Steps to achieve {goal}:\n1. Analyze requirements\n2. Implement solution\n3. Test and verify";
let planner = TemplatePlanner::new(template);
let plan = planner.create_plan("the goal").unwrap();
assert_eq!(plan.steps.len(), 3);
assert_eq!(plan.steps[0].description, "Analyze requirements");
}
#[test]
fn template_planner_with_generator() {
let planner = TemplatePlanner::new("Goal: {goal}")
.with_generator(|_prompt| Ok("1. Generated step A\n2. Generated step B".to_string()));
let plan = planner.create_plan("anything").unwrap();
assert_eq!(plan.steps.len(), 2);
assert_eq!(plan.steps[0].description, "Generated step A");
assert_eq!(plan.steps[1].description, "Generated step B");
}
#[test]
fn template_planner_generator_fallback() {
let planner =
TemplatePlanner::new("Goal: {goal}").with_generator(|_| Ok("no numbers here".into()));
let plan = planner.create_plan("test").unwrap();
assert_eq!(plan.steps.len(), 1);
assert_eq!(plan.steps[0].description, "test");
}
#[test]
fn builder_requires_planner() {
let result = PlanAndExecuteAgentBuilder::new()
.executor(MockExecutor::new("ok"))
.build();
assert!(result.is_err());
}
#[test]
fn builder_requires_executor() {
let result = PlanAndExecuteAgentBuilder::new()
.planner(MockPlanner::new(vec!["step"]))
.build();
assert!(result.is_err());
}
#[test]
fn builder_default_max_replans() {
let agent = PlanAndExecuteAgent::builder()
.planner(MockPlanner::new(vec!["step"]))
.executor(MockExecutor::new("ok"))
.build()
.unwrap();
assert_eq!(agent.max_replans, 3);
}
#[test]
fn builder_custom_max_replans() {
let agent = PlanAndExecuteAgent::builder()
.planner(MockPlanner::new(vec!["step"]))
.executor(MockExecutor::new("ok"))
.max_replans(5)
.build()
.unwrap();
assert_eq!(agent.max_replans, 5);
}
#[tokio::test]
async fn run_simple_plan() {
let agent = PlanAndExecuteAgent::builder()
.planner(MockPlanner::new(vec!["step 1", "step 2"]))
.executor(MockExecutor::new("done"))
.build()
.unwrap();
let result = agent.run("test goal").await.unwrap();
assert_eq!(result.total_steps, 2);
assert_eq!(result.replans, 0);
assert_eq!(result.output, "done");
assert!(result.plan.is_complete());
}
#[tokio::test]
async fn run_single_step_plan() {
let agent = PlanAndExecuteAgent::builder()
.planner(MockPlanner::new(vec!["only step"]))
.executor(MockExecutor::new("single result"))
.build()
.unwrap();
let result = agent.run("simple").await.unwrap();
assert_eq!(result.total_steps, 1);
assert_eq!(result.output, "single result");
assert_eq!(result.step_results.len(), 1);
assert_eq!(result.step_results[0].0, "only step");
}
#[tokio::test]
async fn run_with_callback_invoked() {
let agent = PlanAndExecuteAgent::builder()
.planner(MockPlanner::new(vec!["a", "b", "c"]))
.executor(MockExecutor::new("ok"))
.build()
.unwrap();
let callback_count = std::sync::Arc::new(AtomicUsize::new(0));
let count_clone = callback_count.clone();
let result = agent
.run_with_callback("goal", move |_step| {
count_clone.fetch_add(1, Ordering::SeqCst);
})
.await
.unwrap();
assert_eq!(result.total_steps, 3);
assert_eq!(callback_count.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn run_replan_on_failure() {
let agent = PlanAndExecuteAgent::builder()
.planner(MockPlanner::new(vec!["step 1", "step 2"]))
.executor(FailingExecutor::new(vec![0]))
.max_replans(2)
.build()
.unwrap();
let result = agent.run("goal").await.unwrap();
assert_eq!(result.replans, 1);
assert!(result.plan.is_complete());
}
#[tokio::test]
async fn run_exhaust_replans() {
let agent = PlanAndExecuteAgent::builder()
.planner(MockPlanner::new(vec!["step"]))
.executor(FailingExecutor::new(vec![0, 1, 2, 3]))
.max_replans(2)
.build()
.unwrap();
let result = agent.run("goal").await.unwrap();
assert_eq!(result.replans, 2);
}
#[tokio::test]
async fn run_step_results_collected() {
let agent = PlanAndExecuteAgent::builder()
.planner(MockPlanner::new(vec!["alpha", "beta", "gamma"]))
.executor(MockExecutor::new("result"))
.build()
.unwrap();
let result = agent.run("goal").await.unwrap();
assert_eq!(result.step_results.len(), 3);
assert_eq!(
result.step_results[0],
("alpha".to_string(), "result".to_string())
);
assert_eq!(
result.step_results[1],
("beta".to_string(), "result".to_string())
);
assert_eq!(
result.step_results[2],
("gamma".to_string(), "result".to_string())
);
}
#[tokio::test]
async fn run_executor_sees_all_steps_in_order() {
let recording = Arc::new(RecordingExecutor::new());
let agent = PlanAndExecuteAgent::builder()
.planner(MockPlanner::new(vec!["first", "second", "third"]))
.executor(RecordingExecutorWrapper(recording.clone()))
.build()
.unwrap();
agent.run("goal").await.unwrap();
let recorded = recording.get_recorded();
assert_eq!(recorded, vec!["first", "second", "third"]);
}
struct RecordingExecutorWrapper(Arc<RecordingExecutor>);
#[async_trait]
impl StepExecutor for RecordingExecutorWrapper {
async fn execute_step(&self, step: &PlanStep, context: &Value) -> Result<String> {
self.0.execute_step(step, context).await
}
}
#[tokio::test]
async fn run_max_replans_zero_no_replan() {
let agent = PlanAndExecuteAgent::builder()
.planner(MockPlanner::new(vec!["fail step"]))
.executor(FailingExecutor::new(vec![0]))
.max_replans(0)
.build()
.unwrap();
let result = agent.run("goal").await.unwrap();
assert_eq!(result.replans, 0);
assert!(result.output.contains("Error"));
}
#[tokio::test]
async fn result_output_is_last_step_result() {
let agent = PlanAndExecuteAgent::builder()
.planner(MockPlanner::new(vec!["a", "b"]))
.executor(MockExecutor::new("final"))
.build()
.unwrap();
let result = agent.run("goal").await.unwrap();
assert_eq!(result.output, "final");
}
#[test]
fn result_serialization() {
let result = PlanAndExecuteResult {
plan: Plan::new("goal", vec![]),
output: "output".to_string(),
total_steps: 2,
replans: 1,
step_results: vec![("s1".to_string(), "r1".to_string())],
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"output\":\"output\""));
assert!(json.contains("\"total_steps\":2"));
assert!(json.contains("\"replans\":1"));
}
#[tokio::test]
async fn tool_executor_matches_tool_by_name() {
use cognis_core::tools::SimpleTool;
let tool: Arc<dyn BaseTool> = Arc::new(SimpleTool::new(
"search",
"Search for information",
|q: &str| Ok(format!("Found: {}", q)),
));
let executor = ToolStepExecutor::new(vec![tool]);
let step = PlanStep::new(0, "search for rust documentation");
let result = executor.execute_step(&step, &Value::Null).await.unwrap();
assert!(result.contains("Found:"));
}
#[tokio::test]
async fn tool_executor_no_match_passthrough() {
let executor = ToolStepExecutor::new(vec![]);
let step = PlanStep::new(0, "do something");
let result = executor.execute_step(&step, &Value::Null).await.unwrap();
assert!(result.contains("No matching tool found"));
}
#[tokio::test]
async fn tool_executor_uses_context_when_provided() {
use cognis_core::tools::SimpleTool;
let tool: Arc<dyn BaseTool> = Arc::new(SimpleTool::new("calc", "Calculator", |q: &str| {
Ok(format!("Calculated: {}", q))
}));
let executor = ToolStepExecutor::new(vec![tool]);
let step = PlanStep::new(0, "calc something");
let context = Value::String("2+2".to_string());
let result = executor.execute_step(&step, &context).await.unwrap();
assert!(result.contains("Calculated:"));
}
}