packages feed

agentic 0.2.0.5 → 0.2.1.0

raw patch · 17 files changed

+1277/−545 lines, 17 filesdep ~agenticPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: agentic

API changes (from Hackage documentation)

- Agentic.Contract: Enumeration :: a -> Enumeration a
- Agentic.Contract: GCase :: Case a -> Maybe (Codec a) -> GCase a
- Agentic.Contract: [_gcaseBare] :: GCase a -> Maybe (Codec a)
- Agentic.Contract: [gcase] :: GCase a -> Case a
- Agentic.Contract: data GCase a
- Agentic.Contract: data ObjectCodec i o
- Agentic.Contract: gbare :: GFields f => Maybe (Codec (f p))
- Agentic.Contract: instance (Agentic.Contract.Options a, GHC.Internal.Classes.Eq a) => Agentic.Contract.Contract (Agentic.Contract.Enumeration a)
- Agentic.Contract: instance (GHC.Internal.Generics.Datatype d, Agentic.Contract.GCases f) => Agentic.Contract.GContract (GHC.Internal.Generics.M1 GHC.Internal.Generics.D d f)
- Agentic.Contract: instance GHC.Internal.Base.Applicative (Agentic.Contract.ObjectCodec i)
- Agentic.Contract: instance GHC.Internal.Base.Functor (Agentic.Contract.ObjectCodec i)
- Agentic.Contract: newtype Enumeration a
+ Agentic: ($dmcontract) :: (Contract a, Generic a, Typeable a, GContract (Rep a)) => Structure a
+ Agentic: ($dmoptions) :: (Options a, Generic a, GEnum (Rep a), Show a) => OptionSet a
+ Agentic: Codec :: Schema -> (a -> Value) -> (Value -> Either Text a) -> Codec a
+ Agentic: Constructor :: Text -> FieldStyle -> Constructor
+ Agentic: OChecked :: Text -> Outline -> Outline
+ Agentic: ODocumented :: Text -> Outline -> Outline
+ Agentic: OList :: Outline -> Outline
+ Agentic: ONamed :: SomeTypeRep -> Outline -> Outline
+ Agentic: OOptional :: Outline -> Outline
+ Agentic: ORecord :: Constructor -> [OutlineField] -> Outline
+ Agentic: OScalar :: SomeTypeRep -> ScalarInfo -> Outline
+ Agentic: OSum :: [OutlineCase] -> Outline
+ Agentic: OUnit :: Outline
+ Agentic: Option :: a -> Text -> Maybe Text -> Option a
+ Agentic: OptionSet :: Maybe Text -> [Option a] -> OptionSet a
+ Agentic: OutlineCase :: Constructor -> Maybe Text -> [OutlineField] -> OutlineCase
+ Agentic: OutlineField :: Text -> Maybe Text -> Outline -> OutlineField
+ Agentic: PositionalFields :: FieldStyle
+ Agentic: ProjectionError :: [Text] -> Text -> ProjectionError
+ Agentic: RecordFields :: FieldStyle
+ Agentic: ScalarInfo :: Text -> [(Text, Text)] -> ScalarInfo
+ Agentic: UnsupportedContract :: [Note] -> ProjectionError -> FlowError
+ Agentic: [constructor] :: OutlineCase -> Constructor
+ Agentic: [decode] :: Codec a -> Value -> Either Text a
+ Agentic: [doc] :: OutlineField -> Maybe Text
+ Agentic: [encode] :: Codec a -> a -> Value
+ Agentic: [fields] :: OutlineCase -> [OutlineField]
+ Agentic: [label] :: Option a -> Text
+ Agentic: [options] :: OptionSet a -> [Option a]
+ Agentic: [parameters] :: ScalarInfo -> [(Text, Text)]
+ Agentic: [problem] :: ProjectionError -> Text
+ Agentic: [schema] :: Codec a -> Schema
+ Agentic: [structure] :: OutlineField -> Outline
+ Agentic: [style] :: Constructor -> FieldStyle
+ Agentic: [value] :: Option a -> a
+ Agentic: between :: (Ord a, Show a) => a -> a -> Structure a -> Structure a
+ Agentic: checked :: Text -> (a -> Bool) -> Structure a -> Structure a
+ Agentic: class Contract a
+ Agentic: class GCases (f :: Type -> Type)
+ Agentic: class GContract (f :: Type -> Type)
+ Agentic: class GEnum (f :: Type -> Type)
+ Agentic: class GFields (f :: Type -> Type)
+ Agentic: class Options a
+ Agentic: compileCaseWith :: (forall x. () => Structure x -> Either ProjectionError (Codec x)) -> Case a -> Either ProjectionError (CaseCodec a)
+ Agentic: compileFieldsWith :: (forall x. () => Structure x -> Either ProjectionError (Codec x)) -> Fields r a -> Either ProjectionError (ObjectCodec r a)
+ Agentic: compileModel :: Structure a -> Either ProjectionError (Codec a)
+ Agentic: compileWith :: (forall x. () => Structure x -> Maybe (Either ProjectionError (Codec x))) -> Structure a -> Either ProjectionError (Codec a)
+ Agentic: constructor :: Text -> Text -> (a -> Bool) -> Fields a a -> Case a
+ Agentic: contract :: Contract a => Structure a
+ Agentic: data Case a
+ Agentic: data Codec a
+ Agentic: data Constructor
+ Agentic: data FieldStyle
+ Agentic: data Fields r a
+ Agentic: data Option a
+ Agentic: data OptionSet a
+ Agentic: data Outline
+ Agentic: data OutlineCase
+ Agentic: data OutlineField
+ Agentic: data ProjectionError
+ Agentic: data ScalarInfo
+ Agentic: data Structure a
+ Agentic: documented :: Text -> Structure a -> Structure a
+ Agentic: documentedOptions :: Text -> [Option a] -> OptionSet a
+ Agentic: enumeration :: (Options a, Eq a) => Structure a
+ Agentic: field :: Text -> Text -> Structure a -> Structure a
+ Agentic: gcases :: GCases f => [Case (f p)]
+ Agentic: gcontract :: GContract f => Structure (f p)
+ Agentic: genericContract :: (Generic a, Typeable a, GContract (Rep a)) => Structure a
+ Agentic: genum :: GEnum f => [f p]
+ Agentic: gfields :: GFields f => Int -> (Int, Fields (f p) (f p))
+ Agentic: lmapObject :: (s -> r) -> Fields r a -> Fields s a
+ Agentic: mapCodec :: (a -> b) -> (b -> a) -> Codec a -> Codec b
+ Agentic: mapStructure :: (a -> b) -> (b -> a) -> Structure a -> Structure b
+ Agentic: nominal :: Typeable a => Structure a -> Structure a
+ Agentic: option :: Show a => a -> Text -> Option a
+ Agentic: optional :: Contract a => Text -> Text -> (r -> Maybe a) -> Fields r (Maybe a)
+ Agentic: options :: Options a => OptionSet a
+ Agentic: outline :: Structure a -> Outline
+ Agentic: outlineLabel :: Outline -> Text
+ Agentic: record :: Text -> Fields a a -> Structure a
+ Agentic: required :: Contract a => Text -> Text -> (r -> a) -> Fields r a
+ Agentic: requiredWith :: Text -> Maybe Text -> Structure a -> (r -> a) -> Fields r a
+ Agentic: reschema :: (Schema -> Schema) -> Codec a -> Codec a
+ Agentic: scalar :: Typeable a => ScalarInfo -> Maybe (Codec a) -> Structure a
+ Agentic: sumOf :: Text -> [Case a] -> Structure a
+ Agentic.Codec: CaseCodec :: Text -> Maybe Text -> [Field] -> (a -> Maybe [(Text, Value)]) -> ([(Text, Value)] -> Either Text a) -> CaseCodec a
+ Agentic.Codec: Codec :: Schema -> (a -> Value) -> (Value -> Either Text a) -> Codec a
+ Agentic.Codec: ObjectCodec :: [Field] -> (i -> [(Text, Value)]) -> ([(Text, Value)] -> Either Text o) -> ObjectCodec i o
+ Agentic.Codec: [decode] :: CaseCodec a -> [(Text, Value)] -> Either Text a
+ Agentic.Codec: [doc] :: CaseCodec a -> Maybe Text
+ Agentic.Codec: [encode] :: CaseCodec a -> a -> Maybe [(Text, Value)]
+ Agentic.Codec: [fields] :: CaseCodec a -> [Field]
+ Agentic.Codec: [schema] :: Codec a -> Schema
+ Agentic.Codec: [tag] :: CaseCodec a -> Text
+ Agentic.Codec: data CaseCodec a
+ Agentic.Codec: data Codec a
+ Agentic.Codec: data ObjectCodec i o
+ Agentic.Codec: instance GHC.Internal.Base.Applicative (Agentic.Codec.ObjectCodec i)
+ Agentic.Codec: instance GHC.Internal.Base.Functor (Agentic.Codec.ObjectCodec i)
+ Agentic.Codec: lmapObject :: (j -> i) -> ObjectCodec i o -> ObjectCodec j o
+ Agentic.Codec: mapCodec :: (a -> b) -> (b -> a) -> Codec a -> Codec b
+ Agentic.Codec: mismatch :: Text -> Value -> Either Text a
+ Agentic.Codec: primitive :: Shape -> (a -> Value) -> (Value -> Either Text a) -> Codec a
+ Agentic.Codec: record :: Text -> ObjectCodec a a -> Codec a
+ Agentic.Codec: requiredWith :: Text -> Maybe Text -> Codec a -> (r -> a) -> ObjectCodec r a
+ Agentic.Codec: reschema :: (Schema -> Schema) -> Codec a -> Codec a
+ Agentic.Codec: sumOf :: Text -> [CaseCodec a] -> Codec a
+ Agentic.Contract: Constructor :: Text -> FieldStyle -> Constructor
+ Agentic.Contract: OChecked :: Text -> Outline -> Outline
+ Agentic.Contract: ODocumented :: Text -> Outline -> Outline
+ Agentic.Contract: OList :: Outline -> Outline
+ Agentic.Contract: ONamed :: SomeTypeRep -> Outline -> Outline
+ Agentic.Contract: OOptional :: Outline -> Outline
+ Agentic.Contract: ORecord :: Constructor -> [OutlineField] -> Outline
+ Agentic.Contract: OScalar :: SomeTypeRep -> ScalarInfo -> Outline
+ Agentic.Contract: OSum :: [OutlineCase] -> Outline
+ Agentic.Contract: OUnit :: Outline
+ Agentic.Contract: OutlineCase :: Constructor -> Maybe Text -> [OutlineField] -> OutlineCase
+ Agentic.Contract: OutlineField :: Text -> Maybe Text -> Outline -> OutlineField
+ Agentic.Contract: PositionalFields :: FieldStyle
+ Agentic.Contract: ProjectionError :: [Text] -> Text -> ProjectionError
+ Agentic.Contract: RecordFields :: FieldStyle
+ Agentic.Contract: ScalarInfo :: Text -> [(Text, Text)] -> ScalarInfo
+ Agentic.Contract: [Apply] :: forall r a1 a. Fields r (a1 -> a) -> Fields r a1 -> Fields r a
+ Agentic.Contract: [Case] :: forall a p. Constructor -> Maybe Text -> (a -> Maybe p) -> (p -> a) -> Fields p p -> Case a
+ Agentic.Contract: [Checked] :: forall a. Text -> (a -> Bool) -> Structure a -> Structure a
+ Agentic.Contract: [Documented] :: forall a. Text -> Structure a -> Structure a
+ Agentic.Contract: [Field] :: forall a r. Text -> Maybe Text -> Structure a -> (r -> a) -> Fields r a
+ Agentic.Contract: [List] :: forall a1. Structure a1 -> Structure [a1]
+ Agentic.Contract: [Mapped] :: forall a1 a. (a1 -> a) -> (a -> a1) -> Structure a1 -> Structure a
+ Agentic.Contract: [Named] :: forall a. TypeRep a -> Structure a -> Structure a
+ Agentic.Contract: [Optional] :: forall a1. Structure a1 -> Structure (Maybe a1)
+ Agentic.Contract: [Pure] :: forall a r. a -> Fields r a
+ Agentic.Contract: [Record] :: forall a. Constructor -> Fields a a -> Structure a
+ Agentic.Contract: [Scalar] :: forall a. Typeable a => ScalarInfo -> Maybe (Codec a) -> Structure a
+ Agentic.Contract: [Sum] :: forall a. [Case a] -> Structure a
+ Agentic.Contract: [Unit] :: Structure ()
+ Agentic.Contract: [constructor] :: OutlineCase -> Constructor
+ Agentic.Contract: [fields] :: OutlineCase -> [OutlineField]
+ Agentic.Contract: [name] :: OutlineField -> Text
+ Agentic.Contract: [parameters] :: ScalarInfo -> [(Text, Text)]
+ Agentic.Contract: [path] :: ProjectionError -> [Text]
+ Agentic.Contract: [problem] :: ProjectionError -> Text
+ Agentic.Contract: [structure] :: OutlineField -> Outline
+ Agentic.Contract: [style] :: Constructor -> FieldStyle
+ Agentic.Contract: compileCaseWith :: (forall x. () => Structure x -> Either ProjectionError (Codec x)) -> Case a -> Either ProjectionError (CaseCodec a)
+ Agentic.Contract: compileFieldsWith :: (forall x. () => Structure x -> Either ProjectionError (Codec x)) -> Fields r a -> Either ProjectionError (ObjectCodec r a)
+ Agentic.Contract: compileModel :: Structure a -> Either ProjectionError (Codec a)
+ Agentic.Contract: compileWith :: (forall x. () => Structure x -> Maybe (Either ProjectionError (Codec x))) -> Structure a -> Either ProjectionError (Codec a)
+ Agentic.Contract: data Constructor
+ Agentic.Contract: data FieldStyle
+ Agentic.Contract: data Fields r a
+ Agentic.Contract: data Outline
+ Agentic.Contract: data OutlineCase
+ Agentic.Contract: data OutlineField
+ Agentic.Contract: data ProjectionError
+ Agentic.Contract: data ScalarInfo
+ Agentic.Contract: data Structure a
+ Agentic.Contract: instance (Agentic.Contract.GCases f, Agentic.Contract.GCases g) => Agentic.Contract.GContract (f GHC.Internal.Generics.:+: g)
+ Agentic.Contract: instance (GHC.Internal.Generics.Constructor c, Agentic.Contract.GFields f) => Agentic.Contract.GContract (GHC.Internal.Generics.M1 GHC.Internal.Generics.C c f)
+ Agentic.Contract: instance Agentic.Contract.GContract GHC.Internal.Generics.V1
+ Agentic.Contract: instance Agentic.Contract.GContract f => Agentic.Contract.GContract (GHC.Internal.Generics.M1 GHC.Internal.Generics.D d f)
+ Agentic.Contract: instance GHC.Internal.Base.Applicative (Agentic.Contract.Fields r)
+ Agentic.Contract: instance GHC.Internal.Base.Functor (Agentic.Contract.Fields r)
+ Agentic.Contract: instance GHC.Internal.Classes.Eq Agentic.Contract.Constructor
+ Agentic.Contract: instance GHC.Internal.Classes.Eq Agentic.Contract.FieldStyle
+ Agentic.Contract: instance GHC.Internal.Classes.Eq Agentic.Contract.Outline
+ Agentic.Contract: instance GHC.Internal.Classes.Eq Agentic.Contract.OutlineCase
+ Agentic.Contract: instance GHC.Internal.Classes.Eq Agentic.Contract.OutlineField
+ Agentic.Contract: instance GHC.Internal.Classes.Eq Agentic.Contract.ProjectionError
+ Agentic.Contract: instance GHC.Internal.Classes.Eq Agentic.Contract.ScalarInfo
+ Agentic.Contract: instance GHC.Internal.Show.Show Agentic.Contract.Constructor
+ Agentic.Contract: instance GHC.Internal.Show.Show Agentic.Contract.FieldStyle
+ Agentic.Contract: instance GHC.Internal.Show.Show Agentic.Contract.Outline
+ Agentic.Contract: instance GHC.Internal.Show.Show Agentic.Contract.OutlineCase
+ Agentic.Contract: instance GHC.Internal.Show.Show Agentic.Contract.OutlineField
+ Agentic.Contract: instance GHC.Internal.Show.Show Agentic.Contract.ProjectionError
+ Agentic.Contract: instance GHC.Internal.Show.Show Agentic.Contract.ScalarInfo
+ Agentic.Contract: mapStructure :: (a -> b) -> (b -> a) -> Structure a -> Structure b
+ Agentic.Contract: nominal :: Typeable a => Structure a -> Structure a
+ Agentic.Contract: outline :: Structure a -> Outline
+ Agentic.Contract: outlineLabel :: Outline -> Text
+ Agentic.Contract: scalar :: Typeable a => ScalarInfo -> Maybe (Codec a) -> Structure a
+ Agentic.Interpret: preflight :: forall (m :: Type -> Type) i o. Agentic m i o -> Either FlowError ()
+ Agentic.Runtime: UnsupportedContract :: [Note] -> ProjectionError -> FlowError
+ Agentic.Scripted: modelContract :: Contract a => Codec a
- Agentic.Contract: ($dmcontract) :: (Contract a, Generic a, GContract (Rep a)) => Codec a
+ Agentic.Contract: ($dmcontract) :: (Contract a, Generic a, Typeable a, GContract (Rep a)) => Structure a
- Agentic.Contract: between :: (Ord a, Show a) => a -> a -> Codec a -> Codec a
+ Agentic.Contract: between :: (Ord a, Show a) => a -> a -> Structure a -> Structure a
- Agentic.Contract: checked :: Text -> (a -> Bool) -> Codec a -> Codec a
+ Agentic.Contract: checked :: Text -> (a -> Bool) -> Structure a -> Structure a
- Agentic.Contract: constructor :: Text -> Text -> (a -> Bool) -> ObjectCodec a a -> Case a
+ Agentic.Contract: constructor :: Text -> Text -> (a -> Bool) -> Fields a a -> Case a
- Agentic.Contract: contract :: Contract a => Codec a
+ Agentic.Contract: contract :: Contract a => Structure a
- Agentic.Contract: documented :: Text -> Codec a -> Codec a
+ Agentic.Contract: documented :: Text -> Structure a -> Structure a
- Agentic.Contract: enumeration :: (Options a, Eq a) => Codec a
+ Agentic.Contract: enumeration :: (Options a, Eq a) => Structure a
- Agentic.Contract: field :: Text -> Text -> Codec a -> Codec a
+ Agentic.Contract: field :: Text -> Text -> Structure a -> Structure a
- Agentic.Contract: gcases :: GCases f => [GCase (f p)]
+ Agentic.Contract: gcases :: GCases f => [Case (f p)]
- Agentic.Contract: gcontract :: GContract f => Codec (f p)
+ Agentic.Contract: gcontract :: GContract f => Structure (f p)
- Agentic.Contract: genericContract :: (Generic a, GContract (Rep a)) => Codec a
+ Agentic.Contract: genericContract :: (Generic a, Typeable a, GContract (Rep a)) => Structure a
- Agentic.Contract: gfields :: GFields f => Int -> (Int, ObjectCodec (f p) (f p))
+ Agentic.Contract: gfields :: GFields f => Int -> (Int, Fields (f p) (f p))
- Agentic.Contract: lmapObject :: (j -> i) -> ObjectCodec i o -> ObjectCodec j o
+ Agentic.Contract: lmapObject :: (s -> r) -> Fields r a -> Fields s a
- Agentic.Contract: optional :: Contract a => Text -> Text -> (r -> Maybe a) -> ObjectCodec r (Maybe a)
+ Agentic.Contract: optional :: Contract a => Text -> Text -> (r -> Maybe a) -> Fields r (Maybe a)
- Agentic.Contract: record :: Text -> ObjectCodec a a -> Codec a
+ Agentic.Contract: record :: Text -> Fields a a -> Structure a
- Agentic.Contract: required :: Contract a => Text -> Text -> (r -> a) -> ObjectCodec r a
+ Agentic.Contract: required :: Contract a => Text -> Text -> (r -> a) -> Fields r a
- Agentic.Contract: requiredWith :: Text -> Maybe Text -> Codec a -> (r -> a) -> ObjectCodec r a
+ Agentic.Contract: requiredWith :: Text -> Maybe Text -> Structure a -> (r -> a) -> Fields r a
- Agentic.Contract: sumOf :: Text -> [Case a] -> Codec a
+ Agentic.Contract: sumOf :: Text -> [Case a] -> Structure a
- Agentic.Core: [Draft] :: forall i o (m :: Type -> Type). Codec i -> Codec o -> Instruction -> [Tool m] -> Step m i o
+ Agentic.Core: [Draft] :: forall i o (m :: Type -> Type). Structure i -> Structure o -> Instruction -> [Tool m] -> Step m i o
- Agentic.Core: [Judge] :: forall i o (m :: Type -> Type). Codec i -> Questions o -> Step m i o
+ Agentic.Core: [Judge] :: forall i o (m :: Type -> Type). Structure i -> Questions o -> Step m i o
- Agentic.Core: [Tool] :: forall i o (m :: Type -> Type). Text -> Text -> Codec i -> Codec o -> Agentic m i o -> Tool m
+ Agentic.Core: [Tool] :: forall i o (m :: Type -> Type). Text -> Text -> Structure i -> Structure o -> Agentic m i o -> Tool m
- Agentic.Describe: DraftInfo :: Instruction -> Schema -> Schema -> [ToolInfo] -> StepInfo
+ Agentic.Describe: DraftInfo :: Instruction -> Outline -> Outline -> [ToolInfo] -> StepInfo
- Agentic.Describe: JudgeInfo :: Schema -> [QuestionSpec] -> StepInfo
+ Agentic.Describe: JudgeInfo :: Outline -> [QuestionSpec] -> StepInfo
- Agentic.Describe: ToolInfo :: Text -> Text -> Schema -> Schema -> Description -> ToolInfo
+ Agentic.Describe: ToolInfo :: Text -> Text -> Outline -> Outline -> Description -> ToolInfo
- Agentic.Describe: [input] :: ToolInfo -> Schema
+ Agentic.Describe: [input] :: ToolInfo -> Outline
- Agentic.Describe: [output] :: ToolInfo -> Schema
+ Agentic.Describe: [output] :: ToolInfo -> Outline

