langchain 1.0.0a13__tar.gz → 1.0.0a14__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.

Potentially problematic release.


This version of langchain might be problematic. Click here for more details.

Files changed (108) hide show
  1. {langchain-1.0.0a13 → langchain-1.0.0a14}/PKG-INFO +1 -1
  2. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/factory.py +107 -12
  3. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/middleware/pii.py +6 -8
  4. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/middleware/types.py +152 -13
  5. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/embeddings/__init__.py +0 -2
  6. langchain-1.0.0a14/langchain/messages/__init__.py +63 -0
  7. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/tools/__init__.py +2 -6
  8. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/tools/tool_node.py +30 -11
  9. {langchain-1.0.0a13 → langchain-1.0.0a14}/pyproject.toml +2 -3
  10. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/test_middleware_agent.py +21 -12
  11. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/test_middleware_decorators.py +58 -8
  12. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/test_middleware_tools.py +6 -7
  13. langchain-1.0.0a14/tests/unit_tests/agents/test_sync_async_tool_wrapper_composition.py +427 -0
  14. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/test_tool_node.py +43 -43
  15. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/embeddings/test_imports.py +0 -1
  16. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/tools/test_imports.py +1 -1
  17. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/tools/test_on_tool_call.py +31 -31
  18. {langchain-1.0.0a13 → langchain-1.0.0a14}/uv.lock +1 -3
  19. langchain-1.0.0a13/langchain/documents/__init__.py +0 -7
  20. langchain-1.0.0a13/langchain/embeddings/cache.py +0 -361
  21. langchain-1.0.0a13/langchain/messages/__init__.py +0 -31
  22. langchain-1.0.0a13/langchain/storage/__init__.py +0 -22
  23. langchain-1.0.0a13/langchain/storage/encoder_backed.py +0 -122
  24. langchain-1.0.0a13/langchain/storage/exceptions.py +0 -5
  25. langchain-1.0.0a13/langchain/storage/in_memory.py +0 -13
  26. langchain-1.0.0a13/tests/unit_tests/embeddings/test_caching.py +0 -245
  27. langchain-1.0.0a13/tests/unit_tests/storage/test_imports.py +0 -12
  28. langchain-1.0.0a13/tests/unit_tests/tools/__init__.py +0 -0
  29. {langchain-1.0.0a13 → langchain-1.0.0a14}/.gitignore +0 -0
  30. {langchain-1.0.0a13 → langchain-1.0.0a14}/LICENSE +0 -0
  31. {langchain-1.0.0a13 → langchain-1.0.0a14}/Makefile +0 -0
  32. {langchain-1.0.0a13 → langchain-1.0.0a14}/README.md +0 -0
  33. {langchain-1.0.0a13 → langchain-1.0.0a14}/extended_testing_deps.txt +0 -0
  34. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/__init__.py +0 -0
  35. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/__init__.py +0 -0
  36. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/middleware/__init__.py +0 -0
  37. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/middleware/context_editing.py +0 -0
  38. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/middleware/human_in_the_loop.py +0 -0
  39. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/middleware/model_call_limit.py +0 -0
  40. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/middleware/model_fallback.py +0 -0
  41. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/middleware/planning.py +0 -0
  42. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/middleware/prompt_caching.py +0 -0
  43. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/middleware/summarization.py +0 -0
  44. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/middleware/tool_call_limit.py +0 -0
  45. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/middleware/tool_emulator.py +0 -0
  46. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/middleware/tool_selection.py +0 -0
  47. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/agents/structured_output.py +0 -0
  48. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/chat_models/__init__.py +0 -0
  49. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/chat_models/base.py +0 -0
  50. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/embeddings/base.py +0 -0
  51. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/py.typed +0 -0
  52. {langchain-1.0.0a13 → langchain-1.0.0a14}/langchain/rate_limiters/__init__.py +0 -0
  53. {langchain-1.0.0a13 → langchain-1.0.0a14}/scripts/check_imports.py +0 -0
  54. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/__init__.py +0 -0
  55. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/integration_tests/__init__.py +0 -0
  56. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/integration_tests/agents/__init__.py +0 -0
  57. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/integration_tests/agents/test_response_format.py +0 -0
  58. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/integration_tests/cache/__init__.py +0 -0
  59. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/integration_tests/cache/fake_embeddings.py +0 -0
  60. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/integration_tests/chat_models/__init__.py +0 -0
  61. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/integration_tests/chat_models/test_base.py +0 -0
  62. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/integration_tests/conftest.py +0 -0
  63. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/integration_tests/embeddings/__init__.py +0 -0
  64. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/integration_tests/embeddings/test_base.py +0 -0
  65. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/integration_tests/test_compile.py +0 -0
  66. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/__init__.py +0 -0
  67. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/__init__.py +0 -0
  68. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/__snapshots__/test_middleware_agent.ambr +0 -0
  69. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/__snapshots__/test_middleware_decorators.ambr +0 -0
  70. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/any_str.py +0 -0
  71. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/compose-postgres.yml +0 -0
  72. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/compose-redis.yml +0 -0
  73. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/conftest.py +0 -0
  74. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/conftest_checkpointer.py +0 -0
  75. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/conftest_store.py +0 -0
  76. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/memory_assert.py +0 -0
  77. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/messages.py +0 -0
  78. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/middleware/__init__.py +0 -0
  79. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/middleware/test_before_after_agent.py +0 -0
  80. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/middleware/test_llm_tool_selection.py +0 -0
  81. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/middleware/test_tool_emulator.py +0 -0
  82. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/middleware/test_wrap_model_call_decorator.py +0 -0
  83. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/middleware/test_wrap_model_call_middleware.py +0 -0
  84. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/middleware/test_wrap_tool_call_decorator.py +0 -0
  85. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/model.py +0 -0
  86. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/specifications/responses.json +0 -0
  87. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/specifications/return_direct.json +0 -0
  88. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/test_context_editing_middleware.py +0 -0
  89. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/test_handler_composition.py +0 -0
  90. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/test_on_tool_call_middleware.py +0 -0
  91. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/test_pii_middleware.py +0 -0
  92. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/test_react_agent.py +0 -0
  93. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/test_response_format.py +0 -0
  94. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/test_responses.py +0 -0
  95. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/test_responses_spec.py +0 -0
  96. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/test_return_direct_spec.py +0 -0
  97. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/test_tool_call_limit.py +0 -0
  98. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/agents/utils.py +0 -0
  99. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/chat_models/__init__.py +0 -0
  100. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/chat_models/test_chat_models.py +0 -0
  101. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/conftest.py +0 -0
  102. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/embeddings/__init__.py +0 -0
  103. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/embeddings/test_base.py +0 -0
  104. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/stubs.py +0 -0
  105. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/test_dependencies.py +0 -0
  106. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/test_imports.py +0 -0
  107. {langchain-1.0.0a13 → langchain-1.0.0a14}/tests/unit_tests/test_pytest_config.py +0 -0
  108. {langchain-1.0.0a13/tests/unit_tests/storage → langchain-1.0.0a14/tests/unit_tests/tools}/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain
