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