Files

CHANGELOG.md view
@@ -1,5 +1,40 @@ # Changelog for agentic +## 0.2.1.0 - 2026-11-06++* `Contract` now produces a typed `Structure`, shared by generic derivation+  and handwritten builders. `outline` exposes nominal/applied identities,+  constructor/field order and style, and extensible scalar parameters.+* `compileModel` and open typed `compileWith` produce fallible codecs. Scalar+  definitions can supply model defaults; independent adapters can override both+  scalar and structural representations, including upstream types.+* Flow interpretation preflights all contracts and tool bodies before effects+  or provider calls. Unsupported projections raise structured+  `UnsupportedContract [Note] ProjectionError` failures;+  discovery remains usable. Lossy nested model optionals are rejected.+* Structural mappings use `mapStructure`; compiled mappings retain `mapCodec`.+  Replace `deriving via Enumeration T` with `contract = enumeration` in an+  explicit `Contract T` instance. Flow descriptions now carry `Outline`.+  Direct codec consumers must call `compileModel` (or another interpreter)+  before accessing `.schema`, `.encode`, or `.decode`. `reschema` modifies+  compiled codecs; `documented`, `field`, and `checked` modify contract trees.+  Generic traversal helpers now produce typed trees, cases and fields rather+  than compiled codecs. Handwritten nominal contracts can use+  `nominal`; generic derivation preserves their identity automatically.+* Expose compiled builders in `Agentic.Codec` and reusable+  `compileFieldsWith`/`compileCaseWith` helpers. Sum cases use existential payloads+  with total matching; singleton constructors retain product structure.+* Retire MicroHs support in favor of GHC generics and type evidence. Keep the+  handwritten-contract workflow suite on GHC.+* Add independent-projection acceptance fixtures under `agentic/test`, covering+  exact values, malformed scalars, nested containers and preflight failures.+* `Outline` omits representation-only mappings. `OutlineField`, `OutlineCase`+  and existential `Case` expose named record fields. Applicative fields use+  `Pure`, `Apply` and `Field`; compiled sum cases are `CaseCodec`.+* `field` applies its annotation immediately and reports an absent field at+  first evaluation. Codec-level annotation/check builders are no longer public.+* Share the test-fixture `modelContract` helper through `Agentic.Scripted`.+ ## 0.2.0.5 - 2026-10-06  * `takeFirst` and `takeSecond` do what `arr fst` and `arr snd` do, but a
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`, and builds with MicroHs too) |+| `agentic` | flows, contracts, questions, the runtime and the interpreter (depends only on `base` and `text`; GHC 9.6+) | | `agentic-jev` | Jev as System One | | `agentic-anthropic` | Claude as System Two (or System One) | | `agentic-openai` | OpenAI as System Two (or System One) |@@ -64,8 +64,8 @@   Wrap  :: (i -> o)                                            -> Step m i o  -- Left, Right   Arr   :: (i -> o)                                            -> Step m i o   Act   :: (i -> m o)                                          -> Step m i o-  Draft :: Codec i -> Codec o -> Instruction -> [Tool m]       -> Step m i o-  Judge :: Codec i -> Questions o                              -> Step m i o+  Draft :: Structure i -> Structure o -> Instruction -> [Tool m]       -> Step m i o+  Judge :: Structure i -> Questions o                              -> Step m i o  data Agentic m i o where   Step   :: Step m i o                     -> Agentic m i o@@ -159,6 +159,30 @@ And describe an enumeration once - Claude and Jev both see the same wording (see `Options` below). +### One contract, several representations++A contract is a typed description of the structure. The model codec is one+interpretation of it; a storage adapter can choose another, with its own+encoding and validation. You still derive `Contract` once.++Normal `draft`, `judge`, and `tool` calls handle compilation for you. When you+want a codec directly:++```haskell+case compileModel (contract @Joke) of+  Left problem -> print problem+  Right codec -> print (codec.encode joke)+```++Custom scalars keep their identity inside records and containers. They can+supply a default model codec, and another library can supply a different+projection without changing their definition. `outline` exposes the+structure even when no model representation is available.++`interpret` checks the whole flow before running effects, including unused+branches and tool results. Unsupported model projections fail there. The core+now requires GHC; MicroHs support has been retired.+ ## Is it actually funny?  An LLM is great at writing. Jev is great at judging - quickly, and with a@@ -192,7 +216,7 @@     , option Solid      "An audible groan from most of the room"     , option Unbearable "People get up and leave" ] -deriving via Enumeration Groan instance Contract Groan+instance Contract Groan where contract = enumeration  groan :: Questions (Score Groan) groan = score "How much will the audience groan?"
agentic.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               agentic-version:            0.2.0.5+version:            0.2.1.0 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.@@ -45,6 +45,7 @@   exposed-modules:     Agentic     Agentic.Contract+    Agentic.Codec     Agentic.Core     Agentic.Describe     Agentic.Interpret@@ -65,12 +66,11 @@   hs-source-dirs:   test   main-is:          Spec.hs   build-depends:-    , agentic          ==0.2.*+    , agentic          >=0.2.1 && <0.3     , 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.+-- Handwritten contracts and interpreter behavior in a pure monad. test-suite agentic-portable-test   import:           shared   type:             exitcode-stdio-1.0@@ -80,3 +80,16 @@     , agentic     , base     , text++-- Independent projection adapters and acceptance fixtures.+test-suite agentic-projection-test+  import:           shared+  type:             exitcode-stdio-1.0+  hs-source-dirs:   test+  main-is:          ProjectionSpec.hs+  other-modules:    ProjectionExample+  build-depends:+    , agentic          >=0.2.1 && <0.3+    , base             >=4.18 && <5+    , hspec            >=2.10 && <3+    , text             >=2.0 && <2.2
src/Agentic.hs view
@@ -19,15 +19,16 @@   , module Agentic.Settings   ) where -import Agentic.Contract+import Agentic.Contract (Case, Structure, Fields)+import Agentic.Contract hiding (Case (..), Structure (..), Fields (..)) import Agentic.Core import Agentic.Describe import Agentic.Interpret import Agentic.Questions-import Agentic.Runtime hiding (Conversation (..)) import Agentic.Runtime (Conversation)-import Agentic.Schema hiding (Field (..), Format (..), Schema (..), Shape (..), Variant (..))+import Agentic.Runtime hiding (Conversation (..)) import Agentic.Schema (Field, Format, Schema, Shape, Variant)+import Agentic.Schema hiding (Field (..), Format (..), Schema (..), Shape (..), Variant (..)) import Agentic.Settings import Agentic.Value import Agentic.ViaLLM
+ src/Agentic/Codec.hs view
@@ -0,0 +1,149 @@+-- | Compiled codecs and builders for projection interpreters. Structural contracts live in+-- "Agentic.Contract"; this module contains no contract instances.+module Agentic.Codec+  ( Codec (..)+  , mapCodec+  , reschema+  , primitive+  , mismatch+  , ObjectCodec (..)+  , record+  , requiredWith+  , lmapObject+  , CaseCodec (..)+  , sumOf+  ) where++import Agentic.Schema+import Agentic.Value+import Data.List (find)+import Data.Maybe (isJust)+import Data.Text (Text)+import Data.Text qualified as T++-- | A compiled schema with a matching typed encoder and validating decoder.+data Codec a = Codec+  { schema :: Schema+  , encode :: a -> Value+  , decode :: Value -> Either Text a+  }++-- | Change the Haskell value type using inverse mappings, retaining the schema.+mapCodec :: (a -> b) -> (b -> a) -> Codec a -> Codec b+mapCodec to' from' c = Codec c.schema (c.encode . from') (fmap to' . c.decode)++-- | Change a codec's schema. (A record update can't do this: t'Field' has a+-- @schema@ too, so @c {schema = ...}@ is ambiguous.)+reschema :: (Schema -> Schema) -> Codec a -> Codec a+reschema f (Codec s e d) = Codec (f s) e d++-- | Build a leaf codec from its projected shape and typed conversion functions.+primitive :: Shape -> (a -> Value) -> (Value -> Either Text a) -> Codec a+primitive s = Codec (schemaOf s)++-- | Report an unexpected value with its expected shape and JSON rendering.+mismatch :: Text -> Value -> Either Text a+mismatch expected v = Left ("expected " <> expected <> ", got " <> renderJson v)++-- | Compiled fields: encode an input object and applicatively reconstruct an+-- output object. Field order is retained for schemas and encoded values.+data ObjectCodec i o = ObjectCodec+  { fields :: [Field]+  , encode :: i -> [(Text, Value)]+  , decode :: [(Text, Value)] -> Either Text o+  }++instance Functor (ObjectCodec i) where+  fmap f (ObjectCodec fs e d) = ObjectCodec fs e (fmap f . d)++instance Applicative (ObjectCodec i) where+  pure x = ObjectCodec [] (const []) (const (Right x))+  f <*> x =+    ObjectCodec+      (f.fields <> x.fields)+      (\i -> f.encode i <> x.encode i)+      (\kvs -> f.decode kvs <*> x.decode kvs)++-- | Adapt the input supplied to a compiled object's field getters.+lmapObject :: (j -> i) -> ObjectCodec i o -> ObjectCodec j o+lmapObject g (ObjectCodec fs e d) = ObjectCodec fs (e . g) d++-- | A field with an explicit codec. Nullable fields ('Maybe') may be absent.+requiredWith :: Text -> Maybe Text -> Codec a -> (r -> a) -> ObjectCodec r a+requiredWith name d c get =+  ObjectCodec+    [Field name schema (not nullable)]+    (\r -> [(name, c.encode (get r))])+    ( \kvs -> case lookupField name kvs of+        Just v -> prefix (c.decode v)+        Nothing+          | nullable -> prefix (c.decode Null)+          | otherwise -> Left ("missing field " <> name)+    )+ where+  schema = maybe id documentedSchema d c.schema+  nullable = case c.schema.shape of+    SNullable _ -> True+    _ -> False+  prefix = either (\e -> Left (name <> ": " <> e)) Right++-- | Close compiled fields into a documented record codec.+record :: Text -> ObjectCodec a a -> Codec a+record d o =+  Codec+    (documentedSchema' (nonEmpty d) (schemaOf (SObject o.fields)))+    (Object . o.encode)+    ( \case+        Object kvs -> o.decode kvs+        v -> mismatch "an object" v+    )++-- ---------------------------------------------------------------------------+-- Sums++-- | One constructor of a sum type.+data CaseCodec a = CaseCodec+  { tag :: Text+  , doc :: Maybe Text+  , fields :: [Field]+  , encode :: a -> Maybe [(Text, Value)]+  , decode :: [(Text, Value)] -> Either Text a+  }++-- | A sum type. If no constructor has fields, it's encoded as an enumeration of+-- tags; otherwise each value is an object with a @tag@ field.+sumOf :: Text -> [CaseCodec a] -> Codec a+sumOf d = sumCodec (nonEmpty d)++sumCodec :: Maybe Text -> [CaseCodec a] -> Codec a+sumCodec d cases+  | all (null . (.fields)) cases =+      Codec+        (documentedSchema' d (schemaOf (SEnum [(c.tag, c.doc) | c <- cases])))+        (\a -> maybe Null (String . (.tag)) (matching a))+        ( \case+            String t | Just c <- byTag t -> c.decode []+            v -> mismatch ("one of " <> T.intercalate ", " (map (.tag) cases)) v+        )+  | otherwise =+      Codec+        (documentedSchema' d (schemaOf (SSum [Variant c.tag c.doc c.fields | c <- cases])))+        ( \a -> case [(c.tag, kvs) | c <- cases, Just kvs <- [c.encode a]] of+            (t, kvs) : _ -> Object (("tag", String t) : kvs)+            [] -> Null+        )+        ( \case+            Object kvs+              | Just (String t) <- lookupField "tag" kvs ->+                  maybe (Left ("unknown tag " <> t)) (\c -> c.decode kvs) (byTag t)+            v -> mismatch "an object with a tag" v+        )+ where+  matching a = find (\c -> isJust (c.encode a)) cases+  byTag t = find ((== t) . (.tag)) cases++documentedSchema' :: Maybe Text -> Schema -> Schema+documentedSchema' = maybe id documentedSchema++nonEmpty :: Text -> Maybe Text+nonEmpty t = if T.null t then Nothing else Just t
src/Agentic/Contract.hs view
@@ -1,437 +1,524 @@ {-# LANGUAGE AllowAmbiguousTypes #-}-{-# LANGUAGE CPP #-}+{-# LANGUAGE TypeOperators #-} --- | 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', 'Agentic.Contract.required', 'sumOf' and 'constructor', and--- options with 'option'.+-- | One typed structural contract, interpreted into independently chosen codecs. module Agentic.Contract-  ( -- * Codecs-    Codec (..)-  , Contract (..)+  ( Contract (..)+  , Structure (..)+  , Fields (..)+  , Case (..)+  , Constructor (..)+  , FieldStyle (..)+  , ScalarInfo (..)+  , Outline (..)+  , OutlineField (..)+  , OutlineCase (..)+  , Codec (..)+  , ProjectionError (..)+  , compileModel+  , compileWith+  , compileFieldsWith+  , compileCaseWith+  , outline+  , outlineLabel+  , scalar+  , nominal+  , mapStructure   , mapCodec   , reschema-    -- * Records-  , ObjectCodec   , record   , required   , requiredWith   , optional   , lmapObject-    -- * Sums-  , Case   , sumOf   , constructor-    -- * Adjusting contracts   , documented   , field   , checked   , between-#ifndef __MHS__-    -- * Generic deriving   , genericContract   , GContract (..)   , GCases (..)-  , GCase (..)   , GFields (..)-#endif-    -- * Enumerations+  , GEnum (..)   , Options (..)   , OptionSet (..)   , Option (..)   , option   , documentedOptions-  , Enumeration (..)   , enumeration-#ifndef __MHS__-  , GEnum (..)-#endif   ) where -import Agentic.Schema+import Agentic.Codec (Codec (..), mapCodec, reschema)+import Agentic.Codec qualified as C+import Agentic.Schema hiding (Field)+import Agentic.Schema qualified as Schema import Agentic.Value-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--data Codec a = Codec-  { schema :: Schema-  , encode :: a -> Value-  , decode :: Value -> Either Text a-  }+import Data.Text (Text)+import Data.Text qualified as T+import GHC.Generics hiding (Constructor)+import GHC.Generics qualified+import Type.Reflection -class Contract a where-  contract :: Codec a-#ifndef __MHS__-  default contract :: (Generic a, GContract (Rep a)) => Codec a-  contract = genericContract-#endif+-- | Stable semantic identity chosen by the definer and explicit parameters.+-- The identity need not be a Haskell module path. Type evidence is+-- carried separately by 'Scalar'; do not identify scalars by an unqualified name.+data ScalarInfo = ScalarInfo {name :: Text, parameters :: [(Text, Text)]}+  deriving (Eq, Show) -mapCodec :: (a -> b) -> (b -> a) -> Codec a -> Codec b-mapCodec to' from' c = Codec c.schema (c.encode . from') (fmap to' . c.decode)+-- | Whether the author declared named record fields or positional arguments.+data FieldStyle = RecordFields | PositionalFields deriving (Eq, Show) --- | Change a codec's schema. (A record update can't do this: t'Field' has a--- @schema@ too, so @c {schema = ...}@ is ambiguous.)-reschema :: (Schema -> Schema) -> Codec a -> Codec a-reschema f (Codec s e d) = Codec (f s) e d+-- | The declared constructor name and field style, independent of wire layout.+data Constructor = Constructor {name :: Text, style :: FieldStyle} deriving (Eq, Show) -primitive :: Shape -> (a -> Value) -> (Value -> Either Text a) -> Codec a-primitive s = Codec (schemaOf s)+-- | Executable structure. No projection is chosen by constructing this tree.+-- Mappings preserve the inner structure; use Named for a nominal wrapper and+-- Scalar for a deliberately opaque semantic leaf.+data Structure a where+  Scalar :: Typeable a => ScalarInfo -> Maybe (Codec a) -> Structure a+  Named :: TypeRep a -> Structure a -> Structure a+  Record :: Constructor -> Fields a a -> Structure a+  Sum :: [Case a] -> Structure a+  List :: Structure a -> Structure [a]+  Optional :: Structure a -> Structure (Maybe a)+  Unit :: Structure ()+  Mapped :: (a -> b) -> (b -> a) -> Structure a -> Structure b+  Checked :: Text -> (a -> Bool) -> Structure a -> Structure a+  Documented :: Text -> Structure a -> Structure a -mismatch :: Text -> Value -> Either Text a-mismatch expected v = Left ("expected " <> expected <> ", got " <> renderJson v)+-- | Applicative fields retain getters, reconstruction, and declaration order.+data Fields r a where+  Pure :: a -> Fields r a+  Apply :: Fields r (a -> b) -> Fields r a -> Fields r b+  Field :: Text -> Maybe Text -> Structure a -> (r -> a) -> Fields r a -instance Contract Text where-  contract = primitive (SString Nothing) String $ \case-    String s -> Right s-    v -> mismatch "text" v+instance Functor (Fields r) where+  fmap f x = Apply (Pure f) x+instance Applicative (Fields r) where+  pure = Pure+  (<*>) = Apply -instance Contract Bool where-  contract = primitive SBool Bool $ \case-    Bool b -> Right b-    v -> mismatch "a boolean" v+-- | Adapt the input seen by field getters, retaining their order and reconstruction.+lmapObject :: (s -> r) -> Fields r a -> Fields s a+lmapObject f = \case+  Pure a -> Pure a+  Apply g x -> Apply (lmapObject f g) (lmapObject f x)+  Field n d c get -> Field n d c (get . f) -instance Contract Integer where-  contract = primitive SInteger Integer $ \case-    Integer n -> Right n-    Number d | d == fromInteger (round d) -> Right (round d)-    v -> mismatch "an integer" v+-- | A sum case exposes its payload only after successful matching. The payload+-- type is existential: generic field getters cannot be applied to another arm.+data Case a where+  Case ::+    { constructor :: Constructor+    , doc :: Maybe Text+    , match :: a -> Maybe p+    , inject :: p -> a+    , fields :: Fields p p+    } ->+    Case a -instance Contract Int where-  contract = mapCodec fromInteger toInteger contract+-- | A single structural authority for discovery and independently compiled codecs.+-- Derive with Generic, or build a tree explicitly with record, sumOf and scalar.+class Contract a where+  -- | The typed description; no storage or model projection is selected yet.+  contract :: Structure a+  default contract :: (Generic a, Typeable a, GContract (Rep a)) => Structure a+  contract = genericContract -instance Contract Double where-  contract = primitive SNumber Number $ \case-    Number d -> Right d-    Integer n -> Right (fromInteger n)-    v -> mismatch "a number" v+-- | Define an identifiable semantic leaf with optional default model behavior.+-- Other interpreters can supply codecs even when that default is absent.+scalar :: Typeable a => ScalarInfo -> Maybe (Codec a) -> Structure a+scalar = Scalar -instance Contract () where-  contract = primitive SNull (const Null) (const (Right ()))+-- | Preserve a type's resolved nominal identity and applied arguments. Useful+-- around handwritten contracts and mappings; generic derivation adds it itself.+nominal :: forall a. Typeable a => Structure a -> Structure a+nominal = Named (typeRep @a) -instance Contract a => Contract [a] where-  contract =-    let c = contract @a-     in Codec-          (schemaOf (SArray c.schema))-          (Array . map c.encode)-          ( \case-              Array vs -> traverse c.decode vs-              v -> mismatch "a list" v-          )+-- | Change the Haskell representation without discarding the inner structure.+-- The functions should be inverse on supported values. Wrap with nominal+-- when the outer type has a nominal identity that discovery must preserve.+mapStructure :: (a -> b) -> (b -> a) -> Structure a -> Structure b+mapStructure = Mapped -instance Contract a => Contract (Maybe a) where-  contract =-    let c = contract @a-     in Codec-          (schemaOf (SNullable c.schema))-          (maybe Null c.encode)-          ( \case-              Null -> Right Nothing-              v -> Just <$> c.decode v-          )+-- | Close applicative fields into a documented record description. Use Named+-- (or nominal) when a handwritten record also needs nominal identity.+record :: Text -> Fields a a -> Structure a+record d = documented d . Record (Constructor "" RecordFields) -instance (Contract a, Contract b) => Contract (a, b) where-  contract =-    record "A pair" $-      (,) <$> required "_1" "" fst <*> required "_2" "" snd+-- | A named, documented field using its type's contract and a getter.+required :: Contract a => Text -> Text -> (r -> a) -> Fields r a+required n d = requiredWith n (nonEmpty d) contract -instance (Contract a, Contract b, Contract c) => Contract (a, b, c) where-  contract =-    record "A triple" $-      (,,)-        <$> required "_1" "" (\(a, _, _) -> a)-        <*> required "_2" "" (\(_, b, _) -> b)-        <*> required "_3" "" (\(_, _, c) -> c)+-- | A named field with an explicit structural description. Missing-field+-- behavior is chosen by the projection; the model permits absent Maybe fields.+requiredWith :: Text -> Maybe Text -> Structure a -> (r -> a) -> Fields r a+requiredWith = Field --- ------------------------------------------------------------------------------ Records+-- | A Maybe field. Models use null/absence; other projections may use tags.+optional :: Contract a => Text -> Text -> (r -> Maybe a) -> Fields r (Maybe a)+optional = required --- | The fields of an object: encodes an @i@, decodes an @o@. Build one--- applicatively with 'Agentic.Contract.required', then close it with 'record'.-data ObjectCodec i o = ObjectCodec-  { fields :: [Field]-  , encode :: i -> [(Text, Value)]-  , decode :: [(Text, Value)] -> Either Text o-  }+-- | A handwritten sum case with a predicate and applicative fields. Getters+-- must be defined for every value accepted by the predicate. For typed payload+-- matching and injection, use the existential Case constructor directly.+constructor :: Text -> Text -> (a -> Bool) -> Fields a a -> Case a+constructor n d matches = Case (Constructor n RecordFields) (nonEmpty d) (\a -> if matches a then Just a else Nothing) id -instance Functor (ObjectCodec i) where-  fmap f (ObjectCodec fs e d) = ObjectCodec fs e (fmap f . d)+-- | Describe an ordered set of sum cases. The interpreter chooses their layout.+sumOf :: Text -> [Case a] -> Structure a+sumOf d = documented d . Sum -instance Applicative (ObjectCodec i) where-  pure x = ObjectCodec [] (const []) (const (Right x))-  f <*> x =-    ObjectCodec-      (f.fields <> x.fields)-      (\i -> f.encode i <> x.encode i)-      (\kvs -> f.decode kvs <*> x.decode kvs)+-- | Describe the whole contract, without selecting a representation.+documented :: Text -> Structure a -> Structure a+documented "" = id+documented d = Documented d -lmapObject :: (j -> i) -> ObjectCodec i o -> ObjectCodec j o-lmapObject g (ObjectCodec fs e d) = ObjectCodec fs (e . g) d+-- | Describe one field of a record or each matching sum arm. Annotation is+-- applied immediately so it follows layout changes. A missing field raises an+-- error when the annotated structure is first evaluated.+field :: Text -> Text -> Structure a -> Structure a+field n d s = case annotateField n d s of+  Just annotated -> annotated+  Nothing -> error ("Agentic.Contract.field: no field named " <> T.unpack n) --- | A described field, using the field type's contract.-required :: Contract a => Text -> Text -> (r -> a) -> ObjectCodec r a-required name d = requiredWith name (nonEmpty d) contract+-- | Share a semantic predicate across projections. Decoders reject values that+-- fail it; model schemas also carry the rule as explanatory text.+checked :: Text -> (a -> Bool) -> Structure a -> Structure a+checked = Checked --- | A field with an explicit codec. Nullable fields ('Maybe') may be absent.-requiredWith :: Text -> Maybe Text -> Codec a -> (r -> a) -> ObjectCodec r a-requiredWith name d c get =-  ObjectCodec-    [Field name schema (not nullable)]-    (\r -> [(name, c.encode (get r))])-    ( \kvs -> case lookupField name kvs of-        Just v -> prefix (c.decode v)-        Nothing-          | nullable -> prefix (c.decode Null)-          | otherwise -> Left ("missing field " <> name)-    )-  where-    schema = maybe id documentedSchema d c.schema-    nullable = case c.schema.shape of-      SNullable _ -> True-      _ -> False-    prefix = either (\e -> Left (name <> ": " <> e)) Right+-- | Require a decoded value to lie within the inclusive bounds.+between :: (Ord a, Show a) => a -> a -> Structure a -> Structure a+between lo hi = checked ("between " <> T.pack (show lo) <> " and " <> T.pack (show hi)) (\a -> a >= lo && a <= hi) --- | A field that may be absent.-optional :: Contract a => Text -> Text -> (r -> Maybe a) -> ObjectCodec r (Maybe a)-optional = required+nonEmpty :: Text -> Maybe Text+nonEmpty t = if T.null t then Nothing else Just t --- | Close an object codec into a contract for a record.-record :: Text -> ObjectCodec a a -> Codec a-record d o =-  Codec-    (documentedSchema' (nonEmpty d) (schemaOf (SObject o.fields)))-    (Object . o.encode)-    ( \case-        Object kvs -> o.decode kvs-        v -> mismatch "an object" v-    )+-- | Pure discovery data derived from the structure, independent of any codec.+-- Representation-only mappings are omitted.+data Outline+  = OScalar SomeTypeRep ScalarInfo+  | ONamed SomeTypeRep Outline+  | ORecord Constructor [OutlineField]+  | OSum [OutlineCase]+  | OList Outline+  | OOptional Outline+  | OUnit+  | OChecked Text Outline+  | ODocumented Text Outline+  deriving (Eq, Show) --- ------------------------------------------------------------------------------ Sums+-- | A field's name, optional documentation, and structural outline.+data OutlineField = OutlineField+  { name :: Text+  , doc :: Maybe Text+  , structure :: Outline+  }+  deriving (Eq, Show) --- | One constructor of a sum type.-data Case a = Case-  { tag :: Text+-- | Constructor metadata, optional documentation, and fields in declaration order.+data OutlineCase = OutlineCase+  { constructor :: Constructor   , doc :: Maybe Text-  , fields :: [Field]-  , encode :: a -> Maybe [(Text, Value)]-  , decode :: [(Text, Value)] -> Either Text a+  , fields :: [OutlineField]   }+  deriving (Eq, Show) --- | A constructor: its tag, a description, how to recognise it, and its fields.------ > constructor "OneLiner" "A single line" isOneLiner (OneLiner <$> required "line" "" line)-constructor :: Text -> Text -> (a -> Bool) -> ObjectCodec a a -> Case a-constructor tag d matches o =-  Case-    tag-    (nonEmpty d)-    o.fields-    (\a -> if matches a then Just (o.encode a) else Nothing)-    o.decode+-- | Discover a finite contract without compiling or invoking any codec.+-- Functions and predicates remain in the tree rather than this pure view.+outline :: forall a. Structure a -> Outline+outline = \case+  Scalar info _ -> OScalar (SomeTypeRep (typeRep @a)) info+  Named rep c -> ONamed (SomeTypeRep rep) (outline c)+  Record con fs -> ORecord con (describeFields fs)+  Sum cs -> OSum [OutlineCase con d (describeFields fs) | Case con d _ _ fs <- cs]+  List c -> OList (outline c)+  Optional c -> OOptional (outline c)+  Unit -> OUnit+  Mapped _ _ c -> outline c+  Checked rule _ c -> OChecked rule (outline c)+  Documented d c -> ODocumented d (outline c) --- | A sum type. If no constructor has fields, it's encoded as an enumeration of--- tags; otherwise each value is an object with a @tag@ field.-sumOf :: Text -> [Case a] -> Codec a-sumOf d = sumCodec (nonEmpty d)+describeFields :: Fields r a -> [OutlineField]+describeFields = \case+  Pure _ -> []+  Apply f x -> describeFields f <> describeFields x+  Field n d c _ -> [OutlineField n d (outline c)] -sumCodec :: Maybe Text -> [Case a] -> Codec a-sumCodec d cases-  | all (null . (.fields)) cases =-      Codec-        (documentedSchema' d (schemaOf (SEnum [(c.tag, c.doc) | c <- cases])))-        (\a -> maybe Null (String . (.tag)) (matching a))-        ( \case-            String t | Just c <- byTag t -> c.decode []-            v -> mismatch ("one of " <> T.intercalate ", " (map (.tag) cases)) v-        )-  | otherwise =-      Codec-        (documentedSchema' d (schemaOf (SSum [Variant c.tag c.doc c.fields | c <- cases])))-        ( \a -> case [(c.tag, kvs) | c <- cases, Just kvs <- [c.encode a]] of-            (t, kvs) : _ -> Object (("tag", String t) : kvs)-            [] -> Null-        )-        ( \case-            Object kvs-              | Just (String t) <- lookupField "tag" kvs ->-                  maybe (Left ("unknown tag " <> t)) (\c -> c.decode kvs) (byTag t)-            v -> mismatch "an object with a tag" v-        )-  where-    matching a = find (\c -> isJust (c.encode a)) cases-    byTag t = find ((== t) . (.tag)) cases+-- | A concise display label. Scalar identity strings remain available separately.+outlineLabel :: Outline -> Text+outlineLabel = \case+  ONamed rep _ -> T.pack (show rep)+  OScalar rep _ -> T.pack (show rep)+  ORecord con _ -> if T.null con.name then "object" else con.name+  OSum _ -> "sum"+  OList c -> "[" <> outlineLabel c <> "]"+  OOptional c -> outlineLabel c <> "?"+  OUnit -> "()"+  OChecked _ c -> outlineLabel c+  ODocumented _ c -> outlineLabel c --- ------------------------------------------------------------------------------ Adjusting contracts+-- | Failures are discovered while compiling the entire structure, before values+-- are encoded. Paths identify fields, constructors and container elements.+data ProjectionError = ProjectionError {path :: [Text], problem :: Text}+  deriving (Eq, Show) --- | Describe the whole type.-documented :: Text -> Codec a -> Codec a-documented d = reschema (documentedSchema d)+-- | Compile the default model representation, rejecting unsupported leaves and+-- lossy nullable combinations across the entire finite tree. Single positional+-- fields are transparent; any named zero-field Record becomes a string enum.+-- All-nullary sums use string enums; other sums use tagged objects.+compileModel :: Structure a -> Either ProjectionError (Codec a)+compileModel = compileWith (const Nothing) --- | 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.-field :: Text -> Text -> Codec a -> Codec a-field name d = reschema (\s -> s {shape = update s.shape})-  where-    update = \case-      SObject fs | any named fs -> SObject (map describeField fs)-      SSum vs | any (any named . (.fields)) vs ->-        SSum [Variant t vd (map describeField vfs) | Variant t vd vfs <- vs]-      _ -> error ("Agentic.Contract.field: no field named " <> T.unpack name)-    named f = f.name == name-    describeField f@(Field n s r)-      | named f = Field n (documentedSchema d s) r-      | otherwise = f+-- | An open interpreter: override any typed node, delegate the remainder to the+-- model rules. An override owns both its schema and codec and must compile all+-- children it supports. The hook is consulted at EVERY node, including Named,+-- Mapped, Checked and Documented. Recursive overrides can call compileWith+-- themselves, or use compileFieldsWith / compileCaseWith for compound nodes.+-- Scalar matching can use eqTypeRep and HRefl, without unchecked casts.+compileWith ::+  (forall x. Structure x -> Maybe (Either ProjectionError (Codec x))) ->+  Structure a ->+  Either ProjectionError (Codec a)+compileWith override = go+ where+  at n = either (\(ProjectionError p e) -> Left (ProjectionError (n : p) e)) Right+  go :: Structure x -> Either ProjectionError (Codec x)+  go tree = case override tree of+    Just result -> result+    Nothing -> case tree of+      Scalar info mc -> maybe (Left (ProjectionError [] ("unsupported model scalar " <> info.name))) Right mc+      Named rep c -> reschema (titled (T.pack (show rep))) <$> go c+      Mapped to' from' c -> mapCodec to' from' <$> go c+      Checked rule ok c -> do+        codec <- go c+        let s = codec.schema+        pure $ Codec (s{checks = s.checks <> [rule]}) codec.encode $ \v -> do+          a <- codec.decode v+          if ok a then Right a else Left ("must be " <> rule)+      Documented d c -> reschema (documentedSchema d) <$> go c+      Unit -> Right (Codec (schemaOf SNull) (const Null) (\v -> case v of Null -> Right (); _ -> Left "expected null"))+      List c -> do+        inner <- at "[]" (go c)+        pure+          ( Codec+              (schemaOf (SArray inner.schema))+              (Array . map inner.encode)+              (\case Array vs -> traverse inner.decode vs; _ -> Left "expected a list")+          )+      Optional c -> do+        inner <- at "?" (go c)+        if acceptsNull inner.schema+          then Left (ProjectionError [] "model optional cannot distinguish nested nullable values")+          else+            pure+              ( Codec+                  (schemaOf (SNullable inner.schema))+                  (maybe Null inner.encode)+                  (\case Null -> Right Nothing; v -> Just <$> inner.decode v)+              )+      Record con fs -> do+        obj <- compileFieldsWith go fs+        let codec = C.record "" obj+        case (con.style, obj.fields) of+          (_, [])+            | not (T.null con.name) ->+                pure $+                  Codec+                    (schemaOf (SEnum [(con.name, Nothing)]))+                    (const (String con.name))+                    (\case String tag | tag == con.name -> codec.decode (Object []); _ -> Left "expected the constructor name")+          (PositionalFields, [Schema.Field n s _]) ->+            pure+              ( Codec+                  s+                  (\a -> case codec.encode a of Object kvs -> maybe Null id (lookup n kvs); _ -> Null)+                  (\v -> codec.decode (Object [(n, v)]))+              )+          _ -> pure codec+      Sum cs -> C.sumOf "" <$> traverse (compileCaseWith go) cs+  acceptsNull s = case s.shape of SNullable _ -> True; SNull -> True; _ -> False --- | A constraint the wire schemas can't express. It's stated to the model and--- checked locally; a value that fails it goes back to the model.-checked :: Text -> (a -> Bool) -> Codec a -> Codec a-checked rule ok c =-  Codec-    (s {checks = s.checks <> [rule]})-    c.encode-    ( \v -> do-        a <- c.decode v-        if ok a then Right a else Left ("must be " <> rule)-    )-  where-    s = c.schema+-- Apply field annotations before choosing a layout: an adapter may put fields+-- inside a payload object or use a completely different projected schema.+annotateField :: Text -> Text -> Structure a -> Maybe (Structure a)+annotateField n d = go+ where+  go :: Structure x -> Maybe (Structure x)+  go = \case+    Named rep c -> Named rep <$> go c+    Mapped f g c -> Mapped f g <$> go c+    Checked rule ok c -> Checked rule ok <$> go c+    Documented doc c -> Documented doc <$> go c+    Record con fs | contains fs -> Just (Record con (update fs))+    Sum cs+      | any (\(Case _ _ _ _ fs) -> contains fs) cs ->+          Just (Sum [Case con doc match inject (update fs) | Case con doc match inject fs <- cs])+    _ -> Nothing+  contains :: Fields r x -> Bool+  contains = \case+    Pure _ -> False+    Apply f x -> contains f || contains x+    Field name _ _ _ -> name == n+  update :: Fields r x -> Fields r x+  update = \case+    Pure x -> Pure x+    Apply f x -> Apply (update f) (update x)+    Field name doc c get -> Field name (if name == n then Just d else doc) c get -between :: (Ord a, Show a) => a -> a -> Codec a -> Codec a-between lo hi =-  checked-    ("between " <> T.pack (show lo) <> " and " <> T.pack (show hi))-    (\a -> a >= lo && a <= hi)+-- | Compile an applicative field structure using the supplied child interpreter.+-- The resulting ObjectCodec can be assembled with Agentic.Codec builders or+-- used directly for a different object layout. Nullable fields use that codec's+-- schema; an adapter can override requiredness/absence policy if necessary.+compileFieldsWith ::+  (forall x. Structure x -> Either ProjectionError (Codec x)) ->+  Fields r a ->+  Either ProjectionError (C.ObjectCodec r a)+compileFieldsWith interpretField = go+ where+  go :: Fields r x -> Either ProjectionError (C.ObjectCodec r x)+  go = \case+    Pure a -> Right (pure a)+    Apply f x -> (<*>) <$> go f <*> go x+    Field n d c get -> C.requiredWith n d <$> atPath n (interpretField c) <*> pure get -documentedSchema' :: Maybe Text -> Schema -> Schema-documentedSchema' = maybe id documentedSchema+-- | Compile a case with total matching and reconstruction. This does not choose+-- the enclosing sum's wire layout; the caller owns that decision.+compileCaseWith ::+  (forall x. Structure x -> Either ProjectionError (Codec x)) ->+  Case a ->+  Either ProjectionError (C.CaseCodec a)+compileCaseWith interpretField (Case con d match inject fs) = do+  obj <- atPath con.name (compileFieldsWith interpretField fs)+  pure $ C.CaseCodec con.name d obj.fields (fmap obj.encode . match) (fmap inject . obj.decode) -nonEmpty :: Text -> Maybe Text-nonEmpty t = if T.null t then Nothing else Just t+atPath :: Text -> Either ProjectionError a -> Either ProjectionError a+atPath n = either (\(ProjectionError p e) -> Left (ProjectionError (n : p) e)) Right -#ifndef __MHS__--- ------------------------------------------------------------------------------ Generic deriving+instance Contract Text where+  contract =+    scalar (ScalarInfo "Data.Text.Text" []) $+      Just $+        C.primitive (SString Nothing) String (\case String s -> Right s; v -> C.mismatch "text" v)+instance Contract Bool where+  contract =+    scalar (ScalarInfo "Prelude.Bool" []) $+      Just $+        C.primitive SBool Bool (\case Bool b -> Right b; v -> C.mismatch "a boolean" v)+instance Contract Integer where+  contract =+    scalar (ScalarInfo "Prelude.Integer" []) $+      Just $+        C.primitive SInteger Integer (\case Integer n -> Right n; Number d | not (isNaN d || isInfinite d), d == fromInteger (round d) -> Right (round d); v -> C.mismatch "an integer" v)+instance Contract Int where+  contract =+    scalar (ScalarInfo "Prelude.Int" []) $+      Just $+        C.primitive+          SInteger+          (Integer . toInteger)+          ( \case+              Integer n | inRange n -> Right (fromInteger n)+              Number d | not (isNaN d || isInfinite d), let n = round d, d == fromInteger n, inRange n -> Right (fromInteger n)+              v -> C.mismatch "an Int" v+          )+   where+    inRange n = n >= toInteger (minBound :: Int) && n <= toInteger (maxBound :: Int)+instance Contract Double where+  contract =+    scalar (ScalarInfo "Prelude.Double" []) $+      Just $+        C.primitive SNumber Number (\case Number d -> Right d; Integer n -> Right (fromInteger n); v -> C.mismatch "a number" v)+instance Contract () where contract = Unit+instance Contract a => Contract [a] where contract = List contract+instance Contract a => Contract (Maybe a) where contract = Optional contract+instance (Contract a, Contract b) => Contract (a, b) where+  contract = documented "A pair" $ Record (Constructor "(,)" PositionalFields) ((,) <$> required "_1" "" fst <*> required "_2" "" snd)+instance (Contract a, Contract b, Contract c) => Contract (a, b, c) where+  contract = documented "A triple" $ Record (Constructor "(,,)" PositionalFields) ((,,) <$> required "_1" "" (\(a, _, _) -> a) <*> required "_2" "" (\(_, b, _) -> b) <*> required "_3" "" (\(_, _, c) -> c)) --- | A contract built from the type's 'Generic' representation, without--- descriptions. Records become objects, sums become tagged objects, sums of--- constructors without fields become enumerations, and a constructor with a--- single unnamed field is transparent.-genericContract :: forall a. (Generic a, GContract (Rep a)) => Codec a-genericContract = mapCodec to from (gcontract @(Rep a))+-- | Derive from Generic while preserving nominal/applied identity, constructor+-- style and field order. Model compilation alone decides whether a positional+-- wrapper is transparent. The resulting contract tree must be finite.+genericContract :: forall a. (Generic a, Typeable a, GContract (Rep a)) => Structure a+genericContract = nominal (mapStructure to from (gcontract @(Rep a))) +-- | Constraint supporting generic derivation. Use genericContract rather than+-- depending on the representation traversal itself. Trees must be finite;+-- arbitrary recursive contracts are not supported by discovery or compilation. class GContract (f :: Type -> Type) where-  gcontract :: Codec (f p)--instance (Datatype d, GCases f) => GContract (M1 D d f) where-  gcontract = named $ mapCodec M1 unM1 $ case gcases @f of-    [GCase _ (Just bare)] -> bare-    [GCase c Nothing] | not (null c.fields) -> recordFromCase c-    cs -> sumCodec Nothing (map (.gcase) cs)-    where-      named = reschema (titled (T.pack (datatypeName (undefined :: M1 D d f ()))))--recordFromCase :: Case a -> Codec a-recordFromCase c =-  Codec-    (schemaOf (SObject c.fields))-    (Object . fromMaybe [] . c.encode)-    ( \case-        Object kvs -> c.decode kvs-        v -> mismatch "an object" v-    )+  -- | Describe a datatype representation without a nominal outer wrapper.+  gcontract :: Structure (f p) -data GCase a = GCase {gcase :: Case a, _gcaseBare :: Maybe (Codec a)}+instance GContract f => GContract (M1 D d f) where+  gcontract = mapStructure M1 unM1 (gcontract @f)+instance (GHC.Generics.Constructor c, GFields f) => GContract (M1 C c f) where+  gcontract = Record (genericConstructor @c) (M1 <$> lmapObject unM1 (snd (gfields @f 1)))+instance (GCases f, GCases g) => GContract (f :+: g) where+  gcontract = Sum (gcases @(f :+: g))+instance GContract V1 where gcontract = Sum [] +-- | Generic sum traversal retaining constructor order and typed payloads. class GCases (f :: Type -> Type) where-  gcases :: [GCase (f p)]--instance GCases V1 where-  gcases = []+  -- | Describe the cases of a generic sum.+  gcases :: [Case (f p)] +instance GCases V1 where gcases = [] instance (GCases f, GCases g) => GCases (f :+: g) where-  gcases = map (inject L1 (\case L1 x -> Just x; R1 _ -> Nothing)) (gcases @f)-    <> map (inject R1 (\case R1 x -> Just x; L1 _ -> Nothing)) (gcases @g)-    where-      inject :: (x -> y) -> (y -> Maybe x) -> GCase x -> GCase y-      inject wrap unwrap (GCase (Case t d fs e dec) _) =-        GCase (Case t d fs (\y -> unwrap y >>= e) (fmap wrap . dec)) Nothing--instance (Constructor c, GFields f) => GCases (M1 C c f) where   gcases =-    [ GCase-        (Case tag Nothing o.fields (Just . o.encode) o.decode)-        (mapCodec M1 unM1 <$> gbare @f)-    ]-    where-      tag = T.pack (conName (undefined :: M1 C c f ()))-      o = lmapObject unM1 (M1 <$> snd (gfields @f 1))+    map (inject L1 (\case L1 x -> Just x; _ -> Nothing)) (gcases @f)+      <> map (inject R1 (\case R1 x -> Just x; _ -> Nothing)) (gcases @g)+   where+    inject wrap unwrap (Case con d match construct fs) =+      Case con d (\y -> unwrap y >>= match) (wrap . construct) fs+instance (GHC.Generics.Constructor c, GFields f) => GCases (M1 C c f) where+  gcases = [Case (genericConstructor @c) Nothing (Just . unM1) M1 (snd (gfields @f 1))] -class GFields (f :: Type -> Type) where-  -- | The fields, numbering unnamed ones from the given index.-  gfields :: Int -> (Int, ObjectCodec (f p) (f p))-  -- | The codec of a lone unnamed field, if that's what this is.-  gbare :: Maybe (Codec (f p))+genericConstructor :: forall (c :: Meta). GHC.Generics.Constructor c => Constructor+genericConstructor =+  Constructor+    (T.pack (conName (undefined :: M1 C c U1 ())))+    (if conIsRecord (undefined :: M1 C c U1 ()) then RecordFields else PositionalFields) -instance GFields U1 where-  gfields n = (n, pure U1)-  gbare = Nothing+-- | Generic field traversal retaining selector names and declaration order.+class GFields (f :: Type -> Type) where+  -- | Number positional fields from the supplied index, returning the next index.+  gfields :: Int -> (Int, Fields (f p) (f p)) +instance GFields U1 where gfields n = (n, pure U1) instance (GFields f, GFields g) => GFields (f :*: g) where   gfields n =-    let (n1, a) = gfields @f n-        (n2, b) = gfields @g n1+    let (n1, a) = gfields @f n; (n2, b) = gfields @g n1      in (n2, (:*:) <$> lmapObject (\(x :*: _) -> x) a <*> lmapObject (\(_ :*: y) -> y) b)-  gbare = Nothing- instance (Selector s, Contract a) => GFields (M1 S s (K1 i a)) where-  gfields n = (n + 1, M1 . K1 <$> requiredWith name Nothing contract (unK1 . unM1))-    where-      selector = selName (undefined :: M1 S s (K1 i a) ())-      name = if null selector then "_" <> T.pack (show n) else T.pack selector-  gbare-    | null (selName (undefined :: M1 S s (K1 i a) ())) = Just (mapCodec (M1 . K1) (unK1 . unM1) contract)-    | otherwise = Nothing--#endif---- ------------------------------------------------------------------------------ Enumerations+  gfields n = (n + 1, M1 . K1 <$> Field name Nothing contract (unK1 . unM1))+   where+    selector = selName (undefined :: M1 S s (K1 i a) ())+    name = if null selector then "_" <> T.pack (show n) else T.pack selector +-- | An enumeration value with its model-facing label and optional description. data Option a = Option   { value :: a   , label :: Text   , doc :: Maybe Text   } +-- | A documented enumeration in declaration order (lowest to highest for scores). data OptionSet a = OptionSet   { doc :: Maybe Text   , options :: [Option a]-    -- ^ In order. For a score, this is the level order, lowest first.+  -- ^ In order. For a score, this is the level order, lowest first.   }  -- | Types whose values are a fixed, ordered list of described options. Jev's -- @choice@ and @score@ need one. class Options a where   options :: OptionSet a-#ifndef __MHS__   default options :: (Generic a, GEnum (Rep a), Show a) => OptionSet a   options = OptionSet Nothing [Option v (showLabel v) Nothing | v <- map to (genum @(Rep a))]-#endif  -- | One option, labelled by 'show' (the constructor's name, for an enumeration). option :: Show a => a -> Text -> Option a@@ -446,30 +533,20 @@ showLabel :: Show a => a -> Text showLabel = T.pack . show --- | Use with @deriving via@ to give an 'Options' type a matching 'Contract':------ > deriving via Enumeration Groan instance Contract Groan-newtype Enumeration a = Enumeration a--instance (Options a, Eq a) => Contract (Enumeration a) where-  contract = mapCodec Enumeration (\(Enumeration a) -> a) enumeration---- | The contract of an 'Options' type: its labels, with their descriptions.-enumeration :: forall a. (Options a, Eq a) => Codec a+-- | Describe an Options type as a sum of documented nullary cases.+enumeration :: forall a. (Options a, Eq a) => Structure a enumeration =-  Codec-    (documentedSchema' set.doc (schemaOf (SEnum [(o.label, o.doc) | o <- opts])))-    (\a -> maybe Null (String . (.label)) (find ((== a) . (.value)) opts))-    ( \case-        String t | Just o <- find ((== t) . (.label)) opts -> Right o.value-        v -> mismatch ("one of " <> T.intercalate ", " (map (.label) opts)) v-    )-  where-    set = options @a-    opts = set.options+  documented (maybe "" id set.doc) $+    Sum+      [ Case (Constructor o.label PositionalFields) o.doc (\a -> if a == o.value then Just () else Nothing) (const o.value) (pure ())+      | o <- set.options+      ]+ where+  set = options @a -#ifndef __MHS__+-- | Constraint supporting generic Options derivation. class GEnum (f :: Type -> Type) where+  -- | Enumerate nullary constructors in declaration order.   genum :: [f p]  instance GEnum f => GEnum (M1 D d f) where@@ -480,4 +557,3 @@  instance GEnum (M1 C c U1) where   genum = [M1 U1]-#endif
src/Agentic/Core.hs view
@@ -35,16 +35,14 @@   , module Control.Arrow   ) where -import Agentic.Contract (Codec (..), Contract (..), reschema)+import Agentic.Contract (Structure, Contract (..), nominal) import Agentic.Questions (Probability, Questions, YesNo (..)) import Control.Arrow import qualified Control.Category as Category import Data.String (IsString (..))-import Agentic.Schema (titled) import Data.Text (Text) import qualified Data.Text as T-import Data.Typeable (Typeable, typeRep)-import Data.Proxy (Proxy (..))+import Data.Typeable (Typeable)  -- | What a model step is asked to do. newtype Instruction = Instruction {text :: Text}@@ -74,8 +72,8 @@     -- which half it keeps.   TakeSecond :: Step m (a, b) b   Act :: (i -> m o) -> Step m i o-  Draft :: Codec i -> Codec o -> Instruction -> [Tool m] -> Step m i o-  Judge :: Codec i -> Questions o -> Step m i o+  Draft :: Structure i -> Structure o -> Instruction -> [Tool m] -> Step m i o+  Judge :: Structure i -> Questions o -> Step m i o  -- | A flow from @i@ to @o@ in effect @m@. Build flows from steps with the -- 'Arrow' combinators; run them with 'Agentic.Interpret.interpret'.@@ -94,7 +92,7 @@ data Tool m where   -- | 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+  Tool :: Text -> Text -> Structure i -> Structure o -> Agentic m i o -> Tool m  toolName :: Tool m -> Text toolName (Tool name _ _ _ _) = name@@ -166,9 +164,9 @@ tool :: forall i o m. (Contract i, Contract o, Typeable i, Typeable o) => Text -> Text -> Agentic m i o -> Tool m tool name description = Tool name description (titledContract @i) (titledContract @o) --- | A type's contract, with its schema named after the type if it isn't already.-titledContract :: forall a. (Contract a, Typeable a) => Codec a-titledContract = reschema (titled (T.pack (show (typeRep (Proxy @a))))) (contract @a)+-- | Preserve a step's nominal type identity, including handwritten contracts.+titledContract :: forall a. (Contract a, Typeable a) => Structure a+titledContract = nominal (contract @a)  -- | Map a flow over a list. The runtime may run the items concurrently. each :: Agentic m a b -> Agentic m [a] [b]
src/Agentic/Describe.hs view
@@ -16,10 +16,9 @@   , descriptionValue   ) where -import Agentic.Contract (Codec (..))+import Agentic.Contract (Outline, outline, outlineLabel) import Agentic.Core import Agentic.Questions (QuestionSpec (..), Questions (..))-import Agentic.Schema (Schema, typeLabel) import Agentic.Value (Value (..), renderJson) import Data.List (mapAccumL) import Data.Text (Text)@@ -52,20 +51,20 @@     -- ^ @act@: plain code with an effect.   | DraftInfo       { instruction :: Instruction-      , input :: Schema-      , output :: Schema+      , input :: Outline+      , output :: Outline       , tools :: [ToolInfo]       }   | JudgeInfo-      { input :: Schema+      { input :: Outline       , questions :: [QuestionSpec]       }  data ToolInfo = ToolInfo   { name :: Text   , description :: Text-  , input :: Schema-  , output :: Schema+  , input :: Outline+  , output :: Outline   , body :: Description   } @@ -100,12 +99,12 @@   TakeSecond -> SecondHalf   Act _ -> Effect   Draft input out instruction tools ->-    DraftInfo instruction input.schema out.schema (map toolInfo tools)-  Judge input qs -> JudgeInfo input.schema qs.specs+    DraftInfo instruction (outline input) (outline out) (map toolInfo tools)+  Judge input qs -> JudgeInfo (outline input) qs.specs  toolInfo :: Tool m -> ToolInfo toolInfo (Tool name description input out body) =-  ToolInfo name description input.schema out.schema (describe body)+  ToolInfo name description (outline input) (outline out) (describe body)  -- --------------------------------------------------------------------------- -- The tree view@@ -425,7 +424,7 @@       FirstHalf -> ("takeFirst", [])       SecondHalf -> ("takeSecond", [])       Effect -> ("act", [])-      DraftInfo instruction _ out _ -> ("draft @" <> typeLabel out, [quoted instruction.text])+      DraftInfo instruction _ out _ -> ("draft @" <> outlineLabel out, [quoted instruction.text])       JudgeInfo _ [q] -> ("judge", [questionText q])       JudgeInfo _ qs -> ("judge " <> T.pack (show (length qs)) <> " questions in one request", map questionText qs) @@ -542,25 +541,24 @@         node           "draft"           [ ("instruction", String instruction.text)-          , ("input", String (typeLabel input))-          , ("output", String (typeLabel out))+          , ("input", String (outlineLabel input))+          , ("output", String (outlineLabel out))           , ("tools", Array (map tool tools))           ]       JudgeInfo input qs ->-        node "judge" [("input", String (typeLabel input)), ("questions", Array (map question qs))]+        node "judge" [("input", String (outlineLabel input)), ("questions", Array (map question qs))]     tool t =       Object         [ ("name", String t.name)         , ("description", String t.description)-        , ("input", String (typeLabel t.input))-        , ("output", String (typeLabel t.output))+        , ("input", String (outlineLabel t.input))+        , ("output", String (outlineLabel t.output))         ]     question = \case       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])] --- | Apply a function to a pair's second half. (MicroHs has no Functor instance--- for pairs.)+-- | Apply a function to a pair's second half. onSnd :: (b -> c) -> (a, b) -> (a, c) onSnd f (a, b) = (a, f b)
src/Agentic/Interpret.hs view
@@ -1,97 +1,143 @@ -- | Running flows. module Agentic.Interpret   ( interpret+  , preflight   ) where -import Agentic.Contract (Codec (..))+import Agentic.Contract (Codec (..), Structure, ProjectionError (..), compileModel) import Agentic.Core import Agentic.Questions (JudgeRequest (..), Questions (..), decodeAnswers) import Agentic.Runtime import Data.List (find)+import Data.Text (Text)  -- | Run a flow with a runtime. interpret :: forall m i o. Monad m => Runtime m -> Agentic m i o -> i -> m o-interpret rt = go []-  where-    go :: forall a b. [Note] -> Agentic m a b -> a -> m b-    go path flow x = case flow of-      Step s -> step path s x-      Seq f g -> go path f x >>= go path g-      Fanout f g -> do-        results <- inParallel rt [Left <$> go path f x, Right <$> go path g x]+interpret rt flow input = case preflight flow of+  Left e -> failWith rt e+  Right () -> go [] flow input+ where+  go :: forall a b. [Note] -> Agentic m a b -> a -> m b+  go path flow x = case flow of+    Step s -> step path s x+    Seq f g -> go path f x >>= go path g+    Fanout f g -> do+      results <- inParallel rt [Left <$> go path f x, Right <$> go path g x]+      case results of+        [Left b, Right c] -> pure (b, c)+        _ -> error "Agentic.interpret: the runtime's parallel changed its results"+    First f -> case x of (a, c) -> (\b -> (b, c)) <$> go path f a+    Split f g -> case x of+      (a, c) -> do+        results <- inParallel rt [Left <$> go path f a, Right <$> go path g c]         case results of-          [Left b, Right c] -> pure (b, c)+          [Left b, Right d] -> pure (b, d)           _ -> error "Agentic.interpret: the runtime's parallel changed its results"-      First f -> case x of (a, c) -> (\b -> (b, c)) <$> go path f a-      Split f g -> case x of-        (a, c) -> do-          results <- inParallel rt [Left <$> go path f a, Right <$> go path g c]-          case results of-            [Left b, Right d] -> pure (b, d)-            _ -> error "Agentic.interpret: the runtime's parallel changed its results"-      Choose f g -> either (go path f) (go path g) x-      Each f -> inParallel rt (map (go path f) x)-      Repeat done f ->-        let loop a = if done a then pure a else go path f a >>= loop-         in loop x-      Noted n f -> go (path <> [n]) f x+    Choose f g -> either (go path f) (go path g) x+    Each f -> inParallel rt (map (go path f) x)+    Repeat done f ->+      let loop a = if done a then pure a else go path f a >>= loop+       in loop x+    Noted n f -> go (path <> [n]) f x -    emit :: [Note] -> Happened -> m ()-    emit path = rt.observe . Event path+  emit :: [Note] -> Happened -> m ()+  emit path = rt.observe . Event path -    step :: forall a b. [Note] -> Step m a b -> a -> m b-    step path s x = case s of-      Pass -> pure x-      Wrap f -> pure (f x)-      TakeFirst -> pure (fst x)-      TakeSecond -> pure (snd x)-      Arr f -> pure (f x)-      Act f -> emit path Acted >> f x-      Judge input qs-        | null qs.specs -> answered (decodeAnswers qs [])-        | otherwise -> do-            let request = JudgeRequest (input.encode x) qs.specs-            answers <- rt.systemOne.ask request-            emit path (Judged request answers)-            answered (decodeAnswers qs answers)-      Draft inCodec out instruction tools -> do-        let conversation =-              Conversation-                { path = path-                , instruction = instruction-                , input = inCodec.encode x-                , inputSchema = inCodec.schema-                , tools = map toolSpec tools-                , outputSchema = out.schema-                , history = []-                }-        emit path (Drafting conversation)-        let loop past = do-              turn <- rt.systemTwo.ask conversation {history = past}-              emit path (Turned turn)-              case turn.action of-                Respond v -> case out.decode v of-                  Right b -> pure b-                  Left problem -> do-                    emit path (OutputRejected problem)-                    loop (past <> [Rejected turn.raw problem])-                CallTools calls -> do-                  results <- inParallel rt (map (runTool path tools) calls)-                  loop (past <> [Called turn.raw (zip (map (.callId) calls) results)])-        loop []-      where-        answered = either (failWith rt . MalformedAnswers) pure+  step :: forall a b. [Note] -> Step m a b -> a -> m b+  step path s x = case s of+    Pass -> pure x+    Wrap f -> pure (f x)+    TakeFirst -> pure (fst x)+    TakeSecond -> pure (snd x)+    Arr f -> pure (f x)+    Act f -> emit path Acted >> f x+    Judge inputTree qs -> do+      input <- compile path inputTree+      if null qs.specs+        then answered (decodeAnswers qs [])+        else do+          let request = JudgeRequest (input.encode x) qs.specs+          answers <- rt.systemOne.ask request+          emit path (Judged request answers)+          answered (decodeAnswers qs answers)+    Draft inTree outTree instruction toolTrees -> do+      inCodec <- compile path inTree+      out <- compile path outTree+      tools <- traverse (compileTool path) toolTrees+      let conversation =+            Conversation+              { path = path+              , instruction = instruction+              , input = inCodec.encode x+              , inputSchema = inCodec.schema+              , tools = map toolSpec tools+              , outputSchema = out.schema+              , history = []+              }+      emit path (Drafting conversation)+      let loop past = do+            turn <- rt.systemTwo.ask conversation{history = past}+            emit path (Turned turn)+            case turn.action of+              Respond v -> case out.decode v of+                Right b -> pure b+                Left problem -> do+                  emit path (OutputRejected problem)+                  loop (past <> [Rejected turn.raw problem])+              CallTools calls -> do+                results <- inParallel rt (map (runTool path tools) calls)+                loop (past <> [Called turn.raw (zip (map (.callId) calls) results)])+      loop []+   where+    answered = either (failWith rt . MalformedAnswers) pure -    runTool :: [Note] -> [Tool m] -> ToolCall -> m ToolResult-    runTool path tools call = do-      emit path (ToolCalled call)-      result <- case find ((== call.name) . toolName) tools of-        Nothing -> pure (ToolFailed ("there is no tool named " <> call.name))-        Just (Tool name _ input out body) -> case input.decode call.input of-          Left problem -> pure (ToolFailed ("invalid input: " <> problem))-          Right i -> ToolOk . out.encode <$> go (path <> [Note name Nothing]) body i-      emit path (ToolReturned call.callId result)-      pure result+  compile :: [Note] -> Structure a -> m (Codec a)+  compile path = either (failWith rt . UnsupportedContract path) pure . compileModel -toolSpec :: Tool m -> ToolSpec-toolSpec (Tool name description input _ _) = ToolSpec name description input.schema+  compileTool :: [Note] -> Tool m -> m (CompiledTool m)+  compileTool path (Tool name doc input out body) = CompiledTool name doc <$> compile path input <*> compile path out <*> pure body++  runTool :: [Note] -> [CompiledTool m] -> ToolCall -> m ToolResult+  runTool path tools call = do+    emit path (ToolCalled call)+    result <- case find ((== call.name) . compiledToolName) tools of+      Nothing -> pure (ToolFailed ("there is no tool named " <> call.name))+      Just (CompiledTool name _ input out body) -> case input.decode call.input of+        Left problem -> pure (ToolFailed ("invalid input: " <> problem))+        Right i -> ToolOk . out.encode <$> go (path <> [Note name Nothing]) body i+    emit path (ToolReturned call.callId result)+    pure result++data CompiledTool m where+  CompiledTool :: Text -> Text -> Codec i -> Codec o -> Agentic m i o -> CompiledTool m++compiledToolName :: CompiledTool m -> Text+compiledToolName (CompiledTool n _ _ _ _) = n++toolSpec :: CompiledTool m -> ToolSpec+toolSpec (CompiledTool name description input _ _) = ToolSpec name description input.schema++-- | Validate all contracts, branches and tool bodies without executing effects.+-- Interpret runs this automatically before running any step.+preflight :: Agentic m i o -> Either FlowError ()+preflight = go []+ where+  check notes label c = case compileModel c of+    Left (ProjectionError p e) -> Left (UnsupportedContract notes (ProjectionError (label : p) e))+    Right _ -> Right ()+  go :: [Note] -> Agentic m a b -> Either FlowError ()+  go notes = \case+    Step (Draft i o _ ts) -> check notes "draft.input" i >> check notes "draft.output" o >> mapM_ (toolCheck notes) ts+    Step (Judge i _) -> check notes "judge.input" i+    Step _ -> Right ()+    Seq f g -> go notes f >> go notes g+    Fanout f g -> go notes f >> go notes g+    Split f g -> go notes f >> go notes g+    Choose f g -> go notes f >> go notes g+    First f -> go notes f+    Each f -> go notes f+    Repeat _ f -> go notes f+    Noted n f -> go (notes <> [n]) f+  toolCheck notes (Tool n _ i o body) =+    let path = notes <> [Note n Nothing]+     in check path "tool.input" i >> check path "tool.output" o >> go path body
src/Agentic/Questions.hs view
@@ -8,6 +8,7 @@   , yesNo   , choice   , score+     -- * Answers   , Probability   , probability@@ -17,6 +18,7 @@   , YesNo (..)   , Choice (..)   , Score (..)+     -- * Wire types   , QuestionSpec (..)   , Answer (..)@@ -24,11 +26,12 @@   , decodeAnswers   ) where -import Agentic.Contract (Contract (..), Option (..), OptionSet (..), Options (..), mapCodec, record, required)+import Agentic.Contract (Codec (..), Contract (..), Option (..), OptionSet (..), Options (..), ScalarInfo (..), record, required, scalar)+import Agentic.Schema (Shape (SNumber), schemaOf) import Agentic.Value (Value (..)) import Data.List (find) import Data.Text (Text)-import qualified Data.Text as T+import Data.Text qualified as T  -- --------------------------------------------------------------------------- -- Probabilities@@ -85,7 +88,7 @@  data Score a = Score   { position :: Double-    -- ^ The probability-weighted position, from 0 (the first option) upwards.+  -- ^ The probability-weighted position, from 0 (the first option) upwards.   , probabilities :: [(a, Probability)]   , confidence :: Probability   }@@ -94,7 +97,17 @@ -- Answers have contracts, so a judgement can be a tool's output.  instance Contract Probability where-  contract = mapCodec toProbability probability (contract @Double)+  contract =+    scalar (ScalarInfo "Agentic.Questions.Probability" []) $+      Just $+        Codec+          (schemaOf SNumber)+          (Number . probability)+          ( \case+              Number d | not (isNaN d || isInfinite d) -> Right (if d <= 0 then 0 else if d >= 1 then 1 else toProbability d)+              Integer n -> Right (if n <= 0 then 0 else 1)+              _ -> Left "expected a finite probability"+          )  instance Contract YesNo where   contract = record "A yes/no judgement" (YesNo <$> required "yes" "The probability that the answer is yes" (.yes))@@ -120,18 +133,18 @@  data QuestionSpec   = AskYesNo Text-  | AskChoice Text [(Text, Maybe Text)]-    -- ^ Option labels with their descriptions.-  | AskScore Text [(Text, Maybe Text)]-    -- ^ Levels in order, lowest first.+  | -- | Option labels with their descriptions.+    AskChoice Text [(Text, Maybe Text)]+  | -- | Levels in order, lowest first.+    AskScore Text [(Text, Maybe Text)]   deriving (Eq, Ord, Show)  data Answer   = YesNoAnswer Probability-  | ChoiceAnswer Text [(Text, Probability)] Probability-    -- ^ The chosen label, every label's probability, and the confidence.-  | ScoreAnswer Double [(Int, Probability)] Probability-    -- ^ The position, each level's probability (by index), and the confidence.+  | -- | The chosen label, every label's probability, and the confidence.+    ChoiceAnswer Text [(Text, Probability)] Probability+  | -- | The position, each level's probability (by index), and the confidence.+    ScoreAnswer Double [(Int, Probability)] Probability   deriving (Eq, Show)  -- | What a System One provider receives: the encoded input and the questions.@@ -154,7 +167,7 @@   }  instance Functor Questions where-  fmap f q = q {decoder = fmap f . q.decoder}+  fmap f q = q{decoder = fmap f . q.decoder}  instance Applicative Questions where   pure x = Questions [] (\case [] -> Right x; _ -> Left "too many answers")@@ -182,8 +195,8 @@   ChoiceAnswer picked ps conf ->     Choice <$> byLabel opts picked <*> traverse (\(l, p) -> (,p) <$> byLabel opts l) ps <*> pure conf   other -> Left ("expected a choice answer, got " <> T.pack (show other))-  where-    opts = (options @a).options+ where+  opts = (options @a).options  -- | Place the input on an 'Options' type's levels, lowest first. score :: forall a. Options a => Text -> Questions (Score a)@@ -191,11 +204,11 @@   ScoreAnswer pos ps conf ->     Score pos <$> traverse (\(i, p) -> (,p) <$> byIndex i) ps <*> pure conf   other -> Left ("expected a score answer, got " <> T.pack (show other))-  where-    opts = (options @a).options-    byIndex i = case drop i opts of-      o : _ | i >= 0 -> Right o.value-      _ -> Left ("no level " <> T.pack (show i))+ where+  opts = (options @a).options+  byIndex i = case drop i opts of+    o : _ | i >= 0 -> Right o.value+    _ -> Left ("no level " <> T.pack (show i))  labels :: [Option a] -> [(Text, Maybe Text)] labels = map (\o -> (o.label, o.doc))
src/Agentic/Runtime.hs view
@@ -30,6 +30,7 @@   , FlowError (..)   ) where +import Agentic.Contract (ProjectionError) import Agentic.Core (Instruction, Note) import Agentic.Questions (Answer, JudgeRequest) import Agentic.Schema (Schema)@@ -119,6 +120,7 @@ data FlowError   = NoSystemOne   | NoSystemTwo+  | UnsupportedContract [Note] ProjectionError   | MalformedAnswers Text   | TurnLimit Int   deriving (Eq, Show)
src/Agentic/Scripted.hs view
@@ -4,13 +4,15 @@     scripted   , replyingWith   , respond+  , modelContract   , callTools+     -- * System One   , answering   , fixedAnswers   ) where -import Agentic.Contract (Codec (..), Contract (..))+import Agentic.Contract (Codec (..), Contract (..), compileModel) import Agentic.Questions import Agentic.Runtime import Agentic.Value (Value (..))@@ -31,9 +33,16 @@ replyingWith :: Applicative m => (Conversation -> Action) -> SystemTwo m replyingWith f = SystemTwo (pure . Turn (Raw Null) . f) --- | A final answer, encoded with its contract.+-- | A final answer, encoded with its model contract. This test-fixture helper+-- raises an error for unsupported contracts; use compileModel directly when+-- constructing responses from contracts that may be unsupported. respond :: Contract a => a -> Action-respond = Respond . contract.encode+respond = Respond . modelContract.encode++-- | Compile a known-supported model contract for test fixtures. Unsupported+-- contracts raise an error; use compileModel to handle failure explicitly.+modelContract :: Contract a => Codec a+modelContract = either (error . ("Agentic.Scripted.modelContract: " <>) . show) id (compileModel contract)  callTools :: [(Text, Value)] -> Action callTools calls = CallTools [ToolCall ("call-" <> name) name input | (name, input) <- calls]
src/Agentic/Value.hs view
@@ -49,6 +49,6 @@ lookupField :: Text -> [(Text, Value)] -> Maybe Value lookupField = lookup --- | 'T.concatMap', which MicroHs's "Data.Text" doesn't provide.+-- | Map and concatenate text fragments. concatMapText :: (Char -> Text) -> Text -> Text concatMapText f = T.concat . map f . T.unpack
test/Portable.hs view
@@ -5,15 +5,15 @@ {-# 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).+-- | Handwritten contracts, ordinary combinators, and a pure monad in place+-- of IO. Kept as the agentic-portable-test workflow regression suite on GHC. module Main (main) where  import Agentic+import Agentic.Scripted (modelContract) import Data.IORef (modifyIORef, newIORef, readIORef) import Data.Text (Text)-import qualified Data.Text as T+import Data.Text qualified as T import System.Exit (exitFailure)  -- ---------------------------------------------------------------------------@@ -39,12 +39,12 @@       [ 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+   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@@ -64,31 +64,32 @@   Pure g >>= k = Pure (\w -> let (a, w1) = g w in (k a).runPure w1)  say :: Text -> Pure ()-say t = Pure (\w -> ((), w {logged = w.logged <> [t]}))+say t = Pure (\w -> ((), w{logged = w.logged <> [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 w.script of-        a : rest -> (Turn (Raw Null) a, w {script = rest})+        a : rest -> (Turn (Raw Null) a, w{script = rest})         [] -> error "the script ran out of turns"     , systemOne = SystemOne $ \request -> pure (map answer request.questions)-    , observe = \e -> Pure (\w -> ((), w {events = w.events <> [e.happened]}))+    , observe = \e -> Pure (\w -> ((), w{events = w.events <> [e.happened]}))     }-  where-    answer = \case-      AskYesNo _ -> YesNoAnswer 0.9-      AskChoice _ ((l, _) : _) -> ChoiceAnswer l [(l, 1)] 1-      AskChoice _ [] -> ChoiceAnswer "" [] 0-      AskScore _ _ -> ScoreAnswer 1 [(1, 1)] 1+ 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))+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")@@ -114,20 +115,20 @@         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" (contract.decode (contract.encode joke) == Right joke)-  check "a sum with payloads round-trips" (contract.decode (contract.encode (Rect 2 3)) == Right (Rect 2 3))-  check "a sum encodes its constructor as a tag" (contract.encode (Circle 1) == Object [("tag", String "Circle"), ("radius", Number 1)])-  check "a record missing a field is rejected" (either (const True) (const False) ((contract @Joke).decode (Object [("setup", String "x")])))+  check "a record round-trips through its codec" (modelContract.decode (modelContract.encode joke) == Right joke)+  check "a sum with payloads round-trips" (modelContract.decode (modelContract.encode (Rect 2 3)) == Right (Rect 2 3))+  check "a sum encodes its constructor as a tag" (modelContract.encode (Circle 1) == Object [("tag", String "Circle"), ("radius", Number 1)])+  check "a record missing a field is rejected" (either (const True) (const False) ((modelContract @Joke).decode (Object [("setup", String "x")])))    let world0 =         World           { script =               [ CallTools [ToolCall "c1" "count_letters" (String "scarecrow")]               , CallTools [ToolCall "c2" "write_joke" (String "farms")]-              , Respond (contract.encode joke) -- answers the nested write_joke draft+              , Respond (modelContract.encode joke) -- answers the nested write_joke draft               , Respond (Object [("setup", String "only a setup")]) -- invalid: no punchline-              , Respond (contract.encode joke) -- the correction-              , Respond (contract.encode (Rect 2 3)) -- the shape+              , Respond (modelContract.encode joke) -- the correction+              , Respond (modelContract.encode (Rect 2 3)) -- the shape               ]           , logged = []           , events = []@@ -143,7 +144,7 @@    -- 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) -> contract.decode v == Right joke; _ -> False) seen)+  check "the nested draft's result went back as the tool's result" (any (\case ToolReturned "c2" (ToolOk v) -> modelContract.decode v == Right joke; _ -> False) seen)    -- 4. Invalid output, then a corrected response.   check "the invalid output was rejected" (length [() | OutputRejected _ <- seen] == 1)
+ test/ProjectionExample.hs view
@@ -0,0 +1,147 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}+{-# OPTIONS_GHC -Wno-orphans #-}++-- | An independent adapter fixture. All storage conventions are local to this+-- module; Agentic owns neither the identifier nor this storage protocol.+module ProjectionExample where++import Agentic hiding (model)+import Agentic.Codec (CaseCodec (..), ObjectCodec (..))+import Agentic.Contract (Structure (..))+import Agentic.Schema (Field (..), Shape (..), Variant (..))+import Data.Fixed (E2, Fixed (..), showFixed)+import Data.Text (Text)+import Data.Text qualified as T+import GHC.Generics (Generic)+import Text.Read (readMaybe)+import Type.Reflection++newtype FactId = FactId Text deriving (Eq, Show)+instance Contract FactId where+  contract = scalar (ScalarInfo "example.FactId" []) (Just identifierCodec)++identifierCodec :: Codec FactId+identifierCodec = Codec (schemaOf (SString Nothing)) (\(FactId t) -> String t) $ \case+  String t | not (T.null t) -> Right (FactId t)+  _ -> Left "expected a nonempty identifier"++-- An adapter supplied once for a library type, not for every containing record.+instance Contract (Fixed E2) where+  contract = scalar (ScalarInfo "Data.Fixed.Fixed" [("resolution", "100")]) (Just decimalCodec)++decimalCodec :: Codec (Fixed E2)+decimalCodec = Codec (schemaOf (SString Nothing)) (String . T.pack . showFixed False) $ \case+  String t -> do+    let (sign, unsigned) = case T.stripPrefix "-" t of Just u -> (-1, u); Nothing -> (1, t)+    case T.splitOn "." unsigned of+      [whole, frac] | digits whole && digits frac && T.length frac == 2 ->+        case (readMaybe (T.unpack whole), readMaybe (T.unpack frac)) of+          (Just w, Just f) ->+            let result = MkFixed (sign * (w * 100 + f))+             in if T.pack (showFixed False result) == t then Right result else Left "noncanonical decimal"+          _ -> Left "invalid decimal"+      _ -> Left "expected decimal text with two fractional digits"+  _ -> Left "expected exact decimal text"+ where+  digits t = not (T.null t) && T.all (\c -> c >= '0' && c <= '9') t++data Sample = Sample {key :: FactId, amount :: Fixed E2, confidence :: Probability}+  deriving (Generic, Contract, Eq, Show)+data Box a = Box {value :: a} deriving (Generic, Contract, Eq, Show)+data Payload = Empty | Amount (Fixed E2) | Confidence Probability+  deriving (Generic, Contract, Eq, Show)++sample :: Sample+sample = Sample (FactId "sample-1") (MkFixed 1234) (fromBasisPoints 8500)++integerText :: Codec Integer+integerText = Codec (schemaOf (SString Nothing)) (String . T.pack . show) $ \case+  String t | Just n <- readMaybe (T.unpack t), T.pack (show (n :: Integer)) == t -> Right n+  _ -> Left "expected a canonical integer string"++probabilityStorage :: Codec Probability+probabilityStorage = Codec integerText.schema (integerText.encode . toInteger . basisPoints) $ \v -> do+  n <- integerText.decode v+  if n < 0 || n > 10000+    then Left "basis points must be between 0 and 10000"+    else Right (fromBasisPoints (fromInteger n))++-- | An independent projection for upstream and downstream leaves. Named types,+-- mappings, validation and lists reuse the compiler's traversal.+-- Overrides are typed, and return the schema together with its matching codec.+storage :: Structure a -> Either ProjectionError (Codec a)+storage = compileWith storageRules++storageRules :: forall a. Structure a -> Maybe (Either ProjectionError (Codec a))+storageRules = \case+  Scalar info modelDefault -> Just $ case () of+    _+      | Just HRefl <- eqTypeRep (typeRep @a) (typeRep @Probability) -> Right probabilityStorage+      | Just HRefl <- eqTypeRep (typeRep @a) (typeRep @(Fixed E2)) -> Right decimalCodec+      | Just HRefl <- eqTypeRep (typeRep @a) (typeRep @FactId) -> Right identifierCodec+      | Just HRefl <- eqTypeRep (typeRep @a) (typeRep @Integer) -> Right integerText+      | Just HRefl <- eqTypeRep (typeRep @a) (typeRep @Int) ->+          Right $+            Codec+              integerText.schema+              (integerText.encode . toInteger)+              ( \v -> do+                  n <- integerText.decode v+                  if n < toInteger (minBound :: Int) || n > toInteger (maxBound :: Int)+                    then Left "Int out of range"+                    else Right (fromInteger n)+              )+      | Just HRefl <- eqTypeRep (typeRep @a) (typeRep @Text) -> defaultCodec info modelDefault+      | Just HRefl <- eqTypeRep (typeRep @a) (typeRep @Bool) -> defaultCodec info modelDefault+      | otherwise -> Left (ProjectionError [] ("unsupported storage scalar " <> info.name))+  Optional child -> Just $ do+    c <- storage child+    pure $+      Codec+        (schemaOf (SSum [Variant "None" Nothing [], Variant "Some" Nothing [Field "value" c.schema True]]))+        (\case Nothing -> Object [("tag", String "None")]; Just x -> Object [("tag", String "Some"), ("value", c.encode x)])+        ( \case+            Object [("tag", String "None")] -> Right Nothing+            Object fields | lookup "tag" fields == Just (String "Some"), Just v <- lookup "value" fields -> Just <$> c.decode v+            _ -> Left "expected a None/Some tagged optional"+        )+  Record con fields -> Just $ do+    fs <- compileFieldsWith storage fields+    let tagged = con.style == PositionalFields && not (null fs.fields)+        tagField = Field "tag" (schemaOf (SEnum [(con.name, Nothing)])) True+    pure $+      Codec+        (schemaOf (SObject (if tagged then tagField : fs.fields else fs.fields)))+        (\a -> Object ((if tagged then [("tag", String con.name)] else []) <> fs.encode a))+        ( \case+            Object values | not tagged || lookup "tag" values == Just (String con.name) -> fs.decode values+            _ -> Left "expected a record with its constructor tag"+        )+  Sum cases -> Just $ do+    compiled <- traverse (compileCaseWith storage) cases+    pure $+      Codec+        (schemaOf (SSum [Variant c.tag c.doc (if null c.fields then [] else [Field "payload" (schemaOf (SObject c.fields)) True]) | c <- compiled]))+        ( \a -> case [(c.tag, values) | c <- compiled, Just values <- [c.encode a]] of+            (tag, values) : _ -> Object ([("tag", String tag)] <> if null values then [] else [("payload", Object values)])+            [] -> Null+        )+        ( \case+            Object values | Just (String tag) <- lookup "tag" values ->+              case [c | c <- compiled, c.tag == tag] of+                c : _+                  | null c.fields -> c.decode []+                  | Just (Object payload) <- lookup "payload" values -> c.decode payload+                _ -> Left "unknown constructor or missing payload"+            _ -> Left "expected a tagged sum"+        )+  Unit -> Just $ Right $ Codec (schemaOf (SObject [])) (const (Object [])) $ \case+    Object [] -> Right ()+    _ -> Left "expected an empty object"+  _ -> Nothing+ where+  defaultCodec info = maybe (Left (ProjectionError [] ("unsupported storage scalar " <> info.name))) Right
+ test/ProjectionSpec.hs view
@@ -0,0 +1,220 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE TypeOperators #-}++module Main (main) where++import Agentic hiding (model)+import Agentic.Codec qualified as C+import Agentic.Contract (Case (..), Fields (..), Structure (..))+import Agentic.Schema (Schema (..), Shape (..), Variant (..))+import Agentic.Schema qualified as Schema+import Agentic.Scripted (modelContract)+import Control.Exception (evaluate, try)+import Data.Either (isLeft)+import Data.Fixed (E2, Fixed (..))+import Data.IORef+import Data.Text (Text)+import GHC.Generics (Generic)+import ProjectionExample+import Test.Hspec hiding (describe)+import Test.Hspec qualified as H+import Type.Reflection++newtype Hidden = Hidden Text deriving (Eq, Show)+instance Contract Hidden where+  contract = scalar (ScalarInfo "example.Hidden" []) Nothing++data VariantChoice = Safe Int | Unsafe Hidden deriving (Generic, Contract)+newtype Wrapper = Wrapper (Maybe Int)+  deriving stock (Generic)+  deriving anyclass (Contract)+data EmptyRoot = EmptyRoot deriving (Generic, Contract, Eq, Show)++newtype Age = Age Int+  deriving stock (Generic, Eq, Show)+  deriving anyclass (Contract)+type Alias = Age++good :: Either ProjectionError a -> a+good = either (error . show) id+roundTrip :: (Eq a, Show a) => Codec a -> a -> Expectation+roundTrip c x = c.decode (c.encode x) `shouldBe` Right x++main :: IO ()+main = hspec $ do+  H.describe "independent projections" $ do+    it "compiles one derived Sample to storage and model codecs" $ do+      let stored = good (storage (contract @Sample))+          model = modelContract @Sample+      stored.encode sample `shouldBe` Object [("key", String "sample-1"), ("amount", String "12.34"), ("confidence", String "8500")]+      model.encode sample `shouldBe` Object [("key", String "sample-1"), ("amount", String "12.34"), ("confidence", Number 0.85)]+      roundTrip stored sample+      roundTrip model sample+    it "preserves negative and huge exact decimals" $ do+      let c = good (storage (contract @(Fixed E2)))+      mapM_ (roundTrip c . MkFixed) [-1234, 123456789012345678901234567890123456789, 0]+      c.encode (MkFixed (-1234)) `shouldBe` String "-12.34"+    it "rejects malformed decimal and identifier values" $ do+      mapM_+        (\v -> decimalCodec.decode v `shouldSatisfy` isLeft)+        [Number 12.34, String "12.345", String "01.00", String "NaN", String "-0.00", String "1e2"]+      identifierCodec.decode (String "") `shouldSatisfy` isLeft+    it "validates storage probability before clamping, independently of model behavior" $ do+      let model = modelContract @Probability+      probabilityStorage.decode (String "8500") `shouldBe` Right (fromBasisPoints 8500)+      mapM_+        (\v -> probabilityStorage.decode v `shouldSatisfy` isLeft)+        [String "10001", String "-1", String "85.5", String "08500", Number 8500]+      model.decode (Number 1.2) `shouldBe` Right 1+      model.decode (Number 1e308) `shouldBe` Right 1+      model.decode (Number (0 / 0)) `shouldSatisfy` isLeft+      model.decode (Number (1 / 0)) `shouldSatisfy` isLeft+    it "shares contract checks across projections" $ do+      let c = good (storage (checked "positive" (> (0 :: Integer)) contract))+      c.decode (String "-1") `shouldBe` Left "must be positive"+    it "composes through lists, optionals, and payload sums" $ do+      roundTrip+        (good (storage (contract @[Maybe Payload])))+        [Nothing, Just Empty, Just (Amount (MkFixed (-1234))), Just (Confidence 0.85)]+    it "retains all nested optional distinctions" $ do+      let c = good (storage (contract @(Maybe (Maybe Int))))+          values = [Nothing, Just Nothing, Just (Just 7)]+      mapM_ (roundTrip c) values+      map c.encode values+        `shouldBe` [ Object [("tag", String "None")]+                   , Object [("tag", String "Some"), ("value", Object [("tag", String "None")])]+                   , Object [("tag", String "Some"), ("value", Object [("tag", String "Some"), ("value", String "7")])]+                   ]+    it "rejects lossy model optionals even through nominal and documentation wrappers" $ do+      isLeft (compileModel (contract @(Maybe (Maybe Int)))) `shouldBe` True+      isLeft (compileModel (Optional (documented "wrapper" (contract @Wrapper)))) `shouldBe` True+      isLeft (compileModel (contract @(Maybe ()))) `shouldBe` True+      isLeft (compileModel (Optional (checked "anything" (const True) (mapStructure id id (documented "nullable" (contract @(Maybe Int))))))) `shouldBe` True+    it "lets storage choose unit as an empty record" $+      (good (storage (contract @()))).encode () `shouldBe` Object []+  H.describe "structural overrides" $ do+    it "stores positional wrappers with tags while models remain transparent" $ do+      let stored = good (storage (contract @Age))+          model = modelContract @Age+      stored.encode (Age 42) `shouldBe` Object [("tag", String "Age"), ("_1", String "42")]+      model.encode (Age 42) `shouldBe` Integer 42+      stored.decode (Object [("tag", String "Other"), ("_1", String "42")]) `shouldSatisfy` isLeft+    it "projects a singleton nullary root to an empty object" $ do+      let stored = good (storage (contract @EmptyRoot))+      stored.encode EmptyRoot `shouldBe` Object []+      roundTrip stored EmptyRoot+      (modelContract @EmptyRoot).encode EmptyRoot `shouldBe` String "EmptyRoot"+    it "moves sum payloads under a payload object independently of the model layout" $ do+      let stored = good (storage (contract @Payload))+          model = modelContract @Payload+      stored.encode (Amount (MkFixed 1234)) `shouldBe` Object [("tag", String "Amount"), ("payload", Object [("_1", String "12.34")])]+      model.encode (Amount (MkFixed 1234)) `shouldBe` Object [("tag", String "Amount"), ("_1", String "12.34")]+      roundTrip stored (Amount (MkFixed 1234))+    it "applies field documentation before a projection changes the sum layout" $ do+      let stored = good (storage (field "_1" "quantity" (contract @Payload)))+      roundTrip stored (Amount (MkFixed 1234))+      case stored.schema.shape of+        SSum (_ : Variant "Amount" _ [Schema.Field "payload" (Schema _ _ _ (SObject [Schema.Field "_1" amount _])) _] : _) ->+          amount.doc `shouldBe` Just "quantity"+        other -> expectationFailure (show other)+    it "rejects a missing field as soon as the annotation is evaluated" $+      evaluate (field "missing" "quantity" (contract @Sample))+        `shouldThrow` errorCall "Agentic.Contract.field: no field named missing"+    it "applies repeated field annotations immediately, with the last one winning" $+      case outline (field "key" "second" (field "key" "first" (contract @Sample))) of+        ONamed _ (ORecord _ (keyField : _)) -> do+          keyField.name `shouldBe` "key"+          keyField.doc `shouldBe` Just "second"+          keyField.structure `shouldBe` outline (contract @FactId)+        other -> expectationFailure (show other)+    it "compiles existential cases without exposing nonmatching payload getters" $ do+      let c :: Case (Either Int Text)+          c =+            Case+              { constructor = Constructor "Left" PositionalFields+              , doc = Nothing+              , match = \case Left n -> Just n; Right _ -> Nothing+              , inject = Left+              , fields = Field "_1" Nothing (contract @Int) id+              }+          compiled :: C.CaseCodec (Either Int Text)+          compiled = good (compileCaseWith compileModel c)+      compiled.encode (Right "not this case") `shouldBe` Nothing+      compiled.encode (Left 3) `shouldBe` Just [("_1", Integer 3)]+      compiled.decode [("_1", Integer 3)] `shouldBe` Right (Left 3)+    it "accepts whole in-range model Numbers for Int and rejects overflow" $ do+      let c = modelContract @Int+      c.decode (Number 3.0) `shouldBe` Right 3+      c.decode (Number 3.5) `shouldSatisfy` isLeft+      c.decode (Integer (toInteger (maxBound :: Int) + 1)) `shouldSatisfy` isLeft+  H.describe "discovery" $ do+    it "omits representation mappings from outlines" $ do+      let original = contract @Sample+          mapped = mapStructure id id (mapStructure id id original)+      outline mapped `shouldBe` outline original+      outline (nominal original) `shouldBe` ONamed (SomeTypeRep (typeRep @Sample)) (outline original)+    it "retains applied identity and scalar parameters" $ do+      outline (contract @(Box (Fixed E2))) `shouldNotBe` outline (contract @(Box Probability))+      leaves (outline (contract @Sample))+        `shouldBe` [ ScalarInfo "example.FactId" []+                   , ScalarInfo "Data.Fixed.Fixed" [("resolution", "100")]+                   , ScalarInfo "Agentic.Questions.Probability" []+                   ]+    it "preserves nominal wrappers and aliases without flattening them to scalars" $ do+      outline (contract @Alias) `shouldBe` outline (contract @Age)+      case outline (contract @Age) of+        ONamed rep tree+          | ORecord (Constructor "Age" PositionalFields) [OutlineField "_1" _ _] <- tree ->+              rep `shouldBe` SomeTypeRep (typeRep @Age)+        other -> expectationFailure (show other)+    it "describes a singleton nullary type as a product" $+      case outline (contract @EmptyRoot) of+        ONamed _ tree -> tree `shouldBe` ORecord (Constructor "EmptyRoot" PositionalFields) []+        other -> expectationFailure (show other)+    it "keeps constructor declaration order and positional payload metadata" $ do+      case outline (contract @Payload) of+        ONamed _ tree+          | OSum cs <- tree ->+              [con | OutlineCase con _ _ <- cs]+                `shouldBe` [Constructor "Empty" PositionalFields, Constructor "Amount" PositionalFields, Constructor "Confidence" PositionalFields]+        other -> expectationFailure (show other)+  H.describe "preflight" $ do+    it "checks empty-list elements, absent optionals, and unused sum arms" $ do+      isLeft (compileModel (contract @[Hidden])) `shouldBe` True+      isLeft (compileModel (contract @(Maybe Hidden))) `shouldBe` True+      isLeft (compileModel (contract @VariantChoice)) `shouldBe` True+    it "describes a flow even when its model projection is unsupported" $+      renderTree (describe (draft @Hidden @() @IO "hidden")) `shouldSatisfy` (not . null . show)+    it "rejects unsupported tool results before any effect or provider call" $ do+      effects <- newIORef (0 :: Int)+      calls <- newIORef (0 :: Int)+      let rt = runtime{systemTwo = SystemTwo (\_ -> modifyIORef' calls (+ 1) >> pure (Turn (Raw Null) (Respond (String "done"))))}+          hiddenTool = tool @() @Hidden "hidden" "hidden output" (arr (const (Hidden "secret")))+          flow = act (\() -> modifyIORef' effects (+ 1)) >>> draftWith @Text [hiddenTool] "test"+      result <- try @FlowError (interpret rt flow ())+      result `shouldSatisfy` (\case Left (UnsupportedContract _ _) -> True; _ -> False)+      readIORef calls `shouldReturn` 0+      readIORef effects `shouldReturn` 0+    it "retains named flow context and structured projection paths" $+      case preflight (draft @Hidden @() @IO "hidden" `named` "make hidden") of+        Left (UnsupportedContract notes (ProjectionError path _)) -> do+          notes `shouldBe` [Note "make hidden" Nothing]+          path `shouldBe` ["draft.output"]+        other -> expectationFailure (show other)+    it "rejects unsupported tool inputs and nested tool body contracts" $ do+      let inputTool = tool @Hidden @Text "input" "" (arr (const "ok"))+          bodyTool = tool @() @Text "body" "" (draft @Hidden "hidden" >>> arr (const "ok"))+      preflight (draftWith @Text @() @IO [inputTool] "test") `shouldSatisfy` isLeft+      preflight (draftWith @Text @() @IO [bodyTool] "test") `shouldSatisfy` isLeft++leaves :: Outline -> [ScalarInfo]+leaves = \case+  OScalar _ info -> [info]+  ONamed _ c -> leaves c+  ORecord _ fs -> concat [leaves c | OutlineField _ _ c <- fs]+  OSum cs -> concat [leaves c | OutlineCase _ _ fs <- cs, OutlineField _ _ c <- fs]+  OList c -> leaves c+  OOptional c -> leaves c+  OUnit -> []+  OChecked _ c -> leaves c+  ODocumented _ c -> leaves c
test/Spec.hs view
@@ -1,14 +1,14 @@ module Main (main) where  import Agentic-import Agentic.Scripted import Agentic.Schema (Field (..), Schema (..), Shape (..))+import Agentic.Scripted import Data.IORef import Data.Text (Text)-import qualified Data.Text as T+import Data.Text qualified as T import GHC.Generics (Generic) import Test.Hspec hiding (describe)-import qualified Test.Hspec+import Test.Hspec qualified  -- --------------------------------------------------------------------------- -- README types@@ -34,18 +34,18 @@       , option Unbearable "People get up and leave"       ] -deriving via Enumeration Groan instance Contract Groan+instance Contract Groan where contract = enumeration  newtype Rating = Rating Int   deriving (Show, Eq)  instance Contract Rating where-  contract = mapCodec Rating (\(Rating n) -> n) (between 1 10 contract)+  contract = mapStructure Rating (\(Rating n) -> n) (between 1 10 contract)  data Review = Review {funny :: YesNo, groan :: Score Groan}   deriving (Show, Eq) -documentedJoke :: Codec Joke+documentedJoke :: Structure Joke documentedJoke =   record "A joke, split into its parts" $     Joke@@ -69,7 +69,7 @@ testRuntime :: [Action] -> Probability -> IO (Runtime IO) testRuntime turns p = do   two <- scripted turns-  pure runtime {systemOne = fixedAnswers p, systemTwo = two}+  pure runtime{systemOne = fixedAnswers p, systemTwo = two}  roundTrips :: (Eq a, Show a) => Codec a -> a -> Expectation roundTrips c a = c.decode (c.encode a) `shouldBe` Right a@@ -78,33 +78,33 @@ main = hspec $ do   describe' "Contracts" $ do     it "round-trips a derived record" $-      roundTrips contract joke+      roundTrips modelContract joke      it "round-trips a derived sum as tagged objects" $ do-      roundTrips contract (OneLiner "I'm on a seafood diet.")-      contract.encode (OneLiner "x")+      roundTrips modelContract (OneLiner "I'm on a seafood diet.")+      modelContract.encode (OneLiner "x")         `shouldBe` Object [("tag", String "OneLiner"), ("line", String "x")]      it "encodes an Options type as its labels" $ do-      contract.encode Solid `shouldBe` String "Solid"-      contract.decode (String "Unbearable") `shouldBe` Right Unbearable+      modelContract.encode Solid `shouldBe` String "Solid"+      modelContract.decode (String "Unbearable") `shouldBe` Right Unbearable      it "names derived schemas after their type" $-      (contract @Joke).schema.title `shouldBe` Just "Joke"+      (modelContract @Joke).schema.title `shouldBe` Just "Joke"      it "keeps descriptions written in the codec" $-      case documentedJoke.schema.shape of+      case (either (error . show) id (compileModel documentedJoke)).schema.shape of         SObject fs -> map ((.doc) . (.schema)) fs `shouldBe` map Just ["The style of joke", "The setup line", "The line that lands it"]         other -> expectationFailure (show other)      it "adds descriptions to a derived contract" $-      case (field "punchline" "No explanation" (contract @Joke)).schema.shape of+      case (either (error . show) id (compileModel (field "punchline" "No explanation" (contract @Joke)))).schema.shape of         SObject fs -> map ((.doc) . (.schema)) fs `shouldBe` [Nothing, Nothing, Just "No explanation"]         other -> expectationFailure (show other)      it "checks constraints the schema can't express" $ do-      (contract @Rating).decode (Integer 7) `shouldBe` Right (Rating 7)-      (contract @Rating).decode (Integer 11) `shouldBe` Left "must be between 1 and 10"+      (modelContract @Rating).decode (Integer 7) `shouldBe` Right (Rating 7)+      (modelContract @Rating).decode (Integer 11) `shouldBe` Left "must be between 1 and 10"    describe' "Questions" $ do     it "batches combined questions into one request" $@@ -149,9 +149,9 @@       interpret rt (keep 0.9 funny) [joke, joke] `shouldReturn` []      it "gates into branches" $ do-      rt <- testRuntime [respond joke {genre = "kids"}] 0.5+      rt <- testRuntime [respond joke{genre = "kids"}] 0.5       let kidFriendly = gate 0.9 funny >>> (draft @Joke "rewrite this joke for a 10-year-old" ||| returnA)-      interpret rt kidFriendly joke `shouldReturn` joke {genre = "kids"}+      interpret rt kidFriendly joke `shouldReturn` joke{genre = "kids"}      it "runs structure: fanout and each" $ do       rt <- testRuntime [respond joke, respond (Rating 3)] 1@@ -277,5 +277,5 @@                    , "└─ tool research  draft @Text  \"research this\""                    , "   └─ tool research  (see above)"                    ]-  where-    describe' = Test.Hspec.describe+ where+  describe' = Test.Hspec.describe