1use crate::event::{Event, EventKind};
21use serde::de::DeserializeOwned;
22use serde::{Deserialize, Serialize};
23
24pub trait EventPayload: Serialize + DeserializeOwned + Send + Sync + 'static {
33 const KIND: EventKind;
35}
36
37impl Event<serde_json::Value> {
42 pub fn decode<P: EventPayload>(&self) -> Option<Event<P>> {
46 if self.header.event_kind != P::KIND {
47 return None;
48 }
49 let payload: P = serde_json::from_value(self.payload.clone()).ok()?;
50 Some(Event {
51 header: self.header.clone(),
52 payload,
53 hash_chain: self.hash_chain,
54 })
55 }
56
57 pub fn decode_payload<P: EventPayload>(&self) -> Option<P> {
61 if self.header.event_kind != P::KIND {
62 return None;
63 }
64 serde_json::from_value(self.payload.clone()).ok()
65 }
66}
67
68impl<P: Serialize> Event<P> {
69 pub fn erase(self) -> Event<serde_json::Value> {
74 Event {
75 header: self.header,
76 payload: serde_json::to_value(self.payload).unwrap_or(serde_json::Value::Null),
77 hash_chain: self.hash_chain,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct TrajectoryCreated {
89 pub trajectory_id: crate::TrajectoryId,
90 #[serde(default)]
91 pub name: String,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub description: Option<String>,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub parent_trajectory_id: Option<crate::TrajectoryId>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub root_trajectory_id: Option<crate::TrajectoryId>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub agent_id: Option<crate::AgentId>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub metadata: Option<serde_json::Value>,
102}
103
104impl EventPayload for TrajectoryCreated {
105 const KIND: EventKind = EventKind::TRAJECTORY_CREATED;
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct TrajectoryUpdated {
115 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub name: Option<String>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub description: Option<String>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub status: Option<crate::TrajectoryStatus>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
122 pub metadata: Option<serde_json::Value>,
123}
124
125impl EventPayload for TrajectoryUpdated {
126 const KIND: EventKind = EventKind::TRAJECTORY_UPDATED;
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct TrajectoryCompleted {
132 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub outcome: Option<crate::TrajectoryOutcome>,
134}
135
136impl EventPayload for TrajectoryCompleted {
137 const KIND: EventKind = EventKind::TRAJECTORY_COMPLETED;
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct TrajectoryFailed {
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub error: Option<String>,
145}
146
147impl EventPayload for TrajectoryFailed {
148 const KIND: EventKind = EventKind::TRAJECTORY_FAILED;
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct TrajectorySuspended {}
154
155impl EventPayload for TrajectorySuspended {
156 const KIND: EventKind = EventKind::TRAJECTORY_SUSPENDED;
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct TrajectoryResumed {}
162
163impl EventPayload for TrajectoryResumed {
164 const KIND: EventKind = EventKind::TRAJECTORY_RESUMED;
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct TrajectoryDeleted {}
170
171impl EventPayload for TrajectoryDeleted {
172 const KIND: EventKind = EventKind::TRAJECTORY_DELETED;
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct ScopeCreated {
182 pub scope_id: crate::ScopeId,
183 pub trajectory_id: crate::TrajectoryId,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub parent_scope_id: Option<crate::ScopeId>,
186 #[serde(default)]
187 pub name: String,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub purpose: Option<String>,
190 #[serde(default = "default_token_budget")]
191 pub token_budget: i32,
192 #[serde(default)]
193 pub tokens_used: i32,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub metadata: Option<serde_json::Value>,
196}
197
198fn default_token_budget() -> i32 {
199 8000
200}
201
202impl EventPayload for ScopeCreated {
203 const KIND: EventKind = EventKind::SCOPE_CREATED;
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct ScopeUpdated {
209 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub name: Option<String>,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub purpose: Option<String>,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub token_budget: Option<i32>,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub tokens_used: Option<i32>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub metadata: Option<serde_json::Value>,
219}
220
221impl EventPayload for ScopeUpdated {
222 const KIND: EventKind = EventKind::SCOPE_UPDATED;
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct ScopeClosed {}
228
229impl EventPayload for ScopeClosed {
230 const KIND: EventKind = EventKind::SCOPE_CLOSED;
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct ScopeCheckpointed {
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub checkpoint: Option<serde_json::Value>,
238}
239
240impl EventPayload for ScopeCheckpointed {
241 const KIND: EventKind = EventKind::SCOPE_CHECKPOINTED;
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ArtifactCreated {
251 pub artifact_id: crate::ArtifactId,
252 pub trajectory_id: crate::TrajectoryId,
253 pub scope_id: crate::ScopeId,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub artifact_type: Option<crate::ArtifactType>,
256 #[serde(default)]
257 pub name: String,
258 #[serde(default)]
259 pub content: String,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub content_hash: Option<crate::ContentHash>,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
263 pub provenance: Option<crate::Provenance>,
264 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub ttl: Option<crate::TTL>,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub metadata: Option<serde_json::Value>,
268}
269
270impl EventPayload for ArtifactCreated {
271 const KIND: EventKind = EventKind::ARTIFACT_CREATED;
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct ArtifactUpdated {
277 #[serde(default, skip_serializing_if = "Option::is_none")]
278 pub name: Option<String>,
279 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub content: Option<String>,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub content_hash: Option<crate::ContentHash>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub metadata: Option<serde_json::Value>,
285}
286
287impl EventPayload for ArtifactUpdated {
288 const KIND: EventKind = EventKind::ARTIFACT_UPDATED;
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct ArtifactSuperseded {
294 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub superseded_by: Option<crate::ArtifactId>,
296}
297
298impl EventPayload for ArtifactSuperseded {
299 const KIND: EventKind = EventKind::ARTIFACT_SUPERSEDED;
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct ArtifactDeleted {}
305
306impl EventPayload for ArtifactDeleted {
307 const KIND: EventKind = EventKind::ARTIFACT_DELETED;
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct NoteCreated {
317 pub note_id: crate::NoteId,
318 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub note_type: Option<crate::NoteType>,
320 #[serde(default)]
321 pub title: String,
322 #[serde(default)]
323 pub content: String,
324 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub content_hash: Option<crate::ContentHash>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
327 pub ttl: Option<crate::TTL>,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub abstraction_level: Option<crate::AbstractionLevel>,
330 #[serde(default)]
331 pub source_trajectory_ids: Vec<crate::TrajectoryId>,
332 #[serde(default)]
333 pub source_artifact_ids: Vec<crate::ArtifactId>,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub metadata: Option<serde_json::Value>,
336}
337
338impl EventPayload for NoteCreated {
339 const KIND: EventKind = EventKind::NOTE_CREATED;
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct NoteUpdated {
345 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub title: Option<String>,
347 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub content: Option<String>,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub content_hash: Option<crate::ContentHash>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub metadata: Option<serde_json::Value>,
353}
354
355impl EventPayload for NoteUpdated {
356 const KIND: EventKind = EventKind::NOTE_UPDATED;
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct NoteSuperseded {
362 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub superseded_by: Option<crate::NoteId>,
364}
365
366impl EventPayload for NoteSuperseded {
367 const KIND: EventKind = EventKind::NOTE_SUPERSEDED;
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct NoteDeleted {}
373
374impl EventPayload for NoteDeleted {
375 const KIND: EventKind = EventKind::NOTE_DELETED;
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct NoteAccessed {}
381
382impl EventPayload for NoteAccessed {
383 const KIND: EventKind = EventKind::NOTE_ACCESSED;
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct GoalCreated {
393 pub agent_id: uuid::Uuid,
394 pub goal_id: crate::GoalId,
395 #[serde(default)]
396 pub description: String,
397 #[serde(default, skip_serializing_if = "Option::is_none")]
398 pub goal_type: Option<String>,
399 #[serde(default)]
400 pub priority: i32,
401 #[serde(default, skip_serializing_if = "Option::is_none")]
402 pub parent_id: Option<crate::GoalId>,
403 #[serde(default, skip_serializing_if = "Option::is_none")]
404 pub deadline: Option<chrono::DateTime<chrono::Utc>>,
405}
406
407impl EventPayload for GoalCreated {
408 const KIND: EventKind = EventKind::GOAL_CREATED;
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct GoalActivated {
414 pub agent_id: uuid::Uuid,
415 pub goal_id: crate::GoalId,
416}
417
418impl EventPayload for GoalActivated {
419 const KIND: EventKind = EventKind::GOAL_ACTIVATED;
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct GoalAchieved {
425 pub agent_id: uuid::Uuid,
426 pub goal_id: crate::GoalId,
427}
428
429impl EventPayload for GoalAchieved {
430 const KIND: EventKind = EventKind::GOAL_ACHIEVED;
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct GoalFailed {
436 pub agent_id: uuid::Uuid,
437 pub goal_id: crate::GoalId,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
439 pub reason: Option<String>,
440}
441
442impl EventPayload for GoalFailed {
443 const KIND: EventKind = EventKind::GOAL_FAILED;
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize)]
448pub struct PlanCreated {
449 pub agent_id: uuid::Uuid,
450 pub plan_id: crate::PlanId,
451 pub goal_id: crate::GoalId,
452 #[serde(default)]
453 pub step_count: usize,
454}
455
456impl EventPayload for PlanCreated {
457 const KIND: EventKind = EventKind::PLAN_CREATED;
458}
459
460#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct PlanCompleted {
463 pub agent_id: uuid::Uuid,
464 pub plan_id: crate::PlanId,
465 pub goal_id: crate::GoalId,
466}
467
468impl EventPayload for PlanCompleted {
469 const KIND: EventKind = EventKind::PLAN_COMPLETED;
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize)]
474pub struct PlanFailed {
475 pub agent_id: uuid::Uuid,
476 pub plan_id: crate::PlanId,
477 pub goal_id: crate::GoalId,
478}
479
480impl EventPayload for PlanFailed {
481 const KIND: EventKind = EventKind::PLAN_FAILED;
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct StepCompleted {
487 pub agent_id: uuid::Uuid,
488 pub plan_id: crate::PlanId,
489 pub step_id: crate::StepId,
490}
491
492impl EventPayload for StepCompleted {
493 const KIND: EventKind = EventKind::STEP_COMPLETED;
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize)]
498pub struct StepFailed {
499 pub agent_id: uuid::Uuid,
500 pub plan_id: crate::PlanId,
501 pub step_id: crate::StepId,
502 #[serde(default, skip_serializing_if = "Option::is_none")]
503 pub error: Option<String>,
504}
505
506impl EventPayload for StepFailed {
507 const KIND: EventKind = EventKind::STEP_FAILED;
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct BeliefCreated {
513 pub agent_id: uuid::Uuid,
514 pub belief_id: crate::BeliefId,
515 #[serde(default)]
516 pub content: String,
517 #[serde(default)]
518 pub confidence: f32,
519 #[serde(default, skip_serializing_if = "Option::is_none")]
520 pub source: Option<String>,
521}
522
523impl EventPayload for BeliefCreated {
524 const KIND: EventKind = EventKind::BELIEF_CREATED;
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct BeliefSuperseded {
530 pub agent_id: uuid::Uuid,
531 pub old_belief_id: crate::BeliefId,
532 pub new_belief_id: crate::BeliefId,
533 #[serde(default)]
534 pub new_content: String,
535}
536
537impl EventPayload for BeliefSuperseded {
538 const KIND: EventKind = EventKind::BELIEF_SUPERSEDED;
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize)]
545pub struct EngineStatePersisted {
546 pub agent_id: uuid::Uuid,
547 pub snapshot: serde_json::Value,
549}
550
551impl EventPayload for EngineStatePersisted {
552 const KIND: EventKind = EventKind::ENGINE_STATE_PERSISTED;
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize)]
563pub struct MemoryCommitCreated {
564 pub commit_id: uuid::Uuid,
565 pub trajectory_id: crate::TrajectoryId,
566 pub scope_id: crate::ScopeId,
567 #[serde(default)]
568 pub query: String,
569 #[serde(default)]
570 pub mode: String,
571 #[serde(default)]
572 pub tokens_used: i32,
573 #[serde(default)]
574 pub cost: f64,
575}
576
577impl EventPayload for MemoryCommitCreated {
578 const KIND: EventKind = EventKind::MEMORY_COMMIT_CREATED;
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct ContextCommit {
591 pub tenant_id: crate::TenantId,
592 pub trajectory_id: crate::TrajectoryId,
593 pub scope_id: crate::ScopeId,
594 #[serde(default, skip_serializing_if = "Option::is_none")]
595 pub agent_id: Option<crate::AgentId>,
596 pub content: String,
598 #[serde(default)]
600 pub mode: String,
601 #[serde(default, skip_serializing_if = "Option::is_none")]
603 pub query: Option<String>,
604 #[serde(default, skip_serializing_if = "Option::is_none")]
605 pub reasoning_trace: Option<serde_json::Value>,
606 #[serde(default, skip_serializing_if = "Vec::is_empty")]
607 pub artifacts_referenced: Vec<crate::ArtifactId>,
608 #[serde(default, skip_serializing_if = "Vec::is_empty")]
609 pub notes_referenced: Vec<crate::NoteId>,
610 #[serde(default, skip_serializing_if = "Vec::is_empty")]
611 pub tools_invoked: Vec<String>,
612 #[serde(default)]
613 pub tokens_input: i64,
614 #[serde(default)]
615 pub tokens_output: i64,
616 #[serde(default, skip_serializing_if = "Option::is_none")]
617 pub metadata: Option<serde_json::Value>,
618}
619
620impl EventPayload for ContextCommit {
621 const KIND: EventKind = EventKind::CONTEXT_COMMIT;
622}
623
624#[cfg(test)]
629mod tests {
630 use super::*;
631 use crate::event::{DagPosition, EventHeader};
632 use crate::EntityIdType;
633
634 fn make_event(kind: EventKind, payload: serde_json::Value) -> Event<serde_json::Value> {
635 let event_id = crate::EventId::now_v7();
636 let header = EventHeader::new(
637 event_id,
638 event_id,
639 chrono::Utc::now().timestamp_micros(),
640 DagPosition::root(),
641 0,
642 kind,
643 );
644 Event::new(header, payload)
645 }
646
647 #[test]
648 fn test_decode_trajectory_created() {
649 let tid = crate::TrajectoryId::now_v7();
650 let payload = serde_json::json!({
651 "trajectory_id": tid.as_uuid().to_string(),
652 "name": "test-trajectory",
653 "description": "A test trajectory",
654 });
655 let event = make_event(EventKind::TRAJECTORY_CREATED, payload);
656
657 let decoded = event.decode_payload::<TrajectoryCreated>().unwrap();
658 assert_eq!(decoded.trajectory_id, tid);
659 assert_eq!(decoded.name, "test-trajectory");
660 assert_eq!(decoded.description, Some("A test trajectory".to_string()));
661 assert!(decoded.parent_trajectory_id.is_none());
662 }
663
664 #[test]
665 fn test_decode_wrong_kind_returns_none() {
666 let payload = serde_json::json!({"name": "test"});
667 let event = make_event(EventKind::SCOPE_CREATED, payload);
668
669 assert!(event.decode_payload::<TrajectoryCreated>().is_none());
670 }
671
672 #[test]
673 fn test_decode_invalid_payload_returns_none() {
674 let payload = serde_json::json!({"invalid": true});
676 let event = make_event(EventKind::TRAJECTORY_CREATED, payload);
677
678 assert!(event.decode_payload::<TrajectoryCreated>().is_none());
679 }
680
681 #[test]
682 fn test_decode_empty_event_succeeds() {
683 let payload = serde_json::json!({"extra_field": "ignored"});
685 let event = make_event(EventKind::TRAJECTORY_SUSPENDED, payload);
686
687 assert!(event.decode_payload::<TrajectorySuspended>().is_some());
688 }
689
690 #[test]
691 fn test_erase_roundtrip() {
692 let tid = crate::TrajectoryId::now_v7();
693 let created = TrajectoryCreated {
694 trajectory_id: tid,
695 name: "roundtrip-test".to_string(),
696 description: Some("desc".to_string()),
697 parent_trajectory_id: None,
698 root_trajectory_id: None,
699 agent_id: None,
700 metadata: None,
701 };
702
703 let event_id = crate::EventId::now_v7();
704 let header = EventHeader::new(
705 event_id,
706 event_id,
707 chrono::Utc::now().timestamp_micros(),
708 DagPosition::root(),
709 0,
710 EventKind::TRAJECTORY_CREATED,
711 );
712
713 let typed_event = Event::new(header, created);
715 let erased = typed_event.erase();
716 let decoded = erased.decode_payload::<TrajectoryCreated>().unwrap();
717
718 assert_eq!(decoded.trajectory_id, tid);
719 assert_eq!(decoded.name, "roundtrip-test");
720 assert_eq!(decoded.description, Some("desc".to_string()));
721 }
722
723 #[test]
724 fn test_decode_full_event() {
725 let tid = crate::TrajectoryId::now_v7();
726 let payload = serde_json::json!({
727 "trajectory_id": tid.as_uuid().to_string(),
728 "name": "full-event",
729 });
730 let event = make_event(EventKind::TRAJECTORY_CREATED, payload);
731
732 let decoded = event.decode::<TrajectoryCreated>().unwrap();
733 assert_eq!(decoded.payload.trajectory_id, tid);
734 assert_eq!(decoded.payload.name, "full-event");
735 assert_eq!(decoded.header.event_kind, EventKind::TRAJECTORY_CREATED);
736 }
737
738 #[test]
739 fn test_scope_created_defaults() {
740 let sid = crate::ScopeId::now_v7();
741 let tid = crate::TrajectoryId::now_v7();
742 let payload = serde_json::json!({
743 "scope_id": sid.as_uuid().to_string(),
744 "trajectory_id": tid.as_uuid().to_string(),
745 });
746 let event = make_event(EventKind::SCOPE_CREATED, payload);
747
748 let decoded = event.decode_payload::<ScopeCreated>().unwrap();
749 assert_eq!(decoded.scope_id, sid);
750 assert_eq!(decoded.trajectory_id, tid);
751 assert_eq!(decoded.token_budget, 8000); assert_eq!(decoded.tokens_used, 0); assert!(decoded.name.is_empty());
754 }
755}