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