Skip to main content

hirn_engine/
agent_context.rs

1//! Agent-scoped context for namespace-isolated memory operations.
2//!
3//! `AgentContext` wraps a `HirnDB` reference and enforces that all operations
4//! respect namespace boundaries. An agent can only access its private namespace,
5//! the shared namespace, and any team namespaces it belongs to.
6
7use 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
24/// Agent-scoped database context enforcing namespace isolation.
25///
26/// Created via `db.as_agent(agent_id)`.
27pub 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    /// The agent ID for this context.
47    #[must_use]
48    pub fn agent_id(&self) -> &AgentId {
49        &self.agent_id
50    }
51
52    /// The namespaces accessible to this agent.
53    #[must_use]
54    pub fn accessible_namespaces(&self) -> &[Namespace] {
55        &self.accessible_namespaces
56    }
57
58    /// The agent's private namespace.
59    #[must_use]
60    pub fn private_namespace(&self) -> Namespace {
61        Namespace::private_for(&self.agent_id)
62    }
63
64    /// Check whether this agent can access a given namespace.
65    pub fn can_access(&self, ns: &Namespace) -> bool {
66        self.accessible_namespaces.contains(ns)
67    }
68
69    /// Verify namespace access, returning an error if denied.
70    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    // ── Remember ────────────────────────────────────────────────────────
83
84    /// Store an episodic record in the agent's private namespace (default)
85    /// or a specified namespace.
86    pub async fn remember(&self, mut record: EpisodicRecord) -> HirnResult<MemoryId> {
87        // Default to private namespace if record uses the default namespace.
88        if record.namespace == Namespace::default() {
89            record.namespace = self.private_namespace();
90        }
91        self.check_access(&record.namespace)?;
92
93        // Run anomaly detection before storing.
94        let anomaly_score = self.db.compute_anomaly_score(&record).await?;
95        let threshold = 0.8_f32; // memories with anomaly_score >= 0.8 are quarantined
96
97        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    /// Store a record explicitly in a named namespace.
108    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        // Run anomaly detection before storing.
117        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    // ── Recall ──────────────────────────────────────────────────────────
131
132    /// Recall memories, searching only accessible namespaces.
133    /// By default searches private + shared.
134    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    // ── Think ───────────────────────────────────────────────────────────
150
151    /// Think (context assembly) scoped to this agent's accessible namespaces.
152    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    /// Create a watch subscription scoped to the agent's accessible namespaces.
165    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    // ── Inspect / Trace ─────────────────────────────────────────────────
172
173    /// Inspect a record, verifying the agent has access to its namespace.
174    pub async fn inspect(&self, id: MemoryId) -> HirnResult<InspectResult> {
175        // Verify the record is accessible.
176        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    /// Trace a record, verifying the agent has access to its namespace.
189    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    // ── Store Semantic ─────────────────────────────────────────────────
203
204    /// Store a semantic record, enforcing namespace access.
205    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    // ── Forget ──────────────────────────────────────────────────────────
214
215    /// Archive an episodic record, verifying namespace access.
216    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    /// Delete an episodic record, verifying namespace access.
223    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    /// Retract a semantic record, verifying namespace access.
230    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    /// Apply a durable semantic override, verifying namespace access.
242    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    /// Correct a semantic record, verifying namespace access.
254    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    /// Supersede a semantic record, verifying namespace access.
266    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    /// Merge semantic logical memories, verifying namespace access for the target and sources.
278    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    /// Purge all revisions for a semantic logical memory, verifying namespace access.
293    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    // ── Connect ─────────────────────────────────────────────────────────
301
302    /// Create a graph edge between two records, verifying namespace access for both.
303    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    // ── Execute (HirnQL) ────────────────────────────────────────────────
321
322    /// Execute a HirnQL query scoped to the agent's accessible namespaces.
323    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    // ── Share / Promote ─────────────────────────────────────────────────
330
331    /// Share a memory from this agent's accessible namespaces to a target namespace.
332    pub async fn share_memory(
333        &self,
334        id: MemoryId,
335        target_namespace: &Namespace,
336    ) -> HirnResult<MemoryId> {
337        // Verify source access.
338        let record = self.db.get_memory(id).await?;
339        let source_ns = record_namespace(&record);
340        self.check_access(&source_ns)?;
341
342        // Verify target access.
343        self.check_access(target_namespace)?;
344
345        // Clone the record into the target namespace.
346        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                // Create DerivedFrom edge.
354                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    /// Promote a private semantic record to the shared namespace.
416    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    /// Get the underlying database reference.
451    #[must_use]
452    pub fn db(&self) -> &HirnDB {
453        self.db
454    }
455}
456
457// ── Agent Recall Builder ────────────────────────────────────────────────
458
459/// Builder for namespace-scoped recall queries.
460pub 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    /// Maximum number of results.
475    pub fn limit(mut self, k: usize) -> Self {
476        self.limit = k;
477        self
478    }
479
480    /// Minimum similarity threshold.
481    pub fn threshold(mut self, min: f32) -> Self {
482        self.threshold = Some(min);
483        self
484    }
485
486    /// Restrict to a specific namespace (must be accessible).
487    pub fn namespace(mut self, ns: Namespace) -> Self {
488        self.namespace = Some(ns);
489        self
490    }
491
492    /// Resolve semantic recall as a point-in-time snapshot.
493    pub fn as_of(mut self, ts: Timestamp) -> Self {
494        self.snapshot = Some(RecallSnapshot::observed(ts));
495        self
496    }
497
498    /// Resolve semantic recall as a recorded-time snapshot.
499    pub fn as_recorded(mut self, ts: Timestamp) -> Self {
500        self.snapshot = Some(RecallSnapshot::recorded(ts));
501        self
502    }
503
504    /// Resolve recall using the transaction boundary of a specific revision.
505    pub fn at_revision(mut self, revision_id: RevisionId) -> Self {
506        self.snapshot = Some(RecallSnapshot::revision(revision_id));
507        self
508    }
509
510    /// Resolve recall using an explicit snapshot target.
511    pub fn snapshot(mut self, snapshot: RecallSnapshot) -> Self {
512        self.snapshot = Some(snapshot);
513        self
514    }
515
516    /// Enable hybrid search and preview-aware reranking with the raw text query.
517    pub fn query_text(mut self, text: impl Into<String>) -> Self {
518        self.query_text = Some(text.into());
519        self
520    }
521
522    /// Enable hybrid BM25+vector search when `query_text` is provided.
523    pub fn hybrid(mut self, enable: bool) -> Self {
524        self.hybrid = enable;
525        self
526    }
527
528    /// Select the presentation mode for returned results.
529    pub fn view_mode(mut self, mode: RecallViewMode) -> Self {
530        self.view_mode = mode;
531        self
532    }
533
534    /// Prefer memory summaries ahead of linked evidence.
535    pub fn summary_first(self) -> Self {
536        self.view_mode(RecallViewMode::SummaryFirst)
537    }
538
539    /// Prefer linked evidence ahead of memory summaries.
540    pub fn evidence_first(self) -> Self {
541        self.view_mode(RecallViewMode::EvidenceFirst)
542    }
543
544    /// Present summaries and linked evidence together.
545    pub fn mixed_view(self) -> Self {
546        self.view_mode(RecallViewMode::Mixed)
547    }
548
549    /// Override preview-package limits for RECALL JSON output.
550    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    /// Override preview-aware rerank limits for this recall.
556    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    /// Execute the recall query, filtered to accessible namespaces.
562    pub async fn execute(self) -> HirnResult<Vec<RecallResult>> {
563        // If a specific namespace is requested, verify access.
564        if let Some(ref ns) = self.namespace {
565            self.ctx.check_access(ns)?;
566            // Use the standard recall with namespace filter.
567            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        // No specific namespace: execute the shared recall pipeline with the
600        // agent's allowed namespace set instead of over-fetching then trimming.
601        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
634// ── Agent Think Builder ─────────────────────────────────────────────────
635
636/// Builder for namespace-scoped think queries.
637pub 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    /// Token budget.
649    pub fn budget(mut self, tokens: usize) -> Self {
650        self.budget = Some(tokens);
651        self
652    }
653
654    /// Maximum candidates.
655    pub fn limit(mut self, k: usize) -> Self {
656        self.limit = k;
657        self
658    }
659
660    /// Restrict to a specific namespace.
661    pub fn namespace(mut self, ns: Namespace) -> Self {
662        self.namespace = Some(ns);
663        self
664    }
665
666    /// Override the output format.
667    pub fn format(mut self, format: crate::ql::context::ContextFormat) -> Self {
668        self.format = Some(format);
669        self
670    }
671
672    /// Override preview-package limits for THINK JSON output.
673    ///
674    /// Setting either value to `0` disables preview packaging.
675    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    /// Override the full context configuration.
686    pub fn context_config(mut self, config: crate::ql::context::ContextConfig) -> Self {
687        self.context_config = Some(config);
688        self
689    }
690
691    /// Execute the think query.
692    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        // For multi-namespace search, recall from accessible IDs then assemble.
715        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
764// ── Helpers ─────────────────────────────────────────────────────────────
765
766/// Extract the namespace from a memory record.
767fn record_namespace(record: &hirn_core::record::MemoryRecord) -> Namespace {
768    record.effective_namespace()
769}