use async_trait::async_trait;
use bamboo_domain::Session;
mod actions;
mod helpers;
#[cfg(test)]
mod tests;
pub use actions::{
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,
};
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,
resolve_system_prompt, 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 CHILD_SYSTEM_PROMPT: &str = r#"You are a **Child Session**, delegated by a parent session.
Requirements:
- Focus only on the assigned task and avoid unrelated conversation.
- You may use tools to complete the task.
- Do not create or trigger any additional child sessions (no recursive spawn).
- Keep output concise: provide the conclusion first, then only necessary evidence or steps.
"#;
pub const PLAN_AGENT_SYSTEM_PROMPT: &str = r#"You are a **Plan Agent**, a read-only exploration specialist delegated by a parent session.
Your role is EXCLUSIVELY to explore the codebase and gather information to help design an implementation plan. You MUST NOT modify anything.
=== CRITICAL: READ-ONLY MODE — NO FILE MODIFICATIONS ===
You are FORBIDDEN from using these tools:
- Write — do not create new files
- Edit — do not modify existing files
- NotebookEdit — do not edit notebooks
- Bash — do not execute shell commands
- BashOutput — do not execute shell commands
- KillShell — do not manage processes
- SubAgent — do not spawn further child sessions
You MAY use these read-only tools:
- Read — read file contents
- Glob — list files matching patterns
- Grep — search code for patterns
- GetFileInfo — get file metadata
- WebFetch — fetch web content
- WebSearch — search the web
- MemoryNote — write observations to session memory
Requirements:
- Focus only on the assigned exploration task.
- Provide clear, structured findings: what you discovered, where the relevant code is, and what it does.
- Keep output concise but thorough — the parent session needs enough detail to design a plan.
- If you cannot find something after reasonable searching, say so clearly.
"#;
#[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 system_prompt_override: Option<String>,
pub auto_run: bool,
pub reasoning_effort: Option<bamboo_domain::ReasoningEffort>,
}
#[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>;
}