langchain 1.0.0a11__py3-none-any.whl → 1.0.0a13__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.

Files changed (34) hide show
  1. langchain/__init__.py +1 -1
  2. langchain/agents/factory.py +511 -180
  3. langchain/agents/middleware/__init__.py +9 -3
  4. langchain/agents/middleware/context_editing.py +15 -14
  5. langchain/agents/middleware/human_in_the_loop.py +213 -170
  6. langchain/agents/middleware/model_call_limit.py +2 -2
  7. langchain/agents/middleware/model_fallback.py +46 -36
  8. langchain/agents/middleware/pii.py +19 -19
  9. langchain/agents/middleware/planning.py +16 -11
  10. langchain/agents/middleware/prompt_caching.py +14 -11
  11. langchain/agents/middleware/summarization.py +1 -1
  12. langchain/agents/middleware/tool_call_limit.py +5 -5
  13. langchain/agents/middleware/tool_emulator.py +200 -0
  14. langchain/agents/middleware/tool_selection.py +25 -21
  15. langchain/agents/middleware/types.py +484 -225
  16. langchain/chat_models/base.py +85 -90
  17. langchain/embeddings/base.py +20 -20
  18. langchain/embeddings/cache.py +21 -21
  19. langchain/messages/__init__.py +2 -0
  20. langchain/storage/encoder_backed.py +22 -23
  21. langchain/tools/tool_node.py +388 -80
  22. {langchain-1.0.0a11.dist-info → langchain-1.0.0a13.dist-info}/METADATA +8 -5
  23. langchain-1.0.0a13.dist-info/RECORD +36 -0
  24. langchain/_internal/__init__.py +0 -0
  25. langchain/_internal/_documents.py +0 -35
  26. langchain/_internal/_lazy_import.py +0 -35
  27. langchain/_internal/_prompts.py +0 -158
  28. langchain/_internal/_typing.py +0 -70
  29. langchain/_internal/_utils.py +0 -7
  30. langchain/agents/_internal/__init__.py +0 -1
  31. langchain/agents/_internal/_typing.py +0 -13
  32. langchain-1.0.0a11.dist-info/RECORD +0 -43
  33. {langchain-1.0.0a11.dist-info → langchain-1.0.0a13.dist-info}/WHEEL +0 -0
  34. {langchain-1.0.0a11.dist-info → langchain-1.0.0a13.dist-info}/licenses/LICENSE +0 -0
@@ -4,7 +4,10 @@ from .context_editing import (
4
4
  ClearToolUsesEdit,
5
5
  ContextEditingMiddleware,
6
6
  )
7
- from .human_in_the_loop import HumanInTheLoopMiddleware
7
+ from .human_in_the_loop import (
8
+ HumanInTheLoopMiddleware,
9
+ InterruptOnConfig,
10
+ )
8
11
  from .model_call_limit import ModelCallLimitMiddleware
9
12
  from .model_fallback import ModelFallbackMiddleware
10
13
  from .pii import PIIDetectionError, PIIMiddleware
@@ -12,6 +15,7 @@ from .planning import PlanningMiddleware
12
15
  from .prompt_caching import AnthropicPromptCachingMiddleware
13
16
  from .summarization import SummarizationMiddleware
14
17
  from .tool_call_limit import ToolCallLimitMiddleware
18
+ from .tool_emulator import LLMToolEmulator
15
19
  from .tool_selection import LLMToolSelectorMiddleware
