Packages
fnord
0.8.83
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/ai/completion/compaction.ex
defmodule AI.Completion.Compaction do
@moduledoc """
Compacts conversation history for AI completion requests. Compacts by
summarizing tool calls and assistant messages following each user message,
while preserving user messages and special system messages.
"""
@spec compact(map(), map()) :: map()
@spec compact(map(), map()) :: map()
def compact(state, opts) do
with {:ok, {name_msg, msgs}} <- extract_name_and_msgs(state),
{:ok, segments} <- build_segments(msgs),
{:ok, first_pass} <- run_first_pass(state, name_msg, segments, opts),
{:ok, final_state} <- maybe_run_second_pass(state, name_msg, first_pass, opts) do
final_state
else
{:error, _reason} ->
# On any internal error, log and preserve the original state
UI.warn(
"Compaction skipped",
"Internal error during compaction; original history preserved"
)
state
end
end
defp extract_name_and_msgs(state) do
messages = Map.get(state, :messages, [])
name_msg =
Enum.find(messages, fn
%{role: "system", content: content} when is_binary(content) ->
content =~ ~r/Your name is .+\./
_ ->
false
end)
msgs =
case name_msg do
nil -> messages
msg -> List.delete(messages, msg)
end
{:ok, {name_msg, msgs}}
end
defp build_segments(msgs) do
{segments, last_user, last_non_user} =
Enum.reduce(msgs, {[], nil, []}, fn msg, {segs, curr_user, curr_non_user} ->
if Map.get(msg, :role) == "user" do
new_segs =
if curr_user do
segs ++ [%{user: curr_user, non_user: curr_non_user}]
else
segs
end
{new_segs, msg, []}
else
{segs, curr_user, curr_non_user ++ [msg]}
end
end)
segments =
if last_user do
segments ++ [%{user: last_user, non_user: last_non_user}]
else
segments
end
{:ok, segments}
end
defp run_first_pass(_state, name_msg, segments, _opts) do
{head_segments, last_segment_list} =
case segments do
[] -> {[], []}
_ -> Enum.split(segments, -1)
end
{compacted_head, ctx_after_head} =
Enum.reduce(head_segments, {[], []}, fn %{user: user, non_user: non_user}, {acc, ctx} ->
to_summarize = ctx ++ [user] ++ non_user
summary_result =
AI.Agent.Compactor
|> AI.Agent.new(named?: false)
|> AI.Agent.get_response(%{messages: to_summarize})
case summary_result do
{:ok, [summary_msg]} ->
msgs = [user, summary_msg]
{acc ++ msgs, ctx ++ msgs}
_ ->
msgs = [user] ++ non_user
{acc ++ msgs, ctx ++ msgs}
end
end)
raw_last_msgs =
case last_segment_list do
[%{user: user, non_user: non_user}] -> [user] ++ non_user
_ -> []
end
first_pass_messages =
if(name_msg, do: [name_msg], else: []) ++
compacted_head ++
raw_last_msgs
# Filter out all but the latest summary system message
filtered_first_messages = keep_latest_summary(first_pass_messages)
new_usage_1 =
filtered_first_messages
|> Enum.map(&Map.get(&1, :content))
|> Enum.filter(&is_binary/1)
|> Enum.map(&AI.PretendTokenizer.guesstimate_tokens/1)
|> Enum.sum()
{:ok,
%{
messages: filtered_first_messages,
usage: new_usage_1,
compacted_head: compacted_head,
ctx_after_head: ctx_after_head,
last_segment_list: last_segment_list
}}
end
defp maybe_run_second_pass(state, name_msg, first_pass, opts) do
%{
messages: first_msgs,
usage: usage_1,
compacted_head: compacted_head,
ctx_after_head: ctx_after_head,
last_segment_list: last_segment_list
} = first_pass
target_pct = Map.get(opts, :target_pct, 0.8)
if last_segment_list == [] or
meets_target?(state.model.context, usage_1, target_pct) do
UI.info("Compaction run", "Simplified partial compaction completed")
{:ok, %{state | messages: first_msgs, usage: usage_1}}
else
{compacted_last, _} =
Enum.reduce(last_segment_list, {[], ctx_after_head}, fn %{user: user, non_user: non_user},
{acc, ctx} ->
to_summarize = ctx ++ [user] ++ non_user
summary_result =
AI.Agent.Compactor
|> AI.Agent.new(named?: false)
|> AI.Agent.get_response(%{messages: to_summarize})
case summary_result do
{:ok, [summary_msg]} ->
msgs = [user, summary_msg]
{acc ++ msgs, ctx ++ msgs}
_ ->
msgs = [user] ++ non_user
{acc ++ msgs, ctx ++ msgs}
end
end)
second_pass_messages =
if(name_msg, do: [name_msg], else: []) ++
compacted_head ++
compacted_last
filtered_second_messages = keep_latest_summary(second_pass_messages)
new_usage_2 =
filtered_second_messages
|> Enum.map(&Map.get(&1, :content))
|> Enum.filter(&is_binary/1)
|> Enum.map(&AI.PretendTokenizer.guesstimate_tokens/1)
|> Enum.sum()
UI.info("Compaction run", "Simplified partial compaction completed")
{:ok, %{state | messages: filtered_second_messages, usage: new_usage_2}}
end
end
defp meets_target?(context, usage, target_pct) when is_number(context) and context > 0 do
usage <= target_pct * context
end
defp meets_target?(_context, _usage, _target_pct), do: true
# Retains only the latest summary system message, removing earlier ones with the same prefix
defp keep_latest_summary(messages) do
prefix = "Summary of conversation and research thus far:"
# Helper to identify summary system messages
summary_system_msg? = fn msg ->
case msg do
%{role: "system", content: content} when is_binary(content) ->
String.starts_with?(content, prefix)
_ ->
false
end
end
last_summary_index =
messages
|> Enum.with_index()
|> Enum.filter(fn {msg, _idx} -> summary_system_msg?.(msg) end)
|> Enum.map(fn {_msg, idx} -> idx end)
|> List.last()
if last_summary_index == nil do
messages
else
messages
|> Enum.with_index()
|> Enum.reject(fn {msg, idx} ->
summary_system_msg?.(msg) and idx != last_summary_index
end)
|> Enum.map(fn {msg, _idx} -> msg end)
end
end
end