{"id":49580522,"url":"https://github.com/francemazzi/worldsim","last_synced_at":"2026-05-03T19:07:53.060Z","repository":{"id":348699520,"uuid":"1198971697","full_name":"francemazzi/worldsim","owner":"francemazzi","description":"Abstract virtual world emulator with LangGraph agents. A stateless, plugin-based multi-agent simulation engine for Node.js/TypeScript.","archived":false,"fork":false,"pushed_at":"2026-04-19T13:55:31.000Z","size":4945,"stargazers_count":7,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-19T15:37:04.169Z","etag":null,"topics":["agent-based-framework","agent-based-simulation","ai-societies","langchain","langraph","llm-agents","typescript"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/worldsim","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/francemazzi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-02T00:04:53.000Z","updated_at":"2026-04-19T13:55:34.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/francemazzi/worldsim","commit_stats":null,"previous_names":["francemazzi/worldsim"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/francemazzi/worldsim","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/francemazzi%2Fworldsim","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/francemazzi%2Fworldsim/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/francemazzi%2Fworldsim/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/francemazzi%2Fworldsim/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/francemazzi","download_url":"https://codeload.github.com/francemazzi/worldsim/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/francemazzi%2Fworldsim/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32581124,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T06:36:36.687Z","status":"ssl_error","status_checked_at":"2026-05-03T06:36:09.306Z","response_time":103,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-based-framework","agent-based-simulation","ai-societies","langchain","langraph","llm-agents","typescript"],"created_at":"2026-05-03T19:07:46.316Z","updated_at":"2026-05-03T19:07:53.054Z","avatar_url":"https://github.com/francemazzi.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"https://raw.githubusercontent.com/francemazzi/worldsim/main/docs/worldsim_img.webp\" alt=\"WorldSim\" width=\"100%\" style=\"border-radius: 16px; display: block; max-width: 100%;\" /\u003e\n\n# worldsim\n\n[![GitHub stars](https://img.shields.io/github/stars/francemazzi/worldsim?style=social)](https://github.com/francemazzi/worldsim)\n\n**Simulate how communities react to new rules, events, or policies — in TypeScript, in 5 minutes.**\n\nWorldSim is an embeddable multi-agent simulation engine for Node.js. Define agents with distinct personalities, drop in a policy change, and watch coalitions form, conflicts emerge, and consensus build — all powered by LLM reasoning loops.\n\n## Quick Start\n\n```bash\nnpm install worldsim\nOPENAI_API_KEY=sk-... npx worldsim demo\n# Open http://localhost:4400 — watch a village react to water rationing\n```\n\nOr with Docker:\n\n```bash\nOPENAI_API_KEY=sk-... docker compose up\n# Open http://localhost:4400\n```\n\n## What You Can Simulate\n\n**Community Policy Impact** — 8 villagers face a new water rationing policy. The farmer resists, the mayor defends, the priest mediates, the technologist proposes solutions. Who forms coalitions? Who complies?\n\n**Market Price Shocks** — 10 marketplace agents react when grain prices double overnight. Sellers profit, buyers protest, regulators intervene. Economic reasoning emerges from personality-driven agents.\n\n**Information Cascades** — 12 agents in 4 social groups. A rumor starts with one person. Watch it spread (or not) through the social graph, distorted by each personality along the way.\n\nSee [`evaluation/`](evaluation/) for repeatable scenarios with expected behaviors and quality criteria.\n\n## Code Example\n\n```typescript\nimport { WorldEngine, ConsoleLoggerPlugin, InMemoryMemoryStore, InMemoryGraphStore } from \"worldsim\";\n\nconst world = new WorldEngine({\n  worldId: \"my-village\",\n  maxTicks: 20,\n  llm: {\n    baseURL: \"https://api.openai.com/v1\",\n    apiKey: process.env.OPENAI_API_KEY!,\n    model: \"gpt-4o-mini\",\n  },\n  memoryStore: new InMemoryMemoryStore(),\n  graphStore: new InMemoryGraphStore(),\n});\n\nworld.use(ConsoleLoggerPlugin);\n\nworld.addAgent({\n  id: \"maria\", role: \"person\", name: \"Maria Rossi\",\n  iterationsPerTick: 2,\n  profile: { name: \"Maria Rossi\", personality: [\"practical\", \"stubborn\"], goals: [\"Save the harvest\"] },\n  systemPrompt: \"You are Maria, a farmer worried about water rationing.\",\n});\n\n// Add more agents...\n\nawait world.start();\n```\n\n## Studio Dashboard\n\nWorldSim includes a built-in web dashboard for real-time simulation monitoring:\n\n```bash\nnpx worldsim studio\n# Open http://localhost:4400\n# Optional: --port 5000, --no-open\n```\n\n- **Live agent state** — mood, energy, goals, status\n- **Event timeline** — every action, every tick\n- **Relationship graph** — force-directed visualization of social connections\n- **Simulation report** — mood heatmaps, energy charts, action distribution, timeline\n- **Multi-world operations** — monitor and compare multiple world runs (city/country shards)\n\nMain sections available in the dashboard:\n\n- **Agents** — inspect profile, current status, goals, mood and energy in real time\n- **Timeline** — follow what happens at each tick, with a chronological event stream\n- **Relationship Graph** — visualize who influences whom and how social ties evolve\n- **Report** — review post-run metrics, trends, and behavior distribution\n\n```typescript\nimport { studioPlugin } from \"worldsim\";\n\nengine.use(studioPlugin({ engine, port: 4400, memoryStore, graphStore }));\n```\n\n### UI Examples\n\nRelationship Graph view (real-time social connection map):\n\n![Relationship Graph](https://raw.githubusercontent.com/francemazzi/worldsim/main/docs/relationships.png)\n\nAgent Details view (profile, internal state, and memory timeline):\n\n![Agent Details](https://raw.githubusercontent.com/francemazzi/worldsim/main/docs/agent_details.png)\n\n## Architecture\n\n```mermaid\nflowchart LR\n  WorldEngine --\u003e RulesLoader\n  WorldEngine --\u003e PluginRegistry\n  WorldEngine --\u003e PersonAgent\n  WorldEngine --\u003e ControlAgent\n  PersonAgent --\u003e LLM\n  ControlAgent --\u003e LLM\n  PersonAgent -.-\u003e MemoryStore\n  PersonAgent -.-\u003e GraphStore\n```\n\n- **WorldEngine** orchestrates ticks, agents, plugins, and lifecycle\n- **PersonAgent** — LangGraph-powered agents with personality, mood, energy, goals, and tool use\n- **ControlAgent** — governance agent that monitors rules and can pause/stop violators\n- **Plugin system** — hooks on every world event + registerable tools for agents\n- **Rules engine** — load from JSON or PDF, with priorities and enforcement levels\n\n## How Simulation Time Works — Ticks\n\nWorldSim is a **discrete-time simulation**: time does not flow continuously, it advances in integer steps called **ticks**. There is no internal notion of \"seconds\" or \"minutes\" — a tick is simply a logical unit of simulated time, and _you_ decide what it represents in your scenario (one minute, one hour, one day, one \"turn\"…).\n\n### The tick loop\n\nOnce you call `world.start()`, the engine enters a loop that keeps running until `maxTicks` is reached or `stop()` is called. Every iteration:\n\n1. Increments the world clock (`tick = tick + 1`).\n2. Runs one full tick (see pipeline below).\n3. Optionally sleeps for `tickIntervalMs` before starting the next one.\n\nTwo knobs in `WorldConfig` control the rhythm:\n\n| Option | Meaning | Typical use |\n| --- | --- | --- |\n| `maxTicks` | How long the simulation runs (default `Infinity`) | `30` for the Villaggio del Sole demo |\n| `tickIntervalMs` | Real-time pause between ticks (default `0`) | `2000` in the demo to let a human follow along in the dashboard; `0` in tests/benchmarks to run as fast as possible |\n\n`tickIntervalMs` is **only** wall-clock pacing — it doesn't change what happens inside the simulation. Setting it to `0` just makes the same 30-tick scenario finish faster.\n\n### What happens inside a single tick\n\nEach tick executes a deterministic pipeline (see [`TickOrchestrator.executeTick`](src/engine/internal/TickOrchestrator.ts)):\n\n1. **Clock increment** → `tick`, `context.tickCount`, `messageBus.newTick(tick)`.\n2. **`onWorldTick` hook** fires on every registered plugin, then any handler attached via `world.on(\"tick\", …)` runs.\n3. **Per-tick resets** — token budget counters, stale conversations cleanup, neighborhood cache.\n4. **Active agent selection** — which `PersonAgent`s will actually \"think\" this tick:\n   - Paused/stopped agents are skipped.\n   - Agents with pending messages always run (they have a stimulus to react to).\n   - `defaultActiveTickRatio` and per-agent schedules (via `ActivityScheduler`) downsample the rest, so you don't pay an LLM call for every agent on every tick.\n   - Remaining agents are sorted by number of pending messages (busiest first).\n5. **Parallel agent reasoning** — selected agents run their `tick()` through `BatchExecutor`, which enforces `maxConcurrentAgents`. Each agent can internally loop up to `iterationsPerTick` times (recall memory → build context → call LLM → execute tools → emit messages/actions).\n6. **Plugin action transforms** — collected `AgentAction`s flow through `onAgentAction` hooks, which can rewrite or annotate them.\n7. **Relationship decay** is batched across all active agents.\n8. **Control events applied** — pending lifecycle commands (`pause`, `resume`, `stop`) emitted during the tick take effect.\n9. **ControlAgent evaluation** — governance agents rule each action as `allowed`, `warned`, or `blocked`.\n10. **Action batch hooks + ControlAgent tick** — plugins see the final batch, governance agents run their own reasoning.\n\nEverything temporal in the world is expressed in ticks: memory consolidation windows, relationship strength decay, conversation idle timeout, per-tick token budgets, scheduled control events, and so on.\n\n### Concrete example — Villaggio del Sole\n\nThe `community-demo` scenario ([`examples/community-demo/`](examples/community-demo/)) runs 8 villagers + 1 governance agent for **30 ticks**, pacing at **2 seconds per tick**:\n\n```json\n{\n  \"name\": \"Villaggio del Sole — Razionamento Idrico\",\n  \"maxTicks\": 30,\n  \"tickIntervalMs\": 2000,\n  \"trigger\": { \"atTick\": 10, \"announcement\": \"Il sindaco annuncia il razionamento…\" }\n}\n```\n\nWith `iterationsPerTick: 2` on each villager, a single tick can contain up to two internal LLM reasoning steps per agent — enough to read incoming messages, check a tool (`check_weather`, `observe_environment`), decide how to react, and reply.\n\nThe scenario uses the tick counter as a **narrative timeline**:\n\n- **Ticks 1–9** — baseline life in the village. Paolo (the journalist) checks the weather forecast, Maria (the farmer) observes her well drying up, gossip starts spreading through Giuseppe's bakery.\n- **Tick 10** — the `on(\"tick\", …)` handler fires the policy trigger:\n\n  ```typescript\n  world.on(\"tick\", (tick) =\u003e {\n    if (tick === triggerTick \u0026\u0026 announcement) {\n      console.log(`POLICY TRIGGER — Tick ${tick}`);\n    }\n  });\n  ```\n\n  The water-rationing rules become active and the governance agent starts enforcing them.\n- **Ticks 11–30** — coalitions form, resistance emerges, Sara proposes rainwater harvesting, Padre Lorenzo mediates. Every action, every message, every mood change is stamped with the tick it happened on, which is exactly what the Studio timeline and final report replay.\n\n### Concrete example — lifecycle control\n\nThe `basic-world` example ([`examples/basic-world/index.ts`](examples/basic-world/index.ts)) shows ticks as **injection points** for host-driven events:\n\n```typescript\nworld.on(\"tick\", (tick) =\u003e {\n  if (tick === 5)  world.pauseAgent(\"person-2\", \"Fase di test\");\n  if (tick === 8)  world.resumeAgent(\"person-2\");\n  if (tick === 15) world.stopAgent(\"person-4\", \"Missione completata\");\n});\n```\n\nBecause the tick loop is the single clock of the simulation, scheduling \"at tick N do X\" is trivial — no cron, no timers, no race conditions. You can inject policy changes, simulated crises (a price shock, a rumor, a blackout) or agent lifecycle events deterministically at specific ticks, and the same scenario will replay identically if you fix the LLM seed.\n\n### Pause, resume, stop\n\n- `pause()` sets status to `paused` and the `while` loop exits cleanly; the clock freezes at the current tick.\n- `resume()` re-enters `runLoop()` from the same tick — no state is lost.\n- `stop()` ends the loop, fires the `onWorldStop` plugin hook with the full event log, and lets you collect the final report.\n\n### Choosing `maxTicks` and `tickIntervalMs`\n\n| Use case | `maxTicks` | `tickIntervalMs` | Notes |\n| --- | --- | --- | --- |\n| Live demo in the Studio dashboard | 20–50 | 1000–2000 | Human-watchable pace |\n| Automated evaluation / CI | 20–100 | `0` | Run at full speed |\n| Benchmarks | 100+ | `0` | Measure throughput |\n| Long-horizon emergent dynamics | 200+ | `0` or small | Combine with `defaultActiveTickRatio` \u003c 1 to keep LLM costs bounded |\n\nIf you're unsure, start with the community demo's numbers (`maxTicks: 30`, `tickIntervalMs: 2000`) and tune from there.\n\n## Phones, Calls \u0026 Movement\n\nAgents that own the right assets can text each other, place phone calls (their dialog is transcribed as a chat), and move around the world under rules you control.\n\n```typescript\nimport {\n  WorldEngine,\n  InMemoryAssetStore,\n  PhonePlugin,\n  MovementPlugin,\n  LocationIndex,\n  createPhoneAsset,\n  defaultMovementPolicy,\n} from \"worldsim\";\n\nconst assetStore = new InMemoryAssetStore();\nconst locationIndex = new LocationIndex();\n\nconst engine = new WorldEngine({\n  /* ...llm, stores... */\n  assetStore,\n  // Default policy allows walking within 1.5 km, requires a vehicle beyond.\n  walkingRadiusMeters: 1500,\n  // Or replace with your own rules (health data, public transit, licenses, …):\n  // movementPolicy: (req) =\u003e ({ allowed: req.distanceMeters \u003c 500, mode: \"walking\" }),\n});\n\nengine.use(new MovementPlugin(locationIndex));\nengine.use(\n  new PhonePlugin({\n    assetStore,\n    messageBus: engine.getMessageBus(),\n    conversationManager: engine.getConversationManager(),\n  }),\n);\n\n// Give Alice a phone and a car so she can text, call, and drive long distances.\nawait assetStore.addAssets([\n  createPhoneAsset({ agentId: \"alice\", phoneNumber: \"+39 111\" }),\n  { id: \"car-alice\", type: \"vehicle\", name: \"Panda\", owner: \"alice\", ownerType: \"agent\" },\n]);\n```\n\nOnce their phone is registered, agents automatically get four tools: `send_sms`, `start_call`, `speak_in_call`, `hang_up`. Call transcripts land on the bus as regular `Message`s with `type: \"call_transcript\"` and `metadata.callId`, so UIs and the reporting plugin can render them as chat turns.\n\nMovement is governed by a `MovementPolicy` — a pure function that receives `{ agentId, from, to, distanceMeters, assets, profile }` and returns `{ allowed, mode?, reason? }`. Swap `defaultMovementPolicy` for anything you need: public transit, HealthKit steps, weather, curfews. WorldSim stays agnostic.\n\n## Groups \u0026 Gatherings — Beyond Egocentric Agents\n\nOut of the box, each `PersonAgent` reasons in the first person. Calls, SMS, conversations at venues and bilateral relationships already let agents coordinate ad-hoc, but there was no structured concept of \"a group of N agents with a stable identity\" or \"a scheduled multi-agent event\". From `1.3.x` WorldSim ships two minimal, **unopinionated** core primitives to fill this gap:\n\n- **`Group`** — an arbitrary, explicitly-declared formal collection of agents with a stable `id`. Different from `Household` (cohabitation + shared assets) and `NeighborhoodManager` (geographic clustering + relationship decay). The installer gives a Group its meaning via the free-form `kind` field: `\"book-club\"`, `\"party-crew\"`, `\"protest-org\"`, `\"research-team\"`…\n- **`Gathering`** — a scheduled moment in time at an optional venue, with a participant list and RSVP state machine (`invited` → `accepted`/`declined` → `attended`/`no_show`). Lifecycle (`scheduled` → `in_progress` → `ended` / `cancelled`) is driven mechanically by `advanceLifecycle(tick)` — the core engine fires no opinionated events on its own.\n\nThe stores are plain interfaces with zero-dependency in-memory implementations. Wire them on `WorldConfig` just like `assetStore` or `graphStore`:\n\n```typescript\nimport {\n  WorldEngine,\n  InMemoryGroupStore,\n  InMemoryGatheringStore,\n} from \"worldsim\";\n\nconst groupStore = new InMemoryGroupStore();\nconst gatheringStore = new InMemoryGatheringStore();\n\nconst world = new WorldEngine({\n  worldId: \"my-village\",\n  llm: { /* … */ },\n  groupStore,\n  gatheringStore,\n});\n```\n\n**By design, WorldSim ships no built-in tools on top of these stores.** No `organize_party`, no `invite_to_gathering`, no `rsvp` in the core. You compose your own plugin — the vocabulary, the tool names, the side-effects (phone notifications, movement to the venue, mood boosts on attendance, capacity checks) are *your* simulation's story to tell. Minimal sketch:\n\n```typescript\nimport type { WorldSimPlugin, AgentTool } from \"worldsim\";\n\nfunction partyPlugin(opts: { groupStore, gatheringStore }): WorldSimPlugin {\n  const tools: AgentTool[] = [\n    {\n      name: \"invite_friends_to_bar\",\n      description: \"Organizza una serata al bar con amici\",\n      inputSchema: { /* friendIds, venueId, atTick */ },\n      async execute(input, ctx) {\n        const organizer = ctx.metadata?.currentAgentId as string;\n        await opts.groupStore.addGroup({\n          id: `grp_${crypto.randomUUID()}`,\n          kind: \"party-crew\",\n          members: [organizer, ...input.friendIds],\n          owner: organizer,\n          createdAtTick: ctx.tickCount,\n        });\n        await opts.gatheringStore.addGathering({\n          id: `gth_${crypto.randomUUID()}`,\n          kind: \"party\",\n          scheduledTick: input.atTick,\n          venueId: input.venueId,\n          organizer,\n          participants: [\n            { agentId: organizer, rsvp: \"accepted\" },\n            ...input.friendIds.map(id =\u003e ({ agentId: id, rsvp: \"invited\" })),\n          ],\n          status: \"scheduled\",\n          createdAtTick: ctx.tickCount,\n        });\n        return { ok: true };\n      },\n    },\n    // accept_invite, list_my_upcoming_gatherings, …\n  ];\n\n  return {\n    name: \"party-plugin\",\n    version: \"0.1.0\",\n    tools,\n    async onWorldTick(tick) {\n      const changed = await opts.gatheringStore.advanceLifecycle(tick);\n      for (const g of changed) {\n        // When a gathering flips to \"in_progress\", make accepted participants\n        // converge at the venue (delegate to MovementPlugin / PhonePlugin / …).\n      }\n    },\n  };\n}\n```\n\nThis way worldsim stays neutral and the *package installer* decides what a \"gathering\" means in their simulation — a wedding, a town assembly, a protest, a book club night, a birthday. Types available: `Group`, `Gathering`, `GatheringParticipant`, `RsvpState`, `GatheringStatus`, `GatheringQuery`, `GroupStore`, `GatheringStore`.\n\n## Creating Your Own Scenario\n\nA WorldSim scenario is just a folder with four ingredients. The simplest way to start is to copy [`evaluation/scenarios/water-rationing/`](evaluation/scenarios/water-rationing/) and adapt it.\n\n```\nmy-scenario/\n├── scenario.json          # agents + trigger + timing\n├── rules/\n│   ├── base-rules.json    # rules active from tick 1\n│   └── trigger-rules.json # rules loaded when the shock fires\n├── expected.md            # (optional) qualitative rubric for evaluation\n└── index.ts               # runner that wires engine + plugins\n```\n\n### 1. `scenario.json` — the \"script\"\n\nA declarative file with timing, the policy trigger, and the cast:\n\n| Field | Purpose |\n| --- | --- |\n| `name`, `description` | Human-readable identity of the run |\n| `maxTicks` | How long the simulation runs (e.g. `30`) |\n| `tickIntervalMs` | Wall-clock pause between ticks (`2000` for live demo, `0` for tests) |\n| `trigger.atTick` | When the disruptive event fires |\n| `trigger.addRules` | Relative paths of rule files to load at the trigger |\n| `trigger.announcement` | Broadcast text delivered to every agent |\n| `agents[]` | The list of actors |\n\nEach agent declares its identity and — crucially — its personality:\n\n```jsonc\n{\n  \"id\": \"maria\",\n  \"role\": \"person\",            // \"person\" | \"control\" (governance)\n  \"name\": \"Maria Rossi\",\n  \"iterationsPerTick\": 2,      // internal LLM reasoning steps per tick\n  \"systemPrompt\": \"Sei Maria, contadina di 52 anni, pratica e testarda…\",\n  \"profile\": {\n    \"age\": 52,\n    \"profession\": \"Contadina\",\n    \"personality\": [\"pratica\", \"testarda\", \"generosa\"],\n    \"goals\": [\"Salvare il raccolto\", \"Proteggere la famiglia\"],\n    \"backstory\": \"…\",\n    \"skills\": [\"farming\", \"cooking\"]\n  }\n}\n```\n\nThe `systemPrompt` is where simulation quality lives: the more specific it is about tone, values and internal conflicts, the longer the agent stays in character across the run.\n\n### 2. `rules/*.json` — the normative fabric\n\nRules are interpreted by the `RuleEngine` and enforced by the governance `ControlAgent`:\n\n```json\n{\n  \"version\": \"1.0\",\n  \"name\": \"Village rules\",\n  \"rules\": [\n    {\n      \"id\": \"rispetto\",\n      \"priority\": 1,\n      \"scope\": \"all\",\n      \"instruction\": \"All members must communicate respectfully. Insults are forbidden.\",\n      \"enforcement\": \"hard\"\n    }\n  ]\n}\n```\n\n- `scope` — `\"all\"` (everyone), `\"person\"` (only human agents), `\"control\"` (only governance agents).\n- `enforcement` — `\"hard\"` blocks the action, `\"soft\"` only warns.\n- `priority` — lower number = evaluated first.\n- `instruction` — free text passed to the ControlAgent as judgement context.\n\nThe convention across the existing scenarios is two files: a **base** rulebook loaded from tick 1 (e.g. `community-rules.json`) and a **trigger** rulebook loaded at `trigger.atTick` via `trigger.addRules` (e.g. `water-rationing.json`).\n\n### 3. `expected.md` — the qualitative rubric (optional)\n\nNot required to run the simulation, but essential when you want to *judge* its output. It lists, per agent, what the run should look like, the expected dynamics over time, and the failure modes that signal a broken scenario. Together with [`evaluation/criteria.md`](evaluation/criteria.md) it forms the rubric used to score simulation reports.\n\n### 4. `index.ts` — the runner\n\nThe runner wires the scenario into a `WorldEngine`, registers plugins and fires the trigger. A minimal template:\n\n```typescript\nimport {\n  WorldEngine,\n  ConsoleLoggerPlugin,\n  InMemoryMemoryStore,\n  InMemoryGraphStore,\n  studioPlugin,\n} from \"worldsim\";\nimport { reportGeneratorPlugin } from \"worldsim/plugins\";\nimport { readFileSync } from \"node:fs\";\n\nconst scenario = JSON.parse(readFileSync(\"scenario.json\", \"utf-8\"));\n\nconst world = new WorldEngine({\n  worldId: scenario.name,\n  maxTicks: scenario.maxTicks,\n  tickIntervalMs: scenario.tickIntervalMs,\n  llm: {\n    baseURL: \"https://api.openai.com/v1\",\n    apiKey: process.env.OPENAI_API_KEY!,\n    model: \"gpt-4o-mini\",\n  },\n  rulesPath: { json: [\"rules/community-rules.json\"] },\n  memoryStore: new InMemoryMemoryStore(),\n  graphStore: new InMemoryGraphStore(),\n});\n\nworld.use(ConsoleLoggerPlugin);\n\nconst report = reportGeneratorPlugin({ engine: world });\nworld.use(report.plugin);\nworld.use(studioPlugin({ engine: world, port: 4400, open: true }));\n\nfor (const agent of scenario.agents) world.addAgent(agent);\n\nworld.on(\"tick\", (tick) =\u003e {\n  if (tick === scenario.trigger.atTick) {\n    report.recordPolicyTrigger(tick, scenario.trigger.announcement);\n  }\n});\n\nawait world.start();\n```\n\nCalling `report.recordPolicyTrigger(tick, announcement)` at the trigger tick is what lets the report build the `shock` section (pre/post stats, deltas, `recoveryTicks`).\n\n### Optional building blocks\n\nAdd these only when the scenario needs them:\n\n| If you want... | Add |\n| --- | --- |\n| Phones / SMS / calls between agents | `PhonePlugin` + `InMemoryAssetStore` + `createPhoneAsset` |\n| Physical movement across the world | `MovementPlugin` + `LocationIndex` + `MovementPolicy` |\n| Vital skills (farming, cooking…) | `LifeSkillsPlugin([...])` |\n| Real-world tools (weather, environment) | `RealWorldToolsPlugin({ dataSources })` |\n| Live dashboard in the browser | `studioPlugin({ engine, port: 4400 })` |\n| Final report + sociological analysis | `reportGeneratorPlugin({ engine })` |\n| Shock analysis in the report | `report.recordPolicyTrigger(tick, msg)` |\n| Reproducible evaluation | Drop the scenario under `evaluation/scenarios/\u003cname\u003e/` and run `run-evaluation.ts` |\n\n### What you get at the end\n\n`reportGeneratorPlugin` produces a `SimulationReport` — fully JSON-serializable, consumable from the Studio dashboard and exportable to CSV — with:\n\n- `summary`, `timeline`, per-agent trajectories (`mood`, `energy`, status changes).\n- `relationships[]` with initial/final strength and per-tick snapshots.\n- `metrics` (speaks, observations, tool calls, tokens, cost).\n- `network` — degree / betweenness / eigenvector centrality, density over time, communities, reciprocity, homophily.\n- `dialogue` — who-talks-to-whom matrix, voice Gini, response rate, message-length stats.\n- `shock` (when `recordPolicyTrigger` is called) — pre/post windows, deltas, `recoveryTicks`.\n- `archetypes` — each agent classified as `compliant | skeptic | resistant | apathetic` with rationale, plus emotional contagion and mood variance per tick.\n- `narrative` (opt-in, LLM cost) — global story arc, per-agent arcs, emblematic quotes. Triggered via `POST /api/reports/:runId/narrative`.\n\n### Recommended workflow\n\n1. **Copy a template** — duplicate `evaluation/scenarios/water-rationing/` as `evaluation/scenarios/my-case/`.\n2. **Rewrite `scenario.json`** with at least 3 personalities in *tension* (otherwise \"immediate consensus\" kills the narrative).\n3. **Define the rules**: a few soft rules as baseline + one hard rule as the trigger shock.\n4. **Write `expected.md`** — even just for yourself, it makes it obvious when a run is broken.\n5. **Run it live** with `tickIntervalMs: 2000` and the Studio dashboard to watch dynamics unfold.\n6. **Run it headless** (`tickIntervalMs: 0`) and compare `evaluation/results/\u003cname\u003e.json` against `expected.md` using [`criteria.md`](evaluation/criteria.md).\n7. **Iterate on the system prompts** — ~90% of simulation quality comes from prompt specificity and the clarity of the trigger announcement.\n\n### Common pitfalls to avoid\n\n- **Homogeneous cast** → vary age, profession, personality, goals. The `network.homophily` score in the report will flag this.\n- **Ignored trigger** → the announcement must be explicit and at least one rule needs `enforcement: \"hard\"` with a governance agent (`role: \"control\"`) to enforce it.\n- **Monologues** → give agents backstories that *connect* them, and prompt them to address others by name.\n- **Language drift** → if the scenario is in Italian, insist in the prompt: \"parli sempre in italiano\".\n- **No narrative arc** → 30 ticks with a mid-run trigger is the minimum to get pre/reaction/coalition/resolution; below 15 ticks everything collapses.\n\n## Key Capabilities\n\n| Feature | Description |\n| ------- | ----------- |\n| **LLM-agnostic** | OpenAI, Anthropic proxies, Ollama — anything OpenAI-compatible |\n| **Personality system** | Mood, energy, goals, beliefs, knowledge per agent |\n| **Social dynamics** | Relationship tracking with strength decay, neighborhoods |\n| **Rule enforcement** | Hard/soft rules, governance agent with autonomous control |\n| **Scalability** | 1000+ agents via concurrency caps, activity scheduling, token budgets |\n| **Zero-config persistence** | In-memory by default; plug in Redis, Neo4j, PostgreSQL for production |\n| **Real-time streaming** | Socket.IO events for live dashboards |\n| **Simulation reports** | Auto-generated analysis with mood heatmaps and action metrics |\n\n## Documentation\n\n- [Architecture \u0026 internals](docs/architecture.md)\n- [Persistence \u0026 databases](docs/persistence.md)\n- [Scaling to production](docs/scaling.md)\n- [Plugin authoring guide](docs/plugins.md)\n- [Evaluation scenarios](evaluation/README.md)\n- [Development roadmap](docs/ROADMAP.md)\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, PR guidelines, and how to propose new scenarios.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffrancemazzi%2Fworldsim","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffrancemazzi%2Fworldsim","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffrancemazzi%2Fworldsim/lists"}