langchain 1.0.7__tar.gz → 1.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. {langchain-1.0.7 → langchain-1.1.2}/.gitignore +3 -0
  2. {langchain-1.0.7 → langchain-1.1.2}/Makefile +12 -2
  3. {langchain-1.0.7 → langchain-1.1.2}/PKG-INFO +3 -5
  4. {langchain-1.0.7 → langchain-1.1.2}/README.md +1 -1
  5. {langchain-1.0.7 → langchain-1.1.2}/langchain/__init__.py +1 -1
  6. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/factory.py +50 -16
  7. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/__init__.py +2 -0
  8. langchain-1.1.2/langchain/agents/middleware/_retry.py +123 -0
  9. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/context_editing.py +10 -12
  10. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/file_search.py +2 -2
  11. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/human_in_the_loop.py +47 -49
  12. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/model_call_limit.py +1 -0
  13. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/model_fallback.py +2 -4
  14. langchain-1.1.2/langchain/agents/middleware/model_retry.py +300 -0
  15. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/shell_tool.py +1 -1
  16. langchain-1.1.2/langchain/agents/middleware/summarization.py +529 -0
  17. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/todo.py +24 -18
  18. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/tool_call_limit.py +36 -27
  19. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/tool_emulator.py +29 -21
  20. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/tool_retry.py +129 -133
  21. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/tool_selection.py +18 -15
  22. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/types.py +487 -259
  23. {langchain-1.0.7 → langchain-1.1.2}/langchain/chat_models/base.py +1 -1
  24. {langchain-1.0.7 → langchain-1.1.2}/pyproject.toml +2 -3
  25. langchain-1.1.2/tests/cassettes/test_inference_to_native_output[False].yaml.gz +0 -0
  26. langchain-1.1.2/tests/cassettes/test_inference_to_native_output[True].yaml.gz +0 -0
  27. langchain-1.1.2/tests/cassettes/test_inference_to_tool_output[False].yaml.gz +0 -0
  28. langchain-1.1.2/tests/cassettes/test_inference_to_tool_output[True].yaml.gz +0 -0
  29. langchain-1.1.2/tests/unit_tests/agents/__snapshots__/test_middleware_framework.ambr +212 -0
  30. langchain-1.1.2/tests/unit_tests/agents/middleware/__snapshots__/test_middleware_decorators.ambr +95 -0
  31. langchain-1.1.2/tests/unit_tests/agents/middleware/__snapshots__/test_middleware_diagram.ambr +289 -0
  32. langchain-1.1.2/tests/unit_tests/agents/middleware/__snapshots__/test_middleware_framework.ambr +212 -0
  33. langchain-1.1.2/tests/unit_tests/agents/middleware/core/__snapshots__/test_decorators.ambr +95 -0
  34. langchain-1.1.2/tests/unit_tests/agents/middleware/core/__snapshots__/test_diagram.ambr +289 -0
  35. langchain-1.1.2/tests/unit_tests/agents/middleware/core/__snapshots__/test_framework.ambr +212 -0
  36. langchain-1.0.7/tests/unit_tests/agents/test_handler_composition.py → langchain-1.1.2/tests/unit_tests/agents/middleware/core/test_composition.py +1 -3
  37. langchain-1.0.7/tests/unit_tests/agents/test_middleware_decorators.py → langchain-1.1.2/tests/unit_tests/agents/middleware/core/test_decorators.py +3 -5
  38. langchain-1.1.2/tests/unit_tests/agents/middleware/core/test_diagram.py +193 -0
  39. langchain-1.1.2/tests/unit_tests/agents/middleware/core/test_framework.py +1051 -0
  40. langchain-1.0.7/tests/unit_tests/agents/test_sync_async_tool_wrapper_composition.py → langchain-1.1.2/tests/unit_tests/agents/middleware/core/test_sync_async_wrappers.py +1 -1
  41. langchain-1.0.7/tests/unit_tests/agents/test_middleware_tools.py → langchain-1.1.2/tests/unit_tests/agents/middleware/core/test_tools.py +11 -9
  42. langchain-1.0.7/tests/unit_tests/agents/middleware/test_wrap_model_call_middleware.py → langchain-1.1.2/tests/unit_tests/agents/middleware/core/test_wrap_model_call.py +549 -72
  43. langchain-1.0.7/tests/unit_tests/agents/middleware/test_wrap_tool_call_decorator.py → langchain-1.1.2/tests/unit_tests/agents/middleware/core/test_wrap_tool_call.py +1 -1
  44. langchain-1.0.7/tests/unit_tests/agents/test_context_editing_middleware.py → langchain-1.1.2/tests/unit_tests/agents/middleware/implementations/test_context_editing.py +76 -28
  45. {langchain-1.0.7/tests/unit_tests/agents/middleware → langchain-1.1.2/tests/unit_tests/agents/middleware/implementations}/test_file_search.py +105 -0
  46. langchain-1.1.2/tests/unit_tests/agents/middleware/implementations/test_human_in_the_loop.py +737 -0
  47. langchain-1.1.2/tests/unit_tests/agents/middleware/implementations/test_model_call_limit.py +224 -0
  48. langchain-1.0.7/tests/unit_tests/agents/test_model_fallback_middleware.py → langchain-1.1.2/tests/unit_tests/agents/middleware/implementations/test_model_fallback.py +145 -9
  49. langchain-1.1.2/tests/unit_tests/agents/middleware/implementations/test_model_retry.py +689 -0
  50. langchain-1.0.7/tests/unit_tests/agents/test_pii_middleware.py → langchain-1.1.2/tests/unit_tests/agents/middleware/implementations/test_pii.py +1 -1
  51. langchain-1.1.2/tests/unit_tests/agents/middleware/implementations/test_summarization.py +886 -0
  52. langchain-1.1.2/tests/unit_tests/agents/middleware/implementations/test_todo.py +516 -0
  53. {langchain-1.0.7/tests/unit_tests/agents → langchain-1.1.2/tests/unit_tests/agents/middleware/implementations}/test_tool_call_limit.py +183 -1
  54. {langchain-1.0.7/tests/unit_tests/agents/middleware → langchain-1.1.2/tests/unit_tests/agents/middleware/implementations}/test_tool_retry.py +143 -32
  55. langchain-1.0.7/tests/unit_tests/agents/middleware/test_llm_tool_selection.py → langchain-1.1.2/tests/unit_tests/agents/middleware/implementations/test_tool_selection.py +17 -8
  56. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/model.py +2 -3
  57. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/test_injected_runtime_create_agent.py +244 -1
  58. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/test_response_format.py +2 -3
  59. langchain-1.1.2/tests/unit_tests/agents/test_response_format_integration.py +142 -0
  60. langchain-1.1.2/tests/unit_tests/agents/test_system_message.py +1013 -0
  61. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/conftest.py +44 -0
  62. langchain-1.1.2/tests/unit_tests/embeddings/__init__.py +0 -0
  63. langchain-1.1.2/tests/unit_tests/tools/__init__.py +0 -0
  64. langchain-1.1.2/uv.lock +5439 -0
  65. langchain-1.0.7/langchain/agents/middleware/summarization.py +0 -249
  66. langchain-1.0.7/tests/integration_tests/agents/test_response_format.py +0 -79
  67. langchain-1.0.7/tests/unit_tests/agents/middleware/test_before_after_agent.py +0 -286
  68. langchain-1.0.7/tests/unit_tests/agents/middleware/test_shell_tool.py +0 -175
  69. langchain-1.0.7/tests/unit_tests/agents/middleware/test_wrap_model_call_decorator.py +0 -355
  70. langchain-1.0.7/tests/unit_tests/agents/test_middleware_agent.py +0 -2600
  71. langchain-1.0.7/tests/unit_tests/agents/test_on_tool_call_middleware.py +0 -828
  72. langchain-1.0.7/tests/unit_tests/agents/test_parallel_tool_call_limits.py +0 -192
  73. langchain-1.0.7/tests/unit_tests/agents/test_todo_middleware.py +0 -172
  74. langchain-1.0.7/uv.lock +0 -5129
  75. {langchain-1.0.7 → langchain-1.1.2}/LICENSE +0 -0
  76. {langchain-1.0.7 → langchain-1.1.2}/extended_testing_deps.txt +0 -0
  77. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/__init__.py +0 -0
  78. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/_execution.py +0 -0
  79. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/_redaction.py +0 -0
  80. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/middleware/pii.py +0 -0
  81. {langchain-1.0.7 → langchain-1.1.2}/langchain/agents/structured_output.py +0 -0
  82. {langchain-1.0.7 → langchain-1.1.2}/langchain/chat_models/__init__.py +0 -0
  83. {langchain-1.0.7 → langchain-1.1.2}/langchain/embeddings/__init__.py +0 -0
  84. {langchain-1.0.7 → langchain-1.1.2}/langchain/embeddings/base.py +0 -0
  85. {langchain-1.0.7 → langchain-1.1.2}/langchain/messages/__init__.py +0 -0
  86. {langchain-1.0.7 → langchain-1.1.2}/langchain/py.typed +0 -0
  87. {langchain-1.0.7 → langchain-1.1.2}/langchain/rate_limiters/__init__.py +0 -0
  88. {langchain-1.0.7 → langchain-1.1.2}/langchain/tools/__init__.py +0 -0
  89. {langchain-1.0.7 → langchain-1.1.2}/langchain/tools/tool_node.py +0 -0
  90. {langchain-1.0.7 → langchain-1.1.2}/scripts/check_imports.py +0 -0
  91. {langchain-1.0.7 → langchain-1.1.2}/tests/__init__.py +0 -0
  92. {langchain-1.0.7 → langchain-1.1.2}/tests/integration_tests/__init__.py +0 -0
  93. {langchain-1.0.7 → langchain-1.1.2}/tests/integration_tests/agents/__init__.py +0 -0
  94. {langchain-1.0.7 → langchain-1.1.2}/tests/integration_tests/agents/middleware/__init__.py +0 -0
  95. {langchain-1.0.7 → langchain-1.1.2}/tests/integration_tests/agents/middleware/test_shell_tool_integration.py +0 -0
  96. {langchain-1.0.7 → langchain-1.1.2}/tests/integration_tests/cache/__init__.py +0 -0
  97. {langchain-1.0.7 → langchain-1.1.2}/tests/integration_tests/cache/fake_embeddings.py +0 -0
  98. {langchain-1.0.7 → langchain-1.1.2}/tests/integration_tests/chat_models/__init__.py +0 -0
  99. {langchain-1.0.7 → langchain-1.1.2}/tests/integration_tests/chat_models/test_base.py +0 -0
  100. {langchain-1.0.7 → langchain-1.1.2}/tests/integration_tests/conftest.py +0 -0
  101. {langchain-1.0.7 → langchain-1.1.2}/tests/integration_tests/embeddings/__init__.py +0 -0
  102. {langchain-1.0.7 → langchain-1.1.2}/tests/integration_tests/embeddings/test_base.py +0 -0
  103. {langchain-1.0.7 → langchain-1.1.2}/tests/integration_tests/test_compile.py +0 -0
  104. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/__init__.py +0 -0
  105. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/__init__.py +0 -0
  106. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/__snapshots__/test_middleware_agent.ambr +0 -0
  107. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/__snapshots__/test_middleware_decorators.ambr +0 -0
  108. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/__snapshots__/test_return_direct_graph.ambr +0 -0
  109. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/any_str.py +0 -0
  110. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/compose-postgres.yml +0 -0
  111. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/compose-redis.yml +0 -0
  112. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/conftest.py +0 -0
  113. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/conftest_checkpointer.py +0 -0
  114. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/conftest_store.py +0 -0
  115. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/memory_assert.py +0 -0
  116. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/messages.py +0 -0
  117. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/middleware/__init__.py +0 -0
  118. {langchain-1.0.7/tests/unit_tests/chat_models → langchain-1.1.2/tests/unit_tests/agents/middleware/core}/__init__.py +0 -0
  119. /langchain-1.0.7/tests/unit_tests/agents/middleware/test_override_methods.py → /langchain-1.1.2/tests/unit_tests/agents/middleware/core/test_overrides.py +0 -0
  120. {langchain-1.0.7/tests/unit_tests/embeddings → langchain-1.1.2/tests/unit_tests/agents/middleware/implementations}/__init__.py +0 -0
  121. {langchain-1.0.7/tests/unit_tests/agents/middleware → langchain-1.1.2/tests/unit_tests/agents/middleware/implementations}/test_shell_execution_policies.py +0 -0
  122. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/middleware/implementations/test_shell_tool.py +0 -0
  123. {langchain-1.0.7/tests/unit_tests/agents/middleware → langchain-1.1.2/tests/unit_tests/agents/middleware/implementations}/test_structured_output_retry.py +0 -0
  124. {langchain-1.0.7/tests/unit_tests/agents/middleware → langchain-1.1.2/tests/unit_tests/agents/middleware/implementations}/test_tool_emulator.py +0 -0
  125. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/specifications/responses.json +0 -0
  126. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/specifications/return_direct.json +0 -0
  127. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/test_create_agent_tool_validation.py +0 -0
  128. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/test_react_agent.py +0 -0
  129. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/test_responses.py +0 -0
  130. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/test_responses_spec.py +0 -0
  131. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/test_return_direct_graph.py +0 -0
  132. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/test_return_direct_spec.py +0 -0
  133. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/test_state_schema.py +0 -0
  134. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/agents/utils.py +0 -0
  135. {langchain-1.0.7/tests/unit_tests/tools → langchain-1.1.2/tests/unit_tests/chat_models}/__init__.py +0 -0
  136. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/chat_models/test_chat_models.py +0 -0
  137. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/embeddings/test_base.py +0 -0
  138. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/embeddings/test_imports.py +0 -0
  139. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/stubs.py +0 -0
  140. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/test_dependencies.py +0 -0
  141. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/test_imports.py +0 -0
  142. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/test_pytest_config.py +0 -0
  143. {langchain-1.0.7 → langchain-1.1.2}/tests/unit_tests/tools/test_imports.py +0 -0
