Current section
Files
Jump to
Current section
Files
lib/cmdc/tool/compact_conversation.ex
defmodule CMDC.Tool.CompactConversation do
@moduledoc """
手动触发上下文压缩,对标 DeepAgents `compact_conversation` 工具。
在消息历史过长但尚未达到自动压缩阈值时,Agent 可主动调用此工具
立即执行压缩,释放上下文窗口空间。
## 执行流程
1. 读取当前 `Agent.State`(通过 `agent_pid` 上下文字段)
2. 调用 `CMDC.Agent.Compactor.force_compact/1`
3. 更新 Agent 状态
4. 返回压缩统计信息
## 与 Agent 的交互
此工具需要在 context 中包含 `agent_pid`,由 Agent 在构建工具上下文时注入。
若无 `agent_pid`,工具仍可运行但不会实际修改 Agent 状态。
## 返回格式
Conversation compacted successfully.
- Messages before: 42
- Messages after: 8
- Messages removed: 34
- History saved to: /conversation_history/session-abc.md
"""
@behaviour CMDC.Tool
alias CMDC.Agent.Compactor
@impl true
def name, do: "compact_conversation"
@impl true
def description do
"Manually compact the conversation history to free up context window space. " <>
"Use this when the conversation is getting long and you need more room " <>
"for complex tasks. The full history is saved to a file for reference."
end
@impl true
def meta(_), do: "CompactConversation"
@impl true
def parameters do
%{
"type" => "object",
"properties" => %{
"reason" => %{
"type" => "string",
"description" =>
"Optional reason for compacting (e.g. \"context getting long before complex task\")."
}
},
"required" => []
}
end
@impl true
def execute(args, context) do
reason = Map.get(args, "reason", "manual compact")
agent_pid = Map.get(context, :agent_pid)
if is_pid(agent_pid) and Process.alive?(agent_pid) do
compact_via_agent(agent_pid, reason)
else
{:error,
"compact_conversation requires a running Agent process. " <>
"This tool must be called within an active Agent session."}
end
end
# ==========================================================================
# 私有辅助
# ==========================================================================
defp compact_via_agent(agent_pid, reason) do
state = CMDC.Agent.get_state(agent_pid)
before_count = length(state.messages)
case Compactor.force_compact(state) do
{:compacted, new_state} ->
after_count = length(new_state.messages)
removed = before_count - after_count
apply_state_update(agent_pid, new_state)
offload_path = extract_offload_path(new_state, state)
{:ok, build_result(before_count, after_count, removed, offload_path, reason)}
{:skip, _state} ->
{:ok,
"No compaction needed — conversation is too short to compact " <>
"(#{before_count} messages)."}
end
end
defp apply_state_update(agent_pid, new_state) do
:sys.replace_state(agent_pid, fn {state_name, _old_state} ->
{state_name, new_state}
end)
rescue
_ -> :ok
end
defp extract_offload_path(new_state, old_state) do
opts = Compactor.compact_config(old_state)
path = Path.join(opts.offload_dir, "#{new_state.session_id}.md")
full = Path.join(new_state.working_dir, path)
if File.exists?(full), do: path, else: nil
end
defp build_result(before, after_count, removed, offload_path, reason) do
lines = [
"Conversation compacted successfully. (reason: #{reason})",
"- Messages before: #{before}",
"- Messages after: #{after_count}",
"- Messages removed: #{removed}"
]
lines =
if offload_path do
lines ++ ["- History saved to: #{offload_path}"]
else
lines
end
Enum.join(lines, "\n")
end
end