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 — 面向人物仿真的默认智能体(薄编排器)
  • AgentBase directly owns all generic capabilities: workspace binding, the skill runtime (AgentSkillRuntime), the ReAct tool loop, LLM calls, TODO state, trace, and AGENT.json persistence. It has no mixins and no multiple inheritance.

  • PersonAgent only 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 on AgentBase and PersonAgent only.

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

AgentBase.create(workspace_path, profile, config)

Static one-time write: writes config.json, the initial AGENT.json, and the standard empty directories into the workspace. It does not return an agent instance.

await AgentBase.from_workspace(workspace_path, service_proxy)

Rebuilds a ready agent: agent = cls() (no arguments) -> await agent.restore(ws, proxy) -> return.

AgentBase.__init__(self)

No arguments. It only initializes empty slots, does not parse a profile, and does not require an id. Real initialization happens in restore().

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

self._service_proxy

Raw container holding env / llm / trace / replay handles.

self._env

Environment router (RouterBase), accessed through ask_env.

self._dispatcher

Default LLM dispatcher (service_proxy.llm.default).

model

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 / write / append / list / grep

Read agent workspace files, including pagination.

activate_skill

Inject or remove a skill’s SKILL.md from the context.

read_skill

Read files inside a skill directory for progressive disclosure.

execute_skill

Execute a skill script, using the in-process entrypoint by default; see Agent Skills.

plan

Send a request to the environment router, querying or modifying the environment, and return the answer.

finish

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

id (int)

Unique identifier of the agent.

name (str, optional)

Agent type, the registered class name such as PersonAgent.

id (int)

All initialization parameters for this agent, including profile.

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

ask_env(ctx, message, readonly, template_mode)

Required. Answer external questions through the agent’s reasoning flow.

get_system_prompt(tick, t)

Required. Execute one simulation step.

preload_workspace_paths

Required. Write current dynamic state back to the workspace.

restore(self, workspace_path, service_proxy)

Recommended override: after await super().restore(...), restore additional business state.

build_react_messages(...)

Required when reusing the base run_react_loop: construct the ReAct prompt (chat messages).

get_system_prompt(tick, t)

Recommended override: extend AGENT.json fields by calling data = super().build_agent_json(...) first and then appending fields.

dispatch_react_tool(action, args, *, readonly=False)

Recommended override: add tool prefixes such as memory_* / todo_* and forward misses to super().

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:

  1. Thread: short-term context that keeps recent tool calls and LLM interactions, with automatic compression when it grows too long.

  2. AgentMemory: runtime persistent memory that keeps the current task, completed actions, error records, and similar state across steps. It lives in AGENT_MEMORY.md and is updated by thread-compression summaries and handoff_to_memory().

  3. Event memory: PersonMemoryRuntime writes memories carried by step-mode finish into memory/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.

  4. Workspace state files: state/*.json written 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 routers

  • Agent Module - AgentBase / PersonAgent API reference

  • Source agentsociety2/agent/base/README.md - the authoritative guide for developing AgentBase subclasses