Skip to main content

agents_kit/
agent_storage.rs

1use std::collections::BTreeSet;
2use std::sync::{Arc, Mutex};
3
4use storage_kit::{DatabaseBackend, StateStoreBackend, StateTransactionBackend};
5
6use crate::{AgentId, AgentInfo, KitError, KnowledgeOwnerId, ManagedKnowledge};
7
8const AGENT_NAMESPACE: &str = "agents_kit_agents_v1";
9const AGENT_RECORD_VERSION: u32 = 1;
10const MAX_AGENT_RECORDS: usize = 65_536;
11const MAX_AGENT_INVENTORY_PAGES: usize = 1_024;
12const KNOWLEDGE_FORGET_NAMESPACE: &str = "agents_kit_knowledge_forget_v1";
13const KNOWLEDGE_FORGET_RECORD_VERSION: u32 = 1;
14const MAX_KNOWLEDGE_FORGET_RECORDS: usize = 65_536;
15const MAX_KNOWLEDGE_FORGET_PAGES: usize = 1_024;
16
17/// Coordinates durable agent identities through a caller-supplied state store.
18///
19/// The concrete `S` is retained directly, and its associated `S::Transaction`
20/// remains the only transaction type used for agent control-plane work. Clones
21/// share the same store access and live-agent ownership domain without requiring
22/// `S: Clone`.
23///
24/// # Examples
25///
26/// ```
27/// use agents_kit::AgentStorage;
28/// use storage_kit::{StateStore, state::config::{StateStoreConfig, LanceStateStoreConfig}};
29/// let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
30/// runtime.block_on(async {
31///     let store = StateStore::connect(StateStoreConfig::Lance(LanceStateStoreConfig::memory())).await?;
32///     let storage = AgentStorage::new(store);
33///     assert!(storage.list_agents().await?.is_empty());
34///     let id = "agent_00000000000000000000000000000000".parse()?;
35///     let error = storage.agent_info(id).await.unwrap_err();
36///     assert_eq!(error.kind(), agents_kit::error::ErrorKind::NotFound);
37///     drop(storage);
38///     Ok::<(), Box<dyn std::error::Error>>(())
39/// })?;
40/// # Ok::<(), Box<dyn std::error::Error>>(())
41/// ```
42pub struct AgentStorage<S: StateStoreBackend = storage_kit::StateStore> {
43    inner: Arc<AgentStorageInner<S>>,
44}
45
46struct AgentStorageInner<S: StateStoreBackend> {
47    store: S,
48    live_agents: Arc<Mutex<BTreeSet<AgentId>>>,
49}
50
51pub(crate) struct AgentStorageClaim {
52    live_agents: Arc<Mutex<BTreeSet<AgentId>>>,
53    id: Option<AgentId>,
54    release_on_drop: bool,
55}
56
57pub(crate) enum AgentStorageCreationError {
58    TransactionConflict,
59    Failure(KitError),
60}
61
62pub(crate) enum AgentStorageKnowledgeError {
63    TransactionConflict(crate::error::ErrorPhase),
64    Failure(KitError),
65}
66
67#[derive(serde::Deserialize, serde::Serialize)]
68#[serde(deny_unknown_fields)]
69struct AgentRecord {
70    version: u32,
71    id: AgentId,
72    display_name: Option<String>,
73}
74
75#[derive(serde::Deserialize, serde::Serialize)]
76#[serde(deny_unknown_fields)]
77struct KnowledgeForgetRecord {
78    version: u32,
79    owner: KnowledgeOwnerId,
80}
81
82impl<S: StateStoreBackend> Clone for AgentStorage<S> {
83    fn clone(&self) -> Self {
84        Self {
85            inner: self.inner.clone(),
86        }
87    }
88}
89
90impl<S: StateStoreBackend> AgentStorage<S> {
91    /// Creates the agent control-plane boundary from an already-built state store.
92    ///
93    /// Construction retains the supplied store; it neither creates an Agent nor requires `S: Clone`.
94    ///
95    /// See [`AgentStorage`] for a complete example.
96    pub fn new(store: S) -> Self {
97        Self {
98            inner: Arc::new(AgentStorageInner {
99                store,
100                live_agents: Arc::new(Mutex::new(BTreeSet::new())),
101            }),
102        }
103    }
104
105    /// Lists every durable agent identity visible through this storage domain.
106    ///
107    /// The result is ordered by durable Agent ID. This reads storage, not native runtime inventory.
108    ///
109    /// # Errors
110    ///
111    /// Returns a typed storage failure or `InvalidState` for malformed records or an inventory that exceeds the bounded traversal.
112    ///
113    /// See [`AgentStorage`] for a complete example.
114    pub async fn list_agents(&self) -> Result<Vec<AgentInfo>, KitError> {
115        async {
116            let mut records = Vec::new();
117            let mut cursor = None;
118            let mut pages = 0_usize;
119            loop {
120                pages += 1;
121                if pages > MAX_AGENT_INVENTORY_PAGES {
122                    return Err(KitError::invalid_state(
123                        "agent inventory exceeds its private bound",
124                    ));
125                }
126                let mut request =
127                    storage_kit::state::record::StateListRequest::new(AGENT_NAMESPACE)
128                        .map_err(|error| {
129                            KitError::from_state_store(
130                                error,
131                                "agent inventory request could not be created",
132                            )
133                            .in_phase(crate::error::ErrorPhase::StateRead)
134                        })?
135                        .with_limit(storage_kit::state::record::StateListRequest::MAX_LIMIT)
136                        .map_err(|error| {
137                            KitError::from_state_store(error, "agent inventory limit was rejected")
138                                .in_phase(crate::error::ErrorPhase::StateRead)
139                        })?;
140                if let Some(next) = cursor.take() {
141                    request = request.start_after(next).map_err(|_| {
142                        KitError::invalid_state("agent inventory cursor is invalid")
143                    })?;
144                }
145                let page = self.store().list(request).await.map_err(|error| {
146                    KitError::from_state_store(error, "agent inventory could not be loaded")
147                        .in_phase(crate::error::ErrorPhase::StateRead)
148                })?;
149                for snapshot in page.records {
150                    if records.len() == MAX_AGENT_RECORDS {
151                        return Err(KitError::invalid_state(
152                            "agent inventory exceeds its private bound",
153                        ));
154                    }
155                    records.push(AgentRecord::decode(snapshot)?);
156                }
157                let Some(next) = page.next_start_after else {
158                    break;
159                };
160                cursor = Some(next);
161            }
162            records.sort_by(|left, right| left.id.cmp(&right.id));
163            records.into_iter().map(AgentRecord::into_info).collect()
164        }
165        .await
166        .map_err(|error: KitError| {
167            error
168                .at_operation(crate::error::Operation::ListAgents)
169                .in_phase(crate::error::ErrorPhase::StateRead)
170        })
171    }
172
173    /// Loads descriptive information for one durable agent identity.
174    ///
175    /// This reads the durable description without opening a runtime.
176    ///
177    /// # Errors
178    ///
179    /// Returns `NotFound` for an absent identity, `InvalidState` for malformed records, or a typed storage access failure.
180    ///
181    /// See [`AgentStorage`] for a complete example.
182    pub async fn agent_info(&self, id: AgentId) -> Result<AgentInfo, KitError> {
183        async { self.load_record(&id).await?.into_info() }
184            .await
185            .map_err(|error: KitError| {
186                error
187                    .at_operation(crate::error::Operation::AgentInfo)
188                    .in_phase(crate::error::ErrorPhase::StateRead)
189            })
190    }
191
192    pub(crate) async fn create_agent_in_transaction(
193        &self,
194        transaction: &mut S::Transaction,
195        info: &AgentInfo,
196    ) -> Result<(), AgentStorageCreationError> {
197        let record = AgentRecord::from_info(info);
198        let value = serde_json::to_value(&record).map_err(|_| {
199            AgentStorageCreationError::Failure(KitError::invalid_state(
200                "agent identity could not be encoded",
201            ))
202        })?;
203        let key = AgentRecord::key(info.id()).map_err(AgentStorageCreationError::Failure)?;
204        if transaction
205            .load(key.clone())
206            .await
207            .map_err(|error| match error {
208                storage_kit::StateStoreError::TransactionConflict { .. } => {
209                    AgentStorageCreationError::TransactionConflict
210                }
211                error => AgentStorageCreationError::Failure(
212                    KitError::from_state_store(error, "agent identity could not be checked")
213                        .in_phase(crate::error::ErrorPhase::StateRead),
214                ),
215            })?
216            .is_some()
217        {
218            return Err(AgentStorageCreationError::Failure(KitError::conflict(
219                "agent identity already exists",
220            )));
221        }
222        transaction
223            .put(storage_kit::state::record::StateRecord::new(key, value))
224            .await
225            .map_err(|error| match error {
226                storage_kit::StateStoreError::TransactionConflict { .. } => {
227                    AgentStorageCreationError::TransactionConflict
228                }
229                error => AgentStorageCreationError::Failure(
230                    KitError::from_state_store(error, "agent identity could not be stored")
231                        .in_phase(crate::error::ErrorPhase::StateWrite),
232                ),
233            })?;
234        Ok(())
235    }
236
237    pub(crate) fn store(&self) -> &S {
238        &self.inner.store
239    }
240
241    pub(crate) async fn stage_knowledge_forget(
242        &self,
243        transaction: &mut S::Transaction,
244        owner: &KnowledgeOwnerId,
245    ) -> Result<(), AgentStorageKnowledgeError> {
246        let record = KnowledgeForgetRecord::new(owner.clone())
247            .map_err(AgentStorageKnowledgeError::Failure)?;
248        let key = record.key().map_err(AgentStorageKnowledgeError::Failure)?;
249        let current = transaction
250            .load(key.clone())
251            .await
252            .map_err(|error| match error {
253                storage_kit::StateStoreError::TransactionConflict { .. } => {
254                    AgentStorageKnowledgeError::TransactionConflict(
255                        crate::error::ErrorPhase::StateRead,
256                    )
257                }
258                error => AgentStorageKnowledgeError::Failure(
259                    KitError::from_state_store(
260                        error,
261                        "knowledge forget obligation could not be checked",
262                    )
263                    .in_phase(crate::error::ErrorPhase::StateRead),
264                ),
265            })?;
266        if let Some(current) = current {
267            let current = KnowledgeForgetRecord::decode(current)
268                .map_err(AgentStorageKnowledgeError::Failure)?;
269            return if current.owner == record.owner {
270                Ok(())
271            } else {
272                Err(AgentStorageKnowledgeError::Failure(
273                    KitError::invalid_state(
274                        "knowledge forget obligation contradicts its durable key",
275                    ),
276                ))
277            };
278        }
279        let value = record
280            .encode()
281            .map_err(AgentStorageKnowledgeError::Failure)?;
282        transaction
283            .put(storage_kit::state::record::StateRecord::new(key, value))
284            .await
285            .map_err(|error| match error {
286                storage_kit::StateStoreError::TransactionConflict { .. } => {
287                    AgentStorageKnowledgeError::TransactionConflict(
288                        crate::error::ErrorPhase::StateWrite,
289                    )
290                }
291                error => AgentStorageKnowledgeError::Failure(
292                    KitError::from_state_store(
293                        error,
294                        "knowledge forget obligation could not be stored",
295                    )
296                    .in_phase(crate::error::ErrorPhase::StateWrite),
297                ),
298            })
299    }
300
301    pub(crate) async fn settle_knowledge_forget<D: DatabaseBackend>(
302        &self,
303        knowledge: Option<&ManagedKnowledge<S, D>>,
304        owner: &KnowledgeOwnerId,
305        deadline: tokio::time::Instant,
306        observation_deadline: Option<tokio::time::Instant>,
307    ) -> Result<(), KitError> {
308        let settlement = async {
309            let key = KnowledgeForgetRecord::key_for(owner)?;
310            if tokio::time::Instant::now() >= deadline {
311                return Err(KitError::timeout(
312                    "knowledge reconciliation deadline elapsed",
313                ));
314            }
315            let Some(snapshot) = self.store().load(key.clone()).await.map_err(|error| {
316                KitError::from_state_store(error, "knowledge forget obligation could not be loaded")
317                    .in_phase(crate::error::ErrorPhase::StateRead)
318            })?
319            else {
320                return Ok(());
321            };
322            let record = KnowledgeForgetRecord::decode(snapshot)?;
323            if &record.owner != owner {
324                return Err(KitError::invalid_state(
325                    "knowledge forget obligation contradicts its owner",
326                ));
327            }
328            let knowledge = knowledge.ok_or_else(|| {
329                KitError::conflict(
330                    "durable knowledge cleanup requires the supplied ManagedKnowledge",
331                )
332            })?;
333            if tokio::time::Instant::now() >= deadline {
334                return Err(KitError::timeout(
335                    "knowledge reconciliation deadline elapsed",
336                ));
337            }
338            knowledge.forget(owner.clone()).await?;
339
340            if tokio::time::Instant::now() >= deadline {
341                return Err(KitError::timeout(
342                    "knowledge reconciliation deadline elapsed",
343                ));
344            }
345            let mut transaction = self.store().begin_transaction().await.map_err(|error| {
346                KitError::from_state_store(
347                    error,
348                    "knowledge forget obligation could not be settled",
349                )
350                .in_phase(crate::error::ErrorPhase::StateWrite)
351            })?;
352            let current = transaction.load(key.clone()).await.map_err(|error| {
353                KitError::from_state_store(
354                    error,
355                    "knowledge forget obligation could not be settled",
356                )
357                .in_phase(crate::error::ErrorPhase::StateRead)
358            })?;
359            if let Some(current) = current {
360                let current = KnowledgeForgetRecord::decode(current)?;
361                if current.owner != record.owner {
362                    return Err(KitError::conflict(
363                        "knowledge forget obligation changed during settlement",
364                    ));
365                }
366                transaction.delete(key).await.map_err(|error| {
367                    KitError::from_state_store(
368                        error,
369                        "knowledge forget obligation could not be settled",
370                    )
371                    .in_phase(crate::error::ErrorPhase::StateWrite)
372                })?;
373            }
374            transaction.commit().await.map(|_| ()).map_err(|error| {
375                KitError::from_state_store(
376                    error,
377                    "knowledge forget obligation could not be settled",
378                )
379                .in_phase(crate::error::ErrorPhase::StateCommit)
380            })
381        };
382        match observation_deadline {
383            Some(observation_deadline) => tokio::time::timeout_at(observation_deadline, settlement)
384                .await
385                .map_err(|_| KitError::timeout("knowledge reconciliation deadline elapsed"))?,
386            None => settlement.await,
387        }
388    }
389
390    pub(crate) async fn settle_knowledge_forgets<D: DatabaseBackend>(
391        &self,
392        knowledge: Option<&ManagedKnowledge<S, D>>,
393        agent_id: &AgentId,
394        deadline: tokio::time::Instant,
395    ) -> Result<(), KitError> {
396        let records = self.knowledge_forget_records(agent_id, deadline).await?;
397        if !records.is_empty() && knowledge.is_none() {
398            return Err(KitError::conflict(
399                "durable knowledge cleanup requires the supplied ManagedKnowledge",
400            ));
401        }
402        for record in records {
403            self.settle_knowledge_forget(knowledge, &record.owner, deadline, Some(deadline))
404                .await?;
405        }
406        Ok(())
407    }
408
409    pub(crate) fn claim_agent(&self, id: &AgentId) -> Result<AgentStorageClaim, KitError> {
410        let mut live = self
411            .inner
412            .live_agents
413            .lock()
414            .unwrap_or_else(|poisoned| poisoned.into_inner());
415        if !live.insert(id.clone()) {
416            return Err(KitError::busy("agent identity is already live"));
417        }
418        drop(live);
419        Ok(AgentStorageClaim {
420            live_agents: self.inner.live_agents.clone(),
421            id: Some(id.clone()),
422            release_on_drop: true,
423        })
424    }
425
426    async fn load_record(&self, id: &AgentId) -> Result<AgentRecord, KitError> {
427        let key = AgentRecord::key(id)?;
428        let snapshot = self
429            .store()
430            .load(key)
431            .await
432            .map_err(|error| {
433                KitError::from_state_store(error, "agent identity could not be loaded")
434                    .in_phase(crate::error::ErrorPhase::StateRead)
435            })?
436            .ok_or_else(|| KitError::not_found("agent identity does not exist"))?;
437        let record = AgentRecord::decode(snapshot)?;
438        if &record.id != id {
439            return Err(KitError::invalid_state(
440                "stored agent identity does not match its key",
441            ));
442        }
443        Ok(record)
444    }
445
446    async fn knowledge_forget_records(
447        &self,
448        agent_id: &AgentId,
449        deadline: tokio::time::Instant,
450    ) -> Result<Vec<KnowledgeForgetRecord>, KitError> {
451        let prefix =
452            storage_kit::state::record::StateKeyPrefix::new(format!("{}:", agent_id.as_str()))
453                .map_err(|_| KitError::invalid_state("knowledge forget prefix is invalid"))?;
454        let mut records = Vec::new();
455        let mut cursor = None;
456        let mut cursors = BTreeSet::new();
457        for _ in 0..MAX_KNOWLEDGE_FORGET_PAGES {
458            let mut request =
459                storage_kit::state::record::StateListRequest::new(KNOWLEDGE_FORGET_NAMESPACE)
460                    .map_err(|_| {
461                        KitError::invalid_state("knowledge forget inventory request is invalid")
462                    })?
463                    .with_key_prefix(prefix.clone())
464                    .with_limit(storage_kit::state::record::StateListRequest::MAX_LIMIT)
465                    .map_err(|_| {
466                        KitError::invalid_state("knowledge forget inventory limit is invalid")
467                    })?;
468            if let Some(next) = cursor.take() {
469                request = request.start_after(next).map_err(|_| {
470                    KitError::invalid_state("knowledge forget inventory cursor is invalid")
471                })?;
472            }
473            let page = tokio::time::timeout_at(deadline, self.store().list(request))
474                .await
475                .map_err(|_| KitError::timeout("knowledge reconciliation deadline elapsed"))?
476                .map_err(|error| {
477                    KitError::from_state_store(
478                        error,
479                        "knowledge forget inventory could not be loaded",
480                    )
481                    .in_phase(crate::error::ErrorPhase::StateRead)
482                })?;
483            if page.records.len() > MAX_KNOWLEDGE_FORGET_RECORDS.saturating_sub(records.len()) {
484                return Err(KitError::invalid_state(
485                    "knowledge forget inventory exceeds its private bound",
486                ));
487            }
488            for snapshot in page.records {
489                let record = KnowledgeForgetRecord::decode(snapshot)?;
490                if record.agent_id() != Some(agent_id) {
491                    return Err(KitError::invalid_state(
492                        "knowledge forget obligation has a foreign owner",
493                    ));
494                }
495                records.push(record);
496            }
497            let Some(next) = page.next_start_after else {
498                return Ok(records);
499            };
500            if !cursors.insert(next.clone()) {
501                return Err(KitError::invalid_state(
502                    "knowledge forget inventory repeated its cursor",
503                ));
504            }
505            cursor = Some(next);
506        }
507        Err(KitError::invalid_state(
508            "knowledge forget inventory exceeds its private page bound",
509        ))
510    }
511}
512
513impl AgentStorageCreationError {
514    pub(crate) fn into_sdk(self) -> KitError {
515        match self {
516            Self::TransactionConflict => {
517                KitError::conflict("agent identity transaction conflicted")
518            }
519            Self::Failure(error) => error,
520        }
521    }
522}
523
524impl AgentStorageKnowledgeError {
525    pub(crate) fn into_sdk(self) -> KitError {
526        match self {
527            Self::TransactionConflict(phase) => {
528                KitError::conflict("knowledge forget transaction conflicted")
529                    .with_detail(crate::error::ErrorDetail::TransactionConflict)
530                    .in_phase(phase)
531            }
532            Self::Failure(error) => error,
533        }
534    }
535}
536
537impl Drop for AgentStorageClaim {
538    fn drop(&mut self) {
539        if !self.release_on_drop {
540            return;
541        }
542        self.release();
543    }
544}
545
546impl AgentStorageClaim {
547    pub(crate) fn retain_until_shutdown(&mut self) {
548        self.release_on_drop = false;
549    }
550
551    pub(crate) fn release(&mut self) {
552        let Some(id) = self.id.take() else {
553            return;
554        };
555        self.live_agents
556            .lock()
557            .unwrap_or_else(|poisoned| poisoned.into_inner())
558            .remove(&id);
559    }
560
561    pub(crate) fn complete_shutdown(
562        &mut self,
563        result: Result<(), KitError>,
564    ) -> Result<(), KitError> {
565        if result.is_ok() {
566            self.release();
567        }
568        result
569    }
570}
571
572impl AgentRecord {
573    fn from_info(info: &AgentInfo) -> Self {
574        Self {
575            version: AGENT_RECORD_VERSION,
576            id: info.id().clone(),
577            display_name: info.display_name().map(str::to_owned),
578        }
579    }
580
581    fn into_info(self) -> Result<AgentInfo, KitError> {
582        AgentInfo::new(self.id, self.display_name)
583            .map_err(|_| KitError::invalid_state("stored agent identity is malformed"))
584    }
585
586    fn key(id: &AgentId) -> Result<storage_kit::state::record::StateKey, KitError> {
587        storage_kit::state::record::StateKey::new(AGENT_NAMESPACE, id.as_str())
588            .map_err(|_| KitError::invalid_state("agent identity key is invalid"))
589    }
590
591    fn decode(snapshot: storage_kit::state::record::StateRecordSnapshot) -> Result<Self, KitError> {
592        let key = snapshot.record.key.key().to_owned();
593        let record: Self = serde::Deserialize::deserialize(snapshot.record.value.as_ref())
594            .map_err(|_| KitError::invalid_state("stored agent identity is malformed"))?;
595        if record.version != AGENT_RECORD_VERSION || record.id.as_str() != key {
596            return Err(KitError::invalid_state(
597                "stored agent identity does not match its durable key",
598            ));
599        }
600        if record
601            .display_name
602            .as_ref()
603            .is_some_and(|name| name.is_empty() || name.trim() != name)
604        {
605            return Err(KitError::invalid_state(
606                "stored agent display name is malformed",
607            ));
608        }
609        Ok(record)
610    }
611}
612
613impl KnowledgeForgetRecord {
614    fn new(owner: KnowledgeOwnerId) -> Result<Self, KitError> {
615        if matches!(owner, KnowledgeOwnerId::Database) {
616            return Err(KitError::invalid_input(
617                "Database knowledge is not owned by an agent hierarchy deletion",
618            ));
619        }
620        Ok(Self {
621            version: KNOWLEDGE_FORGET_RECORD_VERSION,
622            owner,
623        })
624    }
625
626    fn key(&self) -> Result<storage_kit::state::record::StateKey, KitError> {
627        Self::key_for(&self.owner)
628    }
629
630    fn key_for(owner: &KnowledgeOwnerId) -> Result<storage_kit::state::record::StateKey, KitError> {
631        let (agent_id, suffix) = match owner {
632            KnowledgeOwnerId::Database => {
633                return Err(KitError::invalid_input(
634                    "Database knowledge has no agent hierarchy key",
635                ));
636            }
637            KnowledgeOwnerId::Agent { agent_id } => (agent_id, "agent".to_owned()),
638            KnowledgeOwnerId::Workspace {
639                agent_id,
640                workspace_id,
641            } => (agent_id, format!("workspace:{}", workspace_id.as_str())),
642            KnowledgeOwnerId::Conversation {
643                agent_id,
644                workspace_id,
645                conversation_id,
646            } => (
647                agent_id,
648                format!(
649                    "conversation:{}:{}",
650                    workspace_id.as_str(),
651                    conversation_id.as_str()
652                ),
653            ),
654        };
655        storage_kit::state::record::StateKey::new(
656            KNOWLEDGE_FORGET_NAMESPACE,
657            format!("{}:{suffix}", agent_id.as_str()),
658        )
659        .map_err(|_| KitError::invalid_state("knowledge forget key is invalid"))
660    }
661
662    fn agent_id(&self) -> Option<&AgentId> {
663        match &self.owner {
664            KnowledgeOwnerId::Database => None,
665            KnowledgeOwnerId::Agent { agent_id }
666            | KnowledgeOwnerId::Workspace { agent_id, .. }
667            | KnowledgeOwnerId::Conversation { agent_id, .. } => Some(agent_id),
668        }
669    }
670
671    fn encode(&self) -> Result<serde_json::Value, KitError> {
672        serde_json::to_value(self).map_err(|_| {
673            KitError::invalid_state("knowledge forget obligation could not be encoded")
674        })
675    }
676
677    fn decode(snapshot: storage_kit::state::record::StateRecordSnapshot) -> Result<Self, KitError> {
678        let key = snapshot.record.key.clone();
679        let record: Self = serde::Deserialize::deserialize(snapshot.record.value.as_ref())
680            .map_err(|_| {
681                KitError::invalid_state("stored knowledge forget obligation is malformed")
682            })?;
683        let expected_key = record.key().map_err(|_| {
684            KitError::invalid_state("stored knowledge forget obligation has an invalid owner")
685        })?;
686        if record.version != KNOWLEDGE_FORGET_RECORD_VERSION || expected_key != key {
687            return Err(KitError::invalid_state(
688                "stored knowledge forget obligation contradicts its key",
689            ));
690        }
691        Ok(record)
692    }
693}