Storage and Result Access

New AgentSociety 2 experiments write results to two locations:

  • Replay directory: <run_dir>/replay/. ReplaySink / ReplayWriter write replay data as append-only JSONL and store the dataset catalog and column metadata in _schema.json.

  • Local working directory: files such as <run_dir>/artifacts/, <run_dir>/agents/agent_<id>/, and <run_dir>/pid.json, which store ask/intervene artifacts, agent workspaces, and runtime metadata.

  • Workspace checkpoint (for resume): each env module writes dynamic state to <run_dir>/env/<module>/state/ENV_STATE.json. Society writes the immutable initialization snapshot to <run_dir>/SOCIETY.json and per-step scalars to <run_dir>/SOCIETY_STEP.json. These files are atomic JSON writes used only by --resume and are not added to the replay catalog. Replay is append-only time-series data for analysis; workspace persistence is for recovery and overwrites the latest state.

There are three ways to read results:

  • Backend REST API /api/v1/replay/...(recommended, together with the frontend or the VSCode replay webview)

  • agentsociety2.storage.ReplayReader, which reads sharded JSONL with DuckDB.

  • Read *.json / *.jsonl files in the workspace directly for agent behavior analysis and debugging.

Note

ReplayWriter no longer writes the three legacy framework tables agent_profile, agent_status, and agent_dialog for new experiments. They remain in agentsociety2.storage.models only for ORM compatibility when reading historical databases; equivalent data in new experiments is stored as catalog-driven datasets.

Run Directory Layout

The CLI entry point python -m agentsociety2.society.cli uses the directory passed with --run-dir as the experiment HOME. ExperimentRunner creates the following structure inside it:

<run_dir>/
├── replay/
│   ├── _schema.json                 # dataset catalog + table/column metadata
│   ├── core_agent_profile.<shard>.jsonl
│   ├── <prefix>_agent_state.<shard>.jsonl
│   └── <prefix>_env_state.<shard>.jsonl
├── pid.json                         # 进程信息与终止状态
├── artifacts/                       # ask / intervene / questionnaire 产物
└── agents/
    └── agent_0001/
        ├── config.json              # 静态配置(create 时写一次)
        ├── AGENT.json               # 动态自描述快照(每步 to_workspace 更新)
        ├── AGENT_MEMORY.md          # 运行时会话摘要
        ├── memory/episodes.jsonl    # event-level memories
        ├── state/
        └── .runtime/logs/
            ├── thread_messages.jsonl
            ├── tool_calls.jsonl
            └── step_replay.jsonl

When experiment data is read through the backend, run_dir is assembled from workspace_path + hypothesis_<id>/experiment_<id>/run. The backend reads replay/_schema.json and the JSONL shards from that directory.

Replay Write Format

ReplaySink is a per-process append-only JSONL writer. ReplayProxy only carries replay_dir and the enabled flag, so it can be serialized across Ray task / actor boundaries; each consumer lazily loads its own ReplaySink inside the current process, without going through a central SQLite actor.

The top-level structure of <run_dir>/replay/_schema.json is:

{
  "tables": {
    "core_agent_profile": {
      "columns": [{"name": "id", "type": "INTEGER"}],
      "primary_key": ["id"],
      "indexes": [["name"]]
    }
  },
  "datasets": {
    "core.agent_profile": {
      "dataset_id": "core.agent_profile",
      "table_name": "core_agent_profile",
      "module_name": "AgentSociety",
      "kind": "entity_static",
      "entity_key": "id",
      "step_key": null,
      "time_key": null,
      "default_order": ["id"],
      "capabilities": ["agent_profile", "entity_static"],
      "version": 1,
      "columns": [{"name": "id", "type": "INTEGER"}]
    }
  }
}

Each physical table is written as <table_name>.<shard>.jsonl. The shard is computed from the row content CRC32, so natural ordering across shards is not stable; readers should sort explicitly with the dataset default_order, step_key, or time_key.

Common capabilities:

  • agent_profile - static agent profiles.

  • agent_snapshot - per-step agent state.

  • env_snapshot - per-step environment state.

  • timeseries - suitable for timeline expansion.

  • geo_point - contains lng / lat and can be drawn on a map.

  • trajectory - suitable for trajectory replay.

  • entity_static - one-time static entity table.

  • event_stream — event stream

Writing Data

Environment-module state data can be written through two paths: declarative (recommended) and manual registration for event streams and irregular shapes. Both paths call the same replay writer; injection is handled by AgentSociety / the environment router.

Method A: declarative _agent_state_columns / _env_state_columns

When subclassing EnvBase, declare the columns you want to write to replay on every step as class variables. Before the first write, EnvBase automatically:

  1. Derives the table-name prefix (PascalCase to snake_case, removing Space / Env / Module suffixes).

  2. Registers the schema for {prefix}_agent_state and / or {prefix}_env_state.

  3. Call register_dataset to write catalog metadata. The default capabilities are ["agent_snapshot", "timeseries"] and ["env_snapshot", "timeseries"]. If _agent_state_columns also declares both lng and lat, geo_point and trajectory are added automatically.

Inside step(), the module only needs to call EnvBase._write_agent_state(), EnvBase._write_agent_state_batch(), or EnvBase._write_env_state().

from typing import ClassVar
from agentsociety2.env import EnvBase
from agentsociety2.storage import ColumnDef


