marla.models

Plan Maker backends (local/remote), prompt construction, and response parsing.

Dependency-injection interface for Plan Maker inference backends.

The Plan Maker agent depends on this interface, not on any concrete model library – local_backend.py (HF transformers) and remote_backend.py (API-based, not implemented in v0.1) both satisfy it, and tests can inject a trivial fake without touching either.

class marla.models.plan_maker_backend.BackendResponse(raw_text: 'str', latency_ms: 'float')[source]

Bases: object

Parameters:
latency_ms: float
raw_text: str
class marla.models.plan_maker_backend.PlanMakerBackend(*args, **kwargs)[source]

Bases: Protocol

async generate(prompt, legal_action_ids)[source]

Generate a response scoring every one of legal_action_ids.

Backends that size their own generation budget (see local_backend.py) need the actual action count – and the actual ID strings, not just how many there are, since a small model’s response is one JSON entry per action ID and longer IDs (e.g. exploit:host-2-0:e_elasticsearch vs finish) cost more tokens – to avoid truncating the response as an episode’s legal action space grows mid-run.

Parameters:
Return type:

BackendResponse

Constructs the configured Plan Maker backend (spec section 18: device resolution is local).

marla.models.backend_factory.build_backend(model_config, device)[source]
Parameters:
Return type:

PlanMakerBackend

Local HuggingFace transformers backend for the Plan Maker (Milestone 9).

Deliberate deviation from spec section 4.1 (“blocking Plan Maker inference must not block the shared asyncio loop… should run via asyncio.to_thread”): in this SPADE deployment, asyncio.to_thread (and an explicit ThreadPoolExecutor – it isn’t the default-executor path specifically) reproducibly hangs indefinitely when used to run a real transformers generate() call from inside SPADE’s multi-agent async context, which unconditionally runs on uvloop with no supported way to disable it. A trivial non-torch call through the same executor (e.g. asyncio.to_thread(time.sleep, ...)) does not hang, so this is specific to torch/transformers generation off the main thread here, not threading or pyjabber/presence in general (both were isolated and ruled out).

Given the spec’s own “no elapsed timeout” design, a hang here is silent and permanent for a real training run – far worse than the brief blocking delay of running generation directly on the event loop thread, which is otherwise idle during inference anyway (nothing else can usefully proceed without the Plan Maker’s response). This backend therefore calls generate() synchronously. Device resolution follows the same cpu | gpu | auto semantics as every other locally executed model (spec section 18): a gpu request that can’t actually initialize CUDA raises before the agent ever loads weights, let alone reports ready.

class marla.models.local_backend.LocalTransformersBackend(model_name, device, max_new_tokens=512)[source]

Bases: object

Loads a chat-instruct causal LM and generates via a plain user-turn prompt.

Parameters:
  • model_name (str)

  • device (DeviceRequest)

  • max_new_tokens (int)

async generate(prompt, legal_action_ids)[source]
Parameters:
Return type:

BackendResponse

Remote API Plan Maker backend – not implemented in this release.

The config schema’s model.backend: local | remote is a real, spec-defined choice, but the first release only ships the local HF transformers backend (see local_backend.py). This stub keeps backend: remote a valid, documented configuration value that fails loudly and early rather than silently pretending to produce advice.

class marla.models.remote_backend.RemoteApiBackend(model_name)[source]

Bases: object

Parameters:

model_name (str)

async generate(prompt, legal_action_ids)[source]
Parameters:
Return type:

BackendResponse

Plan Maker prompt construction (spec section 9).

The model is only asked to produce the {action_id: score} mapping – the surrounding advisory-response envelope (schema/run/request IDs, model and knowledge versions, latency) is assembled deterministically by the Plan Maker agent itself, never hallucinated by the LM.

marla.models.prompt.build_correction_prompt(original_prompt, reason, detail, legal_action_ids)[source]
Parameters:
Return type:

str

marla.models.prompt.build_prompt(retrieved_rules, objective, observation, legal_actions)[source]
Parameters:
Return type:

str

Best-effort extraction of a scores mapping from raw (possibly noisy) LM output.

An unparsable or malformed response is a normal, expected outcome – the Gatekeeper’s correction loop exists precisely to handle it – so this module never raises; it returns None/drops entries it can’t make sense of.

marla.models.response_parser.coerce_scores(raw_scores)[source]

Best-effort coercion of a raw {action_id: value} mapping to floats.

Entries that can’t be coerced are dropped (not defaulted) – the Gatekeeper’s exact-coverage check will then correctly flag the response as invalid rather than MARLA silently inventing a score.

Parameters:

raw_scores (dict)

Return type:

dict[str, float]

marla.models.response_parser.extract_json_object(raw_text)[source]

Extract the first top-level JSON object found in raw_text, if any.

Parameters:

raw_text (str)

Return type:

dict | None