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
17pub 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 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 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 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}