Skip to main content

lean_ctx/core/engine_interface/
context_plan.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! External planning adapter; selection and policy stay in the canonical kernel.
4
5#[path = "context_plan/materialize.rs"]
6pub(crate) mod materialize_impl;
7pub mod presenter;
8pub mod sources;
9
10#[cfg(test)]
11#[path = "context_plan/source_handoff_tests.rs"]
12mod source_handoff_tests;
13
14use std::path::Path;
15
16use chrono::{DateTime, FixedOffset};
17
18use lean_ctx_protocol::{
19    EngineContextPlanRequestV1, EngineContextPlanResponseV1,
20    EngineContextSourceMaterializationRequestV1, EngineContextSourceMaterializationResponseV1,
21    SemanticVersion,
22};
23
24use super::{
25    ENGINE_INTERFACE_VERSION, ENGINE_TRANSPORT_VERSION, EngineTransportError, bind_transport_root,
26};
27use crate::core::context_field::TokenBudget;
28use crate::core::context_kernel::{
29    autopilot::{AutopilotController, TaskAutopilotDecision},
30    bridge::reference_input,
31    enforce::KernelMode,
32    types::RetrievalContext,
33};
34
35/// Replan and materialize an explicit source plan using the canonical planner.
36///
37/// The presentations and the lineage recorded here belong to the presenter
38/// of the request in scope (`core::archive::presented`, "Presenters"): this
39/// process, unless [`presenter::materialize_v2`] runs it under an issued
40/// capability. No field of `request` names or selects a presenter.
41pub fn materialize(
42    root: &Path,
43    request: &EngineContextSourceMaterializationRequestV1,
44) -> Result<EngineContextSourceMaterializationResponseV1, &'static str> {
45    materialize_impl::materialize_source_plan(root, request)
46}
47
48/// Preserve the actual private kernel decision alongside materialized context.
49pub(crate) fn materialize_sources_with_decision(
50    root: &Path,
51    request: &EngineContextSourceMaterializationRequestV1,
52) -> Result<
53    (
54        EngineContextSourceMaterializationResponseV1,
55        TaskAutopilotDecision,
56    ),
57    &'static str,
58> {
59    materialize_impl::materialize_source_plan_with_decision(root, request)
60}
61
62/// Local operator authority only: no tenant authorization is inferred from task labels.
63pub fn plan(
64    root: &Path,
65    request: EngineContextPlanRequestV1,
66) -> Result<EngineContextPlanResponseV1, &'static str> {
67    response(&plan_with_controller(root, request, None)?)
68}
69
70fn plan_with_controller(
71    root: &Path,
72    request: EngineContextPlanRequestV1,
73    controller: Option<AutopilotController>,
74) -> Result<TaskAutopilotDecision, &'static str> {
75    plan_with_controller_at(
76        root,
77        request,
78        controller,
79        Some(chrono::Utc::now().fixed_offset()),
80    )
81}
82
83/// Run the canonical planner at an explicit identity time, never an admission grant.
84///
85/// The time is only an input to the kernel's retention-aware plan identity;
86/// callers must separately perform fresh admission checks before materializing
87/// a replayed plan.
88pub(crate) fn plan_with_controller_at(
89    root: &Path,
90    request: EngineContextPlanRequestV1,
91    controller: Option<AutopilotController>,
92    evaluation_time: Option<DateTime<FixedOffset>>,
93) -> Result<TaskAutopilotDecision, &'static str> {
94    request.validate_payload().map_err(|_| "invalid_request")?;
95    let root = bind_transport_root(root).map_err(EngineTransportError::code)?;
96    let root = root.to_str().ok_or("unsafe_root")?;
97    let mut input = reference_input(
98        RetrievalContext {
99            task: Some(request.query.clone()),
100            query: request.query,
101            project_root: root.to_owned(),
102            budget: TokenBudget {
103                total: request.budget_tokens as usize,
104                used: 0,
105            },
106            max_candidates: usize::from(request.max_candidates),
107        },
108        // The external surface cannot select shadow mode to relax host policy.
109        KernelMode::Enforce,
110    )
111    .map_err(|_| "context_policy_unavailable")?;
112    input.evaluation_time = evaluation_time;
113    controller
114        .unwrap_or_else(|| AutopilotController::for_project(root))
115        .plan_for_task(request.task_id, &input, None)
116        .map_err(|_| "context_planning_failed")
117}
118
119fn response(decision: &TaskAutopilotDecision) -> Result<EngineContextPlanResponseV1, &'static str> {
120    Ok(EngineContextPlanResponseV1 {
121        schema_version: 1,
122        transport_version: ENGINE_TRANSPORT_VERSION,
123        engine_interface_version: SemanticVersion::new(ENGINE_INTERFACE_VERSION)
124            .map_err(|_| "internal")?,
125        plan: decision.context_projection().clone(),
126    })
127}