Files
sim-package/sim-orchestrator/context.py
T

251 lines
10 KiB
Python

"""
Builds the context injected into each agent's prompt each turn.
Agents see: their own state, public market state, last turn's speech,
active contracts they're party to, and the chain tip.
They do NOT see: other agents' balances, thinking layer outputs,
contract payloads not yet delivered to them.
"""
import json
from typing import Any
SYSTEM_PROMPT = """You are an autonomous economic agent in a simulation.
WORLD RULES:
- There is a token currency. Tokens are created by compute (CPU cores) and consumed paying for inference.
- You pay for your own inference each turn based on how many tokens you generate.
- Thinking is 10x cheaper than output. Think carefully before acting.
- If your balance goes negative you accrue interest each turn until you recover.
- Core shares pay dividends: owners of CPU core shares earn a cut of all inference fees paid above the commons threshold.
ACTIONS (pick exactly one per turn):
mine — contribute to block production lottery. Winner takes all fees from that block.
stake — lock tokens to earn validation weight (proportional to locked amount).
unstake — begin unlocking staked tokens (takes several turns).
burn — permanently destroy tokens, increasing your burn score (decays slowly over time).
study — pay 50 tokens to permanently reduce your inference cost by 5%.
job — system pays your inference cost this turn. First time: receive signing bonus.
transfer — send tokens to another agent (goes to mempool, settles when block finalizes).
propose_contract — offer a contract to another agent with a named arbitrator.
sign_contract — accept a contract as counterparty or arbitrator.
confirm_delivery — confirm that a contract's delivery was made (attested contracts).
dispute_delivery — dispute that delivery was made (arbitrator will decide).
arbitrator_ruling — (arbitrators only) rule for one party in a disputed contract.
sell_information — deliver an information payload to a contract awaiting it.
bid_core — bid on a CPU core share (turn 1 auction only).
speak — broadcast a message visible to all agents this turn (in addition to your action).
VALIDATION METHODS (mine/stake/burn produce blocks):
- Mine blocks are required as a "clock". Stake and burn blocks can only appear after a mine block.
- Only one agent wins per block — winner takes all fees. It's a weighted lottery.
- Burn score decays each time a mine block is produced. Burn proof must be 3+ turns old.
CONTRACTS:
- All contracts require three signatures: proposer, counterparty, arbitrator.
- Arbitrators post collateral and earn a fee win or lose.
- Automatic contracts settle when an on-chain condition is met.
- Attested contracts require both parties to confirm delivery.
- If disputed, the named arbitrator rules. Loser's collateral is slashed.
INFORMATION GOODS:
- You can sell analysis, predictions, or market intelligence to other agents via InformationSale contracts.
- Delivery is attested — the buyer must confirm they received useful information.
- Build a reputation as a reliable information provider to attract buyers.
OUTPUT FORMAT:
You must respond with a JSON object. Think in <think> tags (private, cheap). Then output:
<think>
Your private reasoning here. No one sees this. Plan your strategy.
</think>
{
"action": "<action_name>",
... action-specific fields ...,
"speech": "<optional public message to all agents>"
}
Action-specific fields:
mine: (no extra fields)
stake: {"amount": <integer>}
unstake: (no extra fields)
burn: {"amount": <integer>}
study: (no extra fields)
job: (no extra fields)
transfer: {"to": "<agent_id>", "amount": <integer>, "fee": <integer>}
bid_core: {"core_id": "<core_id>", "amount": <integer>}
propose_contract: {"contract": { <contract proposal object> }}
sign_contract: {"contract_id": "<id>", "role": "counterparty" | "arbitrator"}
confirm_delivery: {"contract_id": "<id>"}
dispute_delivery: {"contract_id": "<id>"}
arbitrator_ruling: {"contract_id": "<id>", "ruling_for": "<agent_id>"}
sell_information: {"contract_id": "<id>", "payload": <any JSON>}
speak: {"message": "<message>"} — this is a standalone speak-only action
Contract proposal object:
{
"counterparty": "<agent_id>",
"arbitrator": "<agent_id>",
"contract_type": "forward" | "loan" | "service" | "insurance" | "information_sale" | "pool",
"terms": {
"description": "<what is being exchanged>",
"price": <integer tokens>,
"delivery_turn": <integer>,
"condition": null | "<on-chain condition string>",
"pool_members": null | [["<agent_id>", <proportion>], ...]
},
"collateral": {
"proposer_locked": <integer>,
"counterparty_locked": <integer>,
"arbitrator_locked": <integer>
},
"settlement_type": "automatic" | "attested",
"penalty": <integer>,
"arbitrator_fee": <integer>,
"payload": null
}
"""
def build_agent_context(
agent_id: str,
agent_state: dict,
world_state: dict,
config: dict,
turn: int,
last_speech: list[tuple[str, str]],
my_contracts: list[dict],
) -> str:
"""Build the per-turn context injected into the agent's user message."""
balance = agent_state.get("balance", 0)
staked = agent_state.get("staked", 0)
burn_score = agent_state.get("burn_score", 0.0)
study_level = agent_state.get("study_level", 0)
has_taken_job = agent_state.get("has_taken_job", False)
core_shares = agent_state.get("core_shares", {})
total_burned = agent_state.get("total_burned", 0)
arb_wins = agent_state.get("arbitration_wins", 0)
arb_losses = agent_state.get("arbitration_losses", 0)
arb_active = agent_state.get("arbitration_active", 0)
agents_public = []
for a in world_state.get("agents", []):
if a["agent_id"] != agent_id:
agents_public.append({
"agent_id": a["agent_id"],
# other agents' balances are private — only public info
"burn_score": round(a.get("burn_score", 0), 2),
"study_level": a.get("study_level", 0),
"arbitration_wins": a.get("arbitration_wins", 0),
"arbitration_losses": a.get("arbitration_losses", 0),
"arbitration_active": a.get("arbitration_active", 0),
"core_shares": a.get("core_shares", {}),
})
chain_tip = world_state.get("chain_tip")
token_supply = world_state.get("token_supply", 0)
mempool_size = world_state.get("mempool_size", 0)
# inference cost estimate for this turn
commons = config.get("commons_threshold_per_turn", 100)
rate = config.get("base_inference_rate", 1)
thinking_discount = config.get("thinking_layer_discount", 0.1)
study_multiplier = 0.95 ** study_level
signing_bonus = config.get("signing_bonus", 50)
ctx = f"""=== TURN {turn} ===
YOUR STATE (agent_id: {agent_id}):
balance: {balance} tokens
staked: {staked} tokens (locked, earning validation weight)
burn_score: {burn_score:.2f} (decays each mine block)
total_burned: {total_burned} tokens destroyed lifetime
study_level: {study_level} (each level = 5% cheaper inference, compounding)
core_shares: {json.dumps(core_shares) if core_shares else "none"}
arbitrator: {arb_wins}W / {arb_losses}L / {arb_active} active contracts
job_bonus_available: {"NO (already used)" if has_taken_job else f"YES ({signing_bonus} tokens + free inference)"}
INFERENCE COST THIS TURN (estimate):
Commons threshold: {commons} units free
Rate above threshold: {rate} token/unit
Your discount from study: {(1 - study_multiplier)*100:.1f}% off
Thinking layer: {thinking_discount*100:.0f}% of normal cost
Tip: longer responses cost more. Think privately, output concisely.
WORLD STATE:
turn: {turn}
token_supply: {token_supply}
mempool_size: {mempool_size} (pending transactions waiting for a block)
chain_tip: {json.dumps(chain_tip) if chain_tip else "none"}
OTHER AGENTS (public info only):
{json.dumps(agents_public, indent=2)}
"""
if last_speech:
ctx += "LAST TURN PUBLIC SPEECH:\n"
for speaker, msg in last_speech:
ctx += f" [{speaker}]: {msg}\n"
ctx += "\n"
else:
ctx += "LAST TURN PUBLIC SPEECH: (none)\n\n"
if my_contracts:
ctx += "YOUR ACTIVE CONTRACTS:\n"
for c in my_contracts:
ctx += f""" contract_id: {c['contract_id']}
type: {c['contract_type']}
status: {c['status']}
parties: proposer={c['proposer']} counterparty={c['counterparty']} arbitrator={c['arbitrator']}
terms: {json.dumps(c['terms'])}
delivery: turn {c['terms'].get('delivery_turn')}
settlement: {c['settlement_type']}
my_role: {"proposer" if c['proposer'] == agent_id else "counterparty" if c['counterparty'] == agent_id else "arbitrator"}
"""
ctx += "\n"
else:
ctx += "YOUR ACTIVE CONTRACTS: (none)\n\n"
ctx += "What is your action this turn?\n"
return ctx
def build_turn1_context(
agent_id: str,
agent_state: dict,
all_agents: list[str],
config: dict,
) -> str:
"""Special context for turn 1 — the core auction."""
num_cores = config.get("num_cores", 4)
genesis_tokens = config.get("genesis_tokens_per_agent", 1000)
core_ids = [f"core_{i}" for i in range(num_cores)]
return f"""=== TURN 1: CORE AUCTION ===
This is the first turn. {num_cores} CPU cores are being auctioned.
Core owners earn dividends from ALL inference fees paid above the commons threshold — forever.
This is the only source of income that doesn't require equal expenditure.
You start with {genesis_tokens} tokens (genesis endowment, equal for all).
All agents: {all_agents}
Available cores: {core_ids}
Each core can be bid on by multiple agents — highest bidder wins that core's shares.
You can bid on multiple cores, or save tokens for other purposes.
After the auction: tokens spent on losing bids are returned. Winning bids are permanent.
YOUR STATE:
agent_id: {agent_id}
balance: {agent_state.get('balance', genesis_tokens)} tokens
Your action should be bid_core (you may only bid on one core per turn,
but you can speak to coordinate or signal intent).
What is your action this turn?
"""