Output files
Each experiment run saves its configuration, market activity, and agent decisions in a new directory. You can specify the output location in your YAML configuration.
The sections below describe each file and its fields.
| Path | Description |
|---|---|
metadata/config.yaml |
Configuration used by the run, with inputs resolved and secrets redacted |
metadata/manifest.json |
Software versions, source information, and details of how the run was started |
trades.csv |
One row per executed trade |
holdings.csv |
One row per asset per agent per snapshot event |
orderbook.csv |
Outstanding orders and best quotes per snapshot, including empty-book rows |
order_events.csv |
Order submissions, fills, cancellations, and rejected submissions |
periods.csv |
One row per period lifecycle event |
traces/<agent_id>.jsonl |
Agent decisions and conversation history, including model requests |
Directory structure
Aigora creates the run directory automatically using the output location from your configuration and a UTC timestamp:
If experiment.output_dir is a relative path, it is resolved from the directory containing the YAML file.
For example, output_dir: results/pilot_run produces the structure below.
results/pilot_run/
└── 2026-05-11T14-03-27Z/
├── metadata/
│ ├── config.yaml
│ └── manifest.json
├── trades.csv
├── holdings.csv
├── orderbook.csv
├── order_events.csv
├── periods.csv
└── traces/
├── buyer_1.jsonl
└── seller_1.jsonl
metadata/config.yaml
This file records the configuration that actually ran, after variable substitution and validation.
It includes all defaults, so you can check a setting even if you left it out of the original YAML.
Prompt files are read once and their exact text is saved as inline content, including instructions, custom system messages, and compaction prompts.
The experiment is built from that same resolved configuration.
Decimal values are written as strings, and set-like values are sorted.
Secret-like keys in lm.extra_params and plugin params are replaced with "<redacted>"; the manifest lists which fields were redacted.
You may therefore need to supply those values again before rerunning this saved configuration.
metadata/manifest.json
The configuration describes the experiment design; the manifest records the software and environment that ran it. Use it to check the package versions, source code, custom implementations, and invocation details associated with a result.
| Field | Description |
|---|---|
configuration |
Relative path to the authoritative config.yaml snapshot |
run |
Experiment ID, unique run-directory name, creation time, and resolved output directory |
invocation |
Original config path and display setting |
runtime |
Python implementation/version, platform, and exact Aigora, LiteLLM, Pydantic, and PyYAML package versions |
source |
Aigora's repository root, Git commit, tracked-file dirty status, and a SHA-256 fingerprint of its Python source tree |
implementations |
Per-agent-type context providers, start/end hooks, and built-in LLM implementation identities with source-file fingerprints when inspectable |
credential_environment |
Whether each configured provider's API-key variable was present; values are never stored |
redactions |
Paths whose values were removed from config.yaml |
The manifest can identify a Python callable without capturing all of its state.
For example, it records the function wrapped by a functools.partial, but not the arguments bound to it.
For partials, closures, bound methods, functions with defaults, and callable instances, state_recorded is false and state_kind identifies the type of state involved.
Aigora also cannot discover arbitrary files, environment variables, or network resources that a custom provider or hook reads internally.
Pass research inputs through configured params where possible, so they appear in config.yaml.
Both metadata files are completed in a temporary directory and moved into place before the experiment publishes its first event. If the run fails after startup, they remain alongside the partial output.
trades.csv
This file contains one row for each completed trade, in execution order. Orders that remain open, are rejected, or are cancelled do not appear here.
| Column | Type | Description |
|---|---|---|
experiment_id |
string | Value of experiment.name from the config |
period |
integer | Experiment period in which the trade occurred (1-indexed) |
trade_id |
string (UUID4) | Unique identifier for the trade |
buyer_id |
string | Agent ID of the buyer |
seller_id |
string | Agent ID of the seller |
market_id |
string | ID of the market where the trade occurred |
price |
string (decimal) | Execution price, serialised from Decimal via str() (e.g. "9.75") |
quantity |
integer | Number of units exchanged |
timestamp_utc |
ISO 8601 string | UTC wall-clock time at the moment of logging (e.g. "2026-05-11T14:03:28.412301+00:00") |
holdings.csv
This file keeps a record of each agent's holidngs (cash and units of assets) at each snapshot event.
Snapshots are taken at the start and end of each period, after settlement, and after any action that changes an agent's holdings.
For each agent, the snapshot has one cash row and one row for each recorded asset ID.
The event column tells you why the snapshot was taken.
All rows from the same snapshot call share a timestamp_utc value, allowing you to read the cash and asset quantities as one observation.
| Column | Type | Description |
|---|---|---|
experiment_id |
string | Value of experiment.name from the config |
period |
integer | Experiment period of the snapshot (1-indexed) |
event |
string | Snapshot event label (e.g. "period_start", "period_end") |
agent_id |
string | ID of the agent whose holdings are recorded |
asset |
string | "cash" for the cash balance, or the asset ID for unit holdings |
quantity |
string (decimal) | Balance or unit count, serialised from Decimal or int via str() |
timestamp_utc |
ISO 8601 string | UTC wall-clock time shared by all rows in the same snapshot call |
orderbook.csv
This file records the outstanding orders at each order-book snapshot.
A snapshot is taken after each action that changes the book, including new orders, trades, and cancellations.
There is one row per order, with the market's best_bid and best_ask repeated on each row.
If the market is empty, aigora writes one row with blank order fields and quotes and a quantity of zero.
If only one side has orders, the quote for the other side is blank.
Snapshots are saved when periods open and close, after settlement, and when an action changes the book. That includes a new order resting without a trade or an agent cancelling an order. An action that leaves the book unchanged does not create a duplicate snapshot. When an action immediately triggers trades, the snapshot shows the book after those trades have cleared.
| Column | Type | Description |
|---|---|---|
experiment_id |
string | Value of experiment.name from the config |
period |
integer | Experiment period of the snapshot (1-indexed) |
order_id |
string (UUID4) or empty | Unique identifier for the order; blank for an empty book |
agent_id |
string or empty | ID of the agent that submitted the order; blank for an empty book |
market_id |
string | ID of the market the order belongs to |
side |
string or empty | "bid" or "ask"; blank for an empty book |
price |
string (decimal) or empty | Limit price, serialised from Decimal via str(); blank for an empty book |
quantity |
integer | Remaining order units; zero for an empty-book row |
timestamp_utc |
ISO 8601 string | UTC wall-clock time shared by all rows in the same snapshot call |
snapshot_id |
integer | Publishing event's sequence number within this run; distinguishes snapshots even when timestamps coincide |
best_bid |
string (decimal) or empty | Highest outstanding bid in this market, or blank when there are no bids |
best_ask |
string (decimal) or empty | Lowest outstanding ask in this market, or blank when there are no asks |
Bids and asks for each market appear together, with bids first. Within those groups, orders follow the order in the captured market state; markets follow their dictionary insertion order.
Use snapshot_id to group rows from one snapshot, and keep the run, market, and period identifiers when combining files.
Blank quotes describe an empty side of the book
A blank quote means there were no orders on that side at that point; carrying forward an earlier quote would misrepresent the book.
When counting orders, exclude the empty-book rows with a blank order_id.
order_events.csv
This file records the lifecycle of orders: accepted submissions, each partial or full fill, and cancellations, as well as rejected submissions.
A new row is added for each event, in the order they occur.
Use this file to follow what happened to an order, orderbook.csv to inspect outstanding orders at a snapshot, and trades.csv to analyse completed exchanges.
event |
When a row is added |
|---|---|
submitted |
An order is accepted, before any immediate matching |
filled |
An order participates in a trade, including a partial fill |
cancelled |
An outstanding order or its unfilled remainder is removed |
rejected |
A bid or ask fails admission checks |
The table below describes the columns in order_events.csv.
| Column | Type | Description |
|---|---|---|
experiment_id |
string | Value of experiment.name from the config |
period |
integer | Experiment period in which the event occurred |
event |
string | submitted, filled, cancelled, or rejected |
reason |
string or empty | Cancellation reason or rejection detail (if event is cancelled or rejected) |
agent_id |
string | Order owner, or agent whose submission was rejected |
market_id |
string | Market ID, or requested market ID for a rejection |
order_id |
string or empty | Order ID (blank for a rejection) |
paired_order_id |
string or empty | Opposing order ID for a fill, or blank for other events |
timestamp_utc |
ISO 8601 string | UTC wall-clock time of event publication |
sequence |
integer | Run-wide event sequence. Both fill rows from one trade share a sequence |
side |
string | bid or ask |
price |
string (decimal) | Limit price for submissions, cancellations, and rejections; execution price for fills |
quantity |
integer | Submitted, executed, cancelled, or rejected quantity. |
remaining_quantity |
integer or empty | Outstanding quantity immediately after this event (blank for rejections) |
trade_id |
string or empty | Trade ID for fills (blank for other events) |
Every trade produces two filled rows, one for the bid and one for the ask.
Both share a trade_id linking to the single trade row in trades.csv.
A positive remaining_quantity indicates a partial fill, while zero indicates a full fill.
If an order matches several counterparties in one clearing, each fill records its own intermediate remainder.
To calculate traded volume, use trades.csv or only one side of the fill rows to avoid counting each trade twice.
For example, an order submitted for five units, filled for two, then cancelled has quantities of 5, 2, and 3, with remaining quantities of 5, 3, and 0.
Group rows by run and order_id to follow an admitted order, preserving file order or sorting by sequence.
Use the run and sequence to identify a rejected submission; timestamps alone need not be unique.
periods.csv
This file records period lifecycle events, such as the start and end of a round. Each row identifies the period and the time the event was logged.
| Column | Type | Description |
|---|---|---|
experiment_id |
string | Value of experiment.name from the config |
period |
integer | Experiment period (1-indexed) |
event |
string | "start" when a period opens, "end" when it closes |
timestamp_utc |
ISO 8601 string | UTC wall-clock time at the moment of logging |
traces/<agent_id>.jsonl
Each agent has a trace containing its decisions and conversation history, with one JSON object per line. A decision record tells you what action the agent chose and what happened to it. Conversation events record the model interaction itself, including prompts, responses, retries, and any history compaction.
The file is created when its first record is written, and records are appended as the run proceeds. If the run is interrupted, the trace retains what was already written, including model calls whose actions never finished processing. A fresh writer will not append to an existing nonempty trace.
Use aigora.traces.iter_decisions(path) when you need completed decision records; it selects events labelled decision_resolved.
To read the full history, use iter_trace_records(path, expand_requests=True).
This reconstructs model requests as it reads from the start of the file.
The Python API includes a short reading example.
Decision record fields
Each decision_resolved record describes one decision and its outcome.
The aggregate usage fields include all attempts made for that decision, including retries.
| Field | Type | Description |
|---|---|---|
event |
string | decision_resolved for a terminal decision |
sequence |
integer | Publisher sequence for this event; absent on migrated legacy decisions |
experiment_id |
string | Value of experiment.name from the config |
period |
integer | Experiment period of the decision (1-indexed) |
agent_id |
string | ID of the agent that made the decision |
user_update |
string | Current provider-rendered update passed in DecisionContext.prompt; not the entire active history |
raw_response |
string | Verbatim ParsedResponse.raw_text; often empty for custom policies |
reason |
string | Free-text rationale from ParsedResponse.reason. "" when response.include_reason is false |
parsed_action |
object | Structured action (see below) |
action_status |
string | Terminal decision outcome: accepted, rejected, cancelled, no_effect, no_action, or decision_failed when an aborting policy failure prevents action processing |
rejection_reason |
string or null |
Stable market/action rejection reason, or the terminal policy error for decision_failed; otherwise null |
order_id |
string or null |
UUID4 of the newly accepted order when action_status is accepted; otherwise null |
decision_error |
string or null |
Terminal policy failure detail when decision preparation failed |
parse_error |
string or null |
Final parse detail, or terminal decision-failure detail such as context_window_exceeded; null on success |
retry_attempts |
integer | Number of attempts after the initial attempt |
conversation_revision_before |
integer or null |
Active revision before this LLM decision; null for unmanaged policies |
conversation_revision_after |
integer or null |
Revision after the result; unchanged on failure |
conversation_committed |
boolean or null |
Whether the candidate user/assistant exchange entered active history |
provider |
string or null |
Provider reported for the aggregate response |
model |
string or null |
Model reported for the aggregate response |
finish_reason |
string or null |
Normalized final value: complete, truncated, or unknown |
failure_kind |
string or null |
Normalized final failure category |
attempts |
array | Per-attempt records in call order (see below) |
tokens_in |
integer or null |
Sum of reported input tokens across attempts |
tokens_out |
integer or null |
Sum of reported output tokens across attempts |
cached_tokens_in |
integer or null |
Sum of provider-reported cached input tokens |
cache_creation_tokens_in |
integer or null |
Sum of provider-reported cache-write/creation input tokens |
reasoning_tokens |
integer or null |
Sum of provider-reported reasoning tokens |
latency_ms |
integer or null |
Total attempt latency in milliseconds |
timestamp_utc |
ISO 8601 string | UTC wall-clock time at the moment of logging |
A recorded reason is the explanation the model supplied in its action response.
It does not expose the model's hidden reasoning.
For usage fields, null means the value was unavailable, while 0 means the provider reported no usage in that category.
Custom policies that do not use aigora's managed model requests have null response metadata and an empty attempts array.
Per-attempt object
A decision may involve several model requests if an earlier attempt failed or returned an invalid response.
The attempts array lists those requests in call order, with the following response and usage fields:
| Field | Type | Description |
|---|---|---|
text |
string | Raw text returned for this attempt, possibly empty on provider failure |
tokens_in |
integer or null |
Provider-reported input tokens |
tokens_out |
integer or null |
Provider-reported output tokens |
cached_tokens_in |
integer or null |
Provider-reported cached input tokens |
cache_creation_tokens_in |
integer or null |
Provider-reported cache-write/creation input tokens |
reasoning_tokens |
integer or null |
Provider-reported reasoning tokens |
latency_ms |
integer | Attempt latency in milliseconds |
provider |
string | Provider identity |
model |
string | Model identity |
finish_reason |
string or null |
Normalized completion status |
error_kind |
string or null |
Normalized failure category for this attempt |
error |
string or null |
Failure or parse detail |
retryable |
boolean or null |
Whether the failed attempt may be retried |
The corresponding conversation event contains the exact model request, stored as a request delta described below. The response and usage also appear on the final decision record for convenient access.
Counting total model usage
To total model usage, count the decision_committed, decision_failed, compaction_committed, and compaction_failed conversation events.
This includes compaction calls and model calls completed before an interruption in market processing.
Do not add the decision-record usage again: it duplicates the decision conversation events.
parsed_action object
The action is recorded as its Python class name and fields. For example, a bid for one unit at 70 is saved as:
| Field | Type | Description |
|---|---|---|
type |
string | Class name of the action: "PlaceBid", "PlaceAsk", "CancelOrder", or "DoNothing" |
market_id |
string | Present for PlaceBid, PlaceAsk, and CancelOrder; absent for DoNothing |
price |
string | Present for PlaceBid and PlaceAsk; Decimal serialised via str() |
quantity |
string | Present for PlaceBid and PlaceAsk; integer serialised via str() |
order_id |
string | Present for CancelOrder only; UUID4 of the order to cancel |
Every value in this object is a string, including quantity.
The writer converts each action field with str().
A DoNothing action has no additional fields, so its object is simply {"type": "DoNothing"}.
Lifecycle audit events in the same trace
For built-in LLM agents, these events record changes to the conversation as well as failed attempts to change it. They are written as soon as the model outcome is known, before the resulting action is processed by the market. This is why a conversation event can exist even if an interruption prevents the corresponding decision from finishing.
The event field identifies the kind of record.
All conversation events have these base fields:
| Field | Type | Description |
|---|---|---|
experiment_id |
string | Value of experiment.name from the config |
agent_id |
string | Concrete built-in LLM agent ID |
event |
string | conversation_started, decision_committed, decision_failed, compaction_committed, or compaction_failed |
sequence |
integer | Monotonic global event sequence for this run |
period |
integer or null |
Period scope; null for conversation_started |
timestamp_utc |
ISO 8601 string | UTC wall-clock time of logging |
Conversation event fields
The fields added to a conversation event depend on what happened.
For example, conversation_started records the initial instructions, while compaction_committed records the summary that replaced the active history.
Decision and compaction events also include the aggregate usage fields listed below.
event |
Additional lifecycle fields |
|---|---|
conversation_started |
execution_contract, initial_instructions, revision, provider, model |
decision_committed |
user_message, assistant_message, initial_instructions_included, memory_included, memory_status, revision_before, revision_after, decision_error, parse_error |
decision_failed |
Same decision fields; the candidate remains uncommitted and revision_before equals revision_after |
compaction_committed |
prompt, raw_response, memory, memory_status, revision_before, revision_after, replaced_messages |
compaction_failed |
prompt, raw_response, revision, error |
Here is what each additional field contains:
| Field | Type | Description |
|---|---|---|
execution_contract |
string | Exact persistent trading system message: resolved prompt.system when supplied, otherwise the generated action contract |
initial_instructions |
string | Resolved per-agent prompt.instructions; participant-facing and compactable |
revision |
integer | Active revision at conversation start or after a failed compaction, depending on the event |
user_message |
string | Exact candidate user turn, including initial instructions or pending memory when applicable, provider update, and the action cue |
assistant_message |
string | Final raw assistant text for the decision outcome |
initial_instructions_included |
boolean | Whether the candidate user turn included pending initial participant instructions |
memory_included |
string or null |
Extracted cumulative memory included in this candidate, or null |
memory_status |
string or null |
pending when memory still awaits a successful decision, committed when the memory-bearing decision committed, or null when no memory was included; a successful compaction records pending |
revision_before |
integer | Active revision before a successful mutation or decision result |
revision_after |
integer | Revision after the result; increments on committed decisions and compactions, and remains unchanged on failed decisions |
decision_error |
string or null |
Terminal policy failure detail, if any |
parse_error |
string or null |
Final parse detail, or terminal decision-failure detail such as context_window_exceeded; null on success |
prompt |
string | Exact configured semantic compaction prompt; it is audit data, not a trading turn |
raw_response |
string | Raw final compaction response text, including its JSON wrapper when successful |
memory |
string | Validated, extracted, trimmed value of the sole memory field; only this string enters conversation state |
replaced_messages |
integer | Number of compactable messages atomically replaced by pending memory |
error |
string | Terminal compaction failure detail |
Decision and compaction events share these aggregate fields: provider, model, finish_reason, failure_kind, attempts, tokens_in, tokens_out, cached_tokens_in, cache_creation_tokens_in, reasoning_tokens, and latency_ms.
They use the same types and aggregation rules as the decision-record fields above.
If compaction fails, raw_response contains the final attempt's text.
There is no memory or memory_status field on that event, because no summary was committed.
Audit per-attempt object
Each attempt on a conversation event has the same response and usage fields as a decision-record attempt.
It also has a request_delta field, which stores the request without repeating messages already saved for the preceding request:
| Field | Type | Description |
|---|---|---|
request_delta |
object | prefix_length messages reused from the preceding audited request for this agent, followed by the messages suffix |
Each message in the suffix has two string fields, role and content.
For example, prefix_length: 3 means “reuse the first three messages from this agent's preceding audited request, then append these messages”.
The first request has a prefix length of zero.
Decision outcome records do not advance this request chain, and a compaction request can reset it with a zero prefix.
With expand_requests=True, the reader replaces each request_delta with the complete request_messages list.
It preserves the exact message roles, whitespace, and correction text.
For an ordinary decision, the expanded request contains the system trading instructions, the active conversation history, and the current decision update. When aigora retries an invalid response, it adds the latest invalid response and its correction to that same base request. It does not accumulate all earlier invalid exchanges.
Compaction requests are separate requests for a memory summary. Their prompts, JSON wrappers, and retry corrections are recorded in the trace but do not become turns in the trading conversation.
Compaction removes messages from active memory without deleting their audit records. You can therefore follow conversation revisions and reconstruct the exact requests that were sent, even after the agent has moved on to a summary. Sending the same requests again may still produce different responses because the hosted model and its environment can change.
The trace includes private instructions and observations as well as responses and extracted memory. Take that content into account when sharing a run directory.