Skip to main content

everruns_core/
agent_trigger.rs

1// Agent trigger domain types
2//
3// Design Decision:
4// - AgentTrigger is an org-scoped, agent-owned entity that describes how an
5//   agent gets invoked autonomously (e.g. on a schedule). It mirrors the
6//   AgentIdentity CRUD shape (see `agent_identity.rs`).
7// - The concrete per-type configuration lives in `config` (JSONB). Typed
8//   accessors parse it on demand, mirroring `AppChannel::schedule_config()`.
9
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13// Reuse the app-side invocation/schedule config so schedule triggers and
14// schedule channels share one shape. Do not duplicate these.
15use crate::app::InvocationSessionMode;
16use crate::typed_id::{AgentId, TriggerId};
17
18#[cfg(feature = "openapi")]
19use utoipa::ToSchema;
20
21/// The kind of event that fires an agent trigger. Only scheduled triggers exist
22/// today; the enum leaves room for webhook/event triggers later.
23#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
24#[cfg_attr(feature = "openapi", derive(ToSchema))]
25#[serde(rename_all = "lowercase")]
26pub enum AgentTriggerType {
27    /// Cron-driven schedule trigger.
28    #[default]
29    Schedule,
30}
31
32impl std::fmt::Display for AgentTriggerType {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            AgentTriggerType::Schedule => write!(f, "schedule"),
36        }
37    }
38}
39
40impl From<&str> for AgentTriggerType {
41    fn from(_value: &str) -> Self {
42        // Only one variant exists today; every stored value maps to Schedule.
43        Self::Schedule
44    }
45}
46
47/// Typed configuration for a `Schedule` trigger.
48///
49/// `message` is also the template body. `{{path.to.value}}` placeholders are
50/// expanded at invocation time. Mirrors `app::ScheduleChannelConfig`.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[cfg_attr(feature = "openapi", derive(ToSchema))]
53pub struct ScheduleTriggerConfig {
54    /// Cron expression that drives the durable schedule.
55    pub cron_expression: String,
56    /// IANA timezone identifier for cron evaluation.
57    #[serde(default = "default_timezone")]
58    pub timezone: String,
59    /// Whether invocations reuse a stable session or create a new one.
60    #[serde(default)]
61    pub session_mode: InvocationSessionMode,
62    /// Message content or template sent when the schedule fires.
63    pub message: String,
64}
65
66fn default_timezone() -> String {
67    "UTC".to_string()
68}
69
70/// AgentTrigger is a durable, agent-owned invocation trigger.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72#[cfg_attr(feature = "openapi", derive(ToSchema))]
73pub struct AgentTrigger {
74    /// External identifier (trg_<32-hex>). Shown as `id` in API.
75    #[serde(rename = "id")]
76    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "trg_01933b5a000070008000000000000001"))]
77    pub id: TriggerId,
78    /// Agent that owns this trigger.
79    #[cfg_attr(feature = "openapi", schema(value_type = String, example = "agent_01933b5a000070008000000000000001"))]
80    pub agent_id: AgentId,
81    /// The kind of event that fires this trigger.
82    pub trigger_type: AgentTriggerType,
83    /// Type-specific configuration (parsed via typed accessors).
84    pub config: serde_json::Value,
85    /// Whether the trigger is currently active.
86    pub enabled: bool,
87    /// Creation timestamp.
88    pub created_at: DateTime<Utc>,
89    /// Last update timestamp.
90    pub updated_at: DateTime<Utc>,
91    /// Archive timestamp.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub archived_at: Option<DateTime<Utc>>,
94    /// Delete timestamp.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub deleted_at: Option<DateTime<Utc>>,
97}
98
99impl AgentTrigger {
100    /// Parse `config` as [`ScheduleTriggerConfig`]. Errors if this is not a
101    /// schedule trigger or the config is malformed. Mirrors
102    /// `AppChannel::schedule_config()`.
103    pub fn schedule_config(&self) -> anyhow::Result<ScheduleTriggerConfig> {
104        if self.trigger_type != AgentTriggerType::Schedule {
105            anyhow::bail!("agent trigger {} is not a schedule trigger", self.id);
106        }
107        Ok(serde_json::from_value(self.config.clone())?)
108    }
109}