Environments & Tools
Environments are the worlds agents act in. Each environment owns its state, decides what an
agent can see (get_observations), what an agent can do (get_tools), how an action resolves
(execute_action), and what cross-environment events it emits (publish_events). Three ship
in simswarm/environments/: social, market, economic.
The environment protocol
simswarm/environments/base.py defines the structural protocol every environment satisfies:
class Environment(Protocol):
name: str
def get_observations(self, agent: Agent) -> Observation: ...
def execute_action(self, agent: Agent, action: Action) -> ActionResult: ...
def get_tools(self) -> list[Tool]: ...
def publish_events(self) -> list[Event]: ...
def tick(self) -> None: ...
The engine instantiates environments in Engine._create_environments keyed by type string
("social", "market", "economic"), passing EnvironmentConfig.params straight into the
matching config dataclass.
The tool/action contract
get_tools() returns simswarm.types.Tool objects. Each Tool carries a name,
description, and a JSON-Schema parameters block, and serializes via to_openai_schema()
into the {"type": "function", "function": {...}} shape the LLM consumes. When the LLM emits
a tool call, the engine routes it by tool name to the owning environment (see
Architecture) and calls execute_action, which dispatches on
action.action_type through an internal handler dict. Unknown actions return
ActionResult(success=False, data={"error": "Unknown action: ..."}).
IDs in observations
A recurring engine convention: if an action argument needs an ID, the corresponding observation text must expose that ID. Agents only know IDs they've read in their feed.
- The social env's
get_observationsrenders each post aspost_id=<uuid> author_id=<uuid> [<name>] <text> (score: <likes-dislikes>), so the LLM can passpost_idintoreply/vote/repostandagent_id(theauthor_id) intofollow. - The market env renders
Market [<market_id>]: <question> | YES: x% | NO: y%, wheremarket_idis a deterministic slug derived from the question (_question_to_slug: lowercase, non-alphanumerics → underscores, capped at 40 chars, numeric suffix on collision). Agents reference it inbuy_shares/sell_shares/comment_on_market.
Social environment
SocialEnvironment (environments/social.py) is the default. SocialConfig controls
threading, voting_mode (default "likes_only"), feed weights
(recency_weight=0.3, popularity_weight=0.4, relevance_weight=0.3),
echo_chamber_strength=0.5, and viral_threshold=5.
Tools: create_post (text), reply (post_id, text), vote (post_id, value
enum [1, -1]), repost (post_id), follow (agent_id), do_nothing.
State & handlers: posts are stored in a dict[str, Post] keyed by a generated UUID; each
Post tracks likes, dislikes, reposts, created_round, parent_id (for threads), and
a voters set (one vote per agent — repeat votes fail). _handle_vote increments likes
when value > 0, otherwise dislikes. Posts and replies return {"post_id": <uuid>} in
their ActionResult.data — this is what apply_belief_updates and the extractors key on.
Feed ranking — _rank_feed scores top-level posts (parent_id is None) with
recency_weight * recency + popularity_weight * log1p(likes + reposts) + relevance_weight * relevance, where relevance is 1.0 for followed authors and
1.0 - echo_chamber_strength otherwise. The top 20 are shown, each with up to 3 replies when
threading is on.
Engagement & events — current_engagement() returns {post_id: (likes, dislikes)} (the
engine feeds this into belief updates). tick() emits a viral_post event the first time a
post's likes + reposts >= viral_threshold.
Market environment
MarketEnvironment (environments/market.py) is a constant-product AMM prediction market.
MarketConfig carries markets (list of {"question", "initial_price_yes"}),
initial_balance=1000.0, initial_liquidity=500.0, and price_move_event_threshold=0.1.
Each market initializes reserves from the seed price: reserve_yes = liq*2*(1-price_yes),
reserve_no = liq*2*price_yes.
The
initial_liquidity=500.0default is deliberate. At liquidity 100, a single one-sided round could peg YES to ~0%/~100% (observed in sim 127); 500 keeps consensus prices roughly within[10%, 90%]while still permitting meaningful moves.
Tools: buy_shares (market_id, outcome enum ["yes","no"], amount in USD),
sell_shares (market_id, outcome, shares), browse_markets, comment_on_market
(market_id, text), do_nothing.
AMM math lives in environments/market_amm.py. price_yes = reserve_no / (reserve_yes + reserve_no). Buying YES injects USD into reserve_no and removes YES shares, preserving
k = reserve_yes * reserve_no. Each agent gets a Portfolio (lazily registered) with a
balance and per-market YES/NO share holdings. Buys debit balance and return
{side, market_id, outcome, shares, cost, price, round}; sells credit balance and return
proceeds instead of cost. tick() emits a price_move event when
abs(price_yes - last_price) >= price_move_event_threshold.
Economic environment
EconomicEnvironment (environments/economic.py) is a rule-based macro model. EconomicConfig
has labor_force=1000 and metric_change_threshold=0.05. Each agent becomes an
EconomicActor (role, balance, workforce, price, output).
Tools: set_price (price), hire (count), fire (count), invest (amount),
allocate (target, amount), apply_policy (policy_name, description, optional
variable/value), do_nothing.
tick() snapshots the previous metrics, recomputes aggregates
(employment_rate = min(total_workforce / labor_force, 1.0), avg_price = mean of positive
prices, total_output = sum of outputs, total_investment = cumulative), then emits a
metric_change event for any metric whose absolute delta crosses metric_change_threshold.
apply_policy appends to active_policies and can set a scenario variable that surfaces in
later observations.
Observation assembly
The engine wraps each environment's Observation into the agent's context. Observation text
is what the agent reasons over, so each environment renders a compact, ID-bearing,
human-readable block. The engine additionally appends a bridge digest and a scenario
observation (from SimulationConfig.variables) before the LLM call. See
Architecture for the full per-round flow and build_context in
simswarm/llm.py for how observations become chat messages.