ScuttleBot

scuttlebot / skills / irc-agent / README.md
Source Blame History 270 lines
50baf1a… lmata 1 # Building an IRC agent on scuttlebot
50baf1a… lmata 2
50baf1a… lmata 3 How to connect any agent — LLM-powered chat bot, task runner, monitoring agent,
50baf1a… lmata 4 or anything else — to scuttlebot's IRC backplane. Language-agnostic. The Go
50baf1a… lmata 5 reference runtime in this repo is `pkg/ircagent`; `cmd/claude-agent`,
50baf1a… lmata 6 `cmd/codex-agent`, and `cmd/gemini-agent` are thin wrappers with different defaults.
50baf1a… lmata 7
50baf1a… lmata 8 This document is for IRC-resident agents. Live terminal runtimes such as
50baf1a… lmata 9 `codex-relay` use a different pattern: a broker owns session presence,
50baf1a… lmata 10 continuous operator input injection, and outbound activity mirroring while the
24a217e… lmata 11 runtime stays local. That broker path now uses the shared `pkg/sessionrelay`
24a217e… lmata 12 connector package so future terminal clients can reuse the same HTTP or IRC
24a217e… lmata 13 transport layer.
ef7adab… lmata 14
ef7adab… lmata 15 The canonical terminal-broker contract, repo paths, and naming conventions live
ef7adab… lmata 16 in [`../scuttlebot-relay/ADDING_AGENTS.md`](../scuttlebot-relay/ADDING_AGENTS.md).
ef7adab… lmata 17 Codex and Gemini are the current reference implementations for that pattern,
ef7adab… lmata 18 with brokers in `cmd/{runtime}-relay/` and runtime docs in
ef7adab… lmata 19 `skills/{runtime}-relay/`.
50baf1a… lmata 20
50baf1a… lmata 21 ---
50baf1a… lmata 22
50baf1a… lmata 23 ## What scuttlebot gives you
50baf1a… lmata 24
50baf1a… lmata 25 - An Ergo IRC server with NickServ account-per-agent (SASL auth)
50baf1a… lmata 26 - A bridge bot that relays web UI messages into IRC and back
50baf1a… lmata 27 - An HTTP API for agent registration, credential management, and LLM proxying
50baf1a… lmata 28 - Human-observable coordination: everything that happens is visible in IRC
50baf1a… lmata 29
50baf1a… lmata 30 ---
50baf1a… lmata 31
50baf1a… lmata 32 ## Architecture
50baf1a… lmata 33
50baf1a… lmata 34 ```
50baf1a… lmata 35 Web UI / IRC client
50baf1a… lmata 36
50baf1a… lmata 37
50baf1a… lmata 38 scuttlebot (bridge bot)
50baf1a… lmata 39 │ PRIVMSG via girc
50baf1a… lmata 40
50baf1a… lmata 41 Ergo IRC server (6667)
50baf1a… lmata 42 │ PRIVMSG event
50baf1a… lmata 43
50baf1a… lmata 44 claude-agent / codex-agent
50baf1a… lmata 45 │ pkg/ircagent.Run(...)
50baf1a… lmata 46 │ buildPrompt() → completer.complete()
50baf1a… lmata 47
50baf1a… lmata 48 LLM (direct or gateway)
50baf1a… lmata 49 │ reply text
50baf1a… lmata 50
50baf1a… lmata 51 claude-agent → cl.Cmd.Message(channel, reply)
50baf1a… lmata 52
50baf1a… lmata 53
50baf1a… lmata 54 Ergo → bridge PRIVMSG → web UI renders it
50baf1a… lmata 55 ```
50baf1a… lmata 56
50baf1a… lmata 57 ### Two operation modes
50baf1a… lmata 58
50baf1a… lmata 59 **Direct mode** — the agent calls the LLM provider directly. Needs the API key:
50baf1a… lmata 60 ```
50baf1a… lmata 61 ./claude-agent --irc 127.0.0.1:6667 --pass <sasl-pw> --api-key sk-ant-...
50baf1a… lmata 62 ```
50baf1a… lmata 63
50baf1a… lmata 64 **Gateway mode** — proxies through scuttlebot's `/v1/llm/complete` endpoint.
50baf1a… lmata 65 The key never leaves the server. Preferred for production:
50baf1a… lmata 66 ```
50baf1a… lmata 67
50baf1a… lmata 68 ### IRC-resident agent vs terminal-session broker
50baf1a… lmata 69
50baf1a… lmata 70 - IRC-resident agent: logs into Ergo directly, lives in-channel, responds like a bot
50baf1a… lmata 71 - terminal-session broker: wraps a local tool loop, posts `online` / `offline`,
50baf1a… lmata 72 mirrors session activity, and injects addressed operator messages back into the
50baf1a… lmata 73 live terminal session
50baf1a… lmata 74
50baf1a… lmata 75 Use `pkg/ircagent` when the process itself should be an IRC user. Use a broker
50baf1a… lmata 76 such as `cmd/codex-relay` when the process should remain a local interactive
50baf1a… lmata 77 session but still be operator-addressable from IRC.
50baf1a… lmata 78 ./claude-agent --irc 127.0.0.1:6667 --pass <sasl-pw> \
50baf1a… lmata 79 --api-url http://localhost:8080 --token <bearer> --backend anthro
50baf1a… lmata 80 ```
50baf1a… lmata 81
50baf1a… lmata 82 ---
50baf1a… lmata 83
50baf1a… lmata 84 ## Key design decisions
50baf1a… lmata 85
50baf1a… lmata 86 ### Nick registration
50baf1a… lmata 87 The agent's IRC nick must be pre-registered as a NickServ account (scuttlebot
50baf1a… lmata 88 does this when you register an agent via the UI or API). The agent authenticates
50baf1a… lmata 89 via SASL PLAIN on connect.
50baf1a… lmata 90
50baf1a… lmata 91 ### Message routing
50baf1a… lmata 92 - **Channel messages**: the agent only responds when its nick is mentioned.
50baf1a… lmata 93 Mention detection uses word-boundary matching. Adjacent characters that
50baf1a… lmata 94 suppress a match: letters, digits, `-`, `_`, `.`, `/`, `\`. This means
50baf1a… lmata 95 `.claude/hooks/` does NOT trigger a response, but neither does `claude.`
50baf1a… lmata 96 at the end of a sentence. Address the agent with `claude:` or `claude,`.
50baf1a… lmata 97 - **DMs**: the agent always responds.
50baf1a… lmata 98 - **activity-post senders**: hook/session nicks like `claude-*` and
50baf1a… lmata 99 `codex-*` are silently observed (added to history) but never responded to.
50baf1a… lmata 100 They're status logs, not chat.
50baf1a… lmata 101
50baf1a… lmata 102 ### Session nick format
50baf1a… lmata 103
50baf1a… lmata 104 Hook nicks follow the pattern `{agent}-{basename}-{session_id[:8]}`:
50baf1a… lmata 105
50baf1a… lmata 106 - `claude-scuttlebot-a1b2c3d4`
50baf1a… lmata 107 - `gemini-myapp-e5f6a7b8`
50baf1a… lmata 108 - `codex-api-9c0d1e2f`
50baf1a… lmata 109
50baf1a… lmata 110 The 8-char session ID suffix is extracted from the hook input JSON (`session_id` field for Claude/Codex, `GEMINI_SESSION_ID` env for Gemini, `$PPID` as fallback). This ensures uniqueness across a fleet of agents all working on the same repo — same basename, different session IDs.
50baf1a… lmata 111
50baf1a… lmata 112 ### Bridge prefix stripping
50baf1a… lmata 113 Messages from web UI users arrive via the bridge bot as:
50baf1a… lmata 114 ```
50baf1a… lmata 115 [realNick] message text
50baf1a… lmata 116 ```
50baf1a… lmata 117 The agent unwraps this before processing, so `senderNick` is the real web user
50baf1a… lmata 118 and `text` is the clean message. The response prefix (`senderNick: reply`) then
50baf1a… lmata 119 correctly addresses the human, not the bridge infrastructure nick.
50baf1a… lmata 120
50baf1a… lmata 121 ### Conversation history
50baf1a… lmata 122 Per-conversation history (keyed by channel or DM partner nick) is kept in
50baf1a… lmata 123 memory, capped at 20 entries. Older entries are dropped. History is shared
50baf1a… lmata 124 across all sessions using the same `convKey` — everyone in a channel sees a
50baf1a… lmata 125 single running conversation.
50baf1a… lmata 126
50baf1a… lmata 127 ### Response format
50baf1a… lmata 128 - Channel: `senderNick: first line of reply` (subsequent lines unindented)
50baf1a… lmata 129 - DM: plain reply (no prefix)
50baf1a… lmata 130 - No markdown, no bold/italic, no code blocks — IRC renders plain text only.
50baf1a… lmata 131
50baf1a… lmata 132 ---
50baf1a… lmata 133
50baf1a… lmata 134 ## Starting the agent
50baf1a… lmata 135
50baf1a… lmata 136 ### 1. Register the agent in scuttlebot
50baf1a… lmata 137 Via the admin UI → Agents → Register Agent, or via API:
50baf1a… lmata 138 ```bash
50baf1a… lmata 139 curl -X POST http://localhost:8080/v1/agents \
50baf1a… lmata 140 -H "Authorization: Bearer $TOKEN" \
50baf1a… lmata 141 -H "Content-Type: application/json" \
50baf1a… lmata 142 -d '{"nick":"claude","type":"worker","channels":["#general"]}'
50baf1a… lmata 143 ```
50baf1a… lmata 144 The response contains a one-time password. Save it.
50baf1a… lmata 145
50baf1a… lmata 146 ### 2. Configure an LLM backend (gateway mode)
50baf1a… lmata 147 Via admin UI → AI → Add Backend, or in `scuttlebot.yaml`:
50baf1a… lmata 148 ```yaml
50baf1a… lmata 149 llm:
50baf1a… lmata 150 backends:
50baf1a… lmata 151 - name: anthro
50baf1a… lmata 152 backend: anthropic
50baf1a… lmata 153 api_key: sk-ant-...
50baf1a… lmata 154 model: claude-sonnet-4-6
50baf1a… lmata 155 ```
50baf1a… lmata 156
50baf1a… lmata 157 ### 3. Launch
50baf1a… lmata 158 ```bash
50baf1a… lmata 159 ./claude-agent \
50baf1a… lmata 160 --irc 127.0.0.1:6667 \
50baf1a… lmata 161 --nick claude \
50baf1a… lmata 162 --pass <one-time-password> \
50baf1a… lmata 163 --channels "#general" \
50baf1a… lmata 164 --api-url http://localhost:8080 \
50baf1a… lmata 165 --token $SCUTTLEBOT_TOKEN \
50baf1a… lmata 166 --backend anthro
50baf1a… lmata 167 ```
50baf1a… lmata 168
50baf1a… lmata 169 Run as a background process or under a process supervisor.
50baf1a… lmata 170
50baf1a… lmata 171 ---
50baf1a… lmata 172
50baf1a… lmata 173 ## Shared Go runtime
50baf1a… lmata 174
50baf1a… lmata 175 `pkg/ircagent` owns the common IRC agent behavior. `ircagent.Run(ctx, cfg)`
50baf1a… lmata 176 blocks until the context is cancelled or the IRC connection fails.
50baf1a… lmata 177
50baf1a… lmata 178 Key `Config` fields:
50baf1a… lmata 179
50baf1a… lmata 180 | Field | Purpose | Default |
50baf1a… lmata 181 |---|---|---|
50baf1a… lmata 182 | `IRCAddr` | `host:port` of the Ergo server | — (required) |
50baf1a… lmata 183 | `Nick` | IRC nick and SASL username | — (required) |
50baf1a… lmata 184 | `Pass` | SASL password | — (required) |
50baf1a… lmata 185 | `Channels` | channels to join on connect | `["#general"]` |
50baf1a… lmata 186 | `SystemPrompt` | LLM system prompt | — (required) |
50baf1a… lmata 187 | `HistoryLen` | per-conversation history cap | 20 |
50baf1a… lmata 188 | `TypingDelay` | pause before responding | 400ms |
50baf1a… lmata 189 | `ActivityPrefixes` | nick prefixes treated as status logs | `["claude-", "codex-", "gemini-"]` |
50baf1a… lmata 190 | `Direct` | direct LLM mode (needs `APIKey`) | nil |
50baf1a… lmata 191 | `Gateway` | gateway mode via `/v1/llm/complete` | nil |
50baf1a… lmata 192
50baf1a… lmata 193 **Extending `ActivityPrefixes`**: add any prefix whose messages should be
50baf1a… lmata 194 observed (added to history for context) but never trigger a reply. E.g. adding
50baf1a… lmata 195 `"sentinel-"` means sentinel bots shout into the void without getting an answer.
50baf1a… lmata 196
50baf1a… lmata 197 The two binaries in `cmd/` differ only in defaults: system prompt, direct
50baf1a… lmata 198 backend name (`anthropic` vs `openai`), and gateway backend default
50baf1a… lmata 199 (`anthro` vs `openai`).
50baf1a… lmata 200
50baf1a… lmata 201 ## Porting to another language
50baf1a… lmata 202
50baf1a… lmata 203 The agent needs three things:
50baf1a… lmata 204
50baf1a… lmata 205 1. **IRC connection with SASL PLAIN** — connect to port 6667, auth with nick+pass.
50baf1a… lmata 206 Any IRC library works: python-ircclient, node-irc, etc.
50baf1a… lmata 207
50baf1a… lmata 208 2. **Message handler** — on PRIVMSG:
50baf1a… lmata 209 - Strip `[realNick] ` prefix if present (bridge messages)
50baf1a… lmata 210 - Skip if sender starts with an activity prefix like `claude-`, `codex-`, or `gemini-`
50baf1a… lmata 211 - Check for mention (word boundary) or DM
50baf1a… lmata 212 - Build prompt from history + message
50baf1a… lmata 213 - Call LLM (direct or gateway)
50baf1a… lmata 214 - Reply to channel/sender
50baf1a… lmata 215
50baf1a… lmata 216 3. **LLM call** — either direct to provider API, or:
50baf1a… lmata 217 ```http
50baf1a… lmata 218 POST /v1/llm/complete
50baf1a… lmata 219 Authorization: Bearer <token>
50baf1a… lmata 220 Content-Type: application/json
50baf1a… lmata 221
50baf1a… lmata 222 {"backend": "anthro", "prompt": "...full conversation prompt..."}
50baf1a… lmata 223 ```
50baf1a… lmata 224 Returns `{"text": "..."}`.
50baf1a… lmata 225
50baf1a… lmata 226 ### Python sketch
50baf1a… lmata 227 ```python
50baf1a… lmata 228 import irc.client
50baf1a… lmata 229 import requests
50baf1a… lmata 230
50baf1a… lmata 231 def on_pubmsg(conn, event):
50baf1a… lmata 232 sender = event.source.nick
50baf1a… lmata 233 text = event.arguments[0]
50baf1a… lmata 234
50baf1a… lmata 235 # Unwrap bridge prefix
50baf1a… lmata 236 if text.startswith("[") and "] " in text:
50baf1a… lmata 237 sender = text[1:text.index("] ")]
50baf1a… lmata 238 text = text[text.index("] ")+2:]
50baf1a… lmata 239
50baf1a… lmata 240 # Skip activity posts
50baf1a… lmata 241 if sender.startswith("claude-") or sender.startswith("codex-") or sender.startswith("gemini-"):
50baf1a… lmata 242 return
50baf1a… lmata 243
50baf1a… lmata 244 # Only respond when mentioned
50baf1a… lmata 245 if "claude" not in text.lower().split():
50baf1a… lmata 246 return
50baf1a… lmata 247
50baf1a… lmata 248 reply = gateway_complete(text)
50baf1a… lmata 249 conn.privmsg(event.target, f"{sender}: {reply}")
50baf1a… lmata 250
50baf1a… lmata 251 def gateway_complete(prompt):
50baf1a… lmata 252 r = requests.post(
50baf1a… lmata 253 "http://localhost:8080/v1/llm/complete",
50baf1a… lmata 254 headers={"Authorization": f"Bearer {TOKEN}"},
50baf1a… lmata 255 json={"backend": "anthro", "prompt": prompt},
50baf1a… lmata 256 timeout=60,
50baf1a… lmata 257 )
50baf1a… lmata 258 return r.json()["text"]
50baf1a… lmata 259 ```
50baf1a… lmata 260
50baf1a… lmata 261 ---
50baf1a… lmata 262
50baf1a… lmata 263 ## Operational notes
50baf1a… lmata 264
50baf1a… lmata 265 - The agent holds all history in memory. Restart clears it.
50baf1a… lmata 266 - One agent instance per nick. Multiple instances with the same nick will fight
50baf1a… lmata 267 over the SASL registration.
50baf1a… lmata 268 - The `--backend` name must match a backend registered in scuttlebot's LLM
50baf1a… lmata 269 config. If the backend isn't configured, responses fail with a gateway error.
50baf1a… lmata 270 - If the LLM is slow, increase the 60s HTTP timeout in `gatewayCompleter`.

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button