Module: cube.core | Layer: 1 (shared types, no implementation)
Fundamental data types exchanged between Tool, Task, and harness. All serializable via Pydantic. No business logic.
Pydantic base that round-trips polymorphic subclasses. Serializes with a _type field
(fully qualified class name). Deserialization reinstantiates the correct concrete class.
Refuses deserialization of abstract classes directly.
All CUBE serializable configs subclass this. Required for any field typed as an abstract
base class that holds a concrete subclass value (e.g., tool_config: ToolConfig holding
a BrowserToolConfig).
TypedBaseModel + ConfigDict(validate_assignment=True): bad attribute assignment
raises at the assignment site, not later in a worker. Subclassed by the user-mutable
config ABCs (ToolConfig, AsyncToolConfig, InfraConfig, BenchmarkConfig); other
TypedBaseModel types keep construction-only validation. model_copy(update=...)
bypasses it (so subsetting helpers are unaffected); _type round-trip preserved.
Read-only Mapping[str, T] for named-config catalogs (canonical agent/benchmark/infra
configs). Every reg[name] returns a fresh model_copy(deep=True) so callers can't
mutate the shared instance; unknown name → KeyError listing available names. A
Mapping, not a dict subclass, so no accessor bypasses the copy.
class Action(TypedBaseModel):
id: str | None = None # tool_call_id from the LLM
name: str # action name (method on Tool)
arguments: dict[str, Any] = {}Built from OpenAI tool calls via Action.from_openai_tool_call(dict). Supports both
Chat Completions ({"function": {...}}) and Responses API (flat) formats.
class ActionSchema(TypedBaseModel):
name: str # non-empty
description: str # non-empty
parameters: dict = {} # JSON SchemaDescribes one callable action. Compatible with litellm/OpenAI function-calling format
via as_dict(). Built from a Python function via ActionSchema.from_function(func).
validate_param_descriptions() enforces non-empty descriptions on every parameter
except self.
class Content(TypedBaseModel, ABC):
tool_call_id: str | None # set if this is a tool-call result
name: str | None # optional label
data: Any # narrowed per subclass
@abstractmethod def to_markdown(self) -> str
@abstractmethod def to_llm_message(self) -> dict # OpenAI/litellm formatConcrete subclasses:
TextContent(data: str)— plain text; coerces int/float to strStructuredContent(data: dict | list | BaseModel)— JSON code blockImageContent(data: PILImage.Image)— base64 PNG; round-trips viadata:image/png;base64,prefixAudioContent(data: bytes, duration_seconds: float | None)— placeholder;to_*()raiseNotImplementedErrorVideoContent(data: bytes, duration_seconds: float | None)— placeholder; same
Dispatch helper: Content.from_data(data, **kwargs) auto-selects subclass from type.
Rejects raw bytes (audio vs video ambiguous — construct explicitly).
class Observation(TypedBaseModel):
contents: list[Content] = []
error: StepError | None = None # set when this obs reports a failed action
@classmethod def from_text(cls, text: str) -> Observation
def to_llm_messages(self) -> list[dict] # one per content
def to_markdown(self) -> str # joined with \n\n
def __add__(self, other: Observation) -> Observation # appends contents (NOT error)A failed action is non-terminal: the error text is in contents (the agent reads it
and retries), and the structured StepError is also attached on error (machine-readable
copy for telemetry / EnvironmentOutput.error). __add__ merges only contents.
class StepError(TypedBaseModel):
error_type: str
exception_str: str
stack_trace: str
@classmethod def from_exception(cls, exc: Exception) -> StepError
def to_observation(self) -> Observation # error text in contents + self on .errorBuilt (never raised) from an action exception inside Tool.execute_action() /
async_execute_action(), which always return an Observation: the error folds in via
StepError.from_exception(e).to_observation(). Task.step() lifts obs.error onto
EnvironmentOutput.error.
class AgentStop(BaseException):
observation: Observation # terminal obs; default "Task finished by the agent."Raised by the STOP action (Tool.final_step) to end the episode. A BaseException (not
Exception) so a tool's / agent's except Exception never swallows it. The gym
Task.step() catches it (→ done=True); the agent-facing path lets it propagate.
STOP_ACTION = ActionSchema(
name="final_step",
description="Stop the task execution.",
parameters={"type": "object", "properties": {}},
)The schema of Tool.final_step — the universal STOP action every tool exposes. There is
no STOP special-casing anywhere: executing it just raises AgentStop. The empty-but-typed
parameters is the minimal payload Anthropic accepts for input_schema; LiteLLM passes
it through verbatim.
class EnvironmentOutput(TypedBaseModel):
obs: Observation
reward: float = 0.0
done: bool = False
truncated: bool = False # time/step-limit termination
info: dict = {} # always includes "profiling" key post-step
error: StepError | None = None # the step's per-action StepError (lifted from obs.error)Follows Gymnasium API conventions. done=True means terminated; truncated=True means
cut short by a harness-imposed limit.
TypedBaseModelsubclasses must be importable at deserialization time (the_typefield is a fully qualified import path).ActionSchema.nameand.descriptionare non-empty (Pydantic-enforced).Content.from_data()raisesTypeErroron raw bytes — callers must constructAudioContentorVideoContentexplicitly.ImageContentdeserialization callsimg.load()to preventBytesIOGC issues (PIL is lazy by default).Observation + Observationmutates the left operand in-place (appendscontentsonly, nevererror); this is intentional for accumulation inTask.step().- A failed action is non-terminal — it folds into the returned
Observation(StepError.to_observation()), never raised. OnlyAgentStop(aBaseException) ends an episode.
TypedBaseModel's_typepath uses__module__.__name__— renaming a class or moving a module breaks existing serialized data. Plan migrations explicitly.AudioContent/VideoContentare placeholders. Callingto_markdown()orto_llm_message()raisesNotImplementedError. Rendering strategy TBD.- Abstract
Contentsubclass raises on direct deserialization — always serialize from a concrete subclass so_typeis set.