/** * sim/world/runner.ts — the population-layer driver, ported from mirofish * `engine/world/runner.py`. * * Population execution = an actor event loop: every agent runs its own async * task loop (observe → step → verdict → execute → next observation, until * done/give_up/timeout); peers exchange speech asynchronously through the * Environment mailbox; there is NO super-step barrier. Causal order rides on * per-agent Lamport clocks (+1 per local event, speak carries lc, receive = * max(local, incoming)+1; observations sort by (@me first, lc, source)). * * Two waiting mechanisms compose, both verdict-preserving: * - _awaitInbox: an idle agent in a COUPLED medium cheaply polls its mailbox * (no LLM burn, no history, no budget) so a fast peer doesn't sprint past * a slow lead before the coordination ping arrives; * - quiesce (WorldClosure.quiesce_rounds > 0): when every running agent has * been silently waiting for N consecutive beats, the world closes early * instead of idling to max_steps. */ import type { UserSimulatorAgent } from "../agent.ts"; import { type SessionFrame, type World, type WorldSnapshot } from "../models.ts"; import { Environment } from "./environment.ts"; export type StepProgressFn = () => Promise | void; export type AgentsOpenedFn = (actorIds: string[]) => Promise | void; export type AgentRetireFn = (actorId: string) => Promise | void; export declare function actorId(agent: UserSimulatorAgent): string; export interface WorldRunnerOptions { /** Live-resource injection seam — may only FULFIL the declaration, never * override it (a spec'd medium must match world.environment.kind). */ environment?: Environment; /** Peak-shaving knob (rate limits), not simulation semantics. */ maxConcurrency?: number; onStepProgress?: StepProgressFn; } export declare class WorldRunner { readonly world: World; readonly env: Environment; onAgentsOpened: AgentsOpenedFn | null; onAgentRetire: AgentRetireFn | null; onStepProgress: StepProgressFn | null; private agents; private coupled; private sem; private stepNo; private own; private seeded; private lc; private gclock; private busy; private idle; private quiesced; private quiesceRounds; constructor(world: World, agents: UserSimulatorAgent[], opts?: WorldRunnerOptions); get step_no(): number; private running; private guarded; private retire; /** The private half of the opening: everyone open()s concurrently * (= runtime.init); reports land in each agent's own observation, never * broadcast. A single failed open retires that agent; the world goes on. */ private openAgents; /** Loop until the world converges. Do not run concurrently on one instance. */ run(maxSteps?: number | null): Promise; /** Live frame snapshots at any moment (salvage path when run() is aborted). */ frames(): SessionFrame[]; snapshot(): Promise; private runActor; /** Next observation = opening own (first step) + this step's own progress + * medium increments; @me first, then causal (lc, source) order. */ private observeFor; private emitProgress; /** Route speaks: bump the local clock (with or without a speak — clocks stay * monotonic), stamp lc, publish; listeners drain on their own loops. */ private publishSpeaks; /** Cheap mailbox poll for an idle agent in a coupled medium — returns when * mail is pending for me, no other agent is running, no other agent is * busy (nobody will ever write — avoid the all-idle deadlock), or timeout. */ private awaitInbox; private actorLoop; /** All running agents silently waiting for N consecutive beats ⇒ the world * has converged into mutual waiting; close early instead of idling out. */ private maybeQuiesce; }