9.6 KiB
9.6 KiB
irc
Send and receive messages between agents over a process-global mailbox bus.
Source
- Entry:
packages/coding-agent/src/tools/irc.ts - Model-facing prompt:
packages/coding-agent/src/prompts/tools/irc.md - Key collaborators:
packages/coding-agent/src/irc/bus.ts— process-globalIrcBus: per-agent mailboxes, delivery, waiter matching.packages/coding-agent/src/registry/agent-registry.ts— process-global agent directory and status.packages/coding-agent/src/registry/agent-lifecycle.ts— revival of parked recipients on direct send.packages/coding-agent/src/session/agent-session.ts—deliverIrcMessage(...): recipient-side injection and wake turns.packages/coding-agent/src/prompts/system/irc-incoming.md— incoming-message rendering for the recipient.packages/coding-agent/src/prompts/system/irc-autoreply.md— prompt for the ephemeral auto-reply side turn (busy recipient, async disabled).packages/coding-agent/src/config/settings-schema.ts—irc.timeoutMs.packages/coding-agent/src/modes/controllers/event-controller.ts— renders IRC events into chat UI.
Inputs
| Field | Type | Required | Description |
|---|---|---|---|
op |
"send" | "wait" | "inbox" | "list" |
Yes | Operation. |
to |
string |
send |
Recipient agent id, or "all" for broadcast. Whitespace trimmed; self-send rejected. |
message |
string |
send |
Message body. Empty-after-trim is rejected. |
replyTo |
string |
No | send: message id being answered. |
await |
boolean |
No | send: after delivery, block until the next message from that peer arrives (round-trip sugar). Invalid with to: "all". |
from |
string |
No | wait: only accept a message from this agent id. |
timeoutMs |
number |
No | wait / send await:true: timeout in milliseconds; 0 waits indefinitely. Defaults to irc.timeoutMs. |
peek |
boolean |
No | inbox: list messages without consuming them. |
Outputs
- Single-shot
AgentToolResult; no streaming updates. contentis one text block:list:No other agents.or<n> peer(s):bullets —id [displayName · kind · status]plus unread count, parent, and last-activity age; a footer notes that parked agents are revived automatically when messaged.send: per-recipient delivery receipts (injected/woken/revived/failed — <error>); withawait: true, the reply body or a clean no-reply timeout note.wait: the consumed message as[<msgId>] <from>: <body>(with a reply-to tag), orNo message within <duration>.inbox:Inbox empty.or<n> message(s):bullets.
details: IrcDetails:{ op, from?, to?, receipts?, waited?, inbox?, peers? }.waitedisnullwhen a wait timed out;receiptscarry{ to, outcome, error? }.
Flow
IrcTool.createIfconstructs the tool only whenisIrcEnabledpasses and the session has both anAgentRegistryandgetAgentId. There is noirc.enabledsetting: availability is derived — true for every subagent (taskDepth > 0; a parent always exists) and for any session that can still spawn subagents through the task tool. Only a top-level session with task spawning unavailable has no peers, hence no irc.executeresolves the registry and sender id; missing either returns a text error result instead of throwing.op: "list":registry.list()minus self and minusabortedagents —parkedpeers ARE listed. Each row includes the unread count fromIrcBus.unreadCount(...)and last activity.op: "send"validatesto/message, rejects self-sends, and rejectsawaitwithto: "all".- Target resolution: broadcasts fan out to
registry.listVisibleTo(senderId)(live peers only —running/idle; reviving every parked agent on a broadcast would be a stampede). Direct sends go through the bus unfiltered, so a parked recipient is revived. IrcBus.send(...)is fire-and-forget — it never blocks on the recipient generating anything. Delivery by recipient status:running→ message enqueued and injected as a non-interrupting aside at the recipient's next step boundary (AgentSession.deliverIrcMessage, rendered fromirc-incoming.md, persisted as anirc:incomingcustom message) — receiptinjected. If the sender awaits a reply (expectsReplyfromawait: true) and the recipient hasasync.enabledoff, the recipient also generates an ephemeral no-tools auto-reply (runEphemeralTurn, the/btwpipeline) and sends it back over the bus withreplyToset, recording anirc:autoreplyaside in its own history — a recipient blocked in a synchronous task spawn can never reach a step boundary before the sender's timeout otherwise;idle(live session) → enqueued and a real turn is started — the message wakes the agent — receiptwoken;parked→AgentLifecycleManager.global().ensureLive(to)revives the session first, then the wake path — receiptrevived;- resolution/revival failure → receipt
failedwith the error; other recipients still complete.
sendwithawait: truethen callsIrcBus.wait(senderId, { from: to }, timeoutMs, signal)and appends the reply (or a no-reply note suggestinginbox/wait) to the result. Awaited sends pass{ expectsReply: true }toIrcBus.sendso a busy recipient can auto-reply (see step 6).op: "wait"blocks until a message for the caller (optionally filtered byfrom) arrives, consumes it, and returns it. Timeout returns a clean "no message" result, not an error.op: "inbox"drains pending messages (or peeks withpeek: true) without blocking.- Timeouts resolve as
params.timeoutMs ?? irc.timeoutMs, normalized:0disables the timeout, negative/non-finite values fall back to the default120_000, positive values are truncated and clamped to ≥ 1 ms.
Modes / Variants
list: enumerate peers with status (running/idle/parked), unread counts, and last activity.senddirect: one exact peer id; wakes idle peers, revives parked ones.sendbroadcast:to: "all"to every live peer; parked peers are skipped.send+await: true: round-trip convenience — send, then wait for the next message from that peer. Marks the sendexpectsReply, enabling the busy-recipient auto-reply path when async execution is disabled.wait: block for an incoming message, optionally filtered by sender.inbox: non-blocking drain or peek.
Side Effects
- Session state
- Reads the process-global
AgentRegistry; direct sends to parked agents revive their sessions through the lifecycle manager. - Persists
irc:incomingcustom messages into recipient history; replies are ordinary turns in the recipient's own session. - Waking an idle/parked recipient starts a real agent turn (model requests, tool use) in that recipient.
- Reads the process-global
- User-visible prompts / interactive UI
- IRC events render as transcript cards in the TUI; the Agent Hub shows per-agent unread counts.
- Background work / cancellation
senditself never blocks on reply generation; onlywait(andawait: true) blocks, bounded by the resolved timeout and the caller'sAbortSignal.
- Network
- No IRC server connection. Woken recipients make their own model-provider calls as part of their turn.
- Filesystem
- No direct filesystem writes in the tool itself; recipient turns persist to their session JSONL as usual.
Limits & Caps
- Availability gates:
isIrcEnabled(running as a subagent, or task spawning available — there is noirc.enabledsetting), anAgentRegistry, and a caller agent id. - Mailboxes are bounded at 100 messages per agent (
MAILBOX_CAPinpackages/coding-agent/src/irc/bus.ts); oldest messages are dropped beyond the cap. irc.timeoutMsdefaults to120_000and is the defaultwait/send await:truetimeout;0disables the timeout, non-finite or negative values fall back to the default, positive values are truncated and clamped to at least1ms.- Broadcast scope: live peers only (
running/idle) vialistVisibleTo; direct sends address any non-aborted agent, including parked ones.
Errors
- The tool returns text errors (with
isError: true), not thrown exceptions, for:- missing registry:
IRC is unavailable in this session. - missing sender id:
IRC is unavailable: caller has no agent id. - missing
to/messageonsend - self-send:
Cannot send an IRC message to yourself. awaitwithto: "all"- unknown op
- missing registry:
- Per-recipient delivery failures surface as
failedreceipts with the error message;sendis markedisErroronly when no recipient received the message. waittimeout is a normal result (waited: null), not an error.
Notes
- This is IRC-like naming only: no servers, sockets, channels, or join/part state. Addressing is by exact registry agent id.
- Replies are real turns by the recipient, with one exception: an awaited send to a mid-turn recipient with
async.enabledoff triggers an ephemeral no-tools auto-reply (the oldrespondAsBackgroundpath), because a recipient blocked in a synchronous task spawn whose batch includes the sender can never run a real turn before the sender's timeout. A recipient may otherwise keep working before answering; checkinboxorwaitagain rather than re-sending. - Wake-on-message is the only resume primitive: messaging a parked agent revives it (same
ensureLivepath as the Agent Hub). The task tool has noresumeparameter. - Message ids are Snowflakes; pass them as
replyToto thread an answer to a specific message. - Persistence is per recipient history: the sender gets receipts in the tool result; the recipient sees the injected
irc:incomingmessage in its own transcript (visible viahistory://<id>).