packages feed

shikumi-0.2.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,
  )
where

import Baikai.Error (BaikaiError (..), ErrorCategory (..))
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
  | -- | the provider/transport failed (mapped from baikai)
    ProviderFailure !Text
  | -- | the prompt exceeded the model's context window
    ContextWindowExceeded !Text
  | -- | the call exceeded its time budget
    Timeout !Text
  | -- | the running cost ceiling was reached; the call was refused
    BudgetExceeded !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)
  ProcessFailure ->
    ProviderFailure $
      case exitCode e of
        Just n -> "process exited " <> T.pack (show n) <> ": " <> message e
        Nothing -> message e
  _ -> ProviderFailure (message e)

-- | Which errors are worth retrying. Provider/transport failures and timeouts are
-- transient; decode, schema, validation, and budget errors are deterministic and
-- retrying cannot fix them. Centralizing the policy here keeps it auditable
-- (the resilience interpreter in "Shikumi.LLM" consults exactly this predicate).
isTransient :: ShikumiError -> Bool
isTransient = \case
  ProviderFailure {} -> True
  Timeout {} -> True
  _ -> False