use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::events::Dispatch;
pub trait TypedEvent {
type Payload: Serialize + DeserializeOwned + Clone + Send + Sync + 'static;
const NAME: &'static str;
const MODE: Dispatch;
const AROUND: bool;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AgentAdmitPayload {
pub tenant_id: String,
#[serde(default)]
pub monthly: u64,
#[serde(default)]
pub daily: u64,
#[serde(default)]
pub requests_per_month: Option<u64>,
#[serde(default)]
pub requests_per_day: Option<u64>,
pub tier: String,
}
#[derive(Debug, Clone, Copy)]
pub struct AgentAdmitEvent;
impl TypedEvent for AgentAdmitEvent {
type Payload = AgentAdmitPayload;
const NAME: &'static str = crate::events_catalog::ev::AGENT_ADMIT;
const MODE: Dispatch = Dispatch::Bail;
const AROUND: bool = false;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AgentStartedPayload {
pub agent_name: String,
#[serde(default)]
pub run_id: String,
#[serde(default)]
pub tenant: String,
#[serde(default)]
pub event: String,
}
#[derive(Debug, Clone, Copy)]
pub struct AgentStartedEvent;
impl TypedEvent for AgentStartedEvent {
type Payload = AgentStartedPayload;
const NAME: &'static str = crate::events_catalog::ev::AGENT_STARTED;
const MODE: Dispatch = Dispatch::Parallel;
const AROUND: bool = false;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AgentUsagePayload {
#[serde(default)]
pub tenant: Option<String>,
#[serde(default)]
pub prompt: i64,
#[serde(default)]
pub completion: i64,
#[serde(default)]
pub total: i64,
}
#[derive(Debug, Clone, Copy)]
pub struct AgentUsageEvent;
impl TypedEvent for AgentUsageEvent {
type Payload = AgentUsagePayload;
const NAME: &'static str = crate::events_catalog::ev::AGENT_USAGE;
const MODE: Dispatch = Dispatch::Emit;
const AROUND: bool = false;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AgentCompletedPayload {
pub agent_name: String,
#[serde(default)]
pub run_id: String,
#[serde(default = "default_status")]
pub status: String,
#[serde(default)]
pub event: String,
}
fn default_status() -> String {
"unknown".to_string()
}
#[derive(Debug, Clone, Copy)]
pub struct AgentCompletedEvent;
impl TypedEvent for AgentCompletedEvent {
type Payload = AgentCompletedPayload;
const NAME: &'static str = crate::events_catalog::ev::AGENT_COMPLETED;
const MODE: Dispatch = Dispatch::Emit;
const AROUND: bool = false;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AgentFailedPayload {
pub agent_name: String,
#[serde(default)]
pub run_id: String,
#[serde(default)]
pub tenant: String,
#[serde(default)]
pub event: String,
}
#[derive(Debug, Clone, Copy)]
pub struct AgentFailedEvent;
impl TypedEvent for AgentFailedEvent {
type Payload = AgentFailedPayload;
const NAME: &'static str = crate::events_catalog::ev::AGENT_FAILED;
const MODE: Dispatch = Dispatch::Emit;
const AROUND: bool = false;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AgentRunRequest {
#[serde(default)]
pub agent_name: String,
#[serde(default)]
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AgentRunResult {
#[serde(default)]
pub content: Value,
#[serde(default)]
pub source: Value,
#[serde(default)]
pub agent_name: String,
#[serde(default)]
pub run_id: String,
#[serde(default)]
pub usage: Option<Value>,
#[serde(default)]
pub metadata: Option<Value>,
}
#[derive(Debug, Clone, Copy)]
pub struct AgentRunEvent;
impl TypedEvent for AgentRunEvent {
type Payload = AgentRunRequest;
const NAME: &'static str = crate::events_catalog::ev::AGENT_RUN;
const MODE: Dispatch = Dispatch::Waterfall;
const AROUND: bool = true;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LlmCompleteRequest {
#[serde(default)]
pub prompt: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LlmCompleteResult {
#[serde(default)]
pub prompt: String,
#[serde(default)]
pub content: Value,
}
#[derive(Debug, Clone, Copy)]
pub struct LlmCompleteEvent;
impl TypedEvent for LlmCompleteEvent {
type Payload = LlmCompleteRequest;
const NAME: &'static str = crate::events_catalog::ev::LLM_COMPLETE;
const MODE: Dispatch = Dispatch::Waterfall;
const AROUND: bool = true;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LlmGetClientPayload {
pub capability: String,
#[serde(default)]
pub deny: Option<bool>,
#[serde(default)]
pub model: Option<String>,
}
#[derive(Debug, Clone, Copy)]
pub struct LlmGetClientEvent;
impl TypedEvent for LlmGetClientEvent {
type Payload = LlmGetClientPayload;
const NAME: &'static str = crate::events_catalog::ev::LLM_GET_CLIENT;
const MODE: Dispatch = Dispatch::Waterfall;
const AROUND: bool = true;
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct LlmMessage {
pub role: String,
#[serde(default)]
pub content: Value,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parts: Vec<Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LlmGeneratePayload {
#[serde(default)]
pub messages: Vec<LlmMessage>,
}
#[derive(Debug, Clone, Copy)]
pub struct LlmGenerateEvent;
impl TypedEvent for LlmGenerateEvent {
type Payload = LlmGeneratePayload;
const NAME: &'static str = crate::events_catalog::ev::LLM_GENERATE;
const MODE: Dispatch = Dispatch::Waterfall;
const AROUND: bool = true;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LlmGenerateToolsPayload {
#[serde(default)]
pub messages: Vec<Value>,
#[serde(default)]
pub tools: Vec<Value>,
}
#[derive(Debug, Clone, Copy)]
pub struct LlmGenerateToolsEvent;
impl TypedEvent for LlmGenerateToolsEvent {
type Payload = LlmGenerateToolsPayload;
const NAME: &'static str = crate::events_catalog::ev::LLM_GENERATE_TOOLS;
const MODE: Dispatch = Dispatch::Waterfall;
const AROUND: bool = true;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LlmEmbedRequest {
#[serde(default)]
pub inputs: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LlmEmbedResponse {
#[serde(default)]
pub inputs: Vec<String>,
#[serde(default)]
pub embeddings: Vec<Vec<f32>>,
}
#[derive(Debug, Clone, Copy)]
pub struct LlmEmbedEvent;
impl TypedEvent for LlmEmbedEvent {
type Payload = LlmEmbedRequest;
const NAME: &'static str = crate::events_catalog::ev::LLM_EMBED;
const MODE: Dispatch = Dispatch::Waterfall;
const AROUND: bool = true;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolsExecutePayload {
pub name: String,
#[serde(default)]
pub args: Value,
}
#[derive(Debug, Clone, Copy)]
pub struct ToolsExecuteEvent;
impl TypedEvent for ToolsExecuteEvent {
type Payload = ToolsExecutePayload;
const NAME: &'static str = crate::events_catalog::ev::TOOLS_EXECUTE;
const MODE: Dispatch = Dispatch::Waterfall;
const AROUND: bool = true;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolsListRequest {
#[serde(default)]
pub tenant: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolsListResult {
#[serde(default)]
pub tenant: Option<String>,
#[serde(default)]
pub tools: Vec<Value>,
}
#[derive(Debug, Clone, Copy)]
pub struct ToolsListEvent;
impl TypedEvent for ToolsListEvent {
type Payload = ToolsListRequest;
const NAME: &'static str = crate::events_catalog::ev::TOOLS_LIST;
const MODE: Dispatch = Dispatch::Waterfall;
const AROUND: bool = true;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ToolsResolveRequest {
pub name: String,
#[serde(default)]
pub tenant: Option<String>,
}
#[derive(Debug, Clone, Copy)]
pub struct ToolsResolveEvent;
impl TypedEvent for ToolsResolveEvent {
type Payload = ToolsResolveRequest;
const NAME: &'static str = crate::events_catalog::ev::TOOLS_RESOLVE;
const MODE: Dispatch = Dispatch::Waterfall;
const AROUND: bool = true;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SchedulerBeforeRunPayload {
#[serde(default)]
pub agent_name: String,
#[serde(default)]
pub run_id: String,
#[serde(default)]
pub tenant: Option<String>,
}
#[derive(Debug, Clone, Copy)]
pub struct SchedulerBeforeRunEvent;
impl TypedEvent for SchedulerBeforeRunEvent {
type Payload = SchedulerBeforeRunPayload;
const NAME: &'static str = crate::events_catalog::ev::SCHEDULER_BEFORE_RUN;
const MODE: Dispatch = Dispatch::Waterfall;
const AROUND: bool = true;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SchedulerAdmitPayload {
#[serde(default)]
pub agent_name: String,
#[serde(default)]
pub deny: Option<bool>,
}
#[derive(Debug, Clone, Copy)]
pub struct SchedulerAdmitEvent;
impl TypedEvent for SchedulerAdmitEvent {
type Payload = SchedulerAdmitPayload;
const NAME: &'static str = crate::events_catalog::ev::SCHEDULER_ADMIT;
const MODE: Dispatch = Dispatch::Bail;
const AROUND: bool = false;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ServiceChangedPayload {
pub type_id: String,
#[serde(default)]
pub event: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SchedulerTickPayload {
#[serde(default)]
pub due_count: u64,
#[serde(default)]
pub catchup_count: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct SchedulerTickEvent;
impl TypedEvent for SchedulerTickEvent {
type Payload = SchedulerTickPayload;
const NAME: &'static str = crate::events_catalog::ev::SCHEDULER_TICK;
const MODE: Dispatch = Dispatch::Emit;
const AROUND: bool = false;
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ScheduleDispatchedPayload {
pub schedule_id: String,
#[serde(default)]
pub agent_name: String,
#[serde(default)]
pub tenant_id: String,
#[serde(default)]
pub is_catchup: bool,
#[serde(default)]
pub ok: bool,
#[serde(default)]
pub denied: bool,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Debug, Clone, Copy)]
pub struct ScheduleDispatchedEvent;
impl TypedEvent for ScheduleDispatchedEvent {
type Payload = ScheduleDispatchedPayload;
const NAME: &'static str = crate::events_catalog::ev::SCHEDULER_SCHEDULE_DISPATCHED;
const MODE: Dispatch = Dispatch::Emit;
const AROUND: bool = false;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineStepStartedPayload {
pub pipeline_id: String,
pub target_agent: String,
#[serde(default)]
pub tenant_id: String,
#[serde(default)]
pub run_id: String,
}
#[derive(Debug, Clone, Copy)]
pub struct PipelineStepStartedEvent;
impl TypedEvent for PipelineStepStartedEvent {
type Payload = PipelineStepStartedPayload;
const NAME: &'static str = crate::events_catalog::ev::PIPELINE_STEP_STARTED;
const MODE: Dispatch = Dispatch::Emit;
const AROUND: bool = false;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineStepFinishedPayload {
pub pipeline_id: String,
pub target_agent: String,
#[serde(default)]
pub tenant_id: String,
#[serde(default)]
pub status: String,
#[serde(default)]
pub duration_ms: u64,
#[serde(default)]
pub error: Option<String>,
}
#[derive(Debug, Clone, Copy)]
pub struct PipelineStepFinishedEvent;
impl TypedEvent for PipelineStepFinishedEvent {
type Payload = PipelineStepFinishedPayload;
const NAME: &'static str = crate::events_catalog::ev::PIPELINE_STEP_FINISHED;
const MODE: Dispatch = Dispatch::Emit;
const AROUND: bool = false;
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PipelineFanoutCompletedPayload {
#[serde(default)]
pub source_agent: String,
#[serde(default)]
pub tenant_id: String,
#[serde(default)]
pub triggered: Vec<String>,
}
#[derive(Debug, Clone, Copy)]
pub struct PipelineFanoutCompletedEvent;
impl TypedEvent for PipelineFanoutCompletedEvent {
type Payload = PipelineFanoutCompletedPayload;
const NAME: &'static str = crate::events_catalog::ev::PIPELINE_FANOUT_COMPLETED;
const MODE: Dispatch = Dispatch::Emit;
const AROUND: bool = false;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TriggerFiredPayload {
pub trigger_id: String,
#[serde(default)]
pub event_type: String,
#[serde(default)]
pub target_agent: String,
#[serde(default)]
pub tenant_id: String,
}
#[derive(Debug, Clone, Copy)]
pub struct TriggerFiredEvent;
impl TypedEvent for TriggerFiredEvent {
type Payload = TriggerFiredPayload;
const NAME: &'static str = crate::events_catalog::ev::TRIGGER_FIRED;
const MODE: Dispatch = Dispatch::Emit;
const AROUND: bool = false;
}
#[derive(Debug, Clone, Copy)]
pub struct ServiceChangedEvent;
impl TypedEvent for ServiceChangedEvent {
type Payload = ServiceChangedPayload;
const NAME: &'static str = crate::events_catalog::ev::SERVICE_CHANGED;
const MODE: Dispatch = Dispatch::Emit;
const AROUND: bool = false;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::events_catalog::{contract_for, CONTRACTS};
#[test]
fn typed_events_match_catalog_contracts() {
let bindings: &[(&'static str, Dispatch, bool)] = &[
(
AgentAdmitEvent::NAME,
AgentAdmitEvent::MODE,
AgentAdmitEvent::AROUND,
),
(
AgentCompletedEvent::NAME,
AgentCompletedEvent::MODE,
AgentCompletedEvent::AROUND,
),
(
AgentFailedEvent::NAME,
AgentFailedEvent::MODE,
AgentFailedEvent::AROUND,
),
(
AgentRunEvent::NAME,
AgentRunEvent::MODE,
AgentRunEvent::AROUND,
),
(
AgentStartedEvent::NAME,
AgentStartedEvent::MODE,
AgentStartedEvent::AROUND,
),
(
AgentUsageEvent::NAME,
AgentUsageEvent::MODE,
AgentUsageEvent::AROUND,
),
(
LlmCompleteEvent::NAME,
LlmCompleteEvent::MODE,
LlmCompleteEvent::AROUND,
),
(
LlmGetClientEvent::NAME,
LlmGetClientEvent::MODE,
LlmGetClientEvent::AROUND,
),
(
LlmGenerateEvent::NAME,
LlmGenerateEvent::MODE,
LlmGenerateEvent::AROUND,
),
(
LlmGenerateToolsEvent::NAME,
LlmGenerateToolsEvent::MODE,
LlmGenerateToolsEvent::AROUND,
),
(
LlmEmbedEvent::NAME,
LlmEmbedEvent::MODE,
LlmEmbedEvent::AROUND,
),
(
SchedulerAdmitEvent::NAME,
SchedulerAdmitEvent::MODE,
SchedulerAdmitEvent::AROUND,
),
(
SchedulerBeforeRunEvent::NAME,
SchedulerBeforeRunEvent::MODE,
SchedulerBeforeRunEvent::AROUND,
),
(
ServiceChangedEvent::NAME,
ServiceChangedEvent::MODE,
ServiceChangedEvent::AROUND,
),
(
ToolsExecuteEvent::NAME,
ToolsExecuteEvent::MODE,
ToolsExecuteEvent::AROUND,
),
(
ToolsListEvent::NAME,
ToolsListEvent::MODE,
ToolsListEvent::AROUND,
),
(
ToolsResolveEvent::NAME,
ToolsResolveEvent::MODE,
ToolsResolveEvent::AROUND,
),
(
SchedulerTickEvent::NAME,
SchedulerTickEvent::MODE,
SchedulerTickEvent::AROUND,
),
(
ScheduleDispatchedEvent::NAME,
ScheduleDispatchedEvent::MODE,
ScheduleDispatchedEvent::AROUND,
),
(
PipelineStepStartedEvent::NAME,
PipelineStepStartedEvent::MODE,
PipelineStepStartedEvent::AROUND,
),
(
PipelineStepFinishedEvent::NAME,
PipelineStepFinishedEvent::MODE,
PipelineStepFinishedEvent::AROUND,
),
(
PipelineFanoutCompletedEvent::NAME,
PipelineFanoutCompletedEvent::MODE,
PipelineFanoutCompletedEvent::AROUND,
),
(
TriggerFiredEvent::NAME,
TriggerFiredEvent::MODE,
TriggerFiredEvent::AROUND,
),
];
for (name, mode, around) in bindings {
let contract = contract_for(name)
.unwrap_or_else(|| panic!("typed binding {name} missing from catalog"));
assert_eq!(&contract.mode, mode, "mode drift for {name}");
assert_eq!(&contract.around, around, "around drift for {name}");
}
assert_eq!(
bindings.len(),
CONTRACTS.len(),
"every catalog event must have exactly one typed binding"
);
}
#[test]
fn payload_round_trip_and_defaults() {
let started = AgentStartedPayload {
agent_name: "a".into(),
run_id: "r".into(),
tenant: "t".into(),
event: crate::events_catalog::ev::AGENT_STARTED.into(),
};
let v = serde_json::to_value(&started).unwrap();
assert_eq!(v.get("agent_name").and_then(Value::as_str), Some("a"));
let back: AgentStartedPayload = serde_json::from_value(v).unwrap();
assert_eq!(back, started);
let minimal: AgentCompletedPayload =
serde_json::from_value(serde_json::json!({ "agent_name": "x" })).unwrap();
assert_eq!(minimal.status, "unknown");
assert_eq!(minimal.run_id, "");
let usage = AgentUsagePayload {
tenant: None,
prompt: 3,
completion: 4,
total: 7,
};
let v = serde_json::to_value(&usage).unwrap();
assert!(v.get("tenant").map(Value::is_null).unwrap_or(false));
let back: AgentUsagePayload = serde_json::from_value(v).unwrap();
assert_eq!(back.prompt, 3);
assert!(back.tenant.is_none());
}
}