Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions python/packages/core/agent_framework/_workflows/_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from ._request_info_mixin import RequestInfoMixin
from ._runner_context import MessageType, RunnerContext, WorkflowMessage
from ._state import State
from ._typing_utils import is_instance_of, normalize_type_to_list, resolve_type_annotation
from ._typing_utils import contains_typevar, is_instance_of, normalize_type_to_list, resolve_type_annotation
from ._workflow_context import WorkflowContext, validate_workflow_context_annotation

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -650,6 +650,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),
Comment thread
ogkranthi marked this conversation as resolved.
]:
if param_type is not None and contains_typevar(param_type):
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)
Expand Down Expand Up @@ -680,6 +694,15 @@ def decorator(
"or explicit type parameters (input, output, workflow_output)"
)

# Check for unresolved TypeVar in introspected message type
if contains_typevar(message_type):
raise ValueError(
f"Handler '{func.__name__}' has an unresolved TypeVar '{message_type}' "
f"as its message type. "
Comment thread
ogkranthi marked this conversation as resolved.
f"Use @handler(input=ConcreteType, output=ConcreteType) with concrete types "
f"for parameterized executors."
)
Comment thread
ogkranthi marked this conversation as resolved.
Comment thread
ogkranthi marked this conversation as resolved.

final_output_types = inferred_output_types
final_workflow_output_types = inferred_workflow_output_types

Expand Down Expand Up @@ -765,7 +788,7 @@ def _validate_handler_signature(

# Reject unresolved TypeVar in message annotation -- these are not supported
# for workflow type validation and must be replaced with concrete types.
if not skip_message_annotation and isinstance(message_type, TypeVar):
if not skip_message_annotation and contains_typevar(message_type):
raise ValueError(
f"Handler {func.__name__} has an unresolved TypeVar '{message_type}' as its message type annotation. "
"Generic TypeVar annotations are not supported for workflow type validation. "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from typing import Any

from ._executor import Executor
from ._typing_utils import normalize_type_to_list, resolve_type_annotation
from ._typing_utils import contains_typevar, normalize_type_to_list, resolve_type_annotation
from ._workflow_context import WorkflowContext, validate_workflow_context_annotation

if sys.version_info >= (3, 11):
Expand Down Expand Up @@ -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),
Comment thread
ogkranthi marked this conversation as resolved.
]:
if param_type is not None and contains_typevar(param_type):
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] = (
Expand All @@ -114,6 +127,14 @@ def __init__(
"or an explicit input_type parameter"
)

# Check for unresolved TypeVar in introspected message type
if contains_typevar(message_type):
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."
)
Comment thread
ogkranthi marked this conversation as resolved.

# Store the original function
self._original_func = func
# Determine if function has WorkflowContext parameter
Expand Down Expand Up @@ -351,7 +372,7 @@ def _validate_function_signature(

# Reject unresolved TypeVar in message annotation -- these are not supported
# for workflow type validation and must be replaced with concrete types.
if not skip_message_annotation and isinstance(message_type, typing.TypeVar):
if not skip_message_annotation and contains_typevar(message_type):
raise ValueError(
f"Function instance {func.__name__} has an unresolved TypeVar '{message_type}' as its message type "
"annotation. Generic TypeVar annotations are not supported for workflow type validation. "
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,45 @@
# Copyright (c) Microsoft. All rights reserved.

import typing
from types import UnionType
from typing import Any, TypeGuard, Union, cast, get_args, get_origin

import typing_extensions

from .._agents import Agent

# Pre-compute the TypeVar types for runtime-safe detection.
# isinstance(x, TypeVar) can fail if TypeVar is a factory/callable
# on some Python versions, so we compare against the actual runtime type.
_TYPEVAR_TYPES: tuple[type, ...] = (type(typing.TypeVar("_T")), type(typing_extensions.TypeVar("_T"))) # pyright: ignore[reportUnknownVariableType]


def is_typevar(x: Any) -> bool:
"""Check if x is an unresolved TypeVar instance (from typing or typing_extensions).

Args:
x: The value to check.

Returns:
True if x is a TypeVar instance, False otherwise.
"""
return isinstance(x, _TYPEVAR_TYPES)


def contains_typevar(annotation: Any) -> bool:
"""Check if an annotation contains an unresolved TypeVar at any nesting level.

Args:
annotation: The annotation to inspect.

Returns:
True if the annotation or any nested type argument is a TypeVar, False otherwise.
"""
if is_typevar(annotation):
return True

return any(contains_typevar(arg) for arg in get_args(annotation))


def is_chat_agent(agent: Any) -> TypeGuard[Agent]:
"""Check if the given agent is a Agent.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
)
from ._runner_context import RunnerContext, WorkflowMessage
from ._state import State
from ._typing_utils import contains_typevar

if TYPE_CHECKING:
from ._executor import Executor
Expand Down Expand Up @@ -176,6 +177,15 @@ def _is_type_like(x: Any) -> bool:
if type_arg is Any:
continue

# Check for unresolved TypeVar early with an actionable error message
if contains_typevar(type_arg):
raise ValueError(
f"{context_description} {parameter_name} {param_description} "
f"contains an unresolved TypeVar in '{type_arg}'. "
f"Use @handler(input=ConcreteType, output=ConcreteType) with concrete types "
f"for parameterized executors."
)
Comment thread
ogkranthi marked this conversation as resolved.

# Check if it's a union type and validate each member
union_origin = get_origin(type_arg)
if union_origin in (Union, UnionType):
Expand Down
Loading
Loading