bamboo_server/app_state/mod.rs
1//! Unified application state management for the Bamboo server
2//!
3//! This module provides the central AppState struct that consolidates all
4//! server state including sessions, storage, LLM providers, tools, and metrics.
5//!
6//! # Architecture
7//!
8//! The AppState uses a unified design that eliminates the proxy pattern where
9//! web_service created an AgentAppState that called back via HTTP. Instead, it
10//! provides direct access to all components.
11//!
12//! ```text
13//! ┌────────────────────────────────────────────────────┐
14//! │ AppState (Unified) │
15//! │ │
16//! │ ┌──────────────┐ ┌──────────────┐ │
17//! │ │ Config │ │ Provider │ │
18//! │ │ (Hot-reload)│◄────►│ (LLM) │ │
19//! │ └──────────────┘ └──────────────┘ │
20//! │ │
21//! │ ┌──────────────┐ ┌──────────────┐ │
22//! │ │ Sessions │ │ Storage │ │
23//! │ │ (In-memory) │ │ (Persistent)│ │
24//! │ └──────────────┘ └──────────────┘ │
25//! │ │
26//! │ ┌──────────────┐ ┌──────────────┐ │
27//! │ │ Tools │ │ Skills │ │
28//! │ │ (Builtin+MCP)│ │ Manager │ │
29//! │ └──────────────┘ └──────────────┘ │
30//! │ │
31//! │ ┌──────────────┐ ┌──────────────┐ │
32//! │ │ MCP │ │ Metrics │ │
33//! │ │ Manager │ │ Service │ │
34//! │ └──────────────┘ └──────────────┘ │
35//! └────────────────────────────────────────────────────┘
36//! ```
37//!
38//! # Key Features
39//!
40//! - **Hot-reloadable configuration**: Config and provider can be reloaded at runtime
41//! - **Direct provider access**: No HTTP proxy overhead
42//! - **Session management**: In-memory session cache with persistent storage
43//! - **Tool composition**: Combines built-in and MCP tools
44//! - **Metrics collection**: Integrated metrics and event tracking
45//!
46//! # Usage Example
47//!
48//! ```rust,no_run
49//! use bamboo_server::app_state::AppState;
50//! use std::path::PathBuf;
51//!
52//! #[tokio::main]
53//! async fn main() {
54//! // Initialize app state
55//! let app_data_dir = PathBuf::from("/path/to/.bamboo");
56//! let state = AppState::new(app_data_dir)
57//! .await
58//! .expect("failed to initialize app state");
59//!
60//! // Access components
61//! let provider = state.get_provider().await;
62//! let schemas = state.get_all_tool_schemas();
63//!
64//! // Hot reload configuration
65//! state.reload_config().await;
66//! state.reload_provider().await.ok();
67//! }
68//! ```
69
70use std::collections::HashMap;
71use std::path::PathBuf;
72use std::sync::Arc;
73
74use async_trait::async_trait;
75use tokio::sync::{broadcast, RwLock};
76use tokio_util::sync::CancellationToken;
77
78use crate::error::AppError;
79use crate::schedule_app::{ScheduleManager, ScheduleStore};
80use bamboo_agent_core::storage::Storage;
81use bamboo_agent_core::AgentEvent;
82use bamboo_agent_core::{tools::ToolSchema, Message};
83use bamboo_engine::execution::spawn::SpawnScheduler;
84use bamboo_infrastructure::process::registry::ProcessRegistry;
85use bamboo_llm::Config;
86use bamboo_llm::{LLMError, LLMProvider, LLMStream};
87use bamboo_mcp::manager::McpServerManager;
88use bamboo_metrics::metrics_service::MetricsService;
89use bamboo_skills::SkillManager;
90use bamboo_storage::LockedSessionStore;
91use bamboo_storage::SessionStoreV2;
92
93// Context functions moved to bamboo-agent-runtime::context
94pub use bamboo_engine::context::{
95 build_env_prompt_context, build_workspace_prompt_context, workspace_prompt_guidance,
96 DEFAULT_BASE_PROMPT, ENV_CONTEXT_END_MARKER, ENV_CONTEXT_START_MARKER,
97 WORKSPACE_CONTEXT_END_MARKER, WORKSPACE_CONTEXT_PREFIX, WORKSPACE_CONTEXT_START_MARKER,
98};
99
100/// Placeholder provider used when the configured provider cannot be initialized.
101///
102/// This keeps the server usable for configuration/UX flows while ensuring we fail fast
103/// (instead of silently switching to a different provider or model).
104struct UnconfiguredProvider {
105 message: String,
106}
107
108#[async_trait]
109impl LLMProvider for UnconfiguredProvider {
110 async fn chat_stream(
111 &self,
112 _messages: &[Message],
113 _tools: &[ToolSchema],
114 _max_output_tokens: Option<u32>,
115 _model: &str,
116 ) -> bamboo_llm::provider::Result<LLMStream> {
117 Err(LLMError::Auth(format!(
118 "LLM provider is not configured: {}",
119 self.message
120 )))
121 }
122
123 async fn list_models(&self) -> bamboo_llm::provider::Result<Vec<String>> {
124 Err(LLMError::Auth(format!(
125 "LLM provider is not configured: {}",
126 self.message
127 )))
128 }
129}
130
131// Re-export execution types from the runtime crate.
132pub use bamboo_engine::execution::runner_state::{AgentRunner, AgentStatus};
133
134/// Unified application state consolidating web_service and agent/server state
135///
136/// This struct holds all the state needed to run the Bamboo server, including
137/// configuration, LLM providers, sessions, storage, tools, skills, and metrics.
138///
139/// # Design Goals
140///
141/// - **Direct access**: Components are directly accessible without HTTP proxies
142/// - **Hot reload**: Configuration and providers can be reloaded at runtime
143/// - **Thread safety**: Uses Arc<RwLock> for concurrent access
144/// - **Persistence**: Integrates with JsonlStorage for session persistence
145///
146/// # Component Overview
147///
148/// | Component | Purpose | Thread-Safe |
149/// |-----------|---------|--------------|
150/// | `config` | Application configuration | Yes (RwLock) |
151/// | `provider` | Hot-reloadable LLM provider | Yes (RwLock) |
152/// | `sessions` | Active conversation sessions | Yes (RwLock) |
153/// | `storage` | Persistent session storage | Yes (Arc) |
154/// | `tools` | Tool execution (builtin + MCP) | Yes (Arc) |
155/// | `skill_manager` | Skill registry and execution | Yes (Arc) |
156/// | `mcp_manager` | MCP server lifecycle | Yes (Arc) |
157/// | `metrics_service` | Usage metrics collection | Yes (Arc) |
158/// | `agent_runners` | Active agent executions | Yes (RwLock) |
159pub struct AppState {
160 /// Application data directory (configured via `BAMBOO_DATA_DIR`; default `${HOME}/.bamboo`)
161 pub app_data_dir: PathBuf,
162
163 /// Hot-reloadable application configuration
164 ///
165 /// Can be reloaded from disk at runtime using `reload_config()`.
166 pub config: Arc<RwLock<Config>>,
167
168 /// Serializes a config WRITE's whole [in-memory mutation + disk persist] with
169 /// a `reload_config`'s [disk read + in-memory swap], so a reload can never
170 /// observe an in-flight-but-not-yet-persisted update and clobber it with the
171 /// stale disk copy (the residual of #41). It is NOT the `config` RwLock —
172 /// using a separate mutex keeps config READERS (the hot agent-loop path)
173 /// unblocked during a write's disk I/O. #126.
174 pub config_io_lock: Arc<tokio::sync::Mutex<()>>,
175
176 /// Shared Remote Cluster Fabric deploy engine (one worker registry across the
177 /// HTTP operator handlers and the `cluster` agent tool).
178 pub fabric_deployer: Arc<bamboo_server_tools::FabricDeployer>,
179
180 /// In-process mailbox bus (broker), when not externally configured. Held so
181 /// it lives for the server's lifetime (dropping it aborts the bus). `None`
182 /// when an external broker is configured or the bus couldn't bind. Never read
183 /// — its only job is to keep the bus task alive until AppState drops.
184 #[allow(dead_code)]
185 embedded_broker: Option<builder::EmbeddedBroker>,
186
187 /// The cluster health monitor sweep. Lives for the server's lifetime (dropping
188 /// it aborts the sweep). `None` when the monitor is disabled
189 /// (`health_interval_secs = 0`). Never read — held only to keep the task alive.
190 #[allow(dead_code)]
191 health_monitor: Option<builder::HealthMonitor>,
192
193 /// Hot-reloadable LLM provider with direct access
194 ///
195 /// This eliminates the proxy pattern where we created an AgentAppState
196 /// that called back to web_service via HTTP. Now we have direct provider access.
197 pub provider: Arc<RwLock<Arc<dyn LLMProvider>>>,
198
199 /// Stable handle that always delegates to the latest provider in `provider`.
200 ///
201 /// This avoids stale provider snapshots after runtime config updates.
202 provider_handle: Arc<dyn LLMProvider>,
203
204 /// Active conversation sessions (in-memory cache)
205 ///
206 /// Maps session IDs to Session objects. Persisted to storage
207 /// via the `storage` field.
208 pub sessions: bamboo_engine::SessionCache,
209
210 /// Persistent storage backend for sessions (V2).
211 ///
212 /// Implemented as folder-per-session with a global `sessions.json` index.
213 pub storage: Arc<dyn Storage>,
214
215 /// Concrete session store implementation (for index/list/cleanup APIs).
216 pub session_store: Arc<SessionStoreV2>,
217
218 /// Per-session write serialisation + metadata-merge persistence layer.
219 ///
220 /// Wraps the same [`Storage`] as `self.storage`, adding per-session
221 /// `Mutex` guards and authoritative-metadata-group merge semantics.
222 /// Use `self.persistence.merge_save_runtime(...)` for any write that
223 /// may race with a UI metadata update.
224 pub persistence: Arc<LockedSessionStore>,
225
226 /// Framework-owned session coordinator (cache + storage + persistence).
227 /// The canonical load/save coordination lives here in `bamboo-engine`, not
228 /// on `AppState`; the inherent `AppState::load_session`/`save_and_cache_session`
229 /// methods now delegate to it. Holds clones of the same `Arc`s as the
230 /// `sessions`/`storage`/`persistence` fields above.
231 pub session_repo: bamboo_engine::SessionRepository,
232
233 /// Background scheduler for async sub-session spawning.
234 pub spawn_scheduler: Arc<SpawnScheduler>,
235
236 /// Coordinates child completion notifications into parent resume.
237 pub child_completion_coordinator: Arc<bamboo_engine::ChildCompletionCoordinator>,
238
239 /// Spawner for the guardian adversarial-review child, injected into each run
240 /// so the terminal gate can create a read-only reviewer (the engine runner
241 /// cannot construct a child directly — see [`bamboo_engine::GuardianSpawner`]).
242 /// Backed by a dedicated [`crate::tools::ChildSessionAdapter`].
243 pub guardian_spawner: Arc<dyn bamboo_engine::GuardianSpawner>,
244
245 /// Bash self-resume hook (issue #84 Phase 2b). Backed by the same
246 /// [`ChildCompletionCoordinator`] that handles child-completion resumes —
247 /// it polls the live shell registry and resumes a session once all its
248 /// background bash shells finish.
249 pub bash_resume_hook: Arc<dyn bamboo_engine::BashResumeHook>,
250
251 /// Schedule store (timed tasks).
252 pub schedule_store: Arc<ScheduleStore>,
253
254 /// Background schedule manager that triggers scheduled runs.
255 pub schedule_manager: Arc<ScheduleManager>,
256
257 /// Tool surface factory providing pre-built tool executors for each session type.
258 ///
259 /// Use `state.tools_for(ToolSurface::Root)` for root sessions,
260 /// `state.tools_for(ToolSurface::Child)` for child sessions, etc.
261 pub tool_factory: crate::tools::ToolSurfaceFactory,
262
263 /// Shared tool-execution permission checker — the same `Arc` the tool
264 /// executors use. Retained so request handlers can record session grants
265 /// when the user approves a permission prompt (see the respond handler).
266 pub permission_checker: Arc<dyn bamboo_tools::permission::PermissionChecker>,
267
268 /// Backend notification policy service (preferences + dedup + per-session
269 /// relays). Classifies agent events into `AgentEvent::Notification` for
270 /// clients to render; preferences are persisted server-side.
271 pub notification_service: Arc<bamboo_notification::NotificationService>,
272
273 /// Cancellation tokens for in-flight requests
274 ///
275 /// Maps request/session IDs to their cancellation tokens,
276 /// allowing graceful shutdown of long-running operations.
277 pub cancel_tokens: Arc<RwLock<HashMap<String, CancellationToken>>>,
278
279 /// Cancels the supervised MCP proxy service (issue #47) on shutdown so the
280 /// reconnect/backoff supervisor stops cleanly instead of looping forever
281 /// after an intended stop. Unused when no broker is configured.
282 pub mcp_proxy_shutdown: CancellationToken,
283
284 /// Skill manager for prompt-based skill execution
285 ///
286 /// Manages the skill registry and handles skill lookup,
287 /// validation, and execution.
288 pub skill_manager: Arc<SkillManager>,
289
290 /// MCP server manager for external tool servers
291 ///
292 /// Handles lifecycle of Model Context Protocol servers,
293 /// including initialization, tool discovery, and shutdown.
294 pub mcp_manager: Arc<McpServerManager>,
295
296 /// Metrics collection and persistence service
297 ///
298 /// Tracks token usage, costs, and performance metrics
299 /// across all sessions.
300 pub metrics_service: Arc<MetricsService>,
301
302 /// Active agent runners indexed by session ID
303 ///
304 /// Each runner manages event broadcasting and cancellation
305 /// for an active agent execution.
306 pub agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
307
308 /// Session-scoped event streams (long-lived).
309 ///
310 /// Unlike `agent_runners`, these senders exist even when no agent execution is running.
311 /// They are used for:
312 /// - UI subscriptions to `/api/v1/events/{session_id}` (background tasks, etc.)
313 /// - sub-session forwarding (child -> parent)
314 pub session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
315
316 /// Account-scoped durable change feed (powers `GET /api/v1/stream`).
317 ///
318 /// Unlike `session_event_senders`, this is a single account-wide sink: all
319 /// durable change events (message appended, session metadata, task updates,
320 /// terminal status) across every session are sequenced, journaled to disk,
321 /// and broadcast here for resumable multi-client sync.
322 pub account_sink: Arc<bamboo_engine::events::AccountEventSink>,
323
324 /// Registry for tracking external processes.
325 pub process_registry: Arc<ProcessRegistry>,
326
327 /// Optional metrics bus for event streaming
328 ///
329 /// When enabled, allows subscribing to metrics events
330 /// in real-time.
331 pub metrics_bus: Option<bamboo_metrics::bus::MetricsBus>,
332
333 /// Unified agent execution runtime holding shared resources.
334 pub agent: Arc<bamboo_engine::Agent>,
335
336 /// Multi-provider registry (used when features.provider_model_ref is enabled).
337 pub provider_registry: Arc<bamboo_llm::ProviderRegistry>,
338
339 /// Provider/model router (used when features.provider_model_ref is enabled).
340 pub provider_router: Arc<bamboo_llm::ProviderModelRouter>,
341
342 /// Unified model catalog service (used when features.provider_model_ref is enabled).
343 pub model_catalog: Arc<bamboo_llm::ModelCatalogService>,
344
345 /// Tracks session ids whose auto-title generation is currently in flight.
346 ///
347 /// Used by [`crate::title_gen`] to dedupe concurrent invocations
348 /// (e.g. execute handler firing while a regenerate-title request is running).
349 pub title_gen_in_flight: Arc<dashmap::DashSet<String>>,
350
351 /// v2-P2 (#181, slice 2): in-memory one-time pairing codes. A 6-digit numeric
352 /// code (keyed by the code string) maps to an entry holding its expiry. Codes
353 /// are PROCESS-EPHEMERAL — never persisted to `config.json`; a restart drops
354 /// all outstanding codes by design. Keyed by `Instant`-based expiry; expired
355 /// entries are purged opportunistically on insert/lookup.
356 pub pairing_codes: Arc<dashmap::DashMap<String, crate::handlers::settings::PairingCodeEntry>>,
357
358 /// v2-P2 (#181, slice 2): per-process brute-force guard for the public
359 /// code-redemption path (`POST /v2/pair { code }`). A 6-digit code is only
360 /// ~1M space, so a public redeem endpoint is brute-forceable without a guard.
361 /// Tracks recent FAILED code-redemption attempts and a cooldown deadline.
362 pub pairing_code_guard: Arc<crate::handlers::settings::PairingCodeGuard>,
363
364 /// #190: per-client-IP brute-force guard for the public root-password
365 /// endpoints (`POST /v1/bamboo/access/verify` and the root-password path of
366 /// `POST /v2/pair`). Tracks recent FAILED root-password attempts per IP and a
367 /// per-key cooldown; loopback/desktop requests are exempted by the handlers
368 /// so the desktop can never lock itself out. PROCESS-EPHEMERAL — never
369 /// persisted; a restart clears all counters.
370 pub root_password_guard: Arc<crate::handlers::settings::RootPasswordGuard>,
371}
372
373impl AppState {
374 /// Try to claim the title-generation slot for `session_id`.
375 /// Returns `true` on success, `false` if generation is already in flight.
376 pub fn title_gen_acquire(&self, session_id: &str) -> bool {
377 self.title_gen_in_flight.insert(session_id.to_string())
378 }
379
380 /// Release the title-generation slot for `session_id`. Idempotent.
381 pub fn title_gen_release(&self, session_id: &str) {
382 self.title_gen_in_flight.remove(session_id);
383 }
384}
385
386mod agent_session_context;
387mod builder;
388mod config_runtime;
389pub mod init;
390mod persistence;
391mod provider_api;
392pub mod resume_adapter;
393pub mod runner_lifecycle;
394pub(crate) mod session_events;
395mod session_loader;
396mod tools;
397
398#[cfg(test)]
399mod tests;
400
401#[derive(Debug, Clone, Copy, Default)]
402pub struct ConfigUpdateEffects {
403 pub reload_provider: bool,
404 pub reconcile_mcp: bool,
405}