use std::sync::Arc;
use adk_core::{AgentInvoker, Content};
use super::agent::{AmbientAgent, TriggerHandler};
use super::event_source::TriggerEvent;
type PromptFn = Arc<dyn Fn(&TriggerEvent) -> String + Send + Sync>;
#[derive(Debug, Clone, Default)]
pub enum TriggerSessionPolicy {
#[default]
PerTrigger,
Shared(String),
}
#[derive(Clone)]
pub struct RunnerTriggerConfig {
user_id: String,
session_policy: TriggerSessionPolicy,
prompt: PromptFn,
}
impl RunnerTriggerConfig {
pub fn new(user_id: impl Into<String>) -> Self {
Self {
user_id: user_id.into(),
session_policy: TriggerSessionPolicy::default(),
prompt: Arc::new(default_prompt),
}
}
pub fn with_session_policy(mut self, policy: TriggerSessionPolicy) -> Self {
self.session_policy = policy;
self
}
pub fn with_prompt(
mut self,
prompt: impl Fn(&TriggerEvent) -> String + Send + Sync + 'static,
) -> Self {
self.prompt = Arc::new(prompt);
self
}
pub fn user_id(&self) -> &str {
&self.user_id
}
fn session_id(&self, event: &TriggerEvent) -> String {
match &self.session_policy {
TriggerSessionPolicy::PerTrigger => {
format!("{}-{}", event.source, uuid::Uuid::new_v4())
}
TriggerSessionPolicy::Shared(session_id) => session_id.clone(),
}
}
}
impl std::fmt::Debug for RunnerTriggerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RunnerTriggerConfig")
.field("user_id", &self.user_id)
.field("session_policy", &self.session_policy)
.finish_non_exhaustive()
}
}
fn default_prompt(event: &TriggerEvent) -> String {
format!("Triggered by {}. Payload: {}", event.source, event.payload)
}
impl AmbientAgent {
pub fn with_invoker(
mut self,
invoker: Arc<dyn AgentInvoker>,
config: RunnerTriggerConfig,
) -> Self {
if let Some(executable_agent) = invoker.agent() {
self.agent = executable_agent;
}
let handler: TriggerHandler = Arc::new(move |event, _agent| {
let invoker = Arc::clone(&invoker);
let config = config.clone();
Box::pin(async move {
let session_id = config.session_id(&event);
let content = Content::new("user").with_text((config.prompt)(&event));
invoker.invoke(&config.user_id, &session_id, content).await
})
});
self.with_trigger_handler(handler)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn event() -> TriggerEvent {
TriggerEvent {
source: "cron:0 * * * * *".to_string(),
payload: serde_json::json!({ "tick": "2026-08-22T13:45:00Z" }),
principal: None,
}
}
#[test]
fn per_trigger_is_the_default_policy() {
let config = RunnerTriggerConfig::new("system");
assert!(matches!(config.session_policy, TriggerSessionPolicy::PerTrigger));
}
#[test]
fn per_trigger_gives_every_event_its_own_session() {
let config = RunnerTriggerConfig::new("system");
let first = config.session_id(&event());
let second = config.session_id(&event());
assert_ne!(first, second, "a shared session would grow history without bound across ticks");
assert!(first.starts_with("cron:0 * * * * *-"), "got {first}");
}
#[test]
fn shared_reuses_one_session_across_events() {
let config = RunnerTriggerConfig::new("system")
.with_session_policy(TriggerSessionPolicy::Shared("sweep".to_string()));
assert_eq!(config.session_id(&event()), "sweep");
assert_eq!(config.session_id(&event()), "sweep");
}
#[test]
fn the_default_prompt_names_the_source_and_payload() {
let rendered = default_prompt(&event());
assert!(rendered.contains("cron:0 * * * * *"), "got {rendered}");
assert!(rendered.contains("2026-08-22T13:45:00Z"), "got {rendered}");
}
#[test]
fn a_custom_prompt_replaces_the_default() {
let config =
RunnerTriggerConfig::new("system").with_prompt(|event| format!("go: {}", event.source));
assert_eq!((config.prompt)(&event()), "go: cron:0 * * * * *");
}
#[test]
fn user_id_is_reported() {
assert_eq!(RunnerTriggerConfig::new("service-account").user_id(), "service-account");
}
}