Packages
fnord
0.8.71
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 """
Summarize this conversation transcript concisely while preserving essential context.
You will receive a JSON transcript of messages between a user and an AI assistant, including tool outputs and research.
Focus on: what the user asked for, what was learned or discovered, decisions made, and what work is in progress.
Preserve specific details about files, functions, bugs, and technical decisions.
Use plain text without special characters.
Output format:
# User Request
[What the user is asking for or working on]
# Key Findings
[Important information discovered: 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
)
|> 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