1use hirn_core::episodic::EpisodicRecord;
8use hirn_core::id::MemoryId;
9use hirn_core::metadata::Metadata;
10use hirn_core::semantic::SemanticRecord;
11use hirn_core::timestamp::Timestamp;
12use hirn_core::types::{AgentId, EdgeRelation, Namespace};
13use hirn_core::{HirnError, HirnResult, RecallSnapshot, RevisionId};
14
15use crate::db::{
16 HirnDB, SemanticMerge, SemanticMergeOutcome, SemanticOverride, SemanticRetraction,
17 SemanticSupersession, SemanticUpdate,
18};
19use crate::inspect::InspectResult;
20use crate::recall::{RecallPreviewBudget, RecallPreviewPolicy, RecallResult, RecallViewMode};
21use crate::trace::TraceResult;
22use crate::watch::{WatchFilter, WatchSubscription};
23
24pub struct AgentContext<'a> {
28 db: &'a HirnDB,
29 agent_id: AgentId,
30 accessible_namespaces: Vec<Namespace>,
31}
32
33impl<'a> AgentContext<'a> {
34 pub(crate) fn new(
35 db: &'a HirnDB,
36 agent_id: AgentId,
37 accessible_namespaces: Vec<Namespace>,
38 ) -> Self {
39 Self {
40 db,
41 agent_id,
42 accessible_namespaces,
43 }
44 }
45
46 #[must_use]
48 pub fn agent_id(&self) -> &AgentId {
49 &self.agent_id
50 }
51
52 #[must_use]
54 pub fn accessible_namespaces(&self) -> &[Namespace] {
55 &self.accessible_namespaces
56 }
57
58 #[must_use]
60 pub fn private_namespace(&self) -> Namespace {
61 Namespace::private_for(&self.agent_id)
62 }
63
64 pub fn can_access(&self, ns: &Namespace) -> bool {
66 self.accessible_namespaces.contains(ns)
67 }
68
69 fn check_access(&self, ns: &Namespace) -> HirnResult<()> {
71 if self.can_access(ns) {
72 Ok(())
73 } else {
74 Err(HirnError::AccessDenied(format!(
75 "agent '{}' cannot access namespace '{}'",
76 self.agent_id,
77 ns.as_str()
78 )))
79 }
80 }
81
82 pub async fn remember(&self, mut record: EpisodicRecord) -> HirnResult<MemoryId> {
87 if record.namespace == Namespace::default() {
89 record.namespace = self.private_namespace();
90 }
91 self.check_access(&record.namespace)?;
92
93 let anomaly_score = self.db.compute_anomaly_score(&record).await?;
95 let threshold = 0.8_f32; if anomaly_score >= threshold {
98 return self
99 .db
100 .quarantine_record(&record, anomaly_score, &self.agent_id)
101 .await;
102 }
103
104 self.db.remember(record).await
105 }
106
107 pub async fn remember_in(
109 &self,
110 mut record: EpisodicRecord,
111 namespace: Namespace,
112 ) -> HirnResult<MemoryId> {
113 self.check_access(&namespace)?;
114 record.namespace = namespace;
115
116 let anomaly_score = self.db.compute_anomaly_score(&record).await?;
118 let threshold = 0.8_f32;
119
120 if anomaly_score >= threshold {
121 return self
122 .db
123 .quarantine_record(&record, anomaly_score, &self.agent_id)
124 .await;
125 }
126
127 self.db.remember(record).await
128 }
129
130 pub fn recall(&self, query_embedding: Vec<f32>) -> AgentRecallBuilder<'_> {
135 AgentRecallBuilder {
136 ctx: self,
137 query: query_embedding,
138 limit: 10,
139 threshold: None,
140 namespace: None,
141 snapshot: None,
142 query_text: None,
143 hybrid: false,
144 view_mode: RecallViewMode::default(),
145 preview_policy: RecallPreviewPolicy::from_config(self.db.config()),
146 }
147 }
148
149 pub fn think(&self, query_embedding: Vec<f32>) -> AgentThinkBuilder<'_> {
153 AgentThinkBuilder {
154 ctx: self,
155 query: query_embedding,
156 budget: None,
157 limit: 50,
158 namespace: None,
159 format: None,
160 context_config: None,
161 }
162 }
163
164 pub fn watch(&self, filter: WatchFilter) -> HirnResult<WatchSubscription> {
166 filter.validate_allowed_namespaces(&self.accessible_namespaces)?;
167 self.db
168 .watch(filter.scoped_to_namespaces(&self.accessible_namespaces))
169 }
170
171 pub async fn inspect(&self, id: MemoryId) -> HirnResult<InspectResult> {
175 let record = self.db.get_memory(id).await?;
177 let ns = record_namespace(&record);
178 self.check_access(&ns)?;
179
180 self.db
181 .inspect(id)
182 .allowed_namespaces(self.accessible_namespaces.clone())
183 .agent_id(self.agent_id.as_str())
184 .execute()
185 .await
186 }
187
188 pub async fn trace(&self, id: MemoryId) -> HirnResult<TraceResult> {
190 let record = self.db.get_memory(id).await?;
191 let ns = record_namespace(&record);
192 self.check_access(&ns)?;
193
194 self.db
195 .trace(id)
196 .allowed_namespaces(self.accessible_namespaces.clone())
197 .agent_id(self.agent_id.as_str())
198 .execute()
199 .await
200 }
201
202 pub async fn store_semantic(&self, mut record: SemanticRecord) -> HirnResult<MemoryId> {
206 if record.namespace == Namespace::default() {
207 record.namespace = self.private_namespace();
208 }
209 self.check_access(&record.namespace)?;
210 self.db.store_semantic(record).await
211 }
212
213 pub async fn archive_episode(&self, id: MemoryId) -> HirnResult<()> {
217 let record = self.db.resolve_active_episodic_head(id).await?;
218 self.check_access(&record.namespace)?;
219 self.db.archive_episode(id).await
220 }
221
222 pub async fn delete_episode(&self, id: MemoryId) -> HirnResult<()> {
224 let record = self.db.resolve_active_episodic_head(id).await?;
225 self.check_access(&record.namespace)?;
226 self.db.delete_episode(id).await
227 }
228
229 pub async fn retract_semantic(
231 &self,
232 id: MemoryId,
233 retraction: SemanticRetraction,
234 ) -> HirnResult<SemanticRecord> {
235 let record = self.db.get_memory(id).await?;
236 let ns = record_namespace(&record);
237 self.check_access(&ns)?;
238 self.db.retract_semantic(id, retraction).await
239 }
240
241 pub async fn override_semantic(
243 &self,
244 id: MemoryId,
245 override_request: SemanticOverride,
246 ) -> HirnResult<SemanticRecord> {
247 let record = self.db.get_memory(id).await?;
248 let ns = record_namespace(&record);
249 self.check_access(&ns)?;
250 self.db.override_semantic(id, override_request).await
251 }
252
253 pub async fn correct_semantic(
255 &self,
256 id: MemoryId,
257 update: SemanticUpdate,
258 ) -> HirnResult<SemanticRecord> {
259 let record = self.db.get_memory(id).await?;
260 let ns = record_namespace(&record);
261 self.check_access(&ns)?;
262 self.db.correct_semantic(id, update).await
263 }
264
265 pub async fn supersede_semantic(
267 &self,
268 id: MemoryId,
269 supersession: SemanticSupersession,
270 ) -> HirnResult<SemanticRecord> {
271 let record = self.db.get_memory(id).await?;
272 let ns = record_namespace(&record);
273 self.check_access(&ns)?;
274 self.db.supersede_semantic(id, supersession).await
275 }
276
277 pub async fn merge_semantic(
279 &self,
280 target: MemoryId,
281 merge: SemanticMerge,
282 ) -> HirnResult<SemanticMergeOutcome> {
283 let target_record = self.db.get_memory(target).await?;
284 self.check_access(&record_namespace(&target_record))?;
285 for source_id in &merge.source_ids {
286 let source_record = self.db.get_memory(*source_id).await?;
287 self.check_access(&record_namespace(&source_record))?;
288 }
289 self.db.merge_semantic(target, merge).await
290 }
291
292 pub async fn purge_semantic(&self, id: MemoryId) -> HirnResult<()> {
294 let record = self.db.get_memory(id).await?;
295 let ns = record_namespace(&record);
296 self.check_access(&ns)?;
297 self.db.purge_semantic_as(id, Some(self.agent_id)).await
298 }
299
300 pub async fn connect_with(
304 &self,
305 source: MemoryId,
306 target: MemoryId,
307 relation: EdgeRelation,
308 weight: f32,
309 metadata: Metadata,
310 ) -> HirnResult<crate::graph::EdgeId> {
311 let source_record = self.db.get_memory(source).await?;
312 let target_record = self.db.get_memory(target).await?;
313 self.check_access(&record_namespace(&source_record))?;
314 self.check_access(&record_namespace(&target_record))?;
315 self.db
316 .connect_with(source, target, relation, weight, metadata)
317 .await
318 }
319
320 pub async fn execute_ql(&self, query: &str) -> HirnResult<crate::ql::results::QueryResult> {
324 self.db
325 .execute_ql_scoped_as_agent(query, &self.accessible_namespaces, self.agent_id)
326 .await
327 }
328
329 pub async fn share_memory(
333 &self,
334 id: MemoryId,
335 target_namespace: &Namespace,
336 ) -> HirnResult<MemoryId> {
337 let record = self.db.get_memory(id).await?;
339 let source_ns = record_namespace(&record);
340 self.check_access(&source_ns)?;
341
342 self.check_access(target_namespace)?;
344
345 match record {
347 hirn_core::record::MemoryRecord::Episodic(mut ep) => {
348 let source_namespace = ep.namespace.as_str().to_string();
349 ep.id = MemoryId::new();
350 ep.namespace = target_namespace.clone();
351 let new_id = self.db.remember(ep).await?;
352
353 self.db
355 .connect_with(
356 new_id,
357 id,
358 hirn_core::types::EdgeRelation::DerivedFrom,
359 1.0,
360 hirn_core::metadata::Metadata::new(),
361 )
362 .await?;
363
364 self.db
365 .append_audit(
366 Some(self.agent_id.clone()),
367 hirn_core::audit::AuditAction::ShareMemory {
368 memory_id: id,
369 source_namespace,
370 target_namespace: target_namespace.as_str().to_string(),
371 },
372 )
373 .await?;
374
375 Ok(new_id)
376 }
377 hirn_core::record::MemoryRecord::Semantic(mut sem) => {
378 let source_namespace = sem.namespace.as_str().to_string();
379 sem.id = MemoryId::new();
380 sem.namespace = target_namespace.clone();
381 let new_id = self.db.store_semantic(sem).await?;
382
383 self.db
384 .connect_with(
385 new_id,
386 id,
387 hirn_core::types::EdgeRelation::DerivedFrom,
388 1.0,
389 hirn_core::metadata::Metadata::new(),
390 )
391 .await?;
392
393 self.db
394 .append_audit(
395 Some(self.agent_id.clone()),
396 hirn_core::audit::AuditAction::ShareMemory {
397 memory_id: id,
398 source_namespace,
399 target_namespace: target_namespace.as_str().to_string(),
400 },
401 )
402 .await?;
403
404 Ok(new_id)
405 }
406 hirn_core::record::MemoryRecord::Working(_) => Err(HirnError::InvalidInput(
407 "cannot share working memory entries".into(),
408 )),
409 hirn_core::record::MemoryRecord::Procedural(_) => Err(HirnError::InvalidInput(
410 "cannot share procedural memory entries".into(),
411 )),
412 }
413 }
414
415 pub async fn promote_to_shared(&self, id: MemoryId) -> HirnResult<MemoryId> {
417 let record = self.db.get_memory(id).await?;
418 match &record {
419 hirn_core::record::MemoryRecord::Semantic(_) => {}
420 hirn_core::record::MemoryRecord::Episodic(_) => {
421 return Err(HirnError::InvalidInput(
422 "only semantic records can be promoted to shared".into(),
423 ));
424 }
425 hirn_core::record::MemoryRecord::Working(_) => {
426 return Err(HirnError::InvalidInput(
427 "cannot promote working memory".into(),
428 ));
429 }
430 hirn_core::record::MemoryRecord::Procedural(_) => {
431 return Err(HirnError::InvalidInput(
432 "cannot promote procedural memory".into(),
433 ));
434 }
435 }
436
437 let shared = Namespace::shared();
438 let new_id = self.share_memory(id, &shared).await?;
439
440 self.db
441 .append_audit(
442 Some(self.agent_id.clone()),
443 hirn_core::audit::AuditAction::PromoteToShared { memory_id: id },
444 )
445 .await?;
446
447 Ok(new_id)
448 }
449
450 #[must_use]
452 pub fn db(&self) -> &HirnDB {
453 self.db
454 }
455}
456
457pub struct AgentRecallBuilder<'a> {
461 ctx: &'a AgentContext<'a>,
462 query: Vec<f32>,
463 limit: usize,
464 threshold: Option<f32>,
465 namespace: Option<Namespace>,
466 snapshot: Option<RecallSnapshot>,
467 query_text: Option<String>,
468 hybrid: bool,
469 view_mode: RecallViewMode,
470 preview_policy: RecallPreviewPolicy,
471}
472
473impl<'a> AgentRecallBuilder<'a> {
474 pub fn limit(mut self, k: usize) -> Self {
476 self.limit = k;
477 self
478 }
479
480 pub fn threshold(mut self, min: f32) -> Self {
482 self.threshold = Some(min);
483 self
484 }
485
486 pub fn namespace(mut self, ns: Namespace) -> Self {
488 self.namespace = Some(ns);
489 self
490 }
491
492 pub fn as_of(mut self, ts: Timestamp) -> Self {
494 self.snapshot = Some(RecallSnapshot::observed(ts));
495 self
496 }
497
498 pub fn as_recorded(mut self, ts: Timestamp) -> Self {
500 self.snapshot = Some(RecallSnapshot::recorded(ts));
501 self
502 }
503
504 pub fn at_revision(mut self, revision_id: RevisionId) -> Self {
506 self.snapshot = Some(RecallSnapshot::revision(revision_id));
507 self
508 }
509
510 pub fn snapshot(mut self, snapshot: RecallSnapshot) -> Self {
512 self.snapshot = Some(snapshot);
513 self
514 }
515
516 pub fn query_text(mut self, text: impl Into<String>) -> Self {
518 self.query_text = Some(text.into());
519 self
520 }
521
522 pub fn hybrid(mut self, enable: bool) -> Self {
524 self.hybrid = enable;
525 self
526 }
527
528 pub fn view_mode(mut self, mode: RecallViewMode) -> Self {
530 self.view_mode = mode;
531 self
532 }
533
534 pub fn summary_first(self) -> Self {
536 self.view_mode(RecallViewMode::SummaryFirst)
537 }
538
539 pub fn evidence_first(self) -> Self {
541 self.view_mode(RecallViewMode::EvidenceFirst)
542 }
543
544 pub fn mixed_view(self) -> Self {
546 self.view_mode(RecallViewMode::Mixed)
547 }
548
549 pub fn preview_package_limits(mut self, max_previews: usize, max_chars: usize) -> Self {
551 self.preview_policy.package = RecallPreviewBudget::new(max_previews, max_chars);
552 self
553 }
554
555 pub fn preview_rerank_limits(mut self, max_previews: usize, max_chars: usize) -> Self {
557 self.preview_policy.rerank = RecallPreviewBudget::new(max_previews, max_chars);
558 self
559 }
560
561 pub async fn execute(self) -> HirnResult<Vec<RecallResult>> {
563 if let Some(ref ns) = self.namespace {
565 self.ctx.check_access(ns)?;
566 let mut builder = self
568 .ctx
569 .db
570 .recall(self.query)
571 .limit(self.limit)
572 .namespace(*ns)
573 .agent_id(self.ctx.agent_id.as_str());
574 if let Some(t) = self.threshold {
575 builder = builder.threshold(t);
576 }
577 if let Some(query_text) = self.query_text.clone() {
578 builder = builder.query_text(query_text);
579 }
580 builder = builder
581 .preview_package_limits(
582 self.preview_policy.package.max_previews,
583 self.preview_policy.package.max_chars,
584 )
585 .preview_rerank_limits(
586 self.preview_policy.rerank.max_previews,
587 self.preview_policy.rerank.max_chars,
588 );
589 if self.hybrid {
590 builder = builder.hybrid(true);
591 }
592 if let Some(snapshot) = self.snapshot {
593 builder = builder.snapshot(snapshot);
594 }
595 builder = builder.view_mode(self.view_mode);
596 return builder.execute().await;
597 }
598
599 let mut builder = self
602 .ctx
603 .db
604 .recall(self.query)
605 .limit(self.limit)
606 .allowed_namespaces(self.ctx.accessible_namespaces.clone())
607 .agent_id(self.ctx.agent_id.as_str());
608 if let Some(t) = self.threshold {
609 builder = builder.threshold(t);
610 }
611 if let Some(query_text) = self.query_text {
612 builder = builder.query_text(query_text);
613 }
614 builder = builder
615 .preview_package_limits(
616 self.preview_policy.package.max_previews,
617 self.preview_policy.package.max_chars,
618 )
619 .preview_rerank_limits(
620 self.preview_policy.rerank.max_previews,
621 self.preview_policy.rerank.max_chars,
622 );
623 if self.hybrid {
624 builder = builder.hybrid(true);
625 }
626 if let Some(snapshot) = self.snapshot {
627 builder = builder.snapshot(snapshot);
628 }
629 builder = builder.view_mode(self.view_mode);
630 builder.execute().await
631 }
632}
633
634pub struct AgentThinkBuilder<'a> {
638 ctx: &'a AgentContext<'a>,
639 query: Vec<f32>,
640 budget: Option<usize>,
641 limit: usize,
642 namespace: Option<Namespace>,
643 format: Option<crate::ql::context::ContextFormat>,
644 context_config: Option<crate::ql::context::ContextConfig>,
645}
646
647impl<'a> AgentThinkBuilder<'a> {
648 pub fn budget(mut self, tokens: usize) -> Self {
650 self.budget = Some(tokens);
651 self
652 }
653
654 pub fn limit(mut self, k: usize) -> Self {
656 self.limit = k;
657 self
658 }
659
660 pub fn namespace(mut self, ns: Namespace) -> Self {
662 self.namespace = Some(ns);
663 self
664 }
665
666 pub fn format(mut self, format: crate::ql::context::ContextFormat) -> Self {
668 self.format = Some(format);
669 self
670 }
671
672 pub fn preview_package_limits(mut self, max_previews: usize, max_chars: usize) -> Self {
676 let mut config = self.context_config.unwrap_or_else(|| {
677 crate::ql::context::ContextConfig::from_hirn_config(self.ctx.db.config())
678 });
679 config.max_resource_previews_per_entry = max_previews;
680 config.max_resource_preview_chars = max_chars;
681 self.context_config = Some(config);
682 self
683 }
684
685 pub fn context_config(mut self, config: crate::ql::context::ContextConfig) -> Self {
687 self.context_config = Some(config);
688 self
689 }
690
691 pub async fn execute(self) -> HirnResult<crate::ql::context::ThinkResult> {
693 if let Some(ref ns) = self.namespace {
694 self.ctx.check_access(ns)?;
695 let mut builder = self
696 .ctx
697 .db
698 .think(self.query)
699 .agent_id(*self.ctx.agent_id())
700 .limit(self.limit)
701 .namespace(ns.clone());
702 if let Some(config) = self.context_config.clone() {
703 builder = builder.context_config(config);
704 }
705 if let Some(budget) = self.budget {
706 builder = builder.budget(budget);
707 }
708 if let Some(format) = self.format {
709 builder = builder.format(format);
710 }
711 return builder.execute().await;
712 }
713
714 let recall_results = self
716 .ctx
717 .recall(self.query)
718 .limit(self.limit)
719 .execute()
720 .await?;
721
722 let scored: Vec<crate::ql::results::ScoredMemory> = recall_results
723 .into_iter()
724 .map(|rr| crate::ql::results::ScoredMemory {
725 record: rr.record,
726 revision: rr.revision,
727 score: rr.composite_score,
728 score_breakdown: rr.score_breakdown,
729 resource_evidence: rr.resource_evidence,
730 resource_preview_packages: rr.resource_preview_packages,
731 resource_score_attribution: rr.resource_score_attribution,
732 })
733 .collect();
734
735 let mut config = self.context_config.unwrap_or_else(|| {
736 crate::ql::context::ContextConfig::from_hirn_config(self.ctx.db.config())
737 });
738 if let Some(budget) = self.budget {
739 config.token_budget = budget;
740 }
741 if let Some(format) = self.format {
742 config.output_format = format;
743 }
744
745 let visible_namespaces = self
746 .namespace
747 .as_ref()
748 .map(std::slice::from_ref)
749 .or(Some(self.ctx.accessible_namespaces()));
750
751 Ok(crate::ql::context::assemble_think_context(
752 self.ctx.db,
753 self.ctx.agent_id(),
754 &scored,
755 &config,
756 visible_namespaces,
757 None,
758 None,
759 )
760 .await?)
761 }
762}
763
764fn record_namespace(record: &hirn_core::record::MemoryRecord) -> Namespace {
768 record.effective_namespace()
769}