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    /// BP-4 (catalog:109 "Context-usage introspection"): read the live
127    /// context-window accounting for this runtime. Unlike every family
128    /// above, this one takes no arguments and mutates nothing — it is the
129    /// frontend's door onto cc's `/context` grid and cx's
130    /// `get_context_remaining`, so a UI can render a context meter without
131    /// submitting a turn.
132    Context,
133}
134
135/// One operation the runtime can genuinely invoke.
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137pub struct FrontendOperationDescriptor {
138    /// Stable runtime-scoped identifier supplied back during invocation.
139    pub id: String,
140    /// Typed operation family.
141    pub kind: FrontendOperationKind,
142    /// Optional slash-command trigger rendered by terminal composers.
143    pub command: Option<FrontendCommandDescriptor>,
144}
145
146/// Typed invocation accepted by [`FrontendRuntime::invoke`].
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(tag = "kind", rename_all = "snake_case")]
149pub enum FrontendOperationInvocation {
150    /// Expand and submit one advertised trusted prompt template.
151    Prompt {
152        /// Identifier from [`FrontendOperationDescriptor::id`].
153        operation_id: String,
154        /// Free text replacing the prompt template's `{args}` placeholder.
155        arguments: String,
156    },
157    /// BP-4: read the live context-window accounting. Takes no arguments —
158    /// there is nothing for a caller to steer, and nothing it can mutate.
159    Context {
160        /// Identifier from [`FrontendOperationDescriptor::id`].
161        operation_id: String,
162    },
163    /// BP-13 (catalog D9 "Mid-session model switching"): change the model
164    /// this runtime sends to, WITHOUT losing the session. Advertised only
165    /// by a runtime whose config allows switching
166    /// (`[core.model_switch] allow_switch`), so a frontend never renders a
167    /// control the runtime would refuse.
168    Model {
169        /// Identifier from [`FrontendOperationDescriptor::id`].
170        operation_id: String,
171        /// The model to switch to — an alias or a full slug, resolved
172        /// through the runtime's own routing table. Empty means "report
173        /// the current model", which mutates nothing.
174        model: String,
175    },
176}
177
178impl FrontendOperationInvocation {
179    /// Identifier supplied by the runtime catalog.
180    pub fn operation_id(&self) -> &str {
181        match self {
182            Self::Prompt { operation_id, .. }
183            | Self::Context { operation_id }
184            | Self::Model { operation_id, .. } => operation_id,
185        }
186    }
187}
188
189/// Typed result returned by [`FrontendRuntime::invoke`].
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191#[serde(tag = "kind", rename_all = "snake_case")]
192pub enum FrontendOperationResult {
193    /// Reply from a prompt-template turn.
194    Prompt {
195        /// Final assistant reply.
196        reply: String,
197    },
198    /// BP-4: the live context-window accounting.
199    Context {
200        /// Token/percentage breakdown for the runtime's current context.
201        usage: crate::ContextUsage,
202    },
203    /// BP-13: the model in force after the operation ran.
204    Model {
205        /// The model the runtime now sends to (alias-resolved).
206        model: String,
207        /// The model it sent to before — equal to `model` when the
208        /// invocation only reported the current one.
209        previous: String,
210    },
211}
212
213/// Source/emulation identity supplied by the session-loading surface.
214#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
215pub struct FrontendRuntimeMetadata {
216    /// Source harness whose session semantics are being continued.
217    pub source_harness: Option<String>,
218    /// Resolved composable preset/profile name, when one was selected.
219    pub emulation_profile: Option<String>,
220}
221
222/// Complete frontend-facing description of one SDK-owned runtime.
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224pub struct FrontendRuntimeDescriptor {
225    /// Contract schema version.
226    pub schema_version: u32,
227    /// Stable SDK runtime/session identity.
228    pub session_id: String,
229    /// Source harness whose semantics are being emulated.
230    pub source_harness: Option<String>,
231    /// Resolved composable preset/profile name.
232    pub emulation_profile: Option<String>,
233    /// Active composable modules, using their stable config keys.
234    pub active_modules: Vec<String>,
235    /// Runtime-provided composer commands.
236    pub commands: Vec<FrontendCommandDescriptor>,
237    /// Explicit typed operation catalog. Missing on schema-v1 peers.
238    #[serde(default)]
239    pub operations: Vec<FrontendOperationDescriptor>,
240    /// Supported control actions.
241    pub actions: FrontendActions,
242    /// Display/event capabilities.
243    pub display: FrontendDisplayCapabilities,
244    /// Current model label.
245    pub model: String,
246    /// Current turn state.
247    pub turn_state: FrontendTurnState,
248    /// Current connection state.
249    pub connection_state: FrontendConnectionState,
250    /// Compatible client/adapter metadata with no canonical-session meaning.
251    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
252    pub extensions: BTreeMap<String, Value>,
253}
254
255/// Serializable half of an attachment returned by an out-of-process runtime.
256/// The live receiver is transport-owned and joined to this snapshot locally.
257#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
258pub struct FrontendAttachSnapshot {
259    /// Runtime description captured at attachment time.
260    pub descriptor: FrontendRuntimeDescriptor,
261    /// Bounded canonical history through `history_cursor`.
262    pub history: Vec<ChatMessage>,
263    /// Highest event sequence represented by `history`.
264    pub history_cursor: u64,
265    /// Events after the canonical history boundary and before the response.
266    pub replay: VecDeque<FrontendEvent>,
267}
268
269/// Kind of interactive request surfaced by the SDK runtime.
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
271#[serde(rename_all = "snake_case")]
272pub enum FrontendRequestKind {
273    /// A tool or sandbox action needs a policy-authorized human decision.
274    Approval,
275    /// An MCP server requested structured user input.
276    Elicitation,
277    /// Another versioned runtime request not known to this frontend build.
278    /// Its complete payload remains available for a generic overlay.
279    #[serde(other)]
280    Other,
281}
282
283/// One pending interactive request, emitted as a sequenced frontend event.
284#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
285pub struct FrontendRequest {
286    /// Runtime-scoped request identifier used exactly once by `respond`.
287    pub id: u64,
288    /// Typed request category.
289    pub kind: FrontendRequestKind,
290    /// Complete request payload, including raw tool arguments or schema.
291    pub payload: Value,
292}
293
294/// Typed approval decision accepted by [`FrontendRuntime::respond`].
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
296#[serde(rename_all = "snake_case")]
297pub enum FrontendApprovalDecision {
298    /// Refuse this request.
299    Deny,
300    /// Allow only this request.
301    Allow,
302    /// Allow this request and cache the exact policy key for the session.
303    AllowForSession,
304}
305
306/// MCP elicitation outcome accepted by a frontend response.
307#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
308#[serde(rename_all = "snake_case")]
309pub enum FrontendElicitationAction {
310    /// Submit structured content.
311    Accept,
312    /// Explicitly decline the request.
313    Decline,
314    /// Dismiss the request without a decision.
315    Cancel,
316}
317
318/// Typed response to one SDK-owned interactive request.
319#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
320#[serde(tag = "kind", rename_all = "snake_case")]
321pub enum FrontendResponse {
322    /// Answer an approval request.
323    Approval {
324        /// Identifier from [`FrontendRequest::id`].
325        request_id: u64,
326        /// Human decision.
327        decision: FrontendApprovalDecision,
328    },
329    /// Answer an MCP elicitation request.
330    Elicitation {
331        /// Identifier from [`FrontendRequest::id`].
332        request_id: u64,
333        /// MCP elicitation outcome.
334        action: FrontendElicitationAction,
335        /// Structured content for `accept`.
336        content: Option<Value>,
337    },
338    /// Answer a generic runtime request without discarding its payload.
339    Other {
340        /// Identifier from [`FrontendRequest::id`].
341        request_id: u64,
342        /// Generic accept/decline/cancel outcome.
343        action: FrontendElicitationAction,
344        /// Optional structured response content.
345        content: Option<Value>,
346    },
347}
348
349impl FrontendResponse {
350    pub(crate) fn request_id(&self) -> u64 {
351        match self {
352            Self::Approval { request_id, .. }
353            | Self::Elicitation { request_id, .. }
354            | Self::Other { request_id, .. } => *request_id,
355        }
356    }
357}
358
359/// Atomic history/replay/live attachment to one runtime.
360pub struct FrontendAttachment {
361    /// Runtime description captured at attachment time.
362    pub descriptor: FrontendRuntimeDescriptor,
363    /// Bounded canonical history through `history_cursor`.
364    pub history: Vec<ChatMessage>,
365    /// Highest event sequence already represented by `history`.
366    pub history_cursor: u64,
367    pub(crate) replay: VecDeque<FrontendEvent>,
368    live: broadcast::Receiver<FrontendEvent>,
369    delivered: u64,
370    acknowledged: Option<Arc<AtomicU64>>,
371    _transport_lease: Option<Arc<()>>,
372}
373
374impl FrontendAttachment {
375    /// Build an in-process attachment from a serialized snapshot and a live
376    /// SDK event receiver. Runtime adapters use this constructor in tests and
377    /// protocol bridges without acquiring transport ownership.
378    pub fn from_snapshot(
379        snapshot: FrontendAttachSnapshot,
380        live: broadcast::Receiver<FrontendEvent>,
381    ) -> Self {
382        Self::from_snapshot_after(snapshot, live, 0)
383    }
384
385    /// Build an attachment that resumes after a sequence acknowledged by a
386    /// prior transport connection. Snapshot replay and any overlapping live
387    /// events at or below the cursor are skipped without changing canonical
388    /// history or event payloads.
389    pub fn from_snapshot_after(
390        snapshot: FrontendAttachSnapshot,
391        live: broadcast::Receiver<FrontendEvent>,
392        acknowledged_sequence: u64,
393    ) -> Self {
394        let delivered = snapshot.history_cursor.max(acknowledged_sequence);
395        Self::new_with_delivered(
396            snapshot.descriptor,
397            snapshot.history,
398            snapshot.history_cursor,
399            snapshot.replay,
400            live,
401            None,
402            delivered,
403        )
404    }
405
406    pub(crate) fn new(
407        descriptor: FrontendRuntimeDescriptor,
408        history: Vec<ChatMessage>,
409        history_cursor: u64,
410        replay: VecDeque<FrontendEvent>,
411        live: broadcast::Receiver<FrontendEvent>,
412        transport_lease: Option<Arc<()>>,
413    ) -> Self {
414        let delivered = history_cursor;
415        Self::new_with_delivered(
416            descriptor,
417            history,
418            history_cursor,
419            replay,
420            live,
421            transport_lease,
422            delivered,
423        )
424    }
425
426    fn new_with_delivered(
427        descriptor: FrontendRuntimeDescriptor,
428        history: Vec<ChatMessage>,
429        history_cursor: u64,
430        replay: VecDeque<FrontendEvent>,
431        live: broadcast::Receiver<FrontendEvent>,
432        transport_lease: Option<Arc<()>>,
433        delivered: u64,
434    ) -> Self {
435        Self {
436            descriptor,
437            history,
438            history_cursor,
439            replay,
440            live,
441            delivered,
442            acknowledged: None,
443            _transport_lease: transport_lease,
444        }
445    }
446
447    #[cfg(feature = "adapter-acp")]
448    pub(crate) fn with_acknowledgement(mut self, acknowledged: Arc<AtomicU64>) -> Self {
449        acknowledged.fetch_max(self.history_cursor, Ordering::SeqCst);
450        self.acknowledged = Some(acknowledged);
451        self
452    }
453
454    fn acknowledge(&self, event: &FrontendEvent) {
455        if !event_advances_acknowledgement(event) {
456            return;
457        }
458        if let Some(acknowledged) = &self.acknowledged {
459            acknowledged.fetch_max(event.sequence, Ordering::SeqCst);
460        }
461    }
462
463    /// Receive the next event not already represented by the history or a
464    /// prior replay item. Duplicate events queued during attachment are
465    /// skipped by sequence.
466    pub async fn next_event(&mut self) -> Result<FrontendEvent, FrontendRuntimeError> {
467        loop {
468            let event = match self.next_replay_event() {
469                Some(event) => return Ok(event),
470                None => match self.live.recv().await {
471                    Ok(event) => event,
472                    Err(broadcast::error::RecvError::Lagged(count)) => {
473                        return Err(FrontendRuntimeError::ReplayGap(count));
474                    }
475                    Err(broadcast::error::RecvError::Closed) => {
476                        return Err(FrontendRuntimeError::Closed);
477                    }
478                },
479            };
480            if event.sequence <= self.delivered {
481                continue;
482            }
483            self.delivered = event.sequence;
484            self.acknowledge(&event);
485            return Ok(event);
486        }
487    }
488
489    /// Drain one event from the finite attachment replay without waiting for
490    /// live input. Interactive frontends use this to project the complete
491    /// atomic snapshot before accepting keystrokes, so a historical resolved
492    /// request never appears transiently actionable.
493    pub fn next_replay_event(&mut self) -> Option<FrontendEvent> {
494        while let Some(event) = self.replay.pop_front() {
495            if event.sequence <= self.delivered {
496                continue;
497            }
498            self.delivered = event.sequence;
499            self.acknowledge(&event);
500            return Some(event);
501        }
502        None
503    }
504}
505
506pub(crate) fn event_advances_acknowledgement(event: &FrontendEvent) -> bool {
507    event
508        .payload
509        .pointer("/_meta/supercode/transient")
510        .and_then(Value::as_bool)
511        != Some(true)
512}
513
514/// State protected by `RpcEngine`'s short synchronous projection lock.
515pub(crate) struct FrontendProjectionState {
516    pub(crate) history: Vec<ChatMessage>,
517    pub(crate) history_cursor: u64,
518    pub(crate) next_sequence: u64,
519    pub(crate) replay: VecDeque<FrontendEvent>,
520}
521
522#[async_trait]
523impl FrontendRuntime for RpcEngine {
524    async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
525        Ok(self.frontend_descriptor())
526    }
527
528    async fn attach(
529        &self,
530        history_limit: usize,
531    ) -> Result<FrontendAttachment, FrontendRuntimeError> {
532        self.frontend_attach(history_limit)
533    }
534
535    async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), FrontendRuntimeError> {
536        RpcEngine::send_input(&self, prompt)?;
537        Ok(())
538    }
539
540    async fn send_input_with_images(
541        self: Arc<Self>,
542        prompt: String,
543        image_urls: Vec<String>,
544    ) -> Result<(), FrontendRuntimeError> {
545        RpcEngine::send_input_with_images(&self, prompt, image_urls)?;
546        Ok(())
547    }
548
549    async fn submit(&self, prompt: String) -> Result<String, FrontendRuntimeError> {
550        Ok(RpcEngine::submit(self, prompt).await?)
551    }
552
553    async fn submit_with_images(
554        &self,
555        prompt: String,
556        image_urls: Vec<String>,
557    ) -> Result<String, FrontendRuntimeError> {
558        Ok(RpcEngine::submit_with_images(self, prompt, image_urls).await?)
559    }
560
561    async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
562        Ok(RpcEngine::interrupt(self).await)
563    }
564
565    async fn steer(&self, prompt: String) -> Result<(), FrontendRuntimeError> {
566        RpcEngine::steer(self, prompt)
567    }
568
569    async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
570        RpcEngine::respond(self, response)
571    }
572
573    async fn invoke(
574        &self,
575        operation: FrontendOperationInvocation,
576    ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
577        RpcEngine::invoke(self, operation).await
578    }
579
580    async fn close(&self) -> Result<(), FrontendRuntimeError> {
581        RpcEngine::shutdown(self).await;
582        Ok(())
583    }
584}
585
586/// Authenticated HTTP implementation of [`FrontendRuntime`].
587///
588/// It owns only an RPC/SSE connection. The remote [`RpcEngine`] remains the
589/// sole owner of the agent loop, transcript, scheduler, and persistence.
590#[cfg(feature = "adapter-api")]
591pub struct HttpFrontendRuntime {
592    base_url: String,
593    token: String,
594    client_id: crate::RuntimeClientId,
595    authorization: crate::RuntimeAuthorization,
596    client: reqwest::Client,
597    events: broadcast::Sender<FrontendEvent>,
598    next_id: AtomicU64,
599    lifecycle: Arc<()>,
600    disconnected: AtomicBool,
601}
602
603#[cfg(feature = "adapter-api")]
604impl HttpFrontendRuntime {
605    /// Authenticate, verify the frontend descriptor, and establish the
606    /// sequenced SSE stream before returning.
607    pub async fn connect(
608        base_url: impl Into<String>,
609        token: impl Into<String>,
610    ) -> Result<Arc<Self>, FrontendRuntimeError> {
611        let mut random = [0_u8; 16];
612        getrandom::getrandom(&mut random).map_err(|error| {
613            FrontendRuntimeError::Transport(format!(
614                "cannot generate runtime client identity: {error}"
615            ))
616        })?;
617        let suffix = random
618            .iter()
619            .map(|byte| format!("{byte:02x}"))
620            .collect::<String>();
621        let client_id = crate::RuntimeClientId::parse(format!("http-{suffix}"))
622            .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
623        Self::connect_with_client_id(base_url, token, client_id).await
624    }
625
626    /// Connect with a caller-owned stable client identity. Reconnect tests
627    /// and external bindings use this to retain deterministic lease state.
628    pub async fn connect_with_client_id(
629        base_url: impl Into<String>,
630        token: impl Into<String>,
631        client_id: crate::RuntimeClientId,
632    ) -> Result<Arc<Self>, FrontendRuntimeError> {
633        Self::connect_with_authorization(
634            base_url,
635            token,
636            client_id,
637            crate::RuntimeAuthorization::owner(),
638        )
639        .await
640    }
641
642    /// Connect while requesting an exact subset of the bearer credential's
643    /// permissions. The server intersects this with the authenticated grant;
644    /// this header can narrow authority but can never elevate it.
645    pub async fn connect_with_authorization(
646        base_url: impl Into<String>,
647        token: impl Into<String>,
648        client_id: crate::RuntimeClientId,
649        authorization: crate::RuntimeAuthorization,
650    ) -> Result<Arc<Self>, FrontendRuntimeError> {
651        Self::connect_inner(base_url, token, client_id, authorization, true)
652            .await
653            .map(|(runtime, _)| runtime)
654    }
655
656    /// Authenticated metadata probe that does not open an event stream or
657    /// register an observer, returning the descriptor the connect handshake
658    /// already fetched. Used by the local runtime registry, which runs this on
659    /// every `harness serve` tick for every followed session: asking the same
660    /// runtime to describe itself twice for one read is pure load on that
661    /// path, and each round trip costs its own loopback connection.
662    pub(crate) async fn probe_described(
663        base_url: impl Into<String>,
664        token: impl Into<String>,
665        client_id: crate::RuntimeClientId,
666    ) -> Result<(Arc<Self>, FrontendRuntimeDescriptor), FrontendRuntimeError> {
667        Self::connect_inner(
668            base_url,
669            token,
670            client_id,
671            crate::RuntimeAuthorization::observer(),
672            false,
673        )
674        .await
675    }
676
677    async fn connect_inner(
678        base_url: impl Into<String>,
679        token: impl Into<String>,
680        client_id: crate::RuntimeClientId,
681        authorization: crate::RuntimeAuthorization,
682        stream_events: bool,
683    ) -> Result<(Arc<Self>, FrontendRuntimeDescriptor), FrontendRuntimeError> {
684        let runtime = Arc::new(Self {
685            base_url: base_url.into().trim_end_matches('/').to_string(),
686            token: token.into(),
687            client_id,
688            authorization,
689            client: reqwest::Client::new(),
690            events: broadcast::channel(1024).0,
691            next_id: AtomicU64::new(1),
692            lifecycle: Arc::new(()),
693            disconnected: AtomicBool::new(false),
694        });
695        // Validate auth and schema before opening a long-lived connection.
696        let descriptor: FrontendRuntimeDescriptor = runtime
697            .rpc_typed(crate::FrontendFacadeMethod::Describe.wire_name(), json!({}))
698            .await?;
699        if stream_events {
700            Self::start_event_stream(&runtime).await?;
701        }
702        Ok((runtime, descriptor))
703    }
704
705    async fn start_event_stream(runtime: &Arc<Self>) -> Result<(), FrontendRuntimeError> {
706        let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
707        let weak = Arc::downgrade(runtime);
708        let lifecycle = Arc::downgrade(&runtime.lifecycle);
709        tokio::spawn(async move {
710            Self::run_event_stream(weak, lifecycle, ready_tx).await;
711        });
712        ready_rx.await.map_err(|_| {
713            FrontendRuntimeError::Transport("frontend event stream exited before startup".into())
714        })?
715    }
716
717    async fn run_event_stream(
718        weak: Weak<Self>,
719        lifecycle: Weak<()>,
720        ready: tokio::sync::oneshot::Sender<Result<(), FrontendRuntimeError>>,
721    ) {
722        let Some(runtime) = weak.upgrade() else {
723            let _ = ready.send(Err(FrontendRuntimeError::Closed));
724            return;
725        };
726        let request = runtime
727            .client
728            .get(format!("{}/frontend/events", runtime.base_url))
729            .bearer_auth(&runtime.token)
730            .header("x-supercode-client-id", runtime.client_id.as_str())
731            .header(
732                "x-supercode-permissions",
733                runtime.authorization.header_value(),
734            );
735        let events = runtime.events.clone();
736        drop(runtime);
737        let response = request.send().await;
738        let response = match response {
739            Ok(response) if response.status().is_success() => response,
740            Ok(response) => {
741                let _ = ready.send(Err(FrontendRuntimeError::Transport(format!(
742                    "frontend event stream returned {}",
743                    response.status()
744                ))));
745                return;
746            }
747            Err(error) => {
748                let _ = ready.send(Err(FrontendRuntimeError::Transport(error.to_string())));
749                return;
750            }
751        };
752        let _ = ready.send(Ok(()));
753        let mut stream = response.bytes_stream();
754        let mut pending = Vec::<u8>::new();
755        let mut liveness = tokio::time::interval(std::time::Duration::from_millis(100));
756        loop {
757            let chunk = tokio::select! {
758                _ = liveness.tick() => {
759                    if lifecycle.strong_count() == 0 {
760                        break;
761                    }
762                    if weak
763                        .upgrade()
764                        .is_some_and(|runtime| runtime.disconnected.load(Ordering::SeqCst))
765                    {
766                        break;
767                    }
768                    continue;
769                }
770                chunk = stream.next() => chunk,
771            };
772            let Some(chunk) = chunk else {
773                break;
774            };
775            let Ok(chunk) = chunk else {
776                break;
777            };
778            pending.extend_from_slice(&chunk);
779            while let Some(position) = pending.iter().position(|byte| *byte == b'\n') {
780                let line = pending.drain(..=position).collect::<Vec<_>>();
781                let line = String::from_utf8_lossy(&line);
782                let Some(data) = line.trim_end().strip_prefix("data: ") else {
783                    continue;
784                };
785                if let Ok(event) = serde_json::from_str::<FrontendEvent>(data) {
786                    let _ = events.send(event);
787                }
788            }
789        }
790        if let Some(runtime) = weak.upgrade() {
791            runtime.disconnected.store(true, Ordering::SeqCst);
792            let _ = runtime.events.send(FrontendEvent::new(
793                u64::MAX,
794                json!({
795                    "type": "runtime_disconnected",
796                    "schema_version": FRONTEND_EVENT_SCHEMA_VERSION
797                }),
798            ));
799        }
800    }
801
802    async fn rpc(&self, method: &str, params: Value) -> Result<Value, FrontendRuntimeError> {
803        let id = self.next_id.fetch_add(1, Ordering::SeqCst);
804        let requested_operation = params
805            .pointer("/operation/operation_id")
806            .and_then(Value::as_str)
807            .map(str::to_owned);
808        let response = self
809            .client
810            .post(format!("{}/rpc", self.base_url))
811            .bearer_auth(&self.token)
812            .header("x-supercode-client-id", self.client_id.as_str())
813            .header("x-supercode-permissions", self.authorization.header_value())
814            .json(&json!({"id": id, "method": method, "params": params}))
815            .send()
816            .await
817            .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
818        if !response.status().is_success() {
819            return Err(FrontendRuntimeError::Transport(format!(
820                "SDK HTTP RPC returned {}",
821                response.status()
822            )));
823        }
824        let value: Value = response
825            .json()
826            .await
827            .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))?;
828        if let Some(error) = value.get("error") {
829            let code = error.get("code").and_then(Value::as_i64);
830            let name = error.get("name").and_then(Value::as_str);
831            let operation = error
832                .get("operation")
833                .and_then(Value::as_str)
834                .and_then(crate::SdkOperation::from_action_name);
835            let message = error
836                .get("message")
837                .and_then(Value::as_str)
838                .unwrap_or("SDK runtime request failed")
839                .to_string();
840            return Err(match (name, code) {
841                (Some("unauthenticated"), _) | (_, Some(-32030)) => {
842                    FrontendRuntimeError::Unauthenticated
843                }
844                (Some("unauthorized"), _) | (_, Some(-32031)) => {
845                    FrontendRuntimeError::Unauthorized {
846                        permission: error
847                            .get("permission")
848                            .and_then(Value::as_str)
849                            .unwrap_or("unknown")
850                            .to_string(),
851                    }
852                }
853                (Some("controller_required"), _) | (_, Some(-32032)) => {
854                    FrontendRuntimeError::ControllerRequired {
855                        holder: error
856                            .get("holder")
857                            .and_then(Value::as_str)
858                            .map(str::to_owned),
859                        expires_at_ms: error.get("expiresAtMs").and_then(Value::as_u64),
860                    }
861                }
862                (Some("lease_expired"), _) | (_, Some(-32033)) => {
863                    FrontendRuntimeError::LeaseExpired
864                }
865                (_, Some(-32023)) => FrontendRuntimeError::UnsupportedOperation(
866                    requested_operation.unwrap_or(message),
867                ),
868                (Some("unsupported_action"), _) => FrontendRuntimeError::UnsupportedAction(
869                    operation
870                        .unwrap_or_else(|| {
871                            crate::SdkOperation::from_action_name(method)
872                                .unwrap_or(crate::SdkOperation::Respond)
873                        })
874                        .action_name(),
875                ),
876                (Some("not_found"), Some(-32021)) => {
877                    let request_id = params
878                        .pointer("/response/request_id")
879                        .and_then(Value::as_u64)
880                        .unwrap_or_default();
881                    FrontendRuntimeError::UnknownRequest(request_id)
882                }
883                (Some("invalid_argument"), _) => FrontendRuntimeError::InvalidResponse(message),
884                (_, Some(-32000)) => RuntimeSubmitError::Busy.into(),
885                (_, Some(-32001)) => RuntimeSubmitError::Interrupted.into(),
886                (_, Some(-32002)) => RuntimeSubmitError::Agent(message).into(),
887                (_, Some(-32020)) => FrontendRuntimeError::UnsupportedAction(
888                    crate::SdkOperation::from_action_name(method)
889                        .unwrap_or(crate::SdkOperation::Respond)
890                        .action_name(),
891                ),
892                (_, Some(-32021)) => {
893                    let request_id = params
894                        .pointer("/response/request_id")
895                        .and_then(Value::as_u64)
896                        .unwrap_or_default();
897                    FrontendRuntimeError::UnknownRequest(request_id)
898                }
899                (_, Some(-32022)) => FrontendRuntimeError::InvalidResponse(message),
900                _ => FrontendRuntimeError::Transport(message),
901            });
902        }
903        Ok(value.get("result").cloned().unwrap_or(Value::Null))
904    }
905
906    async fn rpc_typed<T: serde::de::DeserializeOwned>(
907        &self,
908        method: &str,
909        params: Value,
910    ) -> Result<T, FrontendRuntimeError> {
911        serde_json::from_value(self.rpc(method, params).await?)
912            .map_err(|error| FrontendRuntimeError::Transport(error.to_string()))
913    }
914
915    /// Current authenticated client identity.
916    pub fn client_id(&self) -> &crate::RuntimeClientId {
917        &self.client_id
918    }
919
920    /// Whether the remote event stream has already ended or this client
921    /// explicitly detached/closed.
922    pub fn is_disconnected(&self) -> bool {
923        self.disconnected.load(Ordering::SeqCst)
924    }
925
926    /// Explicitly acquire the controller lease, displacing another
927    /// interactive client only through this named operation.
928    pub async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
929        self.rpc_typed(
930            crate::FrontendFacadeMethod::TakeControl.wire_name(),
931            json!({}),
932        )
933        .await
934    }
935
936    /// Renew observer activity and any controller lease owned by this client.
937    pub async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
938        self.rpc_typed(
939            crate::FrontendFacadeMethod::Heartbeat.wire_name(),
940            json!({}),
941        )
942        .await
943    }
944
945    /// Read the coordinated ownership state.
946    pub async fn lease_snapshot(
947        &self,
948    ) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
949        self.rpc_typed(crate::FrontendFacadeMethod::Lease.wire_name(), json!({}))
950            .await
951    }
952
953    /// Release observer and controller state without stopping the runtime.
954    pub async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
955        let snapshot = self
956            .rpc_typed(crate::FrontendFacadeMethod::Detach.wire_name(), json!({}))
957            .await?;
958        self.disconnected.store(true, Ordering::SeqCst);
959        Ok(snapshot)
960    }
961}
962
963#[async_trait]
964#[cfg(feature = "adapter-api")]
965impl FrontendRuntime for HttpFrontendRuntime {
966    async fn describe(&self) -> Result<FrontendRuntimeDescriptor, FrontendRuntimeError> {
967        self.rpc_typed(crate::FrontendFacadeMethod::Describe.wire_name(), json!({}))
968            .await
969    }
970
971    async fn attach(
972        &self,
973        history_limit: usize,
974    ) -> Result<FrontendAttachment, FrontendRuntimeError> {
975        if self.disconnected.load(Ordering::SeqCst) {
976            return Err(FrontendRuntimeError::Closed);
977        }
978        // Subscribe locally before asking the server for its atomic snapshot.
979        // Anything concurrently received over SSE is either in snapshot.replay
980        // or queued here; sequence filtering removes the overlap.
981        let live = self.events.subscribe();
982        let snapshot: FrontendAttachSnapshot = self
983            .rpc_typed(
984                crate::FrontendFacadeMethod::Attach.wire_name(),
985                json!({"limit": history_limit}),
986            )
987            .await?;
988        Ok(FrontendAttachment::new(
989            snapshot.descriptor,
990            snapshot.history,
991            snapshot.history_cursor,
992            snapshot.replay,
993            live,
994            Some(self.lifecycle.clone()),
995        ))
996    }
997
998    async fn send_input(self: Arc<Self>, prompt: String) -> Result<(), FrontendRuntimeError> {
999        self.send_input_with_images(prompt, Vec::new()).await
1000    }
1001
1002    async fn send_input_with_images(
1003        self: Arc<Self>,
1004        prompt: String,
1005        image_urls: Vec<String>,
1006    ) -> Result<(), FrontendRuntimeError> {
1007        self.rpc(
1008            crate::FrontendFacadeMethod::SendInput.wire_name(),
1009            json!({"prompt": prompt, "image_urls": image_urls}),
1010        )
1011        .await?;
1012        Ok(())
1013    }
1014
1015    async fn submit(&self, prompt: String) -> Result<String, FrontendRuntimeError> {
1016        let result = self
1017            .rpc(
1018                crate::FrontendFacadeMethod::Submit.wire_name(),
1019                json!({"prompt": prompt}),
1020            )
1021            .await?;
1022        Ok(result
1023            .get("reply")
1024            .and_then(Value::as_str)
1025            .unwrap_or_default()
1026            .to_string())
1027    }
1028
1029    async fn submit_with_images(
1030        &self,
1031        prompt: String,
1032        image_urls: Vec<String>,
1033    ) -> Result<String, FrontendRuntimeError> {
1034        let result = self
1035            .rpc(
1036                crate::FrontendFacadeMethod::Submit.wire_name(),
1037                json!({"prompt": prompt, "image_urls": image_urls}),
1038            )
1039            .await?;
1040        Ok(result
1041            .get("reply")
1042            .and_then(Value::as_str)
1043            .unwrap_or_default()
1044            .to_string())
1045    }
1046
1047    async fn interrupt(&self) -> Result<bool, FrontendRuntimeError> {
1048        let result = self
1049            .rpc(
1050                crate::FrontendFacadeMethod::Interrupt.wire_name(),
1051                json!({}),
1052            )
1053            .await?;
1054        Ok(result
1055            .get("interrupted")
1056            .and_then(Value::as_bool)
1057            .unwrap_or(false))
1058    }
1059
1060    async fn steer(&self, prompt: String) -> Result<(), FrontendRuntimeError> {
1061        self.rpc(
1062            crate::FrontendFacadeMethod::Steer.wire_name(),
1063            json!({"prompt": prompt}),
1064        )
1065        .await?;
1066        Ok(())
1067    }
1068
1069    async fn respond(&self, response: FrontendResponse) -> Result<(), FrontendRuntimeError> {
1070        self.rpc(
1071            crate::FrontendFacadeMethod::Respond.wire_name(),
1072            json!({"response": response}),
1073        )
1074        .await?;
1075        Ok(())
1076    }
1077
1078    async fn invoke(
1079        &self,
1080        operation: FrontendOperationInvocation,
1081    ) -> Result<FrontendOperationResult, FrontendRuntimeError> {
1082        self.rpc_typed(
1083            crate::FrontendFacadeMethod::Invoke.wire_name(),
1084            json!({"operation": operation}),
1085        )
1086        .await
1087    }
1088
1089    async fn lease_snapshot(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1090        HttpFrontendRuntime::lease_snapshot(self).await
1091    }
1092
1093    async fn take_control(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1094        HttpFrontendRuntime::take_control(self).await
1095    }
1096
1097    async fn heartbeat(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1098        HttpFrontendRuntime::heartbeat(self).await
1099    }
1100
1101    async fn detach(&self) -> Result<crate::RuntimeLeaseSnapshot, FrontendRuntimeError> {
1102        HttpFrontendRuntime::detach(self).await
1103    }
1104
1105    async fn close(&self) -> Result<(), FrontendRuntimeError> {
1106        self.rpc(crate::FrontendFacadeMethod::Close.wire_name(), json!({}))
1107            .await?;
1108        self.disconnected.store(true, Ordering::SeqCst);
1109        Ok(())
1110    }
1111}