Personas
Personas are a way to give each agent a stable, portable identity — demographics, behavioral traits, and an optional prose bio — that gets injected into the system prompt. A single neutral role can drive very different behavior depending on the persona attached to it, which makes personas a clean lever for controlled experiments (factorial demographics, behavioral archetype comparison, ablations).
Concept
A persona is a small Pydantic model with four fields:
class Persona(BaseModel):
id: str
demographics: dict[str, Any] = {}
traits: dict[str, Any] = {}
bio: str = ""
idis a stable unique identifier.demographicsandtraitsare opendicttypes. Trait values should stay machine-readable ("high"/"low"/ numeric).biois an optional prose paragraph appended below the demographics/traits.
Two entry points, different surfaces
How you attach a persona depends on which entry point you use:
YAML experiments declare personas in a top-level
personas:list and reference them by id from each agent mapping. All persona content lives in the YAML; there is no file-based or bundled-by-id resolution from this path.Code-driven experiments call
econagents.personas.load_persona()to resolve a persona by id, with the bundled starter library as a built-in fallback and an optionaluser_dirfor file-based personas.
Attaching personas via YAML
Declare each persona once in the top-level personas: list, then reference it by id from any number of agents:
personas:
- id: "maria-cooperative"
demographics:
age: 37
country: ES
occupation: social-worker
traits:
cooperativeness: high
reciprocity: high
bio: You believe most people are trying their best.
- id: "marcus-strategic"
demographics:
age: 44
country: US
occupation: hedge-fund-analyst
traits:
cooperativeness: low
bio: You evaluate every situation by expected value.
agents:
- id: 1
role_id: 1
persona_id: "maria-cooperative" # multiple agents may share this id
- id: 2
role_id: 1
persona_id: "marcus-strategic"
Persona ids must be unique within the personas list, and any persona_id on an agent mapping must resolve to a persona in that list. Both checks fire at config-load time with a clear error message.
If you need to reference a bundled archetype by id, or to load personas from external files, use the code-driven entry point instead — those resolution mechanisms are not available from YAML.
Attaching personas via code
In code-driven experiments, use econagents.personas.load_persona() and pass the resolved persona to your role's constructor (or to a manager that wraps it).
from econagents.personas import load_persona
# Bundled archetype — works anywhere
cooperator = load_persona("conditional-cooperator")
# User-authored file — picked up automatically from <cwd>/personas/
custom = load_persona("my-custom")
role = Prisoner(persona=custom)
The signature is load_persona(persona_id, user_dir=None). Resolution:
If
user_diris provided, it's used as-is.Otherwise, the loader checks
<cwd>/personasand uses it when that directory exists.If neither yielded a match, the loader falls back to the bundled library, raising
econagents.personas.PersonaNotFoundErrorif nothing matches.
The user directory wins on collision, so you can override a bundled persona by dropping a file with the same id into your local personas/ directory. Pass user_dir= explicitly when you want a different location — for example, scripts that need to run from any working directory:
from pathlib import Path
PERSONAS_DIR = Path(__file__).parent / "my-custom-personas"
custom = load_persona("my-custom", user_dir=PERSONAS_DIR)
Auto-render contract
When a persona is attached to a role, the library automatically appends a standard markdown block to the system prompt — no template changes required. The block looks like:
## About You
- age: 44
- country: US
- occupation: hedge-fund-analyst
Tendencies:
- cooperativeness: low
You evaluate every situation by expected value.
Sections with empty underlying data are omitted, so an archetype-only persona (no demographics, no bio) produces just the Tendencies: list and nothing else.
This behavior is controlled by AgentRole.auto_render_persona (default True):
class MyRole(AgentRole):
auto_render_persona = False # take full control via {{ persona }}
When disabled, the rendered system prompt is exactly what your template produces. The persona dict is still injected into the Jinja context as {{ persona }}, so you can format it however you want:
{% if persona %}
You are a {{ persona.demographics.age }}-year-old
{{ persona.demographics.occupation }}. {{ persona.bio }}
{% endif %}
Auto-render is system-prompt only. User prompts and phase-specific prompt handlers are not augmented — they're full overrides of the default rendering path.
Bundled starter library
The bundled library at econagents/personas/library/ ships two flat sets of primitives, organized into subdirectories that mirror the taxonomy:
Behavioral archetypes (library/archetypes/) — empty demographics, populated traits. Recognizable to economists across prisoner / dictator / public goods / ultimatum / trust games:
unconditional-cooperator,conditional-cooperator,free-rider,altruist,spiteful,risk-averse,risk-seeking,inequity-averse
Demographic profiles (library/demographics/) — empty traits, populated demographics. Spread across age / country / education / occupation cells:
student-us-21,professional-de-35,retiree-jp-68,parent-br-42,worker-in-29,teacher-no-48,entrepreneur-ke-33
The loader treats subdirectories as an organizational convenience. From the code-driven path you always reference a bundled persona by its bare id (load_persona("free-rider")), never by path. Bundled personas are not reachable from YAML; if you want one in a YAML experiment, copy its fields into the top-level personas: list (or inline it on an agent).
Public API
from econagents.personas import (
Persona,
PersonaNotFoundError,
load_persona,
save_persona,
)
See the API page for full signatures.
End-to-end examples
Two examples in the repository demonstrate the entry-point split:
examples/prisoner_personas/prisoner.yaml— YAML-driven, personas declared in the top-levelpersonas:list and referenced by id from each agent. All in one file.examples/prisoner_personas/run_game.py+manager.py— code-driven, bundled and file-based personas viaload_persona.
Both reuse the prisoner server from examples/prisoner/, so the only difference between the two examples is how personas are attached.