SurfSense/surfsense_backend/app/agents/chat/runtime/errors.py
CREDO23 f2a61bc0ef refactor(agents): consolidate chat runtime infra under chat/runtime
Move the lower-level runtime/infra modules out of multi_agent_chat/shared/
(they were never used by subagents, so they failed the shared-by-all-siblings
rule) and unify them with the already-relocated checkpointer:

  agents/runtime/                      -> agents/chat/runtime/
  mac/shared/errors.py                 -> chat/runtime/errors.py
  mac/shared/llm_config.py             -> chat/runtime/llm_config.py
  mac/shared/prompt_caching.py         -> chat/runtime/prompt_caching.py
  mac/shared/mention_resolver.py       -> chat/runtime/mention_resolver.py
  mac/shared/path_resolver.py          -> chat/runtime/path_resolver.py

These sit below the agent packages: the boundary + agent factory + shared
middleware depend on them, and they import no agent code (acyclic).
2026-06-05 13:19:24 +02:00

95 lines
2.6 KiB
Python

"""
Typed error taxonomy for the SurfSense agent stack.
Used by:
- :class:`RetryAfterMiddleware` — its ``retry_on`` callable consults
the error code to decide whether a retry is appropriate.
- :class:`PermissionMiddleware` — emits ``code="permission_denied"``
errors when a deny rule trips.
- All tools — return :class:`StreamingError` payloads in
``ToolMessage.additional_kwargs["error"]`` so the model and the
retry/permission layers share a contract.
"""
from __future__ import annotations
from typing import Literal
from pydantic import BaseModel, Field
ErrorCode = Literal[
"rate_limit",
"auth",
"tool_validation",
"tool_runtime",
"context_overflow",
"provider",
"permission_denied",
"doom_loop",
"busy",
"cancelled",
]
class StreamingError(BaseModel):
"""Structured error payload attached to ``ToolMessage.additional_kwargs["error"]``.
Tools and middleware emit this so retry, permission, and routing
layers can decide what to do without parsing free-form strings.
"""
code: ErrorCode
retryable: bool = False
suggestion: str | None = None
correlation_id: str | None = None
detail: str | None = Field(
default=None,
description="Free-form additional context. Not surfaced to the model.",
)
class Config:
frozen = True
class RejectedError(Exception):
"""Raised when the user rejects a permission ask without feedback.
Caught by :class:`PermissionMiddleware`; the agent stops the current
tool fan-out and surfaces a user-facing rejection.
"""
def __init__(self, *, tool: str | None = None, pattern: str | None = None) -> None:
super().__init__(f"Permission rejected for tool {tool!r}, pattern {pattern!r}")
self.tool = tool
self.pattern = pattern
class CorrectedError(Exception):
"""Raised when the user rejects a permission ask *with* feedback.
The :class:`PermissionMiddleware` translates the feedback into a
synthetic ``ToolMessage`` so the model sees the user's correction
and can retry the request differently.
"""
def __init__(self, feedback: str, *, tool: str | None = None) -> None:
super().__init__(feedback)
self.feedback = feedback
self.tool = tool
class BusyError(Exception):
"""Raised when a second prompt arrives while the same thread is mid-stream."""
def __init__(self, request_id: str | None = None) -> None:
super().__init__("Thread is busy with another request")
self.request_id = request_id
__all__ = [
"BusyError",
"CorrectedError",
"ErrorCode",
"RejectedError",
"StreamingError",
]