Skill Authoring Guide

This document describes how to write a custom Skill for PersonAgent. The goal of Skill is not to write all logic as complex plug-ins, but to clearly explain the trigger conditions, input files, execution steps and output files of a capability so that the agent can call it at the appropriate time.

Overview

Skill is PersonAgent’s behavioral module. There are two common ways of writing:

  • Prompt-only: Only write SKILL.md, suitable for open tasks such as judgment, reflection, summary, and planning.

  • With scripts: provide additional Python scripts for deterministic computation, format conversion, batch processing, and repeatable data maintenance. Scripts execute by default inside the agent process through entrypoint(argv, ctx), which is millisecond-level and concurrency-safe. Without entrypoint, runtime falls back to the dynamic wrapper, and only then to a subprocess.

For environment interaction, no special executor needs to be declared in frontmatter; explain in the body when to use the ask_env tool. For the full design, see agentsociety2/agent/skills/README.md and :doc:/agent_skills.

Quick Start

Create a simple Skill

  1. Create a new folder in the custom/skills/ directory:

custom/skills/my-skill/
└── SKILL.md
  1. Write SKILL.md:

---
name: my-skill
description: 一句话描述功能。什么时候使用。产生什么输出。
---

# My Skill

## 何时使用
描述触发条件。

## 输入文件
- 当前 step 的 observation 已在 ReAct 上下文中,不需要从固定文件读取
- `state/needs.json`:需求状态(如果存在)
- `AGENT_MEMORY.md`:长期摘要(如果需要)

## 执行步骤
1. 首先,用 `read` 读取需要的 workspace 文件
2. 然后,分析内容并做出决策
3. 最后,用 `write` 写入输出文件

