Packages

fnord

0.8.70

AI code archaeology

Current section

221 Versions

Jump to

Compare versions

10 files changed
+806 additions
-50 deletions
  @@ -1,6 +1,6 @@
1 1 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/sysread/fnord">>}]}.
2 2 {<<"name">>,<<"fnord">>}.
3 - {<<"version">>,<<"0.8.69">>}.
3 + {<<"version">>,<<"0.8.70">>}.
4 4 {<<"description">>,<<"AI code archaeology">>}.
5 5 {<<"elixir">>,<<"~> 1.18">>}.
6 6 {<<"app">>,<<"fnord">>}.
  @@ -98,10 +98,12 @@
98 98 <<"lib/ai/tools/list_projects.ex">>,<<"lib/ai/notes.ex">>,
99 99 <<"lib/ai/completion">>,<<"lib/ai/completion/output.ex">>,
100 100 <<"lib/ai/completion/compaction.ex">>,<<"lib/ai/agent">>,
101 - <<"lib/ai/agent/code_mapper.ex">>,<<"lib/ai/agent/compactor.ex">>,
102 - <<"lib/ai/agent/coordinator.ex">>,<<"lib/ai/agent/nomenclater.ex">>,
103 - <<"lib/ai/agent/spelunker.ex">>,<<"lib/ai/agent/code">>,
104 - <<"lib/ai/agent/code/task_implementor.ex">>,
101 + <<"lib/ai/agent/code_mapper.ex">>,<<"lib/ai/agent/compactor.ex.0.3.bak">>,
102 + <<"lib/ai/agent/compactor.ex.0.2.bak">>,<<"lib/ai/agent/compactor.ex">>,
103 + <<"lib/ai/agent/compactor.ex.0.0.bak">>,
104 + <<"lib/ai/agent/compactor.ex.0.1.bak">>,<<"lib/ai/agent/coordinator.ex">>,
105 + <<"lib/ai/agent/nomenclater.ex">>,<<"lib/ai/agent/spelunker.ex">>,
106 + <<"lib/ai/agent/code">>,<<"lib/ai/agent/code/task_implementor.ex">>,
105 107 <<"lib/ai/agent/code/task_planner.ex">>,
106 108 <<"lib/ai/agent/code/task_validator.ex">>,
107 109 <<"lib/ai/agent/code/repatcher.ex">>,<<"lib/ai/agent/code/common.ex">>,
  @@ -7,40 +7,26 @@ defmodule AI.Agent.Compactor do
7 7 @target_ratio 0.8
8 8
9 9 # Minimum acceptable tokens for a compacted summary; prevent trivial context wipes
10 - # Note: set to >0 in production if you want to reject trivial summaries; tests expect tiny outputs.
11 - @min_summary_tokens 0
10 + @min_summary_tokens 100
12 11
13 12 @system_prompt """
14 - You are an AI Agent in a larger system.
15 - 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.
16 - Your task: Reformat into compact meeting minutes while preserving all content needed for context.
17 - Do not use smart quotes, smart apostrophes, emojis, or other special characters.
18 - Read through the messages and reason through a new, more compact narrative that preserves the key points and context.
19 - Consider the conversation from the user's perspective: what expectations would they have for the assistant's memory and understanding of the conversation?
13 + Summarize this conversation transcript concisely while preserving essential context.
14 + You will receive a JSON transcript of messages between a user and an AI assistant, including tool outputs and research.
20 15
21 - Preserve decision points, assumptions, trade-offs, mistakes and corrections, and include rationales leading to any state changes.
16 + Focus on: what the user asked for, what was learned or discovered, decisions made, and what work is in progress.
17 + Preserve specific details about files, functions, bugs, and technical decisions.
18 + Use plain text without special characters.
22 19
23 - Use the following output template:
20 + Output format:
24 21
25 - # Original User Prompt
26 - [full text of the original user prompt]
22 + # User Request
23 + [What the user is asking for or working on]
27 24
28 - # Research and Responses
29 - [outline of facts, findings, and conclusions from the research portions of the conversation]
25 + # Key Findings
26 + [Important information discovered: file locations, function names, patterns, bugs found, etc.]
30 27
31 - # Conversation Timeline
32 - [
33 - Present a timeline of the conversation, listing each message with its role and a brief summary of its content.
34 - Messages at the end of the conversation should include WAY more detail than those at the beginning, reflecting the decision cascade and evolving context
35 - Don't waste space with formatting or JSON; just use plain text.
36 - ]
37 -
38 - # Continuation Context
39 - [
40 - What was the assistant doing before the conversation grew too long?
41 - Your response will form the complete prompt for the LLM's next response.
42 - This section MUST guarantee that the LLM continues exactly where it left off.
43 - ]
28 + # Current Status
29 + [What the assistant was doing when context limit approached. Include enough detail that work can resume exactly where it left off.]
44 30 """
45 31
46 32 @impl AI.Agent
  @@ -53,8 +39,11 @@ defmodule AI.Agent.Compactor do
