M6: auto-compaction
Adds harness-core::engine::compact, a Compactor triggered when a session approaches its context limit, and threads the overflow trigger through the engine loop so a session driven past the limit compacts and continues correctly.
This commit is contained in:
@@ -12,6 +12,7 @@ harness-mcp = { workspace = true }
|
||||
harness-lsp = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@@ -14,8 +14,9 @@ use async_trait::async_trait;
|
||||
use harness_core::agent::{AgentDef, AgentRegistry};
|
||||
use harness_core::config::{self, Config, ConfigError};
|
||||
use harness_core::engine::jobs::{JobBoard, JobState, LaunchSpec};
|
||||
use harness_core::engine::{run_session, RunConfig, StepContext};
|
||||
use harness_core::engine::{run_session, Compactor, RunConfig, StepContext};
|
||||
use harness_core::event::{AppEvent, EventBus, RunOutcome};
|
||||
use harness_core::llm::{Initiator, LlmEvent, LlmRequest, Provider, ProviderError, WireMessage};
|
||||
use harness_core::lsp::DiagnosticsSource;
|
||||
use harness_core::permission::{PermissionReply, PermissionService, Rule, Ruleset};
|
||||
use harness_core::store::{Store, StoreError};
|
||||
@@ -164,6 +165,9 @@ struct EngineInner {
|
||||
skills_prompt: Option<String>,
|
||||
/// Slash commands by name, expanded into the user message by the TUI input layer.
|
||||
commands: HashMap<String, config::CommandDef>,
|
||||
/// Auto-compaction backend (small model). `None` when no `small_model` is configured or
|
||||
/// its provider isn't registered — compaction is then disabled.
|
||||
compactor: Option<Arc<dyn Compactor>>,
|
||||
cwd: PathBuf,
|
||||
data_dir: PathBuf,
|
||||
runs: Mutex<HashMap<SessionId, RunHandle>>,
|
||||
@@ -255,6 +259,19 @@ impl EngineHandle {
|
||||
// `init` warms the cache in the background for the next launch.
|
||||
let catalog = ModelCatalog::load_cached_or_baked(&ModelCatalog::default_cache_path());
|
||||
|
||||
// Auto-compaction backend: the configured `small_model` (provider/model) if its provider
|
||||
// is registered. `None` disables compaction (the engine then runs with full history).
|
||||
let compactor: Option<Arc<dyn Compactor>> = config
|
||||
.small_model
|
||||
.as_deref()
|
||||
.and_then(|m| m.split_once('/'))
|
||||
.and_then(|(provider_id, model_id)| {
|
||||
providers
|
||||
.get(provider_id)
|
||||
.map(|provider| SmallModelCompactor::new(provider, model_id.to_string()))
|
||||
})
|
||||
.map(|c| Arc::new(c) as Arc<dyn Compactor>);
|
||||
|
||||
// LSP pool: built-ins present on PATH, plus any config-declared servers.
|
||||
let lsp_servers: Vec<harness_lsp::ServerConfig> = config
|
||||
.lsp
|
||||
@@ -282,6 +299,7 @@ impl EngineHandle {
|
||||
diagnostics,
|
||||
skills_prompt,
|
||||
commands,
|
||||
compactor,
|
||||
cwd,
|
||||
data_dir,
|
||||
runs: Mutex::new(HashMap::new()),
|
||||
@@ -427,6 +445,7 @@ impl EngineHandle {
|
||||
job_board: Some(board),
|
||||
context_reporter: None, // root session has no parent board to report to
|
||||
diagnostics: self.inner.diagnostics.clone(),
|
||||
compactor: self.inner.compactor.clone(),
|
||||
};
|
||||
let run_config = RunConfig {
|
||||
agent_name: agent.name.clone(),
|
||||
@@ -440,6 +459,12 @@ impl EngineHandle {
|
||||
reminder_turn_start: self.inner.reminder("turn_start"),
|
||||
reminder_after_file_tool: self.inner.reminder("after_file_tool"),
|
||||
skills_prompt: self.inner.skills_prompt.clone(),
|
||||
context_limit: self
|
||||
.inner
|
||||
.catalog
|
||||
.get(provider_id, model_id)
|
||||
.map(|i| i.context_limit)
|
||||
.unwrap_or(0),
|
||||
};
|
||||
|
||||
// Reserve the run slot *before* spawning. If we inserted after spawning, a run that
|
||||
@@ -734,6 +759,7 @@ impl EngineInner {
|
||||
job_board: Some(board),
|
||||
context_reporter: reporter,
|
||||
diagnostics: self.diagnostics.clone(),
|
||||
compactor: self.compactor.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -754,6 +780,44 @@ impl ContextReporter for BoardReporter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto-compaction backend: summarizes a conversation with the configured `small_model`.
|
||||
struct SmallModelCompactor {
|
||||
provider: Arc<dyn Provider>,
|
||||
model_id: String,
|
||||
}
|
||||
|
||||
impl SmallModelCompactor {
|
||||
fn new(provider: Arc<dyn Provider>, model_id: String) -> Self {
|
||||
Self { provider, model_id }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Compactor for SmallModelCompactor {
|
||||
async fn summarize(&self, messages: &[WireMessage]) -> Result<String, ProviderError> {
|
||||
use futures::StreamExt;
|
||||
|
||||
let req = LlmRequest {
|
||||
model: self.model_id.clone(),
|
||||
system: vec![harness_core::engine::compact::SUMMARY_SYSTEM_PROMPT.to_string()],
|
||||
messages: messages.to_vec(),
|
||||
tools: Vec::new(),
|
||||
temperature: None,
|
||||
max_tokens: None,
|
||||
reasoning: None,
|
||||
initiator: Initiator::Agent,
|
||||
};
|
||||
let mut stream = self.provider.stream(req, CancellationToken::new()).await?;
|
||||
let mut summary = String::new();
|
||||
while let Some(event) = stream.next().await {
|
||||
if let LlmEvent::TextDelta { text, .. } = event? {
|
||||
summary.push_str(&text);
|
||||
}
|
||||
}
|
||||
Ok(summary)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SubagentSpawner for EngineInner {
|
||||
async fn spawn(&self, req: SpawnRequest) -> Result<SpawnOutcome, SpawnError> {
|
||||
@@ -853,6 +917,14 @@ impl SubagentSpawner for EngineInner {
|
||||
reminder_turn_start: self.reminder("turn_start"),
|
||||
reminder_after_file_tool: self.reminder("after_file_tool"),
|
||||
skills_prompt: self.skills_prompt.clone(),
|
||||
context_limit: self
|
||||
.catalog
|
||||
.get(
|
||||
&child_session.model.provider_id,
|
||||
&child_session.model.model_id,
|
||||
)
|
||||
.map(|i| i.context_limit)
|
||||
.unwrap_or(0),
|
||||
};
|
||||
|
||||
// Register the launch on the parent board (also makes foreground results reusable).
|
||||
|
||||
Reference in New Issue
Block a user