Redouble AI Nucleo documentation
Part 13 · Inside the runtime

Inside the exception hierarchy

ai.redouble.nucleo.harness.errors · nucleo-core

This page is for people working on the runtime itself: the whole sealed hierarchy, where each member lives, and the exceptions the runtime throws that are never addressed to a model. The guide for choosing an exception in a tool is When a tool fails.

The hierarchy is sealed and built on the LLMReadable marker interface of this package; the harness document describes how the runtime routes its members. The HTTP status exceptions are detailed in HTTP errors as typed failures, the retry signals and the model-correction failures in When a job runs again. The thinker loop checks instanceof LLMReadableException to recognize the checked and the unchecked branch alike, and isCorrectable() for the verdict.

The hierarchy

LLMReadable (marker interface) - getLLMMessage()

LLMReadableException (sealed, extends LLMReadable)
+-- LLMReadableCheckedException (sealed, checked)
|     +-- CorrectableLLMException (non-sealed abstract) - isCorrectable() = true
|     |     InvalidInputException         - bad input: invalid syntax, blank field, HTTP 400/422
|     |       Http400Exception, Http413Exception, Http422Exception
|     |     ResourceNotFoundException    - well-formed ID that doesn't exist: fetch-by-ID 404
|     |       Http404Exception
|     |     GuardrailException           - policy violation: PII, prohibited content
|     |     JsonParseException           - the model's reply was not the JSON object asked for
|     |     ResponseValidationException  - the reply parsed but left required fields empty
|     +-- UncorrectableLLMException (non-sealed abstract) - isCorrectable() = false
|           ExternalServiceException     - API down, HTTP 500+, network failure
|             |   Http500Exception, Http502Exception, Http503Exception
|             |   HttpUnmappedStatusException - any other error status
|             +-- UpstreamThrottleException      - upstream throttle signal (Retry-After, robot detection, etc.)
|                   Http429Exception
|           UnauthorizedException        - external API rejected credentials: HTTP 401/403
|             Http401Exception, Http403Exception
|           PermissionDeniedException    - internal auth: user lacks access
|           SystemException              - unexpected internal error, bug
|           JobCancelledException        - the dispatcher stopped the job on a cancel
|           JobContext.CancellationException - the signal a running job gets from checkCancellation()
|           JobTimeoutException          - the dispatcher stopped the job on its time budget
|           TokenEstimateExceedsLimitException - the conversation cannot fit the model's window
|           ContextOverflowException     - compaction could not make the conversation fit
|           PromptNotFoundException      - no prompt source holds the key a thinker asked for
+-- LLMReadableRuntimeException (sealed, unchecked)
      +-- CorrectableRuntimeLLMException - correctable, unchecked
      +-- UncorrectableRuntimeLLMException - uncorrectable, unchecked
            QuotaExhaustedException      - the provider account is out of money
            SpendCapExceededException    - admission refused the job under its workflow's spend cap
            ModelNotFoundException       - the catalog holds no model under the id
            ProviderRefusalException     - the provider declined the call; carries model, category, explanation for a caller resubmitting elsewhere

Where they live: GuardrailException in ai.redouble.nucleo.guardrails; JobContext.CancellationException nested in ai.redouble.nucleo.harness.JobContext; ContextOverflowException in harness.conversation; PromptNotFoundException in ai.redouble.nucleo.prompt; ModelNotFoundException in harness.models; the Http classes in http; JsonParseException, ResponseValidationException, TokenEstimateExceedsLimitException and QuotaExhaustedException in retry; every other member in this package. ErrorsHierarchyContractTest scans the runtime's classes and holds this list to be the whole membership.

Outside the hierarchy

Not every exception the runtime throws is addressed to the model. DependencyFailedException (a dependency of the job failed for good; carries its id), JobDeadlockException (a job blocked on another while holding resources; carries both jobs and the call site) and NotImplementedException (a planned surface that is not built; a message only) are plain runtime exceptions. The transparent retry signals (UpstreamRetryException and its three subclasses) and the dispatcher's two re-run signals (ResponseCorrectionRetryException, OutputTruncationRetryException) are runtime exceptions the dispatcher consumes; the model never sees them, and unwrap and wrapWithContext rethrow any of them found in a cause chain.

Why the HTTP exceptions share a record and not a class

The ten HttpNNNException classes sit under five different semantic parents, because what the caller should DO about a 400 has nothing to do with what it should do about a 503. That placement is the point of the types, and it is also why they cannot share a base class - Java allows only one parent.

The payload they all carry therefore lives in a record, HttpErrorDetail (status, service, endpoint, response body), reached through the HttpErrorResponse interface that all ten implement, as does HttpUnmappedStatusException, the carrier for any error status without a class of its own.

How a throttle reaches the limiters

The dispatcher catches any ExternalServiceException from a job with a custom rate limiter and calls onRateLimitError(UpstreamFailure) on that limiter before rethrowing, so the elastic window stretches and the circuit breaker eventually opens.

UpstreamFailure is what the server actually said - status, service, detail, and any Retry-After it asked for - assembled from the exception by UpstreamFailure.from. Limiters record it and quote it back in their throttle, circuit-transition, and circuit-open messages, so a tool disabled by an open circuit names the upstream response that disabled it instead of leaving the reason to be reconstructed from surrounding log lines.

^