1use std::collections::HashMap;
22use std::path::{Path, PathBuf};
23use std::time::{Duration, Instant};
24
25#[cfg(unix)]
26use interprocess::local_socket::GenericFilePath;
27#[cfg(windows)]
28use interprocess::local_socket::GenericNamespaced;
29use interprocess::local_socket::Name;
30use interprocess::local_socket::tokio::prelude::*;
31use objectiveai_sdk::cli::websocket_agents_instances_listener::{
32 AgentInstanceEvent, AssistantResponsePart, PartContent, RequestMessageUserPart,
33 ToolResponsePart, VectorRequestChoicePart,
34};
35use tokio::io::AsyncWriteExt;
36use tokio::sync::mpsc;
37
38use super::row::RowValue;
39
40#[derive(Debug, serde::Serialize, serde::Deserialize)]
44#[serde(tag = "type", rename_all = "snake_case")]
45pub enum TeeFrame {
46 Event { event: AgentInstanceEvent },
48 MessageQueueContent {
54 agent_instance_hierarchy: String,
55 response_id: String,
56 message_queue_content_id: i64,
57 delivered_at: String,
59 },
60}
61
62pub fn error_frame(
65 agent_instance_hierarchy: String,
66 response_id: Option<String>,
67 error: serde_json::Value,
68 created_at: i64,
69) -> TeeFrame {
70 TeeFrame::Event {
71 event: AgentInstanceEvent::Error {
72 agent_instance_hierarchy,
73 response_id,
74 error,
75 delivered_at: crate::db::time::unix_to_rfc3339(created_at),
76 },
77 }
78}
79
80#[derive(Default)]
89pub struct FrameMapper {
90 tool_response_heads: HashMap<(String, String, i64), String>,
95 request_tool_heads: HashMap<(String, String, i64), String>,
97 choice_keys: HashMap<(String, String, i64), String>,
100}
101
102impl FrameMapper {
103 pub fn map(&mut self, value: &RowValue<'_>, created_at: i64) -> Option<TeeFrame> {
108 let delivered_at = crate::db::time::unix_to_rfc3339(created_at);
109 let aih = value.agent_instance_hierarchy().to_string();
110 let event = match value {
111 RowValue::MessageQueueContent {
113 response_id,
114 agent_instance_hierarchy,
115 message_queue_content_id,
116 } => {
117 return Some(TeeFrame::MessageQueueContent {
118 agent_instance_hierarchy: (*agent_instance_hierarchy).to_string(),
119 response_id: (*response_id).to_string(),
120 message_queue_content_id: *message_queue_content_id,
121 delivered_at,
122 });
123 }
124
125 RowValue::ToolResponse { tool_call_id, .. } => {
127 self.tool_response_heads.insert(
128 (aih, value.response_id().to_string(), value.row_index()),
129 (*tool_call_id).to_string(),
130 );
131 return None;
132 }
133 RowValue::RequestMessageTool { tool_call_id, .. } => {
134 self.request_tool_heads.insert(
135 (aih, value.response_id().to_string(), value.row_index()),
136 (*tool_call_id).to_string(),
137 );
138 return None;
139 }
140 RowValue::RequestVectorChoice { key, .. } => {
141 self.choice_keys.insert(
142 (aih, value.response_id().to_string(), value.row_index()),
143 (*key).to_string(),
144 );
145 return None;
146 }
147
148 RowValue::AssistantResponseRefusal { text, .. } => assistant_event(
150 value, false,
151 AssistantResponsePart::Refusal { delivered_at, text: (*text).to_string() },
152 ),
153 RowValue::AssistantResponseReasoning { text, .. } => assistant_event(
154 value, false,
155 AssistantResponsePart::Reasoning { delivered_at, text: (*text).to_string() },
156 ),
157 RowValue::AssistantResponseToolCalls {
158 tool_call_id, function_name, arguments, ..
159 } => assistant_event(
160 value, false,
161 AssistantResponsePart::ToolCall {
162 delivered_at,
163 function_name: (*function_name).to_string(),
164 tool_call_id: (*tool_call_id).to_string(),
165 tool_call_index: value.row_sub_index().unwrap_or(0),
166 arguments: (*arguments).to_string(),
167 },
168 ),
169 RowValue::AssistantResponseContentText { text, .. } => assistant_event(
170 value, false,
171 AssistantResponsePart::Text { delivered_at, text: (*text).to_string() },
172 ),
173 RowValue::AssistantResponseContentImage { image_url, .. } => assistant_event(
174 value, false,
175 AssistantResponsePart::Image { delivered_at, image: (*image_url).clone() },
176 ),
177 RowValue::AssistantResponseContentAudio { input_audio, .. } => assistant_event(
178 value, false,
179 AssistantResponsePart::Audio { delivered_at, audio: (*input_audio).clone() },
180 ),
181 RowValue::AssistantResponseContentVideo { video_url, .. } => assistant_event(
182 value, false,
183 AssistantResponsePart::Video { delivered_at, video: (*video_url).clone() },
184 ),
185 RowValue::AssistantResponseContentFile { file, .. } => assistant_event(
186 value, false,
187 AssistantResponsePart::File { delivered_at, file: (*file).clone() },
188 ),
189
190 RowValue::RequestMessageAssistantRefusal { text, .. } => assistant_event(
192 value, true,
193 AssistantResponsePart::Refusal { delivered_at, text: (*text).to_string() },
194 ),
195 RowValue::RequestMessageAssistantReasoning { text, .. } => assistant_event(
196 value, true,
197 AssistantResponsePart::Reasoning { delivered_at, text: (*text).to_string() },
198 ),
199 RowValue::RequestMessageAssistantToolCalls {
200 tool_call_id, function_name, arguments, ..
201 } => assistant_event(
202 value, true,
203 AssistantResponsePart::ToolCall {
204 delivered_at,
205 function_name: (*function_name).to_string(),
206 tool_call_id: (*tool_call_id).to_string(),
207 tool_call_index: value.row_sub_index().unwrap_or(0),
208 arguments: (*arguments).to_string(),
209 },
210 ),
211 RowValue::RequestMessageAssistantContentText { text, .. } => assistant_event(
212 value, true,
213 AssistantResponsePart::Text { delivered_at, text: (*text).to_string() },
214 ),
215 RowValue::RequestMessageAssistantContentImage { image_url, .. } => assistant_event(
216 value, true,
217 AssistantResponsePart::Image { delivered_at, image: (*image_url).clone() },
218 ),
219 RowValue::RequestMessageAssistantContentAudio { input_audio, .. } => assistant_event(
220 value, true,
221 AssistantResponsePart::Audio { delivered_at, audio: (*input_audio).clone() },
222 ),
223 RowValue::RequestMessageAssistantContentVideo { video_url, .. } => assistant_event(
224 value, true,
225 AssistantResponsePart::Video { delivered_at, video: (*video_url).clone() },
226 ),
227 RowValue::RequestMessageAssistantContentFile { file, .. } => assistant_event(
228 value, true,
229 AssistantResponsePart::File { delivered_at, file: (*file).clone() },
230 ),
231
232 RowValue::ToolResponseContentText { text, .. } => self.tool_event(
234 value, delivered_at, PartContent::Text { text: (*text).to_string() }, false,
235 )?,
236 RowValue::ToolResponseContentImage { image_url, .. } => self.tool_event(
237 value, delivered_at, PartContent::Image((*image_url).clone()), false,
238 )?,
239 RowValue::ToolResponseContentAudio { input_audio, .. } => self.tool_event(
240 value, delivered_at, PartContent::Audio((*input_audio).clone()), false,
241 )?,
242 RowValue::ToolResponseContentVideo { video_url, .. } => self.tool_event(
243 value, delivered_at, PartContent::Video((*video_url).clone()), false,
244 )?,
245 RowValue::ToolResponseContentFile { file, .. } => self.tool_event(
246 value, delivered_at, PartContent::File((*file).clone()), false,
247 )?,
248 RowValue::RequestMessageToolContentText { text, .. } => self.tool_event(
249 value, delivered_at, PartContent::Text { text: (*text).to_string() }, true,
250 )?,
251 RowValue::RequestMessageToolContentImage { image_url, .. } => self.tool_event(
252 value, delivered_at, PartContent::Image((*image_url).clone()), true,
253 )?,
254 RowValue::RequestMessageToolContentAudio { input_audio, .. } => self.tool_event(
255 value, delivered_at, PartContent::Audio((*input_audio).clone()), true,
256 )?,
257 RowValue::RequestMessageToolContentVideo { video_url, .. } => self.tool_event(
258 value, delivered_at, PartContent::Video((*video_url).clone()), true,
259 )?,
260 RowValue::RequestMessageToolContentFile { file, .. } => self.tool_event(
261 value, delivered_at, PartContent::File((*file).clone()), true,
262 )?,
263
264 RowValue::RequestMessageUserContentText { text, .. } => user_event(
266 value,
267 RequestMessageUserPart {
268 delivered_at,
269 content: PartContent::Text { text: (*text).to_string() },
270 },
271 ),
272 RowValue::RequestMessageUserContentImage { image_url, .. } => user_event(
273 value,
274 RequestMessageUserPart {
275 delivered_at,
276 content: PartContent::Image((*image_url).clone()),
277 },
278 ),
279 RowValue::RequestMessageUserContentAudio { input_audio, .. } => user_event(
280 value,
281 RequestMessageUserPart {
282 delivered_at,
283 content: PartContent::Audio((*input_audio).clone()),
284 },
285 ),
286 RowValue::RequestMessageUserContentVideo { video_url, .. } => user_event(
287 value,
288 RequestMessageUserPart {
289 delivered_at,
290 content: PartContent::Video((*video_url).clone()),
291 },
292 ),
293 RowValue::RequestMessageUserContentFile { file, .. } => user_event(
294 value,
295 RequestMessageUserPart {
296 delivered_at,
297 content: PartContent::File((*file).clone()),
298 },
299 ),
300
301 RowValue::RequestVectorChoiceContentText { text, .. } => self.choice_event(
303 value, delivered_at, PartContent::Text { text: (*text).to_string() },
304 )?,
305 RowValue::RequestVectorChoiceContentImage { image_url, .. } => self.choice_event(
306 value, delivered_at, PartContent::Image((*image_url).clone()),
307 )?,
308 RowValue::RequestVectorChoiceContentAudio { input_audio, .. } => self.choice_event(
309 value, delivered_at, PartContent::Audio((*input_audio).clone()),
310 )?,
311 RowValue::RequestVectorChoiceContentVideo { video_url, .. } => self.choice_event(
312 value, delivered_at, PartContent::Video((*video_url).clone()),
313 )?,
314 RowValue::RequestVectorChoiceContentFile { file, .. } => self.choice_event(
315 value, delivered_at, PartContent::File((*file).clone()),
316 )?,
317
318 RowValue::ResponseVectorVote { vote, .. } => {
320 AgentInstanceEvent::VectorResponseVote {
321 agent_instance_hierarchy: aih,
322 response_id: value.response_id().to_string(),
323 vote: vote.to_vec(),
324 }
325 }
326 };
327 Some(TeeFrame::Event { event })
328 }
329
330 fn tool_event(
333 &self,
334 value: &RowValue<'_>,
335 delivered_at: String,
336 content: PartContent,
337 request: bool,
338 ) -> Option<AgentInstanceEvent> {
339 let response_id = value.response_id().to_string();
340 let row_index = value.row_index();
341 let heads = if request {
342 &self.request_tool_heads
343 } else {
344 &self.tool_response_heads
345 };
346 let aih = value.agent_instance_hierarchy().to_string();
347 let tool_call_id = heads.get(&(aih, response_id.clone(), row_index))?.clone();
348 let part = ToolResponsePart {
349 delivered_at,
350 content,
351 };
352 Some(if request {
353 AgentInstanceEvent::RequestMessageToolPart {
354 agent_instance_hierarchy: value.agent_instance_hierarchy().to_string(),
355 response_id,
356 tool_call_id,
357 row_index,
358 row_sub_index: value.row_sub_index(),
359 part,
360 }
361 } else {
362 AgentInstanceEvent::ToolResponsePart {
363 agent_instance_hierarchy: value.agent_instance_hierarchy().to_string(),
364 response_id,
365 tool_call_id,
366 row_index,
367 row_sub_index: value.row_sub_index(),
368 part,
369 }
370 })
371 }
372
373 fn choice_event(
376 &self,
377 value: &RowValue<'_>,
378 delivered_at: String,
379 content: PartContent,
380 ) -> Option<AgentInstanceEvent> {
381 let response_id = value.response_id().to_string();
382 let choice_index = value.row_index();
383 let key = self
384 .choice_keys
385 .get(&(
386 value.agent_instance_hierarchy().to_string(),
387 response_id.clone(),
388 choice_index,
389 ))?
390 .clone();
391 Some(AgentInstanceEvent::VectorRequestChoicePart {
392 agent_instance_hierarchy: value.agent_instance_hierarchy().to_string(),
393 response_id,
394 key,
395 choice_index,
396 part_index: value.row_sub_index().unwrap_or(0),
397 part: VectorRequestChoicePart {
398 delivered_at,
399 content,
400 },
401 })
402 }
403}
404
405fn assistant_event(
408 value: &RowValue<'_>,
409 request: bool,
410 part: AssistantResponsePart,
411) -> AgentInstanceEvent {
412 let agent_instance_hierarchy = value.agent_instance_hierarchy().to_string();
413 let response_id = value.response_id().to_string();
414 let row_index = value.row_index();
415 let row_sub_index = value.row_sub_index();
416 if request {
417 AgentInstanceEvent::RequestMessageAssistantPart {
418 agent_instance_hierarchy,
419 response_id,
420 row_index,
421 row_sub_index,
422 part,
423 }
424 } else {
425 AgentInstanceEvent::AssistantResponsePart {
426 agent_instance_hierarchy,
427 response_id,
428 row_index,
429 row_sub_index,
430 part,
431 }
432 }
433}
434
435fn user_event(value: &RowValue<'_>, part: RequestMessageUserPart) -> AgentInstanceEvent {
437 AgentInstanceEvent::RequestMessageUserPart {
438 agent_instance_hierarchy: value.agent_instance_hierarchy().to_string(),
439 response_id: value.response_id().to_string(),
440 row_index: value.row_index(),
441 row_sub_index: value.row_sub_index(),
442 part,
443 }
444}
445
446#[cfg(unix)]
452fn socket_name(state_dir: &Path) -> std::io::Result<Name<'static>> {
453 crate::websockets::mcp_listener::socks_dir(state_dir)
454 .join("conversation.sock")
455 .to_fs_name::<GenericFilePath>()
456}
457
458#[cfg(windows)]
459fn socket_name(state_dir: &Path) -> std::io::Result<Name<'static>> {
460 use std::hash::{Hash, Hasher};
461 let mut hasher = std::collections::hash_map::DefaultHasher::new();
465 state_dir.file_name().hash(&mut hasher);
466 let state = hasher.finish();
467 format!("objectiveai-{state:016x}-conversation.sock").to_ns_name::<GenericNamespaced>()
468}
469
470#[derive(Clone)]
474pub struct ConversationTee {
475 tx: mpsc::UnboundedSender<TeeFrame>,
476}
477
478impl ConversationTee {
479 pub fn spawn(state_dir: PathBuf) -> Self {
484 let (tx, rx) = mpsc::unbounded_channel();
485 tokio::spawn(rx_task(state_dir, rx));
486 Self { tx }
487 }
488
489 pub fn send(&self, frame: TeeFrame) {
492 let _ = self.tx.send(frame);
493 }
494}
495
496async fn rx_task(state_dir: PathBuf, mut rx: mpsc::UnboundedReceiver<TeeFrame>) {
501 let mut write: Option<tokio::io::WriteHalf<LocalSocketStream>> = None;
502 let mut last_attempt: Option<Instant> = None;
503 while let Some(frame) = rx.recv().await {
504 let Ok(mut line) = serde_json::to_vec(&frame) else {
505 continue;
506 };
507 line.push(b'\n');
508 if write.is_none() {
509 let due = last_attempt
510 .map(|at| at.elapsed() >= Duration::from_secs(1))
511 .unwrap_or(true);
512 if due {
513 last_attempt = Some(Instant::now());
514 write = connect(&state_dir).await;
515 }
516 }
517 let Some(sink) = write.as_mut() else {
518 continue;
520 };
521 if sink.write_all(&line).await.is_err() || sink.flush().await.is_err() {
522 write = None;
525 }
526 }
527}
528
529async fn connect(state_dir: &Path) -> Option<tokio::io::WriteHalf<LocalSocketStream>> {
533 const ERROR_PIPE_BUSY: i32 = 231;
534 let mut attempts = 0u32;
535 loop {
536 let name = socket_name(state_dir).ok()?;
537 match LocalSocketStream::connect(name).await {
538 Ok(conn) => {
539 let (_read_half, write_half) = tokio::io::split(conn);
540 return Some(write_half);
541 }
542 Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY) && attempts < 20 => {
543 attempts += 1;
544 tokio::time::sleep(Duration::from_millis(5)).await;
545 }
546 Err(_) => return None,
547 }
548 }
549}