Skip to main content

supercode_harness/
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    #[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    /// Receive the next event not already represented by the history or a
423    /// prior replay item. Duplicate events queued during attachment are
424    /// skipped by sequence.
425    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    /// Drain one event from the finite attachment replay without waiting for
449    /// live input. Interactive frontends use this to project the complete
450    /// atomic snapshot before accepting keystrokes, so a historical resolved
451    /// request never appears transiently actionable.
452    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
473/// State protected by `RpcEngine`'s short synchronous projection lock.
474pub(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 submit(&self, prompt: String) -> Result<String, FrontendRuntimeError> {
500        Ok(RpcEngine::submit(self, prompt).await?)
501    }
502
503    async fn submit_with_images(
504        &self,
505        prompt: String,
506        image_urls: Vec<String>,
507    ) -> Result<String, FrontendRuntimeError> {
508        Ok(RpcEngine::submit_with_images(self, prompt, image_urls).await?)
509    }
510
511    async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
512        Ok(RpcEngine::interrupt(self).await)
513    }
514
515    async fn steer(&self, prompt: String) -> Result<(), FrontendRuntimeError> {
516        RpcEngine::steer(self, prompt)
517    }
518
519    async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
520        RpcEngine::respond(self, response)
521    }
522
523    async fn invoke(
524        &self,
525        operation: FrontendOperationInvocation,
526    ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
527        RpcEngine::invoke(self, operation).await
528    }
529
530    async fn close(&self) -> Result<(), FrontendRuntimeError> {
531        RpcEngine::shutdown(self).await;
532        Ok(())
533    }
534}
535
536/// Authenticated HTTP implementation of [`FrontendRuntime`].
537///
538/// It owns only an RPC/SSE connection. The remote [`RpcEngine`] remains the
539/// sole owner of the agent loop, transcript, scheduler, and persistence.
540#[cfg(feature = "adapter-api")]
541pub struct HttpFrontendRuntime {
542    base_url: String,
543    token: String,
544    client_id: crate::RuntimeClientId,
545    authorization: crate::RuntimeAuthorization,
546    client: reqwest::Client,
547    events: broadcast::Sender<FrontendEvent>,
548    next_id: AtomicU64,
549    lifecycle: Arc<()>,
550    disconnected: AtomicBool,
551}
552
553#[cfg(feature = "adapter-api")]
554impl HttpFrontendRuntime {
555    /// Authenticate, verify the frontend descriptor, and establish the
556    /// sequenced SSE stream before returning.
557    pub async fn connect(
558        base_url: impl Into<String>,
559        token: impl Into<String>,
560    ) -> Result<Arc<Self>, FrontendRuntimeError> {
561        let mut random = [0_u8; 16];
562        getrandom::getrandom(&mut random).map_err(|error| {
563            FrontendRuntimeError::Transport(format!(
564                "cannot generate runtime client identity: {error}"
565            ))
566        })?;
567        let suffix = random
568            .iter()
569            .map(|byte| format!("{byte:02x}"))
570            .collect::<String>();
571        let client_id = crate::RuntimeClientId::parse(format!("http-{suffix}"))
572            .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
573        Self::connect_with_client_id(base_url, token, client_id).await
574    }
575
576    /// Connect with a caller-owned stable client identity. Reconnect tests
577    /// and external bindings use this to retain deterministic lease state.
578    pub async fn connect_with_client_id(
579        base_url: impl Into<String>,
580        token: impl Into<String>,
581        client_id: crate::RuntimeClientId,
582    ) -> Result<Arc<Self>, FrontendRuntimeError> {
583        Self::connect_with_authorization(
584            base_url,
585            token,
586            client_id,
587            crate::RuntimeAuthorization::owner(),
588        )
589        .await
590    }
591
592    /// Connect while requesting an exact subset of the bearer credential's
593    /// permissions. The server intersects this with the authenticated grant;
594    /// this header can narrow authority but can never elevate it.
595    pub async fn connect_with_authorization(
596        base_url: impl Into<String>,
597        token: impl Into<String>,
598        client_id: crate::RuntimeClientId,
599        authorization: crate::RuntimeAuthorization,
600    ) -> Result<Arc<Self>, FrontendRuntimeError> {
601        Self::connect_inner(base_url, token, client_id, authorization, true)
602            .await
603            .map(|(runtime, _)| runtime)
604    }
605
606    /// Authenticated metadata probe that does not open an event stream or
607    /// register an observer, returning the descriptor the connect handshake
608    /// already fetched. Used by the local runtime registry, which runs this on
609    /// every `harness serve` tick for every followed session: asking the same
610    /// runtime to describe itself twice for one read is pure load on that
611    /// path, and each round trip costs its own loopback connection.
612    pub(crate) async fn probe_described(
613        base_url: impl Into<String>,
614        token: impl Into<String>,
615        client_id: crate::RuntimeClientId,
616    ) -> Result<(Arc<Self>, FrontendRuntimeDescriptor), FrontendRuntimeError> {
617        Self::connect_inner(
618            base_url,
619            token,
620            client_id,
621            crate::RuntimeAuthorization::observer(),
622            false,
623        )
624        .await
625    }
626
627    async fn connect_inner(
628        base_url: impl Into<String>,
629        token: impl Into<String>,
630        client_id: crate::RuntimeClientId,
631        authorization: crate::RuntimeAuthorization,
632        stream_events: bool,
633    ) -> Result<(Arc<Self>, FrontendRuntimeDescriptor), FrontendRuntimeError> {
634        let runtime = Arc::new(Self {
635            base_url: base_url.into().trim_end_matches('/').to_string(),
636            token: token.into(),
637            client_id,
638            authorization,
639            client: reqwest::Client::new(),
640            events: broadcast::channel(1024).0,
641            next_id: AtomicU64::new(1),
642            lifecycle: Arc::new(()),
643            disconnected: AtomicBool::new(false),
644        });
645        // Validate auth and schema before opening a long-lived connection.
646        let descriptor: FrontendRuntimeDescriptor = runtime
647            .rpc_typed(crate::FrontendFacadeMethod::Describe.wire_name(), json!({}))
648            .await?;
649        if stream_events {
650            Self::start_event_stream(&runtime).await?;
651        }
652        Ok((runtime, descriptor))
653    }
654
655    async fn start_event_stream(runtime: &Arc<Self>) -> Result<(), FrontendRuntimeError> {
656        let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
657        let weak = Arc::downgrade(runtime);
658        let lifecycle = Arc::downgrade(&runtime.lifecycle);
659        tokio::spawn(async move {
660            Self::run_event_stream(weak, lifecycle, ready_tx).await;
661        });
662        ready_rx.await.map_err(|_| {
663            FrontendRuntimeError::Transport("frontend event stream exited before startup".into())
664        })?
665    }
666
667    async fn run_event_stream(
668        weak: Weak<Self>,
669        lifecycle: Weak<()>,
670        ready: tokio::sync::oneshot::Sender<Result<(), FrontendRuntimeError>>,
671    ) {
672        let Some(runtime) = weak.upgrade() else {
673            let _ = ready.send(Err(FrontendRuntimeError::Closed));
674            return;
675        };
676        let request = runtime
677            .client
678            .get(format!("{}/frontend/events", runtime.base_url))
679            .bearer_auth(&runtime.token)
680            .header("x-supercode-client-id", runtime.client_id.as_str())
681            .header(
682                "x-supercode-permissions",
683                runtime.authorization.header_value(),
684            );
685        let events = runtime.events.clone();
686        drop(runtime);
687        let response = request.send().await;
688        let response = match response {
689            Ok(response) if response.status().is_success() => response,
690            Ok(response) => {
691                let _ = ready.send(Err(FrontendRuntimeError::Transport(format!(
692                    "frontend event stream returned {}",
693                    response.status()
694                ))));
695                return;
696            }
697            Err(error) => {
698                let _ = ready.send(Err(FrontendRuntimeError::Transport(error.to_string())));
699                return;
700            }
701        };
702        let _ = ready.send(Ok(()));
703        let mut stream = response.bytes_stream();
704        let mut pending = Vec::<u8>::new();
705        let mut liveness = tokio::time::interval(std::time::Duration::from_millis(100));
706        loop {
707            let chunk = tokio::select! {
708                _ = liveness.tick() => {
709                    if lifecycle.strong_count() == 0 {
710                        break;
711                    }
712                    if weak
713                        .upgrade()
714                        .is_some_and(|runtime| runtime.disconnected.load(Ordering::SeqCst))
715                    {
716                        break;
717                    }
718                    continue;
719                }
720                chunk = stream.next() => chunk,
721            };
722            let Some(chunk) = chunk else {
723                break;
724            };
725            let Ok(chunk) = chunk else {
726                break;
727            };
728            pending.extend_from_slice(&chunk);
729            while let Some(position) = pending.iter().position(|byte| *byte == b'\n') {
730                let line = pending.drain(..=position).collect::<Vec<_>>();
731                let line = String::from_utf8_lossy(&line);
732                let Some(data) = line.trim_end().strip_prefix("data: ") else {
733                    continue;
734                };
735                if let Ok(event) = serde_json::from_str::<FrontendEvent>(data) {
736                    let _ = events.send(event);
737                }
738            }
739        }
740        if let Some(runtime) = weak.upgrade() {
741            runtime.disconnected.store(true, Ordering::SeqCst);
742            let _ = runtime.events.send(FrontendEvent::new(
743                u64::MAX,
744                json!({
745                    "type": "runtime_disconnected",
746                    "schema_version": FRONTEND_EVENT_SCHEMA_VERSION
747                }),
748            ));
749        }
750    }
751
752    async fn rpc(&self, method: &str, params: Value) -> Result<Value, FrontendRuntimeError> {
753        let id = self.next_id.fetch_add(1, Ordering::SeqCst);
754        let requested_operation = params
755            .pointer("/operation/operation_id")
756            .and_then(Value::as_str)
757            .map(str::to_owned);
758        let response = self
759            .client
760            .post(format!("{}/rpc", self.base_url))
761            .bearer_auth(&self.token)
762            .header("x-supercode-client-id", self.client_id.as_str())
763            .header("x-supercode-permissions", self.authorization.header_value())
764            .json(&json!({"id": id, "method": method, "params": params}))
765            .send()
766            .await
767            .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
768        if !response.status().is_success() {
769            return Err(FrontendRuntimeError::Transport(format!(
770                "SDK HTTP RPC returned {}",
771                response.status()
772            )));
773        }
774        let value: Value = response
775            .json()
776            .await
777            .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
778        if let Some(error) = value.get("error") {
779            let code = error.get("code").and_then(Value::as_i64);
780            let name = error.get("name").and_then(Value::as_str);
781            let operation = error
782                .get("operation")
783                .and_then(Value::as_str)
784                .and_then(crate::SdkOperation::from_action_name);
785            let message = error
786                .get("message")
787                .and_then(Value::as_str)
788                .unwrap_or("SDK runtime request failed")
789                .to_string();
790            return Err(match (name, code) {
791                (Some("unauthenticated"), _) | (_, Some(-32030)) => {
792                    FrontendRuntimeError::Unauthenticated
793                }
794                (Some("unauthorized"), _) | (_, Some(-32031)) => {
795                    FrontendRuntimeError::Unauthorized {
796                        permission: error
797                            .get("permission")
798                            .and_then(Value::as_str)
799                            .unwrap_or("unknown")
800                            .to_string(),
801                    }
802                }
803                (Some("controller_required"), _) | (_, Some(-32032)) => {
804                    FrontendRuntimeError::ControllerRequired {
805                        holder: error
806                            .get("holder")
807                            .and_then(Value::as_str)
808                            .map(str::to_owned),
809                        expires_at_ms: error.get("expiresAtMs").and_then(Value::as_u64),
810                    }
811                }
812                (Some("lease_expired"), _) | (_, Some(-32033)) => {
813                    FrontendRuntimeError::LeaseExpired
814                }
815                (_, Some(-32023)) => FrontendRuntimeError::UnsupportedOperation(
816                    requested_operation.unwrap_or(message),
817                ),
818                (Some("unsupported_action"), _) => FrontendRuntimeError::UnsupportedAction(
819                    operation
820                        .unwrap_or_else(|| {
821                            crate::SdkOperation::from_action_name(method)
822                                .unwrap_or(crate::SdkOperation::Respond)
823                        })
824                        .action_name(),
825                ),
826                (Some("not_found"), Some(-32021)) => {
827                    let request_id = params
828                        .pointer("/response/request_id")
829                        .and_then(Value::as_u64)
830                        .unwrap_or_default();
831                    FrontendRuntimeError::UnknownRequest(request_id)
832                }
833                (Some("invalid_argument"), _) => FrontendRuntimeError::InvalidResponse(message),
834                (_, Some(-32000)) => RuntimeSubmitError::Busy.into(),
835                (_, Some(-32001)) => RuntimeSubmitError::Interrupted.into(),
836                (_, Some(-32002)) => RuntimeSubmitError::Agent(message).into(),
837                (_, Some(-32020)) => FrontendRuntimeError::UnsupportedAction(
838                    crate::SdkOperation::from_action_name(method)
839                        .unwrap_or(crate::SdkOperation::Respond)
840                        .action_name(),
841                ),
842                (_, Some(-32021)) => {
843                    let request_id = params
844                        .pointer("/response/request_id")
845                        .and_then(Value::as_u64)
846                        .unwrap_or_default();
847                    FrontendRuntimeError::UnknownRequest(request_id)
848                }
849                (_, Some(-32022)) => FrontendRuntimeError::InvalidResponse(message),
850                _ => FrontendRuntimeError::Transport(message),
851            });
852        }
853        Ok(value.get("result").cloned().unwrap_or(Value::Null))
854    }
855
856    async fn rpc_typed<T: serde::de::DeserializeOwned>(
857        &self,
858        method: &str,
859        params: Value,
860    ) -> Result<T, FrontendRuntimeError> {
861        serde_json::from_value(self.rpc(method, params).await?)
862            .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))
863    }
864
865    /// Current authenticated client identity.
866    pub fn client_id(&self) -> &crate::RuntimeClientId {
867        &self.client_id
868    }
869
870    /// Whether the remote event stream has already ended or this client
871    /// explicitly detached/closed.
872    pub fn is_disconnected(&self) -> bool {
873        self.disconnected.load(Ordering::SeqCst)
874    }
875
876    /// Explicitly acquire the controller lease, displacing another
877    /// interactive client only through this named operation.
878    pub async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
879        self.rpc_typed(
880            crate::FrontendFacadeMethod::TakeControl.wire_name(),
881            json!({}),
882        )
883        .await
884    }
885
886    /// Renew observer activity and any controller lease owned by this client.
887    pub async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
888        self.rpc_typed(
889            crate::FrontendFacadeMethod::Heartbeat.wire_name(),
890            json!({}),
891        )
892        .await
893    }
894
895    /// Read the coordinated ownership state.
896    pub async fn lease_snapshot(
897        &self,
898    ) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
899        self.rpc_typed(crate::FrontendFacadeMethod::Lease.wire_name(), json!({}))
900            .await
901    }
902
903    /// Release observer and controller state without stopping the runtime.
904    pub async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
905        let snapshot = self
906            .rpc_typed(crate::FrontendFacadeMethod::Detach.wire_name(), json!({}))
907            .await?;
908        self.disconnected.store(true, Ordering::SeqCst);
909        Ok(snapshot)
910    }
911}
912
913#[async_trait]
914#[cfg(feature = "adapter-api")]
915impl FrontendRuntime for HttpFrontendRuntime {
916    async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
917        self.rpc_typed(crate::FrontendFacadeMethod::Describe.wire_name(), json!({}))
918            .await
919    }
920
921    async fn attach(
922        &self,
923        history_limit: usize,
924    ) -> Result<FrontendAttachment, FrontendRuntimeError> {
925        if self.disconnected.load(Ordering::SeqCst) {
926            return Err(FrontendRuntimeError::Closed);
927        }
928        // Subscribe locally before asking the server for its atomic snapshot.
929        // Anything concurrently received over SSE is either in snapshot.replay
930        // or queued here; sequence filtering removes the overlap.
931        let live = self.events.subscribe();
932        let snapshot: FrontendAttachSnapshot = self
933            .rpc_typed(
934                crate::FrontendFacadeMethod::Attach.wire_name(),
935                json!({"limit": history_limit}),
936            )
937            .await?;
938        Ok(FrontendAttachment::new(
939            snapshot.descriptor,
940            snapshot.history,
941            snapshot.history_cursor,
942            snapshot.replay,
943            live,
944            Some(self.lifecycle.clone()),
945        ))
946    }
947
948    async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), FrontendRuntimeError> {
949        self.rpc(
950            crate::FrontendFacadeMethod::SendInput.wire_name(),
951            json!({"prompt": prompt}),
952        )
953        .await?;
954        Ok(())
955    }
956
957    async fn submit(&self, prompt: String) -> Result<String, FrontendRuntimeError> {
958        let result = self
959            .rpc(
960                crate::FrontendFacadeMethod::Submit.wire_name(),
961                json!({"prompt": prompt}),
962            )
963            .await?;
964        Ok(result
965            .get("reply")
966            .and_then(Value::as_str)
967            .unwrap_or_default()
968            .to_string())
969    }
970
971    async fn submit_with_images(
972        &self,
973        prompt: String,
974        image_urls: Vec<String>,
975    ) -> Result<String, FrontendRuntimeError> {
976        let result = self
977            .rpc(
978                crate::FrontendFacadeMethod::Submit.wire_name(),
979                json!({"prompt": prompt, "image_urls": image_urls}),
980            )
981            .await?;
982        Ok(result
983            .get("reply")
984            .and_then(Value::as_str)
985            .unwrap_or_default()
986            .to_string())
987    }
988
989    async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
990        let result = self
991            .rpc(
992                crate::FrontendFacadeMethod::Interrupt.wire_name(),
993                json!({}),
994            )
995            .await?;
996        Ok(result
997            .get("interrupted")
998            .and_then(Value::as_bool)
999            .unwrap_or(false))
1000    }
1001
1002    async fn steer(&self, prompt: String) -> Result<(), FrontendRuntimeError> {
1003        self.rpc(
1004            crate::FrontendFacadeMethod::Steer.wire_name(),
1005            json!({"prompt": prompt}),
1006        )
1007        .await?;
1008        Ok(())
1009    }
1010
1011    async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
1012        self.rpc(
1013            crate::FrontendFacadeMethod::Respond.wire_name(),
1014            json!({"response": response}),
1015        )
1016        .await?;
1017        Ok(())
1018    }
1019
1020    async fn invoke(
1021        &self,
1022        operation: FrontendOperationInvocation,
1023    ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
1024        self.rpc_typed(
1025            crate::FrontendFacadeMethod::Invoke.wire_name(),
1026            json!({"operation": operation}),
1027        )
1028        .await
1029    }
1030
1031    async fn lease_snapshot(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1032        HttpFrontendRuntime::lease_snapshot(self).await
1033    }
1034
1035    async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1036        HttpFrontendRuntime::take_control(self).await
1037    }
1038
1039    async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1040        HttpFrontendRuntime::heartbeat(self).await
1041    }
1042
1043    async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1044        HttpFrontendRuntime::detach(self).await
1045    }
1046
1047    async fn close(&self) -> Result<(), FrontendRuntimeError> {
1048        self.rpc(crate::FrontendFacadeMethod::Close.wire_name(), json!({}))
1049            .await?;
1050        self.disconnected.store(true, Ordering::SeqCst);
1051        Ok(())
1052    }
1053}