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