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 heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
938 self.rpc_typed(
939 crate::FrontendFacadeMethod::Heartbeat.wire_name(),
940 json!({}),
941 )
942 .await
943 }
944
945 pub async fn lease_snapshot(
947 &self,
948 ) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
949 self.rpc_typed(crate::FrontendFacadeMethod::Lease.wire_name(), json!({}))
950 .await
951 }
952
953 pub async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
955 let snapshot = self
956 .rpc_typed(crate::FrontendFacadeMethod::Detach.wire_name(), json!({}))
957 .await?;
958 self.disconnected.store(true, Ordering::SeqCst);
959 Ok(snapshot)
960 }
961}
962
963#[async_trait]
964#[cfg(feature = "adapter-api")]
965impl FrontendRuntime for HttpFrontendRuntime {
966 async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
967 self.rpc_typed(crate::FrontendFacadeMethod::Describe.wire_name(), json!({}))
968 .await
969 }
970
971 async fn attach(
972 &self,
973 history_limit: usize,
974 ) -> Result<FrontendAttachment, FrontendRuntimeError> {
975 if self.disconnected.load(Ordering::SeqCst) {
976 return Err(FrontendRuntimeError::Closed);
977 }
978 let live = self.events.subscribe();
982 let snapshot: FrontendAttachSnapshot = self
983 .rpc_typed(
984 crate::FrontendFacadeMethod::Attach.wire_name(),
985 json!({"limit": history_limit}),
986 )
987 .await?;
988 Ok(FrontendAttachment::new(
989 snapshot.descriptor,
990 snapshot.history,
991 snapshot.history_cursor,
992 snapshot.replay,
993 live,
994 Some(self.lifecycle.clone()),
995 ))
996 }
997
998 async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), FrontendRuntimeError> {
999 self.send_input_with_images(prompt, Vec::new()).await
1000 }
1001
1002 async fn send_input_with_images(
1003 self: Arc<Self>,
1004 prompt: String,
1005 image_urls: Vec<String>,
1006 ) -> Result<(), FrontendRuntimeError> {
1007 self.rpc(
1008 crate::FrontendFacadeMethod::SendInput.wire_name(),
1009 json!({"prompt": prompt, "image_urls": image_urls}),
1010 )
1011 .await?;
1012 Ok(())
1013 }
1014
1015 async fn submit(&self, prompt: String) -> Result<String, FrontendRuntimeError> {
1016 let result = self
1017 .rpc(
1018 crate::FrontendFacadeMethod::Submit.wire_name(),
1019 json!({"prompt": prompt}),
1020 )
1021 .await?;
1022 Ok(result
1023 .get("reply")
1024 .and_then(Value::as_str)
1025 .unwrap_or_default()
1026 .to_string())
1027 }
1028
1029 async fn submit_with_images(
1030 &self,
1031 prompt: String,
1032 image_urls: Vec<String>,
1033 ) -> Result<String, FrontendRuntimeError> {
1034 let result = self
1035 .rpc(
1036 crate::FrontendFacadeMethod::Submit.wire_name(),
1037 json!({"prompt": prompt, "image_urls": image_urls}),
1038 )
1039 .await?;
1040 Ok(result
1041 .get("reply")
1042 .and_then(Value::as_str)
1043 .unwrap_or_default()
1044 .to_string())
1045 }
1046
1047 async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
1048 let result = self
1049 .rpc(
1050 crate::FrontendFacadeMethod::Interrupt.wire_name(),
1051 json!({}),
1052 )
1053 .await?;
1054 Ok(result
1055 .get("interrupted")
1056 .and_then(Value::as_bool)
1057 .unwrap_or(false))
1058 }
1059
1060 async fn steer(&self, prompt: String) -> Result<(), FrontendRuntimeError> {
1061 self.rpc(
1062 crate::FrontendFacadeMethod::Steer.wire_name(),
1063 json!({"prompt": prompt}),
1064 )
1065 .await?;
1066 Ok(())
1067 }
1068
1069 async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
1070 self.rpc(
1071 crate::FrontendFacadeMethod::Respond.wire_name(),
1072 json!({"response": response}),
1073 )
1074 .await?;
1075 Ok(())
1076 }
1077
1078 async fn invoke(
1079 &self,
1080 operation: FrontendOperationInvocation,
1081 ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
1082 self.rpc_typed(
1083 crate::FrontendFacadeMethod::Invoke.wire_name(),
1084 json!({"operation": operation}),
1085 )
1086 .await
1087 }
1088
1089 async fn lease_snapshot(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1090 HttpFrontendRuntime::lease_snapshot(self).await
1091 }
1092
1093 async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1094 HttpFrontendRuntime::take_control(self).await
1095 }
1096
1097 async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1098 HttpFrontendRuntime::heartbeat(self).await
1099 }
1100
1101 async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1102 HttpFrontendRuntime::detach(self).await
1103 }
1104
1105 async fn close(&self) -> Result<(), FrontendRuntimeError> {
1106 self.rpc(crate::FrontendFacadeMethod::Close.wire_name(), json!({}))
1107 .await?;
1108 self.disconnected.store(true, Ordering::SeqCst);
1109 Ok(())
1110 }
1111}