Skip to content

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 serve to read it locally, or read the docstrings in the module itself.

The environment

morpheus_env

A Gymnasium environment over a live MORPHEUS world.

DEFAULT_LAYOUT module-attribute

DEFAULT_LAYOUT = 'process-outbound'

DEFAULT_MAX_STEPS module-attribute

DEFAULT_MAX_STEPS = 256

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.

metadata class-attribute instance-attribute
metadata = {'render_modes': []}
observation_space instance-attribute
observation_space = self._projector.space()
action_space instance-attribute
action_space = self._task_view.action_space()
world_id property
world_id: str | None

The world being driven, or None before the first reset.

owns_world property
owns_world: bool

Whether this environment created the world, and may therefore dispose of it.

reward_spec property
reward_spec: RewardSpec | None

The spec scoring this run, once a world has settled it.

regime_id property
regime_id: str

The active configuration's id. Reported for logging; withheld from the policy.

step_index property
step_index: int

Steps taken since the last reset.

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
step(
    action: Any,
) -> tuple[
    dict[str, Any], float, bool, bool, dict[str, Any]
]

Advance one step against the world.

describe
describe() -> dict[str, Any]

What this run is configured with, for logging and for recording alongside a policy.

close
close() -> None

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

PRIORITIES: tuple[str, ...] = (
    "low",
    "medium",
    "high",
    "critical",
)

OPEN_STATUSES module-attribute

OPEN_STATUSES: tuple[str, ...] = (
    "new",
    "open",
    "in_progress",
    "on_hold",
)

ObservedTicket dataclass

One incident ticket, normalised out of whatever the deployment reported.

ticket_id instance-attribute
ticket_id: str
status instance-attribute
status: str
priority instance-attribute
priority: str
chaos_type class-attribute instance-attribute
chaos_type: str | None = None
affected_service class-attribute instance-attribute
affected_service: str | None = None
created_at class-attribute instance-attribute
created_at: str = ''
updated_at class-attribute instance-attribute
updated_at: str = ''
due_at class-attribute instance-attribute
due_at: str | None = None
from_payload classmethod
from_payload(
    payload: Mapping[str, Any],
) -> ObservedTicket | None

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.

summary
summary() -> dict[str, Any]

The form carried in the observation payload.

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.

max_steps instance-attribute
max_steps = int(max_steps)
max_open_tickets instance-attribute
max_open_tickets = int(max_open_tickets)
max_tickets_in_payload instance-attribute
max_tickets_in_payload = int(max_tickets_in_payload)
max_payload_chars instance-attribute
max_payload_chars = int(max_payload_chars)
space
space() -> spaces.Dict

The observation space. Identical for every world, layout, and configuration.

project
project(
    tickets: Sequence[ObservedTicket],
    *,
    step: int,
    mean_verifier_progress: float = 0.0,
    recent_invalid_actions: int = 0,
) -> dict[str, Any]

Project outstanding tickets into an observation.

normalize_tickets

normalize_tickets(payloads: Any) -> list[ObservedTicket]

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

ALLOWED_METHODS: tuple[str, ...] = (
    "GET",
    "POST",
    "PUT",
    "PATCH",
    "DELETE",
)

DEFAULT_DOMAINS module-attribute

DEFAULT_DOMAINS: tuple[str, ...] = (
    "tickets",
    "erp",
    "wms",
    "tms",
    "finance",
    "ledger",
    "planning",
    "logs",
)

TEXT_CHARSET module-attribute

TEXT_CHARSET = string.printable

ToolCall dataclass

One domain operation against a world.

method instance-attribute
method: str
path instance-attribute
path: str
body class-attribute instance-attribute
body: Mapping[str, Any] | None = None
query_params class-attribute instance-attribute
query_params: Mapping[str, Any] | None = None
as_dict
as_dict() -> dict[str, Any]

The call as plain data, for recording in a trace.

resolved
resolved(world_id: str) -> ToolCall

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.

valid instance-attribute
valid: bool
executed instance-attribute
executed: bool
reason class-attribute instance-attribute
reason: str = ''
tool_call class-attribute instance-attribute
tool_call: ToolCall | None = None
result class-attribute instance-attribute
result: Mapping[str, Any] = field(default_factory=dict)
touched_tickets class-attribute instance-attribute
touched_tickets: tuple[str, ...] = ()
succeeded property
succeeded: bool

