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,
17};
18
19const SNAPSHOT_VERSION: u32 = 1;
20const DEFAULT_TIMEOUT_MS: u64 = 30_000;
21const MAX_TIMEOUT_MS: u64 = 120_000;
22const DEFAULT_EFFECT_LIMIT: usize = 64;
23const MAX_IN_FLIGHT_INVOCATIONS: usize = 64;
24const MAX_RETAINED_INVOCATIONS: usize = 256;
25const MAX_RETAINED_INVOCATION_EVENTS: usize = 256;
26const MAX_COALESCED_EVENT_BYTES: usize = 64 * 1024;
27const WORKER_QUEUE_CAPACITY: usize = 64;
28
29pub type ProviderFuture<'a> =
31 Pin<Box<dyn Future<Output = Result<ProviderResponse, RuntimeError>> + Send + 'a>>;
32
33#[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#[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#[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#[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 #[serde(default = "default_timeout_ms")]
134 pub timeout_ms: u64,
135}
136
137#[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#[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#[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#[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#[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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
265#[serde(tag = "type", rename_all = "camelCase")]
266pub enum ProviderEvent {
267 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#[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#[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#[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 error: Option<String>,
339 },
340 InvocationFinished {
341 trace_id: String,
342 invocation_id: String,
343 status: RuntimeMonitorStatus,
344 duration_ms: u64,
345 ended_at_ms: u64,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
347 error: Option<String>,
348 },
349}
350
351pub type RuntimeMonitorObserver = Arc<dyn Fn(RuntimeMonitorEvent) + Send + Sync>;
354
355#[derive(Clone, PartialEq, Serialize, Deserialize)]
357#[serde(rename_all = "camelCase")]
358pub struct InvocationEvent {
359 pub sequence: u64,
360 pub kind: InvocationEventKind,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
362 pub data: Option<InvocationData>,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub error: Option<String>,
365}
366
367impl fmt::Debug for InvocationEvent {
368 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
369 formatter
370 .debug_struct("InvocationEvent")
371 .field("sequence", &self.sequence)
372 .field("kind", &self.kind)
373 .field("data", &self.data.as_ref().map(|_| "[REDACTED]"))
374 .field("error", &self.error.as_ref().map(|_| "[REDACTED]"))
375 .finish()
376 }
377}
378
379#[derive(Clone)]
385pub struct ProviderEventSink {
386 emit: Arc<dyn Fn(ProviderEvent) + Send + Sync>,
387}
388
389impl ProviderEventSink {
390 fn new(emit: impl Fn(ProviderEvent) + Send + Sync + 'static) -> Self {
391 Self {
392 emit: Arc::new(emit),
393 }
394 }
395
396 pub fn from_fn(emit: impl Fn(ProviderEvent) + Send + Sync + 'static) -> Self {
398 Self::new(emit)
399 }
400
401 pub fn discard() -> Self {
402 Self::new(|_event| {})
403 }
404
405 pub fn output_delta(&self, data: InvocationData) {
406 (self.emit)(ProviderEvent::OutputDelta { data });
407 }
408
409 pub fn activity(&self) {
410 (self.emit)(ProviderEvent::Activity);
411 }
412
413 pub fn stage_started(&self, stage: ProviderStage, metadata: Value) {
414 (self.emit)(ProviderEvent::StageStarted { stage, metadata });
415 }
416
417 pub fn stage_completed(&self, stage: ProviderStage, elapsed_ms: u64, metadata: Value) {
418 (self.emit)(ProviderEvent::StageCompleted {
419 stage,
420 elapsed_ms,
421 metadata,
422 });
423 }
424
425 pub fn stage_failed(
426 &self,
427 stage: ProviderStage,
428 elapsed_ms: u64,
429 error: impl Into<String>,
430 metadata: Value,
431 ) {
432 (self.emit)(ProviderEvent::StageFailed {
433 stage,
434 elapsed_ms,
435 error: error.into(),
436 metadata,
437 });
438 }
439}
440
441impl fmt::Debug for ProviderEventSink {
442 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
443 formatter.write_str("ProviderEventSink")
444 }
445}
446
447pub trait AgentProvider: Send + Sync + 'static {
453 fn supports(&self, capability: &str) -> bool;
454
455 fn invoke<'a>(
456 &'a self,
457 request: ProviderRequest,
458 cancellation: CancellationToken,
459 ) -> ProviderFuture<'a>;
460
461 fn invoke_with_events<'a>(
462 &'a self,
463 request: ProviderRequest,
464 cancellation: CancellationToken,
465 _events: ProviderEventSink,
466 ) -> ProviderFuture<'a> {
467 self.invoke(request, cancellation)
468 }
469}
470
471pub trait RuntimeStore: Send + Sync + 'static {
476 fn load(
477 &self,
478 project_id: &str,
479 session_id: &str,
480 ) -> Result<Option<RuntimeSnapshot>, RuntimeError>;
481
482 fn save(
483 &self,
484 project_id: &str,
485 session_id: &str,
486 snapshot: &RuntimeSnapshot,
487 ) -> Result<(), RuntimeError>;
488
489 fn save_release(&self, _release: &RuntimeRelease) -> Result<(), RuntimeError> {
490 Err(RuntimeError::store(
491 "this runtime store does not support releases".to_string(),
492 ))
493 }
494
495 fn load_release(
496 &self,
497 _project_id: &str,
498 _version: u64,
499 ) -> Result<Option<RuntimeRelease>, RuntimeError> {
500 Ok(None)
501 }
502
503 fn list_releases(&self, _project_id: &str) -> Result<Vec<RuntimeRelease>, RuntimeError> {
504 Ok(Vec::new())
505 }
506
507 fn active_release(&self, _project_id: &str) -> Result<Option<u64>, RuntimeError> {
508 Ok(None)
509 }
510
511 fn set_active_release(&self, _project_id: &str, _version: u64) -> Result<(), RuntimeError> {
512 Err(RuntimeError::store(
513 "this runtime store does not support releases".to_string(),
514 ))
515 }
516
517 fn save_local_provider_binding(
518 &self,
519 _project_id: &str,
520 _binding: &LocalProviderBinding,
521 ) -> Result<(), RuntimeError> {
522 Err(RuntimeError::store(
523 "this runtime store does not support provider bindings".to_string(),
524 ))
525 }
526
527 fn local_provider_bindings(
528 &self,
529 _project_id: &str,
530 ) -> Result<Vec<LocalProviderBinding>, RuntimeError> {
531 Ok(Vec::new())
532 }
533
534 fn enqueue_trace(&self, _trace: &RuntimeTraceRecord) -> Result<(), RuntimeError> {
535 Ok(())
536 }
537
538 fn pending_traces(&self, _limit: usize) -> Result<Vec<RuntimeTraceRecord>, RuntimeError> {
539 Ok(Vec::new())
540 }
541
542 fn acknowledge_traces(&self, _trace_ids: &[String]) -> Result<(), RuntimeError> {
543 Ok(())
544 }
545}
546
547#[derive(Default)]
549pub struct MemoryRuntimeStore {
550 snapshots: RwLock<HashMap<(String, String), RuntimeSnapshot>>,
551 releases: RwLock<HashMap<(String, u64), RuntimeRelease>>,
552 active_releases: RwLock<HashMap<String, u64>>,
553 provider_bindings: RwLock<HashMap<(String, String), LocalProviderBinding>>,
554 trace_outbox: RwLock<VecDeque<RuntimeTraceRecord>>,
555}
556
557impl RuntimeStore for MemoryRuntimeStore {
558 fn load(
559 &self,
560 project_id: &str,
561 session_id: &str,
562 ) -> Result<Option<RuntimeSnapshot>, RuntimeError> {
563 let snapshots = self.snapshots.read().map_err(|_| RuntimeError::Internal)?;
564 Ok(snapshots
565 .get(&(project_id.to_string(), session_id.to_string()))
566 .cloned())
567 }
568
569 fn save(
570 &self,
571 project_id: &str,
572 session_id: &str,
573 snapshot: &RuntimeSnapshot,
574 ) -> Result<(), RuntimeError> {
575 let mut snapshots = self.snapshots.write().map_err(|_| RuntimeError::Internal)?;
576 snapshots.insert(
577 (project_id.to_string(), session_id.to_string()),
578 snapshot.clone(),
579 );
580 Ok(())
581 }
582
583 fn save_release(&self, release: &RuntimeRelease) -> Result<(), RuntimeError> {
584 release.validate()?;
585 let key = (release.manifest.project_id.clone(), release.version);
586 let mut releases = self.releases.write().map_err(|_| RuntimeError::Internal)?;
587 if let Some(existing) = releases.get(&key) {
588 if existing != release {
589 return Err(RuntimeError::store(
590 "runtime release versions are immutable".to_string(),
591 ));
592 }
593 return Ok(());
594 }
595 releases.insert(key, release.clone());
596 Ok(())
597 }
598
599 fn load_release(
600 &self,
601 project_id: &str,
602 version: u64,
603 ) -> Result<Option<RuntimeRelease>, RuntimeError> {
604 Ok(self
605 .releases
606 .read()
607 .map_err(|_| RuntimeError::Internal)?
608 .get(&(project_id.to_string(), version))
609 .cloned())
610 }
611
612 fn list_releases(&self, project_id: &str) -> Result<Vec<RuntimeRelease>, RuntimeError> {
613 let mut releases = self
614 .releases
615 .read()
616 .map_err(|_| RuntimeError::Internal)?
617 .iter()
618 .filter(|((stored_project_id, _), _)| stored_project_id == project_id)
619 .map(|(_, release)| release.clone())
620 .collect::<Vec<_>>();
621 releases.sort_by_key(|release| std::cmp::Reverse(release.version));
622 Ok(releases)
623 }
624
625 fn active_release(&self, project_id: &str) -> Result<Option<u64>, RuntimeError> {
626 Ok(self
627 .active_releases
628 .read()
629 .map_err(|_| RuntimeError::Internal)?
630 .get(project_id)
631 .copied())
632 }
633
634 fn set_active_release(&self, project_id: &str, version: u64) -> Result<(), RuntimeError> {
635 if !self
636 .releases
637 .read()
638 .map_err(|_| RuntimeError::Internal)?
639 .contains_key(&(project_id.to_string(), version))
640 {
641 return Err(RuntimeError::store("runtime release was not found"));
642 }
643 self.active_releases
644 .write()
645 .map_err(|_| RuntimeError::Internal)?
646 .insert(project_id.to_string(), version);
647 Ok(())
648 }
649
650 fn save_local_provider_binding(
651 &self,
652 project_id: &str,
653 binding: &LocalProviderBinding,
654 ) -> Result<(), RuntimeError> {
655 self.provider_bindings
656 .write()
657 .map_err(|_| RuntimeError::Internal)?
658 .insert(
659 (project_id.to_string(), binding.provider_id.clone()),
660 binding.clone(),
661 );
662 Ok(())
663 }
664
665 fn local_provider_bindings(
666 &self,
667 project_id: &str,
668 ) -> Result<Vec<LocalProviderBinding>, RuntimeError> {
669 let mut bindings = self
670 .provider_bindings
671 .read()
672 .map_err(|_| RuntimeError::Internal)?
673 .iter()
674 .filter(|((stored_project_id, _), _)| stored_project_id == project_id)
675 .map(|(_, binding)| binding.clone())
676 .collect::<Vec<_>>();
677 bindings.sort_by(|left, right| left.provider_id.cmp(&right.provider_id));
678 Ok(bindings)
679 }
680
681 fn enqueue_trace(&self, trace: &RuntimeTraceRecord) -> Result<(), RuntimeError> {
682 const MAX_MEMORY_TRACES: usize = 1_000;
683 let mut traces = self
684 .trace_outbox
685 .write()
686 .map_err(|_| RuntimeError::Internal)?;
687 if traces.iter().any(|stored| stored.id == trace.id) {
688 return Ok(());
689 }
690 traces.push_back(trace.clone());
691 while traces.len() > MAX_MEMORY_TRACES {
692 traces.pop_front();
693 }
694 Ok(())
695 }
696
697 fn pending_traces(&self, limit: usize) -> Result<Vec<RuntimeTraceRecord>, RuntimeError> {
698 Ok(self
699 .trace_outbox
700 .read()
701 .map_err(|_| RuntimeError::Internal)?
702 .iter()
703 .take(limit)
704 .cloned()
705 .collect())
706 }
707
708 fn acknowledge_traces(&self, trace_ids: &[String]) -> Result<(), RuntimeError> {
709 self.trace_outbox
710 .write()
711 .map_err(|_| RuntimeError::Internal)?
712 .retain(|trace| !trace_ids.contains(&trace.id));
713 Ok(())
714 }
715}
716
717#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
719#[serde(rename_all = "camelCase")]
720pub struct InvocationTraceEvent {
721 pub name: String,
722 pub status: String,
723 pub duration_ms: u64,
724 #[serde(default)]
725 pub attributes: Value,
726}
727
728#[derive(Clone, PartialEq, Serialize, Deserialize)]
730#[serde(rename_all = "camelCase")]
731pub struct InvocationOutput {
732 pub invocation_id: String,
733 pub project_id: String,
734 pub endpoint: String,
735 pub session_id: String,
736 pub agent: String,
737 pub provider: String,
738 pub capability: String,
739 pub data: InvocationData,
740 #[serde(default)]
741 pub metadata: Value,
742 pub snapshot: RuntimeSnapshot,
743 pub trace: Vec<InvocationTraceEvent>,
744}
745
746impl fmt::Debug for InvocationOutput {
747 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
748 formatter
749 .debug_struct("InvocationOutput")
750 .field("invocation_id", &self.invocation_id)
751 .field("project_id", &self.project_id)
752 .field("endpoint", &self.endpoint)
753 .field("session_id", &self.session_id)
754 .field("agent", &self.agent)
755 .field("provider", &self.provider)
756 .field("capability", &self.capability)
757 .field("data", &"[REDACTED]")
758 .field("metadata", &"[REDACTED]")
759 .field("snapshot_revision", &self.snapshot.revision)
760 .field("trace_count", &self.trace.len())
761 .finish()
762 }
763}
764
765#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
767pub struct InvocationHandle(pub String);
768
769#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
771#[serde(rename_all = "camelCase")]
772pub enum InvocationStatus {
773 Pending,
774 Running,
775 Completed,
776 Failed,
777 Cancelled,
778}
779
780#[derive(Clone, PartialEq, Serialize, Deserialize)]
782#[serde(rename_all = "camelCase")]
783pub struct InvocationPoll {
784 pub handle: InvocationHandle,
785 pub status: InvocationStatus,
786 #[serde(default, skip_serializing_if = "Option::is_none")]
787 pub output: Option<InvocationOutput>,
788 #[serde(default, skip_serializing_if = "Option::is_none")]
789 pub error: Option<String>,
790}
791
792impl fmt::Debug for InvocationPoll {
793 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
794 formatter
795 .debug_struct("InvocationPoll")
796 .field("handle", &self.handle)
797 .field("status", &self.status)
798 .field("output", &self.output.as_ref().map(|_| "[REDACTED]"))
799 .field("error", &self.error.as_ref().map(|_| "[REDACTED]"))
800 .finish()
801 }
802}
803
804#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
806#[serde(rename_all = "camelCase")]
807pub struct EffectExecution {
808 pub results: Vec<EffectResult>,
809 pub unhandled: Vec<EffectRequest>,
810}
811
812pub enum RuntimeError {
814 InvalidDefinition(String),
815 EndpointNotFound(String),
816 AgentNotFound(String),
817 ProviderNotFound(String),
818 CapabilityUnavailable {
819 provider: String,
820 capability: String,
821 },
822 Timeout(u64),
823 Cancelled,
824 Unavailable(String),
825 Backpressure(String),
826 Provider {
827 provider: String,
828 message: String,
829 },
830 Store(String),
831 Snapshot(String),
832 EffectLimitExceeded(usize),
833 InvocationNotFound(String),
834 Internal,
835}
836
837impl RuntimeError {
838 pub fn provider(provider: impl Into<String>, message: impl Into<String>) -> Self {
839 Self::Provider {
840 provider: provider.into(),
841 message: message.into(),
842 }
843 }
844
845 pub fn store(message: impl Into<String>) -> Self {
846 Self::Store(message.into())
847 }
848
849 pub fn public_message(&self) -> String {
850 match self {
851 Self::Provider { provider, .. } => {
852 format!("provider {provider} request failed")
853 }
854 Self::Store(_) => "runtime state could not be persisted".to_string(),
855 Self::Snapshot(_) => "runtime snapshot is invalid".to_string(),
856 Self::Unavailable(_) => "provider is not available".to_string(),
857 Self::Backpressure(_) => "runtime is busy".to_string(),
858 _ => self.to_string(),
859 }
860 }
861}
862
863impl fmt::Debug for RuntimeError {
864 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
865 match self {
866 Self::InvalidDefinition(_) => formatter.write_str("InvalidDefinition([REDACTED])"),
867 Self::EndpointNotFound(endpoint) => formatter
868 .debug_tuple("EndpointNotFound")
869 .field(endpoint)
870 .finish(),
871 Self::AgentNotFound(agent) => {
872 formatter.debug_tuple("AgentNotFound").field(agent).finish()
873 }
874 Self::ProviderNotFound(provider) => formatter
875 .debug_tuple("ProviderNotFound")
876 .field(provider)
877 .finish(),
878 Self::CapabilityUnavailable {
879 provider,
880 capability,
881 } => formatter
882 .debug_struct("CapabilityUnavailable")
883 .field("provider", provider)
884 .field("capability", capability)
885 .finish(),
886 Self::Timeout(timeout) => formatter.debug_tuple("Timeout").field(timeout).finish(),
887 Self::Cancelled => formatter.write_str("Cancelled"),
888 Self::Unavailable(_) => formatter.write_str("Unavailable([REDACTED])"),
889 Self::Backpressure(_) => formatter.write_str("Backpressure([REDACTED])"),
890 Self::Provider { provider, .. } => formatter
891 .debug_struct("Provider")
892 .field("provider", provider)
893 .field("message", &"[REDACTED]")
894 .finish(),
895 Self::Store(_) => formatter.write_str("Store([REDACTED])"),
896 Self::Snapshot(_) => formatter.write_str("Snapshot([REDACTED])"),
897 Self::EffectLimitExceeded(limit) => formatter
898 .debug_tuple("EffectLimitExceeded")
899 .field(limit)
900 .finish(),
901 Self::InvocationNotFound(handle) => formatter
902 .debug_tuple("InvocationNotFound")
903 .field(handle)
904 .finish(),
905 Self::Internal => formatter.write_str("Internal"),
906 }
907 }
908}
909
910impl fmt::Display for RuntimeError {
911 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
912 match self {
913 Self::InvalidDefinition(message) => {
914 write!(formatter, "invalid runtime definition: {message}")
915 }
916 Self::EndpointNotFound(endpoint) => {
917 write!(formatter, "endpoint {endpoint} is not registered")
918 }
919 Self::AgentNotFound(agent) => write!(formatter, "agent {agent} is not registered"),
920 Self::ProviderNotFound(provider) => {
921 write!(formatter, "provider {provider} is not registered")
922 }
923 Self::CapabilityUnavailable {
924 provider,
925 capability,
926 } => write!(
927 formatter,
928 "provider {provider} does not support capability {capability}"
929 ),
930 Self::Timeout(timeout_ms) => {
931 write!(formatter, "agent invocation was idle for {timeout_ms} ms")
932 }
933 Self::Cancelled => formatter.write_str("agent invocation was cancelled"),
934 Self::Unavailable(message) => {
935 write!(formatter, "provider is not available: {message}")
936 }
937 Self::Backpressure(message) => write!(formatter, "runtime is busy: {message}"),
938 Self::Provider { provider, message } => {
939 write!(formatter, "provider {provider} failed: {message}")
940 }
941 Self::Store(message) => write!(formatter, "runtime store failed: {message}"),
942 Self::Snapshot(message) => write!(formatter, "runtime snapshot failed: {message}"),
943 Self::EffectLimitExceeded(limit) => {
944 write!(formatter, "runtime effect limit {limit} was exceeded")
945 }
946 Self::InvocationNotFound(handle) => {
947 write!(formatter, "invocation {handle} was not found")
948 }
949 Self::Internal => formatter.write_str("runtime internal error"),
950 }
951 }
952}
953
954impl std::error::Error for RuntimeError {}
955
956#[derive(Default)]
957struct RuntimeRegistry {
958 providers: HashMap<String, Arc<dyn AgentProvider>>,
959 agents: HashMap<String, AgentDefinition>,
960 endpoints: HashMap<String, EndpointDefinition>,
961}
962
963struct RuntimeCore {
964 project_id: String,
965 registry: RwLock<RuntimeRegistry>,
966 manifest: RwLock<Option<RuntimeManifest>>,
967 store: Arc<dyn RuntimeStore>,
968 sessions: RwLock<HashMap<String, RuntimeSnapshot>>,
969 session_locks: Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>,
970 invocations: Mutex<InvocationRegistry>,
971 next_invocation: AtomicU64,
972 monitor_observer: RwLock<Option<RuntimeMonitorObserver>>,
973}
974
975struct InvocationEntry {
976 poll: InvocationPoll,
977 cancellation: CancellationToken,
978 events: VecDeque<InvocationEvent>,
979 next_event_sequence: u64,
980}
981
982#[derive(Default)]
983struct InvocationRegistry {
984 entries: HashMap<String, InvocationEntry>,
985 terminal_order: VecDeque<String>,
986 active_count: usize,
987}
988
989impl InvocationRegistry {
990 fn insert(
991 &mut self,
992 handle: InvocationHandle,
993 cancellation: CancellationToken,
994 ) -> Result<(), RuntimeError> {
995 if self.active_count >= MAX_IN_FLIGHT_INVOCATIONS {
996 return Err(RuntimeError::Backpressure(
997 "too many invocations are already running".to_string(),
998 ));
999 }
1000 self.entries.insert(
1001 handle.0.clone(),
1002 InvocationEntry {
1003 poll: InvocationPoll {
1004 handle,
1005 status: InvocationStatus::Pending,
1006 output: None,
1007 error: None,
1008 },
1009 cancellation,
1010 events: VecDeque::new(),
1011 next_event_sequence: 1,
1012 },
1013 );
1014 self.active_count += 1;
1015 Ok(())
1016 }
1017
1018 fn update(
1019 &mut self,
1020 handle: &InvocationHandle,
1021 status: InvocationStatus,
1022 output: Option<InvocationOutput>,
1023 error: Option<String>,
1024 ) {
1025 let Some(entry) = self.entries.get_mut(&handle.0) else {
1026 return;
1027 };
1028 if is_terminal_status(entry.poll.status) {
1029 return;
1030 }
1031 entry.poll.status = status;
1032 entry.poll.output = output;
1033 entry.poll.error = error;
1034 let event = match status {
1035 InvocationStatus::Pending => None,
1036 InvocationStatus::Running => Some((InvocationEventKind::Started, None, None)),
1037 InvocationStatus::Completed => Some((
1038 InvocationEventKind::Completed,
1039 entry.poll.output.as_ref().map(|value| value.data.clone()),
1040 None,
1041 )),
1042 InvocationStatus::Failed => {
1043 Some((InvocationEventKind::Failed, None, entry.poll.error.clone()))
1044 }
1045 InvocationStatus::Cancelled => Some((InvocationEventKind::Cancelled, None, None)),
1046 };
1047 if let Some((kind, data, error)) = event {
1048 entry.push_event(kind, data, error);
1049 }
1050 if is_terminal_status(status) {
1051 self.active_count = self.active_count.saturating_sub(1);
1052 self.terminal_order.push_back(handle.0.clone());
1053 self.evict_old_terminal_entries();
1054 }
1055 }
1056
1057 fn remove(&mut self, handle: &InvocationHandle) {
1058 if let Some(entry) = self.entries.remove(&handle.0) {
1059 if !is_terminal_status(entry.poll.status) {
1060 self.active_count = self.active_count.saturating_sub(1);
1061 }
1062 }
1063 self.terminal_order.retain(|stored| stored != &handle.0);
1064 }
1065
1066 fn take(&mut self, handle: &InvocationHandle) -> Result<InvocationPoll, RuntimeError> {
1067 let poll = self
1068 .entries
1069 .get(&handle.0)
1070 .map(|entry| entry.poll.clone())
1071 .ok_or_else(|| RuntimeError::InvocationNotFound(handle.0.clone()))?;
1072 if is_terminal_status(poll.status) {
1073 self.remove(handle);
1074 }
1075 Ok(poll)
1076 }
1077
1078 fn push_provider_event(&mut self, handle: &InvocationHandle, event: ProviderEvent) {
1079 let Some(entry) = self.entries.get_mut(&handle.0) else {
1080 return;
1081 };
1082 if entry.poll.status != InvocationStatus::Running {
1083 return;
1084 }
1085 match event {
1086 ProviderEvent::Activity => {}
1087 ProviderEvent::OutputDelta { data } => {
1088 entry.push_event(InvocationEventKind::OutputDelta, Some(data), None);
1089 }
1090 ProviderEvent::StageStarted { .. }
1091 | ProviderEvent::StageCompleted { .. }
1092 | ProviderEvent::StageFailed { .. } => {}
1093 }
1094 }
1095
1096 fn drain_events(
1097 &mut self,
1098 handle: &InvocationHandle,
1099 ) -> Result<Vec<InvocationEvent>, RuntimeError> {
1100 let entry = self
1101 .entries
1102 .get_mut(&handle.0)
1103 .ok_or_else(|| RuntimeError::InvocationNotFound(handle.0.clone()))?;
1104 Ok(entry.events.drain(..).collect())
1105 }
1106
1107 fn evict_old_terminal_entries(&mut self) {
1108 while self.terminal_order.len() > MAX_RETAINED_INVOCATIONS {
1109 if let Some(handle) = self.terminal_order.pop_front() {
1110 self.entries.remove(&handle);
1111 }
1112 }
1113 }
1114}
1115
1116impl InvocationEntry {
1117 fn push_event(
1118 &mut self,
1119 kind: InvocationEventKind,
1120 data: Option<InvocationData>,
1121 error: Option<String>,
1122 ) {
1123 if kind == InvocationEventKind::OutputDelta {
1124 if let (
1125 Some(InvocationEvent {
1126 kind: InvocationEventKind::OutputDelta,
1127 data: Some(previous),
1128 ..
1129 }),
1130 Some(next),
1131 ) = (self.events.back_mut(), data.as_ref())
1132 {
1133 if merge_invocation_data(previous, next) {
1134 return;
1135 }
1136 }
1137 }
1138 self.events.push_back(InvocationEvent {
1139 sequence: self.next_event_sequence,
1140 kind,
1141 data,
1142 error,
1143 });
1144 self.next_event_sequence = self.next_event_sequence.saturating_add(1);
1145 while self.events.len() > MAX_RETAINED_INVOCATION_EVENTS {
1146 self.events.pop_front();
1147 }
1148 }
1149}
1150
1151impl RuntimeCore {
1152 async fn invoke(
1153 self: &Arc<Self>,
1154 invocation_id: String,
1155 input: InvocationInput,
1156 cancellation: CancellationToken,
1157 forwarded_events: ProviderEventSink,
1158 ) -> Result<InvocationOutput, RuntimeError> {
1159 let endpoint = input.endpoint.clone();
1160 let created_at_ms = crate::unix_time_ms();
1161 let trace_id = format!("trace-{created_at_ms}-{invocation_id}");
1162 let started = Instant::now();
1163 let result = self
1164 .invoke_provider(
1165 trace_id.clone(),
1166 created_at_ms,
1167 invocation_id.clone(),
1168 input,
1169 cancellation,
1170 forwarded_events,
1171 )
1172 .await;
1173 let elapsed_ms = duration_ms(started.elapsed());
1174 let trace = match &result {
1175 Ok(output) => RuntimeTraceRecord {
1176 id: trace_id.clone(),
1177 project_id: self.project_id.clone(),
1178 invocation_id: invocation_id.clone(),
1179 endpoint: endpoint.clone(),
1180 agent: Some(output.agent.clone()),
1181 provider: Some(output.provider.clone()),
1182 capability: Some(output.capability.clone()),
1183 status: "completed".to_string(),
1184 duration_ms: elapsed_ms,
1185 created_at_ms,
1186 },
1187 Err(error) => RuntimeTraceRecord {
1188 id: trace_id.clone(),
1189 project_id: self.project_id.clone(),
1190 invocation_id: invocation_id.clone(),
1191 endpoint,
1192 agent: None,
1193 provider: None,
1194 capability: None,
1195 status: match error {
1196 RuntimeError::Cancelled => "cancelled",
1197 _ => "error",
1198 }
1199 .to_string(),
1200 duration_ms: elapsed_ms,
1201 created_at_ms,
1202 },
1203 };
1204 let _ = self.store.enqueue_trace(&trace);
1205 self.emit_monitor_event(RuntimeMonitorEvent::InvocationFinished {
1206 trace_id,
1207 invocation_id,
1208 status: match &result {
1209 Ok(_) => RuntimeMonitorStatus::Completed,
1210 Err(RuntimeError::Cancelled) => RuntimeMonitorStatus::Cancelled,
1211 Err(_) => RuntimeMonitorStatus::Error,
1212 },
1213 duration_ms: elapsed_ms,
1214 ended_at_ms: created_at_ms.saturating_add(elapsed_ms),
1218 error: result.as_ref().err().map(RuntimeError::public_message),
1219 });
1220 result
1221 }
1222
1223 async fn invoke_provider(
1224 self: &Arc<Self>,
1225 trace_id: String,
1226 started_at_ms: u64,
1227 invocation_id: String,
1228 input: InvocationInput,
1229 cancellation: CancellationToken,
1230 forwarded_events: ProviderEventSink,
1231 ) -> Result<InvocationOutput, RuntimeError> {
1232 validate_identifier("endpoint", &input.endpoint)?;
1233 validate_identifier("session", &input.session_id)?;
1234 let session_lock = {
1235 let mut locks = self
1236 .session_locks
1237 .lock()
1238 .map_err(|_| RuntimeError::Internal)?;
1239 Arc::clone(
1240 locks
1241 .entry(input.session_id.clone())
1242 .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(()))),
1243 )
1244 };
1245 let _session_guard = session_lock.lock().await;
1246 let (endpoint, agent, provider) = {
1247 let registry = self.registry.read().map_err(|_| RuntimeError::Internal)?;
1248 let endpoint = registry
1249 .endpoints
1250 .get(&input.endpoint)
1251 .cloned()
1252 .ok_or_else(|| RuntimeError::EndpointNotFound(input.endpoint.clone()))?;
1253 let agent = registry
1254 .agents
1255 .get(&endpoint.agent)
1256 .cloned()
1257 .ok_or_else(|| RuntimeError::AgentNotFound(endpoint.agent.clone()))?;
1258 let provider = registry
1259 .providers
1260 .get(&agent.provider)
1261 .cloned()
1262 .ok_or_else(|| RuntimeError::ProviderNotFound(agent.provider.clone()))?;
1263 (endpoint, agent, provider)
1264 };
1265 if !agent
1266 .capabilities
1267 .iter()
1268 .any(|capability| capability == &endpoint.capability)
1269 || !provider.supports(&endpoint.capability)
1270 {
1271 return Err(RuntimeError::CapabilityUnavailable {
1272 provider: agent.provider.clone(),
1273 capability: endpoint.capability,
1274 });
1275 }
1276 if cancellation.is_cancelled() {
1277 return Err(RuntimeError::Cancelled);
1278 }
1279
1280 self.emit_monitor_event(RuntimeMonitorEvent::InvocationStarted {
1281 trace_id: trace_id.clone(),
1282 invocation_id: invocation_id.clone(),
1283 project_id: self.project_id.clone(),
1284 endpoint: endpoint.name.clone(),
1285 agent_id: agent.id.clone(),
1286 provider_id: agent.provider.clone(),
1287 capability: endpoint.capability.clone(),
1288 started_at_ms,
1289 });
1290
1291 let snapshot = self.load_snapshot(&input.session_id)?;
1292 let request = ProviderRequest {
1293 project_id: self.project_id.clone(),
1294 endpoint: endpoint.name.clone(),
1295 session_id: input.session_id.clone(),
1296 agent: agent.clone(),
1297 capability: endpoint.capability.clone(),
1298 data: input.data,
1299 metadata: input.metadata,
1300 snapshot: snapshot.clone(),
1301 };
1302 let started = Instant::now();
1303 let (activity_sender, mut activity_receiver) = watch::channel(0_u64);
1304 let events = self.provider_event_sink(
1305 &InvocationHandle(invocation_id.clone()),
1306 trace_id,
1307 started,
1308 activity_sender,
1309 forwarded_events,
1310 );
1311 let provider_call = provider.invoke_with_events(request, cancellation.clone(), events);
1312 tokio::pin!(provider_call);
1313 let idle_timeout = Duration::from_millis(endpoint.timeout_ms);
1314 let idle_deadline = tokio::time::sleep(idle_timeout);
1315 tokio::pin!(idle_deadline);
1316 let mut activity_open = true;
1317 let response = loop {
1318 tokio::select! {
1319 biased;
1320 _ = cancellation.cancelled() => return Err(RuntimeError::Cancelled),
1321 response = &mut provider_call => break response?,
1322 changed = activity_receiver.changed(), if activity_open => {
1323 if changed.is_err() {
1324 activity_open = false;
1325 } else {
1326 idle_deadline.as_mut().reset(tokio::time::Instant::now() + idle_timeout);
1327 }
1328 }
1329 _ = &mut idle_deadline => {
1330 cancellation.cancel();
1331 return Err(RuntimeError::Timeout(endpoint.timeout_ms));
1332 }
1333 }
1334 };
1335 if cancellation.is_cancelled() {
1336 return Err(RuntimeError::Cancelled);
1337 }
1338
1339 let next_snapshot = RuntimeSnapshot {
1340 revision: snapshot.revision.saturating_add(1),
1341 state: response.state.unwrap_or(snapshot.state),
1342 };
1343 self.store
1344 .save(&self.project_id, &input.session_id, &next_snapshot)?;
1345 self.sessions
1346 .write()
1347 .map_err(|_| RuntimeError::Internal)?
1348 .insert(input.session_id.clone(), next_snapshot.clone());
1349 Ok(InvocationOutput {
1350 invocation_id,
1351 project_id: self.project_id.clone(),
1352 endpoint: endpoint.name,
1353 session_id: input.session_id,
1354 agent: agent.id,
1355 provider: agent.provider,
1356 capability: endpoint.capability,
1357 data: response.data,
1358 metadata: response.metadata,
1359 snapshot: next_snapshot,
1360 trace: vec![InvocationTraceEvent {
1361 name: "provider.invoke".to_string(),
1362 status: "completed".to_string(),
1363 duration_ms: duration_ms(started.elapsed()),
1364 attributes: json!({
1365 "endpoint": input.endpoint,
1366 }),
1367 }],
1368 })
1369 }
1370
1371 fn load_snapshot(&self, session_id: &str) -> Result<RuntimeSnapshot, RuntimeError> {
1372 if let Some(snapshot) = self
1373 .sessions
1374 .read()
1375 .map_err(|_| RuntimeError::Internal)?
1376 .get(session_id)
1377 .cloned()
1378 {
1379 return Ok(snapshot);
1380 }
1381 let snapshot = self
1382 .store
1383 .load(&self.project_id, session_id)?
1384 .unwrap_or_default();
1385 self.sessions
1386 .write()
1387 .map_err(|_| RuntimeError::Internal)?
1388 .insert(session_id.to_string(), snapshot.clone());
1389 Ok(snapshot)
1390 }
1391
1392 fn next_invocation_id(&self) -> String {
1393 let id = self.next_invocation.fetch_add(1, Ordering::Relaxed);
1394 format!("invocation-{id}")
1395 }
1396
1397 fn update_poll(
1398 &self,
1399 handle: &InvocationHandle,
1400 status: InvocationStatus,
1401 output: Option<InvocationOutput>,
1402 error: Option<String>,
1403 ) {
1404 if let Ok(mut invocations) = self.invocations.lock() {
1405 invocations.update(handle, status, output, error);
1406 }
1407 }
1408
1409 fn provider_event_sink(
1410 self: &Arc<Self>,
1411 handle: &InvocationHandle,
1412 trace_id: String,
1413 request_started: Instant,
1414 activity: watch::Sender<u64>,
1415 forwarded_events: ProviderEventSink,
1416 ) -> ProviderEventSink {
1417 let core = Arc::clone(self);
1418 let handle = handle.clone();
1419 ProviderEventSink::new(move |event| {
1420 activity.send_modify(|sequence| *sequence = sequence.saturating_add(1));
1421 (forwarded_events.emit)(event.clone());
1422 if let Ok(mut invocations) = core.invocations.lock() {
1423 invocations.push_provider_event(&handle, event.clone());
1424 }
1425 let (stage, status, elapsed_ms, error) = match event {
1426 ProviderEvent::Activity => return,
1427 ProviderEvent::OutputDelta { .. } => return,
1428 ProviderEvent::StageStarted { stage, .. } => {
1429 (stage, RuntimeMonitorStageStatus::Started, None, None)
1430 }
1431 ProviderEvent::StageCompleted {
1432 stage, elapsed_ms, ..
1433 } => (
1434 stage,
1435 RuntimeMonitorStageStatus::Completed,
1436 Some(elapsed_ms),
1437 None,
1438 ),
1439 ProviderEvent::StageFailed {
1440 stage,
1441 elapsed_ms,
1442 error,
1443 ..
1444 } => (
1445 stage,
1446 RuntimeMonitorStageStatus::Failed,
1447 Some(elapsed_ms),
1448 Some(error),
1449 ),
1450 };
1451 core.emit_monitor_event(RuntimeMonitorEvent::ProviderStage {
1452 trace_id: trace_id.clone(),
1453 invocation_id: handle.0.clone(),
1454 stage,
1455 status,
1456 elapsed_ms,
1457 request_elapsed_ms: duration_ms(request_started.elapsed()),
1458 error,
1459 });
1460 })
1461 }
1462
1463 fn emit_monitor_event(&self, event: RuntimeMonitorEvent) {
1464 let observer = self
1465 .monitor_observer
1466 .read()
1467 .ok()
1468 .and_then(|observer| observer.clone());
1469 if let Some(observer) = observer {
1470 observer(event);
1471 }
1472 }
1473}
1474
1475fn is_null(value: &Value) -> bool {
1476 value.is_null()
1477}
1478
1479fn merge_invocation_data(previous: &mut InvocationData, next: &InvocationData) -> bool {
1480 match (previous, next) {
1481 (
1482 InvocationData::Json(Value::String(previous)),
1483 InvocationData::Json(Value::String(next)),
1484 ) if previous.len().saturating_add(next.len()) <= MAX_COALESCED_EVENT_BYTES => {
1485 previous.push_str(next);
1486 true
1487 }
1488 (InvocationData::Binary(previous), InvocationData::Binary(next))
1489 if previous.len().saturating_add(next.len()) <= MAX_COALESCED_EVENT_BYTES =>
1490 {
1491 previous.extend_from_slice(next);
1492 true
1493 }
1494 _ => false,
1495 }
1496}
1497
1498enum WorkerCommand {
1499 Start {
1500 handle: InvocationHandle,
1501 input: InvocationInput,
1502 cancellation: CancellationToken,
1503 },
1504}
1505
1506struct RuntimeWorker {
1507 sender: Mutex<Option<mpsc::Sender<WorkerCommand>>>,
1508 thread: Mutex<Option<JoinHandle<()>>>,
1509}
1510
1511impl RuntimeWorker {
1512 fn spawn(core: Arc<RuntimeCore>) -> Result<Self, RuntimeError> {
1513 let (sender, mut receiver) = mpsc::channel(WORKER_QUEUE_CAPACITY);
1514 let thread = std::thread::Builder::new()
1515 .name(format!("vifu-runtime-{}", core.project_id))
1516 .spawn(move || {
1517 let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
1518 .enable_time()
1519 .build()
1520 else {
1521 return;
1522 };
1523 runtime.block_on(async move {
1524 while let Some(command) = receiver.recv().await {
1525 match command {
1526 WorkerCommand::Start {
1527 handle,
1528 input,
1529 cancellation,
1530 } => {
1531 let invocation_core = Arc::clone(&core);
1532 tokio::spawn(async move {
1533 invocation_core.update_poll(
1534 &handle,
1535 InvocationStatus::Running,
1536 None,
1537 None,
1538 );
1539 let result = invocation_core
1540 .invoke(
1541 handle.0.clone(),
1542 input,
1543 cancellation,
1544 ProviderEventSink::discard(),
1545 )
1546 .await;
1547 match result {
1548 Ok(output) => invocation_core.update_poll(
1549 &handle,
1550 InvocationStatus::Completed,
1551 Some(output),
1552 None,
1553 ),
1554 Err(RuntimeError::Cancelled) => invocation_core
1555 .update_poll(
1556 &handle,
1557 InvocationStatus::Cancelled,
1558 None,
1559 None,
1560 ),
1561 Err(error) => invocation_core.update_poll(
1562 &handle,
1563 InvocationStatus::Failed,
1564 None,
1565 Some(error.public_message()),
1566 ),
1567 }
1568 });
1569 }
1570 }
1571 }
1572 });
1573 })
1574 .map_err(|_error| RuntimeError::Internal)?;
1575 Ok(Self {
1576 sender: Mutex::new(Some(sender)),
1577 thread: Mutex::new(Some(thread)),
1578 })
1579 }
1580
1581 fn send(&self, command: WorkerCommand) -> Result<(), RuntimeError> {
1582 self.sender
1583 .lock()
1584 .map_err(|_| RuntimeError::Internal)?
1585 .as_ref()
1586 .ok_or(RuntimeError::Internal)?
1587 .try_send(command)
1588 .map_err(|error| match error {
1589 mpsc::error::TrySendError::Full(_) => {
1590 RuntimeError::Backpressure("invocation queue is full".to_string())
1591 }
1592 mpsc::error::TrySendError::Closed(_) => RuntimeError::Internal,
1593 })
1594 }
1595}
1596
1597impl Drop for RuntimeWorker {
1598 fn drop(&mut self) {
1599 if let Ok(sender) = self.sender.get_mut() {
1600 sender.take();
1601 }
1602 if let Ok(thread) = self.thread.get_mut() {
1603 if let Some(thread) = thread.take() {
1604 let _ = thread.join();
1605 }
1606 }
1607 }
1608}
1609
1610#[derive(Clone)]
1616pub struct VifuRuntime {
1617 core: Arc<RuntimeCore>,
1618 worker: Arc<Mutex<Option<RuntimeWorker>>>,
1619}
1620
1621impl fmt::Debug for VifuRuntime {
1622 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1623 let counts = self.core.registry.read().ok().map(|registry| {
1624 (
1625 registry.providers.len(),
1626 registry.agents.len(),
1627 registry.endpoints.len(),
1628 )
1629 });
1630 formatter
1631 .debug_struct("VifuRuntime")
1632 .field("project_id", &self.core.project_id)
1633 .field("resource_counts", &counts)
1634 .finish()
1635 }
1636}
1637
1638impl VifuRuntime {
1639 pub fn new(project_id: impl Into<String>) -> Result<Self, RuntimeError> {
1640 Self::with_store(project_id, Arc::new(MemoryRuntimeStore::default()))
1641 }
1642
1643 pub fn with_store(
1644 project_id: impl Into<String>,
1645 store: Arc<dyn RuntimeStore>,
1646 ) -> Result<Self, RuntimeError> {
1647 let project_id = project_id.into();
1648 validate_identifier("project", &project_id)?;
1649 let core = Arc::new(RuntimeCore {
1650 project_id,
1651 registry: RwLock::new(RuntimeRegistry::default()),
1652 manifest: RwLock::new(None),
1653 store,
1654 sessions: RwLock::new(HashMap::new()),
1655 session_locks: Mutex::new(HashMap::new()),
1656 invocations: Mutex::new(InvocationRegistry::default()),
1657 next_invocation: AtomicU64::new(1),
1658 monitor_observer: RwLock::new(None),
1659 });
1660 let worker = Arc::new(Mutex::new(None));
1661 Ok(Self { core, worker })
1662 }
1663
1664 pub fn project_id(&self) -> &str {
1665 &self.core.project_id
1666 }
1667
1668 pub fn set_monitor_observer(
1670 &self,
1671 observer: Option<RuntimeMonitorObserver>,
1672 ) -> Result<(), RuntimeError> {
1673 *self
1674 .core
1675 .monitor_observer
1676 .write()
1677 .map_err(|_| RuntimeError::Internal)? = observer;
1678 Ok(())
1679 }
1680
1681 pub fn register_provider(
1682 &self,
1683 name: impl Into<String>,
1684 provider: Arc<dyn AgentProvider>,
1685 ) -> Result<(), RuntimeError> {
1686 let name = name.into();
1687 validate_identifier("provider", &name)?;
1688 self.core
1689 .registry
1690 .write()
1691 .map_err(|_| RuntimeError::Internal)?
1692 .providers
1693 .insert(name, provider);
1694 Ok(())
1695 }
1696
1697 pub fn register_agent(&self, mut agent: AgentDefinition) -> Result<(), RuntimeError> {
1698 validate_identifier("agent", &agent.id)?;
1699 validate_identifier("provider", &agent.provider)?;
1700 if agent.name.trim().is_empty() || agent.capabilities.is_empty() {
1701 return Err(RuntimeError::InvalidDefinition(
1702 "agent name and at least one capability are required".to_string(),
1703 ));
1704 }
1705 for capability in &mut agent.capabilities {
1706 *capability = capability.trim().to_ascii_lowercase();
1707 validate_identifier("capability", capability)?;
1708 }
1709 agent.capabilities.sort();
1710 agent.capabilities.dedup();
1711 let mut registry = self
1712 .core
1713 .registry
1714 .write()
1715 .map_err(|_| RuntimeError::Internal)?;
1716 if !registry.providers.contains_key(&agent.provider) {
1717 return Err(RuntimeError::ProviderNotFound(agent.provider));
1718 }
1719 registry.agents.insert(agent.id.clone(), agent);
1720 Ok(())
1721 }
1722
1723 pub fn register_endpoint(&self, mut endpoint: EndpointDefinition) -> Result<(), RuntimeError> {
1724 validate_identifier("endpoint", &endpoint.name)?;
1725 validate_identifier("agent", &endpoint.agent)?;
1726 endpoint.capability = endpoint.capability.trim().to_ascii_lowercase();
1727 validate_identifier("capability", &endpoint.capability)?;
1728 if !(1..=MAX_TIMEOUT_MS).contains(&endpoint.timeout_ms) {
1729 return Err(RuntimeError::InvalidDefinition(format!(
1730 "endpoint timeout must be between 1 and {MAX_TIMEOUT_MS} ms"
1731 )));
1732 }
1733 let mut registry = self
1734 .core
1735 .registry
1736 .write()
1737 .map_err(|_| RuntimeError::Internal)?;
1738 let agent = registry
1739 .agents
1740 .get(&endpoint.agent)
1741 .ok_or_else(|| RuntimeError::AgentNotFound(endpoint.agent.clone()))?;
1742 if !agent
1743 .capabilities
1744 .iter()
1745 .any(|capability| capability == &endpoint.capability)
1746 {
1747 return Err(RuntimeError::CapabilityUnavailable {
1748 provider: agent.provider.clone(),
1749 capability: endpoint.capability,
1750 });
1751 }
1752 registry.endpoints.insert(endpoint.name.clone(), endpoint);
1753 Ok(())
1754 }
1755
1756 pub fn agent_definitions(&self) -> Result<Vec<AgentDefinition>, RuntimeError> {
1757 let mut agents = self
1758 .core
1759 .registry
1760 .read()
1761 .map_err(|_| RuntimeError::Internal)?
1762 .agents
1763 .values()
1764 .cloned()
1765 .collect::<Vec<_>>();
1766 agents.sort_by(|left, right| left.id.cmp(&right.id));
1767 Ok(agents)
1768 }
1769
1770 pub fn endpoint_definitions(&self) -> Result<Vec<EndpointDefinition>, RuntimeError> {
1771 let mut endpoints = self
1772 .core
1773 .registry
1774 .read()
1775 .map_err(|_| RuntimeError::Internal)?
1776 .endpoints
1777 .values()
1778 .cloned()
1779 .collect::<Vec<_>>();
1780 endpoints.sort_by(|left, right| left.name.cmp(&right.name));
1781 Ok(endpoints)
1782 }
1783
1784 pub fn apply_manifest(&self, manifest: RuntimeManifest) -> Result<(), RuntimeError> {
1787 manifest.validate()?;
1788 if manifest.project_id != self.core.project_id {
1789 return Err(RuntimeError::InvalidDefinition(
1790 "project settings belong to another project".to_string(),
1791 ));
1792 }
1793 let mut registry = self
1794 .core
1795 .registry
1796 .write()
1797 .map_err(|_| RuntimeError::Internal)?;
1798 for requirement in &manifest.providers {
1799 let provider = registry
1800 .providers
1801 .get(&requirement.id)
1802 .ok_or_else(|| RuntimeError::ProviderNotFound(requirement.id.clone()))?;
1803 for capability in &requirement.capabilities {
1804 if !provider.supports(capability) {
1805 return Err(RuntimeError::CapabilityUnavailable {
1806 provider: requirement.id.clone(),
1807 capability: capability.clone(),
1808 });
1809 }
1810 }
1811 }
1812 registry.agents = manifest
1813 .agents
1814 .iter()
1815 .cloned()
1816 .map(|agent| (agent.id.clone(), agent))
1817 .collect();
1818 registry.endpoints = manifest
1819 .endpoints
1820 .iter()
1821 .cloned()
1822 .map(|endpoint| (endpoint.name.clone(), endpoint))
1823 .collect();
1824 *self
1825 .core
1826 .manifest
1827 .write()
1828 .map_err(|_| RuntimeError::Internal)? = Some(manifest);
1829 Ok(())
1830 }
1831
1832 pub fn apply_project_settings(&self, settings: ProjectSettings) -> Result<(), RuntimeError> {
1833 self.apply_manifest(settings)
1834 }
1835
1836 pub fn current_manifest(&self) -> Result<Option<RuntimeManifest>, RuntimeError> {
1837 Ok(self
1838 .core
1839 .manifest
1840 .read()
1841 .map_err(|_| RuntimeError::Internal)?
1842 .clone())
1843 }
1844
1845 pub fn current_project_settings(&self) -> Result<Option<ProjectSettings>, RuntimeError> {
1846 self.current_manifest()
1847 }
1848
1849 pub fn install_release(&self, release: &RuntimeRelease) -> Result<(), RuntimeError> {
1850 release.validate()?;
1851 if release.manifest.project_id != self.core.project_id {
1852 return Err(RuntimeError::InvalidDefinition(
1853 "runtime release belongs to another project".to_string(),
1854 ));
1855 }
1856 self.core.store.save_release(release)
1857 }
1858
1859 pub fn releases(&self) -> Result<Vec<RuntimeRelease>, RuntimeError> {
1860 self.core.store.list_releases(&self.core.project_id)
1861 }
1862
1863 pub fn active_release_version(&self) -> Result<Option<u64>, RuntimeError> {
1864 self.core.store.active_release(&self.core.project_id)
1865 }
1866
1867 pub fn activate_release(&self, version: u64) -> Result<RuntimeRelease, RuntimeError> {
1868 let release = self
1869 .core
1870 .store
1871 .load_release(&self.core.project_id, version)?
1872 .ok_or_else(|| RuntimeError::store("runtime release was not found"))?;
1873 self.apply_manifest(release.manifest.clone())?;
1874 self.core
1875 .store
1876 .set_active_release(&self.core.project_id, version)?;
1877 Ok(release)
1878 }
1879
1880 pub fn restore_active_release(&self) -> Result<Option<RuntimeRelease>, RuntimeError> {
1881 self.active_release_version()?
1882 .map(|version| self.activate_release(version))
1883 .transpose()
1884 }
1885
1886 pub fn bootstrap_release(
1887 &self,
1888 manifest: RuntimeManifest,
1889 ) -> Result<RuntimeRelease, RuntimeError> {
1890 if let Some(active) = self.restore_active_release()? {
1891 return Ok(active);
1892 }
1893 let release = RuntimeRelease::new(1, manifest)?;
1894 self.install_release(&release)?;
1895 self.activate_release(release.version)
1896 }
1897
1898 pub fn bootstrap_project_settings(
1899 &self,
1900 settings: ProjectSettings,
1901 ) -> Result<RuntimeRelease, RuntimeError> {
1902 self.bootstrap_release(settings)
1903 }
1904
1905 pub fn save_local_provider_binding(
1906 &self,
1907 binding: &LocalProviderBinding,
1908 ) -> Result<(), RuntimeError> {
1909 validate_identifier("provider", &binding.provider_id)?;
1910 self.core
1911 .store
1912 .save_local_provider_binding(&self.core.project_id, binding)
1913 }
1914
1915 pub fn local_provider_bindings(&self) -> Result<Vec<LocalProviderBinding>, RuntimeError> {
1916 self.core
1917 .store
1918 .local_provider_bindings(&self.core.project_id)
1919 }
1920
1921 pub fn pending_traces(&self, limit: usize) -> Result<Vec<RuntimeTraceRecord>, RuntimeError> {
1922 self.core.store.pending_traces(limit.min(1_000))
1923 }
1924
1925 pub fn acknowledge_traces(&self, trace_ids: &[String]) -> Result<(), RuntimeError> {
1926 self.core.store.acknowledge_traces(trace_ids)
1927 }
1928
1929 pub fn session(&self, session_id: impl Into<String>) -> Result<RuntimeSession, RuntimeError> {
1930 let session_id = session_id.into();
1931 validate_identifier("session", &session_id)?;
1932 Ok(RuntimeSession {
1933 runtime: self.clone(),
1934 session_id,
1935 })
1936 }
1937
1938 pub async fn invoke(&self, input: InvocationInput) -> Result<InvocationOutput, RuntimeError> {
1939 self.invoke_with_cancellation(input, CancellationToken::default())
1940 .await
1941 }
1942
1943 pub async fn invoke_with_cancellation(
1946 &self,
1947 input: InvocationInput,
1948 cancellation: CancellationToken,
1949 ) -> Result<InvocationOutput, RuntimeError> {
1950 self.invoke_with_events_and_cancellation(input, cancellation, ProviderEventSink::discard())
1951 .await
1952 }
1953
1954 pub async fn invoke_with_events_and_cancellation(
1957 &self,
1958 input: InvocationInput,
1959 cancellation: CancellationToken,
1960 events: ProviderEventSink,
1961 ) -> Result<InvocationOutput, RuntimeError> {
1962 let invocation_id = self.core.next_invocation_id();
1963 self.core
1964 .invoke(invocation_id, input, cancellation, events)
1965 .await
1966 }
1967
1968 pub fn start_invoke(&self, input: InvocationInput) -> Result<InvocationHandle, RuntimeError> {
1969 validate_identifier("endpoint", &input.endpoint)?;
1970 validate_identifier("session", &input.session_id)?;
1971 let handle = InvocationHandle(self.core.next_invocation_id());
1972 let cancellation = CancellationToken::default();
1973 let mut worker = self.worker.lock().map_err(|_| RuntimeError::Internal)?;
1974 if worker.is_none() {
1975 *worker = Some(RuntimeWorker::spawn(Arc::clone(&self.core))?);
1976 }
1977 self.core
1978 .invocations
1979 .lock()
1980 .map_err(|_| RuntimeError::Internal)?
1981 .insert(handle.clone(), cancellation.clone())?;
1982 let send_result =
1983 worker
1984 .as_ref()
1985 .ok_or(RuntimeError::Internal)?
1986 .send(WorkerCommand::Start {
1987 handle: handle.clone(),
1988 input,
1989 cancellation,
1990 });
1991 if let Err(error) = send_result {
1992 self.core
1993 .invocations
1994 .lock()
1995 .map_err(|_| RuntimeError::Internal)?
1996 .remove(&handle);
1997 return Err(error);
1998 }
1999 Ok(handle)
2000 }
2001
2002 pub fn poll_invocation(
2003 &self,
2004 handle: &InvocationHandle,
2005 ) -> Result<InvocationPoll, RuntimeError> {
2006 self.core
2007 .invocations
2008 .lock()
2009 .map_err(|_| RuntimeError::Internal)?
2010 .entries
2011 .get(&handle.0)
2012 .map(|entry| entry.poll.clone())
2013 .ok_or_else(|| RuntimeError::InvocationNotFound(handle.0.clone()))
2014 }
2015
2016 pub fn drain_invocation_events(
2018 &self,
2019 handle: &InvocationHandle,
2020 ) -> Result<Vec<InvocationEvent>, RuntimeError> {
2021 self.core
2022 .invocations
2023 .lock()
2024 .map_err(|_| RuntimeError::Internal)?
2025 .drain_events(handle)
2026 }
2027
2028 pub fn take_invocation(
2033 &self,
2034 handle: &InvocationHandle,
2035 ) -> Result<InvocationPoll, RuntimeError> {
2036 self.core
2037 .invocations
2038 .lock()
2039 .map_err(|_| RuntimeError::Internal)?
2040 .take(handle)
2041 }
2042
2043 pub fn cancel_invocation(&self, handle: &InvocationHandle) -> Result<(), RuntimeError> {
2044 let cancellation = self
2045 .core
2046 .invocations
2047 .lock()
2048 .map_err(|_| RuntimeError::Internal)?
2049 .entries
2050 .get(&handle.0)
2051 .map(|entry| entry.cancellation.clone())
2052 .ok_or_else(|| RuntimeError::InvocationNotFound(handle.0.clone()))?;
2053 cancellation.cancel();
2054 self.core
2055 .update_poll(handle, InvocationStatus::Cancelled, None, None);
2056 Ok(())
2057 }
2058
2059 pub async fn execute_effects(
2060 &self,
2061 effects: Vec<EffectRequest>,
2062 ) -> Result<EffectExecution, RuntimeError> {
2063 self.execute_effects_with_limit(effects, DEFAULT_EFFECT_LIMIT)
2064 .await
2065 }
2066
2067 pub async fn execute_effects_with_limit(
2068 &self,
2069 effects: Vec<EffectRequest>,
2070 limit: usize,
2071 ) -> Result<EffectExecution, RuntimeError> {
2072 if effects.len() > limit {
2073 return Err(RuntimeError::EffectLimitExceeded(limit));
2074 }
2075 let mut results = Vec::new();
2076 let mut unhandled = Vec::new();
2077 for effect in effects {
2078 if effect.kind != "agent.invoke" {
2079 unhandled.push(effect);
2080 continue;
2081 }
2082 let input = serde_json::from_value::<InvocationInput>(effect.payload.clone())
2083 .map_err(|error| RuntimeError::InvalidDefinition(error.to_string()))?;
2084 let result = self.invoke(input).await;
2085 match result {
2086 Ok(output) => results.push(EffectResult {
2087 effect_id: effect.id,
2088 succeeded: true,
2089 output: serde_json::to_value(output)
2090 .map_err(|_error| RuntimeError::Internal)?,
2091 }),
2092 Err(error) => results.push(EffectResult {
2093 effect_id: effect.id,
2094 succeeded: false,
2095 output: json!({ "error": error.public_message() }),
2096 }),
2097 }
2098 }
2099 Ok(EffectExecution { results, unhandled })
2100 }
2101
2102 pub fn export_snapshot(&self) -> Result<Vec<u8>, RuntimeError> {
2103 let snapshot = PortableProjectSnapshot {
2104 version: SNAPSHOT_VERSION,
2105 project_id: self.core.project_id.clone(),
2106 sessions: self
2107 .core
2108 .sessions
2109 .read()
2110 .map_err(|_| RuntimeError::Internal)?
2111 .clone(),
2112 };
2113 serde_json::to_vec(&snapshot).map_err(|error| RuntimeError::Snapshot(error.to_string()))
2114 }
2115
2116 pub fn restore_snapshot(&self, bytes: &[u8]) -> Result<(), RuntimeError> {
2117 let snapshot = serde_json::from_slice::<PortableProjectSnapshot>(bytes)
2118 .map_err(|error| RuntimeError::Snapshot(error.to_string()))?;
2119 if snapshot.version != SNAPSHOT_VERSION || snapshot.project_id != self.core.project_id {
2120 return Err(RuntimeError::Snapshot(
2121 "snapshot version or project does not match".to_string(),
2122 ));
2123 }
2124 for (session_id, state) in &snapshot.sessions {
2125 validate_identifier("session", session_id)?;
2126 self.core
2127 .store
2128 .save(&self.core.project_id, session_id, state)?;
2129 }
2130 *self
2131 .core
2132 .sessions
2133 .write()
2134 .map_err(|_| RuntimeError::Internal)? = snapshot.sessions;
2135 Ok(())
2136 }
2137}
2138
2139#[derive(Clone, Debug)]
2141pub struct RuntimeSession {
2142 runtime: VifuRuntime,
2143 session_id: String,
2144}
2145
2146impl RuntimeSession {
2147 pub fn id(&self) -> &str {
2148 &self.session_id
2149 }
2150
2151 pub async fn invoke(
2152 &self,
2153 mut input: InvocationInput,
2154 ) -> Result<InvocationOutput, RuntimeError> {
2155 input.session_id.clone_from(&self.session_id);
2156 self.runtime.invoke(input).await
2157 }
2158
2159 pub fn start_invoke(
2160 &self,
2161 mut input: InvocationInput,
2162 ) -> Result<InvocationHandle, RuntimeError> {
2163 input.session_id.clone_from(&self.session_id);
2164 self.runtime.start_invoke(input)
2165 }
2166}
2167
2168#[derive(Serialize, Deserialize)]
2169#[serde(rename_all = "camelCase")]
2170struct PortableProjectSnapshot {
2171 version: u32,
2172 project_id: String,
2173 sessions: HashMap<String, RuntimeSnapshot>,
2174}
2175
2176fn default_session_id() -> String {
2177 "default".to_string()
2178}
2179
2180const fn default_timeout_ms() -> u64 {
2181 DEFAULT_TIMEOUT_MS
2182}
2183
2184fn validate_identifier(kind: &str, value: &str) -> Result<(), RuntimeError> {
2185 if value.is_empty()
2186 || value.len() > 128
2187 || !value
2188 .bytes()
2189 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':'))
2190 {
2191 return Err(RuntimeError::InvalidDefinition(format!(
2192 "{kind} must be a portable identifier"
2193 )));
2194 }
2195 Ok(())
2196}
2197
2198fn duration_ms(duration: Duration) -> u64 {
2199 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
2200}
2201
2202const fn is_terminal_status(status: InvocationStatus) -> bool {
2203 matches!(
2204 status,
2205 InvocationStatus::Completed | InvocationStatus::Failed | InvocationStatus::Cancelled
2206 )
2207}
2208
2209#[cfg(test)]
2210mod tests {
2211 use super::*;
2212
2213 struct TestProvider {
2214 fail: bool,
2215 delay: Duration,
2216 }
2217
2218 impl TestProvider {
2219 fn immediate() -> Self {
2220 Self {
2221 fail: false,
2222 delay: Duration::ZERO,
2223 }
2224 }
2225 }
2226
2227 impl AgentProvider for TestProvider {
2228 fn supports(&self, capability: &str) -> bool {
2229 matches!(capability, "chat" | "speech" | "transcription")
2230 }
2231
2232 fn invoke<'a>(
2233 &'a self,
2234 request: ProviderRequest,
2235 cancellation: CancellationToken,
2236 ) -> ProviderFuture<'a> {
2237 Box::pin(async move {
2238 if !self.delay.is_zero() {
2239 tokio::select! {
2240 _ = tokio::time::sleep(self.delay) => {}
2241 _ = cancellation.cancelled() => {
2242 return Err(RuntimeError::Cancelled);
2243 }
2244 }
2245 }
2246 if self.fail {
2247 return Err(RuntimeError::provider(
2248 request.agent.provider,
2249 "synthetic provider failure",
2250 ));
2251 }
2252 Ok(ProviderResponse {
2253 data: match request.data {
2254 InvocationData::Json(data) => InvocationData::Json(json!({
2255 "capability": request.capability,
2256 "input": data,
2257 })),
2258 InvocationData::Binary(bytes) => InvocationData::Binary(bytes),
2259 },
2260 metadata: json!({}),
2261 state: Some(json!({
2262 "lastEndpoint": request.endpoint,
2263 "previousRevision": request.snapshot.revision,
2264 })),
2265 })
2266 })
2267 }
2268 }
2269
2270 struct StreamingTestProvider;
2271
2272 impl AgentProvider for StreamingTestProvider {
2273 fn supports(&self, capability: &str) -> bool {
2274 capability == "chat"
2275 }
2276
2277 fn invoke<'a>(
2278 &'a self,
2279 request: ProviderRequest,
2280 cancellation: CancellationToken,
2281 ) -> ProviderFuture<'a> {
2282 self.invoke_with_events(request, cancellation, ProviderEventSink::discard())
2283 }
2284
2285 fn invoke_with_events<'a>(
2286 &'a self,
2287 _request: ProviderRequest,
2288 cancellation: CancellationToken,
2289 events: ProviderEventSink,
2290 ) -> ProviderFuture<'a> {
2291 Box::pin(async move {
2292 if cancellation.is_cancelled() {
2293 return Err(RuntimeError::Cancelled);
2294 }
2295 events.stage_started(ProviderStage::Tokenize, Value::Null);
2296 events.stage_completed(ProviderStage::Tokenize, 2, json!({ "inputTokens": 4 }));
2297 events.output_delta(InvocationData::Json(Value::String("Hello".to_string())));
2298 events.output_delta(InvocationData::Json(Value::String(", world".to_string())));
2299 Ok(ProviderResponse::json(json!({ "text": "Hello, world" })))
2300 })
2301 }
2302 }
2303
2304 struct ActiveSlowProvider;
2305
2306 impl AgentProvider for ActiveSlowProvider {
2307 fn supports(&self, capability: &str) -> bool {
2308 capability == "chat"
2309 }
2310
2311 fn invoke<'a>(
2312 &'a self,
2313 request: ProviderRequest,
2314 cancellation: CancellationToken,
2315 ) -> ProviderFuture<'a> {
2316 self.invoke_with_events(request, cancellation, ProviderEventSink::discard())
2317 }
2318
2319 fn invoke_with_events<'a>(
2320 &'a self,
2321 _request: ProviderRequest,
2322 cancellation: CancellationToken,
2323 events: ProviderEventSink,
2324 ) -> ProviderFuture<'a> {
2325 Box::pin(async move {
2326 for _ in 0..4 {
2327 tokio::select! {
2328 _ = tokio::time::sleep(Duration::from_millis(8)) => events.activity(),
2329 _ = cancellation.cancelled() => return Err(RuntimeError::Cancelled),
2330 }
2331 }
2332 Ok(ProviderResponse::json(json!({ "ok": true })))
2333 })
2334 }
2335 }
2336
2337 fn configured_runtime(provider: Arc<dyn AgentProvider>) -> VifuRuntime {
2338 let runtime = VifuRuntime::new("test-project").expect("runtime should start");
2339 runtime
2340 .register_provider("test-provider", provider)
2341 .expect("provider should register");
2342 runtime
2343 .register_agent(AgentDefinition {
2344 id: "guide".to_string(),
2345 name: "Guide".to_string(),
2346 provider: "test-provider".to_string(),
2347 capabilities: vec![
2348 "chat".to_string(),
2349 "speech".to_string(),
2350 "transcription".to_string(),
2351 ],
2352 metadata: json!({ "public": true }),
2353 })
2354 .expect("agent should register");
2355 for capability in ["chat", "speech", "transcription"] {
2356 runtime
2357 .register_endpoint(EndpointDefinition {
2358 name: capability.to_string(),
2359 agent: "guide".to_string(),
2360 capability: capability.to_string(),
2361 timeout_ms: 500,
2362 })
2363 .expect("endpoint should register");
2364 }
2365 runtime
2366 }
2367
2368 #[tokio::test(flavor = "current_thread")]
2369 async fn embedded_runtime_invokes_chat_speech_and_transcription_without_a_server() {
2370 let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2371
2372 for capability in ["chat", "speech", "transcription"] {
2373 let output = runtime
2374 .invoke(InvocationInput::json(
2375 capability,
2376 json!({ "message": capability }),
2377 ))
2378 .await
2379 .expect("endpoint should invoke");
2380 assert_eq!(output.capability, capability);
2381 }
2382 }
2383
2384 #[tokio::test(flavor = "current_thread")]
2385 async fn runtime_sessions_keep_independent_durable_state() {
2386 let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2387 let first = runtime
2388 .session("player-one")
2389 .expect("first session should open");
2390 let second = runtime
2391 .session("player-two")
2392 .expect("second session should open");
2393
2394 let first_output = first
2395 .invoke(InvocationInput::json("chat", json!({ "text": "one" })))
2396 .await
2397 .expect("first session should invoke");
2398 let second_output = second
2399 .invoke(InvocationInput::json("chat", json!({ "text": "two" })))
2400 .await
2401 .expect("second session should invoke");
2402
2403 assert_eq!(first_output.snapshot.revision, 1);
2404 assert_eq!(second_output.snapshot.revision, 1);
2405 }
2406
2407 #[tokio::test(flavor = "current_thread")]
2408 async fn concurrent_calls_serialize_state_updates_for_one_session() {
2409 let runtime = configured_runtime(Arc::new(TestProvider {
2410 fail: false,
2411 delay: Duration::from_millis(5),
2412 }));
2413 let first = runtime.invoke(
2414 InvocationInput::json("chat", json!({ "text": "one" })).with_session("shared-session"),
2415 );
2416 let second = runtime.invoke(
2417 InvocationInput::json("chat", json!({ "text": "two" })).with_session("shared-session"),
2418 );
2419
2420 let (first, second) = tokio::join!(first, second);
2421 let mut revisions = [
2422 first
2423 .expect("first invocation should complete")
2424 .snapshot
2425 .revision,
2426 second
2427 .expect("second invocation should complete")
2428 .snapshot
2429 .revision,
2430 ];
2431 revisions.sort_unstable();
2432 assert_eq!(revisions, [1, 2]);
2433 }
2434
2435 #[tokio::test(flavor = "current_thread")]
2436 async fn runtime_round_trips_binary_provider_results() {
2437 let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2438 let output = runtime
2439 .invoke(InvocationInput {
2440 endpoint: "speech".to_string(),
2441 session_id: "audio-session".to_string(),
2442 data: InvocationData::Binary(vec![1, 2, 3, 4]),
2443 metadata: json!({}),
2444 })
2445 .await
2446 .expect("binary invocation should complete");
2447
2448 assert_eq!(output.data, InvocationData::Binary(vec![1, 2, 3, 4]));
2449 }
2450
2451 #[tokio::test(flavor = "current_thread")]
2452 async fn runtime_times_out_slow_providers() {
2453 let runtime = VifuRuntime::new("timeout-project").expect("runtime should start");
2454 runtime
2455 .register_provider(
2456 "slow",
2457 Arc::new(TestProvider {
2458 fail: false,
2459 delay: Duration::from_millis(100),
2460 }),
2461 )
2462 .expect("provider should register");
2463 runtime
2464 .register_agent(AgentDefinition {
2465 id: "slow-agent".to_string(),
2466 name: "Slow agent".to_string(),
2467 provider: "slow".to_string(),
2468 capabilities: vec!["chat".to_string()],
2469 metadata: json!({}),
2470 })
2471 .expect("agent should register");
2472 runtime
2473 .register_endpoint(EndpointDefinition {
2474 name: "slow-chat".to_string(),
2475 agent: "slow-agent".to_string(),
2476 capability: "chat".to_string(),
2477 timeout_ms: 10,
2478 })
2479 .expect("endpoint should register");
2480
2481 let error = runtime
2482 .invoke(InvocationInput::json("slow-chat", json!({})))
2483 .await
2484 .expect_err("slow invocation should time out");
2485 assert!(matches!(error, RuntimeError::Timeout(10)));
2486 }
2487
2488 #[tokio::test(flavor = "current_thread")]
2489 async fn provider_activity_resets_the_runtime_idle_timeout() {
2490 let runtime = VifuRuntime::new("active-project").expect("runtime should start");
2491 runtime
2492 .register_provider("active", Arc::new(ActiveSlowProvider))
2493 .expect("provider should register");
2494 runtime
2495 .register_agent(AgentDefinition {
2496 id: "active-agent".to_string(),
2497 name: "Active agent".to_string(),
2498 provider: "active".to_string(),
2499 capabilities: vec!["chat".to_string()],
2500 metadata: json!({}),
2501 })
2502 .expect("agent should register");
2503 runtime
2504 .register_endpoint(EndpointDefinition {
2505 name: "active-chat".to_string(),
2506 agent: "active-agent".to_string(),
2507 capability: "chat".to_string(),
2508 timeout_ms: 10,
2509 })
2510 .expect("endpoint should register");
2511
2512 let output = runtime
2513 .invoke(InvocationInput::json("active-chat", json!({})))
2514 .await
2515 .expect("ongoing provider activity should renew the idle timeout");
2516 assert_eq!(output.data, InvocationData::Json(json!({ "ok": true })));
2517 }
2518
2519 #[test]
2520 fn game_loop_api_starts_polls_and_cancels_invocations() {
2521 let runtime = configured_runtime(Arc::new(TestProvider {
2522 fail: false,
2523 delay: Duration::from_secs(5),
2524 }));
2525 let handle = runtime
2526 .start_invoke(InvocationInput::json("chat", json!({})))
2527 .expect("invocation should start");
2528 let running_deadline = Instant::now() + Duration::from_secs(1);
2529 loop {
2530 let poll = runtime
2531 .poll_invocation(&handle)
2532 .expect("invocation should remain pollable");
2533 if poll.status == InvocationStatus::Running {
2534 break;
2535 }
2536 assert!(
2537 Instant::now() < running_deadline,
2538 "invocation did not start"
2539 );
2540 std::thread::sleep(Duration::from_millis(5));
2541 }
2542 runtime
2543 .cancel_invocation(&handle)
2544 .expect("invocation should cancel");
2545
2546 let deadline = Instant::now() + Duration::from_secs(1);
2547 loop {
2548 let poll = runtime
2549 .poll_invocation(&handle)
2550 .expect("invocation should remain pollable");
2551 if poll.status == InvocationStatus::Cancelled {
2552 break;
2553 }
2554 assert!(
2555 Instant::now() < deadline,
2556 "cancelled provider did not observe cancellation"
2557 );
2558 std::thread::sleep(Duration::from_millis(5));
2559 }
2560 }
2561
2562 #[test]
2563 fn game_loop_poll_returns_the_same_provider_result_shape_as_async_invoke() {
2564 let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2565 let handle = runtime
2566 .start_invoke(
2567 InvocationInput::json("chat", json!({ "text": "hello" }))
2568 .with_session("poll-session"),
2569 )
2570 .expect("invocation should start");
2571 let deadline = Instant::now() + Duration::from_secs(1);
2572 let output = loop {
2573 let poll = runtime
2574 .poll_invocation(&handle)
2575 .expect("invocation should remain pollable");
2576 if let Some(output) = poll.output {
2577 break output;
2578 }
2579 assert!(
2580 !matches!(
2581 poll.status,
2582 InvocationStatus::Failed | InvocationStatus::Cancelled
2583 ),
2584 "invocation unexpectedly failed: {poll:?}"
2585 );
2586 assert!(Instant::now() < deadline, "invocation did not complete");
2587 std::thread::sleep(Duration::from_millis(5));
2588 };
2589
2590 assert_eq!(
2591 output.data,
2592 InvocationData::Json(json!({
2593 "capability": "chat",
2594 "input": { "text": "hello" },
2595 }))
2596 );
2597 }
2598
2599 #[test]
2600 fn game_loop_v1_event_stream_ignores_provider_stages() {
2601 let runtime = configured_runtime(Arc::new(StreamingTestProvider));
2602 let handle = runtime
2603 .start_invoke(InvocationInput::json("chat", json!({})))
2604 .expect("invocation should start");
2605 let deadline = Instant::now() + Duration::from_secs(1);
2606 loop {
2607 let poll = runtime
2608 .poll_invocation(&handle)
2609 .expect("invocation should remain pollable");
2610 if poll.status == InvocationStatus::Completed {
2611 break;
2612 }
2613 assert!(Instant::now() < deadline, "invocation did not complete");
2614 std::thread::sleep(Duration::from_millis(5));
2615 }
2616
2617 let events = runtime
2618 .drain_invocation_events(&handle)
2619 .expect("events should be available");
2620 assert_eq!(
2621 events.iter().map(|event| event.kind).collect::<Vec<_>>(),
2622 vec![
2623 InvocationEventKind::Started,
2624 InvocationEventKind::OutputDelta,
2625 InvocationEventKind::Completed,
2626 ]
2627 );
2628 assert_eq!(
2629 events[1].data,
2630 Some(InvocationData::Json(Value::String(
2631 "Hello, world".to_string()
2632 )))
2633 );
2634 }
2635
2636 #[test]
2637 fn invocation_registry_ignores_output_after_terminal_event() {
2638 let handle = InvocationHandle("late-output".to_string());
2639 let mut registry = InvocationRegistry::default();
2640 registry
2641 .insert(handle.clone(), CancellationToken::default())
2642 .expect("invocation should be registered");
2643 registry.update(&handle, InvocationStatus::Running, None, None);
2644 registry.update(
2645 &handle,
2646 InvocationStatus::Failed,
2647 None,
2648 Some("provider failed".to_string()),
2649 );
2650
2651 registry.push_provider_event(
2652 &handle,
2653 ProviderEvent::OutputDelta {
2654 data: InvocationData::Json(Value::String("too late".to_string())),
2655 },
2656 );
2657
2658 let events = registry
2659 .drain_events(&handle)
2660 .expect("events should remain available");
2661 assert_eq!(
2662 events.iter().map(|event| event.kind).collect::<Vec<_>>(),
2663 vec![InvocationEventKind::Started, InvocationEventKind::Failed]
2664 );
2665 }
2666
2667 #[test]
2668 fn taking_a_terminal_invocation_releases_its_result() {
2669 let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2670 let handle = runtime
2671 .start_invoke(InvocationInput::json("chat", json!({})))
2672 .expect("invocation should start");
2673 let deadline = Instant::now() + Duration::from_secs(1);
2674 loop {
2675 let poll = runtime
2676 .take_invocation(&handle)
2677 .expect("invocation should remain available until terminal");
2678 if is_terminal_status(poll.status) {
2679 break;
2680 }
2681 assert!(Instant::now() < deadline, "invocation did not complete");
2682 std::thread::sleep(Duration::from_millis(5));
2683 }
2684
2685 assert!(matches!(
2686 runtime.poll_invocation(&handle),
2687 Err(RuntimeError::InvocationNotFound(_))
2688 ));
2689 }
2690
2691 #[test]
2692 fn game_loop_api_applies_backpressure_to_excess_invocations() {
2693 let runtime = configured_runtime(Arc::new(TestProvider {
2694 fail: false,
2695 delay: Duration::from_secs(5),
2696 }));
2697 let handles = (0..MAX_IN_FLIGHT_INVOCATIONS)
2698 .map(|index| {
2699 runtime
2700 .start_invoke(
2701 InvocationInput::json("chat", json!({}))
2702 .with_session(format!("session-{index}")),
2703 )
2704 .expect("invocation within the bound should start")
2705 })
2706 .collect::<Vec<_>>();
2707
2708 let error = runtime
2709 .start_invoke(
2710 InvocationInput::json("chat", json!({})).with_session("one-session-too-many"),
2711 )
2712 .expect_err("invocations above the bound should be rejected");
2713 assert!(matches!(error, RuntimeError::Backpressure(_)));
2714
2715 for handle in handles {
2716 runtime
2717 .cancel_invocation(&handle)
2718 .expect("test invocation should cancel");
2719 }
2720 }
2721
2722 #[test]
2723 fn terminal_invocation_history_is_bounded() {
2724 let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2725 let first = runtime
2726 .start_invoke(
2727 InvocationInput::json("chat", json!({})).with_session("retained-session-0"),
2728 )
2729 .expect("first invocation should start");
2730 let mut last = first.clone();
2731 for index in 0..=MAX_RETAINED_INVOCATIONS {
2732 let handle = if index == 0 {
2733 first.clone()
2734 } else {
2735 runtime
2736 .start_invoke(
2737 InvocationInput::json("chat", json!({}))
2738 .with_session(format!("retained-session-{index}")),
2739 )
2740 .expect("invocation should start")
2741 };
2742 let deadline = Instant::now() + Duration::from_secs(1);
2743 loop {
2744 let poll = runtime
2745 .poll_invocation(&handle)
2746 .expect("latest invocation should remain available");
2747 if is_terminal_status(poll.status) {
2748 break;
2749 }
2750 assert!(Instant::now() < deadline, "invocation did not complete");
2751 std::thread::sleep(Duration::from_millis(2));
2752 }
2753 last = handle;
2754 }
2755
2756 assert!(matches!(
2757 runtime.poll_invocation(&first),
2758 Err(RuntimeError::InvocationNotFound(_))
2759 ));
2760 assert!(runtime.poll_invocation(&last).is_ok());
2761 }
2762
2763 #[tokio::test(flavor = "current_thread")]
2764 async fn runtime_executes_agent_effects_and_returns_custom_effects_to_the_host() {
2765 let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2766 let execution = runtime
2767 .execute_effects(vec![
2768 EffectRequest {
2769 id: "agent-effect".to_string(),
2770 kind: "agent.invoke".to_string(),
2771 payload: serde_json::to_value(InvocationInput::json(
2772 "chat",
2773 json!({ "text": "hello" }),
2774 ))
2775 .unwrap(),
2776 },
2777 EffectRequest {
2778 id: "host-effect".to_string(),
2779 kind: "game.play_animation".to_string(),
2780 payload: json!({ "name": "wave" }),
2781 },
2782 ])
2783 .await
2784 .expect("effects should execute");
2785
2786 assert_eq!(execution.results.len(), 1);
2787 assert_eq!(execution.unhandled[0].kind, "game.play_animation");
2788 }
2789
2790 #[tokio::test(flavor = "current_thread")]
2791 async fn runtime_rejects_effect_batches_above_the_bound() {
2792 let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2793 let effects = (0..3)
2794 .map(|index| EffectRequest {
2795 id: format!("effect-{index}"),
2796 kind: "host.effect".to_string(),
2797 payload: json!({}),
2798 })
2799 .collect();
2800
2801 let error = runtime
2802 .execute_effects_with_limit(effects, 2)
2803 .await
2804 .expect_err("oversized effect batch should fail");
2805 assert!(matches!(error, RuntimeError::EffectLimitExceeded(2)));
2806 }
2807
2808 #[tokio::test(flavor = "current_thread")]
2809 async fn snapshots_restore_session_state_without_runtime_definitions_or_secrets() {
2810 let secret = "synthetic-secret-must-not-leak";
2811 let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2812 runtime
2813 .invoke(
2814 InvocationInput::json("chat", json!({ "text": "hello" }))
2815 .with_session("saved-session"),
2816 )
2817 .await
2818 .expect("invocation should create state");
2819 let bytes = runtime.export_snapshot().expect("snapshot should export");
2820 assert!(!String::from_utf8_lossy(&bytes).contains(secret));
2821
2822 let restored = configured_runtime(Arc::new(TestProvider::immediate()));
2823 restored
2824 .restore_snapshot(&bytes)
2825 .expect("snapshot should restore");
2826 let output = restored
2827 .invoke(
2828 InvocationInput::json("chat", json!({ "text": "again" }))
2829 .with_session("saved-session"),
2830 )
2831 .await
2832 .expect("restored session should invoke");
2833
2834 assert_eq!(output.snapshot.revision, 2);
2835 }
2836
2837 #[test]
2838 fn debug_output_redacts_payloads_provider_errors_and_snapshots() {
2839 let secret = "synthetic-secret-must-not-leak";
2840 let input = InvocationInput::json("chat", json!({ "secret": secret }));
2841 let error = RuntimeError::provider("test-provider", secret);
2842 let runtime = configured_runtime(Arc::new(TestProvider::immediate()));
2843
2844 assert!(!format!("{input:?}").contains(secret));
2845 assert!(!format!("{error:?}").contains(secret));
2846 assert!(!format!("{runtime:?}").contains(secret));
2847 }
2848}