Skip to content

Commit 07c2ad7

Browse files
committed
Fix tool call output
1 parent cdf9e28 commit 07c2ad7

File tree

3 files changed

+60
-35
lines changed

3 files changed

+60
-35
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Unreleased
44

5-
- Support user configured custom tools.
5+
- Support user configured custom tools via `customTools` config.
66

77
## 0.44.1
88

src/eca/features/tools/custom.clj

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,53 @@
11
(ns eca.features.tools.custom
22
(:require
3-
[babashka.process :as process]
4-
[clojure.string :as string]))
3+
[babashka.process :as p]
4+
[clojure.string :as string]
5+
[eca.features.tools.util :as tools.util]
6+
[eca.logger :as logger]
7+
[eca.shared :as shared]))
58

69
(set! *warn-on-reflection* true)
710

11+
(def ^:private logger-tag "[CUSTOM-TOOLS]")
12+
813
(defn ^:private build-tool-fn
914
"Creates a function that safely executes the command from a custom tool config.
1015
It substitutes {{placeholders}} in the command vector with LLM-provided arguments."
1116
[{:keys [command]}]
12-
;; The handler function takes arguments and a context map. We only need the arguments.
13-
(fn [llm-args _context]
14-
(let [resolved-command (mapv
15-
(fn [part]
16-
(if (and (string? part) (string/starts-with? part "{{") (string/ends-with? part "}}"))
17-
(let [key-name (keyword (subs part 2 (- (count part) 2)))]
18-
(str (get llm-args key-name "")))
19-
part))
20-
command)
21-
{:keys [out exit]} (process/sh resolved-command {:error-to-out true})]
17+
(fn [args {:keys [db]}]
18+
(let [resolved-command (reduce
19+
(fn [s [arg-name arg-value]]
20+
(string/replace s (str "{{" arg-name "}}") arg-value))
21+
command
22+
args)
23+
work-dir (some-> (:workspace-folders db)
24+
first
25+
:uri
26+
shared/uri->filename)
27+
_ (logger/debug logger-tag "Running custom tool:" resolved-command)
28+
result (try
29+
(p/shell {:dir work-dir
30+
:out :string
31+
:err :string
32+
:continue true} "bash -c" resolved-command)
33+
(catch Exception e
34+
{:exit 1 :err (.getMessage e)}))
35+
exit (:exit result)
36+
err (some-> (:err result) string/trim)
37+
out (some-> (:out result) string/trim)]
38+
(logger/debug logger-tag "custom tool executed:" result)
2239
(if (zero? exit)
23-
out
24-
(str "Error: Command failed with exit code " exit "\nOutput:\n" out)))))
40+
(tools.util/single-text-content out)
41+
{:error true
42+
:contents (remove nil?
43+
(concat [{:type :text
44+
:text (str "Exit code " exit)}]
45+
(when-not (string/blank? err)
46+
[{:type :text
47+
:text (str "Stderr:\n" err)}])
48+
(when-not (string/blank? out)
49+
[{:type :text
50+
:text (str "Stdout:\n" out)}])))}))))
2551

2652
(defn ^:private custom-tool->tool-def
2753
"Transforms a single custom tool from the config map into a full tool definition."

test/eca/features/tools/custom_test.clj

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,49 @@
11
(ns eca.features.tools.custom-test
22
(:require
3-
[clojure.string :as string]
3+
[babashka.process :as p]
44
[clojure.test :refer [deftest is testing]]
5-
[babashka.process :as process]
65
[eca.features.tools.custom :as f.tools.custom]))
76

87
(deftest definitions-test
98
(testing "when a valid tool is configured"
109
(let [mock-custom-tools {"file-search"
1110
{:description "Finds files."
12-
:command ["find" "{{directory}}" "-name" "{{pattern}}"]
13-
:schema {:properties {:directory {:type "string"}
14-
:pattern {:type "string"}}
11+
:command "find {{directory}} -name {{pattern}}"
12+
:schema {:properties {"directory" {:type "string"}
13+
"pattern" {:type "string"}}
1514
:required ["directory" "pattern"]}}}]
1615
(testing "and the command executes successfully"
17-
(with-redefs [process/sh (fn [command-vec & _]
18-
(is (= ["find" "/tmp" "-name" "*.clj"] command-vec))
19-
{:out "mocked-output" :exit 0})]
16+
(with-redefs [p/shell (fn [_opts _bash command]
17+
(is (= "find /tmp -name *.clj" command))
18+
{:out "mocked-output" :exit 0})]
2019
(let [config {:customTools mock-custom-tools}
2120
custom-defs (f.tools.custom/definitions config)
2221
custom-tool-def (get custom-defs "file-search")]
2322
(is (some? custom-tool-def) "The custom tool should be loaded.")
24-
(let [result ((:handler custom-tool-def) {:directory "/tmp" :pattern "*.clj"} {})]
25-
(is (= "mocked-output" result) "The tool should return the mocked shell output.")))))))
23+
(let [result ((:handler custom-tool-def) {"directory" "/tmp" "pattern" "*.clj"} {})]
24+
(is (= {:contents [{:text "mocked-output", :type :text}], :error false} result) "The tool should return the mocked shell output.")))))))
2625

2726
(testing "when multiple tools are configured"
2827
(let [mock-custom-tools {"git-status"
2928
{:description "Gets git status"
30-
:command ["git" "status"]}
29+
:command "git status"}
3130
"echo-message"
3231
{:description "Echoes a message"
33-
:command ["echo" "{{message}}"]
34-
:schema {:properties {:message {:type "string"}} :required ["message"]}}}]
35-
(with-redefs [process/sh (fn [command-vec & _]
36-
(condp = command-vec
37-
["git" "status"] {:out "On branch main" :exit 0}
38-
["echo" "Hello World"] {:out "Hello World" :exit 0}
39-
(is false "Unexpected command received by mock p/sh")))]
32+
:command "echo {{message}}"
33+
:schema {:properties {"message" {:type "string"}} :required ["message"]}}}]
34+
(with-redefs [p/shell (fn [_opts _bash command]
35+
(condp = command
36+
"git status" {:out "On branch main" :exit 0}
37+
"echo Hello World" {:out "Hello World" :exit 0}
38+
(is false "Unexpected command received by mock p/sh")))]
4039
(let [config {:customTools mock-custom-tools}
4140
custom-defs (f.tools.custom/definitions config)
4241
git-status-handler (get-in custom-defs ["git-status" :handler])
4342
echo-handler (get-in custom-defs ["echo-message" :handler])]
4443
(is (some? git-status-handler) "Git status tool should be loaded.")
4544
(is (some? echo-handler) "Echo message tool should be loaded.")
46-
(is (= "On branch main" (git-status-handler {} {})))
47-
(is (= "Hello World" (echo-handler {:message "Hello World"} {})))))))
45+
(is (= {:contents [{:text "On branch main", :type :text}], :error false} (git-status-handler {} {})))
46+
(is (= {:contents [{:text "Hello World", :type :text}], :error false} (echo-handler {"message" "Hello World"} {})))))))
4847

4948
(testing "when the custom tools config is empty or missing"
5049
(testing "with an empty map"

0 commit comments

Comments
 (0)