use super::{Tool, ToolContext, ToolError, ToolResult};
use async_trait::async_trait;
use roboticus_core::RiskLevel;
use serde_json::{Value, json};
pub struct GetRuntimeContextTool;
#[async_trait]
impl Tool for GetRuntimeContextTool {
fn name(&self) -> &str {
"get_runtime_context"
}
fn description(&self) -> &str {
"Returns runtime context: session, channel, workspace, allowed paths, and sandbox boundaries \
from roboticus.toml (filesystem + skill script policy). Includes `how_to_change_boundaries` \
with TOML keys and docs references, plus hippocampus-backed storage awareness when a database \
is available — call this before claiming the user is confined to a path; then explain how to \
widen access if needed."
}
fn risk_level(&self) -> RiskLevel {
RiskLevel::Safe
}
fn parameters_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {},
"required": []
})
}
async fn execute(
&self,
_params: Value,
ctx: &ToolContext,
) -> std::result::Result<ToolResult, ToolError> {
let tool_paths: Vec<String> = ctx
.tool_allowed_paths
.iter()
.map(|p| p.display().to_string())
.collect();
let script_paths: Vec<String> = ctx
.sandbox
.script_allowed_paths
.iter()
.map(|p| p.display().to_string())
.collect();
let storage = if let Some(db) = ctx.db.as_ref() {
match roboticus_db::hippocampus::list_tables(db) {
Ok(entries) => {
let owned_tables: Vec<Value> = entries
.iter()
.filter(|entry| entry.agent_owned && entry.created_by == ctx.agent_id)
.map(|entry| {
serde_json::json!({
"table_name": entry.table_name,
"description": entry.description,
"row_count": entry.row_count,
"access_level": entry.access_level,
"updated_at": entry.updated_at,
})
})
.collect();
let knowledge_sources: Vec<Value> = entries
.iter()
.filter(|entry| entry.table_name.starts_with("knowledge:"))
.map(|entry| {
serde_json::json!({
"table_name": entry.table_name,
"description": entry.description,
"row_count": entry.row_count,
"access_level": entry.access_level,
"updated_at": entry.updated_at,
})
})
.collect();
let compact_summary =
roboticus_db::hippocampus::compact_summary(db).unwrap_or_default();
serde_json::json!({
"hippocampus_available": true,
"table_count": entries.len(),
"owned_table_count": owned_tables.len(),
"knowledge_source_count": knowledge_sources.len(),
"compact_summary": compact_summary,
"owned_tables": owned_tables,
"knowledge_sources": knowledge_sources,
})
}
Err(e) => serde_json::json!({
"hippocampus_available": false,
"error": format!("failed to load hippocampus: {e}"),
}),
}
} else {
serde_json::json!({
"hippocampus_available": false,
"note": "database not available",
})
};
let wallet = if let Some(db) = ctx.db.as_ref() {
match roboticus_db::treasury::get_treasury_state(db) {
Ok(Some(ts)) => serde_json::json!({
"available": true,
"usdc_balance": ts.usdc_balance,
"native_balance": ts.native_balance,
"survival_tier": format!("{:?}", ts.survival_tier),
"last_updated": ts.updated_at,
}),
_ => serde_json::json!({
"available": true,
"note": "treasury state not yet cached — balance will appear after first heartbeat tick",
}),
}
} else {
serde_json::json!({ "available": false })
};
let info = serde_json::json!({
"agent_id": ctx.agent_id,
"agent_name": ctx.agent_name,
"session_id": ctx.session_id,
"channel": ctx.channel,
"workspace_root": ctx.workspace_root.display().to_string(),
"authority": format!("{:?}", ctx.authority),
"tool_allowed_paths": tool_paths,
"wallet": wallet,
"storage": storage,
"sandbox": {
"filesystem": {
"workspace_only": ctx.sandbox.filesystem_workspace_only,
"script_fs_confinement": ctx.sandbox.filesystem_script_fs_confinement,
"script_allowed_paths": script_paths,
},
"skills": {
"sandbox_env": ctx.sandbox.skills_sandbox_env,
"network_allowed": ctx.sandbox.skills_network_allowed,
"skills_dir": ctx.sandbox.skills_dir.display().to_string(),
},
"effective_file_tool_roots": {
"primary_workspace": ctx.workspace_root.display().to_string(),
"extra_tool_allowed_paths": ctx.tool_allowed_paths.iter().map(|p| p.display().to_string()).collect::<Vec<_>>(),
"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)."
}
},
"how_to_change_boundaries": {
"documentation": "docs/CONFIGURATION.md — sections [agent], [security.filesystem], [skills]",
"toml_keys": [
"[agent].workspace — default workspace root for file tools; ROBOTICUS_WORKSPACE for plugins/skills",
"[security.filesystem].tool_allowed_paths — allow file tools / bash cwd under extra absolute directories",
"[security.filesystem].script_allowed_paths — extra directories for sandboxed skill scripts",
"[security.filesystem].workspace_only — when true, keep relative file paths inside workspace",
"[security.filesystem].script_fs_confinement — OS-level sandbox for skill scripts",
"[skills].sandbox_env — sanitize environment for skill subprocesses",
"[skills].network_allowed — outbound network for skill scripts"
],
"after_editing": "Restart roboticus serve (or your Roboticus process) so the new boundaries load.",
"cli": "roboticus config get <key> | roboticus config set <key> <value> | roboticus check"
}
});
Ok(ToolResult {
output: serde_json::to_string_pretty(&info).unwrap_or_else(|_| "{}".into()),
metadata: Some(info),
})
}
}
pub struct GetMemoryStatsTool;
#[async_trait]
impl Tool for GetMemoryStatsTool {
fn name(&self) -> &str {
"get_memory_stats"
}
fn description(&self) -> &str {
"Returns memory retrieval tier allocations plus live memory health when a database is available, \
including active vs stale episodic/semantic counts, procedural utilization, and relationship interaction volume"
}
fn risk_level(&self) -> RiskLevel {
RiskLevel::Safe
}
fn parameters_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {},
"required": []
})
}
async fn execute(
&self,
_params: Value,
ctx: &ToolContext,
) -> std::result::Result<ToolResult, ToolError> {
let mut payload = serde_json::json!({
"tiers": {
"working": { "budget_pct": 30, "description": "Active conversation context" },
"episodic": { "budget_pct": 25, "description": "Session digests and summaries" },
"semantic": { "budget_pct": 20, "description": "Vector-similarity recalled facts" },
"procedural": { "budget_pct": 15, "description": "How-to knowledge and procedures" },
"relationship": { "budget_pct": 10, "description": "Entity relationships and graph" },
},
"retrieval_method": "5-tier hybrid (FTS5 + vector cosine)",
"lifecycle_policy": {
"inactive_states_suppressed_by_default": true,
"history_queries_can_include_inactive": true,
"notes": "stale episodic digests and superseded semantic summaries are hidden unless the task explicitly asks for historical or resolved context"
}
});
let live = if let Some(db) = ctx.db.as_ref() {
match roboticus_db::memory::memory_health_snapshot(db, &ctx.session_id) {
Ok(snapshot) => serde_json::json!({
"available": true,
"snapshot": snapshot,
}),
Err(e) => serde_json::json!({
"available": false,
"error": format!("failed to inspect live memory health: {e}"),
}),
}
} else {
serde_json::json!({
"available": false,
"note": "database not available",
})
};
payload["live"] = live;
Ok(ToolResult {
output: serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "{}".into()),
metadata: Some(payload),
})
}
}
pub struct GetChannelHealthTool;
#[async_trait]
impl Tool for GetChannelHealthTool {
fn name(&self) -> &str {
"get_channel_health"
}
fn description(&self) -> &str {
"Returns the health status of the current delivery channel"
}
fn risk_level(&self) -> RiskLevel {
RiskLevel::Safe
}
fn parameters_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {},
"required": []
})
}
async fn execute(
&self,
_params: Value,
ctx: &ToolContext,
) -> std::result::Result<ToolResult, ToolError> {
let channel = ctx.channel.as_deref().unwrap_or("unknown");
let health = serde_json::json!({
"channel": channel,
"status": "operational",
"note": "Detailed channel health metrics require a ChannelRouter reference; \
basic connectivity confirmed by successful tool invocation.",
});
Ok(ToolResult {
output: serde_json::to_string_pretty(&health).unwrap_or_else(|_| "{}".into()),
metadata: Some(health),
})
}
}
pub struct GetSubagentStatusTool;
#[async_trait]
impl Tool for GetSubagentStatusTool {
fn name(&self) -> &str {
"get_subagent_status"
}
fn description(&self) -> &str {
"Returns what subagents (specialists) are available, their skills, capabilities, \
and current status. Use this tool when asked about subagent capabilities, what \
specialists can do, or the delegation roster."
}
fn risk_level(&self) -> RiskLevel {
RiskLevel::Safe
}
fn parameters_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {},
"required": []
})
}
async fn execute(
&self,
_params: Value,
ctx: &ToolContext,
) -> std::result::Result<ToolResult, ToolError> {
let db = match &ctx.db {
Some(db) => db,
None => {
let result = serde_json::json!({
"error": "database not available",
"subagents": [],
"tasks": [],
});
return Ok(ToolResult {
output: serde_json::to_string_pretty(&result).unwrap_or_else(|_| "{}".into()),
metadata: Some(result),
});
}
};
let subagents = roboticus_db::agents::list_sub_agents(db)
.unwrap_or_default()
.into_iter()
.map(|a| {
serde_json::json!({
"name": a.name,
"display_name": a.display_name,
"model": a.model,
"role": a.role,
"enabled": a.enabled,
"session_count": a.session_count,
})
})
.collect::<Vec<_>>();
let tasks = {
let conn = db.conn();
conn.prepare(
"SELECT id, title, status, priority, source, created_at \
FROM tasks WHERE status IN ('pending', 'in_progress') \
ORDER BY priority DESC, created_at ASC LIMIT 50",
)
.ok()
.map(|mut stmt| {
stmt.query_map([], |row| {
let source_raw: Option<String> = row.get(4)?;
Ok(serde_json::json!({
"id": row.get::<_, String>(0)?,
"title": row.get::<_, String>(1)?,
"status": row.get::<_, String>(2)?,
"priority": row.get::<_, i64>(3)?,
"source": roboticus_db::tasks::normalize_task_source_value(source_raw.as_deref()),
"created_at": row.get::<_, String>(5)?,
}))
})
.inspect_err(|e| tracing::warn!("failed to query tasks: {e}"))
.ok()
.map(|rows| rows.filter_map(|r| {
r.inspect_err(|e| tracing::warn!("skipping corrupted task row: {e}"))
.ok()
}).collect::<Vec<_>>())
.unwrap_or_default()
})
.unwrap_or_default()
};
let result = serde_json::json!({
"subagents": subagents,
"subagent_count": subagents.len(),
"tasks": tasks,
"open_task_count": tasks.len(),
});
Ok(ToolResult {
output: serde_json::to_string_pretty(&result).unwrap_or_else(|_| "{}".into()),
metadata: Some(result),
})
}
}
pub struct RecallMemoryTool;
#[async_trait::async_trait]
impl Tool for RecallMemoryTool {
fn name(&self) -> &str {
"recall_memory"
}
fn description(&self) -> &str {
"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."
}
fn risk_level(&self) -> RiskLevel {
RiskLevel::Safe
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The memory index entry ID from the [Memory Index] block"
}
},
"required": ["id"]
})
}
async fn execute(
&self,
params: Value,
ctx: &ToolContext,
) -> std::result::Result<ToolResult, ToolError> {
let index_id = params
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError {
message: "missing 'id' parameter".into(),
})?;
let db = ctx.db.as_ref().ok_or_else(|| ToolError {
message: "database not available".into(),
})?;
let entry = roboticus_db::memory_index::top_entries(db, 100)
.map_err(|e| ToolError {
message: format!("index query failed: {e}"),
})?
.into_iter()
.find(|e| e.id == index_id);
let entry = match entry {
Some(e) => e,
None => {
return Ok(ToolResult {
output: format!("No memory index entry found with ID: {index_id}"),
metadata: None,
});
}
};
let content =
roboticus_db::memory_index::recall_content(db, &entry.source_table, &entry.source_id)
.map_err(|e| ToolError {
message: format!("recall failed: {e}"),
})?;
let result = json!({
"id": index_id,
"source": entry.source_table,
"category": entry.category,
"confidence": entry.confidence,
"content": content.unwrap_or_else(|| "(content no longer available — source may have been pruned)".into()),
});
Ok(ToolResult {
output: serde_json::to_string_pretty(&result).unwrap_or_else(|_| "{}".into()),
metadata: Some(result),
})
}
}