langchain 1.0.1__tar.gz → 1.0.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 (117) hide show
  1. {langchain-1.0.1 → langchain-1.0.2}/PKG-INFO +1 -1
  2. {langchain-1.0.1 → langchain-1.0.2}/langchain/tools/tool_node.py +103 -19
  3. {langchain-1.0.1 → langchain-1.0.2}/pyproject.toml +1 -1
  4. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_tool_node.py +170 -4
  5. langchain-1.0.2/tests/unit_tests/agents/test_tool_node_validation_error_filtering.py +678 -0
  6. {langchain-1.0.1 → langchain-1.0.2}/uv.lock +2 -3
  7. {langchain-1.0.1 → langchain-1.0.2}/.gitignore +0 -0
  8. {langchain-1.0.1 → langchain-1.0.2}/LICENSE +0 -0
  9. {langchain-1.0.1 → langchain-1.0.2}/Makefile +0 -0
  10. {langchain-1.0.1 → langchain-1.0.2}/README.md +0 -0
  11. {langchain-1.0.1 → langchain-1.0.2}/extended_testing_deps.txt +0 -0
  12. {langchain-1.0.1 → langchain-1.0.2}/langchain/__init__.py +0 -0
  13. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/__init__.py +0 -0
  14. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/factory.py +0 -0
  15. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/__init__.py +0 -0
  16. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/_execution.py +0 -0
  17. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/_redaction.py +0 -0
  18. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/context_editing.py +0 -0
  19. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/file_search.py +0 -0
  20. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/human_in_the_loop.py +0 -0
  21. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/model_call_limit.py +0 -0
  22. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/model_fallback.py +0 -0
  23. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/pii.py +0 -0
  24. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/shell_tool.py +0 -0
  25. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/summarization.py +0 -0
  26. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/todo.py +0 -0
  27. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/tool_call_limit.py +0 -0
  28. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/tool_emulator.py +0 -0
  29. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/tool_retry.py +0 -0
  30. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/tool_selection.py +0 -0
  31. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/middleware/types.py +0 -0
  32. {langchain-1.0.1 → langchain-1.0.2}/langchain/agents/structured_output.py +0 -0
  33. {langchain-1.0.1 → langchain-1.0.2}/langchain/chat_models/__init__.py +0 -0
  34. {langchain-1.0.1 → langchain-1.0.2}/langchain/chat_models/base.py +0 -0
  35. {langchain-1.0.1 → langchain-1.0.2}/langchain/embeddings/__init__.py +0 -0
  36. {langchain-1.0.1 → langchain-1.0.2}/langchain/embeddings/base.py +0 -0
  37. {langchain-1.0.1 → langchain-1.0.2}/langchain/messages/__init__.py +0 -0
  38. {langchain-1.0.1 → langchain-1.0.2}/langchain/py.typed +0 -0
  39. {langchain-1.0.1 → langchain-1.0.2}/langchain/rate_limiters/__init__.py +0 -0
  40. {langchain-1.0.1 → langchain-1.0.2}/langchain/tools/__init__.py +0 -0
  41. {langchain-1.0.1 → langchain-1.0.2}/scripts/check_imports.py +0 -0
  42. {langchain-1.0.1 → langchain-1.0.2}/tests/__init__.py +0 -0
  43. {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/__init__.py +0 -0
  44. {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/agents/__init__.py +0 -0
  45. {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/agents/middleware/__init__.py +0 -0
  46. {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/agents/middleware/test_shell_tool_integration.py +0 -0
  47. {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/agents/test_response_format.py +0 -0
  48. {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/cache/__init__.py +0 -0
  49. {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/cache/fake_embeddings.py +0 -0
  50. {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/chat_models/__init__.py +0 -0
  51. {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/chat_models/test_base.py +0 -0
  52. {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/conftest.py +0 -0
  53. {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/embeddings/__init__.py +0 -0
  54. {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/embeddings/test_base.py +0 -0
  55. {langchain-1.0.1 → langchain-1.0.2}/tests/integration_tests/test_compile.py +0 -0
  56. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/__init__.py +0 -0
  57. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/__init__.py +0 -0
  58. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/__snapshots__/test_middleware_agent.ambr +0 -0
  59. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/__snapshots__/test_middleware_decorators.ambr +0 -0
  60. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/__snapshots__/test_return_direct_graph.ambr +0 -0
  61. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/any_str.py +0 -0
  62. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/compose-postgres.yml +0 -0
  63. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/compose-redis.yml +0 -0
  64. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/conftest.py +0 -0
  65. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/conftest_checkpointer.py +0 -0
  66. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/conftest_store.py +0 -0
  67. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/memory_assert.py +0 -0
  68. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/messages.py +0 -0
  69. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/__init__.py +0 -0
  70. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_before_after_agent.py +0 -0
  71. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_file_search.py +0 -0
  72. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_llm_tool_selection.py +0 -0
  73. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_override_methods.py +0 -0
  74. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_shell_execution_policies.py +0 -0
  75. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_shell_tool.py +0 -0
  76. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_tool_emulator.py +0 -0
  77. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_tool_retry.py +0 -0
  78. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_wrap_model_call_decorator.py +0 -0
  79. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_wrap_model_call_middleware.py +0 -0
  80. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/middleware/test_wrap_tool_call_decorator.py +0 -0
  81. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/model.py +0 -0
  82. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/specifications/responses.json +0 -0
  83. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/specifications/return_direct.json +0 -0
  84. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_context_editing_middleware.py +0 -0
  85. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_handler_composition.py +0 -0
  86. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_injected_runtime_create_agent.py +0 -0
  87. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_middleware_agent.py +0 -0
  88. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_middleware_decorators.py +0 -0
  89. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_middleware_tools.py +0 -0
  90. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_model_fallback_middleware.py +0 -0
  91. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_on_tool_call_middleware.py +0 -0
  92. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_pii_middleware.py +0 -0
  93. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_react_agent.py +0 -0
  94. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_response_format.py +0 -0
  95. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_responses.py +0 -0
  96. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_responses_spec.py +0 -0
  97. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_return_direct_graph.py +0 -0
  98. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_return_direct_spec.py +0 -0
  99. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_state_schema.py +0 -0
  100. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_sync_async_tool_wrapper_composition.py +0 -0
  101. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_todo_middleware.py +0 -0
  102. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_tool_call_limit.py +0 -0
  103. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/test_tool_node_interceptor_unregistered.py +0 -0
  104. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/agents/utils.py +0 -0
  105. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/chat_models/__init__.py +0 -0
  106. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/chat_models/test_chat_models.py +0 -0
  107. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/conftest.py +0 -0
  108. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/embeddings/__init__.py +0 -0
  109. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/embeddings/test_base.py +0 -0
  110. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/embeddings/test_imports.py +0 -0
  111. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/stubs.py +0 -0
  112. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/test_dependencies.py +0 -0
  113. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/test_imports.py +0 -0
  114. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/test_pytest_config.py +0 -0
  115. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/tools/__init__.py +0 -0
  116. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/tools/test_imports.py +0 -0
  117. {langchain-1.0.1 → langchain-1.0.2}/tests/unit_tests/tools/test_on_tool_call.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain
3
- Version: 1.0.1
3
+ Version: 1.0.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/
@@ -89,6 +89,7 @@ if TYPE_CHECKING:
89
89
  from collections.abc import Sequence
90
90
 
91
91
  from langgraph.runtime import Runtime
92
+ from pydantic_core import ErrorDetails
92
93
 
93
94
  # right now we use a dict as the default, can change this to AgentState, but depends
94
95
  # on if this lives in LangChain or LangGraph... ideally would have some typed
@@ -303,7 +304,11 @@ class ToolInvocationError(ToolException):
303
304
  """
304
305
 
305
306
  def __init__(
306
- self, tool_name: str, source: ValidationError, tool_kwargs: dict[str, Any]
307
+ self,
308
+ tool_name: str,
309
+ source: ValidationError,
310
+ tool_kwargs: dict[str, Any],
311
+ filtered_errors: list[ErrorDetails] | None = None,
307
312
  ) -> None:
308
313
  """Initialize the ToolInvocationError.
309
314
 
@@ -311,13 +316,28 @@ class ToolInvocationError(ToolException):
311
316
  tool_name: The name of the tool that failed.
312
317
  source: The exception that occurred.
313
318
  tool_kwargs: The keyword arguments that were passed to the tool.
319
+ filtered_errors: Optional list of filtered validation errors excluding
320
+ injected arguments.
314
321
  """
322
+ # Format error display based on filtered errors if provided
323
+ if filtered_errors is not None:
324
+ # Manually format the filtered errors without URLs or fancy formatting
325
+ error_str_parts = []
326
+ for error in filtered_errors:
327
+ loc_str = ".".join(str(loc) for loc in error.get("loc", ()))
328
+ msg = error.get("msg", "Unknown error")
329
+ error_str_parts.append(f"{loc_str}: {msg}")
330
+ error_display_str = "\n".join(error_str_parts)
331
+ else:
332
+ error_display_str = str(source)
333
+
315
334
  self.message = TOOL_INVOCATION_ERROR_TEMPLATE.format(
316
- tool_name=tool_name, tool_kwargs=tool_kwargs, error=source
335
+ tool_name=tool_name, tool_kwargs=tool_kwargs, error=error_display_str
317
336
  )
318
337
  self.tool_name = tool_name
319
338
  self.tool_kwargs = tool_kwargs
320
339
  self.source = source
340
+ self.filtered_errors = filtered_errors
321
341
  super().__init__(self.message)
322
342
 
323
343
 
@@ -442,6 +462,59 @@ def _infer_handled_types(handler: Callable[..., str]) -> tuple[type[Exception],
442
462
  return (Exception,)
443
463
 
444
464
 
465
+ def _filter_validation_errors(
466
+ validation_error: ValidationError,
467
+ tool_to_state_args: dict[str, str | None],
468
+ tool_to_store_arg: str | None,
469
+ tool_to_runtime_arg: str | None,
470
+ ) -> list[ErrorDetails]:
471
+ """Filter validation errors to only include LLM-controlled arguments.
472
+
473
+ When a tool invocation fails validation, only errors for arguments that the LLM
474
+ controls should be included in error messages. This ensures the LLM receives
475
+ focused, actionable feedback about parameters it can actually fix. System-injected
476
+ arguments (state, store, runtime) are filtered out since the LLM has no control
477
+ over them.
478
+
479
+ This function also removes injected argument values from the `input` field in error
480
+ details, ensuring that only LLM-provided arguments appear in error messages.
481
+
482
+ Args:
483
+ validation_error: The Pydantic ValidationError raised during tool invocation.
484
+ tool_to_state_args: Mapping of state argument names to state field names.
485
+ tool_to_store_arg: Name of the store argument, if any.
486
+ tool_to_runtime_arg: Name of the runtime argument, if any.
487
+
488
+ Returns:
489
+ List of ErrorDetails containing only errors for LLM-controlled arguments,
490
+ with system-injected argument values removed from the input field.
491
+ """
492
+ injected_args = set(tool_to_state_args.keys())
493
+ if tool_to_store_arg:
494
+ injected_args.add(tool_to_store_arg)
495
+ if tool_to_runtime_arg:
496
+ injected_args.add(tool_to_runtime_arg)
497
+
498
+ filtered_errors: list[ErrorDetails] = []
499
+ for error in validation_error.errors():
500
+ # Check if error location contains any injected argument
501
+ # error['loc'] is a tuple like ('field_name',) or ('field_name', 'nested_field')
502
+ if error["loc"] and error["loc"][0] not in injected_args:
503
+ # Create a copy of the error dict to avoid mutating the original
504
+ error_copy: dict[str, Any] = {**error}
505
+
506
+ # Remove injected arguments from input_value if it's a dict
507
+ if isinstance(error_copy.get("input"), dict):
508
+ input_dict = error_copy["input"]
509
+ input_copy = {k: v for k, v in input_dict.items() if k not in injected_args}
510
+ error_copy["input"] = input_copy
511
+
512
+ # Cast is safe because ErrorDetails is a TypedDict compatible with this structure
513
+ filtered_errors.append(error_copy) # type: ignore[arg-type]
514
+
515
+ return filtered_errors
516
+
517
+
445
518
  class _ToolNode(RunnableCallable):
446
519
  """A node for executing tools in LangGraph workflows.
447
520
 
@@ -623,17 +696,10 @@ class _ToolNode(RunnableCallable):
623
696
  )
624
697
  tool_runtimes.append(tool_runtime)
625
698
 
626
- # Inject tool arguments (including runtime)
627
-
628
- injected_tool_calls = []
699
+ # Pass original tool calls without injection
629
700
  input_types = [input_type] * len(tool_calls)
630
- for call, tool_runtime in zip(tool_calls, tool_runtimes, strict=False):
631
- injected_call = self._inject_tool_args(call, tool_runtime) # type: ignore[arg-type]
632
- injected_tool_calls.append(injected_call)
633
701
  with get_executor_for_config(config) as executor:
634
- outputs = list(
635
- executor.map(self._run_one, injected_tool_calls, input_types, tool_runtimes)
636
- )
702
+ outputs = list(executor.map(self._run_one, tool_calls, input_types, tool_runtimes))
637
703
 
638
704
  return self._combine_tool_outputs(outputs, input_type)
639
705
 
@@ -660,12 +726,10 @@ class _ToolNode(RunnableCallable):
660
726
  )
661
727
  tool_runtimes.append(tool_runtime)
662
728
 
663
- injected_tool_calls = []
729
+ # Pass original tool calls without injection
664
730
  coros = []
665
731
  for call, tool_runtime in zip(tool_calls, tool_runtimes, strict=False):
666
- injected_call = self._inject_tool_args(call, tool_runtime) # type: ignore[arg-type]
667
- injected_tool_calls.append(injected_call)
668
- coros.append(self._arun_one(injected_call, input_type, tool_runtime)) # type: ignore[arg-type]
732
+ coros.append(self._arun_one(call, input_type, tool_runtime)) # type: ignore[arg-type]
669
733
  outputs = await asyncio.gather(*coros)
670
734
 
671
735
  return self._combine_tool_outputs(outputs, input_type)
@@ -742,13 +806,23 @@ class _ToolNode(RunnableCallable):
742
806
  msg = f"Tool {call['name']} is not registered with ToolNode"
743
807
  raise TypeError(msg)
744
808
 
745
- call_args = {**call, "type": "tool_call"}
809
+ # Inject state, store, and runtime right before invocation
810
+ injected_call = self._inject_tool_args(call, request.runtime)
811
+ call_args = {**injected_call, "type": "tool_call"}
746
812
 
747
813
  try:
748
814
  try:
749
815
  response = tool.invoke(call_args, config)
750
816
  except ValidationError as exc:
751
- raise ToolInvocationError(call["name"], exc, call["args"]) from exc
817
+ # Filter out errors for injected arguments
818
+ filtered_errors = _filter_validation_errors(
819
+ exc,
820
+ self._tool_to_state_args.get(call["name"], {}),
821
+ self._tool_to_store_arg.get(call["name"]),
822
+ self._tool_to_runtime_arg.get(call["name"]),
823
+ )
824
+ # Use original call["args"] without injected values for error reporting
825
+ raise ToolInvocationError(call["name"], exc, call["args"], filtered_errors) from exc
752
826
 
753
827
  # GraphInterrupt is a special exception that will always be raised.
754
828
  # It can be triggered in the following scenarios,
@@ -887,13 +961,23 @@ class _ToolNode(RunnableCallable):
887
961
  msg = f"Tool {call['name']} is not registered with ToolNode"
888
962
  raise TypeError(msg)
889
963
 
890
- call_args = {**call, "type": "tool_call"}
964
+ # Inject state, store, and runtime right before invocation
965
+ injected_call = self._inject_tool_args(call, request.runtime)
966
+ call_args = {**injected_call, "type": "tool_call"}
891
967
 
892
968
  try:
893
969
  try:
894
970
  response = await tool.ainvoke(call_args, config)
895
971
  except ValidationError as exc:
896
- raise ToolInvocationError(call["name"], exc, call["args"]) from exc
972
+ # Filter out errors for injected arguments
973
+ filtered_errors = _filter_validation_errors(
974
+ exc,
975
+ self._tool_to_state_args.get(call["name"], {}),
976
+ self._tool_to_store_arg.get(call["name"]),
977
+ self._tool_to_runtime_arg.get(call["name"]),
978
+ )
979
+ # Use original call["args"] without injected values for error reporting
980
+ raise ToolInvocationError(call["name"], exc, call["args"], filtered_errors) from exc
897
981
 
898
982
  # GraphInterrupt is a special exception that will always be raised.
899
983
  # It can be triggered in the following scenarios,
@@ -13,7 +13,7 @@ dependencies = [
13
13
  ]
14
14
 
15
15
  name = "langchain"
16
- version = "1.0.1"
16
+ version = "1.0.2"
17
17
  description = "Building applications with LLMs through composability"
18
18
  readme = "README.md"
19
19
 
@@ -10,6 +10,8 @@ from typing import (
10
10
  TypeVar,
11
11
  )
12
12
  from unittest.mock import Mock
13
+ from langchain.agents import create_agent
14
+ from langchain.agents.middleware.types import AgentState
13
15
 
14
16
  import pytest
15
17
  from langchain_core.messages import (
@@ -302,6 +304,172 @@ def test_tool_node_error_handling_default_exception() -> None:
302
304
  )
303
305
 
304
306
 
307
+ @pytest.mark.skipif(
308
+ sys.version_info >= (3, 14), reason="Pydantic model rebuild issue in Python 3.14"
309
+ )
310
+ def test_tool_invocation_error_excludes_injected_state() -> None:
311
+ """Test that tool invocation errors only include LLM-controllable arguments.
312
+
313
+ When a tool has InjectedState parameters and the LLM makes an incorrect
314
+ invocation (e.g., missing required arguments), the error message should only
315
+ contain the arguments from the tool call that the LLM controls. This ensures
316
+ the LLM receives relevant context to correct its mistakes, without being
317
+ distracted by system-injected parameters it has no control over.
318
+
319
+ This test uses create_agent to ensure the behavior works in a full agent context.
320
+ """
321
+
322
+ # Define a custom state schema with injected data
323
+ class TestState(AgentState):
324
+ secret_data: str # Example of state data not controlled by LLM
325
+
326
+ @dec_tool
327
+ def tool_with_injected_state(
328
+ some_val: int,
329
+ state: Annotated[TestState, InjectedState],
330
+ ) -> str:
331
+ """Tool that uses injected state."""
332
+ return f"some_val: {some_val}"
333
+
334
+ # Create a fake model that makes an incorrect tool call (missing 'some_val')
335
+ # Then returns no tool calls on the second iteration to end the loop
336
+ model = FakeToolCallingModel(
337
+ tool_calls=[
338
+ [
339
+ {
340
+ "name": "tool_with_injected_state",
341
+ "args": {"wrong_arg": "value"}, # Missing required 'some_val'
342
+ "id": "call_1",
343
+ }
344
+ ],
345
+ [], # No tool calls on second iteration to end the loop
346
+ ]
347
+ )
348
+
349
+ # Create an agent with the tool and custom state schema
350
+ agent = create_agent(
351
+ model=model,
352
+ tools=[tool_with_injected_state],
353
+ state_schema=TestState,
354
+ )
355
+
356
+ # Invoke the agent with injected state data
357
+ result = agent.invoke(
358
+ {
359
+ "messages": [HumanMessage("Test message")],
360
+ "secret_data": "sensitive_secret_123",
361
+ }
362
+ )
363
+
364
+ # Find the tool error message
365
+ tool_messages = [m for m in result["messages"] if m.type == "tool"]
366
+ assert len(tool_messages) == 1
367
+ tool_message = tool_messages[0]
368
+ assert tool_message.status == "error"
369
+
370
+ # The error message should contain only the LLM-provided args (wrong_arg)
371
+ # and NOT the system-injected state (secret_data)
372
+ assert "{'wrong_arg': 'value'}" in tool_message.content
373
+ assert "secret_data" not in tool_message.content
374
+ assert "sensitive_secret_123" not in tool_message.content
375
+
376
+
377
+ @pytest.mark.skipif(
378
+ sys.version_info >= (3, 14), reason="Pydantic model rebuild issue in Python 3.14"
379
+ )
380
+ async def test_tool_invocation_error_excludes_injected_state_async() -> None:
381
+ """Test that async tool invocation errors only include LLM-controllable arguments.
382
+
383
+ This test verifies that the async execution path (_execute_tool_async and _arun_one)
384
+ properly filters validation errors to exclude system-injected arguments, ensuring
385
+ the LLM receives only relevant context for correction.
386
+ """
387
+
388
+ # Define a custom state schema
389
+ class TestState(AgentState):
390
+ internal_data: str
391
+
392
+ @dec_tool
393
+ async def async_tool_with_injected_state(
394
+ query: str,
395
+ max_results: int,
396
+ state: Annotated[TestState, InjectedState],
397
+ ) -> str:
398
+ """Async tool that uses injected state."""
399
+ return f"query: {query}, max_results: {max_results}"
400
+
401
+ # Create a fake model that makes an incorrect tool call
402
+ # - query has wrong type (int instead of str)
403
+ # - max_results is missing
404
+ model = FakeToolCallingModel(
405
+ tool_calls=[
406
+ [
407
+ {
408
+ "name": "async_tool_with_injected_state",
409
+ "args": {"query": 999}, # Wrong type, missing max_results
410
+ "id": "call_async_1",
411
+ }
412
+ ],
413
+ [], # End the loop
414
+ ]
415
+ )
416
+
417
+ # Create an agent with the async tool
418
+ agent = create_agent(
419
+ model=model,
420
+ tools=[async_tool_with_injected_state],
421
+ state_schema=TestState,
422
+ )
423
+
424
+ # Invoke with state data
425
+ result = await agent.ainvoke(
426
+ {
427
+ "messages": [HumanMessage("Test async")],
428
+ "internal_data": "secret_internal_value_xyz",
429
+ }
430
+ )
431
+
432
+ # Find the tool error message
433
+ tool_messages = [m for m in result["messages"] if m.type == "tool"]
434
+ assert len(tool_messages) == 1
435
+ tool_message = tool_messages[0]
436
+ assert tool_message.status == "error"
437
+
438
+ # Verify error mentions LLM-controlled parameters only
439
+ content = tool_message.content
440
+ assert "query" in content.lower(), "Error should mention 'query' (LLM-controlled)"
441
+ assert "max_results" in content.lower(), "Error should mention 'max_results' (LLM-controlled)"
442
+
443
+ # Verify system-injected state does not appear in the validation errors
444
+ # This keeps the error focused on what the LLM can actually fix
445
+ assert "internal_data" not in content, (
446
+ "Error should NOT mention 'internal_data' (system-injected field)"
447
+ )
448
+ assert "secret_internal_value" not in content, (
449
+ "Error should NOT contain system-injected state values"
450
+ )
451
+
452
+ # Verify only LLM-controlled parameters are in the error list
453
+ # Should see "query" and "max_results" errors, but not "state"
454
+ lines = content.split("\n")
455
+ error_lines = [line.strip() for line in lines if line.strip()]
456
+ # Find lines that look like field names (single words at start of line)
457
+ field_errors = [
458
+ line
459
+ for line in error_lines
460
+ if line
461
+ and not line.startswith("input")
462
+ and not line.startswith("field")
463
+ and not line.startswith("error")
464
+ and not line.startswith("please")
465
+ and len(line.split()) <= 2
466
+ ]
467
+ # Verify system-injected 'state' is not in the field error list
468
+ assert not any("state" == field.lower() for field in field_errors), (
469
+ "The field 'state' (system-injected) should not appear in validation errors"
470
+ )
471
+
472
+
305
473
  async def test_tool_node_error_handling() -> None:
306
474
  def handle_all(e: ValueError | ToolException | ToolInvocationError):
307
475
  return TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e))
@@ -355,10 +523,8 @@ async def test_tool_node_error_handling() -> None:
355
523
  result_error["messages"][1].content
356
524
  == f"Error: {ToolException('Test error')!r}\n Please fix your mistakes."
357
525
  )
358
- assert (
359
- "ValidationError" in result_error["messages"][2].content
360
- or "validation error" in result_error["messages"][2].content
361
- )
526
+ # Check that the validation error contains the field name
527
+ assert "some_other_val" in result_error["messages"][2].content
362
528
 
363
529
  assert result_error["messages"][0].tool_call_id == "some id"
364
530
  assert result_error["messages"][1].tool_call_id == "some other id"