3
- Version: 1.0.0a13
3
+ Version: 1.0.0a14
4
4
  Summary: Building applications with LLMs through composability
5
5
  Project-URL: homepage, https://docs.langchain.com/
6
6
  Project-URL: repository, https://github.com/langchain-ai/langchain/tree/master/libs/langchain
@@ -13,9 +13,6 @@ from typing import (
13
13
  get_type_hints,
14
14
  )
15
15
 
16
- if TYPE_CHECKING:
17
- from collections.abc import Awaitable
18
-
19
16
  from langchain_core.language_models.chat_models import BaseChatModel
20
17
  from langchain_core.messages import AIMessage, AnyMessage, SystemMessage, ToolMessage
21
18
  from langchain_core.tools import BaseTool
@@ -47,11 +44,10 @@ from langchain.agents.structured_output import (
47
44
  ToolStrategy,
48
45
  )
49
46
  from langchain.chat_models import init_chat_model
50
- from langchain.tools import ToolNode
51
- from langchain.tools.tool_node import ToolCallWithContext
47
+ from langchain.tools.tool_node import ToolCallWithContext, _ToolNode
52
48
 
53
49
  if TYPE_CHECKING:
54
- from collections.abc import Callable, Sequence
50
+ from collections.abc import Awaitable, Callable, Sequence
55
51
 
56
52
  from langchain_core.runnables import Runnable
57
53
  from langgraph.cache.base import BaseCache
