packages feed

shikumi-trace-0.3.0.0: src/Shikumi/Trace/Observation.hs

{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Structured evidence from one sequential execution, isolated from other runs.
module Shikumi.Trace.Observation (NodeObservation (..), runProgramObserved) where

import Data.Aeson (Value)
import Data.Either (isRight)
import Data.Text (Text)
import Effectful (Eff, (:>))
import Effectful.Error.Static (Error, catchError, throwError)
import Effectful.Prim (Prim)
import Effectful.Prim.IORef (modifyIORef', newIORef, readIORef)
import Shikumi.Adapter (ToPrompt (..))
import Shikumi.Error (ShikumiError)
import Shikumi.LLM (LLM)
import Shikumi.Program (CaptureCodec (..), Program (..), runProgram)
import Shikumi.Trace (SpanKind)
import Shikumi.Trace.Node (NodePath)
import Shikumi.Trace.Program (walkProgramWith)

-- | Invocation numbers start at zero per example, across all leaves. Rejection
-- lineage records the enclosing scope label and its first invocation number.
-- Opaque Embed observations never claim to identify internal predictors.
data NodeObservation = NodeObservation
  { observationPath :: !NodePath,
    observationInvocation :: !Int,
    observationInput :: !(Maybe Value),
    observationInputFields :: ![(Text, Text)],
    observationOutputFields :: ![(Text, Text)],
    observationOutput :: !(Maybe Value),
    observationStatus :: !(Either ShikumiError ()),
    observationEligible :: !Bool,
    observationRejectedBy :: ![(Text, Int)],
    observationOpaque :: !Bool
  }
  deriving stock (Eq, Show)

-- | Typed failures retain observations. Host exceptions and cancellation escape.
-- Uses private Prim cells, so concurrent callers never exchange evidence.
runProgramObserved ::
  forall i o es.
  (LLM :> es, Error ShikumiError :> es, Prim :> es) =>
  Program i o -> i -> Eff es (Either ShikumiError o, [NodeObservation])
runProgramObserved program input = do
  observations <- newIORef []
  next <- newIORef 0
  let scope :: forall a. SpanKind -> Text -> Eff es a -> Eff es a
      scope _ label action = do
        start <- readIORef next
        action `catchError` \_ (err :: ShikumiError) -> do
          modifyIORef'
            observations
            ( map
                ( \obs ->
                    if observationInvocation obs >= start
                      then obs {observationEligible = False, observationRejectedBy = (label, start) : observationRejectedBy obs}
                      else obs
                )
            )
          throwError err
      record ::
        forall x y.
        NodePath ->
        Bool ->
        (x -> Maybe Value) ->
        (y -> Maybe Value) ->
        (x -> [(Text, Text)]) ->
        (y -> [(Text, Text)]) ->
        Program x y ->
        x ->
        Eff es y
      record path opaque encodeIn encodeOut fieldsIn fieldsOut node x = do
        ordinal <- readIORef next
        modifyIORef' next (+ 1)
        result <- (Right <$> runProgram node x) `catchError` \_ err -> pure (Left err)
        let obs =
              NodeObservation
                path
                ordinal
                (encodeIn x)
                (fieldsIn x)
                (either (const []) fieldsOut result)
                (either (const Nothing) encodeOut result)
                (() <$ result)
                (isRight result && not opaque)
                []
                opaque
        modifyIORef' observations (obs :)
        either throwError pure result
      leaf :: forall x y. NodePath -> Program x y -> x -> Eff es y
      leaf path node@(PredictCaptured codec _ _) =
        record
          path
          False
          (Just . encodeCaptureInput codec)
          (Just . encodeCaptureOutput codec)
          toPromptFields
          toPromptFields
          node
      leaf path node@(Predict _ _) = record path False (const Nothing) (const Nothing) toPromptFields toPromptFields node
      leaf path node = record path True (const Nothing) (const Nothing) (const []) (const []) node
  result <- (Right <$> walkProgramWith scope leaf (pure ()) program input) `catchError` \_ err -> pure (Left err)
  obs <- reverse <$> readIORef observations
  pure (result, obs)