diff --git a/python/packages/core/agent_framework/_workflows/_executor.py b/python/packages/core/agent_framework/_workflows/_executor.py index d2bb2ac598..8b9369d675 100644 --- a/python/packages/core/agent_framework/_workflows/_executor.py +++ b/python/packages/core/agent_framework/_workflows/_executor.py @@ -622,6 +622,20 @@ def decorator( resolve_type_annotation(workflow_output, func.__globals__) if workflow_output is not None else None ) + # Check for unresolved TypeVars in explicit type parameters + for param_name, param_type in [ + ("input", resolved_input_type), + ("output", resolved_output_type), + ("workflow_output", resolved_workflow_output_type), + ]: + if param_type is not None and isinstance(param_type, TypeVar): + raise ValueError( + f"Handler '{func.__name__}' has an unresolved TypeVar '{param_type}' " + f"as its {param_name} type. " + f"Use @handler(input=ConcreteType, output=ConcreteType) with concrete types " + f"for parameterized executors." + ) + # Validate signature structure (correct number of params, ctx is WorkflowContext) # but skip type extraction since we're using explicit types _validate_handler_signature(func, skip_message_annotation=True) @@ -652,6 +666,15 @@ def decorator( "or explicit type parameters (input, output, workflow_output)" ) + # Check for unresolved TypeVar in introspected message type + if isinstance(message_type, TypeVar): + raise ValueError( + f"Handler '{func.__name__}' has an unresolved TypeVar '{message_type}' " + f"as its message type. " + f"Use @handler(input=ConcreteType, output=ConcreteType) with concrete types " + f"for parameterized executors." + ) + final_output_types = inferred_output_types final_workflow_output_types = inferred_workflow_output_types diff --git a/python/packages/core/agent_framework/_workflows/_function_executor.py b/python/packages/core/agent_framework/_workflows/_function_executor.py index 326145b6c4..d5058e2138 100644 --- a/python/packages/core/agent_framework/_workflows/_function_executor.py +++ b/python/packages/core/agent_framework/_workflows/_function_executor.py @@ -21,7 +21,7 @@ import types import typing from collections.abc import Awaitable, Callable -from typing import Any +from typing import Any, TypeVar from ._executor import Executor from ._typing_utils import normalize_type_to_list, resolve_type_annotation @@ -94,6 +94,19 @@ def __init__( _validate_function_signature(func, skip_message_annotation=resolved_input_type is not None) ) + # Check for unresolved TypeVars in explicit type parameters + for param_name, param_type in [ + ("input", resolved_input_type), + ("output", resolved_output_type), + ("workflow_output", resolved_workflow_output_type), + ]: + if param_type is not None and isinstance(param_type, TypeVar): + raise ValueError( + f"Executor '{func.__name__}' has an unresolved TypeVar '{param_type}' " + f"as its {param_name} type. " + f"Use @executor(input=ConcreteType, output=ConcreteType) with concrete types." + ) + # Use explicit types if provided, otherwise fall back to introspection message_type = resolved_input_type if resolved_input_type is not None else introspected_message_type output_types: list[type[Any] | types.UnionType] = ( @@ -114,6 +127,14 @@ def __init__( "or an explicit input_type parameter" ) + # Check for unresolved TypeVar in introspected message type + if isinstance(message_type, TypeVar): + raise ValueError( + f"Executor '{func.__name__}' has an unresolved TypeVar '{message_type}' " + f"as its message type. " + f"Use @executor(input=ConcreteType, output=ConcreteType) with concrete types." + ) + # Store the original function self._original_func = func # Determine if function has WorkflowContext parameter diff --git a/python/packages/core/agent_framework/_workflows/_workflow_context.py b/python/packages/core/agent_framework/_workflows/_workflow_context.py index 51add07a5c..f3603f473e 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow_context.py +++ b/python/packages/core/agent_framework/_workflows/_workflow_context.py @@ -176,10 +176,27 @@ def _is_type_like(x: Any) -> bool: if type_arg is Any: continue + # Check for unresolved TypeVar early with an actionable error message + if isinstance(type_arg, TypeVar): + raise ValueError( + f"{context_description} {parameter_name} {param_description} " + f"has an unresolved TypeVar '{type_arg}'. " + f"Use @handler(input=ConcreteType, output=ConcreteType) with concrete types " + f"for parameterized executors." + ) + # Check if it's a union type and validate each member union_origin = get_origin(type_arg) if union_origin in (Union, UnionType): union_members = get_args(type_arg) + typevar_members = [m for m in union_members if isinstance(m, TypeVar)] + if typevar_members: + raise ValueError( + f"{context_description} {parameter_name} {param_description} " + f"contains unresolved TypeVar(s): {typevar_members}. " + f"Use @handler(input=ConcreteType, output=ConcreteType) with concrete types " + f"for parameterized executors." + ) invalid_members = [m for m in union_members if not _is_type_like(m) and m is not Any] if invalid_members: raise ValueError(