Λ-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:
-Walland-Werrorto help us remove unnecesary code-fno-warn-orphansas we most likely aren’t going to use all provided effects, we don’t want to recieve any warnings about itSafewe will tell our agent to ONLY execute safe-code and behave nicely!!!NoGeneralizedNewtypeDerivingand do NOT allow inheritance by deriving fornewtype.
> {-# 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
.lhsfile and then convert it to an executable file withchmox +x sample.lhs, you have now defined your very own simpleΛ-gentscript (*) and you will now be able to execute it like this:LLM_CHAT_LOCALHOST_API="http://…:8080/v1" ./sample.lhsfrom a terminal and interact with your LLM. Smart huh? This is what we call literate programming (Donald Knuth, 1984).(*) - You MUST install the
nixpackage 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
Λ-gentwill accept that
(*) - SSPL-1.0 is just a better version of
AGPL-3.0 to keep Big Tech at bay.