mirror of
https://github.com/hwchase17/langchain.git
synced 2026-06-09 10:17:00 +00:00
feat(langchain_v1): Add ShellToolMiddleware and ClaudeBashToolMiddleware (#33527)
- Both middleware share the same implementation, the only difference is one uses Claude's server-side tool definition, whereas the other one uses a generic tool definition compatible with all models - Implemented 3 execution policies (responsible for actually running the shell process) - HostExecutionPolicy runs the shell as subprocess, appropriate for already sandboxed environments, eg when run inside a dedicated docker container - CodexSandboxExecutionPolicy runs the shell using the sandbox command from the Codex CLI which implements sandboxing techniques for Linux and Mac OS. - DockerExecutionPolicy runs the shell inside a dedicated Docker container for isolation. - Implements all behaviours described in https://docs.claude.com/en/docs/agents-and-tools/tool-use/bash-tool#handle-large-outputs including timeouts, truncation, output redaction, etc --------- Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com> Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com> Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
This commit is contained in:
@@ -6,6 +6,7 @@ from langchain_anthropic.middleware.anthropic_tools import (
|
||||
StateClaudeMemoryMiddleware,
|
||||
StateClaudeTextEditorMiddleware,
|
||||
)
|
||||
from langchain_anthropic.middleware.bash import ClaudeBashToolMiddleware
|
||||
from langchain_anthropic.middleware.file_search import (
|
||||
StateFileSearchMiddleware,
|
||||
)
|
||||
@@ -15,6 +16,7 @@ from langchain_anthropic.middleware.prompt_caching import (
|
||||
|
||||
__all__ = [
|
||||
"AnthropicPromptCachingMiddleware",
|
||||
"ClaudeBashToolMiddleware",
|
||||
"FilesystemClaudeMemoryMiddleware",
|
||||
"FilesystemClaudeTextEditorMiddleware",
|
||||
"StateClaudeMemoryMiddleware",
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Anthropic-specific middleware for the Claude bash tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, Literal
|
||||
|
||||
from langchain.agents.middleware.shell_tool import ShellToolMiddleware
|
||||
from langchain.agents.middleware.types import ModelRequest, ModelResponse
|
||||
from langchain.tools.tool_node import ToolCallRequest
|
||||
from langchain_core.messages import ToolMessage
|
||||
from langgraph.types import Command
|
||||
|
||||
_CLAUDE_BASH_DESCRIPTOR = {"type": "bash_20250124", "name": "bash"}
|
||||
|
||||
|
||||
class ClaudeBashToolMiddleware(ShellToolMiddleware):
|
||||
"""Middleware that exposes Anthropic's native bash tool to models."""
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Initialize middleware without registering a client-side tool."""
|
||||
kwargs["shell_command"] = ("/bin/bash",)
|
||||
super().__init__(*args, **kwargs)
|
||||
# Remove the base tool so Claude's native descriptor is the sole entry.
|
||||
self._tool = None # type: ignore[assignment]
|
||||
self.tools = []
|
||||
|
||||
def wrap_model_call(
|
||||
self,
|
||||
request: ModelRequest,
|
||||
handler: Callable[[ModelRequest], ModelResponse],
|
||||
) -> ModelResponse:
|
||||
"""Ensure the Claude bash descriptor is available to the model."""
|
||||
tools = request.tools
|
||||
if all(tool is not _CLAUDE_BASH_DESCRIPTOR for tool in tools):
|
||||
tools = [*tools, _CLAUDE_BASH_DESCRIPTOR]
|
||||
request = request.override(tools=tools)
|
||||
return handler(request)
|
||||
|
||||
def wrap_tool_call(
|
||||
self,
|
||||
request: ToolCallRequest,
|
||||
handler: Callable[[ToolCallRequest], Command | ToolMessage],
|
||||
) -> Command | ToolMessage:
|
||||
"""Intercept Claude bash tool calls and execute them locally."""
|
||||
tool_call = request.tool_call
|
||||
if tool_call.get("name") != "bash":
|
||||
return handler(request)
|
||||
resources = self._ensure_resources(request.state)
|
||||
return self._run_shell_tool(
|
||||
resources,
|
||||
tool_call["args"],
|
||||
tool_call_id=tool_call.get("id"),
|
||||
)
|
||||
|
||||
async def awrap_tool_call(
|
||||
self,
|
||||
request: ToolCallRequest,
|
||||
handler: Callable[[ToolCallRequest], Awaitable[Command | ToolMessage]],
|
||||
) -> Command | ToolMessage:
|
||||
"""Async interception mirroring the synchronous implementation."""
|
||||
tool_call = request.tool_call
|
||||
if tool_call.get("name") != "bash":
|
||||
return await handler(request)
|
||||
resources = self._ensure_resources(request.state)
|
||||
return self._run_shell_tool(
|
||||
resources,
|
||||
tool_call["args"],
|
||||
tool_call_id=tool_call.get("id"),
|
||||
)
|
||||
|
||||
def _format_tool_message(
|
||||
self,
|
||||
content: str,
|
||||
tool_call_id: str | None,
|
||||
*,
|
||||
status: Literal["success", "error"],
|
||||
artifact: dict[str, Any] | None = None,
|
||||
) -> ToolMessage | str:
|
||||
"""Format tool responses using Claude's bash descriptor."""
|
||||
if tool_call_id is None:
|
||||
return content
|
||||
return ToolMessage(
|
||||
content=content,
|
||||
tool_call_id=tool_call_id,
|
||||
name=_CLAUDE_BASH_DESCRIPTOR["name"],
|
||||
status=status,
|
||||
artifact=artifact or {},
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["ClaudeBashToolMiddleware"]
|
||||
Reference in New Issue
Block a user