Skip to content

Commit e3cb2be

Browse files
committed
Support tool call approval and configuration to manual approval.
1 parent 8b91e13 commit e3cb2be

File tree

9 files changed

+176
-32
lines changed

9 files changed

+176
-32
lines changed

CHANGELOG.md

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

33
## Unreleased
44

5+
- Support tool call approval and configuration to manual approval.
6+
57
## 0.7.0
68

79
- Add client request to delete a chat.

docs/configuration.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,10 @@ interface Config {
132132
rules: [{path: string;}];
133133
nativeTools: {
134134
{[key: string] {enabled: boolean}}
135-
}
135+
};
136+
toolCall?: {
137+
manualApproval?: boolean,
138+
};
136139
mcpTimeoutSeconds: number;
137140
mcpServers: {[key: string]: {
138141
command: string;
@@ -174,6 +177,9 @@ interface Config {
174177
"nativeTools": {"filesystem": {"enabled": true}
175178
"shell": {"enabled": true
176179
"excludeCommands": []}},
180+
"toolCall": {
181+
"manualApproval": false,
182+
},
177183
"mcpTimeoutSeconds" : 10,
178184
"mcpServers" : [],
179185
"customProviders": {},

docs/protocol.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,32 @@ interface ToolCalledContent {
604604
}];
605605
}
606606

607+
interface ToolCallRejected {
608+
type: 'toolCallRejected';
609+
610+
origin: ToolCallOrigin;
611+
612+
/**
613+
* id of the tool call
614+
*/
615+
id: string;
616+
617+
/**
618+
* Name of the tool
619+
*/
620+
name: string;
621+
622+
/*
623+
* Arguments of this tool call
624+
*/
625+
arguments: {[key: string]: string};
626+
627+
/**
628+
* The reason why this tool call was rejected
629+
*/
630+
reason: 'user';
631+
}
632+
607633
type ToolCallOrigin = 'mcp' | 'native';
608634
```
609635

@@ -651,6 +677,54 @@ interface ChatQueryContextResponse {
651677
}
652678
```
653679

680+
### Chat approve tool call (➡️)
681+
682+
A client notification for server to approve a waiting tool call.
683+
This will execute the tool call and continue the LLM chat loop.
684+
685+
_Notification:_
686+
687+
* method: `chat/toolCallApprove`
688+
* params: `ChatToolCallApproveParams` defined as follows:
689+
690+
```typescript
691+
interface ChatToolCallApproveParams {
692+
/**
693+
* The chat session identifier.
694+
*/
695+
chatId: string;
696+
697+
/**
698+
* The tool call identifier to approve.
699+
*/
700+
toolCallId: string;
701+
}
702+
```
703+
704+
### Chat reject tool call (➡️)
705+
706+
A client notification for server to reject a waiting tool call.
707+
This will not execute the tool call and return to the LLM chat loop.
708+
709+
_Notification:_
710+
711+
* method: `chat/toolCallReject`
712+
* params: `ChatToolCallRejectParams` defined as follows:
713+
714+
```typescript
715+
interface ChatToolCallRejectParams {
716+
/**
717+
* The chat session identifier.
718+
*/
719+
chatId: string;
720+
721+
/**
722+
* The tool call identifier to reject.
723+
*/
724+
toolCallId: string;
725+
}
726+
```
727+
654728
### Chat stop prompt (➡️)
655729

656730
A client notification for server to stop the current chat prompt with LLM if running.

src/eca/features/chat.clj

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
(:workspace-folders db)
121121
{:behavior (behavior->behavior-str (or behavior (:chat-default-behavior db)))})
122122
refined-contexts (raw-contexts->refined contexts)
123+
manual-approval? (get-in config [:toolCall :manualApproval] false)
123124
context-str (build-context-str refined-contexts rules)
124125
chosen-model (or model (default-model db config))
125126
past-messages (get-in db [:chats chat-id :messages] [])
@@ -221,7 +222,7 @@
221222
:origin (tool-name->origin name all-tools)
222223
:arguments-text (get @tool-call-args-by-id* id)
223224
:id id
224-
:manual-approval false}}))
225+
:manual-approval manual-approval?}}))
225226
:on-tool-called (fn [{:keys [id name arguments] :as tool-call}]
226227
(assert-chat-not-stopped! chat-id db* messenger)
227228
(messenger/chat-content-received
@@ -234,27 +235,66 @@
234235
:origin (tool-name->origin name all-tools)
235236
:arguments arguments
236237
:id id
237-
:manual-approval false}})
238-
(let [result (f.tools/call-tool! name arguments @db* config)]
238+
:manual-approval manual-approval?}})
239+
(let [approved?* (promise)]
240+
(swap! db* assoc-in [:chats chat-id :tool-calls id :approved?*] approved?*)
239241
(when-not (string/blank? @received-msgs*)
240242
(add-to-history! {:role "assistant" :content @received-msgs*})
241243
(reset! received-msgs* ""))
242-
(add-to-history! {:role "tool_call" :content tool-call})
243-
(add-to-history! {:role "tool_call_output" :content (assoc tool-call :output result)})
244-
(swap! tool-call-args-by-id* dissoc id)
245-
(messenger/chat-content-received
246-
messenger
247-
{:chat-id chat-id
248-
:request-id request-id
249-
:role :assistant
250-
:content {:type :toolCalled
251-
:origin (tool-name->origin name all-tools)
252-
:name name
253-
:arguments arguments
254-
:id id
255-
:outputs (:contents result)}})
256-
{:result result
257-
:past-messages (get-in @db* [:chats chat-id :messages] [])}))
244+
(if manual-approval?
245+
(messenger/chat-content-received
246+
messenger
247+
{:chat-id chat-id
248+
:request-id request-id
249+
:role :system
250+
:content {:type :progress
251+
:state :running
252+
:text "Waiting for tool call approval"}})
253+
;; Otherwise auto approve
254+
(deliver approved?* true))
255+
(if @approved?*
256+
(let [output (f.tools/call-tool! name arguments @db* config)]
257+
(add-to-history! {:role "tool_call" :content tool-call})
258+
(add-to-history! {:role "tool_call_output" :content (assoc tool-call :output output)})
259+
(swap! tool-call-args-by-id* dissoc id)
260+
(messenger/chat-content-received
261+
messenger
262+
{:chat-id chat-id
263+
:request-id request-id
264+
:role :assistant
265+
:content {:type :toolCalled
266+
:origin (tool-name->origin name all-tools)
267+
:name name
268+
:arguments arguments
269+
:id id
270+
:outputs (:contents output)}})
271+
{:new-messages (get-in @db* [:chats chat-id :messages])})
272+
(do
273+
(add-to-history! {:role "tool_call" :content tool-call})
274+
(add-to-history! {:role "tool_call_output" :content (assoc tool-call :output {:contents [{:content "Tool call rejected by user"
275+
:error true
276+
:type :text}]})})
277+
(swap! tool-call-args-by-id* dissoc id)
278+
(messenger/chat-content-received
279+
messenger
280+
{:chat-id chat-id
281+
:request-id request-id
282+
:role :system
283+
:content {:type :progress
284+
:state :running
285+
:text "Generating"}})
286+
(messenger/chat-content-received
287+
messenger
288+
{:chat-id chat-id
289+
:request-id request-id
290+
:role :assistant
291+
:content {:type :toolCallRejected
292+
:origin (tool-name->origin name all-tools)
293+
:name name
294+
:arguments arguments
295+
:reason :user
296+
:id id}})
297+
{:new-messages (get-in @db* [:chats chat-id :messages])}))))
258298
:on-reason (fn [{:keys [status]}]
259299
(assert-chat-not-stopped! chat-id db* messenger)
260300
(let [msg (case status
@@ -282,6 +322,12 @@
282322
:model chosen-model
283323
:status :success}))
284324

325+
(defn tool-call-approve [{:keys [chat-id tool-call-id]} db*]
326+
(deliver (get-in @db* [:chats chat-id :tool-calls tool-call-id :approved?*]) true))
327+
328+
(defn tool-call-reject [{:keys [chat-id tool-call-id]} db*]
329+
(deliver (get-in @db* [:chats chat-id :tool-calls tool-call-id :approved?*]) false))
330+
285331
(defn ^:private contexts-for [root-filename query config]
286332
(let [all-files (fs/glob root-filename (str "**" (or query "") "**"))
287333
allowed-files (f.index/filter-allowed all-files root-filename config)]

src/eca/handlers.clj

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,20 @@
7474
:eca/chat-query-context
7575
(f.chat/query-context params db* config)))
7676

77+
(defn chat-tool-call-approve [{:keys [db*]} params]
78+
(logger/logging-task
79+
:eca/chat-tool-call-approve
80+
(f.chat/tool-call-approve params db*)))
81+
82+
(defn chat-tool-call-reject [{:keys [db*]} params]
83+
(logger/logging-task
84+
:eca/chat-tool-call-reject
85+
(f.chat/tool-call-reject params db*)))
86+
7787
(defn chat-prompt-stop [{:keys [db* messenger]} params]
7888
(logger/logging-task
79-
:eca/chat-prompt-stop
80-
(f.chat/prompt-stop params db* messenger)))
89+
:eca/chat-prompt-stop
90+
(f.chat/prompt-stop params db* messenger)))
8191

