Skip to main content

bamboo_server_tools/session_inspector/
mod.rs

1use async_trait::async_trait;
2use serde_json::json;
3use std::sync::Arc;
4
5use bamboo_agent_core::storage::Storage;
6use bamboo_agent_core::tools::{Tool, ToolClass, ToolCtx, ToolError, ToolOutcome, ToolResult};
7use bamboo_storage::SessionStoreV2;
8
9mod args;
10mod handlers;
11mod helpers;
12
13use args::SessionInspectorArgs;
14
15/// Server-only tool for inspecting V2 sessions stored under the Bamboo home dir.
16///
17/// Design goals:
18/// - Return metadata first (index-backed) so the model can narrow scope.
19/// - Allow bounded reads (pagination; from end; truncation).
20/// - Support lightweight search across session titles and (optionally) tail messages.
21/// - Keep inspection local by default; use child-session delegation only if the user explicitly asks.
22pub struct SessionInspectorTool {
23    pub(super) session_store: Arc<SessionStoreV2>,
24    pub(super) storage: Arc<dyn Storage>,
25}
26
27impl SessionInspectorTool {
28    pub fn new(session_store: Arc<SessionStoreV2>, storage: Arc<dyn Storage>) -> Self {
29        Self {
30            session_store,
31            storage,
32        }
33    }
34
35    pub(super) async fn load_session(
36        &self,
37        session_id: &str,
38    ) -> Result<bamboo_agent_core::Session, ToolError> {
39        match self.storage.load_session(session_id).await {
40            Ok(Some(s)) => Ok(s),
41            Ok(None) => Err(ToolError::Execution(format!(
42                "session not found: {session_id}"
43            ))),
44            Err(e) => Err(ToolError::Execution(format!(
45                "failed to load session {session_id}: {e}"
46            ))),
47        }
48    }
49}
50
51#[async_trait]
52impl Tool for SessionInspectorTool {
53    fn name(&self) -> &str {
54        "session_history"
55    }
56
57    fn description(&self) -> &str {
58        "Read-only viewer over the local SQLite session history. Use this to list prior sessions, inspect metadata, read bounded message slices, read the compressed conversation cache, and full-text search prior conversation history before asking the user to repeat information. This is purely a read tool — it has no runtime control and cannot influence live sessions. Distinct from the `memory` tool, which manages durable cross-session knowledge."
59    }
60
61    fn parameters_schema(&self) -> serde_json::Value {
62        // Keep schema permissive; Rust parsing enforces action-specific requirements.
63        json!({
64            "type": "object",
65            "properties": {
66                "action": {
67                    "type": "string",
68                    "enum": ["list", "get_meta", "read_messages", "read_compressed_cache", "search"],
69                    "description": "Which inspection action to perform."
70                },
71                "query": { "type": "string", "description": "Search string (list/search)." },
72                "kind": { "type": "string", "enum": ["root", "child"], "description": "Filter by session kind (list)." },
73                "pinned": { "type": "boolean", "description": "Filter pinned sessions (list)." },
74                "parent_session_id": { "type": "string", "description": "Filter child sessions by parent (list)." },
75                "root_session_id": { "type": "string", "description": "Filter by root session (list)." },
76                "created_by_schedule_id": { "type": "string", "description": "Filter sessions created by a schedule (list)." },
77                "limit": { "type": "number", "description": "Max items/messages to return (list/read_messages)." },
78                "offset": { "type": "number", "description": "Offset (list/read_messages)." },
79                "session_id": { "type": "string", "description": "Target session id (get_meta/read_messages)." },
80                "from_end": { "type": "boolean", "description": "Read from end (read_messages)." },
81                "truncate_chars": { "type": "number", "description": "Max chars per message (read_messages)." },
82                "include_system": { "type": "boolean" },
83                "include_tool": { "type": "boolean" },
84                "include_tool_calls": { "type": "boolean" },
85                "include_image_urls": { "type": "boolean" },
86                "include_summary": { "type": "boolean", "description": "Include cached conversation summary when available (read_compressed_cache)." },
87                "mode": { "type": "string", "enum": ["title", "tail_messages"] },
88                "max_sessions": { "type": "number" },
89                "tail_messages": { "type": "number" },
90                "case_sensitive": { "type": "boolean" },
91                "max_matches": { "type": "number" }
92            },
93            "required": ["action"]
94        })
95    }
96
97    fn classify(&self, _args: &serde_json::Value) -> ToolClass {
98        ToolClass::READONLY_PARALLEL
99    }
100
101    async fn invoke(
102        &self,
103        args: serde_json::Value,
104        ctx: ToolCtx,
105    ) -> Result<ToolOutcome, ToolError> {
106        let _caller_session_id = ctx.session_id().ok_or_else(|| {
107            ToolError::Execution(
108                "session_history requires a session_id in tool context".to_string(),
109            )
110        })?;
111
112        let parsed: SessionInspectorArgs = serde_json::from_value(args).map_err(|e| {
113            ToolError::InvalidArguments(format!("Invalid session_history args: {e}"))
114        })?;
115
116        match parsed {
117            SessionInspectorArgs::List {
118                query,
119                kind,
120                pinned,
121                parent_session_id,
122                root_session_id,
123                created_by_schedule_id,
124                limit,
125                offset,
126            } => {
127                handlers::handle_list(
128                    self,
129                    query,
130                    kind,
131                    pinned,
132                    parent_session_id,
133                    root_session_id,
134                    created_by_schedule_id,
135                    limit,
136                    offset,
137                )
138                .await
139            }
140
141            SessionInspectorArgs::GetMeta { session_id } => {
142                handlers::handle_get_meta(self, session_id).await
143            }
144
145            SessionInspectorArgs::ReadMessages {
146                session_id,
147                from_end,
148                offset,
149                limit,
150                truncate_chars,
151                include_system,
152                include_tool,
153                include_tool_calls,
154                include_image_urls,
155            } => {
156                handlers::handle_read_messages(
157                    self,
158                    session_id,
159                    from_end,
160                    offset,
161                    limit,
162                    truncate_chars,
163                    include_system,
164                    include_tool,
165                    include_tool_calls,
166                    include_image_urls,
167                )
168                .await
169            }
170
171            SessionInspectorArgs::ReadCompressedCache {
172                session_id,
173                offset,
174                limit,
175                truncate_chars,
176                include_summary,
177            } => {
178                handlers::handle_read_compressed_cache(
179                    self,
180                    session_id,
181                    offset,
182                    limit,
183                    truncate_chars,
184                    include_summary,
185                )
186                .await
187            }
188
189            SessionInspectorArgs::Search {
190                query,
191                mode,
192                max_sessions,
193                tail_messages,
194                case_sensitive,
195                max_matches,
196            } => {
197                handlers::handle_search(
198                    self,
199                    query,
200                    mode,
201                    max_sessions,
202                    tail_messages,
203                    case_sensitive,
204                    max_matches,
205                )
206                .await
207            }
208        }
209        .map(ToolOutcome::Completed)
210    }
211}