Skip to main content

lean_ctx/core/ocla/adapters/
native_context.rs

1//! Native LeanCTX context optimization adapter.
2
3use std::path::{Path, PathBuf};
4use std::sync::OnceLock;
5use std::time::Instant;
6
7use lean_ctx_protocol::CapabilityManifestV1;
8use sha2::{Digest, Sha256};
9
10use super::read_context_paths;
11use crate::core::compressor;
12use crate::core::ocla::invocation::{
13    CapabilityAdapter, CapabilityInput, CapabilityInvocation, CapabilityObservationV1,
14    CapabilityResult, evidence_ref,
15};
16use crate::core::ocla::{OclaError, OclaResult};
17use crate::core::tokens;
18
19/// Repository-relative manifest used by this adapter.
20pub const MANIFEST_PATH: &str =
21    "docs/contracts/ocla/capability-manifests/leanctx/context-optimization-v1.json";
22const MANIFEST_JSON: &str = include_str!(concat!(
23    env!("CARGO_MANIFEST_DIR"),
24    "/assets/ocla/capability-manifests/leanctx/context-optimization-v1.json"
25));
26
27pub(crate) fn manifest() -> &'static CapabilityManifestV1 {
28    static MANIFEST: OnceLock<CapabilityManifestV1> = OnceLock::new();
29    MANIFEST.get_or_init(|| {
30        let manifest: CapabilityManifestV1 =
31            serde_json::from_str(MANIFEST_JSON).expect("native context manifest is valid JSON");
32        manifest
33            .validate()
34            .expect("native context manifest satisfies the protocol");
35        manifest
36    })
37}
38
39/// Local adapter that combines existing file reads and compression primitives.
40pub struct NativeContextAdapter {
41    root: PathBuf,
42    #[cfg(test)]
43    materialized_test_control: Option<std::sync::Arc<MaterializedTestControl>>,
44}
45
46#[cfg(test)]
47pub(crate) struct MaterializedTestControl {
48    pub(crate) release: std::sync::Barrier,
49    pub(crate) completed: std::sync::Barrier,
50}
51
52#[cfg(test)]
53impl MaterializedTestControl {
54    pub(crate) fn new() -> Self {
55        Self {
56            release: std::sync::Barrier::new(2),
57            completed: std::sync::Barrier::new(2),
58        }
59    }
60}
61
62/// Payload-free result used by the internal Engine proof path.
63///
64/// Native OCLA consumers keep receiving [`CapabilityResult`]. The Engine needs
65/// a SHA-256 identity for the exact derived output, but must not retain or
66/// expose that output in a receipt.
67#[allow(dead_code)]
68pub(crate) struct NativeContextInvocationResult {
69    pub result: CapabilityResult,
70    pub input_digest: String,
71    pub output_digest: String,
72    /// Exact derived bytes are kept inside the Engine boundary solely to
73    /// persist a local, integrity-addressed output artifact. They are never
74    /// serialized, logged, or returned from the OCLA adapter contract.
75    pub(crate) output: Vec<u8>,
76}
77
78/// Typed internal failure boundary for the native Engine proof path.
79///
80/// The public OCLA adapter trait continues to expose `OclaError`; the Engine
81/// must not infer its stable failure taxonomy by parsing those error strings.
82#[derive(Debug)]
83pub(crate) enum NativeContextInvocationFailure {
84    SourceUnavailable(String),
85    ResourceLimit(String),
86    UnsupportedInput,
87    InvalidRequest(String),
88}
89
90impl NativeContextInvocationFailure {
91    fn into_ocla_error(self) -> OclaError {
92        let message = match self {
93            Self::SourceUnavailable(message)
94            | Self::ResourceLimit(message)
95            | Self::InvalidRequest(message) => message,
96            Self::UnsupportedInput => {
97                "native context optimization accepts ContextRequest only".to_owned()
98            }
99        };
100        OclaError::InvalidRequest(message)
101    }
102}
103
104impl NativeContextAdapter {
105    #[must_use]
106    pub fn new() -> Self {
107        Self {
108            root: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
109            #[cfg(test)]
110            materialized_test_control: None,
111        }
112    }
113
114    #[must_use]
115    pub fn with_root(root: impl AsRef<Path>) -> Self {
116        Self {
117            root: root.as_ref().to_path_buf(),
118            #[cfg(test)]
119            materialized_test_control: None,
120        }
121    }
122
123    #[cfg(test)]
124    pub(crate) fn with_materialized_test_control(
125        mut self,
126        control: std::sync::Arc<MaterializedTestControl>,
127    ) -> Self {
128        self.materialized_test_control = Some(control);
129        self
130    }
131
132    #[must_use]
133    pub fn root(&self) -> &Path {
134        &self.root
135    }
136
137    fn optimize(&self, content: &str, paths: &[String], mode: &str) -> String {
138        if matches!(mode, "full" | "raw" | "passthrough") {
139            return content.to_owned();
140        }
141
142        let extension = paths
143            .first()
144            .and_then(|path| Path::new(path).extension())
145            .and_then(|extension| extension.to_str());
146        compressor::aggressive_compress(content, extension)
147    }
148
149    fn invoke_context(
150        &self,
151        invocation: &CapabilityInvocation,
152        paths: &[String],
153        mode: &str,
154        budget_tokens: Option<u64>,
155        start: Instant,
156    ) -> Result<NativeContextInvocationResult, NativeContextInvocationFailure> {
157        let input = read_context_paths(&self.root, paths).map_err(|error| {
158            NativeContextInvocationFailure::SourceUnavailable(error.to_string())
159        })?;
160        self.invoke_materialized_context(invocation, paths, mode, budget_tokens, start, &input)
161    }
162
163    fn invoke_materialized_context(
164        &self,
165        invocation: &CapabilityInvocation,
166        paths: &[String],
167        mode: &str,
168        budget_tokens: Option<u64>,
169        start: Instant,
170        input: &str,
171    ) -> Result<NativeContextInvocationResult, NativeContextInvocationFailure> {
172        let input_tokens = tokens::count_tokens(&input) as u64;
173        if let Some(max) = invocation.policy_constraints.max_input_tokens
174            && input_tokens > max
175        {
176            return Err(NativeContextInvocationFailure::ResourceLimit(format!(
177                "input exceeds policy token limit ({input_tokens} > {max})"
178            )));
179        }
180
181        let optimized = self.optimize(&input, paths, mode);
182        let output = match budget_tokens {
183            Some(budget) => truncate_to_tokens(&optimized, budget),
184            None => optimized,
185        };
186        let output_tokens = tokens::count_tokens(&output) as u64;
187        if let Some(max) = invocation.policy_constraints.max_output_tokens
188            && output_tokens > max
189        {
190            return Err(NativeContextInvocationFailure::ResourceLimit(format!(
191                "output exceeds policy token limit ({output_tokens} > {max})"
192            )));
193        }
194
195        let latency_ms = super::super::invocation::check_timeout(start, invocation.timeout_ms)
196            .map_err(|error| NativeContextInvocationFailure::ResourceLimit(error.to_string()))?;
197        if let Some(max) = invocation.policy_constraints.max_latency_ms
198            && latency_ms > max
199        {
200            return Err(NativeContextInvocationFailure::ResourceLimit(format!(
201                "capability latency exceeds policy limit ({latency_ms} > {max})"
202            )));
203        }
204        let output_ref = evidence_ref(&output);
205        let mut observation = CapabilityObservationV1::success(
206            invocation,
207            input_tokens,
208            output_tokens,
209            latency_ms,
210            Some(output_ref.clone()),
211        );
212        observation.metrics.insert(
213            "compression_saved_tokens".into(),
214            input_tokens.saturating_sub(output_tokens),
215        );
216        observation.metrics.insert(
217            "compression_rate_milli".into(),
218            compression_rate_milli(input_tokens, output_tokens),
219        );
220        Ok(NativeContextInvocationResult {
221            result: CapabilityResult {
222                success: true,
223                output_tokens,
224                latency_ms,
225                observation,
226                evidence_ref: Some(output_ref),
227            },
228            input_digest: format!(
229                "sha256:{}",
230                crate::core::agent_identity::hex_encode(&Sha256::digest(input.as_bytes()))
231            ),
232            output_digest: format!(
233                "sha256:{}",
234                crate::core::agent_identity::hex_encode(&Sha256::digest(output.as_bytes()))
235            ),
236            output: output.into_bytes(),
237        })
238    }
239
240    /// Execute the native context capability and return its payload-free
241    /// SHA-256 output identity for the internal Engine bridge.
242    pub(crate) fn invoke_with_output_identity(
243        &self,
244        invocation: &CapabilityInvocation,
245    ) -> Result<NativeContextInvocationResult, NativeContextInvocationFailure> {
246        self.validate_engine_invocation(invocation)?;
247        let start = Instant::now();
248        match &invocation.input {
249            CapabilityInput::ContextRequest {
250                paths,
251                mode,
252                budget_tokens,
253            } => self.invoke_context(invocation, paths, mode, *budget_tokens, start),
254            CapabilityInput::ShellCommand { .. }
255            | CapabilityInput::ModelRequest { .. }
256            | CapabilityInput::AgentTask(_) => {
257                Err(NativeContextInvocationFailure::UnsupportedInput)
258            }
259        }
260    }
261
262    /// Execute against the exact source snapshot already acquired by a
263    /// production caller. This prevents a second file read and binds the
264    /// Engine digest to the same bytes the caller will render.
265    fn invoke_materialized_with_output_identity(
266        &self,
267        invocation: &CapabilityInvocation,
268        input: &str,
269    ) -> Result<NativeContextInvocationResult, NativeContextInvocationFailure> {
270        self.validate_engine_invocation(invocation)?;
271        let start = Instant::now();
272        match &invocation.input {
273            CapabilityInput::ContextRequest {
274                paths,
275                mode,
276                budget_tokens,
277            } => self.invoke_materialized_context(
278                invocation,
279                paths,
280                mode,
281                *budget_tokens,
282                start,
283                input,
284            ),
285            CapabilityInput::ShellCommand { .. }
286            | CapabilityInput::ModelRequest { .. }
287            | CapabilityInput::AgentTask(_) => {
288                Err(NativeContextInvocationFailure::UnsupportedInput)
289            }
290        }
291    }
292
293    /// Execute a materialized invocation behind an actual host deadline.
294    /// Timed-out workers can finish computation, but cannot persist Engine
295    /// artifacts because persistence remains in the receiving Engine layer.
296    pub(crate) fn invoke_materialized_bounded(
297        &self,
298        invocation: CapabilityInvocation,
299        input: String,
300    ) -> Result<NativeContextInvocationResult, NativeContextInvocationFailure> {
301        if invocation.timeout_ms == 0 {
302            return Err(NativeContextInvocationFailure::ResourceLimit(
303                "native context deadline expired before dispatch".to_owned(),
304            ));
305        }
306        let timeout = std::time::Duration::from_millis(invocation.timeout_ms);
307        let adapter = Self::with_root(&self.root);
308        #[cfg(test)]
309        let adapter = {
310            let mut adapter = adapter;
311            adapter.materialized_test_control = self.materialized_test_control.clone();
312            adapter
313        };
314        let (tx, rx) = std::sync::mpsc::sync_channel(1);
315        std::thread::Builder::new()
316            .name("native-context-engine".to_owned())
317            .spawn(move || {
318                #[cfg(test)]
319                if let Some(control) = adapter.materialized_test_control.as_ref() {
320                    control.release.wait();
321                }
322                let result = adapter.invoke_materialized_with_output_identity(&invocation, &input);
323                #[cfg(test)]
324                if let Some(control) = adapter.materialized_test_control.as_ref() {
325                    control.completed.wait();
326                }
327                let _ = tx.send(result);
328            })
329            .map_err(|error| {
330                NativeContextInvocationFailure::InvalidRequest(format!(
331                    "start native context worker: {error}"
332                ))
333            })?;
334        rx.recv_timeout(timeout).map_err(|error| match error {
335            std::sync::mpsc::RecvTimeoutError::Timeout => {
336                NativeContextInvocationFailure::ResourceLimit(
337                    "native context execution exceeded its deadline".to_owned(),
338                )
339            }
340            std::sync::mpsc::RecvTimeoutError::Disconnected => {
341                NativeContextInvocationFailure::InvalidRequest(
342                    "native context worker disconnected".to_owned(),
343                )
344            }
345        })?
346    }
347
348    fn validate_engine_invocation(
349        &self,
350        invocation: &CapabilityInvocation,
351    ) -> Result<(), NativeContextInvocationFailure> {
352        invocation
353            .validate()
354            .map_err(|error| NativeContextInvocationFailure::InvalidRequest(error.to_string()))?;
355        if invocation.capability_id != self.manifest().capability_id.as_str()
356            || invocation.capability_version != self.manifest().version
357        {
358            return Err(NativeContextInvocationFailure::InvalidRequest(
359                "invocation capability identity does not match adapter manifest".into(),
360            ));
361        }
362        Ok(())
363    }
364}
365
366impl Default for NativeContextAdapter {
367    fn default() -> Self {
368        Self::new()
369    }
370}
371
372impl CapabilityAdapter for NativeContextAdapter {
373    fn manifest(&self) -> &CapabilityManifestV1 {
374        manifest()
375    }
376
377    fn invoke(&self, invocation: CapabilityInvocation) -> OclaResult<CapabilityResult> {
378        self.invoke_with_output_identity(&invocation)
379            .map(|result| result.result)
380            .map_err(NativeContextInvocationFailure::into_ocla_error)
381    }
382
383    fn health_check(&self) -> OclaResult<bool> {
384        Ok(self.root.is_dir() && self.manifest().validate().is_ok())
385    }
386}
387
388fn truncate_to_tokens(content: &str, budget: u64) -> String {
389    if budget == 0 {
390        return String::new();
391    }
392    if tokens::count_tokens(content) as u64 <= budget {
393        return content.to_owned();
394    }
395    crate::core::budget::truncate_to_token_budget(
396        content,
397        usize::try_from(budget).unwrap_or(usize::MAX),
398    )
399}
400
401fn compression_rate_milli(input_tokens: u64, output_tokens: u64) -> u64 {
402    if input_tokens == 0 {
403        return 0;
404    }
405    input_tokens
406        .saturating_sub(output_tokens)
407        .saturating_mul(1000)
408        .checked_div(input_tokens)
409        .unwrap_or(0)
410        .min(1000)
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416    use crate::core::ocla::invocation::{CapabilityAdapter, PolicyConstraints};
417
418    #[test]
419    fn manifest_is_pinned_to_native_path() {
420        assert!(MANIFEST_PATH.ends_with("context-optimization-v1.json"));
421        assert_eq!(manifest().provider, "leanctx");
422    }
423
424    #[test]
425    fn rejects_non_context_inputs() {
426        let adapter = NativeContextAdapter::new();
427        let invocation = CapabilityInvocation {
428            task_id: "task-1".into(),
429            capability_id: adapter.manifest().capability_id.as_str().into(),
430            capability_version: adapter.manifest().version.clone(),
431            input: CapabilityInput::ShellCommand {
432                command: "printf test".into(),
433                workdir: None,
434            },
435            policy_constraints: PolicyConstraints::default(),
436            timeout_ms: 100,
437        };
438        assert!(adapter.invoke(invocation).is_err());
439    }
440
441    #[test]
442    fn internal_output_identity_is_canonical_sha256_without_payload() {
443        let root = tempfile::tempdir().expect("temporary adapter root");
444        std::fs::write(root.path().join("fixture.md"), "hello context").expect("fixture write");
445        let adapter = NativeContextAdapter::with_root(root.path());
446        let invocation = CapabilityInvocation {
447            task_id: "task-1".into(),
448            capability_id: adapter.manifest().capability_id.as_str().into(),
449            capability_version: adapter.manifest().version.clone(),
450            input: CapabilityInput::ContextRequest {
451                paths: vec!["fixture.md".into()],
452                mode: "raw".into(),
453                budget_tokens: None,
454            },
455            policy_constraints: PolicyConstraints::default(),
456            timeout_ms: 0,
457        };
458
459        let result = adapter
460            .invoke_with_output_identity(&invocation)
461            .expect("native invocation succeeds");
462        assert_eq!(
463            result.output_digest,
464            format!(
465                "sha256:{}",
466                crate::core::agent_identity::hex_encode(&Sha256::digest(b"hello context"))
467            )
468        );
469        assert!(result.result.evidence_ref.is_some());
470    }
471}