Packages
fnord
0.8.68
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/code/patcher.ex
defmodule AI.Agent.Code.Patcher do
# ----------------------------------------------------------------------------
# Constants
# ----------------------------------------------------------------------------
@model AI.Model.coding()
@max_retries 2
@prompt """
# Synopsis
You are the Patcher, an AI Agent that applies changes to a file.
You are an expert programmer and can understand the syntax and semantics of the file's contents.
You are clever enough to DWIM and interpret the intent of changes requested by the Coordinating Agent, adapting them to the file's context.
You will be provided with the contents of a file and a series of changes to apply to the file.
If you cannot a change, you must return an error.
NEVER delete comments or documentation unless explicitly instructed to do so.
ALWAYS follow the style and formatting of the code already present in the file.
# Line Numbers
Line numbers appear as a number, followed by a pipe (`|`) character, followed *immediately* by the line text (no additional whitespace).
These numbered lines:
```
1|This is the first line of the file.
2| - This is the second line of the file.
```
Equate to this content:
```
This is the first line of the file.
- This is the second line of the file.
```
# Process
You will be provided with a file's contents and each change in turn.
1. Identify the smallest contiguous range of lines required to apply the *complete* change.
All lines identified will be replaced *in their entirety*.
2. Generate a *complete* replacement for those lines.
3. You MUST ensure that the indentation is correct and consistent, paying special attention to whitespace (e.g. tabs vs spaces).
4. Make the textual changes as requested, preserving the file's existing formatting patterns.
5. Do NOT include the prefixed line numbers in the replacement text.
# Language Agnostic Operation
This tool works with any type of text file (code, configuration, plain text, etc.).
Do not assume any specific programming language, syntax, or tooling is available.
Focus on making the requested textual changes accurately without syntax validation.
# Best practices
- Generate linear, atomic patches; avoid complex nested conditionals in a single patch.
- If a requested change implies multiple steps, propose sequential, small patches rather than nesting logic.
- Use early validation or checks to enforce invariants before proceeding with deeper edits.
"""
@response_format %{
type: "json_schema",
json_schema: %{
name: "patch",
description: """
A JSON object describing an individual patch to a contiguous range of
lines within the file.
If you are unable to identify a suitable range or replacement for the
requested change, return an empty `patch` (setting line numbers to `-1`
and the replacement to an empty string) and set the `error` field to a
descriptive error message with a clear explanation of why the change
could not be applied, and suggestions for what additional information is
needed to successfully apply the changes.
""",
schema: %{
type: "object",
required: ["error", "start_line", "end_line", "replacement"],
additionalProperties: false,
properties: %{
error: %{
type: "string",
description: """
An error message if the patch could not be generated.
If you were able to come up with a patch, this MUST be an empty string.
"""
},
start_line: %{
type: "integer",
description: """
The 1-based line number where the patch starts (inclusive).
If `error` is set, this MUST be `-1`.
"""
},
end_line: %{
type: "integer",
description: """
The 1-based line number where the patch ends (inclusive).
If `error` is set, this MUST be `-1`.
"""
},
replacement: %{
type: "string",
description: """
The corrected replacement text that can be applied to the file.
If the replacement is an empty string, the entire hunk will be deleted.
If `error` is set, this MUST be an empty string.
"""
}
}
}
}
}
# ----------------------------------------------------------------------------
# Behaviour Implementation
# ----------------------------------------------------------------------------
@behaviour AI.Agent
@impl AI.Agent
def get_response(opts) do
with {:ok, state} <- new(opts),
{:ok, %{contents: contents}} <- apply_changes(state) do
{:ok, contents}
end
end
# ----------------------------------------------------------------------------
# Internals
# ----------------------------------------------------------------------------
defstruct [
:agent,
:file,
:changes,
:contents,
:tools,
:retry_counts
]
@type t :: %__MODULE__{
agent: AI.Agent.t(),
file: binary,
changes: list(binary),
contents: binary,
tools: map(),
retry_counts: map()
}
@spec new(map) :: {:ok, t} | {:error, binary}
defp new(opts) do
with {:ok, agent} <- required_arg(opts, :agent),
{:ok, file} <- required_arg(opts, :file),
{:ok, changes} <- required_arg(opts, :changes),
{:ok, contents} <- read_file(file) do
UI.report_from(agent.name, "Working on", file)
# Build the toolbox once per apply_changes session
tools =
%{"shell_tool" => AI.Tools.Shell}
|> Map.merge(Frobs.module_map())
{:ok,
%__MODULE__{
agent: agent,
file: file,
changes: changes,
contents: contents,
tools: tools,
retry_counts: %{}
}}
end
end
defp read_file(path) do
path
|> AI.Tools.get_file_contents()
|> case do
{:ok, contents} -> {:ok, contents}
{:error, _} -> {:ok, ""}
end
end
@spec apply_changes(t) :: {:ok, t} | {:error, binary}
defp apply_changes(%{changes: []} = state), do: {:ok, state}
defp apply_changes(%{changes: [change | remaining], contents: contents} = state) do
UI.report_from(state.agent.name, "Patching #{state.file}", change)
numbered = Util.numbered_lines(contents)
AI.Agent.get_completion(state.agent,
model: @model,
response_format: @response_format,
log_msgs: false,
log_tool_calls: true,
toolbox: state.tools,
messages: [
AI.Util.system_msg(@prompt),
AI.Util.user_msg("""
File contents:
```
#{numbered}
```
Please apply the following change to the file contents above:
#{change}
""")
]
)
|> case do
{:error, reason} ->
attempts = Map.get(state.retry_counts, change, 0) + 1
if attempts <= @max_retries do
updated_state = %{state | retry_counts: Map.put(state.retry_counts, change, attempts)}
apply_changes(updated_state)
else
{:error, "Failed to apply change after #{@max_retries} attempts: #{reason}"}
end
{:ok, %{response: response}} ->
case parse_patch_response(response) do
{:ok, {start_line, end_line, replacement}} ->
%{state | changes: remaining}
|> replace_contents(start_line, end_line, replacement)
|> apply_changes()
{:error, reason} ->
error_response(change, reason)
end
end
end
@spec replace_contents(t, integer, integer, binary) :: t
defp replace_contents(%{contents: ""} = state, 0, 0, replacement) do
%{state | contents: replacement}
end
defp replace_contents(%{contents: contents} = state, start_line, end_line, replacement) do
lines = String.split(contents, "\n")
# Clamp counts to avoid negative indices
before_count = max(start_line - 1, 0)
after_count = max(end_line, 0)
lines_before = Enum.take(lines, before_count)
lines_after = Enum.drop(lines, after_count)
lines_within = String.split(replacement, "\n")
new_contents =
[lines_before, lines_within, lines_after]
|> List.flatten()
|> Enum.join("\n")
%{state | contents: new_contents}
end
@spec error_response(binary, any) :: {:error, binary}
defp error_response(change, reason) do
{:error,
"""
The following change could not be applied:
#{change}
The AI Agent returned an error:
#{inspect(reason, pretty: true, limit: :infinity)}
"""}
end
@doc false
# Parses the LLM JSON response into patch tuple or error
defp parse_patch_response(json) do
case Jason.decode(json) do
{:ok, decoded} when is_map(decoded) ->
decoded
|> Map.get("patch", decoded)
|> case do
patch when is_map(patch) ->
patch
|> Map.get("error", "")
|> case do
"" ->
start_line = Map.get(patch, "start_line")
end_line = Map.get(patch, "end_line")
replacement = Map.get(patch, "replacement")
cond do
!is_integer(start_line) ->
{:error, "Invalid patch structure: start_line is not an integer"}
!is_integer(end_line) ->
{:error, "Invalid patch structure: end_line is not an integer"}
!is_binary(replacement) ->
{:error, "Invalid patch structure: replacement is not a string"}
start_line < 1 ->
{:error, "Invalid patch structure: start_line must be > 0"}
start_line > end_line ->
{:error, "Invalid patch structure: start_line must be <= end_line"}
true ->
{:ok, {start_line, end_line, replacement}}
end
reason ->
{:error, reason}
end
_ ->
{:error, "Invalid patch structure: patch is not an object"}
end
{:ok, _} ->
{:error, "Invalid patch structure: decoded JSON response is not an object"}
{:error, error} ->
{:error, "Invalid patch structure: #{inspect(error)}"}
end
end
@spec required_arg(map, atom) :: {:ok, any} | {:error, binary}
defp required_arg(opts, key) do
case Map.fetch(opts, key) do
{:ok, value} -> {:ok, value}
:error -> {:error, "Missing required parameter: #{key}"}
end
end
end