ScuttleBot

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

Keyboard Shortcuts

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