@@ -163,3 +163,6 @@ node_modules
163
163
 
164
164
  prof
165
165
  virtualenv/
166
+ scratch/
167
+
168
+ .langgraph_api/
@@ -1,4 +1,4 @@
1
- .PHONY: all start_services stop_services coverage test test_fast extended_tests test_watch test_watch_extended integration_tests check_imports lint format lint_diff format_diff lint_package lint_tests help
1
+ .PHONY: all start_services stop_services coverage coverage_agents test test_fast extended_tests test_watch test_watch_extended integration_tests check_imports lint format lint_diff format_diff lint_package lint_tests help
2
2
 
3
3
  # Default target executed when no arguments are given to make.
4
4
  all: help
@@ -27,8 +27,17 @@ coverage:
27
27
  --cov-report term-missing:skip-covered \
28
28
  $(TEST_FILE)
29
29
 
30
+ # Run middleware and agent tests with coverage report.
31
+ coverage_agents:
32
+ uv run --group test pytest \
33
+ tests/unit_tests/agents/middleware/ \
34
+ tests/unit_tests/agents/test_*.py \
35
+ --cov=langchain.agents \
36
+ --cov-report=term-missing \
37
+ --cov-report=html:htmlcov \
38
+
30
39
  test:
31
- make start_services && LANGGRAPH_TEST_FAST=0 uv run --no-sync --active --group test pytest -n auto --disable-socket --allow-unix-socket $(TEST_FILE) --cov-report term-missing:skip-covered; \
40
+ make start_services && LANGGRAPH_TEST_FAST=0 uv run --no-sync --active --group test pytest -n auto --disable-socket --allow-unix-socket $(TEST_FILE) --cov-report term-missing:skip-covered --snapshot-update; \
32
41
  EXIT_CODE=$$?; \
