Skip to main content

cordis/
events_payload.rs

1//! Typed event payloads bound to the declared catalog via [`TypedEvent`].
2//!
3//! Upstream Cordis binds payload types to events through TypeScript
4//! declaration merging with an `@mode` contract. Rust has no declaration
5//! merging; the equivalent here is one zero-sized marker type per catalog
6//! event implementing [`TypedEvent`] with an associated payload struct plus
7//! `NAME`/`MODE`/`AROUND` constants. A unit test enforces that every binding
8//! matches [`crate::events_catalog::CONTRACTS`], keeping the catalog the
9//! single source of truth.
10//!
11//! Dispatch sites construct the payload struct and call
12//! [`crate::EventsService::dispatch_typed`]; listeners register through
13//! [`crate::EventsService::on_typed`] / [`crate::EventsService::on_typed_waterfall`],
14//! which deserialize into the payload type and skip malformed payloads
15//! (warn + passthrough) instead of failing the chain. The raw
16//! `serde_json::Value` API stays authoritative for kernel mechanics, dynamic
17//! cases, and mid-chain JSON rewriting.
18
19use serde::de::DeserializeOwned;
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22
23use crate::events::Dispatch;
24
25/// Compile-time binding between a catalog event and its payload type.
26///
27/// `NAME`/`MODE`/`AROUND` must mirror the event's
28/// [`crate::events_catalog::EventContract`]; the
29/// `typed_events_match_catalog_contracts` test fails on drift.
30pub trait TypedEvent {
31    /// Wire shape of the event payload.
32    type Payload: Serialize + DeserializeOwned + Clone + Send + Sync + 'static;
33    /// Canonical event name (an `events_catalog::ev::*` constant).
34    const NAME: &'static str;
35    /// Declared dispatch mode.
36    const MODE: Dispatch;
37    /// True for around-middleware waterfalls (`on_waterfall` registry).
38    const AROUND: bool;
39}
40
41// ---------------------------------------------------------------------------
42// agent.admit — Dispatch::Bail
43// ---------------------------------------------------------------------------
44
45/// Payload for [`AgentAdmitEvent`]. Built by the shared admission gate
46/// (`ares-agent::admit`) and the MCP server quota gate.
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48pub struct AgentAdmitPayload {
49    pub tenant_id: String,
50    #[serde(default)]
51    pub monthly: u64,
52    #[serde(default)]
53    pub daily: u64,
54    #[serde(default)]
55    pub requests_per_month: Option<u64>,
56    #[serde(default)]
57    pub requests_per_day: Option<u64>,
58    pub tier: String,
59}
60
61/// `agent.admit` — quota admission policy (`Dispatch::Bail`). A handler
62/// denying the request returns a payload with a `deny`/`error` marker.
63#[derive(Debug, Clone, Copy)]
64pub struct AgentAdmitEvent;
65impl TypedEvent for AgentAdmitEvent {
66    type Payload = AgentAdmitPayload;
67    const NAME: &'static str = crate::events_catalog::ev::AGENT_ADMIT;
68    const MODE: Dispatch = Dispatch::Bail;
69    const AROUND: bool = false;
70}
71
72// ---------------------------------------------------------------------------
73// agent.started — Dispatch::Parallel
74// ---------------------------------------------------------------------------
75
76/// Payload for [`AgentStartedEvent`].
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct AgentStartedPayload {
79    pub agent_name: String,
80    #[serde(default)]
81    pub run_id: String,
82    #[serde(default)]
83    pub tenant: String,
84    #[serde(default)]
85    pub event: String,
86}
87
88/// `agent.started` — joined fan-out at run start (`Dispatch::Parallel`).
89#[derive(Debug, Clone, Copy)]
90pub struct AgentStartedEvent;
91impl TypedEvent for AgentStartedEvent {
92    type Payload = AgentStartedPayload;
93    const NAME: &'static str = crate::events_catalog::ev::AGENT_STARTED;
94    const MODE: Dispatch = Dispatch::Parallel;
95    const AROUND: bool = false;
96}
97
98// ---------------------------------------------------------------------------
99// agent.usage — Dispatch::Emit
100// ---------------------------------------------------------------------------
101
102/// Payload for [`AgentUsageEvent`]. `tenant` is `None` when no tenant scope
103/// was resolved for the run (serialized as JSON `null`, matching the raw API).
104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
105pub struct AgentUsagePayload {
106    #[serde(default)]
107    pub tenant: Option<String>,
108    #[serde(default)]
109    pub prompt: i64,
110    #[serde(default)]
111    pub completion: i64,
112    #[serde(default)]
113    pub total: i64,
114}
115
116/// `agent.usage` — fire-and-forget token accounting (`Dispatch::Emit`).
117#[derive(Debug, Clone, Copy)]
118pub struct AgentUsageEvent;
119impl TypedEvent for AgentUsageEvent {
120    type Payload = AgentUsagePayload;
121    const NAME: &'static str = crate::events_catalog::ev::AGENT_USAGE;
122    const MODE: Dispatch = Dispatch::Emit;
123    const AROUND: bool = false;
124}
125
126// ---------------------------------------------------------------------------
127// agent.completed / agent.failed — Dispatch::Emit
128// ---------------------------------------------------------------------------
129
130/// Payload for [`AgentCompletedEvent`].
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132pub struct AgentCompletedPayload {
133    pub agent_name: String,
134    #[serde(default)]
135    pub run_id: String,
136    #[serde(default = "default_status")]
137    pub status: String,
138    #[serde(default)]
139    pub event: String,
140}
141
142fn default_status() -> String {
143    "unknown".to_string()
144}
145
146/// `agent.completed` — terminal status for every run (`Dispatch::Emit`).
147#[derive(Debug, Clone, Copy)]
148pub struct AgentCompletedEvent;
149impl TypedEvent for AgentCompletedEvent {
150    type Payload = AgentCompletedPayload;
151    const NAME: &'static str = crate::events_catalog::ev::AGENT_COMPLETED;
152    const MODE: Dispatch = Dispatch::Emit;
153    const AROUND: bool = false;
154}
155
156/// Payload for [`AgentFailedEvent`].
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158pub struct AgentFailedPayload {
159    pub agent_name: String,
160    #[serde(default)]
161    pub run_id: String,
162    #[serde(default)]
163    pub tenant: String,
164    #[serde(default)]
165    pub event: String,
166}
167
168/// `agent.failed` — run failure signal feeding scheduler failure control
169/// (`Dispatch::Emit`).
170#[derive(Debug, Clone, Copy)]
171pub struct AgentFailedEvent;
172impl TypedEvent for AgentFailedEvent {
173    type Payload = AgentFailedPayload;
174    const NAME: &'static str = crate::events_catalog::ev::AGENT_FAILED;
175    const MODE: Dispatch = Dispatch::Emit;
176    const AROUND: bool = false;
177}
178
179// ---------------------------------------------------------------------------
180// agent.run — Dispatch::Waterfall (around)
181// ---------------------------------------------------------------------------
182
183/// Initial payload for [`AgentRunEvent`]: the requested agent and message.
184/// Waterfall handlers may rewrite either before the core runs.
185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
186pub struct AgentRunRequest {
187    #[serde(default)]
188    pub agent_name: String,
189    #[serde(default)]
190    pub message: String,
191}
192
193/// Result payload produced by the [`AgentRunEvent`] waterfall core. Handlers
194/// may set `deny` (with optional `reason`) to short-circuit the run, or read
195/// `content`/`usage` from downstream results.
196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
197pub struct AgentRunResult {
198    #[serde(default)]
199    pub content: Value,
200    #[serde(default)]
201    pub source: Value,
202    #[serde(default)]
203    pub agent_name: String,
204    #[serde(default)]
205    pub run_id: String,
206    #[serde(default)]
207    pub usage: Option<Value>,
208    #[serde(default)]
209    pub metadata: Option<Value>,
210}
211
212/// `agent.run` — around-middleware waterfall wrapping whole agent runs
213/// (`Dispatch::Waterfall`).
214#[derive(Debug, Clone, Copy)]
215pub struct AgentRunEvent;
216impl TypedEvent for AgentRunEvent {
217    type Payload = AgentRunRequest;
218    const NAME: &'static str = crate::events_catalog::ev::AGENT_RUN;
219    const MODE: Dispatch = Dispatch::Waterfall;
220    const AROUND: bool = true;
221}
222
223// ---------------------------------------------------------------------------
224// llm.complete — Dispatch::Waterfall (around)
225// ---------------------------------------------------------------------------
226
227/// Initial payload for [`LlmCompleteEvent`].
228#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
229pub struct LlmCompleteRequest {
230    #[serde(default)]
231    pub prompt: String,
232}
233
234/// Result payload of the [`LlmCompleteEvent`] waterfall core.
235#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
236pub struct LlmCompleteResult {
237    #[serde(default)]
238    pub prompt: String,
239    #[serde(default)]
240    pub content: Value,
241}
242
243/// `llm.complete` — around-middleware waterfall wrapping single completions
244/// (`Dispatch::Waterfall`). Handlers may rewrite `prompt` or short-circuit by
245/// returning their own `content`.
246#[derive(Debug, Clone, Copy)]
247pub struct LlmCompleteEvent;
248impl TypedEvent for LlmCompleteEvent {
249    type Payload = LlmCompleteRequest;
250    const NAME: &'static str = crate::events_catalog::ev::LLM_COMPLETE;
251    const MODE: Dispatch = Dispatch::Waterfall;
252    const AROUND: bool = true;
253}
254
255// ---------------------------------------------------------------------------
256// llm.get_client — Dispatch::Waterfall (around)
257// ---------------------------------------------------------------------------
258
259/// Payload for [`LlmGetClientEvent`]. The core is identity; handlers may set
260/// `deny` to refuse client resolution or pin `model`.
261#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
262pub struct LlmGetClientPayload {
263    pub capability: String,
264    #[serde(default)]
265    pub deny: Option<bool>,
266    #[serde(default)]
267    pub model: Option<String>,
268}
269
270/// `llm.get_client` — around-middleware waterfall over client resolution
271/// (`Dispatch::Waterfall`).
272#[derive(Debug, Clone, Copy)]
273pub struct LlmGetClientEvent;
274impl TypedEvent for LlmGetClientEvent {
275    type Payload = LlmGetClientPayload;
276    const NAME: &'static str = crate::events_catalog::ev::LLM_GET_CLIENT;
277    const MODE: Dispatch = Dispatch::Waterfall;
278    const AROUND: bool = true;
279}
280
281// ---------------------------------------------------------------------------
282// llm.generate / llm.generate_tools — Dispatch::Waterfall (around)
283// ---------------------------------------------------------------------------
284
285/// One conversation message in [`LlmGeneratePayload`]. Free-form shape: the
286/// waterfall core re-parses messages into provider-native types, so `content`
287/// stays a raw JSON value.
288#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
289pub struct LlmMessage {
290    pub role: String,
291    #[serde(default)]
292    pub content: Value,
293    /// Multimodal parts as raw JSON so this leaf crate stays independent of
294    /// `ares-types::ContentPart`. Empty means use `content` only.
295    #[serde(default, skip_serializing_if = "Vec::is_empty")]
296    pub parts: Vec<Value>,
297}
298
299/// Initial payload for [`LlmGenerateEvent`].
300#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
301pub struct LlmGeneratePayload {
302    #[serde(default)]
303    pub messages: Vec<LlmMessage>,
304}
305
306/// `llm.generate` — around-middleware waterfall wrapping history generation
307/// (`Dispatch::Waterfall`). Handlers may rewrite `messages` mid-chain.
308#[derive(Debug, Clone, Copy)]
309pub struct LlmGenerateEvent;
310impl TypedEvent for LlmGenerateEvent {
311    type Payload = LlmGeneratePayload;
312    const NAME: &'static str = crate::events_catalog::ev::LLM_GENERATE;
313    const MODE: Dispatch = Dispatch::Waterfall;
314    const AROUND: bool = true;
315}
316
317/// Initial payload for [`LlmGenerateToolsEvent`]. Messages are serialized
318/// conversation messages; tools are serialized tool definitions (kept as raw
319/// values because this leaf crate cannot depend on the crates defining them).
320#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
321pub struct LlmGenerateToolsPayload {
322    #[serde(default)]
323    pub messages: Vec<Value>,
324    #[serde(default)]
325    pub tools: Vec<Value>,
326}
327
328/// `llm.generate_tools` — around-middleware waterfall wrapping tool-calling
329/// generation (`Dispatch::Waterfall`).
330#[derive(Debug, Clone, Copy)]
331pub struct LlmGenerateToolsEvent;
332impl TypedEvent for LlmGenerateToolsEvent {
333    type Payload = LlmGenerateToolsPayload;
334    const NAME: &'static str = crate::events_catalog::ev::LLM_GENERATE_TOOLS;
335    const MODE: Dispatch = Dispatch::Waterfall;
336    const AROUND: bool = true;
337}
338
339// ---------------------------------------------------------------------------
340// llm.embed — Dispatch::Waterfall (around)
341// ---------------------------------------------------------------------------
342
343/// Initial payload for [`LlmEmbedEvent`].
344#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
345pub struct LlmEmbedRequest {
346    #[serde(default)]
347    pub inputs: Vec<String>,
348}
349
350/// Result payload of the [`LlmEmbedEvent`] waterfall core.
351#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
352pub struct LlmEmbedResponse {
353    #[serde(default)]
354    pub inputs: Vec<String>,
355    #[serde(default)]
356    pub embeddings: Vec<Vec<f32>>,
357}
358
359/// `llm.embed` — around-middleware waterfall wrapping embedding batches
360/// (`Dispatch::Waterfall`). Handlers may rewrite `inputs` or short-circuit by
361/// returning their own `embeddings`. Core calls `LLMClient::embed`.
362#[derive(Debug, Clone, Copy)]
363pub struct LlmEmbedEvent;
364impl TypedEvent for LlmEmbedEvent {
365    type Payload = LlmEmbedRequest;
366    const NAME: &'static str = crate::events_catalog::ev::LLM_EMBED;
367    const MODE: Dispatch = Dispatch::Waterfall;
368    const AROUND: bool = true;
369}
370
371// ---------------------------------------------------------------------------
372// tools.execute / tools.list / tools.resolve — Dispatch::Waterfall (around)
373// ---------------------------------------------------------------------------
374
375/// Initial payload for [`ToolsExecuteEvent`]. The core injects `result`.
376#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
377pub struct ToolsExecutePayload {
378    pub name: String,
379    #[serde(default)]
380    pub args: Value,
381}
382
383/// `tools.execute` — around-middleware waterfall wrapping tool execution
384/// (`Dispatch::Waterfall`).
385#[derive(Debug, Clone, Copy)]
386pub struct ToolsExecuteEvent;
387impl TypedEvent for ToolsExecuteEvent {
388    type Payload = ToolsExecutePayload;
389    const NAME: &'static str = crate::events_catalog::ev::TOOLS_EXECUTE;
390    const MODE: Dispatch = Dispatch::Waterfall;
391    const AROUND: bool = true;
392}
393
394/// Initial payload for [`ToolsListEvent`].
395#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
396pub struct ToolsListRequest {
397    #[serde(default)]
398    pub tenant: Option<String>,
399}
400
401/// Result payload of the [`ToolsListEvent`] waterfall core: serialized tool
402/// definitions visible to the tenant.
403#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
404pub struct ToolsListResult {
405    #[serde(default)]
406    pub tenant: Option<String>,
407    #[serde(default)]
408    pub tools: Vec<Value>,
409}
410
411/// `tools.list` — around-middleware waterfall wrapping tool listing
412/// (`Dispatch::Waterfall`).
413#[derive(Debug, Clone, Copy)]
414pub struct ToolsListEvent;
415impl TypedEvent for ToolsListEvent {
416    type Payload = ToolsListRequest;
417    const NAME: &'static str = crate::events_catalog::ev::TOOLS_LIST;
418    const MODE: Dispatch = Dispatch::Waterfall;
419    const AROUND: bool = true;
420}
421
422/// Initial payload for [`ToolsResolveEvent`]. The core adds `found`; handlers
423/// may set `deny` to block resolution.
424#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
425pub struct ToolsResolveRequest {
426    pub name: String,
427    #[serde(default)]
428    pub tenant: Option<String>,
429}
430
431/// `tools.resolve` — around-middleware waterfall wrapping tool resolution
432/// (`Dispatch::Waterfall`).
433#[derive(Debug, Clone, Copy)]
434pub struct ToolsResolveEvent;
435impl TypedEvent for ToolsResolveEvent {
436    type Payload = ToolsResolveRequest;
437    const NAME: &'static str = crate::events_catalog::ev::TOOLS_RESOLVE;
438    const MODE: Dispatch = Dispatch::Waterfall;
439    const AROUND: bool = true;
440}
441
442// ---------------------------------------------------------------------------
443// scheduler.before_run / scheduler.admit
444// ---------------------------------------------------------------------------
445
446/// Payload for [`SchedulerBeforeRunEvent`]: a scheduled run about to execute.
447/// Waterfall handlers may enrich fields before the executor reads them back.
448#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
449pub struct SchedulerBeforeRunPayload {
450    #[serde(default)]
451    pub agent_name: String,
452    #[serde(default)]
453    pub run_id: String,
454    #[serde(default)]
455    pub tenant: Option<String>,
456}
457
458/// `scheduler.before_run` — around-middleware waterfall ahead of scheduled
459/// execution (`Dispatch::Waterfall`).
460#[derive(Debug, Clone, Copy)]
461pub struct SchedulerBeforeRunEvent;
462impl TypedEvent for SchedulerBeforeRunEvent {
463    type Payload = SchedulerBeforeRunPayload;
464    const NAME: &'static str = crate::events_catalog::ev::SCHEDULER_BEFORE_RUN;
465    const MODE: Dispatch = Dispatch::Waterfall;
466    const AROUND: bool = true;
467}
468
469/// Payload for [`SchedulerAdmitEvent`]. A handler denying the run returns a
470/// payload with `deny: true`.
471#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
472pub struct SchedulerAdmitPayload {
473    #[serde(default)]
474    pub agent_name: String,
475    #[serde(default)]
476    pub deny: Option<bool>,
477}
478
479/// `scheduler.admit` — admission policy for scheduled runs (`Dispatch::Bail`).
480#[derive(Debug, Clone, Copy)]
481pub struct SchedulerAdmitEvent;
482impl TypedEvent for SchedulerAdmitEvent {
483    type Payload = SchedulerAdmitPayload;
484    const NAME: &'static str = crate::events_catalog::ev::SCHEDULER_ADMIT;
485    const MODE: Dispatch = Dispatch::Bail;
486    const AROUND: bool = false;
487}
488
489// ---------------------------------------------------------------------------
490// service.changed — Dispatch::Emit
491// ---------------------------------------------------------------------------
492
493/// Payload for [`ServiceChangedEvent`]. `type_id` is the `Debug` rendering of
494/// the changed service's `TypeId` (matches the raw API's `format!("{tid:?}")`).
495#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
496pub struct ServiceChangedPayload {
497    pub type_id: String,
498    #[serde(default)]
499    pub event: String,
500}
501
502// ---------------------------------------------------------------------------
503// Engine boundary events — Dispatch::Emit (fire-and-forget observability)
504// ---------------------------------------------------------------------------
505
506/// Payload for [`SchedulerTickEvent`]: counts from one due-schedule pass.
507#[derive(Debug, Clone, Default, Serialize, Deserialize)]
508pub struct SchedulerTickPayload {
509    #[serde(default)]
510    pub due_count: u64,
511    #[serde(default)]
512    pub catchup_count: u64,
513}
514
515/// `scheduler.tick` — emitted once per completed scheduler execution pass
516/// (`Dispatch::Emit`).
517#[derive(Debug, Clone, Copy)]
518pub struct SchedulerTickEvent;
519impl TypedEvent for SchedulerTickEvent {
520    type Payload = SchedulerTickPayload;
521    const NAME: &'static str = crate::events_catalog::ev::SCHEDULER_TICK;
522    const MODE: Dispatch = Dispatch::Emit;
523    const AROUND: bool = false;
524}
525
526/// Payload for [`ScheduleDispatchedEvent`]: outcome of one scheduled run.
527/// `denied: true` means admission policy skipped the run; a denial is not a
528/// failure and still advances the schedule's next_run.
529#[derive(Debug, Clone, Default, Serialize, Deserialize)]
530pub struct ScheduleDispatchedPayload {
531    pub schedule_id: String,
532    #[serde(default)]
533    pub agent_name: String,
534    #[serde(default)]
535    pub tenant_id: String,
536    #[serde(default)]
537    pub is_catchup: bool,
538    #[serde(default)]
539    pub ok: bool,
540    #[serde(default)]
541    pub denied: bool,
542    #[serde(default)]
543    pub error: Option<String>,
544}
545
546/// `scheduler.schedule.dispatched` — emitted after each scheduled-run attempt
547/// (`Dispatch::Emit`).
548#[derive(Debug, Clone, Copy)]
549pub struct ScheduleDispatchedEvent;
550impl TypedEvent for ScheduleDispatchedEvent {
551    type Payload = ScheduleDispatchedPayload;
552    const NAME: &'static str = crate::events_catalog::ev::SCHEDULER_SCHEDULE_DISPATCHED;
553    const MODE: Dispatch = Dispatch::Emit;
554    const AROUND: bool = false;
555}
556
557/// Payload for [`PipelineStepStartedEvent`].
558#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct PipelineStepStartedPayload {
560    pub pipeline_id: String,
561    pub target_agent: String,
562    #[serde(default)]
563    pub tenant_id: String,
564    #[serde(default)]
565    pub run_id: String,
566}
567
568/// `pipeline.step.started` — emitted just before each pipeline target runs
569/// (`Dispatch::Emit`).
570#[derive(Debug, Clone, Copy)]
571pub struct PipelineStepStartedEvent;
572impl TypedEvent for PipelineStepStartedEvent {
573    type Payload = PipelineStepStartedPayload;
574    const NAME: &'static str = crate::events_catalog::ev::PIPELINE_STEP_STARTED;
575    const MODE: Dispatch = Dispatch::Emit;
576    const AROUND: bool = false;
577}
578
579/// Payload for [`PipelineStepFinishedEvent`].
580#[derive(Debug, Clone, Serialize, Deserialize)]
581pub struct PipelineStepFinishedPayload {
582    pub pipeline_id: String,
583    pub target_agent: String,
584    #[serde(default)]
585    pub tenant_id: String,
586    #[serde(default)]
587    pub status: String,
588    #[serde(default)]
589    pub duration_ms: u64,
590    #[serde(default)]
591    pub error: Option<String>,
592}
593
594/// `pipeline.step.finished` — emitted after each pipeline target's status is
595/// known (`Dispatch::Emit`).
596#[derive(Debug, Clone, Copy)]
597pub struct PipelineStepFinishedEvent;
598impl TypedEvent for PipelineStepFinishedEvent {
599    type Payload = PipelineStepFinishedPayload;
600    const NAME: &'static str = crate::events_catalog::ev::PIPELINE_STEP_FINISHED;
601    const MODE: Dispatch = Dispatch::Emit;
602    const AROUND: bool = false;
603}
604
605/// Payload for [`PipelineFanoutCompletedEvent`].
606#[derive(Debug, Clone, Default, Serialize, Deserialize)]
607pub struct PipelineFanoutCompletedPayload {
608    #[serde(default)]
609    pub source_agent: String,
610    #[serde(default)]
611    pub tenant_id: String,
612    #[serde(default)]
613    pub triggered: Vec<String>,
614}
615
616/// `pipeline.fanout.completed` — emitted at the end of a pipeline fan-out
617/// (`Dispatch::Emit`).
618#[derive(Debug, Clone, Copy)]
619pub struct PipelineFanoutCompletedEvent;
620impl TypedEvent for PipelineFanoutCompletedEvent {
621    type Payload = PipelineFanoutCompletedPayload;
622    const NAME: &'static str = crate::events_catalog::ev::PIPELINE_FANOUT_COMPLETED;
623    const MODE: Dispatch = Dispatch::Emit;
624    const AROUND: bool = false;
625}
626
627/// Payload for [`TriggerFiredEvent`].
628#[derive(Debug, Clone, Serialize, Deserialize)]
629pub struct TriggerFiredPayload {
630    pub trigger_id: String,
631    #[serde(default)]
632    pub event_type: String,
633    #[serde(default)]
634    pub target_agent: String,
635    #[serde(default)]
636    pub tenant_id: String,
637}
638
639/// `trigger.fired` — emitted when a trigger executes its target agent
640/// successfully (`Dispatch::Emit`).
641#[derive(Debug, Clone, Copy)]
642pub struct TriggerFiredEvent;
643impl TypedEvent for TriggerFiredEvent {
644    type Payload = TriggerFiredPayload;
645    const NAME: &'static str = crate::events_catalog::ev::TRIGGER_FIRED;
646    const MODE: Dispatch = Dispatch::Emit;
647    const AROUND: bool = false;
648}
649
650/// `service.changed` — hot-reload notification emitted by ReflectService
651/// (`Dispatch::Emit`).
652#[derive(Debug, Clone, Copy)]
653pub struct ServiceChangedEvent;
654impl TypedEvent for ServiceChangedEvent {
655    type Payload = ServiceChangedPayload;
656    const NAME: &'static str = crate::events_catalog::ev::SERVICE_CHANGED;
657    const MODE: Dispatch = Dispatch::Emit;
658    const AROUND: bool = false;
659}
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664    use crate::events_catalog::{contract_for, CONTRACTS};
665
666    /// Every typed binding must exist in the catalog and agree on mode and
667    /// middleware shape, and every catalog event must have a typed binding.
668    #[test]
669    fn typed_events_match_catalog_contracts() {
670        let bindings: &[(&'static str, Dispatch, bool)] = &[
671            (
672                AgentAdmitEvent::NAME,
673                AgentAdmitEvent::MODE,
674                AgentAdmitEvent::AROUND,
675            ),
676            (
677                AgentCompletedEvent::NAME,
678                AgentCompletedEvent::MODE,
679                AgentCompletedEvent::AROUND,
680            ),
681            (
682                AgentFailedEvent::NAME,
683                AgentFailedEvent::MODE,
684                AgentFailedEvent::AROUND,
685            ),
686            (
687                AgentRunEvent::NAME,
688                AgentRunEvent::MODE,
689                AgentRunEvent::AROUND,
690            ),
691            (
692                AgentStartedEvent::NAME,
693                AgentStartedEvent::MODE,
694                AgentStartedEvent::AROUND,
695            ),
696            (
697                AgentUsageEvent::NAME,
698                AgentUsageEvent::MODE,
699                AgentUsageEvent::AROUND,
700            ),
701            (
702                LlmCompleteEvent::NAME,
703                LlmCompleteEvent::MODE,
704                LlmCompleteEvent::AROUND,
705            ),
706            (
707                LlmGetClientEvent::NAME,
708                LlmGetClientEvent::MODE,
709                LlmGetClientEvent::AROUND,
710            ),
711            (
712                LlmGenerateEvent::NAME,
713                LlmGenerateEvent::MODE,
714                LlmGenerateEvent::AROUND,
715            ),
716            (
717                LlmGenerateToolsEvent::NAME,
718                LlmGenerateToolsEvent::MODE,
719                LlmGenerateToolsEvent::AROUND,
720            ),
721            (
722                LlmEmbedEvent::NAME,
723                LlmEmbedEvent::MODE,
724                LlmEmbedEvent::AROUND,
725            ),
726            (
727                SchedulerAdmitEvent::NAME,
728                SchedulerAdmitEvent::MODE,
729                SchedulerAdmitEvent::AROUND,
730            ),
731            (
732                SchedulerBeforeRunEvent::NAME,
733                SchedulerBeforeRunEvent::MODE,
734                SchedulerBeforeRunEvent::AROUND,
735            ),
736            (
737                ServiceChangedEvent::NAME,
738                ServiceChangedEvent::MODE,
739                ServiceChangedEvent::AROUND,
740            ),
741            (
742                ToolsExecuteEvent::NAME,
743                ToolsExecuteEvent::MODE,
744                ToolsExecuteEvent::AROUND,
745            ),
746            (
747                ToolsListEvent::NAME,
748                ToolsListEvent::MODE,
749                ToolsListEvent::AROUND,
750            ),
751            (
752                ToolsResolveEvent::NAME,
753                ToolsResolveEvent::MODE,
754                ToolsResolveEvent::AROUND,
755            ),
756            (
757                SchedulerTickEvent::NAME,
758                SchedulerTickEvent::MODE,
759                SchedulerTickEvent::AROUND,
760            ),
761            (
762                ScheduleDispatchedEvent::NAME,
763                ScheduleDispatchedEvent::MODE,
764                ScheduleDispatchedEvent::AROUND,
765            ),
766            (
767                PipelineStepStartedEvent::NAME,
768                PipelineStepStartedEvent::MODE,
769                PipelineStepStartedEvent::AROUND,
770            ),
771            (
772                PipelineStepFinishedEvent::NAME,
773                PipelineStepFinishedEvent::MODE,
774                PipelineStepFinishedEvent::AROUND,
775            ),
776            (
777                PipelineFanoutCompletedEvent::NAME,
778                PipelineFanoutCompletedEvent::MODE,
779                PipelineFanoutCompletedEvent::AROUND,
780            ),
781            (
782                TriggerFiredEvent::NAME,
783                TriggerFiredEvent::MODE,
784                TriggerFiredEvent::AROUND,
785            ),
786        ];
787        for (name, mode, around) in bindings {
788            let contract = contract_for(name)
789                .unwrap_or_else(|| panic!("typed binding {name} missing from catalog"));
790            assert_eq!(&contract.mode, mode, "mode drift for {name}");
791            assert_eq!(&contract.around, around, "around drift for {name}");
792        }
793        assert_eq!(
794            bindings.len(),
795            CONTRACTS.len(),
796            "every catalog event must have exactly one typed binding"
797        );
798    }
799
800    /// Serialization keeps field names/types stable across a round trip, and
801    /// missing optional keys fall back to documented defaults (lenient
802    /// listener behavior preserved).
803    #[test]
804    fn payload_round_trip_and_defaults() {
805        let started = AgentStartedPayload {
806            agent_name: "a".into(),
807            run_id: "r".into(),
808            tenant: "t".into(),
809            event: crate::events_catalog::ev::AGENT_STARTED.into(),
810        };
811        let v = serde_json::to_value(&started).unwrap();
812        assert_eq!(v.get("agent_name").and_then(Value::as_str), Some("a"));
813        let back: AgentStartedPayload = serde_json::from_value(v).unwrap();
814        assert_eq!(back, started);
815
816        let minimal: AgentCompletedPayload =
817            serde_json::from_value(serde_json::json!({ "agent_name": "x" })).unwrap();
818        assert_eq!(minimal.status, "unknown");
819        assert_eq!(minimal.run_id, "");
820
821        let usage = AgentUsagePayload {
822            tenant: None,
823            prompt: 3,
824            completion: 4,
825            total: 7,
826        };
827        let v = serde_json::to_value(&usage).unwrap();
828        assert!(v.get("tenant").map(Value::is_null).unwrap_or(false));
829        let back: AgentUsagePayload = serde_json::from_value(v).unwrap();
830        assert_eq!(back.prompt, 3);
831        assert!(back.tenant.is_none());
832    }
833}