Building Agents¶
For API-level contracts for runtime agents and builder hooks, see Simulation Extensibility API.
The agent contract
Every agent (native, fixed, or custom) implements the same small interface:
name, observe(observation), and act(action_spec). The platform decides
what an agent perceives and how its output is resolved; the agent just
reasons and responds.
There are two ways to produce agent specs for your simulation:
- YAML Pipeline (recommended for most cases): define agent classes declaratively in your scenario YAML
- Custom Builder: write a Python class for full programmatic control
The default PersonaPipelineAgentBuilder reads the YAML pipeline config. If a
world needs programmatic logic, set agents.builder.class_path explicitly.
Builders return AgentConfig records; the runtime still owns live agent
construction and model injection.
Builder output contract:
- return
list[AgentConfig]; - set
class_pathto the runtime agent class; - put constructor kwargs under
params; - do not construct live
Agentinstances; - do not attach a
LanguageModel; runtime assembly injects it.
Method 1: YAML Pipeline (Declarative)¶
Define a persona_pipeline section in your scenario YAML. The builder
reads class definitions, loads data from various sources, and maps fields
to agent parameters: no Python code needed.
Minimal Example¶
# scenarios/my_world/conf/agents/default.yaml
builder:
class_path: null
params: {}
persona_pipeline:
defaults:
params:
world_context: "A community discussion platform."
shared_memories:
- "Users are active on a social media platform."
classes:
user:
count: 2
class_path: silisocs.agents.native.NativeAgent
data:
source: inline
records:
- name: Alex
persona: Alex follows local policy and posts practical updates.
- name: Blair
persona: Blair follows technology news and likes concise debates.
field_map:
name: name
context: persona
Data Sources¶
| Source | Config keys | Description |
|---|---|---|
local_json |
path |
Local JSON file (array of objects) |
inline |
records |
Records defined directly in YAML |
config_path |
path |
Dot-path into another config section (e.g. candidates) |
hf_dataset |
dataset, split, subset |
Hugging Face dataset; requires silisocs[hf] |
Field Mapping¶
Map data record fields to agent parameters:
field_map:
name: full_name # Simple dot-path into the record
context: persona # Maps "persona" field → agent "context"
bio: "{role}\n{interests}" # Template combining multiple fields
context is required. The final AgentConfig records must also contain a
unique name; SiliSocS uses agent names as runtime identities for observations,
backend state, flows, probes, logs, and checkpoints. Most data sources should map
field_map.name explicitly. The default builder also derives names for the
known nvidia/Nemotron-Personas-USA persona dataset, and classes can opt into
the same behavior with derive_name_from_context: true. Custom builders may
derive names however they need, but runtime construction rejects unnamed or
duplicate specs.
Supported target fields: name, context, style, goal, bio, seed_post.
Defaults and Overrides¶
Pipeline-level defaults apply to all classes. Per-class settings override:
persona_pipeline:
defaults:
params:
goal: "Have a productive discussion."
field_map:
context: persona
shared_memories:
- "A shared memory for all agents."
classes:
admin:
count: 2
params:
goal: "Moderate the discussion." # overrides default
shared_memories:
- "Admins have moderation powers." # appended to defaults
Method 2: Custom Builder (Programmatic)¶
For worlds that need logic beyond what YAML can express, create an importable
Python builder class and point agents.builder.class_path at it.
Config Slot¶
class_path: null uses PersonaPipelineAgentBuilder.
Example¶
# scenarios/my_world/builders.py
from silisocs.runtime.construction.agent_builders import AgentBuilder
from silisocs.runtime.construction.specs import AgentConfig
class MyScenarioAgentBuilder(AgentBuilder):
def build_agent_configs(self) -> list[AgentConfig]:
agents = []
for i in range(3):
agents.append(AgentConfig(
class_path="silisocs.agents.native.NativeAgent",
params={
"name": f"Participant {i}",
"context": "A participant in the simulation.",
"sim_role_name": "participant",
"style": "",
"seed_post": "",
"bio": "",
"goal": None,
},
))
return agents
Mixing Both Methods¶
A custom builder can call PersonaPipelineAgentBuilder internally for the
ordinary YAML-defined cohorts, then append custom AgentConfig records for
special cases. That keeps bespoke logic explicit without hiding it behind
world-name auto-detection.
Available Helpers in PersonaPipelineAgentBuilder¶
These helpers are useful when a custom builder wants to reuse the default
persona-pipeline behavior. If your builder only needs ordinary records and field
mapping, prefer instantiating PersonaPipelineAgentBuilder and appending to its
result.
| Method | Description |
|---|---|
self._resolve_file_path(path) |
Resolve path relative to world dir |
self.load_news_data(news_file) |
Load news headlines from JSON |
self._load_memories(value) |
Load memories from string, file path, or list |
self._coerce_text(value) |
Normalize any value to a trimmed string |
self._normalize_memories(value) |
Normalize to list[str] |
self._extract_path(record, "a.b.c") |
Extract nested value from dict |
self._resolve_source(record, spec) |
Resolve dot-path or {template} |
self._derive_name(context, words=2) |
Derive a compact name when a source intentionally has persona text but no name field |
Custom Agent Runtime Shape¶
All runtime agents are constructed with a LanguageModel. Custom agents should
keep act() responsible for deciding what context the agent needs, then use the
protected _call_model(context, action_spec) helper to route the requested
output type to the correct model method.
The native 0.x runtime exposes the concrete Agent base class as the custom
agent contract. Older aliases such as AgentLike and formative-initializer
shim names are intentionally removed rather than kept as compatibility paths.
from silisocs.agents.base_agent import Agent
from silisocs.runtime.language_models import LanguageModel
from silisocs.runtime.types import ActionOutput, ActionSpec
class JournalAgent(Agent):
def __init__(self, *, name: str, model: LanguageModel, persona: str) -> None:
super().__init__(model)
self._name = name
self._persona = persona
self._observations: list[str] = []
@property
def name(self) -> str:
return self._name
def observe(self, observation: str) -> None:
if observation.strip():
self._observations.append(observation.strip())
def act(self, action_spec: ActionSpec) -> ActionOutput:
context = "\n\n".join(
[
f"Persona: {self._persona}",
"Recent observations:",
"\n".join(self._observations[-5:]),
]
)
return self._call_model(context, action_spec)
_call_model() handles text, choices, floats, tool calls, structured outputs,
and skip actions. It fails loudly when a spec is missing required typed data,
such as extra_args["tools"] for tool calls or extra_args["schema"] for
structured outputs.
Per-Class LLM Models¶
Assign different LLM models per agent class:
classes:
voter:
count: 100
model: gpt-4o-mini # Cheaper model for background agents
candidate:
count: 2
model: gpt-4o # Better model for key agents
Or per-agent via field mapping (your data source must include a model field):
See Usage Overview for the full priority chain.
Related¶
- Memory Initialization: How agents get their starting knowledge
- Configuration Reference: Full persona_pipeline config options
- Election Walkthrough: Real-world multi-class world
- Usage Overview: Engine/GM/backend customization map