Compactの実装
This commit is contained in:
@@ -45,6 +45,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
WorkerResult::Finished => println!("✅ Task completed normally"),
|
||||
WorkerResult::Paused => println!("⏸️ Task paused"),
|
||||
WorkerResult::LimitReached => println!("🔒 Turn limit reached"),
|
||||
WorkerResult::Yielded => println!("↩️ Task yielded"),
|
||||
},
|
||||
Err(e) => {
|
||||
println!("❌ Task error: {}", e);
|
||||
|
||||
@@ -30,8 +30,13 @@ pub enum PromptAction {
|
||||
pub enum PreRequestAction {
|
||||
/// Proceed normally.
|
||||
Continue,
|
||||
/// Cancel with a reason.
|
||||
/// Cancel with a reason (treated as an error).
|
||||
Cancel(String),
|
||||
/// Yield control to the caller for external processing.
|
||||
///
|
||||
/// The Worker exits the turn loop cleanly with `WorkerResult::Yielded`.
|
||||
/// The caller is expected to resume execution later.
|
||||
Yield,
|
||||
}
|
||||
|
||||
/// Action before a tool call.
|
||||
|
||||
@@ -71,6 +71,12 @@ pub enum WorkerResult {
|
||||
Paused,
|
||||
/// Turn limit reached (max_turns exceeded)
|
||||
LimitReached,
|
||||
/// Yielded to caller for external processing (e.g. context compaction).
|
||||
///
|
||||
/// Distinct from `Paused`: internal machinery, not user-facing. The
|
||||
/// caller is expected to perform some side work and then call `resume()`
|
||||
/// to continue the turn loop.
|
||||
Yielded,
|
||||
}
|
||||
|
||||
/// Result of [`Worker<C, Mutable>::run()`] / [`Worker<C, Mutable>::resume()`].
|
||||
@@ -702,6 +708,14 @@ impl<C: LlmClient, S: WorkerState> Worker<C, S> {
|
||||
self.last_run_interrupted = true;
|
||||
return Err(WorkerError::Aborted(reason));
|
||||
}
|
||||
PreRequestAction::Yield => {
|
||||
info!("Yielded by interceptor");
|
||||
for cb in &self.turn_end_cbs {
|
||||
cb(current_turn);
|
||||
}
|
||||
self.last_run_interrupted = true;
|
||||
return Ok(WorkerResult::Yielded);
|
||||
}
|
||||
PreRequestAction::Continue => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//! CompactInterceptor — wraps HookInterceptor with urgent compaction check.
|
||||
//!
|
||||
//! Decorator that delegates all [`Interceptor`] methods to the inner
|
||||
//! `HookInterceptor`, then adds a token-count check in `pre_llm_request`.
|
||||
//! When `last_input_tokens` exceeds the turn threshold, returns
|
||||
//! `PreRequestAction::Yield` so the Worker exits the turn loop cleanly
|
||||
//! with `WorkerResult::Yielded` and Pod can perform compaction.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use llm_worker::interceptor::{
|
||||
Interceptor, PostToolAction, PreRequestAction, PreToolAction, PromptAction, ToolCallInfo,
|
||||
ToolResultInfo, TurnEndAction,
|
||||
};
|
||||
use llm_worker::Item;
|
||||
use tracing::info;
|
||||
|
||||
use crate::compact_state::CompactState;
|
||||
use crate::hook_interceptor::HookInterceptor;
|
||||
|
||||
/// Interceptor that wraps HookInterceptor and adds between-turns
|
||||
/// compaction threshold check.
|
||||
pub(crate) struct CompactInterceptor {
|
||||
inner: HookInterceptor,
|
||||
state: Arc<CompactState>,
|
||||
}
|
||||
|
||||
impl CompactInterceptor {
|
||||
pub(crate) fn new(inner: HookInterceptor, state: Arc<CompactState>) -> Self {
|
||||
Self { inner, state }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Interceptor for CompactInterceptor {
|
||||
async fn on_prompt_submit(&self, item: &mut Item) -> PromptAction {
|
||||
self.inner.on_prompt_submit(item).await
|
||||
}
|
||||
|
||||
async fn pre_llm_request(&self, context: &mut Vec<Item>) -> PreRequestAction {
|
||||
// Step 1: Delegate to inner (PruneHook and other hooks run first).
|
||||
let inner_action = self.inner.pre_llm_request(context).await;
|
||||
if !matches!(inner_action, PreRequestAction::Continue) {
|
||||
return inner_action;
|
||||
}
|
||||
|
||||
// Step 2: Check between-turns compaction threshold.
|
||||
if !self.state.is_disabled() && self.state.exceeds_turn() {
|
||||
info!(
|
||||
input_tokens = self.state.last_input_tokens(),
|
||||
threshold = self.state.turn_threshold(),
|
||||
"Between-turns compaction threshold exceeded, yielding"
|
||||
);
|
||||
return PreRequestAction::Yield;
|
||||
}
|
||||
|
||||
PreRequestAction::Continue
|
||||
}
|
||||
|
||||
async fn pre_tool_call(&self, info: &mut ToolCallInfo) -> PreToolAction {
|
||||
self.inner.pre_tool_call(info).await
|
||||
}
|
||||
|
||||
async fn post_tool_call(&self, info: &mut ToolResultInfo) -> PostToolAction {
|
||||
self.inner.post_tool_call(info).await
|
||||
}
|
||||
|
||||
async fn on_turn_end(&self, history: &[Item]) -> TurnEndAction {
|
||||
self.inner.on_turn_end(history).await
|
||||
}
|
||||
|
||||
async fn on_abort(&self, reason: &str) {
|
||||
self.inner.on_abort(reason).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
//! Shared state for compaction decisions.
|
||||
//!
|
||||
//! Holds atomic counters shared between:
|
||||
//! - `on_usage` callback (writes `last_input_tokens`)
|
||||
//! - `CompactInterceptor` (reads token count, checks thresholds)
|
||||
//! - `Pod::run()`/`resume()` (circuit breaker, thrash detection)
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
|
||||
|
||||
const MAX_COMPACT_FAILURES: usize = 3;
|
||||
|
||||
/// Shared mutable state for compaction decisions.
|
||||
pub(crate) struct CompactState {
|
||||
/// Last observed input_tokens from `on_usage` callback.
|
||||
last_input_tokens: AtomicU64,
|
||||
/// Proactive threshold — checked in `pre_llm_request` (between turns).
|
||||
turn_threshold: u64,
|
||||
/// Post-run threshold — checked by Controller after run completes.
|
||||
post_run_threshold: u64,
|
||||
/// Number of recent turns to retain after compaction.
|
||||
retained_turns: usize,
|
||||
/// Consecutive compact failures. At `MAX_COMPACT_FAILURES`, compaction is disabled.
|
||||
consecutive_failures: AtomicUsize,
|
||||
/// `true` immediately after a successful compact, cleared on next normal completion.
|
||||
just_compacted: AtomicBool,
|
||||
/// `true` when circuit breaker has tripped.
|
||||
disabled: AtomicBool,
|
||||
}
|
||||
|
||||
impl CompactState {
|
||||
/// Create a new CompactState.
|
||||
///
|
||||
/// `turn_threshold` is the proactive (80%) threshold from the manifest.
|
||||
/// `post_run_threshold` is derived as `turn_threshold * 9 / 8` (≈90%).
|
||||
pub(crate) fn new(turn_threshold: u64, retained_turns: usize) -> Self {
|
||||
Self {
|
||||
last_input_tokens: AtomicU64::new(0),
|
||||
turn_threshold,
|
||||
post_run_threshold: turn_threshold * 9 / 8,
|
||||
retained_turns,
|
||||
consecutive_failures: AtomicUsize::new(0),
|
||||
just_compacted: AtomicBool::new(false),
|
||||
disabled: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the last observed input_tokens (called from `on_usage`).
|
||||
pub(crate) fn update_input_tokens(&self, tokens: u64) {
|
||||
self.last_input_tokens.store(tokens, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Read the last observed input_tokens.
|
||||
pub(crate) fn last_input_tokens(&self) -> u64 {
|
||||
self.last_input_tokens.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// The between-turns threshold value.
|
||||
pub(crate) fn turn_threshold(&self) -> u64 {
|
||||
self.turn_threshold
|
||||
}
|
||||
|
||||
/// Number of turns to retain after compaction.
|
||||
pub(crate) fn retained_turns(&self) -> usize {
|
||||
self.retained_turns
|
||||
}
|
||||
|
||||
/// Whether compaction has been disabled by the circuit breaker.
|
||||
pub(crate) fn is_disabled(&self) -> bool {
|
||||
self.disabled.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Whether `last_input_tokens` exceeds the between-turns threshold.
|
||||
pub(crate) fn exceeds_turn(&self) -> bool {
|
||||
self.last_input_tokens() > self.turn_threshold
|
||||
}
|
||||
|
||||
/// Whether `last_input_tokens` exceeds the post-run threshold.
|
||||
pub(crate) fn exceeds_post_run(&self) -> bool {
|
||||
self.last_input_tokens() > self.post_run_threshold
|
||||
}
|
||||
|
||||
/// Whether a compact just completed (for thrash detection).
|
||||
pub(crate) fn just_compacted(&self) -> bool {
|
||||
self.just_compacted.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Set or clear the just_compacted flag.
|
||||
pub(crate) fn set_just_compacted(&self, val: bool) {
|
||||
self.just_compacted.store(val, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a successful compaction: reset failure counter, set just_compacted.
|
||||
pub(crate) fn record_compact_success(&self) {
|
||||
self.consecutive_failures.store(0, Ordering::Relaxed);
|
||||
self.just_compacted.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Record a compaction failure. Disables compaction after MAX_COMPACT_FAILURES.
|
||||
pub(crate) fn record_compact_failure(&self) {
|
||||
let prev = self.consecutive_failures.fetch_add(1, Ordering::Relaxed);
|
||||
if prev + 1 >= MAX_COMPACT_FAILURES {
|
||||
self.disabled.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn threshold_derivation() {
|
||||
let state = CompactState::new(80_000, 2);
|
||||
assert_eq!(state.turn_threshold, 80_000);
|
||||
assert_eq!(state.post_run_threshold, 90_000);
|
||||
assert_eq!(state.retained_turns(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exceeds_checks() {
|
||||
let state = CompactState::new(80_000, 2);
|
||||
assert!(!state.exceeds_turn());
|
||||
assert!(!state.exceeds_post_run());
|
||||
|
||||
state.update_input_tokens(85_000);
|
||||
assert!(state.exceeds_turn());
|
||||
assert!(!state.exceeds_post_run());
|
||||
|
||||
state.update_input_tokens(95_000);
|
||||
assert!(state.exceeds_turn());
|
||||
assert!(state.exceeds_post_run());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circuit_breaker_trips_after_max_failures() {
|
||||
let state = CompactState::new(80_000, 2);
|
||||
assert!(!state.is_disabled());
|
||||
|
||||
state.record_compact_failure();
|
||||
assert!(!state.is_disabled());
|
||||
state.record_compact_failure();
|
||||
assert!(!state.is_disabled());
|
||||
state.record_compact_failure();
|
||||
assert!(state.is_disabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn success_resets_failure_count() {
|
||||
let state = CompactState::new(80_000, 2);
|
||||
state.record_compact_failure();
|
||||
state.record_compact_failure();
|
||||
assert!(!state.is_disabled());
|
||||
|
||||
state.record_compact_success();
|
||||
assert!(state.just_compacted());
|
||||
|
||||
// After success + 2 more failures, still not disabled (count was reset).
|
||||
state.record_compact_failure();
|
||||
state.record_compact_failure();
|
||||
assert!(!state.is_disabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_compacted_lifecycle() {
|
||||
let state = CompactState::new(80_000, 2);
|
||||
assert!(!state.just_compacted());
|
||||
|
||||
state.record_compact_success();
|
||||
assert!(state.just_compacted());
|
||||
|
||||
state.set_just_compacted(false);
|
||||
assert!(!state.just_compacted());
|
||||
}
|
||||
}
|
||||
@@ -191,6 +191,13 @@ impl PodController {
|
||||
)
|
||||
.await;
|
||||
|
||||
// Proactive post-run compaction (best-effort).
|
||||
if new_status == PodStatus::Idle {
|
||||
if let Err(e) = pod.try_post_run_compact().await {
|
||||
tracing::warn!(error = %e, "Post-run compaction error");
|
||||
}
|
||||
}
|
||||
|
||||
let items = pod.worker().history().to_vec();
|
||||
shared_state.update_history(items);
|
||||
shared_state.set_status(new_status);
|
||||
@@ -218,6 +225,13 @@ impl PodController {
|
||||
)
|
||||
.await;
|
||||
|
||||
// Proactive post-run compaction (best-effort).
|
||||
if new_status == PodStatus::Idle {
|
||||
if let Err(e) = pod.try_post_run_compact().await {
|
||||
tracing::warn!(error = %e, "Post-run compaction error");
|
||||
}
|
||||
}
|
||||
|
||||
let items = pod.worker().history().to_vec();
|
||||
shared_state.update_history(items);
|
||||
shared_state.set_status(new_status);
|
||||
|
||||
@@ -6,6 +6,8 @@ pub mod socket_server;
|
||||
|
||||
pub mod prune_hook;
|
||||
|
||||
mod compact_interceptor;
|
||||
mod compact_state;
|
||||
mod hook_interceptor;
|
||||
mod pod;
|
||||
|
||||
|
||||
+170
-27
@@ -9,9 +9,12 @@ use llm_worker::{Worker, WorkerError, WorkerResult};
|
||||
use session_store::{
|
||||
EntryHash, Outcome, SessionId, SessionStartState, Store, StoreError,
|
||||
};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use manifest::{PodManifest, Scope, WorkerManifest};
|
||||
|
||||
use crate::compact_interceptor::CompactInterceptor;
|
||||
use crate::compact_state::CompactState;
|
||||
use crate::hook::{
|
||||
Hook, HookRegistryBuilder, OnAbort, OnPromptSubmit, OnTurnEnd, PostToolCall, PreLlmRequest,
|
||||
PreToolCall,
|
||||
@@ -48,6 +51,8 @@ pub struct Pod<C: LlmClient, St: Store> {
|
||||
interceptor_installed: bool,
|
||||
/// Directory containing the manifest file (needed for api_key_file resolution).
|
||||
manifest_dir: Option<PathBuf>,
|
||||
/// Shared compaction state (present when compact_threshold is configured).
|
||||
compact_state: Option<Arc<CompactState>>,
|
||||
}
|
||||
|
||||
impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
@@ -74,6 +79,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
interceptor_installed: false,
|
||||
manifest_dir: None,
|
||||
compact_state: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -105,6 +111,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
interceptor_installed: false,
|
||||
manifest_dir: None,
|
||||
compact_state: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -187,34 +194,59 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
}
|
||||
|
||||
/// Install the hook-based interceptor on the Worker if not already done.
|
||||
///
|
||||
/// When `compact_threshold` is configured in the manifest, wraps the
|
||||
/// `HookInterceptor` in a [`CompactInterceptor`] and registers an
|
||||
/// `on_usage` callback to track `input_tokens`.
|
||||
fn ensure_interceptor_installed(&mut self) {
|
||||
if !self.interceptor_installed {
|
||||
let builder = std::mem::take(&mut self.hook_builder);
|
||||
let registry = Arc::new(builder.build());
|
||||
let interceptor = HookInterceptor::new(registry);
|
||||
self.worker_mut().set_interceptor(interceptor);
|
||||
let hook_interceptor = HookInterceptor::new(registry);
|
||||
|
||||
let compact_threshold = self
|
||||
.manifest
|
||||
.compaction
|
||||
.as_ref()
|
||||
.and_then(|c| c.compact_threshold);
|
||||
|
||||
if let Some(threshold) = compact_threshold {
|
||||
let retained = self
|
||||
.manifest
|
||||
.compaction
|
||||
.as_ref()
|
||||
.map(|c| c.compact_retained_turns)
|
||||
.unwrap_or(2);
|
||||
|
||||
let state = Arc::new(CompactState::new(threshold, retained));
|
||||
|
||||
// Track input_tokens via on_usage callback.
|
||||
let state_for_usage = state.clone();
|
||||
self.worker_mut().on_usage(move |event| {
|
||||
if let Some(tokens) = event.input_tokens {
|
||||
state_for_usage.update_input_tokens(tokens);
|
||||
}
|
||||
});
|
||||
|
||||
let interceptor = CompactInterceptor::new(hook_interceptor, state.clone());
|
||||
self.worker_mut().set_interceptor(interceptor);
|
||||
self.compact_state = Some(state);
|
||||
} else {
|
||||
self.worker_mut().set_interceptor(hook_interceptor);
|
||||
}
|
||||
|
||||
self.interceptor_installed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Send user input and run until the LLM turn completes.
|
||||
///
|
||||
/// If the between-turns compaction threshold is exceeded mid-run,
|
||||
/// the Worker is aborted, history is compacted, and execution resumes
|
||||
/// automatically.
|
||||
pub async fn run(&mut self, input: impl Into<String>) -> Result<PodRunResult, PodError> {
|
||||
self.ensure_interceptor_installed();
|
||||
|
||||
// Split borrow: access worker field directly to allow concurrent
|
||||
// mutable borrows on session_id / head_hash.
|
||||
let w = self.worker.as_ref().unwrap();
|
||||
session_store::ensure_head_or_fork(
|
||||
&self.store,
|
||||
&mut self.session_id,
|
||||
&mut self.head_hash,
|
||||
SessionStartState {
|
||||
system_prompt: w.get_system_prompt(),
|
||||
config: w.request_config(),
|
||||
history: w.history(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
self.ensure_session_head().await?;
|
||||
|
||||
let history_before = self.worker.as_ref().unwrap().history().len();
|
||||
|
||||
@@ -224,14 +256,27 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
let result = locked.run(input).await;
|
||||
self.worker = Some(locked.unlock());
|
||||
|
||||
self.persist_turn(history_before, &result).await?;
|
||||
result.map(PodRunResult::from).map_err(PodError::Worker)
|
||||
self.handle_worker_result(result, history_before).await
|
||||
}
|
||||
|
||||
/// Resume from a paused state.
|
||||
pub async fn resume(&mut self) -> Result<PodRunResult, PodError> {
|
||||
self.ensure_interceptor_installed();
|
||||
self.ensure_session_head().await?;
|
||||
|
||||
let history_before = self.worker.as_ref().unwrap().history().len();
|
||||
|
||||
// lock → resume → unlock
|
||||
let worker = self.worker.take().expect("worker taken during run");
|
||||
let mut locked = worker.lock();
|
||||
let result = locked.resume().await;
|
||||
self.worker = Some(locked.unlock());
|
||||
|
||||
self.handle_worker_result(result, history_before).await
|
||||
}
|
||||
|
||||
/// Ensure session head exists (fork if needed).
|
||||
async fn ensure_session_head(&mut self) -> Result<(), PodError> {
|
||||
let w = self.worker.as_ref().unwrap();
|
||||
session_store::ensure_head_or_fork(
|
||||
&self.store,
|
||||
@@ -244,19 +289,109 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let history_before = self.worker.as_ref().unwrap().history().len();
|
||||
|
||||
// lock → resume → unlock
|
||||
let worker = self.worker.take().expect("worker taken during run");
|
||||
let mut locked = worker.lock();
|
||||
let result = locked.resume().await;
|
||||
self.worker = Some(locked.unlock());
|
||||
|
||||
/// Handle Worker result: always persist the turn first, then if
|
||||
/// `Yielded`, perform compaction and resume.
|
||||
///
|
||||
/// Persisting before compaction ensures that if compact fails, the
|
||||
/// turn is fully recorded in the old session (interrupted, outcome
|
||||
/// `Yielded`), so restore remains consistent.
|
||||
async fn handle_worker_result(
|
||||
&mut self,
|
||||
result: Result<WorkerResult, WorkerError>,
|
||||
history_before: usize,
|
||||
) -> Result<PodRunResult, PodError> {
|
||||
self.persist_turn(history_before, &result).await?;
|
||||
|
||||
if matches!(result, Ok(WorkerResult::Yielded)) {
|
||||
return self.do_compact_and_resume().await;
|
||||
}
|
||||
|
||||
if result.is_ok() {
|
||||
if let Some(ref state) = self.compact_state {
|
||||
state.set_just_compacted(false);
|
||||
}
|
||||
}
|
||||
result.map(PodRunResult::from).map_err(PodError::Worker)
|
||||
}
|
||||
|
||||
/// Perform compaction after a `compact_needed` abort and resume execution.
|
||||
///
|
||||
/// Uses `Box::pin` for the recursive `resume()` call to break the
|
||||
/// async layout cycle (`run → handle_worker_result → do_compact_and_resume → resume`).
|
||||
fn do_compact_and_resume(
|
||||
&mut self,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<PodRunResult, PodError>> + Send + '_>>
|
||||
{
|
||||
Box::pin(async move {
|
||||
// Thrash detection: if we just compacted and hit the threshold again,
|
||||
// something is wrong.
|
||||
if let Some(ref state) = self.compact_state {
|
||||
if state.just_compacted() {
|
||||
state.set_just_compacted(false);
|
||||
return Err(PodError::CompactThrash);
|
||||
}
|
||||
}
|
||||
|
||||
let retained = self
|
||||
.compact_state
|
||||
.as_ref()
|
||||
.map(|s| s.retained_turns())
|
||||
.unwrap_or(2);
|
||||
|
||||
match self.compact(retained).await {
|
||||
Ok(new_session_id) => {
|
||||
info!(
|
||||
new_session_id = %new_session_id,
|
||||
"Compaction succeeded, resuming execution"
|
||||
);
|
||||
if let Some(ref state) = self.compact_state {
|
||||
state.record_compact_success();
|
||||
}
|
||||
self.resume().await
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(error = %e, "Compaction failed during run");
|
||||
if let Some(ref state) = self.compact_state {
|
||||
state.record_compact_failure();
|
||||
}
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt proactive compaction (called by Controller after run).
|
||||
///
|
||||
/// Best-effort: failures are logged but do not propagate.
|
||||
pub async fn try_post_run_compact(&mut self) -> Result<(), PodError> {
|
||||
let state = match self.compact_state.as_ref() {
|
||||
Some(s) if !s.is_disabled() && s.exceeds_post_run() && !s.just_compacted() => {
|
||||
s.clone()
|
||||
}
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
let retained = state.retained_turns();
|
||||
match self.compact(retained).await {
|
||||
Ok(new_session_id) => {
|
||||
info!(
|
||||
new_session_id = %new_session_id,
|
||||
"Proactive post-run compaction succeeded"
|
||||
);
|
||||
state.record_compact_success();
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(error = %e, "Proactive post-run compaction failed");
|
||||
state.record_compact_failure();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist delta + turn end + outcome after a run/resume.
|
||||
async fn persist_turn(
|
||||
&mut self,
|
||||
@@ -289,6 +424,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
Ok(WorkerResult::Finished) => Outcome::Finished,
|
||||
Ok(WorkerResult::Paused) => Outcome::Paused,
|
||||
Ok(WorkerResult::LimitReached) => Outcome::LimitReached,
|
||||
Ok(WorkerResult::Yielded) => Outcome::Yielded,
|
||||
Err(e) => Outcome::Error {
|
||||
message: e.to_string(),
|
||||
},
|
||||
@@ -440,6 +576,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
interceptor_installed: false,
|
||||
manifest_dir,
|
||||
compact_state: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -478,6 +615,9 @@ impl From<WorkerResult> for PodRunResult {
|
||||
WorkerResult::Finished => PodRunResult::Finished,
|
||||
WorkerResult::Paused => PodRunResult::Paused,
|
||||
WorkerResult::LimitReached => PodRunResult::LimitReached,
|
||||
// Yielded is internal to Pod: it's always caught by
|
||||
// handle_worker_result and never converted to PodRunResult.
|
||||
WorkerResult::Yielded => unreachable!("Yielded never converts to PodRunResult"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -527,4 +667,7 @@ pub enum PodError {
|
||||
|
||||
#[error(transparent)]
|
||||
Provider(#[from] provider::ProviderError),
|
||||
|
||||
#[error("compaction thrash: context still exceeds threshold immediately after compact")]
|
||||
CompactThrash,
|
||||
}
|
||||
|
||||
@@ -159,6 +159,9 @@ pub enum Outcome {
|
||||
Finished,
|
||||
Paused,
|
||||
LimitReached,
|
||||
/// Worker yielded control to the caller for external processing.
|
||||
/// Distinct from `Paused`: caller handles internally and resumes.
|
||||
Yielded,
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ async fn run_and_persist(
|
||||
Ok(llm_worker::WorkerResult::Finished) => Outcome::Finished,
|
||||
Ok(llm_worker::WorkerResult::Paused) => Outcome::Paused,
|
||||
Ok(llm_worker::WorkerResult::LimitReached) => Outcome::LimitReached,
|
||||
Ok(llm_worker::WorkerResult::Yielded) => Outcome::Yielded,
|
||||
Err(e) => Outcome::Error {
|
||||
message: e.to_string(),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user