Skip to content
Merged
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
26 changes: 26 additions & 0 deletions docs/discord.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ Empty (default) = any bot can pass through (subject to the mode check).

Role mentions are ignored because they are shared across bots and cause false positives in multi-bot setups. This is intentional since v0.7.8-beta.3 (#420, #440).

### User mention UIDs

When a user mentions another user (e.g. `@SomeUser`) in a message to the bot, the raw Discord mention `<@UID>` is preserved in the prompt sent to the LLM. This means:

- The LLM can copy `<@UID>` into its reply to produce a clickable Discord mention
- The bot's own mention is stripped (so the bot doesn't see itself being triggered)
- Role mentions are replaced with `@(role)` placeholder

To help the LLM know who each UID refers to, provide a UID→name mapping via system prompt or context entry (see [Multi-Bot Setup](#multi-bot-setup) below).

---

## Thread Behavior
Expand Down Expand Up @@ -172,6 +182,22 @@ To enable bots to collaborate (e.g. code review → deploy handoff):
allow_bot_messages = "mentions"
```

### Ice-breaking: teaching bots who's in the room

Since user mentions are preserved as raw `<@UID>`, bots need a UID→name mapping to know who is who. Add an ice-breaking greeting to each bot's system prompt or context entry:

```
We have 3 participants in this room:

MY_NICIKNAME <@MY_NAME>
BOT1_NICKNAME <@BOT1>
BOT2_NICKNAME <@BOT2>

Always use <@UID> format to mention someone in your messages.
```

This lets each bot build the mapping in its own context from the start and correctly mention others using `<@UID>`.

See [multi-agent.md](multi-agent.md) for detailed examples.

---
Expand Down
31 changes: 6 additions & 25 deletions src/discord.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use serenity::http::Http;
use serenity::model::channel::{AutoArchiveDuration, Message, ReactionType};
use serenity::model::gateway::Ready;
use serenity::model::id::{ChannelId, MessageId, UserId};
use serenity::model::user::User;
use serenity::prelude::*;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, OnceLock};
Expand Down Expand Up @@ -321,11 +320,7 @@ impl EventHandler for Handler {
return;
}

let prompt = if is_mentioned {
resolve_mentions(&msg.content, bot_id, &msg.mentions)
} else {
msg.content.trim().to_string()
};
let prompt = resolve_mentions(&msg.content, bot_id);

// No text and no attachments → skip
if prompt.is_empty() && msg.attachments.is_empty() {
Expand Down Expand Up @@ -474,28 +469,14 @@ async fn get_or_create_thread(
static ROLE_MENTION_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"<@&\d+>").unwrap()
});
static USER_MENTION_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r"<@!?\d+>").unwrap()
});

fn resolve_mentions(content: &str, bot_id: UserId, mentions: &[User]) -> String {
fn resolve_mentions(content: &str, bot_id: UserId) -> String {
// 1. Strip the bot's own trigger mention
let mut out = content
let out = content
.replace(&format!("<@{}>", bot_id), "")
.replace(&format!("<@!{}>", bot_id), "");
// 2. Resolve known user mentions to @DisplayName
for user in mentions {
if user.id == bot_id {
continue;
}
let label = user.global_name.as_deref().unwrap_or(&user.name);
let display = format!("@{}", label);
out = out
.replace(&format!("<@{}>", user.id), &display)
.replace(&format!("<@!{}>", user.id), &display);
}
// 3. Fallback: replace any remaining unresolved mentions
let out = ROLE_MENTION_RE.replace_all(&out, "@(role)");
let out = USER_MENTION_RE.replace_all(&out, "@(user)").to_string();
// 2. Other user mentions: keep <@UID> as-is so the LLM can mention back
// 3. Fallback: replace role mentions only (user mentions are preserved)
let out = ROLE_MENTION_RE.replace_all(&out, "@(role)").to_string();
out.trim().to_string()
}
Loading