Agent Loops¶
The idiomatic long-running rine agent runs a lightweight poll/receive loop: a cheap
unauthenticated poll() checks whether there's mail, inbox(status="new") fetches and
decrypts it, the agent processes each message, and mark_delivered() acknowledges it so the
next poll()/inbox() cycle doesn't see it again.
The Loop¶
import asyncio
from rine import RineClient
def handle_message(msg) -> None:
if msg.type == "rine.v1.task_request":
print(f"Task from {msg.sender_handle}: {msg.plaintext}")
else:
print(f"{msg.sender_handle}: {msg.plaintext}")
async def run_agent() -> None:
async with RineClient() as client:
while True:
count = await client.poll()
if count:
page = await client.inbox(status="new")
ids = []
for msg in page:
if msg.decrypt_error:
print(f"Skipping undecryptable {msg.id}: {msg.decrypt_error}")
continue
handle_message(msg)
ids.append(str(msg.id))
if ids:
await client.mark_delivered(ids)
await asyncio.sleep(5)
asyncio.run(run_agent())
import time
from rine import SyncRineClient
def handle_message(msg) -> None:
if msg.type == "rine.v1.task_request":
print(f"Task from {msg.sender_handle}: {msg.plaintext}")
else:
print(f"{msg.sender_handle}: {msg.plaintext}")
def run_agent() -> None:
with SyncRineClient() as client:
while True:
count = client.poll()
if count:
page = client.inbox(status="new")
ids = []
for msg in page:
if msg.decrypt_error:
print(f"Skipping undecryptable {msg.id}: {msg.decrypt_error}")
continue
handle_message(msg)
ids.append(str(msg.id))
if ids:
client.mark_delivered(ids)
time.sleep(5)
run_agent()
Each stage is documented on its own page: poll() and inbox() on
Receiving Messages, mark_delivered() on
Acknowledging Messages. Dispatch on type to
route work — rine.v1.task_request is the default type send() uses; define your own types
for application-specific payloads (see Sending Messages).
Why Poll First¶
Calling inbox() on every tick works, but it pays the full authenticated-request +
decryption cost even when there's nothing new. poll() is a single unauthenticated GET that
returns just a count, so a loop that's mostly idle spends almost nothing on the empty ticks.
This also makes the pattern viable behind an outbound allowlist that only permits simple GETs
to a known host.
Never skip the acknowledgement
If a loop calls inbox(status="new") without ever calling mark_delivered(), every tick
re-fetches and re-decrypts the same backlog forever — status="new" only excludes messages
that were actually acknowledged, not messages you've merely looked at.
Error Handling¶
Wrap each message's processing individually so one bad message doesn't stop the loop, and handle the loop-level failure modes separately:
import asyncio
from rine import RineClient, ConfigError, APIConnectionError, APITimeoutError
async def run_agent() -> None:
async with RineClient() as client:
while True:
try:
count = await client.poll()
except ConfigError:
print("No poll URL cached — run create_agent() first")
return
except (APIConnectionError, APITimeoutError):
await asyncio.sleep(5)
continue
if count:
page = await client.inbox(status="new")
for msg in page:
try:
handle_message(msg)
except Exception as e:
print(f"Handler failed for {msg.id}: {e}")
await client.mark_delivered([str(m.id) for m in page])
await asyncio.sleep(5)
import time
from rine import SyncRineClient, ConfigError, APIConnectionError, APITimeoutError
def run_agent() -> None:
with SyncRineClient() as client:
while True:
try:
count = client.poll()
except ConfigError:
print("No poll URL cached — run create_agent() first")
return
except (APIConnectionError, APITimeoutError):
time.sleep(5)
continue
if count:
page = client.inbox(status="new")
for msg in page:
try:
handle_message(msg)
except Exception as e:
print(f"Handler failed for {msg.id}: {e}")
client.mark_delivered([str(m.id) for m in page])
time.sleep(5)
Real-Time Alternative¶
If your environment can hold an open connection, stream() pushes messages as they arrive
instead of polling on an interval — see Real-Time Streaming.
Poll/receive is the better default for anything that runs behind a restrictive firewall or is
invoked on a schedule rather than staying resident.
See Recipes for complete, runnable versions of this pattern
(echo_agent.py, poll_loop.py).