Packages
fnord
0.8.73
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/agent/compactor.ex
defmodule AI.Agent.Compactor do
@behaviour AI.Agent
@model AI.Model.large_context(:balanced)
@max_attempts 3
@min_length 512
@target_ratio 0.8
# Minimum acceptable tokens for a compacted summary; prevent trivial context wipes
@min_summary_tokens 100
@system_prompt """
You are summarizing a conversation transcript between a user and an AI assistant.
The transcript you will receive shows the actual conversation messages, including tool outputs and research.
Your task is to extract and organize information FROM the transcript:
- What was the original prompt or topic of the transcript?
- How did the topic and intent evolve over the course of the conversation?
- What was learned or discovered during that conversation?
- What decisions were made?
- What work was in progress when the conversation got too long?
Preserve specific details about files, functions, bugs, and technical decisions.
Use plain text without special characters.
Output format:
# Synopsis
[What is the topic of the conversation transcript? What was the original request? Summarize how the conversation evolved over the course of the transcript.]
# Evolution
[Explain the evolution of the transcript in more detail. Focus on changes in direction, new findings, the "cascade" of decision-making, and shifts in user intent.]
# Key Findings
[Important information discovered during the conversation: file locations, function names, patterns, bugs found, etc.]
# Current Status
[What the assistant was doing when context limit approached. Include enough detail that work can resume exactly where it left off.]
"""
@impl AI.Agent
def get_response(%{messages: [%{role: "developer", content: @system_prompt} | _]}) do
raise "Refusing to compact a compaction prompt"
end
def get_response(%{messages: messages} = opts) do
attempts = Map.get(opts, :attempts, 0)
tx_list = transcript(messages, [])
# Early guard: empty transcript or user-only transcript -> skip model call and retries
# User messages are preserved separately, so if there's nothing but user messages, there's nothing to summarize
has_non_user = Enum.any?(tx_list, fn msg -> msg.role != "user" end)
if tx_list == [] or not has_non_user do
{:error, :empty_after_filtering}
else
transcript_json = Jason.encode!(tx_list, pretty: true)
original_length = byte_size(transcript_json)
UI.info(
"Compaction starting",
"Transcript JSON size: #{original_length} bytes. Recent messages preserved; proceeding with compaction."
)
UI.info(
"Summarizing conversation transcript (expected)",
"No new user prompt was added for this compaction pass. We are summarizing the transcript for compactness; " <>
"recent messages (including your latest prompts) remain intact."
)
AI.Accumulator.get_response(
model: @model,
prompt: @system_prompt,
input: transcript_json,
question:
"Review this conversation transcript and extract: what the user originally requested, key findings, and current work status."
)
|> case do
{:ok, %{response: response}} ->
summary =
"""
Summary of conversation and research thus far:
#{response}
"""
# Append task lists if any exist
summary =
case Services.Task.list_ids() do
[] ->
summary
list_ids ->
task_sections =
list_ids
|> Enum.map(&Services.Task.as_string/1)
|> Enum.join("\n\n")
"""
#{summary}
## Active Task Lists
The following task lists were active when compaction occurred.
These tasks represent work in progress and should be consulted when resuming work.
#{task_sections}
"""
end
# Guard against trivial, near-empty summaries that would wipe context
new_tokens = AI.PretendTokenizer.guesstimate_tokens(summary)
if new_tokens < @min_summary_tokens do
UI.error(
"Compaction failed",
"Summary too small (#{new_tokens} < #{@min_summary_tokens} tokens)"
)
{:error, :summary_too_small}
else
new_length = byte_size(summary)
difference = original_length - new_length
percent = difference / original_length * 100.0
UI.info("""
Compaction results:
Original: #{Util.format_number(original_length)} bytes
Compacted: #{Util.format_number(new_length)} bytes
Savings: #{percent}% (#{Util.format_number(difference)} bytes)
""")
cond do
original_length < @min_length ->
UI.debug("Compaction retries skipped", "Original is too small to justify retries")
if new_length < original_length do
summary |> AI.Util.system_msg() |> then(&{:ok, [&1]})
else
UI.warn("Compaction failed", "Summary is larger than original; aborting")
{:error, :compaction_failed}
end
new_length > original_length * @target_ratio and attempts < @max_attempts ->
UI.warn(
"Compaction insufficient",
"Attempting another pass (#{attempts + 1}/#{@max_attempts})"
)
get_response(%{messages: messages, attempts: attempts + 1})
true ->
UI.debug("Compacted conversation", summary)
if new_length < original_length do
summary |> AI.Util.system_msg() |> then(&{:ok, [&1]})
else
UI.warn("Compaction failed", "Summary is larger than original; aborting")
{:error, :compaction_failed}
end
end
end
{:error, reason} ->
{:error, reason}
end
end
end
defp transcript([], acc), do: Enum.reverse(acc)
defp transcript([%{role: "system"} | rest], acc), do: transcript(rest, acc)
defp transcript([%{role: "developer"} | rest], acc), do: transcript(rest, acc)
defp transcript([%{role: "user"} = msg | rest], acc), do: transcript(rest, [msg | acc])
defp transcript([%{role: "tool", name: "notify_tool"} | rest], acc), do: transcript(rest, acc)
defp transcript([%{role: "tool", name: name, content: content} | rest], acc) do
transcript(rest, [%{role: "tool", name: name, content: content} | acc])
end
defp transcript([%{role: "assistant", content: nil} | rest], acc), do: transcript(rest, acc)
defp transcript([%{role: "assistant", content: content} = msg | rest], acc)
when is_binary(content) do
if String.starts_with?(content, "<think>") do
# skip internal reasoning
transcript(rest, acc)
else
# include assistant message
transcript(rest, [msg | acc])
end
end
defp transcript([msg | rest], acc), do: transcript(rest, [msg | acc])
end