Whether the world accepted the call.

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.

domains class-attribute instance-attribute
domains: tuple[str, ...] = DEFAULT_DOMAINS
denied_suffixes class-attribute instance-attribute
denied_suffixes: tuple[str, ...] = ('/execute',)
denied_segments class-attribute instance-attribute
denied_segments: tuple[str, ...] = ('verification',)
permits
permits(path: str, world_id: str) -> tuple[bool, str]

Return whether path is in scope, and why not when it is not.

validate
validate(call: ToolCall, world_id: str) -> tuple[bool, str]

Return whether call may be executed against world_id.

parse_tool_call

parse_tool_call(text: str) -> ToolCall | None

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

extract_ticket_ids(
    action: Mapping[str, Any] | None,
) -> tuple[str, ...]

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.

DEFAULT_PATH_CHARS module-attribute

DEFAULT_PATH_CHARS = 256

DEFAULT_BODY_CHARS module-attribute

DEFAULT_BODY_CHARS = 4096

TaskView

Bases: ABC

The action surface an agent works against.

action_space abstractmethod
action_space() -> spaces.Space

The space actions are drawn from.

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
parse_action(text: str) -> Any | None

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
touched_tickets(action: Any) -> tuple[str, ...]

The tickets this action claims to address, so they can be verified.

ToolCallTaskView

Bases: TaskView

The default view: the world's tool surface, expressed directly.

max_path_chars instance-attribute
max_path_chars = int(max_path_chars)
max_body_chars instance-attribute
max_body_chars = int(max_body_chars)
action_space
action_space() -> spaces.Space
to_tool_call
to_tool_call(
    action: Any,
    *,
    world_id: str,
    observation: Mapping[str, Any] | None = None,
) -> ToolCall | None
touched_tickets
touched_tickets(action: Any) -> tuple[str, ...]

Read the ticket out of the path when the action does not name one.

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.

Read module-attribute

Read = Callable[[str], Any]

FinancialState dataclass

A world's financial position at one moment.

planned_cost class-attribute instance-attribute
planned_cost: float = 0.0
actual_cost class-attribute instance-attribute
actual_cost: float = 0.0
units_processed class-attribute instance-attribute
units_processed: float = 0.0
total_revenue class-attribute instance-attribute
total_revenue: float = 0.0
total_costs class-attribute instance-attribute
total_costs: float = 0.0
delta
delta(earlier: FinancialState) -> FinancialState

The movement since earlier, which is what an episode is scored on.

as_ledger
as_ledger() -> dict[str, float]

The mapping the ledger and profit signals read.

as_throughput
as_throughput(capacity: float) -> dict[str, float]

The mapping the throughput signal reads.

FinancialProjector

Reads a world's domains and reconstructs the figures the reward needs.

orders_path instance-attribute
orders_path = orders_path
invoices_path instance-attribute
invoices_path = invoices_path
shipments_path instance-attribute
shipments_path = shipments_path
project
project(read: Read, world_id: str) -> FinancialState

Build the current position, tolerating any domain that is unavailable.

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.

envs instance-attribute
envs: list[Env] = [make() for make in env_fns]
num_envs instance-attribute
num_envs = len(self.envs)
single_observation_space instance-attribute
single_observation_space = self.envs[0].observation_space
single_action_space instance-attribute
single_action_space = self.envs[0].action_space
observation_space instance-attribute
observation_space = batch_space(
    self.single_observation_space, self.num_envs
)
action_space instance-attribute
action_space = batch_space(
    self.single_action_space, self.num_envs
)
metadata class-attribute instance-attribute
metadata = {
    **self.envs[0].metadata,
    "autoreset_mode": AutoresetMode.DISABLED,
}
closed instance-attribute
closed = False
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
step(
    actions: Any,
) -> tuple[
    Any, np.ndarray, np.ndarray, np.ndarray, dict[str, Any]
]

Advance every world by one step.

No world is reset when it reports truncation: that decision belongs to the caller.

close
close(**kwargs: Any) -> None

Release every world, each on its own terms about disposal.

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
create_world(
    *, name: str, layout: str, **options: Any
) -> str

