Skip to main content

lean_ctx/core/context_kernel/
client_wiring.rs

1//! Request-scoped wiring for client profiles, identity, and live ETPAO metrics.
2
3use super::client_profile::{self, ClientEfficiencyProfile};
4use super::context_broker::{BrokerBudget, ContextBroker};
5use super::coverage_class::{self, CoverageClass};
6use super::etpao_live::{EtpaoLive, OutcomeMetrics, RequestMetrics};
7use super::identity::CallerIdentity;
8use super::identity_resolver;
9
10/// Client and caller metadata resolved for one request.
11#[derive(Debug, Clone)]
12pub struct RequestContext {
13    /// Identity used for attribution and cost-center accounting.
14    pub identity: CallerIdentity,
15    /// Client capabilities detected from transport headers.
16    pub profile: ClientEfficiencyProfile,
17    /// Effective integration coverage for this request path.
18    pub coverage: CoverageClass,
19    /// Token allocations computed for the detected client.
20    pub broker_budget: BrokerBudget,
21}
22
23/// Amount of context optimization available for a request.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum OptimizationLevel {
26    /// Compress, route, cache, and measure inline traffic.
27    Full,
28    /// Compress and cache context delivered through MCP.
29    #[default]
30    Partial,
31    /// Collect metrics without modifying request context.
32    ObserveOnly,
33    /// Leave unmanaged traffic unchanged.
34    None,
35}
36
37/// Resolves request identity, coverage, profile, and broker token allocations.
38#[must_use]
39pub fn build_request_context(
40    headers: &[(String, String)],
41    has_proxy: bool,
42    has_mcp: bool,
43    has_hooks: bool,
44) -> RequestContext {
45    let identity = identity_resolver::resolve_with_defaults(headers);
46    let coverage = coverage_class::detect_coverage(has_proxy, has_mcp, has_hooks);
47    let mut profile = client_profile::detect_from_headers(headers);
48    profile.coverage = coverage;
49    let broker_budget = ContextBroker::new(profile.clone()).compute_budget();
50
51    RequestContext {
52        identity,
53        profile,
54        coverage,
55        broker_budget,
56    }
57}
58
59/// Returns whether lean-ctx can modify context for the request.
60#[must_use]
61pub fn should_optimize(ctx: &RequestContext) -> bool {
62    coverage_class::is_addressable(ctx.coverage)
63}
64
65/// Maps request coverage to the supported optimization level.
66#[must_use]
67pub fn optimization_level(ctx: &RequestContext) -> OptimizationLevel {
68    match ctx.coverage {
69        CoverageClass::FullInline => OptimizationLevel::Full,
70        CoverageClass::ContextControlled => OptimizationLevel::Partial,
71        CoverageClass::ObserveOnly => OptimizationLevel::ObserveOnly,
72        CoverageClass::Unmanaged => OptimizationLevel::None,
73    }
74}
75
76/// Records request token usage with detected client and coverage metadata.
77pub fn record_request_etpao(
78    etpao: &mut EtpaoLive,
79    ctx: &RequestContext,
80    input_tokens: usize,
81    output_tokens: usize,
82) {
83    etpao.record_request(RequestMetrics {
84        input_tokens,
85        output_tokens,
86        reasoning_tokens: 0,
87        schema_tokens: 0,
88        cache_write_tokens: 0,
89        retry_count: 0,
90        client_id: ctx.profile.client_id.clone(),
91        coverage_class: ctx.coverage,
92    });
93}
94
95/// Records an evaluated outcome with detected client attribution.
96pub fn record_outcome_etpao(
97    etpao: &mut EtpaoLive,
98    ctx: &RequestContext,
99    accepted: bool,
100    quality: f64,
101) {
102    etpao.record_outcome(OutcomeMetrics {
103        accepted,
104        quality_score: quality,
105        first_pass: accepted,
106        client_id: ctx.profile.client_id.clone(),
107    });
108}
109
110/// Formats a compact request summary for diagnostics.
111#[must_use]
112pub fn format_request_summary(ctx: &RequestContext) -> String {
113    format!(
114        "[{}] user={} team={} budget={}",
115        coverage_class::coverage_label(ctx.coverage),
116        ctx.identity.user_id.as_deref().unwrap_or("-"),
117        ctx.identity.team_id.as_deref().unwrap_or("-"),
118        ctx.broker_budget.context_tokens
119    )
120}
121
122#[cfg(test)]
123mod tests {
124    use super::{
125        OptimizationLevel, build_request_context, format_request_summary, optimization_level,
126        record_outcome_etpao, record_request_etpao, should_optimize,
127    };
128    use crate::core::context_kernel::coverage_class::CoverageClass;
129    use crate::core::context_kernel::etpao_live::EtpaoLive;
130
131    fn context(coverage: CoverageClass) -> super::RequestContext {
132        let (has_proxy, has_mcp, has_hooks) = match coverage {
133            CoverageClass::FullInline => (true, false, false),
134            CoverageClass::ContextControlled => (false, true, false),
135            CoverageClass::ObserveOnly => (false, false, true),
136            CoverageClass::Unmanaged => (false, false, false),
137        };
138        build_request_context(&[], has_proxy, has_mcp, has_hooks)
139    }
140
141    #[test]
142    fn build_context_from_empty_headers() {
143        let ctx = build_request_context(&[], false, false, false);
144        assert_eq!(ctx.profile.client_id, "unknown");
145        assert_eq!(ctx.profile.coverage, CoverageClass::Unmanaged);
146        assert_eq!(ctx.coverage, CoverageClass::Unmanaged);
147        assert!(ctx.broker_budget.context_tokens > 0);
148    }
149
150    #[test]
151    fn build_context_with_proxy() {
152        let ctx = build_request_context(&[], true, false, false);
153        assert_eq!(ctx.coverage, CoverageClass::FullInline);
154        assert_eq!(ctx.profile.coverage, CoverageClass::FullInline);
155    }
156
157    #[test]
158    fn should_optimize_full_inline() {
159        assert!(should_optimize(&context(CoverageClass::FullInline)));
160    }
161
162    #[test]
163    fn should_optimize_unmanaged() {
164        assert!(!should_optimize(&context(CoverageClass::Unmanaged)));
165    }
166
167    #[test]
168    fn optimization_level_mapping() {
169        for (coverage, expected) in [
170            (CoverageClass::FullInline, OptimizationLevel::Full),
171            (CoverageClass::ContextControlled, OptimizationLevel::Partial),
172            (CoverageClass::ObserveOnly, OptimizationLevel::ObserveOnly),
173            (CoverageClass::Unmanaged, OptimizationLevel::None),
174        ] {
175            assert_eq!(optimization_level(&context(coverage)), expected);
176        }
177    }
178
179    #[test]
180    fn format_summary_includes_identity() {
181        let ctx = build_request_context(&[], false, true, false);
182        let expected_user = format!("user={}", ctx.identity.user_id.as_deref().unwrap_or("-"));
183        let expected_team = format!("team={}", ctx.identity.team_id.as_deref().unwrap_or("-"));
184        let summary = format_request_summary(&ctx);
185        assert!(summary.contains(&expected_user));
186        assert!(summary.contains(&expected_team));
187        assert!(summary.contains("budget="));
188    }
189
190    #[test]
191    fn etpao_recording_works() {
192        let ctx = context(CoverageClass::FullInline);
193        let mut etpao = EtpaoLive::new();
194        record_request_etpao(&mut etpao, &ctx, 100, 25);
195        record_outcome_etpao(&mut etpao, &ctx, true, 0.9);
196
197        let summary = etpao.summary();
198        assert_eq!(etpao.request_count(), 1);
199        assert_eq!(etpao.outcome_count(), 1);
200        assert_eq!(summary.total_tokens, 125);
201        assert_eq!(summary.accepted_outcomes, 1);
202    }
203}