1use crate::core::diff::ChangeType;
7use crate::core::enforcement::ScopeViolation;
8use crate::core::error::HivemindError;
9use crate::core::flow::{RetryMode, TaskExecState};
10use crate::core::graph::GraphTask;
11use crate::core::scope::{RepoAccessMode, Scope};
12use crate::core::verification::CheckConfig;
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::path::PathBuf;
17use uuid::Uuid;
18
19const fn default_max_parallel_tasks() -> u16 {
20 1
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub struct EventId(Uuid);
26
27impl EventId {
28 #[must_use]
30 pub fn new() -> Self {
31 Self(Uuid::new_v4())
32 }
33
34 #[must_use]
39 pub fn from_ordered_u64(sequence: u64) -> Self {
40 let mut bytes = *Uuid::new_v4().as_bytes();
41 bytes[..8].copy_from_slice(&sequence.to_be_bytes());
42 Self(Uuid::from_bytes(bytes))
43 }
44
45 #[must_use]
47 pub fn as_uuid(&self) -> Uuid {
48 self.0
49 }
50}
51
52impl Default for EventId {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58impl std::fmt::Display for EventId {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 write!(f, "{}", self.0)
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct CorrelationIds {
67 pub project_id: Option<Uuid>,
69 #[serde(default)]
70 pub graph_id: Option<Uuid>,
71 pub flow_id: Option<Uuid>,
73 pub task_id: Option<Uuid>,
75 pub attempt_id: Option<Uuid>,
77}
78
79impl CorrelationIds {
80 #[must_use]
82 pub fn none() -> Self {
83 Self {
84 project_id: None,
85 graph_id: None,
86 flow_id: None,
87 task_id: None,
88 attempt_id: None,
89 }
90 }
91
92 #[must_use]
94 pub fn for_project(project_id: Uuid) -> Self {
95 Self {
96 project_id: Some(project_id),
97 graph_id: None,
98 flow_id: None,
99 task_id: None,
100 attempt_id: None,
101 }
102 }
103
104 #[must_use]
105 pub fn for_graph(project_id: Uuid, graph_id: Uuid) -> Self {
106 Self {
107 project_id: Some(project_id),
108 graph_id: Some(graph_id),
109 flow_id: None,
110 task_id: None,
111 attempt_id: None,
112 }
113 }
114
115 #[must_use]
117 pub fn for_task(project_id: Uuid, task_id: Uuid) -> Self {
118 Self {
119 project_id: Some(project_id),
120 graph_id: None,
121 flow_id: None,
122 task_id: Some(task_id),
123 attempt_id: None,
124 }
125 }
126
127 #[must_use]
128 pub fn for_flow(project_id: Uuid, flow_id: Uuid) -> Self {
129 Self {
130 project_id: Some(project_id),
131 graph_id: None,
132 flow_id: Some(flow_id),
133 task_id: None,
134 attempt_id: None,
135 }
136 }
137
138 #[must_use]
139 pub fn for_graph_flow(project_id: Uuid, graph_id: Uuid, flow_id: Uuid) -> Self {
140 Self {
141 project_id: Some(project_id),
142 graph_id: Some(graph_id),
143 flow_id: Some(flow_id),
144 task_id: None,
145 attempt_id: None,
146 }
147 }
148
149 #[must_use]
150 pub fn for_flow_task(project_id: Uuid, flow_id: Uuid, task_id: Uuid) -> Self {
151 Self {
152 project_id: Some(project_id),
153 graph_id: None,
154 flow_id: Some(flow_id),
155 task_id: Some(task_id),
156 attempt_id: None,
157 }
158 }
159
160 #[must_use]
161 pub fn for_graph_flow_task(
162 project_id: Uuid,
163 graph_id: Uuid,
164 flow_id: Uuid,
165 task_id: Uuid,
166 ) -> Self {
167 Self {
168 project_id: Some(project_id),
169 graph_id: Some(graph_id),
170 flow_id: Some(flow_id),
171 task_id: Some(task_id),
172 attempt_id: None,
173 }
174 }
175
176 #[must_use]
177 pub fn for_graph_flow_task_attempt(
178 project_id: Uuid,
179 graph_id: Uuid,
180 flow_id: Uuid,
181 task_id: Uuid,
182 attempt_id: Uuid,
183 ) -> Self {
184 Self {
185 project_id: Some(project_id),
186 graph_id: Some(graph_id),
187 flow_id: Some(flow_id),
188 task_id: Some(task_id),
189 attempt_id: Some(attempt_id),
190 }
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196pub struct EventMetadata {
197 pub id: EventId,
199 pub timestamp: DateTime<Utc>,
201 pub correlation: CorrelationIds,
203 pub sequence: Option<u64>,
205}
206
207impl EventMetadata {
208 #[must_use]
210 pub fn new(correlation: CorrelationIds) -> Self {
211 Self {
212 id: EventId::new(),
213 timestamp: Utc::now(),
214 correlation,
215 sequence: None,
216 }
217 }
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
222#[serde(tag = "type", rename_all = "snake_case")]
223pub enum EventPayload {
224 ErrorOccurred {
226 error: HivemindError,
227 },
228
229 ProjectCreated {
231 id: Uuid,
232 name: String,
233 description: Option<String>,
234 },
235 ProjectUpdated {
237 id: Uuid,
238 name: Option<String>,
239 description: Option<String>,
240 },
241 ProjectRuntimeConfigured {
242 project_id: Uuid,
243 adapter_name: String,
244 binary_path: String,
245 #[serde(default)]
246 model: Option<String>,
247 #[serde(default)]
248 args: Vec<String>,
249 #[serde(default)]
250 env: HashMap<String, String>,
251 timeout_ms: u64,
252 #[serde(default = "default_max_parallel_tasks")]
253 max_parallel_tasks: u16,
254 },
255 TaskCreated {
257 id: Uuid,
258 project_id: Uuid,
259 title: String,
260 description: Option<String>,
261 #[serde(default)]
262 scope: Option<Scope>,
263 },
264 TaskUpdated {
266 id: Uuid,
267 title: Option<String>,
268 description: Option<String>,
269 },
270 TaskRuntimeConfigured {
271 task_id: Uuid,
272 adapter_name: String,
273 binary_path: String,
274 #[serde(default)]
275 model: Option<String>,
276 #[serde(default)]
277 args: Vec<String>,
278 #[serde(default)]
279 env: HashMap<String, String>,
280 timeout_ms: u64,
281 },
282 TaskRuntimeCleared {
283 task_id: Uuid,
284 },
285 TaskClosed {
287 id: Uuid,
288 #[serde(default)]
289 reason: Option<String>,
290 },
291 RepositoryAttached {
293 project_id: Uuid,
294 path: String,
295 name: String,
296 #[serde(default)]
297 access_mode: RepoAccessMode,
298 },
299 RepositoryDetached {
301 project_id: Uuid,
302 name: String,
303 },
304
305 TaskGraphCreated {
306 graph_id: Uuid,
307 project_id: Uuid,
308 name: String,
309 #[serde(default)]
310 description: Option<String>,
311 },
312 TaskAddedToGraph {
313 graph_id: Uuid,
314 task: GraphTask,
315 },
316 DependencyAdded {
317 graph_id: Uuid,
318 from_task: Uuid,
319 to_task: Uuid,
320 },
321 GraphTaskCheckAdded {
322 graph_id: Uuid,
323 task_id: Uuid,
324 check: CheckConfig,
325 },
326 ScopeAssigned {
327 graph_id: Uuid,
328 task_id: Uuid,
329 scope: Scope,
330 },
331
332 TaskGraphValidated {
333 graph_id: Uuid,
334 project_id: Uuid,
335 valid: bool,
336 #[serde(default)]
337 issues: Vec<String>,
338 },
339
340 TaskGraphLocked {
341 graph_id: Uuid,
342 project_id: Uuid,
343 },
344
345 TaskFlowCreated {
346 flow_id: Uuid,
347 graph_id: Uuid,
348 project_id: Uuid,
349 #[serde(default)]
350 name: Option<String>,
351 task_ids: Vec<Uuid>,
352 },
353 TaskFlowStarted {
354 flow_id: Uuid,
355 #[serde(default)]
356 base_revision: Option<String>,
357 },
358 TaskFlowPaused {
359 flow_id: Uuid,
360 #[serde(default)]
361 running_tasks: Vec<Uuid>,
362 },
363 TaskFlowResumed {
364 flow_id: Uuid,
365 },
366 TaskFlowCompleted {
367 flow_id: Uuid,
368 },
369 TaskFlowAborted {
370 flow_id: Uuid,
371 #[serde(default)]
372 reason: Option<String>,
373 forced: bool,
374 },
375
376 TaskReady {
377 flow_id: Uuid,
378 task_id: Uuid,
379 },
380 TaskBlocked {
381 flow_id: Uuid,
382 task_id: Uuid,
383 #[serde(default)]
384 reason: Option<String>,
385 },
386 ScopeConflictDetected {
387 flow_id: Uuid,
388 task_id: Uuid,
389 conflicting_task_id: Uuid,
390 severity: String,
391 action: String,
392 reason: String,
393 },
394 TaskSchedulingDeferred {
395 flow_id: Uuid,
396 task_id: Uuid,
397 reason: String,
398 },
399 TaskExecutionStateChanged {
400 flow_id: Uuid,
401 task_id: Uuid,
402 from: TaskExecState,
403 to: TaskExecState,
404 },
405
406 TaskExecutionStarted {
407 flow_id: Uuid,
408 task_id: Uuid,
409 attempt_id: Uuid,
410 attempt_number: u32,
411 },
412 TaskExecutionSucceeded {
413 flow_id: Uuid,
414 task_id: Uuid,
415 #[serde(default)]
416 attempt_id: Option<Uuid>,
417 },
418 TaskExecutionFailed {
419 flow_id: Uuid,
420 task_id: Uuid,
421 #[serde(default)]
422 attempt_id: Option<Uuid>,
423 #[serde(default)]
424 reason: Option<String>,
425 },
426
427 AttemptStarted {
428 flow_id: Uuid,
429 task_id: Uuid,
430 attempt_id: Uuid,
431 attempt_number: u32,
432 },
433
434 BaselineCaptured {
435 flow_id: Uuid,
436 task_id: Uuid,
437 attempt_id: Uuid,
438 baseline_id: Uuid,
439 #[serde(default)]
440 git_head: Option<String>,
441 file_count: usize,
442 },
443
444 FileModified {
445 flow_id: Uuid,
446 task_id: Uuid,
447 attempt_id: Uuid,
448 path: String,
449 change_type: ChangeType,
450 #[serde(default)]
451 old_hash: Option<String>,
452 #[serde(default)]
453 new_hash: Option<String>,
454 },
455
456 DiffComputed {
457 flow_id: Uuid,
458 task_id: Uuid,
459 attempt_id: Uuid,
460 diff_id: Uuid,
461 baseline_id: Uuid,
462 change_count: usize,
463 },
464
465 CheckStarted {
466 flow_id: Uuid,
467 task_id: Uuid,
468 attempt_id: Uuid,
469 check_name: String,
470 required: bool,
471 },
472
473 CheckCompleted {
474 flow_id: Uuid,
475 task_id: Uuid,
476 attempt_id: Uuid,
477 check_name: String,
478 passed: bool,
479 exit_code: i32,
480 output: String,
481 duration_ms: u64,
482 required: bool,
483 },
484
485 MergeCheckStarted {
486 flow_id: Uuid,
487 #[serde(default)]
488 task_id: Option<Uuid>,
489 check_name: String,
490 required: bool,
491 },
492
493 MergeCheckCompleted {
494 flow_id: Uuid,
495 #[serde(default)]
496 task_id: Option<Uuid>,
497 check_name: String,
498 passed: bool,
499 exit_code: i32,
500 output: String,
501 duration_ms: u64,
502 required: bool,
503 },
504
505 TaskExecutionFrozen {
506 flow_id: Uuid,
507 task_id: Uuid,
508 #[serde(default)]
509 commit_sha: Option<String>,
510 },
511
512 TaskIntegratedIntoFlow {
513 flow_id: Uuid,
514 task_id: Uuid,
515 #[serde(default)]
516 commit_sha: Option<String>,
517 },
518
519 MergeConflictDetected {
520 flow_id: Uuid,
521 #[serde(default)]
522 task_id: Option<Uuid>,
523 details: String,
524 },
525
526 FlowFrozenForMerge {
527 flow_id: Uuid,
528 },
529
530 FlowIntegrationLockAcquired {
531 flow_id: Uuid,
532 operation: String,
533 },
534
535 CheckpointDeclared {
536 flow_id: Uuid,
537 task_id: Uuid,
538 attempt_id: Uuid,
539 checkpoint_id: String,
540 order: u32,
541 total: u32,
542 },
543
544 CheckpointActivated {
545 flow_id: Uuid,
546 task_id: Uuid,
547 attempt_id: Uuid,
548 checkpoint_id: String,
549 order: u32,
550 },
551
552 CheckpointCompleted {
553 flow_id: Uuid,
554 task_id: Uuid,
555 attempt_id: Uuid,
556 checkpoint_id: String,
557 order: u32,
558 commit_hash: String,
559 timestamp: DateTime<Utc>,
560 #[serde(default)]
561 summary: Option<String>,
562 },
563
564 AllCheckpointsCompleted {
565 flow_id: Uuid,
566 task_id: Uuid,
567 attempt_id: Uuid,
568 },
569
570 CheckpointCommitCreated {
571 flow_id: Uuid,
572 task_id: Uuid,
573 attempt_id: Uuid,
574 commit_sha: String,
575 },
576
577 ScopeValidated {
578 flow_id: Uuid,
579 task_id: Uuid,
580 attempt_id: Uuid,
581 verification_id: Uuid,
582 verified_at: DateTime<Utc>,
583 scope: Scope,
584 },
585
586 ScopeViolationDetected {
587 flow_id: Uuid,
588 task_id: Uuid,
589 attempt_id: Uuid,
590 verification_id: Uuid,
591 verified_at: DateTime<Utc>,
592 scope: Scope,
593 #[serde(default)]
594 violations: Vec<ScopeViolation>,
595 },
596
597 RetryContextAssembled {
598 flow_id: Uuid,
599 task_id: Uuid,
600 attempt_id: Uuid,
601 attempt_number: u32,
602 max_attempts: u32,
603 #[serde(default)]
604 prior_attempt_ids: Vec<Uuid>,
605 #[serde(default)]
606 required_check_failures: Vec<String>,
607 #[serde(default)]
608 optional_check_failures: Vec<String>,
609 #[serde(default)]
610 runtime_exit_code: Option<i32>,
611 #[serde(default)]
612 runtime_terminated_reason: Option<String>,
613 context: String,
614 },
615
616 TaskRetryRequested {
617 task_id: Uuid,
618 reset_count: bool,
619 #[serde(default)]
620 retry_mode: RetryMode,
621 },
622 TaskAborted {
623 task_id: Uuid,
624 #[serde(default)]
625 reason: Option<String>,
626 },
627
628 HumanOverride {
629 task_id: Uuid,
630 override_type: String,
631 decision: String,
632 reason: String,
633 #[serde(default)]
634 user: Option<String>,
635 },
636
637 MergePrepared {
638 flow_id: Uuid,
639 #[serde(default)]
640 target_branch: Option<String>,
641 #[serde(default)]
642 conflicts: Vec<String>,
643 },
644 MergeApproved {
645 flow_id: Uuid,
646 #[serde(default)]
647 user: Option<String>,
648 },
649 MergeCompleted {
650 flow_id: Uuid,
651 #[serde(default)]
652 commits: Vec<String>,
653 },
654 RuntimeStarted {
655 adapter_name: String,
656 task_id: Uuid,
657 attempt_id: Uuid,
658 },
659 RuntimeOutputChunk {
660 attempt_id: Uuid,
661 stream: RuntimeOutputStream,
662 content: String,
663 },
664 RuntimeInputProvided {
665 attempt_id: Uuid,
666 content: String,
667 },
668 RuntimeInterrupted {
669 attempt_id: Uuid,
670 },
671 RuntimeExited {
672 attempt_id: Uuid,
673 exit_code: i32,
674 duration_ms: u64,
675 },
676 RuntimeTerminated {
677 attempt_id: Uuid,
678 reason: String,
679 },
680 RuntimeFilesystemObserved {
681 attempt_id: Uuid,
682 #[serde(default)]
683 files_created: Vec<PathBuf>,
684 #[serde(default)]
685 files_modified: Vec<PathBuf>,
686 #[serde(default)]
687 files_deleted: Vec<PathBuf>,
688 },
689 RuntimeCommandObserved {
690 attempt_id: Uuid,
691 stream: RuntimeOutputStream,
692 command: String,
693 },
694 RuntimeToolCallObserved {
695 attempt_id: Uuid,
696 stream: RuntimeOutputStream,
697 tool_name: String,
698 details: String,
699 },
700 RuntimeTodoSnapshotUpdated {
701 attempt_id: Uuid,
702 stream: RuntimeOutputStream,
703 #[serde(default)]
704 items: Vec<String>,
705 },
706 RuntimeNarrativeOutputObserved {
707 attempt_id: Uuid,
708 stream: RuntimeOutputStream,
709 content: String,
710 },
711
712 #[serde(other)]
713 Unknown,
714}
715
716#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
718#[serde(rename_all = "lowercase")]
719pub enum RuntimeOutputStream {
720 Stdout,
721 Stderr,
722}
723
724#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
726pub struct Event {
727 pub metadata: EventMetadata,
729 pub payload: EventPayload,
731}
732
733impl Event {
734 #[must_use]
736 pub fn new(payload: EventPayload, correlation: CorrelationIds) -> Self {
737 Self {
738 metadata: EventMetadata::new(correlation),
739 payload,
740 }
741 }
742
743 #[must_use]
745 pub fn id(&self) -> EventId {
746 self.metadata.id
747 }
748
749 #[must_use]
751 pub fn timestamp(&self) -> DateTime<Utc> {
752 self.metadata.timestamp
753 }
754}
755
756#[cfg(test)]
757mod tests {
758 use super::*;
759
760 #[test]
761 fn event_id_is_unique() {
762 let id1 = EventId::new();
763 let id2 = EventId::new();
764 assert_ne!(id1, id2);
765 }
766
767 #[test]
768 fn event_serialization_roundtrip() {
769 let event = Event::new(
770 EventPayload::ProjectCreated {
771 id: Uuid::new_v4(),
772 name: "test-project".to_string(),
773 description: Some("A test project".to_string()),
774 },
775 CorrelationIds::none(),
776 );
777
778 let json = serde_json::to_string(&event).expect("serialize");
779 let restored: Event = serde_json::from_str(&json).expect("deserialize");
780
781 assert_eq!(event.payload, restored.payload);
782 }
783
784 #[test]
785 fn correlation_ids_for_project() {
786 let project_id = Uuid::new_v4();
787 let corr = CorrelationIds::for_project(project_id);
788 assert_eq!(corr.project_id, Some(project_id));
789 assert!(corr.task_id.is_none());
790 }
791}