langchain 1.0.0a14__py3-none-any.whl → 1.0.0rc1__py3-none-any.whl

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.

@@ -46,9 +46,10 @@ from typing import (
46
46
  TYPE_CHECKING,
47
47
  Annotated,
48
48
  Any,
49
+ Generic,
49
50
  Literal,
50
- Optional,
51
51
  TypedDict,
52
+ TypeVar,
52
53
  Union,
53
54
  cast,
54
55
  get_args,
@@ -65,6 +66,7 @@ from langchain_core.messages import (
65
66
  convert_to_messages,
66
67
  )
67
68
  from langchain_core.runnables.config import (
69
+ RunnableConfig,
68
70
  get_config_list,
69
71
  get_executor_for_config,
70
72
  )
@@ -78,15 +80,18 @@ from langchain_core.tools.base import (
78
80
  from langgraph._internal._runnable import RunnableCallable
79
81
  from langgraph.errors import GraphBubbleUp
80
82
  from langgraph.graph.message import REMOVE_ALL_MESSAGES
81
- from langgraph.runtime import get_runtime
82
- from langgraph.types import Command, Send
83
+ from langgraph.store.base import BaseStore # noqa: TC002
84
+ from langgraph.types import Command, Send, StreamWriter
83
85
  from pydantic import BaseModel, ValidationError
86
+ from typing_extensions import Unpack
84
87
 
85
88
  if TYPE_CHECKING:
86
89
  from collections.abc import Sequence
87
90
 
88
- from langchain_core.runnables import RunnableConfig
89
- from langgraph.store.base import BaseStore
91
+ from langgraph.runtime import Runtime
92
+
93
+ StateT = TypeVar("StateT")
94
+ ContextT = TypeVar("ContextT")
90
95
 
91
96
  INVALID_TOOL_NAME_ERROR_TEMPLATE = (
92
97
  "Error: {requested_tool} is not a valid tool, try one of [{available_tools}]."
@@ -104,21 +109,55 @@ TOOL_INVOCATION_ERROR_TEMPLATE = (
104
109
  )
105
110
 
106
111
 
112
+ class _ToolCallRequestOverrides(TypedDict, total=False):
113
+ """Possible overrides for ToolCallRequest.override() method."""
114
+
115
+ tool_call: ToolCall
116
+
117
+
107
118
  @dataclass()
108
119
  class ToolCallRequest:
109
120
  """Tool execution request passed to tool call interceptors.
110
121
 
111
122
  Attributes:
112
123
  tool_call: Tool call dict with name, args, and id from model output.
113
- tool: BaseTool instance to be invoked.
124
+ tool: BaseTool instance to be invoked, or None if tool is not
125
+ registered with the ToolNode. When tool is None, interceptors can
126
+ handle the request without validation. If the interceptor calls execute(),
127
+ validation will occur and raise an error for unregistered tools.
114
128
  state: Agent state (dict, list, or BaseModel).
115
129
  runtime: LangGraph runtime context (optional, None if outside graph).
116
130
  """
117
131
 
118
132
  tool_call: ToolCall
119
- tool: BaseTool
133
+ tool: BaseTool | None
120
134
  state: Any
121
- runtime: Any
135
+ runtime: ToolRuntime
136
+
137
+ def override(self, **overrides: Unpack[_ToolCallRequestOverrides]) -> ToolCallRequest:
138
+ """Replace the request with a new request with the given overrides.
139
+
140
+ Returns a new `ToolCallRequest` instance with the specified attributes replaced.
141
+ This follows an immutable pattern, leaving the original request unchanged.
142
+
143
+ Args:
144
+ **overrides: Keyword arguments for attributes to override. Supported keys:
145
+ - tool_call: Tool call dict with name, args, and id
146
+
147
+ Returns:
148
+ New ToolCallRequest instance with specified overrides applied.
149
+
150
+ Examples:
151
+ ```python
152
+ # Modify tool call arguments without mutating original
153
+ modified_call = {**request.tool_call, "args": {"value": 10}}
154
+ new_request = request.override(tool_call=modified_call)
155
+
156
+ # Override multiple attributes
157
+ new_request = request.override(tool_call=modified_call, state=new_state)
158
+ ```
159
+ """
160
+ return replace(self, **overrides)
122
161
 
123
162
 
124
163
  ToolCallWrapper = Callable[
@@ -151,12 +190,15 @@ Examples:
151
190
 
152
191
  Modify request before execution:
153
192
 
193
+ ```python
154
194
  def handler(request, execute):
155
195
  request.tool_call["args"]["value"] *= 2
156
196
  return execute(request)
197
+ ```
157
198
 
158
199
  Retry on error (execute multiple times):
159
200
 
201
+ ```python
160
202
  def handler(request, execute):
161
203
  for attempt in range(3):
162
204
  try:
@@ -167,9 +209,11 @@ Examples:
167
209
  if attempt == 2:
168
210
  raise
169
211
  return result
212
+ ```
170
213
 
171
214
  Conditional retry based on response:
172
215
 
216
+ ```python
173
217
  def handler(request, execute):
174
218
  for attempt in range(3):
175
219
  result = execute(request)
@@ -178,15 +222,18 @@ Examples:
178
222
  if attempt < 2:
179
223
  continue
180
224
  return result
225
+ ```
181
226
 
182
227
  Cache/short-circuit without calling execute:
183
228
 
229
+ ```python
184
230
  def handler(request, execute):
185
231
  if cached := get_cache(request):
186
232
  return ToolMessage(content=cached, tool_call_id=request.tool_call["id"])
187
233
  result = execute(request)
188
234
  save_cache(request, result)
189
235
  return result
236
+ ```
190
237
  """
191
238
 
192
239
  AsyncToolCallWrapper = Callable[
@@ -530,6 +577,7 @@ class _ToolNode(RunnableCallable):
530
577
  self._tools_by_name: dict[str, BaseTool] = {}
531
578
  self._tool_to_state_args: dict[str, dict[str, str | None]] = {}
532
579
  self._tool_to_store_arg: dict[str, str | None] = {}
580
+ self._tool_to_runtime_arg: dict[str, str | None] = {}
533
581
  self._handle_tool_errors = handle_tool_errors
534
582
  self._messages_key = messages_key
535
583
  self._wrap_tool_call = wrap_tool_call
@@ -542,6 +590,7 @@ class _ToolNode(RunnableCallable):
542
590
  self._tools_by_name[tool_.name] = tool_
543
591
  self._tool_to_state_args[tool_.name] = _get_state_args(tool_)
544
592
  self._tool_to_store_arg[tool_.name] = _get_store_arg(tool_)
593
+ self._tool_to_runtime_arg[tool_.name] = _get_runtime_arg(tool_)
545
594
 
546
595
  @property
547
596
  def tools_by_name(self) -> dict[str, BaseTool]:
@@ -552,26 +601,36 @@ class _ToolNode(RunnableCallable):
552
601
  self,
553
602
  input: list[AnyMessage] | dict[str, Any] | BaseModel,
554
603
  config: RunnableConfig,
555
- *,
556
- store: Optional[BaseStore], # noqa: UP045
604
+ runtime: Runtime,
557
605
  ) -> Any:
558
- try:
559
- runtime = get_runtime()
560
- except RuntimeError:
561
- # Running outside of LangGraph runtime context (e.g., unit tests)
562
- runtime = None
563
-
564
606
  tool_calls, input_type = self._parse_input(input)
565
- tool_calls = [self._inject_tool_args(call, input, store) for call in tool_calls]
566
-
567
607
  config_list = get_config_list(config, len(tool_calls))
608
+
609
+ # Construct ToolRuntime instances at the top level for each tool call
610
+ tool_runtimes = []
611
+ for call, cfg in zip(tool_calls, config_list, strict=False):
612
+ state = self._extract_state(input)
613
+ tool_runtime = ToolRuntime(
614
+ state=state,
615
+ tool_call_id=call["id"],
616
+ config=cfg,
617
+ context=runtime.context,
618
+ store=runtime.store,
619
+ stream_writer=runtime.stream_writer,
620
+ )
621
+ tool_runtimes.append(tool_runtime)
622
+
623
+ # Inject tool arguments (including runtime)
624
+
625
+ injected_tool_calls = []
568
626
  input_types = [input_type] * len(tool_calls)
569
- inputs = [input] * len(tool_calls)
570
- runtimes = [runtime] * len(tool_calls)
627
+ for call, tool_runtime in zip(tool_calls, tool_runtimes, strict=False):
628
+ injected_call = self._inject_tool_args(call, tool_runtime)
629
+ injected_tool_calls.append(injected_call)
571
630
  with get_executor_for_config(config) as executor:
572
- outputs = [
573
- *executor.map(self._run_one, tool_calls, input_types, config_list, inputs, runtimes)
574
- ]
631
+ outputs = list(
632
+ executor.map(self._run_one, injected_tool_calls, input_types, tool_runtimes)
633
+ )
575
634
 
576
635
  return self._combine_tool_outputs(outputs, input_type)
577
636
 
@@ -579,20 +638,32 @@ class _ToolNode(RunnableCallable):
579
638
  self,
580
639
  input: list[AnyMessage] | dict[str, Any] | BaseModel,
581
640
  config: RunnableConfig,
582
- *,
583
- store: Optional[BaseStore], # noqa: UP045
641
+ runtime: Runtime,
584
642
  ) -> Any:
585
- try:
586
- runtime = get_runtime()
587
- except RuntimeError:
588
- # Running outside of LangGraph runtime context (e.g., unit tests)
589
- runtime = None
590
-
591
643
  tool_calls, input_type = self._parse_input(input)
592
- tool_calls = [self._inject_tool_args(call, input, store) for call in tool_calls]
593
- outputs = await asyncio.gather(
594
- *(self._arun_one(call, input_type, config, input, runtime) for call in tool_calls)
595
- )
644
+ config_list = get_config_list(config, len(tool_calls))
645
+
646
+ # Construct ToolRuntime instances at the top level for each tool call
647
+ tool_runtimes = []
648
+ for call, cfg in zip(tool_calls, config_list, strict=False):
649
+ state = self._extract_state(input)
650
+ tool_runtime = ToolRuntime(
651
+ state=state,
652
+ tool_call_id=call["id"],
653
+ config=cfg,
654
+ context=runtime.context,
655
+ store=runtime.store,
656
+ stream_writer=runtime.stream_writer,
657
+ )
658
+ tool_runtimes.append(tool_runtime)
659
+
660
+ injected_tool_calls = []
661
+ coros = []
662
+ for call, tool_runtime in zip(tool_calls, tool_runtimes, strict=False):
663
+ injected_call = self._inject_tool_args(call, tool_runtime)
664
+ injected_tool_calls.append(injected_call)
665
+ coros.append(self._arun_one(injected_call, input_type, tool_runtime))
666
+ outputs = await asyncio.gather(*coros)
596
667
 
597
668
  return self._combine_tool_outputs(outputs, input_type)
598
669
 
@@ -659,6 +730,15 @@ class _ToolNode(RunnableCallable):
659
730
  """
660
731
  call = request.tool_call
661
732
  tool = request.tool
733
+
734
+ # Validate tool exists when we actually need to execute it
735
+ if tool is None:
736
+ if invalid_tool_message := self._validate_tool_call(call):
737
+ return invalid_tool_message
738
+ # This should never happen if validation works correctly
739
+ msg = f"Tool {call['name']} is not registered with ToolNode"
740
+ raise TypeError(msg)
741
+
662
742
  call_args = {**call, "type": "tool_call"}
663
743
 
664
744
  try:
@@ -723,38 +803,32 @@ class _ToolNode(RunnableCallable):
723
803
  self,
724
804
  call: ToolCall,
725
805
  input_type: Literal["list", "dict", "tool_calls"],
726
- config: RunnableConfig,
727
- input: list[AnyMessage] | dict[str, Any] | BaseModel,
728
- runtime: Any,
806
+ tool_runtime: ToolRuntime,
729
807
  ) -> ToolMessage | Command:
730
808
  """Execute single tool call with wrap_tool_call wrapper if configured.
731
809
 
732
810
  Args:
733
811
  call: Tool call dict.
734
812
  input_type: Input format.
735
- config: Runnable configuration.
736
- input: Agent state.
737
- runtime: LangGraph runtime or None.
813
+ tool_runtime: Tool runtime.
738
814
 
739
815
  Returns:
740
816
  ToolMessage or Command.
741
817
  """
742
- if invalid_tool_message := self._validate_tool_call(call):
743
- return invalid_tool_message
744
-
745
- tool = self.tools_by_name[call["name"]]
746
-
747
- # Extract state from ToolCallWithContext if present
748
- state = self._extract_state(input)
818
+ # Validation is deferred to _execute_tool_sync to allow interceptors
819
+ # to short-circuit requests for unregistered tools
820
+ tool = self.tools_by_name.get(call["name"])
749
821
 
750
822
  # Create the tool request with state and runtime
751
823
  tool_request = ToolCallRequest(
752
824
  tool_call=call,
753
825
  tool=tool,
754
- state=state,
755
- runtime=runtime,
826
+ state=tool_runtime.state,
827
+ runtime=tool_runtime,
756
828
  )
757
829
 
830
+ config = tool_runtime.config
831
+
758
832
  if self._wrap_tool_call is None:
759
833
  # No wrapper - execute directly
760
834
  return self._execute_tool_sync(tool_request, input_type, config)
@@ -801,6 +875,15 @@ class _ToolNode(RunnableCallable):
801
875
  """
802
876
  call = request.tool_call
803
877
  tool = request.tool
878
+
879
+ # Validate tool exists when we actually need to execute it
880
+ if tool is None:
881
+ if invalid_tool_message := self._validate_tool_call(call):
882
+ return invalid_tool_message
883
+ # This should never happen if validation works correctly
884
+ msg = f"Tool {call['name']} is not registered with ToolNode"
885
+ raise TypeError(msg)
886
+
804
887
  call_args = {**call, "type": "tool_call"}
805
888
 
806
889
  try:
@@ -865,38 +948,32 @@ class _ToolNode(RunnableCallable):
865
948
  self,
866
949
  call: ToolCall,
867
950
  input_type: Literal["list", "dict", "tool_calls"],
868
- config: RunnableConfig,
869
- input: list[AnyMessage] | dict[str, Any] | BaseModel,
870
- runtime: Any,
951
+ tool_runtime: ToolRuntime,
871
952
  ) -> ToolMessage | Command:
872
953
  """Execute single tool call asynchronously with awrap_tool_call wrapper if configured.
873
954
 
874
955
  Args:
875
956
  call: Tool call dict.
876
957
  input_type: Input format.
877
- config: Runnable configuration.
878
- input: Agent state.
879
- runtime: LangGraph runtime or None.
958
+ tool_runtime: Tool runtime.
880
959
 
881
960
  Returns:
882
961
  ToolMessage or Command.
883
962
  """
884
- if invalid_tool_message := self._validate_tool_call(call):
885
- return invalid_tool_message
886
-
887
- tool = self.tools_by_name[call["name"]]
888
-
889
- # Extract state from ToolCallWithContext if present
890
- state = self._extract_state(input)
963
+ # Validation is deferred to _execute_tool_async to allow interceptors
964
+ # to short-circuit requests for unregistered tools
965
+ tool = self.tools_by_name.get(call["name"])
891
966
 
892
967
  # Create the tool request with state and runtime
893
968
  tool_request = ToolCallRequest(
894
969
  tool_call=call,
895
970
  tool=tool,
896
- state=state,
897
- runtime=runtime,
971
+ state=tool_runtime.state,
972
+ runtime=tool_runtime,
898
973
  )
899
974
 
975
+ config = tool_runtime.config
976
+
900
977
  if self._awrap_tool_call is None and self._wrap_tool_call is None:
901
978
  # No wrapper - execute directly
902
979
  return await self._execute_tool_async(tool_request, input_type, config)
@@ -999,15 +1076,16 @@ class _ToolNode(RunnableCallable):
999
1076
  def _inject_state(
1000
1077
  self,
1001
1078
  tool_call: ToolCall,
1002
- input: list[AnyMessage] | dict[str, Any] | BaseModel,
1079
+ state: list[AnyMessage] | dict[str, Any] | BaseModel,
1003
1080
  ) -> ToolCall:
1004
1081
  state_args = self._tool_to_state_args[tool_call["name"]]
1005
- if state_args and isinstance(input, list):
1082
+
1083
+ if state_args and isinstance(state, list):
1006
1084
  required_fields = list(state_args.values())
1007
1085
  if (
1008
1086
  len(required_fields) == 1 and required_fields[0] == self._messages_key
1009
1087
  ) or required_fields[0] is None:
1010
- input = {self._messages_key: input}
1088
+ state = {self._messages_key: state}
1011
1089
  else:
1012
1090
  err_msg = (
1013
1091
  f"Invalid input to ToolNode. Tool {tool_call['name']} requires "
@@ -1018,12 +1096,6 @@ class _ToolNode(RunnableCallable):
1018
1096
  err_msg += f" State should contain fields {required_fields_str}."
1019
1097
  raise ValueError(err_msg)
1020
1098
 
1021
- # Extract state from ToolCallWithContext if present
1022
- if isinstance(input, dict) and input.get("__type") == "tool_call_with_context":
1023
- state = input["state"]
1024
- else:
1025
- state = input
1026
-
1027
1099
  if isinstance(state, dict):
1028
1100
  tool_state_args = {
1029
1101
  tool_arg: state[state_field] if state_field else state
@@ -1059,19 +1131,38 @@ class _ToolNode(RunnableCallable):
1059
1131
  }
1060
1132
  return tool_call
1061
1133
 
1134
+ def _inject_runtime(self, tool_call: ToolCall, tool_runtime: ToolRuntime) -> ToolCall:
1135
+ """Inject ToolRuntime into tool call arguments.
1136
+
1137
+ Args:
1138
+ tool_call: The tool call to inject runtime into.
1139
+ tool_runtime: The ToolRuntime instance to inject.
1140
+
1141
+ Returns:
1142
+ The tool call with runtime injected if needed.
1143
+ """
1144
+ runtime_arg = self._tool_to_runtime_arg.get(tool_call["name"])
1145
+ if not runtime_arg:
1146
+ return tool_call
1147
+
1148
+ tool_call["args"] = {
1149
+ **tool_call["args"],
1150
+ runtime_arg: tool_runtime,
1151
+ }
1152
+ return tool_call
1153
+
1062
1154
  def _inject_tool_args(
1063
1155
  self,
1064
1156
  tool_call: ToolCall,
1065
- input: list[AnyMessage] | dict[str, Any] | BaseModel,
1066
- store: BaseStore | None,
1157
+ tool_runtime: ToolRuntime,
1067
1158
  ) -> ToolCall:
1068
- """Inject graph state and store into tool call arguments.
1159
+ """Inject graph state, store, and runtime into tool call arguments.
1069
1160
 
1070
1161
  This is an internal method that enables tools to access graph context that
1071
1162
  should not be controlled by the model. Tools can declare dependencies on graph
1072
- state or persistent storage using InjectedState and InjectedStore annotations.
1073
- This method automatically identifies these dependencies and injects the
1074
- appropriate values.
1163
+ state, persistent storage, or runtime context using InjectedState, InjectedStore,
1164
+ and ToolRuntime annotations. This method automatically identifies these
1165
+ dependencies and injects the appropriate values.
1075
1166
 
1076
1167
  The injection process preserves the original tool call structure while adding
1077
1168
  the necessary context arguments. This allows tools to be both model-callable
@@ -1080,10 +1171,8 @@ class _ToolNode(RunnableCallable):
1080
1171
  Args:
1081
1172
  tool_call: The tool call dictionary to augment with injected arguments.
1082
1173
  Must contain 'name', 'args', 'id', and 'type' fields.
1083
- input: The current graph state to inject into tools requiring state access.
1084
- Can be a message list, state dictionary, or BaseModel instance.
1085
- store: The persistent store instance to inject into tools requiring storage.
1086
- Will be None if no store is configured for the graph.
1174
+ tool_runtime: The ToolRuntime instance containing all runtime context
1175
+ (state, config, store, context, stream_writer) to inject into tools.
1087
1176
 
1088
1177
  Returns:
1089
1178
  A new ToolCall dictionary with the same structure as the input but with
@@ -1101,8 +1190,9 @@ class _ToolNode(RunnableCallable):
1101
1190
  return tool_call
1102
1191
 
1103
1192
  tool_call_copy: ToolCall = copy(tool_call)
1104
- tool_call_with_state = self._inject_state(tool_call_copy, input)
1105
- return self._inject_store(tool_call_with_state, store)
1193
+ tool_call_with_state = self._inject_state(tool_call_copy, tool_runtime.state)
1194
+ tool_call_with_store = self._inject_store(tool_call_with_state, tool_runtime.store)
1195
+ return self._inject_runtime(tool_call_with_store, tool_runtime)
1106
1196
 
1107
1197
  def _validate_tool_command(
1108
1198
  self,
@@ -1258,11 +1348,71 @@ def tools_condition(
1258
1348
  return "__end__"
1259
1349
 
1260
1350
 
1351
+ @dataclass
1352
+ class ToolRuntime(InjectedToolArg, Generic[ContextT, StateT]):
1353
+ """Runtime context automatically injected into tools.
1354
+
1355
+ When a tool function has a parameter named 'tool_runtime' with type hint
1356
+ 'ToolRuntime', the tool execution system will automatically inject
1357
+ an instance containing:
1358
+
1359
+ - state: The current graph state
1360
+ - tool_call_id: The ID of the current tool call
1361
+ - config: RunnableConfig for the current execution
1362
+ - context: Runtime context (from langgraph Runtime)
1363
+ - store: BaseStore instance for persistent storage (from langgraph Runtime)
1364
+ - stream_writer: StreamWriter for streaming output (from langgraph Runtime)
1365
+
1366
+ No `Annotated` wrapper is needed - just use `runtime: ToolRuntime`
1367
+ as a parameter.
1368
+
1369
+ Example:
1370
+ ```python
1371
+ from langchain_core.tools import tool
1372
+ from langchain.tools import ToolRuntime
1373
+
1374
+ @tool
1375
+ def my_tool(x: int, runtime: ToolRuntime) -> str:
1376
+ \"\"\"Tool that accesses runtime context.\"\"\"
1377
+ # Access state
1378
+ messages = tool_runtime.state["messages"]
1379
+
1380
+ # Access tool_call_id
1381
+ print(f"Tool call ID: {tool_runtime.tool_call_id}")
1382
+
1383
+ # Access config
1384
+ print(f"Run ID: {tool_runtime.config.get('run_id')}")
1385
+
1386
+ # Access runtime context
1387
+ user_id = tool_runtime.context.get("user_id")
1388
+
1389
+ # Access store
1390
+ tool_runtime.store.put(("metrics",), "count", 1)
1391
+
1392
+ # Stream output
1393
+ tool_runtime.stream_writer.write("Processing...")
1394
+
1395
+ return f"Processed {x}"
1396
+ ```
1397
+
1398
+ Note:
1399
+ This is a marker class used for type checking and detection.
1400
+ The actual runtime object will be constructed during tool execution.
1401
+ """
1402
+
1403
+ state: StateT
1404
+ context: ContextT
1405
+ config: RunnableConfig
1406
+ stream_writer: StreamWriter
1407
+ tool_call_id: str | None
1408
+ store: BaseStore | None
1409
+
1410
+
1261
1411
  class InjectedState(InjectedToolArg):
1262
1412
  """Annotation for injecting graph state into tool arguments.
1263
1413
 
1264
1414
  This annotation enables tools to access graph state without exposing state
1265
- management details to the language model. Tools annotated with InjectedState
1415
+ management details to the language model. Tools annotated with `InjectedState`
1266
1416
  receive state data automatically during execution while remaining invisible
1267
1417
  to the model's tool-calling interface.
1268
1418
 
@@ -1320,9 +1470,9 @@ class InjectedState(InjectedToolArg):
1320
1470
  ```
1321
1471
 
1322
1472
  Note:
1323
- - InjectedState arguments are automatically excluded from tool schemas
1473
+ - `InjectedState` arguments are automatically excluded from tool schemas
1324
1474
  presented to language models
1325
- - ToolNode handles the injection process during execution
1475
+ - `ToolNode` handles the injection process during execution
1326
1476
  - Tools can mix regular arguments (controlled by the model) with injected
1327
1477
  arguments (controlled by the system)
1328
1478
  - State injection occurs after the model generates tool calls but before
@@ -1330,7 +1480,7 @@ class InjectedState(InjectedToolArg):
1330
1480
  """
1331
1481
 
1332
1482
  def __init__(self, field: str | None = None) -> None:
1333
- """Initialize the InjectedState annotation."""
1483
+ """Initialize the `InjectedState` annotation."""
1334
1484
  self.field = field
1335
1485
 
1336
1486
 
@@ -1375,7 +1525,7 @@ class InjectedStore(InjectedToolArg):
1375
1525
  return result.value if result else "Not found"
1376
1526
  ```
1377
1527
 
1378
- Usage with ToolNode and graph compilation:
1528
+ Usage with `ToolNode` and graph compilation:
1379
1529
 
1380
1530
  ```python
1381
1531
  from langgraph.graph import StateGraph
@@ -1400,16 +1550,19 @@ class InjectedStore(InjectedToolArg):
1400
1550
  ```
1401
1551
 
1402
1552
  Note:
1403
- - InjectedStore arguments are automatically excluded from tool schemas
1553
+ - `InjectedStore` arguments are automatically excluded from tool schemas
1404
1554
  presented to language models
1405
- - The store instance is automatically injected by ToolNode during execution
1555
+ - The store instance is automatically injected by `ToolNode` during execution
1406
1556
  - Tools can access namespaced storage using the store's get/put methods
1407
1557
  - Store injection requires the graph to be compiled with a store instance
1408
1558
  - Multiple tools can share the same store instance for data consistency
1409
1559
  """
1410
1560
 
1411
1561
 
1412
- def _is_injection(type_arg: Any, injection_type: type[InjectedState | InjectedStore]) -> bool:
1562
+ def _is_injection(
1563
+ type_arg: Any,
1564
+ injection_type: type[InjectedState | InjectedStore | ToolRuntime],
1565
+ ) -> bool:
1413
1566
  """Check if a type argument represents an injection annotation.
1414
1567
 
1415
1568
  This utility function determines whether a type annotation indicates that
@@ -1503,3 +1656,44 @@ def _get_store_arg(tool: BaseTool) -> str | None:
1503
1656
  return name
1504
1657
 
1505
1658
  return None
1659
+
1660
+
1661
+ def _get_runtime_arg(tool: BaseTool) -> str | None:
1662
+ """Extract runtime injection argument from tool annotations.
1663
+
1664
+ This function analyzes a tool's input schema to identify the argument that
1665
+ should be injected with the ToolRuntime instance. Only one runtime argument
1666
+ is supported per tool.
1667
+
1668
+ Args:
1669
+ tool: The tool to analyze for runtime injection requirements.
1670
+
1671
+ Returns:
1672
+ The name of the argument that should receive the runtime injection, or None
1673
+ if no runtime injection is required.
1674
+
1675
+ Raises:
1676
+ ValueError: If a tool argument has multiple ToolRuntime annotations.
1677
+ """
1678
+ full_schema = tool.get_input_schema()
1679
+ for name, type_ in get_all_basemodel_annotations(full_schema).items():
1680
+ # Check if the parameter name is "runtime" (regardless of type)
1681
+ if name == "runtime":
1682
+ return name
1683
+ # Check if the type itself is ToolRuntime (direct usage)
1684
+ if _is_injection(type_, ToolRuntime):
1685
+ return name
1686
+ # Check if ToolRuntime is in Annotated args
1687
+ injections = [
1688
+ type_arg for type_arg in get_args(type_) if _is_injection(type_arg, ToolRuntime)
1689
+ ]
1690
+ if len(injections) > 1:
1691
+ msg = (
1692
+ "A tool argument should not be annotated with ToolRuntime more than "
1693
+ f"once. Received arg {name} with annotations {injections}."
1694
+ )
1695
+ raise ValueError(msg)
1696
+ if len(injections) == 1:
1697
+ return name
1698
+
1699
+ return None