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 {
97 Self {
98 inner: Arc::new(AgentStorageInner {
99 store,
100 live_agents: Arc::new(Mutex::new(BTreeSet::new())),
101 }),
102 }
103 }
104
105 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 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}