This is the single source of truth for every data shape, API route, and constant in this project. All four workstreams build against these definitions. Field names, types, and enum values are final once committed — do not change them without announcing to the full team.
For AI coding agents: Treat every code block in this file as a specification you must implement exactly. Do not invent field names, do not change types, do not add optional fields unless they appear here. When in doubt, the code block wins over any prose description.
Authoritative path precedence: This file > TEAM_DIVISION.md > ARCHITECTURE_FLOW.md
- Enums & String Literals
- Shared Constants
- Pydantic Schemas (Backend — Python)
- TypeScript Interfaces (Frontend)
- Agent Action Shapes (LLM Output Contract)
- API Routes
- LLMClient Interface
- Error Response Shape
- Producer / Consumer Map
Produced by: Person 1 (schemas). Consumed by: Everyone.
from typing import Literal
AgentArchetype = Literal[
"bayesian_updater",
"trend_follower",
"contrarian",
"data_skeptic",
"narrative_focused",
"quantitative_analyst",
]
SimulationStatus = Literal["pending", "building", "running", "complete", "failed"]
ClaimStance = Literal["yes", "no"]
AgentAction = Literal["update_belief", "share_claim"]export type AgentArchetype =
| "bayesian_updater"
| "trend_follower"
| "contrarian"
| "data_skeptic"
| "narrative_focused"
| "quantitative_analyst";
export type SimulationStatus = "pending" | "building" | "running" | "complete" | "failed";
export type ClaimStance = "yes" | "no";
export type AgentAction = "update_belief" | "share_claim";Produced by: Person 3 (enforces in simulation logic). Consumed by: Everyone — do not hardcode these values inline, import or copy from here.
TOTAL_TICKS: int = 30
TOTAL_AGENTS: int = 12
ARCHETYPES: list[str] = [
"bayesian_updater",
"trend_follower",
"contrarian",
"data_skeptic",
"narrative_focused",
"quantitative_analyst",
]
AGENTS_PER_ARCHETYPE: int = 2 # Always 2 of each archetype
VISIBLE_CLAIMS_PER_STANCE: int = 4 # Top 4 yes + top 4 no shown to each agent per tick
CLAIM_RANKING_WEIGHT_STRENGTH: float = 0.7
CLAIM_RANKING_WEIGHT_NOVELTY: float = 0.3
# Ranking score = 0.7 * strength_score + 0.3 * novelty_score
TRUST_SHARE_DELTA: float = 0.02 # Sharer's trust toward recipient increases
TRUST_IGNORE_DELTA: float = -0.01 # Receiver's trust toward sender decreases if ignored
INITIAL_TRUST_MIN: float = 0.4
INITIAL_TRUST_MAX: float = 0.8
INITIAL_BELIEF_MIN: float = 0.35
INITIAL_BELIEF_MAX: float = 0.65
FACTION_THRESHOLD: float = 0.08 # Agents within 0.08 of each other form a factionexport const TOTAL_TICKS = 30;
export const TOTAL_AGENTS = 12;
export const AGENTS_PER_ARCHETYPE = 2;
export const VISIBLE_CLAIMS_PER_STANCE = 4;
export const CLAIM_RANKING_WEIGHT_STRENGTH = 0.7;
export const CLAIM_RANKING_WEIGHT_NOVELTY = 0.3;
export const TRUST_SHARE_DELTA = 0.02;
export const TRUST_IGNORE_DELTA = -0.01;
export const FACTION_THRESHOLD = 0.08;
export const POLLING_INTERVAL_MS = 2000;File locations:
app/schemas/market.py— MarketResponse, ClaimsGenerateResponseapp/schemas/claim.py— ClaimSchemaapp/schemas/simulation.py— everything else belowapp/schemas/report.py— ReportResponse
Import path pattern: from app.schemas.simulation import SimulationResponse
Produced by: Person 2 (Apollo service). Stored by: Person 1 in agent.professional_background JSON column.
from pydantic import BaseModel
class ProfessionalBackground(BaseModel):
title: str
company: str
industry: str
apollo_enriched: bool # True if sourced from Apollo.io; False if K2-generated syntheticProduced by: Person 2 (claims generator). Stored by: Person 1. Read by: Person 3 (simulation) and Person 4 (UI).
from pydantic import BaseModel, Field
from uuid import UUID
from typing import Literal
class ClaimSchema(BaseModel):
id: UUID
text: str
stance: Literal["yes", "no"]
strength_score: float = Field(ge=0.0, le=1.0)
novelty_score: float = Field(ge=0.0, le=1.0)Produced by: Person 3 (world builder). Read by: Person 4 (displayed in replay sidebar).
from pydantic import BaseModel, Field
from uuid import UUID
from typing import Literal
class AgentSummary(BaseModel):
id: UUID
name: str
archetype: Literal[
"bayesian_updater", "trend_follower", "contrarian",
"data_skeptic", "narrative_focused", "quantitative_analyst"
]
initial_belief: float = Field(ge=0.0, le=1.0)
current_belief: float = Field(ge=0.0, le=1.0)
confidence: float = Field(ge=0.0, le=1.0)
professional_background: ProfessionalBackgroundProduced by: Person 3 (simulation runner, one per agent per tick). Read by: Person 4 (debate feed).
from pydantic import BaseModel, Field
from uuid import UUID
from typing import Literal
class AgentTickState(BaseModel):
agent_id: UUID
name: str
belief: float = Field(ge=0.0, le=1.0) # agent's belief at end of this tick
confidence: float = Field(ge=0.0, le=1.0)
action_taken: Literal["update_belief", "share_claim"]
reasoning: str # shown in the debate feedProduced by: Person 3 (simulation runner). Read by: Person 4 (debate feed, shown as claim passing between agents).
from pydantic import BaseModel
from uuid import UUID
class ClaimShareRecord(BaseModel):
from_agent_id: UUID
from_agent_name: str
to_agent_id: UUID
to_agent_name: str
claim_id: UUID
claim_text: str
commentary: str # the sender's commentary attached to the share
tick: int # tick when the share was created (recipient sees it on tick+1)Produced by: Person 3 (simulation runner, after each tick's trust recalculation). Read by: Person 4 (trust network visualization).
from pydantic import BaseModel
from uuid import UUID
class TrustUpdate(BaseModel):
from_agent_id: UUID
to_agent_id: UUID
old_trust: float
new_trust: floatProduced by: Person 3. Stored by: Person 1 in simulation.tick_data JSON column. Read by: Person 4 (entire replay UI).
from pydantic import BaseModel
from uuid import UUID
from typing import List
class TickSnapshot(BaseModel):
tick: int # 1–30
agent_states: List[AgentTickState] # one entry per agent (always 12)
claim_shares: List[ClaimShareRecord] # shares that occurred this tick
trust_updates: List[TrustUpdate] # trust changes that occurred this tick
faction_clusters: List[List[UUID]] # groups of agent_ids with beliefs within FACTION_THRESHOLDProduced by: Person 1 (market import endpoint). Read by: Person 4.
from pydantic import BaseModel
from uuid import UUID
class MarketResponse(BaseModel):
id: UUID
session_id: UUID # the AnalysisSession created alongside the market
polymarket_id: str # the market slug/id from Polymarket
question: str
resolution_criteria: str
current_probability: float # Polymarket's current implied probability (0.0–1.0)
volume: float # total trading volume in USDProduced by: Person 1 route + Person 2 service. Read by: Person 4.
from pydantic import BaseModel
from uuid import UUID
from typing import List
class ClaimsGenerateResponse(BaseModel):
session_id: UUID
market_id: UUID
claims: List[ClaimSchema]Produced by: Person 1 (route). Updated tick-by-tick by: Person 3. Polled by: Person 4 every 2 seconds.
from pydantic import BaseModel
from uuid import UUID
from typing import List, Optional
from datetime import datetime
from typing import Literal
class SimulationResponse(BaseModel):
id: UUID
session_id: UUID
market_id: UUID
status: Literal["pending", "building", "running", "complete", "failed"]
current_tick: int
total_ticks: int # always 30
agents: List[AgentSummary] # populated after build-world; empty list before
tick_data: List[TickSnapshot] # grows as simulation runs; empty list before start
created_at: datetime
completed_at: Optional[datetime] # None until status == "complete"Produced by: Person 3 (report agent). Stored by: Person 1. Read by: Person 4.
from pydantic import BaseModel
from uuid import UUID
from typing import List
class ReportResponse(BaseModel):
id: UUID
simulation_id: UUID
market_probability: float # Polymarket's number at time of market import
simulation_probability: float # average agent belief at tick 30
summary: str
key_drivers: List[str]
faction_analysis: str
trust_insights: str
recommendation: strFile: frontend/lib/types.ts. Owned by: Person 4. UUID fields are string in TypeScript.
export interface ProfessionalBackground {
title: string;
company: string;
industry: string;
apollo_enriched: boolean;
}
export interface ClaimSchema {
id: string;
text: string;
stance: ClaimStance;
strength_score: number;
novelty_score: number;
}
export interface AgentSummary {
id: string;
name: string;
archetype: AgentArchetype;
initial_belief: number;
current_belief: number;
confidence: number;
professional_background: ProfessionalBackground;
}
export interface AgentTickState {
agent_id: string;
name: string;
belief: number;
confidence: number;
action_taken: AgentAction;
reasoning: string;
}
export interface ClaimShareRecord {
from_agent_id: string;
from_agent_name: string;
to_agent_id: string;
to_agent_name: string;
claim_id: string;
claim_text: string;
commentary: string;
tick: number;
}
export interface TrustUpdate {
from_agent_id: string;
to_agent_id: string;
old_trust: number;
new_trust: number;
}
export interface TickSnapshot {
tick: number;
agent_states: AgentTickState[];
claim_shares: ClaimShareRecord[];
trust_updates: TrustUpdate[];
faction_clusters: string[][];
}
export interface MarketResponse {
id: string;
session_id: string;
polymarket_id: string;
question: string;
resolution_criteria: string;
current_probability: number;
volume: number;
}
export interface ClaimsGenerateResponse {
session_id: string;
market_id: string;
claims: ClaimSchema[];
}
export interface SimulationResponse {
id: string;
session_id: string;
market_id: string;
status: SimulationStatus;
current_tick: number;
total_ticks: number;
agents: AgentSummary[];
tick_data: TickSnapshot[];
created_at: string;
completed_at: string | null;
}
export interface ReportResponse {
id: string;
simulation_id: string;
market_probability: number;
simulation_probability: number;
summary: string;
key_drivers: string[];
faction_analysis: string;
trust_insights: string;
recommendation: string;
}
export interface ApiError {
detail: string;
code: string | null;
}Produced by: LLM via Person 3's prompts. Consumed by: Person 3 (simulation runner parses and routes these).
On each tick, the LLM call for every agent MUST return exactly one of these two JSON shapes. No other shapes are valid. Person 3 owns the prompt that produces them.
{
"action": "update_belief",
"new_probability": 0.72,
"confidence": 0.65,
"reasoning": "The Fed's recent statement strongly implies rate cuts are coming, which outweighs the inflation data."
}class UpdateBeliefAction(BaseModel):
action: Literal["update_belief"]
new_probability: float = Field(ge=0.0, le=1.0)
confidence: float = Field(ge=0.0, le=1.0)
reasoning: str # shown in the debate feed{
"action": "share_claim",
"claim_id": "uuid-of-claim-from-visible-or-incoming-list",
"target_agent_ids": ["uuid-agent-b", "uuid-agent-c"],
"commentary": "This data point is being overlooked. Sharing with analysts who track monetary policy.",
"reasoning": "Internal reasoning not shared with other agents — kept private for prompt context only."
}from uuid import UUID
class ShareClaimAction(BaseModel):
action: Literal["share_claim"]
claim_id: UUID # must be an id from the agent's visible_claims or incoming_claims
target_agent_ids: list[UUID] # 1–2 trusted agent ids
commentary: str # shown in the debate feed to the receiving agents
reasoning: str # private — not injected into other agents' promptsRule: claim_id must be one the agent was shown (from visible claims or incoming claims). Person 3 must validate this and discard shares of claims the agent never saw.
Produced by: Person 1 (route shells + wiring). Called by: Person 4.
| Method | Path | Request Body | Response Type | Notes |
|---|---|---|---|---|
POST |
/api/markets/import |
{ "url": string } |
MarketResponse |
Creates market + session |
POST |
/api/sessions/{market_id}/claims/generate |
(none) | ClaimsGenerateResponse |
market_id in path |
POST |
/api/simulations/build-world |
{ "session_id": string } |
SimulationResponse |
status="building", agents populated, tick_data=[] |
POST |
/api/simulations/{id}/start |
(none) | SimulationResponse |
status="running"; kicks off background worker |
GET |
/api/simulations/{id} |
(none) | SimulationResponse |
Polling endpoint; tick_data grows as simulation runs |
GET |
/api/reports/{simulation_id} |
(none) | ReportResponse |
Only available after status="complete" |
All error responses use the shape in Section 8.
CORS: The backend must allow http://localhost:3000 and the Vercel deployment URL. Person 1 configures this in FastAPI middleware.
Produced by: Person 2. Import path for everyone: from app.core.llm_client import llm_client
The singleton llm_client is the only instance that should exist. No other code creates a new LLMClient().
MODEL_GEMINI_FLASH = "gemini-flash" # → gemini-1.5-flash-002 via Lava (fast; use for simulation ticks)
MODEL_GEMINI_PRO = "gemini-pro" # → gemini-1.5-pro-002 via Lava (smart; use for claims + report drafting)
MODEL_K2_THINK = "k2-think" # → Kindo/K2-Think-V2 via LiteLLM (reasoning; use for world-build + report planning)from typing import Optional, Any
class LLMClient:
async def complete(
self,
prompt: str,
system: Optional[str] = None,
model: str = "gemini-flash", # one of MODEL_* constants above
response_format: str = "text", # "text" or "json"
) -> str:
"""
Returns the LLM response as a string.
If response_format="json", the returned string is valid JSON (already parsed and re-serialized).
Caller is responsible for json.loads() if they need a dict.
"""
...
async def call_apollo(
self,
job_titles: list[str],
keywords: list[str],
limit: int = 12,
) -> list[dict[str, Any]]:
"""
Returns a list of professional profile dicts from Apollo.io via Lava.
Each dict contains at minimum: title, company, industry.
Returns empty list if Apollo returns no results or on error.
"""
...
# Module-level singleton — import this, do not instantiate LLMClient directly
llm_client = LLMClient()All API error responses (4xx and 5xx) return this shape.
# FastAPI automatically wraps HTTPException detail in {"detail": ...}
# For structured errors, raise with a dict:
from fastapi import HTTPException
raise HTTPException(
status_code=422,
detail={"detail": "Invalid Polymarket URL format", "code": "INVALID_URL"}
)export interface ApiError {
detail: string;
code: string | null;
}Person 4: check for this shape in all TanStack Query onError handlers. The code field is nullable — don't assume it's always present.
Use this to know who you are waiting on and who is waiting on you.
| What | Produced by | Consumed by | Unblocks |
|---|---|---|---|
llm_client singleton stub |
Person 2 (by minute 30) | Person 3 | Person 3 can write all simulation logic |
| All model stubs + schema files | Person 1 (by minute 45) | Person 2, Person 3, Person 4 | Everyone starts real code |
ClaimSchema, MarketResponse |
Person 1 (schemas) | Person 2 (claims gen), Person 4 (UI) | Claims gen and market UI |
SimulationResponse, TickSnapshot |
Person 1 (schema) + Person 3 (populates) | Person 4 (replay UI) | Replay component |
Real LLMClient implementation |
Person 2 (by hour 3) | Person 3 | Simulation makes real LLM calls |
apollo_service.get_relevant_professionals() |
Person 2 | Person 3 (world builder) | Real agent personas |
world_builder.build_world(session_id, simulation_id) |
Person 3 | Person 1 (wires into build-world route) | Build-world endpoint works |
simulation_worker.run_simulation(simulation_id) |
Person 3 | Person 1 (wires into start route) | Start endpoint triggers simulation |
| Railway backend URL | Person 1 (by hour 6) | Person 4 | Frontend replaces mock data |
ReportResponse |
Person 3 (report agent) | Person 4 (report page) | Report view page |
Person 2 needs from Person 1:
from app.models.claim import Claim
from app.models.session import AnalysisSession
from app.core.database import get_dbPerson 3 needs from Person 1:
from app.models.agent import Agent
from app.models.simulation import Simulation
from app.models.claim_share import ClaimShare
from app.models.claim import Claim
from app.core.database import get_dbPerson 3 needs from Person 2:
from app.core.llm_client import llm_client
from app.services.apollo_service import apollo_servicePerson 4 needs from everyone:
GET/POST the API routes listed in Section 6
All TypeScript interfaces in frontend/lib/types.ts (Person 4 writes these from this file)
This section exists so every AI agent has the same model of how the simulation works.
- One shared claim pool. Claims are generated once per session. Stance (
yes/no) belongs to the claim and never changes. Claims are not modified during simulation. - 12 agents with private beliefs. Each agent has its own
current_belief(float 0–1). Belief belongs to the agent, not the claim. - Each tick: Every agent receives a private prompt with: their current belief and confidence, the top 4 yes + top 4 no claims from the shared pool (ranked by
0.7*strength + 0.3*novelty), and any claims specifically sent to them in the previous tick. Each agent returns one action (update belief OR share a claim). - Shares are not instant. A claim shared at tick N is stored as a
ClaimSharerecord withdelivered=False. It is injected into the recipient's prompt at tick N+1, then markeddelivered=True. - The backend is the mailman. Agents never see each other's prompts directly. All communication goes through
ClaimSharerecords. - Trust updates after each tick. When agent A shares a claim to agent B: A's trust score toward B increases by
TRUST_SHARE_DELTA. When agent B receives a claim from A but does not act on it (no share back, no belief update toward A's position): B's trust toward A decreases byTRUST_IGNORE_DELTA. - Factions. After each tick, group agents whose
current_beliefvalues are withinFACTION_THRESHOLD(0.08) of each other. Store asfaction_clustersinTickSnapshot. - Final probability.
simulation_probabilityinReportResponse= averagecurrent_beliefacross all 12 agents at tick 30.