Skip to main content

supercode/
frontend.rs

1//! Protocol-neutral frontend contract for one SDK-owned Supercode runtime.
2//!
3//! Terminal, HTTP, ACP, and future browser frontends consume this contract;
4//! none of them owns an [`crate::Agent`] or a second model loop.  Events keep
5//! their complete JSON payload and gain a monotonic sequence so a frontend can
6//! cross the history-replay/live-stream boundary without duplicates.
7
8use 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
33/// Frontend contract schema version.
34pub const FRONTEND_RUNTIME_SCHEMA_VERSION: u32 = 2;
35
36/// Runtime lifecycle-event schema version.
37///
38/// Operation descriptors evolve the attach contract independently from the
39/// established event payloads consumed by machine frontends.
40pub(crate) const FRONTEND_EVENT_SCHEMA_VERSION: u32 = 1;
41
42/// Maximum sequenced events retained between canonical history snapshots.
43pub const FRONTEND_REPLAY_CAPACITY: usize = 4096;
44
45/// Whether a model/tool turn currently owns the runtime.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum FrontendTurnState {
49    /// The runtime accepts a new turn.
50    Idle,
51    /// A user, scheduler, or tool turn is active.
52    Busy,
53}
54
55/// Frontend-visible runtime connection state.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum FrontendConnectionState {
59    /// The SDK runtime is reachable.
60    Connected,
61    /// Graceful shutdown has been requested.
62    ShuttingDown,
63}
64
65/// Actions the current runtime adapter can actually perform.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct FrontendActions {
68    /// Submit a new user turn.
69    pub submit: bool,
70    /// Interrupt an active turn.
71    pub interrupt: bool,
72    /// Queue a steering instruction during a turn.
73    pub steer: bool,
74    /// Answer an approval, elicitation, or other protocol request.
75    pub respond: bool,
76    /// Detach without stopping the runtime.
77    pub detach: bool,
78    /// Close the SDK-owned runtime.
79    pub close: bool,
80}
81
82/// Display semantics emitted by the runtime.
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct FrontendDisplayCapabilities {
85    /// Known normalized event kinds at this schema version.
86    pub event_kinds: Vec<String>,
87    /// Whether unknown payloads remain available for generic rendering.
88    pub opaque_fallback: bool,
89}
90
91/// One runtime-provided command surfaced by a composer.
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93pub struct FrontendCommandDescriptor {
94    /// Command name without the leading slash.
95    pub name: String,
96    /// Optional short help text.
97    pub description: Option<String>,
98    /// Optional argument usage shown beside the command.
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub argument_hint: Option<String>,
101}
102
103/// Stable family for an explicitly invocable frontend operation.
104///
105/// Families without a production [`FrontendRuntime::invoke`] implementation
106/// are never advertised. Keeping the full vocabulary here lets frontends
107/// render future file/model/session/subagent/image/reduction controls from the
108/// catalog without inferring them from composable modules.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub enum FrontendOperationKind {
112    /// Invoke a trusted runtime prompt template.
113    Prompt,
114    /// Attach or inspect a file through a typed runtime route.
115    File,
116    /// Inspect or switch the active model through a typed runtime route.
117    Model,
118    /// Perform a session operation through a typed runtime route.
119    Session,
120    /// Perform a subagent operation through a typed runtime route.
121    Subagent,
122    /// Attach an image through a typed runtime route.
123    Image,
124    /// Perform a reversible reduction operation through a typed runtime route.
125    Reduction,
126}
127
128/// One operation the runtime can genuinely invoke.
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct FrontendOperationDescriptor {
131    /// Stable runtime-scoped identifier supplied back during invocation.
132    pub id: String,
133    /// Typed operation family.
134    pub kind: FrontendOperationKind,
135    /// Optional slash-command trigger rendered by terminal composers.
136    pub command: Option<FrontendCommandDescriptor>,
137}
138
139/// Typed invocation accepted by [`FrontendRuntime::invoke`].
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(tag = "kind", rename_all = "snake_case")]
142pub enum FrontendOperationInvocation {
143    /// Expand and submit one advertised trusted prompt template.
144    Prompt {
145        /// Identifier from [`FrontendOperationDescriptor::id`].
146        operation_id: String,
147        /// Free text replacing the prompt template's `{args}` placeholder.
148        arguments: String,
149    },
150}
151
152impl FrontendOperationInvocation {
153    /// Identifier supplied by the runtime catalog.
154    pub fn operation_id(&self) -> &str {
155        match self {
156            Self::Prompt { operation_id, .. } => operation_id,
157        }
158    }
159}
160
161/// Typed result returned by [`FrontendRuntime::invoke`].
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(tag = "kind", rename_all = "snake_case")]
164pub enum FrontendOperationResult {
165    /// Reply from a prompt-template turn.
166    Prompt {
167        /// Final assistant reply.
168        reply: String,
169    },
170}
171
172/// Source/emulation identity supplied by the session-loading surface.
173#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
174pub struct FrontendRuntimeMetadata {
175    /// Source harness whose session semantics are being continued.
176    pub source_harness: Option<String>,
177    /// Resolved composable preset/profile name, when one was selected.
178    pub emulation_profile: Option<String>,
179}
180
181/// Complete frontend-facing description of one SDK-owned runtime.
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183pub struct FrontendRuntimeDescriptor {
184    /// Contract schema version.
185    pub schema_version: u32,
186    /// Stable SDK runtime/session identity.
187    pub session_id: String,
188    /// Source harness whose semantics are being emulated.
189    pub source_harness: Option<String>,
190    /// Resolved composable preset/profile name.
191    pub emulation_profile: Option<String>,
192    /// Active composable modules, using their stable config keys.
193    pub active_modules: Vec<String>,
194    /// Runtime-provided composer commands.
195    pub commands: Vec<FrontendCommandDescriptor>,
196    /// Explicit typed operation catalog. Missing on schema-v1 peers.
197    #[serde(default)]
198    pub operations: Vec<FrontendOperationDescriptor>,
199    /// Supported control actions.
200    pub actions: FrontendActions,
201    /// Display/event capabilities.
202    pub display: FrontendDisplayCapabilities,
203    /// Current model label.
204    pub model: String,
205    /// Current turn state.
206    pub turn_state: FrontendTurnState,
207    /// Current connection state.
208    pub connection_state: FrontendConnectionState,
209    /// Compatible client/adapter metadata with no canonical-session meaning.
210    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
211    pub extensions: BTreeMap<String, Value>,
212}
213
214/// Serializable half of an attachment returned by an out-of-process runtime.
215/// The live receiver is transport-owned and joined to this snapshot locally.
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
217pub struct FrontendAttachSnapshot {
218    /// Runtime description captured at attachment time.
219    pub descriptor: FrontendRuntimeDescriptor,
220    /// Bounded canonical history through `history_cursor`.
221    pub history: Vec<ChatMessage>,
222    /// Highest event sequence represented by `history`.
223    pub history_cursor: u64,
224    /// Events after the canonical history boundary and before the response.
225    pub replay: VecDeque<FrontendEvent>,
226}
227
228/// Kind of interactive request surfaced by the SDK runtime.
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
230#[serde(rename_all = "snake_case")]
231pub enum FrontendRequestKind {
232    /// A tool or sandbox action needs a policy-authorized human decision.
233    Approval,
234    /// An MCP server requested structured user input.
235    Elicitation,
236    /// Another versioned runtime request not known to this frontend build.
237    /// Its complete payload remains available for a generic overlay.
238    #[serde(other)]
239    Other,
240}
241
242/// One pending interactive request, emitted as a sequenced frontend event.
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244pub struct FrontendRequest {
245    /// Runtime-scoped request identifier used exactly once by `respond`.
246    pub id: u64,
247    /// Typed request category.
248    pub kind: FrontendRequestKind,
249    /// Complete request payload, including raw tool arguments or schema.
250    pub payload: Value,
251}
252
253/// Typed approval decision accepted by [`FrontendRuntime::respond`].
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
255#[serde(rename_all = "snake_case")]
256pub enum FrontendApprovalDecision {
257    /// Refuse this request.
258    Deny,
259    /// Allow only this request.
260    Allow,
261    /// Allow this request and cache the exact policy key for the session.
262    AllowForSession,
263}
264
265/// MCP elicitation outcome accepted by a frontend response.
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
267#[serde(rename_all = "snake_case")]
268pub enum FrontendElicitationAction {
269    /// Submit structured content.
270    Accept,
271    /// Explicitly decline the request.
272    Decline,
273    /// Dismiss the request without a decision.
274    Cancel,
275}
276
277/// Typed response to one SDK-owned interactive request.
278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279#[serde(tag = "kind", rename_all = "snake_case")]
280pub enum FrontendResponse {
281    /// Answer an approval request.
282    Approval {
283        /// Identifier from [`FrontendRequest::id`].
284        request_id: u64,
285        /// Human decision.
286        decision: FrontendApprovalDecision,
287    },
288    /// Answer an MCP elicitation request.
289    Elicitation {
290        /// Identifier from [`FrontendRequest::id`].
291        request_id: u64,
292        /// MCP elicitation outcome.
293        action: FrontendElicitationAction,
294        /// Structured content for `accept`.
295        content: Option<Value>,
296    },
297    /// Answer a generic runtime request without discarding its payload.
298    Other {
299        /// Identifier from [`FrontendRequest::id`].
300        request_id: u64,
301        /// Generic accept/decline/cancel outcome.
302        action: FrontendElicitationAction,
303        /// Optional structured response content.
304        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
318/// Atomic history/replay/live attachment to one runtime.
319pub struct FrontendAttachment {
320    /// Runtime description captured at attachment time.
321    pub descriptor: FrontendRuntimeDescriptor,
322    /// Bounded canonical history through `history_cursor`.
323    pub history: Vec<ChatMessage>,
324    /// Highest event sequence already represented by `history`.
325    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    /// Build an in-process attachment from a serialized snapshot and a live
335    /// SDK event receiver. Runtime adapters use this constructor in tests and
336    /// protocol bridges without acquiring transport ownership.
337    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    /// Build an attachment that resumes after a sequence acknowledged by a
345    /// prior transport connection. Snapshot replay and any overlapping live
346    /// events at or below the cursor are skipped without changing canonical
347    /// history or event payloads.
348    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    /// Receive the next event not already represented by the history or a
422    /// prior replay item. Duplicate events queued during attachment are
423    /// skipped by sequence.
424    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    /// Drain one event from the finite attachment replay without waiting for
448    /// live input. Interactive frontends use this to project the complete
449    /// atomic snapshot before accepting keystrokes, so a historical resolved
450    /// request never appears transiently actionable.
451    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
472/// State protected by `RpcEngine`'s short synchronous projection lock.
473pub(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/// Authenticated HTTP implementation of [`FrontendRuntime`].
536///
537/// It owns only an RPC/SSE connection. The remote [`RpcEngine`] remains the
538/// sole owner of the agent loop, transcript, scheduler, and persistence.
539#[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    /// Authenticate, verify the frontend descriptor, and establish the
555    /// sequenced SSE stream before returning.
556    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    /// Connect with a caller-owned stable client identity. Reconnect tests
576    /// and external bindings use this to retain deterministic lease state.
577    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    /// Connect while requesting an exact subset of the bearer credential's
592    /// permissions. The server intersects this with the authenticated grant;
593    /// this header can narrow authority but can never elevate it.
594    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    /// Authenticated metadata probe that does not open an event stream or
606    /// register an observer, returning the descriptor the connect handshake
607    /// already fetched. Used by the local runtime registry, which runs this on
608    /// every `harness serve` tick for every followed session: asking the same
609    /// runtime to describe itself twice for one read is pure load on that
610    /// path, and each round trip costs its own loopback connection.
611    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        // Validate auth and schema before opening a long-lived connection.
645        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    /// Current authenticated client identity.
865    pub fn client_id(&self) -> &crate::RuntimeClientId {
866        &self.client_id
867    }
868
869    /// Whether the remote event stream has already ended or this client
870    /// explicitly detached/closed.
871    pub fn is_disconnected(&self) -> bool {
872        self.disconnected.load(Ordering::SeqCst)
873    }
874
875    /// Explicitly acquire the controller lease, displacing another
876    /// interactive client only through this named operation.
877    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    /// Renew observer activity and any controller lease owned by this client.
886    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    /// Read the coordinated ownership state.
895    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    /// Release observer and controller state without stopping the runtime.
903    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        // Subscribe locally before asking the server for its atomic snapshot.
928        // Anything concurrently received over SSE is either in snapshot.replay
929        // or queued here; sequence filtering removes the overlap.
930        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}