Entities
Stateful objects with automatic persistence and single-writer consistency
An Entity is a stateful object identified by a unique key, with automatic persistence and single-writer consistency per key.
Think of an Entity like a database row with methods — each key is an isolated instance with its own state that survives crashes and serializes concurrent access.
Why It Exists
When two concurrent requests update the same conversation history, one overwrites the other. When a process crashes, in-memory state is lost. Normal objects don’t coordinate concurrent access or survive failures.
Entities solve this. Each key is an isolated instance. State persists automatically. Concurrent updates to the same key are serialized — no race conditions, no lost updates.
When to Use
- Conversation history that survives crashes
- User preferences or session state
- Counters, budgets, or balances that need atomic updates
- Any state that multiple requests might access concurrently
Example
from agnt5 import Entity
class Conversation(Entity):
async def add_message(self, role: str, content: str) -> dict:
messages = self.state.get("messages", [])
messages.append({"role": role, "content": content})
self.state.set("messages", messages)
return {"message_count": len(messages)}
async def get_history(self) -> list[dict]:
return self.state.get("messages", [])Call it via the client:
from agnt5 import Client
client = Client()
# Each key is an isolated instance
conversation = client.entity("Conversation", "session-abc")
conversation.add_message(role="user", content="Explain quantum computing")
# Same key = same state
history = conversation.get_history() # Returns the message we just addedHow It Works
Entities provide three guarantees:
- Automatic Persistence — State is saved after each successful method call and loaded before the next
- Single-Writer Consistency — Only one method executes at a time per key (concurrent calls are serialized)
- Isolation by Key — Different keys are independent instances that can run in parallel
sequenceDiagram
participant C as Client
participant G as Gateway
participant E as Entity
participant S as State Store
C->>G: entity(type, key).method()
G->>S: Load state for key
S-->>G: Current state
G->>E: Execute method with state
E-->>G: Result + updated state
G->>S: Save state for key
G-->>C: Result
Note over G,S: Concurrent calls to same key wait in queue
Transactional semantics: If a method throws an exception, state changes are not saved. Either the method succeeds and state is persisted, or it fails and state remains unchanged.
class TokenBudget(Entity):
async def consume_tokens(self, tokens: int) -> dict:
budget = self.state.get("remaining", 0)
if tokens > budget:
raise ValueError(f"Insufficient budget: {budget} < {tokens}") # State NOT saved
self.state.set("remaining", budget - tokens) # Only saved if no exception
return {"remaining": budget - tokens}Configuration
State API
Access state through self.state inside entity methods:
| Method | Description |
|---|---|
self.state.get(key, default) | Get value (returns default if not found) |
self.state.set(key, value) | Set value |
self.state.delete(key) | Delete key |
self.state.clear() | Clear all state |
class ResearchSession(Entity):
async def add_finding(self, source: str, content: str) -> dict:
findings = self.state.get("findings", [])
findings.append({"source": source, "content": content})
self.state.set("findings", findings)
return {"total": len(findings)}
async def reset(self) -> dict:
self.state.clear()
return {"reset": True}Key Patterns
The key identifies which instance you’re operating on. Choose keys based on your isolation boundary:
| Pattern | Example | Use Case |
|---|---|---|
| Session-scoped | Conversation:session-abc | Chat history per session |
| User-scoped | Preferences:user-123 | Settings per user |
| Resource-scoped | TokenBudget:org-456 | Shared budget per org |
# Different keys = different instances = parallel execution
session_a = client.entity("Conversation", "session-a")
session_b = client.entity("Conversation", "session-b")
# These run concurrently (different keys)
session_a.add_message(role="user", content="Hello")
session_b.add_message(role="user", content="Hi there")Guidelines
Common Patterns
Entity for conversation history (keyed by session)
Entity for user preferences (keyed by user ID)
Entity for rate limiting (keyed by API key or user)
Workflow → Entity (checkpoint workflow state in entity)Common Pitfalls
-
❌ Don’t access
self.stateoutside methods — State is only available inside entity methods. It’s loaded before and saved after each call. -
❌ Don’t use entities for stateless operations — If you don’t need persistence or coordination, use Functions instead.
-
❌ Don’t share keys across unrelated data — Each key should represent one logical instance. Don’t overload a key with multiple concerns.
What Entities Don’t Do
- Not for stateless computation — Use Functions for LLM calls, embeddings, or pure transformations
- Not for multi-step orchestration — Use Workflows for pipelines with checkpointing
- Not for LLM reasoning — Use Agents for autonomous decision-making
API Reference
Entity— Base class for stateful entitiesself.state.get(key, default)— Get state valueself.state.set(key, value)— Set state valueself.state.delete(key)— Delete state keyself.state.clear()— Clear all stateclient.entity(type, key)— Get entity proxy for calling methods