Skip to main content

A world-simulation engine for LLM-driven agents: event-sourced core, behavior-tree scheduling, memory and relationships, packaged as portable .cyberworld files

Project description

anima-world

A world-simulation engine for LLM-driven agents. Characters wake up, get hungry, go to work, gossip about each other, form cliques, spend money, remember what happened last week, and change how they feel about you. One SQLite file is one world.

License Python PyPI

English | 中文


What this is

Most "AI agent" frameworks give you a chatbot with tools. This gives you a place — one that keeps running whether or not anyone is watching, and remembers what happened while they were away.

A world is a tick-driven simulation. On every tick each character runs a behavior tree against its own needs and the state of the world, and acts. Those acts become events in an append-only log, which is the world's only source of truth — balances, relationships, and locations are all projections of that log, never separately stored rows that could drift out of sync.

The LLM sits beside the simulation, not inside it. It writes the narration, plans what a character does with free time, and judges how a conversation changed a relationship — all on background threads. The clock never blocks on a network call. Take the LLM away entirely and the world still runs; it just narrates in templates.

The engine is a library, not a service. There is no HTTP server and no port. Your application imports it, and the world lives inside your process:

your app  ──import anima_world.api──▶  world.db

See it run

$ pip install anima-world
$ anima-world start

  ANIMA 世界引擎
  ────────────────────────────────────────────

  ① LLM
     ! LLM 未配置 —— 叙事、空闲计划、关系判定都会降级成模板文本
       修复:anima-world config set llm.api_key sk-…

  ② 世界
     ✓ 新建 saves/world.db
     时钟:1 tick/秒 —— 约 5 分钟走完一个世界日(现实时间的 300 倍速)
     ✓ 3 个角色就位: 苏晚夏、陆知遥、沈亦柔

  ③ 运行
     世界在本进程里运行,叙事会打印在下面;停止:Ctrl-C

  [第0天 00:10] 遥:遥 wandered around
  [第0天 00:10] 夏:夏 went to sleep
  [第0天 00:35] 遥:遥 went to sleep
  ^C
  世界已停下,快照已保存。下次接着跑:anima-world start

start configures the LLM, creates the world, and runs it — in that order, with no documentation required first. Without an API key everything still works; narration is templated and characters have no plans. That degradation is deliberate, and it is never silent (anima-world doctor will tell you, and World.state() carries the reason).

Heads up on language: the engine speaks Chinese. CLI output, narration prompts, the built-in seed world, and the reference docs are all in Chinese. The API itself is English (World.open, world.tick, world.chat), so embedding it in an English-language application works fine — you supply your own seed and prompts.

Install

pip install anima-world          # Python 3.11+

Three runtime dependencies, chosen to stay out of your way since they land in every host that embeds a world: cryptography (encrypts the API key at rest), openai (any OpenAI-compatible endpoint), httpx.

From source:

git clone https://github.com/aubrey-anima/core.git anima-world
cd anima-world && pip install -e ".[dev]" && python -m pytest

Use it in your app

This is the main interface. World is a plain object with a lifetime — open it, drive it, close it.

from anima_world.api import World

with World.open("saves/world.db") as world:
    world.start_clock()                        # background clock; or drive it yourself
    print(world.state()["world_time"])         # {'day': 0, 'hour': 6, 'minute': 25, ...}

    # Talk to a character on a player's behalf. Streams; your app owns the transcript.
    for chunk in world.chat("夏", [{"role": "user", "content": "你好"}],
                            player_id="p1", display_name="阿宇"):
        print(chunk, end="")

    # Commit a finished exchange: summary + one world event + a relationship verdict
    world.record_chat_turn("夏", "p1", [
        {"role": "user", "content": "你好"},
        {"role": "assistant", "content": "你好呀"},
    ])

    world.player_move("p1", "cafe")
    world.config_set("scheduler.tick_rate", 1.0)

For batch work, drive the clock yourself — world.tick(n) is synchronous and deterministic, which is what makes fast-forwarding and testing possible:

with World.open("w.db") as world:
    world.tick(288)                     # one world day
    print(world.memories("夏"))         # what she remembers, ranked
    print(world.needs("夏"))            # {'energy': 0.59, 'hunger': 0.16, ...}
    print(world.cliques())              # who has clustered together

What a world contains

