Skip to content

Train on a MORPHEUS world

The flagship rung: every layer of this package running at once, against a live persistent world. MORPHEUS worlds are operational simulations — logistics, warehousing, order flows — that run continuously, raise incidents, and score remediations with verifiable rewards; this page provisions one, works in it, trains against it under a configuration schedule, scores the run, and tears the world down. It assumes a reachable deployment (http://localhost:8282 below); every output shown was captured from a real one — identifiers and exact figures will differ on yours.

A world you provision costs money until you remove it. Incident content is authored by model calls, so a world with generation enabled keeps spending for as long as the deployment is up, whether or not anything is driving it. This page ends with the teardown, and the teardown is part of the tutorial.

1. Provision, look around, and see the lifecycle

from skyfall_crl.env.morpheus import morpheus_world

env = morpheus_world(layout="process-outbound", max_steps=40,
                     reward_spec="paper", delete_on_close=True)
observation, info = env.reset(seed=1)
print(f"world: {env.world_id}  (owned: {env.owns_world})")
print(f"observation fields: {sorted(observation)}")
print(f"open tickets: {int(observation['open_tickets'][0])}, "
      f"by priority {observation['by_priority'].tolist()}")
print(f"regime: {info['regime']}")
env.close()
print("closed -- delete_on_close removed the world")
world: 6a9e8b6a0ee315d69a40de88  (owned: True)
observation fields: ['by_priority', 'by_status', 'mean_verifier_progress', 'open_tickets', 'recent_invalid_actions', 'step', 'tickets']
open tickets: 0, by priority [0.0, 0.0, 0.0, 0.0]
regime: {'id': 'baseline', 'origin': None, 'step': 0, 'boundary': True, 'axes': {}, 'applied': True}
closed -- delete_on_close removed the world

Three things in that exchange are the adapter in miniature. The observation is the fixed seven-field projection of the incident stream — and it opens quiet: a fresh world has not lived yet, and incidents arrive as its simulation runs, over minutes, not steps. The regime label travels in info, never in the observation. And delete_on_close=True made close() remove the world — the default is False, which is how a world outlives a crashed script and keeps spending; passing it explicitly is the habit this page teaches first.

2. Attach to a world that has lived, and remediate one incident

Give a world a few minutes and the incident stream is real. Attaching by world_id makes the environment a guest — it will not delete, rebuild or reconfigure a world it does not own:

import json

from skyfall_crl.env.morpheus import morpheus_world
from skyfall_crl.env.morpheus.action import ALLOWED_METHODS

env = morpheus_world(world_id="6a9e8b8b0ee315d69a40e281", max_steps=40, reward_spec="eval8")
observation, info = env.reset(seed=1)
print(f"attached as a guest: {not env.owns_world}")
print(f"open tickets: {int(observation['open_tickets'][0])}")

incident = json.loads(observation["tickets"])[0]
print(f"first incident: {incident['id']}  {incident['status']}  "
      f"{incident['priority']}  {incident['chaos_type']}")

action = {
    "method": ALLOWED_METHODS.index("PATCH"),
    "path": f"/{env.world_id}/tickets/{incident['id']}/status",
    "body": json.dumps({"status": "in_progress"}),
}
observation, reward, terminated, truncated, info = env.step(action)
moved = {k: round(v, 3) for k, v in info["reward_components"].items() if v}
print(f"reward {reward:+.4f}   executed: {info['action']['executed']}   moved: {moved}")
env.close()
print("closed; a guest leaves the world alone")
attached as a guest: True
open tickets: 8
first incident: 6a9e8bf60ee315d69a40e83e  new  high  STEP_FAILURE
reward -0.2800   executed: True   moved: {'chaos_tickets': -0.28}

That one step is the whole loop: observe an incident, act on it with one of the world's own domain operations, get scored. The reward deserves reading: the status transition paid — the eval8 specification rewards progressing a ticket — and the step still nets −0.28, because eight incidents are open and the clock is running on all of them. The reward reflects the operational state of the world, not just the action you took, and the per-component breakdown on info["reward_components"] (and every trace row) is how you tell which is which.

3. Train under a schedule, from one document

env:
  id: skyfall_crl.env.morpheus:morpheus_world
  kwargs:
    base_url: http://localhost:8282
    world_id: 6a9e8b8b0ee315d69a40e281
    max_steps: 60
    reward_spec: eval8
    regime:
      regimes:
        - {regime_id: steady,       duration_steps: 20}
        - {regime_id: surge,        duration_steps: 20, ticket_arrival_rate: 0.8}
        - {regime_id: steady_again, alias_of: steady, duration_steps: 20}
algorithm: {name: discrete_hill_climbing, params: {n_actions: 2, seed: 0}}
rollout: {window_steps: 20, total_steps: 60}
run: {name: morpheus-run, seed: 0, trace_path: traces.jsonl}
skyfall-crl run --config experiment.yaml
[1/1] morpheus-run: 60 steps, 3 configurations, 1.1s ->

The schedule sits under the environment's regime, not the harness's, because a MORPHEUS world labels its own steps and — when it owns the world — pushes each regime's axes to the deployment as the shift lands. Attached as a guest, the labels flow and the pushes are declined: this run's surge was a labelled interval, not an applied one, which is exactly what a guest should do to a shared world. Provision inside the run (drop world_id, add delete_on_close: true) and the shifts are applied for real — at the price of the early intervals running in a world that has not lived yet. Two further lines are load-bearing: auto_reset stays off (a MORPHEUS reset destroys and rebuilds the world), and alias_of gives forgetting its second encounter.

4. Score it

skyfall-crl eval --traces traces.jsonl --spec eval8
1 trace(s), 3 configuration(s), reward spec 'eval8'

  Metric                        segment
  -------------------------------------
  Per-configuration reward       -0.925
  Adaptation speed                   20
  Forgetting                      0.045
  Recovery time                       1
  Stability (variance)      0.000868421
  Zero-shot reward                -0.94
  Settled reward                  -0.94
  Return (undiscounted)           -55.5
  ...

Read it the way the metrics chapter taught: an untrained two-action policy in a live operational world watches the meter run — the open backlog charges every step, so per-step reward sits near −0.9 and adaptation reports the interval length (the running mean never crosses under a reward this uniformly negative). Forgetting is a genuine +0.045: the revisit went slightly worse, because the backlog had grown. These are honest numbers about an untrained policy, and the pipeline that produced them — schedule, labels, per-component rows, segmentation, the spec-derived table — is the thing this rung demonstrates. Training something good here is the research problem, and needs the [train] backends and the capstone's scaling section.

5. Tear it down

Generation off first — immediate, and safe with work in flight — then delete. A world with generation off costs nothing, so the two-step is also a fine resting state:

import time

from skyfall_crl.env.morpheus import HttpBackend

backend = HttpBackend(base_url="http://localhost:8282")
backend.set_chaos("6a9e8b8b0ee315d69a40e281", {"enabled": False, "processChaosEnabled": False,
                                               "infraChaosEnabled": False})
print("generation off")
time.sleep(5)
backend.delete_world("6a9e8b8b0ee315d69a40e281")
backend.close()
print("world removed")
generation off
world removed

Never delete a world that still has queued work without switching generation off first — a cascade delete on a busy world can disrupt the deployment itself. The adapter page carries the full ownership rules.

Where next

The capstone is this workflow at experiment scale — families, seeds, aggregation, export; DiscoveryWorld is the same machinery pointed at a simulator with none of this world's shape.