chasm_cli/agency/
proactive.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: Apache-2.0
3//! Proactive Agent System
4//!
5//! Agents that monitor, detect problems, and solve them with user permission.
6//!
7//! ## Household Agent
8//! - Smart home monitoring
9//! - Bill and expense tracking
10//! - Maintenance scheduling
11//! - Grocery and supply management
12//!
13//! ## Business Agent
14//! - Calendar optimization
15//! - Email triage and follow-ups
16//! - Meeting preparation
17//! - Project health monitoring
18
19#![allow(dead_code)]
20
21use chrono::{DateTime, Utc};
22use serde::{Deserialize, Serialize};
23use std::collections::HashMap;
24
25// =============================================================================
26// Permission System
27// =============================================================================
28
29/// Permission level for proactive actions
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32#[derive(Default)]
33pub enum PermissionLevel {
34    /// Agent can only send notifications, no actions taken
35    NotifyOnly,
36    /// Auto-approve low-risk actions (notifications, reading data)
37    #[default]
38    LowRisk,
39    /// Auto-approve medium-risk (scheduling, drafts)
40    MediumRisk,
41    /// High autonomy - approve most except financial/external comms
42    HighAutonomy,
43}
44
45impl PermissionLevel {
46    /// Actions that can be auto-approved at this level
47    pub fn auto_approve_actions(&self) -> Vec<&'static str> {
48        match self {
49            PermissionLevel::NotifyOnly => vec![],
50            PermissionLevel::LowRisk => vec![
51                "send_notification",
52                "calendar_read",
53                "email_read",
54                "web_search",
55                "package_track",
56                "weather_check",
57                "energy_read",
58            ],
59            PermissionLevel::MediumRisk => vec![
60                "send_notification",
61                "calendar_read",
62                "calendar_create",
63                "email_read",
64                "email_draft",
65                "document_create",
66                "web_search",
67                "package_track",
68                "weather_check",
69                "energy_read",
70                "reminder_create",
71                "task_create",
72            ],
73            PermissionLevel::HighAutonomy => vec!["*"], // All except blocklist
74        }
75    }
76
77    /// Actions that always require approval regardless of level
78    pub fn always_require_approval() -> Vec<&'static str> {
79        vec![
80            "bill_pay",
81            "email_send",
82            "slack_send",
83            "teams_send",
84            "discord_send",
85            "sms_send",
86            "expense_submit",
87            "purchase",
88            "transfer_money",
89            "delete_file",
90            "share_external",
91            "change_password",
92        ]
93    }
94
95    /// Check if an action can be auto-approved
96    pub fn can_auto_approve(&self, action: &str) -> bool {
97        // Check blocklist first
98        if Self::always_require_approval().contains(&action) {
99            return false;
100        }
101
102        let allowed = self.auto_approve_actions();
103        allowed.contains(&"*") || allowed.contains(&action)
104    }
105}
106
107// =============================================================================
108// Problem Detection
109// =============================================================================
110
111/// Severity level of a detected problem
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "lowercase")]
114pub enum ProblemSeverity {
115    /// Informational, no action required
116    Info,
117    /// Warning, action recommended
118    Warning,
119    /// Urgent, action needed soon
120    Urgent,
121    /// Critical, immediate action required
122    Critical,
123}
124
125/// Status of a detected problem
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum ProblemStatus {
129    /// Newly detected
130    #[default]
131    New,
132    /// User acknowledged
133    Acknowledged,
134    /// Being addressed
135    InProgress,
136    /// Problem resolved
137    Resolved,
138    /// User dismissed
139    Dismissed,
140}
141
142/// Category of problem (household or business)
143#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(rename_all = "snake_case")]
145pub enum ProblemCategory {
146    // Household
147    BillDue {
148        amount: f64,
149        due_date: DateTime<Utc>,
150        vendor: String,
151    },
152    MaintenanceNeeded {
153        item: String,
154        urgency: String,
155    },
156    SupplyLow {
157        item: String,
158        current_quantity: u32,
159        reorder_threshold: u32,
160    },
161    EnergyAnomaly {
162        device: String,
163        expected_kwh: f64,
164        actual_kwh: f64,
165    },
166    DeviceOffline {
167        device_id: String,
168        device_name: String,
169        last_seen: DateTime<Utc>,
170    },
171    SecurityAlert {
172        alert_type: String,
173        location: String,
174    },
175    PackageDelayed {
176        tracking_id: String,
177        carrier: String,
178        expected_date: String,
179    },
180    AppointmentReminder {
181        title: String,
182        time: DateTime<Utc>,
183        location: Option<String>,
184    },
185    WeatherAlert {
186        alert_type: String,
187        severity: String,
188    },
189    SubscriptionRenewal {
190        service: String,
191        amount: f64,
192        renewal_date: DateTime<Utc>,
193    },
194
195    // Business
196    CalendarConflict {
197        event1: String,
198        event2: String,
199        overlap_minutes: u32,
200    },
201    DeadlineApproaching {
202        project: String,
203        deadline: DateTime<Utc>,
204        days_remaining: u32,
205    },
206    EmailUrgent {
207        from: String,
208        subject: String,
209        received_at: DateTime<Utc>,
210    },
211    MeetingPrepNeeded {
212        meeting: String,
213        time: DateTime<Utc>,
214        prep_items: Vec<String>,
215    },
216    FollowUpDue {
217        context: String,
218        person: String,
219        due_date: DateTime<Utc>,
220    },
221    ExpensePending {
222        amount: f64,
223        category: String,
224        days_pending: u32,
225    },
226    ProjectAtRisk {
227        project: String,
228        risk_factors: Vec<String>,
229    },
230    CompetitorNews {
231        competitor: String,
232        headline: String,
233    },
234    TeamBlocker {
235        team_member: String,
236        blocker: String,
237    },
238    ReportDue {
239        report_name: String,
240        due_date: DateTime<Utc>,
241    },
242
243    /// Custom problem type
244    Custom {
245        category: String,
246        details: HashMap<String, serde_json::Value>,
247    },
248}
249
250/// A detected problem requiring attention
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct DetectedProblem {
253    /// Unique problem ID
254    pub id: String,
255    /// Agent that detected the problem
256    pub agent_id: String,
257    /// Problem category with details
258    pub category: ProblemCategory,
259    /// Human-readable title
260    pub title: String,
261    /// Detailed description
262    pub description: String,
263    /// Problem severity
264    pub severity: ProblemSeverity,
265    /// When the problem was detected
266    pub detected_at: DateTime<Utc>,
267    /// Source of detection (integration, scan, etc.)
268    pub source: String,
269    /// Suggested actions to resolve
270    pub suggested_actions: Vec<ProactiveAction>,
271    /// Current status
272    pub status: ProblemStatus,
273    /// When the problem was resolved
274    pub resolved_at: Option<DateTime<Utc>>,
275    /// Additional metadata
276    pub metadata: HashMap<String, serde_json::Value>,
277}
278
279// =============================================================================
280// Proactive Actions
281// =============================================================================
282
283/// Risk level of an action
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
285#[serde(rename_all = "lowercase")]
286pub enum ActionRisk {
287    Low,
288    Medium,
289    High,
290}
291
292/// Status of a proactive action
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
294#[serde(rename_all = "snake_case")]
295pub enum ActionStatus {
296    /// Waiting for user approval
297    #[default]
298    Pending,
299    /// User approved the action
300    Approved,
301    /// User rejected the action
302    Rejected,
303    /// Action was executed
304    Executed,
305    /// Action was cancelled
306    Cancelled,
307    /// Action failed during execution
308    Failed,
309}
310
311/// A proactive action proposed by an agent
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct ProactiveAction {
314    /// Unique action ID
315    pub id: String,
316    /// Agent proposing the action
317    pub agent_id: String,
318    /// Related problem ID (if any)
319    pub problem_id: Option<String>,
320    /// Type of action (e.g., "send_email", "schedule_maintenance")
321    pub action_type: String,
322    /// Human-readable description
323    pub description: String,
324    /// Why the agent recommends this action
325    pub reasoning: String,
326    /// Estimated impact/benefit
327    pub estimated_impact: Option<String>,
328    /// Risk level
329    pub risk_level: ActionRisk,
330    /// Current status
331    pub status: ActionStatus,
332    /// Whether it was auto-approved based on permission level
333    pub auto_approved: bool,
334    /// When the action was approved
335    pub approved_at: Option<DateTime<Utc>>,
336    /// Who approved (user ID or "auto")
337    pub approved_by: Option<String>,
338    /// When the action was executed
339    pub executed_at: Option<DateTime<Utc>>,
340    /// Result of execution
341    pub result: Option<String>,
342    /// Error if execution failed
343    pub error: Option<String>,
344    /// Action parameters
345    pub parameters: HashMap<String, serde_json::Value>,
346    /// When the action was created
347    pub created_at: DateTime<Utc>,
348}
349
350// =============================================================================
351// Proactive Agent Configuration
352// =============================================================================
353
354/// Configuration for a proactive agent
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct ProactiveAgentConfig {
357    /// Agent ID
358    pub agent_id: String,
359    /// Whether the agent is enabled
360    pub enabled: bool,
361    /// Permission level for auto-approving actions
362    pub permission_level: PermissionLevel,
363    /// Integrations to monitor
364    pub monitored_integrations: Vec<String>,
365    /// Problem categories to watch for
366    pub watch_categories: Vec<String>,
367    /// How often to scan (in seconds)
368    pub scan_interval_secs: u64,
369    /// Quiet hours - don't notify during these times
370    pub quiet_hours: Option<QuietHours>,
371    /// Maximum pending actions before requiring review
372    pub max_pending_actions: u32,
373    /// Custom rules for auto-approval
374    pub custom_rules: Vec<ApprovalRule>,
375}
376
377impl Default for ProactiveAgentConfig {
378    fn default() -> Self {
379        Self {
380            agent_id: String::new(),
381            enabled: true,
382            permission_level: PermissionLevel::LowRisk,
383            monitored_integrations: Vec::new(),
384            watch_categories: Vec::new(),
385            scan_interval_secs: 300, // 5 minutes
386            quiet_hours: None,
387            max_pending_actions: 10,
388            custom_rules: Vec::new(),
389        }
390    }
391}
392
393/// Quiet hours configuration
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct QuietHours {
396    /// Start time (e.g., "22:00")
397    pub start: String,
398    /// End time (e.g., "07:00")
399    pub end: String,
400    /// Days to apply (0=Sunday, 6=Saturday)
401    pub days: Vec<u8>,
402    /// Whether to still allow critical alerts
403    pub allow_critical: bool,
404}
405
406/// Custom approval rule
407#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct ApprovalRule {
409    /// Rule name
410    pub name: String,
411    /// Action type to match
412    pub action_type: String,
413    /// Conditions that must be met
414    pub conditions: Vec<RuleCondition>,
415    /// Whether to auto-approve if conditions match
416    pub auto_approve: bool,
417}
418
419/// Condition for an approval rule
420#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct RuleCondition {
422    /// Field to check
423    pub field: String,
424    /// Operator
425    pub operator: ConditionOperator,
426    /// Value to compare
427    pub value: serde_json::Value,
428}
429
430/// Condition operators
431#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
432#[serde(rename_all = "snake_case")]
433pub enum ConditionOperator {
434    Equals,
435    NotEquals,
436    GreaterThan,
437    LessThan,
438    Contains,
439    StartsWith,
440    EndsWith,
441    Matches,
442}
443
444// =============================================================================
445// Household Agent Specifics
446// =============================================================================
447
448/// Household agent preset configuration
449pub fn household_agent_config() -> ProactiveAgentConfig {
450    ProactiveAgentConfig {
451        agent_id: "household".to_string(),
452        enabled: true,
453        permission_level: PermissionLevel::LowRisk,
454        monitored_integrations: vec![
455            "home_assistant".to_string(),
456            "google_calendar".to_string(),
457            "gmail".to_string(),
458            "todoist".to_string(),
459            "plaid".to_string(),
460            "amazon".to_string(),
461            "instacart".to_string(),
462            "hue".to_string(),
463            "nest".to_string(),
464        ],
465        watch_categories: vec![
466            "bill_due".to_string(),
467            "maintenance_needed".to_string(),
468            "supply_low".to_string(),
469            "energy_anomaly".to_string(),
470            "device_offline".to_string(),
471            "security_alert".to_string(),
472            "package_delayed".to_string(),
473            "appointment_reminder".to_string(),
474            "weather_alert".to_string(),
475            "subscription_renewal".to_string(),
476        ],
477        scan_interval_secs: 300,
478        quiet_hours: Some(QuietHours {
479            start: "22:00".to_string(),
480            end: "07:00".to_string(),
481            days: vec![0, 1, 2, 3, 4, 5, 6],
482            allow_critical: true,
483        }),
484        max_pending_actions: 10,
485        custom_rules: vec![ApprovalRule {
486            name: "auto_remind_low_supplies".to_string(),
487            action_type: "send_notification".to_string(),
488            conditions: vec![RuleCondition {
489                field: "category".to_string(),
490                operator: ConditionOperator::Equals,
491                value: serde_json::json!("supply_low"),
492            }],
493            auto_approve: true,
494        }],
495    }
496}
497
498// =============================================================================
499// Business Agent Specifics
500// =============================================================================
501
502/// Business agent preset configuration
503pub fn business_agent_config() -> ProactiveAgentConfig {
504    ProactiveAgentConfig {
505        agent_id: "business".to_string(),
506        enabled: true,
507        permission_level: PermissionLevel::MediumRisk,
508        monitored_integrations: vec![
509            "google_calendar".to_string(),
510            "outlook".to_string(),
511            "gmail".to_string(),
512            "slack".to_string(),
513            "teams".to_string(),
514            "notion".to_string(),
515            "linear".to_string(),
516            "github".to_string(),
517        ],
518        watch_categories: vec![
519            "calendar_conflict".to_string(),
520            "deadline_approaching".to_string(),
521            "email_urgent".to_string(),
522            "meeting_prep_needed".to_string(),
523            "follow_up_due".to_string(),
524            "expense_pending".to_string(),
525            "project_at_risk".to_string(),
526            "competitor_news".to_string(),
527            "team_blocker".to_string(),
528            "report_due".to_string(),
529        ],
530        scan_interval_secs: 300,
531        quiet_hours: Some(QuietHours {
532            start: "20:00".to_string(),
533            end: "08:00".to_string(),
534            days: vec![0, 6], // Weekends only
535            allow_critical: true,
536        }),
537        max_pending_actions: 15,
538        custom_rules: vec![
539            ApprovalRule {
540                name: "auto_prep_meeting_docs".to_string(),
541                action_type: "document_create".to_string(),
542                conditions: vec![RuleCondition {
543                    field: "category".to_string(),
544                    operator: ConditionOperator::Equals,
545                    value: serde_json::json!("meeting_prep_needed"),
546                }],
547                auto_approve: true,
548            },
549            ApprovalRule {
550                name: "auto_flag_deadline".to_string(),
551                action_type: "send_notification".to_string(),
552                conditions: vec![RuleCondition {
553                    field: "days_remaining".to_string(),
554                    operator: ConditionOperator::LessThan,
555                    value: serde_json::json!(3),
556                }],
557                auto_approve: true,
558            },
559        ],
560    }
561}
562
563// =============================================================================
564// Agent Traits
565// =============================================================================
566
567/// Trait for proactive monitoring agents
568#[async_trait::async_trait]
569pub trait ProactiveMonitor: Send + Sync {
570    /// Scan integrations for problems
571    async fn scan(&self) -> Result<Vec<DetectedProblem>, Box<dyn std::error::Error + Send + Sync>>;
572
573    /// Propose actions for a detected problem
574    async fn propose_actions(
575        &self,
576        problem: &DetectedProblem,
577    ) -> Result<Vec<ProactiveAction>, Box<dyn std::error::Error + Send + Sync>>;
578
579    /// Execute an approved action
580    async fn execute_action(
581        &self,
582        action: &ProactiveAction,
583    ) -> Result<String, Box<dyn std::error::Error + Send + Sync>>;
584
585    /// Check if an action can be auto-approved
586    fn can_auto_approve(&self, action: &ProactiveAction) -> bool;
587}
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592
593    #[test]
594    fn test_permission_levels() {
595        assert!(!PermissionLevel::NotifyOnly.can_auto_approve("send_notification"));
596        assert!(PermissionLevel::LowRisk.can_auto_approve("send_notification"));
597        assert!(PermissionLevel::LowRisk.can_auto_approve("calendar_read"));
598        assert!(!PermissionLevel::LowRisk.can_auto_approve("bill_pay"));
599        assert!(PermissionLevel::MediumRisk.can_auto_approve("calendar_create"));
600        assert!(!PermissionLevel::HighAutonomy.can_auto_approve("email_send")); // Always requires approval
601    }
602
603    #[test]
604    fn test_household_config() {
605        let config = household_agent_config();
606        assert_eq!(config.agent_id, "household");
607        assert!(config
608            .monitored_integrations
609            .contains(&"home_assistant".to_string()));
610    }
611
612    #[test]
613    fn test_business_config() {
614        let config = business_agent_config();
615        assert_eq!(config.agent_id, "business");
616        assert!(config
617            .watch_categories
618            .contains(&"calendar_conflict".to_string()));
619    }
620}