class EconomySpace(EnvBase):
    _agent_state_columns: ClassVar[list[ColumnDef]] = [
        ColumnDef("currency", "REAL", analysis_role="measure"),
        ColumnDef("income", "REAL", analysis_role="measure"),
        ColumnDef("consumption", "REAL", analysis_role="measure"),
    ]
    _env_state_columns: ClassVar[list[ColumnDef]] = [
        ColumnDef("bank_interest_rate", "REAL", analysis_role="measure"),
    ]

    async def step(self, tick: int, t):
        records = [
            {"agent_id": p.id, "currency": p.currency,
             "income": p.income, "consumption": p.consumption}
            for p in self._persons.values()
        ]
        await self._write_agent_state_batch(self._step, t, records)
        await self._write_env_state(self._step, t,
                                    bank_interest_rate=self._bank_interest_rate)
        self._step += 1

Method B: manual register_table + register_dataset

Event streams with kind="event_stream" and data with irregular shapes can still be registered manually:

from agentsociety2.storage import ColumnDef, ReplayDatasetSpec, TableSchema

schema = TableSchema(
    name="social_media_event",
    columns=[
        ColumnDef("id", "INTEGER", nullable=False),
        ColumnDef("step", "INTEGER", nullable=False, logical_type="step"),
        ColumnDef("t", "TIMESTAMP", nullable=False, logical_type="timestamp"),
        ColumnDef("sender_id", "INTEGER", logical_type="identifier"),
        ColumnDef("event_type", "TEXT", logical_type="enum",
                  enum_values=["post", "follow", "like", "comment", "repost"]),
        ColumnDef("payload", "JSON"),
    ],
    primary_key=["id"],
    indexes=[["step"], ["sender_id"], ["event_type"]],
)
await self._replay_writer.register_table(schema)
await self._replay_writer.register_dataset(
    ReplayDatasetSpec(
        dataset_id="social_media.event",
        table_name="social_media_event",
        module_name=self.name,
        kind="event_stream",
        title="Social Media Event Stream",
        entity_key="sender_id",
        step_key="step",
        time_key="t",
        default_order=["step", "id"],
        capabilities=["event_stream", "social_event"],
    ),
    schema.columns,
)

await self._replay_writer.write("social_media_event", {...})
await self._replay_writer.write_batch("social_media_event", [{...}, {...}])

Reading Data

Method 1: backend REST API (recommended)

After starting python -m agentsociety2.backend.run, the replay API is mounted at /api/v1/replay. Every request carries a workspace_path query parameter, and the backend uses it to locate <workspace_path>/hypothesis_<id>/experiment_<id>/run/replay.

Path

Role

GET /info

Experiment summary: total_steps, start_time, end_time, and agent_count

GET /datasets

Return all datasets in the current experiment together with column metadata

GET /datasets/{dataset_id}

Details for a single dataset

GET /datasets/{dataset_id}/rows

Paginated row query.

GET /panel-schema

Dataset grouping required by frontend panels / map layouts.

GET /steps/{step}/bundle

Merged snapshot for one step: all agent states, environment state, and geographic positions

GET /timeline

step t mapping for the timeline

GET /agents/profiles

List of agent profiles, read from core_agent_profile when available, otherwise derived from the primary agent-state dataset

The full prefix is /api/v1/replay/{hypothesis_id}/{experiment_id}/....

/datasets/{dataset_id}/rows supports page, page_size, order_by, desc_order, step, entity_id, start_step, end_step, max_step, columns, and latest_per_entity.

Method 2: ReplayReader + DuckDB

ReplayReader reads _schema.json and registers each dataset’s JSONL shards as DuckDB views. It preserves the metadata-driven query semantics used by the backend:

from agentsociety2.storage import ReplayReader

reader = ReplayReader("hypothesis_1/experiment_1/run/replay")
datasets = reader.load_dataset_catalog()
state = reader.get_dataset_by_id("economy.agent_state")
rows = reader.query_dataset_rows(
    state,
    page=1,
    page_size=200,
    step=42,
    columns=["agent_id", "step", "currency"],
)
reader.close()

Return-value convention: columns marked as JSON in the catalog are automatically deserialized into objects, and datetime columns are returned as ISO-8601 strings.

Method 3: read workspace files directly

<run_dir>/artifacts/ stores Markdown artifacts written by the CLI for ask, intervene, and questionnaire steps, and can be shown to researchers directly.

Common files under <run_dir>/agents/agent_<id>/:

  • agent_config.json — agent capabilities, initial state, skill visibility overrides, and activated skills

  • AGENT.json - dynamic self-description snapshot, updated by to_workspace on each step.

  • AGENT_MEMORY.md - runtime session summary.

  • memory/episodes.jsonl — event-level memories。

  • state/*.json - built-in state and user-defined state files.

  • .runtime/logs/step_replay.jsonl - tool history for each step.

  • .runtime/logs/tool_calls.jsonl - tool-call log.

  • .runtime/logs/thread_messages.jsonl - recent thread messages.

These files are useful for debugging agent behavior, reconstructing thread context, and inspecting skill execution. They are not written into the SQLite database.

Compatibility with Legacy Databases

Legacy databases from v1 or early v2 may still contain the three framework tables agent_profile, agent_status, and agent_dialog. The new backend replay API targets the run/replay directory. To read historical sqlite.db files, use the compatibility models retained in agentsociety2.storage.models or a legacy analysis script.