use std::collections::{BTreeMap, VecDeque};
#[cfg(feature = "adapter-api")]
use std::sync::atomic::AtomicBool;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
#[cfg(feature = "adapter-api")]
use std::sync::Weak;
use async_trait::async_trait;
#[cfg(feature = "adapter-api")]
use futures::StreamExt;
use serde::{Deserialize, Serialize};
#[cfg(feature = "adapter-api")]
use serde_json::json;
use serde_json::Value;
use tokio::sync::broadcast;
#[cfg(feature = "adapter-api")]
use crate::sdk::RuntimeSubmitError;
pub use crate::sdk::SdkError as FrontendRuntimeError;
pub use crate::sdk::SdkEvent as FrontendEvent;
pub use crate::sdk::SdkRuntime as FrontendRuntime;
use crate::server::RpcEngine;
use crate::ChatMessage;
pub const FRONTEND_RUNTIME_SCHEMA_VERSION: u32 = 2;
pub(crate) const FRONTEND_EVENT_SCHEMA_VERSION: u32 = 1;
pub const FRONTEND_REPLAY_CAPACITY: usize = 4096;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendTurnState {
Idle,
Busy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendConnectionState {
Connected,
ShuttingDown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendActions {
pub submit: bool,
pub interrupt: bool,
pub steer: bool,
pub respond: bool,
pub detach: bool,
pub close: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendDisplayCapabilities {
pub event_kinds: Vec<String>,
pub opaque_fallback: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendCommandDescriptor {
pub name: String,
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub argument_hint: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendOperationKind {
Prompt,
File,
Model,
Session,
Subagent,
Image,
Reduction,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendOperationDescriptor {
pub id: String,
pub kind: FrontendOperationKind,
pub command: Option<FrontendCommandDescriptor>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum FrontendOperationInvocation {
Prompt {
operation_id: String,
arguments: String,
},
}
impl FrontendOperationInvocation {
pub fn operation_id(&self) -> &str {
match self {
Self::Prompt { operation_id, .. } => operation_id,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum FrontendOperationResult {
Prompt {
reply: String,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendRuntimeMetadata {
pub source_harness: Option<String>,
pub emulation_profile: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontendRuntimeDescriptor {
pub schema_version: u32,
pub session_id: String,
pub source_harness: Option<String>,
pub emulation_profile: Option<String>,
pub active_modules: Vec<String>,
pub commands: Vec<FrontendCommandDescriptor>,
#[serde(default)]
pub operations: Vec<FrontendOperationDescriptor>,
pub actions: FrontendActions,
pub display: FrontendDisplayCapabilities,
pub model: String,
pub turn_state: FrontendTurnState,
pub connection_state: FrontendConnectionState,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub extensions: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FrontendAttachSnapshot {
pub descriptor: FrontendRuntimeDescriptor,
pub history: Vec<ChatMessage>,
pub history_cursor: u64,
pub replay: VecDeque<FrontendEvent>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendRequestKind {
Approval,
Elicitation,
#[serde(other)]
Other,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FrontendRequest {
pub id: u64,
pub kind: FrontendRequestKind,
pub payload: Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendApprovalDecision {
Deny,
Allow,
AllowForSession,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FrontendElicitationAction {
Accept,
Decline,
Cancel,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum FrontendResponse {
Approval {
request_id: u64,
decision: FrontendApprovalDecision,
},
Elicitation {
request_id: u64,
action: FrontendElicitationAction,
content: Option<Value>,
},
Other {
request_id: u64,
action: FrontendElicitationAction,
content: Option<Value>,
},
}
impl FrontendResponse {
pub(crate) fn request_id(&self) -> u64 {
match self {
Self::Approval { request_id, .. }
| Self::Elicitation { request_id, .. }
| Self::Other { request_id, .. } => *request_id,
}
}
}
pub struct FrontendAttachment {
pub descriptor: FrontendRuntimeDescriptor,
pub history: Vec<ChatMessage>,
pub history_cursor: u64,
pub(crate) replay: VecDeque<FrontendEvent>,
live: broadcast::Receiver<FrontendEvent>,
delivered: u64,
acknowledged: Option<Arc<AtomicU64>>,
_transport_lease: Option<Arc<()>>,
}
impl FrontendAttachment {
pub fn from_snapshot(
snapshot: FrontendAttachSnapshot,
live: broadcast::Receiver<FrontendEvent>,
) -> Self {
Self::from_snapshot_after(snapshot, live, 0)
}
pub fn from_snapshot_after(
snapshot: FrontendAttachSnapshot,
live: broadcast::Receiver<FrontendEvent>,
acknowledged_sequence: u64,
) -> Self {
let delivered = snapshot.history_cursor.max(acknowledged_sequence);
Self::new_with_delivered(
snapshot.descriptor,
snapshot.history,
snapshot.history_cursor,
snapshot.replay,
live,
None,
delivered,
)
}
pub(crate) fn new(
descriptor: FrontendRuntimeDescriptor,
history: Vec<ChatMessage>,
history_cursor: u64,
replay: VecDeque<FrontendEvent>,
live: broadcast::Receiver<FrontendEvent>,
transport_lease: Option<Arc<()>>,
) -> Self {
let delivered = history_cursor;
Self::new_with_delivered(
descriptor,
history,
history_cursor,
replay,
live,
transport_lease,
delivered,
)
}
fn new_with_delivered(
descriptor: FrontendRuntimeDescriptor,
history: Vec<ChatMessage>,
history_cursor: u64,
replay: VecDeque<FrontendEvent>,
live: broadcast::Receiver<FrontendEvent>,
transport_lease: Option<Arc<()>>,
delivered: u64,
) -> Self {
Self {
descriptor,
history,
history_cursor,
replay,
live,
delivered,
acknowledged: None,
_transport_lease: transport_lease,
}
}
#[cfg(feature = "adapter-acp")]
pub(crate) fn with_acknowledgement(mut self, acknowledged: Arc<AtomicU64>) -> Self {
acknowledged.fetch_max(self.history_cursor, Ordering::SeqCst);
self.acknowledged = Some(acknowledged);
self
}
fn acknowledge(&self, event: &FrontendEvent) {
if !event_advances_acknowledgement(event) {
return;
}
if let Some(acknowledged) = &self.acknowledged {
acknowledged.fetch_max(event.sequence, Ordering::SeqCst);
}
}
pub async fn next_event(&mut self) -> Result<FrontendEvent, FrontendRuntimeError> {
loop {
let event = match self.next_replay_event() {
Some(event) => return Ok(event),
None => match self.live.recv().await {
Ok(event) => event,
Err(broadcast::error::RecvError::Lagged(count)) => {
return Err(FrontendRuntimeError::ReplayGap(count));
}
Err(broadcast::error::RecvError::Closed) => {
return Err(FrontendRuntimeError::Closed);
}
},
};
if event.sequence <= self.delivered {
continue;
}
self.delivered = event.sequence;
self.acknowledge(&event);
return Ok(event);
}
}
pub fn next_replay_event(&mut self) -> Option<FrontendEvent> {
while let Some(event) = self.replay.pop_front() {
if event.sequence <= self.delivered {
continue;
}
self.delivered = event.sequence;
self.acknowledge(&event);
return Some(event);
}
None
}
}
pub(crate) fn event_advances_acknowledgement(event: &FrontendEvent) -> bool {
event
.payload
.pointer("/_meta/supercode/transient")
.and_then(Value::as_bool)
!= Some(true)
}
pub(crate) struct FrontendProjectionState {
pub(crate) history: Vec<ChatMessage>,
pub(crate) history_cursor: u64,
pub(crate) next_sequence: u64,
pub(crate) replay: VecDeque<FrontendEvent>,
}
#[async_trait]
impl FrontendRuntime for RpcEngine {
async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
Ok(self.frontend_descriptor())
}
async fn attach(
&self,
history_limit: usize,
) -> Result<FrontendAttachment, FrontendRuntimeError> {
self.frontend_attach(history_limit)
}
async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), FrontendRuntimeError> {
RpcEngine::send_input(&self, prompt)?;
Ok(())
}
async fn submit(&self, prompt: String) -> Result<String, FrontendRuntimeError> {
Ok(RpcEngine::submit(self, prompt).await?)
}
async fn submit_with_images(
&self,
prompt: String,
image_urls: Vec<String>,
) -> Result<String, FrontendRuntimeError> {
Ok(RpcEngine::submit_with_images(self, prompt, image_urls).await?)
}
async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
Ok(RpcEngine::interrupt(self).await)
}
async fn steer(&self, prompt: String) -> Result<(), FrontendRuntimeError> {
RpcEngine::steer(self, prompt)
}
async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
RpcEngine::respond(self, response)
}
async fn invoke(
&self,
operation: FrontendOperationInvocation,
) -> Result<FrontendOperationResult, FrontendRuntimeError> {
RpcEngine::invoke(self, operation).await
}
async fn close(&self) -> Result<(), FrontendRuntimeError> {
RpcEngine::shutdown(self).await;
Ok(())
}
}
#[cfg(feature = "adapter-api")]
pub struct HttpFrontendRuntime {
base_url: String,
token: String,
client_id: crate::RuntimeClientId,
authorization: crate::RuntimeAuthorization,
client: reqwest::Client,
events: broadcast::Sender<FrontendEvent>,
next_id: AtomicU64,
lifecycle: Arc<()>,
disconnected: AtomicBool,
}
#[cfg(feature = "adapter-api")]
impl HttpFrontendRuntime {
pub async fn connect(
base_url: impl Into<String>,
token: impl Into<String>,
) -> Result<Arc<Self>, FrontendRuntimeError> {
let mut random = [0_u8; 16];
getrandom::getrandom(&mut random).map_err(|error| {
FrontendRuntimeError::Transport(format!(
"cannot generate runtime client identity: {error}"
))
})?;
let suffix = random
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
let client_id = crate::RuntimeClientId::parse(format!("http-{suffix}"))
.map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
Self::connect_with_client_id(base_url, token, client_id).await
}
pub async fn connect_with_client_id(
base_url: impl Into<String>,
token: impl Into<String>,
client_id: crate::RuntimeClientId,
) -> Result<Arc<Self>, FrontendRuntimeError> {
Self::connect_with_authorization(
base_url,
token,
client_id,
crate::RuntimeAuthorization::owner(),
)
.await
}
pub async fn connect_with_authorization(
base_url: impl Into<String>,
token: impl Into<String>,
client_id: crate::RuntimeClientId,
authorization: crate::RuntimeAuthorization,
) -> Result<Arc<Self>, FrontendRuntimeError> {
Self::connect_inner(base_url, token, client_id, authorization, true)
.await
.map(|(runtime, _)| runtime)
}
pub(crate) async fn probe_described(
base_url: impl Into<String>,
token: impl Into<String>,
client_id: crate::RuntimeClientId,
) -> Result<(Arc<Self>, FrontendRuntimeDescriptor), FrontendRuntimeError> {
Self::connect_inner(
base_url,
token,
client_id,
crate::RuntimeAuthorization::observer(),
false,
)
.await
}
async fn connect_inner(
base_url: impl Into<String>,
token: impl Into<String>,
client_id: crate::RuntimeClientId,
authorization: crate::RuntimeAuthorization,
stream_events: bool,
) -> Result<(Arc<Self>, FrontendRuntimeDescriptor), FrontendRuntimeError> {
let runtime = Arc::new(Self {
base_url: base_url.into().trim_end_matches('/').to_string(),
token: token.into(),
client_id,
authorization,
client: reqwest::Client::new(),
events: broadcast::channel(1024).0,
next_id: AtomicU64::new(1),
lifecycle: Arc::new(()),
disconnected: AtomicBool::new(false),
});
let descriptor: FrontendRuntimeDescriptor = runtime
.rpc_typed(crate::FrontendFacadeMethod::Describe.wire_name(), json!({}))
.await?;
if stream_events {
Self::start_event_stream(&runtime).await?;
}
Ok((runtime, descriptor))
}
async fn start_event_stream(runtime: &Arc<Self>) -> Result<(), FrontendRuntimeError> {
let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
let weak = Arc::downgrade(runtime);
let lifecycle = Arc::downgrade(&runtime.lifecycle);
tokio::spawn(async move {
Self::run_event_stream(weak, lifecycle, ready_tx).await;
});
ready_rx.await.map_err(|_| {
FrontendRuntimeError::Transport("frontend event stream exited before startup".into())
})?
}
async fn run_event_stream(
weak: Weak<Self>,
lifecycle: Weak<()>,
ready: tokio::sync::oneshot::Sender<Result<(), FrontendRuntimeError>>,
) {
let Some(runtime) = weak.upgrade() else {
let _ = ready.send(Err(FrontendRuntimeError::Closed));
return;
};
let request = runtime
.client
.get(format!("{}/frontend/events", runtime.base_url))
.bearer_auth(&runtime.token)
.header("x-supercode-client-id", runtime.client_id.as_str())
.header(
"x-supercode-permissions",
runtime.authorization.header_value(),
);
let events = runtime.events.clone();
drop(runtime);
let response = request.send().await;
let response = match response {
Ok(response) if response.status().is_success() => response,
Ok(response) => {
let _ = ready.send(Err(FrontendRuntimeError::Transport(format!(
"frontend event stream returned {}",
response.status()
))));
return;
}
Err(error) => {
let _ = ready.send(Err(FrontendRuntimeError::Transport(error.to_string())));
return;
}
};
let _ = ready.send(Ok(()));
let mut stream = response.bytes_stream();
let mut pending = Vec::<u8>::new();
let mut liveness = tokio::time::interval(std::time::Duration::from_millis(100));
loop {
let chunk = tokio::select! {
_ = liveness.tick() => {
if lifecycle.strong_count() == 0 {
break;
}
if weak
.upgrade()
.is_some_and(|runtime| runtime.disconnected.load(Ordering::SeqCst))
{
break;
}
continue;
}
chunk = stream.next() => chunk,
};
let Some(chunk) = chunk else {
break;
};
let Ok(chunk) = chunk else {
break;
};
pending.extend_from_slice(&chunk);
while let Some(position) = pending.iter().position(|byte| *byte == b'\n') {
let line = pending.drain(..=position).collect::<Vec<_>>();
let line = String::from_utf8_lossy(&line);
let Some(data) = line.trim_end().strip_prefix("data: ") else {
continue;
};
if let Ok(event) = serde_json::from_str::<FrontendEvent>(data) {
let _ = events.send(event);
}
}
}
if let Some(runtime) = weak.upgrade() {
runtime.disconnected.store(true, Ordering::SeqCst);
let _ = runtime.events.send(FrontendEvent::new(
u64::MAX,
json!({
"type": "runtime_disconnected",
"schema_version": FRONTEND_EVENT_SCHEMA_VERSION
}),
));
}
}
async fn rpc(&self, method: &str, params: Value) -> Result<Value, FrontendRuntimeError> {
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
let requested_operation = params
.pointer("/operation/operation_id")
.and_then(Value::as_str)
.map(str::to_owned);
let response = self
.client
.post(format!("{}/rpc", self.base_url))
.bearer_auth(&self.token)
.header("x-supercode-client-id", self.client_id.as_str())
.header("x-supercode-permissions", self.authorization.header_value())
.json(&json!({"id": id, "method": method, "params": params}))
.send()
.await
.map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
if !response.status().is_success() {
return Err(FrontendRuntimeError::Transport(format!(
"SDK HTTP RPC returned {}",
response.status()
)));
}
let value: Value = response
.json()
.await
.map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
if let Some(error) = value.get("error") {
let code = error.get("code").and_then(Value::as_i64);
let name = error.get("name").and_then(Value::as_str);
let operation = error
.get("operation")
.and_then(Value::as_str)
.and_then(crate::SdkOperation::from_action_name);
let message = error
.get("message")
.and_then(Value::as_str)
.unwrap_or("SDK runtime request failed")
.to_string();
return Err(match (name, code) {
(Some("unauthenticated"), _) | (_, Some(-32030)) => {
FrontendRuntimeError::Unauthenticated
}
(Some("unauthorized"), _) | (_, Some(-32031)) => {
FrontendRuntimeError::Unauthorized {
permission: error
.get("permission")
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string(),
}
}
(Some("controller_required"), _) | (_, Some(-32032)) => {
FrontendRuntimeError::ControllerRequired {
holder: error
.get("holder")
.and_then(Value::as_str)
.map(str::to_owned),
expires_at_ms: error.get("expiresAtMs").and_then(Value::as_u64),
}
}
(Some("lease_expired"), _) | (_, Some(-32033)) => {
FrontendRuntimeError::LeaseExpired
}
(_, Some(-32023)) => FrontendRuntimeError::UnsupportedOperation(
requested_operation.unwrap_or(message),
),
(Some("unsupported_action"), _) => FrontendRuntimeError::UnsupportedAction(
operation
.unwrap_or_else(|| {
crate::SdkOperation::from_action_name(method)
.unwrap_or(crate::SdkOperation::Respond)
})
.action_name(),
),
(Some("not_found"), Some(-32021)) => {
let request_id = params
.pointer("/response/request_id")
.and_then(Value::as_u64)
.unwrap_or_default();
FrontendRuntimeError::UnknownRequest(request_id)
}
(Some("invalid_argument"), _) => FrontendRuntimeError::InvalidResponse(message),
(_, Some(-32000)) => RuntimeSubmitError::Busy.into(),
(_, Some(-32001)) => RuntimeSubmitError::Interrupted.into(),
(_, Some(-32002)) => RuntimeSubmitError::Agent(message).into(),
(_, Some(-32020)) => FrontendRuntimeError::UnsupportedAction(
crate::SdkOperation::from_action_name(method)
.unwrap_or(crate::SdkOperation::Respond)
.action_name(),
),
(_, Some(-32021)) => {
let request_id = params
.pointer("/response/request_id")
.and_then(Value::as_u64)
.unwrap_or_default();
FrontendRuntimeError::UnknownRequest(request_id)
}
(_, Some(-32022)) => FrontendRuntimeError::InvalidResponse(message),
_ => FrontendRuntimeError::Transport(message),
});
}
Ok(value.get("result").cloned().unwrap_or(Value::Null))
}
async fn rpc_typed<T: serde::de::DeserializeOwned>(
&self,
method: &str,
params: Value,
) -> Result<T, FrontendRuntimeError> {
serde_json::from_value(self.rpc(method, params).await?)
.map_err(|error| FrontendRuntimeError::Transport(error.to_string()))
}
pub fn client_id(&self) -> &crate::RuntimeClientId {
&self.client_id
}
pub fn is_disconnected(&self) -> bool {
self.disconnected.load(Ordering::SeqCst)
}
pub async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
self.rpc_typed(
crate::FrontendFacadeMethod::TakeControl.wire_name(),
json!({}),
)
.await
}
pub async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
self.rpc_typed(
crate::FrontendFacadeMethod::Heartbeat.wire_name(),
json!({}),
)
.await
}
pub async fn lease_snapshot(
&self,
) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
self.rpc_typed(crate::FrontendFacadeMethod::Lease.wire_name(), json!({}))
.await
}
pub async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
let snapshot = self
.rpc_typed(crate::FrontendFacadeMethod::Detach.wire_name(), json!({}))
.await?;
self.disconnected.store(true, Ordering::SeqCst);
Ok(snapshot)
}
}
#[async_trait]
#[cfg(feature = "adapter-api")]
impl FrontendRuntime for HttpFrontendRuntime {
async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
self.rpc_typed(crate::FrontendFacadeMethod::Describe.wire_name(), json!({}))
.await
}
async fn attach(
&self,
history_limit: usize,
) -> Result<FrontendAttachment, FrontendRuntimeError> {
if self.disconnected.load(Ordering::SeqCst) {
return Err(FrontendRuntimeError::Closed);
}
let live = self.events.subscribe();
let snapshot: FrontendAttachSnapshot = self
.rpc_typed(
crate::FrontendFacadeMethod::Attach.wire_name(),
json!({"limit": history_limit}),
)
.await?;
Ok(FrontendAttachment::new(
snapshot.descriptor,
snapshot.history,
snapshot.history_cursor,
snapshot.replay,
live,
Some(self.lifecycle.clone()),
))
}
async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), FrontendRuntimeError> {
self.rpc(
crate::FrontendFacadeMethod::SendInput.wire_name(),
json!({"prompt": prompt}),
)
.await?;
Ok(())
}
async fn submit(&self, prompt: String) -> Result<String, FrontendRuntimeError> {
let result = self
.rpc(
crate::FrontendFacadeMethod::Submit.wire_name(),
json!({"prompt": prompt}),
)
.await?;
Ok(result
.get("reply")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string())
}
async fn submit_with_images(
&self,
prompt: String,
image_urls: Vec<String>,
) -> Result<String, FrontendRuntimeError> {
let result = self
.rpc(
crate::FrontendFacadeMethod::Submit.wire_name(),
json!({"prompt": prompt, "image_urls": image_urls}),
)
.await?;
Ok(result
.get("reply")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string())
}
async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
let result = self
.rpc(
crate::FrontendFacadeMethod::Interrupt.wire_name(),
json!({}),
)
.await?;
Ok(result
.get("interrupted")
.and_then(Value::as_bool)
.unwrap_or(false))
}
async fn steer(&self, prompt: String) -> Result<(), FrontendRuntimeError> {
self.rpc(
crate::FrontendFacadeMethod::Steer.wire_name(),
json!({"prompt": prompt}),
)
.await?;
Ok(())
}
async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
self.rpc(
crate::FrontendFacadeMethod::Respond.wire_name(),
json!({"response": response}),
)
.await?;
Ok(())
}
async fn invoke(
&self,
operation: FrontendOperationInvocation,
) -> Result<FrontendOperationResult, FrontendRuntimeError> {
self.rpc_typed(
crate::FrontendFacadeMethod::Invoke.wire_name(),
json!({"operation": operation}),
)
.await
}
async fn lease_snapshot(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
HttpFrontendRuntime::lease_snapshot(self).await
}
async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
HttpFrontendRuntime::take_control(self).await
}
async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
HttpFrontendRuntime::heartbeat(self).await
}
async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
HttpFrontendRuntime::detach(self).await
}
async fn close(&self) -> Result<(), FrontendRuntimeError> {
self.rpc(crate::FrontendFacadeMethod::Close.wire_name(), json!({}))
.await?;
self.disconnected.store(true, Ordering::SeqCst);
Ok(())
}
}