Provision a world and return its id.

reset_world
reset_world(world_id: str) -> None

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.

delete_world
delete_world(world_id: str) -> None

Tear the world down.

observe
observe(world_id: str) -> Mapping[str, Any]

Return the world's current state payload.

get_tasks
get_tasks(
    world_id: str, *, status: str = "new", limit: int = 20
) -> Sequence[Mapping[str, Any]]

Return the incident-ticket stream the agent remediates.

close
close() -> None

Release any connections held. A backend with nothing to release does nothing.

fetch
fetch(world_id: str, path: str) -> Any

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
get_action_space(
    world_id: str,
    *,
    service: str,
    action: str | None = None,
) -> Mapping[str, Any]

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
set_chaos(world_id: str, config: Mapping[str, Any]) -> None

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
verify(
    world_id: str, *, ticket_id: str
) -> Mapping[str, Any]

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.

DEFAULT_REGIME_ID module-attribute

DEFAULT_REGIME_ID = 'baseline'

ConstantRegime

A world whose configuration never changes.

A run under this provider is stationary -- a useful control, and not a continual-learning setting.

regime_id
regime_id(step: int) -> str
regime_at
regime_at(step: int) -> Regime

Regime dataclass

One configuration: what it is called, how long it lasts, and how it behaves.

regime_id instance-attribute
regime_id: str
duration_steps class-attribute instance-attribute
duration_steps: int | None = None
axes class-attribute instance-attribute
axes: Mapping[str, Any] = field(default_factory=dict)
origin class-attribute instance-attribute
origin: str | None = None
identity property
identity: str

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.

get
get(name: str, default: Any = None) -> Any
select
select(names: Collection[str] | str) -> dict[str, Any]

The axes named here that this configuration actually sets.

exclude
exclude(names: Collection[str] | str) -> dict[str, Any]

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.

to_dict
to_dict() -> dict[str, Any]

RegimeProvider

Bases: Protocol

Reports which configuration is active at a given step.

regime_id
regime_id(step: int) -> str

The active configuration's name.

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.

shift_mode instance-attribute
shift_mode = shift_mode
drift_window instance-attribute
drift_window = max(int(drift_window), 1)
seed instance-attribute
seed = int(seed)
regimes property
regimes: tuple[Regime, ...]
total_steps property
total_steps: int

Steps before the schedule repeats, or 0 if the last configuration runs forever.

from_dict classmethod
from_dict(config: Mapping[str, Any]) -> RegimeSchedule
from_yaml_file classmethod
from_yaml_file(path: str | Path) -> RegimeSchedule
regime_at
regime_at(step: int) -> Regime

The configuration active at step.

regime_id
regime_id(step: int) -> str

The active configuration's name -- the reporting contract the environment uses.

is_boundary
is_boundary(step: int) -> bool

Whether step is where a configuration begins.

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.

schedule instance-attribute
schedule = _as_schedule(schedule)
from_yaml_file classmethod
from_yaml_file(path: str) -> ScheduledRegime
from_dict classmethod
from_dict(config: Mapping[str, Any]) -> ScheduledRegime
regime_id
regime_id(step: int) -> str
regime_at
regime_at(step: int) -> Regime
is_boundary
is_boundary(step: int) -> bool

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.

DEFAULT_BASE_URL module-attribute

DEFAULT_BASE_URL = 'http://localhost:8282'

HttpBackend

Drives a MORPHEUS deployment over its HTTP API.

base_url instance-attribute
base_url = base_url
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
reset_world
reset_world(world_id: str) -> None
delete_world
delete_world(world_id: str) -> None
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
get_tasks(
    world_id: str, *, status: str = "new", limit: int = 20
) -> Sequence[Mapping[str, Any]]

List the incident tickets in a given state.

fetch
fetch(world_id: str, path: str) -> Any

Read a world's domain data, returning the decoded response as-is.

close
close() -> None

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
get_action_space(
    world_id: str,
    *,
    service: str,
    action: str | None = None,
) -> Mapping[str, Any]

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
set_chaos(world_id: str, config: Mapping[str, Any]) -> None

Push a world's chaos configuration.

verify
verify(
    world_id: str, *, ticket_id: str
) -> Mapping[str, Any]