Concepts
Durable addressed agent mail over generic Watchdog work
BirdDog is a durable mail room for AI agents. One agent sends work to another
agent’s session, the work runs as a Watchdog job, and
when that job settles BirdDog delivers the reply to everyone on the envelope’s
replyTo list. Replies sit in a durable outbox and retry on a bounded schedule
until the recipient accepts them.
It does two jobs:
- It translates addressed agent work into an opaque Watchdog job, and decodes that job’s payload back after Watchdog claims it.
- It fans out replies when the job settles, stores them in your outbox, and
delivers them with retries through Carrier’s Post outbox
(
@fungi.computer/carrier/post).
Every sender and recipient is an ordinary agent session. A parent and a child
agent are both plain agents to BirdDog. Their relationship lives in your code,
and all agent work uses Watchdog’s default lane. You own storage, transport,
the clock and wake scheduling. BirdDog owns the envelope format, the reply keys
and the retry schedule.
Core concepts
Section titled “Core concepts”Addresses. An agent address is { kind: "agent", agentId, sessionId }. IDs
are 1 to 256 characters. There is no special kind for subagents.
Mail. A BirdDogMail has a stable key, a to and from address, a
replyTo list, string content and a createdAt time in milliseconds. The key
must identify the request across retries. BirdDog derives every reply key from
it.
Agent jobs. BirdDog.agentJob({ work, mail? }) validates the input and
returns a queued Watchdog job. The job’s queue is the target sessionId, its
lane is default, its priority is 0, and its payload carries the sessionId,
the commandFactId and the optional mail. work.recovery passes straight
through to Watchdog’s recovery policy. Because it is synchronous, you can call
it inside an owner transaction and pass the result to Watchdog’s
transactional.enqueueAcceptedJob. BirdDog.decodeAgentJob(job) parses the
payload back on the executor side. Watchdog never reads it.
Fan-out. fanOut({ job, content, diagnostic? }) takes a settled job, the
content owed to correspondents and the Session’s terminal diagnostic. It writes
one reply per replyTo address into the outbox, keyed
outbox:<mail key>:reply:<index>, then schedules one wake for the earliest.
Each reply’s from is the mail’s recipient, its content is your content
verbatim (or, when it is empty,
The delegated work ended <outcome> without reply text., so a reply is always
admissible text), and its typed reply field carries the settlement facts:
{ commandId: "command-1", // the settled work's commandFactId outcome: "failed", // the exact Watchdog outcome diagnostic: "provider_error: 401 Authorization: Bearer [redacted]",}outcome is exactly the job’s: completed, failed, interrupted,
cancelled or outcome_unknown. Only failed, interrupted and
outcome_unknown replies carry a diagnostic. boundedDiagnostic makes it one
line, masks credential-shaped values (authorization-scheme credentials, named
keys, tokens, secrets and passwords, and long letter-and-digit runs), then cuts
it to BIRD_DOG_DIAGNOSTIC_MAX_LENGTH (200) characters. A cancelled job
delivers the supplied cancellation result as its content. A settlement’s meta
is for your own fan-out wrapper, such as naming who cancelled; replies never
carry it. A job without mail sends nothing.
The recipient’s host owns authentication and presentation. It trusts the reply
facts only after checking the reply against the mail it sent, then decides who
authors the line and how its model sees it.
Draining. drainOutbox() reads one page of up to 64 outbox rows and calls
post for each due reply. post returns true when the recipient accepts the
mail. A false or a throw schedules a retry. If more rows remain, BirdDog
schedules an immediate wake and continues from the same place on the next drain.
Receipts. A delivered reply’s row is replaced by a compact completion
receipt under the same key. Replaying the same settlement after a restart finds
the receipt and does not post the reply again. Keep those receipts as long as
source settlements can replay. BirdDog does not expire them.
hasPendingDelivery(outbox) reads only whether any reply still awaits a
delivery attempt, so a host can tell retained receipts and dead letters from
owed mail.
The Effect mail room’s nextWakeAt() reads Carrier’s earliest owed delivery,
including future retries. Runtime hosts include that hint in their pending-work
answer rather than clearing a retry when other runtime work is idle.
Host ports. BirdDog.make takes four things:
outbox: string rows withput,read, orderedlist(limit, afterKey?)anddelete. Use the same key ordering for sorting and the exclusiveafterKeyfilter so paging cannot skip or repeat rows.post(mail): delivers one reply and reports acceptance.scheduleWake(at): arms a durable timer fordrainOutbox.now(): the current time in milliseconds.