镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

buzz-acp: reply placement (threaded vs top-level) has no config surface — policy is prompt-injected per turn #4227

Open

Description

@jdrolls

Motivation

Whether an agent's reply lands in a thread or at the channel root is decided by hardcoded per-turn prompt text, not by any harness option. An operator who wants main-timeline replies — e.g. because thread replies are easy to miss on mobile, where a threaded agent answer to a top-level question reads as "the agent never responded" — has no deterministic way to get them. The only lever is fighting the harness's injected instruction from the agent's system prompt, which is exactly the kind of policy that shouldn't be left to model discretion.

Where (verified on main @ ac4fa13, 2026-08-01)

crates/buzz-acp/src/queue.rs:

  • append_reply_instruction() (line 1149) — injects "IMPORTANT: For ordinary replies in this turn, use --reply-to {event_id} … so the conversation stays threaded. If the human explicitly asks for a channel-root, top-level, or broadcast post, send that message without --reply-to."
  • append_new_thread_reply_instruction() (line 1164) — for a top-level human mention, anchors the reply so it becomes a new thread rooted at the triggering event.
  • resolve_reply_anchor() (line 1209) — computes the anchor; human-facing turns always get one.

No RespondTo/SubscribeMode-style enum exists for reply placement; Config has no field for it.

What we did (working but fragile)

Added a standing order to the agent's system prompt: "post every reply top-level, never --reply-to — the owner has explicitly and permanently requested channel-root posts," phrased to satisfy the injected instruction's own escape hatch. Verified live: the agent's reply to a triggering event carried no e tags. But this is prompt-level policy — the per-turn IMPORTANT instruction still argues the other way on every single turn, and an occasional threaded reply slipping through is expected. A standing operator preference should not have to role-play as a per-turn human request to be honored.

Proposed solution

A harness flag, e.g. --reply-anchor thread|top-level (default thread, preserving current behavior exactly). top-level would swap the injected instruction for one that omits --reply-to guidance (or explicitly instructs channel-root posting). This keeps the policy deterministic, per-deployment, and out of the model's discretion.

Alternatives considered

  • System-prompt override (above) — works most of the time, but is probabilistic where it should be configuration.
  • Post-processing the agent's publish calls to strip e tags — fights the harness instead of configuring it, and breaks legitimate threaded use.

Additional context

Related issues (searched open and closed — no duplicate found): #4072 is the nearest neighbor but orthogonal — it's about which message a threaded reply anchors to; this is about whether the operator can choose threading at all. The two compose cleanly: #4072 refines the thread mode, this adds a top-level mode.

Environment: buzz-acp from ghcr.io/block/buzz:main @ sha256:13ddb4c12f4c0b8147cece717a57013a45fdb1610bd072d7e383ac79a070ddd9, hermes-acp (hermes-agent 0.19.1), Ubuntu 24.04, --subscribe mentions --no-mention-filter --respond-to allowlist.