@@ -449,6 +445,70 @@ def _chain_tool_call_wrappers(
449
445
  return result
450
446
 
451
447
 
448
+ def _chain_async_tool_call_wrappers(
449
+ wrappers: Sequence[
450
+ Callable[
451
+ [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]]],
452
+ Awaitable[ToolMessage | Command],
453
+ ]
454
+ ],
455
+ ) -> (
456
+ Callable[
457
+ [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]]],
458
+ Awaitable[ToolMessage | Command],
459
+ ]
460
+ | None
461
+ ):
462
+ """Compose async wrappers into middleware stack (first = outermost).
463
+
464
+ Args:
465
+ wrappers: Async wrappers in middleware order.
466
+
467
+ Returns:
468
+ Composed async wrapper, or None if empty.
469
+ """
470
+ if not wrappers:
471
+ return None
472
+
473
+ if len(wrappers) == 1:
474
+ return wrappers[0]
475
+
476
+ def compose_two(
477
+ outer: Callable[
478
+ [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]]],
479
+ Awaitable[ToolMessage | Command],
480
+ ],
481
+ inner: Callable[
482
+ [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]]],
483
+ Awaitable[ToolMessage | Command],
484
+ ],
485
+ ) -> Callable[
486
+ [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]]],
487
+ Awaitable[ToolMessage | Command],
488
+ ]:
489
+ """Compose two async wrappers where outer wraps inner."""
490
+
491
+ async def composed(
492
+ request: ToolCallRequest,
493
+ execute: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
494
+ ) -> ToolMessage | Command:
495
+ # Create an async callable that invokes inner with the original execute
496
+ async def call_inner(req: ToolCallRequest) -> ToolMessage | Command:
497
+ return await inner(req, execute)
498
+
499
+ # Outer can call call_inner multiple times
500
+ return await outer(request, call_inner)
501
+
502
+ return composed
503
+
504
+ # Chain all wrappers: first -> second -> ... -> last
505
+ result = wrappers[-1]
506
+ for wrapper in reversed(wrappers[:-1]):
507
+ result = compose_two(wrapper, result)
508
+
509
+ return result
510
+
511
+
452
512
  def create_agent( # noqa: PLR0915
453
513
  model: str | BaseChatModel,
454
514
  tools: Sequence[BaseTool | Callable | dict[str, Any]] | None = None,
@@ -576,9 +636,14 @@ def create_agent( # noqa: PLR0915
576
636
  structured_output_tools[structured_tool_info.tool.name] = structured_tool_info
577
637
  middleware_tools = [t for m in middleware for t in getattr(m, "tools", [])]
578
638
 
579
- # Collect middleware with wrap_tool_call hooks
639
+ # Collect middleware with wrap_tool_call or awrap_tool_call hooks
640
+ # Include middleware with either implementation to ensure NotImplementedError is raised
641
+ # when middleware doesn't support the execution path
580
642
  middleware_w_wrap_tool_call = [
581
- m for m in middleware if m.__class__.wrap_tool_call is not AgentMiddleware.wrap_tool_call
643
+ m
644
+ for m in middleware
645
+ if m.__class__.wrap_tool_call is not AgentMiddleware.wrap_tool_call
646
+ or m.__class__.awrap_tool_call is not AgentMiddleware.awrap_tool_call
582
647
  ]
583
648
 
584
649
  # Chain all wrap_tool_call handlers into a single composed handler
@@ -587,8 +652,24 @@ def create_agent( # noqa: PLR0915
587
652
  wrappers = [m.wrap_tool_call for m in middleware_w_wrap_tool_call]
588
653
  wrap_tool_call_wrapper = _chain_tool_call_wrappers(wrappers)
589
654
 
655
+ # Collect middleware with awrap_tool_call or wrap_tool_call hooks
656
+ # Include middleware with either implementation to ensure NotImplementedError is raised
657
+ # when middleware doesn't support the execution path
658
+ middleware_w_awrap_tool_call = [
659
+ m
660
+ for m in middleware
661
+ if m.__class__.awrap_tool_call is not AgentMiddleware.awrap_tool_call
662
+ or m.__class__.wrap_tool_call is not AgentMiddleware.wrap_tool_call
663
+ ]
664
+
665
+ # Chain all awrap_tool_call handlers into a single composed async handler
666
+ awrap_tool_call_wrapper = None
667
+ if middleware_w_awrap_tool_call:
668
+ async_wrappers = [m.awrap_tool_call for m in middleware_w_awrap_tool_call]
669
+ awrap_tool_call_wrapper = _chain_async_tool_call_wrappers(async_wrappers)
670
+
590
671
  # Setup tools
591
- tool_node: ToolNode | None = None
672
+ tool_node: _ToolNode | None = None
592
673
  # Extract built-in provider tools (dict format) and regular tools (BaseTool/callables)
593
674
  built_in_tools = [t for t in tools if isinstance(t, dict)]
594
675
  regular_tools = [t for t in tools if not isinstance(t, dict)]
@@ -598,7 +679,11 @@ def create_agent( # noqa: PLR0915
598
679
 
599
680
  # Only create ToolNode if we have client-side tools
600
681
  tool_node = (
601
- ToolNode(tools=available_tools, wrap_tool_call=wrap_tool_call_wrapper)
682
+ _ToolNode(
683
+ tools=available_tools,
684
+ wrap_tool_call=wrap_tool_call_wrapper,
685
+ awrap_tool_call=awrap_tool_call_wrapper,
686
+ )
602
687
  if available_tools
603
688
  else None
604
689
  )
@@ -640,13 +725,23 @@ def create_agent( # noqa: PLR0915
640
725
  if m.__class__.after_agent is not AgentMiddleware.after_agent
641
726
  or m.__class__.aafter_agent is not AgentMiddleware.aafter_agent
642
727
  ]
728
+ # Collect middleware with wrap_model_call or awrap_model_call hooks
729
+ # Include middleware with either implementation to ensure NotImplementedError is raised
730
+ # when middleware doesn't support the execution path
643
731
  middleware_w_wrap_model_call = [
644
- m for m in middleware if m.__class__.wrap_model_call is not AgentMiddleware.wrap_model_call
732
+ m
733
+ for m in middleware
734
+ if m.__class__.wrap_model_call is not AgentMiddleware.wrap_model_call
735
+ or m.__class__.awrap_model_call is not AgentMiddleware.awrap_model_call
645
736
  ]
737
+ # Collect middleware with awrap_model_call or wrap_model_call hooks
738
+ # Include middleware with either implementation to ensure NotImplementedError is raised
739
+ # when middleware doesn't support the execution path
646
740
  middleware_w_awrap_model_call = [
647
741
  m
648
742
  for m in middleware
649
743
  if m.__class__.awrap_model_call is not AgentMiddleware.awrap_model_call
744
+ or m.__class__.wrap_model_call is not AgentMiddleware.wrap_model_call
650
745
  ]
651
746
 
652
747
  # Compose wrap_model_call handlers into a single middleware stack (sync)
@@ -1378,7 +1473,7 @@ def _make_model_to_model_edge(
1378
1473
 
1379
1474
  def _make_tools_to_model_edge(
1380
1475
  *,
1381
- tool_node: ToolNode,
1476
+ tool_node: _ToolNode,
1382
1477
  model_destination: str,
1383
1478
  structured_output_tools: dict[str, OutputToolBinding],
1384
1479
  end_destination: str,
@@ -431,14 +431,12 @@ class PIIMiddleware(AgentMiddleware):
431
431
 
432
432
  Strategy Selection Guide:
433
433
 
434
- ======== =================== =======================================
435
- Strategy Preserves Identity? Best For
436
- ======== =================== =======================================
437
- `block` N/A Avoid PII completely
438
- `redact` No General compliance, log sanitization
439
- `mask` No Human readability, customer service UIs
440
- `hash` Yes (pseudonymous) Analytics, debugging
441
- ======== =================== =======================================
434
+ | Strategy | Preserves Identity? | Best For |
435
+ | -------- | ------------------- | --------------------------------------- |
436
+ | `block` | N/A | Avoid PII completely |
437
+ | `redact` | No | General compliance, log sanitization |
438
+ | `mask` | No | Human readability, customer service UIs |
439
+ | `hash` | Yes (pseudonymous) | Analytics, debugging |
442
440
 
443
441
  Example:
444
442
  ```python
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
21
21
 
22
22
  from langchain.tools.tool_node import ToolCallRequest
23
23
 
24
- # needed as top level import for pydantic schema generation on AgentState
24
+ # Needed as top level import for Pydantic schema generation on AgentState
25
25
  from typing import TypeAlias
26
26
 
27
27
  from langchain_core.messages import AIMessage, AnyMessage, BaseMessage, ToolMessage # noqa: TC002
@@ -263,18 +263,35 @@ class AgentMiddleware(Generic[StateT, ContextT]):
263
263
  return AIMessage(content="Simplified response")
264
264
  ```
265
265
  """
266
- raise NotImplementedError
266
+ msg = (
267
+ "Synchronous implementation of wrap_model_call is not available. "
268
+ "You are likely encountering this error because you defined only the async version "
269
+ "(awrap_model_call) and invoked your agent in a synchronous context "
270
+ "(e.g., using `stream()` or `invoke()`). "
271
+ "To resolve this, either: "
272
+ "(1) subclass AgentMiddleware and implement the synchronous wrap_model_call method, "
273
+ "(2) use the @wrap_model_call decorator on a standalone sync function, or "
274
+ "(3) invoke your agent asynchronously using `astream()` or `ainvoke()`."
275
+ )
276
+ raise NotImplementedError(msg)
267
277
 
268
278
  async def awrap_model_call(
269
279
  self,
270
280
  request: ModelRequest,
271
281
  handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
272
282
  ) -> ModelCallResult:
273
- """Async version of wrap_model_call.
283
+ """Intercept and control async model execution via handler callback.
284
+
285
+ The handler callback executes the model request and returns a ModelResponse.
286
+ Middleware can call the handler multiple times for retry logic, skip calling
287
+ it to short-circuit, or modify the request/response. Multiple middleware
288
+ compose with first in list as outermost layer.
274
289
 
275
290
  Args:
276
291
  request: Model request to execute (includes state and runtime).
277
- handler: Async callback that executes the model request.
292
+ handler: Async callback that executes the model request and returns ModelResponse.
293
+ Call this to execute the model. Can be called multiple times
294
+ for retry logic. Can skip calling it to short-circuit.
278
295
 
279
296
  Returns:
280
297
  ModelCallResult
@@ -291,7 +308,17 @@ class AgentMiddleware(Generic[StateT, ContextT]):
291
308
  raise
292
309
  ```
293
310
  """
294
- raise NotImplementedError
311
+ msg = (
312
+ "Asynchronous implementation of awrap_model_call is not available. "
313
+ "You are likely encountering this error because you defined only the sync version "
314
+ "(wrap_model_call) and invoked your agent in an asynchronous context "
315
+ "(e.g., using `astream()` or `ainvoke()`). "
316
+ "To resolve this, either: "
317
+ "(1) subclass AgentMiddleware and implement the asynchronous awrap_model_call method, "
318
+ "(2) use the @wrap_model_call decorator on a standalone async function, or "
319
+ "(3) invoke your agent synchronously using `stream()` or `invoke()`."
320
+ )
321
+ raise NotImplementedError(msg)
295
322
 
296
323
  def after_agent(self, state: StateT, runtime: Runtime[ContextT]) -> dict[str, Any] | None:
297
324
  """Logic to run after the agent execution completes."""
@@ -353,7 +380,77 @@ class AgentMiddleware(Generic[StateT, ContextT]):
353
380
  continue
354
381
  return result
355
382
  """
356
- raise NotImplementedError
383
+ msg = (
384
+ "Synchronous implementation of wrap_tool_call is not available. "
385
+ "You are likely encountering this error because you defined only the async version "
386
+ "(awrap_tool_call) and invoked your agent in a synchronous context "
387
+ "(e.g., using `stream()` or `invoke()`). "
388
+ "To resolve this, either: "
389
+ "(1) subclass AgentMiddleware and implement the synchronous wrap_tool_call method, "
390
+ "(2) use the @wrap_tool_call decorator on a standalone sync function, or "
391
+ "(3) invoke your agent asynchronously using `astream()` or `ainvoke()`."
392
+ )
393
+ raise NotImplementedError(msg)
394
+
395
+ async def awrap_tool_call(
396
+ self,
397
+ request: ToolCallRequest,
398
+ handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
399
+ ) -> ToolMessage | Command:
400
+ """Intercept and control async tool execution via handler callback.
401
+
402
+ The handler callback executes the tool call and returns a ToolMessage or Command.
403
+ Middleware can call the handler multiple times for retry logic, skip calling
404
+ it to short-circuit, or modify the request/response. Multiple middleware
405
+ compose with first in list as outermost layer.
406
+
407
+ Args:
408
+ request: Tool call request with call dict, BaseTool, state, and runtime.
409
+ Access state via request.state and runtime via request.runtime.
410
+ handler: Async callable to execute the tool and returns ToolMessage or Command.
411
+ Call this to execute the tool. Can be called multiple times
412
+ for retry logic. Can skip calling it to short-circuit.
413
+
414
+ Returns:
415
+ ToolMessage or Command (the final result).
416
+
417
+ The handler callable can be invoked multiple times for retry logic.
418
+ Each call to handler is independent and stateless.
419
+
420
+ Examples:
421
+ Async retry on error:
422
+ ```python
423
+ async def awrap_tool_call(self, request, handler):
424
+ for attempt in range(3):
425
+ try:
426
+ result = await handler(request)
427
+ if is_valid(result):
428
+ return result
429
+ except Exception:
430
+ if attempt == 2:
431
+ raise
432
+ return result
433
+ ```
434
+
435
+
436
+ async def awrap_tool_call(self, request, handler):
437
+ if cached := await get_cache_async(request):
438
+ return ToolMessage(content=cached, tool_call_id=request.tool_call["id"])
439
+ result = await handler(request)
440
+ await save_cache_async(request, result)
441
+ return result
442
+ """
443
+ msg = (
444
+ "Asynchronous implementation of awrap_tool_call is not available. "
445
+ "You are likely encountering this error because you defined only the sync version "
446
+ "(wrap_tool_call) and invoked your agent in an asynchronous context "
447
+ "(e.g., using `astream()` or `ainvoke()`). "
448
+ "To resolve this, either: "
449
+ "(1) subclass AgentMiddleware and implement the asynchronous awrap_tool_call method, "
450
+ "(2) use the @wrap_tool_call decorator on a standalone async function, or "
451
+ "(3) invoke your agent synchronously using `stream()` or `invoke()`."
452
+ )
453
+ raise NotImplementedError(msg)
357
454
 
358
455
 
359
456
  class _CallableWithStateAndRuntime(Protocol[StateT_contra, ContextT]):
@@ -1104,6 +1201,16 @@ def dynamic_prompt(
1104
1201
  request.system_prompt = prompt
1105
1202
  return handler(request)
1106
1203
 
1204
+ async def async_wrapped_from_sync(
1205
+ self: AgentMiddleware[StateT, ContextT], # noqa: ARG001
1206
+ request: ModelRequest,
1207
+ handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
1208
+ ) -> ModelCallResult:
1209
+ # Delegate to sync function
1210
+ prompt = cast("str", func(request))
1211
+ request.system_prompt = prompt
1212
+ return await handler(request)
1213
+
1107
1214
  middleware_name = cast("str", getattr(func, "__name__", "DynamicPromptMiddleware"))
1108
1215
 
1109
1216
  return type(
@@ -1113,6 +1220,7 @@ def dynamic_prompt(
1113
1220
  "state_schema": AgentState,
1114
1221
  "tools": [],
1115
1222
  "wrap_model_call": wrapped,
1223
+ "awrap_model_call": async_wrapped_from_sync,
1116
1224
  },
1117
1225
  )()
1118
1226
 
@@ -1309,6 +1417,7 @@ def wrap_tool_call(
1309
1417
  Args:
1310
1418
  func: Function accepting (request, handler) that calls
1311
1419
  handler(request) to execute the tool and returns final ToolMessage or Command.
1420
+ Can be sync or async.
1312
1421
  tools: Additional tools to register with this middleware.
1313
1422
  name: Middleware class name. Defaults to function name.
1314
1423
 
@@ -1316,13 +1425,6 @@ def wrap_tool_call(
1316
1425
  AgentMiddleware instance if func provided, otherwise a decorator.
1317
1426
 
1318
1427
  Examples:
1319
- Basic passthrough:
1320
- ```python
1321
- @wrap_tool_call
1322
- def passthrough(request, handler):
1323
- return handler(request)
1324
- ```
1325
-
1326
1428
  Retry logic:
1327
1429
  ```python
1328
1430
  @wrap_tool_call
@@ -1336,6 +1438,18 @@ def wrap_tool_call(
1336
1438
  raise
1337
1439
  ```
1338
1440
 
1441
+ Async retry logic:
1442
+ ```python
1443
+ @wrap_tool_call
1444
+ async def async_retry(request, handler):
1445
+ for attempt in range(3):
1446
+ try:
1447
+ return await handler(request)
1448
+ except Exception:
1449
+ if attempt == 2:
1450
+ raise
1451
+ ```
1452
+
1339
1453
  Modify request:
1340
1454
  ```python
1341
1455
  @wrap_tool_call
@@ -1359,6 +1473,31 @@ def wrap_tool_call(
1359
1473
  def decorator(
1360
1474
  func: _CallableReturningToolResponse,
1361
1475
  ) -> AgentMiddleware:
1476
+ is_async = iscoroutinefunction(func)
1477
+
1478
+ if is_async:
1479
+
1480
+ async def async_wrapped(
1481
+ self: AgentMiddleware, # noqa: ARG001
1482
+ request: ToolCallRequest,
1483
+ handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
1484
+ ) -> ToolMessage | Command:
1485
+ return await func(request, handler) # type: ignore[arg-type,misc]
1486
+
1487
+ middleware_name = name or cast(
1488
+ "str", getattr(func, "__name__", "WrapToolCallMiddleware")
1489
+ )
1490
+
1491
+ return type(
1492
+ middleware_name,
1493
+ (AgentMiddleware,),
1494
+ {
1495
+ "state_schema": AgentState,
1496
+ "tools": tools or [],
1497
+ "awrap_tool_call": async_wrapped,
1498
+ },
1499
+ )()
1500
+
1362
1501
  def wrapped(
1363
1502
  self: AgentMiddleware, # noqa: ARG001
1364
1503
  request: ToolCallRequest,
@@ -3,10 +3,8 @@
3
3
  from langchain_core.embeddings import Embeddings
4
4
 
5
5
  from langchain.embeddings.base import init_embeddings
6
- from langchain.embeddings.cache import CacheBackedEmbeddings
7
6
 
8
7
  __all__ = [
9
- "CacheBackedEmbeddings",
10
8
  "Embeddings",
11
9
  "init_embeddings",
12
10
  ]
@@ -0,0 +1,63 @@
1
+ """Message types."""
2
+
3
+ from langchain_core.messages import (
4
+ AIMessage,
5
+ AIMessageChunk,
6
+ Annotation,
7
+ AnyMessage,
8
+ AudioContentBlock,
9
+ Citation,
10
+ ContentBlock,
11
+ DataContentBlock,
12
+ FileContentBlock,
13
+ HumanMessage,
14
+ ImageContentBlock,
15
+ InvalidToolCall,
16
+ MessageLikeRepresentation,
17
+ NonStandardAnnotation,
18
+ NonStandardContentBlock,
19
+ PlainTextContentBlock,
20
+ ReasoningContentBlock,
21
+ RemoveMessage,
22
+ ServerToolCall,
23
+ ServerToolCallChunk,
24
+ ServerToolResult,
25
+ SystemMessage,
26
+ TextContentBlock,
27
+ ToolCall,
28
+ ToolCallChunk,
29
+ ToolMessage,
30
+ VideoContentBlock,
31
+ trim_messages,
32
+ )
33
+
34
+ __all__ = [
35
+ "AIMessage",
36
+ "AIMessageChunk",
37
+ "Annotation",
38
+ "AnyMessage",
39
+ "AudioContentBlock",
40
+ "Citation",
41
+ "ContentBlock",
42
+ "DataContentBlock",
43
+ "FileContentBlock",
44
+ "HumanMessage",
45
+ "ImageContentBlock",
46
+ "InvalidToolCall",
47
+ "MessageLikeRepresentation",
48
+ "NonStandardAnnotation",
49
+ "NonStandardContentBlock",
50
+ "PlainTextContentBlock",
51
+ "ReasoningContentBlock",
52
+ "RemoveMessage",
53
+ "ServerToolCall",
54
+ "ServerToolCallChunk",
55
+ "ServerToolResult",
56
+ "SystemMessage",
57
+ "TextContentBlock",
58
+ "ToolCall",
59
+ "ToolCallChunk",
60
+ "ToolMessage",
61
+ "VideoContentBlock",
62
+ "trim_messages",
63
+ ]
@@ -8,11 +8,7 @@ from langchain_core.tools import (
8
8
  tool,
9
9
  )
10
10
 
11
- from langchain.tools.tool_node import (
12
- InjectedState,
13
- InjectedStore,
14
- ToolNode,
15
- )
11
+ from langchain.tools.tool_node import InjectedState, InjectedStore, ToolInvocationError
16
12
 
17
13
  __all__ = [
18
14
  "BaseTool",
@@ -21,6 +17,6 @@ __all__ = [
21
17
  "InjectedToolArg",
22
18
  "InjectedToolCallId",
23
19
  "ToolException",
24
- "ToolNode",
20
+ "ToolInvocationError",
25
21
  "tool",
26
22
  ]
@@ -38,7 +38,7 @@ from __future__ import annotations
38
38
  import asyncio
39
39
  import inspect
40
40
  import json
41
- from collections.abc import Callable
41
+ from collections.abc import Awaitable, Callable
42
42
  from copy import copy, deepcopy
43
43
  from dataclasses import dataclass, replace
44
44
  from types import UnionType
@@ -72,6 +72,7 @@ from langchain_core.tools import BaseTool, InjectedToolArg
72
72
  from langchain_core.tools import tool as create_tool
73
73
  from langchain_core.tools.base import (
74
74
  TOOL_MESSAGE_BLOCK_TYPES,
75
+ ToolException,
75
76
  get_all_basemodel_annotations,
76
77
  )
77
78
  from langgraph._internal._runnable import RunnableCallable
@@ -188,6 +189,12 @@ Examples:
188
189
  return result
189
190
  """
190
191
 
192
+ AsyncToolCallWrapper = Callable[
193
+ [ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]]],
194
+ Awaitable[ToolMessage | Command],
195
+ ]
196
+ """Async wrapper for tool call execution with multi-call support."""
197
+
191
198
 
192
199
  class ToolCallWithContext(TypedDict):
193
200
  """ToolCall with additional context for graph state.
@@ -239,8 +246,11 @@ def msg_content_output(output: Any) -> str | list[dict]:
239
246
  return str(output)
240
247
 
241
248
 
242
- class ToolInvocationError(Exception):
243
- """Exception raised when a tool invocation fails due to invalid arguments."""
249
+ class ToolInvocationError(ToolException):
250
+ """An error occurred while invoking a tool due to invalid arguments.
251
+
252
+ This exception is only raised when invoking a tool using the ToolNode!
253
+ """
244
254
 
245
255
  def __init__(
246
256
  self, tool_name: str, source: ValidationError, tool_kwargs: dict[str, Any]
@@ -382,7 +392,7 @@ def _infer_handled_types(handler: Callable[..., str]) -> tuple[type[Exception],
382
392
  return (Exception,)
383
393
 
384
394
 
385
- class ToolNode(RunnableCallable):
395
+ class _ToolNode(RunnableCallable):
386
396
  """A node for executing tools in LangGraph workflows.
387
397
 
388
398
  Handles tool execution patterns including function calls, state injection,
@@ -500,6 +510,7 @@ class ToolNode(RunnableCallable):
500
510
  | tuple[type[Exception], ...] = _default_handle_tool_errors,
501
511
  messages_key: str = "messages",
502
512
  wrap_tool_call: ToolCallWrapper | None = None,
513
+ awrap_tool_call: AsyncToolCallWrapper | None = None,
503
514
  ) -> None:
504
515
  """Initialize ToolNode with tools and configuration.
505
516
 
@@ -509,9 +520,11 @@ class ToolNode(RunnableCallable):
509
520
  tags: Optional metadata tags.
510
521
  handle_tool_errors: Error handling configuration.
511
522
  messages_key: State key containing messages.
512
- wrap_tool_call: Wrapper function to intercept tool execution. Receives
523
+ wrap_tool_call: Sync wrapper function to intercept tool execution. Receives
513
524
  ToolCallRequest and execute callable, returns ToolMessage or Command.
514
525
  Enables retries, caching, request modification, and control flow.
526
+ awrap_tool_call: Async wrapper function to intercept tool execution.
527
+ If not provided, falls back to wrap_tool_call for async execution.
515
528
  """
516
529
  super().__init__(self._func, self._afunc, name=name, tags=tags, trace=False)
517
530
  self._tools_by_name: dict[str, BaseTool] = {}
@@ -520,6 +533,7 @@ class ToolNode(RunnableCallable):
520
533
  self._handle_tool_errors = handle_tool_errors
521
534
  self._messages_key = messages_key
522
535
  self._wrap_tool_call = wrap_tool_call
536
+ self._awrap_tool_call = awrap_tool_call
523
537
  for tool in tools:
524
538
  if not isinstance(tool, BaseTool):
525
539
  tool_ = create_tool(cast("type[BaseTool]", tool))
@@ -855,7 +869,7 @@ class ToolNode(RunnableCallable):
855
869
  input: list[AnyMessage] | dict[str, Any] | BaseModel,
856
870
  runtime: Any,
857
871
  ) -> ToolMessage | Command:
858
- """Execute single tool call asynchronously with wrap_tool_call wrapper if configured.
872
+ """Execute single tool call asynchronously with awrap_tool_call wrapper if configured.
859
873
 
860
874
  Args:
861
875
  call: Tool call dict.
@@ -883,7 +897,7 @@ class ToolNode(RunnableCallable):
883
897
  runtime=runtime,
884
898
  )
885
899
 
886
- if self._wrap_tool_call is None:
900
+ if self._awrap_tool_call is None and self._wrap_tool_call is None:
887
901
  # No wrapper - execute directly
888
902
  return await self._execute_tool_async(tool_request, input_type, config)
889
903
 
@@ -892,12 +906,17 @@ class ToolNode(RunnableCallable):
892
906
  """Execute tool with given request. Can be called multiple times."""
893
907
  return await self._execute_tool_async(req, input_type, config)
894
908
 
909
+ def _sync_execute(req: ToolCallRequest) -> ToolMessage | Command:
910
+ """Sync execute fallback for sync wrapper."""
911
+ return self._execute_tool_sync(req, input_type, config)
912
+
895
913
  # Call wrapper with request and execute callable
896
- # Note: wrapper is sync, but execute callable is async
897
914
  try:
898
- result = self._wrap_tool_call(tool_request, execute) # type: ignore[arg-type]
899
- # If result is a coroutine, await it (though wrapper should be sync)
900
- return await result if hasattr(result, "__await__") else result
915
+ if self._awrap_tool_call is not None:
916
+ return await self._awrap_tool_call(tool_request, execute)
917
+ # None check was performed above already
918
+ self._wrap_tool_call = cast("ToolCallWrapper", self._wrap_tool_call)
919
+ return self._wrap_tool_call(tool_request, _sync_execute)
901
920
  except Exception as e:
902
921
  # Wrapper threw an exception
903
922
  if not self._handle_tool_errors: