{ config, lib, pkgs, ... }: let shared = import ./shared-options.nix {inherit lib;}; in with lib; let cfg = config.coding.agents.pi; mcpCfg = config.programs.mcp or null; in { options.coding.agents.pi = { enable = mkEnableOption "Pi agent management via canonical agent.toml definitions"; mcpServers = mkOption { type = types.attrsOf types.anything; default = if mcpCfg != null then mcpCfg.servers else {}; defaultText = literalExpression "config.programs.mcp.servers"; description = '' MCP server configurations for Pi (pi-mcp-adapter). Written to ~/.pi/agent/mcp.json. Automatically inherits from config.programs.mcp.servers. ''; }; agentsInput = shared.mkAgentsInputOption '' The `agents` flake input (your personal AGENTS repo). When set, the primary agent's system prompt is rendered as SYSTEM.md, all agents are listed in AGENTS.md, and subagent .md files are deployed. ''; modelOverrides = shared.mkModelOverridesOption; primaryAgent = mkOption { type = types.nullOr types.str; default = null; description = '' Override which canonical agent is used as primary for SYSTEM.md. When null, the first agent with mode="primary" is used. ''; }; settings = mkOption { type = types.submodule { freeformType = types.attrsOf types.anything; options = { packages = mkOption { type = types.listOf types.str; default = []; description = '' Pi packages to install (npm:, git:, or local paths). These are written to ~/.pi/agent/settings.json. ''; }; defaultProvider = mkOption { type = types.nullOr types.str; default = null; description = "Default LLM provider (e.g. 'anthropic', 'openai', 'zai')."; }; defaultModel = mkOption { type = types.nullOr types.str; default = null; description = "Default model ID."; }; defaultThinkingLevel = mkOption { type = types.nullOr (types.enum ["off" "minimal" "low" "medium" "high" "xhigh"]); default = null; description = "Default extended thinking level."; }; theme = mkOption { type = types.nullOr types.str; default = null; description = "Pi theme name."; }; hideThinkingBlock = mkOption { type = types.nullOr types.bool; default = null; description = "Hide thinking blocks in output."; }; quietStartup = mkOption { type = types.nullOr types.bool; default = null; description = "Hide startup header."; }; compaction = mkOption { type = types.nullOr (types.submodule { options = { enabled = mkOption { type = types.nullOr types.bool; default = null; }; reserveTokens = mkOption { type = types.nullOr types.int; default = null; }; keepRecentTokens = mkOption { type = types.nullOr types.int; default = null; }; }; }); default = null; description = "Auto-compaction settings."; }; enabledModels = mkOption { type = types.nullOr (types.listOf types.str); default = null; description = "Model patterns for Ctrl+P cycling."; }; sessionDir = mkOption { type = types.nullOr types.str; default = null; description = "Directory where session files are stored."; }; extensions = mkOption { type = types.listOf types.str; default = []; description = "Local extension file paths or directories."; }; skills = mkOption { type = types.listOf types.str; default = []; description = "Local skill file paths or directories."; }; }; }; default = {}; description = '' Pi settings written to ~/.pi/agent/settings.json. Only non-null values are included in the generated JSON. See pi docs/settings.md for all options. ''; }; }; config = mkIf cfg.enable (let # Build settings.json by filtering out null values recursively filterNulls = attrs: lib.filterAttrs (_: v: v != null) ( builtins.mapAttrs (_: v: if builtins.isAttrs v then let filtered = filterNulls v; in if filtered == {} then null else filtered else v) attrs ); piSettings = filterNulls cfg.settings; # Rendered agents (only computed when agentsInput is set) rendered = if cfg.agentsInput != null then (import ../../../../lib {inherit lib;}).agents.renderForPi { inherit pkgs; canonical = cfg.agentsInput.lib.loadAgents; modelOverrides = cfg.modelOverrides; primaryAgent = cfg.primaryAgent; } else null; # Dynamic home.file entries for agent .md files agentFiles = if cfg.agentsInput != null then let agentNames = builtins.attrNames cfg.agentsInput.lib.loadAgents; in builtins.listToAttrs ( map (name: { name = ".pi/agent/agents/${name}.md"; value = {source = "${rendered}/agents/${name}.md";}; }) agentNames ) else {}; in { home.file = mkMerge [ # ── MCP servers from programs.mcp → ~/.pi/agent/mcp.json ─────── (mkIf (cfg.mcpServers != {}) { ".pi/agent/mcp.json".text = builtins.toJSON {mcpServers = cfg.mcpServers;}; }) # ── ~/.pi/agent/settings.json ────────────────────────────────── { ".pi/agent/settings.json".text = builtins.toJSON piSettings; } # ── AGENTS.md — agent descriptions and specialist listing ────── (mkIf (cfg.agentsInput != null) { ".pi/agent/AGENTS.md".source = "${rendered}/AGENTS.md"; }) # ── SYSTEM.md — primary agent's system prompt ────────────────── (mkIf (cfg.agentsInput != null) { ".pi/agent/SYSTEM.md".source = "${rendered}/SYSTEM.md"; }) # ── Agents — pi-subagents .md files ──────────────────────────── agentFiles ]; }); }