53 39
54 40 tx_list = transcript(messages, [])
55 41
56 - # Early guard: empty transcript -> skip model call and retries
57 - if tx_list == [] do
42 + # Early guard: empty transcript or user-only transcript -> skip model call and retries
43 + # User messages are preserved separately, so if there's nothing but user messages, there's nothing to summarize
44 + has_non_user = Enum.any?(tx_list, fn msg -> msg.role != "user" end)
45 +
46 + if tx_list == [] or not has_non_user do
58 47 {:error, :empty_after_filtering}
59 48 else
60 49 transcript_json = Jason.encode!(tx_list, pretty: true)
  @@ -146,6 +135,7 @@ defmodule AI.Agent.Compactor do
146 135 defp transcript([], acc), do: Enum.reverse(acc)
147 136 defp transcript([%{role: "system"} | rest], acc), do: transcript(rest, acc)
148 137 defp transcript([%{role: "developer"} | rest], acc), do: transcript(rest, acc)
138 + defp transcript([%{role: "user"} = msg | rest], acc), do: transcript(rest, [msg | acc])
149 139 defp transcript([%{role: "tool", name: "notify_tool"} | rest], acc), do: transcript(rest, acc)
150 140
151 141 defp transcript([%{role: "tool", name: name, content: content} | rest], acc) do
  @@ -0,0 +1,170 @@
