Skip to main content

bamboo_engine/session_app/child_session/
mod.rs

1//! Child session management use cases.
2//!
3//! Provides the application-layer logic for managing child sessions within
4//! a root session. The server layer implements `ChildSessionPort` to supply
5//! the infrastructure operations (load, save, schedule, cancel).
6
7use async_trait::async_trait;
8use bamboo_domain::session::runtime_state::ChildWaitPolicy;
9use bamboo_domain::Session;
10use std::collections::HashMap;
11
12mod actions;
13mod helpers;
14
15#[cfg(test)]
16mod tests;
17
18pub use actions::{
19    cancel_child_action, create_child_action, delete_child_action, get_child_action,
20    list_children_action, run_child_action, send_message_to_child_action, update_child_action,
21};
22pub use helpers::{
23    compute_status_guidance, format_child_assignment, map_child_entry, metadata_text,
24    normalize_non_empty_optional, normalize_required_text, replace_or_append_last_user_message,
25    resolve_system_prompt, truncate_after_index, truncate_after_last_user,
26};
27
28// ---------------------------------------------------------------------------
29// Error type
30// ---------------------------------------------------------------------------
31
32#[derive(Debug, thiserror::Error)]
33pub enum ChildSessionError {
34    #[error("session not found: {0}")]
35    NotFound(String),
36    #[error("session is not a root session: {0}")]
37    NotRootSession(String),
38    #[error("session is not a child session: {0}")]
39    NotChildSession(String),
40    #[error("child session {child_id} does not belong to parent {parent_id}")]
41    NotChildOfParent { child_id: String, parent_id: String },
42    #[error("{0}")]
43    InvalidArguments(String),
44    #[error("{0}")]
45    Execution(String),
46}
47
48// ---------------------------------------------------------------------------
49// Value types
50// ---------------------------------------------------------------------------
51
52/// Summary of a child session for listing.
53#[derive(Debug, Clone)]
54pub struct ChildSessionEntry {
55    pub child_session_id: String,
56    pub title: String,
57    pub pinned: bool,
58    pub message_count: usize,
59    pub updated_at: String,
60    pub last_run_status: Option<String>,
61    pub last_run_error: Option<String>,
62}
63
64/// Result of deleting a child session.
65#[derive(Debug, Clone)]
66pub struct DeleteChildResult {
67    pub deleted: bool,
68    pub cancelled_running_child: bool,
69}
70
71/// Diagnostic snapshot of a running child session runner.
72#[derive(Debug, Clone)]
73pub struct ChildRunnerInfo {
74    pub started_at: Option<chrono::DateTime<chrono::Utc>>,
75    pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
76    pub last_tool_name: Option<String>,
77    pub last_tool_phase: Option<String>,
78    pub last_event_at: Option<chrono::DateTime<chrono::Utc>>,
79    pub round_count: u32,
80}
81
82/// Default system prompt for child sessions.
83pub const CHILD_SYSTEM_PROMPT: &str = r#"You are a **Child Session**, delegated by a parent session.
84
85Requirements:
86- Focus only on the assigned task and avoid unrelated conversation.
87- You may use tools to complete the task.
88- Do not create or trigger any additional child sessions (no recursive spawn).
89- Keep output concise: provide the conclusion first, then only necessary evidence or steps.
90"#;
91
92/// System prompt for plan-mode exploration child sessions.
93pub const PLAN_AGENT_SYSTEM_PROMPT: &str = r#"You are a **Plan Agent**, a read-only exploration specialist delegated by a parent session.
94
95Your role is EXCLUSIVELY to explore the codebase and gather information to help design an implementation plan. You MUST NOT modify anything.
96
97=== CRITICAL: READ-ONLY MODE — NO FILE MODIFICATIONS ===
98
99You are FORBIDDEN from using these tools:
100- Write — do not create new files
101- Edit — do not modify existing files
102- NotebookEdit — do not edit notebooks
103- Bash — do not execute shell commands
104- BashOutput — do not execute shell commands
105- KillShell — do not manage processes
106- SubAgent — do not spawn further child sessions
107
108You MAY use these read-only tools:
109- Read — read file contents
110- Glob — list files matching patterns
111- Grep — search code for patterns
112- GetFileInfo — get file metadata
113- WebFetch — fetch web content
114- WebSearch — search the web
115- MemoryNote — write observations to session memory
116
117Requirements:
118- Focus only on the assigned exploration task.
119- Provide clear, structured findings: what you discovered, where the relevant code is, and what it does.
120- Keep output concise but thorough — the parent session needs enough detail to design a plan.
121- If you cannot find something after reasonable searching, say so clearly.
122"#;
123
124/// Input for creating a child session.
125#[derive(Debug, Clone)]
126pub struct CreateChildInput {
127    pub parent_session: Session,
128    pub child_id: String,
129    pub title: String,
130    pub responsibility: String,
131    pub assignment_prompt: String,
132    pub subagent_type: String,
133    /// Absolute path to the working directory for the child session.
134    pub workspace: String,
135    /// Optional model override resolved from subagent_type routing.
136    /// When `None`, the child inherits the parent session's model.
137    pub model_override: Option<String>,
138    /// Optional provider+model override resolved from subagent routing.
139    /// When present, this preserves cross-provider routing for child execution.
140    pub model_ref_override: Option<bamboo_domain::ProviderModelRef>,
141    /// Runtime metadata resolved from subagent routing (e.g. external agent config).
142    pub runtime_metadata: std::collections::HashMap<String, String>,
143    /// Optional system prompt override resolved from the
144    /// `SubagentProfileRegistry`. When `None`, the child falls back to
145    /// the legacy hard-coded prompts (`PLAN_AGENT_SYSTEM_PROMPT` for
146    /// `subagent_type == "plan"`, `CHILD_SYSTEM_PROMPT` otherwise) so
147    /// that callers that have not yet been wired up keep their pre-PR-3
148    /// behaviour byte-for-byte.
149    pub system_prompt_override: Option<String>,
150    /// Whether to immediately enqueue the child for execution.
151    /// Defaults to `true`.
152    pub auto_run: bool,
153    /// Optional reasoning effort to apply to the child's own LLM calls.
154    /// `None` (the default) leaves `Session::reasoning_effort` at `None`,
155    /// so the provider falls back to its default. The child does NOT
156    /// inherit the parent's reasoning_effort — fan-out children that
157    /// only need a quick lookup should not pay for `xhigh` reasoning
158    /// just because the orchestrator is running at `xhigh`.
159    pub reasoning_effort: Option<bamboo_domain::ReasoningEffort>,
160    /// Lifecycle of this child: `Some("resident")` marks a reusable resident
161    /// agent (one stable session reused for successive tasks under the same
162    /// root); `None`/`Some("oneshot")` is the default throwaway child.
163    pub lifecycle: Option<String>,
164    /// For a resident agent, the stable reuse key (scoped to the root session).
165    pub resident_name: Option<String>,
166    /// For a resident agent, how successive tasks treat prior context:
167    /// `"reset"` (default — independent tasks) or `"accumulate"` (remember).
168    pub resident_context: Option<String>,
169}
170
171/// Result of creating a child session.
172#[derive(Debug, Clone)]
173pub struct CreateChildResult {
174    pub child_session_id: String,
175    pub model: String,
176}
177
178/// A queued follow-up message stored in session metadata for later injection.
179#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
180pub struct QueuedInjectedMessage {
181    pub content: String,
182    #[serde(default)]
183    pub created_at: Option<chrono::DateTime<chrono::Utc>>,
184}
185
186// ---------------------------------------------------------------------------
187// Port trait
188// ---------------------------------------------------------------------------
189
190#[async_trait]
191pub trait ChildSessionPort: Send + Sync {
192    async fn load_root_session(&self, root_id: &str) -> Result<Session, ChildSessionError>;
193    async fn load_child_for_parent(
194        &self,
195        parent_id: &str,
196        child_id: &str,
197    ) -> Result<Session, ChildSessionError>;
198    async fn save_child_session(&self, child: &mut Session) -> Result<(), ChildSessionError>;
199    async fn is_child_running(&self, child_id: &str) -> bool;
200    async fn list_children(&self, parent_id: &str) -> Vec<ChildSessionEntry>;
201    async fn enqueue_child_run(
202        &self,
203        parent: &Session,
204        child: &Session,
205    ) -> Result<(), ChildSessionError>;
206    async fn cancel_child_run_and_wait(&self, child_id: &str) -> Result<(), ChildSessionError>;
207    async fn delete_child_session(
208        &self,
209        parent_id: &str,
210        child_id: &str,
211    ) -> Result<DeleteChildResult, ChildSessionError>;
212    /// Return live diagnostic info for a running child session, if available.
213    async fn get_child_runner_info(&self, child_id: &str) -> Option<ChildRunnerInfo>;
214
215    /// Register a durable parent wait for a single enqueued child. Idempotent
216    /// and coalesced per parent (concurrent sibling spawns merge into one write).
217    async fn register_parent_wait_for_child(
218        &self,
219        parent_session_id: &str,
220        child_session_id: &str,
221        tool_call_id: Option<&str>,
222    ) -> Result<(), ChildSessionError>;
223
224    /// Register a durable parent wait over an explicit set of children with a
225    /// chosen policy (the `SubAgent.wait` action). Returns the number of
226    /// children the wait now covers (0 = nothing to wait on).
227    async fn register_parent_wait_for_children(
228        &self,
229        parent_session_id: &str,
230        child_session_ids: &[String],
231        policy: ChildWaitPolicy,
232    ) -> Result<usize, ChildSessionError>;
233
234    /// The parent's currently-active (non-terminal) child session ids.
235    async fn active_child_ids(&self, parent_session_id: &str) -> Vec<String>;
236
237    /// Find an existing resident agent in the same root tree by its stable
238    /// `resident_name`, returning its child session id if one exists. Used to
239    /// reuse a resident agent for a new task instead of minting a new child.
240    /// Index-backed (matches `root_session_id` + `metadata["resident_name"]`).
241    async fn find_resident_child(
242        &self,
243        root_session_id: &str,
244        resident_name: &str,
245    ) -> Option<String>;
246
247    /// Best-effort: ensure the child's session-index entry is visible
248    /// immediately after creation (the index is otherwise eventually
249    /// consistent). Failures are ignored by the caller.
250    async fn ensure_child_indexed(&self, child_session_id: &str);
251}
252
253// ---------------------------------------------------------------------------
254// Subagent resolution port
255// ---------------------------------------------------------------------------
256
257/// Resolves subagent-type–specific configuration (model, runtime metadata,
258/// system prompt) for the `SubAgent` tool.
259///
260/// Kept separate from [`ChildSessionPort`] (session CRUD/lifecycle/state): this
261/// port is pure `subagent_type` → config resolution. The server layer
262/// implements it; the tool depends only on the trait, carrying no `AppState`
263/// coupling.
264#[async_trait]
265pub trait SubagentResolutionPort: Send + Sync {
266    /// Provider+model ref for a `subagent_type`, or `None` to use defaults.
267    async fn resolve_subagent_model(
268        &self,
269        subagent_type: &str,
270    ) -> Option<bamboo_domain::ProviderModelRef>;
271
272    /// Runtime metadata (e.g. external-agent routing) for a `subagent_type`.
273    async fn resolve_runtime_metadata(&self, subagent_type: &str) -> HashMap<String, String>;
274
275    /// Canonical system prompt for a `subagent_type` (falls back to
276    /// `general-purpose` for unknown/empty values; never empty).
277    fn resolve_subagent_prompt(&self, subagent_type: &str) -> String;
278}
279
280/// Models available from one configured provider (best-effort listing).
281#[derive(Debug, Clone, serde::Serialize)]
282pub struct ProviderModelList {
283    pub provider: String,
284    pub models: Vec<String>,
285    /// Set when this provider's listing failed (auth missing, network, …);
286    /// the provider is still usable with an explicitly known model id.
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub error: Option<String>,
289}
290
291/// Lists the models the parent can pin a child session to
292/// (`SubAgent` tool `action=list_models` / `create.model`).
293///
294/// Separate from [`SubagentResolutionPort`] because it is backed by the live
295/// provider registry rather than per-`subagent_type` config resolution.
296#[async_trait]
297pub trait ModelCatalogPort: Send + Sync {
298    /// Best-effort model listing per configured provider. Providers whose
299    /// listing fails are still returned (with `error` set) so the caller can
300    /// see they exist.
301    async fn list_models(&self) -> Vec<ProviderModelList>;
302
303    /// The default provider name (used to resolve a bare model id without a
304    /// `provider:` prefix).
305    fn default_provider(&self) -> String;
306}