libs/langchain_v1/langchain/agents/factory.py PYTHON 1,982 lines View on github.com → Search inside
1"""Agent factory for creating agents with middleware support."""23from __future__ import annotations45import functools6import itertools7import re8from dataclasses import dataclass, field, fields9from typing import (10    TYPE_CHECKING,11    Annotated,12    Any,13    Generic,14    cast,15    get_args,16    get_origin,17    get_type_hints,18)1920from langchain_core.language_models.chat_models import BaseChatModel21from langchain_core.messages import AIMessage, AnyMessage, SystemMessage, ToolMessage22from langchain_core.tools import BaseTool23from langgraph._internal._runnable import RunnableCallable24from langgraph.constants import END, START25from langgraph.graph.state import StateGraph26from langgraph.prebuilt import ToolCallTransformer27from langgraph.prebuilt.tool_node import ToolNode28from langgraph.types import Command, Send29from langsmith import traceable30from typing_extensions import NotRequired, Required, TypedDict, overload3132from langchain.agents._subagent_transformer import SubagentTransformer33from langchain.agents.middleware.types import (34    AgentMiddleware,35    AgentState,36    ContextT,37    ExtendedModelResponse,38    InputAgentState,39    JumpTo,40    ModelRequest,41    ModelResponse,42    OmitFromSchema,43    OutputAgentState,44    ResponseT,45    StateT_co,46    ToolCallRequest,47)48from langchain.agents.structured_output import (49    AutoStrategy,50    MultipleStructuredOutputsError,51    OutputToolBinding,52    ProviderStrategy,53    ProviderStrategyBinding,54    ResponseFormat,55    StructuredOutputError,56    StructuredOutputValidationError,57    ToolStrategy,58)59from langchain.chat_models import init_chat_model606162@dataclass63class _ComposedExtendedModelResponse(Generic[ResponseT]):64    """Internal result from composed `wrap_model_call` middleware.6566    Unlike `ExtendedModelResponse` (user-facing, single command), this holds the67    full list of commands accumulated across all middleware layers during68    composition.69    """7071    model_response: ModelResponse[ResponseT]72    """The underlying model response."""7374    commands: list[Command[Any]] = field(default_factory=list)75    """Commands accumulated from all middleware layers (inner-first, then outer)."""767778if TYPE_CHECKING:79    from collections.abc import Awaitable, Callable, Sequence8081    from langchain_core.runnables import Runnable, RunnableConfig82    from langgraph.cache.base import BaseCache83    from langgraph.graph.state import CompiledStateGraph84    from langgraph.runtime import Runtime85    from langgraph.store.base import BaseStore86    from langgraph.stream._mux import TransformerFactory87    from langgraph.types import Checkpointer8889    from langchain.agents.middleware.types import ToolCallWrapper9091    _ModelCallHandler = Callable[92        [ModelRequest[ContextT], Callable[[ModelRequest[ContextT]], ModelResponse]],93        ModelResponse | AIMessage | ExtendedModelResponse,94    ]9596    _ComposedModelCallHandler = Callable[97        [ModelRequest[ContextT], Callable[[ModelRequest[ContextT]], ModelResponse]],98        _ComposedExtendedModelResponse,99    ]100101    _AsyncModelCallHandler = Callable[102        [ModelRequest[ContextT], Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse]]],103        Awaitable[ModelResponse | AIMessage | ExtendedModelResponse],104    ]105106    _ComposedAsyncModelCallHandler = Callable[107        [ModelRequest[ContextT], Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse]]],108        Awaitable[_ComposedExtendedModelResponse],109    ]110111112STRUCTURED_OUTPUT_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes."113114DYNAMIC_TOOL_ERROR_TEMPLATE = """115Middleware added tools that the agent doesn't know how to execute.116117Unknown tools: {unknown_tool_names}118Registered tools: {available_tool_names}119120This happens when middleware modifies `request.tools` in `wrap_model_call` to include121tools that weren't passed to `create_agent()`.122123How to fix this:124125Option 1: Register tools at agent creation (recommended for most cases)126    Pass the tools to `create_agent(tools=[...])` or set them on `middleware.tools`.127    This makes tools available for every agent invocation.128129Option 2: Handle dynamic tools in middleware (for tools created at runtime)130    Implement `wrap_tool_call` to execute tools that are added dynamically:131132    class MyMiddleware(AgentMiddleware):133        def wrap_tool_call(self, request, handler):134            if request.tool_call["name"] == "dynamic_tool":135                # Execute the dynamic tool yourself or override with tool instance136                return handler(request.override(tool=my_dynamic_tool))137            return handler(request)138""".strip()139140141def _scrub_inputs(inputs: dict[str, Any]) -> dict[str, Any]:142    """Remove `runtime` and `handler` from trace inputs before sending to LangSmith."""143    filtered = inputs.copy()144    filtered.pop("handler", None)145    req = filtered.get("request")146    if isinstance(req, (ModelRequest, ToolCallRequest)):147        filtered["request"] = {148            f.name: getattr(req, f.name) for f in fields(req) if f.name != "runtime"149        }150    return filtered151152153FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT = [154    # If model profile data are not available, model names matching these patterns155    # are assumed to support provider-native structured output. These are regexes156    # so matches stay bounded to model-name segments instead of arbitrary substrings.157    r"(^|[/:.])gpt-4\.1($|[-/:])",158    r"(^|[/:.])gpt-4o($|[-/:])",159    r"(^|[/:.])gpt-5($|[-/:])",160    r"(^|[/:.])gpt-5\.1($|[-/:])",161    r"(^|[/:.])gpt-5\.2($|[/:])",162    r"(^|[/:.])gpt-5\.2-(chat|codex)($|[-/:])",163    r"(^|[/:.])gpt-5\.3($|[-/:])",164    r"(^|[/:.])gpt-5\.4($|[/:])",165    r"(^|[/:.])gpt-5\.4-(mini|nano)($|[-/:])",166    r"(^|[/:.])gpt-5\.5($|[-/:])",167    r"(^|[/:.])claude-(fable|mythos)-5(?:-\d{8})?(?:-v\d(?::\d)?)?($|[/:])",168    r"(^|[/:.])claude-haiku-4-5(?:-\d{8})?(?:-v\d(?::\d)?)?($|[/:])",169    r"(^|[/:.])claude-opus-4-(5|6|7|8)(?:-\d{8})?(?:-v\d(?::\d)?)?($|[/:])",170    r"(^|[/:.])claude-sonnet-4-(5|6)(?:-\d{8})?(?:-v\d(?::\d)?)?($|[/:])",171    r"(^|[/:.])grok-4($|[-.:/])",172    r"(^|[/:.])grok-build($|[-/:])",173]174175176def _normalize_to_model_response(177    result: ModelResponse | AIMessage | ExtendedModelResponse,178) -> ModelResponse:179    """Normalize middleware return value to ModelResponse.180181    At inner composition boundaries, `ExtendedModelResponse` is unwrapped to its182    underlying `ModelResponse` so that inner middleware always sees `ModelResponse`183    from the handler.184    """185    if isinstance(result, AIMessage):186        return ModelResponse(result=[result], structured_response=None)187    if isinstance(result, ExtendedModelResponse):188        return result.model_response189    return result190191192def _build_commands(193    model_response: ModelResponse,194    middleware_commands: list[Command[Any]] | None = None,195) -> list[Command[Any]]:196    """Build a list of Commands from a model response and middleware commands.197198    The first Command contains the model response state (messages and optional199    structured_response). Middleware commands are appended as-is.200201    Args:202        model_response: The model response containing messages and optional203            structured output.204        middleware_commands: Commands accumulated from middleware layers during205            composition (inner-first ordering).206207    Returns:208        List of `Command` objects ready to be returned from a model node.209    """210    state: dict[str, Any] = {"messages": model_response.result}211212    if model_response.structured_response is not None:213        state["structured_response"] = model_response.structured_response214215    for cmd in middleware_commands or []:216        if cmd.goto:217            msg = (218                "Command goto is not yet supported in wrap_model_call middleware. "219                "Use the jump_to state field with before_model/after_model hooks instead."220            )221            raise NotImplementedError(msg)222        if cmd.resume:223            msg = "Command resume is not yet supported in wrap_model_call middleware."224            raise NotImplementedError(msg)225        if cmd.graph:226            msg = "Command graph is not yet supported in wrap_model_call middleware."227            raise NotImplementedError(msg)228229    commands: list[Command[Any]] = [Command(update=state)]230    commands.extend(middleware_commands or [])231    return commands232233234def _chain_model_call_handlers(235    handlers: Sequence[_ModelCallHandler[ContextT]],236) -> _ComposedModelCallHandler[ContextT] | None:237    """Compose multiple `wrap_model_call` handlers into single middleware stack.238239    Composes handlers so first in list becomes outermost layer. Each handler receives a240    handler callback to execute inner layers. Commands from each layer are accumulated241    into a list (inner-first, then outer) without merging.242243    Args:244        handlers: List of handlers.245246            First handler wraps all others.247248    Returns:249        Composed handler returning `_ComposedExtendedModelResponse`,250        or `None` if handlers empty.251    """252    if not handlers:253        return None254255    def _to_composed_result(256        result: ModelResponse | AIMessage | ExtendedModelResponse | _ComposedExtendedModelResponse,257        extra_commands: list[Command[Any]] | None = None,258    ) -> _ComposedExtendedModelResponse:259        """Normalize any handler result to _ComposedExtendedModelResponse."""260        commands: list[Command[Any]] = list(extra_commands or [])261        if isinstance(result, _ComposedExtendedModelResponse):262            commands.extend(result.commands)263            model_response = result.model_response264        elif isinstance(result, ExtendedModelResponse):265            model_response = result.model_response266            if result.command is not None:267                commands.append(result.command)268        else:269            model_response = _normalize_to_model_response(result)270271        return _ComposedExtendedModelResponse(model_response=model_response, commands=commands)272273    if len(handlers) == 1:274        single_handler = handlers[0]275276        def normalized_single(277            request: ModelRequest[ContextT],278            handler: Callable[[ModelRequest[ContextT]], ModelResponse],279        ) -> _ComposedExtendedModelResponse:280            return _to_composed_result(single_handler(request, handler))281282        return normalized_single283284    def compose_two(285        outer: _ModelCallHandler[ContextT] | _ComposedModelCallHandler[ContextT],286        inner: _ModelCallHandler[ContextT] | _ComposedModelCallHandler[ContextT],287    ) -> _ComposedModelCallHandler[ContextT]:288        """Compose two handlers where outer wraps inner."""289290        def composed(291            request: ModelRequest[ContextT],292            handler: Callable[[ModelRequest[ContextT]], ModelResponse],293        ) -> _ComposedExtendedModelResponse:294            # Closure variable to capture inner's commands before normalizing295            accumulated_commands: list[Command[Any]] = []296297            def inner_handler(req: ModelRequest[ContextT]) -> ModelResponse:298                # Clear on each call for retry safety299                accumulated_commands.clear()300                inner_result = inner(req, handler)301                if isinstance(inner_result, _ComposedExtendedModelResponse):302                    accumulated_commands.extend(inner_result.commands)303                    return inner_result.model_response304                if isinstance(inner_result, ExtendedModelResponse):305                    if inner_result.command is not None:306                        accumulated_commands.append(inner_result.command)307                    return inner_result.model_response308                return _normalize_to_model_response(inner_result)309310            outer_result = outer(request, inner_handler)311            return _to_composed_result(312                outer_result,313                extra_commands=accumulated_commands or None,314            )315316        return composed317318    # Compose right-to-left: outer(inner(innermost(handler)))319    composed_handler = compose_two(handlers[-2], handlers[-1])320    for h in reversed(handlers[:-2]):321        composed_handler = compose_two(h, composed_handler)322323    return composed_handler324325326def _chain_async_model_call_handlers(327    handlers: Sequence[_AsyncModelCallHandler[ContextT]],328) -> _ComposedAsyncModelCallHandler[ContextT] | None:329    """Compose multiple async `wrap_model_call` handlers into single middleware stack.330331    Commands from each layer are accumulated into a list (inner-first, then outer)332    without merging.333334    Args:335        handlers: List of async handlers.336337            First handler wraps all others.338339    Returns:340        Composed async handler returning `_ComposedExtendedModelResponse`,341        or `None` if handlers empty.342    """343    if not handlers:344        return None345346    def _to_composed_result(347        result: ModelResponse | AIMessage | ExtendedModelResponse | _ComposedExtendedModelResponse,348        extra_commands: list[Command[Any]] | None = None,349    ) -> _ComposedExtendedModelResponse:350        """Normalize any handler result to _ComposedExtendedModelResponse."""351        commands: list[Command[Any]] = list(extra_commands or [])352        if isinstance(result, _ComposedExtendedModelResponse):353            commands.extend(result.commands)354            model_response = result.model_response355        elif isinstance(result, ExtendedModelResponse):356            model_response = result.model_response357            if result.command is not None:358                commands.append(result.command)359        else:360            model_response = _normalize_to_model_response(result)361362        return _ComposedExtendedModelResponse(model_response=model_response, commands=commands)363364    if len(handlers) == 1:365        single_handler = handlers[0]366367        async def normalized_single(368            request: ModelRequest[ContextT],369            handler: Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse]],370        ) -> _ComposedExtendedModelResponse:371            return _to_composed_result(await single_handler(request, handler))372373        return normalized_single374375    def compose_two(376        outer: _AsyncModelCallHandler[ContextT] | _ComposedAsyncModelCallHandler[ContextT],377        inner: _AsyncModelCallHandler[ContextT] | _ComposedAsyncModelCallHandler[ContextT],378    ) -> _ComposedAsyncModelCallHandler[ContextT]:379        """Compose two async handlers where outer wraps inner."""380381        async def composed(382            request: ModelRequest[ContextT],383            handler: Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse]],384        ) -> _ComposedExtendedModelResponse:385            # Closure variable to capture inner's commands before normalizing386            accumulated_commands: list[Command[Any]] = []387388            async def inner_handler(req: ModelRequest[ContextT]) -> ModelResponse:389                # Clear on each call for retry safety390                accumulated_commands.clear()391                inner_result = await inner(req, handler)392                if isinstance(inner_result, _ComposedExtendedModelResponse):393                    accumulated_commands.extend(inner_result.commands)394                    return inner_result.model_response395                if isinstance(inner_result, ExtendedModelResponse):396                    if inner_result.command is not None:397                        accumulated_commands.append(inner_result.command)398                    return inner_result.model_response399                return _normalize_to_model_response(inner_result)400401            outer_result = await outer(request, inner_handler)402            return _to_composed_result(403                outer_result,404                extra_commands=accumulated_commands or None,405            )406407        return composed408409    # Compose right-to-left: outer(inner(innermost(handler)))410    composed_handler = compose_two(handlers[-2], handlers[-1])411    for h in reversed(handlers[:-2]):412        composed_handler = compose_two(h, composed_handler)413414    return composed_handler415416417@functools.lru_cache(maxsize=100)418def _get_schema_type_hints(schema: type) -> dict[str, Any]:419    """Return cached type hints for a schema."""420    return get_type_hints(schema, include_extras=True)421422423def _resolve_schemas(schemas: list[type]) -> tuple[type, type, type]:424    """Resolve state, input, and output schemas for the given schemas.425426    Schemas are merged in list order; later entries override earlier ones when the427    same field is declared by multiple schemas.  Duplicates are harmless  a type428    that appears more than once is processed at its last position.429    """430    schema_hints = {schema: _get_schema_type_hints(schema) for schema in schemas}431    return (432        _resolve_schema(schema_hints, "StateSchema", None),433        _resolve_schema(schema_hints, "InputSchema", "input"),434        _resolve_schema(schema_hints, "OutputSchema", "output"),435    )436437438def _resolve_schema(439    schema_hints: dict[type, dict[str, Any]],440    schema_name: str,441    omit_flag: str | None = None,442) -> type:443    """Resolve schema by merging schemas and optionally respecting `OmitFromSchema` annotations.444445    Args:446        schema_hints: Resolved schema annotations to merge447        schema_name: Name for the generated `TypedDict`448        omit_flag: If specified, omit fields with this flag set (`'input'` or449            `'output'`)450451    Returns:452        Merged schema as `TypedDict`453    """454    all_annotations = {}455456    for hints in schema_hints.values():457        for field_name, field_type in hints.items():458            should_omit = False459460            if omit_flag:461                metadata = _extract_metadata(field_type)462                for meta in metadata:463                    if isinstance(meta, OmitFromSchema) and getattr(meta, omit_flag) is True:464                        should_omit = True465                        break466467            if not should_omit:468                all_annotations[field_name] = field_type469470    # `TypedDict` dynamically creates a class, but type checkers don't infer that471    # the runtime result satisfies this function's `type` return contract.472    return cast("type", TypedDict(schema_name, all_annotations))  # type: ignore[operator]473474475def _extract_metadata(type_: type) -> list[Any]:476    """Extract metadata from a field type, handling `Required`/`NotRequired` and `Annotated` wrappers."""  # noqa: E501477    # Handle Required[Annotated[...]] or NotRequired[Annotated[...]]478    if get_origin(type_) in {Required, NotRequired}:479        inner_type = get_args(type_)[0]480        if get_origin(inner_type) is Annotated:481            return list(get_args(inner_type)[1:])482483    # Handle direct Annotated[...]484    elif get_origin(type_) is Annotated:485        return list(get_args(type_)[1:])486487    return []488489490def _get_can_jump_to(middleware: AgentMiddleware[Any, Any], hook_name: str) -> list[JumpTo]:491    """Get the `can_jump_to` list from either sync or async hook methods.492493    Args:494        middleware: The middleware instance to inspect.495        hook_name: The name of the hook (`'before_model'` or `'after_model'`).496497    Returns:498        List of jump destinations, or empty list if not configured.499    """500    # Get the base class method for comparison501    base_sync_method = getattr(AgentMiddleware, hook_name, None)502    base_async_method = getattr(AgentMiddleware, f"a{hook_name}", None)503504    # Try sync method first - only if it's overridden from base class505    sync_method = getattr(middleware.__class__, hook_name, None)506    if (507        sync_method508        and sync_method is not base_sync_method509        and hasattr(sync_method, "__can_jump_to__")510    ):511        # `hasattr` proves the metadata exists at runtime, but not its value type.512        return cast("list[JumpTo]", sync_method.__can_jump_to__)513514    # Try async method - only if it's overridden from base class515    async_method = getattr(middleware.__class__, f"a{hook_name}", None)516    if (517        async_method518        and async_method is not base_async_method519        and hasattr(async_method, "__can_jump_to__")520    ):521        # `hasattr` proves the metadata exists at runtime, but not its value type.522        return cast("list[JumpTo]", async_method.__can_jump_to__)523524    return []525526527def _supports_provider_strategy(528    model: str | BaseChatModel, tools: list[BaseTool | dict[str, Any]] | None = None529) -> bool:530    """Check if a model supports provider-specific structured output.531532    Args:533        model: Model name string or `BaseChatModel` instance.534        tools: Optional list of tools provided to the agent.535536            Needed because some models don't support structured output together with tool calling.537538    Returns:539        `True` if the model supports provider-specific structured output, `False` otherwise.540    """541    model_name: str | None = None542    if isinstance(model, str):543        model_name = model544    elif isinstance(model, BaseChatModel):545        model_name = (546            getattr(model, "model_name", None)547            or getattr(model, "model", None)548            or getattr(model, "model_id", "")549        )550        model_profile = model.profile551        if (552            model_profile is not None553            and model_profile.get("structured_output")554            # We make an exception for Gemini < 3-series models, which currently do not support555            # simultaneous tool use with structured output; 3-series can.556            and not (557                tools558                and isinstance(model_name, str)559                and "gemini" in model_name.lower()560                and "gemini-3" not in model_name.lower()561            )562        ):563            return True564565    return (566        any(567            re.search(pattern, model_name.lower())568            for pattern in FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT569        )570        if model_name571        else False572    )573574575def _handle_structured_output_error(576    exception: Exception,577    response_format: ResponseFormat[Any],578) -> tuple[bool, str]:579    """Handle structured output error.580581    Returns `(should_retry, retry_tool_message)`.582    """583    if not isinstance(response_format, ToolStrategy):584        return False, ""585586    handle_errors = response_format.handle_errors587588    if handle_errors is False:589        return False, ""590    if handle_errors is True:591        return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))592    if isinstance(handle_errors, str):593        return True, handle_errors594    if isinstance(handle_errors, type):595        if issubclass(handle_errors, Exception) and isinstance(exception, handle_errors):596            return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))597        return False, ""598    if isinstance(handle_errors, tuple):599        if any(isinstance(exception, exc_type) for exc_type in handle_errors):600            return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))601        return False, ""602    return True, handle_errors(exception)603604605def _chain_tool_call_wrappers(606    wrappers: Sequence[ToolCallWrapper],607) -> ToolCallWrapper | None:608    """Compose wrappers into middleware stack (first = outermost).609610    Args:611        wrappers: Wrappers in middleware order.612613    Returns:614        Composed wrapper, or `None` if empty.615616    Example:617        ```python618        wrapper = _chain_tool_call_wrappers([auth, cache, retry])619        # Request flows: auth -> cache -> retry -> tool620        # Response flows: tool -> retry -> cache -> auth621        ```622    """623    if not wrappers:624        return None625626    if len(wrappers) == 1:627        return wrappers[0]628629    def compose_two(outer: ToolCallWrapper, inner: ToolCallWrapper) -> ToolCallWrapper:630        """Compose two wrappers where outer wraps inner."""631632        def composed(633            request: ToolCallRequest,634            execute: Callable[[ToolCallRequest], ToolMessage | Command[Any]],635        ) -> ToolMessage | Command[Any]:636            # Create a callable that invokes inner with the original execute637            def call_inner(req: ToolCallRequest) -> ToolMessage | Command[Any]:638                return inner(req, execute)639640            # Outer can call call_inner multiple times641            return outer(request, call_inner)642643        return composed644645    # Chain all wrappers: first -> second -> ... -> last646    result = wrappers[-1]647    for wrapper in reversed(wrappers[:-1]):648        result = compose_two(wrapper, result)649650    return result651652653def _chain_async_tool_call_wrappers(654    wrappers: Sequence[655        Callable[656            [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],657            Awaitable[ToolMessage | Command[Any]],658        ]659    ],660) -> (661    Callable[662        [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],663        Awaitable[ToolMessage | Command[Any]],664    ]665    | None666):667    """Compose async wrappers into middleware stack (first = outermost).668669    Args:670        wrappers: Async wrappers in middleware order.671672    Returns:673        Composed async wrapper, or `None` if empty.674    """675    if not wrappers:676        return None677678    if len(wrappers) == 1:679        return wrappers[0]680681    def compose_two(682        outer: Callable[683            [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],684            Awaitable[ToolMessage | Command[Any]],685        ],686        inner: Callable[687            [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],688            Awaitable[ToolMessage | Command[Any]],689        ],690    ) -> Callable[691        [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],692        Awaitable[ToolMessage | Command[Any]],693    ]:694        """Compose two async wrappers where outer wraps inner."""695696        async def composed(697            request: ToolCallRequest,698            execute: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],699        ) -> ToolMessage | Command[Any]:700            # Create an async callable that invokes inner with the original execute701            async def call_inner(req: ToolCallRequest) -> ToolMessage | Command[Any]:702                return await inner(req, execute)703704            # Outer can call call_inner multiple times705            return await outer(request, call_inner)706707        return composed708709    # Chain all wrappers: first -> second -> ... -> last710    result = wrappers[-1]711    for wrapper in reversed(wrappers[:-1]):712        result = compose_two(wrapper, result)713714    return result715716717# No `response_format`: there is no structured output, so `ResponseT` resolves to `Any`.718@overload719def create_agent(720    model: str | BaseChatModel,721    tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,722    *,723    system_prompt: str | SystemMessage | None = None,724    middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),725    response_format: None = None,726    state_schema: None = None,727    context_schema: type[ContextT] | None = None,728    checkpointer: Checkpointer | None = None,729    store: BaseStore | None = None,730    interrupt_before: list[str] | None = None,731    interrupt_after: list[str] | None = None,732    debug: bool = False,733    name: str | None = None,734    cache: BaseCache[Any] | None = None,735    transformers: Sequence[TransformerFactory] | None = None,736) -> CompiledStateGraph[AgentState[Any], ContextT, InputAgentState, OutputAgentState[Any]]: ...737738739# Raw-dict `response_format`: structured output is an untyped `dict[str, Any]`.740@overload741def create_agent(742    model: str | BaseChatModel,743    tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,744    *,745    system_prompt: str | SystemMessage | None = None,746    middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),747    response_format: dict[str, Any],748    state_schema: type[AgentState[dict[str, Any]]] | None = None,749    context_schema: type[ContextT] | None = None,750    checkpointer: Checkpointer | None = None,751    store: BaseStore | None = None,752    interrupt_before: list[str] | None = None,753    interrupt_after: list[str] | None = None,754    debug: bool = False,755    name: str | None = None,756    cache: BaseCache[Any] | None = None,757    transformers: Sequence[TransformerFactory] | None = None,758) -> CompiledStateGraph[759    AgentState[dict[str, Any]], ContextT, InputAgentState, OutputAgentState[dict[str, Any]]760]: ...761762763# Schema-typed `response_format`: `ResponseT` is inferred from the schema/type.764@overload765def create_agent(766    model: str | BaseChatModel,767    tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,768    *,769    system_prompt: str | SystemMessage | None = None,770    middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),771    response_format: ResponseFormat[ResponseT] | type[ResponseT] | None = None,772    state_schema: type[AgentState[ResponseT]] | None = None,773    context_schema: type[ContextT] | None = None,774    checkpointer: Checkpointer | None = None,775    store: BaseStore | None = None,776    interrupt_before: list[str] | None = None,777    interrupt_after: list[str] | None = None,778    debug: bool = False,779    name: str | None = None,780    cache: BaseCache[Any] | None = None,781    transformers: Sequence[TransformerFactory] | None = None,782) -> CompiledStateGraph[783    AgentState[ResponseT], ContextT, InputAgentState, OutputAgentState[ResponseT]784]: ...785786787def create_agent(788    model: str | BaseChatModel,789    tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,790    *,791    system_prompt: str | SystemMessage | None = None,792    middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),793    response_format: ResponseFormat[ResponseT] | type[ResponseT] | dict[str, Any] | None = None,794    state_schema: type[AgentState[ResponseT]] | None = None,795    context_schema: type[ContextT] | None = None,796    checkpointer: Checkpointer | None = None,797    store: BaseStore | None = None,798    interrupt_before: list[str] | None = None,799    interrupt_after: list[str] | None = None,800    debug: bool = False,801    name: str | None = None,802    cache: BaseCache[Any] | None = None,803    transformers: Sequence[TransformerFactory] | None = None,804) -> CompiledStateGraph[805    AgentState[ResponseT], ContextT, InputAgentState, OutputAgentState[ResponseT]806]:807    """Creates an agent graph that calls tools in a loop until a stopping condition is met.808809    For more details on using `create_agent`,810    visit the [Agents](https://docs.langchain.com/oss/python/langchain/agents) docs.811812    Args:813        model: The language model for the agent.814815            Can be a string identifier (e.g., `"openai:gpt-5.5"`) or a direct chat model816            instance (e.g., [`ChatOpenAI`][langchain_openai.ChatOpenAI] or other another817            [LangChain chat model](https://docs.langchain.com/oss/python/integrations/chat)).818819            For a full list of supported model strings, see820            [`init_chat_model`][langchain.chat_models.init_chat_model(model_provider)].821822            !!! tip ""823824                See the [Models](https://docs.langchain.com/oss/python/langchain/models)825                docs for more information.826        tools: A list of tools, `dict`, or `Callable`.827828            If `None` or an empty list, the agent will consist of a model node without a829            tool calling loop.830831832            !!! tip ""833834                See the [Tools](https://docs.langchain.com/oss/python/langchain/tools)835                docs for more information.836        system_prompt: An optional system prompt for the LLM.837838            Can be a `str` (which will be converted to a `SystemMessage`) or a839            `SystemMessage` instance directly. The system message is added to the840            beginning of the message list when calling the model.841        middleware: A sequence of middleware instances to apply to the agent.842843            Middleware can intercept and modify agent behavior at various stages.844845            !!! tip ""846847                See the [Middleware](https://docs.langchain.com/oss/python/langchain/middleware)848                docs for more information.849        response_format: An optional configuration for structured responses.850851            Can be a `ToolStrategy`, `ProviderStrategy`, or a Pydantic model class.852853            If provided, the agent will handle structured output during the854            conversation flow.855856            Raw schemas will be wrapped in an appropriate strategy based on model857            capabilities.858859            !!! tip ""860861                See the [Structured output](https://docs.langchain.com/oss/python/langchain/structured-output)862                docs for more information.863        state_schema: An optional `TypedDict` schema that extends `AgentState`.864865            When provided, this schema is used instead of `AgentState` as the base866            schema for merging with middleware state schemas. This allows users to867            add custom state fields without needing to create custom middleware.868869            Generally, it's recommended to use `state_schema` extensions via middleware870            to keep relevant extensions scoped to corresponding hooks / tools.871        context_schema: An optional schema for runtime context.872        checkpointer: An optional checkpoint saver object.873874            Used for persisting the state of the graph (e.g., as chat memory) for a875            single thread (e.g., a single conversation).876        store: An optional store object.877878            Used for persisting data across multiple threads (e.g., multiple879            conversations / users).880        interrupt_before: An optional list of node names to interrupt before.881882            Useful if you want to add a user confirmation or other interrupt883            before taking an action.884        interrupt_after: An optional list of node names to interrupt after.885886            Useful if you want to return directly or run additional processing887            on an output.888        debug: Whether to enable verbose logging for graph execution.889890            When enabled, prints detailed information about each node execution, state891            updates, and transitions during agent runtime. Useful for debugging892            middleware behavior and understanding agent execution flow.893        name: An optional name for the `CompiledStateGraph`.894895            This name will be automatically used when adding the agent graph to896            another graph as a subgraph node - particularly useful for building897            multi-agent systems.898        cache: An optional `BaseCache` instance to enable caching of graph execution.899        transformers: Optional sequence of scope-aware `StreamTransformer`900            factories to register on the compiled graph in addition to901            the agent defaults. Each factory is invoked as `factory(scope)`902            so every invocation receives a fresh instance. The final order903            on the compiled graph is: `ToolCallTransformer`, then any904            factories declared by middleware via905            `AgentMiddleware.transformers`, then any factories supplied here.906907    Returns:908        A compiled `StateGraph` that can be used for chat interactions.909910    Raises:911        AssertionError: If duplicate middleware instances are provided.912913    The agent node calls the language model with the messages list (after applying914    the system prompt). If the resulting [`AIMessage`][langchain.messages.AIMessage]915    contains `tool_calls`, the graph will then call the tools. The tools node executes916    the tools and adds the responses to the messages list as917    [`ToolMessage`][langchain.messages.ToolMessage] objects. The agent node then calls918    the language model again. The process repeats until no more `tool_calls` are present919    in the response. The agent then returns the full list of messages.920921    Example:922        ```python923        from langchain.agents import create_agent924925926        def check_weather(location: str) -> str:927            '''Return the weather forecast for the specified location.'''928            return f"It's always sunny in {location}"929930931        graph = create_agent(932            model="anthropic:claude-sonnet-4-5-20250929",933            tools=[check_weather],934            system_prompt="You are a helpful assistant",935        )936        inputs = {"messages": [{"role": "user", "content": "what is the weather in sf"}]}937        for chunk in graph.stream(inputs, stream_mode="updates"):938            print(chunk)939        ```940    """941    # init chat model942    if isinstance(model, str):943        model = init_chat_model(model)944945    # Convert system_prompt to SystemMessage if needed946    system_message: SystemMessage | None = None947    if system_prompt is not None:948        if isinstance(system_prompt, SystemMessage):949            system_message = system_prompt950        else:951            system_message = SystemMessage(content=system_prompt)952953    # Handle tools being None or empty954    if tools is None:955        tools = []956957    # Convert response format and setup structured output tools958    # Raw schemas are wrapped in AutoStrategy to preserve auto-detection intent.959    # AutoStrategy is converted to ToolStrategy upfront to calculate tools during agent creation,960    # but may be replaced with ProviderStrategy later based on model capabilities.961    initial_response_format: ToolStrategy[Any] | ProviderStrategy[Any] | AutoStrategy[Any] | None962    if response_format is None:963        initial_response_format = None964    elif isinstance(response_format, (ToolStrategy, ProviderStrategy, AutoStrategy)):965        # Explicit Tool/Provider strategy, or AutoStrategy for later capability detection966        initial_response_format = response_format967    else:968        # Raw schema - wrap in AutoStrategy to enable auto-detection969        initial_response_format = AutoStrategy(schema=response_format)970971    # For AutoStrategy, convert to ToolStrategy to setup tools upfront972    # (may be replaced with ProviderStrategy later based on model)973    tool_strategy_for_setup: ToolStrategy[Any] | None = None974    if isinstance(initial_response_format, AutoStrategy):975        tool_strategy_for_setup = ToolStrategy(schema=initial_response_format.schema)976    elif isinstance(initial_response_format, ToolStrategy):977        tool_strategy_for_setup = initial_response_format978979    structured_output_tools: dict[str, OutputToolBinding[Any]] = {}980    if tool_strategy_for_setup:981        for response_schema in tool_strategy_for_setup.schema_specs:982            structured_tool_info = OutputToolBinding.from_schema_spec(response_schema)983            structured_output_tools[structured_tool_info.tool.name] = structured_tool_info984    middleware_tools = [t for m in middleware for t in getattr(m, "tools", [])]985986    # Collect middleware with wrap_tool_call or awrap_tool_call hooks987    # Include middleware with either implementation to ensure NotImplementedError is raised988    # when middleware doesn't support the execution path989    middleware_w_wrap_tool_call = [990        m991        for m in middleware992        if m.__class__.wrap_tool_call is not AgentMiddleware.wrap_tool_call993        or m.__class__.awrap_tool_call is not AgentMiddleware.awrap_tool_call994    ]995996    # Chain all wrap_tool_call handlers into a single composed handler997    wrap_tool_call_wrapper = None998    if middleware_w_wrap_tool_call:999        wrappers = [1000            traceable(name=f"{m.name}.wrap_tool_call", process_inputs=_scrub_inputs)(1001                m.wrap_tool_call1002            )1003            for m in middleware_w_wrap_tool_call1004        ]1005        wrap_tool_call_wrapper = _chain_tool_call_wrappers(wrappers)10061007    # Collect middleware with awrap_tool_call or wrap_tool_call hooks1008    # Include middleware with either implementation to ensure NotImplementedError is raised1009    # when middleware doesn't support the execution path1010    middleware_w_awrap_tool_call = [1011        m1012        for m in middleware1013        if m.__class__.awrap_tool_call is not AgentMiddleware.awrap_tool_call1014        or m.__class__.wrap_tool_call is not AgentMiddleware.wrap_tool_call1015    ]10161017    # Chain all awrap_tool_call handlers into a single composed async handler1018    awrap_tool_call_wrapper = None1019    if middleware_w_awrap_tool_call:1020        async_wrappers = [1021            traceable(name=f"{m.name}.awrap_tool_call", process_inputs=_scrub_inputs)(1022                m.awrap_tool_call1023            )1024            for m in middleware_w_awrap_tool_call1025        ]1026        awrap_tool_call_wrapper = _chain_async_tool_call_wrappers(async_wrappers)10271028    # Setup tools1029    tool_node: ToolNode | None = None1030    # Extract built-in provider tools (dict format) and regular tools (BaseTool/callables)1031    built_in_tools = [t for t in tools if isinstance(t, dict)]1032    regular_tools = [t for t in tools if not isinstance(t, dict)]10331034    # Tools that require client-side execution (must be in ToolNode)1035    available_tools = middleware_tools + regular_tools10361037    # Create ToolNode if we have client-side tools OR if middleware defines wrap_tool_call1038    # (which may handle dynamically registered tools)1039    tool_node = (1040        ToolNode(1041            tools=available_tools,1042            wrap_tool_call=wrap_tool_call_wrapper,1043            awrap_tool_call=awrap_tool_call_wrapper,1044        )1045        if available_tools or wrap_tool_call_wrapper or awrap_tool_call_wrapper1046        else None1047    )10481049    # Default tools for ModelRequest initialization1050    # Use converted BaseTool instances from ToolNode (not raw callables)1051    # Include built-ins and converted tools (can be changed dynamically by middleware)1052    # Structured tools are NOT included - they're added dynamically based on response_format1053    if tool_node:1054        default_tools = list(tool_node.tools_by_name.values()) + built_in_tools1055    else:1056        default_tools = list(built_in_tools)10571058    # validate middleware1059    if len({m.name for m in middleware}) != len(middleware):1060        msg = "Please remove duplicate middleware instances."1061        raise AssertionError(msg)1062    middleware_w_before_agent = [1063        m1064        for m in middleware1065        if m.__class__.before_agent is not AgentMiddleware.before_agent1066        or m.__class__.abefore_agent is not AgentMiddleware.abefore_agent1067    ]1068    middleware_w_before_model = [1069        m1070        for m in middleware1071        if m.__class__.before_model is not AgentMiddleware.before_model1072        or m.__class__.abefore_model is not AgentMiddleware.abefore_model1073    ]1074    middleware_w_after_model = [1075        m1076        for m in middleware1077        if m.__class__.after_model is not AgentMiddleware.after_model1078        or m.__class__.aafter_model is not AgentMiddleware.aafter_model1079    ]1080    middleware_w_after_agent = [1081        m1082        for m in middleware1083        if m.__class__.after_agent is not AgentMiddleware.after_agent1084        or m.__class__.aafter_agent is not AgentMiddleware.aafter_agent1085    ]1086    # Collect middleware with wrap_model_call or awrap_model_call hooks1087    # Include middleware with either implementation to ensure NotImplementedError is raised1088    # when middleware doesn't support the execution path1089    middleware_w_wrap_model_call = [1090        m1091        for m in middleware1092        if m.__class__.wrap_model_call is not AgentMiddleware.wrap_model_call1093        or m.__class__.awrap_model_call is not AgentMiddleware.awrap_model_call1094    ]1095    # Collect middleware with awrap_model_call or wrap_model_call hooks1096    # Include middleware with either implementation to ensure NotImplementedError is raised1097    # when middleware doesn't support the execution path1098    middleware_w_awrap_model_call = [1099        m1100        for m in middleware1101        if m.__class__.awrap_model_call is not AgentMiddleware.awrap_model_call1102        or m.__class__.wrap_model_call is not AgentMiddleware.wrap_model_call1103    ]11041105    # Compose wrap_model_call handlers into a single middleware stack (sync)1106    wrap_model_call_handler = None1107    if middleware_w_wrap_model_call:1108        sync_handlers = [1109            traceable(name=f"{m.name}.wrap_model_call", process_inputs=_scrub_inputs)(1110                m.wrap_model_call1111            )1112            for m in middleware_w_wrap_model_call1113        ]1114        wrap_model_call_handler = _chain_model_call_handlers(sync_handlers)11151116    # Compose awrap_model_call handlers into a single middleware stack (async)1117    awrap_model_call_handler = None1118    if middleware_w_awrap_model_call:1119        async_handlers = [1120            traceable(name=f"{m.name}.awrap_model_call", process_inputs=_scrub_inputs)(1121                m.awrap_model_call1122            )1123            for m in middleware_w_awrap_model_call1124        ]1125        awrap_model_call_handler = _chain_async_model_call_handlers(async_handlers)11261127    base_state = state_schema if state_schema is not None else AgentState1128    # Build an ordered list: middleware schemas first (in registration order),1129    # base_state last so it wins any field conflict.  This lets the caller's1130    # explicit state_schema override middleware annotations  e.g. passing1131    # a DeltaChannel-annotated schema wins over BinaryOperatorAggregate from1132    # AgentState without requiring a post-compilation patch.1133    state_schemas: list[type] = [*(m.state_schema for m in middleware), base_state]11341135    resolved_state_schema, input_schema, output_schema = _resolve_schemas(state_schemas)11361137    # create graph, add nodes1138    graph: StateGraph[1139        AgentState[ResponseT], ContextT, InputAgentState, OutputAgentState[ResponseT]1140    ] = StateGraph(1141        state_schema=resolved_state_schema,1142        input_schema=input_schema,1143        output_schema=output_schema,1144        context_schema=context_schema,1145    )11461147    def _handle_model_output(1148        output: AIMessage, effective_response_format: ResponseFormat[Any] | None1149    ) -> dict[str, Any]:1150        """Handle model output including structured responses.11511152        Args:1153            output: The AI message output from the model.1154            effective_response_format: The actual strategy used (may differ from initial1155                if auto-detected).1156        """1157        # Handle structured output with provider strategy1158        if isinstance(effective_response_format, ProviderStrategy):1159            if not output.tool_calls:1160                provider_strategy_binding = ProviderStrategyBinding.from_schema_spec(1161                    effective_response_format.schema_spec1162                )1163                try:1164                    structured_response = provider_strategy_binding.parse(output)1165                except Exception as exc:1166                    schema_name = getattr(1167                        effective_response_format.schema_spec.schema, "__name__", "response_format"1168                    )1169                    validation_error = StructuredOutputValidationError(schema_name, exc, output)1170                    raise validation_error from exc1171                else:1172                    return {"messages": [output], "structured_response": structured_response}1173            return {"messages": [output]}11741175        # Handle structured output with tool strategy1176        if (1177            isinstance(effective_response_format, ToolStrategy)1178            and isinstance(output, AIMessage)1179            and output.tool_calls1180        ):1181            structured_tool_calls = [1182                tc for tc in output.tool_calls if tc["name"] in structured_output_tools1183            ]11841185            if structured_tool_calls:1186                exception: StructuredOutputError | None = None1187                if len(structured_tool_calls) > 1:1188                    # Handle multiple structured outputs error1189                    tool_names = [tc["name"] for tc in structured_tool_calls]1190                    exception = MultipleStructuredOutputsError(tool_names, output)1191                    should_retry, error_message = _handle_structured_output_error(1192                        exception, effective_response_format1193                    )1194                    if not should_retry:1195                        raise exception11961197                    # Add error messages and retry1198                    tool_messages = [1199                        ToolMessage(1200                            content=error_message,1201                            tool_call_id=tc["id"],1202                            name=tc["name"],1203                        )1204                        for tc in structured_tool_calls1205                    ]1206                    return {"messages": [output, *tool_messages]}12071208                # Handle single structured output1209                tool_call = structured_tool_calls[0]1210                try:1211                    structured_tool_binding = structured_output_tools[tool_call["name"]]1212                    structured_response = structured_tool_binding.parse(tool_call["args"])12131214                    tool_message_content = (1215                        effective_response_format.tool_message_content1216                        or f"Returning structured response: {structured_response}"1217                    )12181219                    return {1220                        "messages": [1221                            output,1222                            ToolMessage(1223                                content=tool_message_content,1224                                tool_call_id=tool_call["id"],1225                                name=tool_call["name"],1226                            ),1227                        ],1228                        "structured_response": structured_response,1229                    }1230                except Exception as exc:1231                    exception = StructuredOutputValidationError(tool_call["name"], exc, output)1232                    should_retry, error_message = _handle_structured_output_error(1233                        exception, effective_response_format1234                    )1235                    if not should_retry:1236                        raise exception from exc12371238                    return {1239                        "messages": [1240                            output,1241                            ToolMessage(1242                                content=error_message,1243                                tool_call_id=tool_call["id"],1244                                name=tool_call["name"],1245                            ),1246                        ],1247                    }12481249        return {"messages": [output]}12501251    def _get_bound_model(1252        request: ModelRequest[ContextT],1253    ) -> tuple[Runnable[Any, Any], ResponseFormat[Any] | None]:1254        """Get the model with appropriate tool bindings.12551256        Performs auto-detection of strategy if needed based on model capabilities.12571258        Args:1259            request: The model request containing model, tools, and response format.12601261        Returns:1262            Tuple of `(bound_model, effective_response_format)` where1263            `effective_response_format` is the actual strategy used (may differ from1264            initial if auto-detected).12651266        Raises:1267            ValueError: If middleware returned unknown client-side tool names.1268            ValueError: If `ToolStrategy` specifies tools not declared upfront.1269        """1270        # Validate ONLY client-side tools that need to exist in tool_node1271        # Skip validation when wrap_tool_call is defined, as middleware may handle1272        # dynamic tools that are added at runtime via wrap_model_call1273        has_wrap_tool_call = wrap_tool_call_wrapper or awrap_tool_call_wrapper12741275        # Build map of available client-side tools from the ToolNode1276        # (which has already converted callables)1277        available_tools_by_name = {}1278        if tool_node:1279            available_tools_by_name = tool_node.tools_by_name.copy()12801281        # Check if any requested tools are unknown CLIENT-SIDE tools1282        # Only validate if wrap_tool_call is NOT defined (no dynamic tool handling)1283        if not has_wrap_tool_call:1284            unknown_tool_names = []1285            for t in request.tools:1286                # Only validate BaseTool instances (skip built-in dict tools)1287                if isinstance(t, dict):1288                    continue1289                if isinstance(t, BaseTool) and t.name not in available_tools_by_name:1290                    unknown_tool_names.append(t.name)12911292            if unknown_tool_names:1293                available_tool_names = sorted(available_tools_by_name.keys())1294                msg = DYNAMIC_TOOL_ERROR_TEMPLATE.format(1295                    unknown_tool_names=unknown_tool_names,1296                    available_tool_names=available_tool_names,1297                )1298                raise ValueError(msg)12991300        # Normalize raw schemas to AutoStrategy1301        # (handles middleware override with raw Pydantic classes)1302        response_format: ResponseFormat[Any] | Any | None = request.response_format1303        if response_format is not None and not isinstance(1304            response_format, (AutoStrategy, ToolStrategy, ProviderStrategy)1305        ):1306            response_format = AutoStrategy(schema=response_format)13071308        # Determine effective response format (auto-detect if needed)1309        effective_response_format: ResponseFormat[Any] | None1310        if isinstance(response_format, AutoStrategy):1311            # User provided raw schema via AutoStrategy - auto-detect best strategy based on model1312            if _supports_provider_strategy(request.model, tools=request.tools):1313                # Model supports provider strategy - use it1314                effective_response_format = ProviderStrategy(schema=response_format.schema)1315            elif response_format is initial_response_format and tool_strategy_for_setup is not None:1316                # Model doesn't support provider strategy - use ToolStrategy1317                # Reuse the strategy from setup if possible to preserve tool names1318                effective_response_format = tool_strategy_for_setup1319            else:1320                effective_response_format = ToolStrategy(schema=response_format.schema)1321        else:1322            # User explicitly specified a strategy - preserve it1323            effective_response_format = response_format13241325        # Build final tools list including structured output tools1326        # request.tools now only contains BaseTool instances (converted from callables)1327        # and dicts (built-ins)1328        final_tools = list(request.tools)1329        if isinstance(effective_response_format, ToolStrategy):1330            # Add structured output tools to final tools list1331            structured_tools = [info.tool for info in structured_output_tools.values()]1332            final_tools.extend(structured_tools)13331334        # Bind model based on effective response format1335        if isinstance(effective_response_format, ProviderStrategy):1336            # (Backward compatibility) Use OpenAI format structured output1337            kwargs = effective_response_format.to_model_kwargs()1338            return (1339                request.model.bind_tools(1340                    final_tools, strict=True, **kwargs, **request.model_settings1341                ),1342                effective_response_format,1343            )13441345        if isinstance(effective_response_format, ToolStrategy):1346            # Current implementation requires that tools used for structured output1347            # have to be declared upfront when creating the agent as part of the1348            # response format. Middleware is allowed to change the response format1349            # to a subset of the original structured tools when using ToolStrategy,1350            # but not to add new structured tools that weren't declared upfront.1351            # Compute output binding1352            for tc in effective_response_format.schema_specs:1353                if tc.name not in structured_output_tools:1354                    msg = (1355                        f"ToolStrategy specifies tool '{tc.name}' "1356                        "which wasn't declared in the original "1357                        "response format when creating the agent."1358                    )1359                    raise ValueError(msg)13601361            # Force tool use if we have structured output tools1362            tool_choice = "any" if structured_output_tools else request.tool_choice1363            return (1364                request.model.bind_tools(1365                    final_tools, tool_choice=tool_choice, **request.model_settings1366                ),1367                effective_response_format,1368            )13691370        # No structured output - standard model binding1371        if final_tools:1372            return (1373                request.model.bind_tools(1374                    final_tools, tool_choice=request.tool_choice, **request.model_settings1375                ),1376                None,1377            )1378        return request.model.bind(**request.model_settings), None13791380    def _execute_model_sync(request: ModelRequest[ContextT]) -> ModelResponse:1381        """Execute model and return response.13821383        This is the core model execution logic wrapped by `wrap_model_call` handlers.13841385        Raises any exceptions that occur during model invocation.1386        """1387        # Get the bound model (with auto-detection if needed)1388        model_, effective_response_format = _get_bound_model(request)1389        messages = request.messages1390        if request.system_message:1391            messages = [request.system_message, *messages]13921393        output = model_.invoke(messages)1394        if name:1395            output.name = name13961397        # Handle model output to get messages and structured_response1398        handled_output = _handle_model_output(output, effective_response_format)1399        messages_list = handled_output["messages"]1400        structured_response = handled_output.get("structured_response")14011402        return ModelResponse(1403            result=messages_list,1404            structured_response=structured_response,1405        )14061407    def model_node(state: AgentState[Any], runtime: Runtime[ContextT]) -> list[Command[Any]]:1408        """Sync model request handler with sequential middleware processing."""1409        request = ModelRequest(1410            model=model,1411            tools=default_tools,1412            system_message=system_message,1413            response_format=initial_response_format,1414            messages=state["messages"],1415            tool_choice=None,1416            state=state,1417            runtime=runtime,1418        )14191420        if wrap_model_call_handler is None:1421            model_response = _execute_model_sync(request)1422            return _build_commands(model_response)14231424        result = wrap_model_call_handler(request, _execute_model_sync)1425        return _build_commands(result.model_response, result.commands)14261427    async def _execute_model_async(request: ModelRequest[ContextT]) -> ModelResponse:1428        """Execute model asynchronously and return response.14291430        This is the core async model execution logic wrapped by `wrap_model_call`1431        handlers.14321433        Raises any exceptions that occur during model invocation.1434        """1435        # Get the bound model (with auto-detection if needed)1436        model_, effective_response_format = _get_bound_model(request)1437        messages = request.messages1438        if request.system_message:1439            messages = [request.system_message, *messages]14401441        output = await model_.ainvoke(messages)1442        if name:1443            output.name = name14441445        # Handle model output to get messages and structured_response1446        handled_output = _handle_model_output(output, effective_response_format)1447        messages_list = handled_output["messages"]1448        structured_response = handled_output.get("structured_response")14491450        return ModelResponse(1451            result=messages_list,1452            structured_response=structured_response,1453        )14541455    async def amodel_node(state: AgentState[Any], runtime: Runtime[ContextT]) -> list[Command[Any]]:1456        """Async model request handler with sequential middleware processing."""1457        request = ModelRequest(1458            model=model,1459            tools=default_tools,1460            system_message=system_message,1461            response_format=initial_response_format,1462            messages=state["messages"],1463            tool_choice=None,1464            state=state,1465            runtime=runtime,1466        )14671468        if awrap_model_call_handler is None:1469            model_response = await _execute_model_async(request)1470            return _build_commands(model_response)14711472        result = await awrap_model_call_handler(request, _execute_model_async)1473        return _build_commands(result.model_response, result.commands)14741475    # Use sync or async based on model capabilities1476    graph.add_node("model", RunnableCallable(model_node, amodel_node, trace=False))14771478    # Only add tools node if we have tools1479    if tool_node is not None:1480        graph.add_node("tools", tool_node)14811482    # Add middleware nodes1483    for m in middleware:1484        if (1485            m.__class__.before_agent is not AgentMiddleware.before_agent1486            or m.__class__.abefore_agent is not AgentMiddleware.abefore_agent1487        ):1488            # Use RunnableCallable to support both sync and async1489            # Pass None for sync if not overridden to avoid signature conflicts1490            sync_before_agent = (1491                m.before_agent1492                if m.__class__.before_agent is not AgentMiddleware.before_agent1493                else None1494            )1495            async_before_agent = (1496                m.abefore_agent1497                if m.__class__.abefore_agent is not AgentMiddleware.abefore_agent1498                else None1499            )1500            before_agent_node = RunnableCallable(sync_before_agent, async_before_agent, trace=False)1501            graph.add_node(1502                f"{m.name}.before_agent", before_agent_node, input_schema=resolved_state_schema1503            )15041505        if (1506            m.__class__.before_model is not AgentMiddleware.before_model1507            or m.__class__.abefore_model is not AgentMiddleware.abefore_model1508        ):1509            # Use RunnableCallable to support both sync and async1510            # Pass None for sync if not overridden to avoid signature conflicts1511            sync_before = (1512                m.before_model1513                if m.__class__.before_model is not AgentMiddleware.before_model1514                else None1515            )1516            async_before = (1517                m.abefore_model1518                if m.__class__.abefore_model is not AgentMiddleware.abefore_model1519                else None1520            )1521            before_node = RunnableCallable(sync_before, async_before, trace=False)1522            graph.add_node(1523                f"{m.name}.before_model", before_node, input_schema=resolved_state_schema1524            )15251526        if (1527            m.__class__.after_model is not AgentMiddleware.after_model1528            or m.__class__.aafter_model is not AgentMiddleware.aafter_model1529        ):1530            # Use RunnableCallable to support both sync and async1531            # Pass None for sync if not overridden to avoid signature conflicts1532            sync_after = (1533                m.after_model1534                if m.__class__.after_model is not AgentMiddleware.after_model1535                else None1536            )1537            async_after = (1538                m.aafter_model1539                if m.__class__.aafter_model is not AgentMiddleware.aafter_model1540                else None1541            )1542            after_node = RunnableCallable(sync_after, async_after, trace=False)1543            graph.add_node(f"{m.name}.after_model", after_node, input_schema=resolved_state_schema)15441545        if (1546            m.__class__.after_agent is not AgentMiddleware.after_agent1547            or m.__class__.aafter_agent is not AgentMiddleware.aafter_agent1548        ):1549            # Use RunnableCallable to support both sync and async1550            # Pass None for sync if not overridden to avoid signature conflicts1551            sync_after_agent = (1552                m.after_agent1553                if m.__class__.after_agent is not AgentMiddleware.after_agent1554                else None1555            )1556            async_after_agent = (1557                m.aafter_agent1558                if m.__class__.aafter_agent is not AgentMiddleware.aafter_agent1559                else None1560            )1561            after_agent_node = RunnableCallable(sync_after_agent, async_after_agent, trace=False)1562            graph.add_node(1563                f"{m.name}.after_agent", after_agent_node, input_schema=resolved_state_schema1564            )15651566    # Determine the entry node (runs once at start): before_agent -> before_model -> model1567    if middleware_w_before_agent:1568        entry_node = f"{middleware_w_before_agent[0].name}.before_agent"1569    elif middleware_w_before_model:1570        entry_node = f"{middleware_w_before_model[0].name}.before_model"1571    else:1572        entry_node = "model"15731574    # Determine the loop entry node (beginning of agent loop, excludes before_agent)1575    # This is where tools will loop back to for the next iteration1576    if middleware_w_before_model:1577        loop_entry_node = f"{middleware_w_before_model[0].name}.before_model"1578    else:1579        loop_entry_node = "model"15801581    # Determine the loop exit node (end of each iteration, can run multiple times)1582    # This is after_model or model, but NOT after_agent1583    if middleware_w_after_model:1584        loop_exit_node = f"{middleware_w_after_model[0].name}.after_model"1585    else:1586        loop_exit_node = "model"15871588    # Determine the exit node (runs once at end): after_agent or END1589    if middleware_w_after_agent:1590        exit_node = f"{middleware_w_after_agent[-1].name}.after_agent"1591    else:1592        exit_node = END15931594    graph.add_edge(START, entry_node)1595    # add conditional edges only if tools exist1596    if tool_node is not None:1597        # Only include exit_node in destinations if any tool has return_direct=True1598        # or if there are structured output tools1599        tools_to_model_destinations = [loop_entry_node]1600        if (1601            any(tool.return_direct for tool in tool_node.tools_by_name.values())1602            or structured_output_tools1603        ):1604            tools_to_model_destinations.append(exit_node)16051606        graph.add_conditional_edges(1607            "tools",1608            RunnableCallable(1609                _make_tools_to_model_edge(1610                    tool_node=tool_node,1611                    model_destination=loop_entry_node,1612                    structured_output_tools=structured_output_tools,1613                    end_destination=exit_node,1614                ),1615                trace=False,1616            ),1617            tools_to_model_destinations,1618        )16191620        # base destinations are tools and exit_node1621        # we add the loop_entry node to edge destinations if:1622        # - there is an after model hook(s) -- allows jump_to to model1623        #   potentially artificially injected tool messages, ex HITL1624        # - there is a response format -- to allow for jumping to model to handle1625        #   regenerating structured output tool calls1626        model_to_tools_destinations = ["tools", exit_node]1627        if response_format or loop_exit_node != "model":1628            model_to_tools_destinations.append(loop_entry_node)16291630        graph.add_conditional_edges(1631            loop_exit_node,1632            RunnableCallable(1633                _make_model_to_tools_edge(1634                    model_destination=loop_entry_node,1635                    structured_output_tools=structured_output_tools,1636                    end_destination=exit_node,1637                ),1638                trace=False,1639            ),1640            model_to_tools_destinations,1641        )1642    elif len(structured_output_tools) > 0:1643        graph.add_conditional_edges(1644            loop_exit_node,1645            RunnableCallable(1646                _make_model_to_model_edge(1647                    model_destination=loop_entry_node,1648                    end_destination=exit_node,1649                ),1650                trace=False,1651            ),1652            [loop_entry_node, exit_node],1653        )1654    elif loop_exit_node == "model":1655        # If no tools and no after_model, go directly to exit_node1656        graph.add_edge(loop_exit_node, exit_node)1657    # No tools but we have after_model - connect after_model to exit_node1658    else:1659        _add_middleware_edge(1660            graph,1661            name=f"{middleware_w_after_model[0].name}.after_model",1662            default_destination=exit_node,1663            model_destination=loop_entry_node,1664            end_destination=exit_node,1665            can_jump_to=_get_can_jump_to(middleware_w_after_model[0], "after_model"),1666        )16671668    # Add before_agent middleware edges1669    if middleware_w_before_agent:1670        for m1, m2 in itertools.pairwise(middleware_w_before_agent):1671            _add_middleware_edge(1672                graph,1673                name=f"{m1.name}.before_agent",1674                default_destination=f"{m2.name}.before_agent",1675                model_destination=loop_entry_node,1676                end_destination=exit_node,1677                can_jump_to=_get_can_jump_to(m1, "before_agent"),1678            )1679        # Connect last before_agent to loop_entry_node (before_model or model)1680        _add_middleware_edge(1681            graph,1682            name=f"{middleware_w_before_agent[-1].name}.before_agent",1683            default_destination=loop_entry_node,1684            model_destination=loop_entry_node,1685            end_destination=exit_node,1686            can_jump_to=_get_can_jump_to(middleware_w_before_agent[-1], "before_agent"),1687        )16881689    # Add before_model middleware edges1690    if middleware_w_before_model:1691        for m1, m2 in itertools.pairwise(middleware_w_before_model):1692            _add_middleware_edge(1693                graph,1694                name=f"{m1.name}.before_model",1695                default_destination=f"{m2.name}.before_model",1696                model_destination=loop_entry_node,1697                end_destination=exit_node,1698                can_jump_to=_get_can_jump_to(m1, "before_model"),1699            )1700        # Go directly to model after the last before_model1701        _add_middleware_edge(1702            graph,1703            name=f"{middleware_w_before_model[-1].name}.before_model",1704            default_destination="model",1705            model_destination=loop_entry_node,1706            end_destination=exit_node,1707            can_jump_to=_get_can_jump_to(middleware_w_before_model[-1], "before_model"),1708        )17091710    # Add after_model middleware edges1711    if middleware_w_after_model:1712        graph.add_edge("model", f"{middleware_w_after_model[-1].name}.after_model")1713        for idx in range(len(middleware_w_after_model) - 1, 0, -1):1714            m1 = middleware_w_after_model[idx]1715            m2 = middleware_w_after_model[idx - 1]1716            _add_middleware_edge(1717                graph,1718                name=f"{m1.name}.after_model",1719                default_destination=f"{m2.name}.after_model",1720                model_destination=loop_entry_node,1721                end_destination=exit_node,1722                can_jump_to=_get_can_jump_to(m1, "after_model"),1723            )1724        # Note: Connection from after_model to after_agent/END is handled above1725        # in the conditional edges section17261727    # Add after_agent middleware edges1728    if middleware_w_after_agent:1729        # Chain after_agent middleware (runs once at the very end, before END)1730        for idx in range(len(middleware_w_after_agent) - 1, 0, -1):1731            m1 = middleware_w_after_agent[idx]1732            m2 = middleware_w_after_agent[idx - 1]1733            _add_middleware_edge(1734                graph,1735                name=f"{m1.name}.after_agent",1736                default_destination=f"{m2.name}.after_agent",1737                model_destination=loop_entry_node,1738                end_destination=exit_node,1739                can_jump_to=_get_can_jump_to(m1, "after_agent"),1740            )17411742        # Connect the last after_agent to END1743        _add_middleware_edge(1744            graph,1745            name=f"{middleware_w_after_agent[0].name}.after_agent",1746            default_destination=END,1747            model_destination=loop_entry_node,1748            end_destination=exit_node,1749            can_jump_to=_get_can_jump_to(middleware_w_after_agent[0], "after_agent"),1750        )17511752    # Set recursion limit to 9_9991753    # https://github.com/langchain-ai/langgraph/issues/73131754    config: RunnableConfig = {"recursion_limit": 9_999}1755    config["metadata"] = {"ls_integration": "langchain_create_agent"}1756    if name:1757        config["metadata"]["lc_agent_name"] = name17581759    middleware_transformers = [t for m in middleware for t in getattr(m, "transformers", ())]17601761    return graph.compile(1762        checkpointer=checkpointer,1763        store=store,1764        interrupt_before=interrupt_before,1765        interrupt_after=interrupt_after,1766        debug=debug,1767        name=name,1768        cache=cache,1769        transformers=[1770            ToolCallTransformer,1771            SubagentTransformer,1772            *middleware_transformers,1773            *(transformers or ()),1774        ],1775    ).with_config(config)177617771778def _resolve_jump(1779    jump_to: JumpTo | None,1780    *,1781    model_destination: str,1782    end_destination: str,1783) -> str | None:1784    if jump_to == "model":1785        return model_destination1786    if jump_to == "end":1787        return end_destination1788    if jump_to == "tools":1789        return "tools"1790    return None179117921793def _fetch_last_ai_and_tool_messages(1794    messages: list[AnyMessage],1795) -> tuple[AIMessage | None, list[ToolMessage]]:1796    """Return the last AI message and any subsequent tool messages.17971798    Args:1799        messages: List of messages to search through.18001801    Returns:1802        A tuple of (last_ai_message, tool_messages). If no AIMessage is found,1803        returns (None, []). Callers must handle the None case appropriately.1804    """1805    for i in range(len(messages) - 1, -1, -1):1806        if isinstance(messages[i], AIMessage):1807            last_ai_message = cast("AIMessage", messages[i])1808            tool_messages = [m for m in messages[i + 1 :] if isinstance(m, ToolMessage)]1809            return last_ai_message, tool_messages18101811    return None, []181218131814def _make_model_to_tools_edge(1815    *,1816    model_destination: str,1817    structured_output_tools: dict[str, OutputToolBinding[Any]],1818    end_destination: str,1819) -> Callable[[dict[str, Any]], str | list[Send] | None]:1820    def model_to_tools(1821        state: dict[str, Any],1822    ) -> str | list[Send] | None:1823        # 1. If there's an explicit jump_to in the state, use it1824        if jump_to := state.get("jump_to"):1825            return _resolve_jump(1826                jump_to,1827                model_destination=model_destination,1828                end_destination=end_destination,1829            )18301831        last_ai_message, tool_messages = _fetch_last_ai_and_tool_messages(state["messages"])18321833        # 2. if no AIMessage exists (e.g., messages were cleared), exit the loop1834        if last_ai_message is None:1835            return end_destination18361837        tool_message_ids = [m.tool_call_id for m in tool_messages]18381839        # 3. If the model hasn't called any tools, exit the loop1840        # this is the classic exit condition for an agent loop1841        if len(last_ai_message.tool_calls) == 0:1842            return end_destination18431844        pending_tool_calls = [1845            c1846            for c in last_ai_message.tool_calls1847            if c["id"] not in tool_message_ids and c["name"] not in structured_output_tools1848        ]18491850        # 4. If there are pending tool calls, jump to the tool node.1851        # The tool node hydrates ToolRuntime.state from channels via1852        # CONFIG_KEY_READ at execution time, so we no longer inline the1853        # full state into each Send (previously O(N^2) in TASKS writes).1854        if pending_tool_calls:1855            return [Send("tools", [tool_call]) for tool_call in pending_tool_calls]18561857        # 5. If there is a structured response, exit the loop1858        if "structured_response" in state:1859            return end_destination18601861        # 6. AIMessage has tool calls, but there are no pending tool calls which suggests1862        # the injection of artificial tool messages. Jump to the model node1863        return model_destination18641865    return model_to_tools186618671868def _make_model_to_model_edge(1869    *,1870    model_destination: str,1871    end_destination: str,1872) -> Callable[[dict[str, Any]], str | list[Send] | None]:1873    def model_to_model(1874        state: dict[str, Any],1875    ) -> str | list[Send] | None:1876        # 1. Priority: Check for explicit jump_to directive from middleware1877        if jump_to := state.get("jump_to"):1878            return _resolve_jump(1879                jump_to,1880                model_destination=model_destination,1881                end_destination=end_destination,1882            )18831884        # 2. Exit condition: A structured response was generated1885        if "structured_response" in state:1886            return end_destination18871888        # 3. Default: Continue the loop, there may have been an issue with structured1889        # output generation, so we need to retry1890        return model_destination18911892    return model_to_model189318941895def _make_tools_to_model_edge(1896    *,1897    tool_node: ToolNode,1898    model_destination: str,1899    structured_output_tools: dict[str, OutputToolBinding[Any]],1900    end_destination: str,1901) -> Callable[[dict[str, Any]], str | None]:1902    def tools_to_model(state: dict[str, Any]) -> str | None:1903        last_ai_message, tool_messages = _fetch_last_ai_and_tool_messages(state["messages"])19041905        # 1. If no AIMessage exists (e.g., messages were cleared), route to model1906        if last_ai_message is None:1907            return model_destination19081909        # 2. Exit condition: All executed tools have return_direct=True1910        # Filter to only client-side tools (provider tools are not in tool_node)1911        client_side_tool_calls = [1912            c for c in last_ai_message.tool_calls if c["name"] in tool_node.tools_by_name1913        ]1914        if client_side_tool_calls and all(1915            tool_node.tools_by_name[c["name"]].return_direct for c in client_side_tool_calls1916        ):1917            return end_destination19181919        # 3. Exit condition: A structured output tool was executed1920        if any(t.name in structured_output_tools for t in tool_messages):1921            return end_destination19221923        # 4. Default: Continue the loop1924        #    Tool execution completed successfully, route back to the model1925        #    so it can process the tool results and decide the next action.1926        return model_destination19271928    return tools_to_model192919301931def _add_middleware_edge(1932    graph: StateGraph[1933        AgentState[ResponseT], ContextT, InputAgentState, OutputAgentState[ResponseT]1934    ],1935    *,1936    name: str,1937    default_destination: str,1938    model_destination: str,1939    end_destination: str,1940    can_jump_to: list[JumpTo] | None,1941) -> None:1942    """Add an edge to the graph for a middleware node.19431944    Args:1945        graph: The graph to add the edge to.1946        name: The name of the middleware node.1947        default_destination: The default destination for the edge.1948        model_destination: The destination for the edge to the model.1949        end_destination: The destination for the edge to the end.1950        can_jump_to: The conditionally jumpable destinations for the edge.1951    """1952    if can_jump_to:19531954        def jump_edge(state: dict[str, Any]) -> str:1955            return (1956                _resolve_jump(1957                    state.get("jump_to"),1958                    model_destination=model_destination,1959                    end_destination=end_destination,1960                )1961                or default_destination1962            )19631964        destinations = [default_destination]19651966        if "end" in can_jump_to:1967            destinations.append(end_destination)1968        if "tools" in can_jump_to:1969            destinations.append("tools")1970        if "model" in can_jump_to and name != model_destination:1971            destinations.append(model_destination)19721973        graph.add_conditional_edges(name, RunnableCallable(jump_edge, trace=False), destinations)19741975    else:1976        graph.add_edge(name, default_destination)197719781979__all__ = [1980    "create_agent",1981]

Code quality findings 70

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
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.