33
42
  make stop_services; \
34
43
  exit $$EXIT_CODE
@@ -93,6 +102,7 @@ help:
93
102
  @echo 'lint - run linters'
94
103
  @echo '-- TESTS --'
95
104
  @echo 'coverage - run unit tests and generate coverage report'
105
+ @echo 'coverage_agents - run middleware and agent tests with coverage report'
96
106
  @echo 'test - run unit tests with all services'
97
107
  @echo 'test_fast - run unit tests with in-memory services only'
98
108
  @echo 'tests - run unit tests (alias for "make test")'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain
3
- Version: 1.0.7
3
+ Version: 1.1.2
4
4
  Summary: Building applications with LLMs through composability
5
5
  Project-URL: Homepage, https://docs.langchain.com/
6
6
  Project-URL: Documentation, https://reference.langchain.com/python/langchain/langchain/
@@ -12,7 +12,7 @@ Project-URL: Reddit, https://www.reddit.com/r/LangChain/
12
12
  License: MIT
13
13
  License-File: LICENSE
14
14
  Requires-Python: <4.0.0,>=3.10.0
15
- Requires-Dist: langchain-core<2.0.0,>=1.0.4
15
+ Requires-Dist: langchain-core<2.0.0,>=1.1.0
16
16
  Requires-Dist: langgraph<1.1.0,>=1.0.2
