Ensure try blocks have corresponding except or finally blocks
try:
1"""Agent factory for creating agents with middleware support."""23from __future__ import annotations45import functools6import importlib7import itertools8import re9from dataclasses import dataclass, field, fields10from typing import (11 TYPE_CHECKING,12 Annotated,13 Any,14 Generic,15 cast,16 get_args,17 get_origin,18 get_type_hints,19)2021from langchain_core.language_models.chat_models import BaseChatModel22from langchain_core.messages import AIMessage, AnyMessage, SystemMessage, ToolMessage23from langchain_core.tools import BaseTool24from langgraph._internal._runnable import RunnableCallable25from langgraph.constants import END, START26from langgraph.graph.state import StateGraph27from langgraph.prebuilt import ToolCallTransformer28from langgraph.prebuilt.tool_node import ToolNode29from langgraph.types import Command, Send30from langsmith import traceable31from typing_extensions import NotRequired, Required, TypedDict, overload3233from langchain.agents._subagent_transformer import SubagentTransformer34from langchain.agents.middleware._trace_policy import (35 _node_trace_policy,36 _resolved_transform,37)38from langchain.agents.middleware.types import (39 AgentMiddleware,40 AgentState,41 ContextT,42 ExtendedModelResponse,43 InputAgentState,44 JumpTo,45 ModelRequest,46 ModelResponse,47 OmitFromSchema,48 OutputAgentState,49 ResponseT,50 StateT_co,51 ToolCallRequest,52)53from langchain.agents.structured_output import (54 AutoStrategy,55 MultipleStructuredOutputsError,56 OutputToolBinding,57 ProviderStrategy,58 ProviderStrategyBinding,59 ResponseFormat,60 StructuredOutputError,61 StructuredOutputValidationError,62 ToolStrategy,63)64from langchain.chat_models import init_chat_model656667@dataclass68class _ComposedExtendedModelResponse(Generic[ResponseT]):69 """Internal result from composed `wrap_model_call` middleware.7071 Unlike `ExtendedModelResponse` (user-facing, single command), this holds the72 full list of commands accumulated across all middleware layers during73 composition.74 """7576 model_response: ModelResponse[ResponseT]77 """The underlying model response."""7879 commands: list[Command[Any]] = field(default_factory=list)80 """Commands accumulated from all middleware layers (inner-first, then outer)."""818283if TYPE_CHECKING:84 from collections.abc import Awaitable, Callable, Iterable, Sequence8586 from langchain_core.runnables import Runnable, RunnableConfig87 from langgraph.cache.base import BaseCache88 from langgraph.graph.state import CompiledStateGraph89 from langgraph.runtime import Runtime90 from langgraph.store.base import BaseStore91 from langgraph.stream._mux import TransformerFactory92 from langgraph.types import Checkpointer9394 from langchain.agents.middleware.types import ToolCallWrapper9596 _ModelCallHandler = Callable[97 [ModelRequest[ContextT], Callable[[ModelRequest[ContextT]], ModelResponse]],98 ModelResponse | AIMessage | ExtendedModelResponse,99 ]100101 _ComposedModelCallHandler = Callable[102 [ModelRequest[ContextT], Callable[[ModelRequest[ContextT]], ModelResponse]],103 _ComposedExtendedModelResponse,104 ]105106 _AsyncModelCallHandler = Callable[107 [ModelRequest[ContextT], Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse]]],108 Awaitable[ModelResponse | AIMessage | ExtendedModelResponse],109 ]110111 _ComposedAsyncModelCallHandler = Callable[112 [ModelRequest[ContextT], Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse]]],113 Awaitable[_ComposedExtendedModelResponse],114 ]115116117STRUCTURED_OUTPUT_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes."118119DYNAMIC_TOOL_ERROR_TEMPLATE = """120Middleware added tools that the agent doesn't know how to execute.121122Unknown tools: {unknown_tool_names}123Registered tools: {available_tool_names}124125This happens when middleware modifies `request.tools` in `wrap_model_call` to include126tools that weren't passed to `create_agent()`.127128How to fix this:129130Option 1: Register tools at agent creation (recommended for most cases)131 Pass the tools to `create_agent(tools=[...])` or set them on `middleware.tools`.132 This makes tools available for every agent invocation.133134Option 2: Handle dynamic tools in middleware (for tools created at runtime)135 Implement `wrap_tool_call` to execute tools that are added dynamically:136137 class MyMiddleware(AgentMiddleware):138 def wrap_tool_call(self, request, handler):139 if request.tool_call["name"] == "dynamic_tool":140 # Execute the dynamic tool yourself or override with tool instance141 return handler(request.override(tool=my_dynamic_tool))142 return handler(request)143""".strip()144145146def _scrub_inputs(inputs: dict[str, Any]) -> dict[str, Any]:147 """Remove `runtime` and `handler` from trace inputs before sending to LangSmith."""148 filtered = inputs.copy()149 filtered.pop("handler", None)150 req = filtered.get("request")151 if isinstance(req, (ModelRequest, ToolCallRequest)):152 filtered["request"] = {153 f.name: getattr(req, f.name) for f in fields(req) if f.name != "runtime"154 }155 return filtered156157158def _wrap_trace_kwargs(middleware: AgentMiddleware[Any, Any]) -> dict[str, Any]:159 """`traceable` kwargs for a middleware's `wrap_*` hook spans.160161 The `_scrub_inputs` baseline (strip the unserializable `handler`/`runtime`) always162 runs first; the effective `TracePolicy` (the middleware's own, else the process-wide163 default) composes on top. The effective policy is resolved at call time, so a164 `configure_trace_policy` call after `create_agent` still applies.165 """166 process_inputs = _resolved_transform(middleware.trace_policy, "process_inputs")167 process_outputs = _resolved_transform(middleware.trace_policy, "process_outputs")168 return {169 "process_inputs": lambda inputs: process_inputs(_scrub_inputs(inputs)),170 "process_outputs": process_outputs,171 }172173174FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT = [175 # If model profile data are not available, model names matching these patterns176 # are assumed to support provider-native structured output. These are regexes177 # so matches stay bounded to model-name segments instead of arbitrary substrings.178 r"(^|[/:.])gpt-4\.1($|[-/:])",179 r"(^|[/:.])gpt-4o($|[-/:])",180 r"(^|[/:.])gpt-5($|[-/:])",181 r"(^|[/:.])gpt-5\.1($|[-/:])",182 r"(^|[/:.])gpt-5\.2(-\d{4}-\d{2}-\d{2})?($|[/:])",183 r"(^|[/:.])gpt-5\.2-(chat|codex)($|[-/:])",184 r"(^|[/:.])gpt-5\.3($|[-/:])",185 r"(^|[/:.])gpt-5\.4(-\d{4}-\d{2}-\d{2})?($|[/:])",186 r"(^|[/:.])gpt-5\.4-(mini|nano)($|[-/:])",187 r"(^|[/:.])gpt-5\.5($|[-/:])",188 r"(^|[/:.])claude-(fable|mythos)-5(?:-\d{8})?(?:-v\d(?::\d)?)?($|[/:])",189 r"(^|[/:.])claude-haiku-4-5(?:-\d{8})?(?:-v\d(?::\d)?)?($|[/:])",190 r"(^|[/:.])claude-opus-4-(5|6|7|8)(?:-\d{8})?(?:-v\d(?::\d)?)?($|[/:])",191 r"(^|[/:.])claude-sonnet-4-(5|6)(?:-\d{8})?(?:-v\d(?::\d)?)?($|[/:])",192 r"(^|[/:.])grok-4($|[-.:/])",193 r"(^|[/:.])grok-build($|[-/:])",194]195196197def _normalize_to_model_response(198 result: ModelResponse | AIMessage | ExtendedModelResponse,199) -> ModelResponse:200 """Normalize middleware return value to ModelResponse.201202 At inner composition boundaries, `ExtendedModelResponse` is unwrapped to its203 underlying `ModelResponse` so that inner middleware always sees `ModelResponse`204 from the handler.205 """206 if isinstance(result, AIMessage):207 return ModelResponse(result=[result], structured_response=None)208 if isinstance(result, ExtendedModelResponse):209 return result.model_response210 return result211212213def _build_commands(214 model_response: ModelResponse,215 middleware_commands: list[Command[Any]] | None = None,216 *,217 has_structured_output: bool = False,218) -> list[Command[Any]]:219 """Build a list of Commands from a model response and middleware commands.220221 The first Command contains the model response state (messages and optional222 structured_response). Middleware commands are appended as-is.223224 Args:225 model_response: The model response containing messages and optional226 structured output.227 middleware_commands: Commands accumulated from middleware layers during228 composition (inner-first ordering).229 has_structured_output: Whether the agent was configured with a230 `response_format`. When `True` and no structured response was231 produced, `structured_response` is explicitly cleared to avoid a232 stale value from a previous checkpointed turn.233234 Returns:235 List of `Command` objects ready to be returned from a model node.236 """237 state: dict[str, Any] = {"messages": model_response.result}238239 if model_response.structured_response is not None:240 state["structured_response"] = model_response.structured_response241 elif has_structured_output:242 state["structured_response"] = None243244 for cmd in middleware_commands or []:245 if cmd.goto:246 msg = (247 "Command goto is not yet supported in wrap_model_call middleware. "248 "Use the jump_to state field with before_model/after_model hooks instead."249 )250 raise NotImplementedError(msg)251 if cmd.resume:252 msg = "Command resume is not yet supported in wrap_model_call middleware."253 raise NotImplementedError(msg)254 if cmd.graph:255 msg = "Command graph is not yet supported in wrap_model_call middleware."256 raise NotImplementedError(msg)257258 commands: list[Command[Any]] = [Command(update=state)]259 commands.extend(middleware_commands or [])260 return commands261262263def _chain_model_call_handlers(264 handlers: Sequence[_ModelCallHandler[ContextT]],265) -> _ComposedModelCallHandler[ContextT] | None:266 """Compose multiple `wrap_model_call` handlers into single middleware stack.267268 Composes handlers so first in list becomes outermost layer. Each handler receives a269 handler callback to execute inner layers. Commands from each layer are accumulated270 into a list (inner-first, then outer) without merging.271272 Args:273 handlers: List of handlers.274275 First handler wraps all others.276277 Returns:278 Composed handler returning `_ComposedExtendedModelResponse`,279 or `None` if handlers empty.280 """281 if not handlers:282 return None283284 def _to_composed_result(285 result: ModelResponse | AIMessage | ExtendedModelResponse | _ComposedExtendedModelResponse,286 extra_commands: list[Command[Any]] | None = None,287 ) -> _ComposedExtendedModelResponse:288 """Normalize any handler result to _ComposedExtendedModelResponse."""289 commands: list[Command[Any]] = list(extra_commands or [])290 if isinstance(result, _ComposedExtendedModelResponse):291 commands.extend(result.commands)292 model_response = result.model_response293 elif isinstance(result, ExtendedModelResponse):294 model_response = result.model_response295 if result.command is not None:296 commands.append(result.command)297 else:298 model_response = _normalize_to_model_response(result)299300 return _ComposedExtendedModelResponse(model_response=model_response, commands=commands)301302 if len(handlers) == 1:303 single_handler = handlers[0]304305 def normalized_single(306 request: ModelRequest[ContextT],307 handler: Callable[[ModelRequest[ContextT]], ModelResponse],308 ) -> _ComposedExtendedModelResponse:309 return _to_composed_result(single_handler(request, handler))310311 return normalized_single312313 def compose_two(314 outer: _ModelCallHandler[ContextT] | _ComposedModelCallHandler[ContextT],315 inner: _ModelCallHandler[ContextT] | _ComposedModelCallHandler[ContextT],316 ) -> _ComposedModelCallHandler[ContextT]:317 """Compose two handlers where outer wraps inner."""318319 def composed(320 request: ModelRequest[ContextT],321 handler: Callable[[ModelRequest[ContextT]], ModelResponse],322 ) -> _ComposedExtendedModelResponse:323 # Closure variable to capture inner's commands before normalizing324 accumulated_commands: list[Command[Any]] = []325326 def inner_handler(req: ModelRequest[ContextT]) -> ModelResponse:327 # Clear on each call for retry safety328 accumulated_commands.clear()329 inner_result = inner(req, handler)330 if isinstance(inner_result, _ComposedExtendedModelResponse):331 accumulated_commands.extend(inner_result.commands)332 return inner_result.model_response333 if isinstance(inner_result, ExtendedModelResponse):334 if inner_result.command is not None:335 accumulated_commands.append(inner_result.command)336 return inner_result.model_response337 return _normalize_to_model_response(inner_result)338339 outer_result = outer(request, inner_handler)340 return _to_composed_result(341 outer_result,342 extra_commands=accumulated_commands or None,343 )344345 return composed346347 # Compose right-to-left: outer(inner(innermost(handler)))348 composed_handler = compose_two(handlers[-2], handlers[-1])349 for h in reversed(handlers[:-2]):350 composed_handler = compose_two(h, composed_handler)351352 return composed_handler353354355def _chain_async_model_call_handlers(356 handlers: Sequence[_AsyncModelCallHandler[ContextT]],357) -> _ComposedAsyncModelCallHandler[ContextT] | None:358 """Compose multiple async `wrap_model_call` handlers into single middleware stack.359360 Commands from each layer are accumulated into a list (inner-first, then outer)361 without merging.362363 Args:364 handlers: List of async handlers.365366 First handler wraps all others.367368 Returns:369 Composed async handler returning `_ComposedExtendedModelResponse`,370 or `None` if handlers empty.371 """372 if not handlers:373 return None374375 def _to_composed_result(376 result: ModelResponse | AIMessage | ExtendedModelResponse | _ComposedExtendedModelResponse,377 extra_commands: list[Command[Any]] | None = None,378 ) -> _ComposedExtendedModelResponse:379 """Normalize any handler result to _ComposedExtendedModelResponse."""380 commands: list[Command[Any]] = list(extra_commands or [])381 if isinstance(result, _ComposedExtendedModelResponse):382 commands.extend(result.commands)383 model_response = result.model_response384 elif isinstance(result, ExtendedModelResponse):385 model_response = result.model_response386 if result.command is not None:387 commands.append(result.command)388 else:389 model_response = _normalize_to_model_response(result)390391 return _ComposedExtendedModelResponse(model_response=model_response, commands=commands)392393 if len(handlers) == 1:394 single_handler = handlers[0]395396 async def normalized_single(397 request: ModelRequest[ContextT],398 handler: Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse]],399 ) -> _ComposedExtendedModelResponse:400 return _to_composed_result(await single_handler(request, handler))401402 return normalized_single403404 def compose_two(405 outer: _AsyncModelCallHandler[ContextT] | _ComposedAsyncModelCallHandler[ContextT],406 inner: _AsyncModelCallHandler[ContextT] | _ComposedAsyncModelCallHandler[ContextT],407 ) -> _ComposedAsyncModelCallHandler[ContextT]:408 """Compose two async handlers where outer wraps inner."""409410 async def composed(411 request: ModelRequest[ContextT],412 handler: Callable[[ModelRequest[ContextT]], Awaitable[ModelResponse]],413 ) -> _ComposedExtendedModelResponse:414 # Closure variable to capture inner's commands before normalizing415 accumulated_commands: list[Command[Any]] = []416417 async def inner_handler(req: ModelRequest[ContextT]) -> ModelResponse:418 # Clear on each call for retry safety419 accumulated_commands.clear()420 inner_result = await inner(req, handler)421 if isinstance(inner_result, _ComposedExtendedModelResponse):422 accumulated_commands.extend(inner_result.commands)423 return inner_result.model_response424 if isinstance(inner_result, ExtendedModelResponse):425 if inner_result.command is not None:426 accumulated_commands.append(inner_result.command)427 return inner_result.model_response428 return _normalize_to_model_response(inner_result)429430 outer_result = await outer(request, inner_handler)431 return _to_composed_result(432 outer_result,433 extra_commands=accumulated_commands or None,434 )435436 return composed437438 # Compose right-to-left: outer(inner(innermost(handler)))439 composed_handler = compose_two(handlers[-2], handlers[-1])440 for h in reversed(handlers[:-2]):441 composed_handler = compose_two(h, composed_handler)442443 return composed_handler444445446@functools.lru_cache(maxsize=100)447def _get_schema_type_hints(schema: type) -> dict[str, Any]:448 """Return cached type hints for a schema."""449 return get_type_hints(schema, include_extras=True)450451452def _resolve_schemas(schemas: list[type]) -> tuple[type, type, type]:453 """Resolve state, input, and output schemas for the given schemas.454455 Schemas are merged in list order; later entries override earlier ones when the456 same field is declared by multiple schemas. Duplicates are harmless — a type457 that appears more than once is processed at its last position.458 """459 schema_hints: dict[type, dict[str, Any]] = {}460 for schema in schemas:461 # Reinsert duplicates so dict iteration reflects their final position.462 schema_hints.pop(schema, None)463 schema_hints[schema] = _get_schema_type_hints(schema)464 return (465 _resolve_schema(schema_hints, "StateSchema", None),466 _resolve_schema(schema_hints, "InputSchema", "input"),467 _resolve_schema(schema_hints, "OutputSchema", "output"),468 )469470471def _resolve_schema(472 schema_hints: dict[type, dict[str, Any]],473 schema_name: str,474 omit_flag: str | None = None,475) -> type:476 """Resolve schema by merging schemas and optionally respecting `OmitFromSchema` annotations.477478 Args:479 schema_hints: Resolved schema annotations to merge480 schema_name: Name for the generated `TypedDict`481 omit_flag: If specified, omit fields with this flag set (`'input'` or482 `'output'`)483484 Returns:485 Merged schema as `TypedDict`486 """487 all_annotations = {}488489 for hints in schema_hints.values():490 for field_name, field_type in hints.items():491 should_omit = False492493 if omit_flag:494 metadata = _extract_metadata(field_type)495 for meta in metadata:496 if isinstance(meta, OmitFromSchema) and getattr(meta, omit_flag) is True:497 should_omit = True498 break499500 if not should_omit:501 all_annotations[field_name] = field_type502503 # `TypedDict` dynamically creates a class, but type checkers don't infer that504 # the runtime result satisfies this function's `type` return contract.505 return cast("type", TypedDict(schema_name, all_annotations)) # type: ignore[operator]506507508def _extract_metadata(type_: type) -> list[Any]:509 """Extract metadata from a field type, handling `Required`/`NotRequired` and `Annotated` wrappers.""" # noqa: E501510 # Handle Required[Annotated[...]] or NotRequired[Annotated[...]]511 if get_origin(type_) in {Required, NotRequired}:512 inner_type = get_args(type_)[0]513 if get_origin(inner_type) is Annotated:514 return list(get_args(inner_type)[1:])515516 # Handle direct Annotated[...]517 elif get_origin(type_) is Annotated:518 return list(get_args(type_)[1:])519520 return []521522523def _get_can_jump_to(middleware: AgentMiddleware[Any, Any], hook_name: str) -> list[JumpTo]:524 """Get the `can_jump_to` list from either sync or async hook methods.525526 Args:527 middleware: The middleware instance to inspect.528 hook_name: The name of the hook (`'before_model'` or `'after_model'`).529530 Returns:531 List of jump destinations, or empty list if not configured.532 """533 # Get the base class method for comparison534 base_sync_method = getattr(AgentMiddleware, hook_name, None)535 base_async_method = getattr(AgentMiddleware, f"a{hook_name}", None)536537 # Try sync method first - only if it's overridden from base class538 sync_method = getattr(middleware.__class__, hook_name, None)539 if (540 sync_method541 and sync_method is not base_sync_method542 and hasattr(sync_method, "__can_jump_to__")543 ):544 # `hasattr` proves the metadata exists at runtime, but not its value type.545 return cast("list[JumpTo]", sync_method.__can_jump_to__)546547 # Try async method - only if it's overridden from base class548 async_method = getattr(middleware.__class__, f"a{hook_name}", None)549 if (550 async_method551 and async_method is not base_async_method552 and hasattr(async_method, "__can_jump_to__")553 ):554 # `hasattr` proves the metadata exists at runtime, but not its value type.555 return cast("list[JumpTo]", async_method.__can_jump_to__)556557 return []558559560def _supports_provider_strategy(561 model: str | BaseChatModel, tools: list[BaseTool | dict[str, Any]] | None = None562) -> bool:563 """Check if a model supports provider-specific structured output.564565 Args:566 model: Model name string or `BaseChatModel` instance.567 tools: Optional list of tools provided to the agent.568569 Needed because some models don't support structured output together with tool calling.570571 Returns:572 `True` if the model supports provider-specific structured output, `False` otherwise.573 """574 model_name: str | None = None575 if isinstance(model, str):576 model_name = model577 elif isinstance(model, BaseChatModel):578 model_name = (579 getattr(model, "model_name", None)580 or getattr(model, "model", None)581 or getattr(model, "model_id", "")582 )583 model_profile = model.profile584 if (585 model_profile is not None586 and model_profile.get("structured_output")587 # We make an exception for Gemini < 3-series models, which currently do not support588 # simultaneous tool use with structured output; 3-series can.589 and not (590 tools591 and isinstance(model_name, str)592 and "gemini" in model_name.lower()593 and "gemini-3" not in model_name.lower()594 )595 ):596 return True597598 return (599 any(600 re.search(pattern, model_name.lower())601 for pattern in FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT602 )603 if model_name604 else False605 )606607608def _is_openai_compatible_model(model: BaseChatModel) -> bool:609 """Check if a model inherits from `BaseChatOpenAI`.610611 Used to redundantly set `strict=True` on tools when `response_format` is612 provided, as older versions of `langchain-openai` do not auto-set it.613 Covers `ChatOpenAI`, `ChatDeepSeek`, `ChatXAI`, etc.614615 Args:616 model: The chat model to check.617618 Returns:619 `True` if the model inherits from `BaseChatOpenAI`, `False` otherwise.620 """621 try:622 base_chat_openai = importlib.import_module("langchain_openai.chat_models.base")623 except ImportError:624 return False625 return isinstance(model, base_chat_openai.BaseChatOpenAI)626627628def _handle_structured_output_error(629 exception: Exception,630 response_format: ResponseFormat[Any],631) -> tuple[bool, str]:632 """Handle structured output error.633634 Returns `(should_retry, retry_tool_message)`.635 """636 if not isinstance(response_format, ToolStrategy):637 return False, ""638639 handle_errors = response_format.handle_errors640641 if handle_errors is False:642 return False, ""643 if handle_errors is True:644 return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))645 if isinstance(handle_errors, str):646 return True, handle_errors647 if isinstance(handle_errors, type):648 if issubclass(handle_errors, Exception) and isinstance(exception, handle_errors):649 return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))650 return False, ""651 if isinstance(handle_errors, tuple):652 if any(isinstance(exception, exc_type) for exc_type in handle_errors):653 return True, STRUCTURED_OUTPUT_ERROR_TEMPLATE.format(error=str(exception))654 return False, ""655 return True, handle_errors(exception)656657658def _chain_tool_call_wrappers(659 wrappers: Sequence[ToolCallWrapper],660) -> ToolCallWrapper | None:661 """Compose wrappers into middleware stack (first = outermost).662663 Args:664 wrappers: Wrappers in middleware order.665666 Returns:667 Composed wrapper, or `None` if empty.668669 Example:670 ```python671 wrapper = _chain_tool_call_wrappers([auth, cache, retry])672 # Request flows: auth -> cache -> retry -> tool673 # Response flows: tool -> retry -> cache -> auth674 ```675 """676 if not wrappers:677 return None678679 if len(wrappers) == 1:680 return wrappers[0]681682 def compose_two(outer: ToolCallWrapper, inner: ToolCallWrapper) -> ToolCallWrapper:683 """Compose two wrappers where outer wraps inner."""684685 def composed(686 request: ToolCallRequest,687 execute: Callable[[ToolCallRequest], ToolMessage | Command[Any]],688 ) -> ToolMessage | Command[Any]:689 # Create a callable that invokes inner with the original execute690 def call_inner(req: ToolCallRequest) -> ToolMessage | Command[Any]:691 return inner(req, execute)692693 # Outer can call call_inner multiple times694 return outer(request, call_inner)695696 return composed697698 # Chain all wrappers: first -> second -> ... -> last699 result = wrappers[-1]700 for wrapper in reversed(wrappers[:-1]):701 result = compose_two(wrapper, result)702703 return result704705706def _chain_async_tool_call_wrappers(707 wrappers: Sequence[708 Callable[709 [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],710 Awaitable[ToolMessage | Command[Any]],711 ]712 ],713) -> (714 Callable[715 [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],716 Awaitable[ToolMessage | Command[Any]],717 ]718 | None719):720 """Compose async wrappers into middleware stack (first = outermost).721722 Args:723 wrappers: Async wrappers in middleware order.724725 Returns:726 Composed async wrapper, or `None` if empty.727 """728 if not wrappers:729 return None730731 if len(wrappers) == 1:732 return wrappers[0]733734 def compose_two(735 outer: Callable[736 [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],737 Awaitable[ToolMessage | Command[Any]],738 ],739 inner: Callable[740 [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],741 Awaitable[ToolMessage | Command[Any]],742 ],743 ) -> Callable[744 [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]]],745 Awaitable[ToolMessage | Command[Any]],746 ]:747 """Compose two async wrappers where outer wraps inner."""748749 async def composed(750 request: ToolCallRequest,751 execute: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],752 ) -> ToolMessage | Command[Any]:753 # Create an async callable that invokes inner with the original execute754 async def call_inner(req: ToolCallRequest) -> ToolMessage | Command[Any]:755 return await inner(req, execute)756757 # Outer can call call_inner multiple times758 return await outer(request, call_inner)759760 return composed761762 # Chain all wrappers: first -> second -> ... -> last763 result = wrappers[-1]764 for wrapper in reversed(wrappers[:-1]):765 result = compose_two(wrapper, result)766767 return result768769770# No `response_format`: there is no structured output, so `ResponseT` resolves to `Any`.771@overload772def create_agent(773 model: str | BaseChatModel,774 tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,775 *,776 system_prompt: str | SystemMessage | None = None,777 middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),778 response_format: None = None,779 state_schema: None = None,780 context_schema: type[ContextT] | None = None,781 checkpointer: Checkpointer | None = None,782 store: BaseStore | None = None,783 interrupt_before: list[str] | None = None,784 interrupt_after: list[str] | None = None,785 debug: bool = False,786 name: str | None = None,787 cache: BaseCache[Any] | None = None,788 transformers: Sequence[TransformerFactory] | None = None,789) -> CompiledStateGraph[AgentState[Any], ContextT, InputAgentState, OutputAgentState[Any]]: ...790791792# Raw-dict `response_format`: structured output is an untyped `dict[str, Any]`.793@overload794def create_agent(795 model: str | BaseChatModel,796 tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,797 *,798 system_prompt: str | SystemMessage | None = None,799 middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),800 response_format: dict[str, Any],801 state_schema: type[AgentState[dict[str, Any]]] | None = None,802 context_schema: type[ContextT] | None = None,803 checkpointer: Checkpointer | None = None,804 store: BaseStore | None = None,805 interrupt_before: list[str] | None = None,806 interrupt_after: list[str] | None = None,807 debug: bool = False,808 name: str | None = None,809 cache: BaseCache[Any] | None = None,810 transformers: Sequence[TransformerFactory] | None = None,811) -> CompiledStateGraph[812 AgentState[dict[str, Any]], ContextT, InputAgentState, OutputAgentState[dict[str, Any]]813]: ...814815816# Schema-typed `response_format`: `ResponseT` is inferred from the schema/type.817@overload818def create_agent(819 model: str | BaseChatModel,820 tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,821 *,822 system_prompt: str | SystemMessage | None = None,823 middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),824 response_format: ResponseFormat[ResponseT] | type[ResponseT] | None = None,825 state_schema: type[AgentState[ResponseT]] | None = None,826 context_schema: type[ContextT] | None = None,827 checkpointer: Checkpointer | None = None,828 store: BaseStore | None = None,829 interrupt_before: list[str] | None = None,830 interrupt_after: list[str] | None = None,831 debug: bool = False,832 name: str | None = None,833 cache: BaseCache[Any] | None = None,834 transformers: Sequence[TransformerFactory] | None = None,835) -> CompiledStateGraph[836 AgentState[ResponseT], ContextT, InputAgentState, OutputAgentState[ResponseT]837]: ...838839840def create_agent(841 model: str | BaseChatModel,842 tools: Sequence[BaseTool | Callable[..., Any] | dict[str, Any]] | None = None,843 *,844 system_prompt: str | SystemMessage | None = None,845 middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),846 response_format: ResponseFormat[ResponseT] | type[ResponseT] | dict[str, Any] | None = None,847 state_schema: type[AgentState[ResponseT]] | None = None,848 context_schema: type[ContextT] | None = None,849 checkpointer: Checkpointer | None = None,850 store: BaseStore | None = None,851 interrupt_before: list[str] | None = None,852 interrupt_after: list[str] | None = None,853 debug: bool = False,854 name: str | None = None,855 cache: BaseCache[Any] | None = None,856 transformers: Sequence[TransformerFactory] | None = None,857) -> CompiledStateGraph[858 AgentState[ResponseT], ContextT, InputAgentState, OutputAgentState[ResponseT]859]:860 """Creates an agent graph that calls tools in a loop until a stopping condition is met.861862 For more details on using `create_agent`,863 visit the [Agents](https://docs.langchain.com/oss/python/langchain/agents) docs.864865 Args:866 model: The language model for the agent.867868 Can be a string identifier (e.g., `"openai:gpt-5.5"`) or a direct chat model869 instance (e.g., [`ChatOpenAI`][langchain_openai.ChatOpenAI] or other another870 [LangChain chat model](https://docs.langchain.com/oss/python/integrations/chat)).871872 For a full list of supported model strings, see873 [`init_chat_model`][langchain.chat_models.init_chat_model(model_provider)].874875 !!! tip ""876877 See the [Models](https://docs.langchain.com/oss/python/langchain/models)878 docs for more information.879 tools: A list of tools, `dict`, or `Callable`.880881 If `None` or an empty list, the agent will consist of a model node without a882 tool calling loop.883884885 !!! tip ""886887 See the [Tools](https://docs.langchain.com/oss/python/langchain/tools)888 docs for more information.889 system_prompt: An optional system prompt for the LLM.890891 Can be a `str` (which will be converted to a `SystemMessage`) or a892 `SystemMessage` instance directly. The system message is added to the893 beginning of the message list when calling the model.894 middleware: A sequence of middleware instances to apply to the agent.895896 Middleware can intercept and modify agent behavior at various stages.897898 !!! tip ""899900 See the [Middleware](https://docs.langchain.com/oss/python/langchain/middleware)901 docs for more information.902 response_format: An optional configuration for structured responses.903904 Can be a `ToolStrategy`, `ProviderStrategy`, or a Pydantic model class.905906 If provided, the agent will handle structured output during the907 conversation flow.908909 Raw schemas will be wrapped in an appropriate strategy based on model910 capabilities.911912 !!! tip ""913914 See the [Structured output](https://docs.langchain.com/oss/python/langchain/structured-output)915 docs for more information.916 state_schema: An optional `TypedDict` schema that extends `AgentState`.917918 When provided, this schema is used instead of `AgentState` as the base919 schema for merging with middleware state schemas. This allows users to920 add custom state fields without needing to create custom middleware.921922 Generally, it's recommended to use `state_schema` extensions via middleware923 to keep relevant extensions scoped to corresponding hooks / tools.924 context_schema: An optional schema for runtime context.925 checkpointer: An optional checkpoint saver object.926927 Used for persisting the state of the graph (e.g., as chat memory) for a928 single thread (e.g., a single conversation).929 store: An optional store object.930931 Used for persisting data across multiple threads (e.g., multiple932 conversations / users).933 interrupt_before: An optional list of node names to interrupt before.934935 Useful if you want to add a user confirmation or other interrupt936 before taking an action.937 interrupt_after: An optional list of node names to interrupt after.938939 Useful if you want to return directly or run additional processing940 on an output.941 debug: Whether to enable verbose logging for graph execution.942943 When enabled, prints detailed information about each node execution, state944 updates, and transitions during agent runtime. Useful for debugging945 middleware behavior and understanding agent execution flow.946 name: An optional name for the `CompiledStateGraph`.947948 This name will be automatically used when adding the agent graph to949 another graph as a subgraph node - particularly useful for building950 multi-agent systems.951 cache: An optional `BaseCache` instance to enable caching of graph execution.952 transformers: Optional sequence of scope-aware `StreamTransformer`953 factories to register on the compiled graph in addition to954 the agent defaults. Each factory is invoked as `factory(scope)`955 so every invocation receives a fresh instance. The final order956 on the compiled graph is: `ToolCallTransformer`, then any957 factories declared by middleware via958 `AgentMiddleware.transformers`, then any factories supplied here.959960 Returns:961 A compiled `StateGraph` that can be used for chat interactions.962963 Raises:964 AssertionError: If duplicate middleware instances are provided.965966 The agent node calls the language model with the messages list (after applying967 the system prompt). If the resulting [`AIMessage`][langchain.messages.AIMessage]968 contains `tool_calls`, the graph will then call the tools. The tools node executes969 the tools and adds the responses to the messages list as970 [`ToolMessage`][langchain.messages.ToolMessage] objects. The agent node then calls971 the language model again. The process repeats until no more `tool_calls` are present972 in the response. The agent then returns the full list of messages.973974 Example:975 ```python976 from langchain.agents import create_agent977978979 def check_weather(location: str) -> str:980 '''Return the weather forecast for the specified location.'''981 return f"It's always sunny in {location}"982983984 graph = create_agent(985 model="anthropic:claude-sonnet-4-5-20250929",986 tools=[check_weather],987 system_prompt="You are a helpful assistant",988 )989 inputs = {"messages": [{"role": "user", "content": "what is the weather in sf"}]}990 for chunk in graph.stream(inputs, stream_mode="updates"):991 print(chunk)992 ```993 """994 # init chat model995 if isinstance(model, str):996 model = init_chat_model(model)997998 # Convert system_prompt to SystemMessage if needed999 system_message: SystemMessage | None = None1000 if system_prompt is not None:1001 if isinstance(system_prompt, SystemMessage):1002 system_message = system_prompt1003 else:1004 system_message = SystemMessage(content=system_prompt)10051006 # Handle tools being None or empty1007 if tools is None:1008 tools = []10091010 # Convert response format and setup structured output tools1011 # Raw schemas are wrapped in AutoStrategy to preserve auto-detection intent.1012 # AutoStrategy is converted to ToolStrategy upfront to calculate tools during agent creation,1013 # but may be replaced with ProviderStrategy later based on model capabilities.1014 initial_response_format: ToolStrategy[Any] | ProviderStrategy[Any] | AutoStrategy[Any] | None1015 if response_format is None:1016 initial_response_format = None1017 elif isinstance(response_format, (ToolStrategy, ProviderStrategy, AutoStrategy)):1018 # Explicit Tool/Provider strategy, or AutoStrategy for later capability detection1019 initial_response_format = response_format1020 else:1021 # Raw schema - wrap in AutoStrategy to enable auto-detection1022 initial_response_format = AutoStrategy(schema=response_format)10231024 # For AutoStrategy, convert to ToolStrategy to setup tools upfront1025 # (may be replaced with ProviderStrategy later based on model)1026 tool_strategy_for_setup: ToolStrategy[Any] | None = None1027 if isinstance(initial_response_format, AutoStrategy):1028 tool_strategy_for_setup = ToolStrategy(schema=initial_response_format.schema)1029 elif isinstance(initial_response_format, ToolStrategy):1030 tool_strategy_for_setup = initial_response_format10311032 structured_output_tools: dict[str, OutputToolBinding[Any]] = {}1033 if tool_strategy_for_setup:1034 for response_schema in tool_strategy_for_setup.schema_specs:1035 structured_tool_info = OutputToolBinding.from_schema_spec(response_schema)1036 structured_output_tools[structured_tool_info.tool.name] = structured_tool_info1037 middleware_tools = [t for m in middleware for t in getattr(m, "tools", [])]10381039 # Collect middleware with wrap_tool_call or awrap_tool_call hooks1040 # Include middleware with either implementation to ensure NotImplementedError is raised1041 # when middleware doesn't support the execution path1042 middleware_w_wrap_tool_call = [1043 m1044 for m in middleware1045 if m.__class__.wrap_tool_call is not AgentMiddleware.wrap_tool_call1046 or m.__class__.awrap_tool_call is not AgentMiddleware.awrap_tool_call1047 ]10481049 # Chain all wrap_tool_call handlers into a single composed handler1050 wrap_tool_call_wrapper = None1051 if middleware_w_wrap_tool_call:1052 wrappers = [1053 traceable(name=f"{m.name}.wrap_tool_call", **_wrap_trace_kwargs(m))(m.wrap_tool_call)1054 for m in middleware_w_wrap_tool_call1055 ]1056 wrap_tool_call_wrapper = _chain_tool_call_wrappers(wrappers)10571058 # Collect middleware with awrap_tool_call or wrap_tool_call hooks1059 # Include middleware with either implementation to ensure NotImplementedError is raised1060 # when middleware doesn't support the execution path1061 middleware_w_awrap_tool_call = [1062 m1063 for m in middleware1064 if m.__class__.awrap_tool_call is not AgentMiddleware.awrap_tool_call1065 or m.__class__.wrap_tool_call is not AgentMiddleware.wrap_tool_call1066 ]10671068 # Chain all awrap_tool_call handlers into a single composed async handler1069 awrap_tool_call_wrapper = None1070 if middleware_w_awrap_tool_call:1071 async_wrappers = [1072 traceable(name=f"{m.name}.awrap_tool_call", **_wrap_trace_kwargs(m))(m.awrap_tool_call)1073 for m in middleware_w_awrap_tool_call1074 ]1075 awrap_tool_call_wrapper = _chain_async_tool_call_wrappers(async_wrappers)10761077 # Setup tools1078 tool_node: ToolNode | None = None1079 # Extract built-in provider tools (dict format) and regular tools (BaseTool/callables)1080 built_in_tools = [t for t in tools if isinstance(t, dict)]1081 regular_tools = [t for t in tools if not isinstance(t, dict)]10821083 # Tools that require client-side execution (must be in ToolNode)1084 available_tools = middleware_tools + regular_tools10851086 # Create ToolNode if we have client-side tools OR if middleware defines wrap_tool_call1087 # (which may handle dynamically registered tools)1088 tool_node = (1089 ToolNode(1090 tools=available_tools,1091 wrap_tool_call=wrap_tool_call_wrapper,1092 awrap_tool_call=awrap_tool_call_wrapper,1093 )1094 if available_tools or wrap_tool_call_wrapper or awrap_tool_call_wrapper1095 else None1096 )10971098 # Default tools for ModelRequest initialization1099 # Use converted BaseTool instances from ToolNode (not raw callables)1100 # Include built-ins and converted tools (can be changed dynamically by middleware)1101 # Structured tools are NOT included - they're added dynamically based on response_format1102 if tool_node:1103 default_tools = list(tool_node.tools_by_name.values()) + built_in_tools1104 else:1105 default_tools = list(built_in_tools)11061107 # validate middleware1108 if len({m.name for m in middleware}) != len(middleware):1109 msg = "Please remove duplicate middleware instances."1110 raise AssertionError(msg)1111 middleware_w_before_agent = [1112 m1113 for m in middleware1114 if m.__class__.before_agent is not AgentMiddleware.before_agent1115 or m.__class__.abefore_agent is not AgentMiddleware.abefore_agent1116 ]1117 middleware_w_before_model = [1118 m1119 for m in middleware1120 if m.__class__.before_model is not AgentMiddleware.before_model1121 or m.__class__.abefore_model is not AgentMiddleware.abefore_model1122 ]1123 middleware_w_after_model = [1124 m1125 for m in middleware1126 if m.__class__.after_model is not AgentMiddleware.after_model1127 or m.__class__.aafter_model is not AgentMiddleware.aafter_model1128 ]1129 middleware_w_after_agent = [1130 m1131 for m in middleware1132 if m.__class__.after_agent is not AgentMiddleware.after_agent1133 or m.__class__.aafter_agent is not AgentMiddleware.aafter_agent1134 ]1135 # Collect middleware with wrap_model_call or awrap_model_call hooks1136 # Include middleware with either implementation to ensure NotImplementedError is raised1137 # when middleware doesn't support the execution path1138 middleware_w_wrap_model_call = [1139 m1140 for m in middleware1141 if m.__class__.wrap_model_call is not AgentMiddleware.wrap_model_call1142 or m.__class__.awrap_model_call is not AgentMiddleware.awrap_model_call1143 ]1144 # Collect middleware with awrap_model_call or wrap_model_call hooks1145 # Include middleware with either implementation to ensure NotImplementedError is raised1146 # when middleware doesn't support the execution path1147 middleware_w_awrap_model_call = [1148 m1149 for m in middleware1150 if m.__class__.awrap_model_call is not AgentMiddleware.awrap_model_call1151 or m.__class__.wrap_model_call is not AgentMiddleware.wrap_model_call1152 ]11531154 # Compose wrap_model_call handlers into a single middleware stack (sync)1155 wrap_model_call_handler = None1156 if middleware_w_wrap_model_call:1157 sync_handlers = [1158 traceable(name=f"{m.name}.wrap_model_call", **_wrap_trace_kwargs(m))(m.wrap_model_call)1159 for m in middleware_w_wrap_model_call1160 ]1161 wrap_model_call_handler = _chain_model_call_handlers(sync_handlers)11621163 # Compose awrap_model_call handlers into a single middleware stack (async)1164 awrap_model_call_handler = None1165 if middleware_w_awrap_model_call:1166 async_handlers = [1167 traceable(name=f"{m.name}.awrap_model_call", **_wrap_trace_kwargs(m))(1168 m.awrap_model_call1169 )1170 for m in middleware_w_awrap_model_call1171 ]1172 awrap_model_call_handler = _chain_async_model_call_handlers(async_handlers)11731174 base_state = state_schema if state_schema is not None else AgentState1175 # Build an ordered list: middleware schemas first (in registration order),1176 # base_state last so it wins any field conflict. This lets the caller's1177 # explicit state_schema override middleware annotations — e.g. passing1178 # a DeltaChannel-annotated schema wins over BinaryOperatorAggregate from1179 # AgentState without requiring a post-compilation patch.1180 state_schemas: list[type] = [*(m.state_schema for m in middleware), base_state]11811182 resolved_state_schema, input_schema, output_schema = _resolve_schemas(state_schemas)11831184 # create graph, add nodes1185 graph: StateGraph[1186 AgentState[ResponseT], ContextT, InputAgentState, OutputAgentState[ResponseT]1187 ] = StateGraph(1188 state_schema=resolved_state_schema,1189 input_schema=input_schema,1190 output_schema=output_schema,1191 context_schema=context_schema,1192 )11931194 def _handle_model_output(1195 output: AIMessage, effective_response_format: ResponseFormat[Any] | None1196 ) -> dict[str, Any]:1197 """Handle model output including structured responses.11981199 Args:1200 output: The AI message output from the model.1201 effective_response_format: The actual strategy used (may differ from initial1202 if auto-detected).1203 """1204 # Handle structured output with provider strategy1205 if isinstance(effective_response_format, ProviderStrategy):1206 if not output.tool_calls:1207 provider_strategy_binding = ProviderStrategyBinding.from_schema_spec(1208 effective_response_format.schema_spec1209 )1210 try:1211 structured_response = provider_strategy_binding.parse(output)1212 except Exception as exc:1213 schema_name = getattr(1214 effective_response_format.schema_spec.schema, "__name__", "response_format"1215 )1216 validation_error = StructuredOutputValidationError(schema_name, exc, output)1217 raise validation_error from exc1218 else:1219 return {"messages": [output], "structured_response": structured_response}1220 return {"messages": [output]}12211222 # Handle structured output with tool strategy1223 if (1224 isinstance(effective_response_format, ToolStrategy)1225 and isinstance(output, AIMessage)1226 and output.tool_calls1227 ):1228 structured_tool_calls = [1229 tc for tc in output.tool_calls if tc["name"] in structured_output_tools1230 ]12311232 if structured_tool_calls:1233 exception: StructuredOutputError | None = None1234 if len(structured_tool_calls) > 1:1235 # Handle multiple structured outputs error1236 tool_names = [tc["name"] for tc in structured_tool_calls]1237 exception = MultipleStructuredOutputsError(tool_names, output)1238 should_retry, error_message = _handle_structured_output_error(1239 exception, effective_response_format1240 )1241 if not should_retry:1242 raise exception12431244 # Add error messages and retry1245 tool_messages = [1246 ToolMessage(1247 content=error_message,1248 tool_call_id=tc["id"],1249 name=tc["name"],1250 )1251 for tc in structured_tool_calls1252 ]1253 return {"messages": [output, *tool_messages]}12541255 # Handle single structured output1256 tool_call = structured_tool_calls[0]1257 try:1258 structured_tool_binding = structured_output_tools[tool_call["name"]]1259 structured_response = structured_tool_binding.parse(tool_call["args"])12601261 tool_message_content = (1262 effective_response_format.tool_message_content1263 or f"Returning structured response: {structured_response}"1264 )12651266 return {1267 "messages": [1268 output,1269 ToolMessage(1270 content=tool_message_content,1271 tool_call_id=tool_call["id"],1272 name=tool_call["name"],1273 ),1274 ],1275 "structured_response": structured_response,1276 }1277 except Exception as exc:1278 exception = StructuredOutputValidationError(tool_call["name"], exc, output)1279 should_retry, error_message = _handle_structured_output_error(1280 exception, effective_response_format1281 )1282 if not should_retry:1283 raise exception from exc12841285 return {1286 "messages": [1287 output,1288 ToolMessage(1289 content=error_message,1290 tool_call_id=tool_call["id"],1291 name=tool_call["name"],1292 ),1293 ],1294 }12951296 return {"messages": [output]}12971298 def _get_bound_model(1299 request: ModelRequest[ContextT],1300 ) -> tuple[Runnable[Any, Any], ResponseFormat[Any] | None]:1301 """Get the model with appropriate tool bindings.13021303 Performs auto-detection of strategy if needed based on model capabilities.13041305 Args:1306 request: The model request containing model, tools, and response format.13071308 Returns:1309 Tuple of `(bound_model, effective_response_format)` where1310 `effective_response_format` is the actual strategy used (may differ from1311 initial if auto-detected).13121313 Raises:1314 ValueError: If middleware returned unknown client-side tool names.1315 ValueError: If `ToolStrategy` specifies tools not declared upfront.1316 """1317 # Validate ONLY client-side tools that need to exist in tool_node1318 # Skip validation when wrap_tool_call is defined, as middleware may handle1319 # dynamic tools that are added at runtime via wrap_model_call1320 has_wrap_tool_call = wrap_tool_call_wrapper or awrap_tool_call_wrapper13211322 # Build map of available client-side tools from the ToolNode1323 # (which has already converted callables)1324 available_tools_by_name = {}1325 if tool_node:1326 available_tools_by_name = tool_node.tools_by_name.copy()13271328 # Check if any requested tools are unknown CLIENT-SIDE tools1329 # Only validate if wrap_tool_call is NOT defined (no dynamic tool handling)1330 if not has_wrap_tool_call:1331 unknown_tool_names = []1332 for t in request.tools:1333 # Only validate BaseTool instances (skip built-in dict tools)1334 if isinstance(t, dict):1335 continue1336 if isinstance(t, BaseTool) and t.name not in available_tools_by_name:1337 unknown_tool_names.append(t.name)13381339 if unknown_tool_names:1340 available_tool_names = sorted(available_tools_by_name.keys())1341 msg = DYNAMIC_TOOL_ERROR_TEMPLATE.format(1342 unknown_tool_names=unknown_tool_names,1343 available_tool_names=available_tool_names,1344 )1345 raise ValueError(msg)13461347 # Normalize raw schemas to AutoStrategy1348 # (handles middleware override with raw Pydantic classes)1349 response_format: ResponseFormat[Any] | Any | None = request.response_format1350 if response_format is not None and not isinstance(1351 response_format, (AutoStrategy, ToolStrategy, ProviderStrategy)1352 ):1353 response_format = AutoStrategy(schema=response_format)13541355 # Determine effective response format (auto-detect if needed)1356 effective_response_format: ResponseFormat[Any] | None1357 if isinstance(response_format, AutoStrategy):1358 # User provided raw schema via AutoStrategy - auto-detect best strategy based on model1359 if _supports_provider_strategy(request.model, tools=request.tools):1360 # Model supports provider strategy - use it1361 effective_response_format = ProviderStrategy(schema=response_format.schema)1362 elif response_format is initial_response_format and tool_strategy_for_setup is not None:1363 # Model doesn't support provider strategy - use ToolStrategy1364 # Reuse the strategy from setup if possible to preserve tool names1365 effective_response_format = tool_strategy_for_setup1366 else:1367 effective_response_format = ToolStrategy(schema=response_format.schema)1368 else:1369 # User explicitly specified a strategy - preserve it1370 effective_response_format = response_format13711372 # Build final tools list including structured output tools1373 # request.tools now only contains BaseTool instances (converted from callables)1374 # and dicts (built-ins)1375 final_tools = list(request.tools)1376 if isinstance(effective_response_format, ToolStrategy):1377 # Add structured output tools to final tools list, narrowed to only the1378 # schemas present in the (possibly middleware-narrowed) response format.1379 # This ensures middleware that narrows a union `ToolStrategy` to a subset1380 # via `request.override()` actually restricts which structured output1381 # tools the model can choose from.1382 narrowed_tool_names = {spec.name for spec in effective_response_format.schema_specs}1383 structured_tools = [1384 info.tool1385 for name, info in structured_output_tools.items()1386 if name in narrowed_tool_names1387 ]1388 final_tools.extend(structured_tools)13891390 # Bind model based on effective response format1391 if isinstance(effective_response_format, ProviderStrategy):1392 # (Backward compatibility) Use OpenAI format structured output1393 # Redundantly set strict=True on tools for OpenAI-compatible models, as older1394 # versions of langchain-openai do not auto-set it in bind_tools.1395 kwargs = effective_response_format.to_model_kwargs()1396 bind_kwargs: dict[str, Any] = {**kwargs, **request.model_settings}1397 if _is_openai_compatible_model(request.model) and not getattr(1398 request.model, "use_responses_api", False1399 ):1400 bind_kwargs["strict"] = True1401 return (1402 request.model.bind_tools(final_tools, **bind_kwargs),1403 effective_response_format,1404 )14051406 if isinstance(effective_response_format, ToolStrategy):1407 # Current implementation requires that tools used for structured output1408 # have to be declared upfront when creating the agent as part of the1409 # response format. Middleware is allowed to change the response format1410 # to a subset of the original structured tools when using ToolStrategy,1411 # but not to add new structured tools that weren't declared upfront.1412 # Compute output binding1413 for tc in effective_response_format.schema_specs:1414 if tc.name not in structured_output_tools:1415 msg = (1416 f"ToolStrategy specifies tool '{tc.name}' "1417 "which wasn't declared in the original "1418 "response format when creating the agent."1419 )1420 raise ValueError(msg)14211422 # Force tool use if we have structured output tools1423 tool_choice = "any" if structured_output_tools else request.tool_choice1424 return (1425 request.model.bind_tools(1426 final_tools, tool_choice=tool_choice, **request.model_settings1427 ),1428 effective_response_format,1429 )14301431 # No structured output - standard model binding1432 if final_tools:1433 return (1434 request.model.bind_tools(1435 final_tools, tool_choice=request.tool_choice, **request.model_settings1436 ),1437 None,1438 )1439 return request.model.bind(**request.model_settings), None14401441 def _execute_model_sync(request: ModelRequest[ContextT]) -> ModelResponse:1442 """Execute model and return response.14431444 This is the core model execution logic wrapped by `wrap_model_call` handlers.14451446 Raises any exceptions that occur during model invocation.1447 """1448 # Get the bound model (with auto-detection if needed)1449 model_, effective_response_format = _get_bound_model(request)1450 messages = request.messages1451 if request.system_message:1452 messages = [request.system_message, *messages]14531454 output = model_.invoke(messages)1455 if name:1456 output.name = name14571458 # Handle model output to get messages and structured_response1459 handled_output = _handle_model_output(output, effective_response_format)1460 messages_list = handled_output["messages"]1461 structured_response = handled_output.get("structured_response")14621463 return ModelResponse(1464 result=messages_list,1465 structured_response=structured_response,1466 )14671468 def model_node(state: AgentState[Any], runtime: Runtime[ContextT]) -> list[Command[Any]]:1469 """Sync model request handler with sequential middleware processing."""1470 request = ModelRequest(1471 model=model,1472 tools=default_tools,1473 system_message=system_message,1474 response_format=initial_response_format,1475 messages=state["messages"],1476 tool_choice=None,1477 state=state,1478 runtime=runtime,1479 )14801481 has_structured_output = initial_response_format is not None1482 if wrap_model_call_handler is None:1483 model_response = _execute_model_sync(request)1484 return _build_commands(model_response, has_structured_output=has_structured_output)14851486 result = wrap_model_call_handler(request, _execute_model_sync)1487 return _build_commands(1488 result.model_response, result.commands, has_structured_output=has_structured_output1489 )14901491 async def _execute_model_async(request: ModelRequest[ContextT]) -> ModelResponse:1492 """Execute model asynchronously and return response.14931494 This is the core async model execution logic wrapped by `wrap_model_call`1495 handlers.14961497 Raises any exceptions that occur during model invocation.1498 """1499 # Get the bound model (with auto-detection if needed)1500 model_, effective_response_format = _get_bound_model(request)1501 messages = request.messages1502 if request.system_message:1503 messages = [request.system_message, *messages]15041505 output = await model_.ainvoke(messages)1506 if name:1507 output.name = name15081509 # Handle model output to get messages and structured_response1510 handled_output = _handle_model_output(output, effective_response_format)1511 messages_list = handled_output["messages"]1512 structured_response = handled_output.get("structured_response")15131514 return ModelResponse(1515 result=messages_list,1516 structured_response=structured_response,1517 )15181519 async def amodel_node(state: AgentState[Any], runtime: Runtime[ContextT]) -> list[Command[Any]]:1520 """Async model request handler with sequential middleware processing."""1521 request = ModelRequest(1522 model=model,1523 tools=default_tools,1524 system_message=system_message,1525 response_format=initial_response_format,1526 messages=state["messages"],1527 tool_choice=None,1528 state=state,1529 runtime=runtime,1530 )15311532 has_structured_output = initial_response_format is not None1533 if awrap_model_call_handler is None:1534 model_response = await _execute_model_async(request)1535 return _build_commands(model_response, has_structured_output=has_structured_output)15361537 result = await awrap_model_call_handler(request, _execute_model_async)1538 return _build_commands(1539 result.model_response, result.commands, has_structured_output=has_structured_output1540 )15411542 # Use sync or async based on model capabilities1543 graph.add_node("model", RunnableCallable(model_node, amodel_node, trace=False))15441545 # Only add tools node if we have tools1546 if tool_node is not None:1547 graph.add_node("tools", tool_node)15481549 # Add middleware nodes1550 for m in middleware:1551 if (1552 m.__class__.before_agent is not AgentMiddleware.before_agent1553 or m.__class__.abefore_agent is not AgentMiddleware.abefore_agent1554 ):1555 # Use RunnableCallable to support both sync and async1556 # Pass None for sync if not overridden to avoid signature conflicts1557 sync_before_agent = (1558 m.before_agent1559 if m.__class__.before_agent is not AgentMiddleware.before_agent1560 else None1561 )1562 async_before_agent = (1563 m.abefore_agent1564 if m.__class__.abefore_agent is not AgentMiddleware.abefore_agent1565 else None1566 )1567 before_agent_node = RunnableCallable(sync_before_agent, async_before_agent, trace=False)1568 graph.add_node(1569 f"{m.name}.before_agent",1570 before_agent_node,1571 input_schema=resolved_state_schema,1572 trace_policy=_node_trace_policy(m.trace_policy),1573 )15741575 if (1576 m.__class__.before_model is not AgentMiddleware.before_model1577 or m.__class__.abefore_model is not AgentMiddleware.abefore_model1578 ):1579 # Use RunnableCallable to support both sync and async1580 # Pass None for sync if not overridden to avoid signature conflicts1581 sync_before = (1582 m.before_model1583 if m.__class__.before_model is not AgentMiddleware.before_model1584 else None1585 )1586 async_before = (1587 m.abefore_model1588 if m.__class__.abefore_model is not AgentMiddleware.abefore_model1589 else None1590 )1591 before_node = RunnableCallable(sync_before, async_before, trace=False)1592 graph.add_node(1593 f"{m.name}.before_model",1594 before_node,1595 input_schema=resolved_state_schema,1596 trace_policy=_node_trace_policy(m.trace_policy),1597 )15981599 if (1600 m.__class__.after_model is not AgentMiddleware.after_model1601 or m.__class__.aafter_model is not AgentMiddleware.aafter_model1602 ):1603 # Use RunnableCallable to support both sync and async1604 # Pass None for sync if not overridden to avoid signature conflicts1605 sync_after = (1606 m.after_model1607 if m.__class__.after_model is not AgentMiddleware.after_model1608 else None1609 )1610 async_after = (1611 m.aafter_model1612 if m.__class__.aafter_model is not AgentMiddleware.aafter_model1613 else None1614 )1615 after_node = RunnableCallable(sync_after, async_after, trace=False)1616 graph.add_node(1617 f"{m.name}.after_model",1618 after_node,1619 input_schema=resolved_state_schema,1620 trace_policy=_node_trace_policy(m.trace_policy),1621 )16221623 if (1624 m.__class__.after_agent is not AgentMiddleware.after_agent1625 or m.__class__.aafter_agent is not AgentMiddleware.aafter_agent1626 ):1627 # Use RunnableCallable to support both sync and async1628 # Pass None for sync if not overridden to avoid signature conflicts1629 sync_after_agent = (1630 m.after_agent1631 if m.__class__.after_agent is not AgentMiddleware.after_agent1632 else None1633 )1634 async_after_agent = (1635 m.aafter_agent1636 if m.__class__.aafter_agent is not AgentMiddleware.aafter_agent1637 else None1638 )1639 after_agent_node = RunnableCallable(sync_after_agent, async_after_agent, trace=False)1640 graph.add_node(1641 f"{m.name}.after_agent",1642 after_agent_node,1643 input_schema=resolved_state_schema,1644 trace_policy=_node_trace_policy(m.trace_policy),1645 )16461647 # Determine the entry node (runs once at start): before_agent -> before_model -> model1648 if middleware_w_before_agent:1649 entry_node = f"{middleware_w_before_agent[0].name}.before_agent"1650 elif middleware_w_before_model:1651 entry_node = f"{middleware_w_before_model[0].name}.before_model"1652 else:1653 entry_node = "model"16541655 # Determine the loop entry node (beginning of agent loop, excludes before_agent)1656 # This is where tools will loop back to for the next iteration1657 if middleware_w_before_model:1658 loop_entry_node = f"{middleware_w_before_model[0].name}.before_model"1659 else:1660 loop_entry_node = "model"16611662 # Determine the loop exit node (end of each iteration, can run multiple times)1663 # This is after_model or model, but NOT after_agent1664 if middleware_w_after_model:1665 loop_exit_node = f"{middleware_w_after_model[0].name}.after_model"1666 else:1667 loop_exit_node = "model"16681669 # Determine the exit node (runs once at end): after_agent or END1670 if middleware_w_after_agent:1671 exit_node = f"{middleware_w_after_agent[-1].name}.after_agent"1672 else:1673 exit_node = END16741675 graph.add_edge(START, entry_node)1676 # add conditional edges only if tools exist1677 if tool_node is not None:1678 # Only include exit_node in destinations if any tool has return_direct=True1679 # or if there are structured output tools1680 tools_to_model_destinations = [loop_entry_node]1681 if (1682 any(tool.return_direct for tool in tool_node.tools_by_name.values())1683 or structured_output_tools1684 ):1685 tools_to_model_destinations.append(exit_node)16861687 graph.add_conditional_edges(1688 "tools",1689 RunnableCallable(1690 _make_tools_to_model_edge(1691 tool_node=tool_node,1692 model_destination=loop_entry_node,1693 structured_output_tools=structured_output_tools,1694 end_destination=exit_node,1695 ),1696 trace=False,1697 ),1698 tools_to_model_destinations,1699 )17001701 # Include loop_entry_node when middleware can inject synthetic tool1702 # messages, or when structured output or after-model hooks can reroute there.1703 model_to_tools_destinations = ["tools", exit_node]1704 if response_format or loop_exit_node != "model" or middleware_w_wrap_model_call:1705 model_to_tools_destinations.append(loop_entry_node)17061707 graph.add_conditional_edges(1708 loop_exit_node,1709 RunnableCallable(1710 _make_model_to_tools_edge(1711 model_destination=loop_entry_node,1712 structured_output_tools=structured_output_tools,1713 end_destination=exit_node,1714 ),1715 trace=False,1716 ),1717 model_to_tools_destinations,1718 )1719 elif len(structured_output_tools) > 0:1720 graph.add_conditional_edges(1721 loop_exit_node,1722 RunnableCallable(1723 _make_model_to_model_edge(1724 model_destination=loop_entry_node,1725 end_destination=exit_node,1726 ),1727 trace=False,1728 ),1729 [loop_entry_node, exit_node],1730 )1731 elif loop_exit_node == "model":1732 # If no tools and no after_model, go directly to exit_node1733 graph.add_edge(loop_exit_node, exit_node)1734 # No tools but we have after_model - connect after_model to exit_node1735 else:1736 _add_middleware_edge(1737 graph,1738 name=f"{middleware_w_after_model[0].name}.after_model",1739 default_destination=exit_node,1740 model_destination=loop_entry_node,1741 end_destination=exit_node,1742 can_jump_to=_get_can_jump_to(middleware_w_after_model[0], "after_model"),1743 )17441745 # Add before_agent middleware edges1746 if middleware_w_before_agent:1747 for m1, m2 in itertools.pairwise(middleware_w_before_agent):1748 _add_middleware_edge(1749 graph,1750 name=f"{m1.name}.before_agent",1751 default_destination=f"{m2.name}.before_agent",1752 model_destination=loop_entry_node,1753 end_destination=exit_node,1754 can_jump_to=_get_can_jump_to(m1, "before_agent"),1755 )1756 # Connect last before_agent to loop_entry_node (before_model or model)1757 _add_middleware_edge(1758 graph,1759 name=f"{middleware_w_before_agent[-1].name}.before_agent",1760 default_destination=loop_entry_node,1761 model_destination=loop_entry_node,1762 end_destination=exit_node,1763 can_jump_to=_get_can_jump_to(middleware_w_before_agent[-1], "before_agent"),1764 )17651766 # Add before_model middleware edges1767 if middleware_w_before_model:1768 for m1, m2 in itertools.pairwise(middleware_w_before_model):1769 _add_middleware_edge(1770 graph,1771 name=f"{m1.name}.before_model",1772 default_destination=f"{m2.name}.before_model",1773 model_destination=loop_entry_node,1774 end_destination=exit_node,1775 can_jump_to=_get_can_jump_to(m1, "before_model"),1776 )1777 # Go directly to model after the last before_model1778 _add_middleware_edge(1779 graph,1780 name=f"{middleware_w_before_model[-1].name}.before_model",1781 default_destination="model",1782 model_destination=loop_entry_node,1783 end_destination=exit_node,1784 can_jump_to=_get_can_jump_to(middleware_w_before_model[-1], "before_model"),1785 )17861787 # Add after_model middleware edges1788 if middleware_w_after_model:1789 graph.add_edge("model", f"{middleware_w_after_model[-1].name}.after_model")1790 for idx in range(len(middleware_w_after_model) - 1, 0, -1):1791 m1 = middleware_w_after_model[idx]1792 m2 = middleware_w_after_model[idx - 1]1793 _add_middleware_edge(1794 graph,1795 name=f"{m1.name}.after_model",1796 default_destination=f"{m2.name}.after_model",1797 model_destination=loop_entry_node,1798 end_destination=exit_node,1799 can_jump_to=_get_can_jump_to(m1, "after_model"),1800 )1801 # Note: Connection from after_model to after_agent/END is handled above1802 # in the conditional edges section18031804 # Add after_agent middleware edges1805 if middleware_w_after_agent:1806 # Chain after_agent middleware (runs once at the very end, before END)1807 for idx in range(len(middleware_w_after_agent) - 1, 0, -1):1808 m1 = middleware_w_after_agent[idx]1809 m2 = middleware_w_after_agent[idx - 1]1810 _add_middleware_edge(1811 graph,1812 name=f"{m1.name}.after_agent",1813 default_destination=f"{m2.name}.after_agent",1814 model_destination=loop_entry_node,1815 end_destination=exit_node,1816 can_jump_to=_get_can_jump_to(m1, "after_agent"),1817 )18181819 # Connect the last after_agent to END1820 _add_middleware_edge(1821 graph,1822 name=f"{middleware_w_after_agent[0].name}.after_agent",1823 default_destination=END,1824 model_destination=loop_entry_node,1825 end_destination=exit_node,1826 can_jump_to=_get_can_jump_to(middleware_w_after_agent[0], "after_agent"),1827 )18281829 # Set recursion limit to 9_9991830 # https://github.com/langchain-ai/langgraph/issues/73131831 config: RunnableConfig = {"recursion_limit": 9_999}1832 config["metadata"] = {"ls_integration": "langchain_create_agent"}1833 if name:1834 config["metadata"]["lc_agent_name"] = name18351836 # Middleware that makes internal model calls (e.g. `SummarizationMiddleware`)1837 # each declare `InternalCallTransformer` on their own `transformers` tuple so1838 # it's only registered when one of them is actually in use.1839 middleware_transformers = [t for m in middleware for t in getattr(m, "transformers", ())]18401841 return graph.compile(1842 checkpointer=checkpointer,1843 store=store,1844 interrupt_before=interrupt_before,1845 interrupt_after=interrupt_after,1846 debug=debug,1847 name=name,1848 cache=cache,1849 transformers=_dedupe_transformers(1850 [1851 ToolCallTransformer,1852 SubagentTransformer,1853 *middleware_transformers,1854 *(transformers or ()),1855 ]1856 ),1857 ).with_config(config)185818591860def _dedupe_transformers(1861 factories: Iterable[TransformerFactory],1862) -> list[TransformerFactory]:1863 """Order-preserving de-dup of transformer factories, by identity.18641865 `AgentMiddleware.transformers` accepts any scope-aware callable, and1866 callables aren't required to be hashable (e.g. a dataclass-based factory1867 with `__hash__ = None`), so this can't use a `set`/`dict` keyed on the1868 factories themselves. Combining several middleware that each declare the1869 same shared transformer class (e.g. `InternalCallTransformer`) would1870 otherwise register it once per middleware.18711872 Args:1873 factories: Transformer factories to de-dup, in registration order.18741875 Returns:1876 The same factories with exact repeats (by `is`) removed, order kept.1877 """1878 seen_ids: set[int] = set()1879 deduped: list[TransformerFactory] = []1880 for factory in factories:1881 if id(factory) not in seen_ids:1882 seen_ids.add(id(factory))1883 deduped.append(factory)1884 return deduped188518861887def _resolve_jump(1888 jump_to: JumpTo | None,1889 *,1890 model_destination: str,1891 end_destination: str,1892) -> str | None:1893 if jump_to == "model":1894 return model_destination1895 if jump_to == "end":1896 return end_destination1897 if jump_to == "tools":1898 return "tools"1899 return None190019011902def _fetch_last_ai_and_tool_messages(1903 messages: list[AnyMessage],1904) -> tuple[AIMessage | None, list[ToolMessage]]:1905 """Return the last AI message and any subsequent tool messages.19061907 Args:1908 messages: List of messages to search through.19091910 Returns:1911 A tuple of (last_ai_message, tool_messages). If no AIMessage is found,1912 returns (None, []). Callers must handle the None case appropriately.1913 """1914 for i in range(len(messages) - 1, -1, -1):1915 if isinstance(messages[i], AIMessage):1916 last_ai_message = cast("AIMessage", messages[i])1917 tool_messages = [m for m in messages[i + 1 :] if isinstance(m, ToolMessage)]1918 return last_ai_message, tool_messages19191920 return None, []192119221923def _make_model_to_tools_edge(1924 *,1925 model_destination: str,1926 structured_output_tools: dict[str, OutputToolBinding[Any]],1927 end_destination: str,1928) -> Callable[[dict[str, Any]], str | list[Send] | None]:1929 def model_to_tools(1930 state: dict[str, Any],1931 ) -> str | list[Send] | None:1932 # 1. If there's an explicit jump_to in the state, use it1933 if jump_to := state.get("jump_to"):1934 return _resolve_jump(1935 jump_to,1936 model_destination=model_destination,1937 end_destination=end_destination,1938 )19391940 last_ai_message, tool_messages = _fetch_last_ai_and_tool_messages(state["messages"])19411942 # 2. if no AIMessage exists (e.g., messages were cleared), exit the loop1943 if last_ai_message is None:1944 return end_destination19451946 tool_message_ids = [m.tool_call_id for m in tool_messages]19471948 # 3. If the model hasn't called any tools, exit the loop1949 # this is the classic exit condition for an agent loop1950 if len(last_ai_message.tool_calls) == 0:1951 return end_destination19521953 pending_tool_calls = [1954 c1955 for c in last_ai_message.tool_calls1956 if c["id"] not in tool_message_ids and c["name"] not in structured_output_tools1957 ]19581959 # 4. If there are pending tool calls, jump to the tool node.1960 # The tool node hydrates ToolRuntime.state from channels via1961 # CONFIG_KEY_READ at execution time, so we no longer inline the1962 # full state into each Send (previously O(N^2) in TASKS writes).1963 if pending_tool_calls:1964 return [Send("tools", [tool_call]) for tool_call in pending_tool_calls]19651966 # 5. If a fresh structured response was produced this call, exit the loop1967 if state.get("structured_response") is not None:1968 return end_destination19691970 # 6. AIMessage has tool calls, but there are no pending tool calls which suggests1971 # the injection of artificial tool messages. Jump to the model node1972 return model_destination19731974 return model_to_tools197519761977def _make_model_to_model_edge(1978 *,1979 model_destination: str,1980 end_destination: str,1981) -> Callable[[dict[str, Any]], str | list[Send] | None]:1982 def model_to_model(1983 state: dict[str, Any],1984 ) -> str | list[Send] | None:1985 # 1. Priority: Check for explicit jump_to directive from middleware1986 if jump_to := state.get("jump_to"):1987 return _resolve_jump(1988 jump_to,1989 model_destination=model_destination,1990 end_destination=end_destination,1991 )19921993 # 2. Exit condition: a fresh structured response was generated this call1994 if state.get("structured_response") is not None:1995 return end_destination19961997 # 3. Default: Continue the loop, there may have been an issue with structured1998 # output generation, so we need to retry1999 return model_destination
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.