bamboo_server_tools/session_inspector/
mod.rs1use 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
15pub 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 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}