aidaemon 0.10.0

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::fs;
use std::path::{Path, PathBuf};

const MAX_SCHEMA_SEGMENT_CHARS: usize = 6_500;
const MAX_TOTAL_SCHEMA_SEGMENT_CHARS: usize = 100_000;

fn tools_dir() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("src/tools")
}

fn tool_source_files() -> Vec<PathBuf> {
    let mut files = fs::read_dir(tools_dir())
        .expect("read src/tools")
        .filter_map(|entry| entry.ok())
        .map(|entry| entry.path())
        .filter(|path| path.extension().is_some_and(|ext| ext == "rs"))
        .filter(|path| path.file_name().and_then(|n| n.to_str()) != Some("mod.rs"))
        .filter(|path| path.file_name().and_then(|n| n.to_str()) != Some("schema_lint_tests.rs"))
        .filter(|path| {
            fs::read_to_string(path)
                .ok()
                .is_some_and(|src| src.contains("impl Tool for"))
        })
        .collect::<Vec<_>>();
    files.sort();
    files
}

fn schema_segment(source: &str) -> Option<&str> {
    let (_, after_schema_start) = source.split_once("fn schema(&self) -> Value {")?;
    let (segment, _) = after_schema_start.split_once("async fn call(")?;
    Some(segment)
}

#[test]
fn all_tool_schemas_disable_additional_properties() {
    for file in tool_source_files() {
        let source = fs::read_to_string(&file).expect("read tool source");
        let segment = schema_segment(&source)
            .unwrap_or_else(|| panic!("Could not locate schema segment in {}", file.display()));
        assert!(
            segment.contains("\"additionalProperties\": false"),
            "Schema must include parameters.additionalProperties=false: {}",
            file.display()
        );
    }
}

#[test]
fn all_tools_define_explicit_capabilities() {
    for file in tool_source_files() {
        let source = fs::read_to_string(&file).expect("read tool source");
        assert!(
            source.contains("fn capabilities(&self) -> ToolCapabilities"),
            "Tool must define explicit capabilities: {}",
            file.display()
        );
    }
}

#[test]
fn tools_do_not_silently_fallback_to_empty_arguments() {
    for file in tool_source_files() {
        let source = fs::read_to_string(&file).expect("read tool source");
        assert!(
            !source.contains("from_str(arguments).unwrap_or(json!({}))"),
            "Tool must not swallow argument parse errors via empty-object fallback: {}",
            file.display()
        );
    }
}

#[test]
fn schema_payload_budget_stays_bounded() {
    let mut total = 0usize;
    for file in tool_source_files() {
        let source = fs::read_to_string(&file).expect("read tool source");
        let segment = schema_segment(&source)
            .unwrap_or_else(|| panic!("Could not locate schema segment in {}", file.display()));
        total += segment.len();
        assert!(
            segment.len() <= MAX_SCHEMA_SEGMENT_CHARS,
            "Schema segment too large ({} chars) in {}",
            segment.len(),
            file.display()
        );
    }

    assert!(
        total <= MAX_TOTAL_SCHEMA_SEGMENT_CHARS,
        "Total schema segment payload too large: {} chars (max {})",
        total,
        MAX_TOTAL_SCHEMA_SEGMENT_CHARS
    );
}