bamboo-engine 2026.4.30

Execution engine and orchestration for the Bamboo agent framework
Documentation
use bamboo_agent_core::tools::ToolCall;
use bamboo_domain::TaskItemStatus;
use std::collections::HashSet;

use super::TaskItemUpdate;

fn status_to_wire_value(status: &TaskItemStatus) -> &'static str {
    match status {
        TaskItemStatus::Pending => "pending",
        TaskItemStatus::InProgress => "in_progress",
        TaskItemStatus::Completed => "completed",
        TaskItemStatus::Blocked => "blocked",
    }
}

pub(super) fn summarize_updates(updates: &[TaskItemUpdate]) -> String {
    if updates.is_empty() {
        return "No task status changes needed.".to_string();
    }

    let details: Vec<String> = updates
        .iter()
        .map(|update| {
            format!(
                "{} -> {}",
                update.item_id,
                status_to_wire_value(&update.status)
            )
        })
        .collect();

    format!(
        "Applied {} task update(s): {}",
        updates.len(),
        details.join(", ")
    )
}

pub(super) fn parse_item_updates_from_tool_calls(tool_calls: &[ToolCall]) -> Vec<TaskItemUpdate> {
    let mut updates = Vec::new();

    for tool_call in tool_calls {
        if tool_call.function.name != "update_task_item" {
            continue;
        }

        let Ok(args) = serde_json::from_str::<serde_json::Value>(&tool_call.function.arguments)
        else {
            continue;
        };

        let Some(item_id) = args["item_id"].as_str() else {
            continue;
        };
        let Some(status_str) = args["status"].as_str() else {
            continue;
        };

        let status = match status_str {
            "completed" => TaskItemStatus::Completed,
            "blocked" => TaskItemStatus::Blocked,
            _ => continue,
        };

        let criteria_met = args
            .get("criteria_met")
            .or_else(|| args.get("criteriaMet"))
            .and_then(serde_json::Value::as_array)
            .and_then(|values| parse_criteria_met_from_array(values));

        updates.push(TaskItemUpdate {
            item_id: item_id.to_string(),
            status,
            notes: args["notes"].as_str().map(String::from),
            evidence: args["evidence"].as_str().map(String::from),
            blocker: args["blocker"].as_str().map(String::from),
            criteria_met,
        });
    }

    updates
}

fn parse_criteria_met_from_array(values: &[serde_json::Value]) -> Option<Vec<String>> {
    let mut seen = HashSet::new();
    let mut parsed = Vec::new();
    for value in values {
        let Some(text) = value.as_str().map(str::trim) else {
            continue;
        };
        if text.is_empty() {
            continue;
        }
        if seen.insert(text.to_string()) {
            parsed.push(text.to_string());
        }
    }
    if parsed.is_empty() {
        None
    } else {
        Some(parsed)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use bamboo_agent_core::tools::FunctionCall;

    fn update_call(arguments: serde_json::Value) -> ToolCall {
        ToolCall {
            id: "call-1".to_string(),
            tool_type: "function".to_string(),
            function: FunctionCall {
                name: "update_task_item".to_string(),
                arguments: arguments.to_string(),
            },
        }
    }

    #[test]
    fn parse_structured_update_fields() {
        let updates = parse_item_updates_from_tool_calls(&[update_call(serde_json::json!({
            "item_id": "task_1",
            "status": "blocked",
            "notes": "waiting on fixtures",
            "evidence": "pytest auth suite failed on setup",
            "blocker": "need seed database for test env",
            "criteria_met": ["seed db exists", "seed db exists", "  "]
        }))]);

        assert_eq!(updates.len(), 1);
        assert_eq!(updates[0].item_id, "task_1");
        assert_eq!(updates[0].status, TaskItemStatus::Blocked);
        assert_eq!(updates[0].notes.as_deref(), Some("waiting on fixtures"));
        assert_eq!(
            updates[0].evidence.as_deref(),
            Some("pytest auth suite failed on setup")
        );
        assert_eq!(
            updates[0].blocker.as_deref(),
            Some("need seed database for test env")
        );
        assert_eq!(
            updates[0].criteria_met.as_ref(),
            Some(&vec!["seed db exists".to_string()])
        );
    }

    #[test]
    fn parse_structured_update_fields_accepts_camel_case_criteria() {
        let updates = parse_item_updates_from_tool_calls(&[update_call(serde_json::json!({
            "item_id": "task_2",
            "status": "completed",
            "criteriaMet": ["All tests pass", "All tests pass"]
        }))]);

        assert_eq!(updates.len(), 1);
        assert_eq!(updates[0].item_id, "task_2");
        assert_eq!(updates[0].status, TaskItemStatus::Completed);
        assert_eq!(
            updates[0].criteria_met.as_ref(),
            Some(&vec!["All tests pass".to_string()])
        );
    }
}