251 lines
10 KiB
Python
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?
|
|
"""
|