1use std::collections::{BTreeMap, VecDeque};
9#[cfg(feature = "adapter-api")]
10use std::sync::atomic::AtomicBool;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::Arc;
13#[cfg(feature = "adapter-api")]
14use std::sync::Weak;
15
16use async_trait::async_trait;
17#[cfg(feature = "adapter-api")]
18use futures::StreamExt;
19use serde::{Deserialize, Serialize};
20#[cfg(feature = "adapter-api")]
21use serde_json::json;
22use serde_json::Value;
23use tokio::sync::broadcast;
24
25#[cfg(feature = "adapter-api")]
26use crate::sdk::RuntimeSubmitError;
27pub use crate::sdk::SdkError as FrontendRuntimeError;
28pub use crate::sdk::SdkEvent as FrontendEvent;
29pub use crate::sdk::SdkRuntime as FrontendRuntime;
30use crate::server::RpcEngine;
31use crate::ChatMessage;
32
33pub const FRONTEND_RUNTIME_SCHEMA_VERSION: u32 = 2;
35
36pub(crate) const FRONTEND_EVENT_SCHEMA_VERSION: u32 = 1;
41
42pub const FRONTEND_REPLAY_CAPACITY: usize = 4096;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum FrontendTurnState {
49 Idle,
51 Busy,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum FrontendConnectionState {
59 Connected,
61 ShuttingDown,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct FrontendActions {
68 pub submit: bool,
70 pub interrupt: bool,
72 pub steer: bool,
74 pub respond: bool,
76 pub detach: bool,
78 pub close: bool,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct FrontendDisplayCapabilities {
85 pub event_kinds: Vec<String>,
87 pub opaque_fallback: bool,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93pub struct FrontendCommandDescriptor {
94 pub name: String,
96 pub description: Option<String>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub argument_hint: Option<String>,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub enum FrontendOperationKind {
112 Prompt,
114 File,
116 Model,
118 Session,
120 Subagent,
122 Image,
124 Reduction,
126 Context,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137pub struct FrontendOperationDescriptor {
138 pub id: String,
140 pub kind: FrontendOperationKind,
142 pub command: Option<FrontendCommandDescriptor>,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(tag = "kind", rename_all = "snake_case")]
149pub enum FrontendOperationInvocation {
150 Prompt {
152 operation_id: String,
154 arguments: String,
156 },
157 Context {
160 operation_id: String,
162 },
163 Model {
169 operation_id: String,
171 model: String,
175 },
176}
177
178impl FrontendOperationInvocation {
179 pub fn operation_id(&self) -> &str {
181 match self {
182 Self::Prompt { operation_id, .. }
183 | Self::Context { operation_id }
184 | Self::Model { operation_id, .. } => operation_id,
185 }
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191#[serde(tag = "kind", rename_all = "snake_case")]
192pub enum FrontendOperationResult {
193 Prompt {
195 reply: String,
197 },
198 Context {
200 usage: crate::ContextUsage,
202 },
203 Model {
205 model: String,
207 previous: String,
210 },
211}
212
213#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
215pub struct FrontendRuntimeMetadata {
216 pub source_harness: Option<String>,
218 pub emulation_profile: Option<String>,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224pub struct FrontendRuntimeDescriptor {
225 pub schema_version: u32,
227 pub session_id: String,
229 pub source_harness: Option<String>,
231 pub emulation_profile: Option<String>,
233 pub active_modules: Vec<String>,
235 pub commands: Vec<FrontendCommandDescriptor>,
237 #[serde(default)]
239 pub operations: Vec<FrontendOperationDescriptor>,
240 pub actions: FrontendActions,
242 pub display: FrontendDisplayCapabilities,
244 pub model: String,
246 pub turn_state: FrontendTurnState,
248 pub connection_state: FrontendConnectionState,
250 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
252 pub extensions: BTreeMap<String, Value>,
253}
254
255#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
258pub struct FrontendAttachSnapshot {
259 pub descriptor: FrontendRuntimeDescriptor,
261 pub history: Vec<ChatMessage>,
263 pub history_cursor: u64,
265 pub replay: VecDeque<FrontendEvent>,
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
271#[serde(rename_all = "snake_case")]
272pub enum FrontendRequestKind {
273 Approval,
275 Elicitation,
277 #[serde(other)]
280 Other,
281}
282
283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
285pub struct FrontendRequest {
286 pub id: u64,
288 pub kind: FrontendRequestKind,
290 pub payload: Value,
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
296#[serde(rename_all = "snake_case")]
297pub enum FrontendApprovalDecision {
298 Deny,
300 Allow,
302 AllowForSession,
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
308#[serde(rename_all = "snake_case")]
309pub enum FrontendElicitationAction {
310 Accept,
312 Decline,
314 Cancel,
316}
317
318#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
320#[serde(tag = "kind", rename_all = "snake_case")]
321pub enum FrontendResponse {
322 Approval {
324 request_id: u64,
326 decision: FrontendApprovalDecision,
328 },
329 Elicitation {
331 request_id: u64,
333 action: FrontendElicitationAction,
335 content: Option<Value>,
337 },
338 Other {
340 request_id: u64,
342 action: FrontendElicitationAction,
344 content: Option<Value>,
346 },
347}
348
349impl FrontendResponse {
350 pub(crate) fn request_id(&self) -> u64 {
351 match self {
352 Self::Approval { request_id, .. }
353 | Self::Elicitation { request_id, .. }
354 | Self::Other { request_id, .. } => *request_id,
355 }
356 }
357}
358
359pub struct FrontendAttachment {
361 pub descriptor: FrontendRuntimeDescriptor,
363 pub history: Vec<ChatMessage>,
365 pub history_cursor: u64,
367 pub(crate) replay: VecDeque<FrontendEvent>,
368 live: broadcast::Receiver<FrontendEvent>,
369 delivered: u64,
370 acknowledged: Option<Arc<AtomicU64>>,
371 _transport_lease: Option<Arc<()>>,
372}
373
374impl FrontendAttachment {
375 pub fn from_snapshot(
379 snapshot: FrontendAttachSnapshot,
380 live: broadcast::Receiver<FrontendEvent>,
381 ) -> Self {
382 Self::from_snapshot_after(snapshot, live, 0)
383 }
384
385 pub fn from_snapshot_after(
390 snapshot: FrontendAttachSnapshot,
391 live: broadcast::Receiver<FrontendEvent>,
392 acknowledged_sequence: u64,
393 ) -> Self {
394 let delivered = snapshot.history_cursor.max(acknowledged_sequence);
395 Self::new_with_delivered(
396 snapshot.descriptor,
397 snapshot.history,
398 snapshot.history_cursor,
399 snapshot.replay,
400 live,
401 None,
402 delivered,
403 )
404 }
405
406 pub(crate) fn new(
407 descriptor: FrontendRuntimeDescriptor,
408 history: Vec<ChatMessage>,
409 history_cursor: u64,
410 replay: VecDeque<FrontendEvent>,
411 live: broadcast::Receiver<FrontendEvent>,
412 transport_lease: Option<Arc<()>>,
413 ) -> Self {
414 let delivered = history_cursor;
415 Self::new_with_delivered(
416 descriptor,
417 history,
418 history_cursor,
419 replay,
420 live,
421 transport_lease,
422 delivered,
423 )
424 }
425
426 fn new_with_delivered(
427 descriptor: FrontendRuntimeDescriptor,
428 history: Vec<ChatMessage>,
429 history_cursor: u64,
430 replay: VecDeque<FrontendEvent>,
431 live: broadcast::Receiver<FrontendEvent>,
432 transport_lease: Option<Arc<()>>,
433 delivered: u64,
434 ) -> Self {
435 Self {
436 descriptor,
437 history,
438 history_cursor,
439 replay,
440 live,
441 delivered,
442 acknowledged: None,
443 _transport_lease: transport_lease,
444 }
445 }
446
447 #[cfg(feature = "adapter-acp")]
448 pub(crate) fn with_acknowledgement(mut self, acknowledged: Arc<AtomicU64>) -> Self {
449 acknowledged.fetch_max(self.history_cursor, Ordering::SeqCst);
450 self.acknowledged = Some(acknowledged);
451 self
452 }
453
454 fn acknowledge(&self, event: &FrontendEvent) {
455 if !event_advances_acknowledgement(event) {
456 return;
457 }
458 if let Some(acknowledged) = &self.acknowledged {
459 acknowledged.fetch_max(event.sequence, Ordering::SeqCst);
460 }
461 }
462
463 pub async fn next_event(&mut self) -> Result<FrontendEvent, FrontendRuntimeError> {
467 loop {
468 let event = match self.next_replay_event() {
469 Some(event) => return Ok(event),
470 None => match self.live.recv().await {
471 Ok(event) => event,
472 Err(broadcast::error::RecvError::Lagged(count)) => {
473 return Err(FrontendRuntimeError::ReplayGap(count));
474 }
475 Err(broadcast::error::RecvError::Closed) => {
476 return Err(FrontendRuntimeError::Closed);
477 }
478 },
479 };
480 if event.sequence <= self.delivered {
481 continue;
482 }
483 self.delivered = event.sequence;
484 self.acknowledge(&event);
485 return Ok(event);
486 }
487 }
488
489 pub fn next_replay_event(&mut self) -> Option<FrontendEvent> {
494 while let Some(event) = self.replay.pop_front() {
495 if event.sequence <= self.delivered {
496 continue;
497 }
498 self.delivered = event.sequence;
499 self.acknowledge(&event);
500 return Some(event);
501 }
502 None
503 }
504}
505
506pub(crate) fn event_advances_acknowledgement(event: &FrontendEvent) -> bool {
507 event
508 .payload
509 .pointer("/_meta/supercode/transient")
510 .and_then(Value::as_bool)
511 != Some(true)
512}
513
514pub(crate) struct FrontendProjectionState {
516 pub(crate) history: Vec<ChatMessage>,
517 pub(crate) history_cursor: u64,
518 pub(crate) next_sequence: u64,
519 pub(crate) replay: VecDeque<FrontendEvent>,
520}
521
522#[async_trait]
523impl FrontendRuntime for RpcEngine {
524 async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
525 Ok(self.frontend_descriptor())
526 }
527
528 async fn attach(
529 &self,
530 history_limit: usize,
531 ) -> Result<FrontendAttachment, FrontendRuntimeError> {
532 self.frontend_attach(history_limit)
533 }
534
535 async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), FrontendRuntimeError> {
536 RpcEngine::send_input(&self, prompt)?;
537 Ok(())
538 }
539
540 async fn send_input_with_images(
541 self: Arc<Self>,
542 prompt: String,
543 image_urls: Vec<String>,
544 ) -> Result<(), FrontendRuntimeError> {
545 RpcEngine::send_input_with_images(&self, prompt, image_urls)?;
546 Ok(())
547 }
548
549 async fn submit(&self, prompt: String) -> Result<String, FrontendRuntimeError> {
550 Ok(RpcEngine::submit(self, prompt).await?)
551 }
552
553 async fn submit_with_images(
554 &self,
555 prompt: String,
556 image_urls: Vec<String>,
557 ) -> Result<String, FrontendRuntimeError> {
558 Ok(RpcEngine::submit_with_images(self, prompt, image_urls).await?)
559 }
560
561 async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
562 Ok(RpcEngine::interrupt(self).await)
563 }
564
565 async fn steer(&self, prompt: String) -> Result<(), FrontendRuntimeError> {
566 RpcEngine::steer(self, prompt)
567 }
568
569 async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
570 RpcEngine::respond(self, response)
571 }
572
573 async fn invoke(
574 &self,
575 operation: FrontendOperationInvocation,
576 ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
577 RpcEngine::invoke(self, operation).await
578 }
579
580 async fn close(&self) -> Result<(), FrontendRuntimeError> {
581 RpcEngine::shutdown(self).await;
582 Ok(())
583 }
584}
585
586#[cfg(feature = "adapter-api")]
591pub struct HttpFrontendRuntime {
592 base_url: String,
593 token: String,
594 client_id: crate::RuntimeClientId,
595 authorization: crate::RuntimeAuthorization,
596 client: reqwest::Client,
597 events: broadcast::Sender<FrontendEvent>,
598 next_id: AtomicU64,
599 lifecycle: Arc<()>,
600 disconnected: AtomicBool,
601}
602
603#[cfg(feature = "adapter-api")]
604impl HttpFrontendRuntime {
605 pub async fn connect(
608 base_url: impl Into<String>,
609 token: impl Into<String>,
610 ) -> Result<Arc<Self>, FrontendRuntimeError> {
611 let mut random = [0_u8; 16];
612 getrandom::getrandom(&mut random).map_err(|error| {
613 FrontendRuntimeError::Transport(format!(
614 "cannot generate runtime client identity: {error}"
615 ))
616 })?;
617 let suffix = random
618 .iter()
619 .map(|byte| format!("{byte:02x}"))
620 .collect::<String>();
621 let client_id = crate::RuntimeClientId::parse(format!("http-{suffix}"))
622 .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
623 Self::connect_with_client_id(base_url, token, client_id).await
624 }
625
626 pub async fn connect_with_client_id(
629 base_url: impl Into<String>,
630 token: impl Into<String>,
631 client_id: crate::RuntimeClientId,
632 ) -> Result<Arc<Self>, FrontendRuntimeError> {
633 Self::connect_with_authorization(
634 base_url,
635 token,
636 client_id,
637 crate::RuntimeAuthorization::owner(),
638 )
639 .await
640 }
641
642 pub async fn connect_with_authorization(
646 base_url: impl Into<String>,
647 token: impl Into<String>,
648 client_id: crate::RuntimeClientId,
649 authorization: crate::RuntimeAuthorization,
650 ) -> Result<Arc<Self>, FrontendRuntimeError> {
651 Self::connect_inner(base_url, token, client_id, authorization, true)
652 .await
653 .map(|(runtime, _)| runtime)
654 }
655
656 pub(crate) async fn probe_described(
663 base_url: impl Into<String>,
664 token: impl Into<String>,
665 client_id: crate::RuntimeClientId,
666 ) -> Result<(Arc<Self>, FrontendRuntimeDescriptor), FrontendRuntimeError> {
667 Self::connect_inner(
668 base_url,
669 token,
670 client_id,
671 crate::RuntimeAuthorization::observer(),
672 false,
673 )
674 .await
675 }
676
677 async fn connect_inner(
678 base_url: impl Into<String>,
679 token: impl Into<String>,
680 client_id: crate::RuntimeClientId,
681 authorization: crate::RuntimeAuthorization,
682 stream_events: bool,
683 ) -> Result<(Arc<Self>, FrontendRuntimeDescriptor), FrontendRuntimeError> {
684 let runtime = Arc::new(Self {
685 base_url: base_url.into().trim_end_matches('/').to_string(),
686 token: token.into(),
687 client_id,
688 authorization,
689 client: reqwest::Client::new(),
690 events: broadcast::channel(1024).0,
691 next_id: AtomicU64::new(1),
692 lifecycle: Arc::new(()),
693 disconnected: AtomicBool::new(false),
694 });
695 let descriptor: FrontendRuntimeDescriptor = runtime
697 .rpc_typed(crate::FrontendFacadeMethod::Describe.wire_name(), json!({}))
698 .await?;
699 if stream_events {
700 Self::start_event_stream(&runtime).await?;
701 }
702 Ok((runtime, descriptor))
703 }
704
705 async fn start_event_stream(runtime: &Arc<Self>) -> Result<(), FrontendRuntimeError> {
706 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
707 let weak = Arc::downgrade(runtime);
708 let lifecycle = Arc::downgrade(&runtime.lifecycle);
709 tokio::spawn(async move {
710 Self::run_event_stream(weak, lifecycle, ready_tx).await;
711 });
712 ready_rx.await.map_err(|_| {
713 FrontendRuntimeError::Transport("frontend event stream exited before startup".into())
714 })?
715 }
716
717 async fn run_event_stream(
718 weak: Weak<Self>,
719 lifecycle: Weak<()>,
720 ready: tokio::sync::oneshot::Sender<Result<(), FrontendRuntimeError>>,
721 ) {
722 let Some(runtime) = weak.upgrade() else {
723 let _ = ready.send(Err(FrontendRuntimeError::Closed));
724 return;
725 };
726 let request = runtime
727 .client
728 .get(format!("{}/frontend/events", runtime.base_url))
729 .bearer_auth(&runtime.token)
730 .header("x-supercode-client-id", runtime.client_id.as_str())
731 .header(
732 "x-supercode-permissions",
733 runtime.authorization.header_value(),
734 );
735 let events = runtime.events.clone();
736 drop(runtime);
737 let response = request.send().await;
738 let response = match response {
739 Ok(response) if response.status().is_success() => response,
740 Ok(response) => {
741 let _ = ready.send(Err(FrontendRuntimeError::Transport(format!(
742 "frontend event stream returned {}",
743 response.status()
744 ))));
745 return;
746 }
747 Err(error) => {
748 let _ = ready.send(Err(FrontendRuntimeError::Transport(error.to_string())));
749 return;
750 }
751 };
752 let _ = ready.send(Ok(()));
753 let mut stream = response.bytes_stream();
754 let mut pending = Vec::<u8>::new();
755 let mut liveness = tokio::time::interval(std::time::Duration::from_millis(100));
756 loop {
757 let chunk = tokio::select! {
758 _ = liveness.tick() => {
759 if lifecycle.strong_count() == 0 {
760 break;
761 }
762 if weak
763 .upgrade()
764 .is_some_and(|runtime| runtime.disconnected.load(Ordering::SeqCst))
765 {
766 break;
767 }
768 continue;
769 }
770 chunk = stream.next() => chunk,
771 };
772 let Some(chunk) = chunk else {
773 break;
774 };
775 let Ok(chunk) = chunk else {
776 break;
777 };
778 pending.extend_from_slice(&chunk);
779 while let Some(position) = pending.iter().position(|byte| *byte == b'\n') {
780 let line = pending.drain(..=position).collect::<Vec<_>>();
781 let line = String::from_utf8_lossy(&line);
782 let Some(data) = line.trim_end().strip_prefix("data: ") else {
783 continue;
784 };
785 if let Ok(event) = serde_json::from_str::<FrontendEvent>(data) {
786 let _ = events.send(event);
787 }
788 }
789 }
790 if let Some(runtime) = weak.upgrade() {
791 runtime.disconnected.store(true, Ordering::SeqCst);
792 let _ = runtime.events.send(FrontendEvent::new(
793 u64::MAX,
794 json!({
795 "type": "runtime_disconnected",
796 "schema_version": FRONTEND_EVENT_SCHEMA_VERSION
797 }),
798 ));
799 }
800 }
801
802 async fn rpc(&self, method: &str, params: Value) -> Result<Value, FrontendRuntimeError> {
803 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
804 let requested_operation = params
805 .pointer("/operation/operation_id")
806 .and_then(Value::as_str)
807 .map(str::to_owned);
808 let response = self
809 .client
810 .post(format!("{}/rpc", self.base_url))
811 .bearer_auth(&self.token)
812 .header("x-supercode-client-id", self.client_id.as_str())
813 .header("x-supercode-permissions", self.authorization.header_value())
814 .json(&json!({"id": id, "method": method, "params": params}))
815 .send()
816 .await
817 .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
818 if !response.status().is_success() {
819 return Err(FrontendRuntimeError::Transport(format!(
820 "SDK HTTP RPC returned {}",
821 response.status()
822 )));
823 }
824 let value: Value = response
825 .json()
826 .await
827 .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
828 if let Some(error) = value.get("error") {
829 let code = error.get("code").and_then(Value::as_i64);
830 let name = error.get("name").and_then(Value::as_str);
831 let operation = error
832 .get("operation")
833 .and_then(Value::as_str)
834 .and_then(crate::SdkOperation::from_action_name);
835 let message = error
836 .get("message")
837 .and_then(Value::as_str)
838 .unwrap_or("SDK runtime request failed")
839 .to_string();
840 return Err(match (name, code) {
841 (Some("unauthenticated"), _) | (_, Some(-32030)) => {
842 FrontendRuntimeError::Unauthenticated
843 }
844 (Some("unauthorized"), _) | (_, Some(-32031)) => {
845 FrontendRuntimeError::Unauthorized {
846 permission: error
847 .get("permission")
848 .and_then(Value::as_str)
849 .unwrap_or("unknown")
850 .to_string(),
851 }
852 }
853 (Some("controller_required"), _) | (_, Some(-32032)) => {
854 FrontendRuntimeError::ControllerRequired {
855 holder: error
856 .get("holder")
857 .and_then(Value::as_str)
858 .map(str::to_owned),
859 expires_at_ms: error.get("expiresAtMs").and_then(Value::as_u64),
860 }
861 }
862 (Some("lease_expired"), _) | (_, Some(-32033)) => {
863 FrontendRuntimeError::LeaseExpired
864 }
865 (_, Some(-32023)) => FrontendRuntimeError::UnsupportedOperation(
866 requested_operation.unwrap_or(message),
867 ),
868 (Some("unsupported_action"), _) => FrontendRuntimeError::UnsupportedAction(
869 operation
870 .unwrap_or_else(|| {
871 crate::SdkOperation::from_action_name(method)
872 .unwrap_or(crate::SdkOperation::Respond)
873 })
874 .action_name(),
875 ),
876 (Some("not_found"), Some(-32021)) => {
877 let request_id = params
878 .pointer("/response/request_id")
879 .and_then(Value::as_u64)
880 .unwrap_or_default();
881 FrontendRuntimeError::UnknownRequest(request_id)
882 }
883 (Some("invalid_argument"), _) => FrontendRuntimeError::InvalidResponse(message),
884 (_, Some(-32000)) => RuntimeSubmitError::Busy.into(),
885 (_, Some(-32001)) => RuntimeSubmitError::Interrupted.into(),
886 (_, Some(-32002)) => RuntimeSubmitError::Agent(message).into(),
887 (_, Some(-32020)) => FrontendRuntimeError::UnsupportedAction(
888 crate::SdkOperation::from_action_name(method)
889 .unwrap_or(crate::SdkOperation::Respond)
890 .action_name(),
891 ),
892 (_, Some(-32021)) => {
893 let request_id = params
894 .pointer("/response/request_id")
895 .and_then(Value::as_u64)
896 .unwrap_or_default();
897 FrontendRuntimeError::UnknownRequest(request_id)
898 }
899 (_, Some(-32022)) => FrontendRuntimeError::InvalidResponse(message),
900 _ => FrontendRuntimeError::Transport(message),
901 });
902 }
903 Ok(value.get("result").cloned().unwrap_or(Value::Null))
904 }
905
906 async fn rpc_typed<T: serde::de::DeserializeOwned>(
907 &self,
908 method: &str,
909 params: Value,
910 ) -> Result<T, FrontendRuntimeError> {
911 serde_json::from_value(self.rpc(method, params).await?)
912 .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))
913 }
914
915 pub fn client_id(&self) -> &crate::RuntimeClientId {
917 &self.client_id
918 }
919
920 pub fn is_disconnected(&self) -> bool {
923 self.disconnected.load(Ordering::SeqCst)
924 }
925
926 pub async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
929 self.rpc_typed(
930 crate::FrontendFacadeMethod::TakeControl.wire_name(),
931 json!({}),
932 )
933 .await
934 }
935
936 pub async fn acquire_control(
938 &self,
939 ) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
940 self.rpc_typed(
941 crate::FrontendFacadeMethod::AcquireControl.wire_name(),
942 json!({}),
943 )
944 .await
945 }
946
947 pub async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
949 self.rpc_typed(
950 crate::FrontendFacadeMethod::Heartbeat.wire_name(),
951 json!({}),
952 )
953 .await
954 }
955
956 pub async fn lease_snapshot(
958 &self,
959 ) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
960 self.rpc_typed(crate::FrontendFacadeMethod::Lease.wire_name(), json!({}))
961 .await
962 }
963
964 pub async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
966 let snapshot = self
967 .rpc_typed(crate::FrontendFacadeMethod::Detach.wire_name(), json!({}))
968 .await?;
969 self.disconnected.store(true, Ordering::SeqCst);
970 Ok(snapshot)
971 }
972}
973
974#[async_trait]
975#[cfg(feature = "adapter-api")]
976impl FrontendRuntime for HttpFrontendRuntime {
977 async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
978 self.rpc_typed(crate::FrontendFacadeMethod::Describe.wire_name(), json!({}))
979 .await
980 }
981
982 async fn attach(
983 &self,
984 history_limit: usize,
985 ) -> Result<FrontendAttachment, FrontendRuntimeError> {
986 if self.disconnected.load(Ordering::SeqCst) {
987 return Err(FrontendRuntimeError::Closed);
988 }
989 let live = self.events.subscribe();
993 let snapshot: FrontendAttachSnapshot = self
994 .rpc_typed(
995 crate::FrontendFacadeMethod::Attach.wire_name(),
996 json!({"limit": history_limit}),
997 )
998 .await?;
999 Ok(FrontendAttachment::new(
1000 snapshot.descriptor,
1001 snapshot.history,
1002 snapshot.history_cursor,
1003 snapshot.replay,
1004 live,
1005 Some(self.lifecycle.clone()),
1006 ))
1007 }
1008
1009 async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), FrontendRuntimeError> {
1010 self.send_input_with_images(prompt, Vec::new()).await
1011 }
1012
1013 async fn send_input_with_images(
1014 self: Arc<Self>,
1015 prompt: String,
1016 image_urls: Vec<String>,
1017 ) -> Result<(), FrontendRuntimeError> {
1018 self.rpc(
1019 crate::FrontendFacadeMethod::SendInput.wire_name(),
1020 json!({"prompt": prompt, "image_urls": image_urls}),
1021 )
1022 .await?;
1023 Ok(())
1024 }
1025
1026 async fn submit(&self, prompt: String) -> Result<String, FrontendRuntimeError> {
1027 let result = self
1028 .rpc(
1029 crate::FrontendFacadeMethod::Submit.wire_name(),
1030 json!({"prompt": prompt}),
1031 )
1032 .await?;
1033 Ok(result
1034 .get("reply")
1035 .and_then(Value::as_str)
1036 .unwrap_or_default()
1037 .to_string())
1038 }
1039
1040 async fn submit_with_images(
1041 &self,
1042 prompt: String,
1043 image_urls: Vec<String>,
1044 ) -> Result<String, FrontendRuntimeError> {
1045 let result = self
1046 .rpc(
1047 crate::FrontendFacadeMethod::Submit.wire_name(),
1048 json!({"prompt": prompt, "image_urls": image_urls}),
1049 )
1050 .await?;
1051 Ok(result
1052 .get("reply")
1053 .and_then(Value::as_str)
1054 .unwrap_or_default()
1055 .to_string())
1056 }
1057
1058 async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
1059 let result = self
1060 .rpc(
1061 crate::FrontendFacadeMethod::Interrupt.wire_name(),
1062 json!({}),
1063 )
1064 .await?;
1065 Ok(result
1066 .get("interrupted")
1067 .and_then(Value::as_bool)
1068 .unwrap_or(false))
1069 }
1070
1071 async fn steer(&self, prompt: String) -> Result<(), FrontendRuntimeError> {
1072 self.rpc(
1073 crate::FrontendFacadeMethod::Steer.wire_name(),
1074 json!({"prompt": prompt}),
1075 )
1076 .await?;
1077 Ok(())
1078 }
1079
1080 async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
1081 self.rpc(
1082 crate::FrontendFacadeMethod::Respond.wire_name(),
1083 json!({"response": response}),
1084 )
1085 .await?;
1086 Ok(())
1087 }
1088
1089 async fn invoke(
1090 &self,
1091 operation: FrontendOperationInvocation,
1092 ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
1093 self.rpc_typed(
1094 crate::FrontendFacadeMethod::Invoke.wire_name(),
1095 json!({"operation": operation}),
1096 )
1097 .await
1098 }
1099
1100 async fn lease_snapshot(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1101 HttpFrontendRuntime::lease_snapshot(self).await
1102 }
1103
1104 async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1105 HttpFrontendRuntime::take_control(self).await
1106 }
1107
1108 async fn acquire_control(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1109 HttpFrontendRuntime::acquire_control(self).await
1110 }
1111
1112 async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1113 HttpFrontendRuntime::heartbeat(self).await
1114 }
1115
1116 async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1117 HttpFrontendRuntime::detach(self).await
1118 }
1119
1120 async fn close(&self) -> Result<(), FrontendRuntimeError> {
1121 self.rpc(crate::FrontendFacadeMethod::Close.wire_name(), json!({}))
1122 .await?;
1123 self.disconnected.store(true, Ordering::SeqCst);
1124 Ok(())
1125 }
1126}