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}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct FrontendOperationDescriptor {
131 pub id: String,
133 pub kind: FrontendOperationKind,
135 pub command: Option<FrontendCommandDescriptor>,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(tag = "kind", rename_all = "snake_case")]
142pub enum FrontendOperationInvocation {
143 Prompt {
145 operation_id: String,
147 arguments: String,
149 },
150}
151
152impl FrontendOperationInvocation {
153 pub fn operation_id(&self) -> &str {
155 match self {
156 Self::Prompt { operation_id, .. } => operation_id,
157 }
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(tag = "kind", rename_all = "snake_case")]
164pub enum FrontendOperationResult {
165 Prompt {
167 reply: String,
169 },
170}
171
172#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
174pub struct FrontendRuntimeMetadata {
175 pub source_harness: Option<String>,
177 pub emulation_profile: Option<String>,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183pub struct FrontendRuntimeDescriptor {
184 pub schema_version: u32,
186 pub session_id: String,
188 pub source_harness: Option<String>,
190 pub emulation_profile: Option<String>,
192 pub active_modules: Vec<String>,
194 pub commands: Vec<FrontendCommandDescriptor>,
196 #[serde(default)]
198 pub operations: Vec<FrontendOperationDescriptor>,
199 pub actions: FrontendActions,
201 pub display: FrontendDisplayCapabilities,
203 pub model: String,
205 pub turn_state: FrontendTurnState,
207 pub connection_state: FrontendConnectionState,
209 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
211 pub extensions: BTreeMap<String, Value>,
212}
213
214#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
217pub struct FrontendAttachSnapshot {
218 pub descriptor: FrontendRuntimeDescriptor,
220 pub history: Vec<ChatMessage>,
222 pub history_cursor: u64,
224 pub replay: VecDeque<FrontendEvent>,
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
230#[serde(rename_all = "snake_case")]
231pub enum FrontendRequestKind {
232 Approval,
234 Elicitation,
236 #[serde(other)]
239 Other,
240}
241
242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244pub struct FrontendRequest {
245 pub id: u64,
247 pub kind: FrontendRequestKind,
249 pub payload: Value,
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
255#[serde(rename_all = "snake_case")]
256pub enum FrontendApprovalDecision {
257 Deny,
259 Allow,
261 AllowForSession,
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
267#[serde(rename_all = "snake_case")]
268pub enum FrontendElicitationAction {
269 Accept,
271 Decline,
273 Cancel,
275}
276
277#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279#[serde(tag = "kind", rename_all = "snake_case")]
280pub enum FrontendResponse {
281 Approval {
283 request_id: u64,
285 decision: FrontendApprovalDecision,
287 },
288 Elicitation {
290 request_id: u64,
292 action: FrontendElicitationAction,
294 content: Option<Value>,
296 },
297 Other {
299 request_id: u64,
301 action: FrontendElicitationAction,
303 content: Option<Value>,
305 },
306}
307
308impl FrontendResponse {
309 pub(crate) fn request_id(&self) -> u64 {
310 match self {
311 Self::Approval { request_id, .. }
312 | Self::Elicitation { request_id, .. }
313 | Self::Other { request_id, .. } => *request_id,
314 }
315 }
316}
317
318pub struct FrontendAttachment {
320 pub descriptor: FrontendRuntimeDescriptor,
322 pub history: Vec<ChatMessage>,
324 pub history_cursor: u64,
326 pub(crate) replay: VecDeque<FrontendEvent>,
327 live: broadcast::Receiver<FrontendEvent>,
328 delivered: u64,
329 acknowledged: Option<Arc<AtomicU64>>,
330 _transport_lease: Option<Arc<()>>,
331}
332
333impl FrontendAttachment {
334 pub fn from_snapshot(
338 snapshot: FrontendAttachSnapshot,
339 live: broadcast::Receiver<FrontendEvent>,
340 ) -> Self {
341 Self::from_snapshot_after(snapshot, live, 0)
342 }
343
344 pub fn from_snapshot_after(
349 snapshot: FrontendAttachSnapshot,
350 live: broadcast::Receiver<FrontendEvent>,
351 acknowledged_sequence: u64,
352 ) -> Self {
353 let delivered = snapshot.history_cursor.max(acknowledged_sequence);
354 Self::new_with_delivered(
355 snapshot.descriptor,
356 snapshot.history,
357 snapshot.history_cursor,
358 snapshot.replay,
359 live,
360 None,
361 delivered,
362 )
363 }
364
365 pub(crate) fn new(
366 descriptor: FrontendRuntimeDescriptor,
367 history: Vec<ChatMessage>,
368 history_cursor: u64,
369 replay: VecDeque<FrontendEvent>,
370 live: broadcast::Receiver<FrontendEvent>,
371 transport_lease: Option<Arc<()>>,
372 ) -> Self {
373 let delivered = history_cursor;
374 Self::new_with_delivered(
375 descriptor,
376 history,
377 history_cursor,
378 replay,
379 live,
380 transport_lease,
381 delivered,
382 )
383 }
384
385 fn new_with_delivered(
386 descriptor: FrontendRuntimeDescriptor,
387 history: Vec<ChatMessage>,
388 history_cursor: u64,
389 replay: VecDeque<FrontendEvent>,
390 live: broadcast::Receiver<FrontendEvent>,
391 transport_lease: Option<Arc<()>>,
392 delivered: u64,
393 ) -> Self {
394 Self {
395 descriptor,
396 history,
397 history_cursor,
398 replay,
399 live,
400 delivered,
401 acknowledged: None,
402 _transport_lease: transport_lease,
403 }
404 }
405
406 #[cfg(feature = "adapter-acp")]
407 pub(crate) fn with_acknowledgement(mut self, acknowledged: Arc<AtomicU64>) -> Self {
408 acknowledged.fetch_max(self.history_cursor, Ordering::SeqCst);
409 self.acknowledged = Some(acknowledged);
410 self
411 }
412
413 fn acknowledge(&self, event: &FrontendEvent) {
414 if !event_advances_acknowledgement(event) {
415 return;
416 }
417 if let Some(acknowledged) = &self.acknowledged {
418 acknowledged.fetch_max(event.sequence, Ordering::SeqCst);
419 }
420 }
421
422 pub async fn next_event(&mut self) -> Result<FrontendEvent, FrontendRuntimeError> {
426 loop {
427 let event = match self.next_replay_event() {
428 Some(event) => return Ok(event),
429 None => match self.live.recv().await {
430 Ok(event) => event,
431 Err(broadcast::error::RecvError::Lagged(count)) => {
432 return Err(FrontendRuntimeError::ReplayGap(count));
433 }
434 Err(broadcast::error::RecvError::Closed) => {
435 return Err(FrontendRuntimeError::Closed);
436 }
437 },
438 };
439 if event.sequence <= self.delivered {
440 continue;
441 }
442 self.delivered = event.sequence;
443 self.acknowledge(&event);
444 return Ok(event);
445 }
446 }
447
448 pub fn next_replay_event(&mut self) -> Option<FrontendEvent> {
453 while let Some(event) = self.replay.pop_front() {
454 if event.sequence <= self.delivered {
455 continue;
456 }
457 self.delivered = event.sequence;
458 self.acknowledge(&event);
459 return Some(event);
460 }
461 None
462 }
463}
464
465pub(crate) fn event_advances_acknowledgement(event: &FrontendEvent) -> bool {
466 event
467 .payload
468 .pointer("/_meta/supercode/transient")
469 .and_then(Value::as_bool)
470 != Some(true)
471}
472
473pub(crate) struct FrontendProjectionState {
475 pub(crate) history: Vec<ChatMessage>,
476 pub(crate) history_cursor: u64,
477 pub(crate) next_sequence: u64,
478 pub(crate) replay: VecDeque<FrontendEvent>,
479}
480
481#[async_trait]
482impl FrontendRuntime for RpcEngine {
483 async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
484 Ok(self.frontend_descriptor())
485 }
486
487 async fn attach(
488 &self,
489 history_limit: usize,
490 ) -> Result<FrontendAttachment, FrontendRuntimeError> {
491 self.frontend_attach(history_limit)
492 }
493
494 async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), FrontendRuntimeError> {
495 RpcEngine::send_input(&self, prompt)?;
496 Ok(())
497 }
498
499 async fn send_input_with_images(
500 self: Arc<Self>,
501 prompt: String,
502 image_urls: Vec<String>,
503 ) -> Result<(), FrontendRuntimeError> {
504 RpcEngine::send_input_with_images(&self, prompt, image_urls)?;
505 Ok(())
506 }
507
508 async fn submit(&self, prompt: String) -> Result<String, FrontendRuntimeError> {
509 Ok(RpcEngine::submit(self, prompt).await?)
510 }
511
512 async fn submit_with_images(
513 &self,
514 prompt: String,
515 image_urls: Vec<String>,
516 ) -> Result<String, FrontendRuntimeError> {
517 Ok(RpcEngine::submit_with_images(self, prompt, image_urls).await?)
518 }
519
520 async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
521 Ok(RpcEngine::interrupt(self).await)
522 }
523
524 async fn steer(&self, prompt: String) -> Result<(), FrontendRuntimeError> {
525 RpcEngine::steer(self, prompt)
526 }
527
528 async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
529 RpcEngine::respond(self, response)
530 }
531
532 async fn invoke(
533 &self,
534 operation: FrontendOperationInvocation,
535 ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
536 RpcEngine::invoke(self, operation).await
537 }
538
539 async fn close(&self) -> Result<(), FrontendRuntimeError> {
540 RpcEngine::shutdown(self).await;
541 Ok(())
542 }
543}
544
545#[cfg(feature = "adapter-api")]
550pub struct HttpFrontendRuntime {
551 base_url: String,
552 token: String,
553 client_id: crate::RuntimeClientId,
554 authorization: crate::RuntimeAuthorization,
555 client: reqwest::Client,
556 events: broadcast::Sender<FrontendEvent>,
557 next_id: AtomicU64,
558 lifecycle: Arc<()>,
559 disconnected: AtomicBool,
560}
561
562#[cfg(feature = "adapter-api")]
563impl HttpFrontendRuntime {
564 pub async fn connect(
567 base_url: impl Into<String>,
568 token: impl Into<String>,
569 ) -> Result<Arc<Self>, FrontendRuntimeError> {
570 let mut random = [0_u8; 16];
571 getrandom::getrandom(&mut random).map_err(|error| {
572 FrontendRuntimeError::Transport(format!(
573 "cannot generate runtime client identity: {error}"
574 ))
575 })?;
576 let suffix = random
577 .iter()
578 .map(|byte| format!("{byte:02x}"))
579 .collect::<String>();
580 let client_id = crate::RuntimeClientId::parse(format!("http-{suffix}"))
581 .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
582 Self::connect_with_client_id(base_url, token, client_id).await
583 }
584
585 pub async fn connect_with_client_id(
588 base_url: impl Into<String>,
589 token: impl Into<String>,
590 client_id: crate::RuntimeClientId,
591 ) -> Result<Arc<Self>, FrontendRuntimeError> {
592 Self::connect_with_authorization(
593 base_url,
594 token,
595 client_id,
596 crate::RuntimeAuthorization::owner(),
597 )
598 .await
599 }
600
601 pub async fn connect_with_authorization(
605 base_url: impl Into<String>,
606 token: impl Into<String>,
607 client_id: crate::RuntimeClientId,
608 authorization: crate::RuntimeAuthorization,
609 ) -> Result<Arc<Self>, FrontendRuntimeError> {
610 Self::connect_inner(base_url, token, client_id, authorization, true)
611 .await
612 .map(|(runtime, _)| runtime)
613 }
614
615 pub(crate) async fn probe_described(
622 base_url: impl Into<String>,
623 token: impl Into<String>,
624 client_id: crate::RuntimeClientId,
625 ) -> Result<(Arc<Self>, FrontendRuntimeDescriptor), FrontendRuntimeError> {
626 Self::connect_inner(
627 base_url,
628 token,
629 client_id,
630 crate::RuntimeAuthorization::observer(),
631 false,
632 )
633 .await
634 }
635
636 async fn connect_inner(
637 base_url: impl Into<String>,
638 token: impl Into<String>,
639 client_id: crate::RuntimeClientId,
640 authorization: crate::RuntimeAuthorization,
641 stream_events: bool,
642 ) -> Result<(Arc<Self>, FrontendRuntimeDescriptor), FrontendRuntimeError> {
643 let runtime = Arc::new(Self {
644 base_url: base_url.into().trim_end_matches('/').to_string(),
645 token: token.into(),
646 client_id,
647 authorization,
648 client: reqwest::Client::new(),
649 events: broadcast::channel(1024).0,
650 next_id: AtomicU64::new(1),
651 lifecycle: Arc::new(()),
652 disconnected: AtomicBool::new(false),
653 });
654 let descriptor: FrontendRuntimeDescriptor = runtime
656 .rpc_typed(crate::FrontendFacadeMethod::Describe.wire_name(), json!({}))
657 .await?;
658 if stream_events {
659 Self::start_event_stream(&runtime).await?;
660 }
661 Ok((runtime, descriptor))
662 }
663
664 async fn start_event_stream(runtime: &Arc<Self>) -> Result<(), FrontendRuntimeError> {
665 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
666 let weak = Arc::downgrade(runtime);
667 let lifecycle = Arc::downgrade(&runtime.lifecycle);
668 tokio::spawn(async move {
669 Self::run_event_stream(weak, lifecycle, ready_tx).await;
670 });
671 ready_rx.await.map_err(|_| {
672 FrontendRuntimeError::Transport("frontend event stream exited before startup".into())
673 })?
674 }
675
676 async fn run_event_stream(
677 weak: Weak<Self>,
678 lifecycle: Weak<()>,
679 ready: tokio::sync::oneshot::Sender<Result<(), FrontendRuntimeError>>,
680 ) {
681 let Some(runtime) = weak.upgrade() else {
682 let _ = ready.send(Err(FrontendRuntimeError::Closed));
683 return;
684 };
685 let request = runtime
686 .client
687 .get(format!("{}/frontend/events", runtime.base_url))
688 .bearer_auth(&runtime.token)
689 .header("x-supercode-client-id", runtime.client_id.as_str())
690 .header(
691 "x-supercode-permissions",
692 runtime.authorization.header_value(),
693 );
694 let events = runtime.events.clone();
695 drop(runtime);
696 let response = request.send().await;
697 let response = match response {
698 Ok(response) if response.status().is_success() => response,
699 Ok(response) => {
700 let _ = ready.send(Err(FrontendRuntimeError::Transport(format!(
701 "frontend event stream returned {}",
702 response.status()
703 ))));
704 return;
705 }
706 Err(error) => {
707 let _ = ready.send(Err(FrontendRuntimeError::Transport(error.to_string())));
708 return;
709 }
710 };
711 let _ = ready.send(Ok(()));
712 let mut stream = response.bytes_stream();
713 let mut pending = Vec::<u8>::new();
714 let mut liveness = tokio::time::interval(std::time::Duration::from_millis(100));
715 loop {
716 let chunk = tokio::select! {
717 _ = liveness.tick() => {
718 if lifecycle.strong_count() == 0 {
719 break;
720 }
721 if weak
722 .upgrade()
723 .is_some_and(|runtime| runtime.disconnected.load(Ordering::SeqCst))
724 {
725 break;
726 }
727 continue;
728 }
729 chunk = stream.next() => chunk,
730 };
731 let Some(chunk) = chunk else {
732 break;
733 };
734 let Ok(chunk) = chunk else {
735 break;
736 };
737 pending.extend_from_slice(&chunk);
738 while let Some(position) = pending.iter().position(|byte| *byte == b'\n') {
739 let line = pending.drain(..=position).collect::<Vec<_>>();
740 let line = String::from_utf8_lossy(&line);
741 let Some(data) = line.trim_end().strip_prefix("data: ") else {
742 continue;
743 };
744 if let Ok(event) = serde_json::from_str::<FrontendEvent>(data) {
745 let _ = events.send(event);
746 }
747 }
748 }
749 if let Some(runtime) = weak.upgrade() {
750 runtime.disconnected.store(true, Ordering::SeqCst);
751 let _ = runtime.events.send(FrontendEvent::new(
752 u64::MAX,
753 json!({
754 "type": "runtime_disconnected",
755 "schema_version": FRONTEND_EVENT_SCHEMA_VERSION
756 }),
757 ));
758 }
759 }
760
761 async fn rpc(&self, method: &str, params: Value) -> Result<Value, FrontendRuntimeError> {
762 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
763 let requested_operation = params
764 .pointer("/operation/operation_id")
765 .and_then(Value::as_str)
766 .map(str::to_owned);
767 let response = self
768 .client
769 .post(format!("{}/rpc", self.base_url))
770 .bearer_auth(&self.token)
771 .header("x-supercode-client-id", self.client_id.as_str())
772 .header("x-supercode-permissions", self.authorization.header_value())
773 .json(&json!({"id": id, "method": method, "params": params}))
774 .send()
775 .await
776 .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
777 if !response.status().is_success() {
778 return Err(FrontendRuntimeError::Transport(format!(
779 "SDK HTTP RPC returned {}",
780 response.status()
781 )));
782 }
783 let value: Value = response
784 .json()
785 .await
786 .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
787 if let Some(error) = value.get("error") {
788 let code = error.get("code").and_then(Value::as_i64);
789 let name = error.get("name").and_then(Value::as_str);
790 let operation = error
791 .get("operation")
792 .and_then(Value::as_str)
793 .and_then(crate::SdkOperation::from_action_name);
794 let message = error
795 .get("message")
796 .and_then(Value::as_str)
797 .unwrap_or("SDK runtime request failed")
798 .to_string();
799 return Err(match (name, code) {
800 (Some("unauthenticated"), _) | (_, Some(-32030)) => {
801 FrontendRuntimeError::Unauthenticated
802 }
803 (Some("unauthorized"), _) | (_, Some(-32031)) => {
804 FrontendRuntimeError::Unauthorized {
805 permission: error
806 .get("permission")
807 .and_then(Value::as_str)
808 .unwrap_or("unknown")
809 .to_string(),
810 }
811 }
812 (Some("controller_required"), _) | (_, Some(-32032)) => {
813 FrontendRuntimeError::ControllerRequired {
814 holder: error
815 .get("holder")
816 .and_then(Value::as_str)
817 .map(str::to_owned),
818 expires_at_ms: error.get("expiresAtMs").and_then(Value::as_u64),
819 }
820 }
821 (Some("lease_expired"), _) | (_, Some(-32033)) => {
822 FrontendRuntimeError::LeaseExpired
823 }
824 (_, Some(-32023)) => FrontendRuntimeError::UnsupportedOperation(
825 requested_operation.unwrap_or(message),
826 ),
827 (Some("unsupported_action"), _) => FrontendRuntimeError::UnsupportedAction(
828 operation
829 .unwrap_or_else(|| {
830 crate::SdkOperation::from_action_name(method)
831 .unwrap_or(crate::SdkOperation::Respond)
832 })
833 .action_name(),
834 ),
835 (Some("not_found"), Some(-32021)) => {
836 let request_id = params
837 .pointer("/response/request_id")
838 .and_then(Value::as_u64)
839 .unwrap_or_default();
840 FrontendRuntimeError::UnknownRequest(request_id)
841 }
842 (Some("invalid_argument"), _) => FrontendRuntimeError::InvalidResponse(message),
843 (_, Some(-32000)) => RuntimeSubmitError::Busy.into(),
844 (_, Some(-32001)) => RuntimeSubmitError::Interrupted.into(),
845 (_, Some(-32002)) => RuntimeSubmitError::Agent(message).into(),
846 (_, Some(-32020)) => FrontendRuntimeError::UnsupportedAction(
847 crate::SdkOperation::from_action_name(method)
848 .unwrap_or(crate::SdkOperation::Respond)
849 .action_name(),
850 ),
851 (_, Some(-32021)) => {
852 let request_id = params
853 .pointer("/response/request_id")
854 .and_then(Value::as_u64)
855 .unwrap_or_default();
856 FrontendRuntimeError::UnknownRequest(request_id)
857 }
858 (_, Some(-32022)) => FrontendRuntimeError::InvalidResponse(message),
859 _ => FrontendRuntimeError::Transport(message),
860 });
861 }
862 Ok(value.get("result").cloned().unwrap_or(Value::Null))
863 }
864
865 async fn rpc_typed<T: serde::de::DeserializeOwned>(
866 &self,
867 method: &str,
868 params: Value,
869 ) -> Result<T, FrontendRuntimeError> {
870 serde_json::from_value(self.rpc(method, params).await?)
871 .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))
872 }
873
874 pub fn client_id(&self) -> &crate::RuntimeClientId {
876 &self.client_id
877 }
878
879 pub fn is_disconnected(&self) -> bool {
882 self.disconnected.load(Ordering::SeqCst)
883 }
884
885 pub async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
888 self.rpc_typed(
889 crate::FrontendFacadeMethod::TakeControl.wire_name(),
890 json!({}),
891 )
892 .await
893 }
894
895 pub async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
897 self.rpc_typed(
898 crate::FrontendFacadeMethod::Heartbeat.wire_name(),
899 json!({}),
900 )
901 .await
902 }
903
904 pub async fn lease_snapshot(
906 &self,
907 ) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
908 self.rpc_typed(crate::FrontendFacadeMethod::Lease.wire_name(), json!({}))
909 .await
910 }
911
912 pub async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
914 let snapshot = self
915 .rpc_typed(crate::FrontendFacadeMethod::Detach.wire_name(), json!({}))
916 .await?;
917 self.disconnected.store(true, Ordering::SeqCst);
918 Ok(snapshot)
919 }
920}
921
922#[async_trait]
923#[cfg(feature = "adapter-api")]
924impl FrontendRuntime for HttpFrontendRuntime {
925 async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
926 self.rpc_typed(crate::FrontendFacadeMethod::Describe.wire_name(), json!({}))
927 .await
928 }
929
930 async fn attach(
931 &self,
932 history_limit: usize,
933 ) -> Result<FrontendAttachment, FrontendRuntimeError> {
934 if self.disconnected.load(Ordering::SeqCst) {
935 return Err(FrontendRuntimeError::Closed);
936 }
937 let live = self.events.subscribe();
941 let snapshot: FrontendAttachSnapshot = self
942 .rpc_typed(
943 crate::FrontendFacadeMethod::Attach.wire_name(),
944 json!({"limit": history_limit}),
945 )
946 .await?;
947 Ok(FrontendAttachment::new(
948 snapshot.descriptor,
949 snapshot.history,
950 snapshot.history_cursor,
951 snapshot.replay,
952 live,
953 Some(self.lifecycle.clone()),
954 ))
955 }
956
957 async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), FrontendRuntimeError> {
958 self.send_input_with_images(prompt, Vec::new()).await
959 }
960
961 async fn send_input_with_images(
962 self: Arc<Self>,
963 prompt: String,
964 image_urls: Vec<String>,
965 ) -> Result<(), FrontendRuntimeError> {
966 self.rpc(
967 crate::FrontendFacadeMethod::SendInput.wire_name(),
968 json!({"prompt": prompt, "image_urls": image_urls}),
969 )
970 .await?;
971 Ok(())
972 }
973
974 async fn submit(&self, prompt: String) -> Result<String, FrontendRuntimeError> {
975 let result = self
976 .rpc(
977 crate::FrontendFacadeMethod::Submit.wire_name(),
978 json!({"prompt": prompt}),
979 )
980 .await?;
981 Ok(result
982 .get("reply")
983 .and_then(Value::as_str)
984 .unwrap_or_default()
985 .to_string())
986 }
987
988 async fn submit_with_images(
989 &self,
990 prompt: String,
991 image_urls: Vec<String>,
992 ) -> Result<String, FrontendRuntimeError> {
993 let result = self
994 .rpc(
995 crate::FrontendFacadeMethod::Submit.wire_name(),
996 json!({"prompt": prompt, "image_urls": image_urls}),
997 )
998 .await?;
999 Ok(result
1000 .get("reply")
1001 .and_then(Value::as_str)
1002 .unwrap_or_default()
1003 .to_string())
1004 }
1005
1006 async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
1007 let result = self
1008 .rpc(
1009 crate::FrontendFacadeMethod::Interrupt.wire_name(),
1010 json!({}),
1011 )
1012 .await?;
1013 Ok(result
1014 .get("interrupted")
1015 .and_then(Value::as_bool)
1016 .unwrap_or(false))
1017 }
1018
1019 async fn steer(&self, prompt: String) -> Result<(), FrontendRuntimeError> {
1020 self.rpc(
1021 crate::FrontendFacadeMethod::Steer.wire_name(),
1022 json!({"prompt": prompt}),
1023 )
1024 .await?;
1025 Ok(())
1026 }
1027
1028 async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
1029 self.rpc(
1030 crate::FrontendFacadeMethod::Respond.wire_name(),
1031 json!({"response": response}),
1032 )
1033 .await?;
1034 Ok(())
1035 }
1036
1037 async fn invoke(
1038 &self,
1039 operation: FrontendOperationInvocation,
1040 ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
1041 self.rpc_typed(
1042 crate::FrontendFacadeMethod::Invoke.wire_name(),
1043 json!({"operation": operation}),
1044 )
1045 .await
1046 }
1047
1048 async fn lease_snapshot(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1049 HttpFrontendRuntime::lease_snapshot(self).await
1050 }
1051
1052 async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1053 HttpFrontendRuntime::take_control(self).await
1054 }
1055
1056 async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1057 HttpFrontendRuntime::heartbeat(self).await
1058 }
1059
1060 async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1061 HttpFrontendRuntime::detach(self).await
1062 }
1063
1064 async fn close(&self) -> Result<(), FrontendRuntimeError> {
1065 self.rpc(crate::FrontendFacadeMethod::Close.wire_name(), json!({}))
1066 .await?;
1067 self.disconnected.store(true, Ordering::SeqCst);
1068 Ok(())
1069 }
1070}