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
4 changes: 3 additions & 1 deletion crates/buzz-acp/src/base_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ 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.

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.
Direct messages are always flat. Send each ordinary DM response as a top-level message in the conversation. Do not pass `--reply-to` in a DM, even when the incoming DM carries thread tags from an earlier exchange.

For human-facing channel 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
7 changes: 7 additions & 0 deletions crates/buzz-acp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3626,6 +3626,13 @@ mod agent_draft_prompt_tests {
assert!(prompt.contains("buzz messages send ... --content -"));
}

#[test]
fn shared_base_prompt_teaches_flat_dm_replies() {
let prompt = include_str!("base_prompt.md");
assert!(prompt.contains("Direct messages are always flat"));
assert!(prompt.contains("Do not pass `--reply-to` in a DM"));
}

#[test]
fn shared_base_prompt_teaches_single_command_mentions_and_preflight() {
let prompt = include_str!("base_prompt.md");
Expand Down
52 changes: 33 additions & 19 deletions crates/buzz-acp/src/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,18 @@ fn append_reply_instruction(s: &mut String, event_id: &str) {
));
}

/// Append the reply instruction for direct messages.
///
/// DMs use a flat timeline instead of threads, even when the triggering event
/// carries legacy thread tags.
fn append_dm_reply_instruction(s: &mut String) {
s.push_str(
"\nIMPORTANT: DMs are a flat conversation. Send an ordinary message to \
this channel and keep it at the top level. Do not pass `--reply-to` to \
`buzz messages send`.",
);
}

/// Append a new-thread reply instruction for a human-facing top-level mention.
///
/// The triggering mention has no thread tags, so the agent's reply becomes the
Expand Down Expand Up @@ -1226,10 +1238,10 @@ fn resolve_reply_anchor(
/// 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
/// 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.
/// [`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. DMs
/// always remain flat and ignore reply anchors.
fn format_context_hints(
channel_id: Uuid,
channel_info: Option<&PromptChannelInfo>,
Expand Down Expand Up @@ -1272,10 +1284,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_reply_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 @@ -1463,13 +1473,10 @@ pub fn format_prompt(batch: &FlushBatch, args: &FormatPromptArgs<'_>) -> Vec<Str
// - 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 remain flat and never receive a reply anchor.
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
} else {
resolve_reply_anchor(
&sender_pubkey,
Expand Down Expand Up @@ -3904,14 +3911,13 @@ mod tests {
}

#[test]
fn test_reply_instruction_present_for_dm_thread_reply() {
fn test_dm_thread_reply_instruction_uses_flat_message() {
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 @@ -3936,8 +3942,12 @@ mod tests {
)
.join("\n\n");
assert!(
prompt.contains(&format!("--reply-to {event_id}")),
"DM thread reply should include reply instruction"
prompt.contains("DMs are a flat conversation"),
"DM reply should instruct the agent to keep the conversation flat"
);
assert!(
prompt.contains("Do not pass `--reply-to`"),
"DM reply should explicitly omit the thread flag"
);
}

Expand Down Expand Up @@ -3972,7 +3982,7 @@ mod tests {
}

#[test]
fn test_reply_instruction_absent_for_dm_non_reply() {
fn test_dm_top_level_reply_instruction_uses_flat_message() {
let ch = Uuid::new_v4();
let event = make_event("hey there");
let batch = FlushBatch {
Expand All @@ -3999,8 +4009,12 @@ mod tests {
)
.join("\n\n");
assert!(
!prompt.contains("--reply-to"),
"DM non-reply should NOT include reply instruction"
prompt.contains("DMs are a flat conversation"),
"top-level DM should instruct the agent to keep the conversation flat"
);
assert!(
prompt.contains("Do not pass `--reply-to`"),
"top-level DM should explicitly omit the thread flag"
);
}

Expand Down