Λ-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 cacert curl
#! nix-shell --keep --pure -p '(haskellPackages.extend (self: super: {pin = self.callHackageDirect { pkg = "A-gent"; ver = "0.11.0.18"; sha256 = "BsvlGOludmvVvfjLYvjBkQumj2VuIe6M8kB/9CYkLlo=";} {};})).ghcWithPackages (ps: with ps; [ pin ])'w

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:

> {-# 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)

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 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 .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/

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
☐️ 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.