Packages
fnord
0.8.12
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/tools/coder.ex
defmodule AI.Tools.Coder do
@max_attempts 3
@behaviour AI.Tools
@impl AI.Tools
def is_available?, do: AI.Tools.has_indexed_project()
@impl AI.Tools
def ui_note_on_request(%{"file" => file, "instructions" => instructions}) do
{"Editing file #{file}", instructions}
end
@impl AI.Tools
def ui_note_on_result(%{"file" => file, "instructions" => instructions}, result) do
{"Changes applied to #{file}",
"""
# Instructions
#{instructions}
# Result
#{result}
"""}
end
@impl AI.Tools
def read_args(args), do: {:ok, args}
@impl AI.Tools
def spec() do
%{
type: "function",
function: %{
name: "coder_tool",
description: """
Triggers an LLM agent to perform a coding task to a contiguous region within a single file in the project source root.
Instructions must include ALL relevant context; this agent has no access to the prior conversation.
Instructions must include clear, unambiguous "anchors", identifying a *single* region of the file to edit.
Examples:
- "Add a new, private function at the end of the file named `blarg`. The function accepts 2 positional arguments, ..."
- "In the import list at the top of the file, remove the import for `foo.bar` and add an import for `baz.qux`."
- "This file contains a mix of spaces and tabs. Convert all tabs to spaces, and ensure the indentation is consistent with 2 spaces per level."
""",
parameters: %{
type: "object",
required: ["file", "instructions"],
properties: %{
file: %{
type: "string",
description: "The path to the file to edit, relative to the project source root."
},
instructions: %{
type: "string",
description: "Clear, detailed instructions for the changes to make to the file."
}
}
}
}
}
end
@impl AI.Tools
def call(args) do
with {:ok, project} <- Store.get_project(),
{:ok, file} <- AI.Tools.get_arg(args, "file"),
{:ok, instructions} <- AI.Tools.get_arg(args, "instructions"),
:ok <- validate_path(file, project.source_root),
{:ok, {start_line, end_line}} <- identify_range(file, instructions),
{:ok, replacement, preview} <- dry_run_changes(file, instructions, start_line, end_line),
:ok <- confirm_changes(file, instructions, preview) do
{:ok, result} = apply_changes(file, start_line, end_line, replacement)
UI.info("Changes applied to #{file}:#{start_line}-#{end_line}", result)
{:ok, result}
else
{:identify_error, error} ->
{:error,
"""
The agent was unable to identify a single, contiguous range of lines in the file based on the provided instructions:
#{error}
"""}
{:confirm_error, error, preview} ->
{:error,
"""
The syntax checking agent found an error in the requested change:
#{error}
The change attempted was:
#{preview}
"""}
{:error, reason} ->
{:error, reason}
end
end
@spec validate_path(binary, binary) :: :ok | {:error, binary | :enoent}
defp validate_path(path, root) do
cond do
!Util.path_within_root?(path, root) -> {:error, "not within project root"}
!File.exists?(path) -> {:error, :enoent}
true -> :ok
end
end
@spec validate_range(binary, integer, integer) :: :ok | {:error, binary}
defp validate_range(contents, start_line, end_line) do
lines = num_lines(contents)
cond do
start_line < 1 ->
{:error, "Start line must be greater than or equal to 1."}
start_line > lines ->
{:error, "Start line exceeds the number of lines in the file."}
end_line < start_line ->
{:error, "End line must be greater than or equal to start line."}
lines > 0 && end_line > lines ->
{:error, "End line exceeds the number of lines in the file."}
true ->
:ok
end
end
@spec num_lines(binary) :: integer
defp num_lines(str) do
str
|> String.split("\n", trim: false)
|> length()
end
@spec identify_range(binary, binary, integer) ::
{:ok, {integer, integer}}
| {:error, binary}
| {:error, :max_attempts_reached}
| {:identify_error, binary}
defp identify_range(file, instructions, attempt \\ 1)
defp identify_range(_file, _instructions, attempt)
when attempt > @max_attempts do
{:error, :max_attempts_reached}
end
defp identify_range(file, instructions, attempt) do
file_contents =
file
|> File.read!()
|> Util.numbered_lines()
system = """
You are an AI coding assistant within a larger AI system.
The Coordinating Agent asks you to identify a contiguous range of lines in a file based on the provided instructions for a coding task it is planning.
Identify the *single*, contiguous range of lines in the file to be replaced, based on the instructions.
If the instructions are ambiguous or do not clearly identify a single range, respond with an error message that clearly explains the problem.
# Response Format
You MUST respond ONLY with a single JSON object with the following fields, based on whether you were able to identify a range:
**Success:** `{"start_line": <start_line>, "end_line": <end_line>}`
**Failure:** `{"error": "<error message>"}`
Do not include any other text, comments, explanations, or markdown fences in your response.
"""
user = """
Please identify the range of lines in the file that should be replaced based on the following instructions.
# Instructions
#{instructions}
# File: #{file}
The file contents include 1-based line numbers at the start of each line, separated by a pipe (|).
Constraints:
- `start_line` <= `end_line`
- `start_line` >= 1
- `end_line` <= the number of lines in the file
- if `start_line` == `end_line`, the range is a single line
- The instructions will be applied to *fully* replace the identified range, so the range must cover the entire area to be replaced.
```
#{file_contents}
```
"""
AI.Completion.get(
model: AI.Model.balanced(),
messages: [
AI.Util.system_msg(system),
AI.Util.user_msg(user)
]
)
|> case do
{:ok, %{response: response}} ->
response
|> Jason.decode()
|> case do
{:ok, %{"start_line" => start_line, "end_line" => end_line}} ->
file_contents
|> validate_range(start_line, end_line)
|> case do
:ok ->
UI.info("Hunk identified in #{file}", "Lines #{start_line}...#{end_line}")
{:ok, {start_line, end_line}}
{:error, error} ->
UI.warn("LLM identified range but it is invalid: #{error}")
identify_range(file, instructions, attempt + 1)
end
{:ok, %{"error" => msg}} ->
{:identify_error, msg}
end
{:error, reason} ->
UI.warn("Error identifying range: #{inspect(reason)}")
identify_range(file, instructions, attempt + 1)
end
end
@spec dry_run_changes(binary, binary, integer, integer, integer) ::
{:ok, binary, binary}
| {:error, binary}
| {:error, :max_attempts_reached}
defp dry_run_changes(file, instructions, start_line, end_line, attempt \\ 1)
defp dry_run_changes(_file, _instructions, _start_line, _end_line, attempt)
when attempt > @max_attempts do
{:error, :max_attempts_reached}
end
defp dry_run_changes(file, instructions, start_line, end_line, attempt) do
file_contents =
file
|> File.read!()
|> Util.numbered_lines()
system = """
You are an AI coding assistant within a larger AI system.
The Coordinating Agent asks you to apply changes to a file based on the provided instructions for a coding task.
Your changes will COMPLETELY replace the specified range of lines in the file.
Respond with a block of code that will replace all other content within the specified range of lines in the file.
Do not include any other text, comments, explanations, or markdown fences in your response.
"""
user = """
Please apply the following changes to the specified range of lines in the file.
Perform NO other edits.
Make ONLY the changes explicitly requested by the Coordinating Agent.
# Instructions
#{instructions}
# File: #{file}
The file contents include 1-based line numbers at the start of each line, separated by a pipe (|).
IMPORTANT: Your response must NOT include any line numbers or their pipe (`|`) separators.
Only output the raw file content for this region.
If you include line numbers or `|`, that will break the file.
```
#{file_contents}
```
"""
AI.Completion.get(
model: AI.Model.balanced(),
messages: [
AI.Util.system_msg(system),
AI.Util.user_msg(user)
]
)
|> case do
{:ok, %{response: replacement}} ->
AI.Tools.File.Edit.call(%{
"path" => file,
"start_line" => start_line,
"end_line" => end_line,
"replacement" => replacement,
"dry_run" => true,
"context_lines" => 5
})
|> case do
{:ok, preview} ->
UI.info("Patch prepared", """
# #{file} | #{start_line}...#{end_line}
```
#{replacement}
```
""")
{:ok, replacement, preview}
{:error, reason} ->
UI.warn("Error applying dry run changes: #{inspect(reason)}")
dry_run_changes(file, instructions, start_line, end_line, attempt + 1)
end
{:error, reason} ->
UI.warn("Error applying dry run changes: #{inspect(reason)}")
dry_run_changes(file, instructions, start_line, end_line, attempt + 1)
end
end
@spec confirm_changes(binary, binary, binary, integer) ::
:ok
| {:error, binary}
| {:error, :max_attempts_reached}
| {:confirm_error, binary, binary}
defp confirm_changes(file, instructions, preview, attempt \\ 1)
defp confirm_changes(_file, _instructions, _preview, attempt)
when attempt > @max_attempts do
{:error, :max_attempts_reached}
end
defp confirm_changes(file, instructions, preview, attempt) do
file_contents =
file
|> File.read!()
|> Util.numbered_lines()
system = """
You are an AI coding assistant within a larger AI system.
The Coordinating Agent has asked you to confirm the changes made to a file based on the provided preview.
You are required to confirm whether the changes are 1) correct, and, 2) syntactically valid.
Use your programming knowledge to determine if the changes are *syntactically* correct.
# Response Format
Respond ONLY with one of the two following JSON objects:
**Success:** `{"valid": true}`
**Failure:** `{"valid": false, "error": "<clear explanation of the problem>"}`
Do not include any other text, comments, explanations, or markdown fences in your response.
"""
user = """
Please confirm the changes made to the file based on the file contents and preview of the change.
# Instructions
#{instructions}
# File: #{file}
The file contents include 1-based line numbers at the start of each line, separated by a pipe (|).
```
#{file_contents}
```
# Preview of Change
#{preview}
"""
AI.Completion.get(
model: AI.Model.balanced(),
messages: [
AI.Util.system_msg(system),
AI.Util.user_msg(user)
]
)
|> case do
{:ok, %{response: response}} ->
response
|> Jason.decode()
|> case do
{:ok, %{"valid" => true}} ->
UI.info("Reviewer approved changes", file)
:ok
{:ok, %{"valid" => false, "error" => msg}} ->
UI.info("Reviewer rejected changes to #{file}", msg)
{:confirm_error, msg, preview}
{:ok, %{"error" => error}} ->
UI.info("Reviewer rejected changes to #{file}", error)
{:confirm_error, error, preview}
{:error, reason} ->
UI.warn("Error decoding confirmation response: #{inspect(reason)}")
confirm_changes(file, instructions, preview, attempt + 1)
end
{:error, reason} ->
UI.warn("Error confirming changes: #{inspect(reason)}")
confirm_changes(file, instructions, preview, attempt + 1)
end
end
defp apply_changes(file, start_line, end_line, replacement) do
AI.Tools.File.Edit.call(%{
"path" => file,
"start_line" => start_line,
"end_line" => end_line,
"replacement" => replacement,
"dry_run" => false,
"context_lines" => 5
})
end
end