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