1pub mod actions;
3
4pub use actions::JsonRpcClientProtocol;
5
6use anyhow::{Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::sync::mpsc;
10use tracing::{error, info, warn};
11
12use crate::client::http_fetch::FetchClient;
13use crate::client::jsonrpc::actions::JSONRPC_CLIENT_RESPONSE_RECEIVED_EVENT;
14use crate::client::llm_budget::call_llm_for_client;
15use crate::llm::actions::client_trait::{Client, ClientActionResult};
16use crate::llm::ollama_client::OllamaClient;
17use crate::llm::ClientLlmResult;
18use crate::protocol::Event;
19use crate::state::app_state::AppState;
20use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
21use crate::state::{AccessLogOwner, ClientId, ClientStatus};
22
23enum Applied {
27 Ran(String),
29 Disconnect,
31}
32
33#[derive(Clone, Copy)]
35enum Dispatch {
36 Inline,
39 Deferred,
43}
44
45enum Notification {
47 Single((u16, Option<serde_json::Value>)),
48 Batch((u16, Option<serde_json::Value>)),
49}
50
51const MAX_FOLLOWUP_DEPTH: usize = 6;
60
61pub struct JsonRpcClient;
63
64impl JsonRpcClient {
65 #[cfg(not(target_arch = "wasm32"))]
89 async fn reqwest_client(base_url: &str) -> Result<reqwest::Client> {
90 use std::collections::HashMap;
91 use std::sync::{Mutex, OnceLock};
92
93 static CLIENTS: OnceLock<Mutex<HashMap<String, reqwest::Client>>> = OnceLock::new();
94 let clients = CLIENTS.get_or_init(|| Mutex::new(HashMap::new()));
95
96 let host = crate::llm::ollama_client::host_of(base_url).to_string();
97 if let Some(client) = clients.lock().ok().and_then(|map| map.get(&host).cloned()) {
98 return Ok(client);
99 }
100
101 let build_host = host.clone();
102 let built = tokio::task::spawn_blocking(move || {
103 let mut builder = reqwest::Client::builder()
104 .timeout(std::time::Duration::from_secs(30))
105 .use_rustls_tls();
106 if let Ok(ip) = build_host.parse::<std::net::IpAddr>() {
110 builder = builder.resolve(&build_host, std::net::SocketAddr::new(ip, 0));
113 }
114 builder
115 .build()
116 .context("Failed to build HTTP client for JSON-RPC")
117 })
118 .await
119 .context("JSON-RPC HTTP client build task panicked")??;
120
121 match clients.lock() {
124 Ok(mut map) => Ok(map.entry(host).or_insert(built).clone()),
125 Err(_) => Ok(built),
126 }
127 }
128
129 async fn http_client(base_url: &str) -> Result<FetchClient> {
133 #[cfg(not(target_arch = "wasm32"))]
134 let client = FetchClient::from_reqwest(Self::reqwest_client(base_url).await?);
135 #[cfg(target_arch = "wasm32")]
136 let client = {
137 let _ = base_url;
138 FetchClient::transport(std::time::Duration::from_secs(30))
139 };
140 Ok(client)
141 }
142
143 pub async fn connect_with_llm_actions(
145 remote_addr: String,
146 llm_client: OllamaClient,
147 app_state: Arc<AppState>,
148 status_tx: mpsc::UnboundedSender<String>,
149 client_id: ClientId,
150 startup_params: Option<crate::protocol::StartupParams>,
151 ) -> Result<SocketAddr> {
152 let remote_addr = if remote_addr.contains("://") {
161 remote_addr
162 } else {
163 format!("http://{remote_addr}")
164 };
165 crate::client::http_fetch::check_url(&remote_addr)?;
168
169 info!(
170 "JSON-RPC client {} initialized for {}",
171 client_id, remote_addr
172 );
173
174 let default_headers = match &startup_params {
184 Some(params) => params.get_optional_object("default_headers")?.cloned(),
185 None => None,
186 };
187
188 app_state
190 .with_client_mut(client_id, |client| {
191 client.set_protocol_field(
192 "jsonrpc_client".to_string(),
193 serde_json::json!("initialized"),
194 );
195 client.set_protocol_field(
196 "endpoint".to_string(),
197 serde_json::json!(remote_addr.clone()),
198 );
199 client.set_protocol_field("next_id".to_string(), serde_json::json!(1));
200 if let Some(headers) = &default_headers {
201 client.set_protocol_field(
202 "default_headers".to_string(),
203 serde_json::Value::Object(headers.clone()),
204 );
205 }
206 })
207 .await;
208
209 app_state
211 .update_client_status(client_id, ClientStatus::Connected)
212 .await;
213 let _ = status_tx.send(format!(
214 "[CLIENT] JSON-RPC client {} ready for {}",
215 client_id, remote_addr
216 ));
217 let _ = status_tx.send("__UPDATE_UI__".to_string());
218
219 let command_rx =
224 crate::client::command_support::register_command_channel(&app_state, client_id).await;
225 let cmd_task = tokio::spawn(Self::command_loop(
226 command_rx,
227 client_id,
228 app_state.clone(),
229 llm_client.clone(),
230 status_tx.clone(),
231 ));
232 app_state.register_client_task(client_id, cmd_task).await;
233
234 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
236 let protocol = Arc::new(crate::client::jsonrpc::actions::JsonRpcClientProtocol::new());
237 let event = Event::new(
238 &crate::client::jsonrpc::actions::JSONRPC_CLIENT_CONNECTED_EVENT,
239 serde_json::json!({
240 "endpoint": remote_addr,
241 }),
242 );
243
244 let memory = app_state
245 .get_memory_for_client(client_id)
246 .await
247 .unwrap_or_default();
248
249 let app_state_clone = app_state.clone();
251 let llm_client_clone = llm_client.clone();
252 let status_tx_clone = status_tx.clone();
253 let task_registrar = app_state.clone();
256 let task_handle = tokio::spawn(async move {
257 match call_llm_for_client(
258 &llm_client_clone,
259 &app_state_clone,
260 client_id.to_string(),
261 &instruction,
262 &memory,
263 Some(&event),
264 protocol.as_ref(),
265 &status_tx_clone,
266 )
267 .await
268 {
269 Ok(ClientLlmResult {
270 actions,
271 memory_updates,
272 }) => {
273 if let Some(mem) = memory_updates {
275 app_state_clone.set_memory_for_client(client_id, mem).await;
276 }
277
278 for action in actions {
280 let result = match protocol.execute_action(action) {
281 Ok(result) => result,
282 Err(e) => {
283 error!("JSON-RPC client {} rejected action: {}", client_id, e);
284 continue;
285 }
286 };
287 match Self::apply_action(
288 result,
289 Dispatch::Inline,
290 client_id,
291 &app_state_clone,
292 &llm_client_clone,
293 &status_tx_clone,
294 0,
295 )
296 .await
297 {
298 Ok(Applied::Ran(detail)) => {
299 info!("JSON-RPC client {}: {}", client_id, detail);
300 }
301 Ok(Applied::Disconnect) => break,
302 Err(e) => {
303 error!("JSON-RPC client {} action failed: {}", client_id, e);
304 }
305 }
306 }
307 }
308 Err(e) => {
309 error!("LLM error for JSON-RPC client {}: {}", client_id, e);
310 }
311 }
312 });
313 task_registrar
314 .register_client_task(client_id, task_handle)
315 .await;
316 }
317
318 Ok("0.0.0.0:0".parse().unwrap())
324 }
325
326 fn apply_action<'a>(
343 result: ClientActionResult,
344 dispatch: Dispatch,
345 client_id: ClientId,
346 app_state: &'a Arc<AppState>,
347 llm_client: &'a OllamaClient,
348 status_tx: &'a mpsc::UnboundedSender<String>,
349 depth: usize,
350 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Applied>> + Send + 'a>> {
351 Box::pin(async move {
352 match result {
353 ClientActionResult::Custom { name, data } if name == "jsonrpc_request" => {
354 let method = data
355 .get("method")
356 .and_then(|v| v.as_str())
357 .unwrap_or("")
358 .to_string();
359 let params = data.get("params").cloned();
360 let id = data.get("id").cloned();
361
362 let exchange =
363 Self::perform_request(client_id, &method, params, id, app_state).await?;
364 let detail = format!(
365 "jsonrpc_request '{}' sent; HTTP {} ({})",
366 method,
367 exchange.0,
368 if exchange.1.is_some() {
369 "JSON-RPC response received"
370 } else {
371 "body was not valid JSON"
372 }
373 );
374 Self::deliver_response(
375 Notification::Single(exchange),
376 dispatch,
377 client_id,
378 app_state,
379 llm_client,
380 status_tx,
381 depth,
382 )
383 .await;
384 Ok(Applied::Ran(detail))
385 }
386 ClientActionResult::Custom { name, data } if name == "jsonrpc_batch" => {
387 let requests = data
388 .get("requests")
389 .and_then(|v| v.as_array())
390 .cloned()
391 .unwrap_or_default();
392 let count = requests.len();
393
394 let exchange =
395 Self::perform_batch_request(client_id, requests, app_state).await?;
396 let detail = format!(
397 "jsonrpc_batch of {} request(s) sent; HTTP {} ({})",
398 count,
399 exchange.0,
400 if exchange.1.is_some() {
401 "batch response received"
402 } else {
403 "body was not valid JSON"
404 }
405 );
406 Self::deliver_response(
407 Notification::Batch(exchange),
408 dispatch,
409 client_id,
410 app_state,
411 llm_client,
412 status_tx,
413 depth,
414 )
415 .await;
416 Ok(Applied::Ran(detail))
417 }
418 ClientActionResult::Disconnect => {
419 info!("JSON-RPC client {} disconnecting", client_id);
420 app_state
421 .update_client_status(client_id, ClientStatus::Disconnected)
422 .await;
423 let _ = status_tx.send("__UPDATE_UI__".to_string());
424 Ok(Applied::Disconnect)
425 }
426 ClientActionResult::Custom { name, .. } => Err(anyhow::anyhow!(
427 "JSON-RPC client cannot execute custom result '{}'",
428 name
429 )),
430 _ => Ok(Applied::Ran(
433 "no request made (action produced no JSON-RPC call)".to_string(),
434 )),
435 }
436 })
437 }
438
439 async fn deliver_response(
441 notification: Notification,
442 dispatch: Dispatch,
443 client_id: ClientId,
444 app_state: &Arc<AppState>,
445 llm_client: &OllamaClient,
446 status_tx: &mpsc::UnboundedSender<String>,
447 depth: usize,
448 ) {
449 match dispatch {
450 Dispatch::Inline => match notification {
451 Notification::Single(exchange) => {
452 Self::notify_response(
453 client_id, exchange, app_state, llm_client, status_tx, depth,
454 )
455 .await
456 }
457 Notification::Batch(exchange) => {
458 Self::notify_batch_response(
459 client_id, exchange, app_state, llm_client, status_tx, depth,
460 )
461 .await
462 }
463 },
464 Dispatch::Deferred => {
465 let state = app_state.clone();
466 let llm = llm_client.clone();
467 let tx = status_tx.clone();
468 let handle = tokio::spawn(async move {
469 match notification {
470 Notification::Single(exchange) => {
471 Self::notify_response(client_id, exchange, &state, &llm, &tx, depth)
472 .await
473 }
474 Notification::Batch(exchange) => {
475 Self::notify_batch_response(
476 client_id, exchange, &state, &llm, &tx, depth,
477 )
478 .await
479 }
480 }
481 });
482 app_state.register_client_task(client_id, handle).await;
484 }
485 }
486 }
487
488 async fn command_loop(
492 mut command_rx: mpsc::Receiver<ClientCommand>,
493 client_id: ClientId,
494 app_state: Arc<AppState>,
495 llm_client: OllamaClient,
496 status_tx: mpsc::UnboundedSender<String>,
497 ) {
498 use crate::llm::actions::protocol_trait::Protocol;
499
500 let protocol = crate::client::jsonrpc::actions::JsonRpcClientProtocol::new();
501
502 while let Some(command) = command_rx.recv().await {
503 let action = command.action.clone();
504 let outcome = match protocol.execute_action(action.clone()) {
505 Err(e) => Ok(ClientSendOutcome::Rejected {
506 error: e.to_string(),
507 }),
508 Ok(result) => Self::apply_action(
509 result,
510 Dispatch::Deferred,
511 client_id,
512 &app_state,
513 &llm_client,
514 &status_tx,
515 0,
516 )
517 .await
518 .map(|applied| match applied {
519 Applied::Disconnect => ClientSendOutcome::Disconnected,
520 Applied::Ran(detail) => ClientSendOutcome::Executed { detail },
521 }),
522 };
523
524 let outcome_json = match &outcome {
525 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
526 Err(e) => serde_json::json!({"error": e.to_string()}),
527 };
528 app_state
529 .record_access_log(
530 AccessLogOwner::Client(client_id.as_u32()),
531 protocol.protocol_name(),
532 None,
533 "injected_action",
534 action,
535 vec![outcome_json],
536 )
537 .await;
538
539 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
540 if let Err(e) = &outcome {
541 error!(
542 "JSON-RPC client {} injected action failed: {}",
543 client_id, e
544 );
545 let _ = status_tx.send(format!(
546 "[WARN] Client {} injected action failed: {}",
547 client_id, e
548 ));
549 }
550 let _ = status_tx.send("__UPDATE_UI__".to_string());
551 crate::client::command_support::reply(command, outcome);
552
553 if disconnect {
554 break;
555 }
556 }
557
558 app_state.remove_client_handle(client_id).await;
560 let _ = status_tx.send("__UPDATE_UI__".to_string());
561 info!("JSON-RPC client {} command loop ended", client_id);
562 }
563
564 pub async fn make_request(
566 client_id: ClientId,
567 method: String,
568 params: Option<serde_json::Value>,
569 id: Option<serde_json::Value>,
570 app_state: Arc<AppState>,
571 llm_client: OllamaClient,
572 status_tx: mpsc::UnboundedSender<String>,
573 ) -> Result<()> {
574 let exchange = Self::perform_request(client_id, &method, params, id, &app_state).await?;
575 Self::notify_response(client_id, exchange, &app_state, &llm_client, &status_tx, 0).await;
576 Ok(())
577 }
578
579 async fn endpoint_and_headers(
591 client_id: ClientId,
592 app_state: &Arc<AppState>,
593 ) -> Result<(String, serde_json::Map<String, serde_json::Value>)> {
594 let (endpoint, defaults) = app_state
595 .with_client_mut(client_id, |client| {
596 (
597 client
598 .get_protocol_field("endpoint")
599 .and_then(|v| v.as_str())
600 .map(|s| s.to_string()),
601 client
602 .get_protocol_field("default_headers")
603 .and_then(|v| v.as_object().cloned()),
604 )
605 })
606 .await
607 .unwrap_or((None, None));
608 let endpoint = endpoint.context("No endpoint found")?;
609
610 let mut headers = serde_json::Map::new();
611 for (key, value) in defaults.unwrap_or_default() {
612 headers.insert(key.to_ascii_lowercase(), value);
613 }
614 headers.insert(
615 "content-type".to_string(),
616 serde_json::json!("application/json"),
617 );
618 Ok((endpoint, headers))
619 }
620
621 async fn perform_request(
622 client_id: ClientId,
623 method: &str,
624 params: Option<serde_json::Value>,
625 id: Option<serde_json::Value>,
626 app_state: &Arc<AppState>,
627 ) -> Result<(u16, Option<serde_json::Value>)> {
628 let (endpoint, headers) = Self::endpoint_and_headers(client_id, app_state).await?;
629
630 info!("JSON-RPC client {} calling method: {}", client_id, method);
631
632 let mut request = serde_json::json!({
634 "jsonrpc": "2.0",
635 "method": method,
636 });
637
638 if let Some(p) = params {
639 request["params"] = p;
640 }
641
642 if let Some(request_id) = id {
643 request["id"] = request_id.clone();
644 }
645
646 let http_client = Self::http_client(&endpoint).await?;
648
649 let mut http_request = http_client.post(&endpoint);
650 for (key, value) in &headers {
651 if let Some(val_str) = value.as_str() {
652 http_request = http_request.header(key, val_str);
653 }
654 }
655 let response = http_request.json(&request).send().await?;
656
657 let status_code = response.status().as_u16();
658 let body_text = response.text().await?;
659
660 info!(
661 "JSON-RPC client {} received response: {}",
662 client_id, status_code
663 );
664
665 match serde_json::from_str::<serde_json::Value>(&body_text) {
666 Ok(response_json) => Ok((status_code, Some(response_json))),
667 Err(_) => {
668 error!(
669 "JSON-RPC client {} received invalid JSON response",
670 client_id
671 );
672 Ok((status_code, None))
673 }
674 }
675 }
676
677 async fn run_follow_ups(
692 client_id: ClientId,
693 actions: Vec<serde_json::Value>,
694 app_state: &Arc<AppState>,
695 llm_client: &OllamaClient,
696 status_tx: &mpsc::UnboundedSender<String>,
697 depth: usize,
698 ) {
699 use crate::llm::actions::client_trait::Client;
700
701 if actions.is_empty() {
702 return;
703 }
704 if depth >= MAX_FOLLOWUP_DEPTH {
705 warn!(
706 "JSON-RPC client {} reached the follow-up depth bound ({}); {} action(s) from \
707 this response were not run",
708 client_id,
709 MAX_FOLLOWUP_DEPTH,
710 actions.len()
711 );
712 let _ = status_tx.send(format!(
713 "[WARN] Client {} stopped chaining JSON-RPC requests at depth {}",
714 client_id, MAX_FOLLOWUP_DEPTH
715 ));
716 return;
717 }
718
719 let protocol = crate::client::jsonrpc::actions::JsonRpcClientProtocol::new();
720 for action in actions {
721 let result = match protocol.execute_action(action) {
722 Ok(result) => result,
723 Err(e) => {
724 error!(
725 "JSON-RPC client {} rejected follow-up action: {}",
726 client_id, e
727 );
728 continue;
729 }
730 };
731 match Self::apply_action(
732 result,
733 Dispatch::Inline,
734 client_id,
735 app_state,
736 llm_client,
737 status_tx,
738 depth + 1,
739 )
740 .await
741 {
742 Ok(Applied::Ran(detail)) => info!("JSON-RPC client {}: {}", client_id, detail),
743 Ok(Applied::Disconnect) => break,
744 Err(e) => error!(
745 "JSON-RPC client {} follow-up action failed: {}",
746 client_id, e
747 ),
748 }
749 }
750 }
751
752 async fn notify_response(
754 client_id: ClientId,
755 exchange: (u16, Option<serde_json::Value>),
756 app_state: &Arc<AppState>,
757 llm_client: &OllamaClient,
758 status_tx: &mpsc::UnboundedSender<String>,
759 depth: usize,
760 ) {
761 let Some(response_json) = exchange.1 else {
762 return;
763 };
764 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
765 return;
766 };
767
768 let protocol = Arc::new(crate::client::jsonrpc::actions::JsonRpcClientProtocol::new());
769 let event = Event::new(&JSONRPC_CLIENT_RESPONSE_RECEIVED_EVENT, response_json);
770
771 let memory = app_state
772 .get_memory_for_client(client_id)
773 .await
774 .unwrap_or_default();
775
776 match call_llm_for_client(
777 llm_client,
778 app_state,
779 client_id.to_string(),
780 &instruction,
781 &memory,
782 Some(&event),
783 protocol.as_ref(),
784 status_tx,
785 )
786 .await
787 {
788 Ok(ClientLlmResult {
789 actions,
790 memory_updates,
791 }) => {
792 if let Some(mem) = memory_updates {
794 app_state.set_memory_for_client(client_id, mem).await;
795 }
796 Self::run_follow_ups(client_id, actions, app_state, llm_client, status_tx, depth)
797 .await;
798 }
799 Err(e) => {
800 error!("LLM error for JSON-RPC client {}: {}", client_id, e);
801 }
802 }
803 }
804
805 pub async fn make_batch_request(
807 client_id: ClientId,
808 requests: Vec<serde_json::Value>,
809 app_state: Arc<AppState>,
810 llm_client: OllamaClient,
811 status_tx: mpsc::UnboundedSender<String>,
812 ) -> Result<()> {
813 let exchange = Self::perform_batch_request(client_id, requests, &app_state).await?;
814 Self::notify_batch_response(client_id, exchange, &app_state, &llm_client, &status_tx, 0)
815 .await;
816 Ok(())
817 }
818
819 async fn perform_batch_request(
822 client_id: ClientId,
823 requests: Vec<serde_json::Value>,
824 app_state: &Arc<AppState>,
825 ) -> Result<(u16, Option<serde_json::Value>)> {
826 let (endpoint, headers) = Self::endpoint_and_headers(client_id, app_state).await?;
827
828 info!(
829 "JSON-RPC client {} sending batch with {} requests",
830 client_id,
831 requests.len()
832 );
833
834 let mut batch = Vec::new();
836 for req in requests {
837 let mut request = serde_json::json!({
838 "jsonrpc": "2.0",
839 });
840
841 if let Some(method) = req.get("method") {
843 request["method"] = method.clone();
844 }
845 if let Some(params) = req.get("params") {
846 request["params"] = params.clone();
847 }
848 if let Some(id) = req.get("id") {
849 request["id"] = id.clone();
850 }
851
852 batch.push(request);
853 }
854
855 let http_client = Self::http_client(&endpoint).await?;
857
858 let mut http_request = http_client.post(&endpoint);
859 for (key, value) in &headers {
860 if let Some(val_str) = value.as_str() {
861 http_request = http_request.header(key, val_str);
862 }
863 }
864 let response = http_request.json(&batch).send().await?;
865
866 let status_code = response.status().as_u16();
867 let body_text = response.text().await?;
868
869 info!(
870 "JSON-RPC client {} received batch response: {}",
871 client_id, status_code
872 );
873
874 match serde_json::from_str::<serde_json::Value>(&body_text) {
875 Ok(response_json) => Ok((status_code, Some(response_json))),
876 Err(_) => {
877 error!(
878 "JSON-RPC client {} received invalid JSON batch response",
879 client_id
880 );
881 Ok((status_code, None))
882 }
883 }
884 }
885
886 async fn notify_batch_response(
888 client_id: ClientId,
889 exchange: (u16, Option<serde_json::Value>),
890 app_state: &Arc<AppState>,
891 llm_client: &OllamaClient,
892 status_tx: &mpsc::UnboundedSender<String>,
893 depth: usize,
894 ) {
895 let Some(response_json) = exchange.1 else {
896 return;
897 };
898 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
899 return;
900 };
901
902 let protocol = Arc::new(crate::client::jsonrpc::actions::JsonRpcClientProtocol::new());
903 let event = Event::new(
904 &JSONRPC_CLIENT_RESPONSE_RECEIVED_EVENT,
905 serde_json::json!({
906 "batch": true,
907 "responses": response_json,
908 }),
909 );
910
911 let memory = app_state
912 .get_memory_for_client(client_id)
913 .await
914 .unwrap_or_default();
915
916 match call_llm_for_client(
917 llm_client,
918 app_state,
919 client_id.to_string(),
920 &instruction,
921 &memory,
922 Some(&event),
923 protocol.as_ref(),
924 status_tx,
925 )
926 .await
927 {
928 Ok(ClientLlmResult {
929 actions,
930 memory_updates,
931 }) => {
932 if let Some(mem) = memory_updates {
934 app_state.set_memory_for_client(client_id, mem).await;
935 }
936 Self::run_follow_ups(client_id, actions, app_state, llm_client, status_tx, depth)
937 .await;
938 }
939 Err(e) => {
940 error!("LLM error for JSON-RPC client {}: {}", client_id, e);
941 }
942 }
943 }
944}