1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use crate::*;
5
6#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
7#[serde(rename_all = "snake_case")]
8pub enum CatalogSource {
9 Network,
10 Cache,
11 Bootstrap,
12}
13
14#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
15#[serde(deny_unknown_fields)]
16pub struct CatalogSafeErrorMeta {
17 pub code: SafeCode,
18 pub message: SafeErrorMessage,
19 pub time: jiff::Timestamp,
20}
21
22#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
23#[serde(deny_unknown_fields)]
24pub struct CatalogRuntimeState {
25 pub stale: bool,
26 pub provider_quarantine_count: u32,
27 pub model_quarantine_count: u32,
28 pub quarantine_digest: Sha256Digest,
29 #[serde(deserialize_with = "crate::deserialize_required_option")]
30 #[schemars(with = "crate::NullableSchema<CatalogSafeErrorMeta>", required)]
31 pub last_error: Option<CatalogSafeErrorMeta>,
32}
33
34#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
35#[serde(deny_unknown_fields)]
36pub struct RuntimeSnapshotV1 {
37 pub snapshot_schema_version: RuntimeSnapshotSchemaVersion,
38 pub recipe_registry_revision: RecipeRegistryRevision,
39 pub catalog_revision: CatalogRevision,
40 pub catalog_source: CatalogSource,
41 pub catalog_state: CatalogRuntimeState,
42 pub provider_state_revision: ProviderStateRevision,
43 pub provider_store_generation: ProviderStoreGeneration,
44 pub model_revision: ModelRevision,
45 pub agent_revision: AgentRevision,
46 pub runtime_revision: RuntimeRevision,
47 #[schemars(length(max = 4096))]
48 pub providers: Vec<ProviderDescriptor>,
49 #[schemars(length(max = 4096))]
50 pub models: Vec<AvailableModelDescriptor>,
51 #[schemars(length(max = 4096))]
52 pub agents: Vec<AgentDescriptor>,
53}
54
55impl RuntimeSnapshotV1 {
56 pub fn validate(&self) -> Result<(), &'static str> {
57 if self.catalog_source == CatalogSource::Bootstrap && !self.catalog_state.stale {
58 return Err("bootstrap catalog state must be stale");
59 }
60 if self.catalog_source == CatalogSource::Network && self.catalog_state.stale {
61 return Err("network catalog state cannot be stale");
62 }
63 if self.providers.len() > 4096
64 || self
65 .providers
66 .windows(2)
67 .any(|pair| pair[0].id >= pair[1].id)
68 {
69 return Err("providers must be strictly sorted and unique with at most 4096 entries");
70 }
71 if self.models.len() > 4096
72 || self
73 .models
74 .windows(2)
75 .any(|pair| pair[0].key >= pair[1].key)
76 {
77 return Err("models must be strictly sorted and unique with at most 4096 entries");
78 }
79 if self.agents.len() > 4096
80 || self
81 .agents
82 .windows(2)
83 .any(|pair| (&pair[0].preset, &pair[0].id) >= (&pair[1].preset, &pair[1].id))
84 || self.agents.iter().any(|agent| agent.validate().is_err())
85 {
86 return Err(
87 "agents must be strictly sorted and unique by preset and ID with at most 4096 entries",
88 );
89 }
90 Ok(())
91 }
92}
93
94impl<'de> Deserialize<'de> for RuntimeSnapshotV1 {
95 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
96 where
97 D: serde::Deserializer<'de>,
98 {
99 #[derive(Deserialize)]
100 #[serde(deny_unknown_fields)]
101 struct Wire {
102 snapshot_schema_version: RuntimeSnapshotSchemaVersion,
103 recipe_registry_revision: RecipeRegistryRevision,
104 catalog_revision: CatalogRevision,
105 catalog_source: CatalogSource,
106 catalog_state: CatalogRuntimeState,
107 provider_state_revision: ProviderStateRevision,
108 provider_store_generation: ProviderStoreGeneration,
109 model_revision: ModelRevision,
110 agent_revision: AgentRevision,
111 runtime_revision: RuntimeRevision,
112 providers: Vec<ProviderDescriptor>,
113 models: Vec<AvailableModelDescriptor>,
114 agents: Vec<AgentDescriptor>,
115 }
116 let wire = Wire::deserialize(deserializer)?;
117 let value = Self {
118 snapshot_schema_version: wire.snapshot_schema_version,
119 recipe_registry_revision: wire.recipe_registry_revision,
120 catalog_revision: wire.catalog_revision,
121 catalog_source: wire.catalog_source,
122 catalog_state: wire.catalog_state,
123 provider_state_revision: wire.provider_state_revision,
124 provider_store_generation: wire.provider_store_generation,
125 model_revision: wire.model_revision,
126 agent_revision: wire.agent_revision,
127 runtime_revision: wire.runtime_revision,
128 providers: wire.providers,
129 models: wire.models,
130 agents: wire.agents,
131 };
132 value.validate().map_err(serde::de::Error::custom)?;
133 Ok(value)
134 }
135}
136
137#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
138#[serde(deny_unknown_fields)]
139pub struct RuntimeSnapshotResult {
140 pub snapshot: RuntimeSnapshotV1,
141}
142
143#[derive(
144 Clone, Copy, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
145)]
146#[serde(rename_all = "snake_case")]
147pub enum RuntimeChangeReason {
148 Startup,
149 CatalogRefreshed,
150 CatalogFallback,
151 ConfigReloaded,
152 ProviderConnected,
153 ProviderDisconnected,
154 ProviderStoreChanged,
155 ProviderStoreReloaded,
156 AgentReloaded,
157}
158
159#[derive(Clone, Debug, Eq, JsonSchema, PartialEq, Serialize)]
160#[serde(deny_unknown_fields)]
161pub struct RuntimeChangedNotification {
162 #[serde(deserialize_with = "crate::deserialize_required_option")]
163 #[schemars(with = "crate::NullableSchema<RuntimeRevision>", required)]
164 pub previous_revision: Option<RuntimeRevision>,
165 pub snapshot: RuntimeSnapshotV1,
166 #[schemars(length(min = 1, max = 9))]
167 pub reasons: Vec<RuntimeChangeReason>,
168}
169impl<'de> Deserialize<'de> for RuntimeChangedNotification {
170 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
171 where
172 D: serde::Deserializer<'de>,
173 {
174 #[derive(Deserialize)]
175 #[serde(deny_unknown_fields)]
176 struct Wire {
177 #[serde(deserialize_with = "crate::deserialize_required_option")]
178 previous_revision: Option<RuntimeRevision>,
179 snapshot: RuntimeSnapshotV1,
180 reasons: Vec<RuntimeChangeReason>,
181 }
182 let wire = Wire::deserialize(deserializer)?;
183 if wire.reasons.is_empty()
184 || wire.reasons.len() > 9
185 || wire.reasons.windows(2).any(|pair| pair[0] >= pair[1])
186 {
187 return Err(serde::de::Error::custom(
188 "runtime change reasons must be a nonempty strictly sorted unique set",
189 ));
190 }
191 Ok(Self {
192 previous_revision: wire.previous_revision,
193 snapshot: wire.snapshot,
194 reasons: wire.reasons,
195 })
196 }
197}
198
199pub const RUNTIME_SNAPSHOT_GET_METHOD: &str = "runtime.snapshot.get";
200pub const RUNTIME_CHANGED_METHOD: &str = "runtime.changed";
201pub const PROVIDER_CONNECT_METHOD: &str = "provider.connect";
202pub const PROVIDER_DISCONNECT_METHOD: &str = "provider.disconnect";
203
204#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
205#[serde(deny_unknown_fields)]
206pub struct RuntimeSnapshotGetParams {}