Skip to content
Concepts

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.

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:

TS
{
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 with put, read, ordered list(limit, afterKey?) and delete. Use the same key ordering for sorting and the exclusive afterKey filter so paging cannot skip or repeat rows.
  • post(mail): delivers one reply and reports acceptance.
  • scheduleWake(at): arms a durable timer for drainOutbox.
  • now(): the current time in milliseconds.