1 + defmodule AI.Agent.Compactor do
2 + @behaviour AI.Agent
3 +
4 + @model AI.Model.large_context(:balanced)
5 + @max_attempts 3
6 + @min_length 512
7 + @target_ratio 0.8
8 +
9 + # Minimum acceptable tokens for a compacted summary; prevent trivial context wipes
10 + # Note: set to >0 in production if you want to reject trivial summaries; tests expect tiny outputs.
11 + @min_summary_tokens 0
12 +
13 + @system_prompt """
14 + You are an AI Agent in a larger system.
15 + 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.
16 + Your task: Reformat into compact meeting minutes while preserving all content needed for context.
17 + Do not use smart quotes, smart apostrophes, emojis, or other special characters.
18 + Read through the messages and reason through a new, more compact narrative that preserves the key points and context.
19 + Consider the conversation from the user's perspective: what expectations would they have for the assistant's memory and understanding of the conversation?
20 +
21 + Preserve decision points, assumptions, trade-offs, mistakes and corrections, and include rationales leading to any state changes.
22 +
23 + Use the following output template:
24 +
25 + # Original User Prompt
26 + [full text of the original user prompt]
27 +
28 + # Research and Responses
29 + [outline of facts, findings, and conclusions from the research portions of the conversation]
30 +
31 + # Conversation Timeline
32 + [
33 + Present a timeline of the conversation, listing each message with its role and a brief summary of its content.
34 + Messages at the end of the conversation should include WAY more detail than those at the beginning, reflecting the decision cascade and evolving context
35 + Don't waste space with formatting or JSON; just use plain text.
36 + ]
37 +
38 + # Continuation Context
39 + [
40 + What was the assistant doing before the conversation grew too long?
41 + Your response will form the complete prompt for the LLM's next response.
42 + This section MUST guarantee that the LLM continues exactly where it left off.
43 + ]
44 + """
45 +
46 + @impl AI.Agent
47 + def get_response(%{messages: [%{role: "developer", content: @system_prompt} | _]}) do
48 + raise "Refusing to compact a compaction prompt"
49 + end
50 +
51 + def get_response(%{messages: messages} = opts) do
52 + attempts = Map.get(opts, :attempts, 0)
53 +
54 + tx_list = transcript(messages, [])
55 +
56 + # Early guard: empty transcript -> skip model call and retries
57 + if tx_list == [] do
58 + {:error, :empty_after_filtering}
59 + else
60 + transcript_json = Jason.encode!(tx_list, pretty: true)
61 + original_length = byte_size(transcript_json)
62 +
63 + UI.info(
64 + "Compaction starting",
65 + "Transcript JSON size: #{original_length} bytes. Recent messages preserved; proceeding with compaction."
66 + )
67 +
68 + UI.info(
69 + "Summarizing conversation transcript (expected)",
70 + "No new user prompt was added for this compaction pass. We are summarizing the transcript for compactness; " <>
71 + "recent messages (including your latest prompts) remain intact."
72 + )
73 +
74 + AI.Accumulator.get_response(
75 + model: @model,
76 + prompt: @system_prompt,
77 + input: transcript_json
78 + )
79 + |> case do
80 + {:ok, %{response: response}} ->
81 + summary =
82 + """
83 + Summary of conversation and research thus far:
84 + #{response}
85 + """
86 +
87 + # Guard against trivial, near-empty summaries that would wipe context
88 + new_tokens = AI.PretendTokenizer.guesstimate_tokens(summary)
89 +
90 + if new_tokens < @min_summary_tokens do
91 + UI.error(
92 + "Compaction failed",
93 + "Summary too small (#{new_tokens} < #{@min_summary_tokens} tokens)"
94 + )
95 +
96 + {:error, :summary_too_small}
97 + else
98 + new_length = byte_size(summary)
99 + difference = original_length - new_length
100 + percent = difference / original_length * 100.0
101 +
102 + UI.info("""
103 + Compaction results:
104 + Original: #{Util.format_number(original_length)} bytes
105 + Compacted: #{Util.format_number(new_length)} bytes
106 + Savings: #{percent}% (#{Util.format_number(difference)} bytes)
107 + """)
108 +
109 + cond do
110 + original_length < @min_length ->
111 + UI.debug("Compaction retries skipped", "Original is too small to justify retries")
112 +
113 + if new_length < original_length do
114 + summary |> AI.Util.system_msg() |> then(&{:ok, [&1]})
115 + else
116 + UI.warn("Compaction failed", "Summary is larger than original; aborting")
117 + {:error, :compaction_failed}
118 + end
119 +
120 + new_length > original_length * @target_ratio and attempts < @max_attempts ->
121 + UI.warn(
122 + "Compaction insufficient",
123 + "Attempting another pass (#{attempts + 1}/#{@max_attempts})"
124 + )
125 +
126 + get_response(%{messages: messages, attempts: attempts + 1})
127 +
128 + true ->
129 + UI.debug("Compacted conversation", summary)
130 +
131 + if new_length < original_length do
132 + summary |> AI.Util.system_msg() |> then(&{:ok, [&1]})
133 + else
134 + UI.warn("Compaction failed", "Summary is larger than original; aborting")
135 + {:error, :compaction_failed}
136 + end
137 + end
138 + end
139 +
140 + {:error, reason} ->
141 + {:error, reason}
142 + end
143 + end
144 + end
145 +
146 + defp transcript([], acc), do: Enum.reverse(acc)
147 + defp transcript([%{role: "system"} | rest], acc), do: transcript(rest, acc)
148 + defp transcript([%{role: "developer"} | rest], acc), do: transcript(rest, acc)
149 + defp transcript([%{role: "user"} | rest], acc), do: transcript(rest, acc)
150 + defp transcript([%{role: "tool", name: "notify_tool"} | rest], acc), do: transcript(rest, acc)
151 +
152 + defp transcript([%{role: "tool", name: name, content: content} | rest], acc) do
153 + transcript(rest, [%{role: "tool", name: name, content: content} | acc])
154 + end
155 +
156 + defp transcript([%{role: "assistant", content: nil} | rest], acc), do: transcript(rest, acc)
157 +
158 + defp transcript([%{role: "assistant", content: content} = msg | rest], acc)
159 + when is_binary(content) do
160 + if String.starts_with?(content, "<think>") do
161 + # skip internal reasoning
162 + transcript(rest, acc)
163 + else
164 + # include assistant message
165 + transcript(rest, [msg | acc])
166 + end
167 + end
168 +
169 + defp transcript([msg | rest], acc), do: transcript(rest, [msg | acc])
170 + end
  @@ -0,0 +1,182 @@
