From 8588e7e7aa7c912f6080892a33f56cbfec8c0578 Mon Sep 17 00:00:00 2001 From: Santhi Prakash Date: Tue, 4 Aug 2026 16:44:52 +0000 Subject: [PATCH] fix(acp): honor persona `thread_replies` in prompt builder `thread_replies` was parsed and merged in `buzz-persona` but never read in `buzz-acp`. The harness always injected an `IMPORTANT:` directive forcing `--reply-to ` for new top-level channel mentions, so a persona with `thread_replies: false` still opened a thread. Plumb the resolved flag through `Config` / `PromptContext` / `FormatPromptArgs` with a default of `true` (per PERSONA_PACK_SPEC.md). When `thread_replies` is `false`, `format_prompt` skips the forced reply anchor for new top-level channel mentions. Existing thread and DM anchors continue to work, so the agent can still participate in an active thread or DM. - Add `--thread-replies` / `BUZZ_ACP_THREAD_REPLIES` to `CliArgs` and `Config`. - Thread the value through `PromptContext` and `FormatPromptArgs`. - Skip `resolve_reply_anchor` for human-facing top-level channel turns when `thread_replies == false`. - Add `queue.rs` unit tests covering top-level suppression, existing threads, and DMs when `thread_replies: false`. Fixes #4699. Signed-off-by: Santhi Prakash --- crates/buzz-acp/src/config.rs | 16 ++++ crates/buzz-acp/src/lib.rs | 3 + crates/buzz-acp/src/pool.rs | 8 ++ crates/buzz-acp/src/queue.rs | 147 +++++++++++++++++++++++++++++++++- 4 files changed, 172 insertions(+), 2 deletions(-) diff --git a/crates/buzz-acp/src/config.rs b/crates/buzz-acp/src/config.rs index 35aaec188d..6f91e9672b 100644 --- a/crates/buzz-acp/src/config.rs +++ b/crates/buzz-acp/src/config.rs @@ -474,6 +474,15 @@ pub struct CliArgs { #[arg(long, env = "BUZZ_ACP_RELAY_OBSERVER", default_value_t = false)] pub relay_observer: bool, + /// Whether the agent should default to replying in the current thread. + /// + /// Mirrors the `thread_replies` behavioral config from persona packs. + /// When false, the harness does NOT append `--reply-to` instructions for + /// new top-level channel mentions, letting the agent post flat at channel root. + /// Existing thread and DM reply anchors are unaffected. + #[arg(long, env = "BUZZ_ACP_THREAD_REPLIES", default_value_t = true)] + pub thread_replies: bool, + /// Exit after this many seconds with no dispatched events and no turn in flight. /// 0 disables inactivity self-termination. #[arg(long, env = "BUZZ_ACP_EXIT_AFTER_INACTIVITY", default_value_t = 0)] @@ -544,6 +553,11 @@ pub struct Config { pub respond_to_allowlist: HashSet, /// Allowed `respond_to` modes. Empty = all modes allowed. pub allowed_respond_to: Vec, + /// Whether the agent should default to replying in the current thread. + /// + /// Mirrors the `thread_replies` behavioral config from persona packs. + /// Defaults to `true` per `PERSONA_PACK_SPEC.md`. + pub thread_replies: bool, /// Per-persona env vars to inject at agent spawn time (e.g., GOOSE_PROVIDER, GOOSE_MODEL, BUZZ_AGENT_MODEL). /// Populated from persona pack resolution. Empty when no pack is configured. pub persona_env_vars: Vec<(String, String)>, @@ -1102,6 +1116,7 @@ impl Config { respond_to: args.respond_to, respond_to_allowlist, allowed_respond_to, + thread_replies: args.thread_replies, persona_env_vars, has_generated_codex_config, relay_observer: args.relay_observer, @@ -1473,6 +1488,7 @@ mod tests { respond_to: RespondTo::Anyone, respond_to_allowlist: HashSet::new(), allowed_respond_to: Vec::new(), + thread_replies: true, persona_env_vars: vec![], has_generated_codex_config: false, relay_observer: false, diff --git a/crates/buzz-acp/src/lib.rs b/crates/buzz-acp/src/lib.rs index 811253e4ac..0857697e9b 100644 --- a/crates/buzz-acp/src/lib.rs +++ b/crates/buzz-acp/src/lib.rs @@ -1610,6 +1610,7 @@ async fn tokio_main() -> Result<()> { .as_deref() .and_then(|hex| nostr::PublicKey::from_hex(hex).ok()), memory_enabled: config.memory_enabled, + thread_replies: config.thread_replies, harness_name: crate::config::normalize_agent_command_identity(&config.agent_command), relay_url: config.relay_url.clone(), }); @@ -5129,6 +5130,7 @@ mod build_mcp_servers_tests { respond_to: config::RespondTo::Anyone, respond_to_allowlist: std::collections::HashSet::new(), allowed_respond_to: vec![], + thread_replies: true, persona_env_vars: vec![], has_generated_codex_config: false, relay_observer: false, @@ -5351,6 +5353,7 @@ mod error_outcome_emission_tests { respond_to: config::RespondTo::Anyone, respond_to_allowlist: HashSet::new(), allowed_respond_to: vec![], + thread_replies: true, persona_env_vars: vec![], has_generated_codex_config: false, relay_observer: false, diff --git a/crates/buzz-acp/src/pool.rs b/crates/buzz-acp/src/pool.rs index 64edf68ee2..ee01b047ae 100644 --- a/crates/buzz-acp/src/pool.rs +++ b/crates/buzz-acp/src/pool.rs @@ -557,6 +557,12 @@ pub struct PromptContext { /// `[Agent Memory — core]` section. On by default; disabled via /// `--no-memory` / `BUZZ_ACP_NO_MEMORY`. pub memory_enabled: bool, + /// Whether the agent should default to replying in the current thread. + /// + /// Mirrors the `thread_replies` behavioral config from persona packs. + /// When false, `format_prompt` skips forced `--reply-to` anchors for new + /// top-level channel mentions. Existing threads and DMs are unaffected. + pub thread_replies: bool, /// Harness identity string for NIP-AM `harness` field. Derived from the /// configured `agent_command` at startup (e.g. `"goose"`, `"buzz-agent"`). pub harness_name: String, @@ -1875,6 +1881,7 @@ pub async fn run_prompt_task( system_prompt: ctx.system_prompt.as_deref(), team_instructions: ctx.team_instructions.as_deref(), agent_canvas: agent_canvas.as_deref(), + thread_replies: ctx.thread_replies, }, ) } else { @@ -6455,6 +6462,7 @@ mod tests { agent_keys: agent_keys.clone(), agent_owner_pubkey: owner_pubkey, memory_enabled: false, + thread_replies: true, harness_name: "goose".to_string(), relay_url: "ws://127.0.0.1:3000".to_string(), } diff --git a/crates/buzz-acp/src/queue.rs b/crates/buzz-acp/src/queue.rs index 5c960de202..805c3186d5 100644 --- a/crates/buzz-acp/src/queue.rs +++ b/crates/buzz-acp/src/queue.rs @@ -1354,7 +1354,6 @@ fn format_conversation_context( } /// Arguments for [`format_prompt`] beyond the required [`FlushBatch`]. -#[derive(Default)] pub struct FormatPromptArgs<'a> { pub agent_core: Option<&'a str>, pub channel_info: Option<&'a PromptChannelInfo>, @@ -1377,6 +1376,31 @@ pub struct FormatPromptArgs<'a> { /// For legacy agents it rides in the user message on every turn of the /// session, alongside `[Base]`/`[System]`/`[Agent Memory — core]`. pub agent_canvas: Option<&'a str>, + /// Whether the agent should default to replying in the current thread. + /// + /// Mirrors the `thread_replies` behavioral config from persona packs. + /// When false, the harness does NOT append a forced `--reply-to` anchor + /// for new top-level channel mentions, leaving the agent free to post flat + /// at the channel root. Existing thread and DM anchors are unaffected. + /// Defaults to `true` to preserve existing behavior. + pub thread_replies: bool, +} + +impl<'a> Default for FormatPromptArgs<'a> { + fn default() -> Self { + Self { + agent_core: None, + channel_info: None, + conversation_context: None, + profile_lookup: None, + has_system_prompt_support: false, + base_prompt: None, + system_prompt: None, + team_instructions: None, + agent_canvas: None, + thread_replies: true, + } + } } /// Format the `[Base]` section for the base prompt. @@ -1469,8 +1493,14 @@ pub fn format_prompt(batch: &FlushBatch, args: &FormatPromptArgs<'_>) -> Vec