1pub mod actions;
3
4pub use actions::Http2ClientProtocol;
5
6use anyhow::{Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use std::time::Duration;
10use tokio::sync::mpsc;
11use tracing::{error, info, warn};
12
13use crate::client::http2::actions::HTTP2_CLIENT_RESPONSE_RECEIVED_EVENT;
14use crate::client::http_fetch::FetchClient;
15use crate::client::llm_budget::call_llm_for_client;
16use crate::llm::actions::client_trait::{Client, ClientActionResult};
17use crate::llm::actions::protocol_trait::Protocol;
18use crate::llm::ollama_client::OllamaClient;
19use crate::llm::ClientLlmResult;
20use crate::logging::emit::Log;
21use crate::protocol::Event;
22use crate::state::app_state::AppState;
23use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
24use crate::state::{AccessLogOwner, ClientId, ClientStatus};
25
26const REMOVAL_CHECK_INTERVAL: Duration = Duration::from_secs(5);
30
31pub struct Http2Exchange {
37 pub status_code: u16,
38 pub status_text: String,
39 pub http_version: String,
40 pub headers: serde_json::Map<String, serde_json::Value>,
41 pub body: String,
42}
43
44enum Applied {
46 Executed(String),
48 Disconnect,
50}
51
52pub struct Http2Client;
54
55impl Http2Client {
56 #[cfg(not(target_arch = "wasm32"))]
80 async fn reqwest_client(url: &str) -> Result<reqwest::Client> {
81 use std::collections::HashMap;
82 use std::sync::{Mutex, OnceLock};
83
84 static CLIENTS: OnceLock<Mutex<HashMap<String, reqwest::Client>>> = OnceLock::new();
85 let clients = CLIENTS.get_or_init(|| Mutex::new(HashMap::new()));
86
87 let host = crate::llm::ollama_client::host_of(url).to_string();
88 if let Some(client) = clients.lock().ok().and_then(|map| map.get(&host).cloned()) {
89 return Ok(client);
90 }
91
92 let build_host = host.clone();
93 let built = tokio::task::spawn_blocking(move || {
94 let mut builder = reqwest::Client::builder()
95 .timeout(std::time::Duration::from_secs(30))
96 .redirect(crate::client::http_fetch::same_origin_redirects())
97 .http2_prior_knowledge();
98 if let Ok(ip) = build_host.parse::<std::net::IpAddr>() {
101 builder = builder.resolve(&build_host, std::net::SocketAddr::new(ip, 0));
103 }
104 builder.build().context("Failed to build HTTP/2 client")
105 })
106 .await
107 .context("HTTP/2 client build task panicked")??;
108
109 match clients.lock() {
110 Ok(mut map) => Ok(map.entry(host).or_insert(built).clone()),
111 Err(_) => Ok(built),
114 }
115 }
116 async fn http2_client(url: &str) -> Result<FetchClient> {
121 #[cfg(not(target_arch = "wasm32"))]
122 let client = FetchClient::from_reqwest(Self::reqwest_client(url).await?);
123 #[cfg(target_arch = "wasm32")]
124 let client = {
125 let _ = url;
126 FetchClient::transport(Duration::from_secs(30)).http2_prior_knowledge()
127 };
128 Ok(client)
129 }
130
131 pub async fn connect_with_llm_actions(
133 remote_addr: String,
134 llm_client: OllamaClient,
135 app_state: Arc<AppState>,
136 status_tx: mpsc::UnboundedSender<String>,
137 client_id: ClientId,
138 startup_params: Option<crate::protocol::StartupParams>,
139 ) -> Result<SocketAddr> {
140 info!(
144 "HTTP/2 client {} initialized for {}",
145 client_id, remote_addr
146 );
147
148 Self::http2_client(&remote_addr).await?;
152 crate::client::http_fetch::check_url(&if remote_addr.contains("://") {
156 remote_addr.clone()
157 } else {
158 format!("http://{remote_addr}")
159 })?;
160
161 let default_headers = match &startup_params {
164 Some(params) => params.get_optional_object("default_headers")?.cloned(),
165 None => None,
166 };
167
168 app_state
170 .with_client_mut(client_id, |client| {
171 client.set_protocol_field(
172 "http2_client".to_string(),
173 serde_json::json!("initialized"),
174 );
175 client.set_protocol_field("base_url".to_string(), serde_json::json!(remote_addr));
176 if let Some(headers) = &default_headers {
177 client.set_protocol_field(
178 "default_headers".to_string(),
179 serde_json::Value::Object(headers.clone()),
180 );
181 }
182 })
183 .await;
184
185 app_state
187 .update_client_status(client_id, ClientStatus::Connected)
188 .await;
189 let _ = status_tx.send(format!(
190 "[CLIENT] HTTP/2 client {} ready for {}",
191 client_id, remote_addr
192 ));
193 let _ = status_tx.send("__UPDATE_UI__".to_string());
194
195 let command_rx =
203 crate::client::command_support::register_command_channel(&app_state, client_id).await;
204 let cmd_state = app_state.clone();
205 let cmd_llm = llm_client.clone();
206 let cmd_tx = status_tx.clone();
207 let cmd_task = tokio::spawn(async move {
208 Self::command_loop(command_rx, client_id, cmd_state, cmd_llm, cmd_tx).await;
209 });
210 app_state.register_client_task(client_id, cmd_task).await;
211
212 let conn_base_url = remote_addr.clone();
224 let conn_state = app_state.clone();
225 let conn_llm = llm_client.clone();
226 let status_tx_c = status_tx.clone();
227 let conn_task = tokio::spawn(async move {
228 let Some(instruction) = conn_state.get_instruction_for_client(client_id).await else {
229 return;
230 };
231 let protocol = crate::client::http2::actions::Http2ClientProtocol::new();
232 let event = Event::new(
233 &crate::client::http2::actions::HTTP2_CLIENT_CONNECTED_EVENT,
234 serde_json::json!({ "base_url": conn_base_url.clone() }),
235 );
236 match crate::client::llm_budget::call_llm_for_client(
237 &conn_llm,
238 &conn_state,
239 client_id.to_string(),
240 &instruction,
241 "",
242 Some(&event),
243 &protocol,
244 &status_tx_c,
245 )
246 .await
247 {
248 Ok(result) => {
249 if let Some(mem) = result.memory_updates {
250 conn_state.set_memory_for_client(client_id, mem).await;
251 }
252 use crate::llm::actions::client_trait::{Client, ClientActionResult};
253 for action in result.actions {
254 let Ok(ClientActionResult::Custom { name, data }) =
255 protocol.execute_action(action.clone())
256 else {
257 continue;
258 };
259 if name != "http2_request" {
260 continue;
261 }
262 match Self::perform_request(
263 client_id,
264 data["method"].as_str().unwrap_or("GET").to_string(),
265 data["path"].as_str().unwrap_or("/").to_string(),
266 data["headers"].as_object().cloned(),
267 data["body"].as_str().map(|s| s.to_string()),
268 &conn_state,
269 &status_tx_c,
270 )
271 .await
272 {
273 Ok(exchange) => {
278 Self::notify_response(
279 client_id,
280 exchange,
281 conn_state.clone(),
282 conn_llm.clone(),
283 status_tx_c.clone(),
284 0,
285 )
286 .await;
287 }
288 Err(e) => error!(
289 "HTTP/2 client {} connect-time request failed: {}",
290 client_id, e
291 ),
292 }
293 }
294 }
295 Err(e) => error!(
296 "HTTP/2 client {} LLM error on connected event: {}",
297 client_id, e
298 ),
299 }
300 });
301 app_state.register_client_task(client_id, conn_task).await;
302
303 Ok("0.0.0.0:0".parse().unwrap())
305 }
306
307 async fn command_loop(
315 mut command_rx: mpsc::Receiver<ClientCommand>,
316 client_id: ClientId,
317 app_state: Arc<AppState>,
318 llm_client: OllamaClient,
319 status_tx: mpsc::UnboundedSender<String>,
320 ) {
321 let protocol = Http2ClientProtocol::new();
322 let mut removal_check = tokio::time::interval(REMOVAL_CHECK_INTERVAL);
323 removal_check.tick().await; loop {
326 tokio::select! {
327 received = command_rx.recv() => {
328 let Some(command) = received else { break };
329 if Self::handle_command(
330 &protocol,
331 command,
332 client_id,
333 &app_state,
334 &llm_client,
335 &status_tx,
336 )
337 .await
338 {
339 break;
340 }
341 }
342 _ = removal_check.tick() => {
343 if app_state.get_client(client_id).await.is_none() {
344 info!("HTTP/2 client {} stopped", client_id);
345 break;
346 }
347 }
348 }
349 }
350
351 app_state.remove_client_handle(client_id).await;
352 let _ = status_tx.send("__UPDATE_UI__".to_string());
353 }
354
355 async fn handle_command(
358 protocol: &Http2ClientProtocol,
359 command: ClientCommand,
360 client_id: ClientId,
361 app_state: &Arc<AppState>,
362 llm_client: &OllamaClient,
363 status_tx: &mpsc::UnboundedSender<String>,
364 ) -> bool {
365 let action = command.action.clone();
366 let outcome = match protocol.execute_action(action.clone()) {
367 Err(e) => Ok(ClientSendOutcome::Rejected {
368 error: e.to_string(),
369 }),
370 Ok(action_result) => {
371 match Self::apply_action(action_result, client_id, app_state, llm_client, status_tx)
372 .await
373 {
374 Ok(Applied::Executed(detail)) => Ok(ClientSendOutcome::Executed { detail }),
378 Ok(Applied::Disconnect) => Ok(ClientSendOutcome::Disconnected),
379 Err(e) => Err(e),
380 }
381 }
382 };
383
384 let outcome_json = match &outcome {
385 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
386 Err(e) => serde_json::json!({"error": e.to_string()}),
387 };
388 app_state
389 .record_access_log(
390 AccessLogOwner::Client(client_id.as_u32()),
391 protocol.protocol_name(),
392 None,
393 "injected_action",
394 action,
395 vec![outcome_json],
396 )
397 .await;
398
399 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
400 if let Err(e) = &outcome {
401 error!("HTTP/2 client {} injected action failed: {}", client_id, e);
402 let _ = status_tx.send(format!(
403 "[WARN] Client {} injected action failed: {}",
404 client_id, e
405 ));
406 }
407 let _ = status_tx.send("__UPDATE_UI__".to_string());
408 crate::client::command_support::reply(command, outcome);
409
410 if disconnect {
411 app_state
412 .update_client_status(client_id, ClientStatus::Disconnected)
413 .await;
414 }
415 disconnect
416 }
417
418 async fn apply_action(
424 action_result: ClientActionResult,
425 client_id: ClientId,
426 app_state: &Arc<AppState>,
427 llm_client: &OllamaClient,
428 status_tx: &mpsc::UnboundedSender<String>,
429 ) -> Result<Applied> {
430 match action_result {
431 ClientActionResult::Custom { name, data } if name == "http2_request" => {
432 let method = data["method"].as_str().unwrap_or("GET").to_string();
433 let path = data["path"].as_str().unwrap_or("/").to_string();
434 let headers = data["headers"].as_object().cloned();
435 let body = data["body"].as_str().map(|s| s.to_string());
436
437 let exchange = Self::perform_request(
438 client_id,
439 method.clone(),
440 path.clone(),
441 headers,
442 body,
443 app_state,
444 status_tx,
445 )
446 .await?;
447 let detail = format!(
448 "http2_request {} {} -> {} ({} byte body)",
449 method,
450 path,
451 exchange.status_code,
452 exchange.body.len()
453 );
454
455 let llm_clone = llm_client.clone();
456 let state_clone = app_state.clone();
457 let status_clone = status_tx.clone();
458 let notify_handle = tokio::spawn(async move {
459 Http2Client::notify_response(
460 client_id,
461 exchange,
462 state_clone,
463 llm_clone,
464 status_clone,
465 0,
466 )
467 .await;
468 });
469 app_state
470 .register_client_task(client_id, notify_handle)
471 .await;
472 Ok(Applied::Executed(detail))
473 }
474 ClientActionResult::Disconnect => Ok(Applied::Disconnect),
475 ClientActionResult::WaitForMore => Ok(Applied::Executed("wait_for_more".to_string())),
476 ClientActionResult::NoAction => Ok(Applied::Executed("no_action".to_string())),
477 ClientActionResult::Custom { name, .. } => Ok(Applied::Executed(format!(
480 "custom result '{name}' is not handled by the HTTP/2 client"
481 ))),
482 ClientActionResult::SendData(_) => Ok(Applied::Executed(
483 "send_data has no meaning for a request/response HTTP/2 client".to_string(),
484 )),
485 ClientActionResult::Multiple(_) => Ok(Applied::Executed(
486 "multiple results are not produced by the HTTP/2 client".to_string(),
487 )),
488 }
489 }
490
491 #[allow(clippy::too_many_arguments)]
493 pub async fn make_request(
494 client_id: ClientId,
495 method: String,
496 path: String,
497 headers: Option<serde_json::Map<String, serde_json::Value>>,
498 body: Option<String>,
499 app_state: Arc<AppState>,
500 llm_client: OllamaClient,
501 status_tx: mpsc::UnboundedSender<String>,
502 ) -> Result<()> {
503 let exchange = Self::perform_request(
504 client_id, method, path, headers, body, &app_state, &status_tx,
505 )
506 .await?;
507 Self::notify_response(client_id, exchange, app_state, llm_client, status_tx, 0).await;
508 Ok(())
509 }
510
511 #[allow(clippy::too_many_arguments)]
514 pub async fn perform_request(
515 client_id: ClientId,
516 method: String,
517 path: String,
518 headers: Option<serde_json::Map<String, serde_json::Value>>,
519 body: Option<String>,
520 app_state: &AppState,
521 status_tx: &mpsc::UnboundedSender<String>,
522 ) -> Result<Http2Exchange> {
523 let (base_url, default_headers) = app_state
525 .with_client_mut(client_id, |client| {
526 (
527 client
528 .get_protocol_field("base_url")
529 .and_then(|v| v.as_str())
530 .map(|s| s.to_string()),
531 client
532 .get_protocol_field("default_headers")
533 .and_then(|v| v.as_object().cloned()),
534 )
535 })
536 .await
537 .unwrap_or((None, None));
538 let base_url = base_url.context("No base URL found")?;
539
540 let url = crate::client::http_fetch::resolve_same_origin(&base_url, &path)?;
545
546 info!(
547 "HTTP/2 client {} making request: {} {}",
548 client_id, method, url
549 );
550
551 let http_client = Self::http2_client(&url).await?;
552
553 let mut request = match method.to_uppercase().as_str() {
554 "GET" => http_client.get(&url),
555 "POST" => http_client.post(&url),
556 "PUT" => http_client.put(&url),
557 "DELETE" => http_client.delete(&url),
558 "HEAD" => http_client.head(&url),
559 "PATCH" => http_client.patch(&url),
560 _ => return Err(anyhow::anyhow!("Unsupported HTTP method: {}", method)),
561 };
562
563 let mut merged = serde_json::Map::new();
568 for (key, value) in default_headers.unwrap_or_default() {
569 merged.insert(key.to_ascii_lowercase(), value);
570 }
571 if let Some(hdrs) = headers {
572 for (key, value) in hdrs {
573 merged.insert(key.to_ascii_lowercase(), value);
574 }
575 }
576 for (key, value) in merged {
577 if let Some(val_str) = value.as_str() {
578 request = request.header(&key, val_str);
579 }
580 }
581
582 if let Some(body_str) = body {
584 request = request.body(body_str);
585 }
586
587 match request.send().await {
589 Ok(response) => {
590 let status = response.status();
591 let status_code = status.as_u16();
592 let version = response.version();
593
594 let mut resp_headers = serde_json::Map::new();
596 for (name, value) in response.headers() {
597 if let Ok(val_str) = value.to_str() {
598 resp_headers.insert(name.to_string(), serde_json::json!(val_str));
599 }
600 }
601
602 let body_text = response.text().await?;
604
605 info!(
606 "HTTP/2 client {} received response: {} ({}) version: {:?}",
607 client_id, status_code, status, version
608 );
609
610 Ok(Http2Exchange {
611 status_code,
612 status_text: status.to_string(),
613 http_version: format!("{:?}", version),
614 headers: resp_headers,
615 body: body_text,
616 })
617 }
618 Err(e) => {
619 Log::new(Some(status_tx))
620 .error(format!("HTTP/2 client {} request failed: {}", client_id, e));
621 Err(e)
622 }
623 }
624 }
625
626 const MAX_FOLLOWUP_DEPTH: u8 = 4;
630
631 fn notify_response(
636 client_id: ClientId,
637 exchange: Http2Exchange,
638 app_state: Arc<AppState>,
639 llm_client: OllamaClient,
640 status_tx: mpsc::UnboundedSender<String>,
641 depth: u8,
642 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> {
643 Box::pin(async move {
644 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
645 return;
646 };
647
648 let protocol = Arc::new(crate::client::http2::actions::Http2ClientProtocol::new());
649 let event = Event::new(
650 &HTTP2_CLIENT_RESPONSE_RECEIVED_EVENT,
651 serde_json::json!({
652 "status_code": exchange.status_code,
653 "status_text": exchange.status_text,
654 "http_version": exchange.http_version,
655 "headers": exchange.headers,
656 "body": exchange.body,
657 }),
658 );
659
660 let memory = app_state
661 .get_memory_for_client(client_id)
662 .await
663 .unwrap_or_default();
664
665 match call_llm_for_client(
666 &llm_client,
667 &app_state,
668 client_id.to_string(),
669 &instruction,
670 &memory,
671 Some(&event),
672 protocol.as_ref(),
673 &status_tx,
674 )
675 .await
676 {
677 Ok(ClientLlmResult {
678 actions,
679 memory_updates,
680 }) => {
681 if let Some(mem) = memory_updates {
683 app_state.set_memory_for_client(client_id, mem).await;
684 }
685
686 use crate::llm::actions::client_trait::{Client, ClientActionResult};
691 for action in actions {
692 let Ok(ClientActionResult::Custom { name, data }) =
693 protocol.execute_action(action.clone())
694 else {
695 continue;
696 };
697 if name != "http2_request" {
698 continue;
699 }
700 match Self::perform_request(
701 client_id,
702 data["method"].as_str().unwrap_or("GET").to_string(),
703 data["path"].as_str().unwrap_or("/").to_string(),
704 data["headers"].as_object().cloned(),
705 data["body"].as_str().map(|s| s.to_string()),
706 &app_state,
707 &status_tx,
708 )
709 .await
710 {
711 Ok(exchange) => {
712 if depth + 1 < Self::MAX_FOLLOWUP_DEPTH {
713 Self::notify_response(
714 client_id,
715 exchange,
716 app_state.clone(),
717 llm_client.clone(),
718 status_tx.clone(),
719 depth + 1,
720 )
721 .await;
722 } else {
723 warn!(
724 "HTTP/2 client {} reached the follow-up depth limit \
725 ({}); not reporting the response to the model",
726 client_id,
727 Self::MAX_FOLLOWUP_DEPTH
728 );
729 }
730 }
731 Err(e) => error!(
732 "HTTP/2 client {} follow-up request failed: {}",
733 client_id, e
734 ),
735 }
736 }
737 }
738 Err(e) => {
739 error!("LLM error for HTTP/2 client {}: {}", client_id, e);
740 }
741 }
742 })
743 }
744}