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 +15/−0
- README.md +8/−8
- agentic.cabal +1/−1
- src/Agentic/Contract.hs +13/−12
- src/Agentic/Describe.hs +16/−19
- src/Agentic/Interpret.hs +5/−7
- src/Agentic/Questions.hs +14/−10
- src/Agentic/Runtime.hs +3/−3
- src/Agentic/Schema.hs +3/−3
- src/Agentic/Scripted.hs +3/−3
- src/Agentic/Settings.hs +1/−1
- src/Agentic/ViaLLM.hs +7/−7
- test/Spec.hs +5/−5
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)