From 7413befa7972aec0c2305b12ad46c1bcd05f6637 Mon Sep 17 00:00:00 2001 From: aran-yogesh Date: Mon, 9 Feb 2026 11:52:54 -0800 Subject: [PATCH 1/5] feat: add error normalization middleware for tool and model calls --- apps/agent/agent/middleware.py | 122 +++++++++++++++++++ apps/agent/agent/server.py | 4 +- apps/agent/agent/tools/commit_and_open_pr.py | 2 +- apps/agent/agent/webapp.py | 2 +- 4 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 apps/agent/agent/middleware.py diff --git a/apps/agent/agent/middleware.py b/apps/agent/agent/middleware.py new file mode 100644 index 00000000..da0f6d6d --- /dev/null +++ b/apps/agent/agent/middleware.py @@ -0,0 +1,122 @@ +"""Error normalization middleware for tool and model calls. + +Wraps all tool calls in try/except so that unhandled exceptions are +returned as error ToolMessages instead of crashing the agent run. +""" + +from __future__ import annotations + +import json +import logging +from collections.abc import Awaitable, Callable + +from langchain.agents.middleware.types import ( + AgentMiddleware, + AgentState, + ModelRequest, + ModelResponse, +) +from langchain_core.messages import ToolMessage +from langgraph.prebuilt.tool_node import ToolCallRequest +from langgraph.types import Command + +logger = logging.getLogger(__name__) + + +def _get_name(candidate: object) -> str | None: + if not candidate: + return None + if isinstance(candidate, str): + return candidate + if isinstance(candidate, dict): + name = candidate.get("name") + else: + name = getattr(candidate, "name", None) + return name if isinstance(name, str) and name else None + + +def _extract_tool_name(request: ToolCallRequest | None) -> str | None: + if request is None: + return None + for attr in ("tool_call", "tool_name", "name"): + name = _get_name(getattr(request, attr, None)) + if name: + return name + return None + + +def _to_error_payload( + e: Exception, request: ToolCallRequest | None = None +) -> dict[str, str]: + data: dict[str, str] = { + "error": str(e), + "error_type": e.__class__.__name__, + "status": "error", + } + tool_name = _extract_tool_name(request) + if tool_name: + data["name"] = tool_name + return data + + +def _get_tool_call_id(request: ToolCallRequest) -> str | None: + if isinstance(request.tool_call, dict): + return request.tool_call.get("id") + return None + + +class ErrorNormalizationMiddleware(AgentMiddleware): + """Normalize tool execution errors into predictable payloads. + + Catches any exception thrown during a tool call and converts it into + a ToolMessage with status="error" so the LLM can see the failure and + self-correct, rather than crashing the entire agent run. + + Model call errors (invalid API key, rate limit, etc.) are logged but + re-raised so they surface to the caller. + """ + + state_schema = AgentState + + def wrap_tool_call( + self, + request: ToolCallRequest, + handler: Callable[[ToolCallRequest], ToolMessage | Command], + ) -> ToolMessage | Command: + try: + return handler(request) + except Exception as e: # noqa: BLE001 + logger.exception("Error during tool call handling; request=%r", request) + data = _to_error_payload(e, request) + return ToolMessage( + content=json.dumps(data), + tool_call_id=_get_tool_call_id(request), + status="error", + ) + + async def awrap_tool_call( + self, + request: ToolCallRequest, + handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]], + ) -> ToolMessage | Command: + try: + return await handler(request) + except Exception as e: # noqa: BLE001 + logger.exception("Error during tool call handling; request=%r", request) + data = _to_error_payload(e, request) + return ToolMessage( + content=json.dumps(data), + tool_call_id=_get_tool_call_id(request), + status="error", + ) + + async def awrap_model_call( + self, + request: ModelRequest, + handler: Callable[[ModelRequest], Awaitable[ModelResponse]], + ) -> ModelResponse: + try: + return await handler(request) + except Exception: + logger.exception("Error during model invocation") + raise diff --git a/apps/agent/agent/server.py b/apps/agent/agent/server.py index 3d06a2e6..1ff8de26 100644 --- a/apps/agent/agent/server.py +++ b/apps/agent/agent/server.py @@ -26,13 +26,14 @@ warnings.filterwarnings("ignore", message=".*Pydantic V1.*", category=UserWarnin # Now safe to import agent (which imports LangChain modules) from deepagents import create_deep_agent -from .protocol import SandboxBackendProtocol # Local import for encryption from langchain_anthropic import ChatAnthropic from .encryption import decrypt_token +from .middleware import ErrorNormalizationMiddleware from .prompt import construct_system_prompt +from .protocol import SandboxBackendProtocol from .tools import commit_and_open_pr, fetch_url, http_request @@ -959,6 +960,7 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915 tools=[http_request, fetch_url, commit_and_open_pr], backend=sandbox_backend, middleware=[ + ErrorNormalizationMiddleware(), check_message_queue_before_model, post_to_linear_after_model, open_pr_if_needed, diff --git a/apps/agent/agent/tools/commit_and_open_pr.py b/apps/agent/agent/tools/commit_and_open_pr.py index c29078c1..3c644767 100644 --- a/apps/agent/agent/tools/commit_and_open_pr.py +++ b/apps/agent/agent/tools/commit_and_open_pr.py @@ -91,4 +91,4 @@ def commit_and_open_pr( "title": title, "body": body, "commit_message": commit_message or title, - } + } \ No newline at end of file diff --git a/apps/agent/agent/webapp.py b/apps/agent/agent/webapp.py index d4c0d6ac..b0a13be2 100644 --- a/apps/agent/agent/webapp.py +++ b/apps/agent/agent/webapp.py @@ -71,7 +71,7 @@ def get_service_jwt_token_for_user( LINEAR_TEAM_TO_REPO: dict[str, dict[str, str]] = { "Brace's test workspace": {"owner": "langchain-ai", "name": "open-swe"}, - "Yogesh-dev": {"owner": "aran-yogesh", "name": "nimedge"}, + "Yogesh-dev": {"owner": "aran-yogesh", "name": "TalkBack"}, } From 03744fbf2daa87feddc72aab64243aca4889d211 Mon Sep 17 00:00:00 2001 From: aran-yogesh Date: Mon, 9 Feb 2026 12:08:40 -0800 Subject: [PATCH 2/5] feat: add error normalization middleware for tool and model calls []Closes #904] --- apps/agent/agent/middleware.py | 4 +--- apps/agent/agent/tools/commit_and_open_pr.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/agent/agent/middleware.py b/apps/agent/agent/middleware.py index da0f6d6d..8c7db6c7 100644 --- a/apps/agent/agent/middleware.py +++ b/apps/agent/agent/middleware.py @@ -45,9 +45,7 @@ def _extract_tool_name(request: ToolCallRequest | None) -> str | None: return None -def _to_error_payload( - e: Exception, request: ToolCallRequest | None = None -) -> dict[str, str]: +def _to_error_payload(e: Exception, request: ToolCallRequest | None = None) -> dict[str, str]: data: dict[str, str] = { "error": str(e), "error_type": e.__class__.__name__, diff --git a/apps/agent/agent/tools/commit_and_open_pr.py b/apps/agent/agent/tools/commit_and_open_pr.py index 3c644767..c29078c1 100644 --- a/apps/agent/agent/tools/commit_and_open_pr.py +++ b/apps/agent/agent/tools/commit_and_open_pr.py @@ -91,4 +91,4 @@ def commit_and_open_pr( "title": title, "body": body, "commit_message": commit_message or title, - } \ No newline at end of file + } From 810bf412e914d3e38b6ad60023c1c0ad54cd8e97 Mon Sep 17 00:00:00 2001 From: aran-yogesh Date: Mon, 9 Feb 2026 12:53:34 -0800 Subject: [PATCH 3/5] Refactor middleware into package and update repo config --- apps/agent/agent/middleware/__init__.py | 3 +++ .../tool_error_handler.py} | 20 ++----------------- apps/agent/agent/server.py | 4 ++-- 3 files changed, 7 insertions(+), 20 deletions(-) create mode 100644 apps/agent/agent/middleware/__init__.py rename apps/agent/agent/{middleware.py => middleware/tool_error_handler.py} (83%) diff --git a/apps/agent/agent/middleware/__init__.py b/apps/agent/agent/middleware/__init__.py new file mode 100644 index 00000000..c5cc72d3 --- /dev/null +++ b/apps/agent/agent/middleware/__init__.py @@ -0,0 +1,3 @@ +from .tool_error_handler import ToolErrorMiddleware + +__all__ = ["ToolErrorMiddleware"] diff --git a/apps/agent/agent/middleware.py b/apps/agent/agent/middleware/tool_error_handler.py similarity index 83% rename from apps/agent/agent/middleware.py rename to apps/agent/agent/middleware/tool_error_handler.py index 8c7db6c7..d4005e6c 100644 --- a/apps/agent/agent/middleware.py +++ b/apps/agent/agent/middleware/tool_error_handler.py @@ -1,4 +1,4 @@ -"""Error normalization middleware for tool and model calls. +"""Tool error handling middleware. Wraps all tool calls in try/except so that unhandled exceptions are returned as error ToolMessages instead of crashing the agent run. @@ -13,8 +13,6 @@ from collections.abc import Awaitable, Callable from langchain.agents.middleware.types import ( AgentMiddleware, AgentState, - ModelRequest, - ModelResponse, ) from langchain_core.messages import ToolMessage from langgraph.prebuilt.tool_node import ToolCallRequest @@ -63,15 +61,12 @@ def _get_tool_call_id(request: ToolCallRequest) -> str | None: return None -class ErrorNormalizationMiddleware(AgentMiddleware): +class ToolErrorMiddleware(AgentMiddleware): """Normalize tool execution errors into predictable payloads. Catches any exception thrown during a tool call and converts it into a ToolMessage with status="error" so the LLM can see the failure and self-correct, rather than crashing the entire agent run. - - Model call errors (invalid API key, rate limit, etc.) are logged but - re-raised so they surface to the caller. """ state_schema = AgentState @@ -107,14 +102,3 @@ class ErrorNormalizationMiddleware(AgentMiddleware): tool_call_id=_get_tool_call_id(request), status="error", ) - - async def awrap_model_call( - self, - request: ModelRequest, - handler: Callable[[ModelRequest], Awaitable[ModelResponse]], - ) -> ModelResponse: - try: - return await handler(request) - except Exception: - logger.exception("Error during model invocation") - raise diff --git a/apps/agent/agent/server.py b/apps/agent/agent/server.py index 1ff8de26..9fa46e8d 100644 --- a/apps/agent/agent/server.py +++ b/apps/agent/agent/server.py @@ -31,7 +31,7 @@ from deepagents import create_deep_agent from langchain_anthropic import ChatAnthropic from .encryption import decrypt_token -from .middleware import ErrorNormalizationMiddleware +from .middleware import ToolErrorMiddleware from .prompt import construct_system_prompt from .protocol import SandboxBackendProtocol from .tools import commit_and_open_pr, fetch_url, http_request @@ -960,7 +960,7 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915 tools=[http_request, fetch_url, commit_and_open_pr], backend=sandbox_backend, middleware=[ - ErrorNormalizationMiddleware(), + ToolErrorMiddleware(), check_message_queue_before_model, post_to_linear_after_model, open_pr_if_needed, From 770fcd4e1db310590f602766459d75c3ccf28c6f Mon Sep 17 00:00:00 2001 From: Aran Yogesh Date: Mon, 9 Feb 2026 13:10:54 -0800 Subject: [PATCH 4/5] Change exception handling to catch all exceptions Removed specific exception handling to catch all exceptions. --- apps/agent/agent/middleware/tool_error_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/agent/agent/middleware/tool_error_handler.py b/apps/agent/agent/middleware/tool_error_handler.py index d4005e6c..f16d4038 100644 --- a/apps/agent/agent/middleware/tool_error_handler.py +++ b/apps/agent/agent/middleware/tool_error_handler.py @@ -78,7 +78,7 @@ class ToolErrorMiddleware(AgentMiddleware): ) -> ToolMessage | Command: try: return handler(request) - except Exception as e: # noqa: BLE001 + except: logger.exception("Error during tool call handling; request=%r", request) data = _to_error_payload(e, request) return ToolMessage( @@ -94,7 +94,7 @@ class ToolErrorMiddleware(AgentMiddleware): ) -> ToolMessage | Command: try: return await handler(request) - except Exception as e: # noqa: BLE001 + except: logger.exception("Error during tool call handling; request=%r", request) data = _to_error_payload(e, request) return ToolMessage( From bfaf361f253e9841b9f91131031cefd6a64f2aae Mon Sep 17 00:00:00 2001 From: Aran Yogesh Date: Mon, 9 Feb 2026 13:16:08 -0800 Subject: [PATCH 5/5] Update exception handling in tool_error_handler.py Refine exception handling in tool error handler to capture specific exceptions. --- apps/agent/agent/middleware/tool_error_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/agent/agent/middleware/tool_error_handler.py b/apps/agent/agent/middleware/tool_error_handler.py index f16d4038..5c310b7b 100644 --- a/apps/agent/agent/middleware/tool_error_handler.py +++ b/apps/agent/agent/middleware/tool_error_handler.py @@ -78,7 +78,7 @@ class ToolErrorMiddleware(AgentMiddleware): ) -> ToolMessage | Command: try: return handler(request) - except: + except Exception as e: logger.exception("Error during tool call handling; request=%r", request) data = _to_error_payload(e, request) return ToolMessage( @@ -94,7 +94,7 @@ class ToolErrorMiddleware(AgentMiddleware): ) -> ToolMessage | Command: try: return await handler(request) - except: + except Exception as e: logger.exception("Error during tool call handling; request=%r", request) data = _to_error_payload(e, request) return ToolMessage(