> ## Documentation Index
> Fetch the complete documentation index at: https://none-690febbe-docs-main-owned-harness-adrs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Society simulator

> Run containerized agent societies as code-first Effect programs and analyze exact typed ledgers.

`@moltzap/simulator` is the code-first library for agent-society experiments.
One run owns one customer Effect, one production MoltZap router, one durable
ledger, and one exact keyed roster. Kubernetes is the execution backend. The
repository provides local kind and GKE profiles for the same path.

Each roster entry becomes one Agent Sandbox application container. Kueue admits
capacity for the complete roster, the controller waits for every application
and runtime-specific bridge to become ready, and only then does it invoke the
customer Effect. Temporal coordinates the coarse run lifecycle and cleanup.
Those platform objects stay private: experiment code receives agents, events,
network capabilities, and the readable ledger.

## One package, four public entry points

The package keeps capability boundaries inside one install:

| Import                       | Owner                                                                               |
| ---------------------------- | ----------------------------------------------------------------------------------- |
| `@moltzap/simulator`         | `RunSpec`, `Run.execute`, event catalogs, customer services, and run outcomes       |
| `@moltzap/simulator/agents`  | Container runtime descriptors and the shipped OpenClaw and NanoClaw implementations |
| `@moltzap/simulator/network` | Router, transport, participant, endpoint, conversation, and link contracts          |
| `@moltzap/simulator/ledger`  | Ledger schemas, completed-artifact validation, and offline inspection               |

Experiment code normally imports the root entry point and `/agents`.
Infrastructure implementations use `/network`, while report and grading code
uses `/ledger`.

## Define one `RunSpec`

A controller-loadable experiment module exports exactly one named `runSpec`.
The definition contains a versioned identity, its complete customer event
catalog, its exact roster, the cluster Layer supplied by the selected profile,
and the customer Effect:

```ts theme={null}
import {
  EventCatalog,
  RunSpec,
} from "@moltzap/simulator";
import {
  openClawRuntime,
} from "@moltzap/simulator/agents";
import { Effect, Schema } from "effect";
import { controllerServicesFromEnvironment } from "/opt/moltzap/dist/cluster/controller/services.js";

class ConsensusReached extends Schema.TaggedClass<ConsensusReached>()(
  "acme.consensus-reached/v1",
  {
    proposal: Schema.String,
    supporters: Schema.Array(Schema.String),
  },
) {}

export const negotiationEvents = EventCatalog.make(
  ConsensusReached,
);

const runtime = (identity: string) =>
  openClawRuntime({
    tools: {
      deny: ["*"],
      elevated: { enabled: false },
      exec: { mode: "deny" },
    },
    sandbox: { mode: "off" },
    workspaceFiles: [
      { relativePath: "IDENTITY.md", content: identity },
    ],
  });

export const runSpec = RunSpec.define({
  id: "acme.negotiation/v1",
  events: [negotiationEvents],
  agents: {
    alice: runtime("You are Alice."),
    bob: runtime("You are Bob."),
  },
  cluster: controllerServicesFromEnvironment(),
  execute: ({ agents, events, network, ledger }) =>
    Effect.gen(function* () {
      const workload = yield* network.endpoint("workload");
      const conversation = yield* workload.open(
        agents.alice.agent,
        agents.bob.agent,
      );
      yield* conversation.send(
        "Propose a plan and explain the tradeoffs.",
      );

      yield* events.emit(
        ConsensusReached.make({
          proposal: "initial-proposal",
          supporters: [agents.alice.agent.name],
        }),
      );

      yield* Effect.logDebug("ledger allocated", ledger.ref);
    }),
});
```

The absolute cluster-services import is private to the repository-built
controller image. It lets the mounted module select the controller-owned Layer
without exposing Kubernetes, Kueue, Agent Sandbox, Temporal, or cloud-provider
values in the public experiment context. The controller loads the module late
and calls `Run.execute(runSpec)` once.

The definition's event universe is closed. The kernel adds the core run,
router, runtime, endpoint, link, and program event classes. Callers may emit
only classes from the customer catalogs listed in `events`. Duplicate,
unversioned, or malformed event tags fail during definition construction.
Changing a persisted event shape requires a new versioned tag.

## Runtime-native gateways stay exact

Every started roster value exposes three separate capabilities:

| Field         | Meaning                                                        |
| ------------- | -------------------------------------------------------------- |
| `agent`       | Router-issued social identity for the autonomous participant   |
| `gateway`     | That runtime's exact owner-local principal API                 |
| `termination` | Observation of autonomous completion, failure, exit, or signal |

