11//! Agent client for message and channel operations.
22
33use crate :: client:: { ClientOptions , HttpClient , RequestOptions } ;
4- use crate :: error:: Result ;
4+ use crate :: error:: { RelayError , Result } ;
55use crate :: types:: * ;
66use crate :: ws:: { EventReceiver , LifecycleReceiver , WsClient , WsClientOptions } ;
77
@@ -34,6 +34,17 @@ impl Default for DmOptions {
3434 }
3535}
3636
37+ /// Result of ensuring a channel exists and is joined.
38+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
39+ pub struct EnsureChannelOutcome {
40+ /// Channel name that was ensured.
41+ pub name : String ,
42+ /// Whether the channel was created by this call.
43+ pub created : bool ,
44+ /// Whether the agent joined the channel during this call.
45+ pub joined : bool ,
46+ }
47+
3748impl AgentClient {
3849 /// Create a new agent client with the given token.
3950 pub fn new ( token : impl Into < String > , base_url : Option < String > ) -> Result < Self > {
@@ -314,28 +325,42 @@ impl AgentClient {
314325 // === DMs ===
315326
316327 /// Send a direct message to another agent.
317- pub async fn dm ( & self , agent : & str , text : & str , opts : Option < DmOptions > ) -> Result < serde_json:: Value > {
328+ pub async fn dm (
329+ & self ,
330+ agent : & str ,
331+ text : & str ,
332+ opts : Option < DmOptions > ,
333+ ) -> Result < serde_json:: Value > {
318334 let opts = opts. unwrap_or_default ( ) ;
319335 let body = SendDmRequest {
320336 to : agent. to_string ( ) ,
321337 text : text. to_string ( ) ,
322338 attachments : opts. attachments ,
323339 mode : Some ( opts. mode ) ,
324340 } ;
325- let options = opts. idempotency_key . map ( RequestOptions :: with_idempotency_key) ;
341+ let options = opts
342+ . idempotency_key
343+ . map ( RequestOptions :: with_idempotency_key) ;
326344 self . client . post ( "/v1/dm" , Some ( body) , options) . await
327345 }
328346
329347 /// Send a direct message to another agent (typed response).
330- pub async fn dm_typed ( & self , agent : & str , text : & str , opts : Option < DmOptions > ) -> Result < DmSendResponse > {
348+ pub async fn dm_typed (
349+ & self ,
350+ agent : & str ,
351+ text : & str ,
352+ opts : Option < DmOptions > ,
353+ ) -> Result < DmSendResponse > {
331354 let opts = opts. unwrap_or_default ( ) ;
332355 let body = SendDmRequest {
333356 to : agent. to_string ( ) ,
334357 text : text. to_string ( ) ,
335358 attachments : opts. attachments ,
336359 mode : Some ( opts. mode ) ,
337360 } ;
338- let options = opts. idempotency_key . map ( RequestOptions :: with_idempotency_key) ;
361+ let options = opts
362+ . idempotency_key
363+ . map ( RequestOptions :: with_idempotency_key) ;
339364 self . client . post ( "/v1/dm" , Some ( body) , options) . await
340365 }
341366
@@ -434,7 +459,10 @@ impl AgentClient {
434459 ) -> Result < serde_json:: Value > {
435460 let opts = opts. unwrap_or_default ( ) ;
436461 let mut body = serde_json:: Map :: new ( ) ;
437- body. insert ( "text" . to_string ( ) , serde_json:: Value :: String ( text. to_string ( ) ) ) ;
462+ body. insert (
463+ "text" . to_string ( ) ,
464+ serde_json:: Value :: String ( text. to_string ( ) ) ,
465+ ) ;
438466 body. insert (
439467 "mode" . to_string ( ) ,
440468 serde_json:: Value :: String ( match opts. mode {
@@ -443,9 +471,14 @@ impl AgentClient {
443471 } ) ,
444472 ) ;
445473 if let Some ( attachments) = opts. attachments {
446- body. insert ( "attachments" . to_string ( ) , serde_json:: to_value ( attachments) ?) ;
474+ body. insert (
475+ "attachments" . to_string ( ) ,
476+ serde_json:: to_value ( attachments) ?,
477+ ) ;
447478 }
448- let options = opts. idempotency_key . map ( RequestOptions :: with_idempotency_key) ;
479+ let options = opts
480+ . idempotency_key
481+ . map ( RequestOptions :: with_idempotency_key) ;
449482 self . client
450483 . post (
451484 & format ! ( "/v1/dm/{}/messages" , urlencoding:: encode( conversation_id) ) ,
@@ -464,7 +497,10 @@ impl AgentClient {
464497 ) -> Result < GroupDmMessageResponse > {
465498 let opts = opts. unwrap_or_default ( ) ;
466499 let mut body = serde_json:: Map :: new ( ) ;
467- body. insert ( "text" . to_string ( ) , serde_json:: Value :: String ( text. to_string ( ) ) ) ;
500+ body. insert (
501+ "text" . to_string ( ) ,
502+ serde_json:: Value :: String ( text. to_string ( ) ) ,
503+ ) ;
468504 body. insert (
469505 "mode" . to_string ( ) ,
470506 serde_json:: Value :: String ( match opts. mode {
@@ -473,9 +509,14 @@ impl AgentClient {
473509 } ) ,
474510 ) ;
475511 if let Some ( attachments) = opts. attachments {
476- body. insert ( "attachments" . to_string ( ) , serde_json:: to_value ( attachments) ?) ;
512+ body. insert (
513+ "attachments" . to_string ( ) ,
514+ serde_json:: to_value ( attachments) ?,
515+ ) ;
477516 }
478- let options = opts. idempotency_key . map ( RequestOptions :: with_idempotency_key) ;
517+ let options = opts
518+ . idempotency_key
519+ . map ( RequestOptions :: with_idempotency_key) ;
479520 self . client
480521 . post (
481522 & format ! ( "/v1/dm/{}/messages" , urlencoding:: encode( conversation_id) ) ,
@@ -544,6 +585,46 @@ impl AgentClient {
544585 self . client . post ( "/v1/channels" , Some ( request) , None ) . await
545586 }
546587
588+ /// Ensure a channel exists and this agent is a member.
589+ ///
590+ /// `409 Conflict` from either create or join is treated as success, which
591+ /// makes this safe to call during startup.
592+ pub async fn ensure_joined_channel (
593+ & self ,
594+ request : CreateChannelRequest ,
595+ ) -> Result < EnsureChannelOutcome > {
596+ let name = request. name . clone ( ) ;
597+ let created = match self . create_channel ( request) . await {
598+ Ok ( _) => true ,
599+ Err ( RelayError :: Api { status : 409 , .. } ) => false ,
600+ Err ( error) => return Err ( error) ,
601+ } ;
602+
603+ let joined = match self . join_channel ( & name) . await {
604+ Ok ( _) => true ,
605+ Err ( RelayError :: Api { status : 409 , .. } ) => false ,
606+ Err ( error) => return Err ( error) ,
607+ } ;
608+
609+ Ok ( EnsureChannelOutcome {
610+ name,
611+ created,
612+ joined,
613+ } )
614+ }
615+
616+ /// Ensure several channels exist and this agent is a member of each.
617+ pub async fn ensure_joined_channels < I > ( & self , requests : I ) -> Result < Vec < EnsureChannelOutcome > >
618+ where
619+ I : IntoIterator < Item = CreateChannelRequest > ,
620+ {
621+ let mut outcomes = Vec :: new ( ) ;
622+ for request in requests {
623+ outcomes. push ( self . ensure_joined_channel ( request) . await ?) ;
624+ }
625+ Ok ( outcomes)
626+ }
627+
547628 /// List channels.
548629 pub async fn list_channels ( & self , include_archived : bool ) -> Result < Vec < Channel > > {
549630 let query = if include_archived {
0 commit comments