Λ-gent
by SPISE MISU ApS
@lambdagent

Λ-gent, pronounced «a gent», where the capital lambda (Λ) is a replacement of the letter A in agent due to their similarities and lambda calculus being the backbone of Haskell and nix {language | package manager | command} / NixOS.

With the substitution, we are now left with the word gent, the abbreviation of gentleman, which will help us define our LLM agent as:

«Polite, well educated, has excellent manners and always behaves well»

# Basics

#! /usr/bin/env nix-shell
#! nix-shell --keep --pure -i runghc
#! nix-shell --keep --pure -p haskell.compiler.ghc9103 cacert curl
#! nix-shell --keep --pure -p 'haskellPackages.ghcWithPackages (ps: with ps; [A-gent])'

On the top of the file, we will add the following header as that will allow us to run the code as a script.

Note: When using --pure it’s necessary to use --keep as well in order to pass environment variables:

Reproducible Interpreted Scripts (RIS):

In order to to achieve trully RIS, you MUST pin to a specific nixpkgs hash:

Once we have defined the header of the file, we can now define the following code options and language features to ensure clean and maintainable code:

> {-# OPTIONS_GHC -Wall -Werror -fno-warn-orphans #-}
>
> {-# LANGUAGE Safe #-}
> {-# LANGUAGE NoGeneralizedNewtypeDeriving #-}

Afterwards, we define the licensing information as well and a few library imports:

> --------------------------------------------------------------------------------
> --
> -- Λ-gent, (c) 2026 SPISE MISU ApS, https://spdx.org/licenses/SSPL-1.0
> --
> --------------------------------------------------------------------------------
>
> import           Prelude hiding
>   ( error
>   )
>
> import qualified Agent.IO.Effects    as EFF
> import qualified Agent.IO.Restricted as RIO
> import           Agent.IO.Restricted
>   ( RIO
>   )
> import           Agent.LLM
>   ( Eval
>   , Context
>   , Mode(Chat)
>   , mode
>   , repl
>   , load
>   )
> import           Agent.JSON as JSON

In order to enforce structured interactions with our LLM, we define (showable) communication data entities, that will be encoded / decoded to JSON automatically by inferring the data types from the provided specification:

> data Message = Message
>   { role      :: String
>   , content   :: String
>   , reasoning :: String -- NOTE: Only used by Response
>   } deriving (Data, Show)
> data Request = Request
>   { messages    :: [Message]
>   , temperature :: Double
>   , model       :: String
>   } deriving (Data, Show)
> data Response = Response
>   { created :: Int
>   , choices :: [Choice]
>   , usage   :: Usage
>   } deriving (Data, Show)
>
> data Choice = Choice
>   { index         :: Int
>   , finish_reason :: String
>   , message       :: Message
>   } deriving (Data, Show)
>
> data Usage = Usage
>   { prompt_tokens     :: Int
>   , completion_tokens :: Int
>   , total_tokens      :: Int
>   } deriving (Data, Show)
> data Error = Error
>   { error :: String
>   } deriving (Data, Show)
>
> data Communicate = Communicate
>   { requests  :: [Request]
>   , responses :: [Either Error Response]
>   } deriving (Data, Show)

Note: Automatic inferring of JSON-schema is still not added to the Λ-gent library as the used local (MLX) server doesn’t have support for it. However, when (iff) added, it will trully ensure output is contained within the domain:

Once we have a domain to interact with the LLM, we then define and execute our logic. In this sample, we will ONLY handle chat-mode and we wil do so separately in order to limit effects:

> eval :: Eval Communicate
> eval ctx txt =
>   case mode ctx of
>     Chat -> chat    ctx  txt
>     ____ -> return (ctx, "Only chat-mode is available for this agent")
> chat
>   :: EFF.LlmChatPost rio
>   => Context Communicate
>   -> String
>   -> rio (Context Communicate, String)
> chat ctx txt =
>   -- NOTE: Trying to inject an output effect to the console here, will result
>   -- in the following suggestion:
>   -- > Possible fix: add (Agent.IO.Restricted.StdOut rio) to the context …
>   -- However, as that effect is not exposed by the Restricted module, it will
>   -- not be possible … «No soup for you!» --Yev Kassem
>   --
>   -- RIO.output txt >>
>   EFF.llmChatWeb (JSON.encode req) >>= \ eres ->
>     case eres of
>       Right json ->
>         return
>           ( nxt
>           , case res of
>               Left  err -> error err
>               Right val -> concatMap (show . message) $ choices val
>           )
>         where
>           nxt =
>             ctx
>               { load =
>                   Just $
>                   com
>                     { responses = res : responses com
>                     }
>               }
>           res =
>             case JSON.decode json of
>               Right a                -> Right a
>               Left  JSON.InvalidJSON -> Left $ Error $ "Invalid: " ++ json
>               Left  JSON.DiffSchema  ->
>                 case JSON.decode json of
>                   Right e                -> Left e
>                   Left  JSON.InvalidJSON -> Left $ Error $ "Invalid: " ++ json
>                   Left  JSON.DiffSchema  -> Left $ Error $ "Schema: "  ++ json
>       Left err ->
>         return (nxt, err)
>         where
>           nxt =
>             ctx
>               { load =
>                   Just $
>                   com
>                     { responses = (Left $ Error err) : responses com
>                     }
>               }
>   where
>     req =
>       Request
>         { messages    =
>             [ Message
>               { role      = "user"
>               , content   = txt
>               , reasoning = []
>               }
>             ]
>         , temperature = 0.7
>         , model       = "mlx-community/Llama-3.2-3B-Instruct"
>         }
>     com =
>         case load ctx of 
>           Just loc ->
>             loc
>               { requests  =  req : requests loc
>               }
>           Nothing  ->
>             Communicate
>               { requests  = [req]
>               , responses = []
>               }
> main :: IO ()
> main =
>   repl eval

Notice how we ONLY need to define effects (and dependencies) we are going to use. At some point the Λ-gent library is going to have many effects. It would be to tedious and cumbersome to define all these instances if you only need to use a very small subset. Also, this ensures backwards compatibility whenever new effects are added to the library as they will not break previous defined script agents.

> instance EFF.LlmChatConf RIO where
>   llmChatAPI = RIO.llmChatAPI
>     -- NOTE: We just use the restricted version. However, we could change it:
>     -- RIO.getEnvVar "LLM_CHAT_REMOTE_API"
>   llmChatKey = RIO.llmChatKey
>     -- NOTE: We just use the restricted version. However, we could change it:
>     -- RIO.getEnvVar "LLM_CHAT_REMOTE_KEY"
> instance EFF.LlmChatPost RIO where
>   llmChatWeb = RIO.llmChatWeb

Fun fact: If you copy all the text from the Basics section above, except this note, and save it to a .lhs file and then convert it to an executable file with chmox +x sample.lhs, you have now defined your very own simple Λ-gent script (*) and you will now be able to execute it like this: LLM_CHAT_LOCALHOST_API="http://…:8080/v1" ./sample.lhs from a terminal and interact with your LLM. Smart huh? This is what we call literate programming (Donald Knuth, 1984).

(*) - You MUST install the nix package manager on your operating system for this script to work: https://nixos.org/download/

# Features

── Description
REPL (Read, Eval, Print and Loop) interface, just like Python
Scripts and not binaries. Stop, change and execute immediately. And you don’t have to wait for newer releases
Development in the open. No hidden agendas or backdoors. All code is accessible from GitLab and is released under the SSPLv1.0 open-source license (*).
Sandbox. As in --pure nix sandbox where only header defined packages are available
Hardening. Trivial by pinning to a specific packages hash
Further hardening. Because of the PoC nature of scripting, once it’s stable, it can then be easily transformed to a binary, which will increase performance. It could further be distributed by organizations to ensure uniform usage
LSP support which makes defining Λ-gent scripts easier
Backwards compatibility. When new effects are added to the Λ-gent library, it will have no impact on already defined scripts. Only used effects, need to be specified
Restricted IO. YOU decide what the YOUR Λ-gent does
Restricting binaries. The agent wraps binaries, such as: curl, git, which, realpath, … allowing them only to provide a subset of their functionality. You get the best of both worlds: performance and restricted IO
Information Flow Control (IFC). Security mechanism for low-level information flow analysis
Mandatory Access Control (MAC). Information security mechanism to enforce the Principle of Least Privilege (PoLP)
Protection rings. Security mechanism to further enforce PoLP
Auto encoding/decoding JSON from showable data payloads
☐️ Strict JSON schemas to limit outcome from LLM’s (Narrow AI)
☐️ Auto-infer JSON schemas from data payloads
☒️ NO malicious injections. The Λ-gent scripts will just NOT execute until removed
…️ (and many more)

Even though all these security mechanism are provided, if you choose to send sensitive data to 3ʳᵈ party services, well, then the Λ-gent will accept that

(*) - SSPL-1.0 is just a better version of AGPL-3.0 to keep Big Tech at bay.