|
173 | 173 | :instructions (m/pred string?)} |
174 | 174 | (llm.mocks/get-req-body :reasoning-1))))))) |
175 | 175 |
|
176 | | -#_(deftest tool-calling |
177 | | - (eca/start-process!) |
| 176 | +(deftest tool-calling-with-thought-signatures |
| 177 | + (eca/start-process!) |
178 | 178 |
|
179 | | - (eca/request! (fixture/initialize-request)) |
180 | | - (eca/notify! (fixture/initialized-notification)) |
181 | | - (let [chat-id* (atom nil)] |
182 | | - (testing "We ask what files LLM see" |
183 | | - (llm.mocks/set-case! :tool-calling-0) |
184 | | - (let [0 |
185 | | - resp (eca/request! (fixture/chat-prompt-request |
186 | | - {:model "google/gemini-2.5-pro" |
187 | | - :message "What files you see?"})) |
188 | | - chat-id (reset! chat-id* (:chatId resp))] |
| 179 | + (eca/request! (fixture/initialize-request)) |
| 180 | + (eca/notify! (fixture/initialized-notification)) |
| 181 | + (llm-mock.openai-chat/set-thinking-tag! "thought") |
| 182 | + (let [chat-id* (atom nil)] |
| 183 | + (testing "We ask what files LLM sees - tool call includes thought signature" |
| 184 | + (llm.mocks/set-case! :tool-calling-with-thought-signature-0) |
| 185 | + (let [resp (eca/request! (fixture/chat-prompt-request |
| 186 | + {:model "google/gemini-2.5-pro" |
| 187 | + :message "What files you see?"})) |
| 188 | + chat-id (reset! chat-id* (:chatId resp))] |
189 | 189 |
|
190 | | - (is (match? |
191 | | - {:chatId (m/pred string?) |
192 | | - :model "google/gemini-2.5-pro" |
193 | | - :status "prompting"} |
194 | | - resp)) |
| 190 | + (is (match? |
| 191 | + {:chatId (m/pred string?) |
| 192 | + :model "google/gemini-2.5-pro" |
| 193 | + :status "prompting"} |
| 194 | + resp)) |
| 195 | + |
| 196 | + (match-content chat-id "user" {:type "text" :text "What files you see?\n"}) |
| 197 | + (match-content chat-id "system" {:type "metadata" :title "Some Cool Title"}) |
| 198 | + (match-content chat-id "system" {:type "progress" :state "running" :text "Waiting model"}) |
| 199 | + (match-content chat-id "system" {:type "progress" :state "running" :text "Generating"}) |
| 200 | + (match-content chat-id "assistant" {:type "reasonStarted" :id (m/pred string?)}) |
| 201 | + ;; Note: The buffering in process-text-think-aware keeps a 9-char tail to detect </thought>, |
| 202 | + ;; so chunks get re-split during streaming. The mock sends "I s", "hould call tool", " eca__directory_tree" |
| 203 | + ;; but after buffering we get these chunks: |
| 204 | + (match-content chat-id "assistant" {:type "reasonText" :id (m/pred string?) :text "I should "}) |
| 205 | + (match-content chat-id "assistant" {:type "reasonText" :id (m/pred string?) :text "call tool eca__direc"}) |
| 206 | + (match-content chat-id "assistant" {:type "reasonText" :id (m/pred string?) :text "tory_tree"}) |
| 207 | + (match-content chat-id "assistant" {:type "reasonFinished" :id (m/pred string?) :totalTimeMs (m/pred number?)}) |
| 208 | + ;; Text is buffered (8-char tail for <thought> detection), then flushed when tool calls start |
| 209 | + (match-content chat-id "assistant" {:type "text" :text "I will li"}) |
| 210 | + (match-content chat-id "assistant" {:type "text" :text "st files"}) |
| 211 | + (match-content chat-id "assistant" {:type "toolCallPrepare" |
| 212 | + :origin "native" |
| 213 | + :id (m/pred string?) |
| 214 | + :name "directory_tree" |
| 215 | + :argumentsText "" |
| 216 | + :summary "Listing file tree"}) |
| 217 | + (match-content chat-id "assistant" {:type "toolCallPrepare" |
| 218 | + :origin "native" |
| 219 | + :id (m/pred string?) |
| 220 | + :name "directory_tree" |
| 221 | + :argumentsText "{\"pat" |
| 222 | + :summary "Listing file tree"}) |
| 223 | + (match-content chat-id "assistant" {:type "toolCallPrepare" |
| 224 | + :origin "native" |
| 225 | + :id (m/pred string?) |
| 226 | + :name "directory_tree" |
| 227 | + :argumentsText (str "h\":\"" (h/json-escape-path (h/project-path->canon-path "resources")) "\"}") |
| 228 | + :summary "Listing file tree"}) |
| 229 | + (match-content chat-id "system" {:type "usage"}) |
| 230 | + (match-content chat-id "assistant" {:type "toolCallRun" |
| 231 | + :origin "native" |
| 232 | + :id (m/pred string?) |
| 233 | + :name "directory_tree" |
| 234 | + :arguments {:path (h/project-path->canon-path "resources")} |
| 235 | + :manualApproval false |
| 236 | + :summary "Listing file tree"}) |
| 237 | + (match-content chat-id "assistant" {:type "toolCallRunning" |
| 238 | + :origin "native" |
| 239 | + :id (m/pred string?) |
| 240 | + :name "directory_tree" |
| 241 | + :arguments {:path (h/project-path->canon-path "resources")} |
| 242 | + :summary "Listing file tree"}) |
| 243 | + (match-content chat-id "system" {:type "progress" :state "running" :text "Calling tool"}) |
| 244 | + (match-content chat-id "assistant" {:type "toolCalled" |
| 245 | + :origin "native" |
| 246 | + :id (m/pred string?) |
| 247 | + :name "directory_tree" |
| 248 | + :arguments {:path (h/project-path->canon-path "resources")} |
| 249 | + :summary "Listing file tree" |
| 250 | + :totalTimeMs (m/pred number?) |
| 251 | + :error false |
| 252 | + :outputs [{:type "text" :text (str (h/project-path->canon-path "resources") "\n" |
| 253 | + " file1.md\n" |
| 254 | + " file2.md\n\n" |
| 255 | + "0 directories, 2 files")}]}) |
| 256 | + ;; Text chunks get re-split due to 8-char tail buffering for <thought> detection. |
| 257 | + ;; Note: We use m/in-any-order for the final text/usage/progress events since their |
| 258 | + ;; relative ordering can vary due to async processing and buffering. |
| 259 | + (match-content chat-id "assistant" {:type "text" :text "The files"}) |
| 260 | + (match-content chat-id "assistant" {:type "text" :text " I see:\nfile"}) |
| 261 | + (match-content chat-id "assistant" {:type "text" :text "1\nfile2\n"}) |
| 262 | + (match-content chat-id "system" {:type "progress" :state "finished"}) |
195 | 263 |
|
196 | | - (match-content chat-id "user" {:type "text" :text "What files you see?\n"}) |
197 | | - (match-content chat-id "system" {:type "progress" :state "running" :text "Waiting model"}) |
198 | | - (match-content chat-id "system" {:type "progress" :state "running" :text "Generating"}) |
199 | | - (match-content chat-id "assistant" {:type "reasonStarted" :id (m/pred string?)}) |
200 | | - (match-content chat-id "assistant" {:type "reasonText" :id (m/pred string?) :text "I should call tool"}) |
201 | | - (match-content chat-id "assistant" {:type "reasonText" :id (m/pred string?) :text " eca__directory_tree"}) |
202 | | - (match-content chat-id "assistant" {:type "reasonFinished" :id (m/pred string?) :totalTimeMs (m/pred number?)}) |
203 | | - (match-content chat-id "assistant" {:type "text" :text "I will list files"}) |
204 | | - (match-content chat-id "assistant" {:type "toolCallPrepare" |
205 | | - :origin "native" |
206 | | - :id "tool-1" |
207 | | - :name "directory_tree" |
208 | | - :argumentsText "" |
209 | | - :manualApproval false |
210 | | - :summary "Listing file tree"}) |
211 | | - (match-content chat-id "assistant" {:type "toolCallPrepare" |
212 | | - :origin "native" |
213 | | - :id "tool-1" |
214 | | - :name "directory_tree" |
215 | | - :argumentsText "{\"pat" |
216 | | - :manualApproval false |
217 | | - :summary "Listing file tree"}) |
218 | | - (match-content chat-id "assistant" {:type "toolCallPrepare" |
219 | | - :origin "native" |
220 | | - :id "tool-1" |
221 | | - :name "directory_tree" |
222 | | - :argumentsText (str "h\":\"" (h/project-path->canon-path "resources") "\"}") |
223 | | - :manualApproval false |
224 | | - :summary "Listing file tree"}) |
225 | | - (match-content chat-id "system" {:type "usage" |
226 | | - :messageInputTokens 5 |
227 | | - :messageOutputTokens 30 |
228 | | - :sessionTokens 35 |
229 | | - :messageCost (m/pred string?) |
230 | | - :sessionCost (m/pred string?)}) |
231 | | - (match-content chat-id "assistant" {:type "toolCallRun" |
232 | | - :origin "native" |
233 | | - :id "tool-1" |
234 | | - :name "directory_tree" |
235 | | - :arguments {:path (h/project-path->canon-path "resources")} |
236 | | - :manualApproval false |
237 | | - :summary "Listing file tree"}) |
238 | | - (match-content chat-id "assistant" {:type "toolCallRunning" |
239 | | - :origin "native" |
240 | | - :id "tool-1" |
241 | | - :name "directory_tree" |
242 | | - :arguments {:path (h/project-path->canon-path "resources")} |
243 | | - :totalTimeMs number? |
244 | | - :summary "Listing file tree"}) |
245 | | - (match-content chat-id "assistant" {:type "toolCalled" |
246 | | - :origin "native" |
247 | | - :id "tool-1" |
248 | | - :name "directory_tree" |
249 | | - :arguments {:path (h/project-path->canon-path "resources")} |
250 | | - :summary "Listing file tree" |
251 | | - :error false |
252 | | - :outputs [{:type "text" :text (str "[FILE] " (h/project-path->canon-path "resources/file1.md\n") |
253 | | - "[FILE] " (h/project-path->canon-path "resources/file2.md\n"))}]}) |
254 | | - (match-content chat-id "assistant" {:type "text" :text "The files I see:\n"}) |
255 | | - (match-content chat-id "assistant" {:type "text" :text "file1\nfile2\n"}) |
256 | | - (match-content chat-id "system" {:type "usage" |
257 | | - :messageInputTokens 5 |
258 | | - :messageOutputTokens 30 |
259 | | - :sessionTokens 70 |
260 | | - :messageCost (m/pred string?) |
261 | | - :sessionCost (m/pred string?)}) |
262 | | - (match-content chat-id "system" {:type "progress" :state "finished"}) |
| 264 | + ;; Verify thought signature was passed back in the second request |
| 265 | + (let [raw-messages (llm.mocks/get-raw-messages :tool-calling-with-thought-signature-0) |
| 266 | + ;; Find the assistant message with tool_calls |
| 267 | + assistant-tool-call-msg (first (filter #(and (= "assistant" (:role %)) |
| 268 | + (seq (:tool_calls %))) |
| 269 | + raw-messages))] |
263 | 270 | (is (match? |
264 | | - {:messages [{:role "user" :content [{:type "text" :text "What files you see?"}]} |
265 | | - {:role "assistant" |
266 | | - :content [{:type "thinking" |
267 | | - :signature "enc-123" |
268 | | - :thinking "I should call tool eca__directory_tree"}]} |
269 | | - {:role "assistant" :content [{:type "text" :text "I will list files"}]} |
270 | | - {:role "assistant" |
271 | | - :content [{:type "tool_use" |
272 | | - :id "tool-1" |
273 | | - :name "eca__directory_tree" |
274 | | - :input {:path (h/project-path->canon-path "resources")}}]} |
275 | | - {:role "user" |
276 | | - :content [{:type "tool_result" |
277 | | - :tool_use_id "tool-1" |
278 | | - :content (str "[FILE] " (h/project-path->canon-path "resources/file1.md\n") |
279 | | - "[FILE] " (h/project-path->canon-path "resources/file2.md\n\n"))}]}] |
280 | | - :tools (m/embeds |
281 | | - [{:name "eca__directory_tree"}]) |
282 | | - :system (m/pred vector?)} |
283 | | - llm.mocks/*last-req-body*)))))) |
| 271 | + {:role "assistant" |
| 272 | + :tool_calls [{:extra_content {:google {:thought_signature "thought-sig-abc123"}}}]} |
| 273 | + assistant-tool-call-msg))))))) |
0 commit comments