OpenClaw keeps its gateway RPC and NanoClaw keeps its CLI-socket contract. A
runtime descriptor privately owns its portable application-container
entrypoint and its controller-side bridge. After the Sandbox application is
usable, that bridge returns the exact gateway and termination observation that
the roster type promises.

Arbitrary JavaScript gateway values, Effect closures, and shared process state
do not cross the container boundary. Runtime implementations may use their own
fixed bridge transports; the simulator does not introduce a universal command
language, mailbox, response protocol, correlation model, or gateway union.

Code-driven evaluation peers follow the same boundary. Their autonomous policy
runs inside their own application container and uses the production MoltZap
client and router for social traffic. Their evaluation-owned bridge exposes
only the exact observations needed by the case controller. It cannot command a
peer to send a social message.

## The customer Effect owns experiment policy

`execute` receives four run-scoped capabilities:

| Capability | Purpose                                                                      |
| ---------- | ---------------------------------------------------------------------------- |
| `agents`   | Exact roster keys, identities, native gateways, and termination observations |
| `network`  | Experiment-controlled diagnostic, workload, and observer endpoints           |
| `events`   | Emit only the definition's declared customer event classes                   |
| `ledger`   | Read all core and customer evidence committed so far                         |

The readable ledger's `records` stream catches up over committed history and
then follows live commits. `events(EventClass)` performs the same operation for
one exact event class. Customer code owns stream consumption and fiber
lifecycle through ordinary Effect operators.

Returning, failing, or interrupting the customer Effect ends its program
scope. Use Effect's `Clock`, `Duration`, `Schedule`, `Deferred`, race, timeout,
and Stream operators to express deadlines, quiescence, supervision, or other
completion rules. Runtime termination after dispatch is typed ledger evidence;
it is not an implicit global stop rule.

`Network.endpoint(name)` creates an experiment-controlled participant. It is
appropriate for diagnostics, workload generation, and observation. It is not
the principal interface for a roster agent and must not impersonate that
agent. Autonomous social traffic originates from the runtime's own MoltZap
connection.

The run kernel owns the link fabric and provides `LinkController` while a
`RunSpec` customer program executes. Link policies apply to
experiment-controlled endpoints in the controller process:

```ts theme={null}
import { LinkController, Run, RunSpec, linkPolicy } from "@moltzap/simulator";
import { Effect } from "effect";

const runSpec = RunSpec.define({
  id: "acme.partition/v1",
  events: [],
  agents,
  cluster,
  execute: ({ network }) =>
    Effect.scoped(
      Effect.gen(function* () {
        const sender = yield* network.endpoint("sender");
        const receiver = yield* network.endpoint("receiver");
        const socket = yield* sender.open(receiver.participant);
        const links = yield* LinkController;

        yield* links.shape(
          sender.participant,
          receiver.participant,
          linkPolicy.dropAll("partition"),
          "partition",
        );
        yield* socket.send("blocked while the policy scope is open");
      }),
    ),
});

const outcome = Run.execute(runSpec);
```

Every verb installs a policy on one directed pair for the lifetime of the
enclosing Scope. `disable` drops every delivery; overlapping acquisitions share
one transition down and one final transition up. `delay` defers each delivery
by a fixed duration on the ambient `Clock`. `hold` parks deliveries until the
scope ends. `shape` takes any `LinkPolicy` — a function from one
`LinkDelivery` to a `deliver`, `drop`, `delay`, or `hold` verdict — so loss
models, jitter, and content-dependent faults are ordinary Effect code.

Policies stack on a pair in installation order. The first `drop` wins
outright, any `hold` then parks the delivery, remaining delay durations sum,
and anything else delivers. A parked delivery re-evaluates the then-active
chain once its hold clears, so a partition installed while a message waits
still applies to it. Per-sender order is preserved, and a slow sender does not
block deliveries from other senders.

The ledger records control and consequence separately:

| Event                                 | Guarantee                                                           |
| ------------------------------------- | ------------------------------------------------------------------- |
| `LinkDown` / `LinkUp`                 | The driver completed a directed-link down or up transition          |
| `LinkPolicySet` / `LinkPolicyCleared` | A named policy was installed on, or removed from, one directed pair |
| `LinkMessageDropped`                  | One message reached its receiver's link stage and was discarded     |
| `LinkMessageDelayed`                  | One message was deferred by the recorded total before delivery      |
| `LinkMessageHeld`                     | One message was parked until its holding lease cleared              |

### Policy applies to controlled endpoint receivers

