Skip to main content

ainl_context_compiler/
lib.rs

1//! # AINL Context Compiler — LLM context-window assembly
2//!
3//! Phase 6 of [`SELF_LEARNING_INTEGRATION_MAP.md`](../../../docs/SELF_LEARNING_INTEGRATION_MAP.md).
4//!
5//! Multi-segment, role-aware, question-aware prompt orchestration with progressive-enhancement
6//! capability tiers (heuristic → LLM-driven anchored summarization → embedding-based relevance).
7//!
8//! ## Boundary vs `ainl-context-freshness`
9//!
10//! These two crates **share a name prefix but solve different problems**:
11//!
12//! | Crate | Lifecycle phase | "Context" means |
13//! |---|---|---|
14//! | [`ainl-context-freshness`](https://docs.rs/ainl-context-freshness) | **Pre-tool execution policy gate** | the agent's *knowledge of the world* (repo/index state vs HEAD) |
15//! | `ainl-context-compiler` (this crate) | **Prompt assembly / window management** | the *LLM's input context window* (prompt bytes about to be sent) |
16//!
17//! `ainl-context-compiler` *consumes* `ainl-context-freshness` as a per-segment rank-down signal
18//! (stale segments are ranked lower) — see [`relevance::HeuristicScorer`].
19//!
20//! See [`docs/ainl-crates-overview.md`](../../../docs/ainl-crates-overview.md) for the broader
21//! `ainl-*` family map.
22//!
23//! ## Design tiers (auto-detected at runtime)
24//!
25//! - **Tier 0 — Heuristic** (always available): question-token overlap × recency × freshness;
26//!   per-segment compression via [`ainl_compression`].
27//! - **Tier 1 — Anchored summarization** (M2): when a [`summarizer::Summarizer`] is injected,
28//!   older history collapses into a structured `AnchoredSummary` (Factory.ai pattern).
29//! - **Tier 2 — Embedding rerank** (M3): when an [`embedder::Embedder`] is injected, segments are
30//!   reranked by cosine similarity to the latest user message.
31//!
32//! Each tier auto-degrades on per-call failure; the system never blocks on optional capabilities.
33//!
34//! ## Telemetry sink (canonical pattern from §15.4)
35//!
36//! Mirrors [`ainl_compression::CompressionTelemetrySink`] exactly. Hosts implement
37//! [`ContextEmissionSink`] once and pass it via [`orchestrator::ContextCompiler::with_sink`];
38//! everything downstream just emits structured events.
39//!
40//! Telemetry field names come from
41//! [`ainl_contracts::telemetry`](ainl_contracts::telemetry) constants prefixed `CONTEXT_COMPILER_*`
42//! so dashboards, Prometheus exporters, and CI gates reference them consistently across hosts.
43
44#![warn(missing_docs)]
45
46pub mod budget;
47pub mod capability;
48pub mod mcp_ainl_prompt;
49pub mod metrics;
50pub mod orchestrator;
51pub mod relevance;
52pub mod segment;
53pub mod summarizer;
54
55pub mod embedder;
56#[cfg(feature = "sources-failure-warnings")]
57pub mod failure_recall;
58#[cfg(feature = "sources-trajectory-recap")]
59pub mod trajectory_recap;
60
61pub use budget::BudgetPolicy;
62pub use capability::{CapabilityProbe, Tier};
63pub use embedder::{cosine, Embedder, EmbedderError, PlaceholderEmbedder};
64#[cfg(feature = "sources-failure-warnings")]
65pub use failure_recall::memory_block_for_user_query;
66pub use mcp_ainl_prompt::{
67    mcp_ainl_run_adapters_cheatsheet_segment, MCP_AINL_RUN_ADAPTERS_CHEATSHEET,
68};
69pub use metrics::{ContextCompilerMetrics, SegmentMetrics};
70pub use orchestrator::{ComposedPrompt, ContextCompiler};
71pub use relevance::{HeuristicScorer, RelevanceScore, RelevanceScorer};
72pub use segment::{Role, Segment, SegmentKind};
73pub use summarizer::{AnchoredSummary, AnchoredSummarySection, Summarizer, SummarizerError};
74#[cfg(feature = "sources-trajectory-recap")]
75pub use trajectory_recap::format_trajectory_recap_lines;
76
77use std::sync::Arc;
78
79/// Optional structured telemetry sink for context-compiler events.
80///
81/// Mirrors [`ainl_compression::CompressionTelemetrySink`] in shape. Hosts (e.g. `openfang-runtime`)
82/// provide a single sink impl that bridges these events to their event bus / SSE stream / audit
83/// log. Other AINL hosts (`ainl-inference-server`, `ainativelang` MCP) can pass `None` to opt out
84/// entirely.
85///
86/// See `SELF_LEARNING_INTEGRATION_MAP.md` §15.4 for the canonical pattern.
87pub trait ContextEmissionSink: Send + Sync {
88    /// Emit a single context-compiler event.
89    fn emit(&self, event: ContextCompilerEvent);
90}
91
92/// Structured event emitted by [`ContextCompiler`] during prompt composition.
93///
94/// All variants are intentionally cheap to construct (no large captures) so emission stays under
95/// 1 ms even under high-frequency turns.
96#[derive(Debug, Clone)]
97pub enum ContextCompilerEvent {
98    /// A single segment survived selection and was emitted into the composed prompt.
99    BlockEmitted {
100        /// Source identifier (e.g. `"system_prompt"`, `"recent_turn"`, `"tool_result"`).
101        source: &'static str,
102        /// Coarse segment kind for dashboard grouping.
103        kind: SegmentKind,
104        /// Original token estimate (pre-compression).
105        original_tokens: usize,
106        /// Token estimate after per-segment compression / pruning.
107        kept_tokens: usize,
108    },
109    /// Total budget allocated and the per-kind reservation.
110    BudgetAllocated {
111        /// Total prompt-window budget in token estimate.
112        total: usize,
113        /// Per-kind reserved tokens (sum may be ≤ `total`).
114        per_kind: Vec<(SegmentKind, usize)>,
115    },
116    /// Capability tier selected for this `compose()` call.
117    TierSelected {
118        /// Which tier the orchestrator activated.
119        tier: Tier,
120        /// Short reason code (e.g. `"summarizer_present"`, `"heuristic_only"`).
121        reason: &'static str,
122    },
123    /// Summarizer was invoked successfully (Tier ≥ 1).
124    SummarizerInvoked {
125        /// Wall-clock duration of the summarizer call.
126        duration_ms: u64,
127        /// Number of segments fed into the summarizer.
128        segments_in: usize,
129        /// Token estimate of the resulting summary.
130        summary_tokens: usize,
131    },
132    /// Summarizer call failed; orchestrator auto-degraded to heuristic for this turn.
133    SummarizerFailed {
134        /// Wall-clock duration of the failed call.
135        duration_ms: u64,
136        /// Short error kind classifier (e.g. `"timeout"`, `"http"`, `"parse"`).
137        error_kind: &'static str,
138    },
139    /// Total budget was exceeded even after compaction; safety-net truncation applied.
140    BudgetExceeded {
141        /// Tokens over budget after best-effort compaction.
142        overage: usize,
143    },
144}
145
146/// Convenience type alias used throughout the crate.
147pub type SinkRef = Option<Arc<dyn ContextEmissionSink>>;
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use std::sync::Mutex;
153
154    struct CapturingSink {
155        events: Mutex<Vec<ContextCompilerEvent>>,
156    }
157
158    impl ContextEmissionSink for CapturingSink {
159        fn emit(&self, event: ContextCompilerEvent) {
160            self.events.lock().expect("lock").push(event);
161        }
162    }
163
164    #[test]
165    fn sink_trait_is_object_safe() {
166        let sink: Arc<dyn ContextEmissionSink> = Arc::new(CapturingSink {
167            events: Mutex::new(Vec::new()),
168        });
169        sink.emit(ContextCompilerEvent::TierSelected {
170            tier: Tier::Heuristic,
171            reason: "test",
172        });
173    }
174}