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 pub(crate) fn with_acknowledgement(mut self, acknowledged: Arc<AtomicU64>) -> Self {
407 acknowledged.fetch_max(self.history_cursor, Ordering::SeqCst);
408 self.acknowledged = Some(acknowledged);
409 self
410 }
411
412 fn acknowledge(&self, event: &FrontendEvent) {
413 if !event_advances_acknowledgement(event) {
414 return;
415 }
416 if let Some(acknowledged) = &self.acknowledged {
417 acknowledged.fetch_max(event.sequence, Ordering::SeqCst);
418 }
419 }
420
421 pub async fn next_event(&mut self) -> Result<FrontendEvent, FrontendRuntimeError> {
425 loop {
426 let event = match self.next_replay_event() {
427 Some(event) => return Ok(event),
428 None => match self.live.recv().await {
429 Ok(event) => event,
430 Err(broadcast::error::RecvError::Lagged(count)) => {
431 return Err(FrontendRuntimeError::ReplayGap(count));
432 }
433 Err(broadcast::error::RecvError::Closed) => {
434 return Err(FrontendRuntimeError::Closed);
435 }
436 },
437 };
438 if event.sequence <= self.delivered {
439 continue;
440 }
441 self.delivered = event.sequence;
442 self.acknowledge(&event);
443 return Ok(event);
444 }
445 }
446
447 pub fn next_replay_event(&mut self) -> Option<FrontendEvent> {
452 while let Some(event) = self.replay.pop_front() {
453 if event.sequence <= self.delivered {
454 continue;
455 }
456 self.delivered = event.sequence;
457 self.acknowledge(&event);
458 return Some(event);
459 }
460 None
461 }
462}
463
464pub(crate) fn event_advances_acknowledgement(event: &FrontendEvent) -> bool {
465 event
466 .payload
467 .pointer("/_meta/supercode/transient")
468 .and_then(Value::as_bool)
469 != Some(true)
470}
471
472pub(crate) struct FrontendProjectionState {
474 pub(crate) history: Vec<ChatMessage>,
475 pub(crate) history_cursor: u64,
476 pub(crate) next_sequence: u64,
477 pub(crate) replay: VecDeque<FrontendEvent>,
478}
479
480#[async_trait]
481impl FrontendRuntime for RpcEngine {
482 async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
483 Ok(self.frontend_descriptor())
484 }
485
486 async fn attach(
487 &self,
488 history_limit: usize,
489 ) -> Result<FrontendAttachment, FrontendRuntimeError> {
490 self.frontend_attach(history_limit)
491 }
492
493 async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), FrontendRuntimeError> {
494 RpcEngine::send_input(&self, prompt)?;
495 Ok(())
496 }
497
498 async fn submit(&self, prompt: String) -> Result<String, FrontendRuntimeError> {
499 Ok(RpcEngine::submit(self, prompt).await?)
500 }
501
502 async fn submit_with_images(
503 &self,
504 prompt: String,
505 image_urls: Vec<String>,
506 ) -> Result<String, FrontendRuntimeError> {
507 Ok(RpcEngine::submit_with_images(self, prompt, image_urls).await?)
508 }
509
510 async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
511 Ok(RpcEngine::interrupt(self).await)
512 }
513
514 async fn steer(&self, prompt: String) -> Result<(), FrontendRuntimeError> {
515 RpcEngine::steer(self, prompt)
516 }
517
518 async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
519 RpcEngine::respond(self, response)
520 }
521
522 async fn invoke(
523 &self,
524 operation: FrontendOperationInvocation,
525 ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
526 RpcEngine::invoke(self, operation).await
527 }
528
529 async fn close(&self) -> Result<(), FrontendRuntimeError> {
530 RpcEngine::shutdown(self).await;
531 Ok(())
532 }
533}
534
535#[cfg(feature = "adapter-api")]
540pub struct HttpFrontendRuntime {
541 base_url: String,
542 token: String,
543 client_id: crate::RuntimeClientId,
544 authorization: crate::RuntimeAuthorization,
545 client: reqwest::Client,
546 events: broadcast::Sender<FrontendEvent>,
547 next_id: AtomicU64,
548 lifecycle: Arc<()>,
549 disconnected: AtomicBool,
550}
551
552#[cfg(feature = "adapter-api")]
553impl HttpFrontendRuntime {
554 pub async fn connect(
557 base_url: impl Into<String>,
558 token: impl Into<String>,
559 ) -> Result<Arc<Self>, FrontendRuntimeError> {
560 let mut random = [0_u8; 16];
561 getrandom::getrandom(&mut random).map_err(|error| {
562 FrontendRuntimeError::Transport(format!(
563 "cannot generate runtime client identity: {error}"
564 ))
565 })?;
566 let suffix = random
567 .iter()
568 .map(|byte| format!("{byte:02x}"))
569 .collect::<String>();
570 let client_id = crate::RuntimeClientId::parse(format!("http-{suffix}"))
571 .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
572 Self::connect_with_client_id(base_url, token, client_id).await
573 }
574
575 pub async fn connect_with_client_id(
578 base_url: impl Into<String>,
579 token: impl Into<String>,
580 client_id: crate::RuntimeClientId,
581 ) -> Result<Arc<Self>, FrontendRuntimeError> {
582 Self::connect_with_authorization(
583 base_url,
584 token,
585 client_id,
586 crate::RuntimeAuthorization::owner(),
587 )
588 .await
589 }
590
591 pub async fn connect_with_authorization(
595 base_url: impl Into<String>,
596 token: impl Into<String>,
597 client_id: crate::RuntimeClientId,
598 authorization: crate::RuntimeAuthorization,
599 ) -> Result<Arc<Self>, FrontendRuntimeError> {
600 Self::connect_inner(base_url, token, client_id, authorization, true)
601 .await
602 .map(|(runtime, _)| runtime)
603 }
604
605 pub(crate) async fn probe_described(
612 base_url: impl Into<String>,
613 token: impl Into<String>,
614 client_id: crate::RuntimeClientId,
615 ) -> Result<(Arc<Self>, FrontendRuntimeDescriptor), FrontendRuntimeError> {
616 Self::connect_inner(
617 base_url,
618 token,
619 client_id,
620 crate::RuntimeAuthorization::observer(),
621 false,
622 )
623 .await
624 }
625
626 async fn connect_inner(
627 base_url: impl Into<String>,
628 token: impl Into<String>,
629 client_id: crate::RuntimeClientId,
630 authorization: crate::RuntimeAuthorization,
631 stream_events: bool,
632 ) -> Result<(Arc<Self>, FrontendRuntimeDescriptor), FrontendRuntimeError> {
633 let runtime = Arc::new(Self {
634 base_url: base_url.into().trim_end_matches('/').to_string(),
635 token: token.into(),
636 client_id,
637 authorization,
638 client: reqwest::Client::new(),
639 events: broadcast::channel(1024).0,
640 next_id: AtomicU64::new(1),
641 lifecycle: Arc::new(()),
642 disconnected: AtomicBool::new(false),
643 });
644 let descriptor: FrontendRuntimeDescriptor = runtime
646 .rpc_typed(crate::FrontendFacadeMethod::Describe.wire_name(), json!({}))
647 .await?;
648 if stream_events {
649 Self::start_event_stream(&runtime).await?;
650 }
651 Ok((runtime, descriptor))
652 }
653
654 async fn start_event_stream(runtime: &Arc<Self>) -> Result<(), FrontendRuntimeError> {
655 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
656 let weak = Arc::downgrade(runtime);
657 let lifecycle = Arc::downgrade(&runtime.lifecycle);
658 tokio::spawn(async move {
659 Self::run_event_stream(weak, lifecycle, ready_tx).await;
660 });
661 ready_rx.await.map_err(|_| {
662 FrontendRuntimeError::Transport("frontend event stream exited before startup".into())
663 })?
664 }
665
666 async fn run_event_stream(
667 weak: Weak<Self>,
668 lifecycle: Weak<()>,
669 ready: tokio::sync::oneshot::Sender<Result<(), FrontendRuntimeError>>,
670 ) {
671 let Some(runtime) = weak.upgrade() else {
672 let _ = ready.send(Err(FrontendRuntimeError::Closed));
673 return;
674 };
675 let request = runtime
676 .client
677 .get(format!("{}/frontend/events", runtime.base_url))
678 .bearer_auth(&runtime.token)
679 .header("x-supercode-client-id", runtime.client_id.as_str())
680 .header(
681 "x-supercode-permissions",
682 runtime.authorization.header_value(),
683 );
684 let events = runtime.events.clone();
685 drop(runtime);
686 let response = request.send().await;
687 let response = match response {
688 Ok(response) if response.status().is_success() => response,
689 Ok(response) => {
690 let _ = ready.send(Err(FrontendRuntimeError::Transport(format!(
691 "frontend event stream returned {}",
692 response.status()
693 ))));
694 return;
695 }
696 Err(error) => {
697 let _ = ready.send(Err(FrontendRuntimeError::Transport(error.to_string())));
698 return;
699 }
700 };
701 let _ = ready.send(Ok(()));
702 let mut stream = response.bytes_stream();
703 let mut pending = Vec::<u8>::new();
704 let mut liveness = tokio::time::interval(std::time::Duration::from_millis(100));
705 loop {
706 let chunk = tokio::select! {
707 _ = liveness.tick() => {
708 if lifecycle.strong_count() == 0 {
709 break;
710 }
711 if weak
712 .upgrade()
713 .is_some_and(|runtime| runtime.disconnected.load(Ordering::SeqCst))
714 {
715 break;
716 }
717 continue;
718 }
719 chunk = stream.next() => chunk,
720 };
721 let Some(chunk) = chunk else {
722 break;
723 };
724 let Ok(chunk) = chunk else {
725 break;
726 };
727 pending.extend_from_slice(&chunk);
728 while let Some(position) = pending.iter().position(|byte| *byte == b'\n') {
729 let line = pending.drain(..=position).collect::<Vec<_>>();
730 let line = String::from_utf8_lossy(&line);
731 let Some(data) = line.trim_end().strip_prefix("data: ") else {
732 continue;
733 };
734 if let Ok(event) = serde_json::from_str::<FrontendEvent>(data) {
735 let _ = events.send(event);
736 }
737 }
738 }
739 if let Some(runtime) = weak.upgrade() {
740 runtime.disconnected.store(true, Ordering::SeqCst);
741 let _ = runtime.events.send(FrontendEvent::new(
742 u64::MAX,
743 json!({
744 "type": "runtime_disconnected",
745 "schema_version": FRONTEND_EVENT_SCHEMA_VERSION
746 }),
747 ));
748 }
749 }
750
751 async fn rpc(&self, method: &str, params: Value) -> Result<Value, FrontendRuntimeError> {
752 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
753 let requested_operation = params
754 .pointer("/operation/operation_id")
755 .and_then(Value::as_str)
756 .map(str::to_owned);
757 let response = self
758 .client
759 .post(format!("{}/rpc", self.base_url))
760 .bearer_auth(&self.token)
761 .header("x-supercode-client-id", self.client_id.as_str())
762 .header("x-supercode-permissions", self.authorization.header_value())
763 .json(&json!({"id": id, "method": method, "params": params}))
764 .send()
765 .await
766 .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
767 if !response.status().is_success() {
768 return Err(FrontendRuntimeError::Transport(format!(
769 "SDK HTTP RPC returned {}",
770 response.status()
771 )));
772 }
773 let value: Value = response
774 .json()
775 .await
776 .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
777 if let Some(error) = value.get("error") {
778 let code = error.get("code").and_then(Value::as_i64);
779 let name = error.get("name").and_then(Value::as_str);
780 let operation = error
781 .get("operation")
782 .and_then(Value::as_str)
783 .and_then(crate::SdkOperation::from_action_name);
784 let message = error
785 .get("message")
786 .and_then(Value::as_str)
787 .unwrap_or("SDK runtime request failed")
788 .to_string();
789 return Err(match (name, code) {
790 (Some("unauthenticated"), _) | (_, Some(-32030)) => {
791 FrontendRuntimeError::Unauthenticated
792 }
793 (Some("unauthorized"), _) | (_, Some(-32031)) => {
794 FrontendRuntimeError::Unauthorized {
795 permission: error
796 .get("permission")
797 .and_then(Value::as_str)
798 .unwrap_or("unknown")
799 .to_string(),
800 }
801 }
802 (Some("controller_required"), _) | (_, Some(-32032)) => {
803 FrontendRuntimeError::ControllerRequired {
804 holder: error
805 .get("holder")
806 .and_then(Value::as_str)
807 .map(str::to_owned),
808 expires_at_ms: error.get("expiresAtMs").and_then(Value::as_u64),
809 }
810 }
811 (Some("lease_expired"), _) | (_, Some(-32033)) => {
812 FrontendRuntimeError::LeaseExpired
813 }
814 (_, Some(-32023)) => FrontendRuntimeError::UnsupportedOperation(
815 requested_operation.unwrap_or(message),
816 ),
817 (Some("unsupported_action"), _) => FrontendRuntimeError::UnsupportedAction(
818 operation
819 .unwrap_or_else(|| {
820 crate::SdkOperation::from_action_name(method)
821 .unwrap_or(crate::SdkOperation::Respond)
822 })
823 .action_name(),
824 ),
825 (Some("not_found"), Some(-32021)) => {
826 let request_id = params
827 .pointer("/response/request_id")
828 .and_then(Value::as_u64)
829 .unwrap_or_default();
830 FrontendRuntimeError::UnknownRequest(request_id)
831 }
832 (Some("invalid_argument"), _) => FrontendRuntimeError::InvalidResponse(message),
833 (_, Some(-32000)) => RuntimeSubmitError::Busy.into(),
834 (_, Some(-32001)) => RuntimeSubmitError::Interrupted.into(),
835 (_, Some(-32002)) => RuntimeSubmitError::Agent(message).into(),
836 (_, Some(-32020)) => FrontendRuntimeError::UnsupportedAction(
837 crate::SdkOperation::from_action_name(method)
838 .unwrap_or(crate::SdkOperation::Respond)
839 .action_name(),
840 ),
841 (_, Some(-32021)) => {
842 let request_id = params
843 .pointer("/response/request_id")
844 .and_then(Value::as_u64)
845 .unwrap_or_default();
846 FrontendRuntimeError::UnknownRequest(request_id)
847 }
848 (_, Some(-32022)) => FrontendRuntimeError::InvalidResponse(message),
849 _ => FrontendRuntimeError::Transport(message),
850 });
851 }
852 Ok(value.get("result").cloned().unwrap_or(Value::Null))
853 }
854
855 async fn rpc_typed<T: serde::de::DeserializeOwned>(
856 &self,
857 method: &str,
858 params: Value,
859 ) -> Result<T, FrontendRuntimeError> {
860 serde_json::from_value(self.rpc(method, params).await?)
861 .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))
862 }
863
864 pub fn client_id(&self) -> &crate::RuntimeClientId {
866 &self.client_id
867 }
868
869 pub fn is_disconnected(&self) -> bool {
872 self.disconnected.load(Ordering::SeqCst)
873 }
874
875 pub async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
878 self.rpc_typed(
879 crate::FrontendFacadeMethod::TakeControl.wire_name(),
880 json!({}),
881 )
882 .await
883 }
884
885 pub async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
887 self.rpc_typed(
888 crate::FrontendFacadeMethod::Heartbeat.wire_name(),
889 json!({}),
890 )
891 .await
892 }
893
894 pub async fn lease_snapshot(
896 &self,
897 ) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
898 self.rpc_typed(crate::FrontendFacadeMethod::Lease.wire_name(), json!({}))
899 .await
900 }
901
902 pub async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
904 let snapshot = self
905 .rpc_typed(crate::FrontendFacadeMethod::Detach.wire_name(), json!({}))
906 .await?;
907 self.disconnected.store(true, Ordering::SeqCst);
908 Ok(snapshot)
909 }
910}
911
912#[async_trait]
913#[cfg(feature = "adapter-api")]
914impl FrontendRuntime for HttpFrontendRuntime {
915 async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
916 self.rpc_typed(crate::FrontendFacadeMethod::Describe.wire_name(), json!({}))
917 .await
918 }
919
920 async fn attach(
921 &self,
922 history_limit: usize,
923 ) -> Result<FrontendAttachment, FrontendRuntimeError> {
924 if self.disconnected.load(Ordering::SeqCst) {
925 return Err(FrontendRuntimeError::Closed);
926 }
927 let live = self.events.subscribe();
931 let snapshot: FrontendAttachSnapshot = self
932 .rpc_typed(
933 crate::FrontendFacadeMethod::Attach.wire_name(),
934 json!({"limit": history_limit}),
935 )
936 .await?;
937 Ok(FrontendAttachment::new(
938 snapshot.descriptor,
939 snapshot.history,
940 snapshot.history_cursor,
941 snapshot.replay,
942 live,
943 Some(self.lifecycle.clone()),
944 ))
945 }
946
947 async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), FrontendRuntimeError> {
948 self.rpc(
949 crate::FrontendFacadeMethod::SendInput.wire_name(),
950 json!({"prompt": prompt}),
951 )
952 .await?;
953 Ok(())
954 }
955
956 async fn submit(&self, prompt: String) -> Result<String, FrontendRuntimeError> {
957 let result = self
958 .rpc(
959 crate::FrontendFacadeMethod::Submit.wire_name(),
960 json!({"prompt": prompt}),
961 )
962 .await?;
963 Ok(result
964 .get("reply")
965 .and_then(Value::as_str)
966 .unwrap_or_default()
967 .to_string())
968 }
969
970 async fn submit_with_images(
971 &self,
972 prompt: String,
973 image_urls: Vec<String>,
974 ) -> Result<String, FrontendRuntimeError> {
975 let result = self
976 .rpc(
977 crate::FrontendFacadeMethod::Submit.wire_name(),
978 json!({"prompt": prompt, "image_urls": image_urls}),
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 interrupt(&self) -> Result<bool, FrontendRuntimeError> {
989 let result = self
990 .rpc(
991 crate::FrontendFacadeMethod::Interrupt.wire_name(),
992 json!({}),
993 )
994 .await?;
995 Ok(result
996 .get("interrupted")
997 .and_then(Value::as_bool)
998 .unwrap_or(false))
999 }
1000
1001 async fn steer(&self, prompt: String) -> Result<(), FrontendRuntimeError> {
1002 self.rpc(
1003 crate::FrontendFacadeMethod::Steer.wire_name(),
1004 json!({"prompt": prompt}),
1005 )
1006 .await?;
1007 Ok(())
1008 }
1009
1010 async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
1011 self.rpc(
1012 crate::FrontendFacadeMethod::Respond.wire_name(),
1013 json!({"response": response}),
1014 )
1015 .await?;
1016 Ok(())
1017 }
1018
1019 async fn invoke(
1020 &self,
1021 operation: FrontendOperationInvocation,
1022 ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
1023 self.rpc_typed(
1024 crate::FrontendFacadeMethod::Invoke.wire_name(),
1025 json!({"operation": operation}),
1026 )
1027 .await
1028 }
1029
1030 async fn lease_snapshot(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1031 HttpFrontendRuntime::lease_snapshot(self).await
1032 }
1033
1034 async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1035 HttpFrontendRuntime::take_control(self).await
1036 }
1037
1038 async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1039 HttpFrontendRuntime::heartbeat(self).await
1040 }
1041
1042 async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1043 HttpFrontendRuntime::detach(self).await
1044 }
1045
1046 async fn close(&self) -> Result<(), FrontendRuntimeError> {
1047 self.rpc(crate::FrontendFacadeMethod::Close.wire_name(), json!({}))
1048 .await?;
1049 self.disconnected.store(true, Ordering::SeqCst);
1050 Ok(())
1051 }
1052}