packages feed

agentic 0.2.0.2 → 0.2.0.3

raw patch · 13 files changed

+94/−79 lines, 13 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Agentic: documentSchema :: Text -> Schema -> Schema
- Agentic.Contract: described :: Text -> [Option a] -> OptionSet a
- Agentic.Describe: [judgeState] :: StepInfo -> Schema
- Agentic.Describe: toValue :: Description -> Value
- Agentic.Questions: [requestState] :: JudgeRequest -> Value
- Agentic.Runtime: [output] :: Conversation -> Schema
- Agentic.Runtime: [stateSchema] :: Conversation -> Schema
- Agentic.Runtime: [state] :: Conversation -> Value
- Agentic.Schema: documentSchema :: Text -> Schema -> Schema
- Agentic.Scripted: alwaysYes :: forall (m :: Type -> Type). Applicative m => Probability -> SystemOne m
+ Agentic: documentedSchema :: Text -> Schema -> Schema
+ Agentic.Contract: documentedOptions :: Text -> [Option a] -> OptionSet a
+ Agentic.Describe: [judgeInput] :: StepInfo -> Schema
+ Agentic.Describe: descriptionValue :: Description -> Value
+ Agentic.Questions: [requestInput] :: JudgeRequest -> Value
+ Agentic.Questions: toProbability :: Double -> Probability
+ Agentic.Runtime: [inputSchema] :: Conversation -> Schema
+ Agentic.Runtime: [input] :: Conversation -> Value
+ Agentic.Runtime: [outputSchema] :: Conversation -> Schema
+ Agentic.Schema: documentedSchema :: Text -> Schema -> Schema
+ Agentic.Scripted: fixedAnswers :: forall (m :: Type -> Type). Applicative m => Probability -> SystemOne m
- Agentic.Questions: fromBasisPoints :: Double -> Probability
+ Agentic.Questions: fromBasisPoints :: Int -> Probability
- Agentic.Settings: endpoint :: HasEndpoint c => String -> c -> c
+ Agentic.Settings: endpoint :: HasEndpoint c => Text -> c -> c

Files

