@@ -229,32 +229,32 @@ def create_agent( # noqa: PLR0915
229
229
# Setup tools
230
230
tool_node : ToolNode | None = None
231
231
if isinstance (tools , list ):
232
- # Extract builtin provider tools (dict format)
233
- builtin_tools = [t for t in tools if isinstance (t , dict )]
232
+ # Extract built-in provider tools (dict format) and regular tools (BaseTool )
233
+ built_in_tools = [t for t in tools if isinstance (t , dict )]
234
234
regular_tools = [t for t in tools if not isinstance (t , dict )]
235
235
236
- # Add structured output tools to regular tools
237
- structured_tools = [info .tool for info in structured_output_tools .values ()]
238
- all_tools = middleware_tools + regular_tools + structured_tools
236
+ # Tools that require client-side execution (must be in ToolNode)
237
+ available_tools = middleware_tools + regular_tools
239
238
240
- # Only create ToolNode if we have tools
241
- tool_node = ToolNode (tools = all_tools ) if all_tools else None
242
- default_tools = regular_tools + builtin_tools + structured_tools + middleware_tools
239
+ # Only create ToolNode if we have client-side tools
240
+ tool_node = ToolNode (tools = available_tools ) if available_tools else None
241
+
242
+ # Default tools for ModelRequest initialization
243
+ # Include built-ins and regular tools (can be changed dynamically by middleware)
244
+ # Structured tools are NOT included - they're added dynamically based on response_format
245
+ default_tools = regular_tools + middleware_tools + built_in_tools
243
246
elif isinstance (tools , ToolNode ):
244
- # tools is ToolNode or None
245
247
tool_node = tools
246
248
if tool_node :
247
- default_tools = list (tool_node .tools_by_name .values ()) + middleware_tools
248
- # Update tool node to know about tools provided by middleware
249
- all_tools = list (tool_node .tools_by_name .values ()) + middleware_tools
250
- tool_node = ToolNode (all_tools )
251
- # Add structured output tools
252
- for info in structured_output_tools .values ():
253
- default_tools .append (info .tool )
249
+ # Add middleware tools to existing ToolNode
250
+ available_tools = list (tool_node .tools_by_name .values ()) + middleware_tools
251
+ tool_node = ToolNode (available_tools )
252
+
253
+ # default_tools includes all client-side tools (no built-ins or structured tools)
254
+ default_tools = available_tools
254
255
else :
255
- default_tools = (
256
- list (structured_output_tools .values ()) if structured_output_tools else []
257
- ) + middleware_tools
256
+ # No tools provided, only middleware_tools available
257
+ default_tools = middleware_tools
258
258
259
259
# validate middleware
260
260
assert len ({m .name for m in middleware }) == len (middleware ), ( # noqa: S101
@@ -405,21 +405,32 @@ def _get_bound_model(request: ModelRequest) -> tuple[Runnable, ResponseFormat |
405
405
Tuple of (bound_model, effective_response_format) where ``effective_response_format``
406
406
is the actual strategy used (may differ from initial if auto-detected).
407
407
"""
408
- # Validate requested tools are available
409
- tools_by_name = {t .name : t for t in default_tools }
410
- unknown_tool_names = [t .name for t in request .tools if t .name not in tools_by_name ]
408
+ # Validate ONLY client-side tools that need to exist in tool_node
409
+ # Build map of available client-side tools (regular_tools + middleware_tools)
410
+ available_tools_by_name = {t .name : t for t in default_tools if isinstance (t , BaseTool )}
411
+
412
+ # Check if any requested tools are unknown CLIENT-SIDE tools
413
+ unknown_tool_names = []
414
+ for t in request .tools :
415
+ # Only validate BaseTool instances (skip built-in dict tools)
416
+ if isinstance (t , dict ):
417
+ continue
418
+ if t .name not in available_tools_by_name :
419
+ unknown_tool_names .append (t .name )
420
+
411
421
if unknown_tool_names :
412
- available_tools = sorted (tools_by_name .keys ())
422
+ available_tool_names = sorted (available_tools_by_name .keys ())
413
423
msg = (
414
424
f"Middleware returned unknown tool names: { unknown_tool_names } \n \n "
415
- f"Available tools: { available_tools } \n \n "
425
+ f"Available client-side tools: { available_tool_names } \n \n "
416
426
"To fix this issue:\n "
417
427
"1. Ensure the tools are passed to create_agent() via "
418
428
"the 'tools' parameter\n "
419
429
"2. If using custom middleware with tools, ensure "
420
430
"they're registered via middleware.tools attribute\n "
421
431
"3. Verify that tool names in ModelRequest.tools match "
422
- "the actual tool.name values"
432
+ "the actual tool.name values\n "
433
+ "Note: Built-in provider tools (dict format) can be added dynamically."
423
434
)
424
435
raise ValueError (msg )
425
436
@@ -437,32 +448,55 @@ def _get_bound_model(request: ModelRequest) -> tuple[Runnable, ResponseFormat |
437
448
# User explicitly specified a strategy - preserve it
438
449
effective_response_format = request .response_format
439
450
451
+ # Build final tools list including structured output tools
452
+ # request.tools already contains both BaseTool and dict (built-in) tools
453
+ final_tools = list (request .tools )
454
+ if isinstance (effective_response_format , ToolStrategy ):
455
+ # Add structured output tools to final tools list
456
+ structured_tools = [info .tool for info in structured_output_tools .values ()]
457
+ final_tools .extend (structured_tools )
458
+
440
459
# Bind model based on effective response format
441
460
if isinstance (effective_response_format , ProviderStrategy ):
442
461
# Use provider-specific structured output
443
462
kwargs = effective_response_format .to_model_kwargs ()
444
463
return (
445
464
request .model .bind_tools (
446
- request . tools , strict = True , ** kwargs , ** request .model_settings
465
+ final_tools , strict = True , ** kwargs , ** request .model_settings
447
466
),
448
467
effective_response_format ,
449
468
)
450
469
451
470
if isinstance (effective_response_format , ToolStrategy ):
471
+ # Current implementation requires that tools used for structured output
472
+ # have to be declared upfront when creating the agent as part of the
473
+ # response format. Middleware is allowed to change the response format
474
+ # to a subset of the original structured tools when using ToolStrategy,
475
+ # but not to add new structured tools that weren't declared upfront.
476
+ # Compute output binding
477
+ for tc in effective_response_format .schema_specs :
478
+ if tc .name not in structured_output_tools :
479
+ msg = (
480
+ f"ToolStrategy specifies tool '{ tc .name } ' "
481
+ "which wasn't declared in the original "
482
+ "response format when creating the agent."
483
+ )
484
+ raise ValueError (msg )
485
+
452
486
# Force tool use if we have structured output tools
453
487
tool_choice = "any" if structured_output_tools else request .tool_choice
454
488
return (
455
489
request .model .bind_tools (
456
- request . tools , tool_choice = tool_choice , ** request .model_settings
490
+ final_tools , tool_choice = tool_choice , ** request .model_settings
457
491
),
458
492
effective_response_format ,
459
493
)
460
494
461
495
# No structured output - standard model binding
462
- if request . tools :
496
+ if final_tools :
463
497
return (
464
498
request .model .bind_tools (
465
- request . tools , tool_choice = request .tool_choice , ** request .model_settings
499
+ final_tools , tool_choice = request .tool_choice , ** request .model_settings
466
500
),
467
501
None ,
468
502
)
0 commit comments