Blame
|
1 | --- |
||||||
| 2 | category: feature |
|||||||
| 3 | tags: [agents, irc, supervisor] |
|||||||
| 4 | last_updated: 2026-03-20 |
|||||||
| 5 | confidence: high |
|||||||
| 6 | --- |
|||||||
| 7 | ||||||||
| 8 | # Directed Message Routing |
|||||||
| 9 | ||||||||
| 10 | Event-driven delivery of IRC messages to specific agents, replacing poll-only communication. |
|||||||
| 11 | ||||||||
| 12 | ## Problem |
|||||||
| 13 | ||||||||
| 14 | Agents only see IRC messages when the supervisor's 30-second heartbeat prompts them to `read_messages`. If the PM or EM posts a message mentioning an agent by nick, it sits unread until the next poll. This creates up to 30 seconds of latency on directed communication and makes the system feel unresponsive. |
|||||||
| 15 | ||||||||
| 16 | The `IrcWatcher` already has an event-driven IRC connection that fires instantly on `PRIVMSG`. It currently only handles `TASK:` prefixes. It should also route messages that mention a specific agent. |
|||||||
| 17 | ||||||||
| 18 | ## Design |
|||||||
| 19 | ||||||||
| 20 | Extend `IrcWatcher` to detect nick mentions and notify the supervisor, which immediately sends the message content to the targeted agent. |
|||||||
| 21 | ||||||||
| 22 | ### Detection |
|||||||
| 23 | ||||||||
| 24 | A message is "directed" if it contains `@AgentName` where `AgentName` is a currently active agent nick. The watcher already has access to a callback — add a second callback for directed messages alongside the existing `on_task`. |
|||||||
| 25 | ||||||||
| 26 | ```python |
|||||||
| 27 | class IrcWatcher: |
|||||||
| 28 | def __init__( |
|||||||
| 29 | self, |
|||||||
| 30 | server: str, |
|||||||
| 31 | port: int, |
|||||||
| 32 | nick: str, |
|||||||
| 33 | channels: list[str], |
|||||||
| 34 | on_task: Callable[[str, str, str], None], |
|||||||
| 35 | on_mention: Callable[[str, str, str, str], None], |
|||||||
| 36 | # on_mention(channel, sender, mentioned_nick, full_message) |
|||||||
| 37 | ) -> None: |
|||||||
| 38 | ``` |
|||||||
| 39 | ||||||||
| 40 | ### Routing |
|||||||
| 41 | ||||||||
| 42 | In `_handle_message`, after the existing `TASK:` check: |
|||||||
| 43 | ||||||||
| 44 | 1. Scan the message for `@{nick}` where nick is in the set of active agent names |
|||||||
| 45 | 2. If found, call `on_mention(channel, sender_nick, mentioned_nick, message)` |
|||||||
| 46 | 3. A message can match both `TASK:` and `@mention` — that's fine, they serve different purposes |
|||||||
| 47 | ||||||||
| 48 | The supervisor registers `on_mention` and calls `agent.send()` with the message content, bypassing the poll loop: |
|||||||
| 49 | ||||||||
| 50 | ```python |
|||||||
| 51 | def _on_mention(self, channel: str, sender: str, target_nick: str, message: str) -> None: |
|||||||
| 52 | agent = self.agents.get(target_nick) |
|||||||
| 53 | if agent: |
|||||||
| 54 | asyncio.get_running_loop().create_task( |
|||||||
| 55 | agent.session.send( |
|||||||
| 56 | f"[IRC {channel}] <{sender}> {message}\n\n" |
|||||||
| 57 | "Respond in the appropriate channel." |
|||||||
| 58 | ) |
|||||||
| 59 | ) |
|||||||
| 60 | ``` |
|||||||
| 61 | ||||||||
| 62 | ### Active nick registry |
|||||||
| 63 | ||||||||
| 64 | The `IrcWatcher` needs to know which nicks are active to avoid matching mentions of non-agent names. The supervisor already tracks this in `self.agents` (keyed by name). Add a method to update the watcher's nick set: |
|||||||
| 65 | ||||||||
| 66 | ```python |
|||||||
| 67 | # On IrcWatcher |
|||||||
| 68 | def update_active_nicks(self, nicks: set[str]) -> None: |
|||||||
| 69 | self._active_nicks = nicks |
|||||||
| 70 | ||||||||
| 71 | # Supervisor calls this after spawn/kill/shift-change |
|||||||
| 72 | self._watcher.update_active_nicks(set(self.agents.keys())) |
|||||||
| 73 | ``` |
|||||||
| 74 | ||||||||
| 75 | ### Deduplication |
|||||||
| 76 | ||||||||
| 77 | Without guard, an agent could receive the same message twice: once via directed routing, once via the next heartbeat's `read_messages`. Two options: |
|||||||
| 78 | ||||||||
| 79 | 1. **Track delivered message timestamps per agent.** The supervisor records the timestamp of the last directed delivery. The heartbeat prompt changes to "Check your IRC channels for messages since {timestamp}." |
|||||||
| 80 | 2. **Accept duplicates.** Agents are LLMs — seeing a message twice is redundant but not harmful. The simpler option for MVP. |
|||||||
| 81 | ||||||||
| 82 | **Recommendation:** Accept duplicates for MVP. Add dedup later if it becomes noisy. |
|||||||
| 83 | ||||||||
| 84 | ## Channels to watch |
|||||||
| 85 | ||||||||
| 86 | The watcher currently joins only `#standup-{slug}`. For mention routing to work across channels, it should also join `#project-{slug}` and any `#work-{task_id}` channels that get created. The supervisor should call `watcher.join_channel()` when creating work channels for new tasks. |
|||||||
| 87 | ||||||||
| 88 | ## Scope |
|||||||
| 89 | ||||||||
| 90 | ### In scope |
|||||||
| 91 | - Extend `IrcWatcher._handle_message` with nick-mention detection |
|||||||
| 92 | - Add `on_mention` callback to `IrcWatcher.__init__` |
|||||||
| 93 | - Add `update_active_nicks()` method |
|||||||
| 94 | - Wire `Supervisor._on_mention` to call `agent.send()` |
|||||||
| 95 | - Update active nicks on spawn/kill/shift-change |
|||||||
| 96 | - Join `#project-{slug}` in addition to `#standup-{slug}` on startup |
|||||||
| 97 | - Tests for mention detection regex |
|||||||
| 98 | ||||||||
| 99 | ### Out of scope |
|||||||
| 100 | - DM (private message) routing — agents only operate in channels for now |
|||||||
| 101 | - Message deduplication — accept duplicates for MVP |
|||||||
| 102 | - Priority/interruption — directed messages queue behind any in-progress `send()` |
|||||||
| 103 | - Rate limiting — a burst of mentions could queue many `send()` calls |
|||||||
| 104 | ||||||||
| 105 | ## Files to change |
|||||||
| 106 | ||||||||
| 107 | - `supervisor/src/minsky_supervisor/irc_watcher.py` — add mention detection, `on_mention` callback, `update_active_nicks()` |
|||||||
| 108 | - `supervisor/src/minsky_supervisor/supervisor.py` — wire `_on_mention`, call `update_active_nicks()` after agent lifecycle changes, join `#project-{slug}` |
|||||||
| 109 | - `supervisor/tests/test_irc_watcher.py` — add mention detection tests |
|||||||
