packages feed

agentic 0.2.0.1 → 0.2.0.2

raw patch · 10 files changed

+254/−34 lines, 10 filesdep ~agenticdep ~basedep ~textPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: agentic, base, text

API changes (from Hackage documentation)

- Agentic: SArray :: Schema -> Shape
- Agentic: SBool :: Shape
- Agentic: SEnum :: [(Text, Maybe Text)] -> Shape
- Agentic: SInteger :: Shape
- Agentic: SNull :: Shape
- Agentic: SNullable :: Schema -> Shape
- Agentic: SNumber :: Shape
- Agentic: SObject :: [Field] -> Shape
- Agentic: SString :: Maybe Format -> Shape
- Agentic: SSum :: [Variant] -> Shape
- Agentic: Variant :: Text -> Maybe Text -> [Field] -> Variant
- Agentic: [variantDoc] :: Variant -> Maybe Text
- Agentic: [variantFields] :: Variant -> [Field]
- Agentic: [variantTag] :: Variant -> Text
- Agentic.Contract: class GConName (f :: Type -> Type)
- Agentic.Contract: gconName :: GConName f => f p -> String
- Agentic.Contract: instance (Agentic.Contract.GConName f, Agentic.Contract.GConName g) => Agentic.Contract.GConName (f GHC.Internal.Generics.:+: g)
- Agentic.Contract: instance Agentic.Contract.GConName f => Agentic.Contract.GConName (GHC.Internal.Generics.M1 GHC.Internal.Generics.D d f)
- Agentic.Contract: instance GHC.Internal.Generics.Constructor c => Agentic.Contract.GConName (GHC.Internal.Generics.M1 GHC.Internal.Generics.C c f)
+ Agentic.Core: toolDescription :: forall (m :: Type -> Type). Tool m -> Text
+ Agentic.Core: toolName :: forall (m :: Type -> Type). Tool m -> Text
- Agentic.Contract: ($dmoptions) :: (Options a, Generic a, GEnum (Rep a), GConName (Rep a)) => OptionSet a
+ Agentic.Contract: ($dmoptions) :: (Options a, Generic a, GEnum (Rep a), Show a) => OptionSet a
- Agentic.Contract: option :: (Generic a, GConName (Rep a)) => a -> Text -> Option a
+ Agentic.Contract: option :: Show a => a -> Text -> Option a

Files

