Agent Lifecycle
Agent Lifecycle
Overview
This document describes the full lifecycle of an agent: how the webhook handler triggers automatic creation via Rivet, how agents are reused, how they sleep/wake, and how they terminate.
Canonical reference: Rivet Actor Model contains the full implementation with code. This document focuses on the flow and state transitions.
What Manages Agents?
Rivet Runtime manages the agents. The FastAPI gateway is a thin router that does nothing but:
- Receive webhook
- Call
rivet.get_actor(ShoppingAgentActor, user_id) - Send message to actor
- Return 200 OK
Rivet handles everything else: creation, routing, concurrency, sleep/wake, termination, state persistence.
Flow: New User Texts
sequenceDiagram
actor User
participant WA as WhatsApp
participant API as FastAPI Gateway
participant RV as Rivet Runtime
participant ACTOR as ShoppingAgentActor
participant PI as Pi Agent
participant MCP as @shopify/dev-mcp
User->>WA: "Hi, show me red shoes"
WA->>API: POST /webhook
API-->>WA: 200 OK (instant)
API->>RV: get_actor(ShoppingAgentActor, "+94771234567")
Note over RV: user_id NOT in cluster<br/>→ CREATE new actor
RV->>ACTOR: Instantiate actor
ACTOR->>ACTOR: on_init()
ACTOR->>MCP: Initialize Shopify MCP client
ACTOR->>PI: Create Pi Agent + register tools
ACTOR-->>RV: Ready
RV-->>API: actor reference
API->>ACTOR: on_message("Hi, show me red shoes")
ACTOR->>ACTOR: Rate limit check
ACTOR->>ACTOR: Injection check
ACTOR->>PI: agent.prompt(text)
PI->>MCP: search_shop_catalog("red shoes")
MCP-->>PI: Products
PI-->>ACTOR: Response
ACTOR->>ACTOR: Save state (durable)
ACTOR-->>API: Response
API->>WA: Send WhatsApp reply
WA-->>User: "Found 5 products..."
Note over ACTOR: Sleeps after 5min idle
Flow: Returning User Texts
sequenceDiagram
actor User
participant WA as WhatsApp
participant API as FastAPI Gateway
participant RV as Rivet Runtime
participant ACTOR as ShoppingAgentActor
User->>WA: "Add the first one to cart"
WA->>API: POST /webhook
API-->>WA: 200 OK
API->>RV: get_actor(ShoppingAgentActor, user_id)
alt Actor is sleeping
RV->>ACTOR: Wake (on_wake)
ACTOR->>ACTOR: Reconnect MCP client
else Actor is running
Note over RV: Queue message<br/>(serialized execution)
end
RV-->>API: actor reference
API->>ACTOR: on_message(text)
Note over ACTOR: Has full conversation history<br/>(from durable state)
ACTOR-->>API: Response
API->>WA: Send reply
WA-->>User: "Added to cart!"
State Lifecycle
stateDiagram-v2
[*] --> NotExists: User has never texted
NotExists --> Creating: First webhook
Creating --> Running: on_init() complete
Running --> Running: on_message() serialized
Running --> Sleeping: 5 min idle
Sleeping --> Running: Wake on message (on_wake)
Sleeping --> Terminated: 1 hr idle
Terminated --> NotExists: Destroyed
Terminated --> Creating: Next message recreates
What Rivet Gives Us
| Concern | What Rivet Does |
|---|---|
| Creation | get_actor() creates if missing, no manual check |
| Routing | Routes message to correct actor by actor_id |
| Concurrency | on_message() runs one at a time per actor (no locks needed) |
| Queueing | Messages queue during execution, processed in order |
| State | self.state.set/get persists across sleep/wake/restarts |
| Sleep | Auto-sleep after idle_timeout_seconds |
| Wake | on_wake() called when new message arrives |
| Terminate | Auto-terminate after termination_timeout_seconds |
| Distribution | Actors spread across cluster automatically |
| Recovery | Resume from last saved state on crash |
Actor Implementation Sketch
@rivet.actor
class ShoppingAgentActor(Actor):
class Config:
idle_timeout_seconds = 300 # Sleep after 5 min
termination_timeout_seconds = 3600 # Terminate after 1 hr
durable = True
async def on_init(self):
"""Create Pi Agent + register tools."""
self.mcp_client = ShopifyMCPClient(...)
await self.mcp_client.initialize()
self.agent = Agent(AgentOptions(...))
self.agent.set_tools(self.mcp_client.get_tools())
async def on_message(self, text: str) -> str:
"""Called for each incoming message. Serialized by Rivet."""
if not self._check_rate_limit():
return "⏳ Slow down please."
if not self._check_injection(text):
return "I only help with shopping."
response_parts = []
def handler(event):
if event.type == "message_update":
for c in event.message.content:
if c.type == "text":
response_parts.append(c.text)
unsub = self.agent.subscribe(handler)
try:
await self.agent.prompt(text)
response = "".join(response_parts)
finally:
unsub()
await self.state.set("last_messages", [m.dict() for m in self.agent.state.messages])
return response
async def on_wake(self):
"""Reconnect resources after sleep."""
if not self.mcp_client:
self.mcp_client = ShopifyMCPClient(...)
await self.mcp_client.initialize()
async def on_terminate(self):
"""Final cleanup."""
if self.mcp_client:
await self.mcp_client.close()
The Gateway Is 6 Lines
@app.post("/webhook")
async def webhook(request: Request, background_tasks: BackgroundTasks):
body = await request.json()
for msg in extract_whatsapp_messages(body):
actor = await rivet.get_actor(ShoppingAgentActor, msg["sender"])
background_tasks.add_task(actor.on_message, msg["text"])
return {"status": "ok"}
That’s the entire FastAPI gateway. Everything else lives in the actor.
Scaling
Because actors are distributed by Rivet:
graph TB
LB[Load Balancer] --> G1[Gateway Instance 1]
LB --> G2[Gateway Instance 2]
LB --> G3[Gateway Instance 3]
G1 --> RV[Rivet Cluster]
G2 --> RV
G3 --> RV
RV --> A1[Actor: User A]
RV --> A2[Actor: User B]
RV --> A3[Actor: User C]
RV --> AN[Actor: User N]
style RV fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px
- Multiple gateway instances → no sticky sessions needed
- Actors distributed across Rivet nodes
- Any gateway can route to any actor
- Cluster scales horizontally
Comparison: Old vs New Architecture
| Aspect | Old (In-Process Pool) | New (Rivet Actors) |
|---|---|---|
| Pool management | Manual dict[str, Agent] |
Rivet handles automatically |
| Concurrency | asyncio.Lock per user |
Serialized by Rivet |
| Queuing | asyncio.Queue per user |
Built-in per actor |
| State | Manual save to S3/Redis | self.state.set/get |
| Idle | Custom cleanup loop | Built-in sleep/wake/terminate |
| Durability | Lost on crash | Survives restarts |
| Scale | Single process | Cluster-wide |
| Gateway complexity | Heavy (pools, queues, locks) | 6 lines of code |
| Failure | Lose all state | Resume from last checkpoint |
See Also
- Rivet Actor Model — Full implementation with complete code
- Agent Architecture — Detailed agent stack
- Architecture — High-level overview