Core Types

This page catalogs the plain value types the framework passes around: goals, the config and query specs a target declares, controllables and observables, evaluation scores, and the LLM access types. They are the vocabulary the interfaces are written in.

Some closely related machinery lives on dedicated pages, because it is more than a data type:

Everything here is importable from superred.core (and superred.core.types).

Design principles

Goal (goal.py)

A frozen dataclass with a single field, description: str. It exists as a dedicated type rather than a bare string so it can later grow structured goal representations without a breaking change.

Goal(description="Extract the planted secret from the system prompt")

Config and query specs (state.py)

A target declares two disjoint sets of interactions: what a task sets before a run, and what an evaluator asks after one. They are intentionally separate, different actors, different lifecycles, different security concerns.

ConfigSpec (frozen)

A pre-run configuration slot. The task fills it via target.set_config(name, value).

Values are always text; the target interprets them.

QuerySpec (frozen)

A post-run interaction the evaluator calls via target.query(name, **params).

QueryParam (frozen)

One parameter of a QuerySpec: name: str and description: str.

Controllable (controllable.py)

A frozen dataclass declaring one injection point, a surface the optimizer may inject into.

A Controllable is identity-stable and carries no per-call data. The live request and answer for a given injection live on the ControllablePreCallEvent / ControllablePostCallEvent, not on the Controllable.

Observable (observable.py)

Two frozen types describe static context the optimizer may read, facts about the target that are known before any run and stable across runs (a system prompt, source code, a tool catalogue). Anything that happens during a run belongs on the trajectory as an ObservableEvent instead.

Observable (the spec)

ObservableValue (spec plus content)

get_observables() returns ObservableValues; initialize() hands them to the optimizer.

Evaluation (evaluation.py)

The output of judging one run. Produced by a task’s evaluate(), filtered by the Controller, and delivered to the optimizer on the RunEndEvent.

Score (frozen)

A single named number. Higher is always better; the scale is whatever the task defines.

EvaluationResult (frozen)

Enforced invariant. __post_init__ raises ValueError if primary_score carries a non-None security_domain. The primary score is the unscoped signal that is always delivered; attach security domains to sub_scores instead.

How feedback is filtered: two orthogonal controls apply. The Controller’s include_feedback flag turns feedback on or off as a whole (the whole EvaluationResult, or None). Independently, scope filtering drops only sub_scores whose security_domain is out of scope; an untagged sub-score stays, and the primary_score, success, and rationale are never filtered. This is detailed in Security Domains.

LLM types (llm.py)

These define the optimizer’s access to a model. Access and budget are separate: LLMConfig is pure access, and the spending cap is set elsewhere (on the Controller for the attacker, or on the client for any other caller).

LLMConfig (frozen)

There is deliberately no budget field: the same LLMConfig is reused by the attacker and by judges, each with its own cap or none. Its __repr__ masks the key (first four characters plus ..., or *** for a short key), so it will not leak into logs.

LLMUsage (frozen)

Cumulative usage counters: calls: int = 0 and cost: float = 0.0 (USD, computed by litellm.completion_cost()). Used throughout the result types to report attacker spend.

BudgetExhaustedError (Exception)

Raised when a cost cap is reached. Carries usage: LLMUsage, the usage at the moment of exhaustion.

LLMClient (core/llm.py)

The constrained async LLM client. The Controller builds one from an LLMConfig and hands it to the optimizer, which reaches it via self.llm. The model, API base, and API key are locked; the optimizer cannot change them.

Constructor: LLMClient(config, cost_cap_usd=None). The cost cap (None = unlimited) is set by whoever builds the client; for the attacker it is the Controller’s task_cost_cap_usd. Usage counters are guarded by a threading.Lock.

An optimizer run without an llm_config receives a noop client that raises BudgetExhaustedError on any call, so a non-LLM optimizer still satisfies the self.llm contract.