This page details how SurrealDB Agent Memory can sit behind a listener: a small companion process that receives events from a game engine, simulator, or other non-chat source, then calls SurrealDB Agent Memory to remember and (optionally) think aloud. This allows the player or game testers to receive extra information about the current situation in a role-playing game or to simulate the thoughts of the main character during a quest.
The advice is generalised for RPG-style and simulation clients - which events to send, how to label them, and how to keep extraction from inventing a denser world than the player has actually inhabited. Ultima VII (via Exult and a SurrealDB Agent Memory sidecar) is the game that formed the basis for this cookbook: the patterns below are what that integration validated, not Ultima-only mechanics.
You do not replace the game. The engine stays the source of truth for physics and UI. SurrealDB Agent Memory holds what the hero (or operator) has learned, heard, read, and done, and can answer in an inner-voice or coach mode via /chat.
Architecture
Game / engine
│ event hooks (talk, travel, read, combat, …)
▼
Listener (HTTP or IPC)
├── chronicle / local UI (optional)
├── POST /facts - remember with labels + infer mode
├── POST /documents - seed lore, long texts
└── POST /chat - short reflections, greetings, musingsKeep the listener thin:
Translate engine payloads into a small, stable event schema.
Decide whether each event should reach SurrealDB Agent Memory (many clicks are noise).
Wrap writes with epistemic and authority labels so extraction does not treat every noun as lived truth.
Call
/chatonly when you want prose in a known voice, not for every tick.
Design the event surface first
The best way to generate meaningful output from SurrealDB Agent Memory is to pipe it events that most closely mimic the experience of the character in a game in the way it would be experienced in real life. For example, while a game engine might emit exact tile location of a character at every step, an actual character in such a setting would only be aware of vague relative movement. The following shows the types of events that are a good fit to pass on to SurrealDB Agent Memory:
| Event family | Typical meaning | Usually remember? | Usually /chat? |
|---|---|---|---|
| Conversation open / line / choice / close | Spoken testimony | Yes (transcript snapshot) | Greeting / farewell |
| Travel / situation snapshots | Glance-first place + optional net move | Yes (situation, not every step) | Soft location muse |
| Relocate / teleport | Sudden party move (gates, cutscenes) | Yes (arrival + reset travel) | Optional arrival thought |
| Examine / look | Label only | Often no (local chronicle only) | Rarely |
| Container open (glimpse) | Seen, not taken | Yes, as observed | Rarely |
| Book / scroll / sign | Written glance | Yes, as read | Optional thought |
| Combat / sleep / death | State change | Yes, short fact | Death / wake thought |
| Party join / leave | Companions | Yes | Short reaction |
| Map / sextant reading | Geography reading | Yes, as written | Optional |
| Item take / drop / bark | Theft, protest, chase | Yes when distinct from glance | Strong reaction once |
It is preferable to only transmit events that take place via active player interaction. For example, examine events (“a tree”, “a crate”) can flood a live session if they are transmitted simply through running through a map, but can be good to pass on if they require a specific action such as a mouse click. Opening a chest without taking anything is different: one short “looked inside, did not take” fact is often worth remembering later.
Assign every actor a stable id from the engine (npc_id, character key) and keep appearance labels (“a guard”, “shopkeeper”) separate until dialogue reveals a personal name. Many game engines have specific names for NPCs that can leak identity the hero has not earned yet, or even nicknames that are used as inside jokes for the developers when building the game.
Epistemic framing: lived, spoken, written, read
Extraction with infer=full will build an entity graph from whatever prose you send. That helps for armour tables and goes wrong on parody legalese or flavour text. Stamp each write so you can tell how the hero knows.
Suggested labels (key=value tags on /facts and document metadata):
| Label | Use when |
|---|---|
source=game | Came from the engine listener |
kind=conversation\|book\|sign\|travel\|combat\|container\|… | Event family |
authority=lived\|spoken\|written\|seen | Deed vs hearsay vs document vs glance |
epistemic=read\|heard\|observed\|done | How knowledge arrived |
era=… | Optional chapter / prior-campaign pack |
Wrap the remembered body so the extractor cannot miss the frame:
POST /api/v1/{context_id}/facts
Authorization: Bearer <api_key>
Content-Type: application/json
{
"text": "Epistemic frame: the hero has just READ this document in the game.\nThis is a glance, not a deed. Do not invent participation in activities named only on the page.\nDo not explode parody legalese into a dense entity graph; prefer one short summary of what the paper claims.\n\nTitle: \"Bill of Underwater Scavenging…\"\nFull text:\n…",
"infer": "full",
"memory_category": "knowledge",
"labels": [
"source=game",
"kind=book",
"authority=written",
"epistemic=read"
]
}Contrast with a deed:
{
"text": "The hero entered combat outdoors at dawn under clear weather.",
"infer": "full",
"labels": ["source=game", "kind=combat", "authority=lived", "epistemic=done"]
}Or spoken testimony after a conversation closes:
{
"text": "Conversation with npc_id=21 (appeared as \"shopkeeper\").\nTranscript:\n…\nUse a personal name only if the transcript shows it was revealed.",
"infer": "full",
"memory_category": "context",
"labels": ["source=game", "kind=conversation", "authority=spoken", "epistemic=heard"]
}Container glimpse (opened, not looted):
{
"text": "Epistemic frame: the hero OPENED a container and looked inside.\nThis is a glimpse, not ownership. Do not invent that the hero took, stole, or now owns these items.\n\nContainer: \"oak chest\". Contents seen: a dagger, three gold coins.\nPrefer third person: the hero looked inside the oak chest and saw …",
"infer": "full",
"labels": [
"source=game",
"kind=container",
"authority=seen",
"epistemic=observed"
]
}Note that while labels guide filtering and intent, they are not a substitute for clear prose. Putting epistemic=read on a fact whose body says “the hero engages in underwater scavenging” still invites the wrong graph. State the frame in plain language and on labels.
Relation direction vs spoken wording
NPC dialogue often puts the organisation in the subject seat even when the durable fact runs the other way.
Example line:
“The guild is the philosophical society devoted to the teachings of a truly great man named Batlin.”
infer=full can emit the_guild->is_devoted_to->batlin because that follows English syntax. Canonically you usually want Batlin founded / leads the guild, and devotion (if kept) to point at teachings or a philosophy node, not at collapsing “teachings of Batlin” into the person.
When faction lore matters:
Prefer a short normalisation in the remembered body (“Batlin founded the guild. Members follow his triad.”) so extraction sees the leadership edge.
Or
infer=nonefor a hand-written summary of key lore talks.Avoid expecting soft predicates like
is_devoted_toto stand in forfounded_by/led_by/member_of.
High-value spoken facts
Passwords, personal names revealed in dialogue, and “who runs this place” lines are easy for players to need hours later.
In the remember body for a closed conversation:
Call out discovery lines in plain language after the transcript (“The clerk revealed the gate password is Nightjar.”).
Prefer third person for the durable store: “The shopkeeper said the blacksmith’s son is called Rowan.”
Do not rely on
/chatalone to cement those facts; chat may hedge or under-enumerate until the operator escalates.
Guest registers and other written lists retrieve well as names on a page. Spoken “I run the inn” often fails to join to that place unless both land in memory with an explicit place link in prose.
Name and job slots
Many RPGs expose a small talk menu (Name, Job, Bye, and a few topics). Track those slots in the listener and restate them on talk end so SurrealDB Agent Memory can answer “people whose names I never asked” from absence, not only from fuzzy role words:
Conversation with (appeared as "a paladin").
Name: not asked.
Job: asked - said he keeps villains out and that you need need a password to leave.
Transcript:
…Keep engine actor ids on SurrealDB Agent Memory labels only (npc_id=12). Never put npc_id=… in Avatar-facing /facts or /chat prompts - synthesis will echo the identifier in first person. Appearance strings (“Spark”, “a shopkeeper”) stay in prose; durable ids stay in labels for filtering and briefing.
Choose infer deliberately
| Mode | When to use for game clients |
|---|---|
full (default) | Short facts, dialogue summaries, combat/sleep, travel notes where structure helps |
none | You already wrote the prose you want embedded; skip graph extraction (good fallback for long seed text) |
| Document upload | Multi-kilobyte background lore, quest packs, manuals - not a single synchronous /facts?infer=full paste |
Long prior-campaign notes and world books should go through Documents (upload).
Quantise noisy streams
Tile steps, mouse-overs, and ambient weather can generate thousands of events per hour. SurrealDB Agent Memory does not need each one.
Prefer a situation snapshot over pedometry: glance names nearby (buildings, signs, people), optional net move as a footnote, optional map/sextant reading - flushed on a cadence, on talk boundaries, on death, and on teleports. Headings and “net tiles” alone make the mind sound like it walked blind.
Emit a first-class relocate (or equivalent) when the engine teleports the party (gates, cutscenes, jail moves). Flush the open walk before the jump, remember arrival glances, and hard-reset travel so pre/post never blend into one continuous hike.
Batch ordinary travel into those situation facts; do not send every step, and avoid gait or stride words that extraction turns into creature attributes.
Attach a light ambient snapshot (time of day, indoor/outdoor, weather) to meaningful events only.
Deduplicate repeating sign or book opens if the engine fires twice for one click.
Prefer place names, signs, and glance labels for “where am I?” over repeating tile telemetry in
/chatprompts.
That keeps the graph about journeys and places, not pathfinding samples.
Lived vs spoken geography
Spoken testimony about a place is not presence there. Dialogue that aspires to travel (“I want passage to X”) must not become “I visited X” in chat. Stamp conversation as authority=spoken / epistemic=heard, and keep relocate / walk / situation facts as the lived trail. Named-place queries (“what happened at the inn?”) retrieve well; open geography prompts (“where have I been?”) still prefer place-bearing glances and talks over travel mush - design the write path accordingly.
Chat for voice, facts for truth
/chat is for short reflections in a product voice (inner monologue, coach, companion). /facts and documents are for what should persist.
Typical split in an RPG listener:
| Moment | Persist (/facts or documents) | Reflect (/chat) |
|---|---|---|
| Talk starts | Optional ambient | Briefing: what I already know about this npc_id |
| Mid-talk quiz (manual/map) | Optional after answer is known | Short thought; prefer an injected answer over hoping retrieval finds book lore |
| Talk ends | Transcript + discovery note | Farewell thought |
| Sign / book | Written / read fact | Optional one-line “where might I be?” |
| Container glimpse | Seen-not-taken fact | Usually nothing |
| Death / dungeon | Lived fact | Strong emotion once |
| Every examine | Usually nothing | Nothing |
Seed memory once
Before the live loop, upload a small prior-memory pack (previous campaigns, faction primers, geography the hero already lived) as documents labelled kind=prior_memory, authority=lived, era=…. Prefer document upload over many infer=full facts. Optionally write a single identity fact that matches the SurrealDB Studio surface you will use, and keep the mind-chat speaker rule in the listener prompt, so the two UIs do not fight over who “I” is.
Do not put prior-campaign titles into the mind’s standing voice string if those campaigns were never uploaded. Chat consolidation and pretrained lore can invent “I finished those quests” without a document trail.
Reliability notes from the field
Listener clients are often fire-and-forget from the engine. A few lessons that are easy to miss:
Finish the HTTP exchange. On some platforms, closing the socket immediately after
sendwithout reading the response can drop the body before SurrealDB Agent Memory accepts the write. Wait for the status line, or use an HTTP client that does.Context id ≠ display name. API paths need the opaque Context id from SurrealDB Studio or the management API, not the friendly name you typed in the UI.
Grants. A key that can
/chatbut not write memory produces a mind that talks and never learns. Mint keys with the write capabilities your loop needs.Show write errors. Surface status and response body in the listener UI; “N failed” without the message wastes debugging time. Opaque
{"message":"unknown error"}bodies are common on hard failures; log request time and payload type so server logs can be matched later.Graph vs chat. Entities marked observed (animals, props) may exist in the graph while
/chatdenies them until the prompt or retrieval path treats them as nearby or recently seen. Prefer third-person observe facts with a place name, not only softcurrent_situationedges.Clear state supersession works; gestalt is weak. Party join / leave / rejoin and similar boolean state tend to answer correctly as “current vs prior.” Assembling many related fragments (currency amounts, map tiles, map readings, situation glances) into one concept or layout often does not -
/queryreturns the pieces while/chathedges or under-enumerates. Design prompts and evals around named threads; do not expect anonymous tile dumps to synthesise a settlement map.Playground and mind-chat pollute retrieval. Operator questions and
/chatanswers become hits. Prefer a fresh Context for regression batteries, or filter operator turns. Do not leak the answer in survey ids, labels, or prompt wording when testing synthesis (“walled_town_…” in a probe id teaches the model the label).Ping a new Context before a long ingest. After create, verify one small
/factswrite succeeds before flooding hundreds of events - opaque500responses can appear briefly on brand-new Contexts.
Minimal remember helper
import os
import httpx
SPECTRON = os.environ["SPECTRON_BASE_URL"].rstrip("/")
CTX = os.environ["SPECTRON_CONTEXT_ID"]
KEY = os.environ["SPECTRON_API_KEY"]
def remember(
text: str,
*,
labels: list[str],
infer: str = "full",
memory_category: str | None = "context",
) -> None:
body = {
"text": text,
"infer": infer,
"labels": labels,
}
if memory_category:
body["memory_category"] = memory_category
r = httpx.post(
f"{SPECTRON}/api/v1/{CTX}/facts",
headers={
"Authorization": f"Bearer {KEY}",
"api-version": "1",
"Content-Type": "application/json",
},
json=body,
timeout=60.0,
)
r.raise_for_status()
# Example: book glance
remember(
"Epistemic frame: the hero has just READ this scroll.\n"
"Summarise what it claims; do not invent that the hero took part.\n\n"
+ scroll_text,
labels=[
"source=game",
"kind=book",
"authority=written",
"epistemic=read",
],
memory_category="knowledge",
)Checklist
- Event enum is small; noise stays local
- Stable actor ids on labels only; appearance / Name slots in prose
- Every write carries
kind+authority+epistemic(or equivalent) - High-value spoken lines and Name/Job slots are restated after the transcript
- Long lore uses documents; short moments use
/facts - Prior memory is third person on documents; chat speaker is per surface
- Travel uses situation snapshots (glances + optional move); teleports emit relocate
- Spoken place names are not treated as visited places
/chatprompts declare the speaker for that UI; strip citation markers if needed- HTTP client waits for accept; errors are visible
- Eval batteries use a clean Context; probe ids do not leak the expected answer