feat(config): 压缩算法关键参数暴露到设置页面

将 4 个压缩参数(threshold_ratio、llm_compaction_threshold_ratio、
truncate_max_tokens、preserve_count)从硬编码提取到 config.json 顶层
compaction 节,前端新增 CompactionTab 供用户调整。

后端:
- config: 新增 CompactionConfig 结构体,Config 新增 compaction 字段
- context_compressor: 4 个参数内聚到 ContextCompressor 实例字段;
  新增 with_compaction_config() 构造函数和 truncate_tool_results() 方法;
  对用户配置做防御性 clamp(ratio ∈ [0.1,1.0],整数 ≥ 1)
- agent_loop: 调用 compressor.truncate_tool_results() 实现零参数耦合
- agent_factory: 新增 build_compressor() 方法,in-loop 与 sync 兜底
  两条压缩路径共用同一套用户配置
- session: with_factories 改用 agent_factory.build_compressor(),修复
  sync 压缩路径忽略用户配置的 P0 问题

前端:
- types: 新增 CompactionConfig 接口,TabId 新增 'compaction'
- 新建 CompactionTab.tsx,4 个 number input 分两组 SectionCard
- constants + ConfigPage: 注册"上下文压缩" Tab(Archive 图标)

清理:删除无调用方的 from_provider_config/from_runtime_config/with_config
方法及 DEFAULT_THRESHOLD_RATIO/LLM_COMPACTION_THRESHOLD_RATIO 常量。

测试:context_compressor 18 + session 31 + agent_loop 42 全通过。
This commit is contained in:
oudecheng 2026-08-06 11:03:56 +08:00
parent fe7037e4ad
commit 7b459b8ca1
12 changed files with 252 additions and 58 deletions

View File

