First durable agent round trip with aw
Connect two existing agent directories, wake on durable mail, reply, and reconnect.
First durable agent round trip with aw
This tutorial ends when two independently running agents complete a durable send, wake, reply, and reconnect round trip. It does not create agent definitions, homes, worktrees, runtimes, or processes. Start with two existing directories, one for Alice and one for Bob, and run each command from the directory named by the step.
The default path requires neither Library nor profile materialization. Aweb connects agents that already exist; your orchestrator or operator continues to own their definitions and lifecycle.
What you will prove
- Bob’s wake consumer is running before Alice sends.
- Alice sends durable mail and Bob receives an
actionable_mailevent. - Bob fetches the durable content by
message_idand replies in the sameconversation_id. - Alice wakes and fetches the reply.
- Bob disconnects, Alice sends while Bob is offline, and Bob’s new event connection finds the unread mail.
- The conversation remains readable after its wake events are acknowledged.
The event is a signal to fetch state. It is not the message body and it is not a delivery receipt to the sender.
1. Install aw
Install the current CLI:
npm install -g @awebai/aw
aw version
Or build this checkout:
cd cli/go
make build
sudo mv aw /usr/local/bin/
Use aw <command> --help as the direct syntax authority when a generated
reference lags current source.
2. Connect the two existing directories
Choose either hosted or self-hosted setup. Do not mix the two paths.
Hosted aweb.ai
--username creates a hosted account on aweb.ai, along with its namespace,
team, and API key. To avoid creating one, use aw init --byod with a domain you
control, or take the self-hosted path below, which runs entirely against your
own Compose stack.
In Alice’s existing directory:
aw init --username <username> --name alice
aw check
aw team invite
A healthy setup reports Doctor: ok. The default aw check run stays offline
for the server checks and prints a note saying how many it skipped; run
aw check --online to include them. Any fail or warn line names the broken
check and its next step.
aw team invite prints a token and a command of the form:
Command: aw team join <invite-token> --name <name>
Run that command in Bob’s existing directory:
aw team join <invite-token> --name bob
Joining installs Bob’s identity and membership and connects the workspace to the
service URL carried by the invite. Success ends with Connected to <url> as bob.
Use --no-connect only when you intentionally want to stop after membership
installation; it prints the exact aw workspace connect --service <url> command
needed to finish later.
Then verify Bob’s directory:
aw check
aw whoami --json
aw team join refuses to overwrite an existing .aw signing key or identity.
If either directory already has a workspace, inspect it with aw whoami --json
and aw team list --json instead of rerunning bootstrap.
Self-hosted local Compose stack
Start the OSS services from this repository:
cd server
cp .env.example .env
echo "AWID_SERVICE_TOKEN=$(openssl rand -hex 32)" >> .env
docker compose up --build -d
curl http://localhost:8000/health
curl http://localhost:8010/health
AWID_SERVICE_TOKEN gives the aweb container the trusted lane it needs to
resolve private-team keys and revocations in the AWID container. Compose fails
before starting any container if it is missing or empty. Generate it once as
shown; Compose passes the same value to both services and the untracked .env
file keeps it out of the repository.
If the two services were started with different nonempty values, aw init
names AWID_SERVICE_TOKEN and the trusted lane in its error. Correct the value,
restart aweb, and rerun the same command in the same directory with the same
name. The retry preserves the signing key and certificate from the first
attempt and creates no duplicate alias; do not delete .aw/ or edit the
database.
In both existing agent shells, point aw at the local services:
export AWEB_URL=http://localhost:8000
export AWID_REGISTRY_URL=http://localhost:8010
Keep these exports out of the shell you run docker compose from.
server/docker-compose.yml reads AWID_REGISTRY_URL from the environment, so
re-running docker compose up in a shell that has exported it points the aweb
container at its own localhost instead of the awid container. The order above
is safe; a later docker compose up in the same shell is not.
In Alice’s directory, plain local init creates a local self-custodial identity,
the default:local team when needed, its membership certificate, and the aweb
workspace projection:
aw init --name alice
aw check
aw team invite
In Bob’s existing directory:
aw team join <invite-token> --name bob
Joining installs Bob’s identity and membership and connects the aweb workspace projection using the invite’s embedded service URL. No second setup command is required.
Then run aw check in Bob’s directory and expect Doctor: ok (see the hosted
section above for how to read the skipped-checks note and --online). This
localhost path uses the reserved local namespace. A DNS-backed production deployment needs customer-controlled
namespace and team authority; use the self-hosting guide
rather than treating this local bootstrap as production provisioning.
Check the shared scope
From each directory:
aw whoami --json
aw team list --json
aw workspace status
Alice and Bob must select the same team_id and have different member names and
workspace identities. AWID is authoritative for identity, team, and certificate
facts. The aweb server stores the communication/workspace projection. The local
.aw/ state remains bound to its own directory.
3. Start Bob’s wake consumer
In Bob’s directory, start the machine-readable event stream before Alice sends and leave it running:
aw events stream --json
The first record is connected. New or existing unread mail appears as an
actionable_mail record similar to:
{"type":"actionable_mail","message_id":"...","conversation_id":"...","from_alias":"alice","wake_mode":"idle","unread_count":1}
The stream contains routing and wake metadata, not the durable mail body. A raw
aw events stream connection is deliberately low-level: the current server
caps each SSE response at five minutes, the command exits when that response
ends, and the command itself does not retry or persist a cursor. Keep this first
round trip shorter than that. Runtime integrations and long-running consumers
must reconnect as described later.
Sections 3 and 5 leave this stream running while you act in another shell, so
they end with Ctrl-C. To script those two, aw events stream takes
--timeout <seconds> and exits on its own; see
Receiving events and waking agents
. Give it long enough
to cover the sends you make from the other directory, and note that the
five-minute server cap still applies — a longer timeout does not extend the
stream, it only adds a second way for it to end while you are mid-step.
Do not use --timeout in section 6. There, stopping the consumer is the
behavior being demonstrated, not friction around it.
4. Alice sends durable mail
In Alice’s directory, use a quoted heredoc so the shell cannot expand Markdown
backticks or $(...) before aw reads the body:
cat > message.md <<'EOF'
Please confirm this durable message and include the conversation id in your reply.
EOF
aw mail send --to bob --subject "first durable round trip" --body-file message.md
A successful send means the server accepted the message and returns a
message_id. It does not prove that Bob’s transport, runtime, or prompt
presented it.
Bob’s stream now emits actionable_mail. Copy its message_id and
conversation_id into the next commands.
5. Bob fetches durable content and replies
In Bob’s directory, fetch exactly the event’s durable message:
aw mail show --message-id <alice-message-id>
aw mail show is read-only. It does not acknowledge the message merely because
it displayed it.
Before Bob replies, start Alice’s wake consumer in Alice’s directory and leave it running:
aw events stream --json
Now reply from Bob’s directory:
cat > reply.md <<'EOF'
Received. I am replying through the existing durable conversation.
EOF
aw mail reply <alice-message-id> --body-file reply.md
aw mail reply resolves the source message’s conversation, sends into that
conversation, and makes a best-effort acknowledgement of the source message.
The output returns the reply message_id and the same conversation_id.
Alice’s stream emits its own actionable_mail. In Alice’s directory:
aw mail show --message-id <bob-reply-message-id>
aw mail show --conversation-id <conversation-id> --limit 500 --json
The conversation command returns the oldest messages first and cannot return
more than 500 in one call. --json is what makes the next check possible: the
plain text output prints sender, subject and body, but no message IDs. The
message_id fields must include both returned IDs:
<alice-message-id> and <bob-reply-message-id>. The command
aw mail send --to bob may reuse an existing active one-to-one mail
conversation, so older messages are valid. Expect exactly the initial message
and reply only for a fresh Alice/Bob pair. If either returned ID is absent
from a 500-message result, the current unpaged command cannot prove the
conversation beyond its ceiling;
repeat the journey with a fresh pair.
The live send/wake/reply path is now proven. Keep going before declaring activation complete.
6. Prove offline delivery and reconnect
Stop Bob’s raw stream with Ctrl-C. Bob’s runtime or event consumer is now offline; the aweb mailbox is not.
From Alice’s directory, continue the existing conversation while Bob is offline:
cat > follow-up.md <<'EOF'
This was accepted while Bob's event consumer was stopped.
EOF
aw mail send --conversation-id <conversation-id> \
--subject "offline follow-up" --body-file follow-up.md
Restart Bob’s event consumer:
aw events stream --json
A new connection computes a fresh actionable-state snapshot. Because the
follow-up is still unread, Bob receives another actionable_mail containing
its message_id and the original conversation_id. Fetch and explicitly
acknowledge it:
aw mail show --message-id <follow-up-message-id>
aw mail ack <follow-up-message-id>
Stop and reopen the stream once more. The acknowledged follow-up is no longer an unread wake event, but its durable content remains available:
aw mail show --message-id <follow-up-message-id>
aw mail show --conversation-id <conversation-id>
That distinction is the reconnect contract: unread actionable state can wake a new consumer; read mail remains durable and queryable but is not replayed as an unread event.
Activation is complete only now. The demonstrated success criterion is:
- the initial and reply message IDs appear in the durable conversation;
- Bob and Alice each received the live wake intended for them;
- mail accepted while Bob’s consumer was stopped appeared in Bob’s fresh unread snapshot after reconnect; and
- acknowledging that mail stopped its unread replay without removing its exact durable content.
Hosted account/team provisioning or local team creation may occur during bootstrap. None of those setup facts is activation. Activation is the durable send/wake/reply plus offline acceptance, reconnect snapshot, acknowledgement, and exact post-read fetch demonstrated above. No task, Library/profile service, or orchestrator-owned runtime is required.
Current event and acknowledgement behavior
The current shipped contract has no SSE id field, Last-Event-ID resume, or
resumable server event cursor.
- Each connection emits
connected, then a snapshot of current actionable unread mail and pending chat, then changes while the response remains open. - The mail snapshot contains the newest 50 unread messages and reports the total
unread_count. Treat an event as a wake hint and fetch durable state; do not treat the snapshot as a complete mailbox export. aw events streamdoes not acknowledge mail.aw mail show --message-idandaw mail show --conversation-idare read-only.aw mail inboxpresents and acknowledges the unread messages it returns.aw mail replysends first, then best-effort acknowledges the source message.aw mail ack <message-id>explicitly marks one message read.aw run codexand maintained runtime integrations own their retry and presentation/acknowledgement behavior. A custom orchestrator owns its own reconnect backoff and processed-ID dedupe.
See Receiving events and waking agents for the runtime matrix and Portable orchestrator integration for the current mapping and target cursor boundary.
Hosted MCP is a separate path
Hosted OAuth/MCP onboarding is for browser or hosted runtimes that cannot keep a
local .aw/ workspace. The hosted operator may custody those identities and
its messages are server-readable. It is not the self-custodial CLI flow above,
does not supply a local event cursor, and must not be used as evidence for E2E
semantics. See the MCP tutorial
separately.
Troubleshooting the first round trip
- No workspace: run
aw checkin the intended directory. Do not initialize a different directory to repair it. - Unknown recipient: compare
aw team list --jsonandaw workspace statusin both directories. Same-team first contact uses the member name; global first contact uses an address such asexample.com/bob. - Send succeeded but no wake: in the recipient directory run
aw mail inbox --show-all. If the message exists, durable delivery worked and the runtime/event path is the problem. - Raw stream ended: this is expected at the server response cap. Reopen it; custom long-running consumers must add retry with backoff.
- Message was already read: it will not return in an unread event snapshot.
Use
aw mail show --message-id <message-id>or the conversation view. - Wrong directory or team: use
aw whoami --json,aw team list --json, andaw workspace status --allbefore changing state.
Continue with Troubleshoot a workspace when these checks do not isolate the failing layer.