CHANGELOG.md view
@@ -1,5 +1,20 @@ # Changelog for agentic +## 0.2.0.3 - 2026-10-06++* `fromBasisPoints` is renamed `toProbability`, the inverse of `probability`:+  it takes a probability such as 0.9. `fromBasisPoints` now takes basis points,+  the inverse of `basisPoints`.+* A step's input is called its input everywhere, not its state: the+  `Conversation` fields are `input`, `inputSchema` and `outputSchema`,+  `JudgeRequest`'s is `requestInput`, and `JudgeInfo`'s is `judgeInput`.+* Attaching a description is `documented…` throughout: `documentSchema` is+  `documentedSchema`, and `described` is `documentedOptions`.+* `Agentic.Scripted.alwaysYes` is `fixedAnswers`; it gives yes/no questions+  whatever probability it's given.+* `Agentic.Describe.toValue` is `descriptionValue`.+* `endpoint` takes `Text`, like the other settings.+ ## 0.2.0.2 - 2026-10-01  * The core now builds and runs under MicroHs as well as GHC. Generic
README.md view
@@ -147,10 +147,10 @@ like `between 1 10`, are checked locally, and a failed one goes back to the model to try again. -This is the core design assumption: the types ARE the prompt. The state's types+This is the core design assumption: the types ARE the prompt. The input's types are part of what the model reads, and field names carry meaning. A meeting note wrapped in a record with `setup` and `punchline` fields looks like a joke before-the model reads a word. So give each step the state it should judge, and no more.+the model reads a word. So give each step the input it should judge, and no more.  And describe an enumeration once - Claude and Jev both see the same wording (see `Options` below).@@ -183,7 +183,7 @@ data Groan = Mild | Solid | Unbearable deriving (Generic, Show)  instance Options Groan where-  options = described "How much the audience groans"+  options = documentedOptions "How much the audience groans"     [ option Mild       "A polite smile; most people didn't notice"     , option Solid      "An audible groan from most of the room"     , option Unbearable "People get up and leave" ]@@ -355,9 +355,9 @@   n9 --> output ``` -`describe` returns a plain `Description` you can walk yourself, and `toValue`-turns it into JSON for UIs and other agents. The tree hides unnamed glue between-steps, but never a branch.+`describe` returns a plain `Description` you can walk yourself, and+`descriptionValue` turns it into JSON for UIs and other agents. The tree hides+unnamed glue between steps, but never a branch.  ### Naming things @@ -468,7 +468,7 @@  The library never tells the model how to format its reply - the providers' strict structured outputs take care of that. What the model gets is meaning: the-instruction, the state, and your contracts' descriptions.+instruction, the input, and your contracts' descriptions.  And there are no sessions to manage. Anything a later step needs goes through the types. Memory across runs is yours to own - put it in the flow's types, or@@ -502,7 +502,7 @@ testRuntime :: IO (Runtime IO) testRuntime = do   two <- scripted [respond joke]-  pure runtime { systemOne = alwaysYes 0.95, systemTwo = two }+  pure runtime { systemOne = fixedAnswers 0.95, systemTwo = two } ```  ## History
agentic.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               agentic-version:            0.2.0.2+version:            0.2.0.3 synopsis:           Composable, inspectable agentic workflows mixing LLMs and Jev description:   Typed agentic workflows built from Arrow combinators. A flow is a description: you can describe it as a tree, Mermaid or Graphviz before running anything, then interpret it against System One (Jev) and System Two (an LLM) providers. This is the core: flows, contracts, questions, the runtime and the interpreter. It depends only on base and text.
src/Agentic/Contract.hs view
@@ -43,7 +43,7 @@   , OptionSet (..)   , Option (..)   , option-  , described+  , documentedOptions   , Enumeration (..)   , enumeration #ifndef __MHS__@@ -192,7 +192,7 @@           | otherwise -> Left ("missing field " <> name)     )   where-    schema = maybe id documentSchema d (codecSchema c)+    schema = maybe id documentedSchema d (codecSchema c)     nullable = case shape (codecSchema c) of       SNullable _ -> True       _ -> False@@ -206,7 +206,7 @@ record :: Text -> ObjectCodec a a -> Codec a record d o =   Codec-    (documentSchema' (nonEmpty d) (schemaOf (SObject (objectFields o))))+    (documentedSchema' (nonEmpty d) (schemaOf (SObject (objectFields o))))     (Object . objectEncode o)     ( \case         Object kvs -> objectDecode o kvs@@ -246,7 +246,7 @@ sumCodec d cases   | all (null . caseFields) cases =       Codec-        (documentSchema' d (schemaOf (SEnum [(caseTag c, caseDoc c) | c <- cases])))+        (documentedSchema' d (schemaOf (SEnum [(caseTag c, caseDoc c) | c <- cases])))         (\a -> maybe Null (String . caseTag) (matching a))         ( \case             String t | Just c <- byTag t -> caseDecode c []@@ -254,7 +254,7 @@         )   | otherwise =       Codec-        (documentSchema' d (schemaOf (SSum [Variant (caseTag c) (caseDoc c) (caseFields c) | c <- cases])))+        (documentedSchema' d (schemaOf (SSum [Variant (caseTag c) (caseDoc c) (caseFields c) | c <- cases])))         ( \a -> case [(caseTag c, kvs) | c <- cases, Just kvs <- [caseEncode c a]] of             (t, kvs) : _ -> Object (("tag", String t) : kvs)             [] -> Null@@ -274,7 +274,7 @@  -- | Describe the whole type. documented :: Text -> Codec a -> Codec a-documented d c = c {codecSchema = documentSchema d (codecSchema c)}+documented d c = c {codecSchema = documentedSchema d (codecSchema c)}  -- | Describe one field of a record (or of any constructor of a sum). Naming a -- field that doesn't exist is an error when the schema is first used.@@ -289,7 +289,7 @@       _ -> error ("Agentic.Contract.field: no field named " <> T.unpack name)     named f = fieldName f == name     describeField f-      | named f = f {fieldSchema = documentSchema d (fieldSchema f)}+      | named f = f {fieldSchema = documentedSchema d (fieldSchema f)}       | otherwise = f  -- | A constraint the wire schemas can't express. It's stated to the model and@@ -310,8 +310,8 @@     ("between " <> T.pack (show lo) <> " and " <> T.pack (show hi))     (\a -> a >= lo && a <= hi) -documentSchema' :: Maybe Text -> Schema -> Schema-documentSchema' = maybe id documentSchema+documentedSchema' :: Maybe Text -> Schema -> Schema+documentedSchema' = maybe id documentedSchema  nonEmpty :: Text -> Maybe Text nonEmpty t = if T.null t then Nothing else Just t@@ -434,8 +434,9 @@ option :: Show a => a -> Text -> Option a option v d = Option v (label v) (nonEmpty d) -described :: Text -> [Option a] -> OptionSet a-described d = OptionSet (nonEmpty d)+-- | Options with a description of the whole set.+documentedOptions :: Text -> [Option a] -> OptionSet a+documentedOptions d = OptionSet (nonEmpty d)  -- | An option's label: what the model sees and answers with. For an -- enumeration, 'show' gives the constructor's name.@@ -454,7 +455,7 @@ enumeration :: forall a. (Options a, Eq a) => Codec a enumeration =   Codec-    (documentSchema' (optionsDoc set) (schemaOf (SEnum [(optionLabel o, optionDoc o) | o <- opts])))+    (documentedSchema' (optionsDoc set) (schemaOf (SEnum [(optionLabel o, optionDoc o) | o <- opts])))     (\a -> maybe Null (String . optionLabel) (find ((== a) . optionValue) opts))     ( \case         String t | Just o <- find ((== t) . optionLabel) opts -> Right (optionValue o)
src/Agentic/Describe.hs view
@@ -13,14 +13,14 @@   , NodeKind (..)   , Edge (..)   , EdgeStyle (..)-  , toValue+  , descriptionValue   ) where  import Agentic.Contract (Codec (..)) import Agentic.Core import Agentic.Questions (QuestionSpec (..), Questions (..)) import Agentic.Schema (Schema, typeLabel)-import Agentic.Value (Value (..))+import Agentic.Value (Value (..), renderJson) import Data.List (mapAccumL) import Data.Text (Text) import qualified Data.Text as T@@ -53,7 +53,7 @@       , draftTools :: [ToolInfo]       }   | JudgeInfo-      { judgeState :: Schema+      { judgeInput :: Schema       , judgeQuestions :: [QuestionSpec]       } @@ -413,8 +413,9 @@       Flow -> []       Uses -> ["style=dotted", "arrowhead=none"]       Again -> ["style=dashed"]-    str t = "\"" <> inner t <> "\""-    inner = concatMapText (\case '"' -> "\\\""; '\\' -> "\\\\"; c -> T.singleton c)+    -- JSON's string escapes are also DOT's.+    str = renderJson . String+    inner = T.pack . init . drop 1 . T.unpack . str  -- | Is the node with this id inside the box with that id? inBox :: Text -> Text -> [Item] -> Bool@@ -449,20 +450,20 @@ -- JSON  -- | The description as a JSON-shaped value, for UIs and other agents.-toValue :: Description -> Value-toValue = \case+descriptionValue :: Description -> Value+descriptionValue = \case   Leaf info -> leaf info-  Sequence ds -> node "sequence" [("steps", Array (map toValue ds))]-  Together ds -> node "together" [("steps", Array (map toValue ds))]-  Halves l r -> node "halves" [("first", toValue l), ("second", toValue r)]-  Branch l r -> node "branch" [("left", toValue l), ("right", toValue r)]-  ForEach d -> node "each" [("step", toValue d)]-  Repeated d -> node "repeat" [("step", toValue d)]+  Sequence ds -> node "sequence" [("steps", Array (map descriptionValue ds))]+  Together ds -> node "together" [("steps", Array (map descriptionValue ds))]+  Halves l r -> node "halves" [("first", descriptionValue l), ("second", descriptionValue r)]+  Branch l r -> node "branch" [("left", descriptionValue l), ("right", descriptionValue r)]+  ForEach d -> node "each" [("step", descriptionValue d)]+  Repeated d -> node "repeat" [("step", descriptionValue d)]   Annotated n d ->     node "note" $       [("name", String (noteName n))]         <> maybe [] (\t -> [("description", String t)]) (noteDescription n)-        <> [("step", toValue d)]+        <> [("step", descriptionValue d)]   where     node kind fields = Object (("kind", String kind) : fields)     leaf = \case@@ -478,7 +479,7 @@           , ("tools", Array (map tool tools))           ]       JudgeInfo input qs ->-        node "judge" [("state", String (typeLabel input)), ("questions", Array (map question qs))]+        node "judge" [("input", String (typeLabel input)), ("questions", Array (map question qs))]     tool t =       Object         [ ("name", String (infoName t))@@ -490,10 +491,6 @@       AskYesNo q -> Object [("type", String "yesNo"), ("question", String q)]       AskChoice q opts -> Object [("type", String "choice"), ("question", String q), ("options", Array [String l | (l, _) <- opts])]       AskScore q levels -> Object [("type", String "score"), ("question", String q), ("levels", Array [String l | (l, _) <- levels])]---- | 'T.concatMap', which MicroHs's "Data.Text" doesn't provide.-concatMapText :: (Char -> Text) -> Text -> Text-concatMapText f = T.concat . map f . T.unpack  -- | Apply a function to a pair's second half. (MicroHs has no Functor instance -- for pairs.)
src/Agentic/Interpret.hs view
@@ -52,15 +52,15 @@             answers <- askSystemOne (systemOne rt) request             emit path (Judged request answers)             answered (decodeAnswers qs answers)-      Draft input out instruction tools -> do+      Draft inCodec out instruction tools -> do         let conversation =               Conversation                 { path = path                 , instruction = instruction-                , state = encode input x-                , stateSchema = codecSchema input+                , input = encode inCodec x+                , inputSchema = codecSchema inCodec                 , tools = map toolSpec tools-                , output = codecSchema out+                , outputSchema = codecSchema out                 , history = []                 }         emit path (Drafting conversation)@@ -83,15 +83,13 @@     runTool :: [Note] -> [Tool m] -> ToolCall -> m ToolResult     runTool path tools call = do       emit path (ToolCalled call)-      result <- case find ((== callName call) . nameOf) tools of+      result <- case find ((== callName call) . toolName) tools of         Nothing -> pure (ToolFailed ("there is no tool named " <> callName call))         Just (Tool name _ input out body) -> case decode input (callInput call) of           Left problem -> pure (ToolFailed ("invalid input: " <> problem))           Right i -> ToolOk . encode out <$> go (path <> [Note name Nothing]) body i       emit path (ToolReturned (callId call) result)       pure result-      where-        nameOf (Tool name _ _ _ _) = name  toolSpec :: Tool m -> ToolSpec toolSpec (Tool name description input _ _) = ToolSpec name description (codecSchema input)
src/Agentic/Questions.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE AllowAmbiguousTypes #-} --- | Questions for a System One model such as Jev. Following Jev's terms, a step--- asks t'Questions' about its input, the /state/.+-- | Questions for a System One model such as Jev: a step asks t'Questions'+-- about its input. module Agentic.Questions   ( -- * Questions     Questions (..)@@ -11,8 +11,9 @@     -- * Answers   , Probability   , probability-  , fromBasisPoints+  , toProbability   , basisPoints+  , fromBasisPoints   , YesNo (..)   , Choice (..)   , Score (..)@@ -59,12 +60,15 @@ probability (Probability bp) = fromIntegral bp / 10000  -- | Convert a provider's probability, rounding once (half to even).-fromBasisPoints :: Double -> Probability-fromBasisPoints d = clamp (round (d * 10000))+toProbability :: Double -> Probability+toProbability d = clamp (round (d * 10000))  basisPoints :: Probability -> Int basisPoints (Probability bp) = bp +fromBasisPoints :: Int -> Probability+fromBasisPoints = clamp+ -- --------------------------------------------------------------------------- -- Answers @@ -90,7 +94,7 @@ -- Answers have contracts, so a judgement can be a tool's output.  instance Contract Probability where-  contract = mapCodec fromBasisPoints probability (contract @Double)+  contract = mapCodec toProbability probability (contract @Double)  instance Contract YesNo where   contract = record "A yes/no judgement" (YesNo <$> required "yes" "The probability that the answer is yes" yes)@@ -130,9 +134,9 @@     -- ^ The position, each level's probability (by index), and the confidence.   deriving (Eq, Show) --- | What a System One provider receives: the encoded state and the questions.+-- | What a System One provider receives: the encoded input and the questions. data JudgeRequest = JudgeRequest-  { requestState :: Value+  { requestInput :: Value   , requestQuestions :: [QuestionSpec]   }   deriving (Eq, Ord, Show)@@ -140,7 +144,7 @@ -- --------------------------------------------------------------------------- -- Questions --- | One or more questions about the same state, sent as one request. Combine+-- | One or more questions about the same input, sent as one request. Combine -- them applicatively: -- -- > judge (Review <$> funny <*> groan)@@ -181,7 +185,7 @@   where     opts = optionList (options @a) --- | Place the state on an 'Options' type's levels, lowest first.+-- | Place the input on an 'Options' type's levels, lowest first. score :: forall a. Options a => Text -> Questions (Score a) score q = single (AskScore q (labels opts)) $ \case   ScoreAnswer pos ps conf ->
src/Agentic/Runtime.hs view
@@ -43,11 +43,11 @@   { path :: [Note]     -- ^ Where this step is in the flow.   , instruction :: Instruction-  , state :: Value+  , input :: Value     -- ^ The step's input, encoded by its contract.-  , stateSchema :: Schema+  , inputSchema :: Schema   , tools :: [ToolSpec]-  , output :: Schema+  , outputSchema :: Schema     -- ^ The schema of the step's result.   , history :: [Exchange]     -- ^ Earlier turns of this step, oldest first. Append-only.
src/Agentic/Schema.hs view
@@ -7,7 +7,7 @@   , Variant (..)   , Format (..)   , schemaOf-  , documentSchema+  , documentedSchema   , typeLabel   , titled   ) where@@ -60,8 +60,8 @@ schemaOf :: Shape -> Schema schemaOf = Schema Nothing Nothing [] -documentSchema :: Text -> Schema -> Schema-documentSchema d s = s {doc = Just d}+documentedSchema :: Text -> Schema -> Schema+documentedSchema d s = s {doc = Just d}  -- | Name the schema's type, unless it already has a name. titled :: Text -> Schema -> Schema
src/Agentic/Scripted.hs view
@@ -7,7 +7,7 @@   , callTools     -- * System One   , answering-  , alwaysYes+  , fixedAnswers   ) where  import Agentic.Contract (Codec (..), Contract (..))@@ -44,8 +44,8 @@  -- | Yes/no questions get probability @p@; choices and scores pick the first -- option with certainty.-alwaysYes :: Applicative m => Probability -> SystemOne m-alwaysYes p = answering $ \case+fixedAnswers :: Applicative m => Probability -> SystemOne m+fixedAnswers p = answering $ \case   AskYesNo _ -> YesNoAnswer p   AskChoice _ ((l, _) : _) -> ChoiceAnswer l [(l, 1)] 1   AskChoice _ [] -> ChoiceAnswer "" [] 0
src/Agentic/Settings.hs view
@@ -28,7 +28,7 @@   key :: Text -> c -> c  class HasEndpoint c where-  endpoint :: String -> c -> c+  endpoint :: Text -> c -> c  -- | How long to wait for a response, in seconds. class HasTimeout c where
src/Agentic/ViaLLM.hs view
@@ -22,10 +22,10 @@         Conversation           { path = []           , instruction = Instruction "Answer each question about the input. Give every probability as a number from 0 to 1."-          , state = requestState request-          , stateSchema = schemaOf SNull+          , input = requestInput request+          , inputSchema = schemaOf SNull           , tools = []-          , output = schemaOf (SObject [Field qid (questionSchema q) True | (qid, q) <- qs])+          , outputSchema = schemaOf (SObject [Field qid (questionSchema q) True | (qid, q) <- qs])           , history = []           }   turn <- askSystemTwo two conversation@@ -40,12 +40,12 @@ questionSchema :: QuestionSpec -> Schema questionSchema = \case   AskYesNo q ->-    documentSchema q (schemaOf (SObject [Field "probabilityYes" (documentSchema "The probability that the answer is yes" (schemaOf SNumber)) True]))+    documentedSchema q (schemaOf (SObject [Field "probabilityYes" (documentedSchema "The probability that the answer is yes" (schemaOf SNumber)) True]))   AskChoice q opts -> distribution (q <> " Give each option's probability; they should sum to 1.") opts   AskScore q levels -> distribution (q <> " The options are ordered levels, lowest first. Give each level's probability; they should sum to 1.") levels   where     distribution q opts =-      documentSchema q (schemaOf (SObject [Field l (documentSchema (maybe l id d) (schemaOf SNumber)) True | (l, d) <- opts]))+      documentedSchema q (schemaOf (SObject [Field l (documentedSchema (maybe l id d) (schemaOf SNumber)) True | (l, d) <- opts]))  answer :: QuestionSpec -> Value -> Either Text Answer answer spec v = case (spec, v) of@@ -64,6 +64,6 @@   where     get k kvs = maybe (Left ("missing " <> k)) Right (lookupField k kvs)     number = \case-      Number d -> Right (fromBasisPoints d)-      Integer n -> Right (fromBasisPoints (fromInteger n))+      Number d -> Right (toProbability d)+      Integer n -> Right (toProbability (fromInteger n))       other -> Left ("expected a probability, got " <> renderJson other)
test/Spec.hs view
@@ -27,7 +27,7 @@  instance Options Groan where   options =-    described+    documentedOptions       "How much the audience groans"       [ option Mild "A polite smile"       , option Solid "An audible groan"@@ -45,8 +45,8 @@ data Review = Review {funnyAnswer :: YesNo, groanAnswer :: Score Groan}   deriving (Show, Eq) -described' :: Codec Joke-described' =+documentedJoke :: Codec Joke+documentedJoke =   record "A joke, split into its parts" $     Joke       <$> required "genre" "The style of joke" genre@@ -69,7 +69,7 @@ testRuntime :: [Action] -> Probability -> IO (Runtime IO) testRuntime turns p = do   two <- scripted turns-  pure runtime {systemOne = alwaysYes p, systemTwo = two}+  pure runtime {systemOne = fixedAnswers p, systemTwo = two}  roundTrips :: (Eq a, Show a) => Codec a -> a -> Expectation roundTrips c a = decode c (encode c a) `shouldBe` Right a@@ -93,7 +93,7 @@       title (codecSchema (contract @Joke)) `shouldBe` Just "Joke"      it "keeps descriptions written in the codec" $-      case shape (codecSchema described') of+      case shape (codecSchema documentedJoke) of         SObject fs -> map (doc . fieldSchema) fs `shouldBe` map Just ["The style of joke", "The setup line", "The line that lands it"]         other -> expectationFailure (show other)