lean_ctx/core/ocla/adapters/
native_context.rs1use 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
19pub 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
39pub 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#[allow(dead_code)]
68pub(crate) struct NativeContextInvocationResult {
69 pub result: CapabilityResult,
70 pub input_digest: String,
71 pub output_digest: String,
72 pub(crate) output: Vec<u8>,
76}
77
78#[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 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 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 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}