16
20
  from .types import (
17
21
  AgentMiddleware,
@@ -23,7 +27,7 @@ from .types import (
23
27
  before_model,
24
28
  dynamic_prompt,
25
29
  hook_config,
26
- modify_model_request,
30
+ wrap_model_call,
27
31
  )
28
32
 
29
33
  __all__ = [
@@ -34,6 +38,8 @@ __all__ = [
34
38
  "ClearToolUsesEdit",
35
39
  "ContextEditingMiddleware",
36
40
  "HumanInTheLoopMiddleware",
41
+ "InterruptOnConfig",
42
+ "LLMToolEmulator",
37
43
  "LLMToolSelectorMiddleware",
38
44
  "ModelCallLimitMiddleware",
39
45
  "ModelFallbackMiddleware",
@@ -49,5 +55,5 @@ __all__ = [
49
55
  "before_model",
50
56
  "dynamic_prompt",
51
57
  "hook_config",
52
- "modify_model_request",
58
+ "wrap_model_call",
53
59
  ]
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  from collections.abc import Callable, Iterable, Sequence
12
12
  from dataclasses import dataclass
13
- from typing import TYPE_CHECKING, Literal
13
+ from typing import Literal
14
14
 
15
15
  from langchain_core.messages import (
16
16
  AIMessage,
@@ -22,10 +22,12 @@ from langchain_core.messages import (
22
22
  from langchain_core.messages.utils import count_tokens_approximately
23
23
  from typing_extensions import Protocol
24
24
 
25
- from langchain.agents.middleware.types import AgentMiddleware, AgentState, ModelRequest
26
-
27
- if TYPE_CHECKING:
28
- from langgraph.runtime import Runtime
25
+ from langchain.agents.middleware.types import (
26
+ AgentMiddleware,
27
+ ModelCallResult,
28
+ ModelRequest,
29
+ ModelResponse,
30
+ )
29
31
 
30
32
  DEFAULT_TOOL_PLACEHOLDER = "[cleared]"
31
33
 
@@ -183,8 +185,8 @@ class ContextEditingMiddleware(AgentMiddleware):
183
185
  """Middleware that automatically prunes tool results to manage context size.
184
186
 
185
187
  The middleware applies a sequence of edits when the total input token count
186
- exceeds configured thresholds. Currently the ``ClearToolUsesEdit`` strategy is
187
- supported, aligning with Anthropic's ``clear_tool_uses_20250919`` behaviour.
188
+ exceeds configured thresholds. Currently the `ClearToolUsesEdit` strategy is
189
+ supported, aligning with Anthropic's `clear_tool_uses_20250919` behaviour.
188
190
  """
189
191
 
190
192
  edits: list[ContextEdit]
@@ -209,15 +211,14 @@ class ContextEditingMiddleware(AgentMiddleware):
209
211
  self.edits = list(edits or (ClearToolUsesEdit(),))
210
212
  self.token_count_method = token_count_method
211
213
 
212
- def modify_model_request(
214
+ def wrap_model_call(
213
215
  self,
214
216
  request: ModelRequest,
215
- state: AgentState, # noqa: ARG002
216
- runtime: Runtime, # noqa: ARG002
217
- ) -> ModelRequest:
218
- """Modify the model request by applying context edits before invocation."""
217
+ handler: Callable[[ModelRequest], ModelResponse],
218
+ ) -> ModelCallResult:
219
+ """Apply context edits before invoking the model via handler."""
219
220
  if not request.messages:
220
- return request
221
+ return handler(request)
221
222
 
222
223
  if self.token_count_method == "approximate": # noqa: S105
223
224
 
@@ -236,7 +237,7 @@ class ContextEditingMiddleware(AgentMiddleware):
236
237
  for edit in self.edits:
237
238
  edit.apply(request.messages, count_tokens=count_tokens)
238
239
 
239
- return request
240
+ return handler(request)
240
241
 
241
242
 
242
243
  __all__ = [
@@ -10,89 +10,93 @@ from typing_extensions import NotRequired, TypedDict
10
10
  from langchain.agents.middleware.types import AgentMiddleware, AgentState
11
11
 
12
12
 
13
- class HumanInTheLoopConfig(TypedDict):
14
- """Configuration that defines what actions are allowed for a human interrupt.
13
+ class Action(TypedDict):
14
+ """Represents an action with a name and arguments."""
15
15
 
16
- This controls the available interaction options when the graph is paused for human input.
17
- """
16
+ name: str
17
+ """The type or name of action being requested (e.g., "add_numbers")."""
18
18
 
19
- allow_accept: NotRequired[bool]
20
- """Whether the human can approve the current action without changes."""
21
- allow_edit: NotRequired[bool]
22
- """Whether the human can approve the current action with edited content."""
23
- allow_respond: NotRequired[bool]
24
- """Whether the human can reject the current action with feedback."""
19
+ arguments: dict[str, Any]
20
+ """Key-value pairs of arguments needed for the action (e.g., {"a": 1, "b": 2})."""
25
21
 
26
22
 
27
23
  class ActionRequest(TypedDict):
28
- """Represents a request with a name and arguments."""
24
+ """Represents an action request with a name, arguments, and description."""
29
25
 
30
- action: str
31
- """The type or name of action being requested (e.g., "add_numbers")."""
32
- args: dict
26
+ name: str
27
+ """The name of the action being requested."""
28
+
29
+ arguments: dict[str, Any]
33
30
  """Key-value pairs of arguments needed for the action (e.g., {"a": 1, "b": 2})."""
34
31
 
32
+ description: NotRequired[str]
33
+ """The description of the action to be reviewed."""
35
34
 
36
- class HumanInTheLoopRequest(TypedDict):
37
- """Represents an interrupt triggered by the graph that requires human intervention.
38
35
 
39
- Example:
40
- ```python
41
- # Extract a tool call from the state and create an interrupt request
42
- request = HumanInterrupt(
43
- action_request=ActionRequest(
44
- action="run_command", # The action being requested
45
- args={"command": "ls", "args": ["-l"]}, # Arguments for the action
46
- ),
47
- config=HumanInTheLoopConfig(
48
- allow_accept=True, # Allow approval
49
- allow_respond=True, # Allow rejection with feedback
50
- allow_edit=False, # Don't allow approval with edits
51
- ),
52
- description="Please review the command before execution",
53
- )
54
- # Send the interrupt request and get the response
55
- response = interrupt([request])[0]
56
- ```
57
- """
36
+ DecisionType = Literal["approve", "edit", "reject"]
58
37
 
59
- action_request: ActionRequest
60
- """The specific action being requested from the human."""
61
- config: HumanInTheLoopConfig
62
- """Configuration defining what response types are allowed."""
63
- description: str | None
64
- """Optional detailed description of what input is needed."""
65
38
 
39
+ class ReviewConfig(TypedDict):
40
+ """Policy for reviewing a HITL request."""
66
41
 
67
- class AcceptPayload(TypedDict):
68
- """Response when a human approves the action."""
42
+ action_name: str
43
+ """Name of the action associated with this review configuration."""
69
44
 
70
- type: Literal["accept"]
71
- """The type of response when a human approves the action."""
45
+ allowed_decisions: list[DecisionType]
46
+ """The decisions that are allowed for this request."""
72
47
 
48
+ arguments_schema: NotRequired[dict[str, Any]]
49
+ """JSON schema for the arguments associated with the action, if edits are allowed."""
73
50
 
74
- class ResponsePayload(TypedDict):
75
- """Response when a human rejects the action."""
76
51
 
77
- type: Literal["response"]
78
- """The type of response when a human rejects the action."""
52
+ class HITLRequest(TypedDict):
53
+ """Request for human feedback on a sequence of actions requested by a model."""
54
+
55
+ action_requests: list[ActionRequest]
56
+ """A list of agent actions for human review."""
79
57
 
80
- args: NotRequired[str]
81
- """The message to be sent to the model explaining why the action was rejected."""
58
+ review_configs: list[ReviewConfig]
59
+ """Review configuration for all possible actions."""
82
60
 
83
61
 
84
- class EditPayload(TypedDict):
62
+ class ApproveDecision(TypedDict):
63
+ """Response when a human approves the action."""
64
+
65
+ type: Literal["approve"]
66
+ """The type of response when a human approves the action."""
67
+
68
+
69
+ class EditDecision(TypedDict):
85
70
  """Response when a human edits the action."""
86
71
 
87
72
  type: Literal["edit"]
88
73
  """The type of response when a human edits the action."""
89
74
 
90
- args: ActionRequest
91
- """The action request with the edited content."""
75
+ edited_action: Action
76
+ """Edited action for the agent to perform.
77
+
78
+ Ex: for a tool call, a human reviewer can edit the tool name and args.
79
+ """
80
+
81
+
82
+ class RejectDecision(TypedDict):
83
+ """Response when a human rejects the action."""
84
+
85
+ type: Literal["reject"]
86
+ """The type of response when a human rejects the action."""
87
+
88
+ message: NotRequired[str]
89
+ """The message sent to the model explaining why the action was rejected."""
90
+
91
+
92
+ Decision = ApproveDecision | EditDecision | RejectDecision
92
93
 
93
94
 
94
- HumanInTheLoopResponse = AcceptPayload | ResponsePayload | EditPayload
95
- """Aggregated response type for all possible human in the loop responses."""
95
+ class HITLResponse(TypedDict):
96
+ """Response payload for a HITLRequest."""
97
+
98
+ decisions: list[Decision]
99
+ """The decisions made by the human."""
96
100
 
97
101
 
98
102
  class _DescriptionFactory(Protocol):
@@ -103,49 +107,51 @@ class _DescriptionFactory(Protocol):
103
107
  ...
104
108
 
105
109
 
106
- class ToolConfig(TypedDict):
107
- """Configuration for a tool requiring human in the loop."""
110
+ class InterruptOnConfig(TypedDict):
111
+ """Configuration for an action requiring human in the loop.
112
+
113
+ This is the configuration format used in the `HumanInTheLoopMiddleware.__init__` method.
114
+ """
115
+
116
+ allowed_decisions: list[DecisionType]
117
+ """The decisions that are allowed for this action."""
108
118
 
109
- allow_accept: NotRequired[bool]
110
- """Whether the human can approve the current action without changes."""
111
- allow_edit: NotRequired[bool]
112
- """Whether the human can approve the current action with edited content."""
113
- allow_respond: NotRequired[bool]
114
- """Whether the human can reject the current action with feedback."""
115
119
  description: NotRequired[str | _DescriptionFactory]
116
120
  """The description attached to the request for human input.
117
121
 
118
122
  Can be either:
119
123
  - A static string describing the approval request
120
124
  - A callable that dynamically generates the description based on agent state,
121
- runtime, and tool call information
125
+ runtime, and tool call information
122
126
 
123
127
  Example:
124
- .. code-block:: python
128
+ ```python
129
+ # Static string description
130
+ config = ToolConfig(
131
+ allowed_decisions=["approve", "reject"],
132
+ description="Please review this tool execution"
133
+ )
125
134
 
126
- # Static string description
127
- config = ToolConfig(
128
- allow_accept=True,
129
- description="Please review this tool execution"
135
+ # Dynamic callable description
136
+ def format_tool_description(
137
+ tool_call: ToolCall,
138
+ state: AgentState,
139
+ runtime: Runtime
140
+ ) -> str:
141
+ import json
142
+ return (
143
+ f"Tool: {tool_call['name']}\\n"
144
+ f"Arguments:\\n{json.dumps(tool_call['args'], indent=2)}"
130
145
  )
131
146
 
132
- # Dynamic callable description
133
- def format_tool_description(
134
- tool_call: ToolCall,
135
- state: AgentState,
136
- runtime: Runtime
137
- ) -> str:
138
- import json
139
- return (
140
- f"Tool: {tool_call['name']}\\n"
141
- f"Arguments:\\n{json.dumps(tool_call['args'], indent=2)}"
142
- )
143
-
144
- config = ToolConfig(
145
- allow_accept=True,
146
- description=format_tool_description
147
- )
147
+ config = InterruptOnConfig(
148
+ allowed_decisions=["approve", "edit", "reject"],
149
+ description=format_tool_description
150
+ )
151
+ ```
148
152
  """
153
+ arguments_schema: NotRequired[dict[str, Any]]
154
+ """JSON schema for the arguments associated with the action, if edits are allowed."""
149
155
 
150
156
 
151
157
  class HumanInTheLoopMiddleware(AgentMiddleware):
@@ -153,7 +159,7 @@ class HumanInTheLoopMiddleware(AgentMiddleware):
153
159
 
154
160
  def __init__(
155
161
  self,
156
- interrupt_on: dict[str, bool | ToolConfig],
162
+ interrupt_on: dict[str, bool | InterruptOnConfig],
157
163
  *,
158
164
  description_prefix: str = "Tool execution requires approval",
159
165
  ) -> None:
@@ -163,32 +169,106 @@ class HumanInTheLoopMiddleware(AgentMiddleware):
163
169
  interrupt_on: Mapping of tool name to allowed actions.
164
170
  If a tool doesn't have an entry, it's auto-approved by default.
165
171
 
166
- * ``True`` indicates all actions are allowed: accept, edit, and respond.
167
- * ``False`` indicates that the tool is auto-approved.
168
- * ``ToolConfig`` indicates the specific actions allowed for this tool.
169
- The ToolConfig can include a ``description`` field (str or callable) for
170
- custom formatting of the interrupt description.
172
+ * `True` indicates all decisions are allowed: approve, edit, and reject.
173
+ * `False` indicates that the tool is auto-approved.
174
+ * `InterruptOnConfig` indicates the specific decisions allowed for this tool.
175
+ The InterruptOnConfig can include a `description` field (str or callable) for
176
+ custom formatting of the interrupt description.
171
177
  description_prefix: The prefix to use when constructing action requests.
172
178
  This is used to provide context about the tool call and the action being requested.
173
- Not used if a tool has a ``description`` in its ToolConfig.
179
+ Not used if a tool has a `description` in its InterruptOnConfig.
174
180
  """
175
181
  super().__init__()
176
- resolved_tool_configs: dict[str, ToolConfig] = {}
182
+ resolved_configs: dict[str, InterruptOnConfig] = {}
177
183
  for tool_name, tool_config in interrupt_on.items():
178
184
  if isinstance(tool_config, bool):
179
185
  if tool_config is True:
180
- resolved_tool_configs[tool_name] = ToolConfig(
181
- allow_accept=True,
182
- allow_edit=True,
183
- allow_respond=True,
186
+ resolved_configs[tool_name] = InterruptOnConfig(
187
+ allowed_decisions=["approve", "edit", "reject"]
184
188
  )
185
- elif any(
186
- tool_config.get(x, False) for x in ["allow_accept", "allow_edit", "allow_respond"]
187
- ):
188
- resolved_tool_configs[tool_name] = tool_config
189
- self.interrupt_on = resolved_tool_configs
189
+ elif tool_config.get("allowed_decisions"):
190
+ resolved_configs[tool_name] = tool_config
191
+ self.interrupt_on = resolved_configs
190
192
  self.description_prefix = description_prefix
191
193
 
194
+ def _create_action_and_config(
195
+ self,
196
+ tool_call: ToolCall,
197
+ config: InterruptOnConfig,
198
+ state: AgentState,
199
+ runtime: Runtime,
200
+ ) -> tuple[ActionRequest, ReviewConfig]:
201
+ """Create an ActionRequest and ReviewConfig for a tool call."""
202
+ tool_name = tool_call["name"]
203
+ tool_args = tool_call["args"]
204
+
205
+ # Generate description using the description field (str or callable)
206
+ description_value = config.get("description")
207
+ if callable(description_value):
208
+ description = description_value(tool_call, state, runtime)
209
+ elif description_value is not None:
210
+ description = description_value
211
+ else:
212
+ description = f"{self.description_prefix}\n\nTool: {tool_name}\nArgs: {tool_args}"
213
+
214
+ # Create ActionRequest with description
215
+ action_request = ActionRequest(
216
+ name=tool_name,
217
+ arguments=tool_args,
218
+ description=description,
219
+ )
220
+
221
+ # Create ReviewConfig
222
+ # eventually can get tool information and populate arguments_schema from there
223
+ review_config = ReviewConfig(
224
+ action_name=tool_name,
225
+ allowed_decisions=config["allowed_decisions"],
226
+ )
227
+
228
+ return action_request, review_config
229
+
230
+ def _process_decision(
231
+ self,
232
+ decision: Decision,
233
+ tool_call: ToolCall,
234
+ config: InterruptOnConfig,
235
+ ) -> tuple[ToolCall | None, ToolMessage | None]:
236
+ """Process a single decision and return the revised tool call and optional tool message."""
237
+ allowed_decisions = config["allowed_decisions"]
238
+
239
+ if decision["type"] == "approve" and "approve" in allowed_decisions:
240
+ return tool_call, None
241
+ if decision["type"] == "edit" and "edit" in allowed_decisions:
242
+ edited_action = decision["edited_action"]
243
+ return (
244
+ ToolCall(
245
+ type="tool_call",
246
+ name=edited_action["name"],
247
+ args=edited_action["arguments"],
248
+ id=tool_call["id"],
249
+ ),
250
+ None,
251
+ )
252
+ if decision["type"] == "reject" and "reject" in allowed_decisions:
253
+ # Create a tool message with the human's text response
254
+ content = decision.get("message") or (
255
+ f"User rejected the tool call for `{tool_call['name']}` with id {tool_call['id']}"
256
+ )
257
+ tool_message = ToolMessage(
258
+ content=content,
259
+ name=tool_call["name"],
260
+ tool_call_id=tool_call["id"],
261
+ status="error",
262
+ )
263
+ return tool_call, tool_message
264
+ msg = (
265
+ f"Unexpected human decision: {decision}. "
266
+ f"Decision type '{decision.get('type')}' "
267
+ f"is not allowed for tool '{tool_call['name']}'. "
268
+ f"Expected one of {allowed_decisions} based on the tool's configuration."
269
+ )
270
+ raise ValueError(msg)
271
+
192
272
  def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
193
273
  """Trigger interrupt flows for relevant tool calls after an AIMessage."""
194
274
  messages = state["messages"]
@@ -216,87 +296,50 @@ class HumanInTheLoopMiddleware(AgentMiddleware):
216
296
  revised_tool_calls: list[ToolCall] = auto_approved_tool_calls.copy()
217
297
  artificial_tool_messages: list[ToolMessage] = []
218
298
 
219
- # Create interrupt requests for all tools that need approval
220
- interrupt_requests: list[HumanInTheLoopRequest] = []
299
+ # Create action requests and review configs for all tools that need approval
300
+ action_requests: list[ActionRequest] = []
301
+ review_configs: list[ReviewConfig] = []
302
+
221
303
  for tool_call in interrupt_tool_calls:
222
- tool_name = tool_call["name"]
223
- tool_args = tool_call["args"]
224
- config = self.interrupt_on[tool_name]
225
-
226
- # Generate description using the description field (str or callable)
227
- description_value = config.get("description")
228
- if callable(description_value):
229
- description = description_value(tool_call, state, runtime)
230
- elif description_value is not None:
231
- description = description_value
232
- else:
233
- description = f"{self.description_prefix}\n\nTool: {tool_name}\nArgs: {tool_args}"
234
-
235
- request: HumanInTheLoopRequest = {
236
- "action_request": ActionRequest(
237
- action=tool_name,
238
- args=tool_args,
239
- ),
240
- "config": config,
241
- "description": description,
242
- }
243
- interrupt_requests.append(request)
304
+ config = self.interrupt_on[tool_call["name"]]
244
305
 
245
- responses: list[HumanInTheLoopResponse] = interrupt(interrupt_requests)
306
+ # Create ActionRequest and ReviewConfig using helper method
307
+ action_request, review_config = self._create_action_and_config(
308
+ tool_call, config, state, runtime
309
+ )
310
+ action_requests.append(action_request)
311
+ review_configs.append(review_config)
246
312
 
247
- # Validate that the number of responses matches the number of interrupt tool calls
248
- if (responses_len := len(responses)) != (
313
+ # Create single HITLRequest with all actions and configs
314
+ hitl_request = HITLRequest(
315
+ action_requests=action_requests,
316
+ review_configs=review_configs,
317
+ )
318
+
319
+ # Send interrupt and get response
320
+ hitl_response: HITLResponse = interrupt(hitl_request)
321
+ decisions = hitl_response["decisions"]
322
+
323
+ # Validate that the number of decisions matches the number of interrupt tool calls
324
+ if (decisions_len := len(decisions)) != (
249
325
  interrupt_tool_calls_len := len(interrupt_tool_calls)
250
326
  ):
251
327
  msg = (
252
- f"Number of human responses ({responses_len}) does not match "
328
+ f"Number of human decisions ({decisions_len}) does not match "
253
329
  f"number of hanging tool calls ({interrupt_tool_calls_len})."
254
330
  )
255
331
  raise ValueError(msg)
256
332
 
257
- for i, response in enumerate(responses):
333
+ # Process each decision using helper method
334
+ for i, decision in enumerate(decisions):
258
335
  tool_call = interrupt_tool_calls[i]
259
336
  config = self.interrupt_on[tool_call["name"]]
260
337
 
261
- if response["type"] == "accept" and config.get("allow_accept"):
262
- revised_tool_calls.append(tool_call)
263
- elif response["type"] == "edit" and config.get("allow_edit"):
264
- edited_action = response["args"]
265
- revised_tool_calls.append(
266
- ToolCall(
267
- type="tool_call",
268
- name=edited_action["action"],
269
- args=edited_action["args"],
270
- id=tool_call["id"],
271
- )
272
- )
273
- elif response["type"] == "response" and config.get("allow_respond"):
274
- # Create a tool message with the human's text response
275
- content = response.get("args") or (
276
- f"User rejected the tool call for `{tool_call['name']}` "
277
- f"with id {tool_call['id']}"
278
- )
279
- tool_message = ToolMessage(
280
- content=content,
281
- name=tool_call["name"],
282
- tool_call_id=tool_call["id"],
283
- status="error",
284
- )
285
- revised_tool_calls.append(tool_call)
338
+ revised_tool_call, tool_message = self._process_decision(decision, tool_call, config)
339
+ if revised_tool_call:
340
+ revised_tool_calls.append(revised_tool_call)
341
+ if tool_message:
286
342
  artificial_tool_messages.append(tool_message)
287
- else:
288
- allowed_actions = [
289
- action
290
- for action in ["accept", "edit", "response"]
291
- if config.get(f"allow_{'respond' if action == 'response' else action}")
292
- ]
293
- msg = (
294
- f"Unexpected human response: {response}. "
295
- f"Response action '{response.get('type')}' "
296
- f"is not allowed for tool '{tool_call['name']}'. "
297
- f"Expected one of {allowed_actions} based on the tool's configuration."
298
- )
299
- raise ValueError(msg)
300
343
 
301
344
  # Update the AI message to only include approved tool calls
302
345
  last_ai_msg.tool_calls = revised_tool_calls
@@ -108,9 +108,9 @@ class ModelCallLimitMiddleware(AgentMiddleware):
108
108
 
109
109
  Args:
110
110
  thread_limit: Maximum number of model calls allowed per thread.
111
- None means no limit. Defaults to None.
111
+ None means no limit. Defaults to `None`.
112
112
  run_limit: Maximum number of model calls allowed per run.
113
- None means no limit. Defaults to None.
113
+ None means no limit. Defaults to `None`.
114
114
  exit_behavior: What to do when limits are exceeded.
115
115
  - "end": Jump to the end of the agent execution and
116
116
  inject an artificial AI message indicating that the limit was exceeded.