The MORPHEUS adapter — API reference¶
Every public symbol in skyfall_crl.env.morpheus, generated from the source.
Generated page — built from the source docstrings, so this file is blank when read on GitHub. Run
mkdocs serveto read it locally, or read the docstrings in the module itself.
The environment¶
morpheus_env ¶
A Gymnasium environment over a live MORPHEUS world.
MorpheusEnv ¶
Bases: Env
Drive a MORPHEUS world through the standard reset / step loop.
A world runs its operational descriptors on its own schedule -- that is the simulation, and it carries the agent's fixes forward. The agent does not run them. It watches the incident tickets the world raises and remediates them with tools; a verifier scores each fix.
This class owns the world lifecycle and the reset/step loop. The observation projection, the tool action space, and the reward are pluggable layers that attach to it; until they are attached the environment reports ticket counts, a zero reward, and runs the loop, so the Gymnasium contract can be exercised on its own.
Parameters¶
backend:
How to reach a deployment. Any object satisfying the backend contract works, so a
world, a recorded fixture, or an in-memory double are interchangeable.
layout:
Which world layout to provision on the first reset.
max_steps:
Steps before the episode is truncated. A world has no terminal state, so this is the
administrative boundary, not a goal.
delete_on_close:
Whether close tears the world down. Off by default: deleting a world that still
has work in flight can disrupt the deployment, so disposal is opt-in.
owns_world
property
¶
Whether this environment created the world, and may therefore dispose of it.
reward_spec
property
¶
The spec scoring this run, once a world has settled it.
regime_id
property
¶
The active configuration's id. Reported for logging; withheld from the policy.
reset ¶
reset(
*,
seed: int | None = None,
options: Mapping[str, Any] | None = None,
) -> tuple[dict[str, Any], dict[str, Any]]
Start an episode, provisioning a world the first time.
Resetting a MORPHEUS world destroys and rebuilds it, which is expensive and discards
everything the agent has done. So a world is provisioned once and reused across
episodes; pass options={"reprovision": True} to deliberately rebuild it.
step ¶
Advance one step against the world.
describe ¶
What this run is configured with, for logging and for recording alongside a policy.
close ¶
Release the world.
A world this environment did not create is never deleted, whatever the disposal setting says: tearing down a world that is still doing work disrupts whoever owns it.
Observations¶
observation ¶
The observation schema and the projection that fills it.
The schema is fixed: it does not change with the world, the layout, or a configuration shift. Chaos types and affected services are open-ended and differ per layout, so they are carried inside the ticket payload and never as fields of the space. What a configuration shift moves is the values an agent sees, not the shape it sees them in -- which is what makes learning across shifts a well-posed problem.
PRIORITIES
module-attribute
¶
OPEN_STATUSES
module-attribute
¶
ObservedTicket
dataclass
¶
One incident ticket, normalised out of whatever the deployment reported.
from_payload
classmethod
¶
Build a ticket from a raw payload, or return None if it has no identity.
Field names are accepted in either the deployment's camelCase or snake_case, and unrecognised statuses and priorities fall back to the safest bucket rather than raising -- a malformed ticket should not end an episode.
ObservationProjector ¶
Projects the incident-ticket stream into the fixed observation space.
Some fields only become meaningful once actions are executed and verified. They are declared here from the start and reported as zero until then: a space that grew as later capabilities landed would silently invalidate everything already trained or measured against it.
normalize_tickets ¶
Normalise raw ticket payloads, dropping anything that is not a usable ticket.
Tolerant of the whole response, not just its entries: a deployment that answers with an error object, a bare string, or nothing at all reads as "no tickets" rather than ending the episode. An agent cannot act on a malformed response either way, and a run that dies on one loses everything before it.
Actions and scoping¶
action ¶
The tool call an agent proposes, and the boundary it has to stay inside.
An agent remediates incidents by calling the world's own domain operations. Two rules keep that honest, and both are enforced here rather than trusted to the caller:
- Operational descriptors are the simulation, not an agent action. The deployment runs them on its own schedule and they carry the agent's fixes forward; an agent that could trigger them would be driving the world rather than working in it.
- An agent cannot score itself. The verification endpoints are off limits.
Everything else about a proposed action -- an unknown method, an unparseable body, a path outside the world -- is an outcome to be scored, never an exception. A single malformed token from a policy must not end an episode.
ALLOWED_METHODS
module-attribute
¶
DEFAULT_DOMAINS
module-attribute
¶
DEFAULT_DOMAINS: tuple[str, ...] = (
"tickets",
"erp",
"wms",
"tms",
"finance",
"ledger",
"planning",
"logs",
)
ToolCall
dataclass
¶
One domain operation against a world.
resolved ¶
Normalise the call for a specific world.
Accepts the world as a placeholder so an agent (or a task view) can express a path without knowing which world it is running against.
ActionOutcome
dataclass
¶
What became of a proposed action.
ActionScope
dataclass
¶
The capability boundary an action must fall inside.
Scoping is by the world's own domains rather than a list of endpoints, so a world gaining an operation does not need this updated -- while the two things an agent must never do stay closed regardless of what the deployment exposes.
parse_tool_call ¶
Recover a tool call from a policy's text output.
Understands an ACTION: block and a trailing JSON object (models often prefix their
reasoning). Returns None when nothing parses, leaving it to the caller to decide what
a non-action means rather than inventing a call.
extract_ticket_ids ¶
Return the tickets an action claims to address, so they can be verified.
Task views¶
taskview ¶
Task views: what an agent is asked to do, and how that maps onto the world.
The environment always speaks tool calls. A task view sits in front of that and decides the shape an agent works in -- the general tool surface by default, or a curated schema for a particular benchmark, which it then maps down to tool calls.
Some curated actions have no single operation behind them; a view returns None for those
and the verifier scores the result instead. That is a first-class case, not a failure.
TaskView ¶
Bases: ABC
The action surface an agent works against.
to_tool_call
abstractmethod
¶
to_tool_call(
action: Any,
*,
world_id: str,
observation: Mapping[str, Any] | None = None,
) -> ToolCall | None
Map an action onto a tool call, or None when it has no direct operation.
parse_action ¶
Recover an action from a policy's text output, or None if there isn't one.
Text policies use this to turn what they emit into something the environment takes; it is deliberately the policy's side of the boundary, not the environment's.
touched_tickets ¶
The tickets this action claims to address, so they can be verified.
ToolCallTaskView ¶
Financial projection¶
economics ¶
The world's financial and throughput figures, as the reward reads them.
These are proxies. The deployment records no first-class planned cost, no realised cost, and no throughput capacity, so the figures are reconstructed from what it does record:
- Payment timeliness -- every issued invoice is planned receivable; anything not paid is the shortfall.
- Freight efficiency -- the quoted base freight is planned; surcharges over it are the overrun.
- Units processed -- completed orders plus delivered shipments.
Each source degrades to zero on its own if it is missing or fails, so a world that does not run a domain simply contributes nothing rather than ending the episode. When first-class fields arrive, only this module changes.
FinancialState
dataclass
¶
Many worlds at once¶
vector ¶
Several independent worlds, stepped at once.
A step is a network round-trip, so throughput comes from running worlds concurrently rather than from making any one of them faster.
These environments are never reset automatically, and that is the whole point. Gymnasium's stock vector environments reset a sub-environment as soon as it reports done. Here a reset destroys the world and builds a new one, so auto-resetting at every truncation would throw away the agent's accumulated work and the financial position its reward is computed from. A world is meant to outlive the rollout windows taken from it, so truncation is reported and resetting is left to whoever is driving.
Nothing in the fan-out is MORPHEUS-specific -- it drives any gymnasium.Env whose episodes
should survive truncation, which is the property that matters for a persistent environment. It
lives here rather than in the core because that is where the environments are; importing it
brings the adapter with it.
PersistentVectorEnv ¶
Bases: VectorEnv
Runs N independent persistent worlds, one environment each.
Parameters¶
env_fns: One factory per world. Factories rather than instances because each world is configured independently -- and because building them here keeps the environments this object owns unambiguous. max_workers: Size of the thread pool. Stepping is I/O-bound, so threads are the right tool; defaults to one per environment.
single_observation_space
instance-attribute
¶
observation_space
instance-attribute
¶
action_space
instance-attribute
¶
metadata
class-attribute
instance-attribute
¶
reset ¶
reset(
*,
seed: SupportsIndex
| Sequence[SupportsIndex]
| None = None,
options: dict[str, Any] | None = None,
) -> tuple[Any, dict[str, Any]]
Start an episode in every world.
step ¶
Advance every world by one step.
No world is reset when it reports truncation: that decision belongs to the caller.
The backend contract¶
ports ¶
The backend contract the environment is written against.
MorpheusEnv never imports an HTTP client or the MORPHEUS SDK directly -- it drives a
MorpheusBackend. A live deployment, a recorded fixture, and an in-memory double are
therefore interchangeable, which is what lets the environment be exercised without a server.
world_id is passed on every call rather than bound to the backend, so one backend can
serve several worlds (vectorized runs) without holding per-world state.
BackendError ¶
Bases: RuntimeError
A backend call failed.
MorpheusBackend ¶
Bases: Protocol
Everything the environment needs from a MORPHEUS deployment.
create_world ¶
Provision a world and return its id.
reset_world ¶
Destroy and reprovision the world.
This is not a rewind: the world is torn down and built again. Callers that want a long-lived world should provision once and keep stepping.
get_tasks ¶
Return the incident-ticket stream the agent remediates.
close ¶
Release any connections held. A backend with nothing to release does nothing.
fetch ¶
Read a world's domain data.
Separate from act on purpose: this is the environment reading state for its
own bookkeeping, not the agent doing something. Implementations should return the
decoded payload as-is and let the caller make sense of its shape.
get_action_space ¶
Describe the tools a service exposes, so actions can be discovered.
act ¶
act(
world_id: str,
*,
method: str,
path: str,
body: Mapping[str, Any] | None = None,
) -> Mapping[str, Any]
Run one tool call against the world.
A rejected call is reported in the returned mapping (success false) rather than
raised: an agent proposing an invalid action is an ordinary outcome to be scored,
not a transport failure.
set_chaos ¶
Change how the world behaves.
The one write outside the agent's own actions: reconfiguring a world is what makes a run non-stationary, and it is the environment's job rather than the agent's.
verify ¶
Score a ticket's remediation against its checks.
Building one from config¶
factory ¶
Build a MORPHEUS environment from scalars, so a config document can name one.
The environment takes a backend object, which is right for code and impossible for YAML. That made "selecting an environment is a line of configuration" true for a registered Gymnasium id and false for the substrate this package exists for -- exactly the wrong way round.
This closes it: scalars in, a live MorpheusEnv out.
.. code-block:: yaml
env:
id: skyfall_crl.env.morpheus:morpheus_world
kwargs:
base_url: http://localhost:8282
layout: process-outbound
max_steps: 20
reward_spec: paper
It lives on the adapter side deliberately. A factory that knows what a world is is adapter
code, and the harness reaches it the same way it reaches any other environment -- by resolving a
dotted path out of a config at build time. Nothing in skyfall_crl.train imports this, which
is asserted rather than intended.
morpheus_world ¶
morpheus_world(
*,
base_url: str = "http://localhost:8282",
timeout: float = 30.0,
headers: Mapping[str, str] | None = None,
layout: str = DEFAULT_LAYOUT,
world_id: str | None = None,
max_steps: int = DEFAULT_MAX_STEPS,
**env_kwargs: Any,
) -> MorpheusEnv
A MorpheusEnv over a live deployment, configured entirely by value.
Parameters¶
base_url, timeout, headers:
How to reach the deployment. These are the whole of HttpBackend's surface, which
is why building it from a config is possible at all.
layout:
Which world to provision, when creating one.
world_id:
An existing world to attach to. Supplied, nothing is provisioned and the environment
is a guest: it will not delete or reprovision a world it did not create.
max_steps:
The step budget after which the environment reports truncated.
**env_kwargs:
Passed through to MorpheusEnv -- reward_spec, regime, task_view,
action_scope and the rest. Anything expressible in a config can be set here; anything
that needs a live object still has to be passed in code, which is the honest boundary.
Configuration wiring¶
regime ¶
Which configuration a world is running under.
The service itself lives in skyfall_crl.regime, shared with the harness and the
metrics rather than owned by the environment. Re-exported here because this is where the
environment reaches for it.
ConstantRegime ¶
Regime
dataclass
¶
One configuration: what it is called, how long it lasts, and how it behaves.
identity
property
¶
What configuration this is, whatever it is called here.
Two entries share an identity when one repeats the other. Forgetting is measured over identities rather than labels, because meeting the same conditions twice is the whole of what it asks about.
select ¶
The axes named here that this configuration actually sets.
exclude ¶
Every axis except the ones named.
Which axes an environment can be told about is the environment's business, not the schedule's, so the schedule offers the split and lets the adapter decide where it falls.
RegimeProvider ¶
Bases: Protocol
Reports which configuration is active at a given step.
RegimeSchedule ¶
The configurations a run passes through, and when.
Parameters¶
regimes:
In order. Each needs a regime_id; duration_steps may be omitted on the last
one to mean "until the run ends". An entry with alias_of repeats another
configuration under a new name.
shift_mode:
abrupt switches at the boundary, cyclic loops back to the start, and
linear_drift eases into the next configuration over drift_window steps.
ScheduledRegime ¶
Follows a schedule written in advance -- the default way to shift a world.
schedule may be a RegimeSchedule, the mapping one
is built from, or the path to a YAML file holding it. The last two exist because a run is
selected by configuration: {"provider": "scheduled", "params": {"schedule": ...}} builds
this class directly, and a document cannot name an object. Accepting only the built form made
that path silently produce a provider that raised on its first step.
Backends — http_backend¶
http_backend ¶
A backend that talks to a live MORPHEUS deployment over HTTP.
The deployment's API is a small, stable REST surface, so this speaks to it directly with
httpx rather than through a generated client. That is a reversal of an earlier decision, and
it was made for one concrete reason: the SDK is not on PyPI, so depending on it meant a
direct-URL requirement, and a distribution carrying one of those cannot be uploaded at all. Ten
endpoints are ours to maintain against the engine's API; the alternative was a package nobody
could install.
httpx is a base dependency, so this module imports it at the top and
skyfall_crl.env.morpheus is importable on any install.
One client, not one per world. The SDK's client was bound to a world when it was
constructed, so serving several worlds from one backend meant either a client each or
re-pointing a shared one -- and re-pointing interleaved requests across worlds under threads.
Here the world id is a path segment on each call and the client holds no world state, so there
is nothing to re-point and a single connection pool is safe for the concurrent stepping
PersistentVectorEnv does.
Field names on the wire are per-field. name and layout go over as they are, while
realHoursPerSimDay, processChaosEnabled, infraChaosEnabled and ticketId are
camelCase. A wrong key is silent -- the server ignores it and applies its default -- so the
serialised bodies are pinned by test rather than trusted.
HttpBackend ¶
Drives a MORPHEUS deployment over its HTTP API.
create_world ¶
create_world(
*,
name: str,
layout: str,
description: str | None = None,
real_hours_per_sim_day: float | None = None,
process_chaos: bool = True,
infrastructure_chaos: bool = True,
**_unused: Any,
) -> str
observe ¶
observe(
world_id: str,
*,
logs: bool = True,
audit_log: bool = True,
limit: int = 100,
) -> Mapping[str, Any]
Return the world's operational and audit log slices.
Both are read as plain requests. Through the SDK they could not be read at all: its generated client validated each entry's service against a fixed set the deployment has outgrown, so asking for logs failed the whole call, and the only way to get an answer was to ask for neither -- which is why this returned an empty mapping for as long as it went through the SDK.
get_tasks ¶
List the incident tickets in a given state.
fetch ¶
Read a world's domain data, returning the decoded response as-is.
close ¶
Release the connection pool.
A client that refuses to close is not worth failing teardown over -- but leaving one
open is: the suite treats the resulting ResourceWarning as an error, which fails
whichever test happens to be running when the collector reaches it.
get_action_space ¶
Describe the tools a service exposes.
The one endpoint that is not world-scoped: the service mesh describes the deployment,
not any particular world, so world_id is accepted for the port's sake and unused.
act ¶
act(
world_id: str,
*,
method: str,
path: str,
body: Mapping[str, Any] | None = None,
) -> Mapping[str, Any]
Run one tool call against the world.
Nothing here raises. A rejected call is an outcome to score rather than a transport failure, and the same holds for a deployment that blinks: a single bad model token, or one dropped connection, should not end a run that has been training for hours.
set_chaos ¶
Push a world's chaos configuration.