diff --git a/crates/sprout-acp/src/config.rs b/crates/sprout-acp/src/config.rs index f7cf4923..c39f4bee 100644 --- a/crates/sprout-acp/src/config.rs +++ b/crates/sprout-acp/src/config.rs @@ -328,6 +328,17 @@ pub struct CliArgs { #[arg(long, env = "SPROUT_ACP_NO_TYPING")] pub no_typing: bool, + /// Disable NIP-AE agent core memory injection. + /// + /// When set, the harness skips the per-session core engram fetch and + /// renders prompts with no `[Agent Memory — core]` section, regardless of + /// whether a core engram exists on the relay. The `sprout mem` CLI and + /// the relay's acceptance of kind:30174 engrams are unaffected — this + /// flag is purely an opt-out for prompt-time injection in the ACP + /// harness. + #[arg(long, env = "SPROUT_ACP_NO_MEMORY")] + pub no_memory: bool, + /// Desired LLM model ID. Applied to every new ACP session after creation. /// Use `sprout-acp models` to discover available model IDs. #[arg(long, env = "SPROUT_ACP_MODEL")] @@ -416,6 +427,11 @@ pub struct Config { pub max_turns_per_session: u32, pub presence_enabled: bool, pub typing_enabled: bool, + /// Whether NIP-AE agent core memory injection is enabled. When false, + /// the harness skips the per-session core engram fetch and renders no + /// `[Agent Memory — core]` section. Mirrors the `--no-memory` / + /// `SPROUT_ACP_NO_MEMORY` opt-out. + pub memory_enabled: bool, /// Desired LLM model ID. Applied after every `session_new_full()`. pub model: Option, /// Permission mode to apply after session creation. `Default` = skip. @@ -761,6 +777,7 @@ impl Config { max_turns_per_session: args.max_turns_per_session, presence_enabled: !args.no_presence, typing_enabled: !args.no_typing, + memory_enabled: !args.no_memory, model, permission_mode: args.permission_mode, respond_to: args.respond_to, @@ -782,7 +799,7 @@ impl Config { other => format!("respond_to={other}"), }; format!( - "relay={} pubkey={} agent_cmd={} {} mcp_cmd={} idle_timeout={}s max_turn={}s agents={} heartbeat={}s subscribe={:?} dedup={:?} meh={:?} ignore_self={} context_limit={} max_turns_per_session={} presence={} typing={} model={} permission_mode={} {}", + "relay={} pubkey={} agent_cmd={} {} mcp_cmd={} idle_timeout={}s max_turn={}s agents={} heartbeat={}s subscribe={:?} dedup={:?} meh={:?} ignore_self={} context_limit={} max_turns_per_session={} presence={} typing={} memory={} model={} permission_mode={} {}", self.relay_url, self.keys.public_key().to_hex(), self.agent_command, @@ -800,6 +817,7 @@ impl Config { self.max_turns_per_session, self.presence_enabled, self.typing_enabled, + self.memory_enabled, self.model.as_deref().unwrap_or("(agent default)"), self.permission_mode, respond_to_detail, @@ -1122,6 +1140,7 @@ mod tests { max_turns_per_session: 0, presence_enabled: true, typing_enabled: true, + memory_enabled: true, model: None, permission_mode: PermissionMode::BypassPermissions, respond_to: RespondTo::Anyone, @@ -1705,6 +1724,38 @@ channels = "ALL" ); } + // ── no-memory toggle ──────────────────────────────────────────────────── + + #[test] + fn test_memory_enabled_default_true() { + let config = test_config(SubscribeMode::Mentions); + assert!( + config.memory_enabled, + "memory_enabled should default to true" + ); + } + + #[test] + fn test_summary_includes_memory_enabled() { + let config = test_config(SubscribeMode::Mentions); + let s = config.summary(); + assert!( + s.contains("memory=true"), + "summary should include memory=true by default, got: {s}" + ); + } + + #[test] + fn test_summary_reflects_memory_disabled() { + let mut config = test_config(SubscribeMode::Mentions); + config.memory_enabled = false; + let s = config.summary(); + assert!( + s.contains("memory=false"), + "summary should include memory=false when disabled, got: {s}" + ); + } + // ── permission mode ───────────────────────────────────────────────────── #[test] diff --git a/crates/sprout-acp/src/lib.rs b/crates/sprout-acp/src/lib.rs index a7f93420..ce5932de 100644 --- a/crates/sprout-acp/src/lib.rs +++ b/crates/sprout-acp/src/lib.rs @@ -1089,8 +1089,16 @@ async fn tokio_main() -> Result<()> { agent_owner_pubkey: startup_owner .as_deref() .and_then(|hex| nostr::PublicKey::from_hex(hex).ok()), + memory_enabled: config.memory_enabled, }); + if !config.memory_enabled { + tracing::info!( + target: "engram::core", + "NIP-AE core memory injection disabled (--no-memory / SPROUT_ACP_NO_MEMORY)" + ); + } + // ── Step 6: Heartbeat timer ─────────────────────────────────────────────── let mut heartbeat = if config.heartbeat_interval_secs > 0 { let interval = Duration::from_secs(config.heartbeat_interval_secs); @@ -2753,6 +2761,7 @@ mod build_mcp_servers_tests { max_turns_per_session: 0, presence_enabled: true, typing_enabled: true, + memory_enabled: true, model: None, permission_mode: config::PermissionMode::BypassPermissions, respond_to: config::RespondTo::Anyone, diff --git a/crates/sprout-acp/src/pool.rs b/crates/sprout-acp/src/pool.rs index dbbdcf9e..099e0a24 100644 --- a/crates/sprout-acp/src/pool.rs +++ b/crates/sprout-acp/src/pool.rs @@ -209,6 +209,12 @@ pub struct PromptContext { /// Owner pubkey (hex), if resolved at startup. When unset, NIP-AE core /// injection is skipped entirely (no owner = no `(agent, owner)` pair). pub agent_owner_pubkey: Option, + /// Whether NIP-AE agent core memory injection is enabled. When false, + /// the per-session core engram fetch is skipped and `core_sections` + /// remains empty for every channel, so `format_prompt` renders no + /// `[Agent Memory — core]` section. Driven by `--no-memory` / + /// `SPROUT_ACP_NO_MEMORY`. + pub memory_enabled: bool, } // ── AgentPool impl ──────────────────────────────────────────────────────────── @@ -767,7 +773,13 @@ pub async fn run_prompt_task( // Per Tyler's locked spec: NO mid-session refreshes. Re-fetch only // happens when a session is invalidated and recreated (see // `SessionState::invalidate_channel`). - if is_new_session { + // + // Operator opt-out: `--no-memory` / `SPROUT_ACP_NO_MEMORY` disables the + // entire NIP-AE injection path. We skip the fetch outright and leave + // `state.core_sections` empty, so `format_prompt` renders no core + // section. The `sprout mem` CLI and the relay's acceptance of + // kind:30174 engrams are unaffected. + if is_new_session && ctx.memory_enabled { if let (PromptSource::Channel(cid), Some(owner_pk)) = (&source, ctx.agent_owner_pubkey.as_ref()) {