1- //! Room administration over the LiveKit server API.
1+ //! The agent's room administration over the LiveKit server API.
22//!
3- //! Named apart from the rest because it is the one region here that talks to
4- //! LiveKit over HTTP rather than over the room's own event stream. Everything
5- //! in it exists for one problem: a previous run of this agent can leave a
6- //! participant behind, and a second agent in a room answers over the top of
7- //! the first. `run_room` and `open_session` in the parent call two of these and
8- //! nothing outside this module calls any of them.
3+ //! Named apart from the rest because it talks to LiveKit over HTTP rather than
4+ //! over the room's own event stream; the HTTP itself is `room_service`'s.
5+ //! Everything in it exists for one problem: a previous run of this agent can
6+ //! leave a participant behind, and a second agent in a room answers over the
7+ //! top of the first. `run_room` and `open_session` in the parent call two of
8+ //! these and nothing outside this module calls any of them.
99//!
1010//! Split out of `livekit.rs` along the line its module doc already drew.
1111
@@ -15,6 +15,10 @@ use crate::config::{AgentConfig, livekit_http_base};
1515use crate :: token:: livekit_room_admin_token;
1616
1717use super :: DUPLICATE_AGENT_ISOLATION_ATTEMPTS ;
18+ use super :: room_service:: {
19+ is_twirp_not_found, livekit_room_service_request, parse_room_service_response,
20+ post_room_service,
21+ } ;
1822
1923pub ( super ) async fn isolate_local_agent (
2024 config : & AgentConfig ,
@@ -65,7 +69,9 @@ async fn list_room_participants(
6569 now_seconds : u64 ,
6670) -> Result < serde_json:: Value , Box < dyn std:: error:: Error + Send + Sync > > {
6771 livekit_room_service_request (
68- config,
72+ & config. livekit_url ,
73+ & config. livekit_api_key ,
74+ & config. livekit_api_secret ,
6975 room_name,
7076 "ListParticipants" ,
7177 serde_json:: json!( { "room" : room_name } ) ,
@@ -89,59 +95,6 @@ async fn remove_room_participant(
8995 identity : & str ,
9096 now_seconds : u64 ,
9197) -> Result < ( ) , Box < dyn std:: error:: Error + Send + Sync > > {
92- match livekit_room_service_request (
93- config,
94- room_name,
95- "RemoveParticipant" ,
96- serde_json:: json!( { "room" : room_name, "identity" : identity } ) ,
97- now_seconds,
98- )
99- . await
100- {
101- Ok ( _) => Ok ( ( ) ) ,
102- Err ( error) if is_participant_gone ( & error. to_string ( ) ) => {
103- eprintln ! ( "duplicate LiveKit agent participant already gone identity={identity}" ) ;
104- Ok ( ( ) )
105- }
106- Err ( error) => Err ( error) ,
107- }
108- }
109-
110- /// Matches on the wire response rather than on a status code alone, because
111- /// LiveKit answers Twirp over HTTP and a bare 404 could also mean the route is
112- /// wrong. All three parts are required: the method, so a 404 from any other
113- /// RoomService call stays fatal; the status; and `not_found` in the body, which
114- /// is the participant-specific Twirp code rather than an HTML error page.
115- fn is_participant_gone ( error : & str ) -> bool {
116- error. contains ( "RemoveParticipant" ) && error. contains ( "404" ) && error. contains ( "not_found" )
117- }
118-
119- /// The Twirp POST both RoomService callers make. Only the token and the base
120- /// differ, and both are strings; the body comes back with the status because a
121- /// refusal explains itself there and not in the code.
122- async fn post_room_service (
123- base : & str ,
124- token : & str ,
125- method : & str ,
126- body : & serde_json:: Value ,
127- ) -> Result < ( reqwest:: StatusCode , String ) , Box < dyn std:: error:: Error + Send + Sync > > {
128- let response = crate :: http_client ( )
129- . post ( format ! ( "{base}/twirp/livekit.RoomService/{method}" ) )
130- . bearer_auth ( token)
131- . json ( body)
132- . send ( )
133- . await ?;
134- let status = response. status ( ) ;
135- Ok ( ( status, response. text ( ) . await ?) )
136- }
137-
138- async fn livekit_room_service_request (
139- config : & AgentConfig ,
140- room_name : & str ,
141- method : & str ,
142- body : serde_json:: Value ,
143- now_seconds : u64 ,
144- ) -> Result < serde_json:: Value , Box < dyn std:: error:: Error + Send + Sync > > {
14598 let token = livekit_room_admin_token (
14699 & config. livekit_api_key ,
147100 & config. livekit_api_secret ,
@@ -151,14 +104,28 @@ async fn livekit_room_service_request(
151104 let ( status, text) = post_room_service (
152105 & livekit_http_base ( & config. livekit_url ) ,
153106 & token,
154- method ,
155- & body ,
107+ "RemoveParticipant" ,
108+ & serde_json :: json! ( { "room" : room_name , "identity" : identity } ) ,
156109 )
157110 . await ?;
111+ if is_twirp_not_found ( status, & text) {
112+ eprintln ! ( "duplicate LiveKit agent participant already gone identity={identity}" ) ;
113+ return Ok ( ( ) ) ;
114+ }
158115 if !status. is_success ( ) {
159- return Err ( format ! ( "LiveKit RoomService {method} failed: {status} {text}" ) . into ( ) ) ;
116+ return Err (
117+ format ! ( "LiveKit RoomService RemoveParticipant failed: {status} {text}" ) . into ( ) ,
118+ ) ;
160119 }
161- parse_room_service_response ( method, & text)
120+ parse_room_service_response ( "RemoveParticipant" , & text) . map ( |_| ( ) )
121+ }
122+
123+ /// The old "already gone" check, kept so the unit tests that pin it still
124+ /// compile unedited. Production removal uses [`is_twirp_not_found`] instead of
125+ /// searching the formatted error string.
126+ #[ cfg( test) ]
127+ fn is_participant_gone ( error : & str ) -> bool {
128+ error. contains ( "RemoveParticipant" ) && error. contains ( "404" ) && error. contains ( "not_found" )
162129}
163130
164131/// Proves credentials like `check-gemini` proves a Google key: the server
@@ -199,15 +166,6 @@ pub(crate) async fn validate_livekit_credentials(
199166 ) )
200167}
201168
202- fn parse_room_service_response (
203- method : & str ,
204- text : & str ,
205- ) -> Result < serde_json:: Value , Box < dyn std:: error:: Error + Send + Sync > > {
206- serde_json:: from_str ( text) . map_err ( |error| {
207- format ! ( "LiveKit RoomService {method} returned invalid JSON: {error}" ) . into ( )
208- } )
209- }
210-
211169fn duplicate_agent_identities (
212170 participants : & serde_json:: Value ,
213171 local_identity : & str ,
0 commit comments