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