1 + defmodule AI.Agent.Compactor do
2 + @behaviour AI.Agent
3 +
4 + @model AI.Model.large_context(:balanced)
5 + @max_attempts 3
6 + @min_length 512
7 + @target_ratio 0.8
8 +
9 + # Minimum acceptable tokens for a compacted summary; prevent trivial context wipes
10 + # Note: set to >0 in production if you want to reject trivial summaries; tests expect tiny outputs.
11 + @min_summary_tokens 0
12 +
13 + @system_prompt """
14 + You are an AI Agent in a larger system.
15 + 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.
16 + Your task: Reformat into compact meeting minutes while preserving all content needed for context.
17 + Do not use smart quotes, smart apostrophes, emojis, or other special characters.
18 + Read through the messages and reason through a new, more compact narrative that preserves the key points and context.
19 + Consider the conversation from the user's perspective: what expectations would they have for the assistant's memory and understanding of the conversation?
20 +
21 + Preserve decision points, assumptions, trade-offs, mistakes and corrections, and include rationales leading to any state changes.
22 +
23 + Use the following output template:
24 +
25 + # Original User Prompt
26 + [full text of the original user prompt]
27 +
28 + # Research and Responses
29 + [outline of facts, findings, and conclusions from the research portions of the conversation]
30 +
31 + # Conversation Timeline
32 + [
33 + Present a timeline of the conversation, listing each message with its role and a brief summary of its content.
34 + Messages at the end of the conversation should include WAY more detail than those at the beginning, reflecting the decision cascade and evolving context
35 + Don't waste space with formatting or JSON; just use plain text.
36 + ]
37 +
38 + # Continuation Context
39 + [
40 + What was the assistant doing before the conversation grew too long?
41 + Your response will form the complete prompt for the LLM's next response.
42 + This section MUST guarantee that the LLM continues exactly where it left off.
43 + ]
44 + """
45 +
46 + @impl AI.Agent
47 + def get_response(%{messages: [%{role: "developer", content: @system_prompt} | _]}) do
48 + raise "Refusing to compact a compaction prompt"
49 + end
50 +
51 + def get_response(%{messages: messages} = opts) do
52 + attempts = Map.get(opts, :attempts, 0)
53 +
54 + tx_list = transcript(messages, [])
55 + latest_user_prompt =
56 + messages
57 + |> Enum.reverse()
58 + |> Enum.find_value(fn
59 + %{role: "user", content: content} when is_binary(content) -> content
60 + _ -> nil
61 + end)
62 +
63 + prompt_to_use =
64 + case latest_user_prompt do
65 + nil -> @system_prompt <> "\nLATEST_USER_PROMPT:\n"
66 + content -> @system_prompt <> "\nLATEST_USER_PROMPT:\n" <> content
67 + end
68 +
69 + # Early guard: empty transcript -> skip model call and retries
70 + if tx_list == [] do
71 + {:error, :empty_after_filtering}
72 + else
73 + transcript_json = Jason.encode!(tx_list, pretty: true)
74 + original_length = byte_size(transcript_json)
75 +
76 + UI.info(
77 + "Compaction starting",
78 + "Transcript JSON size: #{original_length} bytes. Recent messages preserved; proceeding with compaction."
79 + )
80 +
81 + UI.info(
82 + "Summarizing conversation transcript (expected)",
83 + "We are summarizing assistant/tool transcript for compactness. All user messages are preserved verbatim and excluded from this transcript. The latest user prompt is provided separately."
84 + )
85 +
86 + AI.Accumulator.get_response(
87 + model: @model,
88 + prompt: prompt_to_use,
89 + input: transcript_json
90 + )
91 + |> case do
92 + {:ok, %{response: response}} ->
93 + summary =
94 + """
95 + Summary of conversation and research thus far:
96 + #{response}
97 + """
98 +
99 + # Guard against trivial, near-empty summaries that would wipe context
100 + new_tokens = AI.PretendTokenizer.guesstimate_tokens(summary)
101 +
102 + if new_tokens < @min_summary_tokens do
103 + UI.error(
104 + "Compaction failed",
105 + "Summary too small (#{new_tokens} < #{@min_summary_tokens} tokens)"
106 + )
107 +
108 + {:error, :summary_too_small}
109 + else
110 + new_length = byte_size(summary)
111 + difference = original_length - new_length
112 + percent = difference / original_length * 100.0
113 +
114 + UI.info("""
115 + Compaction results:
116 + Original: #{Util.format_number(original_length)} bytes
117 + Compacted: #{Util.format_number(new_length)} bytes
118 + Savings: #{percent}% (#{Util.format_number(difference)} bytes)
119 + """)
120 +
121 + cond do
122 + original_length < @min_length ->
123 + UI.debug("Compaction retries skipped", "Original is too small to justify retries")
124 +
125 + if new_length < original_length do
126 + summary |> AI.Util.system_msg() |> then(&{:ok, [&1]})
127 + else
128 + UI.warn("Compaction failed", "Summary is larger than original; aborting")
129 + {:error, :compaction_failed}
130 + end
131 +
132 + new_length > original_length * @target_ratio and attempts < @max_attempts ->
133 + UI.warn(
134 + "Compaction insufficient",
135 + "Attempting another pass (#{attempts + 1}/#{@max_attempts})"
136 + )
137 +
138 + get_response(%{messages: messages, attempts: attempts + 1})
139 +
140 + true ->
141 + UI.debug("Compacted conversation", summary)
142 +
143 + if new_length < original_length do
144 + summary |> AI.Util.system_msg() |> then(&{:ok, [&1]})
145 + else
146 + UI.warn("Compaction failed", "Summary is larger than original; aborting")
147 + {:error, :compaction_failed}
148 + end
149 + end
150 + end
151 +
152 + {:error, reason} ->
153 + {:error, reason}
154 + end
155 + end
156 + end
157 +
158 + defp transcript([], acc), do: Enum.reverse(acc)
159 + defp transcript([%{role: "system"} | rest], acc), do: transcript(rest, acc)
160 + defp transcript([%{role: "developer"} | rest], acc), do: transcript(rest, acc)
161 + defp transcript([%{role: "user"} | rest], acc), do: transcript(rest, acc)
162 + defp transcript([%{role: "tool", name: "notify_tool"} | rest], acc), do: transcript(rest, acc)
163 +
164 + defp transcript([%{role: "tool", name: name, content: content} | rest], acc) do
165 + transcript(rest, [%{role: "tool", name: name, content: content} | acc])
166 + end
167 +
168 + defp transcript([%{role: "assistant", content: nil} | rest], acc), do: transcript(rest, acc)
169 +
170 + defp transcript([%{role: "assistant", content: content} = msg | rest], acc)
171 + when is_binary(content) do
172 + if String.starts_with?(content, "<think>") do
173 + # skip internal reasoning
174 + transcript(rest, acc)
175 + else
176 + # include assistant message
177 + transcript(rest, [msg | acc])
178 + end
179 + end
180 +
181 + defp transcript([msg | rest], acc), do: transcript(rest, [msg | acc])
182 + end
  @@ -0,0 +1,190 @@
