bamboo-engine 2026.7.24

Execution engine and orchestration for the Bamboo agent framework
Documentation
use chrono::Utc;
use tokio::sync::mpsc;

use super::finalize_session;
use crate::runtime::config::AgentLoopConfig;
use crate::runtime::task_context::TaskLoopContext;
use bamboo_agent_core::{AgentEvent, Session};
use bamboo_domain::{AgentRuntimeState, AgentStatusState, TaskItem, TaskItemStatus, TaskList};
use bamboo_skills::{SkillManager, SkillStoreConfig};
use std::sync::Arc;

fn completed_task_list(session_id: &str) -> TaskList {
    TaskList {
        session_id: session_id.to_string(),
        title: "Done".to_string(),
        items: vec![TaskItem {
            id: "done-1".to_string(),
            description: "Completed task".to_string(),
            status: TaskItemStatus::Completed,
            depends_on: Vec::new(),
            notes: String::new(),
            ..TaskItem::default()
        }],
        created_at: Utc::now(),
        updated_at: Utc::now(),
    }
}

#[tokio::test]
async fn finalize_session_emits_complete_when_not_sent_yet() {
    let mut session = Session::new("finalize-session-1", "test-model");
    let (event_tx, mut event_rx) = mpsc::channel(8);

    finalize_session(
        None,
        &mut session,
        &event_tx,
        "finalize-session-1",
        &AgentLoopConfig::default(),
        None,
        false,
        &mut AgentRuntimeState::new("finalize-session-1"),
    )
    .await;

    let event = event_rx.recv().await.expect("complete event expected");
    match event {
        AgentEvent::Complete { usage } => {
            assert_eq!(usage.prompt_tokens, 0);
            assert_eq!(usage.completion_tokens, 0);
            assert_eq!(usage.total_tokens, 0);
        }
        other => panic!("unexpected event: {other:?}"),
    }
}

#[tokio::test]
async fn finalize_session_skips_complete_when_already_sent() {
    let mut session = Session::new("finalize-session-2", "test-model");
    let (event_tx, mut event_rx) = mpsc::channel(8);

    finalize_session(
        None,
        &mut session,
        &event_tx,
        "finalize-session-2",
        &AgentLoopConfig::default(),
        None,
        true,
        &mut AgentRuntimeState::new("finalize-session-2"),
    )
    .await;

    assert!(event_rx.try_recv().is_err());
}

#[tokio::test]
async fn finalize_session_syncs_task_context_and_emits_task_completed() {
    let mut session = Session::new("finalize-session-3", "test-model");
    session.set_task_list(completed_task_list("finalize-session-3"));
    let mut task_context =
        TaskLoopContext::from_session(&session).expect("task context should exist");
    task_context.current_round = 3;

    let (event_tx, mut event_rx) = mpsc::channel(8);

    finalize_session(
        Some(task_context),
        &mut session,
        &event_tx,
        "finalize-session-3",
        &AgentLoopConfig::default(),
        None,
        true,
        &mut AgentRuntimeState::new("finalize-session-3"),
    )
    .await;

    let event = event_rx
        .recv()
        .await
        .expect("task completed event expected");
    match event {
        AgentEvent::TaskListCompleted {
            session_id,
            total_rounds,
            ..
        } => {
            assert_eq!(session_id, "finalize-session-3");
            assert_eq!(total_rounds, 4);
        }
        other => panic!("unexpected event: {other:?}"),
    }

    assert!(session.task_list.is_some());
    assert_eq!(
        session
            .metadata
            .get("task_list_version")
            .map(String::as_str),
        Some("0")
    );
    assert!(event_rx.try_recv().is_err());
}

#[tokio::test]
async fn finalize_releases_completed_activation_but_retains_suspended_activation() {
    let directory = tempfile::tempdir().expect("tempdir");
    let skill_root = directory.path().join("skills/finalize-demo");
    tokio::fs::create_dir_all(&skill_root)
        .await
        .expect("skill root");
    tokio::fs::write(
        skill_root.join("SKILL.md"),
        "---\nname: finalize-demo\ndescription: finalize\n---\nfinalize\n",
    )
    .await
    .expect("skill");
    let manager = Arc::new(SkillManager::with_config(SkillStoreConfig {
        skills_dir: directory.path().join("skills"),
        ..Default::default()
    }));
    manager.initialize().await.expect("initialize");
    let ids = vec!["finalize-demo".to_string()];
    for session_id in ["completed-pin", "suspended-pin"] {
        manager
            .store()
            .pin_current_activation(session_id, &ids, None)
            .await
            .expect("pin activation");
    }
    let config = AgentLoopConfig {
        skill_manager: Some(manager.clone()),
        ..Default::default()
    };
    let (event_tx, _event_rx) = mpsc::channel(8);
    let mut completed_session = Session::new("completed-pin", "model");
    finalize_session(
        None,
        &mut completed_session,
        &event_tx,
        "completed-pin",
        &config,
        None,
        true,
        &mut AgentRuntimeState::new("completed-pin"),
    )
    .await;
    assert!(manager
        .store()
        .activation_descriptor("completed-pin")
        .await
        .is_none());

    let mut suspended_session = Session::new("suspended-pin", "model");
    let mut suspended_state = AgentRuntimeState::new("suspended-pin");
    suspended_state.status = AgentStatusState::Suspended;
    finalize_session(
        None,
        &mut suspended_session,
        &event_tx,
        "suspended-pin",
        &config,
        None,
        true,
        &mut suspended_state,
    )
    .await;
    assert!(manager
        .store()
        .activation_descriptor("suspended-pin")
        .await
        .is_some());
}