Ensure functions have docstrings for documentation
def wrap_tool_call(self, request, handler):
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]
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.