Skip to content

Commit 1d534e2

Browse files
committed
Support reject with reason tool calls
1 parent 2dee21e commit 1d534e2

6 files changed

Lines changed: 141 additions & 128 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Bump MCP java sdk to 0.13.1
66
- Improve MCP logs on stderr.
7+
- Support tool call rejection with reasons inputed by user. #127
78

89
## 0.57.0
910

src/eca/features/chat.clj

Lines changed: 108 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -466,104 +466,117 @@
466466
(when-not (string/blank? @received-msgs*)
467467
(add-to-history! {:role "assistant" :content [{:type :text :text @received-msgs*}]})
468468
(reset! received-msgs* ""))
469-
(run! (fn do-tool-call [{:keys [id name arguments] :as tool-call}]
470-
(let [approved?* (promise)
471-
details (f.tools/tool-call-details-before-invocation name arguments)
472-
summary (f.tools/tool-call-summary all-tools name arguments)
473-
origin (tool-name->origin name all-tools)
474-
approval (f.tools/approval all-tools name arguments db config behavior)
475-
ask? (= :ask approval)]
476-
;; assert: In :preparing or :stopped
477-
;; Inform client the tool is about to run and store approval promise
478-
(when-not (#{:stopped} (:status (get-tool-call-state @db* chat-id id)))
479-
(transition-tool-call! db* chat-ctx id :tool-run
480-
{:approved?* approved?*
481-
:name name
482-
:origin (tool-name->origin name all-tools)
483-
:arguments arguments
484-
:manual-approval ask?
485-
:details details
486-
:summary summary}))
487-
;; assert: In: :check-approval or :stopped or :rejected
488-
(when-not (#{:stopped :rejected} (:status (get-tool-call-state @db* chat-id id)))
489-
(case approval
490-
:ask (transition-tool-call! db* chat-ctx id :approval-ask
491-
{:state :running
492-
:text "Waiting for tool call approval"})
493-
:allow (transition-tool-call! db* chat-ctx id :approval-allow
494-
{:reason {:code :user-config-allow
495-
:text "Tool call allowed by user config"}})
496-
:deny (transition-tool-call! db* chat-ctx id :approval-deny
497-
{:reason {:code :user-config-deny
498-
:text "Tool call denied by user config"}})
499-
(logger/warn logger-tag "Unknown value of approval in config"
500-
{:approval approval :tool-call-id id})))
501-
;; Execute each tool call concurrently
502-
(if @approved?* ;TODO: Should there be a timeout here? If so, what would be the state transitions?
503-
;; assert: In :execution-approved or :stopped
469+
(let [any-rejected-tool-call?* (atom false)]
470+
(run! (fn do-tool-call [{:keys [id name arguments] :as tool-call}]
471+
(let [approved?* (promise)
472+
details (f.tools/tool-call-details-before-invocation name arguments)
473+
summary (f.tools/tool-call-summary all-tools name arguments)
474+
origin (tool-name->origin name all-tools)
475+
approval (f.tools/approval all-tools name arguments db config behavior)
476+
ask? (= :ask approval)]
477+
;; assert: In :preparing or :stopped
478+
;; Inform client the tool is about to run and store approval promise
504479
(when-not (#{:stopped} (:status (get-tool-call-state @db* chat-id id)))
505-
(assert-chat-not-stopped! chat-ctx)
506-
(let [;; Since a future starts executing immediately,
507-
;; we need to delay the future so that the set-call-future action,
508-
;; used implicitly in the transition-tool-call! on the :execution-start event,
509-
;; can activate the future only *after* the state transition to :executing.
510-
delayed-future
511-
(delay
512-
(future
513-
;; assert: In :executing
514-
(let [result (f.tools/call-tool! name arguments behavior chat-id db* config messenger metrics)
515-
details (f.tools/tool-call-details-after-invocation name arguments details result)
516-
{:keys [start-time]} (get-tool-call-state @db* chat-id id)]
517-
(add-to-history! {:role "tool_call"
518-
:content (assoc tool-call
519-
:details details
520-
:summary summary
521-
:origin origin)})
522-
(add-to-history! {:role "tool_call_output"
523-
:content (assoc tool-call
524-
:error (:error result)
525-
:output result
526-
:details details
527-
:summary summary
528-
:origin origin)})
529-
(transition-tool-call! db* chat-ctx id :execution-end
530-
{:origin origin
531-
:name name
532-
:arguments arguments
533-
:error (:error result)
534-
:outputs (:contents result)
535-
:total-time-ms (- (System/currentTimeMillis) start-time)
536-
:details details
537-
:summary summary}))))]
538-
(transition-tool-call! db* chat-ctx id :execution-start
539-
{:delayed-future delayed-future
540-
:origin origin
541-
:name name
542-
:arguments arguments
543-
:start-time (System/currentTimeMillis)
544-
:details details
545-
:summary summary})))
546-
;; assert: In :rejected state
547-
(let [tool-call-state (get-tool-call-state @db* chat-id id)
548-
{:keys [code text]} (:decision-reason tool-call-state)]
549-
(add-to-history! {:role "tool_call" :content tool-call})
550-
(add-to-history! {:role "tool_call_output"
551-
:content (assoc tool-call :output {:error true
552-
:contents [{:text text
553-
:type :text}]})})
554-
(transition-tool-call! db* chat-ctx id :send-reject
555-
{:origin origin
480+
(transition-tool-call! db* chat-ctx id :tool-run
481+
{:approved?* approved?*
556482
:name name
483+
:origin (tool-name->origin name all-tools)
557484
:arguments arguments
558-
:reason code
485+
:manual-approval ask?
559486
:details details
560-
:summary summary})))))
561-
tool-calls)
562-
(assert-chat-not-stopped! chat-ctx)
563-
;; Wait for all tool calls with futures to complete before returning
564-
(run! deref (filter some? (map :future (vals (get-active-tool-calls @db* chat-id)))))
565-
(send-content! chat-ctx :system {:type :progress :state :running :text "Generating"})
566-
{:new-messages (get-in @db* [:chats chat-id :messages])})
487+
:summary summary}))
488+
;; assert: In: :check-approval or :stopped or :rejected
489+
(when-not (#{:stopped :rejected} (:status (get-tool-call-state @db* chat-id id)))
490+
(case approval
491+
:ask (transition-tool-call! db* chat-ctx id :approval-ask
492+
{:state :running
493+
:text "Waiting for tool call approval"})
494+
:allow (transition-tool-call! db* chat-ctx id :approval-allow
495+
{:reason {:code :user-config-allow
496+
:text "Tool call allowed by user config"}})
497+
:deny (transition-tool-call! db* chat-ctx id :approval-deny
498+
{:reason {:code :user-config-deny
499+
:text "Tool call rejected by user config"}})
500+
(logger/warn logger-tag "Unknown value of approval in config"
501+
{:approval approval :tool-call-id id})))
502+
;; Execute each tool call concurrently
503+
(if @approved?* ;TODO: Should there be a timeout here? If so, what would be the state transitions?
504+
;; assert: In :execution-approved or :stopped
505+
(when-not (#{:stopped} (:status (get-tool-call-state @db* chat-id id)))
506+
(assert-chat-not-stopped! chat-ctx)
507+
(let [;; Since a future starts executing immediately,
508+
;; we need to delay the future so that the set-call-future action,
509+
;; used implicitly in the transition-tool-call! on the :execution-start event,
510+
;; can activate the future only *after* the state transition to :executing.
511+
delayed-future
512+
(delay
513+
(future
514+
;; assert: In :executing
515+
(let [result (f.tools/call-tool! name arguments behavior chat-id db* config messenger metrics)
516+
details (f.tools/tool-call-details-after-invocation name arguments details result)
517+
{:keys [start-time]} (get-tool-call-state @db* chat-id id)]
518+
(add-to-history! {:role "tool_call"
519+
:content (assoc tool-call
520+
:details details
521+
:summary summary
522+
:origin origin)})
523+
(add-to-history! {:role "tool_call_output"
524+
:content (assoc tool-call
525+
:error (:error result)
526+
:output result
527+
:details details
528+
:summary summary
529+
:origin origin)})
530+
(transition-tool-call! db* chat-ctx id :execution-end
531+
{:origin origin
532+
:name name
533+
:arguments arguments
534+
:error (:error result)
535+
:outputs (:contents result)
536+
:total-time-ms (- (System/currentTimeMillis) start-time)
537+
:details details
538+
:summary summary}))))]
539+
(transition-tool-call! db* chat-ctx id :execution-start
540+
{:delayed-future delayed-future
541+
:origin origin
542+
:name name
543+
:arguments arguments
544+
:start-time (System/currentTimeMillis)
545+
:details details
546+
:summary summary})))
547+
;; assert: In :rejected state
548+
(let [tool-call-state (get-tool-call-state @db* chat-id id)
549+
{:keys [code text]} (:decision-reason tool-call-state)]
550+
(add-to-history! {:role "tool_call" :content tool-call})
551+
(add-to-history! {:role "tool_call_output"
552+
:content (assoc tool-call :output {:error true
553+
:contents [{:text text
554+
:type :text}]})})
555+
(reset! any-rejected-tool-call?* true)
556+
(transition-tool-call! db* chat-ctx id :send-reject
557+
{:origin origin
558+
:name name
559+
:arguments arguments
560+
:reason code
561+
:details details
562+
:summary summary})))))
563+
tool-calls)
564+
(assert-chat-not-stopped! chat-ctx)
565+
;; Wait for all tool calls with futures to complete before returning
566+
(->> (vals (get-active-tool-calls @db* chat-id))
567+
(map :future)
568+
(filter some?)
569+
(run! deref))
570+
(if @any-rejected-tool-call?*
571+
(do
572+
(send-content! chat-ctx :system
573+
{:type :text
574+
:text "Tell ECA what to do differently for the rejected tool"})
575+
(finish-chat-prompt! :idle chat-ctx)
576+
nil)
577+
(do
578+
(send-content! chat-ctx :system {:type :progress :state :running :text "Generating"})
579+
{:new-messages (get-in @db* [:chats chat-id :messages])}))))
567580
:on-reason (fn [{:keys [status id text external-id]}]
568581
(assert-chat-not-stopped! chat-ctx)
569582
(case status
@@ -727,7 +740,7 @@
727740
:messenger messenger}]
728741
(transition-tool-call! db* chat-ctx tool-call-id :user-reject
729742
{:reason {:code :user-choice-deny
730-
:text "Tool call denied by user choice"}})))
743+
:text "Tool call rejected by user choice"}})))
731744

732745
(defn query-context
733746
[{:keys [query contexts chat-id]}

0 commit comments

Comments
 (0)