8292
(defn chat-delete [{:keys [db*]} params]
8393
(logger/logging-task

src/eca/llm_providers/anthropic.clj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,10 @@
124124
(when (= "tool_use" (:type content-block))
125125
(let [function-name (:name content-block)
126126
function-args (:input-json content-block)
127-
{:keys [past-messages]} (on-tool-called {:id (:id content-block)
128-
:name function-name
129-
:arguments (json/parse-string function-args)})
130-
messages (-> (past-messages->messages past-messages)
127+
{:keys [new-messages]} (on-tool-called {:id (:id content-block)
128+
:name function-name
129+
:arguments (json/parse-string function-args)})
130+
messages (-> (past-messages->messages new-messages)
131131
add-cache-to-last-message)]
132132
(base-request!
133133
{:rid (llm-util/gen-rid)

src/eca/llm_providers/ollama.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,12 @@
101101

102102
done_reason
103103
(if-let [tool-call (get @tool-calls* rid)]
104-
(let [{:keys [past-messages]} (on-tool-called tool-call)]
104+
(let [{:keys [new-messages]} (on-tool-called tool-call)]
105105
(swap! tool-calls* dissoc rid)
106106
(base-completion-request!
107107
{:rid (llm-util/gen-rid)
108108
:url url
109-
:body (assoc body :messages (past-messages->messages past-messages context))
109+
:body (assoc body :messages (past-messages->messages new-messages context))
110110
:on-error on-error
111111
:on-response handle-response}))
112112
(on-message-received {:type :finish

src/eca/llm_providers/openai.clj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@
8686
(case (:type (:item data))
8787
"function_call" (let [function-name (-> data :item :name)
8888
function-args (-> data :item :arguments)
89-
{:keys [past-messages]} (on-tool-called {:id (-> data :item :call_id)
90-
:name function-name
91-
:arguments (json/parse-string function-args)})
92-
input (past-messages->input past-messages)]
89+
{:keys [new-messages]} (on-tool-called {:id (-> data :item :call_id)
90+
:name function-name
91+
:arguments (json/parse-string function-args)})
92+
input (past-messages->input new-messages)]
9393
(base-completion-request!
9494
{:rid (llm-util/gen-rid)
9595
:body (assoc body :input input)

src/eca/server.clj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@
4949
(defmethod lsp.server/receive-request "chat/queryContext" [_ components params]
5050
(handlers/chat-query-context (with-config components) params))
5151

52+
(defmethod lsp.server/receive-notification "chat/toolCallApprove" [_ components params]
53+
(handlers/chat-tool-call-approve (with-config components) params))
54+
55+
(defmethod lsp.server/receive-notification "chat/toolCallReject" [_ components params]
56+
(handlers/chat-tool-call-reject (with-config components) params))
57+
5258
(defmethod lsp.server/receive-notification "chat/promptStop" [_ components params]
5359
(handlers/chat-prompt-stop (with-config components) params))
5460

0 commit comments

Comments
 (0)