Memory events
Reactive programming with memory change subscriptions: trigger agent logic when shared state changes.
Trigger agent logic the instant shared state changes -- no polling, no delays.
Agents often need to react when another agent writes data: reorder inventory when stock drops, alert a human when risk spikes, kick off downstream processing when upstream results land. AgentField's memory event system lets you subscribe to key patterns and run handlers when matching keys change -- across any scope, from any agent.
from agentfield import Agent
app = Agent(node_id="inventory-monitor")
# Reactive inventory — auto-reorder when stock drops below threshold
@app.on_change("warehouse.stock.*")
async def on_stock_change(event):
product_id = event.key.split(".")[-1]
stock_level = event.data.get("quantity", 0)
threshold = event.data.get("reorder_threshold", 10)
if stock_level < threshold:
await app.call(
"procurement.reorder",
product_id=product_id,
current_stock=stock_level,
order_quantity=threshold * 3,
)
await app.memory.global_scope.set(
f"alerts.reorder.{product_id}",
{"status": "reorder_triggered", "quantity": threshold * 3},
)
# Cross-agent notification — alert when any agent flags high risk
@app.memory.global_scope.on_change("risk.scores.*")
async def on_risk_spike(event):
if event.data.get("level") == "critical":
await app.call(
"notifications.slack",
channel="#risk-alerts",
message=f"Critical risk: {event.key} = {event.data}",
)What just happened
- A memory write became an event source for downstream automation
- Handlers reacted immediately without polling loops
- The same reactive pattern worked across agents because events flow through shared memory
Example event payload:
{
"key": "warehouse.stock.widget-42",
"scope": "global",
"scope_id": "global",
"action": "set",
"data": { "quantity": 3, "reorder_threshold": 10 },
"previous_data": { "quantity": 11, "reorder_threshold": 10 }
}
What you get
- Pattern-based subscriptions -- watch specific keys, wildcard ranges, or entire namespaces with glob patterns.
- Scoped listeners -- subscribe at global, session, actor, or workflow scope for precise targeting.
- MemoryChangeEvent -- rich event object with key, new value, old value, scope, timestamp, and source agent.
- Cross-agent reactivity -- one agent writes, another agent's handler fires immediately.
- No polling -- push-based delivery via the control plane's internal event bus.
- Multiple patterns -- TypeScript supports arrays of patterns in a single subscription.
MemoryChangeEvent structure
Every handler receives a MemoryChangeEvent with the following fields:
| Field | Python | TypeScript | Description |
|---|---|---|---|
| Key | event.key | event.key | The memory key that changed (e.g., "warehouse.stock.sku-42") |
| New value | event.data | event.data | The new value written to the key |
| Previous value | event.previous_data / event.old_value | -- | The previous value (Python only) |
| Scope | event.scope | event.scope | Memory scope: "global", "session", "actor", "workflow" |
| Scope ID | event.scope_id | event.scopeId | The scope identifier |
| Timestamp | event.timestamp | event.timestamp | Timestamp of the change |
| Source agent | -- | event.agentId | The agent that wrote the change (TypeScript only) |
| Action | event.action | -- | "set" or "delete" (Python only) |
@app.on_change("orders.*")
async def on_order(event):
print(event.key) # "orders.ord-123"
print(event.data) # {"status": "shipped", "tracking": "1Z999..."}
print(event.previous_data) # {"status": "processing"}
print(event.old_value) # {"status": "processing"} (alias for previous_data)
print(event.scope) # "global"
print(event.timestamp) # "2026-03-24T10:30:00Z"
print(event.action) # "set"Pattern syntax
Memory event patterns use a glob-like syntax to match keys.
| Pattern | Matches | Does Not Match |
|---|---|---|
"orders.pending" | orders.pending (exact) | orders.shipped, orders.pending.item1 |
"orders.*" | orders.pending, orders.shipped, orders.pending.item1 | inventory.stock |
"*.stock" | warehouse.stock, store.stock | warehouse.stock.sku42, inventory.item |
"**" | Everything | -- |
Rules
*is converted to.*regex internally, so it crosses dot boundaries. Unlike filesystem globs,*and**are functionally identical here -- both match across dot-separated segments.- Exact strings match literally.
- Patterns are case-sensitive.
Multiple Patterns (TypeScript)
// Watch multiple patterns in one subscription
agent.watchMemory(
["orders.*", "payments.*", "refunds.*"],
async (event) => {
console.log(`Transaction event: ${event.key}`);
}
);In Python, register multiple decorators:
for pattern in ["orders.*", "payments.*", "refunds.*"]:
@app.on_change(pattern)
async def on_transaction(event):
print(f"Transaction event: {event.key}")Scoped subscriptions
Subscribe to events within a specific memory scope for precise targeting.
# Global scope — watch shared config changes
@app.memory.global_scope.on_change("config.*")
async def on_config_change(event):
print(f"Global config changed: {event.key}")
# Session scope — watch conversation state for a specific session
@app.memory.session("session-abc").on_change("conversation.*")
async def on_conversation(event):
print(f"Session conversation updated: {event.key}")
# Actor scope — watch user preference changes
@app.memory.actor("user-42").on_change("preferences.*")
async def on_prefs_change(event):
print(f"User preferences changed: {event.key}")
# Workflow scope — watch intermediate results
@app.memory.workflow("wf-789").on_change("step.*")
async def on_step_complete(event):
print(f"Workflow step completed: {event.key}")Patterns
Event-Driven Pipeline
Chain agents reactively -- each writes results that trigger the next stage.
# Stage 1: Intake writes classification
@app.reasoner()
async def intake(document: str) -> dict:
classification = await app.ai(user=document, schema=DocType)
await app.memory.global_scope.set(f"pipeline.classified.{doc_id}", classification.dict())
return classification.dict()
# Stage 2: Triggered when classification lands
@app.memory.global_scope.on_change("pipeline.classified.*")
async def on_classified(event):
doc_id = event.key.split(".")[-1]
result = await app.call(
f"{event.data['type']}-analyzer.analyze",
doc_id=doc_id,
classification=event.data,
)
await app.memory.global_scope.set(f"pipeline.analyzed.{doc_id}", result)
# Stage 3: Triggered when analysis completes
@app.memory.global_scope.on_change("pipeline.analyzed.*")
async def on_analyzed(event):
doc_id = event.key.split(".")[-1]
await app.call("reporter.generate", doc_id=doc_id, analysis=event.data)Cross-Agent Notifications
Decouple producers from consumers -- agents write to known keys, any agent can subscribe.
# Producer agent: writes alerts
await app.memory.global_scope.set("alerts.security.sql-injection", {
"severity": "critical",
"source": "waf-agent",
"details": "SQL injection attempt blocked from 192.168.1.50",
})
# Consumer agent: reacts to any security alert
@app.memory.global_scope.on_change("alerts.security.*")
async def on_security_alert(event):
if event.data.get("severity") == "critical":
await app.call("incident.create", **event.data)Saga Coordination
Coordinate distributed transactions across agents using memory events as signals.
@app.memory.global_scope.on_change("saga.payment.*")
async def on_payment_event(event):
saga_id = event.key.split(".")[-1]
status = event.data.get("status")
if status == "completed":
await app.call("shipping.dispatch", saga_id=saga_id)
await app.memory.global_scope.set(f"saga.shipping.{saga_id}", {"status": "dispatching"})
elif status == "failed":
await app.call("orders.rollback", saga_id=saga_id)SDK reference
Event Subscription Methods
| Feature | Python | TypeScript | Go |
|---|---|---|---|
| Watch changes | @app.on_change(pattern) | agent.watchMemory(pattern, handler) | Not supported |
| Scoped watch | @app.memory.<scope>.on_change(pattern) | agent.watchMemory(pattern, handler, { scope }) | Not supported |
| Multiple patterns | Multiple decorators | agent.watchMemory([p1, p2], handler) | Not supported |
| Unsubscribe | subscription.unsubscribe() (only from subscribe(), not @on_change) | -- | Not supported |
MemoryChangeEvent Fields
| Field | Python | TypeScript |
|---|---|---|
| Key | event.key | event.key |
| New value | event.data | event.data |
| Previous value | event.previous_data / event.old_value | -- |
| Scope | event.scope | event.scope |
| Scope ID | event.scope_id | event.scopeId |
| Timestamp | event.timestamp | event.timestamp |
| Source agent | -- | event.agentId |
| Action | event.action | -- |
Go Alternative
Go does not support in-process memory events. Use the REST SSE endpoint instead:
GET /api/v1/memory/events/sse
GET /api/v1/memory/events/ws
These stream memory change events over Server-Sent Events or WebSocket for any client, including Go agents.
For simple cases, a Go agent can also poll the current value directly with a.Memory().Get(ctx, key) (or a scoped variant such as a.Memory().GlobalScope().Get(ctx, key)) on an interval instead of subscribing to events.