17
17
  Requires-Dist: pydantic<3.0.0,>=2.7.4
18
18
  Provides-Extra: anthropic
@@ -37,8 +37,6 @@ Provides-Extra: huggingface
37
37
  Requires-Dist: langchain-huggingface; extra == 'huggingface'
38
38
  Provides-Extra: mistralai
39
39
  Requires-Dist: langchain-mistralai; extra == 'mistralai'
40
- Provides-Extra: model-profiles
41
- Requires-Dist: langchain-model-profiles; extra == 'model-profiles'
42
40
  Provides-Extra: ollama
43
41
  Requires-Dist: langchain-ollama; extra == 'ollama'
44
42
  Provides-Extra: openai
@@ -79,7 +77,7 @@ LangChain [agents](https://docs.langchain.com/oss/python/langchain/agents) are b
79
77
 
80
78
  ## 📖 Documentation
81
79
 
82
- For full documentation, see the [API reference](https://reference.langchain.com/python/langchain/langchain/).
80
+ For full documentation, see the [API reference](https://reference.langchain.com/python/langchain/langchain/). For conceptual guides, tutorials, and examples on using LangChain, see the [LangChain Docs](https://docs.langchain.com/oss/python/langchain/overview).
83
81
 
84
82
  ## 📕 Releases & Versioning
85
83
 
@@ -26,7 +26,7 @@ LangChain [agents](https://docs.langchain.com/oss/python/langchain/agents) are b
26
26
 
27
27
  ## 📖 Documentation
28
28
 
29
- For full documentation, see the [API reference](https://reference.langchain.com/python/langchain/langchain/).
29
+ For full documentation, see the [API reference](https://reference.langchain.com/python/langchain/langchain/). For conceptual guides, tutorials, and examples on using LangChain, see the [LangChain Docs](https://docs.langchain.com/oss/python/langchain/overview).
30
30
 
31
31
  ## 📕 Releases & Versioning
32
32
 
@@ -1,3 +1,3 @@
1
1
  """Main entrypoint into LangChain."""
2
2
 
3
- __version__ = "1.0.5"
3
+ __version__ = "1.1.2"
@@ -63,6 +63,18 @@ if TYPE_CHECKING:
63
63
 
64
64
  STRUCTURED_OUTPUT_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes."
65
65
 
66
+ FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT = [
67
+ # if model profile data are not available, these models are assumed to support
68
+ # structured output
69
+ "grok",
70
+ "gpt-5",
71
+ "gpt-4.1",
72
+ "gpt-4o",
73
+ "gpt-oss",
74
+ "o3-pro",
75
+ "o3-mini",
76
+ ]
77
+
66
78
 
67
79
  def _normalize_to_model_response(result: ModelResponse | AIMessage) -> ModelResponse:
68
80
  """Normalize middleware return value to ModelResponse."""
@@ -349,11 +361,13 @@ def _get_can_jump_to(middleware: AgentMiddleware[Any, Any], hook_name: str) -> l
349
361
  return []
350
362
 
351
363
 
352
- def _supports_provider_strategy(model: str | BaseChatModel) -> bool:
364
+ def _supports_provider_strategy(model: str | BaseChatModel, tools: list | None = None) -> bool:
353
365
  """Check if a model supports provider-specific structured output.
354
366
 
355
367
  Args:
356
368
  model: Model name string or `BaseChatModel` instance.
369
+ tools: Optional list of tools provided to the agent. Needed because some models
370
+ don't support structured output together with tool calling.
357
371
 
358
372
  Returns:
359
373
  `True` if the model supports provider-specific structured output, `False` otherwise.
@@ -362,11 +376,23 @@ def _supports_provider_strategy(model: str | BaseChatModel) -> bool:
362
376
  if isinstance(model, str):
363
377
  model_name = model
364
378
  elif isinstance(model, BaseChatModel):
365
- model_name = getattr(model, "model_name", None)
379
+ model_name = (
380
+ getattr(model, "model_name", None)
381
+ or getattr(model, "model", None)
382
+ or getattr(model, "model_id", "")
383
+ )
384
+ model_profile = model.profile
385
+ if (
386
+ model_profile is not None
387
+ and model_profile.get("structured_output")
388
+ # We make an exception for Gemini models, which currently do not support
389
+ # simultaneous tool use with structured output
390
+ and not (tools and isinstance(model_name, str) and "gemini" in model_name.lower())
391
+ ):
392
+ return True
366
393
 
367
394
  return (
368
- "grok" in model_name.lower()
369
- or any(part in model_name for part in ["gpt-5", "gpt-4.1", "gpt-oss", "o3-pro", "o3-mini"])
395
+ any(part in model_name.lower() for part in FALLBACK_MODELS_WITH_STRUCTURED_OUTPUT)
370
396
  if model_name
371
397
  else False
372
398
  )
@@ -516,7 +542,7 @@ def create_agent( # noqa: PLR0915
516
542
  model: str | BaseChatModel,
517
543
  tools: Sequence[BaseTool | Callable | dict[str, Any]] | None = None,
518
544
  *,
519
- system_prompt: str | None = None,
545
+ system_prompt: str | SystemMessage | None = None,
520
546
  middleware: Sequence[AgentMiddleware[StateT_co, ContextT]] = (),
521
547
  response_format: ResponseFormat[ResponseT] | type[ResponseT] | None = None,
522
548
  state_schema: type[AgentState[ResponseT]] | None = None,
@@ -562,9 +588,9 @@ def create_agent( # noqa: PLR0915
562
588
  docs for more information.
563
589
  system_prompt: An optional system prompt for the LLM.
564
590
 
565
- Prompts are converted to a
566
- [`SystemMessage`][langchain.messages.SystemMessage] and added to the
567
- beginning of the message list.
591
+ Can be a `str` (which will be converted to a `SystemMessage`) or a
592
+ `SystemMessage` instance directly. The system message is added to the
593
+ beginning of the message list when calling the model.
568
594
  middleware: A sequence of middleware instances to apply to the agent.
569
595
 
570
596
  Middleware can intercept and modify agent behavior at various stages.
@@ -659,6 +685,14 @@ def create_agent( # noqa: PLR0915
659
685
  if isinstance(model, str):
660
686
  model = init_chat_model(model)
661
687
 
688
+ # Convert system_prompt to SystemMessage if needed
689
+ system_message: SystemMessage | None = None
690
+ if system_prompt is not None:
691
+ if isinstance(system_prompt, SystemMessage):
692
+ system_message = system_prompt
693
+ else:
694
+ system_message = SystemMessage(content=system_prompt)
695
+
662
696
  # Handle tools being None or empty
663
697
  if tools is None:
664
698
  tools = []
@@ -988,7 +1022,7 @@ def create_agent( # noqa: PLR0915
988
1022
  effective_response_format: ResponseFormat | None
989
1023
  if isinstance(request.response_format, AutoStrategy):
990
1024
  # User provided raw schema via AutoStrategy - auto-detect best strategy based on model
991
- if _supports_provider_strategy(request.model):
1025
+ if _supports_provider_strategy(request.model, tools=request.tools):
992
1026
  # Model supports provider strategy - use it
993
1027
  effective_response_format = ProviderStrategy(schema=request.response_format.schema)
994
1028
  else:
@@ -1009,7 +1043,7 @@ def create_agent( # noqa: PLR0915
1009
1043
 
1010
1044
  # Bind model based on effective response format
1011
1045
  if isinstance(effective_response_format, ProviderStrategy):
1012
- # Use provider-specific structured output
1046
+ # (Backward compatibility) Use OpenAI format structured output
1013
1047
  kwargs = effective_response_format.to_model_kwargs()
1014
1048
  return (
1015
1049
  request.model.bind_tools(
@@ -1062,8 +1096,8 @@ def create_agent( # noqa: PLR0915
1062
1096
  # Get the bound model (with auto-detection if needed)
1063
1097
  model_, effective_response_format = _get_bound_model(request)
1064
1098
  messages = request.messages
1065
- if request.system_prompt:
1066
- messages = [SystemMessage(request.system_prompt), *messages]
1099
+ if request.system_message:
1100
+ messages = [request.system_message, *messages]
1067
1101
 
1068
1102
  output = model_.invoke(messages)
1069
1103
 
@@ -1082,7 +1116,7 @@ def create_agent( # noqa: PLR0915
1082
1116
  request = ModelRequest(
1083
1117
  model=model,
1084
1118
  tools=default_tools,
1085
- system_prompt=system_prompt,
1119
+ system_message=system_message,
1086
1120
  response_format=initial_response_format,
1087
1121
  messages=state["messages"],
1088
1122
  tool_choice=None,
@@ -1115,8 +1149,8 @@ def create_agent( # noqa: PLR0915
1115
1149
  # Get the bound model (with auto-detection if needed)
1116
1150
  model_, effective_response_format = _get_bound_model(request)
1117
1151
  messages = request.messages
1118
- if request.system_prompt:
1119
- messages = [SystemMessage(request.system_prompt), *messages]
1152
+ if request.system_message:
1153
+ messages = [request.system_message, *messages]
1120
1154
 
1121
1155
  output = await model_.ainvoke(messages)
1122
1156
 
@@ -1135,7 +1169,7 @@ def create_agent( # noqa: PLR0915
1135
1169
  request = ModelRequest(
1136
1170
  model=model,
1137
1171
  tools=default_tools,
1138
- system_prompt=system_prompt,
1172
+ system_message=system_message,
1139
1173
  response_format=initial_response_format,
1140
1174
  messages=state["messages"],
1141
1175
  tool_choice=None,
@@ -11,6 +11,7 @@ from .human_in_the_loop import (
11
11
  )
12
12
  from .model_call_limit import ModelCallLimitMiddleware
13
13
  from .model_fallback import ModelFallbackMiddleware
14
+ from .model_retry import ModelRetryMiddleware
14
15
  from .pii import PIIDetectionError, PIIMiddleware
15
16
  from .shell_tool import (
16
17
  CodexSandboxExecutionPolicy,
@@ -57,6 +58,7 @@ __all__ = [
57
58
  "ModelFallbackMiddleware",
58
59
  "ModelRequest",
59
60
  "ModelResponse",
61
+ "ModelRetryMiddleware",
60
62
  "PIIDetectionError",
61
63
  "PIIMiddleware",
62
64
  "RedactionRule",
@@ -0,0 +1,123 @@
1
+ """Shared retry utilities for agent middleware.
2
+
3
+ This module contains common constants, utilities, and logic used by both
4
+ model and tool retry middleware implementations.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import random
10
+ from collections.abc import Callable
11
+ from typing import Literal
12
+
13
+ # Type aliases
14
+ RetryOn = tuple[type[Exception], ...] | Callable[[Exception], bool]
15
+ """Type for specifying which exceptions to retry on.
16
+
17
+ Can be either:
18
+ - A tuple of exception types to retry on (based on `isinstance` checks)
19
+ - A callable that takes an exception and returns `True` if it should be retried
20
+ """
21
+
22
+ OnFailure = Literal["error", "continue"] | Callable[[Exception], str]
23
+ """Type for specifying failure handling behavior.
24
+
25
+ Can be either:
26
+ - A literal action string (`'error'` or `'continue'`)
27
+ - `'error'`: Re-raise the exception, stopping agent execution.
28
+ - `'continue'`: Inject a message with the error details, allowing the agent to continue.
29
+ For tool retries, a `ToolMessage` with the error details will be injected.
30
+ For model retries, an `AIMessage` with the error details will be returned.
31
+ - A callable that takes an exception and returns a string for error message content
32
+ """
33
+
34
+
35
+ def validate_retry_params(
36
+ max_retries: int,
37
+ initial_delay: float,
38
+ max_delay: float,
39
+ backoff_factor: float,
40
+ ) -> None:
41
+ """Validate retry parameters.
42
+
43
+ Args:
44
+ max_retries: Maximum number of retry attempts.
45
+ initial_delay: Initial delay in seconds before first retry.
46
+ max_delay: Maximum delay in seconds between retries.
47
+ backoff_factor: Multiplier for exponential backoff.
48
+
49
+ Raises:
50
+ ValueError: If any parameter is invalid (negative values).
51
+ """
52
+ if max_retries < 0:
53
+ msg = "max_retries must be >= 0"
54
+ raise ValueError(msg)
55
+ if initial_delay < 0:
56
+ msg = "initial_delay must be >= 0"
57
+ raise ValueError(msg)
58
+ if max_delay < 0:
59
+ msg = "max_delay must be >= 0"
60
+ raise ValueError(msg)
61
+ if backoff_factor < 0:
62
+ msg = "backoff_factor must be >= 0"
63
+ raise ValueError(msg)
64
+
65
+
66
+ def should_retry_exception(
67
+ exc: Exception,
68
+ retry_on: RetryOn,
69
+ ) -> bool:
70
+ """Check if an exception should trigger a retry.
71
+
72
+ Args:
73
+ exc: The exception that occurred.
74
+ retry_on: Either a tuple of exception types to retry on, or a callable
75
+ that takes an exception and returns `True` if it should be retried.
76
+
77
+ Returns:
78
+ `True` if the exception should be retried, `False` otherwise.
79
+ """
80
+ if callable(retry_on):
81
+ return retry_on(exc)
82
+ return isinstance(exc, retry_on)
83
+
84
+
85
+ def calculate_delay(
86
+ retry_number: int,
87
+ *,
88
+ backoff_factor: float,
89
+ initial_delay: float,
90
+ max_delay: float,
91
+ jitter: bool,
92
+ ) -> float:
93
+ """Calculate delay for a retry attempt with exponential backoff and optional jitter.
94
+
95
+ Args:
96
+ retry_number: The retry attempt number (0-indexed).
97
+ backoff_factor: Multiplier for exponential backoff.
98
+
99
+ Set to `0.0` for constant delay.
100
+ initial_delay: Initial delay in seconds before first retry.
101
+ max_delay: Maximum delay in seconds between retries.
102
+
103
+ Caps exponential backoff growth.
104
+ jitter: Whether to add random jitter to delay to avoid thundering herd.
105
+
106
+ Returns:
107
+ Delay in seconds before next retry.
108
+ """
109
+ if backoff_factor == 0.0:
110
+ delay = initial_delay
111
+ else:
112
+ delay = initial_delay * (backoff_factor**retry_number)
113
+
114
+ # Cap at max_delay
115
+ delay = min(delay, max_delay)
116
+
117
+ if jitter and delay > 0:
118
+ jitter_amount = delay * 0.25 # ±25% jitter
119
+ delay = delay + random.uniform(-jitter_amount, jitter_amount) # noqa: S311
120
+ # Ensure delay is not negative after jitter
121
+ delay = max(0, delay)
122
+
123
+ return delay
@@ -10,6 +10,7 @@ chat model.
10
10
  from __future__ import annotations
11
11
 
12
12
  from collections.abc import Awaitable, Callable, Iterable, Sequence
13
+ from copy import deepcopy
13
14
  from dataclasses import dataclass
14
15
  from typing import Literal
15
16
 
@@ -17,7 +18,6 @@ from langchain_core.messages import (
17
18
  AIMessage,
18
19
  AnyMessage,
19
20
  BaseMessage,
20
- SystemMessage,
21
21
  ToolMessage,
22
22
  )
23
23
  from langchain_core.messages.utils import count_tokens_approximately
@@ -189,7 +189,7 @@ class ContextEditingMiddleware(AgentMiddleware):
189
189
  configured thresholds.
190
190
 
191
191
  Currently the `ClearToolUsesEdit` strategy is supported, aligning with Anthropic's
192
- `clear_tool_uses_20250919` behavior [(read more)](https://docs.claude.com/en/docs/agents-and-tools/tool-use/memory-tool).
192
+ `clear_tool_uses_20250919` behavior [(read more)](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool).
193
193
  """
194
194
 
195
195
  edits: list[ContextEdit]
@@ -229,19 +229,18 @@ class ContextEditingMiddleware(AgentMiddleware):
229
229
  def count_tokens(messages: Sequence[BaseMessage]) -> int:
230
230
  return count_tokens_approximately(messages)
231
231
  else:
232
- system_msg = (
233
- [SystemMessage(content=request.system_prompt)] if request.system_prompt else []
234
- )
232
+ system_msg = [request.system_message] if request.system_message else []
235
233
 
236
234
  def count_tokens(messages: Sequence[BaseMessage]) -> int:
237
235
  return request.model.get_num_tokens_from_messages(
238
236
  system_msg + list(messages), request.tools
239
237
  )
240
238
 
239
+ edited_messages = deepcopy(list(request.messages))
241
240
  for edit in self.edits:
242
- edit.apply(request.messages, count_tokens=count_tokens)
241
+ edit.apply(edited_messages, count_tokens=count_tokens)
243
242
 
244
- return handler(request)
243
+ return handler(request.override(messages=edited_messages))
245
244
 
246
245
  async def awrap_model_call(
247
246
  self,
@@ -257,19 +256,18 @@ class ContextEditingMiddleware(AgentMiddleware):
257
256
  def count_tokens(messages: Sequence[BaseMessage]) -> int:
258
257
  return count_tokens_approximately(messages)
259
258
  else:
260
- system_msg = (
261
- [SystemMessage(content=request.system_prompt)] if request.system_prompt else []
262
- )
259
+ system_msg = [request.system_message] if request.system_message else []
263
260
 
264
261
  def count_tokens(messages: Sequence[BaseMessage]) -> int:
265
262
  return request.model.get_num_tokens_from_messages(
266
263
  system_msg + list(messages), request.tools
267
264
  )
268
265
 
266
+ edited_messages = deepcopy(list(request.messages))
269
267
  for edit in self.edits:
270
- edit.apply(request.messages, count_tokens=count_tokens)
268
+ edit.apply(edited_messages, count_tokens=count_tokens)
271
269
 
272
- return await handler(request)
270
+ return await handler(request.override(messages=edited_messages))
273
271
 
274
272
 
275
273
  __all__ = [
@@ -120,9 +120,9 @@ class FilesystemFileSearchMiddleware(AgentMiddleware):
120
120
 
121
121
  Args:
122
122
  root_path: Root directory to search.
123
- use_ripgrep: Whether to use ripgrep for search.
123
+ use_ripgrep: Whether to use `ripgrep` for search.
124
124
 
125
- Falls back to Python if ripgrep unavailable.
125
+ Falls back to Python if `ripgrep` unavailable.
126
126
  max_file_size_mb: Maximum file size to search in MB.
127
127
  """
128
128
  self.root_path = Path(root_path).resolve()
@@ -7,7 +7,7 @@ from langgraph.runtime import Runtime
7
7
  from langgraph.types import interrupt
8
8
  from typing_extensions import NotRequired, TypedDict
9
9
 
10
- from langchain.agents.middleware.types import AgentMiddleware, AgentState
10
+ from langchain.agents.middleware.types import AgentMiddleware, AgentState, ContextT, StateT
11
11
 
12
12
 
13
13
  class Action(TypedDict):
@@ -102,7 +102,7 @@ class HITLResponse(TypedDict):
102
102
  class _DescriptionFactory(Protocol):
103
103
  """Callable that generates a description for a tool call."""
104
104
 
105
- def __call__(self, tool_call: ToolCall, state: AgentState, runtime: Runtime) -> str:
105
+ def __call__(self, tool_call: ToolCall, state: AgentState, runtime: Runtime[ContextT]) -> str:
106
106
  """Generate a description for a tool call."""
107
107
  ...
108
108
 
@@ -138,7 +138,7 @@ class InterruptOnConfig(TypedDict):
138
138
  def format_tool_description(
139
139
  tool_call: ToolCall,
140
140
  state: AgentState,
141
- runtime: Runtime
141
+ runtime: Runtime[ContextT]
142
142
  ) -> str:
143
143
  import json
144
144
  return (
@@ -156,7 +156,7 @@ class InterruptOnConfig(TypedDict):
156
156
  """JSON schema for the args associated with the action, if edits are allowed."""
157
157
 
158
158
 
159
- class HumanInTheLoopMiddleware(AgentMiddleware):
159
+ class HumanInTheLoopMiddleware(AgentMiddleware[StateT, ContextT]):
160
160
  """Human in the loop middleware."""
161
161
 
162
162
  def __init__(
@@ -204,7 +204,7 @@ class HumanInTheLoopMiddleware(AgentMiddleware):
204
204
  tool_call: ToolCall,
205
205
  config: InterruptOnConfig,
206
206
  state: AgentState,
207
- runtime: Runtime,
207
+ runtime: Runtime[ContextT],
208
208
  ) -> tuple[ActionRequest, ReviewConfig]:
209
209
  """Create an ActionRequest and ReviewConfig for a tool call."""
210
210
  tool_name = tool_call["name"]
@@ -277,7 +277,7 @@ class HumanInTheLoopMiddleware(AgentMiddleware):
277
277
  )
278
278
  raise ValueError(msg)
279
279
 
280
- def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
280
+ def after_model(self, state: AgentState, runtime: Runtime[ContextT]) -> dict[str, Any] | None:
281
281
  """Trigger interrupt flows for relevant tool calls after an `AIMessage`."""
282
282
  messages = state["messages"]
283
283
  if not messages:
@@ -287,36 +287,23 @@ class HumanInTheLoopMiddleware(AgentMiddleware):
287
287
  if not last_ai_msg or not last_ai_msg.tool_calls:
288
288
  return None
289
289
 
290
- # Separate tool calls that need interrupts from those that don't
291
- interrupt_tool_calls: list[ToolCall] = []
292
- auto_approved_tool_calls = []
293
-
294
- for tool_call in last_ai_msg.tool_calls:
295
- interrupt_tool_calls.append(tool_call) if tool_call[
296
- "name"
297
- ] in self.interrupt_on else auto_approved_tool_calls.append(tool_call)
298
-
299
- # If no interrupts needed, return early
300
- if not interrupt_tool_calls:
301
- return None
302
-
303
- # Process all tool calls that require interrupts
304
- revised_tool_calls: list[ToolCall] = auto_approved_tool_calls.copy()
305
- artificial_tool_messages: list[ToolMessage] = []
306
-
307
- # Create action requests and review configs for all tools that need approval
290
+ # Create action requests and review configs for tools that need approval
308
291
  action_requests: list[ActionRequest] = []
309
292
  review_configs: list[ReviewConfig] = []
293
+ interrupt_indices: list[int] = []
310
294
 
311
- for tool_call in interrupt_tool_calls:
312
- config = self.interrupt_on[tool_call["name"]]
295
+ for idx, tool_call in enumerate(last_ai_msg.tool_calls):
296
+ if (config := self.interrupt_on.get(tool_call["name"])) is not None:
297
+ action_request, review_config = self._create_action_and_config(
298
+ tool_call, config, state, runtime
299
+ )
300
+ action_requests.append(action_request)
301
+ review_configs.append(review_config)
302
+ interrupt_indices.append(idx)
313
303
 
314
- # Create ActionRequest and ReviewConfig using helper method
315
- action_request, review_config = self._create_action_and_config(
316
- tool_call, config, state, runtime
317
- )
318
- action_requests.append(action_request)
319
- review_configs.append(review_config)
304
+ # If no interrupts needed, return early
305
+ if not action_requests:
306
+ return None
320
307
 
321
308
  # Create single HITLRequest with all actions and configs
322
309
  hitl_request = HITLRequest(
@@ -325,35 +312,46 @@ class HumanInTheLoopMiddleware(AgentMiddleware):
325
312
  )
326
313
 
327
314
  # Send interrupt and get response
328
- hitl_response: HITLResponse = interrupt(hitl_request)
329
- decisions = hitl_response["decisions"]
315
+ decisions = interrupt(hitl_request)["decisions"]
330
316
 
331
317
  # Validate that the number of decisions matches the number of interrupt tool calls
332
- if (decisions_len := len(decisions)) != (
333
- interrupt_tool_calls_len := len(interrupt_tool_calls)
334
- ):
318
+ if (decisions_len := len(decisions)) != (interrupt_count := len(interrupt_indices)):
335
319
  msg = (
336
320
  f"Number of human decisions ({decisions_len}) does not match "
337
- f"number of hanging tool calls ({interrupt_tool_calls_len})."
321
+ f"number of hanging tool calls ({interrupt_count})."
338
322
  )
339
323
  raise ValueError(msg)
340
324
 
341
- # Process each decision using helper method
342
- for i, decision in enumerate(decisions):
343
- tool_call = interrupt_tool_calls[i]
344
- config = self.interrupt_on[tool_call["name"]]
345
-
346
- revised_tool_call, tool_message = self._process_decision(decision, tool_call, config)
347
- if revised_tool_call:
348
- revised_tool_calls.append(revised_tool_call)
349
- if tool_message:
350
- artificial_tool_messages.append(tool_message)
325
+ # Process decisions and rebuild tool calls in original order
326
+ revised_tool_calls: list[ToolCall] = []
327
+ artificial_tool_messages: list[ToolMessage] = []
328
+ decision_idx = 0
329
+
330
+ for idx, tool_call in enumerate(last_ai_msg.tool_calls):
331
+ if idx in interrupt_indices:
332
+ # This was an interrupt tool call - process the decision
333
+ config = self.interrupt_on[tool_call["name"]]
334
+ decision = decisions[decision_idx]
335
+ decision_idx += 1
336
+
337
+ revised_tool_call, tool_message = self._process_decision(
338
+ decision, tool_call, config
339
+ )
340
+ if revised_tool_call is not None:
341
+ revised_tool_calls.append(revised_tool_call)
342
+ if tool_message:
343
+ artificial_tool_messages.append(tool_message)
344
+ else:
345
+ # This was auto-approved - keep original
346
+ revised_tool_calls.append(tool_call)
351
347
 
352
348
  # Update the AI message to only include approved tool calls
353
349
  last_ai_msg.tool_calls = revised_tool_calls
354
350
 
355
351
  return {"messages": [last_ai_msg, *artificial_tool_messages]}
356
352
 
357
- async def aafter_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
353
+ async def aafter_model(
354
+ self, state: AgentState, runtime: Runtime[ContextT]
355
+ ) -> dict[str, Any] | None:
358
356
  """Async trigger interrupt flows for relevant tool calls after an `AIMessage`."""
359
357
  return self.after_model(state, runtime)
@@ -133,6 +133,7 @@ class ModelCallLimitMiddleware(AgentMiddleware[ModelCallLimitState, Any]):
133
133
 
134
134
  `None` means no limit.
135
135
  exit_behavior: What to do when limits are exceeded.
136
+
136
137
  - `'end'`: Jump to the end of the agent execution and
137
138
  inject an artificial AI message indicating that the limit was
138
139
  exceeded.
@@ -92,9 +92,8 @@ class ModelFallbackMiddleware(AgentMiddleware):
92
92
 
93
93
  # Try fallback models
94
94
  for fallback_model in self.models:
95
- request.model = fallback_model
96
95
  try:
97
- return handler(request)
96
+ return handler(request.override(model=fallback_model))
98
97
  except Exception as e: # noqa: BLE001
99
98
  last_exception = e
100
99
  continue
@@ -127,9 +126,8 @@ class ModelFallbackMiddleware(AgentMiddleware):
127
126
 
128
127
  # Try fallback models
129
128
  for fallback_model in self.models:
130
- request.model = fallback_model
131
129
  try:
132
- return await handler(request)
130
+ return await handler(request.override(model=fallback_model))
133
131
  except Exception as e: # noqa: BLE001
134
132
  last_exception = e
135
133
  continue