Skip to main content

bamboo_engine/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, ToolError, ToolExecutionContext, ToolResult};
7use bamboo_infrastructure::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    async fn execute(&self, args: serde_json::Value) -> Result<ToolResult, ToolError> {
98        self.execute_with_context(args, ToolExecutionContext::none("tool_call"))
99            .await
100    }
101
102    async fn execute_with_context(
103        &self,
104        args: serde_json::Value,
105        ctx: ToolExecutionContext<'_>,
106    ) -> Result<ToolResult, ToolError> {
107        let _caller_session_id = ctx.session_id.ok_or_else(|| {
108            ToolError::Execution(
109                "session_history requires a session_id in tool context".to_string(),
110            )
111        })?;
112
113        let parsed: SessionInspectorArgs = serde_json::from_value(args).map_err(|e| {
114            ToolError::InvalidArguments(format!("Invalid session_history args: {e}"))
115        })?;
116
117        match parsed {
118            SessionInspectorArgs::List {
119                query,
120                kind,
121                pinned,
122                parent_session_id,
123                root_session_id,
124                created_by_schedule_id,
125                limit,
126                offset,
127            } => {
128                handlers::handle_list(
129                    self,
130                    query,
131                    kind,
132                    pinned,
133                    parent_session_id,
134                    root_session_id,
135                    created_by_schedule_id,
136                    limit,
137                    offset,
138                )
139                .await
140            }
141
142            SessionInspectorArgs::GetMeta { session_id } => {
143                handlers::handle_get_meta(self, session_id).await
144            }
145
146            SessionInspectorArgs::ReadMessages {
147                session_id,
148                from_end,
149                offset,
150                limit,
151                truncate_chars,
152                include_system,
153                include_tool,
154                include_tool_calls,
155                include_image_urls,
156            } => {
157                handlers::handle_read_messages(
158                    self,
159                    session_id,
160                    from_end,
161                    offset,
162                    limit,
163                    truncate_chars,
164                    include_system,
165                    include_tool,
166                    include_tool_calls,
167                    include_image_urls,
168                )
169                .await
170            }
171
172            SessionInspectorArgs::ReadCompressedCache {
173                session_id,
174                offset,
175                limit,
176                truncate_chars,
177                include_summary,
178            } => {
179                handlers::handle_read_compressed_cache(
180                    self,
181                    session_id,
182                    offset,
183                    limit,
184                    truncate_chars,
185                    include_summary,
186                )
187                .await
188            }
189
190            SessionInspectorArgs::Search {
191                query,
192                mode,
193                max_sessions,
194                tail_messages,
195                case_sensitive,
196                max_matches,
197            } => {
198                handlers::handle_search(
199                    self,
200                    query,
201                    mode,
202                    max_sessions,
203                    tail_messages,
204                    case_sensitive,
205                    max_matches,
206                )
207                .await
208            }
209        }
210    }
211}