Skip to main content

zeph_agent_context/
state.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Borrow-lens view types used by [`crate::service::ContextService`].
5//!
6//! Each view holds `&`/`&mut` references to the exact sub-fields that the context
7//! service needs. By accepting lenses instead of `&mut Agent<C>`, this crate avoids
8//! depending on `zeph-core` while still letting the call site in `zeph-core` construct
9//! them from disjoint field projections.
10//!
11//! Views are constructed at the call site in `zeph-core` using one literal struct
12//! expression. The borrow checker proves disjointness at that level without additional
13//! helper methods — each `&mut` resolves to a unique field path under `Agent<C>`.
14
15use parking_lot::RwLock;
16use std::borrow::Cow;
17use std::collections::HashSet;
18use std::future::Future;
19use std::path::PathBuf;
20use std::pin::Pin;
21use std::sync::Arc;
22use zeph_common::PlannedToolHint;
23use zeph_common::SecurityEventCategory;
24use zeph_common::task_supervisor::{BlockingHandle, TaskSupervisor};
25use zeph_config::FidelityConfig;
26use zeph_config::{
27    ContextStrategy, DocumentConfig, GraphConfig, PersonaConfig, ReasoningConfig, TrajectoryConfig,
28    TreeConfig,
29};
30use zeph_context::input::CorrectionConfig;
31use zeph_context::manager::ContextManager;
32use zeph_context::summarization::SummarizationDeps;
33use zeph_context::typed_page::TypedPagesState;
34use zeph_llm::any::AnyProvider;
35use zeph_llm::provider::Message;
36use zeph_memory::semantic::SemanticMemory;
37use zeph_memory::{ConversationId, TokenCounter};
38use zeph_sanitizer::ContentSanitizer;
39use zeph_sanitizer::quarantine::QuarantinedSummarizer;
40use zeph_skills::proactive::ProactiveExplorer;
41use zeph_skills::registry::SkillRegistry;
42
43use crate::compaction::{SubgoalExtractionResult, SubgoalRegistry};
44
45/// Borrow-lens over the agent's conversation window fields.
46///
47/// Holds `&mut` references to every message-list field that the context service
48/// needs to read or write. Constructed by the `zeph-core` shim from disjoint
49/// sub-fields of `Agent<C>::msg`.
50pub struct MessageWindowView<'a> {
51    /// Full message history. The context service reads and filters this list.
52    pub messages: &'a mut Vec<Message>,
53    /// `SQLite` row ID of the most recently persisted message.
54    pub last_persisted_message_id: &'a mut Option<i64>,
55    /// `SQLite` row IDs to be soft-deleted after context assembly completes.
56    pub deferred_db_hide_ids: &'a mut Vec<i64>,
57    /// Deferred summary strings to be appended after context assembly completes.
58    pub deferred_db_summaries: &'a mut Vec<String>,
59    /// Running token count for the current prompt window — updated after every
60    /// message-list mutation to keep provider call budgets accurate.
61    /// Maps to `Agent<C>::runtime.providers.cached_prompt_tokens`.
62    pub cached_prompt_tokens: &'a mut u64,
63    /// Shared token counter — cheap `Arc` clone from `Agent<C>::runtime.metrics.token_counter`.
64    pub token_counter: Arc<TokenCounter>,
65    /// Tool IDs that completed successfully in the current session.
66    /// Maps to `Agent<C>::services.tool_state.completed_tool_ids`.
67    /// Cleared by `clear_history` together with the message list.
68    pub completed_tool_ids: &'a mut HashSet<String>,
69}
70
71/// Accumulated metric deltas for one context-assembly pass.
72///
73/// Holds owned counters that the service increments during `prepare_context`.
74/// After the call returns, the `zeph-core` shim applies these deltas to the agent's
75/// metrics snapshot via `update_metrics`. Using owned values (not references) avoids
76/// borrowing into `MetricsSnapshot`, which lives behind a watch channel.
77#[derive(Debug, Default)]
78pub struct MetricsCounters {
79    /// Sanitizer checks performed during this pass.
80    pub sanitizer_runs: u64,
81    /// Injection flags raised during this pass.
82    pub sanitizer_injection_flags: u64,
83    /// Truncations applied during this pass.
84    pub sanitizer_truncations: u64,
85    /// Quarantine invocations during this pass.
86    pub quarantine_invocations: u64,
87    /// Quarantine failures during this pass.
88    pub quarantine_failures: u64,
89}
90
91/// Abstract sink for security events raised during context assembly.
92///
93/// Implemented in `zeph-core` by a stack-local adapter that appends to
94/// `Agent<C>::runtime.metrics.security_events`. Using a trait keeps this crate
95/// free of `zeph-core` internal types.
96pub trait SecurityEventSink: Send {
97    /// Record a security event.
98    fn push(&mut self, category: SecurityEventCategory, source: &'static str, detail: String);
99}
100
101/// Borrow-lens over all fields needed for `prepare_context` and `Agent<C>::rebuild_system_prompt`.
102///
103/// Every field maps to a single sub-field of `Agent<C>` and uses a type from a
104/// lower-level crate (`zeph-memory`, `zeph-skills`, `zeph-context`, `zeph-sanitizer`,
105/// `zeph-config`, `zeph-common`, `zeph-llm`). No `zeph-core`-internal `*State`
106/// aggregator ever crosses this boundary.
107///
108/// Constructed by the `zeph-core` shim using one literal struct expression. The
109/// borrow checker verifies disjointness because no two `&mut` paths share a prefix.
110pub struct ContextAssemblyView<'a> {
111    // ── Memory (one mut field; the rest are read-only clones/copies) ─────────────────
112    /// `services.memory.persistence.memory` — `Arc` clone is cheap.
113    pub memory: Option<Arc<SemanticMemory>>,
114    /// `services.memory.persistence.conversation_id`.
115    pub conversation_id: Option<ConversationId>,
116    /// `services.memory.persistence.recall_limit`.
117    pub recall_limit: usize,
118    /// `services.memory.persistence.cross_session_score_threshold`.
119    pub cross_session_score_threshold: f32,
120    /// `services.memory.persistence.context_format` — determines recall entry formatting.
121    pub context_format: zeph_config::ContextFormat,
122    /// `services.memory.persistence.last_recall_confidence` — written by apply path.
123    pub last_recall_confidence: &'a mut Option<f32>,
124
125    /// `services.memory.compaction.context_strategy` (Copy enum).
126    pub context_strategy: ContextStrategy,
127    /// `services.memory.compaction.crossover_turn_threshold`.
128    pub crossover_turn_threshold: u32,
129    /// `services.memory.compaction.cached_session_digest` — cloned into assembler input.
130    ///
131    /// The `usize` is the token count of the digest (used by `ContextMemoryView`).
132    pub cached_session_digest: Option<(String, usize)>,
133    /// `services.memory.compaction.digest_config.enabled`.
134    pub digest_enabled: bool,
135
136    /// `services.memory.extraction.graph_config` — cloned (small, `Clone`).
137    pub graph_config: GraphConfig,
138    /// `services.memory.extraction.document_config` — cloned.
139    pub document_config: DocumentConfig,
140    /// `services.memory.extraction.persona_config` — cloned.
141    pub persona_config: PersonaConfig,
142    /// `services.memory.extraction.trajectory_config` — cloned.
143    pub trajectory_config: TrajectoryConfig,
144    /// `services.memory.extraction.reasoning_config` — cloned.
145    pub reasoning_config: ReasoningConfig,
146    /// `services.memory.extraction.memcot_config` — cloned.
147    pub memcot_config: zeph_config::MemCotConfig,
148    /// Current `MemCoT` semantic state buffer. `Some` when the accumulator has a non-empty state.
149    ///
150    /// Snapshot taken at context-assembly time; used to prefix graph recall queries.
151    pub memcot_state: Option<String>,
152    /// `services.memory.subsystems.tree_config` — cloned.
153    pub tree_config: TreeConfig,
154
155    // ── Skill ─────────────────────────────────────────────────────────────────────────
156    /// `services.skill.last_skills_prompt` — written by `Agent<C>::rebuild_system_prompt`.
157    pub last_skills_prompt: &'a mut String,
158    /// `services.skill.active_skill_names` — written by `Agent<C>::rebuild_system_prompt`.
159    pub active_skill_names: &'a mut Vec<String>,
160    /// `services.skill.registry` — `Arc` clone enables concurrent read access.
161    pub skill_registry: Arc<RwLock<SkillRegistry>>,
162    /// `services.skill.skill_paths` — read during proactive reload.
163    pub skill_paths: &'a [PathBuf],
164
165    // ── Index (feature-gated) ─────────────────────────────────────────────────────────
166    /// Built at the shim by `IndexState::as_index_access()`. The lifetime reflects
167    /// the borrow back into `services.index`.
168    ///
169    /// Only populated when the `index` feature is enabled.
170    #[cfg(feature = "index")]
171    pub index: Option<&'a dyn zeph_context::input::IndexAccess>,
172
173    // ── Learning / sidequest / proactive ──────────────────────────────────────────────
174    /// Built at the shim from `services.learning_engine.config` — the engine itself
175    /// never crosses the crate boundary.
176    pub correction_config: Option<CorrectionConfig>,
177    /// `services.sidequest.turn_counter`.
178    pub sidequest_turn_counter: u64,
179    /// `services.proactive_explorer` — `Arc` clone for async use without borrowing self.
180    pub proactive_explorer: Option<Arc<ProactiveExplorer>>,
181
182    // ── Security ──────────────────────────────────────────────────────────────────────
183    /// `services.security.sanitizer` — borrowed from `SecurityState`; not Arc-wrapped in `zeph-core`.
184    pub sanitizer: &'a ContentSanitizer,
185    /// `services.security.quarantine_summarizer` — borrowed from `SecurityState`.
186    pub quarantine_summarizer: Option<&'a QuarantinedSummarizer>,
187
188    // ── Context manager ───────────────────────────────────────────────────────────────
189    /// `self.context_manager` — mutably borrowed for token recompute hooks.
190    pub context_manager: &'a mut ContextManager,
191
192    // ── Runtime / metrics ─────────────────────────────────────────────────────────────
193    /// `runtime.metrics.token_counter` — `Arc` clone is cheap.
194    pub token_counter: Arc<zeph_memory::TokenCounter>,
195    /// Accumulated metric deltas — incremented during the pass, applied to the metrics
196    /// snapshot by the `zeph-core` shim after `prepare_context` returns.
197    pub metrics: MetricsCounters,
198    /// Abstract sink for security events raised during context assembly.
199    pub security_events: &'a mut dyn SecurityEventSink,
200    /// `runtime.providers.cached_prompt_tokens` — read for compression-spectrum ratio.
201    pub cached_prompt_tokens: u64,
202
203    // ── Config flags ──────────────────────────────────────────────────────────────────
204    /// `runtime.config.redact_credentials`.
205    pub redact_credentials: bool,
206    /// `runtime.config.channel_skills` — per-channel skill filter for system prompt rebuild.
207    pub channel_skills: &'a [String],
208
209    // ── Credential scrubber ───────────────────────────────────────────────────────────
210    /// Function pointer for scrubbing credentials from message content.
211    ///
212    /// Passed as a function pointer so `zeph-agent-context` does not need to depend on
213    /// `zeph-core::redact`. The shim in `zeph-core` sets this to `crate::redact::scrub_content`.
214    /// When `redact_credentials = false` the service does not call this function.
215    pub scrub: fn(&str) -> Cow<'_, str>,
216
217    // ── MemFlow tiered retrieval (#3712) ──────────────────────────────────────────────
218    /// `MemFlow` tiered retrieval configuration (`[memory.tiered_retrieval]`).
219    ///
220    /// When `enabled = true`, `inject_semantic_recall` dispatches to [`zeph_memory::recall_tiered`]
221    /// instead of the flat `fetch_semantic_recall_raw` path.
222    pub tiered_retrieval_config: zeph_config::memory::TieredRetrievalConfig,
223    /// Optional provider for LLM-backed intent classification in tiered retrieval.
224    ///
225    /// Resolved from `tiered_retrieval.classifier_provider` at agent construction.
226    /// `None` means the `HeuristicRouter` is used (no LLM call).
227    pub tiered_retrieval_classifier: Option<Arc<zeph_llm::any::AnyProvider>>,
228    /// Optional provider for evidence quality validation and tier escalation.
229    ///
230    /// Resolved from `tiered_retrieval.validator_provider` at agent construction.
231    /// `None` means validation is skipped (evidence accepted as-is).
232    pub tiered_retrieval_validator: Option<Arc<zeph_llm::any::AnyProvider>>,
233
234    // ── MemGuard type-aware retrieval composition (spec 004-16, #6086) ───────────────────
235    /// Type-aware retrieval composition configuration (`[memory.type_aware_compose]`).
236    ///
237    /// When `enabled = true`, `schedule_context_fetchers` composes only the functional memory
238    /// types in the active set (resolved from `default_compose_types` and, when
239    /// `intent_scoped`, a static per-intent widening) instead of every source unconditionally.
240    /// Retrieval-only: no write-path or storage change. `enabled = false` (default) is a
241    /// byte-for-byte no-op.
242    pub type_aware_compose_config: zeph_config::memory::TypeAwareComposeConfig,
243
244    // ── CAM: Context-Adaptive Memory (#4547) ─────────────────────────────────
245    /// Fidelity scoring configuration resolved from `[memory.fidelity]`.
246    ///
247    /// `None` when fidelity scoring is not configured (treated as `enabled = false`).
248    /// `Some(&cfg)` with `cfg.enabled = false` is also a no-op (early-return inside scorer).
249    pub fidelity_config: Option<&'a FidelityConfig>,
250    /// LLM provider used for query and per-message embeddings when
251    /// `fidelity_config.semantic_scoring_provider` is set. Resolved at construction time.
252    /// `None` → keyword overlap fallback is used.
253    pub fidelity_semantic_provider: Option<Arc<zeph_llm::any::AnyProvider>>,
254    /// LLM provider used for `Compressed` rendering when `fidelity_config.compress_provider`
255    /// is set. Resolved by the agent from `[[llm.providers]]` at construction time.
256    /// `None` → truncation fallback is used.
257    pub fidelity_compress_provider: Option<Arc<zeph_llm::any::AnyProvider>>,
258    /// Lookahead tool hints derived from the orchestration DAG.
259    ///
260    /// Empty slice when no DAG lookahead is available (PAACE deferred to P2). The scorer
261    /// simply zeroes the plan signal when the slice is empty.
262    pub planned_next_tools: &'a [PlannedToolHint],
263    /// TUI status channel for spinner updates during fidelity scoring.
264    ///
265    /// Mirrors the channel wired in `ContextSummarizationView::status_tx`. `None` in
266    /// non-TUI modes; the service skips sending when the sender is absent.
267    pub status_tx: Option<tokio::sync::mpsc::UnboundedSender<String>>,
268    /// Background task supervisor for registering `JoinHandle`s produced during context
269    /// assembly (e.g. `mark_reasoning_used` in `fetch_reasoning_strategies`).
270    ///
271    /// Handles drained from `PreparedContext::background_tasks` are wrapped and
272    /// registered here so they remain tracked and abortable instead of being silently
273    /// dropped when `PreparedContext` goes out of scope.
274    pub task_supervisor: Arc<TaskSupervisor>,
275}
276
277/// Values produced by [`crate::service::ContextService::prepare_context`] that must be applied by the caller.
278///
279/// `ContextService` cannot inject code context directly because `inject_code_context` touches
280/// the system prompt (position-0 message), which involves subsystems beyond the context-window
281/// boundary. Instead, the service returns the code-context body and the caller applies it.
282#[derive(Debug, Default)]
283pub struct ContextDelta {
284    /// Sanitized code-context body to inject into the system prompt by the `Agent<C>` shim.
285    ///
286    /// `None` when no code context was fetched or the fetch returned empty.
287    pub code_context: Option<String>,
288}
289
290/// Borrow-lens over all fields needed for compaction and summarization operations.
291///
292/// Every field maps to a specific sub-field of `Agent<C>` and uses a type from a
293/// crate below `zeph-core` in the dependency graph. Constructed in `zeph-core` using
294/// one literal struct expression; the borrow checker verifies disjointness.
295///
296/// The view covers: message history mutation, deferred summary queues, context-manager
297/// compaction state, provider handles for LLM calls, memory persistence for flushing,
298/// subgoal registry for context-compression strategies, and background task handles for
299/// non-blocking goal/subgoal extraction.
300pub struct ContextSummarizationView<'a> {
301    // ── Message window ────────────────────────────────────────────────────────
302    /// Full conversation history. Mutated by pruning, compaction, and deferred summary
303    /// application.
304    pub messages: &'a mut Vec<Message>,
305    /// `SQLite` row IDs to be soft-deleted after deferred summaries are applied.
306    pub deferred_db_hide_ids: &'a mut Vec<i64>,
307    /// Summary strings paired with the hide IDs above — flushed to `SQLite` as a batch.
308    pub deferred_db_summaries: &'a mut Vec<String>,
309    /// Worst-case `MessageMetadata::trust_level` of the summarized tool-pair, one entry per
310    /// `deferred_db_summaries` element at the same index (issue #6558 follow-up, S3) — carries
311    /// the memory-consent gate's context tag through to the persisted summary row so it
312    /// survives a session reload, not just the in-memory summary message.
313    pub deferred_db_trust_levels: &'a mut Vec<Option<u8>>,
314    /// Running token count for the current prompt window. Updated after every mutation
315    /// that changes message content.
316    pub cached_prompt_tokens: &'a mut u64,
317
318    // ── Context manager ───────────────────────────────────────────────────────
319    /// Full context manager — contains compaction state, thresholds, strategy config.
320    pub context_manager: &'a mut ContextManager,
321
322    // ── Runtime ───────────────────────────────────────────────────────────────
323    /// Whether server-side compaction is currently active (skip client compaction when
324    /// true, unless context has grown past the safety fallback threshold).
325    pub server_compaction_active: bool,
326    /// Token counter used for budget calculations and prompt recomputation.
327    pub token_counter: Arc<TokenCounter>,
328    /// Pre-built summarization deps (provider + timeout + `token_counter` + callbacks).
329    /// Built by the `zeph-core` shim from `build_summarization_deps()` before constructing
330    /// the view, so the view does not need to hold a raw `DebugDumper` reference.
331    pub summarization_deps: SummarizationDeps,
332    /// Background task supervisor for spawning non-blocking goal/subgoal extractions.
333    pub task_supervisor: Arc<TaskSupervisor>,
334
335    // ── Memory persistence ────────────────────────────────────────────────────
336    /// Semantic memory store — used to flush deferred summaries and store session digests.
337    pub memory: Option<Arc<SemanticMemory>>,
338    /// Conversation ID for all SQLite/Qdrant persistence calls.
339    pub conversation_id: Option<ConversationId>,
340    /// Maximum unsummarized tool-call pairs before forced deferred summarization kicks in.
341    pub tool_call_cutoff: usize,
342
343    // ── Context-compression (SubgoalRegistry + task handles) ─────────────────
344    /// In-memory registry of all subgoals in the current session.
345    pub subgoal_registry: &'a mut SubgoalRegistry,
346    /// Handle to the background task-goal extraction spawned last turn.
347    pub pending_task_goal: &'a mut Option<BlockingHandle<Option<String>>>,
348    /// Handle to the background subgoal extraction spawned last turn.
349    pub pending_subgoal: &'a mut Option<BlockingHandle<Option<SubgoalExtractionResult>>>,
350    /// Cached task goal for `TaskAware`/`MIG` pruning. `None` before first extraction.
351    pub current_task_goal: &'a mut Option<String>,
352    /// Hash of the last user message when `current_task_goal` was populated.
353    /// Used to detect when a new extraction is needed.
354    pub task_goal_user_msg_hash: &'a mut Option<u64>,
355    /// Hash of the last user message when subgoal extraction was scheduled.
356    pub subgoal_user_msg_hash: &'a mut Option<u64>,
357    /// TUI / channel status sender for spinner messages. `None` when TUI is disabled.
358    pub status_tx: Option<tokio::sync::mpsc::UnboundedSender<String>>,
359
360    // ── Credential scrubber ───────────────────────────────────────────────────
361    /// Function pointer for scrubbing credentials from summary text.
362    ///
363    /// Set to `crate::redact::scrub_content` by the `zeph-core` shim when
364    /// `redact_credentials = true`, or to a no-op identity function otherwise.
365    pub scrub: fn(&str) -> Cow<'_, str>,
366
367    // ── Compaction callbacks (populated by zeph-core shim) ────────────────────
368    /// Compression guidelines text loaded from `SQLite` by the `zeph-core` shim.
369    ///
370    /// `None` when the feature is disabled or the caller does not load guidelines.
371    /// The service passes the contained string (or `""`) to `summarize_with_llm`. Closes #3528.
372    ///
373    /// Set via [`ContextSummarizationView::with_compression_guidelines`]. Both the reactive
374    /// (`compact_context`) and proactive (`maybe_proactive_compress`) paths populate this field.
375    pub compression_guidelines: Option<String>,
376
377    /// Optional probe-validation callback. When `Some`, the service invokes it after LLM
378    /// summarization and before draining/reinsert. See [`CompactionProbeCallback`] for the
379    /// full implementor contract.
380    pub probe: Option<&'a mut dyn CompactionProbeCallback>,
381
382    /// Optional pre-summary archive hook (Memex #2432). The service calls `archive(to_compact)`
383    /// BEFORE summarization and appends the returned reference list as a postfix AFTER the
384    /// LLM call so the LLM cannot destroy the `[archived:UUID]` markers.
385    pub archive: Option<&'a dyn ToolOutputArchive>,
386
387    /// Optional persistence completion callback. The service calls `after_compaction` once
388    /// the in-memory drain+reinsert is finalized. The optional Qdrant future returned by the
389    /// callback is bubbled back through [`CompactionOutcome::Compacted::qdrant_future`].
390    pub persistence: Option<&'a dyn CompactionPersistence>,
391
392    /// Metrics sink for compaction-related counter increments. Used for
393    /// `compaction_hard_count`, `tool_output_prunes`, and the four probe-outcome counters.
394    /// Closes #3527.
395    pub metrics: Option<&'a dyn MetricsCallback>,
396
397    /// Shared typed-page state for invariant-aware compaction (#3630).
398    ///
399    /// `None` when `[memory.compression.typed_pages] enabled = false`.
400    /// Populated by `CompactionAdapters::populate` in `zeph-core`.
401    pub typed_pages: Option<Arc<TypedPagesState>>,
402
403    // ── CAM: proactive regrade (AgeMem, #4547) ────────────────────────────────
404    /// Fidelity scoring config for proactive regrade in `maybe_compact`.
405    ///
406    /// `None` → proactive regrade is skipped (scoring disabled or config absent).
407    pub fidelity_config: Option<FidelityConfig>,
408    /// LLM provider used for query and per-message embeddings during proactive regrade.
409    /// `None` → keyword overlap fallback is used.
410    pub fidelity_semantic_provider: Option<Arc<zeph_llm::any::AnyProvider>>,
411    /// LLM provider used for `Compressed` rendering during proactive regrade.
412    /// `None` → truncation fallback is used.
413    pub fidelity_compress_provider: Option<Arc<zeph_llm::any::AnyProvider>>,
414    /// Most recent user query — passed to the scorer as the semantic signal source.
415    ///
416    /// Empty string when no query is available (`AgeMem` degrades gracefully to
417    /// temporal + importance signals only).
418    pub current_query: String,
419}
420
421impl ContextSummarizationView<'_> {
422    /// Set the compression guidelines text.
423    ///
424    /// Call this on the view returned by `Agent::summarization_view()` before passing it to
425    /// `ContextService::compact_context`. Using a builder method keeps construction uniform
426    /// and avoids direct field mutation.
427    #[must_use]
428    pub fn with_compression_guidelines(mut self, guidelines: Option<String>) -> Self {
429        self.compression_guidelines = guidelines;
430        self
431    }
432}
433
434/// Bundle of LLM provider handles needed for async context operations.
435///
436/// Each handle is an `Arc`-backed clone, suitable for moving into spawned tasks
437/// or passing across async boundaries.
438pub struct ProviderHandles {
439    /// Primary LLM provider used for completions.
440    pub primary: AnyProvider,
441    /// Dedicated embedding provider.
442    pub embedding: AnyProvider,
443    /// Provider for skill disambiguation classification calls.
444    ///
445    /// Falls back to `primary` when the `[skills] disambiguate_provider` config field is empty.
446    pub disambiguate: AnyProvider,
447    /// Provider used for deferred tool-pair summarization (context compaction).
448    ///
449    /// Falls back to `primary` when the `[memory] compaction_provider` config field is empty.
450    pub compaction: AnyProvider,
451}
452
453/// Abstract status sink for emitting short progress strings to the channel.
454///
455/// Implemented in `zeph-core` by a stack-local adapter wrapping `Channel::send_status`.
456/// Using a trait keeps this crate free of the `Channel` trait from `zeph-core`.
457pub trait StatusSink: Send + Sync {
458    /// Send a short status string to the active channel.
459    fn send_status(&self, msg: &str) -> impl Future<Output = ()> + Send + '_;
460}
461
462/// Abstract gate for applying a skill trust level to the tool executor.
463///
464/// Implemented in `zeph-core` by a thin adapter over `Arc<dyn ErasedToolExecutor>`.
465/// Using a trait keeps this crate free of the tool executor abstraction.
466pub trait TrustGate: Send + Sync {
467    /// Apply the given trust level to the underlying tool executor.
468    fn set_effective_trust(&self, level: zeph_common::SkillTrustLevel);
469}
470
471/// Boxed `'static` future for the off-thread Qdrant session-summary write.
472///
473/// Returned from [`CompactionPersistence::after_compaction`] and bubbled back through
474/// [`CompactionOutcome::Compacted`] / [`CompactionOutcome::CompactedWithPersistError`].
475/// The caller (shim in `zeph-core`) dispatches this through `BackgroundSupervisor::spawn_summarization`.
476/// The future must return `bool` (`false` = success, `true` = error) to match the supervisor API.
477pub type QdrantPersistFuture = Pin<Box<dyn Future<Output = bool> + Send + 'static>>;
478
479/// Return type from `compact_context()` that distinguishes between successful compaction,
480/// probe rejection, and no-op.
481///
482/// Gives `maybe_compact()` enough information to handle probe rejection without triggering
483/// the `Exhausted` state — which would only be correct if summarization itself is stuck.
484#[must_use]
485#[non_exhaustive]
486pub enum CompactionOutcome {
487    /// Messages were drained and replaced with a summary. `SQLite` persistence succeeded.
488    ///
489    /// `qdrant_future` is an optional `'static` future for the off-thread Qdrant write;
490    /// the shim must dispatch it through `BackgroundSupervisor::spawn_summarization` and
491    /// must not await it inline.
492    Compacted {
493        /// Optional Qdrant write future to dispatch via the supervisor.
494        qdrant_future: Option<QdrantPersistFuture>,
495        /// Number of messages folded into the summary.
496        compacted_count: usize,
497    },
498    /// Messages were drained and replaced with a summary, but synchronous `SQLite` persistence
499    /// reported failure. The in-memory state is correct; only persistence failed.
500    CompactedWithPersistError {
501        /// Optional Qdrant write future to dispatch via the supervisor.
502        qdrant_future: Option<QdrantPersistFuture>,
503        /// Number of messages folded into the summary.
504        compacted_count: usize,
505    },
506    /// Probe rejected the summary — original messages are preserved.
507    /// Caller must NOT check `freed_tokens` or transition to `Exhausted`.
508    ProbeRejected,
509    /// No compaction was performed (too few messages, empty `to_compact`, etc.).
510    NoChange,
511}
512
513impl PartialEq for CompactionOutcome {
514    fn eq(&self, other: &Self) -> bool {
515        // Compare variants only; qdrant_future is not comparable (it is a dyn Future).
516        matches!(
517            (self, other),
518            (Self::Compacted { .. }, Self::Compacted { .. })
519                | (
520                    Self::CompactedWithPersistError { .. },
521                    Self::CompactedWithPersistError { .. }
522                )
523                | (Self::ProbeRejected, Self::ProbeRejected)
524                | (Self::NoChange, Self::NoChange)
525        )
526    }
527}
528
529impl std::fmt::Debug for CompactionOutcome {
530    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
531        match self {
532            Self::Compacted {
533                qdrant_future,
534                compacted_count,
535            } => f
536                .debug_struct("Compacted")
537                .field("qdrant_future", &qdrant_future.as_ref().map(|_| "<future>"))
538                .field("compacted_count", compacted_count)
539                .finish(),
540            Self::CompactedWithPersistError {
541                qdrant_future,
542                compacted_count,
543            } => f
544                .debug_struct("CompactedWithPersistError")
545                .field("qdrant_future", &qdrant_future.as_ref().map(|_| "<future>"))
546                .field("compacted_count", compacted_count)
547                .finish(),
548            Self::ProbeRejected => write!(f, "ProbeRejected"),
549            Self::NoChange => write!(f, "NoChange"),
550        }
551    }
552}
553
554impl CompactionOutcome {
555    /// Remove and return the Qdrant persistence future embedded in `Compacted` or
556    /// `CompactedWithPersistError` variants. Returns `None` for `ProbeRejected` / `NoChange`.
557    ///
558    /// The shim calls this immediately after the service returns and dispatches the
559    /// future through `BackgroundSupervisor::spawn_summarization`.
560    pub fn qdrant_future_take(&mut self) -> Option<QdrantPersistFuture> {
561        match self {
562            Self::Compacted { qdrant_future, .. }
563            | Self::CompactedWithPersistError { qdrant_future, .. } => qdrant_future.take(),
564            _ => None,
565        }
566    }
567
568    /// Returns `true` when compaction succeeded (either variant of `Compacted`).
569    #[must_use]
570    pub fn is_compacted(&self) -> bool {
571        matches!(
572            self,
573            Self::Compacted { .. } | Self::CompactedWithPersistError { .. }
574        )
575    }
576
577    /// Returns the number of messages folded into the summary, or `None` when no
578    /// compaction occurred (`ProbeRejected` / `NoChange`).
579    #[must_use]
580    pub fn compacted_count(&self) -> Option<usize> {
581        match self {
582            Self::Compacted {
583                compacted_count, ..
584            }
585            | Self::CompactedWithPersistError {
586                compacted_count, ..
587            } => Some(*compacted_count),
588            _ => None,
589        }
590    }
591}
592
593/// Verdict returned by a [`CompactionProbeCallback`] after evaluating a candidate summary.
594///
595/// The implementor — not the service — is responsible for routing the verdict-specific data
596/// (score, `category_scores`, thresholds) through [`MetricsCallback`] and for calling
597/// `dump_compaction_probe` before returning.
598#[derive(Debug, Clone, Copy, PartialEq, Eq)]
599#[non_exhaustive]
600pub enum ProbeOutcome {
601    /// Probe accepted the summary; pipeline continues normally.
602    Pass,
603    /// Probe soft-rejected; pipeline continues but the summary is flagged as borderline.
604    SoftFail,
605    /// Probe hard-rejected; service must abort and return [`CompactionOutcome::ProbeRejected`].
606    HardFail,
607}
608
609/// Probe-validation callback invoked by `ContextService::compact_context` after the LLM
610/// produces a candidate summary.
611///
612/// # Contract (mandatory)
613///
614/// Implementations MUST, before returning:
615/// 1. Call `dump_compaction_probe(result)` if a debug dumper is configured.
616/// 2. Update verdict-specific metric counters via the appropriate
617///    `MetricsCallback::record_compaction_probe_*` method. The score, `category_scores`,
618///    threshold, and `hard_fail_threshold` travel through the metrics adapter and are not
619///    part of the `ProbeOutcome` payload.
620/// 3. On internal validation error (`validate_compaction` returns `Err`), call
621///    `MetricsCallback::record_compaction_probe_error()` and return `ProbeOutcome::Pass`.
622///    An error must not abort compaction.
623///
624/// The service treats the returned `ProbeOutcome` exclusively as routing:
625/// `HardFail` → abort with `ProbeRejected`; `Pass | SoftFail` → continue.
626pub trait CompactionProbeCallback: Send {
627    /// Validate the candidate `summary` produced from `to_compact` messages.
628    fn validate<'a>(
629        &'a mut self,
630        to_compact: &'a [Message],
631        summary: &'a str,
632    ) -> Pin<Box<dyn Future<Output = ProbeOutcome> + Send + 'a>>;
633}
634
635/// Pre-summary tool-output archiving hook (Memex #2432).
636///
637/// The service calls `archive(to_compact)` BEFORE summarization. The returned reference
638/// strings are appended as a postfix AFTER the LLM summary to prevent the LLM from
639/// destroying the `[archived:UUID]` markers.
640pub trait ToolOutputArchive: Send + Sync {
641    /// Archive tool output bodies from `to_compact` and return reference strings.
642    ///
643    /// Returns an empty `Vec` when archiving is disabled or no bodies are archived.
644    fn archive<'a>(
645        &'a self,
646        to_compact: &'a [Message],
647    ) -> Pin<Box<dyn Future<Output = Vec<String>> + Send + 'a>>;
648}
649
650/// Persistence completion hook invoked after the in-memory drain/reinsert is finalized.
651///
652/// Returns:
653/// - `persist_failed`: whether the synchronous `SQLite` persistence step failed.
654/// - `qdrant_future`: optional `'static` future for the off-thread Qdrant write, bubbled
655///   back to the caller via [`CompactionOutcome::Compacted::qdrant_future`].
656pub trait CompactionPersistence: Send + Sync {
657    /// Persist the compaction result and return the Qdrant write future.
658    ///
659    /// `compacted_trust_level` is the worst-case `MessageMetadata::trust_level` across the
660    /// compacted-away messages (issue #6558 follow-up, S3) — implementors must persist it
661    /// alongside the summary row so the memory-consent gate's context tag survives a session
662    /// reload, not just the in-memory summary message.
663    fn after_compaction<'a>(
664        &'a self,
665        compacted_count: usize,
666        summary_content: &'a str,
667        summary: &'a str,
668        compacted_trust_level: Option<u8>,
669    ) -> Pin<Box<dyn Future<Output = (bool, Option<QdrantPersistFuture>)> + Send + 'a>>;
670}
671
672/// Metrics-counter sink for `ContextService` increments.
673///
674/// Implemented in `zeph-core` by an adapter wrapping `Arc<MetricsCollector>`. Keeps
675/// `zeph-agent-context` free of `zeph-core` internal metrics types. Closes #3527.
676///
677/// All four `record_compaction_probe_*` methods are called from inside the
678/// [`CompactionProbeCallback`] implementation — not from the service itself — per the
679/// probe-callback contract.
680pub trait MetricsCallback: Send + Sync {
681    /// Record that a hard-compaction event occurred.
682    ///
683    /// `turns_since_last` is `None` on the first hard compaction of the session.
684    fn record_hard_compaction(&self, turns_since_last: Option<u32>);
685
686    /// Record that tool outputs were pruned.
687    ///
688    /// `count` is the number of tool-output bodies pruned in this pass.
689    fn record_tool_output_prune(&self, count: usize);
690
691    /// Record a probe pass verdict with full score data.
692    fn record_compaction_probe_pass(
693        &self,
694        score: f32,
695        category_scores: Vec<zeph_memory::CategoryScore>,
696        threshold: f32,
697        hard_fail_threshold: f32,
698    );
699
700    /// Record a probe soft-fail verdict with full score data.
701    fn record_compaction_probe_soft_fail(
702        &self,
703        score: f32,
704        category_scores: Vec<zeph_memory::CategoryScore>,
705        threshold: f32,
706        hard_fail_threshold: f32,
707    );
708
709    /// Record a probe hard-fail verdict with full score data.
710    fn record_compaction_probe_hard_fail(
711        &self,
712        score: f32,
713        category_scores: Vec<zeph_memory::CategoryScore>,
714        threshold: f32,
715        hard_fail_threshold: f32,
716    );
717
718    /// Record that the probe returned an error (non-fatal; compaction proceeded).
719    fn record_compaction_probe_error(&self);
720}