.. SPDX-License-Identifier: CC-BY-4.0 SPDX-FileCopyrightText: Netresearch DTT GmbH .. include:: /Includes.rst.txt .. _adr-053: ========================================================= ADR-053: One marker interface for all thrown exceptions ========================================================= :Status: Accepted :Date: 2026-07-12 :Authors: Netresearch DTT GmbH .. _adr-053-context: Context ======= A consumer that wraps an nr_llm call and wants to convert failures into its own domain exception has to enumerate concrete classes today:: } catch ( InvalidArgumentException // PHP's own, from ChatMessage/ToolSpec::fromArray() | NrLlmInvalidArgumentException // options validation | ProviderException // covers the 5 provider subtypes | BudgetExceededException | AccessDeniedException | ConfigurationNotFoundException $e ) { Two problems. The list goes stale silently: when a future version adds or rethrows a new exception type, existing catch lists let it escape as an uncaught 500 instead of the consumer's clean error path. And the chat/tool value objects' ``fromArray()`` normalisation threw **PHP's** global ``\InvalidArgumentException``, a different class from nr_llm's own ``Exception\InvalidArgumentException`` — the first entry in the list above exists only because of that mismatch (``nr_ai_search``'s ``NrLlmChatClient`` documents exactly this trap). .. _adr-053-decision: Decision ======== - ``Netresearch\NrLlm\Exception\NrLlmExceptionInterface`` (extending ``\Throwable``) marks every exception this extension throws on its public API surface. Core, provider, agent, specialized, skill and tool exception hierarchies implement it. The compatible ``Exception\RuntimeException`` and ``Exception\LogicException`` subclasses cover internally generated errors without a more specific failure type; the existing ``Exception\InvalidArgumentException`` covers validation, including delegated credentials and editor actions. - ``ChatMessage`` / ``ToolSpec`` / ``ToolCall`` normalisation errors now throw ``Exception\InvalidArgumentException`` instead of PHP's global class. Backwards compatible: the nr_llm class extends ``\InvalidArgumentException``, so existing catches keep matching. - A reflection test sweeps the complete ``Classes/`` tree, independently of exception names and directories, so a future exception class cannot ship without the marker. A parser-based check also rejects directly constructed native validation, runtime and logic errors in this tree. Consumer-boundary tests verify common catches and native compatibility. Consumers can write ``catch (NrLlmExceptionInterface $e)`` for nr_llm-defined exceptions. Propagated dependency exceptions still need their own catch arm when the consumer wants to handle them. The marker covers errors generated by nr_llm. It does not convert exceptions originating in third-party callbacks or dependencies, nor PHP engine errors. Callers of the tool loop handle its approval/input suspension types before the common marker catch to retain their resumable state. .. _adr-053-consequences: Consequences ============ - The remaining classes that imported the global ``InvalidArgumentException`` for their own validation errors (response parsers, task readers, backend response DTOs, value objects) throw ``Exception\InvalidArgumentException`` now — the compatible follow-up named here is done, guarded by the same reflection test. One deliberate exception: ``Service\Task\TaskInputResolver`` keeps the global import because it only *catches* the exception around ``RecordTableReader::fetchAll()`` — narrowing that catch to the nr_llm subclass would miss a plain ``\InvalidArgumentException`` raised by third-party code inside the read path. - Catch-all remains opt-in: consumers that want to handle budget exhaustion differently from provider outages keep catching the concrete classes. Scope clarification (2026-10-10) =============================== The marker covers exception classes defined by nr_llm. Dependency exceptions that propagate through a public call retain their original class. The bundled streaming adapters, for example, propagate PSR-18 network failures directly. Seven executable adapter contract cases preserve that behavior; they do not claim that the marker catches every dependency failure or every PHP error. See :ref:`developer-streaming` for the pre-first-chunk fallback boundary.