@ -1443,17 +1443,15 @@ impl AgentLoop {
if let Some(prompt_tokens) = last_prompt_tokens {
if compressor.should_compress_by_usage(prompt_tokens) {
// 阶段 1a工程化压缩截断非子代理 tool 结果到 100 token仅改内存
crate::agent::context_compressor::truncate_tool_results_in_place(
&mut messages,
100,
);
// 阶段 1a工程化压缩截断非子代理 tool 结果,仅改内存)
// 参数内聚到 ContextCompressorAgentLoop 不持有截断 token 数
compressor.truncate_tool_results(&mut messages);
engineering_compaction_applied = true;
tracing::info!(
iteration,
prompt_tokens,
threshold = compressor.threshold(),
"Engineering compaction applied (tool results truncated to 100 tokens)"
"Engineering compaction applied (tool results truncated)"
);
// 阶段 1b重新估算判断是否需要 LLM 压缩50% 阈值)

View File

@ -3,7 +3,7 @@ use crate::bus::{
ChatMessage, SYSTEM_CONTEXT_AGENT_PROMPT, SYSTEM_CONTEXT_HISTORY_COMPACTION,
SYSTEM_CONTEXT_SCHEDULED_PROMPT,
};
use crate::config::LLMProviderConfig;
use crate::config::{CompactionConfig, LLMProviderConfig};
use crate::providers::{ChatCompletionRequest, LLMProvider, Message, create_provider};
use crate::text::{char_count, take_prefix_chars};
@ -17,12 +17,6 @@ pub const SYSTEM_CONTEXT_HISTORY_COMPACTION_OLDER: &str = "history_compaction_ol
/// System context marker for the light compression (newer segment) summary.
pub const SYSTEM_CONTEXT_HISTORY_COMPACTION_NEWER: &str = "history_compaction_newer";
/// Default threshold ratio: compress when estimated tokens exceed 70% of context window.
const DEFAULT_THRESHOLD_RATIO: f64 = 0.7;
/// LLM 压缩阈值比例:工程化压缩后仍超过此比例才调 LLM
const LLM_COMPACTION_THRESHOLD_RATIO: f64 = 0.5;
// ============================================================================
// HistoryUnit — atomic message units for compression
// ============================================================================
@ -275,6 +269,12 @@ pub struct ContextCompressor {
context_window: usize,
/// Threshold ratio to trigger compression (70% of context window).
threshold_ratio: f64,
/// LLM 压缩阈值比例(工程化压缩后仍超此比例才调 LLM
llm_compaction_threshold_ratio: f64,
/// 三段压缩保留 unit 数(最旧 N + 最新 N
preserve_count: usize,
/// 工程化压缩时 tool 结果截断 token 数
truncate_max_tokens: usize,
}
impl ContextCompressor {
@ -655,13 +655,13 @@ OLDER SEGMENT (events from earlier in the session):
self.compress_two_segment_inner(history, provider).await
}
/// 三段压缩核心逻辑:保留最旧5 + 最新5 unit中间段用 LLM 压缩。
/// 三段压缩核心逻辑:保留最旧 N + 最新 N unit中间段用 LLM 压缩。
///
/// 策略:
/// - SystemGuard 永远保留在头部(不计入 5 条配额)
/// - SystemGuard 永远保留在头部(不计入 N 条配额)
/// - 可压缩单元UserMessage / AssistantText / ToolRound按时间顺序
/// - 最旧 PRESERVE_COUNT 个 unit 原样保留
/// - 最新 PRESERVE_COUNT 个 unit 原样保留
/// - 最旧 preserve_count 个 unit 原样保留
/// - 最新 preserve_count 个 unit 原样保留
/// - 中间段用 LLM 生成摘要system 消息,无 tool_calls
/// - ToolRound 原子性由 parse_to_units 保证,切分在 unit 边界
/// - 中间段摘要为纯文本 system 消息,符合 API 提交要求
@ -670,14 +670,14 @@ OLDER SEGMENT (events from earlier in the session):
history: &[ChatMessage],
provider: &dyn LLMProvider,
) -> Result<Vec<ChatMessage>, AgentError> {
const PRESERVE_COUNT: usize = 5;
let preserve_count = self.preserve_count;
let tokens = estimate_tokens(history);
tracing::info!(
tokens = tokens,
threshold = self.threshold(),
msg_count = history.len(),
preserve_count = PRESERVE_COUNT,
"Starting three-segment compression (preserve oldest 5 + newest 5)"
preserve_count = preserve_count,
"Starting three-segment compression"
);
let units = parse_to_units(history);
@ -693,10 +693,10 @@ OLDER SEGMENT (events from earlier in the session):
}
// Step 2: If compressible units are too few, skip LLM compression
if compressible.len() <= PRESERVE_COUNT * 2 {
if compressible.len() <= preserve_count * 2 {
tracing::info!(
compressible_count = compressible.len(),
preserve_threshold = PRESERVE_COUNT * 2,
preserve_threshold = preserve_count * 2,
"Too few compressible units, skipping LLM compaction"
);
let mut result = system_guards;
@ -707,10 +707,10 @@ OLDER SEGMENT (events from earlier in the session):
}
// Step 3: Three-segment split
let split = compressible.len() - PRESERVE_COUNT;
let oldest_units = &compressible[..PRESERVE_COUNT];
let split = compressible.len() - preserve_count;
let oldest_units = &compressible[..preserve_count];
let newest_units = &compressible[split..];
let middle_units = &compressible[PRESERVE_COUNT..split];
let middle_units = &compressible[preserve_count..split];
// Step 4: Build middle segment messages and transcript
let middle_messages: Vec<ChatMessage> = middle_units
@ -743,7 +743,7 @@ OLDER SEGMENT (events from earlier in the session):
// System guards first
compressed.extend(system_guards);
// Oldest PRESERVE_COUNT units (raw)
// Oldest preserve_count units (raw)
for unit in oldest_units {
compressed.extend(unit_to_messages(unit));
}
@ -756,7 +756,7 @@ OLDER SEGMENT (events from earlier in the session):
));
}
// Newest PRESERVE_COUNT units (raw)
// Newest preserve_count units (raw)
for unit in newest_units {
compressed.extend(unit_to_messages(unit));
}
@ -766,8 +766,8 @@ OLDER SEGMENT (events from earlier in the session):
original_msg_count = history.len(),
final_tokens = estimate_tokens(&compressed),
final_msg_count = compressed.len(),
oldest_units = PRESERVE_COUNT,
newest_units = PRESERVE_COUNT,
oldest_units = preserve_count,
newest_units = preserve_count,
middle_units = middle_units.len(),
"Three-segment compression completed"
);
@ -780,34 +780,45 @@ OLDER SEGMENT (events from earlier in the session):
// =========================================================================
/// Create a new compressor with the given context window size.
/// 测试 fallback 路径:所有压缩参数用 CompactionConfig::default()。
pub fn new(context_window: usize) -> Self {
let default = CompactionConfig::default();
Self {
config: ContextCompressionConfig::default(),
context_window,
threshold_ratio: DEFAULT_THRESHOLD_RATIO,
threshold_ratio: default.threshold_ratio,
llm_compaction_threshold_ratio: default.llm_compaction_threshold_ratio,
preserve_count: default.preserve_count,
truncate_max_tokens: default.truncate_max_tokens,
}
}
pub fn from_provider_config(provider_config: &LLMProviderConfig) -> Self {
Self::from_runtime_config(&AgentRuntimeConfig::from(provider_config.clone()))
}
pub fn from_runtime_config(config: &AgentRuntimeConfig) -> Self {
Self::with_config(
config.context_window_tokens,
ContextCompressionConfig {
summary_max_chars: config.context_summary_char_budget,
/// 从 runtime config + compaction config 构造AgentFactory 生产路径调用)。
/// 所有压缩参数内聚到 ContextCompressorAgentLoop 零参数耦合。
/// 对用户配置做防御性 clamp避免极端值破坏压缩逻辑
/// - ratio 限制在 [0.1, 1.0]:过低导致每轮压缩,过高导致永不压缩
/// - truncate_max_tokens 限制 >= 10 会把所有 tool 结果截断成空串
/// - preserve_count 限制 >= 10 会丢失全部原始上下文
pub fn with_compaction_config(
context_window: usize,
summary_max_chars: usize,
compaction: &CompactionConfig,
) -> Self {
let clamp_ratio = |r: f64| r.clamp(0.1, 1.0);
let threshold_ratio = clamp_ratio(compaction.threshold_ratio);
let llm_compaction_threshold_ratio = clamp_ratio(compaction.llm_compaction_threshold_ratio);
let preserve_count = compaction.preserve_count.max(1);
let truncate_max_tokens = compaction.truncate_max_tokens.max(1);
Self {
config: ContextCompressionConfig {
summary_max_chars,
..ContextCompressionConfig::default()
},
)
}
/// Create with custom configuration.
pub fn with_config(context_window: usize, config: ContextCompressionConfig) -> Self {
Self {
config,
context_window,
threshold_ratio: DEFAULT_THRESHOLD_RATIO,
threshold_ratio,
llm_compaction_threshold_ratio,
preserve_count,
truncate_max_tokens,
}
}
@ -827,7 +838,13 @@ OLDER SEGMENT (events from earlier in the session):
/// LLM 压缩阈值50%):工程化压缩后用 estimate_tokens 判断是否需要 LLM 压缩。
pub fn llm_compaction_threshold(&self) -> usize {
(self.context_window as f64 * LLM_COMPACTION_THRESHOLD_RATIO) as usize
(self.context_window as f64 * self.llm_compaction_threshold_ratio) as usize
}
/// 工程化压缩:截断非子代理 tool 结果到 self.truncate_max_tokens。
/// AgentLoop 调用此方法,不需要知道截断参数细节(参数内聚)。
pub fn truncate_tool_results(&self, messages: &mut [ChatMessage]) {
truncate_tool_results_in_place(messages, self.truncate_max_tokens);
}
fn user_turn_ranges(&self, history: &[ChatMessage]) -> Vec<UserTurnRange> {

View File

@ -81,6 +81,7 @@ impl InitWizard {
image_context: crate::config::ImageContextConfig::default(),
subagents: crate::config::SubagentsConfig::default(),
experts: crate::config::ExpertsConfig::default(),
compaction: crate::config::CompactionConfig::default(),
}
}
@ -845,6 +846,7 @@ impl InitWizard {
image_context: existing.image_context.clone(),
subagents: existing.subagents.clone(),
experts: existing.experts.clone(),
compaction: existing.compaction.clone(),
}
}

View File

@ -40,6 +40,8 @@ pub struct Config {
pub subagents: SubagentsConfig,
#[serde(default)]
pub experts: ExpertsConfig,
#[serde(default)]
pub compaction: CompactionConfig,
}
/// 图片上下文限制配置
@ -72,6 +74,54 @@ impl Default for ImageContextConfig {
}
}
/// 上下文压缩算法配置(全局,所有 agent 共享)
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CompactionConfig {
/// 工程化压缩触发阈值(占 context_window 的比例0.0-1.0
/// 当 LLM 返回的 prompt_tokens 超过 context_window × 此比例时,触发压缩流程
#[serde(default = "default_threshold_ratio")]
pub threshold_ratio: f64,
/// LLM 压缩触发阈值(占 context_window 的比例0.0-1.0
/// 工程化压缩(截断 tool 结果)后,若估算 token 仍超过此比例才调 LLM 压缩
#[serde(default = "default_llm_compaction_threshold_ratio")]
pub llm_compaction_threshold_ratio: f64,
/// 工程化压缩时 tool 结果截断到的 token 数
/// 子代理返回tool_name="task")不受此限制
#[serde(default = "default_truncate_max_tokens")]
pub truncate_max_tokens: usize,
/// LLM 三段压缩保留的最旧/最新 unit 数
/// 压缩后保留最旧 N 个 unit + 中间段摘要 + 最新 N 个 unit
#[serde(default = "default_preserve_count")]
pub preserve_count: usize,
}
fn default_threshold_ratio() -> f64 {
0.7
}
fn default_llm_compaction_threshold_ratio() -> f64 {
0.5
}
fn default_truncate_max_tokens() -> usize {
100
}
fn default_preserve_count() -> usize {
5
}
impl Default for CompactionConfig {
fn default() -> Self {
Self {
threshold_ratio: default_threshold_ratio(),
llm_compaction_threshold_ratio: default_llm_compaction_threshold_ratio(),
truncate_max_tokens: default_truncate_max_tokens(),
preserve_count: default_preserve_count(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TimeConfig {
#[serde(default = "default_timezone")]

View File

@ -1,8 +1,8 @@
use std::sync::Arc;
use crate::agent::context_compressor::ContextCompressor;
use crate::agent::{AgentError, AgentLoop, CompositeSystemPromptProvider, SystemPromptProvider};
use crate::config::{LLMProviderConfig, ModelResolver};
use crate::agent::{AgentError, AgentLoop, AgentRuntimeConfig, CompositeSystemPromptProvider, SystemPromptProvider};
use crate::config::{CompactionConfig, LLMProviderConfig, ModelResolver};
use crate::domain::CapabilityPolicy;
use crate::experts::ExpertPromptProvider;
use crate::experts::ExpertRuntime;
@ -54,6 +54,8 @@ pub(crate) struct AgentFactory {
model_resolver: Arc<ModelResolver>,
/// per-session 的用户模型选择(最高优先级,覆盖专家配置)
model_selections: Arc<ModelSelectionStore>,
/// 上下文压缩算法配置(所有 agent 共享)
compaction_config: CompactionConfig,
/// 实例创建时间戳(用于区分新旧 AgentFactory 实例)
instance_id: u64,
}
@ -81,6 +83,7 @@ impl AgentFactory {
prompt_repository: Arc<dyn PromptInjectionRepository>,
model_resolver: Arc<ModelResolver>,
model_selections: Arc<ModelSelectionStore>,
compaction_config: CompactionConfig,
) -> Self {
// 使用 Arc 指针地址作为实例标识符,用于区分新旧 AgentFactory 实例
let instance_id = Arc::as_ptr(&tools) as u64;
@ -98,10 +101,22 @@ impl AgentFactory {
prompt_repository,
model_resolver,
model_selections,
compaction_config,
instance_id,
}
}
/// 构造 ContextCompressor参数内聚到 ContextCompressorCompactionConfig 注入)。
/// AgentLoopin-loop 压缩)和 Sessionsync 兜底压缩)共用此方法,
/// 确保两条压缩路径使用同一套用户配置的压缩参数。
pub(crate) fn build_compressor(&self, runtime_config: &AgentRuntimeConfig) -> ContextCompressor {
ContextCompressor::with_compaction_config(
runtime_config.context_window_tokens,
runtime_config.context_summary_char_budget,
&self.compaction_config,
)
}
pub(crate) fn create(&self, request: AgentBuildRequest<'_>) -> Result<AgentLoop, AgentError> {
let session_id = persistent_session_id(request.channel_name, request.session_chat_id);
@ -211,10 +226,9 @@ impl AgentFactory {
let tool_chat_id = request
.notification_chat_id
.unwrap_or(request.session_chat_id);
// 构建上下文压缩器(基于 effective_provider_config 的 context_window_tokens
let compressor = Arc::new(ContextCompressor::from_provider_config(
&effective_provider_config,
));
// 构建上下文压缩器(参数内聚到 ContextCompressorCompactionConfig 注入)
let runtime_config = AgentRuntimeConfig::from(effective_provider_config.clone());
let compressor = Arc::new(self.build_compressor(&runtime_config));
let mut agent = agent
.with_tool_context(ToolContext {
channel_name: Some(request.channel_name.to_string()),

View File

@ -120,6 +120,7 @@ impl GatewayState {
mcp_config,
Some(bus.clone()),
Arc::new(crate::config::ModelResolver::from_config(&config)),
config.compaction.clone(),
)?;
// 诊断日志:记录新 GatewayState 的创建(用于排查重启后是否使用了新状态)

View File

@ -9,7 +9,8 @@ use tokio::sync::RwLock;
use crate::agent::AgentError;
use crate::bus::MessageBus;
use crate::config::{
LLMProviderConfig, MemoryMaintenanceConfig, ModelResolver, SubagentsConfig, TaskConfig,
CompactionConfig, LLMProviderConfig, MemoryMaintenanceConfig, ModelResolver, SubagentsConfig,
TaskConfig,
};
use crate::gateway::model_selection::ModelSelectionStore;
use crate::gateway::tool_registry_factory::ToolRegistryFactory;
@ -57,6 +58,7 @@ pub(crate) fn build_session_manager(
mcp_config: crate::mcp::McpConfig,
bus: Option<Arc<MessageBus>>,
model_resolver: Arc<ModelResolver>,
compaction_config: CompactionConfig,
) -> Result<
(
SessionManager,
@ -84,6 +86,7 @@ pub(crate) fn build_session_manager(
mcp_config,
bus,
model_resolver,
compaction_config,
)
}
@ -105,6 +108,7 @@ pub(crate) fn build_session_manager_with_sender(
mcp_config: crate::mcp::McpConfig,
bus: Option<Arc<MessageBus>>,
model_resolver: Arc<ModelResolver>,
compaction_config: CompactionConfig,
) -> Result<
(
SessionManager,
@ -313,6 +317,7 @@ pub(crate) fn build_session_manager_with_sender(
prompt_repository.clone(),
model_resolver.clone(),
model_selections.clone(),
compaction_config,
);
let session_factory = SessionFactory::new(
provider_config.clone(),

View File

@ -1,4 +1,4 @@
use crate::agent::{AgentError, AgentLoop, ContextCompressor, EmittedMessageHandler};
use crate::agent::{AgentError, AgentLoop, AgentRuntimeConfig, ContextCompressor, EmittedMessageHandler};
#[cfg(test)]
use crate::bus::SYSTEM_CONTEXT_SCHEDULED_PROMPT;
use crate::bus::{ChatMessage, MessageBus, OutboundMessage};
@ -301,6 +301,7 @@ impl Session {
prompt_repository.clone(),
model_resolver,
Arc::new(super::model_selection::ModelSelectionStore::new()),
crate::config::CompactionConfig::default(),
);
Self::with_factories(
channel_name,
@ -325,6 +326,10 @@ impl Session {
skill_events: Arc<dyn SkillEventRepository>,
store: Arc<SessionStore>,
) -> Result<Self, AgentError> {
// Session 的 compressor 用于 sync 兜底压缩路径compaction.rs
// 必须与 AgentLoop 的 compressor 共用同一套用户配置的压缩参数。
let runtime_config = AgentRuntimeConfig::from(provider_config.clone());
let compressor = agent_factory.build_compressor(&runtime_config);
Ok(Self {
id: Uuid::new_v4(),
channel_name: channel_name.clone(),
@ -332,7 +337,7 @@ impl Session {
provider_config: provider_config.clone(),
skills,
agent_factory,
compressor: ContextCompressor::from_provider_config(&provider_config),
compressor,
history: SessionHistory::new(channel_name, conversations, skill_events),
store,
pending_cancel_tokens: HashMap::new(),
@ -744,6 +749,7 @@ impl SessionManager {
mcp_config,
None,
model_resolver,
crate::config::CompactionConfig::default(),
)
.map(|(session_manager, _, _, _, _)| session_manager)
}

View File

@ -28,6 +28,7 @@ const ToolsTab = lazy(() => import('./tabs/ToolsTab').then((m) => ({ default: m.
const MemoryTab = lazy(() => import('./tabs/MemoryTab').then((m) => ({ default: m.MemoryTab })));
const SchedulerTab = lazy(() => import('./tabs/SchedulerTab').then((m) => ({ default: m.SchedulerTab })));
const ImageTab = lazy(() => import('./tabs/ImageTab').then((m) => ({ default: m.ImageTab })));
const CompactionTab = lazy(() => import('./tabs/CompactionTab').then((m) => ({ default: m.CompactionTab })));
const TimeTab = lazy(() => import('./tabs/TimeTab').then((m) => ({ default: m.TimeTab })));
const GatewayTab = lazy(() => import('./tabs/GatewayTab').then((m) => ({ default: m.GatewayTab })));
const ConnectionTab = lazy(() => import('./tabs/ConnectionTab').then((m) => ({ default: m.ConnectionTab })));
@ -212,6 +213,8 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
return <MemoryTab {...commonProps} />;
case 'image':
return <ImageTab {...commonProps} />;
case 'compaction':
return <CompactionTab {...commonProps} />;
case 'subagents':
return <SubagentsTab {...commonProps} setToast={showToast} />;
case 'experts':

View File

@ -14,6 +14,7 @@ import {
Server,
Users,
UserCheck,
Archive,
} from 'lucide-react';
import type { TabId } from './types';
@ -54,6 +55,7 @@ export const TAB_GROUPS: TabGroup[] = [
tabs: [
{ id: 'gateway', label: '网关', icon: Server },
{ id: 'scheduler', label: '调度器', icon: Calendar },
{ id: 'compaction', label: '上下文压缩', icon: Archive },
{ id: 'memory', label: '记忆维护', icon: Users },
{ id: 'image', label: '图片上下文', icon: Image },
{ id: 'time', label: '时间', icon: Clock },

View File

@ -0,0 +1,87 @@
// CompactionTab - 上下文压缩算法配置
import { Field, SectionCard } from '../ui';
import { inputCls } from '../constants';
import type { TabProps } from '../shared';
export function CompactionTab({ config, update }: TabProps) {
return (
<>
<SectionCard title="压缩触发阈值">
<Field
label="工程化压缩阈值"
hint="当 LLM 返回的 prompt_tokens 超过 context_window × 此比例时触发压缩。默认 0.770%"
>
<input
type="number"
step="0.1"
min="0.1"
max="1.0"
value={config.compaction.threshold_ratio}
onChange={(e) =>
update('compaction', {
...config.compaction,
threshold_ratio: +e.target.value,
})
}
className={inputCls}
/>
</Field>
<Field
label="LLM 压缩阈值"
hint="工程化压缩(截断 tool 结果)后,若估算 token 仍超过此比例才调 LLM 压缩。默认 0.550%"
>
<input
type="number"
step="0.1"
min="0.1"
max="1.0"
value={config.compaction.llm_compaction_threshold_ratio}
onChange={(e) =>
update('compaction', {
...config.compaction,
llm_compaction_threshold_ratio: +e.target.value,
})
}
className={inputCls}
/>
</Field>
</SectionCard>
<SectionCard title="压缩参数">
<Field
label="Tool 结果截断 Token 数"
hint="工程化压缩时,非子代理的 tool 结果截断到此 token 数。子代理返回不受此限制。默认 100"
>
<input
type="number"
min="10"
value={config.compaction.truncate_max_tokens}
onChange={(e) =>
update('compaction', {
...config.compaction,
truncate_max_tokens: +e.target.value,
})
}
className={inputCls}
/>
</Field>
<Field
label="三段保留 Unit 数"
hint="LLM 压缩时保留最旧 N 个和最新 N 个 unit 原样,中间段用 LLM 生成摘要。默认 5"
>
<input
type="number"
min="1"
value={config.compaction.preserve_count}
onChange={(e) =>
update('compaction', {
...config.compaction,
preserve_count: +e.target.value,
})
}
className={inputCls}
/>
</Field>
</SectionCard>
</>
);
}

View File

@ -233,6 +233,13 @@ export interface SchedulerJobConfig {
[key: string]: unknown;
}
export interface CompactionConfig {
threshold_ratio: number;
llm_compaction_threshold_ratio: number;
truncate_max_tokens: number;
preserve_count: number;
}
export interface AppConfig {
providers: Record<string, ProviderConfig>;
models: Record<string, ModelConfig>;
@ -246,6 +253,7 @@ export interface AppConfig {
image_context: ImageContextConfig;
subagents: SubagentsConfig;
experts: ExpertsConfig;
compaction: CompactionConfig;
client: ClientConfig;
channels: Record<string, ChannelConfig>;
mcpServers: Record<string, McpServerConfig>;
@ -266,7 +274,8 @@ export type TabId =
| 'subagents'
| 'experts'
| 'mcp'
| 'channels';
| 'channels'
| 'compaction';
export interface ConfigPageProps {
onClose: () => void;