libs/langchain_v1/langchain/agents/factory.py PYTHON 2,008 lines View on github.com → Search inside
File is large — showing lines 1–2,000 of 2,008.
1"""Agent factory for creating agents with middleware support."""23from __future__ import annotations45import functools6import importlib7import itertools8import re9from dataclasses import dataclass, field, fields10from typing import (11    TYPE_CHECKING,12    Annotated,13    Any,14    Generic,15    cast,16    get_args,17    get_origin,18    get_type_hints,19)2021from langchain_core.language_models.chat_models import BaseChatModel22from langchain_core.messages import AIMessage, AnyMessage, SystemMessage, ToolMessage23from langchain_core.tools import BaseTool24from langgraph._internal._runnable import RunnableCallable25from langgraph.constants import END, START26from langgraph.graph.state import StateGraph27from langgraph.prebuilt import ToolCallTransformer28from langgraph.prebuilt.tool_node import ToolNode29from langgraph.types import Command, Send30from langsmith import traceable31from typing_extensions import NotRequired, Required, TypedDict, overload3233from langchain.agents._subagent_transformer import SubagentTransformer34from langchain.agents.middleware.types import (35    AgentMiddleware,36    AgentState,37    ContextT,38    ExtendedModelResponse,39    InputAgentState,40    JumpTo,41    ModelRequest,42    ModelResponse,43    OmitFromSchema,44    OutputAgentState,45    ResponseT,46    StateT_co,47    ToolCallRequest,48)49from langchain.agents.structured_output import (50    AutoStrategy,51    MultipleStructuredOutputsError,52    OutputToolBinding,53    ProviderStrategy,54    ProviderStrategyBinding,55    ResponseFormat,56    StructuredOutputError,57    StructuredOutputValidationError,58    ToolStrategy,59)60from langchain.chat_models import init_chat_model616263@dataclass64class _ComposedExtendedModelResponse(Generic[ResponseT]):65    """Internal result from composed `wrap_model_call` middleware.6667    Unlike `ExtendedModelResponse` (user-facing, single command), this holds the68    full list of commands accumulated across all middleware layers during69    composition.70    """7172    model_response: ModelResponse[ResponseT]73    """The underlying model response."""7475    commands: list[Command[Any]] = field(default_factory=list)76    """Commands accumulated from all middleware layers (inner-first, then outer)."""777879if TYPE_CHECKING:80    from collections.abc import Awaitable, Callable, Sequence8182    from langchain_core.runnables import Runnable, RunnableConfig83    from langgraph.cache.base import BaseCache84    from langgraph.graph.state import CompiledStateGraph85    from langgraph.runtime import Runtime86    from langgraph.store.base import BaseStore87    from langgraph.stream._mux import TransformerFactory88    from langgraph.types import Checkpointer8990    from langchain.agents.middleware.types import ToolCallWrapper9192    _ModelCallHandler = Callable[93        [ModelRequest[ContextT], Callable[[ModelRequest[ContextT]], ModelResponse]],94        ModelResponse | AIMessage | ExtendedModelResponse,95    ]9697    _ComposedModelCallHandler = Callable[98        [ModelRequest[ContextT], Callable[[ModelRequest[ContextT]], ModelResponse]],99        _ComposedExtendedModelResponse,100    ]101102    _AsyncModelCallHandler = Callable[103        [ModelRequest[ContextT], Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse]]],104        Awaitable[ModelResponse | AIMessage | ExtendedModelResponse],105    ]106107    _ComposedAsyncModelCallHandler = Callable[108        [ModelRequest[ContextT], Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse]]],109        Awaitable[_ComposedExtendedModelResponse],110    ]111112113STRUCTURED_OUTPUT_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes."114115DYNAMIC_TOOL_ERROR_TEMPLATE = """116Middleware added tools that the agent doesn't know how to execute.117118Unknown tools: {unknown_tool_names}119Registered tools: {available_tool_names}120121This happens when middleware modifies `request.tools` in `wrap_model_call` to include122tools that weren't passed to `create_agent()`.123124How to fix this:125126Option 1: Register tools at agent creation (recommended for most cases)127    Pass the tools to `create_agent(tools=[...])` or set them on `middleware.tools`.128    This makes tools available for every agent invocation.129130Option 2: Handle dynamic tools in middleware (for tools created at runtime)131    Implement `wrap_tool_call` to execute tools that are added dynamically:132133    class MyMiddleware(AgentMiddleware):134        def wrap_tool_call(self, request, handler):135            if request.tool_call["name"] == "dynamic_tool":136                # Execute the dynamic tool yourself or override with tool instance137                return handler(request.override(tool=my_dynamic_tool))138            return handler(request)139""".strip()140141142def _scrub_inputs(inputs: dict[str, Any]) -> dict[str, Any]:143    """Remove `runtime` and `handler` from trace inputs before sending to LangSmith."""144    filtered = inputs.copy()145    filtered.pop("handler", None)146    req = filtered.get("request")147    if isinstance(req, (ModelRequest, ToolCallRequest)):148        filtered["request"] = {149            f.name: getattr(req, f.name) for f in fields(req) if f.name != "runtime"150        }151    return filtered152153154FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT = [155    # If model profile data are not available, model names matching these patterns156    # are assumed to support provider-native structured output. These are regexes157    # so matches stay bounded to model-name segments instead of arbitrary substrings.158    r"(^|[/:.])gpt-4\.1($|[-/:])",159    r"(^|[/:.])gpt-4o($|[-/:])",160    r"(^|[/:.])gpt-5($|[-/:])",161    r"(^|[/:.])gpt-5\.1($|[-/:])",162    r"(^|[/:.])gpt-5\.2(-\d{4}-\d{2}-\d{2})?($|[/:])",163    r"(^|[/:.])gpt-5\.2-(chat|codex)($|[-/:])",164    r"(^|[/:.])gpt-5\.3($|[-/:])",165    r"(^|[/:.])gpt-5\.4(-\d{4}-\d{2}-\d{2})?($|[/:])",166    r"(^|[/:.])gpt-5\.4-(mini|nano)($|[-/:])",167    r"(^|[/:.])gpt-5\.5($|[-/:])",168    r"(^|[/:.])claude-(fable|mythos)-5(?:-\d{8})?(?:-v\d(?::\d)?)?($|[/:])",169    r"(^|[/:.])claude-haiku-4-5(?:-\d{8})?(?:-v\d(?::\d)?)?($|[/:])",170    r"(^|[/:.])claude-opus-4-(5|6|7|8)(?:-\d{8})?(?:-v\d(?::\d)?)?($|[/:])",171    r"(^|[/:.])claude-sonnet-4-(5|6)(?:-\d{8})?(?:-v\d(?::\d)?)?($|[/:])",172    r"(^|[/:.])grok-4($|[-.:/])",173    r"(^|[/:.])grok-build($|[-/:])",174]175176177def _normalize_to_model_response(178    result: ModelResponse | AIMessage | ExtendedModelResponse,179) -> ModelResponse:180    """Normalize middleware return value to ModelResponse.181182    At inner composition boundaries, `ExtendedModelResponse` is unwrapped to its183    underlying `ModelResponse` so that inner middleware always sees `ModelResponse`184    from the handler.185    """186    if isinstance(result, AIMessage):187        return ModelResponse(result=[result], structured_response=None)188    if isinstance(result, ExtendedModelResponse):189        return result.model_response190    return result191192193def _build_commands(194    model_response: ModelResponse,195    middleware_commands: list[Command[Any]] | None = None,196) -> list[Command[Any]]:197    """Build a list of Commands from a model response and middleware commands.198199    The first Command contains the model response state (messages and optional200    structured_response). Middleware commands are appended as-is.201202    Args:203        model_response: The model response containing messages and optional204            structured output.205        middleware_commands: Commands accumulated from middleware layers during206            composition (inner-first ordering).207208    Returns:209        List of `Command` objects ready to be returned from a model node.210    """211    state: dict[str, Any] = {"messages": model_response.result}212213    if model_response.structured_response is not None:214        state["structured_response"] = model_response.structured_response215216    for cmd in middleware_commands or []:217        if cmd.goto:218            msg = (219                "Command goto is not yet supported in wrap_model_call middleware. "220                "Use the jump_to state field with before_model/after_model hooks instead."221            )222            raise NotImplementedError(msg)223        if cmd.resume:224            msg = "Command resume is not yet supported in wrap_model_call middleware."225            raise NotImplementedError(msg)226        if cmd.graph:227            msg = "Command graph is not yet supported in wrap_model_call middleware."228            raise NotImplementedError(msg)229230    commands: list[Command[Any]] = [Command(update=state)]231    commands.extend(middleware_commands or [])232    return commands233234235def _chain_model_call_handlers(236    handlers: Sequence[_ModelCallHandler[ContextT]],237) -> _ComposedModelCallHandler[ContextT] | None:238    """Compose multiple `wrap_model_call` handlers into single middleware stack.239240    Composes handlers so first in list becomes outermost layer. Each handler receives a241    handler callback to execute inner layers. Commands from each layer are accumulated242    into a list (inner-first, then outer) without merging.243244    Args:245        handlers: List of handlers.246247            First handler wraps all others.248249    Returns:250        Composed handler returning `_ComposedExtendedModelResponse`,251        or `None` if handlers empty.252    """253    if not handlers:254        return None255256    def _to_composed_result(257        result: ModelResponse | AIMessage | ExtendedModelResponse | _ComposedExtendedModelResponse,258        extra_commands: list[Command[Any]] | None = None,259    ) -> _ComposedExtendedModelResponse:260        """Normalize any handler result to _ComposedExtendedModelResponse."""261        commands: list[Command[Any]] = list(extra_commands or [])262        if isinstance(result, _ComposedExtendedModelResponse):263            commands.extend(result.commands)264            model_response = result.model_response265        elif isinstance(result, ExtendedModelResponse):266            model_response = result.model_response267            if result.command is not None:268                commands.append(result.command)269        else:270            model_response = _normalize_to_model_response(result)271272        return _ComposedExtendedModelResponse(model_response=model_response, commands=commands)273274    if len(handlers) == 1:275        single_handler = handlers[0]276277        def normalized_single(278            request: ModelRequest[ContextT],279            handler: Callable[[ModelRequest[ContextT]], ModelResponse],280        ) -> _ComposedExtendedModelResponse:281            return _to_composed_result(single_handler(request, handler))282283        return normalized_single284285    def compose_two(286        outer: _ModelCallHandler[ContextT] | _ComposedModelCallHandler[ContextT],287        inner: _ModelCallHandler[ContextT] | _ComposedModelCallHandler[ContextT],288    ) -> _ComposedModelCallHandler[ContextT]:289        """Compose two handlers where outer wraps inner."""290291        def composed(292            request: ModelRequest[ContextT],293            handler: Callable[[ModelRequest[ContextT]], ModelResponse],294        ) -> _ComposedExtendedModelResponse:295            # Closure variable to capture inner's commands before normalizing296            accumulated_commands: list[Command[Any]] = []297298            def inner_handler(req: ModelRequest[ContextT]) -> ModelResponse:299                # Clear on each call for retry safety300                accumulated_commands.clear()301                inner_result = inner(req, handler)302                if isinstance(inner_result, _ComposedExtendedModelResponse):303                    accumulated_commands.extend(inner_result.commands)304                    return inner_result.model_response305                if isinstance(inner_result, ExtendedModelResponse):306                    if inner_result.command is not None:307                        accumulated_commands.append(inner_result.command)308                    return inner_result.model_response309                return _normalize_to_model_response(inner_result)310311            outer_result = outer(request, inner_handler)312            return _to_composed_result(313                outer_result,314                extra_commands=accumulated_commands or None,315            )316317        return composed318319    # Compose right-to-left: outer(inner(innermost(handler)))320    composed_handler = compose_two(handlers[-2], handlers[-1])321    for h in reversed(handlers[:-2]):322        composed_handler = compose_two(h, composed_handler)323324    return composed_handler325326327def _chain_async_model_call_handlers(328    handlers: Sequence[_AsyncModelCallHandler[ContextT]],329) -> _ComposedAsyncModelCallHandler[ContextT] | None:330    """Compose multiple async `wrap_model_call` handlers into single middleware stack.331332    Commands from each layer are accumulated into a list (inner-first, then outer)333    without merging.334335    Args:336        handlers: List of async handlers.337338            First handler wraps all others.339340    Returns:341        Composed async handler returning `_ComposedExtendedModelResponse`,342        or `None` if handlers empty.343    """344    if not handlers:345        return None346347    def _to_composed_result(348        result: ModelResponse | AIMessage | ExtendedModelResponse | _ComposedExtendedModelResponse,349        extra_commands: list[Command[Any]] | None = None,350    ) -> _ComposedExtendedModelResponse:351        """Normalize any handler result to _ComposedExtendedModelResponse."""352        commands: list[Command[Any]] = list(extra_commands or [])353        if isinstance(result, _ComposedExtendedModelResponse):354            commands.extend(result.commands)355            model_response = result.model_response356        elif isinstance(result, ExtendedModelResponse):357            model_response = result.model_response358            if result.command is not None:359                commands.append(result.command)360        else:361            model_response = _normalize_to_model_response(result)362363        return _ComposedExtendedModelResponse(model_response=model_response, commands=commands)364365    if len(handlers) == 1:366        single_handler = handlers[0]367368        async def normalized_single(369            request: ModelRequest[ContextT],370            handler: Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse]],371        ) -> _ComposedExtendedModelResponse:372            return _to_composed_result(await single_handler(request, handler))373374        return normalized_single375376    def compose_two(377        outer: _AsyncModelCallHandler[ContextT] | _ComposedAsyncModelCallHandler[ContextT],378        inner: _AsyncModelCallHandler[ContextT] | _ComposedAsyncModelCallHandler[ContextT],379    ) -> _ComposedAsyncModelCallHandler[ContextT]:380        """Compose two async handlers where outer wraps inner."""381382        async def composed(383            request: ModelRequest[ContextT],384            handler: Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse]],385        ) -> _ComposedExtendedModelResponse:386            # Closure variable to capture inner's commands before normalizing387            accumulated_commands: list[Command[Any]] = []388389            async def inner_handler(req: ModelRequest[ContextT]) -> ModelResponse:390                # Clear on each call for retry safety391                accumulated_commands.clear()392                inner_result = await inner(req, handler)393                if isinstance(inner_result, _ComposedExtendedModelResponse):394                    accumulated_commands.extend(inner_result.commands)395                    return inner_result.model_response396                if isinstance(inner_result, ExtendedModelResponse):397                    if inner_result.command is not None:398                        accumulated_commands.append(inner_result.command)399                    return inner_result.model_response400                return _normalize_to_model_response(inner_result)401402            outer_result = await outer(request, inner_handler)403            return _to_composed_result(404                outer_result,405                extra_commands=accumulated_commands or None,406            )407408        return composed409410    # Compose right-to-left: outer(inner(innermost(handler)))411    composed_handler = compose_two(handlers[-2], handlers[-1])412    for h in reversed(handlers[:-2]):413        composed_handler = compose_two(h, composed_handler)414415    return composed_handler416417418@functools.lru_cache(maxsize=100)419def _get_schema_type_hints(schema: type) -> dict[str, Any]:420    """Return cached type hints for a schema."""421    return get_type_hints(schema, include_extras=True)422423424def _resolve_schemas(schemas: list[type]) -> tuple[type, type, type]:425    """Resolve state, input, and output schemas for the given schemas.426427    Schemas are merged in list order; later entries override earlier ones when the428    same field is declared by multiple schemas.  Duplicates are harmless  a type429    that appears more than once is processed at its last position.430    """431    schema_hints = {schema: _get_schema_type_hints(schema) for schema in schemas}432    return (433        _resolve_schema(schema_hints, "StateSchema", None),434        _resolve_schema(schema_hints, "InputSchema", "input"),435        _resolve_schema(schema_hints, "OutputSchema", "output"),436    )437438439def _resolve_schema(440    schema_hints: dict[type, dict[str, Any]],441    schema_name: str,442    omit_flag: str | None = None,443) -> type:444    """Resolve schema by merging schemas and optionally respecting `OmitFromSchema` annotations.445446    Args:447        schema_hints: Resolved schema annotations to merge448        schema_name: Name for the generated `TypedDict`449        omit_flag: If specified, omit fields with this flag set (`'input'` or450            `'output'`)451452    Returns:453        Merged schema as `TypedDict`454    """455    all_annotations = {}456457    for hints in schema_hints.values():458        for field_name, field_type in hints.items():459            should_omit = False460461            if omit_flag:462                metadata = _extract_metadata(field_type)463                for meta in metadata:464                    if isinstance(meta, OmitFromSchema) and getattr(meta, omit_flag) is True:465                        should_omit = True466                        break467468            if not should_omit:469                all_annotations[field_name] = field_type470471    # `TypedDict` dynamically creates a class, but type checkers don't infer that472    # the runtime result satisfies this function's `type` return contract.473    return cast("type", TypedDict(schema_name, all_annotations))  # type: ignore[operator]474475476def _extract_metadata(type_: type) -> list[Any]:477    """Extract metadata from a field type, handling `Required`/`NotRequired` and `Annotated` wrappers."""  # noqa: E501478    # Handle Required[Annotated[...]] or NotRequired[Annotated[...]]479    if get_origin(type_) in {Required, NotRequired}:480        inner_type = get_args(type_)[0]481        if get_origin(inner_type) is Annotated:482            return list(get_args(inner_type)[1:])483484    # Handle direct Annotated[...]485    elif get_origin(type_) is Annotated:486        return list(get_args(type_)[1:])487488    return []489490491def _get_can_jump_to(middleware: AgentMiddleware[Any, Any], hook_name: str) -> list[JumpTo]:492    """Get the `can_jump_to` list from either sync or async hook methods.493494    Args:495        middleware: The middleware instance to inspect.496        hook_name: The name of the hook (`'before_model'` or `'after_model'`).497498    Returns:499        List of jump destinations, or empty list if not configured.500    """501    # Get the base class method for comparison502    base_sync_method = getattr(AgentMiddleware, hook_name, None)503    base_async_method = getattr(AgentMiddleware, f"a{hook_name}", None)504505    # Try sync method first - only if it's overridden from base class506    sync_method = getattr(middleware.__class__, hook_name, None)507    if (508        sync_method509        and sync_method is not base_sync_method510        and hasattr(sync_method, "__can_jump_to__")511    ):512        # `hasattr` proves the metadata exists at runtime, but not its value type.513        return cast("list[JumpTo]", sync_method.__can_jump_to__)514515    # Try async method - only if it's overridden from base class516    async_method = getattr(middleware.__class__, f"a{hook_name}", None)517    if (518        async_method519        and async_method is not base_async_method520        and hasattr(async_method, "__can_jump_to__")521    ):522        # `hasattr` proves the metadata exists at runtime, but not its value type.523        return cast("list[JumpTo]", async_method.__can_jump_to__)524525    return []526527528def _supports_provider_strategy(529    model: str | BaseChatModel, tools: list[BaseTool | dict[str, Any]] | None = None530) -> bool:531    """Check if a model supports provider-specific structured output.532533    Args:534        model: Model name string or `BaseChatModel` instance.535        tools: Optional list of tools provided to the agent.536537            Needed because some models don't support structured output together with tool calling.538539    Returns:540        `True` if the model supports provider-specific structured output, `False` otherwise.541    """542    model_name: str | None = None543    if isinstance(model, str):544        model_name = model545    elif isinstance(model, BaseChatModel):546        model_name = (547            getattr(model, "model_name", None)548            or getattr(model, "model", None)549            or getattr(model, "model_id", "")550        )551        model_profile = model.profile552        if (553            model_profile is not None554            and model_profile.get("structured_output")555            # We make an exception for Gemini < 3-series models, which currently do not support556            # simultaneous tool use with structured output; 3-series can.557            and not (558                tools559                and isinstance(model_name, str)560                and "gemini" in model_name.lower()561                and "gemini-3" not in model_name.lower()562            )563        ):564            return True565566    return (567        any(568            re.search(pattern, model_name.lower())569            for pattern in FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT570        )571        if model_name572        else False573    )574575576def _is_openai_compatible_model(model: BaseChatModel) -> bool:577    """Check if a model inherits from `BaseChatOpenAI`.578579    Used to redundantly set `strict=True` on tools when `response_format` is580    provided, as older versions of `langchain-openai` do not auto-set it.581    Covers `ChatOpenAI`, `ChatDeepSeek`, `ChatXAI`, etc.582583    Args:584        model: The chat model to check.585586    Returns:587        `True` if the model inherits from `BaseChatOpenAI`, `False` otherwise.588    """589    try:590        base_chat_openai = importlib.import_module("langchain_openai.chat_models.base")591    except ImportError:592        return False593    return isinstance(model, base_chat_openai.BaseChatOpenAI)594595596def _handle_structured_output_error(597    exception: Exception,598    response_format: ResponseFormat[Any],599) -> tuple[bool, str]:600    """Handle structured output error.601602    Returns `(should_retry, retry_tool_message)`.603    """604    if not isinstance(response_format, ToolStrategy):605        return False, ""606607    handle_errors = response_format.handle_errors608609    if handle_errors is False:610        return False, ""611    if handle_errors is True:612        return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))613    if isinstance(handle_errors, str):614        return True, handle_errors615    if isinstance(handle_errors, type):616        if issubclass(handle_errors, Exception) and isinstance(exception, handle_errors):617            return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))618        return False, ""619    if isinstance(handle_errors, tuple):620        if any(isinstance(exception, exc_type) for exc_type in handle_errors):621            return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))622        return False, ""623    return True, handle_errors(exception)624625626def _chain_tool_call_wrappers(627    wrappers: Sequence[ToolCallWrapper],628) -> ToolCallWrapper | None:629    """Compose wrappers into middleware stack (first = outermost).630631    Args:632        wrappers: Wrappers in middleware order.633634    Returns:635        Composed wrapper, or `None` if empty.636637    Example:638        ```python639        wrapper = _chain_tool_call_wrappers([auth, cache, retry])640        # Request flows: auth -> cache -> retry -> tool641        # Response flows: tool -> retry -> cache -> auth642        ```643    """644    if not wrappers:645        return None646647    if len(wrappers) == 1:648        return wrappers[0]649650    def compose_two(outer: ToolCallWrapper, inner: ToolCallWrapper) -> ToolCallWrapper:651        """Compose two wrappers where outer wraps inner."""652653        def composed(654            request: ToolCallRequest,655            execute: Callable[[ToolCallRequest], ToolMessage | Command[Any]],656        ) -> ToolMessage | Command[Any]:657            # Create a callable that invokes inner with the original execute658            def call_inner(req: ToolCallRequest) -> ToolMessage | Command[Any]:659                return inner(req, execute)660661            # Outer can call call_inner multiple times662            return outer(request, call_inner)663664        return composed665666    # Chain all wrappers: first -> second -> ... -> last667    result = wrappers[-1]668    for wrapper in reversed(wrappers[:-1]):669        result = compose_two(wrapper, result)670671    return result672673674def _chain_async_tool_call_wrappers(675    wrappers: Sequence[676        Callable[677            [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],678            Awaitable[ToolMessage | Command[Any]],679        ]680    ],681) -> (682    Callable[683        [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],684        Awaitable[ToolMessage | Command[Any]],685    ]686    | None687):688    """Compose async wrappers into middleware stack (first = outermost).689690    Args:691        wrappers: Async wrappers in middleware order.692693    Returns:694        Composed async wrapper, or `None` if empty.695    """696    if not wrappers:697        return None698699    if len(wrappers) == 1:700        return wrappers[0]701702    def compose_two(703        outer: Callable[704            [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],705            Awaitable[ToolMessage | Command[Any]],706        ],707        inner: Callable[708            [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],709            Awaitable[ToolMessage | Command[Any]],710        ],711    ) -> Callable[712        [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],713        Awaitable[ToolMessage | Command[Any]],714    ]:715        """Compose two async wrappers where outer wraps inner."""716717        async def composed(718            request: ToolCallRequest,719            execute: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],720        ) -> ToolMessage | Command[Any]:721            # Create an async callable that invokes inner with the original execute722            async def call_inner(req: ToolCallRequest) -> ToolMessage | Command[Any]:723                return await inner(req, execute)724725            # Outer can call call_inner multiple times726            return await outer(request, call_inner)727728        return composed729730    # Chain all wrappers: first -> second -> ... -> last731    result = wrappers[-1]732    for wrapper in reversed(wrappers[:-1]):733        result = compose_two(wrapper, result)734735    return result736737738# No `response_format`: there is no structured output, so `ResponseT` resolves to `Any`.739@overload740def create_agent(741    model: str | BaseChatModel,742    tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,743    *,744    system_prompt: str | SystemMessage | None = None,745    middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),746    response_format: None = None,747    state_schema: None = None,748    context_schema: type[ContextT] | None = None,749    checkpointer: Checkpointer | None = None,750    store: BaseStore | None = None,751    interrupt_before: list[str] | None = None,752    interrupt_after: list[str] | None = None,753    debug: bool = False,754    name: str | None = None,755    cache: BaseCache[Any] | None = None,756    transformers: Sequence[TransformerFactory] | None = None,757) -> CompiledStateGraph[AgentState[Any], ContextT, InputAgentState, OutputAgentState[Any]]: ...758759760# Raw-dict `response_format`: structured output is an untyped `dict[str, Any]`.761@overload762def create_agent(763    model: str | BaseChatModel,764    tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,765    *,766    system_prompt: str | SystemMessage | None = None,767    middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),768    response_format: dict[str, Any],769    state_schema: type[AgentState[dict[str, Any]]] | None = None,770    context_schema: type[ContextT] | None = None,771    checkpointer: Checkpointer | None = None,772    store: BaseStore | None = None,773    interrupt_before: list[str] | None = None,774    interrupt_after: list[str] | None = None,775    debug: bool = False,776    name: str | None = None,777    cache: BaseCache[Any] | None = None,778    transformers: Sequence[TransformerFactory] | None = None,779) -> CompiledStateGraph[780    AgentState[dict[str, Any]], ContextT, InputAgentState, OutputAgentState[dict[str, Any]]781]: ...782783784# Schema-typed `response_format`: `ResponseT` is inferred from the schema/type.785@overload786def create_agent(787    model: str | BaseChatModel,788    tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,789    *,790    system_prompt: str | SystemMessage | None = None,791    middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),792    response_format: ResponseFormat[ResponseT] | type[ResponseT] | None = None,793    state_schema: type[AgentState[ResponseT]] | None = None,794    context_schema: type[ContextT] | None = None,795    checkpointer: Checkpointer | None = None,796    store: BaseStore | None = None,797    interrupt_before: list[str] | None = None,798    interrupt_after: list[str] | None = None,799    debug: bool = False,800    name: str | None = None,801    cache: BaseCache[Any] | None = None,802    transformers: Sequence[TransformerFactory] | None = None,803) -> CompiledStateGraph[804    AgentState[ResponseT], ContextT, InputAgentState, OutputAgentState[ResponseT]805]: ...806807808def create_agent(809    model: str | BaseChatModel,810    tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,811    *,812    system_prompt: str | SystemMessage | None = None,813    middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),814    response_format: ResponseFormat[ResponseT] | type[ResponseT] | dict[str, Any] | None = None,815    state_schema: type[AgentState[ResponseT]] | None = None,816    context_schema: type[ContextT] | None = None,817    checkpointer: Checkpointer | None = None,818    store: BaseStore | None = None,819    interrupt_before: list[str] | None = None,820    interrupt_after: list[str] | None = None,821    debug: bool = False,822    name: str | None = None,823    cache: BaseCache[Any] | None = None,824    transformers: Sequence[TransformerFactory] | None = None,825) -> CompiledStateGraph[826    AgentState[ResponseT], ContextT, InputAgentState, OutputAgentState[ResponseT]827]:828    """Creates an agent graph that calls tools in a loop until a stopping condition is met.829830    For more details on using `create_agent`,831    visit the [Agents](https://docs.langchain.com/oss/python/langchain/agents) docs.832833    Args:834        model: The language model for the agent.835836            Can be a string identifier (e.g., `"openai:gpt-5.5"`) or a direct chat model837            instance (e.g., [`ChatOpenAI`][langchain_openai.ChatOpenAI] or other another838            [LangChain chat model](https://docs.langchain.com/oss/python/integrations/chat)).839840            For a full list of supported model strings, see841            [`init_chat_model`][langchain.chat_models.init_chat_model(model_provider)].842843            !!! tip ""844845                See the [Models](https://docs.langchain.com/oss/python/langchain/models)846                docs for more information.847        tools: A list of tools, `dict`, or `Callable`.848849            If `None` or an empty list, the agent will consist of a model node without a850            tool calling loop.851852853            !!! tip ""854855                See the [Tools](https://docs.langchain.com/oss/python/langchain/tools)856                docs for more information.857        system_prompt: An optional system prompt for the LLM.858859            Can be a `str` (which will be converted to a `SystemMessage`) or a860            `SystemMessage` instance directly. The system message is added to the861            beginning of the message list when calling the model.862        middleware: A sequence of middleware instances to apply to the agent.863864            Middleware can intercept and modify agent behavior at various stages.865866            !!! tip ""867868                See the [Middleware](https://docs.langchain.com/oss/python/langchain/middleware)869                docs for more information.870        response_format: An optional configuration for structured responses.871872            Can be a `ToolStrategy`, `ProviderStrategy`, or a Pydantic model class.873874            If provided, the agent will handle structured output during the875            conversation flow.876877            Raw schemas will be wrapped in an appropriate strategy based on model878            capabilities.879880            !!! tip ""881882                See the [Structured output](https://docs.langchain.com/oss/python/langchain/structured-output)883                docs for more information.884        state_schema: An optional `TypedDict` schema that extends `AgentState`.885886            When provided, this schema is used instead of `AgentState` as the base887            schema for merging with middleware state schemas. This allows users to888            add custom state fields without needing to create custom middleware.889890            Generally, it's recommended to use `state_schema` extensions via middleware891            to keep relevant extensions scoped to corresponding hooks / tools.892        context_schema: An optional schema for runtime context.893        checkpointer: An optional checkpoint saver object.894895            Used for persisting the state of the graph (e.g., as chat memory) for a896            single thread (e.g., a single conversation).897        store: An optional store object.898899            Used for persisting data across multiple threads (e.g., multiple900            conversations / users).901        interrupt_before: An optional list of node names to interrupt before.902903            Useful if you want to add a user confirmation or other interrupt904            before taking an action.905        interrupt_after: An optional list of node names to interrupt after.906907            Useful if you want to return directly or run additional processing908            on an output.909        debug: Whether to enable verbose logging for graph execution.910911            When enabled, prints detailed information about each node execution, state912            updates, and transitions during agent runtime. Useful for debugging913            middleware behavior and understanding agent execution flow.914        name: An optional name for the `CompiledStateGraph`.915916            This name will be automatically used when adding the agent graph to917            another graph as a subgraph node - particularly useful for building918            multi-agent systems.919        cache: An optional `BaseCache` instance to enable caching of graph execution.920        transformers: Optional sequence of scope-aware `StreamTransformer`921            factories to register on the compiled graph in addition to922            the agent defaults. Each factory is invoked as `factory(scope)`923            so every invocation receives a fresh instance. The final order924            on the compiled graph is: `ToolCallTransformer`, then any925            factories declared by middleware via926            `AgentMiddleware.transformers`, then any factories supplied here.927928    Returns:929        A compiled `StateGraph` that can be used for chat interactions.930931    Raises:932        AssertionError: If duplicate middleware instances are provided.933934    The agent node calls the language model with the messages list (after applying935    the system prompt). If the resulting [`AIMessage`][langchain.messages.AIMessage]936    contains `tool_calls`, the graph will then call the tools. The tools node executes937    the tools and adds the responses to the messages list as938    [`ToolMessage`][langchain.messages.ToolMessage] objects. The agent node then calls939    the language model again. The process repeats until no more `tool_calls` are present940    in the response. The agent then returns the full list of messages.941942    Example:943        ```python944        from langchain.agents import create_agent945946947        def check_weather(location: str) -> str:948            '''Return the weather forecast for the specified location.'''949            return f"It's always sunny in {location}"950951952        graph = create_agent(953            model="anthropic:claude-sonnet-4-5-20250929",954            tools=[check_weather],955            system_prompt="You are a helpful assistant",956        )957        inputs = {"messages": [{"role": "user", "content": "what is the weather in sf"}]}958        for chunk in graph.stream(inputs, stream_mode="updates"):959            print(chunk)960        ```961    """962    # init chat model963    if isinstance(model, str):964        model = init_chat_model(model)965966    # Convert system_prompt to SystemMessage if needed967    system_message: SystemMessage | None = None968    if system_prompt is not None:969        if isinstance(system_prompt, SystemMessage):970            system_message = system_prompt971        else:972            system_message = SystemMessage(content=system_prompt)973974    # Handle tools being None or empty975    if tools is None:976        tools = []977978    # Convert response format and setup structured output tools979    # Raw schemas are wrapped in AutoStrategy to preserve auto-detection intent.980    # AutoStrategy is converted to ToolStrategy upfront to calculate tools during agent creation,981    # but may be replaced with ProviderStrategy later based on model capabilities.982    initial_response_format: ToolStrategy[Any] | ProviderStrategy[Any] | AutoStrategy[Any] | None983    if response_format is None:984        initial_response_format = None985    elif isinstance(response_format, (ToolStrategy, ProviderStrategy, AutoStrategy)):986        # Explicit Tool/Provider strategy, or AutoStrategy for later capability detection987        initial_response_format = response_format988    else:989        # Raw schema - wrap in AutoStrategy to enable auto-detection990        initial_response_format = AutoStrategy(schema=response_format)991992    # For AutoStrategy, convert to ToolStrategy to setup tools upfront993    # (may be replaced with ProviderStrategy later based on model)994    tool_strategy_for_setup: ToolStrategy[Any] | None = None995    if isinstance(initial_response_format, AutoStrategy):996        tool_strategy_for_setup = ToolStrategy(schema=initial_response_format.schema)997    elif isinstance(initial_response_format, ToolStrategy):998        tool_strategy_for_setup = initial_response_format9991000    structured_output_tools: dict[str, OutputToolBinding[Any]] = {}1001    if tool_strategy_for_setup:1002        for response_schema in tool_strategy_for_setup.schema_specs:1003            structured_tool_info = OutputToolBinding.from_schema_spec(response_schema)1004            structured_output_tools[structured_tool_info.tool.name] = structured_tool_info1005    middleware_tools = [t for m in middleware for t in getattr(m, "tools", [])]10061007    # Collect middleware with wrap_tool_call or awrap_tool_call hooks1008    # Include middleware with either implementation to ensure NotImplementedError is raised1009    # when middleware doesn't support the execution path1010    middleware_w_wrap_tool_call = [1011        m1012        for m in middleware1013        if m.__class__.wrap_tool_call is not AgentMiddleware.wrap_tool_call1014        or m.__class__.awrap_tool_call is not AgentMiddleware.awrap_tool_call1015    ]10161017    # Chain all wrap_tool_call handlers into a single composed handler1018    wrap_tool_call_wrapper = None1019    if middleware_w_wrap_tool_call:1020        wrappers = [1021            traceable(name=f"{m.name}.wrap_tool_call", process_inputs=_scrub_inputs)(1022                m.wrap_tool_call1023            )1024            for m in middleware_w_wrap_tool_call1025        ]1026        wrap_tool_call_wrapper = _chain_tool_call_wrappers(wrappers)10271028    # Collect middleware with awrap_tool_call or wrap_tool_call hooks1029    # Include middleware with either implementation to ensure NotImplementedError is raised1030    # when middleware doesn't support the execution path1031    middleware_w_awrap_tool_call = [1032        m1033        for m in middleware1034        if m.__class__.awrap_tool_call is not AgentMiddleware.awrap_tool_call1035        or m.__class__.wrap_tool_call is not AgentMiddleware.wrap_tool_call1036    ]10371038    # Chain all awrap_tool_call handlers into a single composed async handler1039    awrap_tool_call_wrapper = None1040    if middleware_w_awrap_tool_call:1041        async_wrappers = [1042            traceable(name=f"{m.name}.awrap_tool_call", process_inputs=_scrub_inputs)(1043                m.awrap_tool_call1044            )1045            for m in middleware_w_awrap_tool_call1046        ]1047        awrap_tool_call_wrapper = _chain_async_tool_call_wrappers(async_wrappers)10481049    # Setup tools1050    tool_node: ToolNode | None = None1051    # Extract built-in provider tools (dict format) and regular tools (BaseTool/callables)1052    built_in_tools = [t for t in tools if isinstance(t, dict)]1053    regular_tools = [t for t in tools if not isinstance(t, dict)]10541055    # Tools that require client-side execution (must be in ToolNode)1056    available_tools = middleware_tools + regular_tools10571058    # Create ToolNode if we have client-side tools OR if middleware defines wrap_tool_call1059    # (which may handle dynamically registered tools)1060    tool_node = (1061        ToolNode(1062            tools=available_tools,1063            wrap_tool_call=wrap_tool_call_wrapper,1064            awrap_tool_call=awrap_tool_call_wrapper,1065        )1066        if available_tools or wrap_tool_call_wrapper or awrap_tool_call_wrapper1067        else None1068    )10691070    # Default tools for ModelRequest initialization1071    # Use converted BaseTool instances from ToolNode (not raw callables)1072    # Include built-ins and converted tools (can be changed dynamically by middleware)1073    # Structured tools are NOT included - they're added dynamically based on response_format1074    if tool_node:1075        default_tools = list(tool_node.tools_by_name.values()) + built_in_tools1076    else:1077        default_tools = list(built_in_tools)10781079    # validate middleware1080    if len({m.name for m in middleware}) != len(middleware):1081        msg = "Please remove duplicate middleware instances."1082        raise AssertionError(msg)1083    middleware_w_before_agent = [1084        m1085        for m in middleware1086        if m.__class__.before_agent is not AgentMiddleware.before_agent1087        or m.__class__.abefore_agent is not AgentMiddleware.abefore_agent1088    ]1089    middleware_w_before_model = [1090        m1091        for m in middleware1092        if m.__class__.before_model is not AgentMiddleware.before_model1093        or m.__class__.abefore_model is not AgentMiddleware.abefore_model1094    ]1095    middleware_w_after_model = [1096        m1097        for m in middleware1098        if m.__class__.after_model is not AgentMiddleware.after_model1099        or m.__class__.aafter_model is not AgentMiddleware.aafter_model1100    ]1101    middleware_w_after_agent = [1102        m1103        for m in middleware1104        if m.__class__.after_agent is not AgentMiddleware.after_agent1105        or m.__class__.aafter_agent is not AgentMiddleware.aafter_agent1106    ]1107    # Collect middleware with wrap_model_call or awrap_model_call hooks1108    # Include middleware with either implementation to ensure NotImplementedError is raised1109    # when middleware doesn't support the execution path1110    middleware_w_wrap_model_call = [1111        m1112        for m in middleware1113        if m.__class__.wrap_model_call is not AgentMiddleware.wrap_model_call1114        or m.__class__.awrap_model_call is not AgentMiddleware.awrap_model_call1115    ]1116    # Collect middleware with awrap_model_call or wrap_model_call hooks1117    # Include middleware with either implementation to ensure NotImplementedError is raised1118    # when middleware doesn't support the execution path1119    middleware_w_awrap_model_call = [1120        m1121        for m in middleware1122        if m.__class__.awrap_model_call is not AgentMiddleware.awrap_model_call1123        or m.__class__.wrap_model_call is not AgentMiddleware.wrap_model_call1124    ]11251126    # Compose wrap_model_call handlers into a single middleware stack (sync)1127    wrap_model_call_handler = None1128    if middleware_w_wrap_model_call:1129        sync_handlers = [1130            traceable(name=f"{m.name}.wrap_model_call", process_inputs=_scrub_inputs)(1131                m.wrap_model_call1132            )1133            for m in middleware_w_wrap_model_call1134        ]1135        wrap_model_call_handler = _chain_model_call_handlers(sync_handlers)11361137    # Compose awrap_model_call handlers into a single middleware stack (async)1138    awrap_model_call_handler = None1139    if middleware_w_awrap_model_call:1140        async_handlers = [1141            traceable(name=f"{m.name}.awrap_model_call", process_inputs=_scrub_inputs)(1142                m.awrap_model_call1143            )1144            for m in middleware_w_awrap_model_call1145        ]1146        awrap_model_call_handler = _chain_async_model_call_handlers(async_handlers)11471148    base_state = state_schema if state_schema is not None else AgentState1149    # Build an ordered list: middleware schemas first (in registration order),1150    # base_state last so it wins any field conflict.  This lets the caller's1151    # explicit state_schema override middleware annotations  e.g. passing1152    # a DeltaChannel-annotated schema wins over BinaryOperatorAggregate from1153    # AgentState without requiring a post-compilation patch.1154    state_schemas: list[type] = [*(m.state_schema for m in middleware), base_state]11551156    resolved_state_schema, input_schema, output_schema = _resolve_schemas(state_schemas)11571158    # create graph, add nodes1159    graph: StateGraph[1160        AgentState[ResponseT], ContextT, InputAgentState, OutputAgentState[ResponseT]1161    ] = StateGraph(1162        state_schema=resolved_state_schema,1163        input_schema=input_schema,1164        output_schema=output_schema,1165        context_schema=context_schema,1166    )11671168    def _handle_model_output(1169        output: AIMessage, effective_response_format: ResponseFormat[Any] | None1170    ) -> dict[str, Any]:1171        """Handle model output including structured responses.11721173        Args:1174            output: The AI message output from the model.1175            effective_response_format: The actual strategy used (may differ from initial1176                if auto-detected).1177        """1178        # Handle structured output with provider strategy1179        if isinstance(effective_response_format, ProviderStrategy):1180            if not output.tool_calls:1181                provider_strategy_binding = ProviderStrategyBinding.from_schema_spec(1182                    effective_response_format.schema_spec1183                )1184                try:1185                    structured_response = provider_strategy_binding.parse(output)1186                except Exception as exc:1187                    schema_name = getattr(1188                        effective_response_format.schema_spec.schema, "__name__", "response_format"1189                    )1190                    validation_error = StructuredOutputValidationError(schema_name, exc, output)1191                    raise validation_error from exc1192                else:1193                    return {"messages": [output], "structured_response": structured_response}1194            return {"messages": [output]}11951196        # Handle structured output with tool strategy1197        if (1198            isinstance(effective_response_format, ToolStrategy)1199            and isinstance(output, AIMessage)1200            and output.tool_calls1201        ):1202            structured_tool_calls = [1203                tc for tc in output.tool_calls if tc["name"] in structured_output_tools1204            ]12051206            if structured_tool_calls:1207                exception: StructuredOutputError | None = None1208                if len(structured_tool_calls) > 1:1209                    # Handle multiple structured outputs error1210                    tool_names = [tc["name"] for tc in structured_tool_calls]1211                    exception = MultipleStructuredOutputsError(tool_names, output)1212                    should_retry, error_message = _handle_structured_output_error(1213                        exception, effective_response_format1214                    )1215                    if not should_retry:1216                        raise exception12171218                    # Add error messages and retry1219                    tool_messages = [1220                        ToolMessage(1221                            content=error_message,1222                            tool_call_id=tc["id"],1223                            name=tc["name"],1224                        )1225                        for tc in structured_tool_calls1226                    ]1227                    return {"messages": [output, *tool_messages]}12281229                # Handle single structured output1230                tool_call = structured_tool_calls[0]1231                try:1232                    structured_tool_binding = structured_output_tools[tool_call["name"]]1233                    structured_response = structured_tool_binding.parse(tool_call["args"])12341235                    tool_message_content = (1236                        effective_response_format.tool_message_content1237                        or f"Returning structured response: {structured_response}"1238                    )12391240                    return {1241                        "messages": [1242                            output,1243                            ToolMessage(1244                                content=tool_message_content,1245                                tool_call_id=tool_call["id"],1246                                name=tool_call["name"],1247                            ),1248                        ],1249                        "structured_response": structured_response,1250                    }1251                except Exception as exc:1252                    exception = StructuredOutputValidationError(tool_call["name"], exc, output)1253                    should_retry, error_message = _handle_structured_output_error(1254                        exception, effective_response_format1255                    )1256                    if not should_retry:1257                        raise exception from exc12581259                    return {1260                        "messages": [1261                            output,1262                            ToolMessage(1263                                content=error_message,1264                                tool_call_id=tool_call["id"],1265                                name=tool_call["name"],1266                            ),1267                        ],1268                    }12691270        return {"messages": [output]}12711272    def _get_bound_model(1273        request: ModelRequest[ContextT],1274    ) -> tuple[Runnable[Any, Any], ResponseFormat[Any] | None]:1275        """Get the model with appropriate tool bindings.12761277        Performs auto-detection of strategy if needed based on model capabilities.12781279        Args:1280            request: The model request containing model, tools, and response format.12811282        Returns:1283            Tuple of `(bound_model, effective_response_format)` where1284            `effective_response_format` is the actual strategy used (may differ from1285            initial if auto-detected).12861287        Raises:1288            ValueError: If middleware returned unknown client-side tool names.1289            ValueError: If `ToolStrategy` specifies tools not declared upfront.1290        """1291        # Validate ONLY client-side tools that need to exist in tool_node1292        # Skip validation when wrap_tool_call is defined, as middleware may handle1293        # dynamic tools that are added at runtime via wrap_model_call1294        has_wrap_tool_call = wrap_tool_call_wrapper or awrap_tool_call_wrapper12951296        # Build map of available client-side tools from the ToolNode1297        # (which has already converted callables)1298        available_tools_by_name = {}1299        if tool_node:1300            available_tools_by_name = tool_node.tools_by_name.copy()13011302        # Check if any requested tools are unknown CLIENT-SIDE tools1303        # Only validate if wrap_tool_call is NOT defined (no dynamic tool handling)1304        if not has_wrap_tool_call:1305            unknown_tool_names = []1306            for t in request.tools:1307                # Only validate BaseTool instances (skip built-in dict tools)1308                if isinstance(t, dict):1309                    continue1310                if isinstance(t, BaseTool) and t.name not in available_tools_by_name:1311                    unknown_tool_names.append(t.name)13121313            if unknown_tool_names:1314                available_tool_names = sorted(available_tools_by_name.keys())1315                msg = DYNAMIC_TOOL_ERROR_TEMPLATE.format(1316                    unknown_tool_names=unknown_tool_names,1317                    available_tool_names=available_tool_names,1318                )1319                raise ValueError(msg)13201321        # Normalize raw schemas to AutoStrategy1322        # (handles middleware override with raw Pydantic classes)1323        response_format: ResponseFormat[Any] | Any | None = request.response_format1324        if response_format is not None and not isinstance(1325            response_format, (AutoStrategy, ToolStrategy, ProviderStrategy)1326        ):1327            response_format = AutoStrategy(schema=response_format)13281329        # Determine effective response format (auto-detect if needed)1330        effective_response_format: ResponseFormat[Any] | None1331        if isinstance(response_format, AutoStrategy):1332            # User provided raw schema via AutoStrategy - auto-detect best strategy based on model1333            if _supports_provider_strategy(request.model, tools=request.tools):1334                # Model supports provider strategy - use it1335                effective_response_format = ProviderStrategy(schema=response_format.schema)1336            elif response_format is initial_response_format and tool_strategy_for_setup is not None:1337                # Model doesn't support provider strategy - use ToolStrategy1338                # Reuse the strategy from setup if possible to preserve tool names1339                effective_response_format = tool_strategy_for_setup1340            else:1341                effective_response_format = ToolStrategy(schema=response_format.schema)1342        else:1343            # User explicitly specified a strategy - preserve it1344            effective_response_format = response_format13451346        # Build final tools list including structured output tools1347        # request.tools now only contains BaseTool instances (converted from callables)1348        # and dicts (built-ins)1349        final_tools = list(request.tools)1350        if isinstance(effective_response_format, ToolStrategy):1351            # Add structured output tools to final tools list1352            structured_tools = [info.tool for info in structured_output_tools.values()]1353            final_tools.extend(structured_tools)13541355        # Bind model based on effective response format1356        if isinstance(effective_response_format, ProviderStrategy):1357            # (Backward compatibility) Use OpenAI format structured output1358            # Redundantly set strict=True on tools for OpenAI-compatible models, as older1359            # versions of langchain-openai do not auto-set it in bind_tools.1360            kwargs = effective_response_format.to_model_kwargs()1361            bind_kwargs: dict[str, Any] = {**kwargs, **request.model_settings}1362            if _is_openai_compatible_model(request.model) and not getattr(1363                request.model, "use_responses_api", False1364            ):1365                bind_kwargs["strict"] = True1366            return (1367                request.model.bind_tools(final_tools, **bind_kwargs),1368                effective_response_format,1369            )13701371        if isinstance(effective_response_format, ToolStrategy):1372            # Current implementation requires that tools used for structured output1373            # have to be declared upfront when creating the agent as part of the1374            # response format. Middleware is allowed to change the response format1375            # to a subset of the original structured tools when using ToolStrategy,1376            # but not to add new structured tools that weren't declared upfront.1377            # Compute output binding1378            for tc in effective_response_format.schema_specs:1379                if tc.name not in structured_output_tools:1380                    msg = (1381                        f"ToolStrategy specifies tool '{tc.name}' "1382                        "which wasn't declared in the original "1383                        "response format when creating the agent."1384                    )1385                    raise ValueError(msg)13861387            # Force tool use if we have structured output tools1388            tool_choice = "any" if structured_output_tools else request.tool_choice1389            return (1390                request.model.bind_tools(1391                    final_tools, tool_choice=tool_choice, **request.model_settings1392                ),1393                effective_response_format,1394            )13951396        # No structured output - standard model binding1397        if final_tools:1398            return (1399                request.model.bind_tools(1400                    final_tools, tool_choice=request.tool_choice, **request.model_settings1401                ),1402                None,1403            )1404        return request.model.bind(**request.model_settings), None14051406    def _execute_model_sync(request: ModelRequest[ContextT]) -> ModelResponse:1407        """Execute model and return response.14081409        This is the core model execution logic wrapped by `wrap_model_call` handlers.14101411        Raises any exceptions that occur during model invocation.1412        """1413        # Get the bound model (with auto-detection if needed)1414        model_, effective_response_format = _get_bound_model(request)1415        messages = request.messages1416        if request.system_message:1417            messages = [request.system_message, *messages]14181419        output = model_.invoke(messages)1420        if name:1421            output.name = name14221423        # Handle model output to get messages and structured_response1424        handled_output = _handle_model_output(output, effective_response_format)1425        messages_list = handled_output["messages"]1426        structured_response = handled_output.get("structured_response")14271428        return ModelResponse(1429            result=messages_list,1430            structured_response=structured_response,1431        )14321433    def model_node(state: AgentState[Any], runtime: Runtime[ContextT]) -> list[Command[Any]]:1434        """Sync model request handler with sequential middleware processing."""1435        request = ModelRequest(1436            model=model,1437            tools=default_tools,1438            system_message=system_message,1439            response_format=initial_response_format,1440            messages=state["messages"],1441            tool_choice=None,1442            state=state,1443            runtime=runtime,1444        )14451446        if wrap_model_call_handler is None:1447            model_response = _execute_model_sync(request)1448            return _build_commands(model_response)14491450        result = wrap_model_call_handler(request, _execute_model_sync)1451        return _build_commands(result.model_response, result.commands)14521453    async def _execute_model_async(request: ModelRequest[ContextT]) -> ModelResponse:1454        """Execute model asynchronously and return response.14551456        This is the core async model execution logic wrapped by `wrap_model_call`1457        handlers.14581459        Raises any exceptions that occur during model invocation.1460        """1461        # Get the bound model (with auto-detection if needed)1462        model_, effective_response_format = _get_bound_model(request)1463        messages = request.messages1464        if request.system_message:1465            messages = [request.system_message, *messages]14661467        output = await model_.ainvoke(messages)1468        if name:1469            output.name = name14701471        # Handle model output to get messages and structured_response1472        handled_output = _handle_model_output(output, effective_response_format)1473        messages_list = handled_output["messages"]1474        structured_response = handled_output.get("structured_response")14751476        return ModelResponse(1477            result=messages_list,1478            structured_response=structured_response,1479        )14801481    async def amodel_node(state: AgentState[Any], runtime: Runtime[ContextT]) -> list[Command[Any]]:1482        """Async model request handler with sequential middleware processing."""1483        request = ModelRequest(1484            model=model,1485            tools=default_tools,1486            system_message=system_message,1487            response_format=initial_response_format,1488            messages=state["messages"],1489            tool_choice=None,1490            state=state,1491            runtime=runtime,1492        )14931494        if awrap_model_call_handler is None:1495            model_response = await _execute_model_async(request)1496            return _build_commands(model_response)14971498        result = await awrap_model_call_handler(request, _execute_model_async)1499        return _build_commands(result.model_response, result.commands)15001501    # Use sync or async based on model capabilities1502    graph.add_node("model", RunnableCallable(model_node, amodel_node, trace=False))15031504    # Only add tools node if we have tools1505    if tool_node is not None:1506        graph.add_node("tools", tool_node)15071508    # Add middleware nodes1509    for m in middleware:1510        if (1511            m.__class__.before_agent is not AgentMiddleware.before_agent1512            or m.__class__.abefore_agent is not AgentMiddleware.abefore_agent1513        ):1514            # Use RunnableCallable to support both sync and async1515            # Pass None for sync if not overridden to avoid signature conflicts1516            sync_before_agent = (1517                m.before_agent1518                if m.__class__.before_agent is not AgentMiddleware.before_agent1519                else None1520            )1521            async_before_agent = (1522                m.abefore_agent1523                if m.__class__.abefore_agent is not AgentMiddleware.abefore_agent1524                else None1525            )1526            before_agent_node = RunnableCallable(sync_before_agent, async_before_agent, trace=False)1527            graph.add_node(1528                f"{m.name}.before_agent", before_agent_node, input_schema=resolved_state_schema1529            )15301531        if (1532            m.__class__.before_model is not AgentMiddleware.before_model1533            or m.__class__.abefore_model is not AgentMiddleware.abefore_model1534        ):1535            # Use RunnableCallable to support both sync and async1536            # Pass None for sync if not overridden to avoid signature conflicts1537            sync_before = (1538                m.before_model1539                if m.__class__.before_model is not AgentMiddleware.before_model1540                else None1541            )1542            async_before = (1543                m.abefore_model1544                if m.__class__.abefore_model is not AgentMiddleware.abefore_model1545                else None1546            )1547            before_node = RunnableCallable(sync_before, async_before, trace=False)1548            graph.add_node(1549                f"{m.name}.before_model", before_node, input_schema=resolved_state_schema1550            )15511552        if (1553            m.__class__.after_model is not AgentMiddleware.after_model1554            or m.__class__.aafter_model is not AgentMiddleware.aafter_model1555        ):1556            # Use RunnableCallable to support both sync and async1557            # Pass None for sync if not overridden to avoid signature conflicts1558            sync_after = (1559                m.after_model1560                if m.__class__.after_model is not AgentMiddleware.after_model1561                else None1562            )1563            async_after = (1564                m.aafter_model1565                if m.__class__.aafter_model is not AgentMiddleware.aafter_model1566                else None1567            )1568            after_node = RunnableCallable(sync_after, async_after, trace=False)1569            graph.add_node(f"{m.name}.after_model", after_node, input_schema=resolved_state_schema)15701571        if (1572            m.__class__.after_agent is not AgentMiddleware.after_agent1573            or m.__class__.aafter_agent is not AgentMiddleware.aafter_agent1574        ):1575            # Use RunnableCallable to support both sync and async1576            # Pass None for sync if not overridden to avoid signature conflicts1577            sync_after_agent = (1578                m.after_agent1579                if m.__class__.after_agent is not AgentMiddleware.after_agent1580                else None1581            )1582            async_after_agent = (1583                m.aafter_agent1584                if m.__class__.aafter_agent is not AgentMiddleware.aafter_agent1585                else None1586            )1587            after_agent_node = RunnableCallable(sync_after_agent, async_after_agent, trace=False)1588            graph.add_node(1589                f"{m.name}.after_agent", after_agent_node, input_schema=resolved_state_schema1590            )15911592    # Determine the entry node (runs once at start): before_agent -> before_model -> model1593    if middleware_w_before_agent:1594        entry_node = f"{middleware_w_before_agent[0].name}.before_agent"1595    elif middleware_w_before_model:1596        entry_node = f"{middleware_w_before_model[0].name}.before_model"1597    else:1598        entry_node = "model"15991600    # Determine the loop entry node (beginning of agent loop, excludes before_agent)1601    # This is where tools will loop back to for the next iteration1602    if middleware_w_before_model:1603        loop_entry_node = f"{middleware_w_before_model[0].name}.before_model"1604    else:1605        loop_entry_node = "model"16061607    # Determine the loop exit node (end of each iteration, can run multiple times)1608    # This is after_model or model, but NOT after_agent1609    if middleware_w_after_model:1610        loop_exit_node = f"{middleware_w_after_model[0].name}.after_model"1611    else:1612        loop_exit_node = "model"16131614    # Determine the exit node (runs once at end): after_agent or END1615    if middleware_w_after_agent:1616        exit_node = f"{middleware_w_after_agent[-1].name}.after_agent"1617    else:1618        exit_node = END16191620    graph.add_edge(START, entry_node)1621    # add conditional edges only if tools exist1622    if tool_node is not None:1623        # Only include exit_node in destinations if any tool has return_direct=True1624        # or if there are structured output tools1625        tools_to_model_destinations = [loop_entry_node]1626        if (1627            any(tool.return_direct for tool in tool_node.tools_by_name.values())1628            or structured_output_tools1629        ):1630            tools_to_model_destinations.append(exit_node)16311632        graph.add_conditional_edges(1633            "tools",1634            RunnableCallable(1635                _make_tools_to_model_edge(1636                    tool_node=tool_node,1637                    model_destination=loop_entry_node,1638                    structured_output_tools=structured_output_tools,1639                    end_destination=exit_node,1640                ),1641                trace=False,1642            ),1643            tools_to_model_destinations,1644        )16451646        # base destinations are tools and exit_node1647        # we add the loop_entry node to edge destinations if:1648        # - there is an after model hook(s) -- allows jump_to to model1649        #   potentially artificially injected tool messages, ex HITL1650        # - there is a response format -- to allow for jumping to model to handle1651        #   regenerating structured output tool calls1652        model_to_tools_destinations = ["tools", exit_node]1653        if response_format or loop_exit_node != "model":1654            model_to_tools_destinations.append(loop_entry_node)16551656        graph.add_conditional_edges(1657            loop_exit_node,1658            RunnableCallable(1659                _make_model_to_tools_edge(1660                    model_destination=loop_entry_node,1661                    structured_output_tools=structured_output_tools,1662                    end_destination=exit_node,1663                ),1664                trace=False,1665            ),1666            model_to_tools_destinations,1667        )1668    elif len(structured_output_tools) > 0:1669        graph.add_conditional_edges(1670            loop_exit_node,1671            RunnableCallable(1672                _make_model_to_model_edge(1673                    model_destination=loop_entry_node,1674                    end_destination=exit_node,1675                ),1676                trace=False,1677            ),1678            [loop_entry_node, exit_node],1679        )1680    elif loop_exit_node == "model":1681        # If no tools and no after_model, go directly to exit_node1682        graph.add_edge(loop_exit_node, exit_node)1683    # No tools but we have after_model - connect after_model to exit_node1684    else:1685        _add_middleware_edge(1686            graph,1687            name=f"{middleware_w_after_model[0].name}.after_model",1688            default_destination=exit_node,1689            model_destination=loop_entry_node,1690            end_destination=exit_node,1691            can_jump_to=_get_can_jump_to(middleware_w_after_model[0], "after_model"),1692        )16931694    # Add before_agent middleware edges1695    if middleware_w_before_agent:1696        for m1, m2 in itertools.pairwise(middleware_w_before_agent):1697            _add_middleware_edge(1698                graph,1699                name=f"{m1.name}.before_agent",1700                default_destination=f"{m2.name}.before_agent",1701                model_destination=loop_entry_node,1702                end_destination=exit_node,1703                can_jump_to=_get_can_jump_to(m1, "before_agent"),1704            )1705        # Connect last before_agent to loop_entry_node (before_model or model)1706        _add_middleware_edge(1707            graph,1708            name=f"{middleware_w_before_agent[-1].name}.before_agent",1709            default_destination=loop_entry_node,1710            model_destination=loop_entry_node,1711            end_destination=exit_node,1712            can_jump_to=_get_can_jump_to(middleware_w_before_agent[-1], "before_agent"),1713        )17141715    # Add before_model middleware edges1716    if middleware_w_before_model:1717        for m1, m2 in itertools.pairwise(middleware_w_before_model):1718            _add_middleware_edge(1719                graph,1720                name=f"{m1.name}.before_model",1721                default_destination=f"{m2.name}.before_model",1722                model_destination=loop_entry_node,1723                end_destination=exit_node,1724                can_jump_to=_get_can_jump_to(m1, "before_model"),1725            )1726        # Go directly to model after the last before_model1727        _add_middleware_edge(1728            graph,1729            name=f"{middleware_w_before_model[-1].name}.before_model",1730            default_destination="model",1731            model_destination=loop_entry_node,1732            end_destination=exit_node,1733            can_jump_to=_get_can_jump_to(middleware_w_before_model[-1], "before_model"),1734        )17351736    # Add after_model middleware edges1737    if middleware_w_after_model:1738        graph.add_edge("model", f"{middleware_w_after_model[-1].name}.after_model")1739        for idx in range(len(middleware_w_after_model) - 1, 0, -1):1740            m1 = middleware_w_after_model[idx]1741            m2 = middleware_w_after_model[idx - 1]1742            _add_middleware_edge(1743                graph,1744                name=f"{m1.name}.after_model",1745                default_destination=f"{m2.name}.after_model",1746                model_destination=loop_entry_node,1747                end_destination=exit_node,1748                can_jump_to=_get_can_jump_to(m1, "after_model"),1749            )1750        # Note: Connection from after_model to after_agent/END is handled above1751        # in the conditional edges section17521753    # Add after_agent middleware edges1754    if middleware_w_after_agent:1755        # Chain after_agent middleware (runs once at the very end, before END)1756        for idx in range(len(middleware_w_after_agent) - 1, 0, -1):1757            m1 = middleware_w_after_agent[idx]1758            m2 = middleware_w_after_agent[idx - 1]1759            _add_middleware_edge(1760                graph,1761                name=f"{m1.name}.after_agent",1762                default_destination=f"{m2.name}.after_agent",1763                model_destination=loop_entry_node,1764                end_destination=exit_node,1765                can_jump_to=_get_can_jump_to(m1, "after_agent"),1766            )17671768        # Connect the last after_agent to END1769        _add_middleware_edge(1770            graph,1771            name=f"{middleware_w_after_agent[0].name}.after_agent",1772            default_destination=END,1773            model_destination=loop_entry_node,1774            end_destination=exit_node,1775            can_jump_to=_get_can_jump_to(middleware_w_after_agent[0], "after_agent"),1776        )17771778    # Set recursion limit to 9_9991779    # https://github.com/langchain-ai/langgraph/issues/73131780    config: RunnableConfig = {"recursion_limit": 9_999}1781    config["metadata"] = {"ls_integration": "langchain_create_agent"}1782    if name:1783        config["metadata"]["lc_agent_name"] = name17841785    middleware_transformers = [t for m in middleware for t in getattr(m, "transformers", ())]17861787    return graph.compile(1788        checkpointer=checkpointer,1789        store=store,1790        interrupt_before=interrupt_before,1791        interrupt_after=interrupt_after,1792        debug=debug,1793        name=name,1794        cache=cache,1795        transformers=[1796            ToolCallTransformer,1797            SubagentTransformer,1798            *middleware_transformers,1799            *(transformers or ()),1800        ],1801    ).with_config(config)180218031804def _resolve_jump(1805    jump_to: JumpTo | None,1806    *,1807    model_destination: str,1808    end_destination: str,1809) -> str | None:1810    if jump_to == "model":1811        return model_destination1812    if jump_to == "end":1813        return end_destination1814    if jump_to == "tools":1815        return "tools"1816    return None181718181819def _fetch_last_ai_and_tool_messages(1820    messages: list[AnyMessage],1821) -> tuple[AIMessage | None, list[ToolMessage]]:1822    """Return the last AI message and any subsequent tool messages.18231824    Args:1825        messages: List of messages to search through.18261827    Returns:1828        A tuple of (last_ai_message, tool_messages). If no AIMessage is found,1829        returns (None, []). Callers must handle the None case appropriately.1830    """1831    for i in range(len(messages) - 1, -1, -1):1832        if isinstance(messages[i], AIMessage):1833            last_ai_message = cast("AIMessage", messages[i])1834            tool_messages = [m for m in messages[i + 1 :] if isinstance(m, ToolMessage)]1835            return last_ai_message, tool_messages18361837    return None, []183818391840def _make_model_to_tools_edge(1841    *,1842    model_destination: str,1843    structured_output_tools: dict[str, OutputToolBinding[Any]],1844    end_destination: str,1845) -> Callable[[dict[str, Any]], str | list[Send] | None]:1846    def model_to_tools(1847        state: dict[str, Any],1848    ) -> str | list[Send] | None:1849        # 1. If there's an explicit jump_to in the state, use it1850        if jump_to := state.get("jump_to"):1851            return _resolve_jump(1852                jump_to,1853                model_destination=model_destination,1854                end_destination=end_destination,1855            )18561857        last_ai_message, tool_messages = _fetch_last_ai_and_tool_messages(state["messages"])18581859        # 2. if no AIMessage exists (e.g., messages were cleared), exit the loop1860        if last_ai_message is None:1861            return end_destination18621863        tool_message_ids = [m.tool_call_id for m in tool_messages]18641865        # 3. If the model hasn't called any tools, exit the loop1866        # this is the classic exit condition for an agent loop1867        if len(last_ai_message.tool_calls) == 0:1868            return end_destination18691870        pending_tool_calls = [1871            c1872            for c in last_ai_message.tool_calls1873            if c["id"] not in tool_message_ids and c["name"] not in structured_output_tools1874        ]18751876        # 4. If there are pending tool calls, jump to the tool node.1877        # The tool node hydrates ToolRuntime.state from channels via1878        # CONFIG_KEY_READ at execution time, so we no longer inline the1879        # full state into each Send (previously O(N^2) in TASKS writes).1880        if pending_tool_calls:1881            return [Send("tools", [tool_call]) for tool_call in pending_tool_calls]18821883        # 5. If there is a structured response, exit the loop1884        if "structured_response" in state:1885            return end_destination18861887        # 6. AIMessage has tool calls, but there are no pending tool calls which suggests1888        # the injection of artificial tool messages. Jump to the model node1889        return model_destination18901891    return model_to_tools189218931894def _make_model_to_model_edge(1895    *,1896    model_destination: str,1897    end_destination: str,1898) -> Callable[[dict[str, Any]], str | list[Send] | None]:1899    def model_to_model(1900        state: dict[str, Any],1901    ) -> str | list[Send] | None:1902        # 1. Priority: Check for explicit jump_to directive from middleware1903        if jump_to := state.get("jump_to"):1904            return _resolve_jump(1905                jump_to,1906                model_destination=model_destination,1907                end_destination=end_destination,1908            )19091910        # 2. Exit condition: A structured response was generated1911        if "structured_response" in state:1912            return end_destination19131914        # 3. Default: Continue the loop, there may have been an issue with structured1915        # output generation, so we need to retry1916        return model_destination19171918    return model_to_model191919201921def _make_tools_to_model_edge(1922    *,1923    tool_node: ToolNode,1924    model_destination: str,1925    structured_output_tools: dict[str, OutputToolBinding[Any]],1926    end_destination: str,1927) -> Callable[[dict[str, Any]], str | None]:1928    def tools_to_model(state: dict[str, Any]) -> str | None:1929        last_ai_message, tool_messages = _fetch_last_ai_and_tool_messages(state["messages"])19301931        # 1. If no AIMessage exists (e.g., messages were cleared), route to model1932        if last_ai_message is None:1933            return model_destination19341935        # 2. Exit condition: All executed tools have return_direct=True1936        # Filter to only client-side tools (provider tools are not in tool_node)1937        client_side_tool_calls = [1938            c for c in last_ai_message.tool_calls if c["name"] in tool_node.tools_by_name1939        ]1940        if client_side_tool_calls and all(1941            tool_node.tools_by_name[c["name"]].return_direct for c in client_side_tool_calls1942        ):1943            return end_destination19441945        # 3. Exit condition: A structured output tool was executed1946        if any(t.name in structured_output_tools for t in tool_messages):1947            return end_destination19481949        # 4. Default: Continue the loop1950        #    Tool execution completed successfully, route back to the model1951        #    so it can process the tool results and decide the next action.1952        return model_destination19531954    return tools_to_model195519561957def _add_middleware_edge(1958    graph: StateGraph[1959        AgentState[ResponseT], ContextT, InputAgentState, OutputAgentState[ResponseT]1960    ],1961    *,1962    name: str,1963    default_destination: str,1964    model_destination: str,1965    end_destination: str,1966    can_jump_to: list[JumpTo] | None,1967) -> None:1968    """Add an edge to the graph for a middleware node.19691970    Args:1971        graph: The graph to add the edge to.1972        name: The name of the middleware node.1973        default_destination: The default destination for the edge.1974        model_destination: The destination for the edge to the model.1975        end_destination: The destination for the edge to the end.1976        can_jump_to: The conditionally jumpable destinations for the edge.1977    """1978    if can_jump_to:19791980        def jump_edge(state: dict[str, Any]) -> str:1981            return (1982                _resolve_jump(1983                    state.get("jump_to"),1984                    model_destination=model_destination,1985                    end_destination=end_destination,1986                )1987                or default_destination1988            )19891990        destinations = [default_destination]19911992        if "end" in can_jump_to:1993            destinations.append(end_destination)1994        if "tools" in can_jump_to:1995            destinations.append("tools")1996        if "model" in can_jump_to and name != model_destination:1997            destinations.append(model_destination)19981999        graph.add_conditional_edges(name, RunnableCallable(jump_edge, trace=False), destinations)

