Skip to content

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:

<output_dir>/<YYYY-MM-DDTHH-MM-SSZ>/

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:

{ "type": "PlaceBid", "market_id": "asset", "price": "70", "quantity": "1" }
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.