packages feed

shikumi-0.4.0.0: src/Shikumi/Error.hs

-- | The single error vocabulary the whole shikumi framework shares, plus a total
-- mapping from baikai's transport-level errors into it.
--
-- This module owns part of the MasterPlan's integration point #1: every later
-- ExecPlan (signatures, caching, tracing, tools) MUST surface failures through
-- 'ShikumiError' rather than inventing its own error type.
module Shikumi.Error
  ( ShikumiError (..),
    fromBaikaiError,
    isTransient,
    renderShikumiError,
  )
where

import Baikai.Error (BaikaiError (..), ErrorCategory (..), isRetryable)
import Data.Text (Text)
import Data.Text qualified as T

-- | The enumerated failure modes named by integration point #1. The decode /
-- schema / validation constructors are produced by higher layers (structured
-- output, program validation); the transport ones are produced here by
-- 'fromBaikaiError'.
data ShikumiError
  = -- | provider returned text that is not valid JSON
    InvalidJSON !Text
  | -- | a required output field was absent (field name)
    MissingField !Text
  | -- | decoded JSON did not match the expected schema
    SchemaMismatch !Text
  | -- | a typed value failed a user/program validation rule
    ValidationFailure !Text
  | -- | Legacy unclassified failure supplied by callers or scripted interpreters.
    ProviderFailure !Text
  | -- | Structured transport failure, including verbatim refusal metadata.
    ProviderError !BaikaiError
  | -- | the prompt exceeded the model's context window
    ContextWindowExceeded !Text
  | -- | the call exceeded its time budget
    Timeout !Text
  | -- | a configured resource allowance was exhausted (cost, calls or session size)
    BudgetExceeded !Text
  | -- | generated code failed after exhausting correction attempts
    CodeExecFailed !Text
  deriving stock (Eq, Show)

-- | Total mapping from baikai's transport-level errors into shikumi's
-- vocabulary. Invalid requests map to 'SchemaMismatch' because in baikai a
-- malformed request is almost always bad schema/parameters; decode failures
-- map to 'InvalidJSON' because baikai's decode failures are JSON parse
-- failures of the provider response.
fromBaikaiError :: BaikaiError -> ShikumiError
fromBaikaiError e = case category e of
  DecodeFailure -> InvalidJSON (message e)
  InvalidRequest -> SchemaMismatch ("invalid request: " <> message e)
  ContextOverflow -> ContextWindowExceeded (message e)
  _ -> ProviderError e

-- | Human-readable detail without dumping the transport record. Baikai's
-- message is its safe-to-log description; opaque request/response data is absent.
renderShikumiError :: ShikumiError -> Text
renderShikumiError = \case
  ProviderError e ->
    "provider "
      <> T.pack (show (category e))
      <> maybe "" (\n -> " (exit " <> T.pack (show n) <> ")") (exitCode e)
      <> ": "
      <> message e
  InvalidJSON t -> t
  MissingField t -> "missing field " <> t
  SchemaMismatch t -> t
  ValidationFailure t -> t
  ProviderFailure t -> t
  ContextWindowExceeded t -> t
  Timeout t -> t
  BudgetExceeded t -> t
  CodeExecFailed t -> t

-- | Retry only typed rate limits and transient failures, using Baikai's
-- classification. Legacy text failures and timeouts retain their retry policy.
-- Unknown/process errors and refusals are terminal; never infer from prose.
isTransient :: ShikumiError -> Bool
isTransient = \case
  ProviderError e -> isRetryable e
  ProviderFailure {} -> True
  Timeout {} -> True
  _ -> False