bamboo-engine 2026.7.23

Execution engine and orchestration for the Bamboo agent framework
Documentation
//! Session setup helpers for the agent loop runner.

use chrono::Utc;

use crate::runtime::config::AgentLoopConfig;
use crate::runtime::task_context::TaskLoopContext;
use bamboo_agent_core::tools::ToolExecutor;
use bamboo_agent_core::{AgentError, Message, PromptSnapshot, Session};
use bamboo_metrics::MetricsCollector;
use bamboo_skills::runtime_metadata::{
    SKILL_RUNTIME_ACTIVATION_ERROR_KEY, SKILL_RUNTIME_ACTIVATION_GENERATION_KEY,
    SKILL_RUNTIME_SELECTED_SKILL_IDS_KEY, SKILL_RUNTIME_SELECTED_SKILL_MODE_KEY,
    SKILL_RUNTIME_SELECTED_SKILL_REVISIONS_KEY, SKILL_RUNTIME_SELECTION_COUNT_KEY,
    SKILL_RUNTIME_SELECTION_SOURCE_KEY, SKILL_RUNTIME_SELECTION_TRACE_KEY,
};
use bamboo_tools::exposure::activated_discoverable_tools;

use super::logging::DebugLogger;

pub(crate) mod compaction;
pub(crate) mod prompt_envelope;
pub(crate) mod prompt_setup;
pub(crate) mod skill_context;
pub(crate) mod tool_schemas;

pub fn read_prompt_snapshot(session: &Session) -> Option<PromptSnapshot> {
    prompt_setup::read_prompt_snapshot_metadata(session)
}

pub fn refresh_prompt_snapshot(session: &mut Session) {
    prompt_setup::refresh_prompt_snapshot_from_session(session)
}