1 + defmodule AI.Agent.Compactor do
2 + @behaviour AI.Agent
3 +
4 + @model AI.Model.large_context(:balanced)
5 + @max_attempts 3
6 + @min_length 512
7 + @target_ratio 0.8
8 +
9 + # Minimum acceptable tokens for a compacted summary; prevent trivial context wipes
10 + # Note: set to >0 in production if you want to reject trivial summaries; tests expect tiny outputs.
11 + @min_summary_tokens 0
12 +
13 + @system_prompt """
14 + You are an AI Agent in a larger system.
15 + 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.
16 + Your task: Reformat into compact meeting minutes while preserving all content needed for context.
17 + Do not use smart quotes, smart apostrophes, emojis, or other special characters.
18 + Read through the messages and reason through a new, more compact narrative that preserves the key points and context.
19 + Consider the conversation from the user's perspective: what expectations would they have for the assistant's memory and understanding of the conversation?
20 +
21 + Important:
22 + - User messages are preserved verbatim elsewhere and are intentionally excluded from the transcript you are given.
23 + - Do NOT claim that no user prompt was provided.
24 + - A section labeled "LATEST_USER_PROMPT:" will be appended to your system prompt. Use it VERBATIM for the "# Original User Prompt" section.
25 + - If the LATEST_USER_PROMPT section is empty, write: "[user prompt preserved verbatim elsewhere]".
26 + - Your summary must anchor continuation clearly; do not reset or reintroduce the conversation as if starting from scratch.
27 +
28 + Preserve decision points, assumptions, trade-offs, mistakes and corrections, and include rationales leading to any state changes.
29 +
30 + Use the following output template:
31 +
32 + # Original User Prompt
33 + [full text of the original user prompt]
34 +
35 + # Research and Responses
36 + [outline of facts, findings, and conclusions from the research portions of the conversation]
37 +
38 + # Conversation Timeline
39 + [
40 + Present a timeline of the conversation, listing each message with its role and a brief summary of its content.
41 + Messages at the end of the conversation should include WAY more detail than those at the beginning, reflecting the decision cascade and evolving context
42 + Don't waste space with formatting or JSON; just use plain text.
43 + ]
44 +
45 + # Continuation Context
46 + [
47 + What was the assistant doing before the conversation grew too long?
48 + Your response will form the complete prompt for the LLM's next response.
49 + This section MUST guarantee that the LLM continues exactly where it left off.
50 + ]
51 + """
52 +
53 + @impl AI.Agent
54 + def get_response(%{messages: [%{role: "developer", content: @system_prompt} | _]}) do
55 + raise "Refusing to compact a compaction prompt"
56 + end
57 +
58 + def get_response(%{messages: messages} = opts) do
59 + attempts = Map.get(opts, :attempts, 0)
60 +
61 + tx_list = transcript(messages, [])
62 +
63 + latest_user_prompt =
64 + messages
65 + |> Enum.reverse()
66 + |> Enum.find_value(fn
67 + %{role: "user", content: content} when is_binary(content) -> content
68 + _ -> nil
69 + end)
70 +
71 + prompt_to_use =
72 + case latest_user_prompt do
73 + nil -> @system_prompt <> "\nLATEST_USER_PROMPT:\n"
74 + content -> @system_prompt <> "\nLATEST_USER_PROMPT:\n" <> content
75 + end
76 +
77 + # Early guard: empty transcript -> skip model call and retries
78 + if tx_list == [] do
79 + {:error, :empty_after_filtering}
80 + else
81 + transcript_json = Jason.encode!(tx_list, pretty: true)
82 + original_length = byte_size(transcript_json)
83 +
84 + UI.info(
85 + "Compaction starting",
86 + "Transcript JSON size: #{original_length} bytes. Recent messages preserved; proceeding with compaction."
87 + )
88 +
89 + UI.info(
90 + "Summarizing conversation transcript (expected)",
91 + "We are summarizing assistant/tool transcript for compactness. All user messages are preserved verbatim and excluded from this transcript. The latest user prompt is provided separately."
92 + )
93 +
94 + AI.Accumulator.get_response(
95 + model: @model,
96 + prompt: prompt_to_use,
97 + input: transcript_json
98 + )
99 + |> case do
100 + {:ok, %{response: response}} ->
101 + summary =
102 + """
103 + Summary of conversation and research thus far:
104 + #{response}
105 + """
106 +
107 + # Guard against trivial, near-empty summaries that would wipe context
108 + new_tokens = AI.PretendTokenizer.guesstimate_tokens(summary)
109 +
110 + if new_tokens < @min_summary_tokens do
111 + UI.error(
112 + "Compaction failed",
113 + "Summary too small (#{new_tokens} < #{@min_summary_tokens} tokens)"
114 + )
115 +
116 + {:error, :summary_too_small}
117 + else
118 + new_length = byte_size(summary)
119 + difference = original_length - new_length
120 + percent = difference / original_length * 100.0
121 +
122 + UI.info("""
123 + Compaction results:
124 + Original: #{Util.format_number(original_length)} bytes
125 + Compacted: #{Util.format_number(new_length)} bytes
126 + Savings: #{percent}% (#{Util.format_number(difference)} bytes)
127 + """)
128 +
129 + cond do
130 + original_length < @min_length ->
131 + UI.debug("Compaction retries skipped", "Original is too small to justify retries")
132 +
133 + if new_length < original_length do
134 + summary |> AI.Util.system_msg() |> then(&{:ok, [&1]})
135 + else
136 + UI.warn("Compaction failed", "Summary is larger than original; aborting")
137 + {:error, :compaction_failed}
138 + end
139 +
140 + new_length > original_length * @target_ratio and attempts < @max_attempts ->
141 + UI.warn(
142 + "Compaction insufficient",
143 + "Attempting another pass (#{attempts + 1}/#{@max_attempts})"
144 + )
145 +
146 + get_response(%{messages: messages, attempts: attempts + 1})
147 +
148 + true ->
149 + UI.debug("Compacted conversation", summary)
150 +
151 + if new_length < original_length do
152 + summary |> AI.Util.system_msg() |> then(&{:ok, [&1]})
153 + else
154 + UI.warn("Compaction failed", "Summary is larger than original; aborting")
155 + {:error, :compaction_failed}
156 + end
157 + end
158 + end
159 +
160 + {:error, reason} ->
161 + {:error, reason}
162 + end
163 + end
164 + end
165 +
166 + defp transcript([], acc), do: Enum.reverse(acc)
167 + defp transcript([%{role: "system"} | rest], acc), do: transcript(rest, acc)
168 + defp transcript([%{role: "developer"} | rest], acc), do: transcript(rest, acc)
169 + defp transcript([%{role: "user"} | rest], acc), do: transcript(rest, acc)
170 + defp transcript([%{role: "tool", name: "notify_tool"} | rest], acc), do: transcript(rest, acc)
171 +
172 + defp transcript([%{role: "tool", name: name, content: content} | rest], acc) do
173 + transcript(rest, [%{role: "tool", name: name, content: content} | acc])
174 + end
175 +
176 + defp transcript([%{role: "assistant", content: nil} | rest], acc), do: transcript(rest, acc)
177 +
178 + defp transcript([%{role: "assistant", content: content} = msg | rest], acc)
179 + when is_binary(content) do
180 + if String.starts_with?(content, "<think>") do
181 + # skip internal reasoning
182 + transcript(rest, acc)
183 + else
184 + # include assistant message
185 + transcript(rest, [msg | acc])
186 + end
187 + end
188 +
189 + defp transcript([msg | rest], acc), do: transcript(rest, [msg | acc])
190 + end
Loading more files…