Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/buzz-acp/src/base_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ For explicit changes to an existing personal agent, use `buzz agents draft-updat

Use the reply destination supplied in the `[Context]` block for ordinary replies in this turn. Do not reuse a remembered thread id, an older event id from prior work, or a stale conversation root.

In DMs, send ordinary responses as top-level messages so the conversation stays directly visible. Do not create reply threads inside a DM. Use `buzz messages send --channel <UUID> --content ...` without a reply anchor.

For human-facing work, keep the conversation flat and easy to read. The app/harness will choose the correct reply destination: the root of the triggering thread when the turn is already threaded, or the triggering top-level event when the human started a new thread.

For agent-to-agent coordination with no human in the loop, deeper nesting is allowed when it helps preserve task structure. Do not flatten agent-only subthreads just because they are inside a thread.
Expand Down
9 changes: 9 additions & 0 deletions crates/buzz-acp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4007,6 +4007,15 @@ mod agent_draft_prompt_tests {
.contains("add them explicitly with `buzz channels add-member` only when authorized"));
assert!(prompt.contains("never changes membership automatically"));
}

#[test]
fn shared_base_prompt_keeps_dm_conversations_top_level() {
let prompt = include_str!("base_prompt.md");
assert!(prompt.contains(
"In DMs, send ordinary responses as top-level messages so the conversation stays directly visible"
));
assert!(prompt.contains("Do not create reply threads inside a DM"));
}
}

fn default_heartbeat_prompt() -> String {
Expand Down
41 changes: 26 additions & 15 deletions crates/buzz-acp/src/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,19 @@ fn append_new_thread_reply_instruction(s: &mut String, event_id: &str) {
));
}

/// Append the DM delivery rule.
///
/// DMs are already private conversations. Keeping agent responses top-level
/// makes them visible in the main DM timeline instead of hiding them in reply
/// panes.
fn append_dm_response_instruction(s: &mut String) {
s.push_str(
"\nIMPORTANT: Send ordinary DM responses as top-level messages. Use \
`buzz messages send --channel <UUID> --content ...` without a reply \
anchor so the response stays directly visible in the DM conversation.",
);
}

/// Decide whether a turn is human-facing for reply-anchor purposes.
///
/// A turn is human-facing when the triggering sender is a human, OR a human
Expand Down Expand Up @@ -1278,10 +1291,10 @@ fn append_channel_description(s: &mut String, channel_info: Option<&PromptChanne
/// Format a `[Context]` hints section based on event scope.
///
/// `reply_anchor` is the pre-resolved `--reply-to` target for this turn (see
/// [`resolve_reply_anchor`]). In the thread/DM branches it threads ordinary
/// [`resolve_reply_anchor`]). In the thread branch it threads ordinary
/// replies; in the channel branch a `Some` anchor means a human-facing
/// top-level mention whose reply should open a new thread rooted at the
/// triggering event.
/// triggering event. DMs intentionally ignore reply anchors and stay top-level.
fn format_context_hints(
channel_id: Uuid,
channel_info: Option<&PromptChannelInfo>,
Expand Down Expand Up @@ -1329,10 +1342,8 @@ fn format_context_hints(
s.push_str(&format!("\nParent: {parent}"));
}
}
if let Some(event_id) = reply_anchor {
append_reply_instruction(&mut s, event_id);
}
}
append_dm_response_instruction(&mut s);
s
} else if let Some(ref root) = thread_tags.root_event_id {
let ctx_hint = if has_conversation_context {
Expand Down Expand Up @@ -1562,17 +1573,14 @@ pub fn format_prompt(batch: &FlushBatch, args: &FormatPromptArgs<'_>) -> Vec<Str

// 2. Context hints (with a human-aware reply anchor).
//
// Human-facing turns are anchored so replies stay readable at layer 1:
// Human-facing channel turns are anchored so replies stay readable at layer 1:
// - in a thread → anchor to the thread ROOT (no depth-2 nesting)
// - top-level → anchor to the triggering event (it becomes the root)
// Agent↔agent turns get no forced anchor — deep nesting is intentional
// there. DMs are always 1:1 with a human, so they always anchor.
// there. DMs stay top-level so replies remain visible in the main timeline.
let sender_pubkey = last_event.event.pubkey.to_hex();
let reply_anchor = if is_dm {
thread_tags
.root_event_id
.is_some()
.then(|| last_event.event.id.to_hex())
None
Comment on lines 1582 to +1583

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve agent-only DM subthreads

When a threaded DM event comes from a profile marked is_agent and mentions no human, this unconditional DM branch bypasses turn_is_human_facing, sets no reply destination, and adds an instruction requiring a top-level send. Consequently agent-to-agent task subthreads in DMs are flattened too, contradicting the retained agent-only threading rule in base_prompt.md; apply the flat-DM policy only to human-facing turns and keep a threaded destination for agent-only ones.

Useful? React with 👍 / 👎.

} else {
resolve_reply_anchor(
&sender_pubkey,
Expand Down Expand Up @@ -4163,14 +4171,13 @@ mod tests {
}

#[test]
fn test_reply_instruction_present_for_dm_thread_reply() {
fn test_dm_thread_reply_instructs_top_level_send_without_reply_to() {
let ch = Uuid::new_v4();
let root_id = "b".repeat(64);
let event = make_event_with_tags(
"thanks",
vec![vec!["e".into(), root_id, "".into(), "reply".into()]],
);
let event_id = event.id.to_hex();
let batch = FlushBatch {
channel_id: ch,
events: vec![BatchEvent {
Expand All @@ -4196,8 +4203,12 @@ mod tests {
)
.join("\n\n");
assert!(
prompt.contains(&format!("--reply-to {event_id}")),
"DM thread reply should include reply instruction"
!prompt.contains("--reply-to"),
"DM replies must remain top-level in the conversation; prompt was:\n{prompt}"
);
assert!(
prompt.contains("Send ordinary DM responses as top-level messages"),
"DM prompt must state the flat-conversation rule; prompt was:\n{prompt}"
);
}

Expand Down
Loading