aidaemon 0.11.13

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 anyhow::Context;
use async_trait::async_trait;
use serde::Deserialize;
use serde_json::{json, Value};

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

pub struct PolicyMetricsTool;

#[derive(Debug, Deserialize)]
struct PolicyMetricsArgs {
    #[serde(default)]
    format: Option<String>,
}

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

    fn description(&self) -> &str {
        "Read runtime policy metrics (response routing outcomes, no-progress iterations, and failed-task token burn)"
    }

    fn schema(&self) -> Value {
        json!({
            "name": "policy_metrics",
            "description": "Read runtime policy/agent-loop metrics. Use this when the user asks about routing behavior, no-progress loops, or token cost of failed tasks.",
            "parameters": {
                "type": "object",
                "properties": {
                    "format": {
                        "type": "string",
                        "enum": ["json", "summary"],
                        "description": "Output format. json returns machine-readable JSON (default). summary returns a concise text summary."
                    }
                },
                "additionalProperties": false
            }
        })
    }

    fn tool_role(&self) -> ToolRole {
        ToolRole::Universal
    }

    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_input = if arguments.trim().is_empty() {
            "{}"
        } else {
            arguments
        };
        let args: PolicyMetricsArgs = serde_json::from_str(args_input)
            .with_context(|| "policy_metrics arguments must be valid JSON")?;
        let metrics = crate::agent::policy_metrics_snapshot();

        let response_total =
            metrics.response_direct_return_total + metrics.response_fallthrough_total;
        let response_direct_return_rate = if response_total > 0 {
            metrics.response_direct_return_total as f64 / response_total as f64
        } else {
            0.0
        };
        let response_fallthrough_rate = if response_total > 0 {
            metrics.response_fallthrough_total as f64 / response_total as f64
        } else {
            0.0
        };
        let route_reason_total = metrics.orchestration_route_clarification_required_total
            + metrics.orchestration_route_tools_required_total
            + metrics.orchestration_route_short_correction_direct_reply_total
            + metrics.orchestration_route_acknowledgment_direct_reply_total
            + metrics.orchestration_route_default_continue_total;
        let route_reason_tools_required_rate = if route_reason_total > 0 {
            metrics.orchestration_route_tools_required_total as f64 / route_reason_total as f64
        } else {
            0.0
        };
        let route_reason_return_rate = if route_reason_total > 0 {
            (metrics.orchestration_route_clarification_required_total
                + metrics.orchestration_route_short_correction_direct_reply_total
                + metrics.orchestration_route_acknowledgment_direct_reply_total) as f64
                / route_reason_total as f64
        } else {
            0.0
        };
        let est_input_token_samples = metrics.est_input_token_samples;
        let avg_est_input_tokens_per_call = if est_input_token_samples > 0 {
            metrics.est_input_tokens_total as f64 / est_input_token_samples as f64
        } else {
            0.0
        };
        let avg_est_tool_tokens_per_call = if est_input_token_samples > 0 {
            metrics.est_tool_tokens_total as f64 / est_input_token_samples as f64
        } else {
            0.0
        };
        let est_tool_token_share_rate = if metrics.est_input_tokens_total > 0 {
            metrics.est_tool_tokens_total as f64 / metrics.est_input_tokens_total as f64
        } else {
            0.0
        };
        let est_tool_tokens_high_share_rate = if est_input_token_samples > 0 {
            metrics.est_tool_tokens_high_share_total as f64 / est_input_token_samples as f64
        } else {
            0.0
        };
        let est_tool_tokens_high_abs_rate = if est_input_token_samples > 0 {
            metrics.est_tool_tokens_high_abs_total as f64 / est_input_token_samples as f64
        } else {
            0.0
        };

        if args
            .format
            .as_deref()
            .is_some_and(|fmt| fmt.eq_ignore_ascii_case("summary"))
        {
            return Ok(format!(
                "Policy metrics summary\n\
                 - response_direct_return_total: {}\n\
                 - response_fallthrough_total: {}\n\
                 - response_direct_return_rate: {:.3}\n\
                 - response_fallthrough_rate: {:.3}\n\
                 - orchestration_route_clarification_required_total: {}\n\
                 - orchestration_route_tools_required_total: {}\n\
                 - orchestration_route_short_correction_direct_reply_total: {}\n\
                 - orchestration_route_acknowledgment_direct_reply_total: {}\n\
                 - orchestration_route_default_continue_total: {}\n\
                 - orchestration_route_tools_required_rate: {:.3}\n\
                 - orchestration_route_return_rate: {:.3}\n\
                 - context_bleed_prevented_total: {}\n\
                 - context_mismatch_preflight_drop_total: {}\n\
                 - followup_mode_overrides_total: {}\n\
                 - cross_scope_blocked_total: {}\n\
                 - tool_schema_contract_rejections_total: {}\n\
                 - route_drift_alert_total: {}\n\
                 - route_drift_failsafe_activation_total: {}\n\
                 - route_failsafe_active_turn_total: {}\n\
                 - tokens_failed_tasks_total: {}\n\
                 - est_input_token_samples: {}\n\
                 - est_input_tokens_total: {}\n\
                 - est_tool_tokens_total: {}\n\
                 - avg_est_input_tokens_per_call: {:.1}\n\
                 - avg_est_tool_tokens_per_call: {:.1}\n\
                 - est_tool_token_share_rate: {:.3}\n\
                 - est_tool_tokens_high_share_total: {}\n\
                 - est_tool_tokens_high_share_rate: {:.3}\n\
                 - est_tool_tokens_high_abs_total: {}\n\
                 - est_tool_tokens_high_abs_rate: {:.3}\n\
                 - no_progress_iterations_total: {}\n\
                 - deferred_no_tool_forced_required_total: {}\n\
                 - deferred_no_tool_deferral_detected_total: {}\n\
                 - deferred_no_tool_model_switch_total: {}\n\
                 - deferred_no_tool_error_marker_total: {}\n\
                 - llm_payload_invalid_total: {}\n\
                 - llm_payload_invalid_breakdown_entries: {}",
                metrics.response_direct_return_total,
                metrics.response_fallthrough_total,
                response_direct_return_rate,
                response_fallthrough_rate,
                metrics.orchestration_route_clarification_required_total,
                metrics.orchestration_route_tools_required_total,
                metrics.orchestration_route_short_correction_direct_reply_total,
                metrics.orchestration_route_acknowledgment_direct_reply_total,
                metrics.orchestration_route_default_continue_total,
                route_reason_tools_required_rate,
                route_reason_return_rate,
                metrics.context_bleed_prevented_total,
                metrics.context_mismatch_preflight_drop_total,
                metrics.followup_mode_overrides_total,
                metrics.cross_scope_blocked_total,
                metrics.tool_schema_contract_rejections_total,
                metrics.route_drift_alert_total,
                metrics.route_drift_failsafe_activation_total,
                metrics.route_failsafe_active_turn_total,
                metrics.tokens_failed_tasks_total,
                metrics.est_input_token_samples,
                metrics.est_input_tokens_total,
                metrics.est_tool_tokens_total,
                avg_est_input_tokens_per_call,
                avg_est_tool_tokens_per_call,
                est_tool_token_share_rate,
                metrics.est_tool_tokens_high_share_total,
                est_tool_tokens_high_share_rate,
                metrics.est_tool_tokens_high_abs_total,
                est_tool_tokens_high_abs_rate,
                metrics.no_progress_iterations_total,
                metrics.deferred_no_tool_forced_required_total,
                metrics.deferred_no_tool_deferral_detected_total,
                metrics.deferred_no_tool_model_switch_total,
                metrics.deferred_no_tool_error_marker_total,
                metrics.llm_payload_invalid_total,
                metrics.llm_payload_invalid_breakdown.len(),
            ));
        }

        let payload = json!({
            "metrics": metrics,
            "derived": {
                "response_total": response_total,
                "response_direct_return_rate": response_direct_return_rate,
                "response_fallthrough_rate": response_fallthrough_rate,
                "orchestration_route_reason_total": route_reason_total,
                "orchestration_route_tools_required_rate": route_reason_tools_required_rate,
                "orchestration_route_return_rate": route_reason_return_rate,
                "route_drift_total": metrics.route_drift_alert_total,
                "context_integrity_guard_events_total": metrics.context_bleed_prevented_total
                    + metrics.context_mismatch_preflight_drop_total
                    + metrics.followup_mode_overrides_total
                    + metrics.cross_scope_blocked_total,
                "avg_est_input_tokens_per_call": avg_est_input_tokens_per_call,
                "avg_est_tool_tokens_per_call": avg_est_tool_tokens_per_call,
                "est_tool_token_share_rate": est_tool_token_share_rate,
                "est_tool_tokens_high_share_rate": est_tool_tokens_high_share_rate,
                "est_tool_tokens_high_abs_rate": est_tool_tokens_high_abs_rate,
                "deferred_no_tool_recovery_effectiveness_rate": if metrics.deferred_no_tool_forced_required_total > 0 {
                    metrics
                        .deferred_no_tool_forced_required_total
                        .saturating_sub(metrics.deferred_no_tool_error_marker_total) as f64
                        / metrics.deferred_no_tool_forced_required_total as f64
                } else {
                    0.0
                }
            }
        });
        Ok(serde_json::to_string_pretty(&payload)?)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn returns_metrics_json_payload() {
        let tool = PolicyMetricsTool;
        let output = tool.call("{}").await.unwrap();
        let parsed: Value = serde_json::from_str(&output).unwrap();

        assert!(parsed.get("metrics").is_some());
        assert!(parsed.get("derived").is_some());
        assert!(parsed
            .get("metrics")
            .and_then(|m| m.get("response_direct_return_total"))
            .is_some());
        assert!(parsed
            .get("metrics")
            .and_then(|m| m.get("response_fallthrough_total"))
            .is_some());
        assert!(parsed
            .get("metrics")
            .and_then(|m| m.get("orchestration_route_tools_required_total"))
            .is_some());
        assert!(parsed
            .get("metrics")
            .and_then(|m| m.get("tokens_failed_tasks_total"))
            .is_some());
        assert!(parsed
            .get("metrics")
            .and_then(|m| m.get("est_input_token_samples"))
            .is_some());
        assert!(parsed
            .get("metrics")
            .and_then(|m| m.get("est_tool_tokens_total"))
            .is_some());
        assert!(parsed
            .get("metrics")
            .and_then(|m| m.get("no_progress_iterations_total"))
            .is_some());
        assert!(parsed
            .get("metrics")
            .and_then(|m| m.get("cross_scope_blocked_total"))
            .is_some());
        assert!(parsed
            .get("metrics")
            .and_then(|m| m.get("deferred_no_tool_forced_required_total"))
            .is_some());
        assert!(parsed
            .get("metrics")
            .and_then(|m| m.get("deferred_no_tool_model_switch_total"))
            .is_some());
    }
}