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