## 输出格式
\`\`\`json
{
  "field1": "描述",
  "field2": 0.5
}
\`\`\`

## 示例

**输入**:
\`\`\`
ReAct step context: "在公园遇到了Alice"
\`\`\`

**输出**:
\`\`\`json
{
  "event": "met Alice at park",
  "emotion": "happy"
}
\`\`\`

No programming required, the model reads files, calls tools, and writes output based on your description. The more specific it is written, the more stable the behavior will be.

Detailed explanation of SKILL.md structure

Frontmatter (required)

Frontmatter is a YAML formatted metadata block located at the beginning of the file:

---
name: skill-name           # 必需:唯一标识符
description: 描述           # 必需:catalog / 选择器用
---

Optional: place scripts/<name>.py in the same directory. Define def entrypoint(argv, ctx) -> str at script top level; runtime calls it in-process first with cached imports and no fork. Without entrypoint, it falls back to the dynamic wrapper / subprocess. Put dependencies in the body so the model can activate_skill on demand.

Body (required)

Body is a Markdown-formatted behavioral guide that tells LLM:

  1. When to use: trigger conditions

  2. Input: which files to read

  3. What it does: execution steps

  4. Output: files produced

Available built-in tools

The Markdown body of the Skill can instruct LLM to use the following tools:

Tool

Usage

Examples

grep

Read workspace files

workspace_write("state/result.json", content)

grep

Write file

workspace_write("state/result.json", content)

append

Append content

workspace_write("state/result.json", content)

list

List files

workspace_list(".")

grep

Search workspace files

grep("pattern", ".")

ask_env

Send a request to the environment router

codegen("<observe>")

activate_skill / read_skill_file / execute_skill_script

Activate / read / execute another skill

See :doc:/agent_skills

finish

End the current simulation step

With an optional summary

There are no bash / glob / codegen / batch tools; environment interaction always goes through ask_env.

Skill type

Type 1: Pure prompt-driven (recommended)

Most skills do not require programming, just clear description:

---
name: mood-check
description: Check and record current mood based on recent events.
---

# Mood Check

Analyze recent events and determine current mood.

## Input
- Current step context: current observation and available environment context
- `state/emotion.json`: Current emotional state
- `AGENT_MEMORY.md`: Persistent runtime summary, if needed

## Output
Write `state/mood.json`:
\`\`\`json
{
  "mood": "happy|sad|neutral|anxious|excited",
  "intensity": 0.0-1.0,
  "reason": "Brief explanation"
}
\`\`\`

Type 2: With Python script

When deterministic calculations are required, add a Python script:

custom/skills/calculator/
├── SKILL.md
└── scripts/
    └── calc.py

SKILL.md:

---
name: calculator
description: Perform precise numerical calculations.
script: scripts/calc.py
---

calc.py, providing entrypoint for in-process execution; CLI and entrypoint share the same dispatcher:

"""Calculator skill script."""
import argparse
import contextvars
import json
from pathlib import Path
from typing import Any

# 用 ContextVar 承载“本次调用的工作区根”,保证多 agent 并发安全
_WS_ROOT: contextvars.ContextVar[Path | None] = contextvars.ContextVar(
    "calc_ws_root", default=None
)


def dispatch(args: argparse.Namespace) -> int:
    ws = _WS_ROOT.get() or Path.cwd()
    state_dir = ws / "state"
    state_dir.mkdir(exist_ok=True)

    # 计算逻辑。真实项目中不要直接 eval 用户输入。
    expression = args.expression
    try:
        result = eval(expression)
        output = {"ok": True, "result": result}
    except Exception as e:
        output = {"ok": False, "error": str(e)}

    (state_dir / "result.json").write_text(
        json.dumps(output, ensure_ascii=False, indent=2)
    )
    print(json.dumps(output))
    return 0


def entrypoint(argv: list[str], ctx: Any) -> str:
    parser = argparse.ArgumentParser()
    parser.add_argument("--args-json", default="{}")
    ns = parser.parse_args(list(argv))
    import json as _json
    ns.expression = _json.loads(ns.args_json or "{}").get("expression", "0")
    _WS_ROOT.set(Path(str(getattr(ctx, "workspace_root"))).resolve())
    dispatch(ns)
    return ""  # 进程内:返回字符串而非依赖 print 捕获


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--args-json", default="{}")
    ns = parser.parse_args()
    import json as _json
    ns.expression = _json.loads(ns.args_json or "{}").get("expression", "0")
    return dispatch(ns)


if __name__ == "__main__":
    raise SystemExit(main())  # 子进程回退路径仍可用

Interact with the environment

Just state in the SKILL body when to call the built-in codegen tool; do not write executor in the frontmatter (the framework will not parse it).

Best practices

1. 保持单一职责

Each Skill only does one thing:

  • mood-check: updates only current mood state

  • social-reflection: updates only social relationships

  • needs_and_emotion: Doing too many things

2. 在正文写清产出文件

List the paths that will be written in Markdown (such as state/result.json); frontmatter does not parse extension fields such as outputs, inputs, requires, etc.

3. 处理缺失文件

The skill should handle gracefully the case where the input file does not exist:

## Input Files
- Current step context contains the observation; do not assume `state/observation.txt` exists
- `state/needs.json`: Need state (use defaults if missing)

4. 提供示例

Examples help LLM understand expected behavior:

## Example

**Input**:
\`\`\`
ReAct step context: "You see a cafe across the street."
\`\`\`

**Output** (intention.json):
\`\`\`json
{
  "intention": "Visit the café for lunch",
  "priority": 2,
  "reasoning": "I'm feeling hungry and there's a café nearby."
}
\`\`\`

5. 避免冗余描述

There is no need to tell LLM to “think carefully” or “analyze carefully”, it will do this naturally.

File structure convention

It is recommended to use the following directory structure:

custom/skills/my-skill/
├── SKILL.md          # 必需:skill定义
├── scripts/          # 可选:Python脚本
│   └── main.py
├── templates/        # 可选:模板文件
│   └── prompt.jinja2
└── tests/            # 可选:测试
    └── test_skill.py

Debugging Tips

  1. Check workspace files: Check whether the output file is generated correctly

  2. Check tool_calls.jsonl: Check which tools are called by LLM

  3. Simplify description: If the Skill behaves abnormally, try to simplify the description

  4. Add examples: Examples often significantly improve LLM understanding

Example: Complete Skill

---
name: social-reflection
description: Reflect on recent social interactions and update relationship state.
---

# Social Reflection

Reflect on recent social interactions and how they affect relationships.

## When to Use
- After a significant social interaction
- When feeling uncertain about a relationship
- Before making social decisions

## Input Files
- Current step context: current perception and recent event text
- `state/relationships.json`: Current relationship state
- `AGENT_MEMORY.md`: Persistent runtime summary, if needed
- `state/emotion.json`: Current emotional state

## Execution Steps

1. Read `state/relationships.json` to understand current state
2. Read `AGENT_MEMORY.md` if additional background is needed
3. Consider current emotional state
4. Reflect on how recent events affect relationships
5. Write reflection to `state/social_reflection.json`

## Output Format

\`\`\`json
{
  "reflection": "What I learned about my relationships",
  "relationship_changes": [
    {
      "agent_id": "2",
      "change": "increased trust",
      "reason": "Alice helped me when I was in trouble"
    }
  ],
  "social_goals": [
    "Spend more time with Alice",
    "Resolve conflict with Bob"
  ]
}
\`\`\`

## Example

**Input**:
- ReAct step context: "Alice smiled and offered to help with my project."
- state/relationships.json: Agent 2 (Alice) is an acquaintance with trust 0.3

**Output**:
\`\`\`json
{
  "reflection": "Alice showed genuine kindness by offering help.",
  "relationship_changes": [
    {
      "agent_id": "2",
      "change": "increased trust and affection",
      "reason": "Alice's offer to help demonstrates reliability"
    }
  ],
  "social_goals": [
    "Accept Alice's help and build friendship"
  ]
}
\`\`\`

FAQ

Q: How do Skills communicate with each other?

A: Through the workspace file. One Skill writes to the file and another Skill reads.

Q: How to control Skill execution order?

A: The main model decides which activate_skill to use first in the tool loop; if there is a hard dependency, write it clearly in the SKILL.md text and guide the dependent skill to be activated first.

Q: Can a Skill call other Skills?

A: Not called directly. Loosely coupled through workspace files, LLM decides when to activate which Skill.

Q: How to test a Skill?

A: Create a test workspace, place input files, run the agent, and check the output files.

Advanced topics

State management

If the Skill needs to maintain state, write to the JSON file:

{
  "state": "active",
  "progress": 0.5,
  "history": ["event1", "event2"]
}

Interact with the environment

Use codegen tools to interact with the environment:

## Environment Actions

1. Observe: `ask_env("<observe>")`
2. Move: `ask_env("Move to {location}")`
3. Speak: `ask_env("Say '{message}' to {target}")`

Time awareness

Skill can receive time information:

The `tick` and `time` fields are auto-injected by the framework.
Use them for time-dependent logic.