Λ-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 cacert curl
#! nix-shell --keep --pure -p '(haskell.packages.ghc9103.extend (self: super: {pin = self.callHackageDirect { pkg = "A-gent"; ver = "0.11.0.19"; sha256 = "j/5LzXJy6SLkXxKuTOtsVbOyxoZtJjHgMiw7C7HQdnY=";} {};})).ghcWithPackages (ps: with ps; [ pin ])'
On the top of the file, we will add the following header as that will allow us
to run the code as a script. We do pin the latest Λ-gent package.
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, mod )
>
> import Data.Either ( rights )
>
> import Agent.Data.JSON ( Data )
> import qualified Agent.Data.JSON as JSON
> import qualified Agent.IO.Effects as EFF
> import qualified Agent.IO.Restricted as RIO
> import Agent.IO.Restricted
> ( RIO
> )
> import Agent.LLM
> ( Context (Context, mode, load)
> , Eval
> , Mode(Chat)
> , repl
> )
> import qualified Agent.LLM.Action as ACT
> import qualified Agent.LLM.Message as MSG
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
> } 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 Communication = Communication
> [Either Error Message] deriving (Data, Show)
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
See below, at Features why we will not longer follow the JSON schemas path to achieve Narrow AI
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 Communication
> eval ctx msg =
> case ctx of
> Context { mode = Chat } -> chat ctx msg
> _______________________ -> return (ctx, ACT.text err)
> where
> mod = mode ctx
> err = show mod ++ " mode is not available for this agent"
> chat
> :: EFF.LlmChatPost rio
> => Context Communication
> -> MSG.Message
> -> rio (Context Communication, ACT.Action)
> chat ctx msg =
> case msg of
> MSG.Text 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 ->
> ACT.text $ error err
> Right val ->
> ACT.text $ concatMap (show . message) $ choices val
> )
> where
> nxt =
> ctx
> { load = Just l
> }
> where
> l =
> case com of
> Communication ems ->
> case res of
> Left err ->
> Communication $ ems ++
> [Left err]
> Right val ->
> Communication $ ems ++
> (map (Right . message) $ choices val)
> 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, ACT.text err)
> where
> nxt =
> ctx
> { load = Just l
> }
> where
> l =
> case com of
> Communication ems ->
> Communication $ ems ++ [Left $ Error err]
> where
> req =
> Request
> { messages = rs
> , temperature = 0.7
> , model = "mlx-community/Llama-3.2-3B-Instruct"
> }
> where
> rs =
> case com of
> Communication ems -> rights ems
> com =
> case load ctx of
> Just (Communication ems) -> Communication $ ems ++ [Right erm]
> Nothing -> Communication [Right erm]
> erm =
> Message
> { role = "user"
> , content = txt
> }
> ____________ ->
> return
> ( ctx
> , ACT.text
> $ "The following message '" ++ show msg ++ "', is not supported"
> )
> 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/
The result from executing the script will be:
# Exit Λ-gent with /e or /exit. For more commands, type /? or /help.
Λ-chat> ping
Message {role = "assistant", content = "*PING*"}
Λ-chat> /drop
Dropping end-user data load
Λ-chat> If I write ping, you write pong. If I write pong, you write ping
Message {role = "assistant", content = "That sounds like a fun game. I'm ready."}
Λ-chat> pong
Message {role = "assistant", content = "ping"}
Λ-chat> pong
Message {role = "assistant", content = "ping"}
Λ-chat> ping
Message {role = "assistant", content = "pong"}
Λ-chat> pong
Message {role = "assistant", content = "ping"}
Λ-chat> /drop
Dropping end-user data load
Λ-chat> ping
Message {role = "assistant", content = "*PING*"}
Λ-chat>
# 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 as well as
Typed Holes Development (THD), which makes
defining Λ-gent scripts easier. A typed hole example:
changeMode = _ and Found hole: _ :: Context a -> Mode |
| ☑ | 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 |
| ▣️ | |
| ▣️ | |
| ☒️ | 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.Notice how we no longer will opt for JSON schemas to achieve Narrow AI. We realized that Typed Holes Development (THD), in combination with MISU and Restricted/Granulated effects, is a way more powerful and sound & safe approach to limit (if not exclude) the possibility of creating
AI-slop.
(*) - SSPL-1.0 is just a better version of
AGPL-3.0 to keep Big Tech at bay.