Four subsystems, each an opt-in flag except memory. They are off by default because a world that only walks around and talks is a legitimate world, and every mechanism you enable is more LLM spend and more surface to reason about.

Subsystem Flag What it adds
Memory always on Retrieval scored on relevance × recency × importance, periodic reflection that writes higher-order memories, and a forgetting curve
Needs needs.enabled energy / hunger / social decay per tick and drive an urgency band in the behavior tree — a tired character stops what it is doing and goes to sleep
Economy economy.enabled Items, money, shops, wages, and price drift. The ledger is a projection of payment events, so balances cannot be forged
Social social.enabled Three-axis relationships (always on) plus gossip that propagates second-hand with confidence decay, and emergent cliques
anima-world config set needs.enabled true --db-path saves/world.db

How it is built

One file is one world. world.db holds events, chats, memories, and config. world.db.key sits next to it and encrypts the API key — it must travel with the database, or the key becomes unreadable and the world silently drops to Mock. (Not actually silently: the engine says so at boot and keeps saying so in state().)

The event log is the only truth. There is no balances table, because two sources of truth eventually disagree and you cannot tell which one is right. world.db does not store "夏 has 50 coins" — it stores why she has 50 coins. Reconciliation is replay.

One running world owns its file. Half the truth lives in memory (clock, projection, locks, thread pools), so a second process writing the same world.db forks it immediately. Offline work — packaging, fast-forwarding — happens after the world closes.

Version is contract. One release freezes (engine code, db format, package format) together. The major version is the db format version, and db.py enforces it at runtime: mount a database from an incompatible format and it refuses on the spot rather than quietly writing garbage.

Ship a world to someone else

Worlds package into a single .cyberworld file — either a template (just the seed, the world builds itself on first boot) or a snapshot (a database that has already lived).

anima-world world export --seed seed.json --output my.cyberworld \
    --world-id my-world --name "我的世界" --mode template

anima-world world import my.cyberworld --destination ./instances

Commands

anima-world start          # create + run, with guided setup — start here
anima-world doctor         # health check: files, keys, a real LLM call, clock speed
anima-world config         # read/write settings, secrets encrypted and masked
anima-world run            # foreground host, no onboarding (for deployment)
anima-world simulate       # headless fast-forward
anima-world world          # export / import .cyberworld packages

Documentation

docs/REFERENCE.md Every command, every World method, every config key, the beat-script format 🇨🇳
docs/ARCHITECTURE.md Why it is shaped this way: the truth model, the tick frame, threads and locks, invariants, known debt 🇨🇳
CONTRIBUTING.md Development setup, the invariants a patch must not break, how to propose changes
CHANGELOG.md Release history

Contributing

Issues and pull requests are welcome. Start with CONTRIBUTING.md — it lists the handful of invariants that a change must not break (there is exactly one lock in the system, the LLM is never called on the tick thread, and a few file formats are mirrored by other repositories).

License

Apache License 2.0. Use it, modify it, ship it inside closed-source products; keep the copyright notice and state what you changed. Apache rather than MIT for the patent grant, and permissive rather than copyleft because an engine that hosts embed cannot be copyleft without infecting every host that embeds it.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

anima_world-1.0.0.tar.gz (154.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

anima_world-1.0.0-py3-none-any.whl (166.8 kB view details)

Uploaded Python 3

File details

Details for the file anima_world-1.0.0.tar.gz.

File metadata

  • Download URL: anima_world-1.0.0.tar.gz
  • Upload date:
  • Size: 154.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for anima_world-1.0.0.tar.gz
Algorithm Hash digest
SHA256 4e62b1ce22db4e88e67d51e1ddb1014a8aadc6605da61966ebadaa73cc702089
MD5 33b256b1fd253b20a130927af7554fe5
BLAKE2b-256 ac5cab3f820dbdef18e004f2c39c16a58098ec107c72702912e7bff38f7e90ae

See more details on using hashes here.

File details

Details for the file anima_world-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: anima_world-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 166.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for anima_world-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5f1039f3cbd72c552531de85d8ae92a04fd6a2f80a5007c041b37b62df8e1caa
MD5 cb32fc8706bf24a6abcda9e67381d16c
BLAKE2b-256 b2d0059b00bf03b5a9f29233704990f5a20f7537ab82060aa1ee0c9ddcb03d46

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page