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] 遥:遥四处走了走
  [第0天 00:10] 夏:夏睡下了
  [第0天 00:35] 遥:遥睡下了
  ^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, the built-in seed world, and the reference docs are all in Chinese. The API itself is English (World.open, world.tick, world.chat), and every piece of text a world says — worldview, prompts, and the no-key narration templates — comes from the seed, not from the engine. So an English world is a matter of supplying an English seed.

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

A package says what it needs without your having to be able to run it — the launcher managing several engine versions is exactly the caller who cannot yet:

anima-world world inspect my.cyberworld --json
# {"world_id": "my-world", "engine_min": "2.0.0", …, "current_engine_version": "1.1.0",
#  "runnable": false}          # answers, exit 0 — refusing here would defeat the format

Commands

anima-world start          # create + run, with guided setup — start here
anima-world chat           # talk to a character; no --agent lists who lives there
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 (--report writes a run summary)
anima-world world          # export / import / inspect .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.1.0.tar.gz (176.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.1.0-py3-none-any.whl (189.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: anima_world-1.1.0.tar.gz
  • Upload date:
  • Size: 176.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for anima_world-1.1.0.tar.gz
Algorithm Hash digest
SHA256 d9e85771fc1457f774e84f99aa3a978a5738b99fac1e05700c3f03ebed028985
MD5 d95f23d54240be65b55e55fbb4e70d22
BLAKE2b-256 3a34ce662a360255b421a908b330b77cb137017793afa2a2b416730664e2c686

See more details on using hashes here.

Provenance

The following attestation bundles were made for anima_world-1.1.0.tar.gz:

Publisher: release.yml on aubrey-anima/core

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

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

File metadata

  • Download URL: anima_world-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 189.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for anima_world-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 01669ff81985810a69f4415dcc40e002d9719db71ccd8bb91a455f611acc9701
MD5 646ebc496ce2d6d9a667823776b65c9c
BLAKE2b-256 f51a04287753c4ff58fa215551724d4360ca23e600ab9fb55df78ec08d7cbd3f

See more details on using hashes here.

Provenance

The following attestation bundles were made for anima_world-1.1.0-py3-none-any.whl:

Publisher: release.yml on aubrey-anima/core

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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