Characters
Seats, perception, action, brains, and memory. You own the inside; the world owns the outside.
Seats
Take over a character the world already has, or add one. Ownership is a right of override, not a transfer of duty: whatever you leave unset keeps running on the world's own defaults.
from terrarium import Terrarium, hosted_connector
with Terrarium() as client:
world = client.worlds.create(scenario="grocery")
for available in client.connectors.list():
print(available.model, available.api_modes)
connector = hosted_connector("gpt-5.6-sol", "openai.responses.v1")
brain = {"connector": connector}
# Take over a character the world already has.
derek = world.characters.get("Derek")
derek.configure(brain=brain)
# Or add a new character.
visitor = world.characters.add(
name="Visitor",
persona="A traveling knife-sharpener passing through town.",
spawn="Town Square",
brain=brain,
)Only name is required on add. Every other argument is an override of the world's default.
Perceive and act
The attempt is yours; the outcome is physics'. perceive() returns the scene as the character sees it. act(action) stages their next attempt in their own words. It substitutes for the decision they would have made on the next tick; physics still decides what actually becomes true.
from terrarium import Terrarium
with Terrarium() as client:
world = client.worlds.create(scenario="grocery")
derek = world.characters.get("Derek")
scene = derek.perceive()
print(scene.clock, scene.narration)
print(scene.text)
derek.act("count the register and lock the back door")
world.step()Brain
brain chooses how a character thinks. Its current fields are:
connector, a model and API mode fromhosted_connector()or a reference to your own endpointreasoning_efforttool_choice
client.connectors.list() returns each model this organization may use without supplying a key and the API modes it accepts. Choose an exact pair from that response. The example uses the deployment's gpt-5.6-sol route with openai.responses.v1.
A connector for your own OpenAI-compatible endpoint includes base_url. Its credential travels separately as a ModelKey in worlds.create(model_keys=[...]). The server seals that key under the world; the definition never contains it.
import os
from terrarium import ModelKey, Terrarium
endpoint = "https://models.example.com/v1"
model = "qwen3.8-27b-nvfp4"
brain = {"connector": {
"model": model,
"base_url": endpoint,
"api_mode": "openai.responses.v1",
}}
definition = {
"schema_version": 2,
"name": "arena",
"locations": [{"name": "Town Square"}],
"agents": [{"name": "Visitor", "location": "Town Square", "brain": brain}],
}
key = ModelKey(endpoint, os.environ["MODEL_API_KEY"], model)
with Terrarium() as client:
world = client.worlds.create(definition=definition, model_keys=[key])Memory
The world compacts a character's memory every night. Turn that off and the history is yours to read and rewrite.
from terrarium import Terrarium
with Terrarium() as client:
world = client.worlds.create(scenario="grocery")
derek = world.characters.get("Derek")
derek.configure(nightly_reflection=False)
messages = list(derek.history())
last_twenty = [
{"role": message.role, "content": message.content}
for message in messages[-20:]
]
derek.set_history(last_twenty)
derek.configure(sysprompt="You are Derek. You do not forget a debt.")
derek.configure(reflect="At bedtime, keep only what you learned about the delivery and any promises you made.")
print(derek.usage()) # tokens and calls spent on this characterset_history() replaces the active message history with exactly the messages supplied. The example keeps the last 20 entries in their original order and content. Persona, system prompt, notes, and world state are separate. derek.history(archived=True) includes what the world compacted away before you took over.
Boundary
The claimant owns their character's inside. The world owns their outside.