use async_trait::async_trait;
use bamboo_domain::session::runtime_state::ChildWaitPolicy;
use bamboo_domain::Session;
use std::collections::HashMap;
mod actions;
mod helpers;
#[cfg(test)]
mod tests;
pub use actions::{
assemble_session_tree, build_session_tree_action, cancel_child_action, create_child_action,
delete_child_action, get_child_action, list_children_action, run_child_action,
send_message_to_child_action, update_child_action, SessionTreeNode,
};
pub use helpers::{
compute_status_guidance, format_child_assignment, map_child_entry, metadata_text,
normalize_non_empty_optional, normalize_required_text, replace_or_append_last_user_message,
truncate_after_index, truncate_after_last_user,
};
#[derive(Debug, thiserror::Error)]
pub enum ChildSessionError {
#[error("session not found: {0}")]
NotFound(String),
#[error("session is not a root session: {0}")]
NotRootSession(String),
#[error("session is not a child session: {0}")]
NotChildSession(String),
#[error("child session {child_id} does not belong to parent {parent_id}")]
NotChildOfParent { child_id: String, parent_id: String },
#[error("{0}")]
InvalidArguments(String),
#[error("{0}")]
Execution(String),
}
#[derive(Debug, Clone)]
pub struct ChildSessionEntry {
pub child_session_id: String,
pub title: String,
pub pinned: bool,
pub message_count: usize,
pub updated_at: String,
pub last_run_status: Option<String>,
pub last_run_error: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DeleteChildResult {
pub deleted: bool,
pub cancelled_running_child: bool,
}
#[derive(Debug, Clone)]
pub struct ChildRunnerInfo {
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
pub last_tool_name: Option<String>,
pub last_tool_phase: Option<String>,
pub last_event_at: Option<chrono::DateTime<chrono::Utc>>,
pub round_count: u32,
}
pub const DELEGATION_NOTE: &str = r#"---
You are running as a delegated sub-agent, spawned by a parent agent to handle a focused task. You have the full capabilities of a top-level agent, including the ability to spawn your own sub-agents when that helps. Stay focused on the assigned task, and when you finish, report a concise conclusion first (the parent receives your final message as the task result)."#;
#[derive(Debug, Clone)]
pub struct CreateChildInput {
pub parent_session: Session,
pub child_id: String,
pub title: String,
pub responsibility: String,
pub assignment_prompt: String,
pub subagent_type: String,
pub workspace: String,
pub model_override: Option<String>,
pub model_ref_override: Option<bamboo_domain::ProviderModelRef>,
pub runtime_metadata: std::collections::HashMap<String, String>,
pub auto_run: bool,
pub reasoning_effort: Option<bamboo_domain::ReasoningEffort>,
pub lifecycle: Option<String>,
pub resident_name: Option<String>,
pub resident_context: Option<String>,
pub disabled_tools: Option<std::collections::BTreeSet<String>>,
pub context_fork: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct CreateChildResult {
pub child_session_id: String,
pub model: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct QueuedInjectedMessage {
pub content: String,
#[serde(default)]
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[async_trait]
pub trait ChildSessionPort: Send + Sync {
async fn load_root_session(&self, root_id: &str) -> Result<Session, ChildSessionError>;
async fn load_child_for_parent(
&self,
parent_id: &str,
child_id: &str,
) -> Result<Session, ChildSessionError>;
async fn save_child_session(&self, child: &mut Session) -> Result<(), ChildSessionError>;
async fn is_child_running(&self, child_id: &str) -> bool;
async fn list_children(&self, parent_id: &str) -> Vec<ChildSessionEntry>;
async fn enqueue_child_run(
&self,
parent: &Session,
child: &Session,
) -> Result<(), ChildSessionError>;
async fn cancel_child_run_and_wait(&self, child_id: &str) -> Result<(), ChildSessionError>;
async fn delete_child_session(
&self,
parent_id: &str,
child_id: &str,
) -> Result<DeleteChildResult, ChildSessionError>;
async fn get_child_runner_info(&self, child_id: &str) -> Option<ChildRunnerInfo>;
async fn register_parent_wait_for_child(
&self,
parent_session_id: &str,
child_session_id: &str,
tool_call_id: Option<&str>,
) -> Result<(), ChildSessionError>;
async fn register_parent_wait_for_children(
&self,
parent_session_id: &str,
child_session_ids: &[String],
policy: ChildWaitPolicy,
) -> Result<usize, ChildSessionError>;
async fn active_child_ids(&self, parent_session_id: &str) -> Vec<String>;
async fn find_resident_child(
&self,
root_session_id: &str,
resident_name: &str,
) -> Option<String>;
async fn ensure_child_indexed(&self, child_session_id: &str);
}
#[async_trait]
pub trait SubagentResolutionPort: Send + Sync {
async fn resolve_subagent_model(
&self,
subagent_type: &str,
) -> Option<bamboo_domain::ProviderModelRef>;
async fn resolve_runtime_metadata(&self, subagent_type: &str) -> HashMap<String, String>;
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ProviderModelList {
pub provider: String,
pub models: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[async_trait]
pub trait ModelCatalogPort: Send + Sync {
async fn list_models(&self) -> Vec<ProviderModelList>;
fn default_provider(&self) -> String;
}