#[allow(clippy::too_many_arguments)]
pub(crate) async fn prepare_session_for_loop(
    session: &mut Session,
    initial_message: &str,
    config: &AgentLoopConfig,
    tools: &dyn ToolExecutor,
    metrics_collector: Option<&MetricsCollector>,
    session_id: &str,
    debug_logger: &DebugLogger,
    must_resume_pinned_activation: bool,
) -> super::Result<Option<TaskLoopContext>> {
    let skill_result = match skill_context::load_skill_context(
        config,
        session,
        session_id,
        initial_message,
        must_resume_pinned_activation,
    )
    .await
    {
        Ok(result) => result,
        Err(error) => {
            session.metadata.insert(
                SKILL_RUNTIME_ACTIVATION_ERROR_KEY.to_string(),
                error.clone(),
            );
            if let Some(persistence) = config.persistence.as_ref() {
                if let Err(save_error) = persistence.save_runtime_session(session).await {
                    tracing::warn!(
                        "[{}] Failed to persist workflow activation setup error: {}",
                        session_id,
                        save_error
                    );
                }
            }
            return Err(AgentError::Tool(format!(
                "Workflow activation failed before model execution: {error}"
            )));
        }
    };
    session.metadata.remove(SKILL_RUNTIME_ACTIVATION_ERROR_KEY);
    let mut skill_context = skill_result.context.clone();

    if let Some(source) = skill_result.selection_source.as_deref() {
        debug_logger.log_event(
            session_id,
            "skill_selection_runtime_state",
            serde_json::json!({
                "source": source,
                "selected_skill_ids": skill_result.selected_skill_ids,
                "selected_skill_mode": skill_result.selected_skill_mode,
                "request_hint_present": skill_result.request_hint_present
            }),
        );
        session.metadata.insert(
            SKILL_RUNTIME_SELECTION_SOURCE_KEY.to_string(),
            source.to_string(),
        );
        session.metadata.insert(
            SKILL_RUNTIME_SELECTION_COUNT_KEY.to_string(),
            skill_result.selected_skill_ids.len().to_string(),
        );
        session.metadata.insert(
            SKILL_RUNTIME_SELECTED_SKILL_IDS_KEY.to_string(),
            serde_json::to_string(&skill_result.selected_skill_ids).unwrap_or("[]".to_string()),
        );
        if let Some(revision) = skill_result.catalog_revision {
            session.metadata.insert(
                SKILL_RUNTIME_ACTIVATION_GENERATION_KEY.to_string(),
                revision.to_string(),
            );
        } else {
            session
                .metadata
                .remove(SKILL_RUNTIME_ACTIVATION_GENERATION_KEY);
        }
        session.metadata.insert(
            SKILL_RUNTIME_SELECTED_SKILL_REVISIONS_KEY.to_string(),
            serde_json::to_string(&skill_result.skill_revisions).unwrap_or("{}".to_string()),
        );
        session.metadata.insert(
            SKILL_RUNTIME_SELECTION_TRACE_KEY.to_string(),
            serde_json::json!({
                "source": source,
                "selected_skill_ids": skill_result.selected_skill_ids,
                "selected_skill_mode": skill_result.selected_skill_mode,
                "request_hint_present": skill_result.request_hint_present
            })
            .to_string(),
        );
        if let Some(mode) = skill_result.selected_skill_mode.as_ref() {
            session.metadata.insert(
                SKILL_RUNTIME_SELECTED_SKILL_MODE_KEY.to_string(),
                mode.clone(),
            );
        } else {
            session
                .metadata
                .remove(SKILL_RUNTIME_SELECTED_SKILL_MODE_KEY);
        }

        skill_context::reset_explicit_activation_state(session, &skill_result);

        // Runtime tools authorize skill loads through the shared session repository.
        // Publish this run's resolved IDs before the first model/tool call so they
        // never observe a missing or previous-run allowlist from the cache.
        if let Some(persistence) = config.persistence.as_ref() {
            if let Err(error) = persistence.save_runtime_session(session).await {
                if let Some(skill_manager) = config.skill_manager.as_ref() {
                    let workspace = session.workspace_path_meta().map(std::path::PathBuf::from);
                    let _ = skill_manager
                        .release_activation_for_workspace(session_id, workspace.as_deref())
                        .await;
                }
                return Err(AgentError::Tool(format!(
                    "Workflow activation metadata could not be published before tool/model execution: {error}"
                )));
            }
        }

        let explicit_activation =
            match skill_context::activate_explicit_skill(tools, session, session_id, &skill_result)
                .await
            {
                Ok(activation) => activation,
                Err(error) => {
                    if let Some(skill_manager) = config.skill_manager.as_ref() {
                        let workspace = session.workspace_path_meta().map(std::path::PathBuf::from);
                        let _ = skill_manager
                            .release_activation_for_workspace(session_id, workspace.as_deref())
                            .await;
                    }
                    return Err(AgentError::Tool(format!(
                        "Explicit workflow activation failed before model execution: {error}"
                    )));
                }
            };
        if let Some(activated_context) = explicit_activation {
            skill_context = activated_context;
            if let Some(persistence) = config.persistence.as_ref() {
                if let Err(error) = persistence.save_runtime_session(session).await {
                    if let Some(skill_manager) = config.skill_manager.as_ref() {
                        let workspace = session.workspace_path_meta().map(std::path::PathBuf::from);
                        let _ = skill_manager
                            .release_activation_for_workspace(session_id, workspace.as_deref())
                            .await;
                    }
                    return Err(AgentError::Tool(format!(
                        "Explicit workflow loaded-state could not be published before model execution: {error}"
                    )));
                }
            }
        }
    }

    let tool_schemas =
        tool_schemas::resolve_available_tool_schemas_for_session(config, tools, session);
    let base_prompt_for_language =
        prompt_setup::resolve_base_prompt_for_language(config, session).to_string();
    let activated = activated_discoverable_tools(session);
    let tool_guide_context = prompt_setup::build_tool_guide_context(
        config,
        &tool_schemas,
        &base_prompt_for_language,
        session_id,
        &activated,
    );

    prompt_setup::apply_system_prompt_contexts(
        session,
        config,
        &skill_context,
        &tool_guide_context,
    );

    if !config.skip_initial_user_message {
        session.add_message(Message::user(initial_message.to_string()));
        if let Some(metrics) = metrics_collector {
            metrics.session_message_count(
                session_id.to_string(),
                session.messages.len() as u32,
                Utc::now(),
            );
        }
    }

    compaction::compact_oversized_tool_messages(session, config, session_id).await;

    let task_context = TaskLoopContext::from_session(session);
    if task_context.is_some() {
        tracing::debug!("[{}] TaskLoopContext initialized", session_id);
    }
    Ok(task_context)
}

#[cfg(test)]
mod tests;