use super::super::config::AgentLoopConfig;
use super::super::tool_processor::{clear_channels, push_both, sync_context_full};
use crate::command::chat::app::types::StreamMsg;
use crate::command::chat::context::compact::{self, AutoCompactParams, InvokedSkillsMap};
use crate::command::chat::error::ChatError;
use crate::command::chat::infra::hook::{HookContext, HookEvent};
use crate::command::chat::storage::{ChatMessage, MessageRole, ToolCallItem};
use crate::util::log::{write_error_log, write_info_log};
use std::sync::{Arc, Mutex, mpsc};
pub(super) struct CompactContext<'a> {
pub config: &'a AgentLoopConfig,
pub messages: &'a mut Vec<ChatMessage>,
pub display_messages: &'a Arc<Mutex<Vec<ChatMessage>>>,
pub context_messages: &'a Arc<Mutex<Vec<ChatMessage>>>,
pub invoked_skills: &'a InvokedSkillsMap,
pub session_id: &'a str,
pub system_prompt: &'a Option<String>,
pub tx: &'a mpsc::Sender<StreamMsg>,
}
pub(super) async fn run_compact_phase(ctx: &mut CompactContext<'_>) -> bool {
if !ctx.config.compact_config.enabled {
return false;
}
let mut compact_aborted = false;
if ctx
.config
.hook_manager
.has_hooks_for(HookEvent::PreMicroCompact)
{
let hook_ctx = HookContext {
event: HookEvent::PreMicroCompact,
messages: Some(ctx.messages.clone()),
model: Some(ctx.config.provider.model.clone()),
session_id: Some(ctx.session_id.to_string()),
..Default::default()
};
if let Some(result) = ctx.config.hook_manager.execute(
HookEvent::PreMicroCompact,
hook_ctx,
&ctx.config.disabled_hooks,
) && result.is_stop()
{
write_info_log(
"PreMicroCompact hook",
"compact 子管线被 hook 中止(跳过 micro + auto)",
);
compact_aborted = true;
}
}
if compact_aborted {
return true;
}
compact::micro_compact(
ctx.messages,
ctx.config.compact_config.keep_recent,
&ctx.config.compact_config.micro_compact_exempt_tools,
);
if ctx
.config
.hook_manager
.has_hooks_for(HookEvent::PostMicroCompact)
{
let hook_ctx = HookContext {
event: HookEvent::PostMicroCompact,
messages: Some(ctx.messages.clone()),
session_id: Some(ctx.session_id.to_string()),
..Default::default()
};
if let Some(result) = ctx.config.hook_manager.execute(
HookEvent::PostMicroCompact,
hook_ctx,
&ctx.config.disabled_hooks,
) && let Some(new_msgs) = result.messages
{
*ctx.messages = new_msgs;
}
}
if compact::estimate_tokens(ctx.messages)
> ctx.config.compact_config.effective_token_threshold()
{
write_info_log(
"agent_loop",
"auto_compact triggered (token threshold exceeded)",
);
let mut protected_context: Option<String> = None;
if ctx
.config
.hook_manager
.has_hooks_for(HookEvent::PreAutoCompact)
{
let hook_ctx = HookContext {
event: HookEvent::PreAutoCompact,
messages: Some(ctx.messages.clone()),
system_prompt: ctx.system_prompt.clone(),
model: Some(ctx.config.provider.model.clone()),
session_id: Some(ctx.session_id.to_string()),
..Default::default()
};
if let Some(result) = ctx.config.hook_manager.execute(
HookEvent::PreAutoCompact,
hook_ctx,
&ctx.config.disabled_hooks,
) {
if result.is_stop() {
write_info_log("PreAutoCompact hook", "auto_compact 被 hook 中止");
compact_aborted = true;
}
if let Some(ac) = result.additional_context {
protected_context = Some(ac);
}
}
}
if !compact_aborted {
let _ = ctx.tx.send(StreamMsg::Compacting);
match compact::auto_compact(
ctx.messages,
&AutoCompactParams {
provider: &ctx.config.provider,
invoked_skills: ctx.invoked_skills,
session_id: ctx.session_id,
protected_context: protected_context.as_deref(),
},
)
.await
{
Err(e) => {
write_error_log("agent_loop", &format!("auto_compact failed: {}", e));
}
Ok(result) => {
clear_channels(ctx.display_messages, ctx.context_messages);
push_compact_tool_messages(
ctx.messages,
ctx.display_messages,
ctx.context_messages,
&result,
);
let _ = ctx.tx.send(StreamMsg::Compacted {
messages_before: result.messages_before,
});
if ctx
.config
.hook_manager
.has_hooks_for(HookEvent::PostAutoCompact)
{
let hook_ctx = HookContext {
event: HookEvent::PostAutoCompact,
messages: Some(ctx.messages.clone()),
session_id: Some(ctx.session_id.to_string()),
..Default::default()
};
if let Some(hook_result) = ctx.config.hook_manager.execute(
HookEvent::PostAutoCompact,
hook_ctx,
&ctx.config.disabled_hooks,
) && let Some(new_msgs) = hook_result.messages
{
*ctx.messages = new_msgs;
sync_context_full(
ctx.display_messages,
ctx.context_messages,
ctx.messages,
);
}
}
}
}
}
}
compact_aborted
}
pub(super) fn push_compact_tool_messages(
messages: &mut Vec<ChatMessage>,
display: &Arc<Mutex<Vec<ChatMessage>>>,
context: &Arc<Mutex<Vec<ChatMessage>>>,
compact_result: &compact::CompactResult,
) {
let tool_call_id = format!("compact_auto_{}", compact_result.messages_before);
for msg in &compact_result.recent_user_messages {
push_both(display, context, msg.clone());
}
let tool_call_item = ToolCallItem {
id: tool_call_id.clone(),
name: "Compact".to_string(),
arguments: r#"{"reason":"auto_compact"}"#.to_string(),
};
let tool_call_msg = ChatMessage {
role: MessageRole::Assistant,
content: String::new(),
tool_calls: Some(vec![tool_call_item]),
tool_call_id: None,
images: None,
reasoning_content: None,
sender_name: None,
};
messages.push(tool_call_msg.clone());
push_both(display, context, tool_call_msg);
let result_content = format!(
"📦 上下文已压缩 ({} 条消息 → 摘要, transcript: {})\n\n{}",
compact_result.messages_before, compact_result.transcript_path, compact_result.summary,
);
let tool_msg = ChatMessage {
role: MessageRole::Tool,
content: result_content,
tool_calls: None,
tool_call_id: Some(tool_call_id),
images: None,
reasoning_content: None,
sender_name: None,
};
messages.push(tool_msg.clone());
push_both(display, context, tool_msg);
}
pub(super) struct RecoverContext<'a> {
pub config: &'a AgentLoopConfig,
pub messages: &'a mut Vec<ChatMessage>,
pub display_messages: &'a Arc<Mutex<Vec<ChatMessage>>>,
pub context_messages: &'a Arc<Mutex<Vec<ChatMessage>>>,
pub streaming_content: &'a Arc<Mutex<String>>,
pub streaming_reasoning_content: &'a Arc<Mutex<String>>,
pub invoked_skills: &'a InvokedSkillsMap,
pub session_id: &'a str,
pub tx: &'a mpsc::Sender<StreamMsg>,
}
pub(super) async fn recover_tool_id_mismatch(ctx: &mut RecoverContext<'_>) -> bool {
use crate::util::safe_lock;
write_info_log(
"agent_loop",
"tool_call_id 不一致错误:将执行 auto_compact 压缩上下文后重试",
);
{
let mut stream_buf = safe_lock(ctx.streaming_content, "agent::tool_id_error_clear");
stream_buf.clear();
}
{
let mut reason_buf = safe_lock(
ctx.streaming_reasoning_content,
"agent::tool_id_error_reason_clear",
);
reason_buf.clear();
}
if ctx.config.compact_config.enabled {
let _ = ctx.tx.send(StreamMsg::Compacting);
match compact::auto_compact(
ctx.messages,
&AutoCompactParams {
provider: &ctx.config.provider,
invoked_skills: ctx.invoked_skills,
session_id: ctx.session_id,
protected_context: None,
},
)
.await
{
Err(e) => {
write_error_log(
"agent_loop",
&format!("tool_call_id 恢复时 auto_compact 失败: {}", e),
);
let _ = ctx.tx.send(StreamMsg::Error(ChatError::Other(format!(
"消息历史损坏且自动修复失败: {}",
e
))));
false
}
Ok(result) => {
clear_channels(ctx.display_messages, ctx.context_messages);
push_compact_tool_messages(
ctx.messages,
ctx.display_messages,
ctx.context_messages,
&result,
);
let _ = ctx.tx.send(StreamMsg::Compacted {
messages_before: result.messages_before,
});
true
}
}
} else {
let _ = ctx.tx.send(StreamMsg::Error(ChatError::Other(
"消息历史中 tool_call_id 不一致,且 compact 未启用,无法自动恢复".to_string(),
)));
false
}
}