@@ -255,3 +255,197 @@ def empty_callback() -> dict[str, str]:
255255
256256 assert result == {}
257257 assert calls == ['none' , 'empty' ]
258+
259+
260+ @pytest .mark .asyncio
261+ async def test_callback_adapts_keyword_args_to_positional_params ():
262+ """Callbacks with positional param names accept keyword arguments."""
263+
264+ def callback (ctx : str , resp : str ) -> str :
265+ return f'{ ctx } :{ resp } '
266+
267+ result = await _run_callbacks (
268+ [callback ],
269+ _stop_on_truthy ,
270+ callback_context = 'context_val' ,
271+ llm_response = 'response_val' ,
272+ )
273+
274+ assert result == 'context_val:response_val'
275+
276+
277+ @pytest .mark .asyncio
278+ async def test_async_callback_adapts_keyword_args_to_positional_params ():
279+ """Async callbacks with positional param names accept keyword arguments."""
280+
281+ async def callback (a : str , b : str ) -> str :
282+ return f'{ a } :{ b } '
283+
284+ result = await _run_callbacks (
285+ [callback ],
286+ _stop_on_truthy ,
287+ callback_context = 'arg_a' ,
288+ llm_response = 'arg_b' ,
289+ )
290+
291+ assert result == 'arg_a:arg_b'
292+
293+
294+ @pytest .mark .asyncio
295+ async def test_lambda_callback_adapts_keyword_args ():
296+ """Lambda callbacks with non-matching param names accept keyword arguments."""
297+ callback = lambda c , r : f'lambda_{ c } _{ r } '
298+
299+ result = await _run_callbacks (
300+ [callback ],
301+ _stop_on_truthy ,
302+ callback_context = 'c1' ,
303+ llm_response = 'r1' ,
304+ )
305+
306+ assert result == 'lambda_c1_r1'
307+
308+
309+ @pytest .mark .asyncio
310+ async def test_callback_with_body_type_error_is_not_masked ():
311+ """A TypeError raised inside the callback body is preserved."""
312+
313+ def callback (ctx : str , resp : str ) -> None :
314+ raise TypeError ('error inside callback body' )
315+
316+ with pytest .raises (TypeError , match = 'error inside callback body' ):
317+ await _run_callbacks (
318+ [callback ],
319+ _stop_on_truthy ,
320+ callback_context = 'ctx' ,
321+ llm_response = 'resp' ,
322+ )
323+
324+
325+ @pytest .mark .asyncio
326+ async def test_callback_with_reordered_kwargs_maintains_canonical_order ():
327+ """Reordering kwargs at call site still binds positional parameters in canonical order."""
328+ callback = lambda ctx , resp : f'{ ctx } :{ resp } '
329+
330+ result = await _run_callbacks (
331+ [callback ],
332+ _stop_on_truthy ,
333+ llm_response = 'resp_val' ,
334+ callback_context = 'ctx_val' ,
335+ )
336+
337+ assert result == 'ctx_val:resp_val'
338+
339+
340+ @pytest .mark .asyncio
341+ async def test_callback_with_callback_kwarg_does_not_collide ():
342+ """A kwarg named 'callback' does not collide with _callback parameter."""
343+ callback = lambda c , cb : f'{ c } :{ cb } '
344+
345+ result = await _run_callbacks (
346+ [callback ],
347+ _stop_on_truthy ,
348+ callback_context = 'ctx_val' ,
349+ callback = 'custom_callback' ,
350+ )
351+
352+ assert result == 'ctx_val:custom_callback'
353+
354+
355+ @pytest .mark .asyncio
356+ async def test_callback_with_incompatible_signature_raises_type_error ():
357+ """When both keyword and positional binding fail, original TypeError is raised."""
358+
359+ def callback (a : str , b : str , c : str , d : str ) -> None :
360+ pass
361+
362+ with pytest .raises (TypeError ):
363+ await _run_callbacks (
364+ [callback ],
365+ _stop_on_truthy ,
366+ callback_context = 'ctx' ,
367+ llm_response = 'resp' ,
368+ )
369+
370+
371+ @pytest .mark .asyncio
372+ async def test_before_tool_callback_with_reordered_kwargs_maintains_canonical_order ():
373+ """Tool callbacks receive (tool, args, tool_context) in canonical order even if kwargs are reordered."""
374+ callback = lambda t , a , tc : f'{ t } :{ a } :{ tc } '
375+
376+ result = await _run_callbacks (
377+ [callback ],
378+ _stop_on_non_none ,
379+ tool_context = 'ctx_val' ,
380+ args = 'args_val' ,
381+ tool = 'tool_val' ,
382+ )
383+
384+ assert result == 'tool_val:args_val:ctx_val'
385+
386+
387+ @pytest .mark .asyncio
388+ async def test_after_tool_callback_with_reordered_kwargs_maintains_canonical_order ():
389+ """After-tool callbacks receive (tool, args, tool_context, response) in canonical order."""
390+ callback = lambda t , a , tc , r : f'{ t } :{ a } :{ tc } :{ r } '
391+
392+ result = await _run_callbacks (
393+ [callback ],
394+ _stop_on_non_none ,
395+ response = 'resp_val' ,
396+ tool_context = 'ctx_val' ,
397+ args = 'args_val' ,
398+ tool = 'tool_val' ,
399+ )
400+
401+ assert result == 'tool_val:args_val:ctx_val:resp_val'
402+
403+
404+ @pytest .mark .asyncio
405+ async def test_after_tool_callback_with_tool_response_kwarg_maintains_canonical_order ():
406+ """After-tool callbacks with tool_response kwarg receive parameters in canonical order."""
407+ callback = lambda t , a , tc , tr : f'{ t } :{ a } :{ tc } :{ tr } '
408+
409+ result = await _run_callbacks (
410+ [callback ],
411+ _stop_on_non_none ,
412+ tool_response = 'tr_val' ,
413+ tool_context = 'ctx_val' ,
414+ args = 'args_val' ,
415+ tool = 'tool_val' ,
416+ )
417+
418+ assert result == 'tool_val:args_val:ctx_val:tr_val'
419+
420+
421+ @pytest .mark .asyncio
422+ async def test_on_tool_error_callback_with_reordered_kwargs_maintains_canonical_order ():
423+ """Tool error callbacks receive (tool, args, tool_context, error) in canonical order."""
424+ callback = lambda t , a , tc , err : f'{ t } :{ a } :{ tc } :{ err } '
425+
426+ result = await _run_callbacks (
427+ [callback ],
428+ _stop_on_non_none ,
429+ error = 'err_val' ,
430+ tool_context = 'ctx_val' ,
431+ args = 'args_val' ,
432+ tool = 'tool_val' ,
433+ )
434+
435+ assert result == 'tool_val:args_val:ctx_val:err_val'
436+
437+
438+ @pytest .mark .asyncio
439+ async def test_on_model_error_callback_with_reordered_kwargs_maintains_canonical_order ():
440+ """Model error callbacks receive (callback_context, llm_request, error) in canonical order."""
441+ callback = lambda ctx , req , err : f'{ ctx } :{ req } :{ err } '
442+
443+ result = await _run_callbacks (
444+ [callback ],
445+ _stop_on_truthy ,
446+ error = 'err_val' ,
447+ llm_request = 'req_val' ,
448+ callback_context = 'ctx_val' ,
449+ )
450+
451+ assert result == 'ctx_val:req_val:err_val'
0 commit comments