aidaemon 0.9.33

A personal AI agent that runs as a background daemon, accessible via Telegram, Slack, or Discord, with tool use, MCP integration, and persistent memory
use std::sync::Arc;

use async_trait::async_trait;
use serde::Deserialize;
use serde_json::{json, Value};

use crate::traits::{StateStore, Tool, ToolCapabilities};

use super::goal_trace::GoalTraceTool;

pub struct ToolTraceTool {
    inner: GoalTraceTool,
}

impl ToolTraceTool {
    pub fn new(state: Arc<dyn StateStore>) -> Self {
        Self {
            inner: GoalTraceTool::new(state),
        }
    }
}

#[derive(Deserialize)]
struct ToolTraceArgs {
    #[serde(default, alias = "goal_id_v3")]
    goal_id: Option<String>,
    #[serde(default)]
    task_id: Option<String>,
    #[serde(default)]
    tool_name: Option<String>,
    #[serde(default)]
    limit: Option<usize>,
}

#[async_trait]
impl Tool for ToolTraceTool {
    fn name(&self) -> &str {
        "tool_trace"
    }

    fn description(&self) -> &str {
        "Quick alias for tool-level execution traces (same backend as goal_trace action=tool_trace)"
    }

    fn schema(&self) -> Value {
        json!({
            "name": "tool_trace",
            "description": "Quick alias for tool-level execution traces. Equivalent to goal_trace(action='tool_trace').",
            "parameters": {
                "type": "object",
                "properties": {
                    "goal_id": {
                        "type": "string",
                        "description": "Goal ID (full or unique prefix). Required when task_id is not provided."
                    },
                    "task_id": {
                        "type": "string",
                        "description": "Task ID for task-scoped trace"
                    },
                    "tool_name": {
                        "type": "string",
                        "description": "Optional tool name filter"
                    },
                    "limit": {
                        "type": "integer",
                        "description": "Max events to return (default 30, max 200)"
                    }
                },
                "additionalProperties": false
            }
        })
    }

    fn capabilities(&self) -> ToolCapabilities {
        ToolCapabilities {
            read_only: true,
            external_side_effect: false,
            needs_approval: false,
            idempotent: true,
            high_impact_write: false,
        }
    }

    async fn call(&self, arguments: &str) -> anyhow::Result<String> {
        let args: ToolTraceArgs = serde_json::from_str(arguments)?;

        let delegated = json!({
            "action": "tool_trace",
            "goal_id": args.goal_id,
            "task_id": args.task_id,
            "tool_name": args.tool_name,
            "limit": args.limit
        });
        self.inner.call(&delegated.to_string()).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::memory::embeddings::EmbeddingService;
    use crate::state::SqliteStateStore;
    use crate::traits::{Goal, Task, TaskActivity};

    async fn setup_state() -> Arc<dyn StateStore> {
        let db_file = tempfile::NamedTempFile::new().unwrap();
        let db_path = db_file.path().to_str().unwrap().to_string();
        let embedding_service = Arc::new(EmbeddingService::new().unwrap());
        let state = Arc::new(
            SqliteStateStore::new(&db_path, 100, None, embedding_service)
                .await
                .unwrap(),
        );
        std::mem::forget(db_file);
        state as Arc<dyn StateStore>
    }

    #[tokio::test]
    async fn alias_returns_tool_trace_output() {
        let state = setup_state().await;
        let tool = ToolTraceTool::new(state.clone());

        let goal = Goal::new_finite("Alias trace goal", "user-session");
        state.create_goal(&goal).await.unwrap();

        let now = chrono::Utc::now().to_rfc3339();
        let task = Task {
            id: uuid::Uuid::new_v4().to_string(),
            goal_id: goal.id.clone(),
            description: "Run task".to_string(),
            status: "completed".to_string(),
            priority: "medium".to_string(),
            task_order: 0,
            parallel_group: None,
            depends_on: None,
            agent_id: None,
            context: None,
            result: Some("ok".to_string()),
            error: None,
            blocker: None,
            idempotent: true,
            retry_count: 0,
            max_retries: 1,
            created_at: now.clone(),
            started_at: Some(now.clone()),
            completed_at: Some(now.clone()),
        };
        state.create_task(&task).await.unwrap();
        state
            .log_task_activity(&TaskActivity {
                id: 0,
                task_id: task.id.clone(),
                activity_type: "tool_result".to_string(),
                tool_name: Some("web_fetch".to_string()),
                tool_args: None,
                result: Some("ok".to_string()),
                success: Some(true),
                tokens_used: Some(7),
                created_at: now,
            })
            .await
            .unwrap();

        let result = tool
            .call(
                &json!({
                    "goal_id": goal.id,
                    "tool_name": "web_fetch"
                })
                .to_string(),
            )
            .await
            .unwrap();

        assert!(result.contains("Tool Trace"));
        assert!(result.contains("web_fetch: calls 1"));
    }
}