Packages
fnord
0.8.57
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.65
@system_prompt """
You are an AI Agent in a larger system.
You will be presented with a transcript of a conversation between a user and an AI assistant, along with any research the assistant has done.
Your task: Reformat into compact meeting minutes while preserving all content needed for context.
Do not use smart quotes, smart apostrophes, emojis, or other special characters.
Read through the messages and reason through a new, more compact narrative that preserves the key points and context.
Consider the conversation from the user's perspective: what expectations would they have for the assistant's memory and understanding of the conversation?
Use the following output template:
# Original User Prompt
[full text of the original user prompt]
# Research and Responses
[outline of facts, findings, and conclusions from the research portions of the conversation]
# Conversation Timeline
[
Present a timeline of the conversation, listing each message with its role and a brief summary of its content.
Messages at the end of the conversation should include WAY more detail than those at the beginning.
Don't waste space with formatting or JSON; just use plain text.
]
# Continuation Context
[
What was the assistant doing before the conversation grew too long?
Your response will form the complete prompt for the LLM's next response.
This section MUST guarantee that the LLM continues 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 -> skip model call and retries
if tx_list == [] do
UI.warn("Compaction skipped", "Transcript is empty after filtering; no-op")
last_visible =
messages
|> Enum.reverse()
|> Enum.find(fn
%{role: role, content: content}
when role in ["user", "assistant"] and is_binary(content) ->
not String.starts_with?(content, "<think>")
_ ->
false
end)
base = "Summary of conversation and research thus far:\n"
summary =
case last_visible do
nil -> base <> "<empty transcript>"
%{content: content} -> base <> content
end
summary
|> AI.Util.system_msg()
|> then(&{:ok, [&1]})
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.Completion.get(
model: @model,
prompt: @system_prompt,
messages: [
AI.Util.system_msg(@system_prompt),
AI.Util.system_msg("""
The complete message transcript in JSON format:
```json
#{transcript_json}
```
Your output MUST use the specified template and include ALL sections.
""")
]
)
|> case do
{:ok, %{response: response}} ->
summary =
"""
Summary of conversation and research thus far:
#{response}
"""
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 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
{: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: "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