Skip to content

symfonic.agent.errors

errors

The agent framework's exception vocabulary — layer-neutral.

Split out of :mod:symfonic.agent.types by TA2.1. The two live in the same package but not in the same LAY-ADR layer, and the split is what makes that true rather than merely asserted.

symfonic.agent.types is facade-compiler: it carries the request and response models, the streaming chunk types and FrameworkTenantScope, and it imports Pydantic to do it. The exception hierarchy has none of that — it is shared vocabulary that a capability, a runtime service and the facade all have to name in order to raise or catch the same thing, and it needs nothing but symfonic.core.contracts.errors. The dependency matrix already says so: kernel-contracts is the one column every other row reads yes on.

Be precise about the purity claim, because only half of it is checked today. kernel-contracts is also the one layer that "imports nothing but the Python standard library" (LAY-ADR §1.1). What the gate verifies for this module is external purity: kernel-purity fails the build on any third-party import, which is what types (Pydantic) could never satisfy. The single internal import above is a different matter — symfonic.core is deliberately unmapped under EXC-2026-001, and boundaries._check_import returns [] when the target has no layer, so that edge is currently unchecked, not verified. It is expected to become a checked kernel-contracts → kernel-contracts edge once symfonic.core.contracts is mapped; until then this docstring claims external purity only.

Concretely, this is what let symfonic.agent.prompts be ruled capability: :class:~symfonic.agent.prompts.system_prompt.HMSSystemPromptSection raises SecurityScopeError on a missing tenant, and capability → facade-compiler is no.

Nothing is renamed or re-parented. symfonic.agent.types re-exports all four names, so from symfonic.agent.types import SecurityScopeError and every except SymfonicAgentError keep catching exactly what they caught before — these are the same class objects, not copies.

ScopeValidationError

ScopeValidationError(message: str, code: str | None = None)

Bases: SymfonicAgentError

Raised when tenant scope validation fails.

Source code in src/symfonic/agent/errors.py
def __init__(self, message: str, code: str | None = None) -> None:
    super().__init__(message)
    self.code = code

SecurityScopeError

SecurityScopeError(message: str, code: str | None = None)

Bases: SymfonicAgentError

Raised when tenant scope is missing or invalid for a security-critical operation.

Source code in src/symfonic/agent/errors.py
def __init__(self, message: str, code: str | None = None) -> None:
    super().__init__(message)
    self.code = code

SymfonicAgentError

SymfonicAgentError(message: str, code: str | None = None)

Bases: SymfonicError

Base exception for all agent framework errors.

Re-parented onto :class:~symfonic.core.contracts.errors.SymfonicError in T2.1.2 (FERR-1). This is a widening of the ancestry, never a rename: except SymfonicAgentError still catches exactly what it caught before, and the new root additionally lets one except SymfonicError span both the legacy facade and the simple one.

Source code in src/symfonic/agent/errors.py
def __init__(self, message: str, code: str | None = None) -> None:
    super().__init__(message)
    self.code = code

TranscriptUnsupportedError

TranscriptUnsupportedError(message: str, code: str | None = None)

Bases: SymfonicAgentError

Raised when a transcript query needs a saver capability it lacks (v7.27.0).

get_transcript(time_range=...) requires the checkpointer to support alist (to read per-checkpoint timestamps). A minimal saver without alist cannot honour a time-range query, so the engine fails loud (naming the saver class) rather than silently returning [] -- a silent empty result is indistinguishable from "no messages in range" and would mislead callers. Ordinal (index) and full-transcript reads use aget_tuple and are unaffected.

Source code in src/symfonic/agent/errors.py
def __init__(self, message: str, code: str | None = None) -> None:
    super().__init__(message)
    self.code = code