Skip to main content

vifu_runtime/
application.rs

1use std::collections::{HashMap, VecDeque};
2use std::fmt;
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::{Arc, Mutex, RwLock};
7use std::thread::JoinHandle;
8use std::time::{Duration, Instant};
9
10use serde::{Deserialize, Serialize};
11use serde_json::{json, Value};
12use tokio::sync::{mpsc, watch, Notify};
13
14use crate::{
15    EffectRequest, EffectResult, LocalProviderBinding, ProjectSettings, RuntimeManifest,
16    RuntimeRelease, RuntimeSnapshot, RuntimeTraceRecord, MAX_ENDPOINT_TIMEOUT_MS,
17};
18
19const SNAPSHOT_VERSION: u32 = 1;
20const DEFAULT_TIMEOUT_MS: u64 = 30_000;
21const DEFAULT_EFFECT_LIMIT: usize = 64;
22const MAX_IN_FLIGHT_INVOCATIONS: usize = 64;
23const MAX_RETAINED_INVOCATIONS: usize = 256;
24const MAX_RETAINED_INVOCATION_EVENTS: usize = 256;
25const MAX_COALESCED_EVENT_BYTES: usize = 64 * 1024;
26const WORKER_QUEUE_CAPACITY: usize = 64;
27const MAX_RUNTIME_MONITOR_IO_BYTES: usize = 128 * 1024;
28
29/// A boxed provider future used by [`AgentProvider`].
30pub type ProviderFuture<'a> =
31    Pin<Box<dyn Future<Output = Result<ProviderResponse, RuntimeError>> + Send + 'a>>;
32
33/// JSON or binary data passed through an embedded runtime invocation.
34#[derive(Clone, PartialEq, Serialize, Deserialize)]
35#[serde(tag = "format", content = "value", rename_all = "camelCase")]
36pub enum InvocationData {
37    Json(Value),
38    Binary(Vec<u8>),
39}
40
41impl fmt::Debug for InvocationData {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            Self::Json(_) => formatter.write_str("InvocationData::Json([REDACTED])"),
45            Self::Binary(bytes) => formatter
46                .debug_tuple("InvocationData::Binary")
47                .field(&format_args!("{} bytes", bytes.len()))
48                .finish(),
49        }
50    }
51}
52
53impl Default for InvocationData {
54    fn default() -> Self {
55        Self::Json(Value::Null)
56    }
57}
58
59/// Input for one application-facing endpoint invocation.
60#[derive(Clone, PartialEq, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct InvocationInput {
63    pub endpoint: String,
64    #[serde(default = "default_session_id")]
65    pub session_id: String,
66    #[serde(default)]
67    pub data: InvocationData,
68    #[serde(default)]
69    pub metadata: Value,
70}
71
72impl InvocationInput {
73    pub fn json(endpoint: impl Into<String>, data: Value) -> Self {
74        Self {
75            endpoint: endpoint.into(),
76            session_id: default_session_id(),
77            data: InvocationData::Json(data),
78            metadata: Value::Object(Default::default()),
79        }
80    }
81
82    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
83        self.session_id = session_id.into();
84        self
85    }
86}
87
88impl fmt::Debug for InvocationInput {
89    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90        formatter
91            .debug_struct("InvocationInput")
92            .field("endpoint", &self.endpoint)
93            .field("session_id", &self.session_id)
94            .field("data", &"[REDACTED]")
95            .field("metadata", &"[REDACTED]")
96            .finish()
97    }
98}
99
100/// An agent registered inside one application runtime.
101#[derive(Clone, PartialEq, Serialize, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct AgentDefinition {
104    pub id: String,
105    pub name: String,
106    pub provider: String,
107    pub capabilities: Vec<String>,
108    #[serde(default)]
109    pub metadata: Value,
110}
111
112impl fmt::Debug for AgentDefinition {
113    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
114        formatter
115            .debug_struct("AgentDefinition")
116            .field("id", &self.id)
117            .field("name", &self.name)
118            .field("provider", &self.provider)
119            .field("capabilities", &self.capabilities)
120            .field("metadata", &"[REDACTED]")
121            .finish()
122    }
123}
124
125/// A stable, named application endpoint backed by one registered agent.
126#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct EndpointDefinition {
129    pub name: String,
130    pub agent: String,
131    pub capability: String,
132    /// Maximum time without a provider event before the invocation is cancelled.
133    #[serde(default = "default_timeout_ms")]
134    pub timeout_ms: u64,
135}
136
137/// Request delivered to a dynamically registered [`AgentProvider`].
138#[derive(Clone)]
139pub struct ProviderRequest {
140    pub project_id: String,
141    pub endpoint: String,
142    pub session_id: String,
143    pub agent: AgentDefinition,
144    pub capability: String,
145    pub data: InvocationData,
146    pub metadata: Value,
147    pub snapshot: RuntimeSnapshot,
148}
149
150impl fmt::Debug for ProviderRequest {
151    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
152        formatter
153            .debug_struct("ProviderRequest")
154            .field("project_id", &self.project_id)
155            .field("endpoint", &self.endpoint)
156            .field("session_id", &self.session_id)
157            .field("agent", &self.agent.id)
158            .field("capability", &self.capability)
159            .field("data", &"[REDACTED]")
160            .field("metadata", &"[REDACTED]")
161            .field("snapshot_revision", &self.snapshot.revision)
162            .finish()
163    }
164}
165
166/// Provider result and an optional replacement for the session's durable state.
167#[derive(Clone, PartialEq, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct ProviderResponse {
170    #[serde(default)]
171    pub data: InvocationData,
172    #[serde(default)]
173    pub metadata: Value,
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub state: Option<Value>,
176}
177
178impl ProviderResponse {
179    pub fn json(data: Value) -> Self {
180        Self {
181            data: InvocationData::Json(data),
182            metadata: Value::Object(Default::default()),
183            state: None,
184        }
185    }
186}
187
188impl fmt::Debug for ProviderResponse {
189    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
190        formatter
191            .debug_struct("ProviderResponse")
192            .field("data", &"[REDACTED]")
193            .field("metadata", &"[REDACTED]")
194            .field("state", &self.state.as_ref().map(|_| "[REDACTED]"))
195            .finish()
196    }
197}
198
199/// Cooperative cancellation signal passed to providers.
200#[derive(Clone, Default)]
201pub struct CancellationToken {
202    inner: Arc<CancellationState>,
203}
204
205#[derive(Default)]
206struct CancellationState {
207    cancelled: std::sync::atomic::AtomicBool,
208    notify: Notify,
209}
210
211impl CancellationToken {
212    pub fn cancel(&self) {
213        if !self.inner.cancelled.swap(true, Ordering::AcqRel) {
214            self.inner.notify.notify_waiters();
215        }
216    }
217
218    pub fn is_cancelled(&self) -> bool {
219        self.inner.cancelled.load(Ordering::Acquire)
220    }
221
222    pub async fn cancelled(&self) {
223        if self.is_cancelled() {
224            return;
225        }
226        self.inner.notify.notified().await;
227    }
228}
229
230impl fmt::Debug for CancellationToken {
231    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
232        formatter
233            .debug_struct("CancellationToken")
234            .field("cancelled", &self.is_cancelled())
235            .finish()
236    }
237}
238
239/// Kind of event emitted while a non-blocking invocation is running.
240#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub enum InvocationEventKind {
243    Started,
244    OutputDelta,
245    Completed,
246    Failed,
247    Cancelled,
248}
249
250/// A provider stage that can be rendered as an observation in a live trace.
251#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
252#[serde(rename_all = "camelCase")]
253pub enum ProviderStage {
254    Queue,
255    Load,
256    Tokenize,
257    Prefill,
258    FirstToken,
259    Decode,
260    Validate,
261}
262
263/// A typed provider event emitted while an invocation is running.
264#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
265#[serde(tag = "type", rename_all = "camelCase")]
266pub enum ProviderEvent {
267    /// Payload-free liveness signal for a provider that is still making
268    /// progress inside a long-running stage.
269    Activity,
270    OutputDelta {
271        data: InvocationData,
272    },
273    StageStarted {
274        stage: ProviderStage,
275        #[serde(default, skip_serializing_if = "is_null")]
276        metadata: Value,
277    },
278    StageCompleted {
279        stage: ProviderStage,
280        elapsed_ms: u64,
281        #[serde(default, skip_serializing_if = "is_null")]
282        metadata: Value,
283    },
284    StageFailed {
285        stage: ProviderStage,
286        elapsed_ms: u64,
287        error: String,
288        #[serde(default, skip_serializing_if = "is_null")]
289        metadata: Value,
290    },
291}
292
293/// Terminal outcome reported to an embedded runtime monitor.
294#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
295#[serde(rename_all = "camelCase")]
296pub enum RuntimeMonitorStatus {
297    Completed,
298    Cancelled,
299    Error,
300}
301
302/// State of a provider stage reported to an embedded runtime monitor.
303#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
304#[serde(rename_all = "camelCase")]
305pub enum RuntimeMonitorStageStatus {
306    Started,
307    Completed,
308    Failed,
309}
310
311/// Payload-safe lifecycle metadata for one embedded runtime invocation.
312///
313/// Prompt content and streamed output are intentionally excluded. Hosts may
314/// forward these events to a remote monitor without exposing model input or
315/// output data.
316#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
317#[serde(tag = "type", rename_all = "camelCase")]
318pub enum RuntimeMonitorEvent {
319    InvocationStarted {
320        trace_id: String,
321        invocation_id: String,
322        project_id: String,
323        endpoint: String,
324        agent_id: String,
325        provider_id: String,
326        capability: String,
327        started_at_ms: u64,
328    },
329    ProviderStage {
330        trace_id: String,
331        invocation_id: String,
332        stage: ProviderStage,
333        status: RuntimeMonitorStageStatus,
334        #[serde(default, skip_serializing_if = "Option::is_none")]
335        elapsed_ms: Option<u64>,
336        request_elapsed_ms: u64,
337        #[serde(default, skip_serializing_if = "Option::is_none")]
338        input_tokens: Option<u64>,
339        #[serde(default, skip_serializing_if = "Option::is_none")]
340        output_tokens: Option<u64>,
341        #[serde(default, skip_serializing_if = "Option::is_none")]
342        resident: Option<bool>,
343        #[serde(default, skip_serializing_if = "Option::is_none")]
344        error: Option<String>,
345    },
346    InvocationFinished {
347        trace_id: String,
348        invocation_id: String,
349        status: RuntimeMonitorStatus,
350        duration_ms: u64,
351        ended_at_ms: u64,
352        #[serde(default, skip_serializing_if = "Option::is_none")]
353        error: Option<String>,
354    },
355}
356
357/// Thread-safe callback installed by an embedding host that wants live,
358/// payload-safe runtime lifecycle metadata.
359pub type RuntimeMonitorObserver = Arc<dyn Fn(RuntimeMonitorEvent) + Send + Sync>;
360
361/// Bounded process-local I/O summary for an opt-in diagnostic observer.
362#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
363#[serde(rename_all = "camelCase")]
364pub struct RuntimeMonitorIoSummary {
365    pub value: Value,
366    pub truncated: bool,
367}
368
369/// Invocation I/O exposed only to an explicitly installed diagnostic observer.
370#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
371#[serde(tag = "type", rename_all = "camelCase")]
372pub enum RuntimeMonitorIoEvent {
373    InvocationInput {
374        trace_id: String,
375        invocation_id: String,
376        summary: RuntimeMonitorIoSummary,
377    },
378    InvocationOutput {
379        trace_id: String,
380        invocation_id: String,
381        summary: RuntimeMonitorIoSummary,
382    },
383}
384
385/// Thread-safe callback for bounded invocation I/O diagnostics.
386pub type RuntimeMonitorIoObserver = Arc<dyn Fn(RuntimeMonitorIoEvent) + Send + Sync>;
387
388/// One ordered event produced by an invocation.
389#[derive(Clone, PartialEq, Serialize, Deserialize)]
390#[serde(rename_all = "camelCase")]
391pub struct InvocationEvent {
392    pub sequence: u64,
393    pub kind: InvocationEventKind,
394    #[serde(default, skip_serializing_if = "Option::is_none")]
395    pub data: Option<InvocationData>,
396    #[serde(default, skip_serializing_if = "Option::is_none")]
397    pub error: Option<String>,
398}
399
400impl fmt::Debug for InvocationEvent {
401    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
402        formatter
403            .debug_struct("InvocationEvent")
404            .field("sequence", &self.sequence)
405            .field("kind", &self.kind)
406            .field("data", &self.data.as_ref().map(|_| "[REDACTED]"))
407            .field("error", &self.error.as_ref().map(|_| "[REDACTED]"))
408            .finish()
409    }
410}
411
412/// Sink supplied to providers that can produce incremental output.
413///
414/// Providers that only return a final response can keep implementing
415/// [`AgentProvider::invoke`]. Streaming providers call [`Self::output_delta`]
416/// while their invocation is running.
417#[derive(Clone)]
418pub struct ProviderEventSink {
419    emit: Arc<dyn Fn(ProviderEvent) + Send + Sync>,
420}
421
422impl ProviderEventSink {
423    fn new(emit: impl Fn(ProviderEvent) + Send + Sync + 'static) -> Self {
424        Self {
425            emit: Arc::new(emit),
426        }
427    }
428
429    /// Creates a sink that forwards every typed provider event to `emit`.
430    pub fn from_fn(emit: impl Fn(ProviderEvent) + Send + Sync + 'static) -> Self {
431        Self::new(emit)
432    }
433
434    pub fn discard() -> Self {
435        Self::new(|_event| {})
436    }
437
438    pub fn output_delta(&self, data: InvocationData) {
439        (self.emit)(ProviderEvent::OutputDelta { data });
440    }
441
442    pub fn activity(&self) {
443        (self.emit)(ProviderEvent::Activity);
444    }
445
446    pub fn stage_started(&self, stage: ProviderStage, metadata: Value) {
447        (self.emit)(ProviderEvent::StageStarted { stage, metadata });
448    }
449
450    pub fn stage_completed(&self, stage: ProviderStage, elapsed_ms: u64, metadata: Value) {
451        (self.emit)(ProviderEvent::StageCompleted {
452            stage,
453            elapsed_ms,
454            metadata,
455        });
456    }
457
458    pub fn stage_failed(
459        &self,
460        stage: ProviderStage,
461        elapsed_ms: u64,
462        error: impl Into<String>,
463        metadata: Value,
464    ) {
465        (self.emit)(ProviderEvent::StageFailed {
466            stage,
467            elapsed_ms,
468            error: error.into(),
469            metadata,
470        });
471    }
472}
473
474impl fmt::Debug for ProviderEventSink {
475    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
476        formatter.write_str("ProviderEventSink")
477    }
478}
479
480/// Runtime-selected provider implementation.
481///
482/// Providers are registered dynamically by name. A provider may hold credentials
483/// internally, but credentials must never be placed in agent definitions,
484/// invocation metadata, snapshots, or returned trace attributes.
485pub trait AgentProvider: Send + Sync + 'static {
486    fn supports(&self, capability: &str) -> bool;
487
488    fn invoke<'a>(
489        &'a self,
490        request: ProviderRequest,
491        cancellation: CancellationToken,
492    ) -> ProviderFuture<'a>;
493
494    fn invoke_with_events<'a>(
495        &'a self,
496        request: ProviderRequest,
497        cancellation: CancellationToken,
498        _events: ProviderEventSink,
499    ) -> ProviderFuture<'a> {
500        self.invoke(request, cancellation)
501    }
502}
503
504/// Persistence adapter supplied by an embedding host.
505///
506/// The default [`MemoryRuntimeStore`] keeps session state in memory. Server
507/// deployments can implement this trait with their database adapter.
508pub trait RuntimeStore: Send + Sync + 'static {
509    fn load(
510        &self,
511        project_id: &str,
512        session_id: &str,
513    ) -> Result<Option<RuntimeSnapshot>, RuntimeError>;
514
515    fn save(
516        &self,
517        project_id: &str,
518        session_id: &str,
519        snapshot: &RuntimeSnapshot,
520    ) -> Result<(), RuntimeError>;
521
522    fn save_release(&self, _release: &RuntimeRelease) -> Result<(), RuntimeError> {
523        Err(RuntimeError::store(
524            "this runtime store does not support releases".to_string(),
525        ))
526    }
527
528    fn load_release(
529        &self,
530        _project_id: &str,
531        _version: u64,
532    ) -> Result<Option<RuntimeRelease>, RuntimeError> {
533        Ok(None)
534    }
535
536    fn list_releases(&self, _project_id: &str) -> Result<Vec<RuntimeRelease>, RuntimeError> {
537        Ok(Vec::new())
538    }
539
540    fn active_release(&self, _project_id: &str) -> Result<Option<u64>, RuntimeError> {
541        Ok(None)
542    }
543
544    fn set_active_release(&self, _project_id: &str, _version: u64) -> Result<(), RuntimeError> {
545        Err(RuntimeError::store(
546            "this runtime store does not support releases".to_string(),
547        ))
548    }
549
550    fn save_local_provider_binding(
551        &self,
552        _project_id: &str,
553        _binding: &LocalProviderBinding,
554    ) -> Result<(), RuntimeError> {
555        Err(RuntimeError::store(
556            "this runtime store does not support provider bindings".to_string(),
557        ))
558    }
559
560    fn local_provider_bindings(
561        &self,
562        _project_id: &str,
563    ) -> Result<Vec<LocalProviderBinding>, RuntimeError> {
564        Ok(Vec::new())
565    }
566
567    fn enqueue_trace(&self, _trace: &RuntimeTraceRecord) -> Result<(), RuntimeError> {
568        Ok(())
569    }
570
571    fn pending_traces(&self, _limit: usize) -> Result<Vec<RuntimeTraceRecord>, RuntimeError> {
572        Ok(Vec::new())
573    }
574
575    fn acknowledge_traces(&self, _trace_ids: &[String]) -> Result<(), RuntimeError> {
576        Ok(())
577    }
578}
579
580/// In-memory persistence used by the standalone embedded runtime.
581#[derive(Default)]
582pub struct MemoryRuntimeStore {
583    snapshots: RwLock<HashMap<(String, String), RuntimeSnapshot>>,
584    releases: RwLock<HashMap<(String, u64), RuntimeRelease>>,
585    active_releases: RwLock<HashMap<String, u64>>,
586    provider_bindings: RwLock<HashMap<(String, String), LocalProviderBinding>>,
587    trace_outbox: RwLock<VecDeque<RuntimeTraceRecord>>,
588}
589
590impl RuntimeStore for MemoryRuntimeStore {
591    fn load(
592        &self,
593        project_id: &str,
594        session_id: &str,
595    ) -> Result<Option<RuntimeSnapshot>, RuntimeError> {
596        let snapshots = self.snapshots.read().map_err(|_| RuntimeError::Internal)?;
597        Ok(snapshots
598            .get(&(project_id.to_string(), session_id.to_string()))
599            .cloned())
600    }
601
602    fn save(
603        &self,
604        project_id: &str,
605        session_id: &str,
606        snapshot: &RuntimeSnapshot,
607    ) -> Result<(), RuntimeError> {
608        let mut snapshots = self.snapshots.write().map_err(|_| RuntimeError::Internal)?;
609        snapshots.insert(
610            (project_id.to_string(), session_id.to_string()),
611            snapshot.clone(),
612        );
613        Ok(())
614    }
615
616    fn save_release(&self, release: &RuntimeRelease) -> Result<(), RuntimeError> {
617        release.validate()?;
618        let key = (release.manifest.project_id.clone(), release.version);
619        let mut releases = self.releases.write().map_err(|_| RuntimeError::Internal)?;
620        if let Some(existing) = releases.get(&key) {
621            if existing != release {
622                return Err(RuntimeError::store(
623                    "runtime release versions are immutable".to_string(),
624                ));
625            }
626            return Ok(());
627        }
628        releases.insert(key, release.clone());
629        Ok(())
630    }
631
632    fn load_release(
633        &self,
634        project_id: &str,
635        version: u64,
636    ) -> Result<Option<RuntimeRelease>, RuntimeError> {
637        Ok(self
638            .releases
639            .read()
640            .map_err(|_| RuntimeError::Internal)?
641            .get(&(project_id.to_string(), version))
642            .cloned())
643    }
644
645    fn list_releases(&self, project_id: &str) -> Result<Vec<RuntimeRelease>, RuntimeError> {
646        let mut releases = self
647            .releases
648            .read()
649            .map_err(|_| RuntimeError::Internal)?
650            .iter()
651            .filter(|((stored_project_id, _), _)| stored_project_id == project_id)
652            .map(|(_, release)| release.clone())
653            .collect::<Vec<_>>();
654        releases.sort_by_key(|release| std::cmp::Reverse(release.version));
655        Ok(releases)
656    }
657
658    fn active_release(&self, project_id: &str) -> Result<Option<u64>, RuntimeError> {
659        Ok(self
660            .active_releases
661            .read()
662            .map_err(|_| RuntimeError::Internal)?
663            .get(project_id)
664            .copied())
665    }
666
667    fn set_active_release(&self, project_id: &str, version: u64) -> Result<(), RuntimeError> {
668        if !self
669            .releases
670            .read()
671            .map_err(|_| RuntimeError::Internal)?
672            .contains_key(&(project_id.to_string(), version))
673        {
674            return Err(RuntimeError::store("runtime release was not found"));
675        }
676        self.active_releases
677            .write()
678            .map_err(|_| RuntimeError::Internal)?
679            .insert(project_id.to_string(), version);
680        Ok(())
681    }
682
683    fn save_local_provider_binding(
684        &self,
685        project_id: &str,
686        binding: &LocalProviderBinding,
687    ) -> Result<(), RuntimeError> {
688        self.provider_bindings
689            .write()
690            .map_err(|_| RuntimeError::Internal)?
691            .insert(
692                (project_id.to_string(), binding.provider_id.clone()),
693                binding.clone(),
694            );
695        Ok(())
696    }
697
698    fn local_provider_bindings(
699        &self,
700        project_id: &str,
701    ) -> Result<Vec<LocalProviderBinding>, RuntimeError> {
702        let mut bindings = self
703            .provider_bindings
704            .read()
705            .map_err(|_| RuntimeError::Internal)?
706            .iter()
707            .filter(|((stored_project_id, _), _)| stored_project_id == project_id)
708            .map(|(_, binding)| binding.clone())
709            .collect::<Vec<_>>();
710        bindings.sort_by(|left, right| left.provider_id.cmp(&right.provider_id));
711        Ok(bindings)
712    }
713
714    fn enqueue_trace(&self, trace: &RuntimeTraceRecord) -> Result<(), RuntimeError> {
715        const MAX_MEMORY_TRACES: usize = 1_000;
716        let mut traces = self
717            .trace_outbox
718            .write()
719            .map_err(|_| RuntimeError::Internal)?;
720        if traces.iter().any(|stored| stored.id == trace.id) {
721            return Ok(());
722        }
723        traces.push_back(trace.clone());
724        while traces.len() > MAX_MEMORY_TRACES {
725            traces.pop_front();
726        }
727        Ok(())
728    }
729
730    fn pending_traces(&self, limit: usize) -> Result<Vec<RuntimeTraceRecord>, RuntimeError> {
731        Ok(self
732            .trace_outbox
733            .read()
734            .map_err(|_| RuntimeError::Internal)?
735            .iter()
736            .take(limit)
737            .cloned()
738            .collect())
739    }
740
741    fn acknowledge_traces(&self, trace_ids: &[String]) -> Result<(), RuntimeError> {
742        self.trace_outbox
743            .write()
744            .map_err(|_| RuntimeError::Internal)?
745            .retain(|trace| !trace_ids.contains(&trace.id));
746        Ok(())
747    }
748}
749
750/// One safe trace event emitted by the application runtime.
751#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
752#[serde(rename_all = "camelCase")]
753pub struct InvocationTraceEvent {
754    pub name: String,
755    pub status: String,
756    pub duration_ms: u64,
757    #[serde(default)]
758    pub attributes: Value,
759}
760
761/// Result of one endpoint invocation.
762#[derive(Clone, PartialEq, Serialize, Deserialize)]
763#[serde(rename_all = "camelCase")]
764pub struct InvocationOutput {
765    pub invocation_id: String,
766    pub project_id: String,
767    pub endpoint: String,
768    pub session_id: String,
769    pub agent: String,
770    pub provider: String,
771    pub capability: String,
772    pub data: InvocationData,
773    #[serde(default)]
774    pub metadata: Value,
775    pub snapshot: RuntimeSnapshot,
776    pub trace: Vec<InvocationTraceEvent>,
777}
778
779impl fmt::Debug for InvocationOutput {
780    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
781        formatter
782            .debug_struct("InvocationOutput")
783            .field("invocation_id", &self.invocation_id)
784            .field("project_id", &self.project_id)
785            .field("endpoint", &self.endpoint)
786            .field("session_id", &self.session_id)
787            .field("agent", &self.agent)
788            .field("provider", &self.provider)
789            .field("capability", &self.capability)
790            .field("data", &"[REDACTED]")
791            .field("metadata", &"[REDACTED]")
792            .field("snapshot_revision", &self.snapshot.revision)
793            .field("trace_count", &self.trace.len())
794            .finish()
795    }
796}
797
798/// Opaque handle returned by the game-loop invocation API.
799#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
800pub struct InvocationHandle(pub String);
801
802/// Current state of an invocation started with [`VifuRuntime::start_invoke`].
803#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
804#[serde(rename_all = "camelCase")]
805pub enum InvocationStatus {
806    Pending,
807    Running,
808    Completed,
809    Failed,
810    Cancelled,
811}
812
813/// Non-blocking game-loop poll result.
814#[derive(Clone, PartialEq, Serialize, Deserialize)]
815#[serde(rename_all = "camelCase")]
816pub struct InvocationPoll {
817    pub handle: InvocationHandle,
818    pub status: InvocationStatus,
819    #[serde(default, skip_serializing_if = "Option::is_none")]
820    pub output: Option<InvocationOutput>,
821    #[serde(default, skip_serializing_if = "Option::is_none")]
822    pub error: Option<String>,
823}
824
825impl fmt::Debug for InvocationPoll {
826    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
827        formatter
828            .debug_struct("InvocationPoll")
829            .field("handle", &self.handle)
830            .field("status", &self.status)
831            .field("output", &self.output.as_ref().map(|_| "[REDACTED]"))
832            .field("error", &self.error.as_ref().map(|_| "[REDACTED]"))
833            .finish()
834    }
835}
836
837/// Result of running host effects through the runtime.
838#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
839#[serde(rename_all = "camelCase")]
840pub struct EffectExecution {
841    pub results: Vec<EffectResult>,
842    pub unhandled: Vec<EffectRequest>,
843}
844
845/// Errors returned by the embedded application runtime.
846pub enum RuntimeError {
847    InvalidDefinition(String),
848    EndpointNotFound(String),
849    AgentNotFound(String),
850    ProviderNotFound(String),
851    CapabilityUnavailable {
852        provider: String,
853        capability: String,
854    },
855    Timeout(u64),
856    Cancelled,
857    Unavailable(String),
858    Backpressure(String),
859    Provider {
860        provider: String,
861        message: String,
862    },
863    Store(String),
864    Snapshot(String),
865    EffectLimitExceeded(usize),
866    InvocationNotFound(String),
867    Internal,
868}
869
870impl RuntimeError {
871    pub fn provider(provider: impl Into<String>, message: impl Into<String>) -> Self {
872        Self::Provider {
873            provider: provider.into(),
874            message: message.into(),
875        }
876    }
877
878    pub fn store(message: impl Into<String>) -> Self {
879        Self::Store(message.into())
880    }
881
882    pub fn public_message(&self) -> String {
883        match self {
884            Self::Provider { provider, .. } => {
885                format!("provider {provider} request failed")
886            }
887            Self::Store(_) => "runtime state could not be persisted".to_string(),
888            Self::Snapshot(_) => "runtime snapshot is invalid".to_string(),
889            Self::Unavailable(_) => "provider is not available".to_string(),
890            Self::Backpressure(_) => "runtime is busy".to_string(),
891            _ => self.to_string(),
892        }
893    }
894}
895
896impl fmt::Debug for RuntimeError {
897    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
898        match self {
899            Self::InvalidDefinition(_) => formatter.write_str("InvalidDefinition([REDACTED])"),
900            Self::EndpointNotFound(endpoint) => formatter
901                .debug_tuple("EndpointNotFound")
902                .field(endpoint)
903                .finish(),
904            Self::AgentNotFound(agent) => {
905                formatter.debug_tuple("AgentNotFound").field(agent).finish()
906            }
907            Self::ProviderNotFound(provider) => formatter
908                .debug_tuple("ProviderNotFound")
909                .field(provider)
910                .finish(),
911            Self::CapabilityUnavailable {
912                provider,
913                capability,
914            } => formatter
915                .debug_struct("CapabilityUnavailable")
916                .field("provider", provider)
917                .field("capability", capability)
918                .finish(),
919            Self::Timeout(timeout) => formatter.debug_tuple("Timeout").field(timeout).finish(),
920            Self::Cancelled => formatter.write_str("Cancelled"),
921            Self::Unavailable(_) => formatter.write_str("Unavailable([REDACTED])"),
922            Self::Backpressure(_) => formatter.write_str("Backpressure([REDACTED])"),
923            Self::Provider { provider, .. } => formatter
924                .debug_struct("Provider")
925                .field("provider", provider)
926                .field("message", &"[REDACTED]")
927                .finish(),
928            Self::Store(_) => formatter.write_str("Store([REDACTED])"),
929            Self::Snapshot(_) => formatter.write_str("Snapshot([REDACTED])"),
930            Self::EffectLimitExceeded(limit) => formatter
931                .debug_tuple("EffectLimitExceeded")
932                .field(limit)
933                .finish(),
934            Self::InvocationNotFound(handle) => formatter
935                .debug_tuple("InvocationNotFound")
936                .field(handle)
937                .finish(),
938            Self::Internal => formatter.write_str("Internal"),
939        }
940    }
941}
942
943impl fmt::Display for RuntimeError {
944    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
945        match self {
946            Self::InvalidDefinition(message) => {
947                write!(formatter, "invalid runtime definition: {message}")
948            }
949            Self::EndpointNotFound(endpoint) => {
950                write!(formatter, "endpoint {endpoint} is not registered")
951            }
952            Self::AgentNotFound(agent) => write!(formatter, "agent {agent} is not registered"),
953            Self::ProviderNotFound(provider) => {
954                write!(formatter, "provider {provider} is not registered")
955            }
956            Self::CapabilityUnavailable {
957                provider,
958                capability,
959            } => write!(
960                formatter,
961                "provider {provider} does not support capability {capability}"
962            ),
963            Self::Timeout(timeout_ms) => {
964                write!(formatter, "agent invocation was idle for {timeout_ms} ms")
965            }
966            Self::Cancelled => formatter.write_str("agent invocation was cancelled"),
967            Self::Unavailable(message) => {
968                write!(formatter, "provider is not available: {message}")
969            }
970            Self::Backpressure(message) => write!(formatter, "runtime is busy: {message}"),
971            Self::Provider { provider, message } => {
972                write!(formatter, "provider {provider} failed: {message}")
973            }
974            Self::Store(message) => write!(formatter, "runtime store failed: {message}"),
975            Self::Snapshot(message) => write!(formatter, "runtime snapshot failed: {message}"),
976            Self::EffectLimitExceeded(limit) => {
977                write!(formatter, "runtime effect limit {limit} was exceeded")
978            }
979            Self::InvocationNotFound(handle) => {
980                write!(formatter, "invocation {handle} was not found")
981            }
982            Self::Internal => formatter.write_str("runtime internal error"),
983        }
984    }
985}
986
987impl std::error::Error for RuntimeError {}
988
989#[derive(Default)]
990struct RuntimeRegistry {
991    providers: HashMap<String, Arc<dyn AgentProvider>>,
992    agents: HashMap<String, AgentDefinition>,
993    endpoints: HashMap<String, EndpointDefinition>,
994}
995
996struct RuntimeCore {
997    project_id: String,
998    registry: RwLock<RuntimeRegistry>,
999    manifest: RwLock<Option<RuntimeManifest>>,
1000    store: Arc<dyn RuntimeStore>,
1001    sessions: RwLock<HashMap<String, RuntimeSnapshot>>,
1002    session_locks: Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>,
1003    invocations: Mutex<InvocationRegistry>,
1004    next_invocation: AtomicU64,
1005    monitor_observer: RwLock<Option<RuntimeMonitorObserver>>,
1006    monitor_io_observer: RwLock<Option<RuntimeMonitorIoObserver>>,
1007}
1008
1009struct InvocationEntry {
1010    poll: InvocationPoll,
1011    cancellation: CancellationToken,
1012    events: VecDeque<InvocationEvent>,
1013    next_event_sequence: u64,
1014}
1015
1016#[derive(Default)]
1017struct InvocationRegistry {
1018    entries: HashMap<String, InvocationEntry>,
1019    terminal_order: VecDeque<String>,
1020    active_count: usize,
1021}
1022
1023impl InvocationRegistry {
1024    fn insert(
1025        &mut self,
1026        handle: InvocationHandle,
1027        cancellation: CancellationToken,
1028    ) -> Result<(), RuntimeError> {
1029        if self.active_count >= MAX_IN_FLIGHT_INVOCATIONS {
1030            return Err(RuntimeError::Backpressure(
1031                "too many invocations are already running".to_string(),
1032            ));
1033        }
1034        self.entries.insert(
1035            handle.0.clone(),
1036            InvocationEntry {
1037                poll: InvocationPoll {
1038                    handle,
1039                    status: InvocationStatus::Pending,
1040                    output: None,
1041                    error: None,
1042                },
1043                cancellation,
1044                events: VecDeque::new(),
1045                next_event_sequence: 1,
1046            },
1047        );
1048        self.active_count += 1;
1049        Ok(())
1050    }
1051
1052    fn update(
1053        &mut self,
1054        handle: &InvocationHandle,
1055        status: InvocationStatus,
1056        output: Option<InvocationOutput>,
1057        error: Option<String>,
1058    ) {
1059        let Some(entry) = self.entries.get_mut(&handle.0) else {
1060            return;
1061        };
1062        if is_terminal_status(entry.poll.status) {
1063            return;
1064        }
1065        entry.poll.status = status;
1066        entry.poll.output = output;
1067        entry.poll.error = error;
1068        let event = match status {
1069            InvocationStatus::Pending => None,
1070            InvocationStatus::Running => Some((InvocationEventKind::Started, None, None)),
1071            InvocationStatus::Completed => Some((
1072                InvocationEventKind::Completed,
1073                entry.poll.output.as_ref().map(|value| value.data.clone()),
1074                None,
1075            )),
1076            InvocationStatus::Failed => {
1077                Some((InvocationEventKind::Failed, None, entry.poll.error.clone()))
1078            }
1079            InvocationStatus::Cancelled => Some((InvocationEventKind::Cancelled, None, None)),
1080        };
1081        if let Some((kind, data, error)) = event {
1082            entry.push_event(kind, data, error);
1083        }
1084        if is_terminal_status(status) {
1085            self.active_count = self.active_count.saturating_sub(1);
1086            self.terminal_order.push_back(handle.0.clone());
1087            self.evict_old_terminal_entries();
1088        }
1089    }
1090
1091    fn remove(&mut self, handle: &InvocationHandle) {
1092        if let Some(entry) = self.entries.remove(&handle.0) {
1093            if !is_terminal_status(entry.poll.status) {
1094                self.active_count = self.active_count.saturating_sub(1);
1095            }
1096        }
1097        self.terminal_order.retain(|stored| stored != &handle.0);
1098    }
1099
1100    fn take(&mut self, handle: &InvocationHandle) -> Result<InvocationPoll, RuntimeError> {
1101        let poll = self
1102            .entries
1103            .get(&handle.0)
1104            .map(|entry| entry.poll.clone())
1105            .ok_or_else(|| RuntimeError::InvocationNotFound(handle.0.clone()))?;
1106        if is_terminal_status(poll.status) {
1107            self.remove(handle);
1108        }
1109        Ok(poll)
1110    }
1111
1112    fn push_provider_event(&mut self, handle: &InvocationHandle, event: ProviderEvent) {
1113        let Some(entry) = self.entries.get_mut(&handle.0) else {
1114            return;
1115        };
1116        if entry.poll.status != InvocationStatus::Running {
1117            return;
1118        }
1119        match event {
1120            ProviderEvent::Activity => {}
1121            ProviderEvent::OutputDelta { data } => {
1122                entry.push_event(InvocationEventKind::OutputDelta, Some(data), None);
1123            }
1124            ProviderEvent::StageStarted { .. }
1125            | ProviderEvent::StageCompleted { .. }
1126            | ProviderEvent::StageFailed { .. } => {}
1127        }
1128    }
1129
1130    fn drain_events(
1131        &mut self,
1132        handle: &InvocationHandle,
1133    ) -> Result<Vec<InvocationEvent>, RuntimeError> {
1134        let entry = self
1135            .entries
1136            .get_mut(&handle.0)
1137            .ok_or_else(|| RuntimeError::InvocationNotFound(handle.0.clone()))?;
1138        Ok(entry.events.drain(..).collect())
1139    }
1140
1141    fn evict_old_terminal_entries(&mut self) {
1142        while self.terminal_order.len() > MAX_RETAINED_INVOCATIONS {
1143            if let Some(handle) = self.terminal_order.pop_front() {
1144                self.entries.remove(&handle);
1145            }
1146        }
1147    }
1148}
1149
1150impl InvocationEntry {
1151    fn push_event(
1152        &mut self,
1153        kind: InvocationEventKind,
1154        data: Option<InvocationData>,
1155        error: Option<String>,
1156    ) {
1157        if kind == InvocationEventKind::OutputDelta {
1158            if let (
1159                Some(InvocationEvent {
1160                    kind: InvocationEventKind::OutputDelta,
1161                    data: Some(previous),
1162                    ..
1163                }),
1164                Some(next),
1165            ) = (self.events.back_mut(), data.as_ref())
1166            {
1167                if merge_invocation_data(previous, next) {
1168                    return;
1169                }
1170            }
1171        }
1172        self.events.push_back(InvocationEvent {
1173            sequence: self.next_event_sequence,
1174            kind,
1175            data,
1176            error,
1177        });
1178        self.next_event_sequence = self.next_event_sequence.saturating_add(1);
1179        while self.events.len() > MAX_RETAINED_INVOCATION_EVENTS {
1180            self.events.pop_front();
1181        }
1182    }
1183}
1184
1185impl RuntimeCore {
1186    async fn invoke(
1187        self: &Arc<Self>,
1188        invocation_id: String,
1189        input: InvocationInput,
1190        cancellation: CancellationToken,
1191        forwarded_events: ProviderEventSink,
1192    ) -> Result<InvocationOutput, RuntimeError> {
1193        let endpoint = input.endpoint.clone();
1194        let created_at_ms = crate::unix_time_ms();
1195        let trace_id = format!("trace-{created_at_ms}-{invocation_id}");
1196        let started = Instant::now();
1197        let result = self
1198            .invoke_provider(
1199                trace_id.clone(),
1200                created_at_ms,
1201                invocation_id.clone(),
1202                input,
1203                cancellation,
1204                forwarded_events,
1205            )
1206            .await;
1207        let elapsed_ms = duration_ms(started.elapsed());
1208        let trace = match &result {
1209            Ok(output) => RuntimeTraceRecord {
1210                id: trace_id.clone(),
1211                project_id: self.project_id.clone(),
1212                invocation_id: invocation_id.clone(),
1213                endpoint: endpoint.clone(),
1214                agent: Some(output.agent.clone()),
1215                provider: Some(output.provider.clone()),
1216                capability: Some(output.capability.clone()),
1217                status: "completed".to_string(),
1218                duration_ms: elapsed_ms,
1219                created_at_ms,
1220            },
1221            Err(error) => RuntimeTraceRecord {
1222                id: trace_id.clone(),
1223                project_id: self.project_id.clone(),
1224                invocation_id: invocation_id.clone(),
1225                endpoint,
1226                agent: None,
1227                provider: None,
1228                capability: None,
1229                status: match error {
1230                    RuntimeError::Cancelled => "cancelled",
1231                    _ => "error",
1232                }
1233                .to_string(),
1234                duration_ms: elapsed_ms,
1235                created_at_ms,
1236            },
1237        };
1238        let _ = self.store.enqueue_trace(&trace);
1239        if let Ok(output) = &result {
1240            self.emit_monitor_io_event(RuntimeMonitorIoEvent::InvocationOutput {
1241                trace_id: trace_id.clone(),
1242                invocation_id: invocation_id.clone(),
1243                summary: runtime_monitor_io_summary(&output.data),
1244            });
1245        }
1246        self.emit_monitor_event(RuntimeMonitorEvent::InvocationFinished {
1247            trace_id,
1248            invocation_id,
1249            status: match &result {
1250                Ok(_) => RuntimeMonitorStatus::Completed,
1251                Err(RuntimeError::Cancelled) => RuntimeMonitorStatus::Cancelled,
1252                Err(_) => RuntimeMonitorStatus::Error,
1253            },
1254            duration_ms: elapsed_ms,
1255            // Derive the terminal timestamp from the invocation start and a
1256            // monotonic duration. A wall-clock adjustment during the request
1257            // must not produce an end time before the start time.
1258            ended_at_ms: created_at_ms.saturating_add(elapsed_ms),
1259            error: result.as_ref().err().map(RuntimeError::public_message),
1260        });
1261        result
1262    }
1263
1264    async fn invoke_provider(
1265        self: &Arc<Self>,
1266        trace_id: String,
1267        started_at_ms: u64,
1268        invocation_id: String,
1269        input: InvocationInput,
1270        cancellation: CancellationToken,
1271        forwarded_events: ProviderEventSink,
1272    ) -> Result<InvocationOutput, RuntimeError> {
1273        validate_identifier("endpoint", &input.endpoint)?;
1274        validate_identifier("session", &input.session_id)?;
1275        let session_lock = {
1276            let mut locks = self
1277                .session_locks
1278                .lock()
1279                .map_err(|_| RuntimeError::Internal)?;
1280            Arc::clone(
1281                locks
1282                    .entry(input.session_id.clone())
1283                    .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(()))),
1284            )
1285        };
1286        let _session_guard = session_lock.lock().await;
1287        let (endpoint, agent, provider) = {
1288            let registry = self.registry.read().map_err(|_| RuntimeError::Internal)?;
1289            let endpoint = registry
1290                .endpoints
1291                .get(&input.endpoint)
1292                .cloned()
1293                .ok_or_else(|| RuntimeError::EndpointNotFound(input.endpoint.clone()))?;
1294            let agent = registry
1295                .agents
1296                .get(&endpoint.agent)
1297                .cloned()
1298                .ok_or_else(|| RuntimeError::AgentNotFound(endpoint.agent.clone()))?;
1299            let provider = registry
1300                .providers
1301                .get(&agent.provider)
1302                .cloned()
1303                .ok_or_else(|| RuntimeError::ProviderNotFound(agent.provider.clone()))?;
1304            (endpoint, agent, provider)
1305        };
1306        if !agent
1307            .capabilities
1308            .iter()
1309            .any(|capability| capability == &endpoint.capability)
1310            || !provider.supports(&endpoint.capability)
1311        {
1312            return Err(RuntimeError::CapabilityUnavailable {
1313                provider: agent.provider.clone(),
1314                capability: endpoint.capability,
1315            });
1316        }
1317        if cancellation.is_cancelled() {
1318            return Err(RuntimeError::Cancelled);
1319        }
1320
1321        self.emit_monitor_event(RuntimeMonitorEvent::InvocationStarted {
1322            trace_id: trace_id.clone(),
1323            invocation_id: invocation_id.clone(),
1324            project_id: self.project_id.clone(),
1325            endpoint: endpoint.name.clone(),
1326            agent_id: agent.id.clone(),
1327            provider_id: agent.provider.clone(),
1328            capability: endpoint.capability.clone(),
1329            started_at_ms,
1330        });
1331        self.emit_monitor_io_event(RuntimeMonitorIoEvent::InvocationInput {
1332            trace_id: trace_id.clone(),
1333            invocation_id: invocation_id.clone(),
1334            summary: runtime_monitor_io_summary(&input.data),
1335        });
1336
1337        let snapshot = self.load_snapshot(&input.session_id)?;
1338        let request = ProviderRequest {
1339            project_id: self.project_id.clone(),
1340            endpoint: endpoint.name.clone(),
1341            session_id: input.session_id.clone(),
1342            agent: agent.clone(),
1343            capability: endpoint.capability.clone(),
1344            data: input.data,
1345            metadata: input.metadata,
1346            snapshot: snapshot.clone(),
1347        };
1348        let started = Instant::now();
1349        let (activity_sender, mut activity_receiver) = watch::channel(0_u64);
1350        let events = self.provider_event_sink(
1351            &InvocationHandle(invocation_id.clone()),
1352            trace_id,
1353            started,
1354            activity_sender,
1355            forwarded_events,
1356        );
1357        let provider_call = provider.invoke_with_events(request, cancellation.clone(), events);
1358        tokio::pin!(provider_call);
1359        let idle_timeout = Duration::from_millis(endpoint.timeout_ms);
1360        let idle_deadline = tokio::time::sleep(idle_timeout);
1361        tokio::pin!(idle_deadline);
1362        let mut activity_open = true;
1363        let response = loop {
1364            tokio::select! {
1365                biased;
1366                _ = cancellation.cancelled() => return Err(RuntimeError::Cancelled),
1367                response = &mut provider_call => break response?,
1368                changed = activity_receiver.changed(), if activity_open => {
1369                    if changed.is_err() {
1370                        activity_open = false;
1371                    } else {
1372                        idle_deadline.as_mut().reset(tokio::time::Instant::now() + idle_timeout);
1373                    }
1374                }
1375                _ = &mut idle_deadline => {
1376                    cancellation.cancel();
1377                    return Err(RuntimeError::Timeout(endpoint.timeout_ms));
1378                }
1379            }
1380        };
1381        if cancellation.is_cancelled() {
1382            return Err(RuntimeError::Cancelled);
1383        }
1384
1385        let next_snapshot = RuntimeSnapshot {
1386            revision: snapshot.revision.saturating_add(1),
1387            state: response.state.unwrap_or(snapshot.state),
1388        };
1389        self.store
1390            .save(&self.project_id, &input.session_id, &next_snapshot)?;
1391        self.sessions
1392            .write()
1393            .map_err(|_| RuntimeError::Internal)?
1394            .insert(input.session_id.clone(), next_snapshot.clone());
1395        Ok(InvocationOutput {
1396            invocation_id,
1397            project_id: self.project_id.clone(),
1398            endpoint: endpoint.name,
1399            session_id: input.session_id,
1400            agent: agent.id,
1401            provider: agent.provider,
1402            capability: endpoint.capability,
1403            data: response.data,
1404            metadata: response.metadata,
1405            snapshot: next_snapshot,
1406            trace: vec![InvocationTraceEvent {
1407                name: "provider.invoke".to_string(),
1408                status: "completed".to_string(),
1409                duration_ms: duration_ms(started.elapsed()),
1410                attributes: json!({
1411                    "endpoint": input.endpoint,
1412                }),
1413            }],
1414        })
1415    }
1416
1417    fn load_snapshot(&self, session_id: &str) -> Result<RuntimeSnapshot, RuntimeError> {
1418        if let Some(snapshot) = self
1419            .sessions
1420            .read()
1421            .map_err(|_| RuntimeError::Internal)?
1422            .get(session_id)
1423            .cloned()
1424        {
1425            return Ok(snapshot);
1426        }
1427        let snapshot = self
1428            .store
1429            .load(&self.project_id, session_id)?
1430            .unwrap_or_default();
1431        self.sessions
1432            .write()
1433            .map_err(|_| RuntimeError::Internal)?
1434            .insert(session_id.to_string(), snapshot.clone());
1435        Ok(snapshot)
1436    }
1437
1438    fn next_invocation_id(&self) -> String {
1439        let id = self.next_invocation.fetch_add(1, Ordering::Relaxed);
1440        format!("invocation-{id}")
1441    }
1442
1443    fn update_poll(
1444        &self,
1445        handle: &InvocationHandle,
1446        status: InvocationStatus,
1447        output: Option<InvocationOutput>,
1448        error: Option<String>,
1449    ) {
1450        if let Ok(mut invocations) = self.invocations.lock() {
1451            invocations.update(handle, status, output, error);
1452        }
1453    }
1454
1455    fn provider_event_sink(
1456        self: &Arc<Self>,
1457        handle: &InvocationHandle,
1458        trace_id: String,
1459        request_started: Instant,
1460        activity: watch::Sender<u64>,
1461        forwarded_events: ProviderEventSink,
1462    ) -> ProviderEventSink {
1463        let core = Arc::clone(self);
1464        let handle = handle.clone();
1465        ProviderEventSink::new(move |event| {
1466            activity.send_modify(|sequence| *sequence = sequence.saturating_add(1));
1467            (forwarded_events.emit)(event.clone());
1468            if let Ok(mut invocations) = core.invocations.lock() {
1469                invocations.push_provider_event(&handle, event.clone());
1470            }
1471            let (stage, status, elapsed_ms, metadata, error) = match event {
1472                ProviderEvent::Activity => return,
1473                ProviderEvent::OutputDelta { .. } => return,
1474                ProviderEvent::StageStarted { stage, metadata } => (
1475                    stage,
1476                    RuntimeMonitorStageStatus::Started,
1477                    None,
1478                    metadata,
1479                    None,
1480                ),
1481                ProviderEvent::StageCompleted {
1482                    stage,
1483                    elapsed_ms,
1484                    metadata,
1485                } => (
1486                    stage,
1487                    RuntimeMonitorStageStatus::Completed,
1488                    Some(elapsed_ms),
1489                    metadata,
1490                    None,
1491                ),
1492                ProviderEvent::StageFailed {
1493                    stage,
1494                    elapsed_ms,
1495                    error,
1496                    metadata,
1497                } => (
1498                    stage,
1499                    RuntimeMonitorStageStatus::Failed,
1500                    Some(elapsed_ms),
1501                    metadata,
1502                    Some(error),
1503                ),
1504            };
1505            core.emit_monitor_event(RuntimeMonitorEvent::ProviderStage {
1506                trace_id: trace_id.clone(),
1507                invocation_id: handle.0.clone(),
1508                stage,
1509                status,
1510                elapsed_ms,
1511                request_elapsed_ms: duration_ms(request_started.elapsed()),
1512                input_tokens: monitor_u64(&metadata, "inputTokens"),
1513                output_tokens: monitor_u64(&metadata, "outputTokens"),
1514                resident: metadata.get("resident").and_then(Value::as_bool),
1515                error,
1516            });
1517        })
1518    }
1519
1520    fn emit_monitor_event(&self, event: RuntimeMonitorEvent) {
1521        let observer = self
1522            .monitor_observer
1523            .read()
1524            .ok()
1525            .and_then(|observer| observer.clone());
1526        if let Some(observer) = observer {
1527            observer(event);
1528        }
1529    }
1530
1531    fn emit_monitor_io_event(&self, event: RuntimeMonitorIoEvent) {
1532        let observer = self
1533            .monitor_io_observer
1534            .read()
1535            .ok()
1536            .and_then(|observer| observer.clone());
1537        if let Some(observer) = observer {
1538            observer(event);
1539        }
1540    }
1541}
1542
1543fn runtime_monitor_io_summary(data: &InvocationData) -> RuntimeMonitorIoSummary {
1544    match data {
1545        InvocationData::Binary(bytes) => RuntimeMonitorIoSummary {
1546            value: json!({
1547                "_vifuBinary": true,
1548                "bytes": bytes.len(),
1549            }),
1550            truncated: true,
1551        },
1552        InvocationData::Json(value)
1553            if serde_json::to_vec(value)
1554                .is_ok_and(|encoded| encoded.len() <= MAX_RUNTIME_MONITOR_IO_BYTES) =>
1555        {
1556            RuntimeMonitorIoSummary {
1557                value: value.clone(),
1558                truncated: false,
1559            }
1560        }
1561        InvocationData::Json(value) => RuntimeMonitorIoSummary {
1562            value: json!({
1563                "summary": monitor_value_shape(value),
1564                "truncated": true,
1565            }),
1566            truncated: true,
1567        },
1568    }
1569}
1570
1571fn monitor_value_shape(value: &Value) -> &'static str {
1572    match value {
1573        Value::Null => "null",
1574        Value::Bool(_) => "boolean",
1575        Value::Number(_) => "number",
1576        Value::String(_) => "string",
1577        Value::Array(_) => "array",
1578        Value::Object(_) => "object",
1579    }
1580}
1581
1582fn monitor_u64(metadata: &Value, key: &str) -> Option<u64> {
1583    metadata.get(key).and_then(Value::as_u64)
1584}
1585
1586fn is_null(value: &Value) -> bool {
1587    value.is_null()
1588}
1589
1590fn merge_invocation_data(previous: &mut InvocationData, next: &InvocationData) -> bool {
1591    match (previous, next) {
1592        (
1593            InvocationData::Json(Value::String(previous)),
1594            InvocationData::Json(Value::String(next)),
1595        ) if previous.len().saturating_add(next.len()) <= MAX_COALESCED_EVENT_BYTES => {
1596            previous.push_str(next);
1597            true
1598        }
1599        (InvocationData::Binary(previous), InvocationData::Binary(next))
1600            if previous.len().saturating_add(next.len()) <= MAX_COALESCED_EVENT_BYTES =>
1601        {
1602            previous.extend_from_slice(next);
1603            true
1604        }
1605        _ => false,
1606    }
1607}
1608
1609enum WorkerCommand {
1610    Start {
1611        handle: InvocationHandle,
1612        input: InvocationInput,
1613        cancellation: CancellationToken,
1614    },
1615}
1616
1617struct RuntimeWorker {
1618    sender: Mutex<Option<mpsc::Sender<WorkerCommand>>>,
1619    thread: Mutex<Option<JoinHandle<()>>>,
1620}
1621
1622impl RuntimeWorker {
1623    fn spawn(core: Arc<RuntimeCore>) -> Result<Self, RuntimeError> {
1624        let (sender, mut receiver) = mpsc::channel(WORKER_QUEUE_CAPACITY);
1625        let thread = std::thread::Builder::new()
1626            .name(format!("vifu-runtime-{}", core.project_id))
1627            .spawn(move || {
1628                let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
1629                    .enable_time()
1630                    .build()
1631                else {
1632                    return;
1633                };
1634                runtime.block_on(async move {
1635                    while let Some(command) = receiver.recv().await {
1636                        match command {
1637                            WorkerCommand::Start {
1638                                handle,
1639                                input,
1640                                cancellation,
1641                            } => {
1642                                let invocation_core = Arc::clone(&core);
1643                                tokio::spawn(async move {
1644                                    invocation_core.update_poll(
1645                                        &handle,
1646                                        InvocationStatus::Running,
1647                                        None,
1648                                        None,
1649                                    );
1650                                    let result = invocation_core
1651                                        .invoke(
1652                                            handle.0.clone(),
1653                                            input,
1654                                            cancellation,
1655                                            ProviderEventSink::discard(),
1656                                        )
1657                                        .await;
1658                                    match result {
1659                                        Ok(output) => invocation_core.update_poll(
1660                                            &handle,
1661                                            InvocationStatus::Completed,
1662                                            Some(output),
1663                                            None,
1664                                        ),
1665                                        Err(RuntimeError::Cancelled) => invocation_core
1666                                            .update_poll(
1667                                                &handle,
1668                                                InvocationStatus::Cancelled,
1669                                                None,
1670                                                None,
1671                                            ),
1672                                        Err(error) => invocation_core.update_poll(
1673                                            &handle,
1674                                            InvocationStatus::Failed,
1675                                            None,
1676                                            Some(error.public_message()),
1677                                        ),
1678                                    }
1679                                });
1680                            }
1681                        }
1682                    }
1683                });
1684            })
1685            .map_err(|_error| RuntimeError::Internal)?;
1686        Ok(Self {
1687            sender: Mutex::new(Some(sender)),
1688            thread: Mutex::new(Some(thread)),
1689        })
1690    }
1691
1692    fn send(&self, command: WorkerCommand) -> Result<(), RuntimeError> {
1693        self.sender
1694            .lock()
1695            .map_err(|_| RuntimeError::Internal)?
1696            .as_ref()
1697            .ok_or(RuntimeError::Internal)?
1698            .try_send(command)
1699            .map_err(|error| match error {
1700                mpsc::error::TrySendError::Full(_) => {
1701                    RuntimeError::Backpressure("invocation queue is full".to_string())
1702                }
1703                mpsc::error::TrySendError::Closed(_) => RuntimeError::Internal,
1704            })
1705    }
1706}
1707
1708impl Drop for RuntimeWorker {
1709    fn drop(&mut self) {
1710        if let Ok(sender) = self.sender.get_mut() {
1711            sender.take();
1712        }
1713        if let Ok(thread) = self.thread.get_mut() {
1714            if let Some(thread) = thread.take() {
1715                let _ = thread.join();
1716            }
1717        }
1718    }
1719}
1720
1721/// A self-contained runtime for one application or project.
1722///
1723/// One runtime may register multiple providers, agents, and stable named
1724/// endpoints. It can run directly inside a Rust host; Vifu Server and Agent
1725/// Gateway are optional deployment components.
1726#[derive(Clone)]
1727pub struct VifuRuntime {
1728    core: Arc<RuntimeCore>,
1729    worker: Arc<Mutex<Option<RuntimeWorker>>>,
1730}
1731
1732impl fmt::Debug for VifuRuntime {
1733    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1734        let counts = self.core.registry.read().ok().map(|registry| {
1735            (
1736                registry.providers.len(),
1737                registry.agents.len(),
1738                registry.endpoints.len(),
1739            )
1740        });
1741        formatter
1742            .debug_struct("VifuRuntime")
1743            .field("project_id", &self.core.project_id)
1744            .field("resource_counts", &counts)
1745            .finish()
1746    }
1747}
1748
1749impl VifuRuntime {
1750    pub fn new(project_id: impl Into<String>) -> Result<Self, RuntimeError> {
1751        Self::with_store(project_id, Arc::new(MemoryRuntimeStore::default()))
1752    }
1753
1754    pub fn with_store(
1755        project_id: impl Into<String>,
1756        store: Arc<dyn RuntimeStore>,
1757    ) -> Result<Self, RuntimeError> {
1758        let project_id = project_id.into();
1759        validate_identifier("project", &project_id)?;
1760        let core = Arc::new(RuntimeCore {
1761            project_id,
1762            registry: RwLock::new(RuntimeRegistry::default()),
1763            manifest: RwLock::new(None),
1764            store,
1765            sessions: RwLock::new(HashMap::new()),
1766            session_locks: Mutex::new(HashMap::new()),
1767            invocations: Mutex::new(InvocationRegistry::default()),
1768            next_invocation: AtomicU64::new(1),
1769            monitor_observer: RwLock::new(None),
1770            monitor_io_observer: RwLock::new(None),
1771        });
1772        let worker = Arc::new(Mutex::new(None));
1773        Ok(Self { core, worker })
1774    }
1775
1776    pub fn project_id(&self) -> &str {
1777        &self.core.project_id
1778    }
1779
1780    /// Installs or clears the payload-safe runtime lifecycle observer.
1781    pub fn set_monitor_observer(
1782        &self,
1783        observer: Option<RuntimeMonitorObserver>,
1784    ) -> Result<(), RuntimeError> {
1785        *self
1786            .core
1787            .monitor_observer
1788            .write()
1789            .map_err(|_| RuntimeError::Internal)? = observer;
1790        Ok(())
1791    }
1792
1793    /// Installs or clears the opt-in bounded invocation I/O observer.
1794    pub fn set_monitor_io_observer(
1795        &self,
1796        observer: Option<RuntimeMonitorIoObserver>,
1797    ) -> Result<(), RuntimeError> {
1798        *self
1799            .core
1800            .monitor_io_observer
1801            .write()
1802            .map_err(|_| RuntimeError::Internal)? = observer;
1803        Ok(())
1804    }
1805
1806    pub fn register_provider(
1807        &self,
1808        name: impl Into<String>,
1809        provider: Arc<dyn AgentProvider>,
1810    ) -> Result<(), RuntimeError> {
1811        let name = name.into();
1812        validate_identifier("provider", &name)?;
1813        self.core
1814            .registry
1815            .write()
1816            .map_err(|_| RuntimeError::Internal)?
1817            .providers
1818            .insert(name, provider);
1819        Ok(())
1820    }
1821
1822    /// Removes a provider from the runtime registry.
1823    ///
1824    /// An invocation that already acquired the provider keeps its own `Arc`
1825    /// until that invocation finishes. New invocations fail normally until a
1826    /// provider with the same name is registered again.
1827    pub fn unregister_provider(&self, name: &str) -> Result<bool, RuntimeError> {
1828        validate_identifier("provider", name)?;
1829        Ok(self
1830            .core
1831            .registry
1832            .write()
1833            .map_err(|_| RuntimeError::Internal)?
1834            .providers
1835            .remove(name)
1836            .is_some())
1837    }
1838
1839    pub fn unregister_agent(&self, id: &str) -> Result<bool, RuntimeError> {
1840        validate_identifier("agent", id)?;
1841        Ok(self
1842            .core
1843            .registry
1844            .write()
1845            .map_err(|_| RuntimeError::Internal)?
1846            .agents
1847            .remove(id)
1848            .is_some())
1849    }
1850
1851    pub fn unregister_endpoint(&self, name: &str) -> Result<bool, RuntimeError> {
1852        validate_identifier("endpoint", name)?;
1853        Ok(self
1854            .core
1855            .registry
1856            .write()
1857            .map_err(|_| RuntimeError::Internal)?
1858            .endpoints
1859            .remove(name)
1860            .is_some())
1861    }
1862
1863    pub fn register_agent(&self, mut agent: AgentDefinition) -> Result<(), RuntimeError> {
1864        validate_identifier("agent", &agent.id)?;
1865        validate_identifier("provider", &agent.provider)?;
1866        if agent.name.trim().is_empty() || agent.capabilities.is_empty() {
1867            return Err(RuntimeError::InvalidDefinition(
1868                "agent name and at least one capability are required".to_string(),
1869            ));
1870        }
1871        for capability in &mut agent.capabilities {
1872            *capability = capability.trim().to_ascii_lowercase();
1873            validate_identifier("capability", capability)?;
1874        }
1875        agent.capabilities.sort();
1876        agent.capabilities.dedup();
1877        let mut registry = self
1878            .core
1879            .registry
1880            .write()
1881            .map_err(|_| RuntimeError::Internal)?;
1882        if !registry.providers.contains_key(&agent.provider) {
1883            return Err(RuntimeError::ProviderNotFound(agent.provider));
1884        }
1885        registry.agents.insert(agent.id.clone(), agent);
1886        Ok(())
1887    }
1888
1889    pub fn register_endpoint(&self, mut endpoint: EndpointDefinition) -> Result<(), RuntimeError> {
1890        validate_identifier("endpoint", &endpoint.name)?;
1891        validate_identifier("agent", &endpoint.agent)?;
1892        endpoint.capability = endpoint.capability.trim().to_ascii_lowercase();
1893        validate_identifier("capability", &endpoint.capability)?;
1894        if !(1..=MAX_ENDPOINT_TIMEOUT_MS).contains(&endpoint.timeout_ms) {
1895            return Err(RuntimeError::InvalidDefinition(format!(
1896                "endpoint timeout must be between 1 and {MAX_ENDPOINT_TIMEOUT_MS} ms"
1897            )));
1898        }
1899        let mut registry = self
1900            .core
1901            .registry
1902            .write()
1903            .map_err(|_| RuntimeError::Internal)?;
1904        let agent = registry
1905            .agents
1906            .get(&endpoint.agent)
1907            .ok_or_else(|| RuntimeError::AgentNotFound(endpoint.agent.clone()))?;
1908        if !agent
1909            .capabilities
1910            .iter()
1911            .any(|capability| capability == &endpoint.capability)
1912        {
1913            return Err(RuntimeError::CapabilityUnavailable {
1914                provider: agent.provider.clone(),
1915                capability: endpoint.capability,
1916            });
1917        }
1918        registry.endpoints.insert(endpoint.name.clone(), endpoint);
1919        Ok(())
1920    }
1921
1922    pub fn agent_definitions(&self) -> Result<Vec<AgentDefinition>, RuntimeError> {
1923        let mut agents = self
1924            .core
1925            .registry
1926            .read()
1927            .map_err(|_| RuntimeError::Internal)?
1928            .agents
1929            .values()
1930            .cloned()
1931            .collect::<Vec<_>>();
1932        agents.sort_by(|left, right| left.id.cmp(&right.id));
1933        Ok(agents)
1934    }
1935
1936    pub fn endpoint_definitions(&self) -> Result<Vec<EndpointDefinition>, RuntimeError> {
1937        let mut endpoints = self
1938            .core
1939            .registry
1940            .read()
1941            .map_err(|_| RuntimeError::Internal)?
1942            .endpoints
1943            .values()
1944            .cloned()
1945            .collect::<Vec<_>>();
1946        endpoints.sort_by(|left, right| left.name.cmp(&right.name));
1947        Ok(endpoints)
1948    }
1949
1950    /// Replaces the portable agent and endpoint graph with a validated manifest.
1951    /// Provider implementations must be registered locally before activation.
1952    pub fn apply_manifest(&self, manifest: RuntimeManifest) -> Result<(), RuntimeError> {
1953        manifest.validate()?;
1954        if manifest.project_id != self.core.project_id {
1955            return Err(RuntimeError::InvalidDefinition(
1956                "project settings belong to another project".to_string(),
1957            ));
1958        }
1959        let mut registry = self
1960            .core
1961            .registry
1962            .write()
1963            .map_err(|_| RuntimeError::Internal)?;
1964        for requirement in &manifest.providers {
1965            let provider = registry
1966                .providers
1967                .get(&requirement.id)
1968                .ok_or_else(|| RuntimeError::ProviderNotFound(requirement.id.clone()))?;
1969            for capability in &requirement.capabilities {
1970                if !provider.supports(capability) {
1971                    return Err(RuntimeError::CapabilityUnavailable {
1972                        provider: requirement.id.clone(),
1973                        capability: capability.clone(),
1974                    });
1975                }
1976            }
1977        }
1978        registry.agents = manifest
1979            .agents
1980            .iter()
1981            .cloned()
1982            .map(|agent| (agent.id.clone(), agent))
1983            .collect();
1984        registry.endpoints = manifest
1985            .endpoints
1986            .iter()
1987            .cloned()
1988            .map(|endpoint| (endpoint.name.clone(), endpoint))
1989            .collect();
1990        *self
1991            .core
1992            .manifest
1993            .write()
1994            .map_err(|_| RuntimeError::Internal)? = Some(manifest);
1995        Ok(())
1996    }
1997
1998    pub fn apply_project_settings(&self, settings: ProjectSettings) -> Result<(), RuntimeError> {
1999        self.apply_manifest(settings)
2000    }
2001
2002    pub fn current_manifest(&self) -> Result<Option<RuntimeManifest>, RuntimeError> {
2003        Ok(self
2004            .core
2005            .manifest
2006            .read()
2007            .map_err(|_| RuntimeError::Internal)?
2008            .clone())
2009    }
2010
2011    pub fn current_project_settings(&self) -> Result<Option<ProjectSettings>, RuntimeError> {
2012        self.current_manifest()
2013    }
2014
2015    pub fn install_release(&self, release: &RuntimeRelease) -> Result<(), RuntimeError> {
2016        release.validate()?;
2017        if release.manifest.project_id != self.core.project_id {
2018            return Err(RuntimeError::InvalidDefinition(
2019                "runtime release belongs to another project".to_string(),
2020            ));
2021        }
2022        self.core.store.save_release(release)
2023    }
2024
2025    pub fn releases(&self) -> Result<Vec<RuntimeRelease>, RuntimeError> {
2026        self.core.store.list_releases(&self.core.project_id)
2027    }
2028
2029    pub fn active_release_version(&self) -> Result<Option<u64>, RuntimeError> {
2030        self.core.store.active_release(&self.core.project_id)
2031    }
2032
2033    pub fn activate_release(&self, version: u64) -> Result<RuntimeRelease, RuntimeError> {
2034        let release = self
2035            .core
2036            .store
2037            .load_release(&self.core.project_id, version)?
2038            .ok_or_else(|| RuntimeError::store("runtime release was not found"))?;
2039        self.apply_manifest(release.manifest.clone())?;
2040        self.core
2041            .store
2042            .set_active_release(&self.core.project_id, version)?;
2043        Ok(release)
2044    }
2045
2046    pub fn restore_active_release(&self) -> Result<Option<RuntimeRelease>, RuntimeError> {
2047        self.active_release_version()?
2048            .map(|version| self.activate_release(version))
2049            .transpose()
2050    }
2051
2052    pub fn bootstrap_release(
2053        &self,
2054        manifest: RuntimeManifest,
2055    ) -> Result<RuntimeRelease, RuntimeError> {
2056        if let Some(active) = self.restore_active_release()? {
2057            return Ok(active);
2058        }
2059        let release = RuntimeRelease::new(1, manifest)?;
2060        self.install_release(&release)?;
2061        self.activate_release(release.version)
2062    }
2063
2064    pub fn bootstrap_project_settings(
2065        &self,
2066        settings: ProjectSettings,
2067    ) -> Result<RuntimeRelease, RuntimeError> {
2068        self.bootstrap_release(settings)
2069    }
2070
2071    pub fn save_local_provider_binding(
2072        &self,
2073        binding: &LocalProviderBinding,
2074    ) -> Result<(), RuntimeError> {
2075        validate_identifier("provider", &binding.provider_id)?;
2076        self.core
2077            .store
2078            .save_local_provider_binding(&self.core.project_id, binding)
2079    }
2080
2081    pub fn local_provider_bindings(&self) -> Result<Vec<LocalProviderBinding>, RuntimeError> {
2082        self.core
2083            .store
2084            .local_provider_bindings(&self.core.project_id)
2085    }
2086
2087    pub fn pending_traces(&self, limit: usize) -> Result<Vec<RuntimeTraceRecord>, RuntimeError> {
2088        self.core.store.pending_traces(limit.min(1_000))
2089    }
2090
2091    pub fn acknowledge_traces(&self, trace_ids: &[String]) -> Result<(), RuntimeError> {
2092        self.core.store.acknowledge_traces(trace_ids)
2093    }
2094
2095    pub fn session(&self, session_id: impl Into<String>) -> Result<RuntimeSession, RuntimeError> {
2096        let session_id = session_id.into();
2097        validate_identifier("session", &session_id)?;
2098        Ok(RuntimeSession {
2099            runtime: self.clone(),
2100            session_id,
2101        })
2102    }
2103
2104    pub async fn invoke(&self, input: InvocationInput) -> Result<InvocationOutput, RuntimeError> {
2105        self.invoke_with_cancellation(input, CancellationToken::default())
2106            .await
2107    }
2108
2109    /// Invokes an endpoint while honoring a cancellation signal owned by the
2110    /// embedding host.
2111    pub async fn invoke_with_cancellation(
2112        &self,
2113        input: InvocationInput,
2114        cancellation: CancellationToken,
2115    ) -> Result<InvocationOutput, RuntimeError> {
2116        self.invoke_with_events_and_cancellation(input, cancellation, ProviderEventSink::discard())
2117            .await
2118    }
2119
2120    /// Invokes an endpoint while forwarding real provider progress to an
2121    /// embedding host and honoring the host's cancellation signal.
2122    pub async fn invoke_with_events_and_cancellation(
2123        &self,
2124        input: InvocationInput,
2125        cancellation: CancellationToken,
2126        events: ProviderEventSink,
2127    ) -> Result<InvocationOutput, RuntimeError> {
2128        let invocation_id = self.core.next_invocation_id();
2129        self.core
2130            .invoke(invocation_id, input, cancellation, events)
2131            .await
2132    }
2133
2134    pub fn start_invoke(&self, input: InvocationInput) -> Result<InvocationHandle, RuntimeError> {
2135        validate_identifier("endpoint", &input.endpoint)?;
2136        validate_identifier("session", &input.session_id)?;
2137        let handle = InvocationHandle(self.core.next_invocation_id());
2138        let cancellation = CancellationToken::default();
2139        let mut worker = self.worker.lock().map_err(|_| RuntimeError::Internal)?;
2140        if worker.is_none() {
2141            *worker = Some(RuntimeWorker::spawn(Arc::clone(&self.core))?);
2142        }
2143        self.core
2144            .invocations
2145            .lock()
2146            .map_err(|_| RuntimeError::Internal)?
2147            .insert(handle.clone(), cancellation.clone())?;
2148        let send_result =
2149            worker
2150                .as_ref()
2151                .ok_or(RuntimeError::Internal)?
2152                .send(WorkerCommand::Start {
2153                    handle: handle.clone(),
2154                    input,
2155                    cancellation,
2156                });
2157        if let Err(error) = send_result {
2158            self.core
2159                .invocations
2160                .lock()
2161                .map_err(|_| RuntimeError::Internal)?
2162                .remove(&handle);
2163            return Err(error);
2164        }
2165        Ok(handle)
2166    }
2167
2168    pub fn poll_invocation(
2169        &self,
2170        handle: &InvocationHandle,
2171    ) -> Result<InvocationPoll, RuntimeError> {
2172        self.core
2173            .invocations
2174            .lock()
2175            .map_err(|_| RuntimeError::Internal)?
2176            .entries
2177            .get(&handle.0)
2178            .map(|entry| entry.poll.clone())
2179            .ok_or_else(|| RuntimeError::InvocationNotFound(handle.0.clone()))
2180    }
2181
2182    /// Drains incremental events produced since the previous call.
2183    pub fn drain_invocation_events(
2184        &self,
2185        handle: &InvocationHandle,
2186    ) -> Result<Vec<InvocationEvent>, RuntimeError> {
2187        self.core
2188            .invocations
2189            .lock()
2190            .map_err(|_| RuntimeError::Internal)?
2191            .drain_events(handle)
2192    }
2193
2194    /// Returns the current poll state and removes it once it is terminal.
2195    ///
2196    /// Pending and running invocations remain registered so callers can keep
2197    /// polling the same handle.
2198    pub fn take_invocation(
2199        &self,
2200        handle: &InvocationHandle,
2201    ) -> Result<InvocationPoll, RuntimeError> {
2202        self.core
2203            .invocations
2204            .lock()
2205            .map_err(|_| RuntimeError::Internal)?
2206            .take(handle)
2207    }
2208
2209    pub fn cancel_invocation(&self, handle: &InvocationHandle) -> Result<(), RuntimeError> {
2210        let cancellation = self
2211            .core
2212            .invocations
2213            .lock()
2214            .map_err(|_| RuntimeError::Internal)?
2215            .entries
2216            .get(&handle.0)
2217            .map(|entry| entry.cancellation.clone())
2218            .ok_or_else(|| RuntimeError::InvocationNotFound(handle.0.clone()))?;
2219        cancellation.cancel();
2220        self.core
2221            .update_poll(handle, InvocationStatus::Cancelled, None, None);
2222        Ok(())
2223    }
2224
2225    pub async fn execute_effects(
2226        &self,
2227        effects: Vec<EffectRequest>,
2228    ) -> Result<EffectExecution, RuntimeError> {
2229        self.execute_effects_with_limit(effects, DEFAULT_EFFECT_LIMIT)
2230            .await
2231    }
2232
2233    pub async fn execute_effects_with_limit(
2234        &self,
2235        effects: Vec<EffectRequest>,
2236        limit: usize,
2237    ) -> Result<EffectExecution, RuntimeError> {
2238        if effects.len() > limit {
2239            return Err(RuntimeError::EffectLimitExceeded(limit));
2240        }
2241        let mut results = Vec::new();
2242        let mut unhandled = Vec::new();
2243        for effect in effects {
2244            if effect.kind != "agent.invoke" {
2245                unhandled.push(effect);
2246                continue;
2247            }
2248            let input = serde_json::from_value::<InvocationInput>(effect.payload.clone())
2249                .map_err(|error| RuntimeError::InvalidDefinition(error.to_string()))?;
2250            let result = self.invoke(input).await;
2251            match result {
2252                Ok(output) => results.push(EffectResult {
2253                    effect_id: effect.id,
2254                    succeeded: true,
2255                    output: serde_json::to_value(output)
2256                        .map_err(|_error| RuntimeError::Internal)?,
2257                }),
2258                Err(error) => results.push(EffectResult {
2259                    effect_id: effect.id,
2260                    succeeded: false,
2261                    output: json!({ "error": error.public_message() }),
2262                }),
2263            }
2264        }
2265        Ok(EffectExecution { results, unhandled })
2266    }
2267
2268    pub fn export_snapshot(&self) -> Result<Vec<u8>, RuntimeError> {
2269        let snapshot = PortableProjectSnapshot {
2270            version: SNAPSHOT_VERSION,
2271            project_id: self.core.project_id.clone(),
2272            sessions: self
2273                .core
2274                .sessions
2275                .read()
2276                .map_err(|_| RuntimeError::Internal)?
2277                .clone(),
2278        };
2279        serde_json::to_vec(&snapshot).map_err(|error| RuntimeError::Snapshot(error.to_string()))
2280    }
2281
2282    pub fn restore_snapshot(&self, bytes: &[u8]) -> Result<(), RuntimeError> {
2283        let snapshot = serde_json::from_slice::<PortableProjectSnapshot>(bytes)
2284            .map_err(|error| RuntimeError::Snapshot(error.to_string()))?;
2285        if snapshot.version != SNAPSHOT_VERSION || snapshot.project_id != self.core.project_id {
2286            return Err(RuntimeError::Snapshot(
2287                "snapshot version or project does not match".to_string(),
2288            ));
2289        }
2290        for (session_id, state) in &snapshot.sessions {
2291            validate_identifier("session", session_id)?;
2292            self.core
2293                .store
2294                .save(&self.core.project_id, session_id, state)?;
2295        }
2296        *self
2297            .core
2298            .sessions
2299            .write()
2300            .map_err(|_| RuntimeError::Internal)? = snapshot.sessions;
2301        Ok(())
2302    }
2303}
2304
2305/// A session-scoped view over [`VifuRuntime`].
2306#[derive(Clone, Debug)]
2307pub struct RuntimeSession {
2308    runtime: VifuRuntime,
2309    session_id: String,
2310}
2311
2312impl RuntimeSession {
2313    pub fn id(&self) -> &str {
2314        &self.session_id
2315    }
2316
2317    pub async fn invoke(
2318        &self,
2319        mut input: InvocationInput,
2320    ) -> Result<InvocationOutput, RuntimeError> {
2321        input.session_id.clone_from(&self.session_id);
2322        self.runtime.invoke(input).await
2323    }
2324
2325    pub fn start_invoke(
2326        &self,
2327        mut input: InvocationInput,
2328    ) -> Result<InvocationHandle, RuntimeError> {
2329        input.session_id.clone_from(&self.session_id);
2330        self.runtime.start_invoke(input)
2331    }
2332}
2333
2334#[derive(Serialize, Deserialize)]
2335#[serde(rename_all = "camelCase")]
2336struct PortableProjectSnapshot {
2337    version: u32,
2338    project_id: String,
2339    sessions: HashMap<String, RuntimeSnapshot>,
2340}
2341
2342fn default_session_id() -> String {
2343    "default".to_string()
2344}
2345
2346const fn default_timeout_ms() -> u64 {
2347    DEFAULT_TIMEOUT_MS
2348}
2349
2350fn validate_identifier(kind: &str, value: &str) -> Result<(), RuntimeError> {
2351    if value.is_empty()
2352        || value.len() > 128
2353        || !value
2354            .bytes()
2355            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':'))
2356    {
2357        return Err(RuntimeError::InvalidDefinition(format!(
2358            "{kind} must be a portable identifier"
2359        )));
2360    }
2361    Ok(())
2362}
2363
2364fn duration_ms(duration: Duration) -> u64 {
2365    u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
2366}
2367
2368const fn is_terminal_status(status: InvocationStatus) -> bool {
2369    matches!(
2370        status,
2371        InvocationStatus::Completed | InvocationStatus::Failed | InvocationStatus::Cancelled
2372    )
2373}
2374
2375#[cfg(test)]
2376mod tests {
2377    use super::*;
2378
2379    struct TestProvider {
2380        fail: bool,
2381        delay: Duration,
2382    }
2383
2384    impl TestProvider {
2385        fn immediate() -> Self {
2386            Self {
2387                fail: false,
2388                delay: Duration::ZERO,
2389            }
2390        }
2391    }
2392
2393    impl AgentProvider for TestProvider {
2394        fn supports(&self, capability: &str) -> bool {
2395            matches!(capability, "chat" | "speech" | "transcription")
2396        }
2397
2398        fn invoke<'a>(
2399            &'a self,
2400            request: ProviderRequest,
2401            cancellation: CancellationToken,
2402        ) -> ProviderFuture<'a> {
2403            Box::pin(async move {
2404                if !self.delay.is_zero() {
2405                    tokio::select! {
2406                        _ = tokio::time::sleep(self.delay) => {}
2407                        _ = cancellation.cancelled() => {
2408                            return Err(RuntimeError::Cancelled);
2409                        }
2410                    }
2411                }
2412                if self.fail {
2413                    return Err(RuntimeError::provider(
2414                        request.agent.provider,
2415                        "synthetic provider failure",
2416                    ));
2417                }
2418                Ok(ProviderResponse {
2419                    data: match request.data {
2420                        InvocationData::Json(data) => InvocationData::Json(json!({
2421                            "capability": request.capability,
2422                            "input": data,
2423                        })),
2424                        InvocationData::Binary(bytes) => InvocationData::Binary(bytes),
2425                    },
2426                    metadata: json!({}),
2427                    state: Some(json!({
2428                        "lastEndpoint": request.endpoint,
2429                        "previousRevision": request.snapshot.revision,
2430                    })),
2431                })
2432            })
2433        }
2434    }
2435
2436    struct StreamingTestProvider;
2437
2438    impl AgentProvider for StreamingTestProvider {
2439        fn supports(&self, capability: &str) -> bool {
2440            capability == "chat"
2441        }
2442
2443        fn invoke<'a>(
2444            &'a self,
2445            request: ProviderRequest,
2446            cancellation: CancellationToken,
2447        ) -> ProviderFuture<'a> {
2448            self.invoke_with_events(request, cancellation, ProviderEventSink::discard())
2449        }
2450
2451        fn invoke_with_events<'a>(
2452            &'a self,
2453            _request: ProviderRequest,
2454            cancellation: CancellationToken,
2455            events: ProviderEventSink,
2456        ) -> ProviderFuture<'a> {
2457            Box::pin(async move {
2458                if cancellation.is_cancelled() {
2459                    return Err(RuntimeError::Cancelled);
2460                }
2461                events.stage_started(ProviderStage::Tokenize, Value::Null);
2462                events.stage_completed(ProviderStage::Tokenize, 2, json!({ "inputTokens": 4 }));
2463                events.output_delta(InvocationData::Json(Value::String("Hello".to_string())));
2464                events.output_delta(InvocationData::Json(Value::String(", world".to_string())));
2465                Ok(ProviderResponse::json(json!({ "text": "Hello, world" })))
2466            })
2467        }
2468    }
2469
2470    struct ActiveSlowProvider;
2471
2472    impl AgentProvider for ActiveSlowProvider {
2473        fn supports(&self, capability: &str) -> bool {
2474            capability == "chat"
2475        }
2476
2477        fn invoke<'a>(
2478            &'a self,
2479            request: ProviderRequest,
2480            cancellation: CancellationToken,
2481        ) -> ProviderFuture<'a> {
2482            self.invoke_with_events(request, cancellation, ProviderEventSink::discard())
2483        }
2484
2485        fn invoke_with_events<'a>(
2486            &'a self,
2487            _request: ProviderRequest,
2488            cancellation: CancellationToken,
2489            events: ProviderEventSink,
2490        ) -> ProviderFuture<'a> {
2491            Box::pin(async move {
2492                for _ in 0..4 {
2493                    tokio::select! {
2494                        _ = tokio::time::sleep(Duration::from_millis(8)) => events.activity(),
2495                        _ = cancellation.cancelled() => return Err(RuntimeError::Cancelled),
2496                    }
2497                }
2498                Ok(ProviderResponse::json(json!({ "ok": true })))
2499            })
2500        }
2501    }
2502
2503    fn configured_runtime(provider: Arc<dyn AgentProvider>) -> VifuRuntime {
2504        let runtime = VifuRuntime::new("test-project").expect("runtime should start");
2505        runtime
2506            .register_provider("test-provider", provider)
2507            .expect("provider should register");
2508        runtime
2509            .register_agent(AgentDefinition {
2510                id: "guide".to_string(),
2511                name: "Guide".to_string(),
2512                provider: "test-provider".to_string(),
2513                capabilities: vec![
2514                    "chat".to_string(),
2515                    "speech".to_string(),
2516                    "transcription".to_string(),
2517                ],
2518                metadata: json!({ "public": true }),
2519            })
2520            .expect("agent should register");
2521        for capability in ["chat", "speech", "transcription"] {
2522            runtime
2523                .register_endpoint(EndpointDefinition {
2524                    name: capability.to_string(),
2525                    agent: "guide".to_string(),
2526                    capability: capability.to_string(),
2527                    timeout_ms: 500,
2528                })
2529                .expect("endpoint should register");
2530        }
2531        runtime
2532    }
2533
2534    #[test]
2535    fn dynamic_endpoint_accepts_slow_local_model_inference() {
2536        let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2537
2538        let result = runtime.register_endpoint(EndpointDefinition {
2539            name: "slow-chat".to_string(),
2540            agent: "guide".to_string(),
2541            capability: "chat".to_string(),
2542            timeout_ms: 300_000,
2543        });
2544
2545        assert!(
2546            result.is_ok(),
2547            "five-minute endpoint should register: {result:?}"
2548        );
2549    }
2550
2551    #[tokio::test(flavor = "current_thread")]
2552    async fn embedded_runtime_invokes_chat_speech_and_transcription_without_a_server() {
2553        let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2554
2555        for capability in ["chat", "speech", "transcription"] {
2556            let output = runtime
2557                .invoke(InvocationInput::json(
2558                    capability,
2559                    json!({ "message": capability }),
2560                ))
2561                .await
2562                .expect("endpoint should invoke");
2563            assert_eq!(output.capability, capability);
2564        }
2565    }
2566
2567    #[tokio::test(flavor = "current_thread")]
2568    async fn monitor_observer_receives_provider_performance_metadata() {
2569        let runtime = configured_runtime(Arc::new(StreamingTestProvider));
2570        let monitor_events = Arc::new(Mutex::new(Vec::new()));
2571        let captured_events = Arc::clone(&monitor_events);
2572        runtime
2573            .set_monitor_observer(Some(Arc::new(move |event| {
2574                captured_events.lock().unwrap().push(event);
2575            })))
2576            .unwrap();
2577
2578        runtime
2579            .invoke(InvocationInput::json("chat", json!({ "text": "hello" })))
2580            .await
2581            .unwrap();
2582
2583        assert!(monitor_events.lock().unwrap().iter().any(|event| matches!(
2584            event,
2585            RuntimeMonitorEvent::ProviderStage {
2586                stage: ProviderStage::Tokenize,
2587                status: RuntimeMonitorStageStatus::Completed,
2588                input_tokens: Some(4),
2589                ..
2590            }
2591        )));
2592    }
2593
2594    #[tokio::test(flavor = "current_thread")]
2595    async fn monitor_io_observer_receives_chat_input_and_output() {
2596        let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2597        let monitor_events = Arc::new(Mutex::new(Vec::new()));
2598        let captured_events = Arc::clone(&monitor_events);
2599        runtime
2600            .set_monitor_io_observer(Some(Arc::new(move |event| {
2601                captured_events.lock().unwrap().push(event);
2602            })))
2603            .unwrap();
2604
2605        runtime
2606            .invoke(InvocationInput::json("chat", json!({ "text": "hello" })))
2607            .await
2608            .unwrap();
2609
2610        let events = monitor_events.lock().unwrap();
2611        assert!(matches!(
2612            events.as_slice(),
2613            [
2614                RuntimeMonitorIoEvent::InvocationInput { summary: input, .. },
2615                RuntimeMonitorIoEvent::InvocationOutput { summary: output, .. }
2616            ] if input.value == json!({ "text": "hello" })
2617                && output.value["input"] == json!({ "text": "hello" })
2618        ));
2619    }
2620
2621    #[tokio::test(flavor = "current_thread")]
2622    async fn runtime_sessions_keep_independent_durable_state() {
2623        let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2624        let first = runtime
2625            .session("player-one")
2626            .expect("first session should open");
2627        let second = runtime
2628            .session("player-two")
2629            .expect("second session should open");
2630
2631        let first_output = first
2632            .invoke(InvocationInput::json("chat", json!({ "text": "one" })))
2633            .await
2634            .expect("first session should invoke");
2635        let second_output = second
2636            .invoke(InvocationInput::json("chat", json!({ "text": "two" })))
2637            .await
2638            .expect("second session should invoke");
2639
2640        assert_eq!(first_output.snapshot.revision, 1);
2641        assert_eq!(second_output.snapshot.revision, 1);
2642    }
2643
2644    #[tokio::test(flavor = "current_thread")]
2645    async fn concurrent_calls_serialize_state_updates_for_one_session() {
2646        let runtime = configured_runtime(Arc::new(TestProvider {
2647            fail: false,
2648            delay: Duration::from_millis(5),
2649        }));
2650        let first = runtime.invoke(
2651            InvocationInput::json("chat", json!({ "text": "one" })).with_session("shared-session"),
2652        );
2653        let second = runtime.invoke(
2654            InvocationInput::json("chat", json!({ "text": "two" })).with_session("shared-session"),
2655        );
2656
2657        let (first, second) = tokio::join!(first, second);
2658        let mut revisions = [
2659            first
2660                .expect("first invocation should complete")
2661                .snapshot
2662                .revision,
2663            second
2664                .expect("second invocation should complete")
2665                .snapshot
2666                .revision,
2667        ];
2668        revisions.sort_unstable();
2669        assert_eq!(revisions, [1, 2]);
2670    }
2671
2672    #[tokio::test(flavor = "current_thread")]
2673    async fn runtime_round_trips_binary_provider_results() {
2674        let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2675        let output = runtime
2676            .invoke(InvocationInput {
2677                endpoint: "speech".to_string(),
2678                session_id: "audio-session".to_string(),
2679                data: InvocationData::Binary(vec![1, 2, 3, 4]),
2680                metadata: json!({}),
2681            })
2682            .await
2683            .expect("binary invocation should complete");
2684
2685        assert_eq!(output.data, InvocationData::Binary(vec![1, 2, 3, 4]));
2686    }
2687
2688    #[tokio::test(flavor = "current_thread")]
2689    async fn runtime_times_out_slow_providers() {
2690        let runtime = VifuRuntime::new("timeout-project").expect("runtime should start");
2691        runtime
2692            .register_provider(
2693                "slow",
2694                Arc::new(TestProvider {
2695                    fail: false,
2696                    delay: Duration::from_millis(100),
2697                }),
2698            )
2699            .expect("provider should register");
2700        runtime
2701            .register_agent(AgentDefinition {
2702                id: "slow-agent".to_string(),
2703                name: "Slow agent".to_string(),
2704                provider: "slow".to_string(),
2705                capabilities: vec!["chat".to_string()],
2706                metadata: json!({}),
2707            })
2708            .expect("agent should register");
2709        runtime
2710            .register_endpoint(EndpointDefinition {
2711                name: "slow-chat".to_string(),
2712                agent: "slow-agent".to_string(),
2713                capability: "chat".to_string(),
2714                timeout_ms: 10,
2715            })
2716            .expect("endpoint should register");
2717
2718        let error = runtime
2719            .invoke(InvocationInput::json("slow-chat", json!({})))
2720            .await
2721            .expect_err("slow invocation should time out");
2722        assert!(matches!(error, RuntimeError::Timeout(10)));
2723    }
2724
2725    #[tokio::test(flavor = "current_thread")]
2726    async fn provider_activity_resets_the_runtime_idle_timeout() {
2727        let runtime = VifuRuntime::new("active-project").expect("runtime should start");
2728        runtime
2729            .register_provider("active", Arc::new(ActiveSlowProvider))
2730            .expect("provider should register");
2731        runtime
2732            .register_agent(AgentDefinition {
2733                id: "active-agent".to_string(),
2734                name: "Active agent".to_string(),
2735                provider: "active".to_string(),
2736                capabilities: vec!["chat".to_string()],
2737                metadata: json!({}),
2738            })
2739            .expect("agent should register");
2740        runtime
2741            .register_endpoint(EndpointDefinition {
2742                name: "active-chat".to_string(),
2743                agent: "active-agent".to_string(),
2744                capability: "chat".to_string(),
2745                timeout_ms: 10,
2746            })
2747            .expect("endpoint should register");
2748
2749        let output = runtime
2750            .invoke(InvocationInput::json("active-chat", json!({})))
2751            .await
2752            .expect("ongoing provider activity should renew the idle timeout");
2753        assert_eq!(output.data, InvocationData::Json(json!({ "ok": true })));
2754    }
2755
2756    #[test]
2757    fn game_loop_api_starts_polls_and_cancels_invocations() {
2758        let runtime = configured_runtime(Arc::new(TestProvider {
2759            fail: false,
2760            delay: Duration::from_secs(5),
2761        }));
2762        let handle = runtime
2763            .start_invoke(InvocationInput::json("chat", json!({})))
2764            .expect("invocation should start");
2765        let running_deadline = Instant::now() + Duration::from_secs(1);
2766        loop {
2767            let poll = runtime
2768                .poll_invocation(&handle)
2769                .expect("invocation should remain pollable");
2770            if poll.status == InvocationStatus::Running {
2771                break;
2772            }
2773            assert!(
2774                Instant::now() < running_deadline,
2775                "invocation did not start"
2776            );
2777            std::thread::sleep(Duration::from_millis(5));
2778        }
2779        runtime
2780            .cancel_invocation(&handle)
2781            .expect("invocation should cancel");
2782
2783        let deadline = Instant::now() + Duration::from_secs(1);
2784        loop {
2785            let poll = runtime
2786                .poll_invocation(&handle)
2787                .expect("invocation should remain pollable");
2788            if poll.status == InvocationStatus::Cancelled {
2789                break;
2790            }
2791            assert!(
2792                Instant::now() < deadline,
2793                "cancelled provider did not observe cancellation"
2794            );
2795            std::thread::sleep(Duration::from_millis(5));
2796        }
2797    }
2798
2799    #[test]
2800    fn game_loop_poll_returns_the_same_provider_result_shape_as_async_invoke() {
2801        let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2802        let handle = runtime
2803            .start_invoke(
2804                InvocationInput::json("chat", json!({ "text": "hello" }))
2805                    .with_session("poll-session"),
2806            )
2807            .expect("invocation should start");
2808        let deadline = Instant::now() + Duration::from_secs(1);
2809        let output = loop {
2810            let poll = runtime
2811                .poll_invocation(&handle)
2812                .expect("invocation should remain pollable");
2813            if let Some(output) = poll.output {
2814                break output;
2815            }
2816            assert!(
2817                !matches!(
2818                    poll.status,
2819                    InvocationStatus::Failed | InvocationStatus::Cancelled
2820                ),
2821                "invocation unexpectedly failed: {poll:?}"
2822            );
2823            assert!(Instant::now() < deadline, "invocation did not complete");
2824            std::thread::sleep(Duration::from_millis(5));
2825        };
2826
2827        assert_eq!(
2828            output.data,
2829            InvocationData::Json(json!({
2830                "capability": "chat",
2831                "input": { "text": "hello" },
2832            }))
2833        );
2834    }
2835
2836    #[test]
2837    fn game_loop_v1_event_stream_ignores_provider_stages() {
2838        let runtime = configured_runtime(Arc::new(StreamingTestProvider));
2839        let handle = runtime
2840            .start_invoke(InvocationInput::json("chat", json!({})))
2841            .expect("invocation should start");
2842        let deadline = Instant::now() + Duration::from_secs(1);
2843        loop {
2844            let poll = runtime
2845                .poll_invocation(&handle)
2846                .expect("invocation should remain pollable");
2847            if poll.status == InvocationStatus::Completed {
2848                break;
2849            }
2850            assert!(Instant::now() < deadline, "invocation did not complete");
2851            std::thread::sleep(Duration::from_millis(5));
2852        }
2853
2854        let events = runtime
2855            .drain_invocation_events(&handle)
2856            .expect("events should be available");
2857        assert_eq!(
2858            events.iter().map(|event| event.kind).collect::<Vec<_>>(),
2859            vec![
2860                InvocationEventKind::Started,
2861                InvocationEventKind::OutputDelta,
2862                InvocationEventKind::Completed,
2863            ]
2864        );
2865        assert_eq!(
2866            events[1].data,
2867            Some(InvocationData::Json(Value::String(
2868                "Hello, world".to_string()
2869            )))
2870        );
2871    }
2872
2873    #[test]
2874    fn invocation_registry_ignores_output_after_terminal_event() {
2875        let handle = InvocationHandle("late-output".to_string());
2876        let mut registry = InvocationRegistry::default();
2877        registry
2878            .insert(handle.clone(), CancellationToken::default())
2879            .expect("invocation should be registered");
2880        registry.update(&handle, InvocationStatus::Running, None, None);
2881        registry.update(
2882            &handle,
2883            InvocationStatus::Failed,
2884            None,
2885            Some("provider failed".to_string()),
2886        );
2887
2888        registry.push_provider_event(
2889            &handle,
2890            ProviderEvent::OutputDelta {
2891                data: InvocationData::Json(Value::String("too late".to_string())),
2892            },
2893        );
2894
2895        let events = registry
2896            .drain_events(&handle)
2897            .expect("events should remain available");
2898        assert_eq!(
2899            events.iter().map(|event| event.kind).collect::<Vec<_>>(),
2900            vec![InvocationEventKind::Started, InvocationEventKind::Failed]
2901        );
2902    }
2903
2904    #[test]
2905    fn taking_a_terminal_invocation_releases_its_result() {
2906        let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2907        let handle = runtime
2908            .start_invoke(InvocationInput::json("chat", json!({})))
2909            .expect("invocation should start");
2910        let deadline = Instant::now() + Duration::from_secs(1);
2911        loop {
2912            let poll = runtime
2913                .take_invocation(&handle)
2914                .expect("invocation should remain available until terminal");
2915            if is_terminal_status(poll.status) {
2916                break;
2917            }
2918            assert!(Instant::now() < deadline, "invocation did not complete");
2919            std::thread::sleep(Duration::from_millis(5));
2920        }
2921
2922        assert!(matches!(
2923            runtime.poll_invocation(&handle),
2924            Err(RuntimeError::InvocationNotFound(_))
2925        ));
2926    }
2927
2928    #[test]
2929    fn game_loop_api_applies_backpressure_to_excess_invocations() {
2930        let runtime = configured_runtime(Arc::new(TestProvider {
2931            fail: false,
2932            delay: Duration::from_secs(5),
2933        }));
2934        let handles = (0..MAX_IN_FLIGHT_INVOCATIONS)
2935            .map(|index| {
2936                runtime
2937                    .start_invoke(
2938                        InvocationInput::json("chat", json!({}))
2939                            .with_session(format!("session-{index}")),
2940                    )
2941                    .expect("invocation within the bound should start")
2942            })
2943            .collect::<Vec<_>>();
2944
2945        let error = runtime
2946            .start_invoke(
2947                InvocationInput::json("chat", json!({})).with_session("one-session-too-many"),
2948            )
2949            .expect_err("invocations above the bound should be rejected");
2950        assert!(matches!(error, RuntimeError::Backpressure(_)));
2951
2952        for handle in handles {
2953            runtime
2954                .cancel_invocation(&handle)
2955                .expect("test invocation should cancel");
2956        }
2957    }
2958
2959    #[test]
2960    fn terminal_invocation_history_is_bounded() {
2961        let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2962        let first = runtime
2963            .start_invoke(
2964                InvocationInput::json("chat", json!({})).with_session("retained-session-0"),
2965            )
2966            .expect("first invocation should start");
2967        let mut last = first.clone();
2968        for index in 0..=MAX_RETAINED_INVOCATIONS {
2969            let handle = if index == 0 {
2970                first.clone()
2971            } else {
2972                runtime
2973                    .start_invoke(
2974                        InvocationInput::json("chat", json!({}))
2975                            .with_session(format!("retained-session-{index}")),
2976                    )
2977                    .expect("invocation should start")
2978            };
2979            let deadline = Instant::now() + Duration::from_secs(1);
2980            loop {
2981                let poll = runtime
2982                    .poll_invocation(&handle)
2983                    .expect("latest invocation should remain available");
2984                if is_terminal_status(poll.status) {
2985                    break;
2986                }
2987                assert!(Instant::now() < deadline, "invocation did not complete");
2988                std::thread::sleep(Duration::from_millis(2));
2989            }
2990            last = handle;
2991        }
2992
2993        assert!(matches!(
2994            runtime.poll_invocation(&first),
2995            Err(RuntimeError::InvocationNotFound(_))
2996        ));
2997        assert!(runtime.poll_invocation(&last).is_ok());
2998    }
2999
3000    #[tokio::test(flavor = "current_thread")]
3001    async fn runtime_executes_agent_effects_and_returns_custom_effects_to_the_host() {
3002        let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
3003        let execution = runtime
3004            .execute_effects(vec![
3005                EffectRequest {
3006                    id: "agent-effect".to_string(),
3007                    kind: "agent.invoke".to_string(),
3008                    payload: serde_json::to_value(InvocationInput::json(
3009                        "chat",
3010                        json!({ "text": "hello" }),
3011                    ))
3012                    .unwrap(),
3013                },
3014                EffectRequest {
3015                    id: "host-effect".to_string(),
3016                    kind: "game.play_animation".to_string(),
3017                    payload: json!({ "name": "wave" }),
3018                },
3019            ])
3020            .await
3021            .expect("effects should execute");
3022
3023        assert_eq!(execution.results.len(), 1);
3024        assert_eq!(execution.unhandled[0].kind, "game.play_animation");
3025    }
3026
3027    #[tokio::test(flavor = "current_thread")]
3028    async fn runtime_rejects_effect_batches_above_the_bound() {
3029        let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
3030        let effects = (0..3)
3031            .map(|index| EffectRequest {
3032                id: format!("effect-{index}"),
3033                kind: "host.effect".to_string(),
3034                payload: json!({}),
3035            })
3036            .collect();
3037
3038        let error = runtime
3039            .execute_effects_with_limit(effects, 2)
3040            .await
3041            .expect_err("oversized effect batch should fail");
3042        assert!(matches!(error, RuntimeError::EffectLimitExceeded(2)));
3043    }
3044
3045    #[tokio::test(flavor = "current_thread")]
3046    async fn snapshots_restore_session_state_without_runtime_definitions_or_secrets() {
3047        let secret = "synthetic-secret-must-not-leak";
3048        let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
3049        runtime
3050            .invoke(
3051                InvocationInput::json("chat", json!({ "text": "hello" }))
3052                    .with_session("saved-session"),
3053            )
3054            .await
3055            .expect("invocation should create state");
3056        let bytes = runtime.export_snapshot().expect("snapshot should export");
3057        assert!(!String::from_utf8_lossy(&bytes).contains(secret));
3058
3059        let restored = configured_runtime(Arc::new(TestProvider::immediate()));
3060        restored
3061            .restore_snapshot(&bytes)
3062            .expect("snapshot should restore");
3063        let output = restored
3064            .invoke(
3065                InvocationInput::json("chat", json!({ "text": "again" }))
3066                    .with_session("saved-session"),
3067            )
3068            .await
3069            .expect("restored session should invoke");
3070
3071        assert_eq!(output.snapshot.revision, 2);
3072    }
3073
3074    #[test]
3075    fn debug_output_redacts_payloads_provider_errors_and_snapshots() {
3076        let secret = "synthetic-secret-must-not-leak";
3077        let input = InvocationInput::json("chat", json!({ "secret": secret }));
3078        let error = RuntimeError::provider("test-provider", secret);
3079        let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
3080
3081        assert!(!format!("{input:?}").contains(secret));
3082        assert!(!format!("{error:?}").contains(secret));
3083        assert!(!format!("{runtime:?}").contains(secret));
3084    }
3085}