Skip to main content

qualia_core_db/inference/inference_agent/
runtime.rs

1// The `AgentRuntime` implementation for `LocalLlmAgent`: the object-safe entry
2// points (backend / agent_did / validate_intent / infer / validate_output /
3// memory_budget_remaining). Moved verbatim from the monolith.
4
5use std::time::{Duration, Instant};
6
7use crate::modalities::logic::n3_compiler::N3OutputMode;
8
9use super::config::{effective_inference_timeout_ms, LLM_MEMORY_BUDGET_BYTES, MAX_OUTPUT_TOKENS};
10use super::local_agent::LocalLlmAgent;
11use super::types::{
12    AgentBackend, AgentError, AgentIntent, AgentOutput, AgentRuntime, WebizenVerdict,
13    LLM_RULE_PROFILE_VIOLATION, LLM_RULE_PROVENANCE_REQUIRED, LLM_RULE_TOKEN_BUDGET,
14};
15
16impl AgentRuntime for LocalLlmAgent {
17    fn backend(&self) -> &AgentBackend {
18        &self.backend
19    }
20    fn agent_did(&self) -> &str {
21        &self.agent_did
22    }
23
24    fn validate_intent(&self, intent: &AgentIntent) -> WebizenVerdict {
25        let sieve_on = matches!(
26            intent.output_mode,
27            N3OutputMode::GraphMutation | N3OutputMode::N3Assertions
28        );
29        self.use_sieve_output
30            .store(sieve_on, std::sync::atomic::Ordering::Relaxed);
31        if sieve_on {
32            let mut spec = crate::neuro_symbolic_sieve::SieveLexSpec::graph_mutation_default();
33            for &scope_hash in &intent.requested_graph_scope {
34                if scope_hash != 0 {
35                    spec.push_predicate(scope_hash);
36                }
37            }
38            for &namespace_hash in &intent.context_namespaces {
39                if namespace_hash != 0 {
40                    spec.push_predicate(namespace_hash);
41                }
42            }
43            *self.sieve_spec.lock().unwrap_or_else(|e| e.into_inner()) = spec;
44        }
45
46        let frame = intent.to_frame();
47        let base = Self::evaluate_intent_frame(self, &frame);
48        if !matches!(base, WebizenVerdict::Permit) {
49            return base;
50        }
51
52        // Rule 7: Profile Constraints (Intent frames and Engine masking)
53        if let Some(profile) = &intent.active_profile {
54            if !profile.allows_intent(intent.intent_predicate) {
55                return WebizenVerdict::DenyWithExplanation {
56                    rule_violated: LLM_RULE_PROFILE_VIOLATION,
57                    reason: "Profile Violation".into(),
58                    explanation: "This capability profile explicitly blocks this intent frame."
59                        .into(),
60                };
61            }
62        }
63
64        WebizenVerdict::Permit
65    }
66
67    fn infer(&self, prompt: &str, graph_context: &str) -> Result<AgentOutput, AgentError> {
68        let t0 = Instant::now();
69
70        // Memory guard
71        let current = self
72            .memory_used_bytes
73            .load(std::sync::atomic::Ordering::Relaxed);
74        if current > LLM_MEMORY_BUDGET_BYTES {
75            return Err(AgentError::MemoryBudgetExceeded);
76        }
77
78        // Timeout guard (production: run in a separate thread with channel)
79        let deadline = Duration::from_millis(effective_inference_timeout_ms());
80        let (text, provenance, tokens, semantic_quin) =
81            self.infer_local_model(prompt, graph_context);
82        if t0.elapsed() > deadline {
83            return Err(AgentError::Timeout);
84        }
85        if text == "[sieve-misaligned]" && semantic_quin.is_none() {
86            return Err(AgentError::SieveMisaligned);
87        }
88
89        Ok(AgentOutput {
90            text,
91            semantic_quin,
92            provenance_quins: provenance,
93            tokens_generated: tokens,
94            inference_duration_ms: t0.elapsed().as_millis() as u64,
95            peak_memory_bytes: current,
96        })
97    }
98
99    fn validate_output(&self, output: &AgentOutput) -> WebizenVerdict {
100        // Rule 3: All outputs MUST be grounded with at least one provenance citation.
101        if output.provenance_quins.is_empty() {
102            return WebizenVerdict::Deny {
103                rule_violated: LLM_RULE_PROVENANCE_REQUIRED,
104                reason: "Output has no provenance citations. Cannot commit ungrounded content to the semantic graph.",
105                conduct_record: None,
106            };
107        }
108        // Rule 4: Output must not exceed token budget (prevents runaway generation).
109        if output.tokens_generated > MAX_OUTPUT_TOKENS {
110            return WebizenVerdict::Deny {
111                rule_violated: LLM_RULE_TOKEN_BUDGET,
112                reason: "Token budget exceeded.",
113                conduct_record: None,
114            };
115        }
116        WebizenVerdict::Permit
117    }
118
119    fn memory_budget_remaining(&self) -> u64 {
120        let used = self
121            .memory_used_bytes
122            .load(std::sync::atomic::Ordering::Relaxed);
123        LLM_MEMORY_BUDGET_BYTES.saturating_sub(used)
124    }
125}