Skip to main content

cookie_agent_protocol/
runtime.rs

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 {}