Python API
Use the Python API to run a YAML experiment, supply custom information to agents, or change their accounts between periods. The examples below introduce each task, followed by the supported functions and types.
Code using await can run directly in a notebook. In a script, put it inside an async def main() function and call asyncio.run(main()) after importing asyncio.
Run an experiment
Most experiments need only two functions from the Python API.
run starts an experiment from a YAML file and returns its output directory.
load_config lets you inspect and validate the configuration before running it.
In a notebook, you can call run with await:
import aigora
run_dir = await aigora.run("examples/tutorials/first_experiment/experiment.yaml")
print(run_dir)
In a Python script, use asyncio.run(aigora.run(path)) to start the asynchronous call.
Set display=False if you do not need the live market display.
To check a configuration without making model requests, load it first:
config = aigora.load_config(
"examples/tutorials/parameter_sweeps/experiment.yaml",
variables={"reasoning_effort": "medium", "seed": 43},
)
print(config.protocol.periods)
print(config.agents[0].lm.reasoning_effort)
This returns an ExperimentConfig containing the resolved settings and an AgentConfig for each agent group.
The example prints 4 and medium.
The variables argument works the same way in run: each key must name a declared YAML variable, and its value replaces that variable for the call.
Configuration checks and runtime checks
load_config resolves variables and validates the configuration itself without making model requests.
Prompt-file loading and provider and hook construction happen when the run is prepared, so a successful configuration check does not guarantee those steps will succeed.
See YAML configuration for all fields and defaults.
run
async
run(
config_path: str | Path,
*,
display: bool = True,
variables: Mapping[str, Any] | None = None,
) -> Path
Run the experiment described by a YAML file and return its output path.
Use variables to override inputs declared in the file's top-level
variables section. These overrides follow the same rules as
load_config and apply only to this call. Set display=False to hide
the live market display, for example when running a batch.
Aigora resolves and validates the inputs before creating a run directory. The returned path contains the run's configuration, market data, and traces.
load_config
Read and validate a YAML configuration without running the experiment.
If the file declares a top-level variables section, references to those
inputs are resolved before validation. Pass overrides through variables;
each key must name a declared input and replaces that input's entire value.
Without a variables section, template-like text stays literal.
The result is an ExperimentConfig that you can inspect before making
model requests. Prompt files are loaded later, when the run is prepared,
and their contents are never interpolated.
ExperimentConfig
The validated settings returned by load_config.
This object contains the protocol, assets, markets, and agent groups after
variable substitution. Access nested settings as attributes, such as
config.protocol.periods or config.agents[0].lm.model.
AgentConfig
The shared configuration for one group of agents.
The n field determines how many participants to create. For example,
id="buyer" and n=3 create buyer_1, buyer_2, and buyer_3,
each with its own conversation and provider instances.
response
class-attribute
instance-attribute
error_handling
class-attribute
instance-attribute
Providers
A provider turns information about the experiment into text an agent can read.
To write one, implement get_context(context) and return a string.
You do not need to inherit from ContextProvider; the class below satisfies the interface by providing that method:
import aigora
class RoundMessage:
def __init__(self, *, message: str):
self.message = message
def get_context(self, context: aigora.ProviderContext) -> str:
return f"Round {context.experiment.period}: {self.message}"
Save the class in an importable module, such as my_providers.py in your working directory, and add it to an agent group's prompt.context_providers list:
Aigora passes params to the constructor as keyword arguments and creates a separate instance for each agent.
Before each decision, it calls get_context with a read-only snapshot.
In round 1, this provider returns Round 1: You may trade one unit in this round.
The custom information example develops the same idea into a provider for private-value schedules.
ProviderContext gives the provider access to the following information:
ProviderContext field |
Contains |
|---|---|
agent |
This agent's ID, holdings, orders, current-period actions, constraints, and custom information |
assets |
Asset catalogue keyed by ID, with names and descriptions |
markets |
Market snapshots keyed by ID |
experiment |
Current period and captured elapsed timing |
trades, cancellations |
Complete trade and cancellation histories for each market, including earlier periods |
previous |
Previous committed observation, or None initially |
trades_since_previous, cancellations_since_previous |
Events since that observation; empty on the first update |
observation |
Current snapshot with per-market history positions |
Cancellation records identify the order, agent, market, and positive quantity cancelled.
ProviderObservation is the saved snapshot used as the baseline for a later update.
It includes positions in the event histories so incremental providers can identify new trades and cancellations.
Providers read state; to change an agent's holdings or stored information, use a period hook.
ContextProvider
Supply a section of text for an agent's decision update.
Implement get_context on your own class and return the information the
agent should receive. The supplied context is a read-only snapshot, so
rendering an update does not change the experiment's state.
ProviderContext
dataclass
ProviderContext(
agent: AgentState,
markets: Mapping[str, MarketState],
experiment: ExperimentState,
trades: Mapping[str, tuple[Trade, ...]] = (
lambda: _mapping()
)(),
previous: ProviderObservation | None = None,
trades_since_previous: Mapping[
str, tuple[Trade, ...]
] = (lambda: _mapping())(),
cancellations: Mapping[
str, tuple[OrderCancellation, ...]
] = _mapping(),
cancellations_since_previous: Mapping[
str, tuple[OrderCancellation, ...]
] = _mapping(),
*,
assets: Mapping[str, AssetInfo],
)
The information available to a provider for the current decision.
Read agent for this participant's state and markets for the market
snapshots. The context also includes timing, asset information, event
histories, and the previous committed observation when one exists.
Snapshot mappings are read-only.
trades
class-attribute
instance-attribute
trades_since_previous
class-attribute
instance-attribute
trades_since_previous: Mapping[str, tuple[Trade, ...]] = (
field(default_factory=lambda: _mapping())
)
cancellations
class-attribute
instance-attribute
ProviderObservation
dataclass
ProviderObservation(
agent: AgentState,
markets: Mapping[str, MarketState],
experiment: ExperimentState,
trade_positions: Mapping[str, int],
cancellation_positions: Mapping[str, int] = _mapping(),
*,
assets: Mapping[str, AssetInfo],
)
A saved observation used as the baseline for a later provider update.
Alongside the agent, market, and experiment snapshots, it records how far the observation reached in each trade and cancellation history. Incremental providers use those positions to identify events the agent has not yet seen.
cancellation_positions
class-attribute
instance-attribute
Hooks
Hooks let you change the experiment at the start or end of a period.
For example, you can restore trading accounts before a round or pay a dividend after it.
Implement on_period_start(context) for a start hook or on_period_end(context) for an end hook.
The corresponding context contains the current period number, the live agents keyed by ID, and a shared seeded random-number generator, rng.
To change an account, assign a new Holdings value to agent.holdings.
To provide information for later decisions, call agent.set_information(key, text); a provider can then read it from context.agent.information.
Use the supplied rng for random assignments that should follow the experiment seed.
Start hooks run before the opening snapshot and trading clock, including in period 1. End hooks run after successful trading, before closing snapshots and order cleanup, including in the final period. Each configured list runs in order, and a failed hook stops the run without undoing changes that have already happened.
Adding multiple rounds shows a start hook that restores trading accounts. Here is an end hook that pays one cash unit for each unit of a chosen asset held by an agent:
import aigora
class Dividend:
def __init__(self, *, asset_id: str):
self.asset_id = asset_id
def on_period_end(self, context: aigora.PeriodEndContext) -> None:
for agent in context.agents.values():
holdings = agent.holdings
units = holdings.units.get(self.asset_id, 0)
agent.holdings = aigora.Holdings(
cash=holdings.cash + units,
units=holdings.units,
)
The loop reads each agent's holdings and replaces them with a new account containing the dividend.
Asset quantities stay the same.
Save this class in an importable module, such as my_hooks.py in the working directory, then add it under experiment.protocol.hooks.period_end:
In this configuration, an agent holding three units of asset receives three cash units at the end of each completed period.
PeriodStartHook
Run custom code before each trading period, including the first.
Implement on_period_start to change accounts or assign information
before the opening snapshots are recorded and the trading clock starts.
The context contains live agents and a seeded random-number generator.
PeriodStartContext
dataclass
The live agents and random-number generator supplied to a start hook.
period identifies the round that is about to begin. The agents
mapping is read-only, but its agent objects can have their holdings or
stored information changed by the hook.
PeriodEndHook
Run custom code after a period finishes successfully.
Implement on_period_end to settle accounts or store information before
the closing snapshots and order cleanup. The context contains live agents
and the experiment's seeded random-number generator.
PeriodEndContext
dataclass
The live agents and random-number generator supplied to an end hook.
period identifies the round that has just completed successfully.
The hook can update each agent before closing snapshots are recorded.
State and actions
These types describe the participants, market state, and actions that providers and hooks work with.
Agent is the live object available to hooks, while AgentState is the read-only snapshot available to providers.
The other value objects capture information at a particular point in the experiment.
| Types | What they represent |
|---|---|
Holdings, TradingConstraints |
An agent's cash and asset quantities, and the rules governing new orders |
AssetInfo, MarketState |
Asset names and descriptions, open orders, recent trades, and market rules |
ExperimentState, ElapsedTiming |
The current period and the elapsed trading time when an observation was captured |
Order, Trade, ActionRecord |
An order, a completed exchange, or a decision with its outcome |
PlaceBid, PlaceAsk |
A request to buy or sell a quantity at a specified limit price |
CancelOrder, DoNothing |
A request to cancel an order, or a decision to take no action |
DecisionContext |
The provider update and captured state supplied for a decision |
ParsedResponse, LLMResponse |
The parsed action and reason, and the model response with usage and attempt details |
For example, a PlaceBid contains a market ID, a price, and a quantity.
It describes the requested action; the market still checks whether the agent has enough cash and whether the order meets its trading constraints.
Agent
Agent(
agent_id: str,
holdings: Holdings,
*,
policy: _AgentPolicy,
trading_constraints: TradingConstraints | None = None,
)
A live participant whose account can be changed by a period hook.
Providers receive an AgentState snapshot instead. To change the live
account, assign a new Holdings value to holdings. Use
set_information to store text for subsequent provider updates.
Attributes:
| Name | Type | Description |
|---|---|---|
agent_id |
str
|
Unique identifier for this agent instance. |
holdings |
Holdings
|
Current cash and unit balances. |
trading_constraints |
TradingConstraints
|
Limits checked when the agent submits an order. |
policy |
_AgentPolicy
|
Decision policy supplied at construction. |
outstanding_orders |
dict[str, Order]
|
Outstanding orders keyed by order ID. |
action_history |
list[ActionRecord]
|
Actions taken during the current period. |
Initialize the agent with starting holdings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agent_id
|
str
|
Unique agent identifier. |
required |
holdings
|
Holdings
|
Initial holdings. Cash and unit mappings are copied. |
required |
policy
|
_AgentPolicy
|
Decision policy for this Agent. |
required |
trading_constraints
|
TradingConstraints | None
|
Optional institutional role, position, and outstanding-order limits. |
None
|
available_cash
property
Return settled cash not reserved by outstanding bids.
state
Return a read-only snapshot of this agent's current state.
The snapshot includes holdings, open orders, current-period actions, trading constraints, and stored information.
set_information
Store text that providers can read in subsequent observations.
Both the key and value must be nonblank strings. A provider accesses
the saved value through context.agent.information[key].
AgentState
dataclass
AgentState(
agent_id: str,
holdings: Holdings,
outstanding_orders: tuple[Order, ...] = (),
action_history: tuple[ActionRecord, ...] = (),
information: Mapping[str, str] = _mapping(),
trading_constraints: TradingConstraints = TradingConstraints(),
)
A read-only snapshot of one agent at the time of an observation.
It includes holdings, open orders, actions from the current period,
trading constraints, and custom information saved through
Agent.set_information. Providers read this snapshot rather than the
live agent object.
information
class-attribute
instance-attribute
AssetInfo
dataclass
An asset's configured ID, display name, and description.
Built-in providers use the name when describing the asset to agents. The description is available to custom providers but is not automatically included in an agent's update.
Holdings
dataclass
An account's settled cash and asset quantities.
units maps asset IDs to integer positions. To change an account in a
hook, create a new Holdings value and assign it to agent.holdings.
The stored mapping is read-only.
TradingConstraints
dataclass
TradingConstraints(
allowed_sides: frozenset[Literal["bid", "ask"]]
| None = None,
max_units: Mapping[str, int] = (lambda: _mapping())(),
max_outstanding_orders: int | None = None,
min_units: Mapping[str, int] = (lambda: _mapping())(),
max_order_quantity: int | None = None,
min_order_quantity: int = 1,
)
Limits checked when an agent submits an order.
These settings restrict order sides, positions, individual quantities, and the number of open orders. The market also checks available cash, inventory, and whether short selling is allowed.
allowed_sides
class-attribute
instance-attribute
max_units
class-attribute
instance-attribute
min_units
class-attribute
instance-attribute
MarketState
dataclass
MarketState(
market_id: str,
bids: tuple[Order, ...] = (),
asks: tuple[Order, ...] = (),
recent_trades: tuple[Trade, ...] = (),
short_selling_allowed: bool | None = None,
)
A snapshot of one market's open orders, recent trades, and rules.
Providers can inspect bids and asks to describe the order book.
When constructing a snapshot yourself, set short_selling_allowed so
holdings providers can determine how much an agent may sell.
ExperimentState
dataclass
ExperimentState(
experiment_id: str,
period: int,
periods_total: int,
periods_remaining: int,
timing: ElapsedTiming,
)
The experiment's identity, current period, and captured timing.
Period numbers start at 1. A provider can use period to select information
for the current round, such as a private value that changes halfway through
the experiment.
ElapsedTiming
dataclass
The trading time captured with an observation.
elapsed_seconds measures time since the period's trading clock started,
and period_duration_seconds gives the configured length of the period.
Order
dataclass
Order(
order_id: str,
agent_id: str,
market_id: str,
side: Literal["bid", "ask"],
price: Decimal,
quantity: int,
period: int,
arrival_sequence: int | None = None,
)
An order in a market, with its owner, price, and remaining quantity.
The side is bid for a buy order or ask for a sell order.
order_id identifies the order when requesting a cancellation or matching
it to a recorded trade.
Trade
dataclass
Trade(
trade_id: str,
market_id: str,
buyer_id: str,
seller_id: str,
price: Decimal,
quantity: int,
bid_order_id: str,
ask_order_id: str,
)
A completed exchange between a buyer and a seller.
The record gives the execution price and quantity, along with both agent IDs and the IDs of the bid and ask that matched.
ActionRecord
dataclass
ActionRecord(
period: int,
parsed_response: ParsedResponse,
status: str = "unknown",
rejection_reason: str | None = None,
order_id: str | None = None,
)
A decision and its outcome within a trading period.
The record pairs the parsed response with a status, an optional rejection reason, and the order ID assigned to an accepted order.
PlaceBid
dataclass
PlaceAsk
dataclass
Request to sell a quantity in a market at the given limit price.
The market checks the agent's available units, position limits, and short-selling permission before accepting it.
CancelOrder
dataclass
DoNothing
dataclass
Take no market action for this decision.
An agent can deliberately choose to wait. Aigora also uses this action to
record a failed decision when the failure policy is do_nothing.
DecisionContext
dataclass
The update text and captured state supplied to a decision policy.
prompt contains the combined provider output for this decision, while
state contains the structured ProviderContext behind that update.
The prompt alone is not the agent's full conversation history.
ParsedResponse
dataclass
ParsedResponse(
reason: str,
action: Action,
raw_text: str,
retry_attempts: int = 0,
parse_error: str | None = None,
llm_response: LLMResponse | None = None,
)
A decision's action and reason, together with its response metadata.
raw_text preserves the original response. Retry and parse-error fields
describe any failed attempts, while llm_response carries model usage
when available. The action still needs to pass the market's trading rules.
LLMResponse
dataclass
LLMResponse(
text: str,
tokens_in: int | None,
tokens_out: int | None,
latency_ms: int,
error: str | None = None,
cached_tokens_in: int | None = None,
cache_creation_tokens_in: int | None = None,
reasoning_tokens: int | None = None,
provider: str | None = None,
model: str | None = None,
finish_reason: FinishReason | None = None,
failure_kind: LLMFailureKind | None = None,
attempts: tuple[LLMAttempt, ...] = (),
conversation: ConversationTurn | None = None,
)
The model response and usage information for a decision.
This value combines the final response text with usage totals, latency,
failure information, and the individual request attempts. Usage categories
that the model provider did not report are None.
cache_creation_tokens_in
class-attribute
instance-attribute
Errors
When an experiment fails after its output directory has been reserved, run raises ExperimentRunError.
The exception gives you the partial output path in run_dir and the original execution error in cause.
This lets a batch script record a failed experiment without losing track of its files:
try:
run_dir = await aigora.run("experiment.yaml", display=False)
except aigora.ExperimentRunError as error:
print("Partial output:", error.run_dir)
print("Run failed:", error.cause)
Configuration errors happen before an output directory is reserved and are raised directly, rather than wrapped in ExperimentRunError.
AgentDecisionError describes a decision that could not be completed within the agent's retry policy.
Its parsed attribute retains the response, retry count, and failure details for the trace.
The default failure policy, abort, stops the experiment.
With do_nothing, aigora records the failure and continues without adding the failed exchange to the agent's conversation.
ExperimentRunError
Report an execution failure after the run directory was created.
run_dir points to the partial output, and cause holds the original
exception. Configuration errors raised before reserving output are passed
through directly instead of being wrapped in this error.
AgentDecisionError
Report a decision that cannot be completed within the retry policy.
A policy raises this error when it has exhausted its retries or encounters a failure that cannot be retried. An intermediate failed attempt should be handled within the policy if another attempt will follow.
The parsed response preserves the final raw text, parse error, number
of retries, and aggregate model usage when available. Aigora uses those
details to record the failed decision before applying its failure policy.
The default policy, abort, stops the run. With do_nothing, aigora
records a failed no-op and continues without committing the failed exchange
to the conversation.
Initialise the error with a human-readable message and trace payload.
message is forwarded to Exception.__init__ as usual and is
therefore available via str(exc) and exc.args[0]. parsed
is stored as self.parsed and is accessed directly by callers —
typically the trace logger inside AsyncPeriodRunner — to record
token usage, retry counts, and the
last error before the configured failure policy is applied.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
str
|
A human-readable description of the failure, typically including the final error string from the last attempt. |
required |
parsed
|
ParsedResponse
|
A fully populated |
required |
Read traces
Each agent's trace contains both decision outcomes and conversation events. Use iter_decisions to read only the completed decisions, for example after the first tutorial's call has returned run_dir:
from aigora.traces import iter_decisions, iter_trace_records
trace = run_dir / "traces" / "buyer_1.jsonl"
for decision in iter_decisions(trace):
print(decision["period"], decision["parsed_action"], decision["action_status"])
To inspect the complete event history and reconstruct model requests, use iter_trace_records with request expansion enabled:
for record in iter_trace_records(trace, expand_requests=True):
for attempt in record.get("attempts", []):
if "request_messages" in attempt:
print(attempt["request_messages"])
Read from the beginning before filtering
The reader reconstructs requests from earlier records in the same file. Pass the complete trace to iter_trace_records and filter the records it yields, so request expansion still has the earlier messages it needs.
See Output files for the field definitions and ordering rules.
aigora.traces.iter_decisions
Read decision outcomes from an agent's trace, one record at a time.
This selects decision_resolved events and leaves out conversation
events. Use it to inspect actions, supplied reasons, and decision usage
without counting the same decision's conversation event again.
Model request deltas are not expanded by this reader.
aigora.traces.iter_trace_records
iter_trace_records(
path: str | Path, *, expand_requests: bool = False
) -> Iterator[dict[str, Any]]
Read every event from one agent's trace in its original order.
Set expand_requests=True to reconstruct complete model requests from
the recorded deltas. Read from the beginning before filtering events,
because later requests can reuse messages from earlier ones.
Expansion restores only messages present in the trace.