|
93 | 93 | {;; Note: transition-tool-call! treats no existing state as :initial state |
94 | 94 | [:initial :tool-prepare] |
95 | 95 | {:status :preparing |
96 | | - :actions [:send-toolCallPrepare]} |
| 96 | + :actions [:send-toolCallPrepare :init-decision-reason]} |
97 | 97 |
|
98 | 98 | [:preparing :tool-prepare] |
99 | 99 | {:status :preparing |
|
102 | 102 | [:preparing :tool-run] |
103 | 103 | {:status :check-approval |
104 | 104 | :actions [:init-approval-promise :send-toolCallRun]} |
| 105 | + ;; TODO: What happens if the promise is created, but no deref happens since the call is stopped? |
105 | 106 |
|
106 | 107 | [:check-approval :config-ask] |
107 | 108 | {:status :waiting-approval |
108 | 109 | :actions [:send-progress]} |
109 | 110 |
|
110 | 111 | [:check-approval :config-allow] |
111 | 112 | {:status :execution-approved |
112 | | - :actions [:deliver-approval-true]} |
| 113 | + :actions [:set-decision-reason :deliver-approval-true]} |
113 | 114 |
|
114 | 115 | [:check-approval :config-deny] |
115 | 116 | {:status :rejected |
116 | | - :actions [:deliver-approval-false]} |
| 117 | + :actions [:set-decision-reason :deliver-approval-false]} |
117 | 118 |
|
118 | 119 | [:waiting-approval :user-approve] |
119 | 120 | {:status :execution-approved |
120 | | - :actions [:deliver-approval-true]} |
| 121 | + :actions [:set-decision-reason :deliver-approval-true]} |
121 | 122 |
|
122 | 123 | [:waiting-approval :user-reject] |
123 | 124 | {:status :rejected |
124 | | - :actions [:deliver-approval-false :log-rejection]} |
| 125 | + :actions [:set-decision-reason :deliver-approval-false :log-rejection]} |
125 | 126 |
|
126 | 127 | [:rejected :send-reject] |
127 | 128 | {:status :rejected |
|
133 | 134 |
|
134 | 135 | [:executing :execution-end] |
135 | 136 | {:status :completed |
136 | | - :actions [:send-toolCalled :record-metrics]} |
| 137 | + :actions [:send-toolCalled :log-metrics]} |
137 | 138 |
|
138 | 139 | ;; And now all the :stop-requested transitions |
139 | 140 |
|
140 | | - ;; TODO: In the future, when calls can be interrupted, |
141 | | - ;; more states and actions will be required. |
| 141 | + ;; TODO: In the future, when calls can be interrupted, more states and actions will be required. |
| 142 | + ;; Therefore, currently, there is no transition from :executing on a :stop-requested event. |
142 | 143 |
|
143 | 144 | [:execution-approved :stop-requested] |
144 | 145 | {:status :stopped |
145 | 146 | :actions [:send-toolCallRejected]} |
146 | 147 |
|
147 | 148 | [:waiting-approval :stop-requested] |
148 | | - {:status :stopped |
149 | | - :actions [:deliver-approval-false]} |
| 149 | + {:status :rejected |
| 150 | + :actions [:set-decision-reason :deliver-approval-false]} |
150 | 151 |
|
151 | 152 | [:check-approval :stop-requested] |
152 | | - {:status :stopped |
153 | | - :actions [:send-toolCallRejected]} |
| 153 | + {:status :rejected |
| 154 | + :actions [:set-decision-reason :send-toolCallRejected]} |
154 | 155 |
|
155 | 156 | [:preparing :stop-requested] |
156 | 157 | {:status :stopped |
157 | | - :actions [:send-toolCallRejected]} |
| 158 | + :actions [:set-decision-reason :send-toolCallRejected]} |
158 | 159 |
|
159 | 160 | [:initial :stop-requested] ; Nothing sent yet, just mark as stopped |
160 | 161 | {:status :stopped |
|
231 | 232 | (deliver (get-in @db* [:chats (:chat-id chat-ctx) :tool-calls tool-call-id :approved?*]) |
232 | 233 | true) |
233 | 234 |
|
234 | | - :deliver-default-approval |
235 | | - (deliver (get-in @db* [:chats (:chat-id chat-ctx) :tool-calls tool-call-id :approved?*]) |
236 | | - (:default-approval event-data)) |
| 235 | + :init-decision-reason |
| 236 | + (swap! db* assoc-in [:chats (:chat-id chat-ctx) :tool-calls tool-call-id :decision-reason] |
| 237 | + {:reason {:code :none |
| 238 | + :text "No reason"}}) |
237 | 239 |
|
238 | | - ;; Logging/metrics actions |
| 240 | + :set-decision-reason |
| 241 | + (swap! db* assoc-in [:chats (:chat-id chat-ctx) :tool-calls tool-call-id :decision-reason] |
| 242 | + (:reason event-data)) |
| 243 | + |
| 244 | + ;; Logging actions |
239 | 245 | :log-rejection |
240 | 246 | (logger/info logger-tag "Tool call rejected" |
241 | 247 | {:tool-call-id tool-call-id :reason (:reason event-data)}) |
242 | 248 |
|
243 | | - :record-metrics |
| 249 | + :log-metrics |
244 | 250 | (logger/debug logger-tag "Tool call completed" |
245 | 251 | {:tool-call-id tool-call-id :duration (:duration event-data)}) |
246 | 252 |
|
|
266 | 272 | transition-key [current-status event] |
267 | 273 | {:keys [status actions]} (get tool-call-state-machine transition-key)] |
268 | 274 |
|
| 275 | + (logger/debug logger-tag "Tool call transition" |
| 276 | + {:current-status current-status :event event :status status}) |
| 277 | + |
269 | 278 | (when-not status |
270 | 279 | (let [valid-events (map second (filter #(= current-status (first %)) |
271 | 280 | (keys tool-call-state-machine)))] |
|
437 | 446 | origin (tool-name->origin name all-tools) |
438 | 447 | approval (f.tools/approval all-tools name arguments db config) |
439 | 448 | ask? (= :ask approval)] |
| 449 | + ;; assert: In :preparing or :stopped |
440 | 450 | ;; Inform client the tool is about to run and store approval promise |
441 | | - (transition-tool-call! db* chat-ctx id :tool-run |
442 | | - {:approved?* approved?* |
443 | | - :name name |
444 | | - :origin (tool-name->origin name all-tools) |
445 | | - :arguments arguments |
446 | | - :manual-approval ask? |
447 | | - :details details |
448 | | - :summary summary}) |
449 | | - (case approval |
450 | | - :ask (transition-tool-call! db* chat-ctx id :config-ask |
451 | | - {:state :running |
452 | | - :text "Waiting for tool call approval"}) |
453 | | - :allow (transition-tool-call! db* chat-ctx id :config-allow) |
454 | | - :deny (transition-tool-call! db* chat-ctx id :config-deny)) |
| 451 | + (when-not (#{:stopped} (:status (get-tool-call-state @db* chat-id id))) |
| 452 | + (transition-tool-call! db* chat-ctx id :tool-run |
| 453 | + {:approved?* approved?* |
| 454 | + :name name |
| 455 | + :origin (tool-name->origin name all-tools) |
| 456 | + :arguments arguments |
| 457 | + :manual-approval ask? |
| 458 | + :details details |
| 459 | + :summary summary})) |
| 460 | + ;; assert: In: :check-approval or :stopped |
| 461 | + (when-not (#{:stopped} (:status (get-tool-call-state @db* chat-id id))) |
| 462 | + (case approval |
| 463 | + :ask (transition-tool-call! db* chat-ctx id :config-ask |
| 464 | + {:state :running |
| 465 | + :text "Waiting for tool call approval"}) |
| 466 | + :allow (transition-tool-call! db* chat-ctx id :config-allow |
| 467 | + {:reason {:code :user-config-allow |
| 468 | + :text "Tool call allowed by user config"}}) |
| 469 | + :deny (transition-tool-call! db* chat-ctx id :config-deny |
| 470 | + {:reason {:code :user-config-deny |
| 471 | + :text "Tool call denied by user config"}}) |
| 472 | + (logger/warn logger-tag "Unknown value of approval in config" |
| 473 | + {:approval approval :tool-call-id id}))) |
455 | 474 | ;; Execute each tool call concurrently |
456 | 475 | (future |
457 | 476 | (if @approved?* ;TODO: Should there be a timeout here? If so, what would be the state transitions? |
458 | | - ;; assert: In :execution-approved state |
459 | | - (do |
| 477 | + ;; assert: In :execution-approved or :stopped |
| 478 | + (when-not (#{:stopped} (:status (get-tool-call-state @db* chat-id id))) |
460 | 479 | (assert-chat-not-stopped! chat-ctx) |
461 | 480 | (transition-tool-call! db* chat-ctx id :execution-start) |
| 481 | + ;; assert: In :executing |
462 | 482 | (let [result (f.tools/call-tool! name arguments @db* config messenger behavior) |
463 | 483 | details (f.tools/tool-call-details-after-invocation name arguments details result)] |
464 | 484 | (add-to-history! {:role "tool_call" :content (assoc tool-call |
|
629 | 649 | :db* db* |
630 | 650 | :request-id request-id |
631 | 651 | :messenger messenger}] |
632 | | - (transition-tool-call! db* chat-ctx tool-call-id :user-approve))) |
| 652 | + (transition-tool-call! db* chat-ctx tool-call-id :user-approve |
| 653 | + {:reason {:code :user-choice-allow |
| 654 | + :text "Tool call allowed by user choice"}}))) |
633 | 655 |
|
634 | 656 | (defn tool-call-reject [{:keys [chat-id tool-call-id request-id]} db* messenger] |
635 | 657 | (let [chat-ctx {;; What else is needed? |
636 | 658 | :chat-id chat-id |
637 | 659 | :db* db* |
638 | 660 | :request-id request-id |
639 | 661 | :messenger messenger}] |
640 | | - (transition-tool-call! db* chat-ctx tool-call-id :user-reject))) |
| 662 | + (transition-tool-call! db* chat-ctx tool-call-id :user-reject |
| 663 | + {:reason {:code :user-choice-deny |
| 664 | + :text "Tool call denied by user choice"}}))) |
641 | 665 |
|
642 | 666 | (defn query-context |
643 | 667 | [{:keys [query contexts chat-id]} |
|
672 | 696 |
|
673 | 697 | ;; Handle each active tool call |
674 | 698 | (doseq [[tool-call-id _] (get-active-tool-calls @db* chat-id)] |
675 | | - (transition-tool-call! db* chat-ctx tool-call-id :stop-requested)) |
| 699 | + (transition-tool-call! db* chat-ctx tool-call-id :stop-requested |
| 700 | + {:reason {:code :user-prompt-stop |
| 701 | + :text "Tool call rejected because of user prompt stop"}})) |
676 | 702 | (finish-chat-prompt! :stopping chat-ctx)))) |
677 | 703 |
|
678 | 704 | (defn delete-chat |
|
0 commit comments