Using Agents¶
LLM agent simulation offers a new research paradigm for computational social science: researchers place individuals, groups, institutions, and environmental mechanisms in runnable systems and observe how micro-level behavior aggregates into macro-level phenomena. Questions that once stayed in theory or small surveys become iterable, comparable, replayable experiments.
This paradigm still has a high practical entry barrier. A complete experiment usually requires researchers to turn high-level social-science ideas into executable code: agent profiles, behavior logic, environment tools, experiment steps, run configuration, and analysis artifacts. The AI Social Scientist in AgentSociety 2 aims to provide an interactive orchestration layer between research ideas and executable simulations, so researchers can focus more on the research question, theoretical mechanism, and experimental logic while the system assists with experiment structuring, configuration generation, run management, and result analysis.
PersonAgent is the default human-oriented agent abstraction in this workflow: it organizes a person profile, behavioral abilities, working memory, and environment interaction into an inspectable, replaceable, and replayable runtime unit. This section starts with how researchers should understand PersonAgent and then covers the engineering details needed for tuning, extension, and debugging.
Note
This page describes the post-Ray-refactor agent architecture: AgentBase directly owns the workspace, skill runtime, ReAct loop, TODO state, and trace; agents are workspace-bound stateless records driven by Ray Tasks. For the system-level execution model, ServiceProxy, and trace, see Architecture and Scalability; for the skill subsystem, see Agent Skills.
Class hierarchy¶
Inheritance in the current agent stack:
AgentBase (ABC) # agent/base/agent.py — 所有智能体的基类
└── PersonAgent # agent/person.py — 面向人物仿真的默认智能体(薄编排器)
AgentBasedirectly owns all generic capabilities: workspace binding, the skill runtime (AgentSkillRuntime), the ReAct tool loop, LLM calls, TODO state, trace, andAGENT.jsonpersistence. It has no mixins and no multiple inheritance.PersonAgentonly implements person-specific logic, such as memory, the person prompt, and daily-guidance behavior; everything else is inherited from the base class.AgentSociety 2 also ships experimental agents under
contrib/agent/(e.g. game-theory scenarios), but the core framework relies onAgentBaseandPersonAgentonly.
Construction Model: Workspace-Bound Stateless Records¶
Agents are no longer constructed by passing parameters to ``__init__(…)``. AgentBase provides two classmethod entry points plus an argument-free __init__:
Entry point |
Description |
|---|---|
|
Static one-time write: writes |
|
Rebuilds a ready agent: |
|
No arguments. It only initializes empty slots, does not parse a profile, and does not require an id. Real initialization happens in |
All dynamic state across ticks is stored in the workspace. Each step() rebuilds context from the workspace, so the agent itself is a stateless record that Ray Tasks can stream to any worker as data (see Architecture and Scalability).
Most researchers do not call create / from_workspace directly. Instead, they declare agents through AgentSociety + InitConfig, and the orchestrator creates them in batches when the experiment starts (see Agent Configuration (AgentConfig) and Command Line Interface below). The following only shows the low-level contract for understanding and extension:
from pathlib import Path
from agentsociety2 import PersonAgent
from agentsociety2.agent.service_proxy import ServiceProxy
workspace = Path("run/agents/agent_0001")
# 1) 写一次 workspace(config.json + 初始 AGENT.json)
PersonAgent.create(
workspace,
profile={"name": "Alice", "age": 28, "bio": "A software engineer."},
config={},
)
# 2) 重建 ready agent(需要 ServiceProxy 注入 env/llm/trace/replay)
agent = await PersonAgent.from_workspace(workspace, service_proxy)
Service Injection: ServiceProxy¶
Inside from_workspace, _bind_services(service_proxy) injects a shared service container into the agent:
slot |
Source / purpose |
|---|---|
|
Raw container holding |
|
Environment router ( |
|
Default LLM dispatcher ( |
|
Default LLM model name. |
ServiceProxy collects env, LLM clients, trace, and replay into one object. This is key to Ray Task parallelism. LLM clients only carry connection parameters, and workers create litellm Routers on demand in their local event loop.
ReAct Tool Loop¶
The core of PersonAgent.step() is a decision-execution-feedback loop. In each round, the model submits only one structured ToolDecision; the runtime validates it, executes it, and writes the result back into context. This design constrains free-form LLM generation to auditable tool results and also makes replay analysis easier when reviewing why each step happened.
The base dispatch_react_tool handles these tools; PersonAgent adds prefixes such as memory_* on top as needed:
Tool |
Description |
|---|---|
|
Read agent workspace files, including pagination. |
|
Inject or remove a skill’s |
|
Read files inside a skill directory for progressive disclosure. |
|
Execute a skill script, using the in-process |
|
Send a request to the environment router, querying or modifying the environment, and return the answer. |
|
End the current simulation step, optionally with a summary. |
Each model turn is parsed as a structured decision: tool name, arguments, and whether to finish. Invalid tool names are corrected or returned as recoverable errors instead of crashing immediately. Tool results are written to the thread: an in-memory window plus on-disk JSONL.
Note
The agent layer has no tools such as bash, glob, codegen, or batch. Environment interaction always goes through ask_env; the environment-side router, such as CodeGenRouter, decides how to call environment module tools (see Environment Modules).
TODO State¶
The base class includes a set of TODO tools (todo_list / todo_add / todo_update / todo_start / todo_complete / todo_defer / todo_clear_completed) so agents can explicitly maintain a task list. TODO items are identified by UUID, archived automatically, and dispatched through dispatch_todo_tool. The goal is to make multi-step plans traceable and recoverable across ticks.
Workspace Layout¶
Each agent has an independent workspace, which is its only source of persistent state:
<run_dir>/agents/agent_XXXX/
├── config.json # 静态配置(create 时写一次,不再重写)
├── AGENT.json # 动态自描述快照(每步 to_workspace 时更新)
├── state/ # 状态文件
│ ├── emotion.json
│ ├── intention.json
│ ├── needs.json
│ ├── plan_state.json
│ └── memory.jsonl # memory skill 写入的长期事件记忆
├── memory/ # 记忆相关文件
├── custom/skills/ # 自定义技能目录(热加载)
└── .runtime/logs/
├── session_state.json
├── thread_messages.jsonl
├── tool_calls.jsonl
└── step_replay.jsonl
config.json is static. AGENT.json is written back by persist_agent_json (which calls build_agent_json) during to_workspace and acts as the agent’s self-description snapshot. Thread and tool logs live under .runtime/logs/.
Agent Configuration (AgentConfig)¶
Experiment-level agent configuration is described by AgentConfig (agentsociety2.society.models) with a compact field set:
Field |
Description |
|---|---|
|
Unique identifier of the agent. |
|
Agent type, the registered class name such as |
|
All initialization parameters for this agent, including |
It appears in the InitConfig.agents list and is provided by the CLI --config JSON. Model / LLM selection uses global config (see the environment variables in Installation and Configuration Module).
Interacting with Agents¶
ask() Method¶
response = await agent.ask(
"What's your opinion on renewable energy?",
readonly=True, # 仅查询,无副作用
)
readonly controls whether the agent may modify the environment: True only queries; False may call state-mutating environment tools. ask uses the ReAct loop but exposes only the read-only tool subset.
step() Method¶
AgentSociety calls this automatically during simulation. tick is the duration of the step in seconds, and t is the simulation time at the end of the step:
action_description = await agent.step(tick=3600, t=datetime.now())
At the orchestrator layer, AgentSociety.step(tick) / run(num_steps, tick) drive step for all agents and support batched Ray Task parallelism (see Architecture and Scalability). External Q&A / interventions go through AgentSociety.ask(question) and AgentSociety.intervene(instruction).
Custom Agents¶
Note
When extending PersonAgent cognitive capabilities, prefer Agent Skills (see Agent Skills). Create a custom agent class only when a completely different agent architecture is needed.
To create a custom agent, inherit from AgentBase, implement the abstract methods, and override restore to add business state. Do not override __init__; set state in restore.
AgentBase Abstract Methods¶
Method |
Description |
|---|---|
|
Required. Answer external questions through the agent’s reasoning flow. |
|
Required. Execute one simulation step. |
|
Required. Write current dynamic state back to the workspace. |
|
Recommended override: after |
|
Required when reusing the base |
|
Recommended override: extend |
|
Recommended override: add tool prefixes such as |
Example:
from datetime import datetime
from pathlib import Path
from typing import Any
from agentsociety2.agent.base import AgentBase
class MinimalAgent(AgentBase):
"""最小子类:自定义状态 + 复用基类 ReAct 循环。"""
async def restore(self, workspace_path: Path, service_proxy: Any) -> None:
await super().restore(workspace_path, service_proxy) # 先恢复 workspace / 服务 / 技能
self._custom_counter: int = 0 # 再追加业务状态
def build_react_messages(self, *, tick, t, observations, question=None,
readonly=False, skill_hooks=None):
system = f"You are agent {self.name} (id={self.id}). tick={tick}, t={t.isoformat()}."
user = question or "Decide next action."
return [{"role": "system", "content": system},
{"role": "user", "content": user}]
async def to_workspace(self, workspace_path: Path) -> None:
self.persist_agent_json(tick=None, t=self._current_time)
async def ask(self, message: str, readonly: bool = True, *, t=None) -> str:
return await self.run_react_loop(tick=0, t=t, observations=[], question=message, readonly=readonly)
async def step(self, tick: int, t: datetime) -> str:
return await self.run_react_loop(tick=tick, t=t)
Public base APIs available to subclasses also include run_react_loop, acompletion, run_lifecycle_hooks, discover_skill_sources, persist_agent_json, trace_span, ask_env, get_profile, skill_runtime, and attributes such as self.id, self.name, and self.logger. For the full list, see Agent Module and the source file agent/base/README.md.
Agent Memory¶
The PersonAgent memory system is divided into three layers:
Thread: short-term context that keeps recent tool calls and LLM interactions, with automatic compression when it grows too long.
AgentMemory: runtime persistent memory that keeps the current task, completed actions, error records, and similar state across steps. It lives in
AGENT_MEMORY.mdand is updated by thread-compression summaries andhandoff_to_memory().Event memory:
PersonMemoryRuntimewritesmemoriescarried by step-modefinishintomemory/episodes.jsonl. It stores retrievable facts such as social relationships, places, commitments, and plan outcomes. Whether to write is decided by the tool loop and is not fixed for every step.Workspace state files:
state/*.jsonwritten by skill scripts, such as emotion, intention, and plan state.
MEMORY.md and memory/episodes.jsonl are not the same thing: the former is a compressed long-term background summary, while the latter is an append-only event stream. To replace or extend memory strategy, prefer extending PersonMemoryRuntime or related tool boundaries instead of restoring the old built-in memory skill.
Reference Implementation¶
Agent Skills - Agent Skills subsystem: discovery, entrypoint execution, and lifecycle hooks
Architecture and Scalability - Ray execution model,
ServiceProxy, trace, and routersAgent Module -
AgentBase/PersonAgentAPI referenceSource
agentsociety2/agent/base/README.md- the authoritative guide for developingAgentBasesubclasses