Skip to content

Commit 308ef37

Browse files
committed
Add support for file change diffs on tool call.
1 parent 907beb6 commit 308ef37

File tree

12 files changed

+156
-28
lines changed

12 files changed

+156
-28
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- Support custom system prompts via config `systemPromptTemplate`.
6+
- Add support for file change diffs on `eca_edit_file` tool call.
7+
58
## 0.15.3
69

710
- Rename `eca_list_directory` to `eca_directory_tree` tool for better overview of project files/dirs.

deps.edn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
babashka/fs {:mvn/version "0.5.26"}
99
hato/hato {:mvn/version "1.0.0"}
1010
org.slf4j/slf4j-simple {:mvn/version "2.0.17"}
11+
com.googlecode.java-diff-utils/diffutils {:mvn/version "1.3.0"}
1112
cheshire/cheshire {:mvn/version "6.0.0"}}
1213
:aliases
1314
{:dev {:extra-paths ["dev"]

docs/configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ interface Config {
132132
openaiApiKey?: string;
133133
anthropicApiKey?: string;
134134
rules: [{path: string;}];
135+
systemPromptTemplate?: string;
135136
nativeTools: {
136137
filesystem: {enabled: boolean};
137138
shell: {enabled: boolean;

docs/protocol.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,10 +293,12 @@ interface FileContext {
293293
/**
294294
* Range of lines to retrive from file, if nil consider whole file.
295295
*/
296-
linesRange?: {
297-
start: number;
298-
end: number;
299-
}
296+
linesRange?: LinesRange;
297+
}
298+
299+
interface LinesRange {
300+
start: number;
301+
end: number;
300302
}
301303

302304
/**
@@ -601,6 +603,38 @@ interface ToolCallRunContent {
601603
* Whether this call requires manual approval from the user.
602604
*/
603605
manualApproval: boolean;
606+
607+
/**
608+
* Extra details about this call.
609+
* Clients may use this to present different UX for this tool call.
610+
*/
611+
details?: TooCallRunDetails;
612+
}
613+
614+
type TooCallRunDetails = FileChangeDetails;
615+
616+
interface FileChangeDetails {
617+
type: 'fileChange';
618+
619+
/**
620+
* The file path of this file change
621+
*/
622+
path: string;
623+
624+
/**
625+
* The content diff of this file change
626+
*/
627+
diff: string;
628+
629+
/**
630+
* The count of lines added in this change.
631+
*/
632+
linesAdded: number;
633+
634+
/**
635+
* The count of lines removed in this change.
636+
*/
637+
linesRemoved: number;
604638
}
605639

606640
/**

resources/eca_prompt.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ When using markdown in assistant messages, use backticks to format file, directo
1010
Pay attention to the langauge name after the code block backticks start, use the full language name like 'javascript' instead of 'js'.
1111
</communication>
1212

13+
<edit_file_instructions>
14+
Before editing a file, ensure you have its content via the provided context or eca_read_file tool.
15+
Use the eca_edit_file tool to modify files.
16+
NEVER show the code edits to the user - only call the tool. The system will apply and display the edits.
17+
For each file, give a short description of what needs to be edited, then use the eca_edit_file tool. You can use the tool multiple times in a response, and you can keep writing text after using a tool.
18+
The eca_edit_file tool is very smart and can understand how to apply your edits to the user's files.
19+
</edit_file_instructions>
20+
1321
<tool_calling>
1422
You have tools at your disposal to solve the coding task. Follow these rules regarding tool calls:
1523
1. ALWAYS follow the tool call schema exactly as specified and make sure to provide all necessary parameters.

src/eca/diff.clj

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
(ns eca.diff
2+
(:require
3+
[clojure.string :as string]
4+
[eca.shared :as shared])
5+
(:import
6+
[difflib
7+
ChangeDelta
8+
DeleteDelta
9+
DiffUtils
10+
InsertDelta]))
11+
12+
(set! *warn-on-reflection* true)
13+
14+
(defn ^:private lines
15+
"Splits S on `\n` or `\r\n`."
16+
[s]
17+
(string/split-lines s))
18+
19+
(defn ^:private unlines
20+
"Joins SS strings coll using the system's line separator."
21+
[ss]
22+
(string/join shared/line-separator ss))
23+
24+
(defn diff
25+
([original revised file]
26+
(let [patch (DiffUtils/diff (lines original) (lines revised))
27+
deltas (.getDeltas patch)
28+
added (->> deltas
29+
(filter #(instance? InsertDelta %))
30+
(mapcat (fn [^InsertDelta delta]
31+
(.getLines (.getRevised delta))))
32+
count)
33+
changed (->> deltas
34+
(filter #(instance? ChangeDelta %))
35+
(mapcat (fn [^ChangeDelta delta]
36+
(.getLines (.getRevised delta))))
37+
count)
38+
removed (->> deltas
39+
(filter #(instance? DeleteDelta %))
40+
(mapcat (fn [^DeleteDelta delta]
41+
(.getLines (.getOriginal delta))))
42+
count)]
43+
{:added (+ added changed)
44+
:removed (+ removed changed)
45+
:diff
46+
(unlines (DiffUtils/generateUnifiedDiff
47+
file
48+
file
49+
(lines original)
50+
patch
51+
3))})))

src/eca/features/chat.clj

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@
3434
:content (llm-api/refine-file-context filename nil)}))))
3535
"repoMap" [{:type :repoMap}]
3636
"mcpResource" (mapv
37-
(fn [{:keys [text]}]
38-
{:type :mcpResource
39-
:uri uri
40-
:content text})
41-
(:contents (f.mcp/get-resource! uri db)))))
37+
(fn [{:keys [text]}]
38+
{:type :mcpResource
39+
:uri uri
40+
:content text})
41+
(:contents (f.mcp/get-resource! uri db)))))
4242
contexts))
4343

4444
(defn default-model [db config]
@@ -146,7 +146,7 @@
146146
rules (f.rules/all config (:workspace-folders db))
147147
refined-contexts (raw-contexts->refined contexts db)
148148
repo-map* (delay (f.index/repo-map db {:as-string? true}))
149-
instructions (f.prompt/build-instructions refined-contexts rules repo-map* (or behavior (:chat-default-behavior db)))
149+
instructions (f.prompt/build-instructions refined-contexts rules repo-map* (or behavior (:chat-default-behavior db)) config)
150150
past-messages (get-in db [:chats chat-id :messages] [])
151151
all-tools (f.tools/all-tools @db* config)
152152
received-msgs* (atom "")
@@ -210,12 +210,14 @@
210210
:on-tool-called (fn [{:keys [id name arguments] :as tool-call}]
211211
(assert-chat-not-stopped! chat-ctx)
212212
(send-content! chat-ctx :assistant
213-
{:type :toolCallRun
214-
:name name
215-
:origin (tool-name->origin name all-tools)
216-
:arguments arguments
217-
:id id
218-
:manual-approval manual-approval?})
213+
(assoc-some
214+
{:type :toolCallRun
215+
:name name
216+
:origin (tool-name->origin name all-tools)
217+
:arguments arguments
218+
:id id
219+
:manual-approval manual-approval?}
220+
:details (f.tools/get-tool-call-details name arguments)))
219221
(let [approved?* (promise)]
220222
(swap! db* assoc-in [:chats chat-id :tool-calls id :approved?*] approved?*)
221223
(when-not (string/blank? @received-msgs*)

src/eca/features/prompt.clj

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88

99
(def ^:private eca-prompt-template (memoize eca-prompt-template*))
1010

11-
(defn ^:private eca-prompt [behavior]
12-
(let [prompt (eca-prompt-template)]
11+
(defn ^:private eca-prompt [behavior config]
12+
(let [prompt (or (:systemPromptTemplate config)
13+
(eca-prompt-template))]
1314
(reduce
1415
(fn [p [k v]]
1516
(string/replace p (str "{" (name k) "}") v))
@@ -18,9 +19,9 @@
1819
"chat" "Answer questions, and provide explanations."
1920
"agent" "You are an agent - please keep going until the user's query is completely resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the problem is solved. Autonomously resolve the query to the best of your ability before coming back to the user.")})))
2021

21-
(defn build-instructions [refined-contexts rules repo-map* behavior]
22+
(defn build-instructions [refined-contexts rules repo-map* behavior config]
2223
(multi-str
23-
(eca-prompt behavior)
24+
(eca-prompt behavior config)
2425
"<rules>"
2526
(reduce
2627
(fn [rule-str {:keys [name content]}]

src/eca/features/tools.clj

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
eca native tools and MCP servers."
44
(:require
55
[clojure.string :as string]
6+
[eca.diff :as diff]
67
[eca.features.tools.filesystem :as f.tools.filesystem]
78
[eca.features.tools.mcp :as f.mcp]
89
[eca.features.tools.shell :as f.tools.shell]
@@ -95,3 +96,19 @@
9596
(messenger/tool-server-updated messenger (-> server
9697
(assoc :type :mcp)
9798
(update :tools #(mapv with-tool-status %)))))})))
99+
(defn get-tool-call-details [name arguments]
100+
(case name
101+
"eca_edit_file" (let [path (get arguments "path")
102+
original-content (get arguments "original_content")
103+
new-content (get arguments "new_content")
104+
all? (get arguments "all_occurrences")]
105+
(when-let [{:keys [original-full-content
106+
new-full-content]} (and path original-content new-content
107+
(f.tools.filesystem/file-change-full-content path original-content new-content all?))]
108+
(let [{:keys [added removed diff]} (diff/diff original-full-content new-full-content path)]
109+
{:type :fileChange
110+
:path path
111+
:linesAdded added
112+
:linesRemoved removed
113+
:diff diff})))
114+
nil))

src/eca/features/tools/filesystem.clj

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,19 +146,25 @@
146146
(tools.util/single-text-content (string/join "\n" paths))
147147
(tools.util/single-text-content "No files found for given pattern" :error)))))
148148

149+
(defn file-change-full-content [path original-content new-content all?]
150+
(let [original-full-content (slurp path)
151+
new-full-content (if all?
152+
(string/replace original-full-content original-content new-content)
153+
(string/replace-first original-full-content original-content new-content))]
154+
(when (string/includes? original-full-content original-content)
155+
{:original-full-content original-full-content
156+
:new-full-content new-full-content})))
157+
149158
(defn ^:private edit-file [arguments {:keys [db]}]
150159
(or (tools.util/invalid-arguments arguments (concat (path-validations db)
151160
[["path" fs/readable? "File $path is not readable"]]))
152161
(let [path (get arguments "path")
153162
original-content (get arguments "original_content")
154163
new-content (get arguments "new_content")
155-
all? (boolean (get arguments "all_occurrences"))
156-
content (slurp path)]
157-
(if (string/includes? content original-content)
158-
(let [content (if all?
159-
(string/replace content original-content new-content)
160-
(string/replace-first content original-content new-content))]
161-
(spit path content)
164+
all? (boolean (get arguments "all_occurrences"))]
165+
(if-let [{:keys [new-full-content]} (file-change-full-content path original-content new-content all?)]
166+
(do
167+
(spit path new-full-content)
162168
(tools.util/single-text-content (format "Successfully replaced content in %s." path)))
163169
(tools.util/single-text-content (format "Original content not found in %s" path) :error)))))
164170

0 commit comments

Comments
 (0)