3535# Use the typing_extensions backport so this works on 3.12 too.
3636from typing_extensions import TypeVar
3737
38- from .. import models , types , util
38+ from .. import models , type_utils , types , util
3939from ..types import builders
4040from ..types import events as events_
4141from ..types .messages import MessageBundle
@@ -351,6 +351,67 @@ async def render(prompt: str) -> StreamingTextTool:
351351"""
352352
353353
354+ def _return_type_from_callable (fn : Callable [..., Any ]) -> Any :
355+ try :
356+ return get_type_hints (fn , include_extras = True ).get ("return" )
357+ except Exception :
358+ return None
359+
360+
361+ def _stream_item_type (return_type : Any ) -> Any :
362+ resolved = type_utils .resolve_type_alias (return_type )
363+ if typing .get_origin (resolved ) is typing .Annotated :
364+ resolved = typing .get_args (resolved )[0 ]
365+
366+ if typing .get_origin (resolved ) is not AsyncGenerator :
367+ return None
368+
369+ args = typing .get_args (resolved )
370+ return args [0 ] if args else Any
371+
372+
373+ def _aggregator_result_type (
374+ aggregator : Callable [[], events_ .Aggregator [Any , Any , Any ]] | None ,
375+ stream_item_type : Any ,
376+ ) -> Any :
377+ agg_cls = _aggregator_cls (aggregator )
378+ if agg_cls is None :
379+ return None
380+
381+ args = type_utils .generic_base_args (agg_cls , events_ .Aggregator )
382+ if args is None :
383+ return None
384+
385+ item_type , result_type , _model_input_type = args
386+ bindings = type_utils .bind_typevars (item_type , stream_item_type )
387+ return type_utils .replace_typevars (result_type , bindings )
388+
389+
390+ def _tool_result_type_from_return_annotation (
391+ return_type : Any ,
392+ aggregator : Callable [[], events_ .Aggregator [Any , Any , Any ]] | None ,
393+ ) -> Any :
394+ """Return the type used to validate ``ToolResultPart.result``.
395+
396+ Normal tools store the function's return value directly, so the function's
397+ return annotation is the result type.
398+
399+ Async-generator tools store ``aggregator.snapshot()`` instead. For those,
400+ read the ``Result`` parameter from ``Aggregator[Item, Result, ModelInput]``
401+ on the configured aggregator class, binding generic aggregators from the
402+ stream item type when possible (for example ``StreamingStatusTool[str]`` +
403+ ``LastAggregator[T]`` becomes ``str | None``).
404+ """
405+ if return_type is None :
406+ return None
407+
408+ item_type = _stream_item_type (return_type )
409+ if item_type is not None :
410+ return _aggregator_result_type (aggregator , item_type )
411+
412+ return return_type
413+
414+
354415def _aggregate_from_return_type (fn : Callable [..., Any ]) -> Aggregate | None :
355416 """Find an ``Aggregate`` marker in *fn*'s return-type metadata, if any.
356417
@@ -360,11 +421,7 @@ def _aggregate_from_return_type(fn: Callable[..., Any]) -> Aggregate | None:
360421 * a PEP 695 alias ``type Foo = Annotated[X, Aggregate(...)]``,
361422 * a parameterized alias ``type Foo[T] = Annotated[X[T], Aggregate(...)]``.
362423 """
363- try :
364- hints = get_type_hints (fn , include_extras = True )
365- except Exception :
366- return None
367- ret = hints .get ("return" )
424+ ret = _return_type_from_callable (fn )
368425 if ret is None :
369426 return None
370427
@@ -398,6 +455,7 @@ class AgentTool:
398455 validator : type [pydantic .BaseModel ] | None = None
399456 is_gen : bool = False
400457 aggregator : Callable [[], events_ .Aggregator [Any , Any , Any ]] | None = None
458+ return_type : Any = None
401459
402460 @property
403461 def name (self ) -> str :
@@ -435,7 +493,17 @@ def tool[**P, T](fn: Callable[P, AsyncGenerator[T]], /) -> AgentTool: ...
435493
436494@overload
437495def tool [** P ](
438- * , require_approval : bool
496+ * ,
497+ require_approval : bool ,
498+ return_type : Any = None ,
499+ ) -> Callable [[Callable [P , Any ]], AgentTool ]: ...
500+
501+
502+ @overload
503+ def tool [** P ](
504+ * ,
505+ return_type : Any ,
506+ require_approval : bool = False ,
439507) -> Callable [[Callable [P , Any ]], AgentTool ]: ...
440508
441509
@@ -444,6 +512,7 @@ def tool[**P](
444512 * ,
445513 aggregator : Callable [[], events_ .Aggregator [Any , Any , Any ]],
446514 require_approval : bool = False ,
515+ return_type : Any = None ,
447516) -> Callable [[Callable [P , AsyncGenerator [Any ]]], AgentTool ]: ...
448517
449518
@@ -455,6 +524,7 @@ def tool[**P, T, R](
455524 * ,
456525 aggregator : Callable [[], events_ .Aggregator [Any , Any , Any ]] | None = None ,
457526 require_approval : bool = False ,
527+ return_type : Any = None ,
458528) -> (
459529 Callable [[Callable [P , AsyncGenerator [Any ]]], AgentTool ]
460530 | Callable [[Callable [P , Awaitable [R ]]], AgentTool ]
@@ -466,7 +536,8 @@ def tool[**P, T, R](
466536 ``aggregator=`` keyword argument or by annotating the return type
467537 with an :class:`Aggregate` marker (e.g. via the :data:`SubAgentTool`
468538 or :data:`StreamingStatusTool` aliases). Specifying both raises
469- ``TypeError``.
539+ ``TypeError``. Pass ``return_type=`` to override the type used when
540+ validating round-tripped tool results.
470541 """
471542
472543 def wrap (fn : Any ) -> AgentTool :
@@ -483,6 +554,7 @@ def wrap(fn: Any) -> AgentTool:
483554
484555 validator = pydantic .create_model (f"{ fn .__name__ } _Args" , ** fields )
485556
557+ annotated_return_type = _return_type_from_callable (fn )
486558 annotated_aggregate = _aggregate_from_return_type (fn )
487559 if annotated_aggregate is not None and aggregator is not None :
488560 raise TypeError (
@@ -508,6 +580,10 @@ def wrap(fn: Any) -> AgentTool:
508580 validator = validator ,
509581 is_gen = inspect .isasyncgenfunction (fn ),
510582 aggregator = effective_aggregator ,
583+ return_type = return_type
584+ or _tool_result_type_from_return_annotation (
585+ annotated_return_type , effective_aggregator
586+ ),
511587 )
512588
513589 if fn is None :
0 commit comments