Skip to content

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).