Architecture & Internals
This document describes the internal architecture of worldsim: how the engine orchestrates agents, how agents are structured, and how the subsystems (plugins, rules, messaging, memory) fit together.
flowchart LR
WorldEngine --> RulesLoader
WorldEngine --> PluginRegistry
WorldEngine --> PersonAgent
WorldEngine --> ControlAgent
PersonAgent --> LLM
ControlAgent --> LLM
PersonAgent -.-> MemoryStore
PersonAgent -.-> GraphStoreWorldEngine orchestration flow
WorldEngine (in ../src/engine/WorldEngine.ts) is the top-level entry point. Its lifecycle has three phases:
- Bootstrap (
WorldBootstrapper) — loads rules viaRulesLoader, firesonBootstrapandonRulesLoadedplugin hooks, auto-composesBrainMemorywhen vector/persistence stores are provided, instantiates allPersonAgentandControlAgentinstances from pending configs, assigns plugin tools to person agents, and starts every agent. - Tick loop (
TickOrchestrator.runLoop) — increments the world clock, executes one tick per iteration, and optionally sleeps fortickIntervalMsbetween ticks. The loop runs untilmaxTicksis reached orstop()is called. - Stop (
WorldLifecycle) — sets status to"stopped"and fires theonWorldStopplugin hook with the full event log.
The engine also exposes lifecycle methods for individual agents: pauseAgent, resumeAgent, stopAgent. Each emits an onAgentStatusChange plugin hook.
Source: ../src/engine/internal/WorldBootstrapper.ts, ../src/engine/internal/WorldLifecycle.ts
TickOrchestrator: per-tick execution
Each tick in TickOrchestrator.executeTick() proceeds through these steps:
- Increment clock and update context (
tickCount,messageBus.newTick). - Fire
onWorldTickplugin hook and call registered tick handlers. - Reset per-tick state — token budget counters, stale conversations, neighborhood cache.
- Filter active person agents — skips non-running agents, always includes agents with pending messages, applies
defaultActiveTickRatioviaActivitySchedulerfor agents without their own schedule. Sorts by pending message count (more messages = higher priority). - Execute person agents — wraps each agent's
tick()call in a task and passes all tasks toBatchExecutor.executeSettled()for concurrency-limited parallel execution. Failed agents are logged, not fatal. - Batch decay/prune relationships via
NeighborhoodManagerfor all active agents. - Apply control events —
ControlEventApplierprocesses any pending agent lifecycle commands (pause, resume, stop) emitted during the tick. - ControlAgent evaluation — if control agents exist and there were actions, each active
ControlAgentevaluates the collected actions (subject tocontrolSamplingRate). Verdicts are"blocked","warned", or"allowed". - Run action plugin hooks — calls
onAgentActionsBatchfor batch plugins,onAgentActionfor per-action plugins. - Tick control agents — each active
ControlAgentruns its owntick()for autonomous governance actions.
Agent system
BaseAgent
BaseAgent is the abstract base class for all agents. It provides:
- Lifecycle via
AgentLifecycle(start, pause, resume, stop). - Internal state — mood, energy, goals, beliefs, knowledge, custom data.
- System prompt builder — assembles profile, state, memories, relationships, personality enforcement, social dynamics, and rules into a single prompt.
- Tick gating —
shouldSkipTick()checks activity schedule and token budget before executing. - Message primitives —
emit()broadcasts to all agents,onMessage()subscribes to directed/broadcast messages.
PersonAgent
PersonAgent (in ../src/agents/PersonAgent.ts) extends BaseAgent and implements the agentic loop via LangGraph:
- Each
tick()runs a multi-step loop: recall memories, build context, call the LLM, execute tool calls, persist new memories, update relationships, and produceAgentActionresults. - Tools are injected by the bootstrapper from the
PluginRegistry. Agents can be given access to all tools or a specific subset viatoolNamesin their config. - Supports a "light" LLM tier (
llmTier: "light") for cheaper agents that use a secondary model.
ControlAgent
ControlAgent (in ../src/agents/ControlAgent.ts) extends BaseAgent with governance responsibilities:
- At bootstrap, it ingests all loaded rules into its cognitive context.
evaluateActions()reviewsAgentAction[]against the rules, producing verdicts (allowed,warned,blocked).- Has a built-in
control_agenttool that can pause/resume/stop other agents autonomously. - Supports
controlSamplingRateto evaluate only a fraction of actions at scale.
Plugin system
Plugins implement the WorldSimPlugin interface defined in ../src/types/PluginTypes.ts. Registration is done via engine.use(plugin), which delegates to PluginRegistry.
Available hooks
| Hook | Signature | When |
|---|---|---|
onBootstrap | (ctx, rules) => Promise<void> | After rules are loaded, before agents start |
onWorldTick | (tick, ctx) => Promise<void> | Start of every tick |
onAgentAction | (action, state) => Promise<AgentAction> | Per-action (can transform) |
onAgentActionsBatch | (actions, ctx) => Promise<void> | Once per tick with all actions (mutual exclusive with onAgentAction per plugin) |
onRulesLoaded | (rules) => Promise<void> | After rules parsing completes |
onWorldStop | (ctx, events) => Promise<void> | When the engine stops |
onAgentStatusChange | (event, oldStatus, newStatus) => Promise<void> | On any agent lifecycle transition |
onMessageRouted | (receipt, ctx) => Promise<void> | After a message is delivered or deliberately dropped |
Tool registration
Plugins expose tools via the tools array. Each tool follows the AgentTool interface: name, description, inputSchema, and an async execute(input, ctx) function.
Parallel flag
When parallel: true is set on a plugin, its hooks run concurrently with other parallel plugins. Sequential plugins (the default) run in registration order.
Source: ../src/plugins/PluginRegistry.ts
Rules engine
Rules are loaded at bootstrap by RulesLoader from two sources:
- JSON files — parsed by
JsonRulesParser, validated against a Zod schema (RulesSchema). - PDF files — text-extracted by
PdfRulesParser, then structured into rules via an LLM call.
Each Rule has:
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier |
priority | number | Lower = higher priority (rules are sorted by this) |
scope | "world" | "control" | "person" | "all" | Which agents see this rule |
enforcement | "hard" | "soft" | Hard rules are strict; soft rules are guidance |
instruction | string | The rule text injected into agent prompts |
condition | string? | Optional condition for contextual rules |
Rules are injected into agent system prompts as [HARD] ... or [SOFT] ... lines, and the ControlAgent evaluates actions against them.
Source: ../src/types/RulesTypes.ts, ../src/rules/RulesSchema.ts
Messaging
MessageBus
MessageBus is the in-memory inter-agent messaging backbone. It supports:
- Directed messages —
publish({ to: "agent-id", ... })delivers to a specific agent. - Broadcast —
broadcast(msg)setsto: "*"and delivers to all subscribers. - Group publish —
publishToGroup(msg, recipientIds)creates one message per recipient. - Tick-scoped storage — messages are indexed by tick and recipient for O(1) lookup. Old ticks are cleaned up automatically.
ConversationManager
ConversationManager adds structured turn-taking on top of the message bus:
- Start a conversation with
startConversation(initiator, participants, topic). - Round-robin speaker rotation via
advanceTurn(). canSpeak(agentId)gates agent speech to prevent out-of-turn talking.- Stale conversations are auto-cleaned after a configurable number of idle ticks.
Proximity-based messaging
When defaultBroadcastRadius is configured (in km), agents without explicit recipients use spatial proximity instead of global broadcast. Agents without a location fall back to broadcast. The LocationIndex tracks agent positions for distance calculations.
Routing outcomes and fallback policy
MessageRouter resolves speech through a single ordered cascade:
- perception, when enabled
- active conversation
- configured neighborhood
- spatial proximity
- the configured unroutable policy
The legacy-compatible fallback is broadcast. Integrators that require a closed audience can select a public policy instead of patching router internals:
const engine = new WorldEngine({
worldId: "private-world",
llm,
messageRouting: {
unroutablePolicy: "drop", // "broadcast" | "drop" | "error"
},
});Every final outcome creates a MessageDeliveryReceipt. Registered onMessageRouted plugin hooks receive exact directed recipients, the route kind, final thread/audience metadata, and a reason for dropped messages. Broadcast receipts use "*" because subscribers, rather than the router, resolve their effective audience.
Perception has its own physical-delivery policy: interaction.disableBroadcastFallback. When strict perception cannot deliver a stimulus, the receipt is dropped; when legacy fallback is explicitly enabled, the router continues through the cascade and emits only the final route receipt.
Memory
BrainMemory
BrainMemory is the unified memory facade that coordinates all storage layers:
- save/saveBatch — writes to
MemoryStore, and optionally toPersistenceStoreandVectorStore(with automatic embedding viaEmbeddingManager). - recall — fetches recent memories from
MemoryStore, performs semantic search viaVectorStoreif a query is provided, deduplicates, and optionally includes consolidated knowledge fromPersistenceStore. - consolidate — delegates to
MemoryConsolidatorto promote, summarize, and prune old memories. - snapshotState / restoreState — persists agent internal state via
PersistenceStore.
MemoryConsolidator
MemoryConsolidator runs periodic memory maintenance:
- Scores memories by importance (type-weighted: knowledge > reflection > conversation > observation > action).
- Promotes important memories to
ConsolidatedKnowledgeentries. - Optionally generates LLM summaries of memory clusters.
- Prunes low-value memories that exceed the retention window.
EmbeddingManager
EmbeddingManager wraps an EmbeddingAdapter and handles caching of already-embedded entries. Supports both single and batch embedding.
Agent lifecycle state machine
Every agent has a lifecycle managed by AgentLifecycle:
idle ──start──> running ──pause──> paused
│ │
│ <──resume── │
│ │
└──stop──> stopped <──stop──┘
^
│
idle ────stop───────┘Transitions:
| Action | From | To |
|---|---|---|
start | idle | running |
pause | running | paused |
resume | paused | running |
stop | running, paused, idle | stopped |
Invalid transitions are silently ignored (return false). The stopped state is terminal.
Lifecycle transitions can be triggered by:
- The host application via
engine.pauseAgent(),engine.resumeAgent(),engine.stopAgent(). - A
ControlAgentautonomously via itscontrol_agenttool. - The
TokenBudgetTrackerwhen a budget policy fires ("pause"or"stop").