Skip to main content

hypha/sink/
mod.rs

1/// A structured event emitted by hypha library functions.
2///
3/// Callers decide how to handle these — print them (CLI), forward to a channel
4/// (GUI/agent), or ignore them entirely (NoopSink).
5#[derive(Debug, Clone)]
6pub enum HyphaEvent {
7    /// A multi-step operation made progress.
8    Progress {
9        current: u32,
10        total: u32,
11        message: String,
12    },
13    /// Byte-level download progress for speed/ETA display.
14    DownloadProgress {
15        downloaded_bytes: u64,
16        total_bytes: Option<u64>,
17    },
18    /// A non-fatal informational message.
19    Log { message: String },
20    /// A non-fatal warning.
21    Warn { message: String },
22}
23
24/// Receiver of events emitted during a hypha operation.
25pub trait EventSink: Send + Sync {
26    fn emit(&self, event: HyphaEvent);
27}
28
29/// Discards all events — use when the caller only cares about the return value.
30pub struct NoopSink;
31
32impl EventSink for NoopSink {
33    fn emit(&self, _: HyphaEvent) {}
34}
35
36/// Structured error returned by hypha library functions.
37///
38/// The `code` field is a machine-readable identifier (e.g. `"invalid_uri"`,
39/// `"dns_failed"`).  The `message` is human-readable detail.
40/// The optional `hint` provides actionable remediation advice.
41#[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
94/// Emits events as afdata JSON to stdout — used by library callers.
95pub 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}