CHANGELOG.md view
@@ -1,5 +1,20 @@ # Changelog for agentic +## 0.2.0.2 - 2026-10-01++* The core now builds and runs under MicroHs as well as GHC. Generic+  deriving of `Contract` and `Options` is GHC only; under MicroHs, write+  contracts with `record`, `required`, `sumOf` and `constructor`.+* `option` takes an option's label from `show` instead of Generics, so it works+  on both compilers. For an enumeration the label is still the constructor's+  name.+* `Tool`'s constructor is positional; `toolName` and `toolDescription` are+  functions.+* `Agentic` no longer exports the constructors of `Shape` and `Variant`, which+  clashed with users' own types. Provider code imports them from+  `Agentic.Schema`.+* A new `agentic-portable-test` suite, which CI also runs under MicroHs.+ ## 0.2.0.1 - 2026-10-01  * The README now appears on the Hackage package page.
README.md view
@@ -21,7 +21,7 @@  | Package | What it's for | |---|---|-| `agentic` | flows, contracts, questions, the runtime and the interpreter (depends only on `base` and `text`) |+| `agentic` | flows, contracts, questions, the runtime and the interpreter (depends only on `base` and `text`, and builds with MicroHs too) | | `agentic-jev` | Jev as System One | | `agentic-anthropic` | Claude as System Two (or System One) | | `agentic-openai` | OpenAI as System Two (or System One) |
agentic.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               agentic-version:            0.2.0.1+version:            0.2.0.2 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.@@ -66,3 +66,14 @@     , base             >=4.18 && <5     , hspec  >=2.10 && <3     , text             >=2.0 && <2.2+-- The core without GHC-only features, in a pure monad. CI also runs this same+-- file under MicroHs.+test-suite agentic-portable-test+  import:           shared+  type:             exitcode-stdio-1.0+  hs-source-dirs:   test+  main-is:          Portable.hs+  build-depends:+    , agentic+    , base+    , text
src/Agentic.hs view
@@ -3,7 +3,7 @@ -- -- This module is for writing and running flows. The fields of the types that -- providers work with (t'Conversation', t'Schema' and t'Field'), and the--- constructors of 'Format', aren't exported here, so they don't clash with your+-- constructors of 'Format', t'Shape' and t'Variant', aren't exported here, so they don't clash with your -- own types; provider code imports "Agentic.Runtime" and "Agentic.Schema" -- directly. module Agentic@@ -26,8 +26,8 @@ import Agentic.Questions import Agentic.Runtime hiding (Conversation (..)) import Agentic.Runtime (Conversation)-import Agentic.Schema hiding (Field (..), Format (..), Schema (..))-import Agentic.Schema (Field, Format, Schema)+import Agentic.Schema hiding (Field (..), Format (..), Schema (..), Shape (..), Variant (..))+import Agentic.Schema (Field, Format, Schema, Shape, Variant) import Agentic.Settings import Agentic.Value import Agentic.ViaLLM
src/Agentic/Contract.hs view
@@ -1,7 +1,14 @@ {-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE CPP #-}  -- | Contracts: two-way codecs with documentation. A contract says how to show a -- value to a model, how to read one back, and what its schema looks like.+--+-- Generic deriving (@deriving (Generic, Contract)@ and+-- @deriving (Generic, Options)@) is GHC only: MicroHs's "GHC.Generics" has no+-- metadata classes to read names from. Under MicroHs, write contracts out+-- with 'record', 'required', 'sumOf' and 'constructor', and options with+-- 'option'. module Agentic.Contract   ( -- * Codecs     Codec (..)@@ -23,12 +30,14 @@   , field   , checked   , between+#ifndef __MHS__     -- * Generic deriving   , genericContract   , GContract (..)   , GCases (..)   , GCase (..)   , GFields (..)+#endif     -- * Enumerations   , Options (..)   , OptionSet (..)@@ -37,18 +46,21 @@   , described   , Enumeration (..)   , enumeration+#ifndef __MHS__   , GEnum (..)-  , GConName (..)+#endif   ) where  import Agentic.Schema import Agentic.Value-import Data.Kind (Type) import Data.List (find) import Data.Maybe (fromMaybe, isJust) import Data.Text (Text) import qualified Data.Text as T+#ifndef __MHS__+import Data.Kind (Type) import GHC.Generics+#endif  -- --------------------------------------------------------------------------- -- Codecs@@ -61,8 +73,10 @@  class Contract a where   contract :: Codec a+#ifndef __MHS__   default contract :: (Generic a, GContract (Rep a)) => Codec a   contract = genericContract+#endif  mapCodec :: (a -> b) -> (b -> a) -> Codec a -> Codec b mapCodec to' from' c = Codec (codecSchema c) (encode c . from') (fmap to' . decode c)@@ -302,6 +316,7 @@ nonEmpty :: Text -> Maybe Text nonEmpty t = if T.null t then Nothing else Just t +#ifndef __MHS__ -- --------------------------------------------------------------------------- -- Generic deriving @@ -389,6 +404,8 @@     | null (selName (undefined :: M1 S s (K1 i a) ())) = Just (mapCodec (M1 . K1) (unK1 . unM1) contract)     | otherwise = Nothing +#endif+ -- --------------------------------------------------------------------------- -- Enumerations @@ -408,18 +425,22 @@ -- @choice@ and @score@ need one. class Options a where   options :: OptionSet a-  default options :: (Generic a, GEnum (Rep a), GConName (Rep a)) => OptionSet a-  options = OptionSet Nothing [Option v (conLabel v) Nothing | v <- map to (genum @(Rep a))]+#ifndef __MHS__+  default options :: (Generic a, GEnum (Rep a), Show a) => OptionSet a+  options = OptionSet Nothing [Option v (label v) Nothing | v <- map to (genum @(Rep a))]+#endif --- | One option, labelled with its constructor name.-option :: (Generic a, GConName (Rep a)) => a -> Text -> Option a-option v d = Option v (conLabel v) (nonEmpty d)+-- | One option, labelled by 'show' (the constructor's name, for an enumeration).+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) -conLabel :: (Generic a, GConName (Rep a)) => a -> Text-conLabel = T.pack . gconName . from+-- | An option's label: what the model sees and answers with. For an+-- enumeration, 'show' gives the constructor's name.+label :: Show a => a -> Text+label = T.pack . show  -- | Use with @deriving via@ to give an 'Options' type a matching 'Contract': --@@ -443,6 +464,7 @@     set = options @a     opts = optionList set +#ifndef __MHS__ class GEnum (f :: Type -> Type) where   genum :: [f p] @@ -454,17 +476,4 @@  instance GEnum (M1 C c U1) where   genum = [M1 U1]--class GConName (f :: Type -> Type) where-  gconName :: f p -> String--instance GConName f => GConName (M1 D d f) where-  gconName (M1 x) = gconName x--instance (GConName f, GConName g) => GConName (f :+: g) where-  gconName = \case-    L1 x -> gconName x-    R1 x -> gconName x--instance Constructor c => GConName (M1 C c f) where-  gconName = conName+#endif
src/Agentic/Core.hs view
@@ -6,6 +6,8 @@     Agentic (..)   , Step (..)   , Tool (..)+  , toolName+  , toolDescription   , Note (..)   , Instruction (..)     -- * Steps@@ -80,7 +82,15 @@  -- | A named flow a model can call. data Tool m where-  Tool :: {toolName :: Text, toolDescription :: Text, toolInput :: Codec i, toolOutput :: Codec o, toolBody :: Agentic m i o} -> Tool m+  -- | A name, a description for the model, the input and output contracts, and+  -- the flow to run.+  Tool :: Text -> Text -> Codec i -> Codec o -> Agentic m i o -> Tool m++toolName :: Tool m -> Text+toolName (Tool name _ _ _ _) = name++toolDescription :: Tool m -> Text+toolDescription (Tool _ description _ _ _) = description  instance Category.Category (Agentic m) where   id = Step Pass
src/Agentic/Describe.hs view
@@ -120,10 +120,10 @@   Leaf Identity -> (seen, [])   Leaf Glue -> (seen, [])   Leaf info -> leaf Nothing info-  Sequence ds -> concat <$> mapAccumL trees seen ds+  Sequence ds -> onSnd concat (mapAccumL trees seen ds)   Together ds ->     let keeping = if any passes ds then "  (keeping its input)" else ""-     in case concat <$> mapAccumL parallel seen (filter (not . passes) ds) of+     in case onSnd concat (mapAccumL parallel seen (filter (not . passes) ds)) of           (seen', [Node t cs]) -> (seen', [Node (t <> keeping) cs])           (seen', ts) -> (seen', [Node ("together" <> keeping) ts])   Halves l r ->@@ -414,7 +414,7 @@       Uses -> ["style=dotted", "arrowhead=none"]       Again -> ["style=dashed"]     str t = "\"" <> inner t <> "\""-    inner = T.concatMap (\case '"' -> "\\\""; '\\' -> "\\\\"; c -> T.singleton c)+    inner = concatMapText (\case '"' -> "\\\""; '\\' -> "\\\\"; c -> T.singleton c)  -- | Is the node with this id inside the box with that id? inBox :: Text -> Text -> [Item] -> Bool@@ -490,3 +490,12 @@       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.)+onSnd :: (b -> c) -> (a, b) -> (a, c)+onSnd f (a, b) = (a, f b)
src/Agentic/Value.hs view
@@ -34,7 +34,7 @@   Object kvs -> "{" <> T.intercalate "," [quote k <> ":" <> renderJson v | (k, v) <- kvs] <> "}"  quote :: Text -> Text-quote s = "\"" <> T.concatMap escape s <> "\""+quote s = "\"" <> concatMapText escape s <> "\""   where     escape = \case       '"' -> "\\\""@@ -48,3 +48,7 @@  lookupField :: Text -> [(Text, Value)] -> Maybe Value lookupField = lookup++-- | 'T.concatMap', which MicroHs's "Data.Text" doesn't provide.+concatMapText :: (Char -> Text) -> Text -> Text+concatMapText f = T.concat . map f . T.unpack
+ test/Portable.hs view
@@ -0,0 +1,162 @@+{-# LANGUAGE GADTs #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}++-- | The core, without GHC-only features: explicit codecs, ordinary combinators,+-- and a pure monad in place of IO. The same file runs under GHC (as the+-- agentic-portable-test suite) and under MicroHs (in CI).+module Main (main) where++import Agentic+import Data.IORef (modifyIORef, newIORef, readIORef)+import Data.Text (Text)+import qualified Data.Text as T+import System.Exit (exitFailure)++-- ---------------------------------------------------------------------------+-- Explicit codecs: a record, and a sum with payloads++data Joke = Joke Text Text+  deriving (Show, Eq)++instance Contract Joke where+  contract =+    record "A joke" $+      Joke+        <$> required "setup" "The setup line" (\(Joke s _) -> s)+        <*> required "punchline" "The line that lands it" (\(Joke _ p) -> p)++data Figure = Circle Double | Rect Double Double+  deriving (Show, Eq)++instance Contract Figure where+  contract =+    sumOf+      "A shape"+      [ constructor "Circle" "A circle" isCircle (Circle <$> required "radius" "" radius)+      , constructor "Rect" "A rectangle" isRect (Rect <$> required "width" "" width <*> required "height" "" height)+      ]+    where+      isCircle = \case Circle _ -> True; _ -> False+      isRect = \case Rect _ _ -> True; _ -> False+      radius = \case Circle r -> r; _ -> 0+      width = \case Rect w _ -> w; _ -> 0+      height = \case Rect _ h -> h; _ -> 0++-- ---------------------------------------------------------------------------+-- A pure monad: a script of model turns, and a log of what happened++data World = World {script :: [Action], logged :: [Text], events :: [Happened]}++newtype Pure a = Pure {runPure :: World -> (a, World)}++instance Functor Pure where+  fmap f (Pure g) = Pure (\w -> let (a, w') = g w in (f a, w'))++instance Applicative Pure where+  pure a = Pure (\w -> (a, w))+  Pure f <*> Pure g = Pure (\w -> let (h, w1) = f w; (a, w2) = g w1 in (h a, w2))++instance Monad Pure where+  Pure g >>= k = Pure (\w -> let (a, w1) = g w in runPure (k a) w1)++say :: Text -> Pure ()+say t = Pure (\w -> ((), w {logged = logged w <> [t]}))++-- | The handlers: scripted turns, fixed judgements, and every event recorded.+handlers :: Runtime Pure+handlers =+  (runtimeWith (\e -> error ("flow error: " <> show e)))+    { systemTwo = SystemTwo $ \_ -> Pure $ \w -> case script w of+        a : rest -> (Turn (Raw Null) a, w {script = rest})+        [] -> error "the script ran out of turns"+    , systemOne = SystemOne $ \request -> pure (map answer (requestQuestions request))+    , observe = \e -> Pure (\w -> ((), w {events = events w <> [happened e]}))+    }+  where+    answer = \case+      AskYesNo _ -> YesNoAnswer 0.9+      AskChoice _ ((l, _) : _) -> ChoiceAnswer l [(l, 1)] 1+      AskChoice _ [] -> ChoiceAnswer "" [] 0+      AskScore _ _ -> ScoreAnswer 1 [(1, 1)] 1++-- ---------------------------------------------------------------------------+-- The flows++countLetters :: Tool Pure+countLetters = tool @Text @Int "count_letters" "Count the letters in some text" $+  act (\t -> say ("count_letters ran on " <> t) >> pure (T.length t))++writeJoke :: Tool Pure+writeJoke = tool @Text @Joke "write_joke" "Write a joke about a topic" (draft @Joke "Write a joke about this topic")++jokeAndFigure :: Agentic Pure Text (Joke, Figure)+jokeAndFigure =+  draftWith @Joke [countLetters, writeJoke] "Write a joke, using the tools"+    &&& draft @Figure "Pick a shape"++review :: Agentic Pure Joke (YesNo, YesNo)+review = judge ((,) <$> yesNo "Is it funny?" <*> yesNo "Is it kind?")++joke :: Joke+joke = Joke "Why was the scarecrow promoted?" "He was outstanding in his field."++-- ---------------------------------------------------------------------------++main :: IO ()+main = do+  failures <- newIORef (0 :: Int)+  let check name ok = do+        putStrLn ((if ok then "ok   " else "FAIL ") <> name)+        if ok then pure () else modifyIORef failures (+ 1)++  -- 1. Explicit codecs for a record and a payload-bearing sum.+  check "a record round-trips through its codec" (decode contract (encode contract joke) == Right joke)+  check "a sum with payloads round-trips" (decode contract (encode contract (Rect 2 3)) == Right (Rect 2 3))+  check "a sum encodes its constructor as a tag" (encode contract (Circle 1) == Object [("tag", String "Circle"), ("radius", Number 1)])+  check "a record missing a field is rejected" (either (const True) (const False) (decode (contract @Joke) (Object [("setup", String "x")])))++  let world0 =+        World+          { script =+              [ CallTools [ToolCall "c1" "count_letters" (String "scarecrow")]+              , CallTools [ToolCall "c2" "write_joke" (String "farms")]+              , Respond (encode contract joke) -- answers the nested write_joke draft+              , Respond (Object [("setup", String "only a setup")]) -- invalid: no punchline+              , Respond (encode contract joke) -- the correction+              , Respond (encode contract (Rect 2 3)) -- the shape+              ]+          , logged = []+          , events = []+          }+      ((result, verdict), world) =+        runPure ((,) <$> interpret handlers jokeAndFigure "scarecrows" <*> interpret handlers review joke) world0+      seen = events world++  -- 2. A scripted tool call runs its typed body, and the draft continues.+  check "the tool's typed body ran with the model's input" (logged world == ["count_letters ran on scarecrow"])+  check "the tool's typed result went back to the model" (any (\case ToolReturned "c1" (ToolOk (Integer 9)) -> True; _ -> False) seen)+  check "the draft continued to a typed response" (result == (joke, Rect 2 3))++  -- 3. A nested drafting tool.+  check "the nested tool ran its own draft" (length [() | Drafting _ <- seen] == 3)+  check "the nested draft's result went back as the tool's result" (any (\case ToolReturned "c2" (ToolOk v) -> decode contract v == Right joke; _ -> False) seen)++  -- 4. Invalid output, then a corrected response.+  check "the invalid output was rejected" (length [() | OutputRejected _ <- seen] == 1)+  check "every scripted turn was used" (null (script world))++  -- 5. An applicative judgement batch: two questions, one request.+  check "two questions went in one request" ([length (requestQuestions r) | Judged r _ <- seen] == [2])+  check "the answers decoded to typed values" (verdict == (YesNo 0.9, YesNo 0.9))++  -- 6. Describing the flow invokes no handlers: describe has no runtime to call.+  let tree = renderTree (describe jokeAndFigure)+  check "describe shows the drafts and both tools" (all (`T.isInfixOf` tree) ["draft @Joke", "tool count_letters  act", "tool write_joke  draft @Joke", "draft @Figure"])++  -- 7. All of the above ran in Pure, not IO.+  n <- readIORef failures+  if n == 0 then putStrLn "all checks passed" else putStrLn (show n <> " checks failed") >> exitFailure
test/Spec.hs view
@@ -2,7 +2,7 @@  import Agentic import Agentic.Scripted-import Agentic.Schema (Field (..), Schema (..))+import Agentic.Schema (Field (..), Schema (..), Shape (..)) import Data.IORef import Data.Text (Text) import qualified Data.Text as T