Code quality findings 71

Ensure functions have docstrings for documentation
missing-docstring
def wrap_tool_call(self, request, handler):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(req, (ModelRequest, ToolCallRequest)):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(result, AIMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(result, ExtendedModelResponse):
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
commands: list[Command[Any]] = list(extra_commands or [])
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(result, _ComposedExtendedModelResponse):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(result, ExtendedModelResponse):
Ensure functions have docstrings for documentation
missing-docstring
def normalized_single(
Ensure functions have docstrings for documentation
missing-docstring
def compose_two(
Ensure functions have docstrings for documentation
missing-docstring
def composed(
Ensure functions have docstrings for documentation
missing-docstring
def inner_handler(req: ModelRequest[ContextT]) -> ModelResponse:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(inner_result, _ComposedExtendedModelResponse):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(inner_result, ExtendedModelResponse):
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
commands: list[Command[Any]] = list(extra_commands or [])
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(result, _ComposedExtendedModelResponse):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(result, ExtendedModelResponse):
Ensure functions have docstrings for documentation
missing-docstring
async def normalized_single(
Ensure functions have docstrings for documentation
missing-docstring
def compose_two(
Ensure functions have docstrings for documentation
missing-docstring
async def composed(
Ensure functions have docstrings for documentation
missing-docstring
async def inner_handler(req: ModelRequest[ContextT]) -> ModelResponse:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(inner_result, _ComposedExtendedModelResponse):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(inner_result, ExtendedModelResponse):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(meta, OmitFromSchema) and getattr(meta, omit_flag) is True:
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
return list(get_args(inner_type)[1:])
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
return list(get_args(type_)[1:])
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(model, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(model, BaseChatModel):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(model_name, str)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
return isinstance(model, base_chat_openai.BaseChatOpenAI)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(response_format, ToolStrategy):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(handle_errors, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(handle_errors, type):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if issubclass(handle_errors, Exception) and isinstance(exception, handle_errors):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(handle_errors, tuple):
Ensure functions have docstrings for documentation
missing-docstring
def composed(
Ensure functions have docstrings for documentation
missing-docstring
def call_inner(req: ToolCallRequest) -> ToolMessage | Command[Any]:
Ensure functions have docstrings for documentation
missing-docstring
def compose_two(
Ensure functions have docstrings for documentation
missing-docstring
async def composed(
Ensure functions have docstrings for documentation
missing-docstring
async def call_inner(req: ToolCallRequest) -> ToolMessage | Command[Any]:
Ensure functions have docstrings for documentation
missing-docstring
def create_agent(
Ensure functions have docstrings for documentation
missing-docstring
def create_agent(
Ensure functions have docstrings for documentation
missing-docstring
def create_agent(
Ensure functions have docstrings for documentation
missing-docstring
def create_agent(
Use logging module for better control and configurability
print-statement
print(chunk)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(model, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(system_prompt, SystemMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(response_format, (ToolStrategy, ProviderStrategy, AutoStrategy)):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(initial_response_format, AutoStrategy):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(initial_response_format, ToolStrategy):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
built_in_tools = [t for t in tools if isinstance(t, dict)]
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
regular_tools = [t for t in tools if not isinstance(t, dict)]
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
default_tools = list(tool_node.tools_by_name.values()) + built_in_tools
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
default_tools = list(built_in_tools)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(effective_response_format, ProviderStrategy):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(effective_response_format, ToolStrategy)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(output, AIMessage)
Ensure try blocks have corresponding except or finally blocks
try-without-except
try:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(t, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(t, BaseTool) and t.name not in available_tools_by_name:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if response_format is not None and not isinstance(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(response_format, AutoStrategy):
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
final_tools = list(request.tools)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(effective_response_format, ToolStrategy):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(effective_response_format, ProviderStrategy):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(effective_response_format, ToolStrategy):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(messages[i], AIMessage):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
tool_messages = [m for m in messages[i + 1 :] if isinstance(m, ToolMessage)]
Ensure functions have docstrings for documentation
missing-docstring
def model_to_tools(
Ensure functions have docstrings for documentation
missing-docstring
def model_to_model(
Ensure functions have docstrings for documentation
missing-docstring
def tools_to_model(state: dict[str, Any]) -> str | None:
Ensure functions have docstrings for documentation
missing-docstring
def jump_edge(state: dict[str, Any]) -> str:

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.