Skip to content
Open
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
75 changes: 75 additions & 0 deletions crates/buzz-acp/src/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,8 @@ pub(crate) fn format_event_block(
block.push_str(&format!("\nTags: {tags_str}"));
}

append_media_instructions(&mut block, &be.event);

// Parsed structural fields.
let thread = parse_thread_tags(&be.event);
let mut parsed_parts = Vec::new();
Expand Down Expand Up @@ -1141,6 +1143,45 @@ pub(crate) fn format_event_block(
block
}

/// Surface NIP-92 attachments as actionable agent context.
///
/// Relay media is intentionally private, so a bare HTTP client receives 401.
/// The event's raw `imeta` tag is not enough guidance for an agent to discover
/// that distinction. Point agents at the authenticated CLI path and require
/// inspection before they claim to have seen the attachment.
fn append_media_instructions(block: &mut String, event: &Event) {
let attachments: Vec<(&str, Option<&str>)> = event
.tags
.iter()
.filter_map(|tag| {
let fields = tag.as_slice();
if fields.first().map(String::as_str) != Some("imeta") {
return None;
}
let url = fields.iter().find_map(|field| field.strip_prefix("url "))?;
let mime = fields.iter().find_map(|field| field.strip_prefix("m "));
Some((url, mime))
})
.collect();

if attachments.is_empty() {
return;
}

block.push_str("\nAttachments:");
for (index, (url, mime)) in attachments.iter().enumerate() {
block.push_str(&format!(
"\n- [{}] {} — {}",
index + 1,
mime.unwrap_or("unknown type"),
url,
));
}
block.push_str(
"\nIMPORTANT: Attachment pixels are not embedded in this text prompt. Before describing an image, download it with authenticated Buzz media access (`buzz media get <url> --output .scratch/buzz-attachment`) and inspect the local file. A plain `curl` may return 401. Never claim to see an attachment you have not inspected.",
);
}

/// Append a reply instruction when the agent is responding to a thread event.
///
/// Tells the agent to default to `--reply-to <event_id>` for ordinary replies
Expand Down Expand Up @@ -3522,6 +3563,40 @@ mod tests {
);
}

#[test]
fn test_format_event_block_surfaces_authenticated_media_instructions() {
let ch = Uuid::new_v4();
let url = "https://relay.test/media/screenshot.png";
let event = make_event_with_tags(
&format!("![image]({url})"),
vec![
vec!["h".into(), ch.to_string()],
vec![
"imeta".into(),
format!("url {url}"),
"m image/png".into(),
"x deadbeef".into(),
],
],
);
let batch = FlushBatch {
channel_id: ch,
events: vec![BatchEvent {
event,
prompt_tag: "test".into(),
received_at: Instant::now(),
}],
cancelled_events: vec![],
cancel_reason: None,
};

let prompt = format_prompt(&batch, &FormatPromptArgs::default()).join("\n\n");
assert!(prompt.contains("Attachments:\n- [1] image/png"));
assert!(prompt.contains(url));
assert!(prompt.contains("buzz media get <url>"));
assert!(prompt.contains("Never claim to see an attachment you have not inspected"));
}

#[test]
fn test_drain_channel_removes_pending_events() {
let mut q = EventQueue::new(DedupMode::Queue);
Expand Down