1use super::{Tool, ToolContext, ToolError, ToolResult};
6use async_trait::async_trait;
7use roboticus_core::RiskLevel;
8use serde_json::{Value, json};
9
10pub struct GetRuntimeContextTool;
12
13#[async_trait]
14impl Tool for GetRuntimeContextTool {
15 fn name(&self) -> &str {
16 "get_runtime_context"
17 }
18
19 fn description(&self) -> &str {
20 "Returns runtime context: session, channel, workspace, allowed paths, and sandbox boundaries \
21 from roboticus.toml (filesystem + skill script policy). Includes `how_to_change_boundaries` \
22 with TOML keys and docs references, plus hippocampus-backed storage awareness when a database \
23 is available — call this before claiming the user is confined to a path; then explain how to \
24 widen access if needed."
25 }
26
27 fn risk_level(&self) -> RiskLevel {
28 RiskLevel::Safe
29 }
30
31 fn parameters_schema(&self) -> Value {
32 serde_json::json!({
33 "type": "object",
34 "properties": {},
35 "required": []
36 })
37 }
38
39 async fn execute(
40 &self,
41 _params: Value,
42 ctx: &ToolContext,
43 ) -> std::result::Result<ToolResult, ToolError> {
44 let tool_paths: Vec<String> = ctx
45 .tool_allowed_paths
46 .iter()
47 .map(|p| p.display().to_string())
48 .collect();
49 let script_paths: Vec<String> = ctx
50 .sandbox
51 .script_allowed_paths
52 .iter()
53 .map(|p| p.display().to_string())
54 .collect();
55 let storage = if let Some(db) = ctx.db.as_ref() {
56 match roboticus_db::hippocampus::list_tables(db) {
57 Ok(entries) => {
58 let owned_tables: Vec<Value> = entries
59 .iter()
60 .filter(|entry| entry.agent_owned && entry.created_by == ctx.agent_id)
61 .map(|entry| {
62 serde_json::json!({
63 "table_name": entry.table_name,
64 "description": entry.description,
65 "row_count": entry.row_count,
66 "access_level": entry.access_level,
67 "updated_at": entry.updated_at,
68 })
69 })
70 .collect();
71 let knowledge_sources: Vec<Value> = entries
72 .iter()
73 .filter(|entry| entry.table_name.starts_with("knowledge:"))
74 .map(|entry| {
75 serde_json::json!({
76 "table_name": entry.table_name,
77 "description": entry.description,
78 "row_count": entry.row_count,
79 "access_level": entry.access_level,
80 "updated_at": entry.updated_at,
81 })
82 })
83 .collect();
84 let compact_summary =
85 roboticus_db::hippocampus::compact_summary(db).unwrap_or_default();
86 serde_json::json!({
87 "hippocampus_available": true,
88 "table_count": entries.len(),
89 "owned_table_count": owned_tables.len(),
90 "knowledge_source_count": knowledge_sources.len(),
91 "compact_summary": compact_summary,
92 "owned_tables": owned_tables,
93 "knowledge_sources": knowledge_sources,
94 })
95 }
96 Err(e) => serde_json::json!({
97 "hippocampus_available": false,
98 "error": format!("failed to load hippocampus: {e}"),
99 }),
100 }
101 } else {
102 serde_json::json!({
103 "hippocampus_available": false,
104 "note": "database not available",
105 })
106 };
107 let wallet = if let Some(db) = ctx.db.as_ref() {
108 match roboticus_db::treasury::get_treasury_state(db) {
109 Ok(Some(ts)) => serde_json::json!({
110 "available": true,
111 "usdc_balance": ts.usdc_balance,
112 "native_balance": ts.native_balance,
113 "survival_tier": format!("{:?}", ts.survival_tier),
114 "last_updated": ts.updated_at,
115 }),
116 _ => serde_json::json!({
117 "available": true,
118 "note": "treasury state not yet cached — balance will appear after first heartbeat tick",
119 }),
120 }
121 } else {
122 serde_json::json!({ "available": false })
123 };
124 let info = serde_json::json!({
125 "agent_id": ctx.agent_id,
126 "agent_name": ctx.agent_name,
127 "session_id": ctx.session_id,
128 "channel": ctx.channel,
129 "workspace_root": ctx.workspace_root.display().to_string(),
130 "authority": format!("{:?}", ctx.authority),
131 "tool_allowed_paths": tool_paths,
132 "wallet": wallet,
133 "storage": storage,
134 "sandbox": {
135 "filesystem": {
136 "workspace_only": ctx.sandbox.filesystem_workspace_only,
137 "script_fs_confinement": ctx.sandbox.filesystem_script_fs_confinement,
138 "script_allowed_paths": script_paths,
139 },
140 "skills": {
141 "sandbox_env": ctx.sandbox.skills_sandbox_env,
142 "network_allowed": ctx.sandbox.skills_network_allowed,
143 "skills_dir": ctx.sandbox.skills_dir.display().to_string(),
144 },
145 "effective_file_tool_roots": {
146 "primary_workspace": ctx.workspace_root.display().to_string(),
147 "extra_tool_allowed_paths": ctx.tool_allowed_paths.iter().map(|p| p.display().to_string()).collect::<Vec<_>>(),
148 "summary": "File tools resolve relative paths inside primary_workspace when workspace_only is true; absolute paths may use extra_tool_allowed_paths entries. Skill scripts use skills_dir plus script_allowed_paths (subject to script_fs_confinement)."
149 }
150 },
151 "how_to_change_boundaries": {
152 "documentation": "docs/CONFIGURATION.md — sections [agent], [security.filesystem], [skills]",
153 "toml_keys": [
154 "[agent].workspace — default workspace root for file tools; ROBOTICUS_WORKSPACE for plugins/skills",
155 "[security.filesystem].tool_allowed_paths — allow file tools / bash cwd under extra absolute directories",
156 "[security.filesystem].script_allowed_paths — extra directories for sandboxed skill scripts",
157 "[security.filesystem].workspace_only — when true, keep relative file paths inside workspace",
158 "[security.filesystem].script_fs_confinement — OS-level sandbox for skill scripts",
159 "[skills].sandbox_env — sanitize environment for skill subprocesses",
160 "[skills].network_allowed — outbound network for skill scripts"
161 ],
162 "after_editing": "Restart roboticus serve (or your Roboticus process) so the new boundaries load.",
163 "cli": "roboticus config get <key> | roboticus config set <key> <value> | roboticus check"
164 }
165 });
166 Ok(ToolResult {
167 output: serde_json::to_string_pretty(&info).unwrap_or_else(|_| "{}".into()),
168 metadata: Some(info),
169 })
170 }
171}
172
173pub struct GetMemoryStatsTool;
175
176#[async_trait]
177impl Tool for GetMemoryStatsTool {
178 fn name(&self) -> &str {
179 "get_memory_stats"
180 }
181
182 fn description(&self) -> &str {
183 "Returns memory retrieval tier allocations plus live memory health when a database is available, \
184 including active vs stale episodic/semantic counts, procedural utilization, and relationship interaction volume"
185 }
186
187 fn risk_level(&self) -> RiskLevel {
188 RiskLevel::Safe
189 }
190
191 fn parameters_schema(&self) -> Value {
192 serde_json::json!({
193 "type": "object",
194 "properties": {},
195 "required": []
196 })
197 }
198
199 async fn execute(
200 &self,
201 _params: Value,
202 ctx: &ToolContext,
203 ) -> std::result::Result<ToolResult, ToolError> {
204 let mut payload = serde_json::json!({
207 "tiers": {
208 "working": { "budget_pct": 30, "description": "Active conversation context" },
209 "episodic": { "budget_pct": 25, "description": "Session digests and summaries" },
210 "semantic": { "budget_pct": 20, "description": "Vector-similarity recalled facts" },
211 "procedural": { "budget_pct": 15, "description": "How-to knowledge and procedures" },
212 "relationship": { "budget_pct": 10, "description": "Entity relationships and graph" },
213 },
214 "retrieval_method": "5-tier hybrid (FTS5 + vector cosine)",
215 "lifecycle_policy": {
216 "inactive_states_suppressed_by_default": true,
217 "history_queries_can_include_inactive": true,
218 "notes": "stale episodic digests and superseded semantic summaries are hidden unless the task explicitly asks for historical or resolved context"
219 }
220 });
221 let live = if let Some(db) = ctx.db.as_ref() {
222 match roboticus_db::memory::memory_health_snapshot(db, &ctx.session_id) {
223 Ok(snapshot) => serde_json::json!({
224 "available": true,
225 "snapshot": snapshot,
226 }),
227 Err(e) => serde_json::json!({
228 "available": false,
229 "error": format!("failed to inspect live memory health: {e}"),
230 }),
231 }
232 } else {
233 serde_json::json!({
234 "available": false,
235 "note": "database not available",
236 })
237 };
238 payload["live"] = live;
239 Ok(ToolResult {
240 output: serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "{}".into()),
241 metadata: Some(payload),
242 })
243 }
244}
245
246pub struct GetChannelHealthTool;
248
249#[async_trait]
250impl Tool for GetChannelHealthTool {
251 fn name(&self) -> &str {
252 "get_channel_health"
253 }
254
255 fn description(&self) -> &str {
256 "Returns the health status of the current delivery channel"
257 }
258
259 fn risk_level(&self) -> RiskLevel {
260 RiskLevel::Safe
261 }
262
263 fn parameters_schema(&self) -> Value {
264 serde_json::json!({
265 "type": "object",
266 "properties": {},
267 "required": []
268 })
269 }
270
271 async fn execute(
272 &self,
273 _params: Value,
274 ctx: &ToolContext,
275 ) -> std::result::Result<ToolResult, ToolError> {
276 let channel = ctx.channel.as_deref().unwrap_or("unknown");
277 let health = serde_json::json!({
278 "channel": channel,
279 "status": "operational",
280 "note": "Detailed channel health metrics require a ChannelRouter reference; \
281 basic connectivity confirmed by successful tool invocation.",
282 });
283 Ok(ToolResult {
284 output: serde_json::to_string_pretty(&health).unwrap_or_else(|_| "{}".into()),
285 metadata: Some(health),
286 })
287 }
288}
289
290pub struct GetSubagentStatusTool;
297
298#[async_trait]
299impl Tool for GetSubagentStatusTool {
300 fn name(&self) -> &str {
301 "get_subagent_status"
302 }
303
304 fn description(&self) -> &str {
305 "Returns what subagents (specialists) are available, their skills, capabilities, \
306 and current status. Use this tool when asked about subagent capabilities, what \
307 specialists can do, or the delegation roster."
308 }
309
310 fn risk_level(&self) -> RiskLevel {
311 RiskLevel::Safe
312 }
313
314 fn parameters_schema(&self) -> Value {
315 serde_json::json!({
316 "type": "object",
317 "properties": {},
318 "required": []
319 })
320 }
321
322 async fn execute(
323 &self,
324 _params: Value,
325 ctx: &ToolContext,
326 ) -> std::result::Result<ToolResult, ToolError> {
327 let db = match &ctx.db {
328 Some(db) => db,
329 None => {
330 let result = serde_json::json!({
331 "error": "database not available",
332 "subagents": [],
333 "tasks": [],
334 });
335 return Ok(ToolResult {
336 output: serde_json::to_string_pretty(&result).unwrap_or_else(|_| "{}".into()),
337 metadata: Some(result),
338 });
339 }
340 };
341
342 let subagents = roboticus_db::agents::list_sub_agents(db)
344 .unwrap_or_default()
345 .into_iter()
346 .map(|a| {
347 serde_json::json!({
348 "name": a.name,
349 "display_name": a.display_name,
350 "model": a.model,
351 "role": a.role,
352 "enabled": a.enabled,
353 "session_count": a.session_count,
354 })
355 })
356 .collect::<Vec<_>>();
357
358 let tasks = {
360 let conn = db.conn();
361 conn.prepare(
362 "SELECT id, title, status, priority, source, created_at \
363 FROM tasks WHERE status IN ('pending', 'in_progress') \
364 ORDER BY priority DESC, created_at ASC LIMIT 50",
365 )
366 .ok()
367 .map(|mut stmt| {
368 stmt.query_map([], |row| {
369 let source_raw: Option<String> = row.get(4)?;
370 Ok(serde_json::json!({
371 "id": row.get::<_, String>(0)?,
372 "title": row.get::<_, String>(1)?,
373 "status": row.get::<_, String>(2)?,
374 "priority": row.get::<_, i64>(3)?,
375 "source": roboticus_db::tasks::normalize_task_source_value(source_raw.as_deref()),
376 "created_at": row.get::<_, String>(5)?,
377 }))
378 })
379 .inspect_err(|e| tracing::warn!("failed to query tasks: {e}"))
380 .ok()
381 .map(|rows| rows.filter_map(|r| {
382 r.inspect_err(|e| tracing::warn!("skipping corrupted task row: {e}"))
383 .ok()
384 }).collect::<Vec<_>>())
385 .unwrap_or_default()
386 })
387 .unwrap_or_default()
388 };
389
390 let result = serde_json::json!({
391 "subagents": subagents,
392 "subagent_count": subagents.len(),
393 "tasks": tasks,
394 "open_task_count": tasks.len(),
395 });
396 Ok(ToolResult {
397 output: serde_json::to_string_pretty(&result).unwrap_or_else(|_| "{}".into()),
398 metadata: Some(result),
399 })
400 }
401}
402
403pub struct RecallMemoryTool;
409
410#[async_trait::async_trait]
411impl Tool for RecallMemoryTool {
412 fn name(&self) -> &str {
413 "recall_memory"
414 }
415
416 fn description(&self) -> &str {
417 "Fetch full content for a memory index entry. Pass the index entry ID (e.g., 'idx-episodic_memory-ep001') to retrieve the complete memory content."
418 }
419
420 fn risk_level(&self) -> RiskLevel {
421 RiskLevel::Safe
422 }
423
424 fn parameters_schema(&self) -> Value {
425 json!({
426 "type": "object",
427 "properties": {
428 "id": {
429 "type": "string",
430 "description": "The memory index entry ID from the [Memory Index] block"
431 }
432 },
433 "required": ["id"]
434 })
435 }
436
437 async fn execute(
438 &self,
439 params: Value,
440 ctx: &ToolContext,
441 ) -> std::result::Result<ToolResult, ToolError> {
442 let index_id = params
443 .get("id")
444 .and_then(|v| v.as_str())
445 .ok_or_else(|| ToolError {
446 message: "missing 'id' parameter".into(),
447 })?;
448
449 let db = ctx.db.as_ref().ok_or_else(|| ToolError {
450 message: "database not available".into(),
451 })?;
452
453 let entry = roboticus_db::memory_index::top_entries(db, 100)
455 .map_err(|e| ToolError {
456 message: format!("index query failed: {e}"),
457 })?
458 .into_iter()
459 .find(|e| e.id == index_id);
460
461 let entry = match entry {
462 Some(e) => e,
463 None => {
464 return Ok(ToolResult {
465 output: format!("No memory index entry found with ID: {index_id}"),
466 metadata: None,
467 });
468 }
469 };
470
471 let content =
473 roboticus_db::memory_index::recall_content(db, &entry.source_table, &entry.source_id)
474 .map_err(|e| ToolError {
475 message: format!("recall failed: {e}"),
476 })?;
477
478 let result = json!({
479 "id": index_id,
480 "source": entry.source_table,
481 "category": entry.category,
482 "confidence": entry.confidence,
483 "content": content.unwrap_or_else(|| "(content no longer available — source may have been pruned)".into()),
484 });
485
486 Ok(ToolResult {
487 output: serde_json::to_string_pretty(&result).unwrap_or_else(|_| "{}".into()),
488 metadata: Some(result),
489 })
490 }
491}