Install
openclaw skills install @arronjack/pasm-agent-authoringThe base kit for building agents on the PASM cognitive engine. Ships a BaseAgent SDK (observe / recall / mood / act / feedback / save with persistent JSON state), an agent framework (registry, isolated repo probing, scenario simulation harness, core-contract check toolboxes) and a skill-packaging library producing the two archive layouts platforms require. Write an agent by subclassing BaseAgent and implementing an action pool plus a reply template; the engine tier (light / core / bionic) is always reported, never hidden. Zero LLM dependency, offline-runnable, stdlib only.
openclaw skills install @arronjack/pasm-agent-authoring从零写一个智能体,并打成能发布的技能包。 零 LLM 依赖、断网可用、只用标准库 —— 装完就有记忆、情绪、行为选择、反馈塑形、状态持久化。
这是基座:它不提供成品智能体,只提供写智能体的能力。 官方成品智能体(游戏 NPC / 老人陪伴 / 学习陪伴 / 长期验证)在独立公开仓 pasm-agents。
_render_reply 里接 —— 基座不替你做这个决定。)agent.tier 如实写明
bionic / core / light。不许假装覆盖了。observe(..., salience=1..5)。容量触顶时按「重要度 + 新旧」淘汰,
salience=5 的关键记忆不会被日常琐事挤掉。feedback(kind, action=)。不指定就只能加强"当前最偏好的动作",
长期下来行为分布会被推到极端 —— 这是实测压出来的产品级要求。~/.pasm-agents/<agent_id>/,跨进程自动恢复。pasm_skills.agents)
或 PASM_SKILLS_PATH 提供。基座不依赖任何智能体。pip install pasm-skills # 基座(零依赖)
python -m pasm_skills selftest # 自检:确认 SDK / 框架 / 发现机制都活着
写一个最小智能体:
from pasm_skills.sdk import BaseAgent
class TeaHouseOwner(BaseAgent):
def action_pool(self):
return ["greet", "brew", "gossip", "rest"]
def _render_reply(self, text, facts, mood):
tone = "没精打采" if mood < -0.2 else "笑呵呵"
if facts:
return f"({tone})你还记得{ facts[0]['title'] }呢……"
return f"({tone})来啦,坐。"
def bootstrap_event(self):
return [{"title": "我在巷口开了家茶馆", "salience": 3, "tags": ["茶馆"]}]
a = TeaHouseOwner(agent_id="owner", persona={"name": "王掌柜", "temper": 0.6, "energy": 0.3, "play": 0.4})
a.observe("客人夸今年的龙井好", salience=3, tags=["茶"])
print(a.act()) # 'brew' / 'greet' / ...
print(a.chat("生意怎么样"))
print(a.mood) # 属性,不是方法
a.feedback("praise", action="brew") # 显式夸"brew"这个动作
a.save()
| 钩子 | 签名 | 必填 | 作用 |
|---|---|---|---|
action_pool() | -> List[str] | ✅ | 当前成长阶段能做的动作(动作名决定它属于哪类偏好,见 §4) |
_render_reply(text, facts, mood) | -> str | ✅ | 收到用户输入时怎么回。facts 是检索到的记忆,mood 是当前情绪 |
bootstrap_event() | -> List[Dict] | 否 | 首次启动时注入的初始记忆,默认空 |
bootstrap_event() 的每一项会被当作 observe(**item):
键包括 title / brief / tags / salience / category。
observe / recalla.observe("客人夸今年的龙井好", # title(必填)
brief="他说比去年的香", # 细节(可选)
tags=["茶", "客人"], # 检索用标签
salience=3, # 1 日常 / 3 值得记 / 5 关键
category="日常") # 分类(记忆层会保留)
print(a.recall("龙井", k=3)) # -> [{'title': ..., 'brief': ..., 'tags': [...], 'sal': 3}, ...]
容量上限 200 条;触顶时按 (salience, -索引) 淘汰 —— 关键记忆优先留下。
salience 不传默认 1(旧行为)。
act() 在两个档位走同一套语义:
得分(action) = 性格基线(action) + 学习权重(action) → 采样
性格基线由 persona 里的 temper / energy / play(0~1)推导。
默认实现按动作名里的关键词软匹配:
| 关键词 | 受哪个特质影响 |
|---|---|
wave talk share teach give | temper(脾气:主动 vs 沉默) |
hop dance spin ball run | energy(精力:爱动 vs 爱歇) |
peek play joke boast | play(玩心:俏皮 vs 正经) |
想让自己的动作名被识别,要么名字里带这些关键词,要么直接覆盖 _fallback_weights(pool)。
学习权重来自 feedback() 的累积:
a.feedback("praise", action="brew") # +0.3
a.feedback("hug", action="brew") # +0.15
a.feedback("poke", action="brew") # -0.05
a.feedback("scold", action="brew") # -0.3
action 强烈建议传(不传就作用在"当前最偏好的动作"上,长期会极端化)。
实测(seed=0,400 次 act 采样,60×praise + 60×scold):被夸的动作占比 0.26 → 1.00(core 档 0.27 → 0.80),
被凶的 0.26 → 0.00(core 档 → 0.03)。两个档位都生效,只是 core 走真 LearningEngine、带 ε-贪心探索,
不会压到 0/1 两端。
mood / feelprint(a.mood) # float,[-1, 1](**属性,不加括号**)
a.feel("茶馆被泼了脏水", valence=-0.6) # 注入一次情绪事件
有 torch 时接真 EmotionSystem(人格模块驱动),没有时用轻量累计。mood 会被传进
_render_reply,所以"同一句话在高兴和生气时说出来的不一样"是自然发生的。
| tier | 含义 | 何时启用 |
|---|---|---|
bionic | 完整 PASM 核心 + emotion 模块 | 装有 torch,且能 import pasm.modules.emotion |
core | PASM 核心(memory_layers + learning) | 能 import pasm.cognitive(不需要 torch) |
light | 纯内置(重要度淘汰 + 字面检索 + 性格/反馈加权) | 任何机器 |
print(a.tier) # 'light' / 'core' / 'bionic'
a = TeaHouseOwner(agent_id="x", persona={...}, use_core=False) # 强制轻量档(测试用)
接口在三个档位下完全一致,调用方不需要分支。
a.save() # -> ~/.pasm-agents/<agent_id>/
a.summary() # -> {'tier': ..., 'persona': ..., 'total_interactions': ..., 'mood': ...}
~/.pasm-agents/<agent_id>/
├── agent_state.json # persona / 交互数 / 反馈历史 / mood / notes
├── episodes.json # light 档的记忆(core/bionic 档走 pasm.cognitive.memory_layers)
└── action_weights.json # 动作权重(性格基线 + 反馈累积)
ElderlyCompanion 那样需要额外结构化状态时,用 state.notes 存(它随 state 一起落盘):
self.state.notes.setdefault("knowledge_state", {})[topic] = 0.8
| 想做的事 | 怎么做 | 官方例子 |
|---|---|---|
| 关键事实永不丢 | 首次启动把 persona.key_facts 逐条 observe(salience=5, tags=[label]),检索时先按标签直查 | pasm-companion |
| 按知识点追踪掌握度 | 状态存 state.notes,用 EMA 平滑 新 = 旧*0.7 + 本次*0.3 | pasm-tutor |
| 行为随成长解锁 | action_pool() 按 state.growth_stage 返回不同池子 | pasm-npc |
| 危险内容升级 | 关键词扫一遍 → observe(salience=5, category="危机") + feel(-0.6) → 返回升级上下文给调用方 | pasm-companion |
| 机读导出 | 加一个 snapshot() -> Dict,别让外部读 state.notes | pasm-tutor |
一个正文 → 两种归档形态(按结构命名,不按平台名):
| 形态 | 归档结构 | 产物 |
|---|---|---|
zip-root | ZIP 根目录直接是 SKILL.md | dist/zip-root/<name>/SKILL.md + <name>-<ver>.zip |
slug-dir | 以 slug 命名的目录,内含 SKILL.md | dist/slug-dir/<name>/SKILL.md |
在自己的仓写 tools/build_skill.py:
from pasm_skills.build import ProjectMeta, SkillSpec, run_cli
META = ProjectMeta(author="you", homepage="https://github.com/you/your-repo")
SKILLS = [SkillSpec(
name="my-teahouse-agent",
body_file="SKILL.teahouse.body.md", # 放在 skill/ 下
display_name_zh="茶馆掌柜智能体",
display_name_en="Tea house owner agent",
desc_zh="……(中文描述,含触发词)",
desc_en="……",
plain_desc="……(英文,最小形态用)",
plain_category="agents",
plain_tags=["pasm", "npc"],
)]
if __name__ == "__main__":
raise SystemExit(run_cli(SKILLS, META))
python tools/build_skill.py --zip --clean # 打两种形态 + ZIP(内置结构自检)
⚠️
zip-root形态必须把SKILL.md放在 ZIP 根目录。包成skills/<name>/SKILL.md会被平台拒收(报「压缩包缺少 SKILL.md 文件」)—— 平台不递归找。打包库已内置校验。 ⚠️ 正文里教用户敲的每条命令,都要在真pip install装出来的环境里跑一遍: 开发时能跑 ≠ 装了能跑 —— 比如tools/xxx.py不进 wheel(packages.find默认只收你的包目录), 照正文做的用户手里根本没这个文件。实现放包里(python -m <包>.<模块>),tools/只留转发壳。
基座还提供一套"给自己/别人的核心做长期体检"的框架 —— 与产品智能体是两套东西:
from pasm_skills.agent import Agent, register
from pasm_skills import scenarios as S
@register
class MyVerifier(Agent):
name = "my-verifier"
goal = "验证 XXX 跑久了会不会坏"
needs = ("core",) # 声明需要哪些仓(core / lite)
def run(self):
m = S.run_scenario(self.ctx, MY_BODY, repo="core") # 隔离子进程跑场景
S.judge(self, m["metrics"], SPEC, tier="...") # 阈值表 → OK/WARN/FAIL
S.choose_python():解释器择优(优先带 torch),档位写进结论;S.run_scenario():场景跑在真实核心组件上,拿不到就 SKIP(不许写自研替身);S.judge():阈值表驱动,越界默认 WARN,bad="fail" 用于安全关键项;pasm_skills.checks:核心契约检查工具箱(文件/契约/安全底线)。放到能 pip 安装的包里,并在 pyproject.toml 声明入口,基座就会自动发现它:
[project.entry-points."pasm_skills.agents"]
my-agents = "my_pkg.agents"
护栏写错比不写更糟:它会把环境残留报成能力缺失,让人去修根本没坏的代码。三条硬规则:
<id>_ref / <id>_brief / <id>_fresh 等变体目录,
只删基础 id 会留下变体、改变检索排序 —— 同一份代码在干净机器上满分、在你机器上扣分。
写法:for p in (Path.home() / ".pasm-agents").glob(<id> + "*"): shutil.rmtree(p, ignore_errors=True)snapshot()["weakest"] 与抖动取值范围,绝不断言某一次 pick_next() 的具体结果
(check(..., reply == pick_next()) 这种必 flaky)。snapshot()),再让文本里的数字与它自洽(如 f"{int(mastery[t] * 100)}%" in reply)。复检口径:同一个包连跑 3 次、并在一台"脏机器"上再跑一遍,结论必须一致。 只跑一次就绿的护栏,等于没护栏。
docs/BUILD-AGENT.md —— 手把手从零到发版docs/SKILL-FORMAT.md —— 技能包格式与两种归档形态templates/ —— 可直接复制的智能体骨架与技能正文模板