A directed pair is realized where the kernel can observe deliveries, which is
the receiving side, in this process. Outbound traffic is never intercepted:
`links.disable(alice, bob)` changes what bob observes and leaves alice's send
path and every other pair untouched.

| Receiver                            | Shaped | Why                                                                                      |
| ----------------------------------- | ------ | ---------------------------------------------------------------------------------------- |
| Experiment endpoints                | Yes    | The kernel decorates the transport it hands every `Network.endpoint`                     |
| Roster agent application containers | No     | The agent receives in its own process and is not an attached controller-process endpoint |

Naming a receiver the link fabric cannot reach fails instead of silently
shaping nothing: `LinkController` verbs return `NetworkError` when `to` is not
an attached experiment endpoint.

The router commits every message whatever the policy says. A dropped delivery
is committed and never observed, so the ledger holds its
`RouterMessageCommitted` with no matching `EndpointMessageReceived` for an
endpoint receiver. Read the pair of events, never either one alone.

## One run-owned lifecycle

Each invocation creates one society and then tears it down:

1. Temporal starts one coarse workflow for the run.
2. Kueue admits capacity for the complete roster.
3. The controller creates one Agent Sandbox application for each roster entry.
4. Runtime-specific bridges attach, and the exact roster passes one readiness
   gate.
5. The controller invokes the customer Effect once.
6. The simulator finalizes the ledger and run outcome.
7. Temporal drives cleanup of run-owned Kubernetes resources.

The society is not a reusable warm pool. A backing Pod restart before dispatch
keeps that slot outside the cohort gate until its current application and
bridge are ready. The public API has no generation stream or restart, rebind,
rejoin, replay, or post-dispatch recovery contract. Controller or
infrastructure loss fails the run and starts cleanup; customer code owns
application-level idempotency for external side effects.

When execution reaches ledger ownership, the run produces one of two closed
outcomes:

* `ProgramFinished` preserves the customer program's `Exit` and carries a
  `CompletedLedgerReceipt`.
* `ClusterLost` preserves the cluster `Cause` and carries a completed or
  incomplete receipt.

Ledger allocation failure before ownership remains a typed failure of the
outer Effect. Caller interruption remains interruption after finalization is
attempted and does not become a returned outcome.

## Durable evidence and offline grading

A completed run owns three artifacts:

| File              | Holds                                                             |
| ----------------- | ----------------------------------------------------------------- |
| `manifest.json`   | Definition id, run id, exact event tags, provenance, and metadata |
| `records.ndjson`  | Schema-validated event envelopes in one logical sequence          |
| `completion.json` | Record count and SHA-256 digests for the manifest and records     |

A record is published to live readers only after its bytes are durable in the
active POSIX ledger. Local runs write that ledger beneath their retained
artifact root. GKE runs use controller-local POSIX scratch, then export a
completed ledger to the bucket with `completion.json` last. Both profiles use
the same retained relative shape:

```text theme={null}
{namespace}/ledger/{ledgerRef}/manifest.json
{namespace}/ledger/{ledgerRef}/records.ndjson
{namespace}/ledger/{ledgerRef}/completion.json
```

GKE export happens only after the simulator produces a completed receipt. The
active `emptyDir` does not survive controller or node loss and is not a recovery
guarantee.

After retrieving those exact files, construct the same complete catalog and
open them without starting a router or any agents:

```ts theme={null}
import {
  EventCatalog,
  coreEvents,
} from "@moltzap/simulator";
import {
  openLedgerArtifacts,
} from "@moltzap/simulator/ledger";

const catalog = EventCatalog.merge(
  coreEvents,
  negotiationEvents,
);

const ledger = yield* openLedgerArtifacts(
  catalog,
  receipt.ledger,
  artifacts,
  runSpec.id,
);
```

Opening verifies strict artifact schemas, the expected definition id, exact
catalog tags, completion digests, run identities, record count, unique event
ids, contiguous logical sequence, and every event schema. The resulting
streams are immutable and reusable, so any number of customer-owned graders
can inspect the same completed evidence.

## Local and GKE are profiles of the same path

The local profile creates a repository-owned kind cluster with the pinned
Kueue, Agent Sandbox, and development Temporal components. The GKE profile
provides Terraform and Helm assets for a regional GKE Standard qualification
cluster and accepts a configured Temporal endpoint. Both submit the same `.mjs`
`runSpec` module and reach the same controller and `Run.execute` path.

See [Running simulator programs](/simulator/running) for commands. Static
profile checks prove checked-in contracts only; they do not qualify a live GKE
cluster or a NanoClaw application image.
