1#[derive(Debug, Clone)]
6pub enum HyphaEvent {
7 Progress {
9 current: u32,
10 total: u32,
11 message: String,
12 },
13 DownloadProgress {
15 downloaded_bytes: u64,
16 total_bytes: Option<u64>,
17 },
18 Log { message: String },
20 Warn { message: String },
22}
23
24pub trait EventSink: Send + Sync {
26 fn emit(&self, event: HyphaEvent);
27}
28
29pub struct NoopSink;
31
32impl EventSink for NoopSink {
33 fn emit(&self, _: HyphaEvent) {}
34}
35
36#[derive(Debug, Clone)]
42pub struct HyphaError {
43 pub code: String,
44 pub message: String,
45 pub hint: Option<String>,
46}
47
48impl HyphaError {
49 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
50 Self {
51 code: code.into(),
52 message: message.into(),
53 hint: None,
54 }
55 }
56
57 pub fn with_hint(
58 code: impl Into<String>,
59 message: impl Into<String>,
60 hint: impl Into<String>,
61 ) -> Self {
62 Self {
63 code: code.into(),
64 message: message.into(),
65 hint: Some(hint.into()),
66 }
67 }
68}
69
70impl std::fmt::Display for HyphaError {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 write!(f, "{}: {}", self.code, self.message)
73 }
74}
75
76impl std::error::Error for HyphaError {}
77
78impl From<String> for HyphaError {
79 fn from(msg: String) -> Self {
80 Self {
81 code: "error".to_string(),
82 message: msg,
83 hint: None,
84 }
85 }
86}
87
88impl From<&str> for HyphaError {
89 fn from(msg: &str) -> Self {
90 Self::from(msg.to_string())
91 }
92}
93
94pub struct AfDataSink;
96
97struct AfDataSinkMessage {
98 event: agent_first_data::Event,
99 reply: std::sync::mpsc::SyncSender<()>,
100}
101
102fn afdata_sink_sender() -> &'static std::sync::mpsc::Sender<AfDataSinkMessage> {
103 static SENDER: std::sync::OnceLock<std::sync::mpsc::Sender<AfDataSinkMessage>> =
104 std::sync::OnceLock::new();
105 SENDER.get_or_init(|| {
106 let (sender, receiver) = std::sync::mpsc::channel::<AfDataSinkMessage>();
107 std::thread::spawn(move || {
108 let mut emitter = agent_first_data::CliEmitter::stream(
109 std::io::stdout(),
110 agent_first_data::OutputFormat::Json,
111 )
112 .with_strict_protocol();
113 while let Ok(message) = receiver.recv() {
114 let _ = emitter.emit(message.event);
115 let _ = message.reply.send(());
116 }
117 });
118 sender
119 })
120}
121
122fn emit(event: agent_first_data::Event) {
123 let (reply, result) = std::sync::mpsc::sync_channel(1);
124 if afdata_sink_sender()
125 .send(AfDataSinkMessage { event, reply })
126 .is_ok()
127 {
128 let _ = result.recv();
129 }
130}
131
132impl EventSink for AfDataSink {
133 fn emit(&self, event: HyphaEvent) {
134 match event {
135 HyphaEvent::Progress {
136 current,
137 total,
138 message,
139 } => {
140 emit(
141 agent_first_data::json_progress(serde_json::json!({
142 "current": current,
143 "total": total,
144 "message": crate::api::redact_urls_in_text(&message),
145 }))
146 .build(),
147 );
148 }
149 HyphaEvent::DownloadProgress {
150 downloaded_bytes,
151 total_bytes,
152 } => {
153 emit(
154 agent_first_data::json_progress(serde_json::json!({
155 "event": "download_progress",
156 "downloaded_bytes": downloaded_bytes,
157 "total_bytes": total_bytes,
158 }))
159 .build(),
160 );
161 }
162 HyphaEvent::Log { message } => {
163 emit(
164 agent_first_data::json_log(serde_json::json!({
165 "level": "info",
166 "message": crate::api::redact_urls_in_text(&message),
167 }))
168 .build(),
169 );
170 }
171 HyphaEvent::Warn { message } => {
172 emit(
173 agent_first_data::json_log(serde_json::json!({
174 "level": "warn",
175 "event": "warn",
176 "message": crate::api::redact_urls_in_text(&message),
177 }))
178 .build(),
179 );
180 }
181 }
182 }
183}