Skip to content

Conversation

@liam-langchain
Copy link

Summary

Add new end_tools exit behavior to ToolCallLimitMiddleware that allows agents to continue with partial tool results when limits are exceeded, instead of stopping entirely.

This enables graceful degradation where agents can work with partial information instead of failing completely when tool limits are reached.

Changes

Core Implementation

  • Added end_tools to the exit_behavior parameter (alongside existing end and error)
  • Implemented wrap_tool_call hook to intercept and block individual tool executions
  • Updated before_model hook to count successful tool executions from previous iterations
  • Modified counting logic to work correctly for both parallel and sequential tool execution patterns

Exit Behaviors

  • end: Stops entire agent execution (unchanged)
  • end_tools (new): Blocks tool execution when limits exceeded, but allows agent to continue with partial results
  • error: Raises ToolCallLimitExceededError (unchanged)

Example Usage

from langchain.agents import create_agent
from langchain.agents.middleware.tool_call_limit import ToolCallLimitMiddleware

# Block tool execution but let agent continue with partial results
middleware = ToolCallLimitMiddleware(
    run_limit=2,
    exit_behavior="end_tools"
)

agent = create_agent(
    "anthropic:claude-3-5-sonnet-20241022",
    tools=[search, calculate, weather],
    middleware=[middleware]
)

# Agent requests 5 tool calls
# - First 2 execute successfully
# - Remaining 3 return warning messages
# - Agent continues and responds with partial results

Testing

  • All 16 tests passing (12 existing + 6 new)
  • New tests cover:
    • test_end_behavior() - Basic end behavior
    • test_end_tools_behavior() - Core end_tools functionality with parallel execution
    • test_error_behavior() - Error behavior
    • test_end_tools_with_specific_tool() - Tool-specific limiting
    • test_end_tools_thread_limit() - Thread-level limits with checkpointer
    • test_comparison_all_three_behaviors() - Side-by-side comparison

Technical Details

Position-Based Blocking

The implementation uses position-based logic to determine which tools execute even with parallel execution:

  • Tool at position 0: count + 0 + 1 = 1 ≤ limit → Execute
  • Tool at position 1: count + 1 + 1 = 2 ≤ limit → Execute
  • Tool at position 2: count + 2 + 1 = 3 > limit → Block

Execution Patterns

Works correctly for both:

  • Parallel execution: All tools requested in single model response
  • Sequential execution: Tools requested one at a time across iterations

Benefits

  1. Graceful degradation: Agents work with partial information instead of failing
  2. Better UX: Users get partial answers rather than "limit exceeded" errors
  3. Flexible control: Combine different behaviors for different tools
  4. Backwards compatible: Existing exit_behavior="end" continues to work unchanged

Add new `end_tools` exit behavior that allows agents to continue with partial tool results when limits are exceeded, instead of stopping entirely.

## Changes

- Add `end_tools` exit behavior to ToolCallLimitMiddleware
- Implement `wrap_tool_call` hook to block tool execution when limits exceeded
- Update counting logic to work for both parallel and sequential execution
- Add comprehensive tests for all three exit behaviors (end, end_tools, error)

## Behavior

- `end`: Stops entire agent execution (unchanged)
- `end_tools` (new): Blocks tool execution but lets agent continue with partial results
- `error`: Raises exception (unchanged)

## Tests

- All 16 tests passing
- Added 6 new tests covering parallel execution, sequential execution, specific tool limiting, and thread limits
- Works correctly for both parallel (batch) and sequential (one-at-a-time) tool calls
@github-actions github-actions bot added langchain Related to the package `langchain` v1 Issue specific to LangChain 1.0 feature labels Oct 22, 2025
- Simplify verbose comments in tool_call_limit.py
- Use .lower() for case-insensitive string comparison
- Add test_end_tools_sequential() to explicitly test sequential tool calls
- All 17 tests passing (10 existing + 7 new)
- Move Callable and Command imports to TYPE_CHECKING block
- Fix line length violations (max 100 characters)
- Run ruff format to fix formatting
- All 17 tests passing
@liam-langchain liam-langchain changed the title feat(agents): add end_tools exit behavior to ToolCallLimitMiddleware feat(langchain_v1): add end_tools exit behavior to ToolCallLimitMiddleware Oct 22, 2025
@github-actions github-actions bot added feature and removed feature labels Oct 22, 2025
- Handle case where msg.content could be list or string
- Convert to string before calling .lower()
- Mypy now passes cleanly
Copy link
Collaborator

@sydney-runkle sydney-runkle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I correct that this doesn't block other tools that haven't yet exceeded their limits?

Is there a better name we could use that's more expressive about this?

Seems super reasonable though to want to continue w/ ones that have not yet hit their limit.

def wrap_tool_call(
self,
request: ToolCallRequest,
execute: Callable[[ToolCallRequest], ToolMessage | Command],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not call this handler?

tool_name=self.tool_name,
)
return ToolMessage(
content=f"{limit_message} Do not call any more tools.",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be specific to this tool?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature langchain Related to the package `langchain` v1 Issue specific to LangChain 1.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants