blob: 32e6241fec05f573a6e2486117c3f225e94261f7 [file] [log] [blame]
Earl Lee2e463fb2025-04-17 11:22:22 -07001//go:build goexperiment.synctest
2
3package loop
4
5import (
6 "context"
Sean McCullough3871a092025-05-05 21:54:56 +00007 "encoding/json"
Earl Lee2e463fb2025-04-17 11:22:22 -07008 "fmt"
Earl Lee2e463fb2025-04-17 11:22:22 -07009 "testing"
10 "testing/synctest"
11
Sean McCullough3871a092025-05-05 21:54:56 +000012 "sketch.dev/llm"
13 "sketch.dev/llm/conversation"
Earl Lee2e463fb2025-04-17 11:22:22 -070014)
15
16func TestLoop_OneTurn_Basic(t *testing.T) {
17 synctest.Run(func() {
18 mockConvo := NewMockConvo(t)
19
20 agent := &Agent{
Sean McCullough3871a092025-05-05 21:54:56 +000021 convo: mockConvo,
22 inbox: make(chan string, 1),
Earl Lee2e463fb2025-04-17 11:22:22 -070023 }
Sean McCullough3871a092025-05-05 21:54:56 +000024 agent.stateMachine = NewStateMachine()
25 userMsg := llm.UserStringMessage("hi")
26 userMsgResponse := &llm.Response{}
Earl Lee2e463fb2025-04-17 11:22:22 -070027 mockConvo.ExpectCall("SendMessage", userMsg).Return(userMsgResponse, nil)
28
29 ctx, cancel := context.WithCancel(context.Background())
30 defer cancel()
31
32 go agent.Loop(ctx)
33
34 agent.UserMessage(ctx, "hi")
35
36 // This makes sure the SendMessage call happens before we assert the expectations.
37 synctest.Wait()
38
39 // Verify results
40 mockConvo.AssertExpectations(t)
41 })
42}
43
44func TestLoop_ToolCall_Basic(t *testing.T) {
45 synctest.Run(func() {
46 mockConvo := NewMockConvo(t)
47
48 agent := &Agent{
Sean McCullough3871a092025-05-05 21:54:56 +000049 convo: mockConvo,
50 inbox: make(chan string, 1),
Earl Lee2e463fb2025-04-17 11:22:22 -070051 }
Sean McCullough3871a092025-05-05 21:54:56 +000052 agent.stateMachine = NewStateMachine()
53 userMsg := llm.Message{
54 Role: llm.MessageRoleUser,
55 Content: []llm.Content{
56 {Type: llm.ContentTypeText, Text: "hi"},
Earl Lee2e463fb2025-04-17 11:22:22 -070057 },
58 }
Sean McCullough3871a092025-05-05 21:54:56 +000059 userMsgResponse := &llm.Response{
60 StopReason: llm.StopReasonToolUse,
61 Content: []llm.Content{
Earl Lee2e463fb2025-04-17 11:22:22 -070062 {
Sean McCullough3871a092025-05-05 21:54:56 +000063 Type: llm.ContentTypeToolUse,
Earl Lee2e463fb2025-04-17 11:22:22 -070064 ID: "tool1",
65 ToolName: "test_tool",
66 ToolInput: []byte(`{"param":"value"}`),
67 },
68 },
Sean McCullough3871a092025-05-05 21:54:56 +000069 Usage: llm.Usage{
Earl Lee2e463fb2025-04-17 11:22:22 -070070 InputTokens: 100,
71 OutputTokens: 200,
72 },
73 }
74
Sean McCullough3871a092025-05-05 21:54:56 +000075 toolUseContents := []llm.Content{
Earl Lee2e463fb2025-04-17 11:22:22 -070076 {
Philip Zeyliger72252cb2025-05-10 17:00:08 -070077 Type: llm.ContentTypeToolResult,
78 ToolUseID: "tool1",
79 Text: "",
80 ToolResult: []llm.Content{{
81 Type: llm.ContentTypeText,
82 Text: "This is a tool result",
83 }},
84 ToolError: false,
Earl Lee2e463fb2025-04-17 11:22:22 -070085 },
86 }
Sean McCullough3871a092025-05-05 21:54:56 +000087 toolUseResultsMsg := llm.Message{
88 Role: llm.MessageRoleUser,
Earl Lee2e463fb2025-04-17 11:22:22 -070089 Content: toolUseContents,
90 }
Sean McCullough3871a092025-05-05 21:54:56 +000091 toolUseResponse := &llm.Response{
92 StopReason: llm.StopReasonEndTurn,
93 Content: []llm.Content{
Earl Lee2e463fb2025-04-17 11:22:22 -070094 {
Sean McCullough3871a092025-05-05 21:54:56 +000095 Type: llm.ContentTypeText,
Earl Lee2e463fb2025-04-17 11:22:22 -070096 Text: "tool_use contents accepted",
97 },
98 },
Sean McCullough3871a092025-05-05 21:54:56 +000099 Usage: llm.Usage{
Earl Lee2e463fb2025-04-17 11:22:22 -0700100 InputTokens: 50,
101 OutputTokens: 75,
102 },
103 }
104
105 // Set up the mock response for tool results
106 mockConvo.ExpectCall("SendMessage", userMsg).Return(userMsgResponse, nil)
Josh Bleecher Snyder64f2aa82025-05-14 18:31:05 +0000107 mockConvo.ExpectCall("ToolResultContents", userMsgResponse).Return(toolUseContents, false, nil)
Earl Lee2e463fb2025-04-17 11:22:22 -0700108 mockConvo.ExpectCall("SendMessage", toolUseResultsMsg).Return(toolUseResponse, nil)
109
110 ctx, cancel := context.WithCancel(context.Background())
111 defer cancel()
112
113 go agent.Loop(ctx)
114
115 agent.UserMessage(ctx, "hi")
116
117 // This makes sure the SendMessage call happens before we assert the expectations.
118 synctest.Wait()
119
120 // Verify results
121 mockConvo.AssertExpectations(t)
122 })
123}
124
125func TestLoop_ToolCall_UserCancelsDuringToolResultContents(t *testing.T) {
126 synctest.Run(func() {
127 mockConvo := NewMockConvo(t)
128
129 agent := &Agent{
Sean McCullough3871a092025-05-05 21:54:56 +0000130 convo: mockConvo,
131 inbox: make(chan string, 1),
Earl Lee2e463fb2025-04-17 11:22:22 -0700132 }
Sean McCullough3871a092025-05-05 21:54:56 +0000133 agent.stateMachine = NewStateMachine()
134 userMsg := llm.UserStringMessage("hi")
135 userMsgResponse := &llm.Response{
136 StopReason: llm.StopReasonToolUse,
137 Content: []llm.Content{
Earl Lee2e463fb2025-04-17 11:22:22 -0700138 {
Sean McCullough3871a092025-05-05 21:54:56 +0000139 Type: llm.ContentTypeToolUse,
Earl Lee2e463fb2025-04-17 11:22:22 -0700140 ID: "tool1",
141 ToolName: "test_tool",
142 ToolInput: []byte(`{"param":"value"}`),
143 },
144 },
Sean McCullough3871a092025-05-05 21:54:56 +0000145 Usage: llm.Usage{
Earl Lee2e463fb2025-04-17 11:22:22 -0700146 InputTokens: 100,
147 OutputTokens: 200,
148 },
149 }
Sean McCullough3871a092025-05-05 21:54:56 +0000150 toolUseResultsMsg := llm.UserStringMessage(cancelToolUseMessage)
151 toolUseResponse := &llm.Response{
152 StopReason: llm.StopReasonEndTurn,
153 Content: []llm.Content{
Earl Lee2e463fb2025-04-17 11:22:22 -0700154 {
Sean McCullough3871a092025-05-05 21:54:56 +0000155 Type: llm.ContentTypeText,
Earl Lee2e463fb2025-04-17 11:22:22 -0700156 Text: "tool_use contents accepted",
157 },
158 },
Sean McCullough3871a092025-05-05 21:54:56 +0000159 Usage: llm.Usage{
Earl Lee2e463fb2025-04-17 11:22:22 -0700160 InputTokens: 50,
161 OutputTokens: 75,
162 },
163 }
164
165 // Set up the mock response for tool results
166
167 userCancelError := fmt.Errorf("user canceled")
168 // This allows the test to block the InnerLoop goroutine that invokes ToolResultsContents so
169 // we can force its context to cancel while it's blocked.
170 waitForToolResultContents := make(chan any, 1)
171
172 mockConvo.ExpectCall("SendMessage", userMsg).Return(userMsgResponse, nil)
173 mockConvo.ExpectCall("ToolResultContents",
Sean McCullough3871a092025-05-05 21:54:56 +0000174 userMsgResponse).BlockAndReturn(waitForToolResultContents, []llm.Content{}, userCancelError)
Earl Lee2e463fb2025-04-17 11:22:22 -0700175 mockConvo.ExpectCall("SendMessage", toolUseResultsMsg).Return(toolUseResponse, nil)
176
177 ctx, cancel := context.WithCancel(context.Background())
178 defer cancel()
179
180 go agent.Loop(ctx)
181
182 // This puts one message into agent.inbox, which should un-block the GatherMessages call
183 // at the top of agent.InnerLoop.
184 agent.UserMessage(ctx, "hi")
185
186 // This makes sure the first SendMessage call happens before we proceed with the cancel.
187 synctest.Wait()
188
189 // The goroutine executing ToolResultContents call should be blocked, simulating a long
190 // running operation that the user wishes to cancel while it's still in progress.
191 // This call invokes that InnerLoop context's cancel() func.
Sean McCullough3871a092025-05-05 21:54:56 +0000192 agent.CancelTurn(userCancelError)
Earl Lee2e463fb2025-04-17 11:22:22 -0700193
194 // This tells the goroutine that's in mockConvo.ToolResultContents to proceed.
195 waitForToolResultContents <- nil
196
197 // This makes sure the final SendMessage call happens before we assert the expectations.
198 synctest.Wait()
199
200 // Verify results
201 mockConvo.AssertExpectations(t)
202 })
203}
204
205func TestLoop_ToolCall_UserCancelsDuringToolResultContents_AndContinuesToChat(t *testing.T) {
206 synctest.Run(func() {
207 mockConvo := NewMockConvo(t)
208
209 agent := &Agent{
Sean McCullough3871a092025-05-05 21:54:56 +0000210 convo: mockConvo,
211 inbox: make(chan string, 1),
Earl Lee2e463fb2025-04-17 11:22:22 -0700212 }
Sean McCullough3871a092025-05-05 21:54:56 +0000213 agent.stateMachine = NewStateMachine()
214 userMsg := llm.Message{
215 Role: llm.MessageRoleUser,
216 Content: []llm.Content{
217 {Type: llm.ContentTypeText, Text: "hi"},
Earl Lee2e463fb2025-04-17 11:22:22 -0700218 },
219 }
Sean McCullough3871a092025-05-05 21:54:56 +0000220 userMsgResponse := &llm.Response{
221 StopReason: llm.StopReasonToolUse,
222 Content: []llm.Content{
Earl Lee2e463fb2025-04-17 11:22:22 -0700223 {
Sean McCullough3871a092025-05-05 21:54:56 +0000224 Type: llm.ContentTypeToolUse,
Earl Lee2e463fb2025-04-17 11:22:22 -0700225 ID: "tool1",
226 ToolName: "test_tool",
227 ToolInput: []byte(`{"param":"value"}`),
228 },
229 },
Sean McCullough3871a092025-05-05 21:54:56 +0000230 Usage: llm.Usage{
Earl Lee2e463fb2025-04-17 11:22:22 -0700231 InputTokens: 100,
232 OutputTokens: 200,
233 },
234 }
Sean McCullough3871a092025-05-05 21:54:56 +0000235 toolUseResultsMsg := llm.Message{
236 Role: llm.MessageRoleUser,
237 Content: []llm.Content{
238 {Type: llm.ContentTypeText, Text: cancelToolUseMessage},
Earl Lee2e463fb2025-04-17 11:22:22 -0700239 },
240 }
Sean McCullough3871a092025-05-05 21:54:56 +0000241 toolUseResultResponse := &llm.Response{
242 StopReason: llm.StopReasonEndTurn,
243 Content: []llm.Content{
Earl Lee2e463fb2025-04-17 11:22:22 -0700244 {
Sean McCullough3871a092025-05-05 21:54:56 +0000245 Type: llm.ContentTypeText,
Earl Lee2e463fb2025-04-17 11:22:22 -0700246 Text: "awaiting further instructions",
247 },
248 },
Sean McCullough3871a092025-05-05 21:54:56 +0000249 Usage: llm.Usage{
Earl Lee2e463fb2025-04-17 11:22:22 -0700250 InputTokens: 50,
251 OutputTokens: 75,
252 },
253 }
Sean McCullough3871a092025-05-05 21:54:56 +0000254 userFollowUpMsg := llm.Message{
255 Role: llm.MessageRoleUser,
256 Content: []llm.Content{
257 {Type: llm.ContentTypeText, Text: "that was the wrong thing to do"},
Earl Lee2e463fb2025-04-17 11:22:22 -0700258 },
259 }
Sean McCullough3871a092025-05-05 21:54:56 +0000260 userFollowUpResponse := &llm.Response{
261 StopReason: llm.StopReasonEndTurn,
262 Content: []llm.Content{
Earl Lee2e463fb2025-04-17 11:22:22 -0700263 {
Sean McCullough3871a092025-05-05 21:54:56 +0000264 Type: llm.ContentTypeText,
Earl Lee2e463fb2025-04-17 11:22:22 -0700265 Text: "sorry about that",
266 },
267 },
Sean McCullough3871a092025-05-05 21:54:56 +0000268 Usage: llm.Usage{
Earl Lee2e463fb2025-04-17 11:22:22 -0700269 InputTokens: 100,
270 OutputTokens: 200,
271 },
272 }
273 // Set up the mock response for tool results
274
275 userCancelError := fmt.Errorf("user canceled")
276 // This allows the test to block the InnerLoop goroutine that invokes ToolResultsContents so
277 // we can force its context to cancel while it's blocked.
278 waitForToolResultContents := make(chan any, 1)
279
280 mockConvo.ExpectCall("SendMessage", userMsg).Return(userMsgResponse, nil)
281 mockConvo.ExpectCall("ToolResultContents",
Sean McCullough3871a092025-05-05 21:54:56 +0000282 userMsgResponse).BlockAndReturn(waitForToolResultContents, []llm.Content{}, userCancelError)
Earl Lee2e463fb2025-04-17 11:22:22 -0700283 mockConvo.ExpectCall("SendMessage", toolUseResultsMsg).Return(toolUseResultResponse, nil)
284
285 mockConvo.ExpectCall("SendMessage", userFollowUpMsg).Return(userFollowUpResponse, nil)
286
287 ctx, cancel := context.WithCancel(context.Background())
288 defer cancel()
289
290 go agent.Loop(ctx)
291
292 // This puts one message into agent.inbox, which should un-block the GatherMessages call
293 // at the top of agent.InnerLoop.
294 agent.UserMessage(ctx, "hi")
295
296 // This makes sure the first SendMessage call happens before we proceed with the cancel.
297 synctest.Wait()
298
299 // The goroutine executing ToolResultContents call should be blocked, simulating a long
300 // running operation that the user wishes to cancel while it's still in progress.
301 // This call invokes that InnerLoop context's cancel() func.
Sean McCullough3871a092025-05-05 21:54:56 +0000302 agent.CancelTurn(userCancelError)
Earl Lee2e463fb2025-04-17 11:22:22 -0700303
304 // This tells the goroutine that's in mockConvo.ToolResultContents to proceed.
305 waitForToolResultContents <- nil
306
307 // Allow InnerLoop to handle the cancellation logic before continuing the conversation.
308 synctest.Wait()
309
310 agent.UserMessage(ctx, "that was the wrong thing to do")
311
312 synctest.Wait()
313
314 // Verify results
315 mockConvo.AssertExpectations(t)
316 })
317}
318
Sean McCullough3871a092025-05-05 21:54:56 +0000319func TestProcessTurn_UserCancels(t *testing.T) {
Earl Lee2e463fb2025-04-17 11:22:22 -0700320 synctest.Run(func() {
321 mockConvo := NewMockConvo(t)
322
323 agent := &Agent{
Sean McCullough3871a092025-05-05 21:54:56 +0000324 convo: mockConvo,
325 inbox: make(chan string, 1),
Earl Lee2e463fb2025-04-17 11:22:22 -0700326 }
Sean McCullough3871a092025-05-05 21:54:56 +0000327 agent.stateMachine = NewStateMachine()
Earl Lee2e463fb2025-04-17 11:22:22 -0700328
329 // Define test message
330 // This simulates something that would result in claude responding with tool_use responses.
Sean McCullough3871a092025-05-05 21:54:56 +0000331 userMsg := llm.UserStringMessage("use test_tool for something")
Earl Lee2e463fb2025-04-17 11:22:22 -0700332 // Mock initial response with tool use
Sean McCullough3871a092025-05-05 21:54:56 +0000333 userMsgResponse := &llm.Response{
334 StopReason: llm.StopReasonToolUse,
335 Content: []llm.Content{
Earl Lee2e463fb2025-04-17 11:22:22 -0700336 {
Sean McCullough3871a092025-05-05 21:54:56 +0000337 Type: llm.ContentTypeToolUse,
Earl Lee2e463fb2025-04-17 11:22:22 -0700338 ID: "tool1",
339 ToolName: "test_tool",
340 ToolInput: []byte(`{"param":"value"}`),
341 },
342 },
Sean McCullough3871a092025-05-05 21:54:56 +0000343 Usage: llm.Usage{
Earl Lee2e463fb2025-04-17 11:22:22 -0700344 InputTokens: 100,
345 OutputTokens: 200,
346 },
347 }
Sean McCullough3871a092025-05-05 21:54:56 +0000348 canceledToolUseContents := []llm.Content{
Earl Lee2e463fb2025-04-17 11:22:22 -0700349 {
Philip Zeyliger72252cb2025-05-10 17:00:08 -0700350 Type: llm.ContentTypeToolResult,
351 ToolUseID: "tool1",
352 ToolError: true,
353 ToolResult: []llm.Content{{
354 Type: llm.ContentTypeText,
355 Text: "user canceled this tool_use",
356 }},
Earl Lee2e463fb2025-04-17 11:22:22 -0700357 },
358 }
Sean McCullough3871a092025-05-05 21:54:56 +0000359 canceledToolUseMsg := llm.Message{
360 Role: llm.MessageRoleUser,
361 Content: append(canceledToolUseContents, llm.StringContent(cancelToolUseMessage)),
Earl Lee2e463fb2025-04-17 11:22:22 -0700362 }
363 // Set up expected behaviors
364 waitForSendMessage := make(chan any)
365 mockConvo.ExpectCall("SendMessage", userMsg).BlockAndReturn(waitForSendMessage, userMsgResponse, nil)
366
367 mockConvo.ExpectCall("ToolResultCancelContents", userMsgResponse).Return(canceledToolUseContents, nil)
368 mockConvo.ExpectCall("SendMessage", canceledToolUseMsg).Return(
Sean McCullough3871a092025-05-05 21:54:56 +0000369 &llm.Response{
370 StopReason: llm.StopReasonToolUse,
Earl Lee2e463fb2025-04-17 11:22:22 -0700371 }, nil)
372
373 ctx, cancel := context.WithCancelCause(context.Background())
374
Sean McCullough3871a092025-05-05 21:54:56 +0000375 // Run one iteration of the processing loop
376 go agent.processTurn(ctx)
Earl Lee2e463fb2025-04-17 11:22:22 -0700377
378 // Send a message to the agent's inbox
379 agent.UserMessage(ctx, "use test_tool for something")
380
381 synctest.Wait()
382
383 // cancel the context before we even call InnerLoop with it, so it will
384 // be .Done() the first time it checks.
385 cancel(fmt.Errorf("user canceled"))
386
387 // unblock the InnerLoop goroutine's SendMessage call
388 waitForSendMessage <- nil
389
390 synctest.Wait()
391
392 // Verify results
393 mockConvo.AssertExpectations(t)
Earl Lee2e463fb2025-04-17 11:22:22 -0700394 })
395}
396
Sean McCullough3871a092025-05-05 21:54:56 +0000397func TestProcessTurn_UserDoesNotCancel(t *testing.T) {
Earl Lee2e463fb2025-04-17 11:22:22 -0700398 mockConvo := NewMockConvo(t)
399
400 agent := &Agent{
Sean McCullough3871a092025-05-05 21:54:56 +0000401 convo: mockConvo,
402 inbox: make(chan string, 100),
Earl Lee2e463fb2025-04-17 11:22:22 -0700403 }
Sean McCullough3871a092025-05-05 21:54:56 +0000404 agent.stateMachine = NewStateMachine()
Earl Lee2e463fb2025-04-17 11:22:22 -0700405
406 // Define test message
407 // This simulates something that would result in claude
408 // responding with tool_use responses.
409 testMsg := "use test_tool for something"
410
411 // Mock initial response with tool use
Sean McCullough3871a092025-05-05 21:54:56 +0000412 initialResponse := &llm.Response{
413 StopReason: llm.StopReasonToolUse,
414 Content: []llm.Content{
Earl Lee2e463fb2025-04-17 11:22:22 -0700415 {
Sean McCullough3871a092025-05-05 21:54:56 +0000416 Type: llm.ContentTypeToolUse,
Earl Lee2e463fb2025-04-17 11:22:22 -0700417 ID: "tool1",
418 ToolName: "test_tool",
419 ToolInput: []byte(`{"param":"value"}`),
420 },
421 },
Sean McCullough3871a092025-05-05 21:54:56 +0000422 Usage: llm.Usage{
Earl Lee2e463fb2025-04-17 11:22:22 -0700423 InputTokens: 100,
424 OutputTokens: 200,
425 },
426 }
427
428 // Set up expected behaviors
429 mockConvo.ExpectCall("SendMessage", nil).Return(initialResponse, nil)
430
Sean McCullough3871a092025-05-05 21:54:56 +0000431 toolUseContents := []llm.Content{
Earl Lee2e463fb2025-04-17 11:22:22 -0700432 {
Philip Zeyliger72252cb2025-05-10 17:00:08 -0700433 Type: llm.ContentTypeToolResult,
434 ToolUseID: "tool1",
435 Text: "",
436 ToolResult: []llm.Content{{
437 Type: llm.ContentTypeText,
438 Text: "This is a tool result",
439 }},
440 ToolError: false,
Earl Lee2e463fb2025-04-17 11:22:22 -0700441 },
442 }
Sean McCullough3871a092025-05-05 21:54:56 +0000443 toolUseResponse := &llm.Response{
444 // StopReason: llm.StopReasonEndTurn,
445 Content: []llm.Content{
Earl Lee2e463fb2025-04-17 11:22:22 -0700446 {
Sean McCullough3871a092025-05-05 21:54:56 +0000447 Type: llm.ContentTypeText,
Earl Lee2e463fb2025-04-17 11:22:22 -0700448 Text: "tool_use contents accepted",
449 },
450 },
Sean McCullough3871a092025-05-05 21:54:56 +0000451 Usage: llm.Usage{
Earl Lee2e463fb2025-04-17 11:22:22 -0700452 InputTokens: 50,
453 OutputTokens: 75,
454 },
455 }
456
457 ctx, cancel := context.WithCancel(context.Background())
458 defer cancel()
459
460 // Setting up the mock response for tool results
Josh Bleecher Snyder64f2aa82025-05-14 18:31:05 +0000461 mockConvo.ExpectCall("ToolResultContents", initialResponse).Return(toolUseContents, false, nil)
Earl Lee2e463fb2025-04-17 11:22:22 -0700462 mockConvo.ExpectCall("SendMessage", nil).Return(toolUseResponse, nil)
Sean McCullough3871a092025-05-05 21:54:56 +0000463 // mockConvo, as a mock, isn't able to run the loop in conversation.Convo that makes this agent.OnToolResult callback.
Earl Lee2e463fb2025-04-17 11:22:22 -0700464 // So we "mock" it out here by calling it explicitly, in order to make sure it calls .pushToOutbox with this message.
465 // This is not a good situation.
Sean McCullough3871a092025-05-05 21:54:56 +0000466 // conversation.Convo and loop.Agent seem to be excessively coupled, and aware of each others' internal details.
467 // TODO: refactor (or clarify in docs somewhere) the boundary between what conversation.Convo is responsible
Earl Lee2e463fb2025-04-17 11:22:22 -0700468 // for vs what loop.Agent is responsible for.
Sean McCullough3871a092025-05-05 21:54:56 +0000469 antConvo := &conversation.Convo{}
Earl Lee2e463fb2025-04-17 11:22:22 -0700470 res := ""
Sean McCullough3871a092025-05-05 21:54:56 +0000471 agent.OnToolResult(ctx, antConvo, "tool1", "test_tool", json.RawMessage(`{"param":"value"}`), toolUseContents[0], &res, nil)
Earl Lee2e463fb2025-04-17 11:22:22 -0700472
473 // Send a message to the agent's inbox
474 agent.UserMessage(ctx, testMsg)
475
Sean McCullough3871a092025-05-05 21:54:56 +0000476 // Run one iteration of the processing loop
477 agent.processTurn(ctx)
Earl Lee2e463fb2025-04-17 11:22:22 -0700478
479 // Verify results
480 mockConvo.AssertExpectations(t)
Earl Lee2e463fb2025-04-17 11:22:22 -0700481}