summary refs log tree commit diff
path: root/modules/foundation/agent.nix
diff options
context:
space:
mode:
Diffstat (limited to 'modules/foundation/agent.nix')
-rw-r--r--modules/foundation/agent.nix196
1 files changed, 196 insertions, 0 deletions
diff --git a/modules/foundation/agent.nix b/modules/foundation/agent.nix
new file mode 100644
index 0000000..6ac25aa
--- /dev/null
+++ b/modules/foundation/agent.nix
@@ -0,0 +1,196 @@
+{
+  config,
+  pkgs,
+  lib,
+  hermes-agent,
+  ...
+}:
+
+let
+  inherit (lib) mkEnableOption mkIf mkMerge;
+  cfg = config.foundation.agent;
+
+  # the set of tools the agent has access to by default.
+  # it should also always be able to call `nix-shell` to gain access to more tools.
+  agentPackages = with pkgs; [
+    nix
+    nixfmt-rfc-style
+
+    openssh
+    tailscale
+
+    docker_29
+
+    jq
+    yq-go
+    ripgrep
+    fd
+    tree
+    file
+    unzip
+    gnused
+    gawk
+
+    curl
+    wget
+
+    htop
+    glances
+    lsof
+    iproute2
+    iputils
+    inetutils
+
+    dig
+    tcpdump
+    nmap
+
+    git
+
+    vim
+  ];
+in
+{
+  imports = [
+    hermes-agent.nixosModules.default
+  ];
+
+  options.foundation.agent = {
+    home = mkEnableOption "the 'renard' agent to live on this machine";
+    neighbor = mkEnableOption "letting the 'renard' agent access this machine";
+  };
+
+  config = mkMerge [
+    # both the home and each neighbor have a user for the agent.
+    (mkIf (cfg.home || cfg.neighbor) {
+      users = {
+        groups.renard = { };
+
+        users.renard = {
+          isSystemUser = true;
+          group = "renard";
+          home = "/home/renard";
+          createHome = true;
+          shell = pkgs.bashInteractive;
+          packages = agentPackages;
+        };
+      };
+    })
+
+    # the agent lives at home.
+    (mkIf cfg.home {
+      age.secrets.renard-agent = {
+        file = ../../secrets/renard-agent.age;
+        owner = "renard";
+        group = "renard";
+        mode = "0440";
+      };
+
+      services.hermes-agent = {
+        enable = true;
+
+        user = "renard";
+        group = "renard";
+        stateDir = "/home/renard";
+        createUser = false;
+
+        extraDependencyGroups = [ "messaging" ];
+
+        # both the user and the path house the same package set
+        extraPackages = agentPackages;
+
+        environmentFiles = [
+          # required secrets:
+          # * OPENROUTER_API_KEY
+          # * DISCORD_BOT_TOKEN
+          # * DISCORD_ALLOWED_USERS
+          config.age.secrets.renard-agent.path
+        ];
+
+        documents = {
+          "SOUL.md" = ../../assets/agent/SOUL.md;
+        };
+
+        settings = {
+          model = {
+            provider = "openrouter";
+            # my current favorite budget model, with recent price
+            # changes by far the cheapest for the performance,
+            # with other chinese models also being close.
+            # only modality is text, requires auxiliary vision model!
+            # native price: $0.44/m in, $0.87/m out.
+            default = "deepseek/deepseek-v4-pro";
+          };
+
+          agent = {
+            reasoning_effort = "high"; # 'high' rated highest in benchmarks, tune to taste!
+            max_turns = 90;
+          };
+
+          auxiliary.vision = {
+            # auxiliary vision model for image comprehension.
+            # this qwen model is built for this exact use-case and is extremely
+            # cheap for the performance (though below gemini), however,
+            # it does not do any reasoning on the visual input!
+            # possible upgrade path for reasoning: qwen3-vl-30b-a3b-thinking
+            # native price: $0.13/m in, $1.56/m out.
+            provider = "openrouter";
+            model = "qwen/qwen3-vl-32b-instruct";
+            timeout = 120;
+          };
+
+          toolsets = [ "all" ];
+
+          terminal = {
+            backend = "local";
+            persistent_shell = true;
+            timeout = 180;
+          };
+
+          memory = {
+            memory_enabled = true;
+            user_profile_enabled = true;
+          };
+
+          # slightly higher limits and targets than default for better
+          # long session preservation.
+          compression = {
+            enabled = true;
+            threshold = 0.75;
+            target_ratio = 0.35;
+            protect_last_n = 40;
+          };
+
+          # progressive message editing, a little silly in discord but looks like
+          # the classic llm chats.
+          streaming = {
+            enabled = true;
+          };
+          display = {
+            streaming = true; # small regression with streaming.enabled, keep both on.
+            tool_progress = "new"; # only show fresh new tool calls, not every call.
+          };
+
+          discord = {
+            require_mention = false;
+            thread_require_mention = false;
+            auto_thread = false;
+            reactions = true;
+            history_backfill = true;
+            allow_mentions = {
+              everyone = false;
+              roles = false;
+              users = true;
+              replied_user = true;
+            };
+          };
+
+          unauthorized_dm_behavior = "ignore"; # ignore strangers
+
+          timezone = "Europe/Berlin";
+          group_sessions_per_user = false;
+        };
+      };
+    })
+  ];
+}