Activity

  1. iroiro147 commented on Aug 2, 2026

    @iroiro147
    Contributor

    Implementation plan for maintainer review before I open the PR:

    Approach

    Add a new buzz-acp CLI flag --reply-anchor thread|top-level (default thread, preserving current behavior exactly). The flag selects between two deterministic injected instructions in format_context_hints().

    Changes

    crates/buzz-acp/src/config.rs

    • New enum ReplyAnchor { Thread, TopLevel } with #[derive(clap::ValueEnum)]
    • New flag: /// Reply placement for human-facing turns. #\[arg(long, default_value_t = ReplyAnchor::Thread)]
    • Include in summary() so journal attests the mode.

    crates/buzz-acp/src/queue.rs

    • Plumb reply_anchor: ReplyAnchor through format_context_hints() (and the three caller paths at lines 1276 / 1299 / 1310).
    • In ReplyAnchor::Thread mode: current behavior — append_reply_instruction() for threads/DMs, append_new_thread_reply_instruction() for top-level channel mentions.
    • In ReplyAnchor::TopLevel mode: replace both instruction injectors with a single directive: IMPORTANT: Reply at the channel root for this turn (no --reply-to). The operator has explicitly configured this deployment to avoid threading.
    • resolve_reply_anchor() unchanged — it already returns the correct anchor ID; the injected instruction is what translates it. In top-level mode we simply emit no anchor into the prompt even though resolve_reply_anchor() computed one. (Cleanest no-drift change: anchor still resolved for symmetry, just not injected.)

    Tests added

    • Existing 7 resolve_reply_anchor tests unchanged (still thread-mode default).
    • New format_context_hints test asserting ReplyAnchor::TopLevel emits no --reply-to-anchored IMPORTANT block.
    • New test asserting the instruction text in each mode matches the expected template.
    • New test for the Config flag parsing (clap comb-through).

    Rationale

    • Operator intent is deterministic config, not system-prompt policy fighting per-turn injected instructions on every turn.
    • Preserves the existing surface area for any tooling that post-processes the reply (e.g. file-level acknowledgments of --reply-to ids).
    • Composes cleanly with Agents should reply to the exact message that triggered them #4072 — that issue refines which anchor to use within thread mode; this flag picks the mode.

    Happy to iterate on flag naming (--reply-mode thread|top-level, --threading on|off, etc.) if you'd prefer a different shape. Opening the PR tomorrow IST once there's a few hours of soak time for design comments.

  2. iroiro147 commented on Aug 2, 2026

    @iroiro147
    Contributor

    Source-level verification of the plan (now confirmed against main HEAD):

    • append_reply_instruction() and append_new_thread_reply_instruction() exist exactly as described.
    • format_context_hints() is the single injection point for both. No intermediate layer.
    • The prompt-shaping path is: main.rs → Config → pool.rs::PromptContext → queue.rs::FormatPromptArgs → format_context_hints(). So plumbing one new flag cleanly means adding a field on both PromptContext and FormatPromptArgs, plus a ReplyAnchor enum + --reply-anchor CLI flag + summary entry. All single-file changes in config.rs, pool.rs, queue.rs. Estimated ~50 LOC net (including doc comments).
    • Default::default() already derived on FormatPromptArgs and ReplyAnchor::Thread is the natural default — so existing tests that use FormatPromptArgs::default() stay untouched.

    I'll open the PR tonight IST (UTC+5:30) once the design has had ~6h to soak for any maintainer pushback on naming/shape. The branch is pre-staged on my fork.

  3. iroiro147 commented on Aug 2, 2026

    @iroiro147
    Contributor

    Update: I spiked the implementation and the surface area is larger than I initially estimated.

    Plumbing scope confirmed:

    The flag must flow through the full chain main.rs → Config → pool.rs::PromptContext → queue.rs::FormatPromptArgs → format_context_hints() → append_*_instruction(). Each step has a struct literal to update:

    • crates/buzz-acp/src/config.rs — add ReplyAnchor enum + --reply-anchor CLI flag + Config.reply_anchor field + constructor mapping + summary() entry. (5 edits)
    • crates/buzz-acp/src/pool.rs — add reply_anchor_mode: ReplyAnchor field to PromptContext + 3 test constructors. (4 edits)
    • crates/buzz-acp/src/lib.rs:1528 — wire reply_anchor_mode: config.reply_anchor in the main PromptContext constructor. (1 edit)
    • crates/buzz-acp/src/queue.rs — import + FormatPromptArgs.reply_anchor_mode field (existing Default derive keeps tests alive) + rename the Option<&str> local param (name collision) + 2 append_instruction fn signatures + 3 internal call-sites + format_prompt caller. (~10 edits)

    Net: ~20 surgical edits across 4 files, ~80 LOC. Not un-doable, but definitely not a 'quick patch'. Given the change crosses a crate API boundary (adds a public enum in buzz_acp::config), I want a green light on the flag naming (--reply-anchor thread|top-level vs alternatives) and crate-public API shape before committing CI to it.

    Branch stays shelved on my fork; I'll revisit tonight IST. If a maintainer has appetite for it earlier, feel free to land the design from above — the structure is yours to use.

  4. iroiro147 commented on Aug 2, 2026

    @iroiro147
    Contributor

    Scope reduction proposal for #4227.

    After two splice attempts, the full "--reply-anchor flag with threading-aware prompt axillary" runs into multi-file AST-aware work that's beyond my line-anchored editing pattern. Failing surfaces:

    1. Config enum + CLI flag + summary + ctor — tractable (one file).
    2. queue.rs FormatPromptArgs + format_context_hints signature + branch-based instruction text — tractable (~30 lines).
    3. pool.rs PromptContext struct field + run_prompt_task FormatPromptArgs literal + 1-3 test ctor fixes — multiple struct-literal close detections in ~6400-line file.
    4. lib.rs PromptContext construction — single line but misidentification rates are high.

    Option A: full implementation paired against a larger context budget and IDE-style tooling.

    Option B (proposed): split into two PRs:

    • PR-A adds the ReplyAnchor enum + --reply-anchor CLI flag + summary display ONLY. Flag is parsed but inert — no prompt changes. Reviewable in <30 LOC.
    • PR-B wires the flag through PromptContext → FormatPromptArgs → format_context_hints, which is where the prompt divergence actually lives. PR-B explains the tradeoff (thread replies vs channel-root visibility for mobile/dense channels) on its own merit.

    B preserves the issue scope, removes the monolithic review risk, and matches the codebase's incremental-verification pattern (every PR carries its own cargo check/test gate).

    Awaiting maintainer ruling. If B is acceptable I can land PR-A today; PR-B scope/timing per maintainer preference.

  5. iroiro147 commented on Aug 2, 2026

    @iroiro147
    Contributor

    PR-A is up: #4285

    • ReplyAnchor enum + --reply-anchor CLI flag (Thread=default, TopLevel)
    • Inert: parsed and stored in Config, does NOT yet affect prompt text
    • 33 LOC across config.rs + lib.rs, 661/661 tests pass, clippy clean

    If the split approach is acceptable, I'll follow up with PR-B (the actual prompt behavior change wiring through PromptContext → FormatPromptArgs → format_context_hints).

  6. iroiro147 commented on Aug 2, 2026

    @iroiro147
    Contributor

    PR-B is up: #4286

    Both PRs are ready for review. Split rationale: PR-A is reviewable in 2 minutes (inert config surface); PR-B carries the actual behavior change in isolation for focused review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions