aidaemon 0.11.1

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

use async_trait::async_trait;
use serde::Deserialize;
use serde_json::{json, Value};
use crate::tools::ApprovalBroker;
use crate::tools::manage_memories::ManageMemoriesTool;
use crate::traits::{
    StateStore, Tool, ToolCallSemantics, ToolCapabilities, ToolSemanticAffordances,
    ToolSemanticFacet, ToolSemanticScope, ToolVerificationMode,
};

const SCHEDULED_GOAL_ACTIONS: &[&str] = &[
    "create_scheduled_goal",
    "list_scheduled",
    "list_scheduled_matching",
    "add_schedule",
    "cancel_scheduled",
    "pause_scheduled",
    "resume_scheduled",
    "retry_scheduled",
    "retry_failed_scheduled",
    "cancel_scheduled_matching",
    "retry_scheduled_matching",
    "diagnose_scheduled",
    "trigger_now",
];

#[derive(Deserialize)]
struct ScheduledGoalArgs {
    action: String,
}

pub struct ScheduledGoalsTool {
    inner: ManageMemoriesTool,
}

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

    pub fn with_approval_tx(mut self, tx: ApprovalBroker) -> Self {
        self.inner = self.inner.with_approval_tx(tx);
        self
    }

    fn action_from_args(arguments: &str) -> Option<String> {
        serde_json::from_str::<ScheduledGoalArgs>(arguments)
            .ok()
            .map(|args| args.action)
    }
}

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

    fn description(&self) -> &str {
        "Create, list, diagnose, and manage scheduled goals and reminders. Use this for new reminders/recurring tasks and for questions about existing scheduled tasks."
    }

    fn schema(&self) -> Value {
        json!({
            "name": "scheduled_goals",
            "description": "Manage scheduled goals and reminders. \
        Use `create_scheduled_goal` for a new reminder or recurring task. \
        Use `list_scheduled`, `list_scheduled_matching`, or `diagnose_scheduled` for existing scheduled-task questions like \"what are my scheduled tasks\" or \"what happened with my Twitter reminder\". \
        `list_scheduled_matching` returns matching goals or explicitly says \"no match\" — if no match is found, that means the goal does not exist; do not retry with different search terms. \
        Use `cancel_scheduled`, `pause_scheduled`, `resume_scheduled`, `retry_scheduled`, `retry_failed_scheduled`, `add_schedule`, or `trigger_now` to modify an existing scheduled goal. \
        If you do not know a goal ID yet, list scheduled goals first.",
            "parameters": {
                "type": "object",
                "properties": {
                    "action": {
                        "type": "string",
                        "enum": SCHEDULED_GOAL_ACTIONS,
                        "description": "Scheduled goal action"
                    },
                    "goal": {
                        "type": "string",
                        "description": "Goal description for a new scheduled goal"
                    },
                    "goal_id": {
                        "type": "string",
                        "description": "Goal ID or unique prefix for an existing scheduled goal"
                    },
                    "schedule_id": {
                        "type": "string",
                        "description": "Specific schedule ID when modifying one schedule on a goal"
                    },
                    "schedule": {
                        "type": "string",
                        "description": "Natural-language schedule like 'in 2 hours' or 'every Monday at 9am'"
                    },
                    "schedules": {
                        "type": "array",
                        "items": { "type": "string" },
                        "description": "Multiple natural-language schedules for batch creation"
                    },
                    "query": {
                        "type": "string",
                        "description": "Text query for matching scheduled goals"
                    },
                    "fire_policy": {
                        "type": "string",
                        "enum": ["coalesce", "always_fire"],
                        "description": "How recurring schedules behave when runs overlap"
                    },
                    "is_one_shot": {
                        "type": "boolean",
                        "description": "Whether the schedule should auto-complete after the first run"
                    },
                    "is_paused": {
                        "type": "boolean",
                        "description": "Whether a new scheduled goal should start paused"
                    },
                    "limit": {
                        "type": "integer",
                        "description": "Max items to return for list actions"
                    }
                },
                "required": ["action"],
                "additionalProperties": false
            }
        })
    }

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

    fn call_semantics(&self, arguments: &str) -> ToolCallSemantics {
        match Self::action_from_args(arguments).as_deref() {
            Some("list_scheduled" | "list_scheduled_matching" | "diagnose_scheduled") => {
                ToolCallSemantics::observation()
                    .with_verification_mode(ToolVerificationMode::ResultContent)
            }
            Some(_) | None => ToolCallSemantics::mutation(),
        }
    }

    fn semantic_affordances(&self) -> Option<ToolSemanticAffordances> {
        Some(ToolSemanticAffordances::new(
            ToolSemanticScope::GoalState,
            &[ToolSemanticFacet::GoalState],
        ))
    }

    async fn call(&self, arguments: &str) -> anyhow::Result<String> {
        let args: ScheduledGoalArgs = serde_json::from_str(arguments)?;
        if !SCHEDULED_GOAL_ACTIONS.contains(&args.action.as_str()) {
            return Ok(format!(
                "Unknown scheduled_goals action: '{}'. Use one of: {}.",
                args.action,
                SCHEDULED_GOAL_ACTIONS.join(", ")
            ));
        }

        <ManageMemoriesTool as Tool>::call(&self.inner, arguments).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::memory::embeddings::EmbeddingService;
    use crate::state::SqliteStateStore;
    use crate::traits::Tool;

    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
    }

    #[tokio::test]
    async fn list_actions_are_observation_semantics() {
        let tool = ScheduledGoalsTool::new(setup_state().await);
        let semantics = tool.call_semantics(r#"{"action":"list_scheduled"}"#);
        assert!(semantics.observes_state());
        assert!(!semantics.mutates_state());
    }

    #[tokio::test]
    async fn create_actions_are_mutation_semantics() {
        let tool = ScheduledGoalsTool::new(setup_state().await);
        let semantics = tool.call_semantics(
            r#"{"action":"create_scheduled_goal","goal":"check logs","schedule":"in 2 hours"}"#,
        );
        assert!(semantics.mutates_state());
    }

    #[tokio::test]
    async fn affordances_are_goal_state() {
        let tool = ScheduledGoalsTool::new(setup_state().await);
        let affordances = tool.semantic_affordances();
        let affordances = affordances.expect("scheduled_goals should advertise affordances");
        assert_eq!(affordances.scope, ToolSemanticScope::GoalState);
        assert!(affordances.supports(ToolSemanticFacet::GoalState));
    }
}